LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
runAnalyzer.m
1function runtime = runAnalyzer(self, options)
2% RUNTIME = RUNANALYZER(OPTIONS)
3% Run the solver
4
5if nargin<2
6 options = self.getOptions;
7end
8
9self.runAnalyzerChecks(options);
10
11sn = self.getStruct();
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');
14end
15
16Solver.resetRandomGeneratorSeed(options.seed);
17
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);
22 if ~isempty(libs)
23 line_printf('The solver will leverage %s.\n', strjoin(libs, ', '));
24 GlobalConstants.setLibraryAttributionShown(true);
25 end
26end
27
28iter = 0;
29%options.lang='java';
30
31switch options.lang
32 case 'java'
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;
42 CN = [];
43 XN = [];
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)';
50 lG = NaN;
51 lastiter = NaN;
52 for ind = 1:sn.nnodes
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()));
56 end
57 end
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;
62 return
63 case 'matlab'
64 line_debug(options, 'MVA: using lang=matlab');
65 sn = getStruct(self); % doesn't need initial state
66 forkLoop = true;
67 forkIter = 0;
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);
71 QN_1 = 0*QN;
72 UN = 0*QN;
73 while (forkLoop && forkIter < options.iter_max)
74 if self.model.hasFork
75 forkIter = forkIter + 1;
76 line_debug(options, 'Fork-join iteration %d', forkIter);
77 if forkIter == 1
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);
86 end
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
91 s = fjclassmap(r);
92 if s>0
93 if fanout(r)>0
94 if ~nonfjSource.arrivalProcess{r}.isDisabled
95 nonfjSource.arrivalProcess{r}.setRate((fanout(r)-1)*forkLambda(r));
96 end
97 end
98 end
99 end
100 nonfjmodel.refreshRates();
101 end
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);
110 forkLoop = false;
111 else
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)));
117 end
118 end
119 QN_1 = QN;
120 end
121 else
122 forkLoop = false;
123 end
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');
127 end
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.');
130 end
131
132 method = options.method;
133
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]);
140 end
141
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;
158 prob(prob>0) = 0.5;
159 self.model.nodes{ind}.setResultHitProb(prob);
160 self.model.nodes{ind}.setResultMissProb(1-prob);
161 end
162 end
163 self.model.refreshChains();
164 % start iteration
165 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mva_cache_analyzer(sn, options);
166
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;
175 h = hitClass(k);
176 m = missClass(k);
177 if h>0 && m>0
178 hitprob(k) = XN(h) / sum(XN(inchain),"omitnan"); %#ok<NANSUM>
179 end
180 end
181 self.model.nodes{ind}.setResultHitProb(hitprob);
182 self.model.nodes{ind}.setResultMissProb(1-hitprob);
183 end
184 end
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,:));
195 end
196 end
197 %self.model.refreshChains();
198 self.model.refreshStruct(true);
199 else % ordinary queueing network
200 switch method
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);
204 otherwise
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);
208 else
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);
211 end
212 end
213 end
214 end
215 if self.model.hasFork
216 line_debug(options, 'Fork-join post-processing: method=%s, computing sync delays', options.config.fork_join);
217 nonfjstruct = sn;
218 sn = self.getStruct;
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);
223 end
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);
228 for c=1:sn.nchains
229 inchain = find(sn.chains(c,:));
230 for r=inchain(:)'
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));
232 end
233 end
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
239 if isempty(joinIdx)
240 forkLambda(s) = mean([forkLambda(s); TNfork(r)],1);
241 else
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);
245 end
246 if isempty(joinIdx) || ~outer_forks(f, r)
247 % No join nodes for this fork, no synchronisation delay
248 continue;
249 end
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);
252 if isempty(ri)
253 % No routing from fork for this class - set sync delay to 0 (Immediate)
254 % Matches JAR behavior where empty Matrix gives syncDelay = 0
255 syncDelay = 0;
256 else
257 lambdai = 1./ri;
258 d0 = 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;
263 end
264 syncDelay = d0*sn.nodeparam{f}.fanOut - mean(ri);
265 end
266 % Set the synchronisation delays
267 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{s}, Exp.fitMean(syncDelay));
268 if outer_forks(f, r)
269 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{r}, Exp.fitMean(syncDelay));
270 end
271 end
272 case {'heidelberger-trivedi', 'ht'}
273 joinIdx = find(sn.fj(f,:));
274 for c=1:sn.nchains
275 inchain = find(sn.chains(c,:));
276 for r=inchain(:)'
277 if sn.nodevisits{c}(f,r) == 0
278 continue;
279 end
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));
284 lambdai = 1./ri;
285 d0 = 0;
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;
290 end
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));
297 idx = 1;
298 for s=find(fjclassmap == r)
299 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{s}, Exp.fitMean(di(idx)));
300 idx = idx + 1;
301 nonfjmodel.nodes{fj_auxiliary_delays{joinIdx}}.setService(nonfjmodel.classes{s}, Exp.fitMean(r0));
302 end
303
304 end
305 end
306 end
307 end
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();
312 end
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;
323
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)),:) = [];
329 % merge back artificial classes into their original classes
330 for r=1:length(fjclassmap)
331 s = fjclassmap(r);
332 if s>0
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);
338 end
339 end
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));
344 % merge back artificial classes into their original classes
345 for r=1:length(fjclassmap)
346 s = fjclassmap(r);
347 if s>0
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);
354 % end
355 RN(:,s) = QN(:,s) ./ TN(:,s);
356 %CN(:,s) = CN(:,s) + CN(:,r);
357 %XN(:,s) = XN(:,s) + XN(:,r);
358 end
359 end
360 TN([nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Source))], nonzeros(fjclassmap)) = TN_orig;
361 end
362 QN(:,fjclassmap>0) = [];
363 UN(:,fjclassmap>0) = [];
364 RN(:,fjclassmap>0) = [];
365 TN(:,fjclassmap>0) = [];
366 CN(:,fjclassmap>0) = [];
367 XN(:,fjclassmap>0) = [];
368 end
369 iter = iter + lastiter;
370 % Cap accumulated iterations for stability (Python parity)
371 if iter > 10000
372 iter = 10000;
373 break; % Exit forkLoop early
374 end
375 end
376
377 sn = self.model.getStruct();
378
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);
384 else
385 self.setAvgResults(QN,UN,RN,TN,AN,WN,CN,XN,runtime,method,iter);
386 end
387 self.result.Prob.logNormConstAggr = lG;
388end
389end
390
391
Definition mmt.m:124