1function runtime = runAnalyzer(self, options)
2% RUNTIME = RUNANALYZER(OPTIONS)
6 options = self.getOptions;
9self.runAnalyzerChecks(options);
12if isfield(sn,
'immfeed') && ~isempty(sn.immfeed) && any(sn.immfeed(:))
13 line_warning(mfilename,
'SolverMVA does not handle immediate feedback (immfeed); the solver will treat self-loops as class-switching with re-queueing.\n');
16Solver.resetRandomGeneratorSeed(options.seed);
18% Show library attribution
if verbose and not yet shown
19if options.verbose ~= VerboseLevel.SILENT && ~GlobalConstants.isLibraryAttributionShown()
20 sn = self.getStruct();
21 libs = SolverMVA.getLibrariesUsed(sn, options);
23 line_printf(
'The solver will leverage %s.\n', strjoin(libs,
', '));
24 GlobalConstants.setLibraryAttributionShown(
true);
33 line_debug(options,
'MVA: using lang=java, delegating to JLINE');
34 sn = getStruct(self); % doesn
't need initial state
35 jmodel = LINE2JLINE(self.model);
36 %M = jmodel.getNumberOfStatefulNodes;
37 M = jmodel.getNumberOfStations;
38 R = jmodel.getNumberOfClasses;
39 jsolver = JLINE.SolverMVA(jmodel, options);
40 [QN,UN,RN,WN,AN,TN] = JLINE.arrayListToResults(jsolver.getAvgTable);
41 runtime = jsolver.result.runtime;
44 QN = reshape(QN',R,M)
';
45 UN = reshape(UN',R,M)
';
46 RN = reshape(RN',R,M)
';
47 TN = reshape(TN',R,M)
';
48 WN = reshape(WN',R,M)
';
49 AN = reshape(AN',R,M)
';
53 if sn.nodetype(ind) == NodeType.Cache
54 jnode = jmodel.getNodeByIndex(ind-1);
55 self.model.nodes{ind}.setResultHitProb(JLINE.from_jline_matrix(jnode.getHitRatio()));
56 self.model.nodes{ind}.setResultMissProb(JLINE.from_jline_matrix(jnode.getMissRatio()));
57 % Retrieval-cache extras (delayed-hit ratio, per-list hit ratio
58 % and expected latency) so getAvgCacheTable matches the native path.
59 self.model.nodes{ind}.setResultDelayedHitProb(JLINE.from_jline_matrix(jnode.getDelayedHitRatio()));
60 self.model.nodes{ind}.setResultHitProbList(JLINE.from_jline_matrix(jnode.getHitRatioByList()));
61 self.model.nodes{ind}.setResultItemProb(JLINE.from_jline_matrix(jnode.getItemProb()));
62 self.model.nodes{ind}.setResultResidT(JLINE.from_jline_matrix(jnode.getResidT()));
65 %self.model.refreshChains();
66 self.model.refreshStruct(true);
67 self.setAvgResults(QN,UN,RN,TN,AN,WN,CN,XN,runtime,options.method,lastiter);
68 self.result.Prob.logNormConstAggr = lG;
71 line_debug(options, 'MVA:
using lang=matlab
');
72 sn = getStruct(self); % doesn't need initial state
75 % create artificial
classes arrival rates
76 forkLambda = GlobalConstants.FineTol * ones(1, 2*sn.nclasses*sum(sn.nodetype==NodeType.Fork));
77 QN = GlobalConstants.Immediate * ones(1, sn.nclasses);
80 while (forkLoop && forkIter < options.iter_max)
82 forkIter = forkIter + 1;
83 line_debug(options,
'Fork-join iteration %d', forkIter);
85 switch options.config.fork_join
86 case {
'heidelberger-trivedi',
'ht'}
87 [nonfjmodel, fjclassmap, fjforkmap, fj_auxiliary_delays] = ModelAdapter.ht(self.model);
88 line_debug(options,
'Fork-join method: heidelberger-trivedi');
89 case {
'mmt',
'default',
'fjt'}
90 [nonfjmodel, fjclassmap, fjforkmap, fanout] = ModelAdapter.mmt(self.model, forkLambda);
91 line_debug(options,
'Fork-join method: mmt');
92 [outer_forks, parent_forks] = ModelAdapter.sortForks(sn, fjforkmap, fjclassmap, nonfjmodel);
94 elseif ~strcmp(options.config.fork_join,
'heidelberger-trivedi') & ~strcmp(options.config.fork_join,
'ht')
95 %line_printf(
'Fork-join iteration %d\n',forkIter);
96 nonfjSource = nonfjmodel.getSource;
97 for r=1:length(fjclassmap) % r
is the auxiliary
class
101 if ~nonfjSource.arrivalProcess{r}.isDisabled
102 nonfjSource.arrivalProcess{r}.setRate((fanout(r)-1)*forkLambda(r));
107 nonfjmodel.refreshRates();
109 sn = nonfjmodel.getStruct(
false); %
this ensures that we solve nonfjmodel instead of the original model
110 line_debug(options,
'Fork-join iter %d: rebuilt nonfjmodel struct (nstations=%d, nclasses=%d)', forkIter, sn.nstations, sn.nclasses);
111 % Convergence check: handle zero/NaN queue lengths from size mismatch and Immediate activities
112 qn_ratio = QN_1 ./ QN;
113 qn_ratio(isnan(qn_ratio)) = 1; % 0/0 = NaN → treat as converged (ratio=1)
114 qn_ratio(isinf(qn_ratio)) = 0; % x/0 = Inf → treat as not converged (ratio=0, error=1)
115 if isequal(size(QN_1), size(QN)) && max(abs(1 - qn_ratio(:))) < GlobalConstants.CoarseTol && (forkIter > 2)
116 line_debug(options, 'Fork-join iter %d: converged (max ratio error < CoarseTol)', forkIter);
119 if self.model.hasOpenClasses
120 sourceIndex = self.model.getSource.index;
121 UNnosource = UN; UNnosource(sourceIndex,:) = 0;
122 if any(find(sum(UNnosource(:,isinf(sn.njobs(1:size(QN,2)))),2)>0.99 * sn.nservers))
123 line_warning(mfilename,'The model may be unstable: the utilization of station %i for open
classes exceeds 99 percent.\n',maxpos(sum(UNnosource,2)));
131 line_debug(options, 'Product-form check: hasProductForm=%d (exact method requested)', self.model.hasProductFormSolution);
132 if strcmp(options.method,'exact') && ~self.model.hasProductFormSolution
133 line_error(mfilename,'The exact method requires the model to have a product-form solution. This model does not have one.\nYou can use Network.hasProductFormSolution() to check before running the solver.\n Run the ''mva'' method to obtain an approximation based on the exact MVA algorithm.\n');
135 if strcmp(options.method,'mva') && ~self.model.hasProductFormSolution
136 line_warning(mfilename,'The exact method requires the model to have a product-form solution. This model does not have one.\nYou can use Network.hasProductFormSolution() to check before running the solver.\nSolverMVA will return an approximation generated by an exact MVA algorithm.');
139 method = options.method;
141 % Check for size-based policies (SRPT, PSJF, FB, LRPT, SETF) in multiclass open systems
142 queueIdx = find(sn.nodetype == NodeType.Queue);
143 isSizeBasedPolicy = false;
144 if ~isempty(queueIdx)
145 schedType = sn.sched(sn.nodeToStation(queueIdx(1)));
146 isSizeBasedPolicy = ismember(schedType, [SchedStrategy.SRPT, SchedStrategy.PSJF, SchedStrategy.FB, SchedStrategy.LRPT, SchedStrategy.SETF]);
149 ci_cache = find(sn.nodetype == NodeType.Cache, 1);
150 hasRetrieval = ~isempty(ci_cache) && isfield(sn.nodeparam{ci_cache},
'retrievalSystemCapacity') ...
151 && sn.nodeparam{ci_cache}.retrievalSystemCapacity > 0;
152 if hasRetrieval % delayed-hit (retrieval-system) cache
153 line_debug(options,
'Delayed-hit retrieval cache, routing to mva_retrieval_analyzer');
154 [QN,UN,RN,TN,CN,XN,lG,hitprob,missprob,delayedprob,hitproblist,itemprob,latency,runtime,actualmethod] = solver_mva_retrieval_analyzer(sn, options);
156 for ind = 1:sn.nnodes
157 if sn.nodetype(ind) == NodeType.Cache
158 self.model.nodes{ind}.setResultHitProb(hitprob);
159 self.model.nodes{ind}.setResultMissProb(missprob);
160 self.model.nodes{ind}.setResultDelayedHitProb(delayedprob);
161 self.model.nodes{ind}.setResultHitProbList(hitproblist);
162 self.model.nodes{ind}.setResultItemProb(itemprob);
163 self.model.nodes{ind}.setResultResidT(latency);
166 self.model.refreshStruct(
true);
167 elseif sn.nclosedjobs == 0 && length(sn.nodetype)==3 && all(sort(sn.nodetype)
' == sort([NodeType.Source,NodeType.Queue,NodeType.Sink])) && isSizeBasedPolicy
168 % Multiclass open system with size-based scheduling (SRPT, PSJF, FB, LRPT, SETF)
169 line_debug(options, 'Size-based scheduling detected (%s), routing to qsys_sizebased_analyzer
', char(schedType));
170 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter] = solver_mva_qsys_sizebased_analyzer(sn, options, schedType);
171 elseif sn.nclasses==1 && sn.nclosedjobs == 0 && length(sn.nodetype)==3 && all(sort(sn.nodetype)' == sort([NodeType.Source,NodeType.Queue,NodeType.Sink])) %
is an open queueing system
172 line_debug(options,
'Single-class open queueing system (Source-Queue-Sink), routing to qsys_analyzer');
173 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter] = solver_mva_qsys_analyzer(sn, options);
174 elseif sn.nclasses>1 && sn.nclosedjobs == 0 && length(sn.nodetype)==3 && all(sort(sn.nodetype)
' == sort([NodeType.Source,NodeType.Queue,NodeType.Sink])) && sn.sched(find(sn.nodetype==NodeType.Queue)) == SchedStrategy.POLLING % is an open polling system
175 line_debug(options, 'Multiclass open polling system, routing to polling_analyzer
');
176 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter] = solver_mva_polling_analyzer(sn, options);
177 elseif sn.nclosedjobs == 0 && length(sn.nodetype)==3 && all(sort(sn.nodetype)' == sort([NodeType.Source,NodeType.Cache,NodeType.Sink])) %
is a non-rentrant cache
178 line_debug(options,
'Non-reentrant cache (Source-Cache-Sink), routing to cache_analyzer');
179 % random initialization
180 for ind = 1:sn.nnodes
181 if sn.nodetype(ind) == NodeType.Cache
182 prob = self.model.nodes{ind}.server.hitClass;
184 self.model.nodes{ind}.setResultHitProb(prob);
185 self.model.nodes{ind}.setResultMissProb(1-prob);
188 self.model.refreshChains();
190 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod,hitproblist,itemprob] = solver_mva_cache_analyzer(sn, options);
192 for ind = 1:sn.nnodes
193 if sn.nodetype(ind) == NodeType.Cache
194 self.model.nodes{ind}.setResultHitProbList(hitproblist);
195 self.model.nodes{ind}.setResultItemProb(itemprob);
196 hitClass = self.model.nodes{ind}.getHitClass;
197 missClass = self.model.nodes{ind}.getMissClass;
198 hitprob = zeros(1,length(hitClass));
199 for k=1:length(self.model.nodes{ind}.getHitClass)
200 chain_k = sn.chains(:,k)>0;
201 inchain = sn.chains(chain_k,:)>0;
205 hitprob(k) = XN(h) / sum(XN(inchain),"omitnan"); %
#ok<NANSUM>
208 self.model.nodes{ind}.setResultHitProb(hitprob);
209 self.model.nodes{ind}.setResultMissProb(1-hitprob);
212 %self.model.refreshChains();
213 self.model.refreshStruct(
true);
214 else % queueing network
215 if any(sn.nodetype == NodeType.Cache) %
if integrated caching-queueing
216 line_debug(options,
'Integrated caching-queueing network, routing to cacheqn_analyzer');
217 [QN,UN,RN,TN,CN,XN,lG,hitprob,missprob,runtime,lastiter] = solver_mva_cacheqn_analyzer(self, options);
218 for ind = 1:sn.nnodes
219 if sn.nodetype(ind) == NodeType.Cache
220 self.model.nodes{ind}.setResultHitProb(hitprob(ind,:));
221 self.model.nodes{ind}.setResultMissProb(missprob(ind,:));
224 %self.model.refreshChains();
225 self.model.refreshStruct(
true);
226 else % ordinary queueing network
228 case {
'aba.upper',
'aba.lower',
'bjb.upper',
'bjb.lower',
'pb.upper',
'pb.lower',
'gb.upper',
'gb.lower',
'sb.upper',
'sb.lower'}
229 line_debug(options,
'Using bound method: %s', method);
230 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter] = solver_mva_bound_analyzer(sn, options);
232 if ~isempty(sn.lldscaling) || ~isempty(sn.cdscaling)
233 line_debug(options, 'Load-dependent scaling detected (lldscaling=%d, cdscaling=%d), routing to mvald_analyzer', ~isempty(sn.lldscaling), ~isempty(sn.cdscaling));
234 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mvald_analyzer(sn, options);
236 line_debug(options, 'Standard queueing network, routing to mva_analyzer (method=%s)', method);
237 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mva_analyzer(sn, options);
242 if self.model.hasFork
243 line_debug(options, 'Fork-join post-processing: method=%s, computing sync delays', options.config.fork_join);
246 % Pre-compute linked routing matrix once (loop-invariant)
247 switch options.config.fork_join
248 case {
'mmt',
'default',
'fjt'}
249 Pcs = cell2mat(nonfjmodel.getLinkedRoutingMatrix);
251 for f=find(sn.nodetype == NodeType.Fork)
'
252 switch options.config.fork_join
253 case {'mmt
', 'default', 'fjt
'}
254 TNfork = zeros(1,sn.nclasses);
256 inchain = find(sn.chains(c,:));
258 TNfork(r) = (sn.nodevisits{c}(parent_forks(f),r) / sum(sn.visits{c}(sn.stationToStateful(sn.refstat(r)),inchain))) * sum(TN(sn.refstat(r),inchain));
261 % find join associated to fork f
262 joinIdx = find(sn.fj(f,:));
263 forkauxclasses = find(fjforkmap==f);
264 for s=forkauxclasses(:)
'
265 r = fjclassmap(s); % original class associated to auxiliary class s
267 forkLambda(s) = mean([forkLambda(s); TNfork(r)],1);
269 joinStat = sn.nodeToStation(joinIdx);
270 TN(sn.nodeToStation(joinIdx),r) = TN(sn.nodeToStation(joinIdx),r) + sum(TN(sn.nodeToStation(joinIdx), find(fjclassmap == r))) - TN(sn.nodeToStation(joinIdx), s);
271 forkLambda(s) = mean([forkLambda(s); TN(sn.nodeToStation(joinIdx),r)],1);
273 if isempty(joinIdx) || ~outer_forks(f, r)
274 % No join nodes for this fork, no synchronisation delay
277 % Find the parallel paths coming out of the fork
278 ri = ModelAdapter.findPathsCS(sn, Pcs, f, joinIdx, r, [r,s], QN, TN, 0, fjclassmap, fjforkmap, nonfjmodel);
280 % No routing from fork for this class - set sync delay to 0 (Immediate)
281 % Matches JAR behavior where empty Matrix gives syncDelay = 0
286 parallel_branches = length(ri);
287 for pow=0:(parallel_branches - 1)
288 current_sum = sum(1./sum(nchoosek(lambdai, pow + 1),2));
289 d0 = d0 + (-1)^pow * current_sum;
291 syncDelay = d0*sn.nodeparam{f}.fanOut - mean(ri);
293 % Set the synchronisation delays
294 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{s}, Exp.fitMean(syncDelay));
296 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{r}, Exp.fitMean(syncDelay));
299 case {'heidelberger-trivedi
', 'ht
'}
300 joinIdx = find(sn.fj(f,:));
302 inchain = find(sn.chains(c,:));
307 % Obtain the response times on the parallel branches
308 ri = RN(:, find(fjclassmap == r));
309 ri(isnan(ri) | isinf(ri)) = 0;
310 ri = sum(ri, 1,
"omitnan") - RN(nonfjstruct.nodeToStation(fj_auxiliary_delays{joinIdx}), find(fjclassmap == r)) - RN(nonfjstruct.nodeToStation(joinIdx), find(fjclassmap == r));
313 parallel_branches = length(self.model.nodes{f}.output.outputStrategy{r}{3});
314 for pow=0:(parallel_branches - 1)
315 current_sum = sum(1./sum(nchoosek(lambdai, pow + 1),2));
316 d0 = d0 + (-1)^pow * current_sum;
318 di = d0*sn.nodeparam{f}.fanOut - ri;
319 r0 = sum(RN(:, inchain), 2);
320 r0(isnan(r0) | isinf(r0)) = 0;
321 r0 = sum(r0, 1,
"omitnan") - RN(nonfjstruct.nodeToStation(joinIdx), r);
322 % Update the delays at the join node and at the auxiliary delay
323 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{r}, Exp.fitMean(d0*sn.nodeparam{f}.fanOut));
325 for s=find(fjclassmap == r)
326 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{s}, Exp.fitMean(di(idx)));
328 nonfjmodel.nodes{fj_auxiliary_delays{joinIdx}}.setService(nonfjmodel.classes{s}, Exp.fitMean(r0));
335 % Batch refreshRates after all sync delay updates (moved out of inner loops
for performance)
336 switch options.config.fork_join
337 case {
'mmt',
'default',
'fjt'}
338 nonfjmodel.refreshRates();
340 switch options.config.fork_join
341 case {
'heidelberger-trivedi',
'ht'}
342 nonfjmodel.refreshStruct();
343 % Delete the queue lengths, response times, throughputs and utilizations of the original
classes at the join
nodes
344 QN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = 0;
345 RN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = 0;
346 % Save the throughputs of the original
classes at the join node
347 TN_orig = TN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap));
348 TN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = 0;
349 UN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = 0;
351 % Remove the times at the auxiliary delay
352 QN(nonfjstruct.nodeToStation(cell2mat(fj_auxiliary_delays)),:) = [];
353 UN(nonfjstruct.nodeToStation(cell2mat(fj_auxiliary_delays)),:) = [];
354 RN(nonfjstruct.nodeToStation(cell2mat(fj_auxiliary_delays)),:) = [];
355 TN(nonfjstruct.nodeToStation(cell2mat(fj_auxiliary_delays)),:) = [];
357 for r=1:length(fjclassmap)
360 QN(:,s) = QN(:,s) + QN(:,r);
361 UN(:,s) = UN(:,s) + UN(:,r);
362 % Add all throughputs of the auxiliary
classes to facilitate the computation of the response times
363 TN(:,s) = TN(:,s) + TN(:,r);
364 RN(:,s) = QN(:,s) ./ TN(:,s);
367 % Re-set the throughputs
for the original
classes
368 TN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = TN_orig;
369 case {
'mmt',
'default',
'fjt'}
370 TN_orig = TN([nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Source))], nonzeros(fjclassmap));
372 for r=1:length(fjclassmap)
375 QN(:,s) = QN(:,s) + QN(:,r);
376 UN(:,s) = UN(:,s) + UN(:,r);
377 TN(:,s) = TN(:,s) + TN(:,r);
378 %RN(:,s) = RN(:,s) + RN(:,r);
379 %
for i=find(snorig.nodetype == NodeType.Delay | snorig.nodetype == NodeType.Queue)
'
380 % TN(snorig.nodeToStation(i),s) = TN(snorig.nodeToStation(i),s) + TN(snorig.nodeToStation(i),r);
382 RN(:,s) = QN(:,s) ./ TN(:,s);
383 %CN(:,s) = CN(:,s) + CN(:,r);
384 %XN(:,s) = XN(:,s) + XN(:,r);
387 TN([nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Source))], nonzeros(fjclassmap)) = TN_orig;
389 QN(:,fjclassmap>0) = [];
390 UN(:,fjclassmap>0) = [];
391 RN(:,fjclassmap>0) = [];
392 TN(:,fjclassmap>0) = [];
393 CN(:,fjclassmap>0) = [];
394 XN(:,fjclassmap>0) = [];
396 iter = iter + lastiter;
397 % Cap accumulated iterations for stability (Python parity)
400 break; % Exit forkLoop early
404 sn = self.model.getStruct();
406 % Compute average residence time at steady-state
407 AN = sn_get_arvr_from_tput(sn, TN, self.getAvgTputHandles());
408 WN = sn_get_residt_from_respt(sn, RN, self.getAvgResidTHandles());
409 if strcmp(method,'default') && exist('actualmethod
','var
')
410 self.setAvgResults(QN,UN,RN,TN,AN,WN,CN,XN,runtime,['default/
' actualmethod],iter);
412 self.setAvgResults(QN,UN,RN,TN,AN,WN,CN,XN,runtime,method,iter);
414 self.result.Prob.logNormConstAggr = lG;