1classdef SolverLN < EnsembleSolver
2 % SolverLN Layered network solver
for hierarchical performance models
4 % SolverLN implements analysis of layered queueing networks (LQNs) which model
5 % hierarchical software systems with clients, application servers, and resource
6 % layers. It uses iterative decomposition to solve multi-layer models by
7 % analyzing each layer separately and propagating service demands between layers.
9 % @brief Layered network solver
for hierarchical software performance models
13 % solver = SolverLN(layered_model,
'maxIter', 100);
14 % solver.runAnalyzer(); % Iterative layer analysis
15 % metrics = solver.getEnsembleAvg(); % Layer performance metrics
18 % Copyright (c) 2012-2026, Imperial College London
19 % All rights reserved.
21 properties %(Hidden) % registries of quantities to update at every iteration
22 nlayers; % number of model layers
23 lqn; % lqn data structure
24 hasconverged; %
true if last iteration converged,
false otherwise
25 averagingstart; % iteration at which result averaging started
26 idxhash; % ensemble model associated to host or task
27 servtmatrix; % auxiliary matrix to determine entry servt
28 ilscaling; % interlock scalings
29 % LQNS V5-style interlock data structures (built once at init)
30 il_table_all; % (nentries x nentries) reachability probability, all phases
31 il_table_ph1; % (nentries x nentries) reachability probability, phase-1 only
32 il_common_entries; % cell(nhosts+ntasks,1) common parent entry indices per server
33 il_source_tasks_all; % cell(nhosts+ntasks,1) all-phase source tasks per server
34 il_source_tasks_ph2; % cell(nhosts+ntasks,1) phase-2 source tasks per server
35 il_num_sources; % (nhosts+ntasks,1) total source multiplicity per server
36 njobs; % number of jobs for each caller in a given submodel
37 njobsorig; % number of jobs for each caller at layer build time
38 routereset; % models that require hard reset of service chains
39 svcreset; % models that require hard reset of service process
40 maxitererr; % maximum error at current iteration over all layers
41 % Under-relaxation state for convergence improvement
42 relax_omega; % Current relaxation
factor
43 relax_err_history; % Error history for adaptive mode
44 % Stochastic iteration (Robbins-Monro / Polyak-Ruppert) state,
45 % used when one or more layer
solvers return noisy estimates
46 stochiter_mode; % resolved mode: 'rm' | 'crn' | 'off'
47 stochiter_auto; % true if mode was resolved from 'auto'
48 stochiter_start; % iteration at which RM averaging started
49 stochlayers; % logical(1,nlayers): layer solver
is stochastic
50 stoch_avg; % cell(1,nlayers) Polyak-Ruppert averages of layer results
51 stoch_avg_count; % iterations accumulated into stoch_avg
52 stoch_servt_avg; % Polyak-Ruppert average of
the servt iterate
53 stoch_residt_avg; % Polyak-Ruppert average of
the residt iterate
54 servt_prev; % Previous service times for relaxation
55 residt_prev; % Previous residence times for relaxation
56 tput_prev; % Previous throughputs for relaxation
57 thinkt_prev; % Previous think times for relaxation
58 callservt_prev; % Previous call service times for relaxation
59 callresidt_prev; % Previous call residence times for growth rate capping
60 singleReplicaTasks; % Task indices modeled as single representative replica (fan-out)
61 % MOL (Method of Layers) properties for hierarchical iteration
62 hostLayerIndices; % Indices of host (processor) layers in ensemble
63 taskLayerIndices; % Indices of task layers in ensemble
64 util_prev_host; % Previous processor utilizations (for delta computation)
65 util_prev_task; % Previous task utilizations (for delta computation)
66 pyMode; % Flag: delegate to native python SolverLN (lang='python')
67 % Phase-2 support properties
68 hasPhase2; % Flag: model has phase-2 activities
69 servt_ph1; % Phase-1 service time per activity (nidx x 1)
70 servt_ph2; % Phase-2 service time per activity (nidx x 1)
71 util_ph1; % Phase-1 utilization per entry
72 util_ph2; % Phase-2 utilization per entry
73 prOvertake; % Overtaking probability per entry (nentries x 1)
76 properties %(Hidden) % performance metrics and related processes
78 util_ilock; % interlock matrix (ntask x ntask), element (i,j) says how much
the utilization of task i
is imputed to task j
81 servt; % this
is the mean service time of an activity, which
is the response time at
the lower layer (if applicable)
82 residt; % this
is the residence time at
the lower layer (if applicable)
83 servtproc; % this
is the service time process with mean fitted to
the servt value
84 servtcdf; % this
is the cdf of
the service time process
94 joint; % join times at AND-Join activities (synchronization delay)
95 ignore; % elements to be ignored (e.g., components disconnected from a REF node)
98 properties %(Access = protected, Hidden) % registries of quantities to update at every iteration
99 arvproc_classes_updmap; % [modelidx, actidx, node, class]
100 thinkt_classes_updmap; % [modelidx, actidx, node, class]
101 actthinkt_classes_updmap; % [modelidx, actidx, node, class] for activity think-times
102 servt_classes_updmap; % [modelidx, actidx, node, class]
103 call_classes_updmap; % [modelidx, callidx, node, class]
104 route_prob_updmap; % [modelidx, actidxfrom, actidxto, nodefrom, nodeto, classfrom, classto]
105 unique_route_prob_updmap; % auxiliary cache of unique route_prob_updmap rows
106 solverFactory; % function handle to create layer
solvers
110 function self = SolverLN(lqnmodel, solverFactory, varargin)
111 % SELF = SOLVERLN(MODEL,SOLVERFACTORY,VARARGIN)
112 self@EnsembleSolver(lqnmodel, mfilename);
114 % Collect all trailing args (solverFactory may itself be
the lang
115 %
string or an options struct) to detect lang='python'/'java'.
118 allArgs = [{solverFactory}, varargin];
120 wantsPython = any(cellfun(@(s) (ischar(s) && strcmpi(s,
'python')) || ...
121 (isstruct(s) && isfield(s,
'lang') && strcmpi(s.lang,
'python')), allArgs));
123 if any(cellfun(@(s) ischar(s) && strcmpi(s,
'java'), allArgs))
124 self.obj = JLINE.SolverLN(JLINE.from_line_layered_network(lqnmodel));
125 self.obj.options.verbose = jline.VerboseLevel.SILENT;
126 % The analysis runs in
the JAR, but getAvgTable still formats
the
127 % rows on
the MATLAB side from
the element names and types, so
128 %
the lqn
struct is needed here exactly as in
the python branch
129 % below. Without it getAvgTable failed on self.lqn.names.
130 self.lqn = lqnmodel.getStruct();
132 % Delegate to
the native python SolverLN via PYLINE. Only
the
133 % lqn
struct (element names/types)
is needed on
the MATLAB side
134 % to format getAvgTable;
the native solver does
the analysis.
136 if nargin > 1 && isstruct(solverFactory)
137 self.setOptions(solverFactory);
139 self.setOptions(SolverLN.defaultOptions);
141 self.options.lang =
'python';
142 self.lqn = lqnmodel.getStruct();
144 % Default solver factory: Use JMT
for open networks, MVA
for closed networks
145 defaultSolverFactory = @(m) adaptiveSolverFactory(m, self.options);
147 if nargin == 1 %
case SolverLN(model)
148 solverFactory = defaultSolverFactory;
149 self.setOptions(SolverLN.defaultOptions);
150 elseif nargin>1 && isstruct(solverFactory)
151 options = solverFactory;
152 self.setOptions(options);
153 solverFactory = defaultSolverFactory;
154 elseif nargin>2 %
case SolverLN(model,
'opt1',...)
155 if ischar(solverFactory)
156 inputvar = {solverFactory,varargin{:}}; %#ok<CCAT>
157 solverFactory = defaultSolverFactory;
158 else %
case SolverLN(model, solverFactory,
'opt1',...)
161 self.setOptions(Solver.parseOptions(inputvar, SolverLN.defaultOptions));
162 else %case SolverLN(model,solverFactory)
163 self.setOptions(SolverLN.defaultOptions);
165 self.lqn = lqnmodel.getStruct();
166 % LQNS-parity forwarding treatment: rewrite forwarding chains
167 % as caller-side pseudo rendezvous calls (phase.cc
168 % addForwardingRendezvous port), see lqn_fwd_rendezvous
169 self.lqn = lqn_fwd_rendezvous(self.lqn);
171 % Detect and initialize phase-2 support
172 if isfield(self.lqn, 'actphase') && any(self.lqn.actphase > 1)
173 self.hasPhase2 = true;
174 self.servt_ph1 = zeros(self.lqn.nidx, 1);
175 self.servt_ph2 = zeros(self.lqn.nidx, 1);
176 self.util_ph1 = zeros(self.lqn.nidx, 1);
177 self.util_ph2 = zeros(self.lqn.nidx, 1);
178 self.prOvertake = zeros(self.lqn.nentries, 1);
180 self.hasPhase2 = false;
184 line_debug('LN: solver factory=%s, constructing layers', func2str(solverFactory));
185 for e=1:self.getNumberOfModels
186 % Only
the function-host layer (
the one carrying a setup
187 % time) needs
the MAM dec.poisson treatment. Every other
188 % layer keeps
the caller's factory: overwriting
the loop
189 % variable here would leak SolverMAM/SolverAuto into all
190 % subsequent layers and into self.solverFactory.
191 if numel(find(self.lqn.isfunction == 1)) && ~isempty(self.ensemble{e}.stations{2}.setupTime)
192 layerFactory = @(m) SolverMAM(m,
'verbose',
false,
'method',
'dec.poisson');
194 layerFactory = solverFactory;
196 layerSolver = layerFactory(self.ensemble{e});
197 self.assertLayerSolverSupportsModel(layerSolver, self.ensemble{e}, e);
198 self.setSolver(layerSolver,e);
200 self.solverFactory = solverFactory; % Store
for later use
204 function runtime = runAnalyzer(self, options) %#ok<INUSD> %
generic method to run
the solver
205 line_error(mfilename,
'Use getEnsembleAvg instead.');
208 function sn = getStruct(self)
211 % Get data structure summarizing
the model
212 sn = self.model.getStruct();
215 function construct(self)
216 % mark down to ignore unreachable disconnected components
217 self.ignore =
false(self.lqn.nidx,1);
218 [~,wccs] = weaklyconncomp(self.lqn.graph
'+self.lqn.graph);
219 uwccs = unique(wccs);
221 % the model has disconnected submodels
222 wccref = false(1,length(uwccs));
223 for t=1:self.lqn.ntasks
224 tidx = self.lqn.tshift+t;
225 if self.lqn.sched(tidx) == SchedStrategy.REF
226 wccref(wccs(tidx)) = true;
229 if any(wccref==false)
230 for dw=find(wccref==false) % disconnected component
231 self.ignore(find(wccs==dw)) = true;
236 % initialize internal data structures
237 self.entrycdfrespt = cell(length(self.lqn.nentries),1);
238 self.hasconverged = false;
240 % initialize svc and think times
241 self.servtproc = self.lqn.hostdem;
242 self.thinkproc = self.lqn.think;
243 self.callservtproc = cell(self.lqn.ncalls,1);
244 for cidx = 1:self.lqn.ncalls
245 self.callservtproc{cidx} = self.lqn.hostdem{self.lqn.callpair(cidx,2)};
249 self.njobs = zeros(self.lqn.tshift + self.lqn.ntasks, self.lqn.tshift + self.lqn.ntasks);
250 buildLayers(self); % build layers
251 line_debug('LN construct: built %d layers from LQN model (%d hosts, %d tasks, %d entries, %d activities)
', ...
252 length(self.ensemble), self.lqn.nhosts, self.lqn.ntasks, self.lqn.nentries, self.lqn.nacts);
253 self.njobsorig = self.njobs;
254 self.nlayers = length(self.ensemble);
256 % interlock data structures are built in init() via initInterlock()
258 % layering generates update maps that we use here to cache the elements that need reset
259 self.routereset = unique(self.idxhash(self.route_prob_updmap(:,1)))';
260 self.svcreset = unique(self.idxhash(self.thinkt_classes_updmap(:,1)))
';
261 self.svcreset = union(self.svcreset,unique(self.idxhash(self.call_classes_updmap(:,1)))');
264 function self = reset(self)
268 bool = converged(self, it); % convergence test at iteration it
269 bool = convergedStoch(self, it); % convergence test
for stochastic layer
solvers (Robbins-Monro mode)
271 function init(self) % operations before starting to iterate
272 % INIT() % OPERATIONS BEFORE STARTING TO ITERATE
273 self.unique_route_prob_updmap = unique(self.route_prob_updmap(:,1))
';
274 self.tput = zeros(self.lqn.nidx,1);
275 self.tputproc = cell(self.lqn.nidx,1);
276 self.util = zeros(self.lqn.nidx,1);
277 self.servt = zeros(self.lqn.nidx,1);
278 self.servtmatrix = getEntryServiceMatrix(self);
280 % Keep the feature-set gate armed on the layer solvers: a layer whose
281 % model a solver cannot represent must be rejected, not solved into
282 % silently wrong numbers (e.g. MVA has no notion of a Signal class and
283 % would return a product-form answer in which no job is ever removed).
284 for e= 1:self.nlayers
285 self.solvers{e}.enableChecks=true;
288 % Initialize under-relaxation state
289 relax_mode = self.options.config.relax;
292 self.relax_omega = 1.0; % Start without relaxation
293 case {'fixed
', 'adaptive
'}
294 self.relax_omega = self.options.config.relax_factor;
295 otherwise % 'none
' or unrecognized
296 self.relax_omega = 1.0; % No relaxation
298 self.relax_err_history = [];
299 line_debug('LN init: %d layers, relaxation=%s (omega=%.3f)
', ...
300 self.nlayers, self.options.config.relax, self.relax_omega);
301 self.servt_prev = NaN(self.lqn.nidx, 1);
302 self.residt_prev = NaN(self.lqn.nidx, 1);
303 self.tput_prev = NaN(self.lqn.nidx, 1);
304 self.thinkt_prev = NaN(self.lqn.nidx, 1);
305 self.thinkt = zeros(self.lqn.nidx, 1); % Initialize to zeros (Python parity)
306 self.callservt_prev = NaN(self.lqn.ncalls, 1);
307 self.callresidt_prev = NaN(self.lqn.ncalls, 1);
309 % Build interlock tables (LQNS V5 static analysis)
310 if self.options.config.interlocking
311 self.initInterlock();
314 % Initialize MOL-specific state
315 self.util_prev_host = zeros(self.lqn.nhosts, 1);
316 self.util_prev_task = zeros(self.lqn.ntasks, 1);
318 % Optional initialization of the layer throughput state from the
319 % Majumdar-Woodside robust box bounds (geometric-mean point estimate
320 % sqrt(Xlo*Xup)). Selected with options.config.layer_init = 'bound
'.
321 % Note: for closed-chain LQNs the outer iteration re-derives
322 % throughputs from the first solve, so this does not alter the
325 if isfield(self.options.config, 'layer_init
') && ~isempty(self.options.config.layer_init)
326 initMode = self.options.config.layer_init;
328 if any(strcmpi(initMode, {'bound
','boxbound
','mwba
'}))
330 bnd = lqn_boxbounds(self.lqn);
331 for idx = 1:self.lqn.nidx
332 u = bnd.TN_up(idx); l = bnd.TN_lo(idx);
333 if isfinite(u) && isfinite(l) && u > 0 && l > 0
344 self.tputproc{idx} = Exp.fitRate(x);
347 line_debug('LN init: throughputs initialized from robust box bounds
');
349 line_debug('LN box-bound initialization skipped: %s
', ME.message);
353 % Resolve the stochastic iteration mode. Simulation-based or
354 % Monte Carlo based layer solvers observe the layer map only up
355 % to noise, for which the deterministic Picard iteration and
356 % its successive-difference test are inadequate (see
357 % convergedStoch.m). The static classification below uses
358 % options.method; a layer running method 'default
' may still
359 % resolve to a stochastic method at runtime, so analyze()
360 % refreshes stochlayers after the first iteration and
361 % converged() upgrades an 'auto
' mode accordingly.
362 self.stochlayers = false(1, self.nlayers);
363 for e = 1:self.nlayers
364 self.stochlayers(e) = self.solvers{e}.isStochastic();
366 mode = self.options.config.stochiter;
367 self.stochiter_auto = strcmpi(mode, 'auto
');
368 if self.stochiter_auto
369 if any(self.stochlayers)
375 self.stochiter_mode = lower(mode);
376 self.stochiter_start = [];
377 self.stoch_avg = cell(1, self.nlayers);
378 self.stoch_avg_count = 0;
379 self.stoch_servt_avg = [];
380 self.stoch_residt_avg = [];
381 line_debug('LN init: stochastic iteration mode=%s (%d stochastic layers)
', ...
382 self.stochiter_mode, sum(self.stochlayers));
386 function pre(self, it) % operations before an iteration
387 % PRE(IT) % OPERATIONS BEFORE AN ITERATION
388 % Seed control for stochastic layer solvers
389 if isempty(self.stochiter_mode)
392 switch self.stochiter_mode
394 % Rotate seeds so successive iterations observe the
395 % layer map under independent noise, as required for
396 % Robbins-Monro averaging to reduce variance
397 for e = find(self.stochlayers)
398 self.solvers{e}.options.seed = self.options.seed + (it-1)*self.nlayers + e;
401 % Common random numbers: pin a constant per-layer seed
402 % so each layer map is deterministic given its seed
403 % (sample average approximation). The standard
404 % convergence test then applies to the sample-average
405 % fixed point, which carries an O(1/sqrt(samples))
406 % bias with respect to the true fixed point.
407 for e = find(self.stochlayers)
408 self.solvers{e}.options.seed = self.options.seed + e;
413 function [result, runtime] = analyze(self, it, e)
414 % [RESULT, RUNTIME] = ANALYZE(IT, E)
416 line_debug('LN analyze: iteration %d, layer %d (%s)
', it, e, class(self.solvers{e}));
419 if e==1 && self.solvers{e}.options.verbose
423 % Protection for unstable queues during LN iterations
424 % If a solver fails (e.g., due to queue instability with open arrivals),
425 % use results from previous iteration if available and continue
427 [result.QN, result.UN, result.RN, result.TN, result.AN, result.WN] = self.solvers{e}.getAvg();
429 if it > 1 && ~isempty(self.results) && size(self.results, 1) >= (it-1) && size(self.results, 2) >= e
430 if self.solvers{e}.options.verbose
431 warning('LINE:SolverLN:Instability
', ...
432 'Layer %d at iteration %d encountered instability (possibly due to high service demand with open arrivals). Using previous iteration values and continuing.
', ...
435 % Use results from previous iteration
436 prevResult = self.results{it-1, e};
437 result.QN = prevResult.QN;
438 result.UN = prevResult.UN;
439 result.RN = prevResult.RN;
440 result.TN = prevResult.TN;
441 result.AN = prevResult.AN;
442 result.WN = prevResult.WN;
444 % First iteration or no previous results, re-throw the exception
445 error('LINE:SolverLN:FirstIterationFailure
', ...
446 'Layer %d failed at iteration %d with no previous iteration to fall back on: %s
', ...
450 % Refresh the stochastic classification from the method the
451 % layer solver actually resolved at runtime (e.g. an NC layer
452 % with method 'default
' falling back to Monte Carlo
453 % integration); post() resets the layer solvers, so this must
454 % be captured here while results are still attached.
455 if it == 1 && ~isempty(self.stochlayers)
456 self.stochlayers(e) = self.solvers{e}.isStochastic();
458 % Warm-start the next AMVA solve of this layer from the current
459 % solution (chain-aggregated queue lengths). The layer AMVA can
460 % admit multiple fixed points (e.g., multiserver FCFS layers near
461 % saturation), so a cold restart may jump between solution
462 % branches under infinitesimal input changes, which prevents
463 % outer-loop convergence.
464 if strcmp(self.solvers{e}.name, 'SolverMVA
')
465 sne = self.ensemble{e}.getStruct(false);
467 QNe(~isfinite(QNe)) = 0;
468 if size(QNe,1) == sne.nstations && size(QNe,2) == sne.nclasses
469 Qch = zeros(sne.nstations, sne.nchains);
470 for c = 1:sne.nchains
471 Qch(:,c) = sum(QNe(:,sne.chains(c,:)>0),2);
473 self.solvers{e}.options.init_sol = Qch;
479 function post(self, it) % operations after an iteration
480 % POST(IT) % OPERATIONS AFTER AN ITERATION
481 line_debug('LN post: iteration %d, updating metrics and layer parameters
', it);
482 % convert the results of QNs into layer metrics
484 self.updateMetrics(it);
486 if self.options.config.interlocking
487 % apply interlock correction to call residence times
488 self.updatePopulations(it);
491 % recompute think times
492 self.updateThinkTimes(it);
494 % update the model parameters
495 self.updateLayers(it);
497 % update entry selection and cache routing probabilities within callers
498 self.updateRoutingProbabilities(it);
500 % reset all layers with routing probability changes
501 for e= self.routereset
502 self.ensemble{e}.refreshChains();
503 % refreshChains can change the chain basis, invalidating the
504 % warm-start solution cached by analyze()
505 self.solvers{e}.options.init_sol = [];
506 % NB: the MMT fork iterate is NOT cleared here. refreshChains
507 % can change the chain basis, which invalidates init_sol, but
508 % fjForkLambda is indexed by auxiliary class and is guarded by
509 % a conformance check at the use site. Clearing it here runs on
510 % every outer iteration and would defeat the warm start.
511 self.solvers{e}.reset();
514 % refresh visits and network model parameters
516 switch self.solvers{e}.name
517 case {'SolverMVA
', 'SolverNC
'} %leaner than refreshProcesses, no need to refresh phases
518 % note: this does not refresh the sn.proc field, only sn.rates and sn.scv
519 % Mirrors updateMetrics.m: only 'moment3
' needs the full
520 % process refresh; every other method takes the leaner rate
521 % refresh. Naming the methods explicitly instead left an
522 % unrecognized one with no refresh at all, so the layers
523 % never updated, the iteration converged trivially and the
524 % solver reported success on wrong numbers.
525 switch self.options.method
527 self.ensemble{e}.refreshProcesses();
529 self.ensemble{e}.refreshRates();
532 self.ensemble{e}.refreshProcesses();
534 self.solvers{e}.reset(); % commenting this out des not seem to produce a problem, but it goes faster with it
537 % Note: interlock correction is done via callresidt adjustment
538 % in updatePopulations, no population changes needed
541 % now disable all solver support checks for future iterations
542 for e=1:length(self.ensemble)
543 self.solvers{e}.setChecks(false);
549 function finish(self) % operations after iterations are completed
550 % FINISH() % OPERATIONS AFTER INTERATIONS ARE COMPLETED
551 line_debug('LN finish: final analysis of %d layers
', size(self.results,2));
552 E = size(self.results,2);
553 % In Robbins-Monro mode, report the Polyak-Ruppert averaged
554 % results rather than the last (noisy) iterate
555 if ~isempty(self.stochiter_mode) && strcmp(self.stochiter_mode,'rm
') && self.stoch_avg_count > 0
557 fnames = fieldnames(self.stoch_avg{e});
558 for f = 1:length(fnames)
559 self.results{end,e}.(fnames{f}) = self.stoch_avg{e}.(fnames{f});
562 self.servt = self.stoch_servt_avg;
563 self.residt = self.stoch_residt_avg;
570 self.model.ensemble = self.ensemble;
573 function [QNlqn_t, UNlqn_t, TNlqn_t] = getTranAvg(self, Qt, Ut, Tt)
574 % [QNLQN_T,UNLQN_T,TNLQN_T] = GETTRANAVG(SELF,QT,UT,TT)
575 % Block-diagonal aggregate transient over the LQN layers.
577 % options.config.ln_transient selects the inter-layer coupling of
579 % 'decoupled
' - freeze inter-layer demands at the converged fixed
580 % point (getAvg) and run each layer's transient in isolation.
581 %
'coupled' - reconcile
the per-layer transients by waveform
582 % relaxation, so layer populations and inter-layer demands
583 % co-evolve in model time (getTranAvgCoupled).
584 % Both modes return
the SAME block-diagonal layout; iteration 0 of
585 %
the coupled relaxation
is exactly
the decoupled result.
586 if nargin < 2, Qt = []; end
587 if nargin < 3, Ut = []; end
588 if nargin < 4, Tt = []; end
589 mode =
'coupled'; %
default: waveform-relaxation coupled transient
590 if isfield(self.options,'config') && isfield(self.options.config,'ln_transient') ...
591 && ~isempty(self.options.config.ln_transient)
592 mode = self.options.config.ln_transient;
596 [QNlqn_t, UNlqn_t, TNlqn_t] = self.getTranAvgCoupled(Qt, Ut, Tt);
598 [QNlqn_t, UNlqn_t, TNlqn_t] = self.getTranAvgDecoupled(Qt, Ut, Tt);
600 line_error(mfilename, sprintf(
'Unknown ln_transient mode ''%s'' (use ''coupled'' or ''decoupled'').', mode));
604 function [QNlqn_t, UNlqn_t, TNlqn_t] = getTranAvgDecoupled(self, Qt, Ut, Tt)
605 % [QNLQN_T,UNLQN_T,TNLQN_T] = GETTRANAVGDECOUPLED(SELF,QT,UT,TT)
606 % Decoupled (frozen-demand) layered transient. The optional
607 % (Qt,Ut,Tt) handles only fix
the aggregate M x K layout, which
the
608 % per-layer block concatenation already reproduces. %#ok<INUSD>
614 % The layered fixed-point iteration above solves each layer in
615 % steady state (layer getAvg rejects a timespan). The transient
616 % window therefore lives on
the SolverLN options and
is applied to
617 % each layer solver only around its transient getTranAvg call.
618 hasTs = isfield(self.options,'timespan') && numel(self.options.timespan)>=2 ...
619 && all(isfinite(self.options.timespan));
621 [crows, ccols] = size(QNlqn_t);
624 savedTs = s.options.timespan;
625 s.options.timespan = self.options.timespan;
627 [QNclass_t{e}, UNclass_t{e}, TNclass_t{e}] = s.getTranAvg();
629 s.options.timespan = savedTs;
631 QNlqn_t(crows+1:crows+size(QNclass_t{e},1),ccols+1:ccols+size(QNclass_t{e},2)) = QNclass_t{e};
632 UNlqn_t(crows+1:crows+size(UNclass_t{e},1),ccols+1:ccols+size(UNclass_t{e},2)) = UNclass_t{e};
633 TNlqn_t(crows+1:crows+size(TNclass_t{e},1),ccols+1:ccols+size(TNclass_t{e},2)) = TNclass_t{e};
637 function varargout = getAvg(varargin)
638 % [QN,UN,RN,TN,AN,WN] = GETAVG(SELF,~,~,~,~,USELQNSNAMING)
639 [varargout{1:nargout}] = getEnsembleAvg( varargin{:} );
642 function [cdfRespT] = getCdfRespT(self)
643 if isempty(self.entrycdfrespt{1})
644 % save user-specified method to temporary variable
645 curMethod = self.getOptions.method;
647 self.options.method =
'moment3';
649 % restore user-specified method
650 self.options.method = curMethod;
652 cdfRespT = self.entrycdfrespt;
655 function [AvgTable,QT,UT,RT,WT,AT,TT] = getAvgTable(self)
656 % [AVGTABLE,QT,UT,RT,WT,TT] = GETAVGTABLE(USELQNSNAMING)
657 if (GlobalConstants.DummyMode)
658 [AvgTable, QT, UT, RT, TT, WT] = deal([]);
663 if isfield(self.options,
'method') && ischar(self.options.method) ...
664 && any(strcmp(self.options.method, {
'mwba.upper',
'mwba.lower'}))
665 boundMethod = self.options.method;
667 if ~isempty(boundMethod)
668 % Majumdar-Woodside robust box bounds for
the LQN
669 bnd = lqn_boxbounds(self.lqn);
670 nidx = self.lqn.nidx;
671 if strcmp(boundMethod,'mwba.upper')
672 TN = bnd.TN_up; UN = bnd.UN_up;
674 TN = bnd.TN_lo; UN = bnd.UN_lo;
676 TN(isnan(TN)) = 0; UN(isnan(UN)) = 0;
677 QN = zeros(nidx,1); RN = zeros(nidx,1);
678 WN = zeros(nidx,1); AN = zeros(nidx,1);
679 elseif ~isempty(self.obj)
680 avgTable = self.obj.getEnsembleAvg();
681 [QN,UN,RN,WN,AN,TN] = JLINE.arrayListToResults(avgTable);
682 elseif ~isempty(self.pyMode) && self.pyMode
683 [QN,UN,RN,TN,AN,WN] = PYLINE.getEnsembleAvg(self.model, self.options, numel(self.lqn.names));
685 [QN,UN,RN,TN,AN,WN] = getAvg(self);
688 % attempt to sanitize small numerical perturbations
689 variables = {QN, UN, RN, TN, AN, WN}; % Put all variables in a cell array
690 for i = 1:length(variables)
691 rVar = round(variables{i} * 10);
692 toRound = abs(variables{i} * 10 - rVar) < GlobalConstants.CoarseTol * variables{i} * 10;
693 variables{i}(toRound) = rVar(toRound) / 10;
694 variables{i}(variables{i}<=GlobalConstants.FineTol) = 0;
696 [QN, UN, RN, TN, AN, WN] = deal(variables{:}); % Assign
the modified values back to
the original variables
699 Node = label(self.lqn.names);
701 NodeType = label(O,1);
703 switch self.lqn.type(o)
704 case LayeredNetworkElement.PROCESSOR
705 NodeType(o,1) = label({
'Processor'});
706 case LayeredNetworkElement.TASK
708 NodeType(o,1) = label({
'RefTask'});
710 NodeType(o,1) = label({
'Task'});
712 case LayeredNetworkElement.ENTRY
713 NodeType(o,1) = label({
'Entry'});
714 case LayeredNetworkElement.ACTIVITY
715 NodeType(o,1) = label({
'Activity'});
716 case LayeredNetworkElement.CALL
717 NodeType(o,1) = label({
'Call'});
721 QT = Table(Node,QLen);
723 UT = Table(Node,Util);
725 RT = Table(Node,RespT);
727 TT = Table(Node,Tput);
729 %ST = Table(Node,SvcT);
731 %PT = Table(Node,ProcUtil);
733 WT = Table(Node,ResidT);
735 AT = Table(Node,ArvR);
736 AvgTable = Table(Node, NodeType, QLen, Util, RespT, ResidT, ArvR, Tput);%, ProcUtil, SvcT);
739 function [AvgTable,QT,UT,RT,WT,AT,TT] = avgTable(self)
740 % AVGTABLE Alias
for getAvgTable
741 [AvgTable,QT,UT,RT,WT,AT,TT] = self.getAvgTable();
744 function [AvgTable,QT,UT,RT,WT,AT,TT] = avgT(self)
745 % AVGT Short alias
for getAvgTable
746 [AvgTable,QT,UT,RT,WT,AT,TT] = self.getAvgTable();
749 function [AvgTable,QT,UT,RT,WT,AT,TT] = aT(self)
750 % AT Short alias
for getAvgTable (MATLAB-compatible)
751 [AvgTable,QT,UT,RT,WT,AT,TT] = self.getAvgTable();
756 [QN,UN,RN,TN,AN,WN] = getEnsembleAvg(self);
757 [QNlqn_t, UNlqn_t, TNlqn_t] = getTranAvgCoupled(self, Qt, Ut, Tt);
759 function [bool, featSupported] = supports(self, model)
760 % [BOOL, FEATSUPPORTED] = SUPPORTS(SELF, MODEL)
761 % This method cannot be
static as otherwise it cannot access self.solvers{e}
762 ensemble = model.getEnsemble;
763 featSupported = cell(length(ensemble),1);
765 for e = 1:length(ensemble)
766 [solverSupports,featSupported{e}] = self.solvers{e}.supports(ensemble{e});
767 bool =
bool && solverSupports;
773 buildLayers(self, lqn, resptproc, callservtproc);
774 buildLayersRecursive(self, idx, callers, ishostlayer);
776 updateLayers(self, it);
777 updatePopulations(self, it);
778 updateThinkTimes(self, it);
779 updateMetrics(self, it);
780 updateRoutingProbabilities(self, it);
781 svcmatrix = getEntryServiceMatrix(self)
782 prOt = overtake_prob(self, eidx); % Phase-2 overtaking probability
786 function state = get_state(self)
787 % GET_STATE Export current solver state for continuation
789 % STATE = GET_STATE() returns a struct containing
the current
790 % solution state, which can be used to continue iteration with
791 % a different solver via set_state().
793 % The exported state includes:
794 % - Service time processes (servtproc)
795 % - Think time processes (thinktproc)
796 % - Call service time processes (callservtproc)
797 % - Throughput processes (tputproc)
798 % - Performance metrics (util, tput, servt, residt, etc.)
800 % - Last iteration results
803 % solver1 = SolverLN(model, @(m) SolverMVA(m));
804 % solver1.getEnsembleAvg();
805 % state = solver1.get_state();
807 % solver2 = SolverLN(model, @(m) SolverNC(m));
808 % solver2.set_state(state);
809 % solver2.getEnsembleAvg(); % Continues from MVA solution
811 % NOTE: a pure-JMT layer factory (@(m) SolverJMT(m))
is
812 % unsupported because LN server layers carry immediate feedback
813 % (sn.immfeed), which SolverJMT rejects; SolverLN raises a clear
814 % error upfront in that
case.
818 % Service/think time processes
819 state.servtproc = self.servtproc;
820 state.thinktproc = self.thinktproc;
821 state.callservtproc = self.callservtproc;
822 state.tputproc = self.tputproc;
823 state.entryproc = self.entryproc;
825 % Performance metrics
826 state.util = self.util;
827 state.tput = self.tput;
828 state.servt = self.servt;
829 state.residt = self.residt;
830 state.thinkt = self.thinkt;
831 state.callresidt = self.callresidt;
832 state.callservt = self.callservt;
835 state.relax_omega = self.relax_omega;
836 state.servt_prev = self.servt_prev;
837 state.residt_prev = self.residt_prev;
838 state.tput_prev = self.tput_prev;
839 state.thinkt_prev = self.thinkt_prev;
840 state.callservt_prev = self.callservt_prev;
841 state.callresidt_prev = self.callresidt_prev;
843 % Results from last iteration
844 state.results = self.results;
847 state.njobs = self.njobs;
848 state.ilscaling = self.ilscaling;
851 function set_state(self, state)
852 % SET_STATE Import solution state
for continuation
854 % SET_STATE(STATE) initializes
the solver with a previously
855 % exported state, allowing iteration to
continue from where
856 % a previous solver left off.
858 % This enables hybrid solving schemes where fast
solvers (MVA)
859 % provide initial estimates and accurate
solvers (JMT, LDES)
860 % refine
the solution.
863 % solver1 = SolverLN(model, @(m) SolverMVA(m));
864 % solver1.getEnsembleAvg();
865 % state = solver1.get_state();
867 % solver2 = SolverLN(model, @(m) SolverNC(m));
868 % solver2.set_state(state);
869 % solver2.getEnsembleAvg(); % Continues from MVA solution
871 % NOTE: a pure-JMT layer factory (@(m) SolverJMT(m))
is
872 % unsupported because LN server layers carry immediate feedback
873 % (sn.immfeed), which SolverJMT rejects; SolverLN raises a clear
874 % error upfront in that
case.
876 % Service/think time processes
877 self.servtproc = state.servtproc;
878 self.thinktproc = state.thinktproc;
879 self.callservtproc = state.callservtproc;
880 self.tputproc = state.tputproc;
881 if isfield(state,
'entryproc')
882 self.entryproc = state.entryproc;
885 % Performance metrics
886 self.util = state.util;
887 self.tput = state.tput;
888 self.servt = state.servt;
889 if isfield(state, 'residt')
890 self.residt = state.residt;
892 if isfield(state, 'thinkt')
893 self.thinkt = state.thinkt;
895 if isfield(state, 'callresidt')
896 self.callresidt = state.callresidt;
898 if isfield(state, 'callservt')
899 self.callservt = state.callservt;
903 if isfield(state, 'relax_omega')
904 self.relax_omega = state.relax_omega;
906 if isfield(state, 'servt_prev')
907 self.servt_prev = state.servt_prev;
909 if isfield(state, 'residt_prev')
910 self.residt_prev = state.residt_prev;
912 if isfield(state, 'tput_prev')
913 self.tput_prev = state.tput_prev;
915 if isfield(state, 'thinkt_prev')
916 self.thinkt_prev = state.thinkt_prev;
918 if isfield(state, 'callservt_prev')
919 self.callservt_prev = state.callservt_prev;
921 if isfield(state, 'callresidt_prev')
922 self.callresidt_prev = state.callresidt_prev;
926 if isfield(state, 'results')
927 self.results = state.results;
931 if isfield(state, 'njobs')
932 self.njobs = state.njobs;
934 if isfield(state, 'ilscaling')
935 self.ilscaling = state.ilscaling;
938 % Update layer models with imported state
940 if ~isempty(self.results)
941 it = size(self.results, 1);
943 self.updateLayers(it);
945 % Refresh all layer
solvers with new parameters
946 for e = 1:self.nlayers
947 % Ensure layer model struct
is fully built before refresh
948 self.ensemble{e}.getStruct(
false);
949 self.ensemble{e}.refreshChains();
950 % refreshChains can change
the chain basis, invalidating
the
951 % warm-start solution cached by analyze()
952 self.
solvers{e}.options.init_sol = [];
953 if isa(self.solvers{e},
'SolverMVA')
954 self.
solvers{e}.resetForkWarmStart();
956 switch self.solvers{e}.name
957 case {
'SolverMVA',
'SolverNC'}
958 self.ensemble{e}.refreshRates();
960 self.ensemble{e}.refreshProcesses();
962 self.solvers{e}.reset();
966 function update_solver(self, solverFactory)
967 % UPDATE_SOLVER Change
the solver
for all layers
969 % UPDATE_SOLVER(FACTORY) replaces all layer
solvers with
970 %
new solvers created by
the given factory function.
972 % This allows switching between different solving methods
973 % (e.g., from MVA to LDES)
while preserving
the current
977 % solver = SolverLN(model, @(m) SolverMVA(m));
978 % solver.getEnsembleAvg(); % Fast initial solution
980 % solver.update_solver(@(m) SolverLDES(m,
'samples', 1e5));
981 % solver.getEnsembleAvg(); % Refine with simulation
983 % NOTE: a pure-JMT layer factory (@(m) SolverJMT(m, ...))
is
984 % unsupported: LN server layers carry immediate feedback
985 % (sn.immfeed), which SolverJMT rejects. update_solver raises a
986 % clear error upfront in that case.
988 self.solverFactory = solverFactory;
991 for e = 1:self.nlayers
992 layerSolver = solverFactory(self.ensemble{e});
993 self.assertLayerSolverSupportsModel(layerSolver, self.ensemble{e}, e);
994 self.setSolver(layerSolver, e);
998 function assertLayerSolverSupportsModel(self, layerSolver, layerModel, e) %#ok<INUSL>
999 % ASSERTLAYERSOLVERSUPPORTSMODEL Reject a layer solver that cannot
1000 % represent its layer model, upfront with a clear message.
1002 % LN server-layer stations carry immediate feedback (sn.immfeed):
1003 % successive same-host activities retain the server, modelled as
1004 % immediate-feedback self-loops so the layer solver does not
1005 % re-queue the job. SolverJMT rejects any model with immfeed and
1006 % returns no solution, so a pure-JMT layer factory otherwise fails
1007 % cryptically at layer 1, iteration 1. Detect it here instead.
1009 % The guard is CONDITIONAL: it fires only when the specific layer
1010 % model actually carries immfeed. A SolverJMT layer solver on an
1011 % immfeed-free layer is allowed, and non-JMT factories are never
1013 if isa(layerSolver, 'SolverJMT')
1014 lsn = layerModel.getStruct();
1015 if isfield(lsn,
'immfeed') && ~isempty(lsn.immfeed) && any(lsn.immfeed(:))
1016 line_error(mfilename, [
'SolverJMT cannot solve LN layer %d: the layer carries immediate feedback (sn.immfeed), which SolverJMT does not support, so LN would fail at the first iteration. Use the default layer factory (MVA/NC) or another layer solver that supports immediate feedback.'], e);
1021 function [allMethods] = listValidMethods(self)
1022 sn = self.model.getStruct();
1023 % allMethods = LISTVALIDMETHODS()
1024 % List valid methods
for this solver
1025 allMethods = {
'default',
'moment3'};
1030 function options = defaultOptions()
1031 % OPTIONS = DEFAULTOPTIONS()
1032 options = SolverOptions('LN');
1035 function libs = getLibrariesUsed(sn, options)
1036 % GETLIBRARIESUSED Get list of external libraries used by LN solver
1037 % LN uses internal algorithms, no external library attribution needed
1043function solver = adaptiveSolverFactory(model, parentOptions)
1044 % ADAPTIVESOLVERFACTORY - Select appropriate solver based on model characteristics
1045 % Use JMT for models with open classes, MVA for pure closed networks
1049 verbose = parentOptions.verbose;
1052 % Create MVA solver with reduced iter_max
for sublayer stability (Python parity)
1053 solver = SolverMVA(model,
'verbose', verbose);
1054 solver.options.iter_max = 1000; % Cap sublayer MVA iterations