1function runtime = runAnalyzer(self, options)
2% RUNTIME = RUNANALYZER(OPTIONS)
6 options = self.getOptions;
9self.runAnalyzerChecks(options);
11Solver.resetRandomGeneratorSeed(options.seed);
13% Show library attribution
if verbose and not yet shown
14if options.verbose ~= VerboseLevel.SILENT && ~GlobalConstants.isLibraryAttributionShown()
15 sn = self.getStruct();
16 libs = SolverMVA.getLibrariesUsed(sn, options);
18 line_printf(
'The solver will leverage %s.\n', strjoin(libs,
', '));
19 GlobalConstants.setLibraryAttributionShown(
true);
28 line_debug(options,
'MVA: using lang=java, delegating to JLINE');
29 sn = getStruct(self); % doesn
't need initial state
30 jmodel = LINE2JLINE(self.model);
31 %M = jmodel.getNumberOfStatefulNodes;
32 M = jmodel.getNumberOfStations;
33 R = jmodel.getNumberOfClasses;
34 jsolver = JLINE.SolverMVA(jmodel, options);
35 [QN,UN,RN,WN,AN,TN] = JLINE.arrayListToResults(jsolver.getAvgTable);
36 runtime = jsolver.result.runtime;
39 QN = reshape(QN',R,M)
';
40 UN = reshape(UN',R,M)
';
41 RN = reshape(RN',R,M)
';
42 TN = reshape(TN',R,M)
';
43 WN = reshape(WN',R,M)
';
44 AN = reshape(AN',R,M)
';
48 if sn.nodetype(ind) == NodeType.Cache
49 self.model.nodes{ind}.setResultHitProb(JLINE.from_jline_matrix(jmodel.getNodeByIndex(ind-1).getHitRatio()));
50 self.model.nodes{ind}.setResultMissProb(JLINE.from_jline_matrix(jmodel.getNodeByIndex(ind-1).getMissRatio()));
53 %self.model.refreshChains();
54 self.model.refreshStruct(true);
55 self.setAvgResults(QN,UN,RN,TN,AN,WN,CN,XN,runtime,options.method,lastiter);
56 self.result.Prob.logNormConstAggr = lG;
59 line_debug(options, 'MVA:
using lang=matlab
');
60 sn = getStruct(self); % doesn't need initial state
63 % create artificial
classes arrival rates
64 forkLambda = GlobalConstants.FineTol * ones(1, 2*sn.nclasses*sum(sn.nodetype==NodeType.Fork));
65 QN = GlobalConstants.Immediate * ones(1, sn.nclasses);
68 while (forkLoop && forkIter < options.iter_max)
70 forkIter = forkIter + 1;
71 line_debug(options,
'Fork-join iteration %d', forkIter);
73 switch options.config.fork_join
74 case {
'heidelberger-trivedi',
'ht'}
75 [nonfjmodel, fjclassmap, fjforkmap, fj_auxiliary_delays] = ModelAdapter.ht(self.model);
76 line_debug(options,
'Fork-join method: heidelberger-trivedi');
77 case {
'mmt',
'default',
'fjt'}
78 [nonfjmodel, fjclassmap, fjforkmap, fanout] = ModelAdapter.mmt(self.model, forkLambda);
79 line_debug(options,
'Fork-join method: mmt');
80 [outer_forks, parent_forks] = ModelAdapter.sortForks(sn, fjforkmap, fjclassmap, nonfjmodel);
82 elseif ~strcmp(options.config.fork_join,
'heidelberger-trivedi') & ~strcmp(options.config.fork_join,
'ht')
83 %line_printf(
'Fork-join iteration %d\n',forkIter);
84 nonfjSource = nonfjmodel.getSource;
85 for r=1:length(fjclassmap) % r
is the auxiliary
class
89 if ~nonfjSource.arrivalProcess{r}.isDisabled
90 nonfjSource.arrivalProcess{r}.setRate((fanout(r)-1)*forkLambda(r));
95 nonfjmodel.refreshRates();
97 sn = nonfjmodel.getStruct(
false); %
this ensures that we solve nonfjmodel instead of the original model
98 line_debug(options,
'Fork-join iter %d: rebuilt nonfjmodel struct (nstations=%d, nclasses=%d)', forkIter, sn.nstations, sn.nclasses);
99 % Convergence check: handle zero/NaN queue lengths from size mismatch and Immediate activities
100 qn_ratio = QN_1 ./ QN;
101 qn_ratio(isnan(qn_ratio)) = 1; % 0/0 = NaN → treat as converged (ratio=1)
102 qn_ratio(isinf(qn_ratio)) = 0; % x/0 = Inf → treat as not converged (ratio=0, error=1)
103 if isequal(size(QN_1), size(QN)) && max(abs(1 - qn_ratio(:))) < GlobalConstants.CoarseTol && (forkIter > 2)
104 line_debug(options, 'Fork-join iter %d: converged (max ratio error < CoarseTol)', forkIter);
107 if self.model.hasOpenClasses
108 sourceIndex = self.model.getSource.index;
109 UNnosource = UN; UNnosource(sourceIndex,:) = 0;
110 if any(find(sum(UNnosource(:,isinf(sn.njobs(1:size(QN,2)))),2)>0.99 * sn.nservers))
111 line_warning(mfilename,'The model may be unstable: the utilization of station %i for open
classes exceeds 99 percent.\n',maxpos(sum(UNnosource,2)));
119 line_debug(options, 'Product-form check: hasProductForm=%d (exact method requested)', self.model.hasProductFormSolution);
120 if strcmp(options.method,'exact') && ~self.model.hasProductFormSolution
121 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');
123 if strcmp(options.method,'mva') && ~self.model.hasProductFormSolution
124 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.');
127 method = options.method;
129 % Check for size-based policies (SRPT, PSJF, FB, LRPT, SETF) in multiclass open systems
130 queueIdx = find(sn.nodetype == NodeType.Queue);
131 isSizeBasedPolicy = false;
132 if ~isempty(queueIdx)
133 schedType = sn.sched(sn.nodeToStation(queueIdx(1)));
134 isSizeBasedPolicy = ismember(schedType, [SchedStrategy.SRPT, SchedStrategy.PSJF, SchedStrategy.FB, SchedStrategy.LRPT, SchedStrategy.SETF]);
137 if sn.nclosedjobs == 0 && length(sn.nodetype)==3 && all(sort(sn.nodetype)' == sort([NodeType.Source,NodeType.Queue,NodeType.Sink])) && isSizeBasedPolicy
138 % Multiclass open system with size-based scheduling (SRPT, PSJF, FB, LRPT, SETF)
139 line_debug(options, 'Size-based scheduling detected (%s), routing to qsys_sizebased_analyzer',
char(schedType));
140 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter] = solver_mva_qsys_sizebased_analyzer(sn, options, schedType);
141 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
142 line_debug(options, 'Single-class open queueing system (Source-Queue-Sink), routing to qsys_analyzer');
143 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter] = solver_mva_qsys_analyzer(sn, options);
144 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
145 line_debug(options, 'Multiclass open polling system, routing to polling_analyzer');
146 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter] = solver_mva_polling_analyzer(sn, options);
147 elseif sn.nclosedjobs == 0 && length(sn.nodetype)==3 && all(sort(sn.nodetype)' == sort([NodeType.Source,NodeType.Cache,NodeType.Sink])) %
is a non-rentrant cache
148 line_debug(options, 'Non-reentrant cache (Source-Cache-Sink), routing to cache_analyzer');
149 % random initialization
150 for ind = 1:sn.nnodes
151 if sn.nodetype(ind) == NodeType.Cache
152 prob = self.model.
nodes{ind}.server.hitClass;
154 self.model.nodes{ind}.setResultHitProb(prob);
155 self.model.nodes{ind}.setResultMissProb(1-prob);
158 self.model.refreshChains();
160 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mva_cache_analyzer(sn, options);
162 for ind = 1:sn.nnodes
163 if sn.nodetype(ind) == NodeType.Cache
164 hitClass = self.model.nodes{ind}.getHitClass;
165 missClass = self.model.nodes{ind}.getMissClass;
166 hitprob = zeros(1,length(hitClass));
167 for k=1:length(self.model.nodes{ind}.getHitClass)
168 chain_k = sn.chains(:,k)>0;
169 inchain = sn.chains(chain_k,:)>0;
173 hitprob(k) = XN(h) / sum(XN(inchain),"omitnan"); %
#ok<NANSUM>
176 self.model.nodes{ind}.setResultHitProb(hitprob);
177 self.model.nodes{ind}.setResultMissProb(1-hitprob);
180 %self.model.refreshChains();
181 self.model.refreshStruct(
true);
182 else % queueing network
183 if any(sn.nodetype == NodeType.Cache) %
if integrated caching-queueing
184 line_debug(options,
'Integrated caching-queueing network, routing to cacheqn_analyzer');
185 [QN,UN,RN,TN,CN,XN,lG,hitprob,missprob,runtime,lastiter] = solver_mva_cacheqn_analyzer(self, options);
186 for ind = 1:sn.nnodes
187 if sn.nodetype(ind) == NodeType.Cache
188 self.model.nodes{ind}.setResultHitProb(hitprob(ind,:));
189 self.model.nodes{ind}.setResultMissProb(missprob(ind,:));
192 %self.model.refreshChains();
193 self.model.refreshStruct(
true);
194 else % ordinary queueing network
196 case {
'aba.upper',
'aba.lower',
'bjb.upper',
'bjb.lower',
'pb.upper',
'pb.lower',
'gb.upper',
'gb.lower',
'sb.upper',
'sb.lower'}
197 line_debug(options,
'Using bound method: %s', method);
198 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter] = solver_mva_bound_analyzer(sn, options);
200 if ~isempty(sn.lldscaling) || ~isempty(sn.cdscaling)
201 line_debug(options, 'Load-dependent scaling detected (lldscaling=%d, cdscaling=%d), routing to mvald_analyzer', ~isempty(sn.lldscaling), ~isempty(sn.cdscaling));
202 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mvald_analyzer(sn, options);
204 line_debug(options, 'Standard queueing network, routing to mva_analyzer (method=%s)', method);
205 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mva_analyzer(sn, options);
210 if self.model.hasFork
211 line_debug(options, 'Fork-join post-processing: method=%s, computing sync delays', options.config.fork_join);
214 % Pre-compute linked routing matrix once (loop-invariant)
215 switch options.config.fork_join
216 case {
'mmt',
'default',
'fjt'}
217 Pcs = cell2mat(nonfjmodel.getLinkedRoutingMatrix);
219 for f=find(sn.nodetype == NodeType.Fork)
'
220 switch options.config.fork_join
221 case {'mmt
', 'default', 'fjt
'}
222 TNfork = zeros(1,sn.nclasses);
224 inchain = find(sn.chains(c,:));
226 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));
229 % find join associated to fork f
230 joinIdx = find(sn.fj(f,:));
231 forkauxclasses = find(fjforkmap==f);
232 for s=forkauxclasses(:)
'
233 r = fjclassmap(s); % original class associated to auxiliary class s
235 forkLambda(s) = mean([forkLambda(s); TNfork(r)],1);
237 joinStat = sn.nodeToStation(joinIdx);
238 TN(sn.nodeToStation(joinIdx),r) = TN(sn.nodeToStation(joinIdx),r) + sum(TN(sn.nodeToStation(joinIdx), find(fjclassmap == r))) - TN(sn.nodeToStation(joinIdx), s);
239 forkLambda(s) = mean([forkLambda(s); TN(sn.nodeToStation(joinIdx),r)],1);
241 if isempty(joinIdx) || ~outer_forks(f, r)
242 % No join nodes for this fork, no synchronisation delay
245 % Find the parallel paths coming out of the fork
246 ri = ModelAdapter.findPathsCS(sn, Pcs, f, joinIdx, r, [r,s], QN, TN, 0, fjclassmap, fjforkmap, nonfjmodel);
248 % No routing from fork for this class - set sync delay to 0 (Immediate)
249 % Matches JAR behavior where empty Matrix gives syncDelay = 0
254 parallel_branches = length(ri);
255 for pow=0:(parallel_branches - 1)
256 current_sum = sum(1./sum(nchoosek(lambdai, pow + 1),2));
257 d0 = d0 + (-1)^pow * current_sum;
259 syncDelay = d0*sn.nodeparam{f}.fanOut - mean(ri);
261 % Set the synchronisation delays
262 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{s}, Exp.fitMean(syncDelay));
264 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{r}, Exp.fitMean(syncDelay));
267 case {'heidelberger-trivedi
', 'ht
'}
268 joinIdx = find(sn.fj(f,:));
270 inchain = find(sn.chains(c,:));
275 % Obtain the response times on the parallel branches
276 ri = RN(:, find(fjclassmap == r));
277 ri(isnan(ri) | isinf(ri)) = 0;
278 ri = sum(ri, 1,
"omitnan") - RN(nonfjstruct.nodeToStation(fj_auxiliary_delays{joinIdx}), find(fjclassmap == r)) - RN(nonfjstruct.nodeToStation(joinIdx), find(fjclassmap == r));
281 parallel_branches = length(self.model.nodes{f}.output.outputStrategy{r}{3});
282 for pow=0:(parallel_branches - 1)
283 current_sum = sum(1./sum(nchoosek(lambdai, pow + 1),2));
284 d0 = d0 + (-1)^pow * current_sum;
286 di = d0*sn.nodeparam{f}.fanOut - ri;
287 r0 = sum(RN(:, inchain), 2);
288 r0(isnan(r0) | isinf(r0)) = 0;
289 r0 = sum(r0, 1,
"omitnan") - RN(nonfjstruct.nodeToStation(joinIdx), r);
290 % Update the delays at the join node and at the auxiliary delay
291 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{r}, Exp.fitMean(d0*sn.nodeparam{f}.fanOut));
293 for s=find(fjclassmap == r)
294 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{s}, Exp.fitMean(di(idx)));
296 nonfjmodel.nodes{fj_auxiliary_delays{joinIdx}}.setService(nonfjmodel.classes{s}, Exp.fitMean(r0));
303 % Batch refreshRates after all sync delay updates (moved out of inner loops
for performance)
304 switch options.config.fork_join
305 case {
'mmt',
'default',
'fjt'}
306 nonfjmodel.refreshRates();
308 switch options.config.fork_join
309 case {
'heidelberger-trivedi',
'ht'}
310 nonfjmodel.refreshStruct();
311 % Delete the queue lengths, response times, throughputs and utilizations of the original
classes at the join
nodes
312 QN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = 0;
313 RN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = 0;
314 % Save the throughputs of the original
classes at the join node
315 TN_orig = TN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap));
316 TN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = 0;
317 UN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = 0;
319 % Remove the times at the auxiliary delay
320 QN(nonfjstruct.nodeToStation(cell2mat(fj_auxiliary_delays)),:) = [];
321 UN(nonfjstruct.nodeToStation(cell2mat(fj_auxiliary_delays)),:) = [];
322 RN(nonfjstruct.nodeToStation(cell2mat(fj_auxiliary_delays)),:) = [];
323 TN(nonfjstruct.nodeToStation(cell2mat(fj_auxiliary_delays)),:) = [];
325 for r=1:length(fjclassmap)
328 QN(:,s) = QN(:,s) + QN(:,r);
329 UN(:,s) = UN(:,s) + UN(:,r);
330 % Add all throughputs of the auxiliary
classes to facilitate the computation of the response times
331 TN(:,s) = TN(:,s) + TN(:,r);
332 RN(:,s) = QN(:,s) ./ TN(:,s);
335 % Re-set the throughputs
for the original
classes
336 TN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = TN_orig;
337 case {
'mmt',
'default',
'fjt'}
338 TN_orig = TN([nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Source))], nonzeros(fjclassmap));
340 for r=1:length(fjclassmap)
343 QN(:,s) = QN(:,s) + QN(:,r);
344 UN(:,s) = UN(:,s) + UN(:,r);
345 TN(:,s) = TN(:,s) + TN(:,r);
346 %RN(:,s) = RN(:,s) + RN(:,r);
347 %
for i=find(snorig.nodetype == NodeType.Delay | snorig.nodetype == NodeType.Queue)
'
348 % TN(snorig.nodeToStation(i),s) = TN(snorig.nodeToStation(i),s) + TN(snorig.nodeToStation(i),r);
350 RN(:,s) = QN(:,s) ./ TN(:,s);
351 %CN(:,s) = CN(:,s) + CN(:,r);
352 %XN(:,s) = XN(:,s) + XN(:,r);
355 TN([nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Source))], nonzeros(fjclassmap)) = TN_orig;
357 QN(:,fjclassmap>0) = [];
358 UN(:,fjclassmap>0) = [];
359 RN(:,fjclassmap>0) = [];
360 TN(:,fjclassmap>0) = [];
361 CN(:,fjclassmap>0) = [];
362 XN(:,fjclassmap>0) = [];
364 iter = iter + lastiter;
365 % Cap accumulated iterations for stability (Python parity)
368 break; % Exit forkLoop early
372 sn = self.model.getStruct();
374 % Compute average residence time at steady-state
375 AN = sn_get_arvr_from_tput(sn, TN, self.getAvgTputHandles());
376 WN = sn_get_residt_from_respt(sn, RN, self.getAvgResidTHandles());
377 if strcmp(method,'default') && exist('actualmethod
','var
')
378 self.setAvgResults(QN,UN,RN,TN,AN,WN,CN,XN,runtime,['default/
' actualmethod],iter);
380 self.setAvgResults(QN,UN,RN,TN,AN,WN,CN,XN,runtime,method,iter);
382 self.result.Prob.logNormConstAggr = lG;