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
11Solver.resetRandomGeneratorSeed(options.seed);
12
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);
17 if ~isempty(libs)
18 line_printf('The solver will leverage %s.\n', strjoin(libs, ', '));
19 GlobalConstants.setLibraryAttributionShown(true);
20 end
21end
22
23iter = 0;
24%options.lang='java';
25
26switch options.lang
27 case 'java'
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;
37 CN = [];
38 XN = [];
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)';
45 lG = NaN;
46 lastiter = NaN;
47 for ind = 1:sn.nnodes
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()));
51 end
52 end
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;
57 return
58 case 'matlab'
59 line_debug(options, 'MVA: using lang=matlab');
60 sn = getStruct(self); % doesn't need initial state
61 forkLoop = true;
62 forkIter = 0;
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);
66 QN_1 = 0*QN;
67 UN = 0*QN;
68 while (forkLoop && forkIter < options.iter_max)
69 if self.model.hasFork
70 forkIter = forkIter + 1;
71 line_debug(options, 'Fork-join iteration %d', forkIter);
72 if forkIter == 1
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);
81 end
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
86 s = fjclassmap(r);
87 if s>0
88 if fanout(r)>0
89 if ~nonfjSource.arrivalProcess{r}.isDisabled
90 nonfjSource.arrivalProcess{r}.setRate((fanout(r)-1)*forkLambda(r));
91 end
92 end
93 end
94 end
95 nonfjmodel.refreshRates();
96 end
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);
105 forkLoop = false;
106 else
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)));
112 end
113 end
114 QN_1 = QN;
115 end
116 else
117 forkLoop = false;
118 end
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');
122 end
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.');
125 end
126
127 method = options.method;
128
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]);
135 end
136
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;
153 prob(prob>0) = 0.5;
154 self.model.nodes{ind}.setResultHitProb(prob);
155 self.model.nodes{ind}.setResultMissProb(1-prob);
156 end
157 end
158 self.model.refreshChains();
159 % start iteration
160 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mva_cache_analyzer(sn, options);
161
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;
170 h = hitClass(k);
171 m = missClass(k);
172 if h>0 && m>0
173 hitprob(k) = XN(h) / sum(XN(inchain),"omitnan"); %#ok<NANSUM>
174 end
175 end
176 self.model.nodes{ind}.setResultHitProb(hitprob);
177 self.model.nodes{ind}.setResultMissProb(1-hitprob);
178 end
179 end
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,:));
190 end
191 end
192 %self.model.refreshChains();
193 self.model.refreshStruct(true);
194 else % ordinary queueing network
195 switch method
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);
199 otherwise
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);
203 else
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);
206 end
207 end
208 end
209 end
210 if self.model.hasFork
211 line_debug(options, 'Fork-join post-processing: method=%s, computing sync delays', options.config.fork_join);
212 nonfjstruct = sn;
213 sn = self.getStruct;
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);
218 end
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);
223 for c=1:sn.nchains
224 inchain = find(sn.chains(c,:));
225 for r=inchain(:)'
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));
227 end
228 end
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
234 if isempty(joinIdx)
235 forkLambda(s) = mean([forkLambda(s); TNfork(r)],1);
236 else
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);
240 end
241 if isempty(joinIdx) || ~outer_forks(f, r)
242 % No join nodes for this fork, no synchronisation delay
243 continue;
244 end
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);
247 if isempty(ri)
248 % No routing from fork for this class - set sync delay to 0 (Immediate)
249 % Matches JAR behavior where empty Matrix gives syncDelay = 0
250 syncDelay = 0;
251 else
252 lambdai = 1./ri;
253 d0 = 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;
258 end
259 syncDelay = d0*sn.nodeparam{f}.fanOut - mean(ri);
260 end
261 % Set the synchronisation delays
262 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{s}, Exp.fitMean(syncDelay));
263 if outer_forks(f, r)
264 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{r}, Exp.fitMean(syncDelay));
265 end
266 end
267 case {'heidelberger-trivedi', 'ht'}
268 joinIdx = find(sn.fj(f,:));
269 for c=1:sn.nchains
270 inchain = find(sn.chains(c,:));
271 for r=inchain(:)'
272 if sn.nodevisits{c}(f,r) == 0
273 continue;
274 end
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));
279 lambdai = 1./ri;
280 d0 = 0;
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;
285 end
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));
292 idx = 1;
293 for s=find(fjclassmap == r)
294 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{s}, Exp.fitMean(di(idx)));
295 idx = idx + 1;
296 nonfjmodel.nodes{fj_auxiliary_delays{joinIdx}}.setService(nonfjmodel.classes{s}, Exp.fitMean(r0));
297 end
298
299 end
300 end
301 end
302 end
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();
307 end
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;
318
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)),:) = [];
324 % merge back artificial classes into their original classes
325 for r=1:length(fjclassmap)
326 s = fjclassmap(r);
327 if s>0
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);
333 end
334 end
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));
339 % merge back artificial classes into their original classes
340 for r=1:length(fjclassmap)
341 s = fjclassmap(r);
342 if s>0
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);
349 % end
350 RN(:,s) = QN(:,s) ./ TN(:,s);
351 %CN(:,s) = CN(:,s) + CN(:,r);
352 %XN(:,s) = XN(:,s) + XN(:,r);
353 end
354 end
355 TN([nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Source))], nonzeros(fjclassmap)) = TN_orig;
356 end
357 QN(:,fjclassmap>0) = [];
358 UN(:,fjclassmap>0) = [];
359 RN(:,fjclassmap>0) = [];
360 TN(:,fjclassmap>0) = [];
361 CN(:,fjclassmap>0) = [];
362 XN(:,fjclassmap>0) = [];
363 end
364 iter = iter + lastiter;
365 % Cap accumulated iterations for stability (Python parity)
366 if iter > 10000
367 iter = 10000;
368 break; % Exit forkLoop early
369 end
370 end
371
372 sn = self.model.getStruct();
373
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);
379 else
380 self.setAvgResults(QN,UN,RN,TN,AN,WN,CN,XN,runtime,method,iter);
381 end
382 self.result.Prob.logNormConstAggr = lG;
383end
384end
385
386
Definition mmt.m:124