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 % see _kb/06-solver-catalog.md (LN section) for rationale
127 self.lqn = lqnmodel.getStruct();
129 % see _kb/06-solver-catalog.md (LN section) for rationale
131 if nargin > 1 && isstruct(solverFactory)
132 self.setOptions(solverFactory);
134 self.setOptions(SolverLN.defaultOptions);
136 self.options.lang =
'python';
137 self.lqn = lqnmodel.getStruct();
139 % Default solver factory: Use JMT
for open networks, MVA
for closed networks
140 defaultSolverFactory = @(m) adaptiveSolverFactory(m, self.options);
142 if nargin == 1 %
case SolverLN(model)
143 solverFactory = defaultSolverFactory;
144 self.setOptions(SolverLN.defaultOptions);
145 elseif nargin>1 && isstruct(solverFactory)
146 options = solverFactory;
147 self.setOptions(options);
148 solverFactory = defaultSolverFactory;
149 elseif nargin>2 %
case SolverLN(model,
'opt1',...)
150 if ischar(solverFactory)
151 inputvar = {solverFactory,varargin{:}}; %#ok<CCAT>
152 solverFactory = defaultSolverFactory;
153 else %
case SolverLN(model, solverFactory,
'opt1',...)
156 self.setOptions(Solver.parseOptions(inputvar, SolverLN.defaultOptions));
157 else %case SolverLN(model,solverFactory)
158 self.setOptions(SolverLN.defaultOptions);
160 self.lqn = lqnmodel.getStruct();
161 % see _kb/06-solver-catalog.md (LN section) for rationale
162 self.lqn = lqn_fwd_rendezvous(self.lqn);
164 % Detect and initialize phase-2 support
165 if isfield(self.lqn, 'actphase') && any(self.lqn.actphase > 1)
166 self.hasPhase2 = true;
167 self.servt_ph1 = zeros(self.lqn.nidx, 1);
168 self.servt_ph2 = zeros(self.lqn.nidx, 1);
169 self.util_ph1 = zeros(self.lqn.nidx, 1);
170 self.util_ph2 = zeros(self.lqn.nidx, 1);
171 self.prOvertake = zeros(self.lqn.nentries, 1);
173 self.hasPhase2 = false;
177 line_debug('LN: solver factory=%s, constructing layers', func2str(solverFactory));
178 for e=1:self.getNumberOfModels
179 % see _kb/06-solver-catalog.md (LN section) for rationale
180 if numel(find(self.lqn.isfunction == 1)) && ~isempty(self.ensemble{e}.stations{2}.setupTime)
181 layerFactory = @(m) SolverMAM(m,
'verbose',
false,
'method',
'dec.poisson');
183 layerFactory = solverFactory;
185 layerSolver = layerFactory(self.ensemble{e});
186 self.assertLayerSolverSupportsModel(layerSolver, self.ensemble{e}, e);
187 self.setSolver(layerSolver,e);
189 self.solverFactory = solverFactory; % Store
for later use
193 function runtime = runAnalyzer(self, options) %#ok<INUSD> %
generic method to run the solver
194 line_error(mfilename,
'Use getEnsembleAvg instead.');
197 function sn = getStruct(self)
200 % Get data structure summarizing the model
201 sn = self.model.getStruct();
204 function construct(self)
205 % mark down to ignore unreachable disconnected components
206 self.ignore =
false(self.lqn.nidx,1);
207 [~,wccs] = weaklyconncomp(self.lqn.graph
'+self.lqn.graph);
208 uwccs = unique(wccs);
210 % the model has disconnected submodels
211 wccref = false(1,length(uwccs));
212 for t=1:self.lqn.ntasks
213 tidx = self.lqn.tshift+t;
214 if self.lqn.sched(tidx) == SchedStrategy.REF
215 wccref(wccs(tidx)) = true;
218 if any(wccref==false)
219 for dw=find(wccref==false) % disconnected component
220 self.ignore(find(wccs==dw)) = true;
225 % initialize internal data structures
226 self.entrycdfrespt = cell(length(self.lqn.nentries),1);
227 self.hasconverged = false;
229 % initialize svc and think times
230 self.servtproc = self.lqn.hostdem;
231 self.thinkproc = self.lqn.think;
232 self.callservtproc = cell(self.lqn.ncalls,1);
233 for cidx = 1:self.lqn.ncalls
234 self.callservtproc{cidx} = self.lqn.hostdem{self.lqn.callpair(cidx,2)};
238 self.njobs = zeros(self.lqn.tshift + self.lqn.ntasks, self.lqn.tshift + self.lqn.ntasks);
239 buildLayers(self); % build layers
240 line_debug('LN construct: built %d layers from LQN model (%d hosts, %d tasks, %d entries, %d activities)
', ...
241 length(self.ensemble), self.lqn.nhosts, self.lqn.ntasks, self.lqn.nentries, self.lqn.nacts);
242 self.njobsorig = self.njobs;
243 self.nlayers = length(self.ensemble);
245 % interlock data structures are built in init() via initInterlock()
247 % layering generates update maps that we use here to cache the elements that need reset
248 self.routereset = unique(self.idxhash(self.route_prob_updmap(:,1)))';
249 self.svcreset = unique(self.idxhash(self.thinkt_classes_updmap(:,1)))
';
250 self.svcreset = union(self.svcreset,unique(self.idxhash(self.call_classes_updmap(:,1)))');
253 function self = reset(self)
257 bool = converged(self, it); % convergence test at iteration it
258 bool = convergedStoch(self, it); % convergence test
for stochastic layer solvers (Robbins-Monro mode)
260 function init(self) % operations before starting to iterate
261 % INIT() % OPERATIONS BEFORE STARTING TO ITERATE
262 self.unique_route_prob_updmap = unique(self.route_prob_updmap(:,1))
';
263 self.tput = zeros(self.lqn.nidx,1);
264 self.tputproc = cell(self.lqn.nidx,1);
265 self.util = zeros(self.lqn.nidx,1);
266 self.servt = zeros(self.lqn.nidx,1);
267 self.servtmatrix = getEntryServiceMatrix(self);
269 % see _kb/06-solver-catalog.md (LN section) for rationale
270 for e= 1:self.nlayers
271 self.solvers{e}.enableChecks=true;
274 % Initialize under-relaxation state
275 relax_mode = self.options.config.relax;
278 self.relax_omega = 1.0; % Start without relaxation
279 case {'fixed
', 'adaptive
'}
280 self.relax_omega = self.options.config.relax_factor;
281 otherwise % 'none
' or unrecognized
282 self.relax_omega = 1.0; % No relaxation
284 self.relax_err_history = [];
285 line_debug('LN init: %d layers, relaxation=%s (omega=%.3f)
', ...
286 self.nlayers, self.options.config.relax, self.relax_omega);
287 self.servt_prev = NaN(self.lqn.nidx, 1);
288 self.residt_prev = NaN(self.lqn.nidx, 1);
289 self.tput_prev = NaN(self.lqn.nidx, 1);
290 self.thinkt_prev = NaN(self.lqn.nidx, 1);
291 self.thinkt = zeros(self.lqn.nidx, 1); % Initialize to zeros (Python parity)
292 self.callservt_prev = NaN(self.lqn.ncalls, 1);
293 self.callresidt_prev = NaN(self.lqn.ncalls, 1);
295 % Build interlock tables (LQNS V5 static analysis)
296 if self.options.config.interlocking
297 self.initInterlock();
300 % Initialize MOL-specific state
301 self.util_prev_host = zeros(self.lqn.nhosts, 1);
302 self.util_prev_task = zeros(self.lqn.ntasks, 1);
304 % see _kb/06-solver-catalog.md (LN section) for rationale
306 if isfield(self.options.config, 'layer_init
') && ~isempty(self.options.config.layer_init)
307 initMode = self.options.config.layer_init;
309 if any(strcmpi(initMode, {'bound
','boxbound
','mwba
'}))
311 bnd = lqn_boxbounds(self.lqn);
312 for idx = 1:self.lqn.nidx
313 u = bnd.TN_up(idx); l = bnd.TN_lo(idx);
314 if isfinite(u) && isfinite(l) && u > 0 && l > 0
325 self.tputproc{idx} = Exp.fitRate(x);
328 line_debug('LN init: throughputs initialized from robust box bounds
');
330 line_debug('LN box-bound initialization skipped: %s
', ME.message);
334 % see _kb/06-solver-catalog.md (LN section) for rationale
335 self.stochlayers = false(1, self.nlayers);
336 for e = 1:self.nlayers
337 self.stochlayers(e) = self.solvers{e}.isStochastic();
339 mode = self.options.config.stochiter;
340 self.stochiter_auto = strcmpi(mode, 'auto
');
341 if self.stochiter_auto
342 if any(self.stochlayers)
348 self.stochiter_mode = lower(mode);
349 self.stochiter_start = [];
350 self.stoch_avg = cell(1, self.nlayers);
351 self.stoch_avg_count = 0;
352 self.stoch_servt_avg = [];
353 self.stoch_residt_avg = [];
354 line_debug('LN init: stochastic iteration mode=%s (%d stochastic layers)
', ...
355 self.stochiter_mode, sum(self.stochlayers));
359 function pre(self, it) % operations before an iteration
360 % PRE(IT) % OPERATIONS BEFORE AN ITERATION
361 % Seed control for stochastic layer solvers
362 if isempty(self.stochiter_mode)
365 switch self.stochiter_mode
367 % see _kb/06-solver-catalog.md (LN section) for rationale
368 for e = find(self.stochlayers)
369 self.solvers{e}.options.seed = self.options.seed + (it-1)*self.nlayers + e;
372 % see _kb/06-solver-catalog.md (LN section) for rationale
373 for e = find(self.stochlayers)
374 self.solvers{e}.options.seed = self.options.seed + e;
379 function [result, runtime] = analyze(self, it, e)
380 % [RESULT, RUNTIME] = ANALYZE(IT, E)
382 line_debug('LN analyze: iteration %d, layer %d (%s)
', it, e, class(self.solvers{e}));
385 if e==1 && self.solvers{e}.options.verbose
389 % Protection for unstable queues during LN iterations
390 % If a solver fails (e.g., due to queue instability with open arrivals),
391 % use results from previous iteration if available and continue
393 [result.QN, result.UN, result.RN, result.TN, result.AN, result.WN] = self.solvers{e}.getAvg();
395 if it > 1 && ~isempty(self.results) && size(self.results, 1) >= (it-1) && size(self.results, 2) >= e
396 if self.solvers{e}.options.verbose
397 warning('LINE:SolverLN:Instability
', ...
398 'Layer %d at iteration %d encountered instability (possibly due to high service demand with open arrivals). Using previous iteration values and continuing.
', ...
401 % Use results from previous iteration
402 prevResult = self.results{it-1, e};
403 result.QN = prevResult.QN;
404 result.UN = prevResult.UN;
405 result.RN = prevResult.RN;
406 result.TN = prevResult.TN;
407 result.AN = prevResult.AN;
408 result.WN = prevResult.WN;
410 % First iteration or no previous results, re-throw the exception
411 error('LINE:SolverLN:FirstIterationFailure
', ...
412 'Layer %d failed at iteration %d with no previous iteration to fall back on: %s
', ...
416 % see _kb/06-solver-catalog.md (LN section) for rationale
417 if it == 1 && ~isempty(self.stochlayers)
418 self.stochlayers(e) = self.solvers{e}.isStochastic();
420 % see _kb/06-solver-catalog.md (LN section) for rationale
421 if strcmp(self.solvers{e}.name, 'SolverMVA
')
422 sne = self.ensemble{e}.getStruct(false);
424 QNe(~isfinite(QNe)) = 0;
425 if size(QNe,1) == sne.nstations && size(QNe,2) == sne.nclasses
426 Qch = zeros(sne.nstations, sne.nchains);
427 for c = 1:sne.nchains
428 Qch(:,c) = sum(QNe(:,sne.chains(c,:)>0),2);
430 self.solvers{e}.options.init_sol = Qch;
436 function post(self, it) % operations after an iteration
437 % POST(IT) % OPERATIONS AFTER AN ITERATION
438 line_debug('LN post: iteration %d, updating metrics and layer parameters
', it);
439 % convert the results of QNs into layer metrics
441 self.updateMetrics(it);
443 if self.options.config.interlocking
444 % apply interlock correction to call residence times
445 self.updatePopulations(it);
448 % recompute think times
449 self.updateThinkTimes(it);
451 % update the model parameters
452 self.updateLayers(it);
454 % update entry selection and cache routing probabilities within callers
455 self.updateRoutingProbabilities(it);
457 % reset all layers with routing probability changes
458 for e= self.routereset
459 self.ensemble{e}.refreshChains();
460 % refreshChains can change the chain basis, invalidating the
461 % warm-start solution cached by analyze()
462 self.solvers{e}.options.init_sol = [];
463 % see _kb/06-solver-catalog.md (LN section) for rationale
464 self.solvers{e}.reset();
467 % refresh visits and network model parameters
469 switch self.solvers{e}.name
470 case {'SolverMVA
', 'SolverNC
'} %leaner than refreshProcesses, no need to refresh phases
471 % see _kb/06-solver-catalog.md (LN section) for rationale
472 switch self.options.method
474 self.ensemble{e}.refreshProcesses();
476 self.ensemble{e}.refreshRates();
479 self.ensemble{e}.refreshProcesses();
481 self.solvers{e}.reset(); % commenting this out des not seem to produce a problem, but it goes faster with it
484 % Note: interlock correction is done via callresidt adjustment
485 % in updatePopulations, no population changes needed
488 % now disable all solver support checks for future iterations
489 for e=1:length(self.ensemble)
490 self.solvers{e}.setChecks(false);
496 function finish(self) % operations after iterations are completed
497 % FINISH() % OPERATIONS AFTER INTERATIONS ARE COMPLETED
498 line_debug('LN finish: final analysis of %d layers
', size(self.results,2));
499 E = size(self.results,2);
500 % In Robbins-Monro mode, report the Polyak-Ruppert averaged
501 % results rather than the last (noisy) iterate
502 if ~isempty(self.stochiter_mode) && strcmp(self.stochiter_mode,'rm
') && self.stoch_avg_count > 0
504 fnames = fieldnames(self.stoch_avg{e});
505 for f = 1:length(fnames)
506 self.results{end,e}.(fnames{f}) = self.stoch_avg{e}.(fnames{f});
509 self.servt = self.stoch_servt_avg;
510 self.residt = self.stoch_residt_avg;
517 self.model.ensemble = self.ensemble;
520 function [QNlqn_t, UNlqn_t, TNlqn_t] = getTranAvg(self, Qt, Ut, Tt)
521 % [QNLQN_T,UNLQN_T,TNLQN_T] = GETTRANAVG(SELF,QT,UT,TT)
522 % Block-diagonal aggregate transient over the LQN layers.
524 % options.config.ln_transient selects the inter-layer coupling of
526 % 'decoupled
' - freeze inter-layer demands at the converged fixed
527 % point (getAvg) and run each layer's transient in isolation.
528 %
'coupled' - reconcile the per-layer transients by waveform
529 % relaxation, so layer populations and inter-layer demands
530 % co-evolve in model time (getTranAvgCoupled).
531 % Both modes return the SAME block-diagonal layout; iteration 0 of
532 % the coupled relaxation
is exactly the decoupled result.
533 if nargin < 2, Qt = []; end
534 if nargin < 3, Ut = []; end
535 if nargin < 4, Tt = []; end
536 mode =
'coupled'; %
default: waveform-relaxation coupled transient
537 if isfield(self.options,'config') && isfield(self.options.config,'ln_transient') ...
538 && ~isempty(self.options.config.ln_transient)
539 mode = self.options.config.ln_transient;
543 [QNlqn_t, UNlqn_t, TNlqn_t] = self.getTranAvgCoupled(Qt, Ut, Tt);
545 [QNlqn_t, UNlqn_t, TNlqn_t] = self.getTranAvgDecoupled(Qt, Ut, Tt);
547 line_error(mfilename, sprintf(
'Unknown ln_transient mode ''%s'' (use ''coupled'' or ''decoupled'').', mode));
551 function [QNlqn_t, UNlqn_t, TNlqn_t] = getTranAvgDecoupled(self, Qt, Ut, Tt)
552 % [QNLQN_T,UNLQN_T,TNLQN_T] = GETTRANAVGDECOUPLED(SELF,QT,UT,TT)
553 % Decoupled (frozen-demand) layered transient. The optional
554 % (Qt,Ut,Tt) handles only fix the aggregate M x K layout, which the
555 % per-layer block concatenation already reproduces. %#ok<INUSD>
561 % see _kb/06-solver-catalog.md (LN section) for rationale
562 hasTs = isfield(self.options,'timespan') && numel(self.options.timespan)>=2 ...
563 && all(isfinite(self.options.timespan));
565 [crows, ccols] = size(QNlqn_t);
568 savedTs = s.options.timespan;
569 s.options.timespan = self.options.timespan;
571 [QNclass_t{e}, UNclass_t{e}, TNclass_t{e}] = s.getTranAvg();
573 s.options.timespan = savedTs;
575 QNlqn_t(crows+1:crows+size(QNclass_t{e},1),ccols+1:ccols+size(QNclass_t{e},2)) = QNclass_t{e};
576 UNlqn_t(crows+1:crows+size(UNclass_t{e},1),ccols+1:ccols+size(UNclass_t{e},2)) = UNclass_t{e};
577 TNlqn_t(crows+1:crows+size(TNclass_t{e},1),ccols+1:ccols+size(TNclass_t{e},2)) = TNclass_t{e};
581 function varargout = getAvg(varargin)
582 % [QN,UN,RN,TN,AN,WN] = GETAVG(SELF,~,~,~,~,USELQNSNAMING)
583 [varargout{1:nargout}] = getEnsembleAvg( varargin{:} );
586 function [cdfRespT] = getCdfRespT(self)
587 if isempty(self.entrycdfrespt{1})
588 % save user-specified method to temporary variable
589 curMethod = self.getOptions.method;
591 self.options.method =
'moment3';
593 % restore user-specified method
594 self.options.method = curMethod;
596 cdfRespT = self.entrycdfrespt;
599 function [AvgTable,QT,UT,RT,WT,AT,TT] = getAvgTable(self)
600 % [AVGTABLE,QT,UT,RT,WT,TT] = GETAVGTABLE(USELQNSNAMING)
601 if (GlobalConstants.DummyMode)
602 [AvgTable, QT, UT, RT, TT, WT] = deal([]);
607 if isfield(self.options,
'method') && ischar(self.options.method) ...
608 && any(strcmp(self.options.method, {
'mwba.upper',
'mwba.lower'}))
609 boundMethod = self.options.method;
611 if ~isempty(boundMethod)
612 % Majumdar-Woodside robust box bounds for the LQN
613 bnd = lqn_boxbounds(self.lqn);
614 nidx = self.lqn.nidx;
615 if strcmp(boundMethod,'mwba.upper')
616 TN = bnd.TN_up; UN = bnd.UN_up;
618 TN = bnd.TN_lo; UN = bnd.UN_lo;
620 TN(isnan(TN)) = 0; UN(isnan(UN)) = 0;
621 QN = zeros(nidx,1); RN = zeros(nidx,1);
622 WN = zeros(nidx,1); AN = zeros(nidx,1);
623 elseif ~isempty(self.obj)
624 avgTable = self.obj.getEnsembleAvg();
625 [QN,UN,RN,WN,AN,TN] = JLINE.arrayListToResults(avgTable);
626 elseif ~isempty(self.pyMode) && self.pyMode
627 [QN,UN,RN,TN,AN,WN] = PYLINE.getEnsembleAvg(self.model, self.options, numel(self.lqn.names));
629 [QN,UN,RN,TN,AN,WN] = getAvg(self);
632 % attempt to sanitize small numerical perturbations
633 variables = {QN, UN, RN, TN, AN, WN}; % Put all variables in a cell array
634 for i = 1:length(variables)
635 rVar = round(variables{i} * 10);
636 toRound = abs(variables{i} * 10 - rVar) < GlobalConstants.CoarseTol * variables{i} * 10;
637 variables{i}(toRound) = rVar(toRound) / 10;
638 variables{i}(variables{i}<=GlobalConstants.FineTol) = 0;
640 [QN, UN, RN, TN, AN, WN] = deal(variables{:}); % Assign the modified values back to the original variables
643 Node = label(self.lqn.names);
645 NodeType = label(O,1);
647 switch self.lqn.type(o)
648 case LayeredNetworkElement.PROCESSOR
649 NodeType(o,1) = label({
'Processor'});
650 case LayeredNetworkElement.TASK
652 NodeType(o,1) = label({
'RefTask'});
654 NodeType(o,1) = label({
'Task'});
656 case LayeredNetworkElement.ENTRY
657 NodeType(o,1) = label({
'Entry'});
658 case LayeredNetworkElement.ACTIVITY
659 NodeType(o,1) = label({
'Activity'});
660 case LayeredNetworkElement.CALL
661 NodeType(o,1) = label({
'Call'});
665 QT = Table(Node,QLen);
667 UT = Table(Node,Util);
669 RT = Table(Node,RespT);
671 TT = Table(Node,Tput);
673 %ST = Table(Node,SvcT);
675 %PT = Table(Node,ProcUtil);
677 WT = Table(Node,ResidT);
679 AT = Table(Node,ArvR);
680 AvgTable = Table(Node, NodeType, QLen, Util, RespT, ResidT, ArvR, Tput);%, ProcUtil, SvcT);
683 function [AvgTable,QT,UT,RT,WT,AT,TT] = avgTable(self)
684 % AVGTABLE Alias
for getAvgTable
685 [AvgTable,QT,UT,RT,WT,AT,TT] = self.getAvgTable();
688 function [AvgTable,QT,UT,RT,WT,AT,TT] = avgT(self)
689 % AVGT Short alias
for getAvgTable
690 [AvgTable,QT,UT,RT,WT,AT,TT] = self.getAvgTable();
693 function [AvgTable,QT,UT,RT,WT,AT,TT] = aT(self)
694 % AT Short alias
for getAvgTable (MATLAB-compatible)
695 [AvgTable,QT,UT,RT,WT,AT,TT] = self.getAvgTable();
700 [QN,UN,RN,TN,AN,WN] = getEnsembleAvg(self);
701 [QNlqn_t, UNlqn_t, TNlqn_t] = getTranAvgCoupled(self, Qt, Ut, Tt);
703 function [bool, featSupported] = supports(self, model)
704 % [BOOL, FEATSUPPORTED] = SUPPORTS(SELF, MODEL)
705 % This method cannot be
static as otherwise it cannot access self.solvers{e}
706 ensemble = model.getEnsemble;
707 featSupported = cell(length(ensemble),1);
709 for e = 1:length(ensemble)
710 [solverSupports,featSupported{e}] = self.solvers{e}.supports(ensemble{e});
711 bool =
bool && solverSupports;
717 buildLayers(self, lqn, resptproc, callservtproc);
718 buildLayersRecursive(self, idx, callers, ishostlayer);
720 updateLayers(self, it);
721 updatePopulations(self, it);
722 updateThinkTimes(self, it);
723 updateMetrics(self, it);
724 updateRoutingProbabilities(self, it);
725 svcmatrix = getEntryServiceMatrix(self)
726 prOt = overtake_prob(self, eidx); % Phase-2 overtaking probability
730 function state = get_state(self)
731 % GET_STATE Export current solver state for continuation
733 % STATE = GET_STATE() returns a struct containing the current
734 % solution state, which can be used to continue iteration with
735 % a different solver via set_state().
737 % The exported state includes:
738 % - Service time processes (servtproc)
739 % - Think time processes (thinktproc)
740 % - Call service time processes (callservtproc)
741 % - Throughput processes (tputproc)
742 % - Performance metrics (util, tput, servt, residt, etc.)
744 % - Last iteration results
747 % solver1 = SolverLN(model, @(m) SolverMVA(m));
748 % solver1.getEnsembleAvg();
749 % state = solver1.get_state();
751 % solver2 = SolverLN(model, @(m) SolverNC(m));
752 % solver2.set_state(state);
753 % solver2.getEnsembleAvg(); % Continues from MVA solution
755 % NOTE: a pure-JMT layer factory (@(m) SolverJMT(m))
is
756 % unsupported because LN server layers carry immediate feedback
757 % (sn.immfeed), which SolverJMT rejects; SolverLN raises a clear
758 % error upfront in that
case.
762 % Service/think time processes
763 state.servtproc = self.servtproc;
764 state.thinktproc = self.thinktproc;
765 state.callservtproc = self.callservtproc;
766 state.tputproc = self.tputproc;
767 state.entryproc = self.entryproc;
769 % Performance metrics
770 state.util = self.util;
771 state.tput = self.tput;
772 state.servt = self.servt;
773 state.residt = self.residt;
774 state.thinkt = self.thinkt;
775 state.callresidt = self.callresidt;
776 state.callservt = self.callservt;
779 state.relax_omega = self.relax_omega;
780 state.servt_prev = self.servt_prev;
781 state.residt_prev = self.residt_prev;
782 state.tput_prev = self.tput_prev;
783 state.thinkt_prev = self.thinkt_prev;
784 state.callservt_prev = self.callservt_prev;
785 state.callresidt_prev = self.callresidt_prev;
787 % Results from last iteration
788 state.results = self.results;
791 state.njobs = self.njobs;
792 state.ilscaling = self.ilscaling;
795 function set_state(self, state)
796 % SET_STATE Import solution state
for continuation
798 % SET_STATE(STATE) initializes the solver with a previously
799 % exported state, allowing iteration to
continue from where
800 % a previous solver left off.
802 % This enables hybrid solving schemes where fast solvers (MVA)
803 % provide initial estimates and accurate solvers (JMT, LDES)
804 % refine the solution.
807 % solver1 = SolverLN(model, @(m) SolverMVA(m));
808 % solver1.getEnsembleAvg();
809 % state = solver1.get_state();
811 % solver2 = SolverLN(model, @(m) SolverNC(m));
812 % solver2.set_state(state);
813 % solver2.getEnsembleAvg(); % Continues from MVA solution
815 % NOTE: a pure-JMT layer factory (@(m) SolverJMT(m))
is
816 % unsupported because LN server layers carry immediate feedback
817 % (sn.immfeed), which SolverJMT rejects; SolverLN raises a clear
818 % error upfront in that
case.
820 % Service/think time processes
821 self.servtproc = state.servtproc;
822 self.thinktproc = state.thinktproc;
823 self.callservtproc = state.callservtproc;
824 self.tputproc = state.tputproc;
825 if isfield(state,
'entryproc')
826 self.entryproc = state.entryproc;
829 % Performance metrics
830 self.util = state.util;
831 self.tput = state.tput;
832 self.servt = state.servt;
833 if isfield(state, 'residt')
834 self.residt = state.residt;
836 if isfield(state, 'thinkt')
837 self.thinkt = state.thinkt;
839 if isfield(state, 'callresidt')
840 self.callresidt = state.callresidt;
842 if isfield(state, 'callservt')
843 self.callservt = state.callservt;
847 if isfield(state, 'relax_omega')
848 self.relax_omega = state.relax_omega;
850 if isfield(state, 'servt_prev')
851 self.servt_prev = state.servt_prev;
853 if isfield(state, 'residt_prev')
854 self.residt_prev = state.residt_prev;
856 if isfield(state, 'tput_prev')
857 self.tput_prev = state.tput_prev;
859 if isfield(state, 'thinkt_prev')
860 self.thinkt_prev = state.thinkt_prev;
862 if isfield(state, 'callservt_prev')
863 self.callservt_prev = state.callservt_prev;
865 if isfield(state, 'callresidt_prev')
866 self.callresidt_prev = state.callresidt_prev;
870 if isfield(state, 'results')
871 self.results = state.results;
875 if isfield(state, 'njobs')
876 self.njobs = state.njobs;
878 if isfield(state, 'ilscaling')
879 self.ilscaling = state.ilscaling;
882 % Update layer models with imported state
884 if ~isempty(self.results)
885 it = size(self.results, 1);
887 self.updateLayers(it);
889 % Refresh all layer solvers with new parameters
890 for e = 1:self.nlayers
891 % Ensure layer model struct
is fully built before refresh
892 self.ensemble{e}.getStruct(
false);
893 self.ensemble{e}.refreshChains();
894 % refreshChains can change the chain basis, invalidating the
895 % warm-start solution cached by analyze()
896 self.solvers{e}.options.init_sol = [];
897 if isa(self.solvers{e},
'SolverMVA')
898 self.solvers{e}.resetForkWarmStart();
900 switch self.solvers{e}.name
901 case {
'SolverMVA',
'SolverNC'}
902 self.ensemble{e}.refreshRates();
904 self.ensemble{e}.refreshProcesses();
906 self.solvers{e}.reset();
910 function update_solver(self, solverFactory)
911 % UPDATE_SOLVER Change the solver
for all layers
913 % UPDATE_SOLVER(FACTORY) replaces all layer solvers with
914 %
new solvers created by the given factory function.
916 % This allows switching between different solving methods
917 % (e.g., from MVA to LDES)
while preserving the current
921 % solver = SolverLN(model, @(m) SolverMVA(m));
922 % solver.getEnsembleAvg(); % Fast initial solution
924 % solver.update_solver(@(m) SolverLDES(m,
'samples', 1e5));
925 % solver.getEnsembleAvg(); % Refine with simulation
927 % NOTE: a pure-JMT layer factory (@(m) SolverJMT(m, ...))
is
928 % unsupported: LN server layers carry immediate feedback
929 % (sn.immfeed), which SolverJMT rejects. update_solver raises a
930 % clear error upfront in that case.
932 self.solverFactory = solverFactory;
934 % Replace all layer solvers
935 for e = 1:self.nlayers
936 layerSolver = solverFactory(self.ensemble{e});
937 self.assertLayerSolverSupportsModel(layerSolver, self.ensemble{e}, e);
938 self.setSolver(layerSolver, e);
942 function assertLayerSolverSupportsModel(self, layerSolver, layerModel, e) %#ok<INUSL>
943 % ASSERTLAYERSOLVERSUPPORTSMODEL Reject a layer solver that cannot
944 % represent its layer model, upfront with a clear message.
946 % LN server-layer stations carry immediate feedback (sn.immfeed):
947 % successive same-host activities retain the server, modelled as
948 % immediate-feedback self-loops so the layer solver does not
949 % re-queue the job. SolverJMT rejects any model with immfeed and
950 % returns no solution, so a pure-JMT layer factory otherwise fails
951 % cryptically at layer 1, iteration 1. Detect it here instead.
953 % The guard is CONDITIONAL: it fires only when the specific layer
954 % model actually carries immfeed. A SolverJMT layer solver on an
955 % immfeed-free layer is allowed, and non-JMT factories are never
957 if isa(layerSolver, 'SolverJMT')
958 lsn = layerModel.getStruct();
959 if isfield(lsn,
'immfeed') && ~isempty(lsn.immfeed) && any(lsn.immfeed(:))
960 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);
965 function [allMethods] = listValidMethods(self)
966 sn = self.model.getStruct();
967 % allMethods = LISTVALIDMETHODS()
968 % List valid methods
for this solver
969 allMethods = {
'default',
'moment3'};
974 function options = defaultOptions()
975 % OPTIONS = DEFAULTOPTIONS()
976 options = SolverOptions('LN');
979 function libs = getLibrariesUsed(sn, options)
980 % GETLIBRARIESUSED Get list of external libraries used by LN solver
981 % LN uses internal algorithms, no external library attribution needed
987function solver = adaptiveSolverFactory(model, parentOptions)
988 % ADAPTIVESOLVERFACTORY - Select appropriate solver based on model characteristics
989 % Use JMT for models with open classes, MVA for pure closed networks
993 verbose = parentOptions.verbose;
996 % Create MVA solver with reduced iter_max
for sublayer stability (Python parity)
997 solver = SolverMVA(model,
'verbose', verbose);
998 solver.options.iter_max = 1000; % Cap sublayer MVA iterations