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 sn = getStruct(self); % doesn
't need initial state
29 jmodel = LINE2JLINE(self.model);
30 %M = jmodel.getNumberOfStatefulNodes;
31 M = jmodel.getNumberOfStations;
32 R = jmodel.getNumberOfClasses;
33 jsolver = JLINE.SolverMVA(jmodel, options);
34 [QN,UN,RN,WN,AN,TN] = JLINE.arrayListToResults(jsolver.getAvgTable);
35 runtime = jsolver.result.runtime;
38 QN = reshape(QN',R,M)
';
39 UN = reshape(UN',R,M)
';
40 RN = reshape(RN',R,M)
';
41 TN = reshape(TN',R,M)
';
42 WN = reshape(WN',R,M)
';
43 AN = reshape(AN',R,M)
';
47 if sn.nodetype(ind) == NodeType.Cache
48 self.model.nodes{ind}.setResultHitProb(JLINE.from_jline_matrix(jmodel.getNodeByIndex(ind-1).getHitRatio()));
49 self.model.nodes{ind}.setResultMissProb(JLINE.from_jline_matrix(jmodel.getNodeByIndex(ind-1).getMissRatio()));
52 %self.model.refreshChains();
53 self.model.refreshStruct(true);
54 self.setAvgResults(QN,UN,RN,TN,AN,WN,CN,XN,runtime,options.method,lastiter);
55 self.result.Prob.logNormConstAggr = lG;
58 sn = getStruct(self); % doesn't need initial state
61 % create artificial
classes arrival rates
62 forkLambda = GlobalConstants.FineTol * ones(1, 2*sn.nclasses*sum(sn.nodetype==NodeType.Fork));
63 QN = GlobalConstants.Immediate * ones(1, sn.nclasses);
66 while (forkLoop && forkIter < options.iter_max)
68 forkIter = forkIter + 1;
70 switch options.config.fork_join
71 case {
'heidelberger-trivedi',
'ht'}
72 [nonfjmodel, fjclassmap, fjforkmap, fj_auxiliary_delays] = ModelAdapter.ht(self.model);
73 case {
'mmt',
'default',
'fjt'}
74 [nonfjmodel, fjclassmap, fjforkmap, fanout] = ModelAdapter.mmt(self.model, forkLambda);
75 [outer_forks, parent_forks] = ModelAdapter.sortForks(sn, fjforkmap, fjclassmap, nonfjmodel);
77 elseif ~strcmp(options.config.fork_join,
'heidelberger-trivedi') & ~strcmp(options.config.fork_join,
'ht')
78 %line_printf(
'Fork-join iteration %d\n',forkIter);
79 nonfjSource = nonfjmodel.getSource;
80 for r=1:length(fjclassmap) % r
is the auxiliary
class
84 if ~nonfjSource.arrivalProcess{r}.isDisabled
85 nonfjSource.arrivalProcess{r}.setRate((fanout(r)-1)*forkLambda(r));
90 nonfjmodel.refreshRates();
92 sn = nonfjmodel.getStruct(
false); %
this ensures that we solve nonfjmodel instead of the original model
93 % Convergence check: handle zero/NaN queue lengths from size mismatch and Immediate activities
94 qn_ratio = QN_1 ./ QN;
95 qn_ratio(isnan(qn_ratio)) = 1; % 0/0 = NaN → treat as converged (ratio=1)
96 qn_ratio(isinf(qn_ratio)) = 0; % x/0 = Inf → treat as not converged (ratio=0, error=1)
97 if isequal(size(QN_1), size(QN)) && max(abs(1 - qn_ratio(:))) < GlobalConstants.CoarseTol && (forkIter > 2)
100 if self.model.hasOpenClasses
101 sourceIndex = self.model.getSource.index;
102 UNnosource = UN; UNnosource(sourceIndex,:) = 0;
103 if any(find(sum(UNnosource(:,isinf(sn.njobs(1:size(QN,2)))),2)>0.99 * sn.nservers))
104 line_warning(mfilename,'The model may be unstable: the utilization of station %i for open
classes exceeds 99 percent.\n',maxpos(sum(UNnosource,2)));
112 if strcmp(options.method,'exact') && ~self.model.hasProductFormSolution
113 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');
115 if strcmp(options.method,'mva') && ~self.model.hasProductFormSolution
116 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.');
119 method = options.method;
121 % Check for size-based policies (SRPT, PSJF, FB, LRPT, SETF) in multiclass open systems
122 queueIdx = find(sn.nodetype == NodeType.Queue);
123 isSizeBasedPolicy = false;
124 if ~isempty(queueIdx)
125 schedType = sn.sched(sn.nodeToStation(queueIdx(1)));
126 isSizeBasedPolicy = ismember(schedType, [SchedStrategy.SRPT, SchedStrategy.PSJF, SchedStrategy.FB, SchedStrategy.LRPT, SchedStrategy.SETF]);
129 if sn.nclosedjobs == 0 && length(sn.nodetype)==3 && all(sort(sn.nodetype)' == sort([NodeType.Source,NodeType.Queue,NodeType.Sink])) && isSizeBasedPolicy
130 % Multiclass open system with size-based scheduling (SRPT, PSJF, FB, LRPT, SETF)
131 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter] = solver_mva_qsys_sizebased_analyzer(sn, options, schedType);
132 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
133 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter] = solver_mva_qsys_analyzer(sn, options);
134 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
135 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter] = solver_mva_polling_analyzer(sn, options);
136 elseif sn.nclosedjobs == 0 && length(sn.nodetype)==3 && all(sort(sn.nodetype)' == sort([NodeType.Source,NodeType.Cache,NodeType.Sink])) %
is a non-rentrant cache
137 % random initialization
138 for ind = 1:sn.nnodes
139 if sn.nodetype(ind) == NodeType.Cache
140 prob = self.model.
nodes{ind}.server.hitClass;
142 self.model.nodes{ind}.setResultHitProb(prob);
143 self.model.nodes{ind}.setResultMissProb(1-prob);
146 self.model.refreshChains();
148 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mva_cache_analyzer(sn, options);
150 for ind = 1:sn.nnodes
151 if sn.nodetype(ind) == NodeType.Cache
152 hitClass = self.model.nodes{ind}.getHitClass;
153 missClass = self.model.nodes{ind}.getMissClass;
154 hitprob = zeros(1,length(hitClass));
155 for k=1:length(self.model.nodes{ind}.getHitClass)
156 chain_k = sn.chains(:,k)>0;
157 inchain = sn.chains(chain_k,:)>0;
161 hitprob(k) = XN(h) / sum(XN(inchain),"omitnan"); %
#ok<NANSUM>
164 self.model.nodes{ind}.setResultHitProb(hitprob);
165 self.model.nodes{ind}.setResultMissProb(1-hitprob);
168 %self.model.refreshChains();
169 self.model.refreshStruct(
true);
170 else % queueing network
171 if any(sn.nodetype == NodeType.Cache) %
if integrated caching-queueing
172 [QN,UN,RN,TN,CN,XN,lG,hitprob,missprob,runtime,lastiter] = solver_mva_cacheqn_analyzer(self, options);
173 for ind = 1:sn.nnodes
174 if sn.nodetype(ind) == NodeType.Cache
175 self.model.nodes{ind}.setResultHitProb(hitprob(ind,:));
176 self.model.nodes{ind}.setResultMissProb(missprob(ind,:));
179 %self.model.refreshChains();
180 self.model.refreshStruct(
true);
181 else % ordinary queueing network
183 case {
'aba.upper',
'aba.lower',
'bjb.upper',
'bjb.lower',
'pb.upper',
'pb.lower',
'gb.upper',
'gb.lower',
'sb.upper',
'sb.lower'}
184 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter] = solver_mva_bound_analyzer(sn, options);
186 if ~isempty(sn.lldscaling) || ~isempty(sn.cdscaling)
187 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mvald_analyzer(sn, options);
189 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mva_analyzer(sn, options);
194 if self.model.hasFork
197 % Pre-compute linked routing matrix once (loop-invariant)
198 switch options.config.fork_join
199 case {
'mmt',
'default',
'fjt'}
200 Pcs = cell2mat(nonfjmodel.getLinkedRoutingMatrix);
202 for f=find(sn.nodetype == NodeType.Fork)
'
203 switch options.config.fork_join
204 case {'mmt
', 'default', 'fjt
'}
205 TNfork = zeros(1,sn.nclasses);
207 inchain = find(sn.chains(c,:));
209 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));
212 % find join associated to fork f
213 joinIdx = find(sn.fj(f,:));
214 forkauxclasses = find(fjforkmap==f);
215 for s=forkauxclasses(:)
'
216 r = fjclassmap(s); % original class associated to auxiliary class s
218 forkLambda(s) = mean([forkLambda(s); TNfork(r)],1);
220 joinStat = sn.nodeToStation(joinIdx);
221 TN(sn.nodeToStation(joinIdx),r) = TN(sn.nodeToStation(joinIdx),r) + sum(TN(sn.nodeToStation(joinIdx), find(fjclassmap == r))) - TN(sn.nodeToStation(joinIdx), s);
222 forkLambda(s) = mean([forkLambda(s); TN(sn.nodeToStation(joinIdx),r)],1);
224 if isempty(joinIdx) || ~outer_forks(f, r)
225 % No join nodes for this fork, no synchronisation delay
228 % Find the parallel paths coming out of the fork
229 ri = ModelAdapter.findPathsCS(sn, Pcs, f, joinIdx, r, [r,s], QN, TN, 0, fjclassmap, fjforkmap, nonfjmodel);
231 % No routing from fork for this class - set sync delay to 0 (Immediate)
232 % Matches JAR behavior where empty Matrix gives syncDelay = 0
237 parallel_branches = length(ri);
238 for pow=0:(parallel_branches - 1)
239 current_sum = sum(1./sum(nchoosek(lambdai, pow + 1),2));
240 d0 = d0 + (-1)^pow * current_sum;
242 syncDelay = d0*sn.nodeparam{f}.fanOut - mean(ri);
244 % Set the synchronisation delays
245 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{s}, Exp.fitMean(syncDelay));
247 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{r}, Exp.fitMean(syncDelay));
250 case {'heidelberger-trivedi
', 'ht
'}
251 joinIdx = find(sn.fj(f,:));
253 inchain = find(sn.chains(c,:));
258 % Obtain the response times on the parallel branches
259 ri = RN(:, find(fjclassmap == r));
260 ri(isnan(ri) | isinf(ri)) = 0;
261 ri = sum(ri, 1,
"omitnan") - RN(nonfjstruct.nodeToStation(fj_auxiliary_delays{joinIdx}), find(fjclassmap == r)) - RN(nonfjstruct.nodeToStation(joinIdx), find(fjclassmap == r));
264 parallel_branches = length(self.model.nodes{f}.output.outputStrategy{r}{3});
265 for pow=0:(parallel_branches - 1)
266 current_sum = sum(1./sum(nchoosek(lambdai, pow + 1),2));
267 d0 = d0 + (-1)^pow * current_sum;
269 di = d0*sn.nodeparam{f}.fanOut - ri;
270 r0 = sum(RN(:, inchain), 2);
271 r0(isnan(r0) | isinf(r0)) = 0;
272 r0 = sum(r0, 1,
"omitnan") - RN(nonfjstruct.nodeToStation(joinIdx), r);
273 % Update the delays at the join node and at the auxiliary delay
274 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{r}, Exp.fitMean(d0*sn.nodeparam{f}.fanOut));
276 for s=find(fjclassmap == r)
277 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{s}, Exp.fitMean(di(idx)));
279 nonfjmodel.nodes{fj_auxiliary_delays{joinIdx}}.setService(nonfjmodel.classes{s}, Exp.fitMean(r0));
286 % Batch refreshRates after all sync delay updates (moved out of inner loops
for performance)
287 switch options.config.fork_join
288 case {
'mmt',
'default',
'fjt'}
289 nonfjmodel.refreshRates();
291 switch options.config.fork_join
292 case {
'heidelberger-trivedi',
'ht'}
293 nonfjmodel.refreshStruct();
294 % Delete the queue lengths, response times, throughputs and utilizations of the original
classes at the join
nodes
295 QN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = 0;
296 RN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = 0;
297 % Save the throughputs of the original
classes at the join node
298 TN_orig = TN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap));
299 TN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = 0;
300 UN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = 0;
302 % Remove the times at the auxiliary delay
303 QN(nonfjstruct.nodeToStation(cell2mat(fj_auxiliary_delays)),:) = [];
304 UN(nonfjstruct.nodeToStation(cell2mat(fj_auxiliary_delays)),:) = [];
305 RN(nonfjstruct.nodeToStation(cell2mat(fj_auxiliary_delays)),:) = [];
306 TN(nonfjstruct.nodeToStation(cell2mat(fj_auxiliary_delays)),:) = [];
308 for r=1:length(fjclassmap)
311 QN(:,s) = QN(:,s) + QN(:,r);
312 UN(:,s) = UN(:,s) + UN(:,r);
313 % Add all throughputs of the auxiliary
classes to facilitate the computation of the response times
314 TN(:,s) = TN(:,s) + TN(:,r);
315 RN(:,s) = QN(:,s) ./ TN(:,s);
318 % Re-set the throughputs
for the original
classes
319 TN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = TN_orig;
320 case {
'mmt',
'default',
'fjt'}
321 TN_orig = TN([nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Source))], nonzeros(fjclassmap));
323 for r=1:length(fjclassmap)
326 QN(:,s) = QN(:,s) + QN(:,r);
327 UN(:,s) = UN(:,s) + UN(:,r);
328 TN(:,s) = TN(:,s) + TN(:,r);
329 %RN(:,s) = RN(:,s) + RN(:,r);
330 %
for i=find(snorig.nodetype == NodeType.Delay | snorig.nodetype == NodeType.Queue)
'
331 % TN(snorig.nodeToStation(i),s) = TN(snorig.nodeToStation(i),s) + TN(snorig.nodeToStation(i),r);
333 RN(:,s) = QN(:,s) ./ TN(:,s);
334 %CN(:,s) = CN(:,s) + CN(:,r);
335 %XN(:,s) = XN(:,s) + XN(:,r);
338 TN([nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Source))], nonzeros(fjclassmap)) = TN_orig;
340 QN(:,fjclassmap>0) = [];
341 UN(:,fjclassmap>0) = [];
342 RN(:,fjclassmap>0) = [];
343 TN(:,fjclassmap>0) = [];
344 CN(:,fjclassmap>0) = [];
345 XN(:,fjclassmap>0) = [];
347 iter = iter + lastiter;
348 % Cap accumulated iterations for stability (Python parity)
351 break; % Exit forkLoop early
355 sn = self.model.getStruct();
357 % Compute average residence time at steady-state
358 AN = sn_get_arvr_from_tput(sn, TN, self.getAvgTputHandles());
359 WN = sn_get_residt_from_respt(sn, RN, self.getAvgResidTHandles());
360 if strcmp(method,'default') && exist('actualmethod
','var
')
361 self.setAvgResults(QN,UN,RN,TN,AN,WN,CN,XN,runtime,['default/
' actualmethod],iter);
363 self.setAvgResults(QN,UN,RN,TN,AN,WN,CN,XN,runtime,method,iter);
365 self.result.Prob.logNormConstAggr = lG;