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 self.model.nodes{ind}.setResultHitProb(JLINE.from_jline_matrix(jmodel.getNodeByIndex(ind-1).getHitRatio()));
55 self.model.nodes{ind}.setResultMissProb(JLINE.from_jline_matrix(jmodel.getNodeByIndex(ind-1).getMissRatio()));
58 %self.model.refreshChains();
59 self.model.refreshStruct(true);
60 self.setAvgResults(QN,UN,RN,TN,AN,WN,CN,XN,runtime,options.method,lastiter);
61 self.result.Prob.logNormConstAggr = lG;
64 line_debug(options, 'MVA:
using lang=matlab
');
65 sn = getStruct(self); % doesn't need initial state
68 % create artificial
classes arrival rates
69 forkLambda = GlobalConstants.FineTol * ones(1, 2*sn.nclasses*sum(sn.nodetype==NodeType.Fork));
70 QN = GlobalConstants.Immediate * ones(1, sn.nclasses);
73 while (forkLoop && forkIter < options.iter_max)
75 forkIter = forkIter + 1;
76 line_debug(options,
'Fork-join iteration %d', forkIter);
78 switch options.config.fork_join
79 case {
'heidelberger-trivedi',
'ht'}
80 [nonfjmodel, fjclassmap, fjforkmap, fj_auxiliary_delays] = ModelAdapter.ht(self.model);
81 line_debug(options,
'Fork-join method: heidelberger-trivedi');
82 case {
'mmt',
'default',
'fjt'}
83 [nonfjmodel, fjclassmap, fjforkmap, fanout] = ModelAdapter.mmt(self.model, forkLambda);
84 line_debug(options,
'Fork-join method: mmt');
85 [outer_forks, parent_forks] = ModelAdapter.sortForks(sn, fjforkmap, fjclassmap, nonfjmodel);
87 elseif ~strcmp(options.config.fork_join,
'heidelberger-trivedi') & ~strcmp(options.config.fork_join,
'ht')
88 %line_printf(
'Fork-join iteration %d\n',forkIter);
89 nonfjSource = nonfjmodel.getSource;
90 for r=1:length(fjclassmap) % r
is the auxiliary
class
94 if ~nonfjSource.arrivalProcess{r}.isDisabled
95 nonfjSource.arrivalProcess{r}.setRate((fanout(r)-1)*forkLambda(r));
100 nonfjmodel.refreshRates();
102 sn = nonfjmodel.getStruct(
false); %
this ensures that we solve nonfjmodel instead of the original model
103 line_debug(options,
'Fork-join iter %d: rebuilt nonfjmodel struct (nstations=%d, nclasses=%d)', forkIter, sn.nstations, sn.nclasses);
104 % Convergence check: handle zero/NaN queue lengths from size mismatch and Immediate activities
105 qn_ratio = QN_1 ./ QN;
106 qn_ratio(isnan(qn_ratio)) = 1; % 0/0 = NaN → treat as converged (ratio=1)
107 qn_ratio(isinf(qn_ratio)) = 0; % x/0 = Inf → treat as not converged (ratio=0, error=1)
108 if isequal(size(QN_1), size(QN)) && max(abs(1 - qn_ratio(:))) < GlobalConstants.CoarseTol && (forkIter > 2)
109 line_debug(options, 'Fork-join iter %d: converged (max ratio error < CoarseTol)', forkIter);
112 if self.model.hasOpenClasses
113 sourceIndex = self.model.getSource.index;
114 UNnosource = UN; UNnosource(sourceIndex,:) = 0;
115 if any(find(sum(UNnosource(:,isinf(sn.njobs(1:size(QN,2)))),2)>0.99 * sn.nservers))
116 line_warning(mfilename,'The model may be unstable: the utilization of station %i for open
classes exceeds 99 percent.\n',maxpos(sum(UNnosource,2)));
124 line_debug(options, 'Product-form check: hasProductForm=%d (exact method requested)', self.model.hasProductFormSolution);
125 if strcmp(options.method,'exact') && ~self.model.hasProductFormSolution
126 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');
128 if strcmp(options.method,'mva') && ~self.model.hasProductFormSolution
129 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.');
132 method = options.method;
134 % Check for size-based policies (SRPT, PSJF, FB, LRPT, SETF) in multiclass open systems
135 queueIdx = find(sn.nodetype == NodeType.Queue);
136 isSizeBasedPolicy = false;
137 if ~isempty(queueIdx)
138 schedType = sn.sched(sn.nodeToStation(queueIdx(1)));
139 isSizeBasedPolicy = ismember(schedType, [SchedStrategy.SRPT, SchedStrategy.PSJF, SchedStrategy.FB, SchedStrategy.LRPT, SchedStrategy.SETF]);
142 if sn.nclosedjobs == 0 && length(sn.nodetype)==3 && all(sort(sn.nodetype)' == sort([NodeType.Source,NodeType.Queue,NodeType.Sink])) && isSizeBasedPolicy
143 % Multiclass open system with size-based scheduling (SRPT, PSJF, FB, LRPT, SETF)
144 line_debug(options, 'Size-based scheduling detected (%s), routing to qsys_sizebased_analyzer',
char(schedType));
145 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter] = solver_mva_qsys_sizebased_analyzer(sn, options, schedType);
146 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
147 line_debug(options, 'Single-class open queueing system (Source-Queue-Sink), routing to qsys_analyzer');
148 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter] = solver_mva_qsys_analyzer(sn, options);
149 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
150 line_debug(options, 'Multiclass open polling system, routing to polling_analyzer');
151 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter] = solver_mva_polling_analyzer(sn, options);
152 elseif sn.nclosedjobs == 0 && length(sn.nodetype)==3 && all(sort(sn.nodetype)' == sort([NodeType.Source,NodeType.Cache,NodeType.Sink])) %
is a non-rentrant cache
153 line_debug(options, 'Non-reentrant cache (Source-Cache-Sink), routing to cache_analyzer');
154 % random initialization
155 for ind = 1:sn.nnodes
156 if sn.nodetype(ind) == NodeType.Cache
157 prob = self.model.
nodes{ind}.server.hitClass;
159 self.model.nodes{ind}.setResultHitProb(prob);
160 self.model.nodes{ind}.setResultMissProb(1-prob);
163 self.model.refreshChains();
165 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mva_cache_analyzer(sn, options);
167 for ind = 1:sn.nnodes
168 if sn.nodetype(ind) == NodeType.Cache
169 hitClass = self.model.nodes{ind}.getHitClass;
170 missClass = self.model.nodes{ind}.getMissClass;
171 hitprob = zeros(1,length(hitClass));
172 for k=1:length(self.model.nodes{ind}.getHitClass)
173 chain_k = sn.chains(:,k)>0;
174 inchain = sn.chains(chain_k,:)>0;
178 hitprob(k) = XN(h) / sum(XN(inchain),"omitnan"); %
#ok<NANSUM>
181 self.model.nodes{ind}.setResultHitProb(hitprob);
182 self.model.nodes{ind}.setResultMissProb(1-hitprob);
185 %self.model.refreshChains();
186 self.model.refreshStruct(
true);
187 else % queueing network
188 if any(sn.nodetype == NodeType.Cache) %
if integrated caching-queueing
189 line_debug(options,
'Integrated caching-queueing network, routing to cacheqn_analyzer');
190 [QN,UN,RN,TN,CN,XN,lG,hitprob,missprob,runtime,lastiter] = solver_mva_cacheqn_analyzer(self, options);
191 for ind = 1:sn.nnodes
192 if sn.nodetype(ind) == NodeType.Cache
193 self.model.nodes{ind}.setResultHitProb(hitprob(ind,:));
194 self.model.nodes{ind}.setResultMissProb(missprob(ind,:));
197 %self.model.refreshChains();
198 self.model.refreshStruct(
true);
199 else % ordinary queueing network
201 case {
'aba.upper',
'aba.lower',
'bjb.upper',
'bjb.lower',
'pb.upper',
'pb.lower',
'gb.upper',
'gb.lower',
'sb.upper',
'sb.lower'}
202 line_debug(options,
'Using bound method: %s', method);
203 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter] = solver_mva_bound_analyzer(sn, options);
205 if ~isempty(sn.lldscaling) || ~isempty(sn.cdscaling)
206 line_debug(options, 'Load-dependent scaling detected (lldscaling=%d, cdscaling=%d), routing to mvald_analyzer', ~isempty(sn.lldscaling), ~isempty(sn.cdscaling));
207 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mvald_analyzer(sn, options);
209 line_debug(options, 'Standard queueing network, routing to mva_analyzer (method=%s)', method);
210 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mva_analyzer(sn, options);
215 if self.model.hasFork
216 line_debug(options, 'Fork-join post-processing: method=%s, computing sync delays', options.config.fork_join);
219 % Pre-compute linked routing matrix once (loop-invariant)
220 switch options.config.fork_join
221 case {
'mmt',
'default',
'fjt'}
222 Pcs = cell2mat(nonfjmodel.getLinkedRoutingMatrix);
224 for f=find(sn.nodetype == NodeType.Fork)
'
225 switch options.config.fork_join
226 case {'mmt
', 'default', 'fjt
'}
227 TNfork = zeros(1,sn.nclasses);
229 inchain = find(sn.chains(c,:));
231 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));
234 % find join associated to fork f
235 joinIdx = find(sn.fj(f,:));
236 forkauxclasses = find(fjforkmap==f);
237 for s=forkauxclasses(:)
'
238 r = fjclassmap(s); % original class associated to auxiliary class s
240 forkLambda(s) = mean([forkLambda(s); TNfork(r)],1);
242 joinStat = sn.nodeToStation(joinIdx);
243 TN(sn.nodeToStation(joinIdx),r) = TN(sn.nodeToStation(joinIdx),r) + sum(TN(sn.nodeToStation(joinIdx), find(fjclassmap == r))) - TN(sn.nodeToStation(joinIdx), s);
244 forkLambda(s) = mean([forkLambda(s); TN(sn.nodeToStation(joinIdx),r)],1);
246 if isempty(joinIdx) || ~outer_forks(f, r)
247 % No join nodes for this fork, no synchronisation delay
250 % Find the parallel paths coming out of the fork
251 ri = ModelAdapter.findPathsCS(sn, Pcs, f, joinIdx, r, [r,s], QN, TN, 0, fjclassmap, fjforkmap, nonfjmodel);
253 % No routing from fork for this class - set sync delay to 0 (Immediate)
254 % Matches JAR behavior where empty Matrix gives syncDelay = 0
259 parallel_branches = length(ri);
260 for pow=0:(parallel_branches - 1)
261 current_sum = sum(1./sum(nchoosek(lambdai, pow + 1),2));
262 d0 = d0 + (-1)^pow * current_sum;
264 syncDelay = d0*sn.nodeparam{f}.fanOut - mean(ri);
266 % Set the synchronisation delays
267 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{s}, Exp.fitMean(syncDelay));
269 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{r}, Exp.fitMean(syncDelay));
272 case {'heidelberger-trivedi
', 'ht
'}
273 joinIdx = find(sn.fj(f,:));
275 inchain = find(sn.chains(c,:));
280 % Obtain the response times on the parallel branches
281 ri = RN(:, find(fjclassmap == r));
282 ri(isnan(ri) | isinf(ri)) = 0;
283 ri = sum(ri, 1,
"omitnan") - RN(nonfjstruct.nodeToStation(fj_auxiliary_delays{joinIdx}), find(fjclassmap == r)) - RN(nonfjstruct.nodeToStation(joinIdx), find(fjclassmap == r));
286 parallel_branches = length(self.model.nodes{f}.output.outputStrategy{r}{3});
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 di = d0*sn.nodeparam{f}.fanOut - ri;
292 r0 = sum(RN(:, inchain), 2);
293 r0(isnan(r0) | isinf(r0)) = 0;
294 r0 = sum(r0, 1,
"omitnan") - RN(nonfjstruct.nodeToStation(joinIdx), r);
295 % Update the delays at the join node and at the auxiliary delay
296 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{r}, Exp.fitMean(d0*sn.nodeparam{f}.fanOut));
298 for s=find(fjclassmap == r)
299 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{s}, Exp.fitMean(di(idx)));
301 nonfjmodel.nodes{fj_auxiliary_delays{joinIdx}}.setService(nonfjmodel.classes{s}, Exp.fitMean(r0));
308 % Batch refreshRates after all sync delay updates (moved out of inner loops
for performance)
309 switch options.config.fork_join
310 case {
'mmt',
'default',
'fjt'}
311 nonfjmodel.refreshRates();
313 switch options.config.fork_join
314 case {
'heidelberger-trivedi',
'ht'}
315 nonfjmodel.refreshStruct();
316 % Delete the queue lengths, response times, throughputs and utilizations of the original
classes at the join
nodes
317 QN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = 0;
318 RN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = 0;
319 % Save the throughputs of the original
classes at the join node
320 TN_orig = TN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap));
321 TN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = 0;
322 UN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = 0;
324 % Remove the times at the auxiliary delay
325 QN(nonfjstruct.nodeToStation(cell2mat(fj_auxiliary_delays)),:) = [];
326 UN(nonfjstruct.nodeToStation(cell2mat(fj_auxiliary_delays)),:) = [];
327 RN(nonfjstruct.nodeToStation(cell2mat(fj_auxiliary_delays)),:) = [];
328 TN(nonfjstruct.nodeToStation(cell2mat(fj_auxiliary_delays)),:) = [];
330 for r=1:length(fjclassmap)
333 QN(:,s) = QN(:,s) + QN(:,r);
334 UN(:,s) = UN(:,s) + UN(:,r);
335 % Add all throughputs of the auxiliary
classes to facilitate the computation of the response times
336 TN(:,s) = TN(:,s) + TN(:,r);
337 RN(:,s) = QN(:,s) ./ TN(:,s);
340 % Re-set the throughputs
for the original
classes
341 TN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = TN_orig;
342 case {
'mmt',
'default',
'fjt'}
343 TN_orig = TN([nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Source))], nonzeros(fjclassmap));
345 for r=1:length(fjclassmap)
348 QN(:,s) = QN(:,s) + QN(:,r);
349 UN(:,s) = UN(:,s) + UN(:,r);
350 TN(:,s) = TN(:,s) + TN(:,r);
351 %RN(:,s) = RN(:,s) + RN(:,r);
352 %
for i=find(snorig.nodetype == NodeType.Delay | snorig.nodetype == NodeType.Queue)
'
353 % TN(snorig.nodeToStation(i),s) = TN(snorig.nodeToStation(i),s) + TN(snorig.nodeToStation(i),r);
355 RN(:,s) = QN(:,s) ./ TN(:,s);
356 %CN(:,s) = CN(:,s) + CN(:,r);
357 %XN(:,s) = XN(:,s) + XN(:,r);
360 TN([nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Source))], nonzeros(fjclassmap)) = TN_orig;
362 QN(:,fjclassmap>0) = [];
363 UN(:,fjclassmap>0) = [];
364 RN(:,fjclassmap>0) = [];
365 TN(:,fjclassmap>0) = [];
366 CN(:,fjclassmap>0) = [];
367 XN(:,fjclassmap>0) = [];
369 iter = iter + lastiter;
370 % Cap accumulated iterations for stability (Python parity)
373 break; % Exit forkLoop early
377 sn = self.model.getStruct();
379 % Compute average residence time at steady-state
380 AN = sn_get_arvr_from_tput(sn, TN, self.getAvgTputHandles());
381 WN = sn_get_residt_from_respt(sn, RN, self.getAvgResidTHandles());
382 if strcmp(method,'default') && exist('actualmethod
','var
')
383 self.setAvgResults(QN,UN,RN,TN,AN,WN,CN,XN,runtime,['default/
' actualmethod],iter);
385 self.setAvgResults(QN,UN,RN,TN,AN,WN,CN,XN,runtime,method,iter);
387 self.result.Prob.logNormConstAggr = lG;