LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
SolverLN.m
1classdef SolverLN < EnsembleSolver
2 % SolverLN Layered network solver for hierarchical performance models
3 %
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.
8 %
9 % @brief Layered network solver for hierarchical software performance models
10 %
11 % Example:
12 % @code
13 % solver = SolverLN(layered_model, 'maxIter', 100);
14 % solver.runAnalyzer(); % Iterative layer analysis
15 % metrics = solver.getEnsembleAvg(); % Layer performance metrics
16 % @endcode
17 %
18 % Copyright (c) 2012-2026, Imperial College London
19 % All rights reserved.
20
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)
74 end
75
76 properties %(Hidden) % performance metrics and related processes
77 util;
78 util_ilock; % interlock matrix (ntask x ntask), element (i,j) says how much the utilization of task i is imputed to task j
79 tput;
80 tputproc;
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
85 thinkt;
86 thinkproc;
87 thinktproc;
88 entryproc;
89 entrycdfrespt;
90 callresidt;
91 callservt;
92 callservtproc;
93 callservtcdf;
94 joint; % join times at AND-Join activities (synchronization delay)
95 ignore; % elements to be ignored (e.g., components disconnected from a REF node)
96 end
97
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
107 end
108
109 methods
110 function self = SolverLN(lqnmodel, solverFactory, varargin)
111 % SELF = SOLVERLN(MODEL,SOLVERFACTORY,VARARGIN)
112 self@EnsembleSolver(lqnmodel, mfilename);
113
114 % Collect all trailing args (solverFactory may itself be the lang
115 % string or an options struct) to detect lang='python'/'java'.
116 allArgs = varargin;
117 if nargin > 1
118 allArgs = [{solverFactory}, varargin];
119 end
120 wantsPython = any(cellfun(@(s) (ischar(s) && strcmpi(s,'python')) || ...
121 (isstruct(s) && isfield(s,'lang') && strcmpi(s.lang,'python')), allArgs));
122
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();
131 elseif wantsPython
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.
135 self.pyMode = true;
136 if nargin > 1 && isstruct(solverFactory)
137 self.setOptions(solverFactory);
138 else
139 self.setOptions(SolverLN.defaultOptions);
140 end
141 self.options.lang = 'python';
142 self.lqn = lqnmodel.getStruct();
143 else
144 % Default solver factory: Use JMT for open networks, MVA for closed networks
145 defaultSolverFactory = @(m) adaptiveSolverFactory(m, self.options);
146
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',...)
159 inputvar = varargin;
160 end
161 self.setOptions(Solver.parseOptions(inputvar, SolverLN.defaultOptions));
162 else %case SolverLN(model,solverFactory)
163 self.setOptions(SolverLN.defaultOptions);
164 end
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);
170
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);
179 else
180 self.hasPhase2 = false;
181 end
182
183 self.construct();
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');
193 else
194 layerFactory = solverFactory;
195 end
196 layerSolver = layerFactory(self.ensemble{e});
197 self.assertLayerSolverSupportsModel(layerSolver, self.ensemble{e}, e);
198 self.setSolver(layerSolver,e);
199 end
200 self.solverFactory = solverFactory; % Store for later use
201 end
202 end
203
204 function runtime = runAnalyzer(self, options) %#ok<INUSD> % generic method to run the solver
205 line_error(mfilename,'Use getEnsembleAvg instead.');
206 end
207
208 function sn = getStruct(self)
209 % SN = GETSTRUCT()
210
211 % Get data structure summarizing the model
212 sn = self.model.getStruct();
213 end
214
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);
220 if length(uwccs)>1
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;
227 end
228 end
229 if any(wccref==false)
230 for dw=find(wccref==false) % disconnected component
231 self.ignore(find(wccs==dw)) = true;
232 end
233 end
234 end
235
236 % initialize internal data structures
237 self.entrycdfrespt = cell(length(self.lqn.nentries),1);
238 self.hasconverged = false;
239
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)};
246 end
247
248 % perform layering
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);
255
256 % interlock data structures are built in init() via initInterlock()
257
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)))');
262 end
263
264 function self = reset(self)
265 % no-op
266 end
267
268 bool = converged(self, it); % convergence test at iteration it
269 bool = convergedStoch(self, it); % convergence test for stochastic layer solvers (Robbins-Monro mode)
270
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);
279
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;
286 end
287
288 % Initialize under-relaxation state
289 relax_mode = self.options.config.relax;
290 switch relax_mode
291 case {'auto'}
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
297 end
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);
308
309 % Build interlock tables (LQNS V5 static analysis)
310 if self.options.config.interlocking
311 self.initInterlock();
312 end
313
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);
317
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
323 % converged result.
324 initMode = 'none';
325 if isfield(self.options.config, 'layer_init') && ~isempty(self.options.config.layer_init)
326 initMode = self.options.config.layer_init;
327 end
328 if any(strcmpi(initMode, {'bound','boxbound','mwba'}))
329 try
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
334 x = sqrt(u*l);
335 elseif isfinite(u)
336 x = u;
337 elseif isfinite(l)
338 x = l;
339 else
340 x = 0;
341 end
342 if x > 0
343 self.tput(idx) = x;
344 self.tputproc{idx} = Exp.fitRate(x);
345 end
346 end
347 line_debug('LN init: throughputs initialized from robust box bounds');
348 catch ME
349 line_debug('LN box-bound initialization skipped: %s', ME.message);
350 end
351 end
352
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();
365 end
366 mode = self.options.config.stochiter;
367 self.stochiter_auto = strcmpi(mode, 'auto');
368 if self.stochiter_auto
369 if any(self.stochlayers)
370 mode = 'rm';
371 else
372 mode = 'off';
373 end
374 end
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));
383 end
384
385
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)
390 return
391 end
392 switch self.stochiter_mode
393 case 'rm'
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;
399 end
400 case 'crn'
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;
409 end
410 end
411 end
412
413 function [result, runtime] = analyze(self, it, e)
414 % [RESULT, RUNTIME] = ANALYZE(IT, E)
415 T0 = tic;
416 line_debug('LN analyze: iteration %d, layer %d (%s)', it, e, class(self.solvers{e}));
417 result = struct();
418 %jresult = struct();
419 if e==1 && self.solvers{e}.options.verbose
420 line_printf('\n');
421 end
422
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
426 try
427 [result.QN, result.UN, result.RN, result.TN, result.AN, result.WN] = self.solvers{e}.getAvg();
428 catch ME
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.', ...
433 e, it);
434 end
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;
443 else
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', ...
447 e, it, ME.message);
448 end
449 end
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();
457 end
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);
466 QNe = result.QN;
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);
472 end
473 self.solvers{e}.options.init_sol = Qch;
474 end
475 end
476 runtime = toc(T0);
477 end
478
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
483
484 self.updateMetrics(it);
485
486 if self.options.config.interlocking
487 % apply interlock correction to call residence times
488 self.updatePopulations(it);
489 end
490
491 % recompute think times
492 self.updateThinkTimes(it);
493
494 % update the model parameters
495 self.updateLayers(it);
496
497 % update entry selection and cache routing probabilities within callers
498 self.updateRoutingProbabilities(it);
499
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();
512 end
513
514 % refresh visits and network model parameters
515 for e= self.svcreset
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
526 case 'moment3'
527 self.ensemble{e}.refreshProcesses();
528 otherwise
529 self.ensemble{e}.refreshRates();
530 end
531 otherwise
532 self.ensemble{e}.refreshProcesses();
533 end
534 self.solvers{e}.reset(); % commenting this out des not seem to produce a problem, but it goes faster with it
535 end
536
537 % Note: interlock correction is done via callresidt adjustment
538 % in updatePopulations, no population changes needed
539
540 if it==1
541 % now disable all solver support checks for future iterations
542 for e=1:length(self.ensemble)
543 self.solvers{e}.setChecks(false);
544 end
545 end
546 end
547
548
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
556 for e = 1:E
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});
560 end
561 end
562 self.servt = self.stoch_servt_avg;
563 self.residt = self.stoch_residt_avg;
564 end
565 for e=1:E
566 s = self.solvers{e};
567 s.getAvg();
568 self.solvers{e} = s;
569 end
570 self.model.ensemble = self.ensemble;
571 end
572
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.
576 %
577 % options.config.ln_transient selects the inter-layer coupling of
578 % the transient:
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;
593 end
594 switch lower(mode)
595 case 'coupled'
596 [QNlqn_t, UNlqn_t, TNlqn_t] = self.getTranAvgCoupled(Qt, Ut, Tt);
597 case 'decoupled'
598 [QNlqn_t, UNlqn_t, TNlqn_t] = self.getTranAvgDecoupled(Qt, Ut, Tt);
599 otherwise
600 line_error(mfilename, sprintf('Unknown ln_transient mode ''%s'' (use ''coupled'' or ''decoupled'').', mode));
601 end
602 end
603
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>
609 self.getAvg;
610 QNclass_t = {};
611 UNclass_t = {};
612 TNclass_t = {};
613 QNlqn_t = cell(0,0);
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));
620 for e=1:self.nlayers
621 [crows, ccols] = size(QNlqn_t);
622 s = self.solvers{e};
623 if hasTs
624 savedTs = s.options.timespan;
625 s.options.timespan = self.options.timespan;
626 end
627 [QNclass_t{e}, UNclass_t{e}, TNclass_t{e}] = s.getTranAvg();
628 if hasTs
629 s.options.timespan = savedTs;
630 end
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};
634 end
635 end
636
637 function varargout = getAvg(varargin)
638 % [QN,UN,RN,TN,AN,WN] = GETAVG(SELF,~,~,~,~,USELQNSNAMING)
639 [varargout{1:nargout}] = getEnsembleAvg( varargin{:} );
640 end
641
642 function [cdfRespT] = getCdfRespT(self)
643 if isempty(self.entrycdfrespt{1})
644 % save user-specified method to temporary variable
645 curMethod = self.getOptions.method;
646 % run with moment 3
647 self.options.method = 'moment3';
648 self.getAvg();
649 % restore user-specified method
650 self.options.method = curMethod;
651 end
652 cdfRespT = self.entrycdfrespt;
653 end
654
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([]);
659 return
660 end
661
662 boundMethod = '';
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;
666 end
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;
673 else
674 TN = bnd.TN_lo; UN = bnd.UN_lo;
675 end
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));
684 else
685 [QN,UN,RN,TN,AN,WN] = getAvg(self);
686 end
687
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;
695 end
696 [QN, UN, RN, TN, AN, WN] = deal(variables{:}); % Assign the modified values back to the original variables
697
698 %%
699 Node = label(self.lqn.names);
700 O = length(Node);
701 NodeType = label(O,1);
702 for o = 1:O
703 switch self.lqn.type(o)
704 case LayeredNetworkElement.PROCESSOR
705 NodeType(o,1) = label({'Processor'});
706 case LayeredNetworkElement.TASK
707 if self.lqn.isref(o)
708 NodeType(o,1) = label({'RefTask'});
709 else
710 NodeType(o,1) = label({'Task'});
711 end
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'});
718 end
719 end
720 QLen = QN;
721 QT = Table(Node,QLen);
722 Util = UN;
723 UT = Table(Node,Util);
724 RespT = RN;
725 RT = Table(Node,RespT);
726 Tput = TN;
727 TT = Table(Node,Tput);
728 %SvcT = SN;
729 %ST = Table(Node,SvcT);
730 %ProcUtil = PN;
731 %PT = Table(Node,ProcUtil);
732 ResidT = WN;
733 WT = Table(Node,ResidT);
734 ArvR = AN;
735 AT = Table(Node,ArvR);
736 AvgTable = Table(Node, NodeType, QLen, Util, RespT, ResidT, ArvR, Tput);%, ProcUtil, SvcT);
737 end
738
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();
742 end
743
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();
747 end
748
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();
752 end
753 end
754
755 methods
756 [QN,UN,RN,TN,AN,WN] = getEnsembleAvg(self);
757 [QNlqn_t, UNlqn_t, TNlqn_t] = getTranAvgCoupled(self, Qt, Ut, Tt);
758
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);
764 bool = true;
765 for e = 1:length(ensemble)
766 [solverSupports,featSupported{e}] = self.solvers{e}.supports(ensemble{e});
767 bool = bool && solverSupports;
768 end
769 end
770 end
771
772 methods (Hidden)
773 buildLayers(self, lqn, resptproc, callservtproc);
774 buildLayersRecursive(self, idx, callers, ishostlayer);
775 initInterlock(self);
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
783 end
784
785 methods
786 function state = get_state(self)
787 % GET_STATE Export current solver state for continuation
788 %
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().
792 %
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.)
799 % - Relaxation state
800 % - Last iteration results
801 %
802 % Example:
803 % solver1 = SolverLN(model, @(m) SolverMVA(m));
804 % solver1.getEnsembleAvg();
805 % state = solver1.get_state();
806 %
807 % solver2 = SolverLN(model, @(m) SolverNC(m));
808 % solver2.set_state(state);
809 % solver2.getEnsembleAvg(); % Continues from MVA solution
810 %
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.
815
816 state = struct();
817
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;
824
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;
833
834 % Relaxation state
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;
842
843 % Results from last iteration
844 state.results = self.results;
845
846 % Interlock data
847 state.njobs = self.njobs;
848 state.ilscaling = self.ilscaling;
849 end
850
851 function set_state(self, state)
852 % SET_STATE Import solution state for continuation
853 %
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.
857 %
858 % This enables hybrid solving schemes where fast solvers (MVA)
859 % provide initial estimates and accurate solvers (JMT, LDES)
860 % refine the solution.
861 %
862 % Example:
863 % solver1 = SolverLN(model, @(m) SolverMVA(m));
864 % solver1.getEnsembleAvg();
865 % state = solver1.get_state();
866 %
867 % solver2 = SolverLN(model, @(m) SolverNC(m));
868 % solver2.set_state(state);
869 % solver2.getEnsembleAvg(); % Continues from MVA solution
870 %
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.
875
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;
883 end
884
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;
891 end
892 if isfield(state, 'thinkt')
893 self.thinkt = state.thinkt;
894 end
895 if isfield(state, 'callresidt')
896 self.callresidt = state.callresidt;
897 end
898 if isfield(state, 'callservt')
899 self.callservt = state.callservt;
900 end
901
902 % Relaxation state
903 if isfield(state, 'relax_omega')
904 self.relax_omega = state.relax_omega;
905 end
906 if isfield(state, 'servt_prev')
907 self.servt_prev = state.servt_prev;
908 end
909 if isfield(state, 'residt_prev')
910 self.residt_prev = state.residt_prev;
911 end
912 if isfield(state, 'tput_prev')
913 self.tput_prev = state.tput_prev;
914 end
915 if isfield(state, 'thinkt_prev')
916 self.thinkt_prev = state.thinkt_prev;
917 end
918 if isfield(state, 'callservt_prev')
919 self.callservt_prev = state.callservt_prev;
920 end
921 if isfield(state, 'callresidt_prev')
922 self.callresidt_prev = state.callresidt_prev;
923 end
924
925 % Results
926 if isfield(state, 'results')
927 self.results = state.results;
928 end
929
930 % Interlock data
931 if isfield(state, 'njobs')
932 self.njobs = state.njobs;
933 end
934 if isfield(state, 'ilscaling')
935 self.ilscaling = state.ilscaling;
936 end
937
938 % Update layer models with imported state
939 it = 1;
940 if ~isempty(self.results)
941 it = size(self.results, 1);
942 end
943 self.updateLayers(it);
944
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();
955 end
956 switch self.solvers{e}.name
957 case {'SolverMVA', 'SolverNC'}
958 self.ensemble{e}.refreshRates();
959 otherwise
960 self.ensemble{e}.refreshProcesses();
961 end
962 self.solvers{e}.reset();
963 end
964 end
965
966 function update_solver(self, solverFactory)
967 % UPDATE_SOLVER Change the solver for all layers
968 %
969 % UPDATE_SOLVER(FACTORY) replaces all layer solvers with
970 % new solvers created by the given factory function.
971 %
972 % This allows switching between different solving methods
973 % (e.g., from MVA to LDES) while preserving the current
974 % solution state.
975 %
976 % Example:
977 % solver = SolverLN(model, @(m) SolverMVA(m));
978 % solver.getEnsembleAvg(); % Fast initial solution
979 %
980 % solver.update_solver(@(m) SolverLDES(m, 'samples', 1e5));
981 % solver.getEnsembleAvg(); % Refine with simulation
982 %
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.
987
988 self.solverFactory = solverFactory;
989
990 % Replace all layer solvers
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);
995 end
996 end
997
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.
1001 %
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.
1008 %
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
1012 % rejected.
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);
1017 end
1018 end
1019 end
1020
1021 function [allMethods] = listValidMethods(self)
1022 sn = self.model.getStruct();
1023 % allMethods = LISTVALIDMETHODS()
1024 % List valid methods for this solver
1025 allMethods = {'default','moment3'};
1026 end
1027 end
1028
1029 methods (Static)
1030 function options = defaultOptions()
1031 % OPTIONS = DEFAULTOPTIONS()
1032 options = SolverOptions('LN');
1033 end
1034
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
1038 libs = {};
1039 end
1040 end
1041end
1042
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
1046 if nargin < 2
1047 verbose = false;
1048 else
1049 verbose = parentOptions.verbose;
1050 end
1051
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
1055end
Definition Station.m:245