1function runtime = runAnalyzer(self, options)
2% RUNTIME = RUNANALYZER(OPTIONS)
6 options = self.getOptions;
9% Wall-clock time-budget launch marker (see options.timeout / lineTimeoutExceeded)
10options.timeout_tic = tic;
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);
19self.runAnalyzerChecks(options);
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.');
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');
31Solver.resetRandomGeneratorSeed(options.seed);
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);
38 line_printf('The solver will leverage %s.\n', strjoin(libs, ', '));
39 GlobalConstants.setLibraryAttributionShown(true);
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
60 if options.config.fj_warmstart && ~isempty(self.fjForkLambda)
61 jsolver.setForkWarmStart(JLINE.from_line_matrix(self.fjForkLambda));
63 [QN,UN,RN,WN,AN,TN] = JLINE.arrayListToResults(jsolver.getAvgTable);
65 self.fjForkLambda = JLINE.from_jline_matrix(jsolver.getForkWarmStart());
67 runtime = jsolver.result.runtime;
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)';
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()));
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;
97 line_debug(options,
'MVA: using lang=matlab');
98 sn = getStruct(self); % doesn
't need initial state
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;
112 QN = GlobalConstants.Immediate * ones(1, sn.nclasses);
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);
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);
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)');
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);
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
164 if ~nonfjSource.arrivalProcess{r}.isDisabled
165 nonfjSource.arrivalProcess{r}.setRate((fanout(r)-1)*forkLambda(r));
170 nonfjmodel.refreshRates();
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);
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)));
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');
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.');
208 method = options.method;
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]);
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).
223 if ~any(isinf(sn.njobs))
224 for ist = 1:sn.nstations
225 if sn.sched(ist) ~= SchedStrategy.PAS && sn.sched(ist) ~= SchedStrategy.OI
228 ind = sn.stationToNode(ist);
230 || ~isfield(sn.nodeparam{ind},
'swapGraph') ...
231 || ~isfield(sn.nodeparam{ind},
'svcRateFun') || isempty(sn.nodeparam{ind}.svcRateFun)
234 sg = sn.nodeparam{ind}.swapGraph;
235 if isempty(sg) || any(sg(:) ~= 0)
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);
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);
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);
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);
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
');
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);
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;
327 self.model.nodes{ind}.setResultHitProb(prob);
328 self.model.nodes{ind}.setResultMissProb(1-prob);
331 self.model.refreshChains();
333 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod,hitproblist,itemprob] = solver_mva_cache_analyzer(sn, options);
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;
348 hitprob(k) = XN(h) / sum(XN(inchain),"omitnan"); %
#ok<NANSUM>
351 self.model.nodes{ind}.setResultHitProb(hitprob);
352 self.model.nodes{ind}.setResultMissProb(1-hitprob);
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,:));
367 %self.model.refreshChains();
368 self.model.refreshStruct(
true);
369 else % ordinary queueing network
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);
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);
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';
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);
398 if self.model.hasFork
399 line_debug(options, 'Fork-join post-processing: method=%s, computing sync delays', options.config.fork_join);
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);
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);
412 inchain = find(sn.chains(c,:));
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));
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
423 forkLambda(s) = mean([forkLambda(s); TNfork(r)],1);
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);
429 if isempty(joinIdx) || ~outer_forks(f, r)
430 % No join nodes for this fork, no synchronisation delay
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);
436 % No routing from fork for this class - set sync delay to 0 (Immediate)
437 % Matches JAR behavior where empty Matrix gives syncDelay = 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;
447 syncDelay = d0*sn.nodeparam{f}.fanOut - mean(ri);
449 % Set the synchronisation delays
450 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{s}, Exp.fitMean(syncDelay));
452 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{r}, Exp.fitMean(syncDelay));
455 case {'heidelberger-trivedi
', 'ht
'}
456 joinIdx = find(sn.fj(f,:));
458 inchain = find(sn.chains(c,:));
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));
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;
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));
481 for s=find(fjclassmap == r)
482 nonfjmodel.nodes{joinIdx}.setService(nonfjmodel.classes{s}, Exp.fitMean(di(idx)));
484 nonfjmodel.nodes{fj_auxiliary_delays{joinIdx}}.setService(nonfjmodel.classes{s}, Exp.fitMean(r0));
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();
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;
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)
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);
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)
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);
538 RN(:,s) = QN(:,s) ./ TN(:,s);
539 %CN(:,s) = CN(:,s) + CN(:,r);
540 %XN(:,s) = XN(:,s) + XN(:,r);
543 TN([nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Join)), nonfjstruct.nodeToStation(find(sn.nodetype == NodeType.Source))], nonzeros(fjclassmap)) = TN_orig;
545 QN(:,fjclassmap>0) = [];
546 UN(:,fjclassmap>0) = [];
547 RN(:,fjclassmap>0) = [];
548 TN(:,fjclassmap>0) = [];
549 CN(:,fjclassmap>0) = [];
550 XN(:,fjclassmap>0) = [];
552 iter = iter + lastiter;
553 % Cap accumulated iterations for stability (Python parity)
556 break; % Exit forkLoop early
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);
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;
572 sn = self.model.getStruct();
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);
580 self.setAvgResults(QN,UN,RN,TN,AN,WN,CN,XN,runtime,method,iter);
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);