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
9% Wall-clock time-budget launch marker (see options.timeout / lineTimeoutExceeded)
10options.timeout_tic = tic;
11
12if strcmp(options.lang,'python')
13 line_debug(options, 'MVA: using lang=python, delegating to native line_solver');
14 [QN,UN,RN,TN,AN,WN,runtime] = PYLINE.getAvg(self.name, self.model, options);
15 self.setAvgResults(QN,UN,RN,TN,AN,WN,[],[],runtime,options.method,NaN);
16 return
17end
18
19self.runAnalyzerChecks(options);
20
21sn = self.getStruct();
22% Finite Capacity Region: MVA does not enforce the aggregate per-region job
23% limit and would silently return the unconstrained product-form answer.
24if isfield(sn,'nregions') && sn.nregions > 0
25 line_error(mfilename,'This model uses a Finite Capacity Region (addRegion), which is not supported by SolverMVA. Use SolverJMT, or setCapacity for a single-station limit.');
26end
27if isfield(sn,'immfeed') && ~isempty(sn.immfeed) && any(sn.immfeed(:))
28 line_warning(mfilename,'SolverMVA does not handle immediate feedback (immfeed); the solver will treat self-loops as class-switching with re-queueing.\n');
29end
30
31Solver.resetRandomGeneratorSeed(options.seed);
32
33% Show library attribution if verbose and not yet shown
34if options.verbose ~= VerboseLevel.SILENT && ~GlobalConstants.isLibraryAttributionShown()
35 sn = self.getStruct();
36 libs = SolverMVA.getLibrariesUsed(sn, options);
37 if ~isempty(libs)
38 line_printf('The solver will leverage %s.\n', strjoin(libs, ', '));
39 GlobalConstants.setLibraryAttributionShown(true);
40 end
41end
42
43iter = 0;
44%options.lang='java';
45
46switch options.lang
47 case 'java'
48 line_debug(options, 'MVA: using lang=java, delegating to JLINE');
49 sn = getStruct(self); % doesn't need initial state
50 jmodel = LINE2JLINE(self.model);
51 %M = jmodel.getNumberOfStatefulNodes;
52 M = jmodel.getNumberOfStations;
53 R = jmodel.getNumberOfClasses;
54 jsolver = JLINE.SolverMVA(jmodel, options);
55 % The JLINE solver object is rebuilt on every call, so the JAR-side
56 % fork-join (MMT) iterate cannot survive an outer iteration on its own.
57 % Carry it across here, exactly as the lang='matlab' branch does with
58 % self.fjForkLambda, so SolverLN layers warm-start identically in both
59 % languages.
60 if options.config.fj_warmstart && ~isempty(self.fjForkLambda)
61 jsolver.setForkWarmStart(JLINE.from_line_matrix(self.fjForkLambda));
62 end
63 [QN,UN,RN,WN,AN,TN] = JLINE.arrayListToResults(jsolver.getAvgTable);
64 if self.model.hasFork
65 self.fjForkLambda = JLINE.from_jline_matrix(jsolver.getForkWarmStart());
66 end
67 runtime = jsolver.result.runtime;
68 CN = [];
69 XN = [];
70 QN = reshape(QN',R,M)';
71 UN = reshape(UN',R,M)';
72 RN = reshape(RN',R,M)';
73 TN = reshape(TN',R,M)';
74 WN = reshape(WN',R,M)';
75 AN = reshape(AN',R,M)';
76 lG = NaN;
77 lastiter = NaN;
78 for ind = 1:sn.nnodes
79 if sn.nodetype(ind) == NodeType.Cache
80 jnode = jmodel.getNodeByIndex(ind-1);
81 self.model.nodes{ind}.setResultHitProb(JLINE.from_jline_matrix(jnode.getHitRatio()));
82 self.model.nodes{ind}.setResultMissProb(JLINE.from_jline_matrix(jnode.getMissRatio()));
83 % Retrieval-cache extras (delayed-hit ratio, per-list hit ratio
84 % and expected latency) so getAvgCacheTable matches the native path.
85 self.model.nodes{ind}.setResultDelayedHitProb(JLINE.from_jline_matrix(jnode.getDelayedHitRatio()));
86 self.model.nodes{ind}.setResultHitProbList(JLINE.from_jline_matrix(jnode.getHitRatioByList()));
87 self.model.nodes{ind}.setResultItemProb(JLINE.from_jline_matrix(jnode.getItemProb()));
88 self.model.nodes{ind}.setResultResidT(JLINE.from_jline_matrix(jnode.getResidT()));
89 end
90 end
91 %self.model.refreshChains();
92 self.model.refreshStruct(true);
93 self.setAvgResults(QN,UN,RN,TN,AN,WN,CN,XN,runtime,options.method,lastiter);
94 self.result.Prob.logNormConstAggr = lG;
95 return
96 case 'matlab'
97 line_debug(options, 'MVA: using lang=matlab');
98 sn = getStruct(self); % doesn't need initial state
99 forkLoop = true;
100 forkIter = 0;
101 % create artificial classes arrival rates. Under an outer iteration
102 % (e.g. SolverLN) the MMT fixed point is re-solved once per outer
103 % iteration on a model whose parameters move only slightly; restarting
104 % from FineTol each time discards the previous converged point and
105 % forces the inner loop to re-converge from scratch. Warm-start from
106 % the retained iterate when it is conformant.
107 forkLambda = GlobalConstants.FineTol * ones(1, 2*sn.nclasses*sum(sn.nodetype==NodeType.Fork));
108 if options.config.fj_warmstart && ~isempty(self.fjForkLambda) ...
109 && isequal(size(self.fjForkLambda), size(forkLambda))
110 forkLambda = self.fjForkLambda;
111 end
112 QN = GlobalConstants.Immediate * ones(1, sn.nclasses);
113 QN_1 = 0*QN;
114 UN = 0*QN;
115 while (forkLoop && forkIter < options.iter_max)
116 if self.model.hasFork
117 forkIter = forkIter + 1;
118 line_debug(options, 'Fork-join iteration %d', forkIter);
119 if forkIter == 1
120 switch options.config.fork_join
121 case {'heidelberger-trivedi', 'ht'}
122 [nonfjmodel, fjclassmap, fjforkmap, fj_auxiliary_delays] = ModelAdapter.ht(self.model);
123 line_debug(options, 'Fork-join method: heidelberger-trivedi');
124 case {'mmt', 'default', 'fjt'}
125 % Reuse the MMT transformation across outer
126 % iterations when nothing structural has changed
127 % since it was built (SolverLN re-solves each layer
128 % once per fixed-point iteration, changing only the
129 % rates). The transformation is a function of the
130 % fork topology, so structVersion is the precise
131 % condition; rebuilding costs a full model.copy()
132 % every time. The cached model still holds the
133 % previous iteration's rates and sync delays, so it
134 % must be re-fed from the base model first.
135 cacheUsable = ~isempty(self.mmtCache) && ...
136 self.mmtCache.structVersion == self.model.structVersion && ...
137 ModelAdapter.refreshServicesFromBase(self.mmtCache.nonfjmodel, self.mmtCache.prov);
138 if cacheUsable
139 nonfjmodel = self.mmtCache.nonfjmodel;
140 fjclassmap = self.mmtCache.fjclassmap;
141 fjforkmap = self.mmtCache.fjforkmap;
142 fanout = self.mmtCache.fanout;
143 outer_forks = self.mmtCache.outer_forks;
144 parent_forks = self.mmtCache.parent_forks;
145 nonfjmodel.refreshRates();
146 line_debug(options, 'Fork-join method: mmt (cached transformation)');
147 else
148 [nonfjmodel, fjclassmap, fjforkmap, fanout, prov] = ModelAdapter.mmt(self.model, forkLambda);
149 line_debug(options, 'Fork-join method: mmt');
150 [outer_forks, parent_forks] = ModelAdapter.sortForks(sn, fjforkmap, fjclassmap, nonfjmodel);
151 self.mmtCache = struct('nonfjmodel', nonfjmodel, 'prov', prov, ...
152 'fjclassmap', fjclassmap, 'fjforkmap', fjforkmap, 'fanout', fanout, ...
153 'outer_forks', outer_forks, 'parent_forks', parent_forks, ...
154 'structVersion', self.model.structVersion);
155 end
156 end
157 elseif ~strcmp(options.config.fork_join, 'heidelberger-trivedi') & ~strcmp(options.config.fork_join, 'ht')
158 %line_printf('Fork-join iteration %d\n',forkIter);
159 nonfjSource = nonfjmodel.getSource;
160 for r=1:length(fjclassmap) % r is the auxiliary class
161 s = fjclassmap(r);
162 if s>0
163 if fanout(r)>0
164 if ~nonfjSource.arrivalProcess{r}.isDisabled
165 nonfjSource.arrivalProcess{r}.setRate((fanout(r)-1)*forkLambda(r));
166 end
167 end
168 end
169 end
170 nonfjmodel.refreshRates();
171 end
172 sn = nonfjmodel.getStruct(false); % this ensures that we solve nonfjmodel instead of the original model
173 line_debug(options, 'Fork-join iter %d: rebuilt nonfjmodel struct (nstations=%d, nclasses=%d)', forkIter, sn.nstations, sn.nclasses);
174 % Mixed absolute/relative convergence test on the MMT iterate.
175 % A purely relative test, max|1 - QN_1./QN| < CoarseTol, cannot
176 % certify convergence to zero. When the auxiliary class
177 % throughput at the join is ~0, forkLambda decays geometrically
178 % under the mean() damping above, so QN halves on every
179 % iteration and the relative change stays pinned at 100% down to
180 % denormals. The loop then ran to options.iter_max and returned a
181 % non-converged fixed point. An entry below the absolute floor is
182 % numerically zero and has converged.
183 qn_converged = abs(QN_1 - QN) <= GlobalConstants.Zero + GlobalConstants.CoarseTol*abs(QN);
184 if isequal(size(QN_1), size(QN)) && all(qn_converged(:)) && (forkIter > 2)
185 line_debug(options, 'Fork-join iter %d: converged (mixed abs/rel test)', forkIter);
186 forkLoop = false;
187 else
188 if self.model.hasOpenClasses
189 sourceIndex = self.model.getSource.index;
190 UNnosource = UN; UNnosource(sourceIndex,:) = 0;
191 if any(find(sum(UNnosource(:,isinf(sn.njobs(1:size(QN,2)))),2)>0.99 * sn.nservers))
192 line_warning(mfilename,'The model may be unstable: the utilization of station %i for open classes exceeds 99 percent.\n',maxpos(sum(UNnosource,2)));
193 end
194 end
195 QN_1 = QN;
196 end
197 else
198 forkLoop = false;
199 end
200 line_debug(options, 'Product-form check: hasProductForm=%d (exact method requested)', self.model.hasProductFormSolution);
201 if strcmp(options.method,'exact') && ~self.model.hasProductFormSolution
202 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');
203 end
204 if strcmp(options.method,'mva') && ~self.model.hasProductFormSolution
205 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.');
206 end
207
208 method = options.method;
209
210 % Check for size-based policies (SRPT, PSJF, FB, LRPT, SETF) in multiclass open systems
211 queueIdx = find(sn.nodetype == NodeType.Queue);
212 isSizeBasedPolicy = false;
213 if ~isempty(queueIdx)
214 schedType = sn.sched(sn.nodeToStation(queueIdx(1)));
215 isSizeBasedPolicy = ismember(schedType, [SchedStrategy.SRPT, SchedStrategy.PSJF, SchedStrategy.FB, SchedStrategy.LRPT, SchedStrategy.SETF]);
216 end
217
218 % Check for order-independent (OI) queues. An OI station is a PAS/OI
219 % queue with an all-zero swap graph and a service-rate function;
220 % detect it the same way the NC-oi path does (nc_is_oi_model),
221 % not via a nodeparam flag (which is never populated).
222 noi_idx = [];
223 if ~any(isinf(sn.njobs))
224 for ist = 1:sn.nstations
225 if sn.sched(ist) ~= SchedStrategy.PAS && sn.sched(ist) ~= SchedStrategy.OI
226 continue;
227 end
228 ind = sn.stationToNode(ist);
229 if ind < 1 || ind > numel(sn.nodeparam) || ~isstruct(sn.nodeparam{ind}) ...
230 || ~isfield(sn.nodeparam{ind}, 'swapGraph') ...
231 || ~isfield(sn.nodeparam{ind}, 'svcRateFun') || isempty(sn.nodeparam{ind}.svcRateFun)
232 continue;
233 end
234 sg = sn.nodeparam{ind}.swapGraph;
235 if isempty(sg) || any(sg(:) ~= 0)
236 continue;
237 end
238 noi_idx = ist;
239 break;
240 end
241 end
242
243 ci_cache = find(sn.nodetype == NodeType.Cache, 1);
244 hasRetrieval = ~isempty(ci_cache) && isfield(sn.nodeparam{ci_cache}, 'retrievalSystemCapacity') ...
245 && sn.nodeparam{ci_cache}.retrievalSystemCapacity > 0;
246 hasOIStation = any(sn.sched == SchedStrategy.OI | sn.sched == SchedStrategy.PAS);
247 if ~isempty(noi_idx) && nc_is_oi_model(sn) && any(strcmpi(options.method,{'default','exact'}))
248 % Order-independent queueing network
249 line_debug(options, 'MVA: order-independent closed network, routing to solver_mva_oi_analyzer');
250 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mva_oi_analyzer(sn, options);
251 elseif hasOIStation
252 % An OI/PAS station carries a rank-rate function mu(n) of the whole
253 % per-class occupancy. The AMVA iteration only ever sees sn.rates
254 % (the single-job rates), so it cannot represent such a station and
255 % would silently return a zero queue-length there. MVA therefore
256 % supports OI/PAS stations only via the exact path above.
257 line_error(mfilename, sprintf(['SolverMVA supports order-independent (OI) and pass-and-swap (PAS) stations only\n' ...
258 'through its exact order-independent analyzer, which requires method ''default'' or ''exact''\n' ...
259 '(got ''%s''), an empty/zero swap graph at every OI/PAS station, a closed model, and every\n' ...
260 'other station to be product-form (INF, PS, LCFS-PR, SIRO, or class-independent-rate FCFS).\n' ...
261 'Use SolverCTMC or SolverLDES for this model.'], options.method));
262 elseif hasRetrieval % delayed-hit (retrieval-system) cache
263 if any(sn.nodetype == NodeType.Source)
264 line_debug(options, 'Open delayed-hit retrieval cache, routing to mva_retrieval_analyzer');
265 [QN,UN,RN,TN,CN,XN,lG,hitprob,missprob,delayedprob,hitproblist,itemprob,latency,runtime,actualmethod] = solver_mva_retrieval_analyzer(sn, options);
266 else
267 line_debug(options, 'Closed integrated delayed-hit retrieval cache, routing to mva_cacheqn_retrieval_analyzer');
268 [QN,UN,RN,TN,CN,XN,lG,hitprob,missprob,delayedprob,hitproblist,itemprob,latency,runtime,actualmethod] = solver_mva_cacheqn_retrieval_analyzer(sn, options);
269 end
270 lastiter = NaN;
271 for ind = 1:sn.nnodes
272 if sn.nodetype(ind) == NodeType.Cache
273 self.model.nodes{ind}.setResultHitProb(hitprob);
274 self.model.nodes{ind}.setResultMissProb(missprob);
275 self.model.nodes{ind}.setResultDelayedHitProb(delayedprob);
276 self.model.nodes{ind}.setResultHitProbList(hitproblist);
277 self.model.nodes{ind}.setResultItemProb(itemprob);
278 self.model.nodes{ind}.setResultResidT(latency);
279 end
280 end
281 self.model.refreshStruct(true);
282 elseif sn.nclosedjobs == 0 && length(sn.nodetype)==3 && all(sort(sn.nodetype)' == sort([NodeType.Source,NodeType.Queue,NodeType.Sink])) && isSizeBasedPolicy
283 % Multiclass open system with size-based scheduling (SRPT, PSJF, FB, LRPT, SETF)
284 line_debug(options, 'Size-based scheduling detected (%s), routing to qsys_sizebased_analyzer', char(schedType));
285 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mva_qsys_sizebased_analyzer(sn, options, schedType);
286 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
287 line_debug(options, 'Single-class open queueing system (Source-Queue-Sink), routing to qsys_analyzer');
288 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mva_qsys_analyzer(sn, options);
289 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
290 line_debug(options, 'Multiclass open polling system, routing to polling_analyzer');
291 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mva_polling_analyzer(sn, options);
292 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.HOL && sn.nservers(sn.nodeToStation(sn.nodetype==NodeType.Queue)) == 1 && all(abs(sn.scv(sn.nodeToStation(sn.nodetype==NodeType.Source),:) - 1) < 1e-6 | ~isfinite(sn.scv(sn.nodeToStation(sn.nodetype==NodeType.Source),:)))
293 % Multiclass open HOL (non-preemptive priority) M/G/1: exact Cobham
294 % formula; the AMVA path applies the preemptive shadow-server formula.
295 line_debug(options, 'Multiclass open HOL priority queue, routing to qsys_prio_analyzer');
296 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mva_qsys_prio_analyzer(sn, options);
297 elseif sn.nclasses>1 && sn.nclasses<=3 && 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.DPS && sn.nservers(sn.nodeToStation(sn.nodetype==NodeType.Queue)) == 1 && all(abs(sn.scv(sn.nodeToStation(sn.nodetype==NodeType.Source),:) - 1) < 1e-6 | ~isfinite(sn.scv(sn.nodeToStation(sn.nodetype==NodeType.Source),:))) && all(abs(sn.scv(sn.nodeToStation(sn.nodetype==NodeType.Queue),:) - 1) < 1e-6 | ~isfinite(sn.scv(sn.nodeToStation(sn.nodetype==NodeType.Queue),:)))
298 % Single open M/M/1-DPS queue: numerically exact DPS via the
299 % truncated multiclass CTMC (qsys_mm1_dps). The AMVA-DPS cross-term
300 % correction violates the equal-rate conservation law.
301 line_debug(options, 'Multiclass open M/M/1-DPS queue, routing to exact qsys_mm1_dps');
302 T0dps = tic;
303 src_dps = sn.nodeToStation(sn.nodetype==NodeType.Source);
304 q_dps = sn.nodeToStation(sn.nodetype==NodeType.Queue);
305 lam_dps = sn.rates(src_dps,:);
306 mu_dps = sn.rates(q_dps,:);
307 w_dps = sn.schedparam(q_dps,:); w_dps(~(w_dps>0)) = 1;
308 Tdps = qsys_mm1_dps(lam_dps, mu_dps, w_dps);
309 QN = zeros(sn.nstations, sn.nclasses); UN = QN; RN = QN; TN = QN; CN = QN;
310 XN = zeros(1, sn.nclasses);
311 for rdps = 1:sn.nclasses
312 RN(q_dps,rdps) = Tdps(rdps); CN(q_dps,rdps) = Tdps(rdps);
313 XN(rdps) = lam_dps(rdps);
314 UN(q_dps,rdps) = lam_dps(rdps)/mu_dps(rdps);
315 TN(q_dps,rdps) = lam_dps(rdps); TN(src_dps,rdps) = lam_dps(rdps);
316 QN(q_dps,rdps) = lam_dps(rdps)*Tdps(rdps);
317 end
318 lG = 0; lastiter = 0; runtime = toc(T0dps);
319 actualmethod = 'mm1.dps';
320 elseif sn.nclosedjobs == 0 && length(sn.nodetype)==3 && all(sort(sn.nodetype)' == sort([NodeType.Source,NodeType.Cache,NodeType.Sink])) % is a non-rentrant cache
321 line_debug(options, 'Non-reentrant cache (Source-Cache-Sink), routing to cache_analyzer');
322 % random initialization
323 for ind = 1:sn.nnodes
324 if sn.nodetype(ind) == NodeType.Cache
325 prob = self.model.nodes{ind}.server.hitClass;
326 prob(prob>0) = 0.5;
327 self.model.nodes{ind}.setResultHitProb(prob);
328 self.model.nodes{ind}.setResultMissProb(1-prob);
329 end
330 end
331 self.model.refreshChains();
332 % start iteration
333 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod,hitproblist,itemprob] = solver_mva_cache_analyzer(sn, options);
334
335 for ind = 1:sn.nnodes
336 if sn.nodetype(ind) == NodeType.Cache
337 self.model.nodes{ind}.setResultHitProbList(hitproblist);
338 self.model.nodes{ind}.setResultItemProb(itemprob);
339 hitClass = self.model.nodes{ind}.getHitClass;
340 missClass = self.model.nodes{ind}.getMissClass;
341 hitprob = zeros(1,length(hitClass));
342 for k=1:length(self.model.nodes{ind}.getHitClass)
343 chain_k = sn.chains(:,k)>0;
344 inchain = sn.chains(chain_k,:)>0;
345 h = hitClass(k);
346 m = missClass(k);
347 if h>0 && m>0
348 hitprob(k) = XN(h) / sum(XN(inchain),"omitnan"); %#ok<NANSUM>
349 end
350 end
351 self.model.nodes{ind}.setResultHitProb(hitprob);
352 self.model.nodes{ind}.setResultMissProb(1-hitprob);
353 end
354 end
355 %self.model.refreshChains();
356 self.model.refreshStruct(true);
357 else % queueing network
358 if any(sn.nodetype == NodeType.Cache) % if integrated caching-queueing
359 line_debug(options, 'Integrated caching-queueing network, routing to cacheqn_analyzer');
360 [QN,UN,RN,TN,CN,XN,lG,hitprob,missprob,runtime,lastiter] = solver_mva_cacheqn_analyzer(self, options);
361 for ind = 1:sn.nnodes
362 if sn.nodetype(ind) == NodeType.Cache
363 self.model.nodes{ind}.setResultHitProb(hitprob(ind,:));
364 self.model.nodes{ind}.setResultMissProb(missprob(ind,:));
365 end
366 end
367 %self.model.refreshChains();
368 self.model.refreshStruct(true);
369 else % ordinary queueing network
370 switch method
371 case {'aba.upper', 'aba.lower', 'bjb.upper', 'bjb.lower', 'pb.upper', 'pb.lower', 'gb.upper', 'gb.lower', 'sb.upper', 'sb.lower', 'mwba.upper', 'mwba.lower'}
372 line_error(mfilename, ['Bound methods have moved to SolverBA. ' ...
373 'Use SolverBA(model,''method'',''%s'') (or LINE with that method) instead of SolverMVA.'], method);
374 case {'marie', 'amva.marie'}
375 line_debug(options, 'Using Marie aggregation-decomposition method');
376 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mva_marie_analyzer(sn, options);
377 otherwise
378 if ~isempty(sn.lldscaling) || ~isempty(sn.cdscaling)
379 line_debug(options, 'Load-dependent scaling detected (lldscaling=%d, cdscaling=%d), routing to mvald_analyzer', ~isempty(sn.lldscaling), ~isempty(sn.cdscaling));
380 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mvald_analyzer(sn, options);
381 else
382 subopts = options;
383 if self.model.hasFork && strcmp(subopts.method,'default')
384 % The fork-join transform (ht/mmt) yields a mixed
385 % model with auxiliary near-zero-rate open classes.
386 % The default dispatch would route it to exact mixed
387 % MVA (pfqn_mvamx), which degenerates to zero here;
388 % the fork-join approximation requires the AMVA
389 % linearizer, matching the pre-mixed-exact behaviour.
390 subopts.method = 'amva';
391 end
392 line_debug(options, 'Standard queueing network, routing to mva_analyzer (method=%s)', subopts.method);
393 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mva_analyzer(sn, subopts);
394 end
395 end
396 end
397 end
398 if self.model.hasFork
399 line_debug(options, 'Fork-join post-processing: method=%s, computing sync delays', options.config.fork_join);
400 nonfjstruct = sn;
401 sn = self.getStruct;
402 % Pre-compute linked routing matrix once (loop-invariant)
403 switch options.config.fork_join
404 case {'mmt', 'default', 'fjt'}
405 Pcs = cell2mat(nonfjmodel.getLinkedRoutingMatrix);
406 end
407 for f=find(sn.nodetype == NodeType.Fork)'
408 switch options.config.fork_join
409 case {'mmt', 'default', 'fjt'}
410 TNfork = zeros(1,sn.nclasses);
411 for c=1:sn.nchains
412 inchain = find(sn.chains(c,:));
413 for r=inchain(:)'
414 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));
415 end
416 end
417 % find join associated to fork f
418 joinIdx = find(sn.fj(f,:));
419 forkauxclasses = find(fjforkmap==f);
420 for s=forkauxclasses(:)'
421 r = fjclassmap(s); % original class associated to auxiliary class s
422 if isempty(joinIdx)
423 forkLambda(s) = mean([forkLambda(s); TNfork(r)],1);
424 else
425 joinStat = sn.nodeToStation(joinIdx);
426 TN(sn.nodeToStation(joinIdx),r) = TN(sn.nodeToStation(joinIdx),r) + sum(TN(sn.nodeToStation(joinIdx), find(fjclassmap == r))) - TN(sn.nodeToStation(joinIdx), s);
427 forkLambda(s) = mean([forkLambda(s); TN(sn.nodeToStation(joinIdx),r)],1);
428 end
429 if isempty(joinIdx) || ~outer_forks(f, r)
430 % No join nodes for this fork, no synchronisation delay
431 continue;
432 end
433 % Find the parallel paths coming out of the fork
434 ri = ModelAdapter.findPathsCS(sn, Pcs, f, joinIdx, r, [r,s], QN, TN, 0, fjclassmap, fjforkmap, nonfjmodel);
435 if isempty(ri)
436 % No routing from fork for this class - set sync delay to 0 (Immediate)
437 % Matches JAR behavior where empty Matrix gives syncDelay = 0
438 syncDelay = 0;
439 else
440 lambdai = 1./ri;
441 d0 = 0;
442 parallel_branches = length(ri);
443 for pow=0:(parallel_branches - 1)
444 current_sum = sum(1./sum(nchoosek(lambdai, pow + 1),2));
445 d0 = d0 + (-1)^pow * current_sum;
446 end
447 syncDelay = d0*sn.nodeparam{f}.fanOut - mean(ri);
448 end
449 % Set the synchronisation delays
450 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{s}, Exp.fitMean(syncDelay));
451 if outer_forks(f, r)
452 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{r}, Exp.fitMean(syncDelay));
453 end
454 end
455 case {'heidelberger-trivedi', 'ht'}
456 joinIdx = find(sn.fj(f,:));
457 for c=1:sn.nchains
458 inchain = find(sn.chains(c,:));
459 for r=inchain(:)'
460 if sn.nodevisits{c}(f,r) == 0
461 continue;
462 end
463 % Obtain the response times on the parallel branches
464 ri = RN(:, find(fjclassmap == r));
465 ri(isnan(ri) | isinf(ri)) = 0;
466 ri = sum(ri, 1, "omitnan") - RN(nonfjstruct.nodeToStation(fj_auxiliary_delays{joinIdx}), find(fjclassmap == r)) - RN(nonfjstruct.nodeToStation(joinIdx), find(fjclassmap == r));
467 lambdai = 1./ri;
468 d0 = 0;
469 parallel_branches = length(self.model.nodes{f}.output.outputStrategy{r}{3});
470 for pow=0:(parallel_branches - 1)
471 current_sum = sum(1./sum(nchoosek(lambdai, pow + 1),2));
472 d0 = d0 + (-1)^pow * current_sum;
473 end
474 di = d0*sn.nodeparam{f}.fanOut - ri;
475 r0 = sum(RN(:, inchain), 2);
476 r0(isnan(r0) | isinf(r0)) = 0;
477 r0 = sum(r0, 1, "omitnan") - RN(nonfjstruct.nodeToStation(joinIdx), r);
478 % Update the delays at the join node and at the auxiliary delay
479 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{r}, Exp.fitMean(d0*sn.nodeparam{f}.fanOut));
480 idx = 1;
481 for s=find(fjclassmap == r)
482 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{s}, Exp.fitMean(di(idx)));
483 idx = idx + 1;
484 nonfjmodel.nodes{fj_auxiliary_delays{joinIdx}}.setService(nonfjmodel.classes{s}, Exp.fitMean(r0));
485 end
486
487 end
488 end
489 end
490 end
491 % Batch refreshRates after all sync delay updates (moved out of inner loops for performance)
492 switch options.config.fork_join
493 case {'mmt', 'default', 'fjt'}
494 nonfjmodel.refreshRates();
495 end
496 switch options.config.fork_join
497 case {'heidelberger-trivedi', 'ht'}
498 nonfjmodel.refreshStruct();
499 % Delete the queue lengths, response times, throughputs and utilizations of the original classes at the join nodes
500 QN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = 0;
501 RN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = 0;
502 % Save the throughputs of the original classes at the join node
503 TN_orig = TN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap));
504 TN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = 0;
505 UN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = 0;
506
507 % Remove the times at the auxiliary delay
508 QN(nonfjstruct.nodeToStation(cell2mat(fj_auxiliary_delays)),:) = [];
509 UN(nonfjstruct.nodeToStation(cell2mat(fj_auxiliary_delays)),:) = [];
510 RN(nonfjstruct.nodeToStation(cell2mat(fj_auxiliary_delays)),:) = [];
511 TN(nonfjstruct.nodeToStation(cell2mat(fj_auxiliary_delays)),:) = [];
512 % merge back artificial classes into their original classes
513 for r=1:length(fjclassmap)
514 s = fjclassmap(r);
515 if s>0
516 QN(:,s) = QN(:,s) + QN(:,r);
517 UN(:,s) = UN(:,s) + UN(:,r);
518 % Add all throughputs of the auxiliary classes to facilitate the computation of the response times
519 TN(:,s) = TN(:,s) + TN(:,r);
520 RN(:,s) = QN(:,s) ./ TN(:,s);
521 end
522 end
523 % Re-set the throughputs for the original classes
524 TN(nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonzeros(fjclassmap)) = TN_orig;
525 case {'mmt', 'default', 'fjt'}
526 TN_orig = TN([nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Source))], nonzeros(fjclassmap));
527 % merge back artificial classes into their original classes
528 for r=1:length(fjclassmap)
529 s = fjclassmap(r);
530 if s>0
531 QN(:,s) = QN(:,s) + QN(:,r);
532 UN(:,s) = UN(:,s) + UN(:,r);
533 TN(:,s) = TN(:,s) + TN(:,r);
534 %RN(:,s) = RN(:,s) + RN(:,r);
535 % for i=find(snorig.nodetype == NodeType.Delay | snorig.nodetype == NodeType.Queue)'
536 % TN(snorig.nodeToStation(i),s) = TN(snorig.nodeToStation(i),s) + TN(snorig.nodeToStation(i),r);
537 % end
538 RN(:,s) = QN(:,s) ./ TN(:,s);
539 %CN(:,s) = CN(:,s) + CN(:,r);
540 %XN(:,s) = XN(:,s) + XN(:,r);
541 end
542 end
543 TN([nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Source))], nonzeros(fjclassmap)) = TN_orig;
544 end
545 QN(:,fjclassmap>0) = [];
546 UN(:,fjclassmap>0) = [];
547 RN(:,fjclassmap>0) = [];
548 TN(:,fjclassmap>0) = [];
549 CN(:,fjclassmap>0) = [];
550 XN(:,fjclassmap>0) = [];
551 end
552 iter = iter + lastiter;
553 % Cap accumulated iterations for stability (Python parity)
554 if iter > 10000
555 iter = 10000;
556 break; % Exit forkLoop early
557 end
558 end
559
560 % The fork-join loop previously exhausted options.iter_max silently:
561 % convergence was only ever reported through line_debug, so a
562 % non-converged MMT fixed point was returned as a normal result.
563 if self.model.hasFork
564 if forkLoop && forkIter >= options.iter_max
565 line_warning(mfilename,'The fork-join (%s) fixed point did not converge in options.iter_max=%d iterations; returning the interim solution.\n', options.config.fork_join, options.iter_max);
566 end
567 % Retain the MMT iterate so that a subsequent runAnalyzer call on
568 % this solver (an outer LN iteration) resumes the fixed point here.
569 self.fjForkLambda = forkLambda;
570 end
571
572 sn = self.model.getStruct();
573
574 % Compute average residence time at steady-state
575 AN = sn_get_arvr_from_tput(sn, TN, self.getAvgTputHandles());
576 WN = sn_get_residt_from_respt(sn, RN, self.getAvgResidTHandles());
577 if strcmp(method,'default') && exist('actualmethod','var')
578 self.setAvgResults(QN,UN,RN,TN,AN,WN,CN,XN,runtime,['default/' actualmethod],iter);
579 else
580 self.setAvgResults(QN,UN,RN,TN,AN,WN,CN,XN,runtime,method,iter);
581 end
582 self.result.Prob.logNormConstAggr = lG;
583 if lineTimeoutExceeded(options)
584 self.result.Avg.timedOut = true;
585 line_warning(mfilename,'Solver stopped after the wall-clock time budget (options.timeout=%gs) was exceeded; returning the interim solution.\n', options.timeout);
586 end
587end
588end
589
590
Definition fjtag.m:157
Definition Station.m:245