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 ptaskcallers; % probability that a task is called by a given task, directly or indirectly (remotely)
29 ptaskcallers_step; % probability that a task is called by a given task, directly or indirectly (remotely) up to a given step distance
30 ilscaling; % interlock scalings
31 njobs; % number of jobs for each caller in a given submodel
32 njobsorig; % number of jobs for each caller at layer build time
33 routereset; % models that require hard reset of service chains
34 svcreset; % models that require hard reset of service process
35 maxitererr; % maximum error at current iteration over all layers
36 % Under-relaxation state for convergence improvement
37 relax_omega; % Current relaxation factor
38 relax_err_history; % Error history for adaptive mode
39 servt_prev; % Previous service times for relaxation
40 residt_prev; % Previous residence times for relaxation
41 tput_prev; % Previous throughputs for relaxation
42 thinkt_prev; % Previous think times for relaxation
43 callservt_prev; % Previous call service times for relaxation
44 % MOL (Method of Layers) properties for hierarchical iteration
45 hostLayerIndices; % Indices of host (processor) layers in ensemble
46 taskLayerIndices; % Indices of task layers in ensemble
47 util_prev_host; % Previous processor utilizations (for delta computation)
48 util_prev_task; % Previous task utilizations (for delta computation)
49 mol_it_host_outer; % MOL host outer iteration counter
50 mol_it_task_inner; % MOL task inner iteration counter
51 % Phase-2 support properties
52 hasPhase2; % Flag: model has phase-2 activities
53 servt_ph1; % Phase-1 service time per activity (nidx x 1)
54 servt_ph2; % Phase-2 service time per activity (nidx x 1)
55 util_ph1; % Phase-1 utilization per entry
56 util_ph2; % Phase-2 utilization per entry
57 prOvertake; % Overtaking probability per entry (nentries x 1)
58 end
59
60 properties %(Hidden) % performance metrics and related processes
61 util;
62 util_ilock; % interlock matrix (ntask x ntask), element (i,j) says how much the utilization of task i is imputed to task j
63 tput;
64 tputproc;
65 servt; % this is the mean service time of an activity, which is the response time at the lower layer (if applicable)
66 residt; % this is the residence time at the lower layer (if applicable)
67 servtproc; % this is the service time process with mean fitted to the servt value
68 servtcdf; % this is the cdf of the service time process
69 thinkt;
70 thinkproc;
71 thinktproc;
72 entryproc;
73 entrycdfrespt;
74 callresidt;
75 callservt;
76 callservtproc;
77 callservtcdf;
78 ignore; % elements to be ignored (e.g., components disconnected from a REF node)
79 end
80
81 properties %(Access = protected, Hidden) % registries of quantities to update at every iteration
82 arvproc_classes_updmap; % [modelidx, actidx, node, class]
83 thinkt_classes_updmap; % [modelidx, actidx, node, class]
84 actthinkt_classes_updmap; % [modelidx, actidx, node, class] for activity think-times
85 servt_classes_updmap; % [modelidx, actidx, node, class]
86 call_classes_updmap; % [modelidx, callidx, node, class]
87 route_prob_updmap; % [modelidx, actidxfrom, actidxto, nodefrom, nodeto, classfrom, classto]
88 unique_route_prob_updmap; % auxiliary cache of unique route_prob_updmap rows
89 solverFactory; % function handle to create layer solvers
90 end
91
92 methods
93 function self = SolverLN(lqnmodel, solverFactory, varargin)
94 % SELF = SOLVERLN(MODEL,SOLVERFACTORY,VARARGIN)
95 self@EnsembleSolver(lqnmodel, mfilename);
96
97 if any(cellfun(@(s) strcmpi(s,'java'), varargin))
98 self.obj = JLINE.SolverLN(JLINE.from_line_layered_network(lqnmodel));
99 self.obj.options.verbose = jline.VerboseLevel.SILENT;
100 else
101 % Default solver factory: Use JMT for open networks, MVA for closed networks
102 defaultSolverFactory = @(m) adaptiveSolverFactory(m, self.options);
103
104 if nargin == 1 %case SolverLN(model)
105 solverFactory = defaultSolverFactory;
106 self.setOptions(SolverLN.defaultOptions);
107 elseif nargin>1 && isstruct(solverFactory)
108 options = solverFactory;
109 self.setOptions(options);
110 solverFactory = defaultSolverFactory;
111 elseif nargin>2 % case SolverLN(model,'opt1',...)
112 if ischar(solverFactory)
113 inputvar = {solverFactory,varargin{:}}; %#ok<CCAT>
114 solverFactory = defaultSolverFactory;
115 else % case SolverLN(model, solverFactory, 'opt1',...)
116 inputvar = varargin;
117 end
118 self.setOptions(Solver.parseOptions(inputvar, SolverLN.defaultOptions));
119 else %case SolverLN(model,solverFactory)
120 self.setOptions(SolverLN.defaultOptions);
121 end
122 self.lqn = lqnmodel.getStruct();
123
124 % Detect and initialize phase-2 support
125 if isfield(self.lqn, 'actphase') && any(self.lqn.actphase > 1)
126 self.hasPhase2 = true;
127 self.servt_ph1 = zeros(self.lqn.nidx, 1);
128 self.servt_ph2 = zeros(self.lqn.nidx, 1);
129 self.util_ph1 = zeros(self.lqn.nidx, 1);
130 self.util_ph2 = zeros(self.lqn.nidx, 1);
131 self.prOvertake = zeros(self.lqn.nentries, 1);
132 else
133 self.hasPhase2 = false;
134 end
135
136 self.construct();
137 for e=1:self.getNumberOfModels
138 if numel(find(self.lqn.isfunction == 1))
139 if ~isempty(self.ensemble{e}.stations{2}.setupTime)
140 solverFactory = @(m) SolverMAM(m,'verbose',false,'method','dec.poisson');
141 else
142 solverFactory = @(m) SolverAuto(m,'verbose',false);
143 end
144 self.setSolver(solverFactory(self.ensemble{e}),e);
145 else
146 self.setSolver(solverFactory(self.ensemble{e}),e);
147 end
148 end
149 self.solverFactory = solverFactory; % Store for later use
150 end
151 end
152
153 function runtime = runAnalyzer(self, options) %#ok<INUSD> % generic method to run the solver
154 line_error(mfilename,'Use getEnsembleAvg instead.');
155 end
156
157 function sn = getStruct(self)
158 % SN = GETSTRUCT()
159
160 % Get data structure summarizing the model
161 sn = self.model.getStruct();
162 end
163
164 function construct(self)
165 % mark down to ignore unreachable disconnected components
166 self.ignore = false(self.lqn.nidx,1);
167 [~,wccs] = weaklyconncomp(self.lqn.graph'+self.lqn.graph);
168 uwccs = unique(wccs);
169 if length(uwccs)>1
170 % the model has disconnected submodels
171 wccref = false(1,length(uwccs));
172 for t=1:self.lqn.ntasks
173 tidx = self.lqn.tshift+t;
174 if self.lqn.sched(tidx) == SchedStrategy.REF
175 wccref(wccs(tidx)) = true;
176 end
177 end
178 if any(wccref==false)
179 for dw=find(wccref==false) % disconnected component
180 self.ignore(find(wccs==dw)) = true;
181 end
182 end
183 end
184
185 % initialize internal data structures
186 self.entrycdfrespt = cell(length(self.lqn.nentries),1);
187 self.hasconverged = false;
188
189 % initialize svc and think times
190 self.servtproc = self.lqn.hostdem;
191 self.thinkproc = self.lqn.think;
192 self.callservtproc = cell(self.lqn.ncalls,1);
193 for cidx = 1:self.lqn.ncalls
194 self.callservtproc{cidx} = self.lqn.hostdem{self.lqn.callpair(cidx,2)};
195 end
196
197 % perform layering
198 self.njobs = zeros(self.lqn.tshift + self.lqn.ntasks, self.lqn.tshift + self.lqn.ntasks);
199 buildLayers(self); % build layers
200 self.njobsorig = self.njobs;
201 self.nlayers = length(self.ensemble);
202
203 % initialize data structures for interlock correction
204 self.ptaskcallers = zeros(self.lqn.nhosts+self.lqn.ntasks, self.lqn.nhosts+self.lqn.ntasks);
205 self.ptaskcallers_step = cell(1,self.nlayers+1);
206 for step=1:self.nlayers % upper bound on maximum dag height
207 self.ptaskcallers_step{step} = zeros(self.lqn.nhosts+self.lqn.ntasks, self.lqn.nhosts+self.lqn.ntasks);
208 end
209
210 % layering generates update maps that we use here to cache the elements that need reset
211 self.routereset = unique(self.idxhash(self.route_prob_updmap(:,1)))';
212 self.svcreset = unique(self.idxhash(self.thinkt_classes_updmap(:,1)))';
213 self.svcreset = union(self.svcreset,unique(self.idxhash(self.call_classes_updmap(:,1)))');
214 end
215
216 function self = reset(self)
217 % no-op
218 end
219
220 bool = converged(self, it); % convergence test at iteration it
221
222 function init(self) % operations before starting to iterate
223 % INIT() % OPERATIONS BEFORE STARTING TO ITERATE
224 self.unique_route_prob_updmap = unique(self.route_prob_updmap(:,1))';
225 self.tput = zeros(self.lqn.nidx,1);
226 self.tputproc = cell(self.lqn.nidx,1);
227 self.util = zeros(self.lqn.nidx,1);
228 self.servt = zeros(self.lqn.nidx,1);
229 self.servtmatrix = getEntryServiceMatrix(self);
230
231 for e= 1:self.nlayers
232 self.solvers{e}.enableChecks=false;
233 end
234
235 % Initialize under-relaxation state
236 relax_mode = self.options.config.relax;
237 switch relax_mode
238 case {'auto'}
239 self.relax_omega = 1.0; % Start without relaxation
240 case {'fixed', 'adaptive'}
241 self.relax_omega = self.options.config.relax_factor;
242 otherwise % 'none' or unrecognized
243 self.relax_omega = 1.0; % No relaxation
244 end
245 self.relax_err_history = [];
246 self.servt_prev = NaN(self.lqn.nidx, 1);
247 self.residt_prev = NaN(self.lqn.nidx, 1);
248 self.tput_prev = NaN(self.lqn.nidx, 1);
249 self.thinkt_prev = NaN(self.lqn.nidx, 1);
250 self.thinkt = zeros(self.lqn.nidx, 1); % Initialize to zeros (Python parity)
251 self.callservt_prev = NaN(self.lqn.ncalls, 1);
252
253 % Initialize MOL-specific state
254 self.mol_it_host_outer = 0;
255 self.mol_it_task_inner = 0;
256 self.util_prev_host = zeros(self.lqn.nhosts, 1);
257 self.util_prev_task = zeros(self.lqn.ntasks, 1);
258 end
259
260
261 function pre(self, it) %#ok<INUSD> % operations before an iteration
262 % PRE(IT) % OPERATIONS BEFORE AN ITERATION
263 % no-op
264 end
265
266 function [result, runtime] = analyze(self, it, e) %#ok<INUSD>
267 % [RESULT, RUNTIME] = ANALYZE(IT, E)
268 T0 = tic;
269 result = struct();
270 %jresult = struct();
271 if e==1 && self.solvers{e}.options.verbose
272 line_printf('\n');
273 end
274
275 % Protection for unstable queues during LN iterations
276 % If a solver fails (e.g., due to queue instability with open arrivals),
277 % use results from previous iteration if available and continue
278 try
279 [result.QN, result.UN, result.RN, result.TN, result.AN, result.WN] = self.solvers{e}.getAvg();
280 catch ME
281 if it > 1 && ~isempty(self.results) && size(self.results, 1) >= (it-1) && size(self.results, 2) >= e
282 if self.solvers{e}.options.verbose
283 warning('LINE:SolverLN:Instability', ...
284 'Layer %d at iteration %d encountered instability (possibly due to high service demand with open arrivals). Using previous iteration values and continuing.', ...
285 e, it);
286 end
287 % Use results from previous iteration
288 prevResult = self.results{it-1, e};
289 result.QN = prevResult.QN;
290 result.UN = prevResult.UN;
291 result.RN = prevResult.RN;
292 result.TN = prevResult.TN;
293 result.AN = prevResult.AN;
294 result.WN = prevResult.WN;
295 else
296 % First iteration or no previous results, re-throw the exception
297 error('LINE:SolverLN:FirstIterationFailure', ...
298 'Layer %d failed at iteration %d with no previous iteration to fall back on: %s', ...
299 e, it, ME.message);
300 end
301 end
302 runtime = toc(T0);
303 end
304
305 function post(self, it) % operations after an iteration
306 % POST(IT) % OPERATIONS AFTER AN ITERATION
307 % convert the results of QNs into layer metrics
308
309 self.updateMetrics(it);
310
311 % recompute think times
312 self.updateThinkTimes(it);
313
314 if self.options.config.interlocking
315 % recompute layer populations
316 self.updatePopulations(it);
317 end
318
319 % update the model parameters
320 self.updateLayers(it);
321
322 % update entry selection and cache routing probabilities within callers
323 self.updateRoutingProbabilities(it);
324
325 % reset all layers with routing probability changes
326 for e= self.routereset
327 self.ensemble{e}.refreshChains();
328 self.solvers{e}.reset();
329 end
330
331 % refresh visits and network model parameters
332 for e= self.svcreset
333 switch self.solvers{e}.name
334 case {'SolverMVA', 'SolverNC'} %leaner than refreshProcesses, no need to refresh phases
335 % note: this does not refresh the sn.proc field, only sn.rates and sn.scv
336 switch self.options.method
337 case 'default'
338 self.ensemble{e}.refreshRates();
339 case 'moment3'
340 self.ensemble{e}.refreshProcesses();
341 end
342 otherwise
343 self.ensemble{e}.refreshProcesses();
344 end
345 self.solvers{e}.reset(); % commenting this out des not seem to produce a problem, but it goes faster with it
346 end
347
348 % this is required to handle population changes due to interlocking
349 if self.options.config.interlocking
350 for e=1:self.nlayers
351 self.ensemble{e}.refreshJobs();
352 end
353 end
354
355 if it==1
356 % now disable all solver support checks for future iterations
357 for e=1:length(self.ensemble)
358 self.solvers{e}.setChecks(false);
359 end
360 end
361 end
362
363
364 function finish(self) % operations after iterations are completed
365 % FINISH() % OPERATIONS AFTER INTERATIONS ARE COMPLETED
366 E = size(self.results,2);
367 for e=1:E
368 s = self.solvers{e};
369 s.getAvg();
370 self.solvers{e} = s;
371 end
372 self.model.ensemble = self.ensemble;
373 end
374
375 function [QNlqn_t, UNlqn_t, TNlqn_t] = getTranAvg(self)
376 self.getAvg;
377 QNclass_t = {};
378 UNclass_t = {};
379 TNclass_t = {};
380 QNlqn_t = cell(0,0);
381 for e=1:self.nlayers
382 [crows, ccols] = size(QNlqn_t);
383 [QNclass_t{e}, UNclass_t{e}, TNclass_t{e}] = self.solvers{e}.getTranAvg();
384 QNlqn_t(crows+1:crows+size(QNclass_t{e},1),ccols+1:ccols+size(QNclass_t{e},2)) = QNclass_t{e};
385 UNlqn_t(crows+1:crows+size(UNclass_t{e},1),ccols+1:ccols+size(UNclass_t{e},2)) = UNclass_t{e};
386 TNlqn_t(crows+1:crows+size(TNclass_t{e},1),ccols+1:ccols+size(TNclass_t{e},2)) = TNclass_t{e};
387 end
388 end
389
390 function varargout = getAvg(varargin)
391 % [QN,UN,RN,TN,AN,WN] = GETAVG(SELF,~,~,~,~,USELQNSNAMING)
392 [varargout{1:nargout}] = getEnsembleAvg( varargin{:} );
393 end
394
395 function [cdfRespT] = getCdfRespT(self)
396 if isempty(self.entrycdfrespt{1})
397 % save user-specified method to temporary variable
398 curMethod = self.getOptions.method;
399 % run with moment 3
400 self.options.method = 'moment3';
401 self.getAvg();
402 % restore user-specified method
403 self.options.method = curMethod;
404 end
405 cdfRespT = self.entrycdfrespt;
406 end
407
408 function [AvgTable,QT,UT,RT,WT,AT,TT] = getAvgTable(self)
409 % [AVGTABLE,QT,UT,RT,WT,TT] = GETAVGTABLE(USELQNSNAMING)
410 if (GlobalConstants.DummyMode)
411 [AvgTable, QT, UT, RT, TT, WT] = deal([]);
412 return
413 end
414
415 if ~isempty(self.obj)
416 avgTable = self.obj.getEnsembleAvg();
417 [QN,UN,RN,WN,AN,TN] = JLINE.arrayListToResults(avgTable);
418 else
419 [QN,UN,RN,TN,AN,WN] = getAvg(self);
420 end
421
422 % attempt to sanitize small numerical perturbations
423 variables = {QN, UN, RN, TN, AN, WN}; % Put all variables in a cell array
424 for i = 1:length(variables)
425 rVar = round(variables{i} * 10);
426 toRound = abs(variables{i} * 10 - rVar) < GlobalConstants.CoarseTol * variables{i} * 10;
427 variables{i}(toRound) = rVar(toRound) / 10;
428 variables{i}(variables{i}<=GlobalConstants.FineTol) = 0;
429 end
430 [QN, UN, RN, TN, AN, WN] = deal(variables{:}); % Assign the modified values back to the original variables
431
432 %%
433 Node = label(self.lqn.names);
434 O = length(Node);
435 NodeType = label(O,1);
436 for o = 1:O
437 switch self.lqn.type(o)
438 case LayeredNetworkElement.PROCESSOR
439 NodeType(o,1) = label({'Processor'});
440 case LayeredNetworkElement.TASK
441 if self.lqn.isref(o)
442 NodeType(o,1) = label({'RefTask'});
443 else
444 NodeType(o,1) = label({'Task'});
445 end
446 case LayeredNetworkElement.ENTRY
447 NodeType(o,1) = label({'Entry'});
448 case LayeredNetworkElement.ACTIVITY
449 NodeType(o,1) = label({'Activity'});
450 case LayeredNetworkElement.CALL
451 NodeType(o,1) = label({'Call'});
452 end
453 end
454 QLen = QN;
455 QT = Table(Node,QLen);
456 Util = UN;
457 UT = Table(Node,Util);
458 RespT = RN;
459 RT = Table(Node,RespT);
460 Tput = TN;
461 TT = Table(Node,Tput);
462 %SvcT = SN;
463 %ST = Table(Node,SvcT);
464 %ProcUtil = PN;
465 %PT = Table(Node,ProcUtil);
466 ResidT = WN;
467 WT = Table(Node,ResidT);
468 ArvR = AN;
469 AT = Table(Node,ArvR);
470 AvgTable = Table(Node, NodeType, QLen, Util, RespT, ResidT, ArvR, Tput);%, ProcUtil, SvcT);
471 end
472
473 function [AvgTable,QT,UT,RT,WT,AT,TT] = avgTable(self)
474 % AVGTABLE Alias for getAvgTable
475 [AvgTable,QT,UT,RT,WT,AT,TT] = self.getAvgTable();
476 end
477
478 function [AvgTable,QT,UT,RT,WT,AT,TT] = avgT(self)
479 % AVGT Short alias for getAvgTable
480 [AvgTable,QT,UT,RT,WT,AT,TT] = self.getAvgTable();
481 end
482
483 function [AvgTable,QT,UT,RT,WT,AT,TT] = aT(self)
484 % AT Short alias for getAvgTable (MATLAB-compatible)
485 [AvgTable,QT,UT,RT,WT,AT,TT] = self.getAvgTable();
486 end
487 end
488
489 methods
490 [QN,UN,RN,TN,AN,WN] = getEnsembleAvg(self);
491
492 function [bool, featSupported] = supports(model)
493 % [BOOL, FEATSUPPORTED] = SUPPORTS(MODEL)
494 % This method cannot be static as otherwise it cannot access self.solvers{e}
495 ensemble = model.getEnsemble;
496 featSupported = cell(length(ensemble),1);
497 bool = true;
498 for e = 1:length(ensemble)
499 [solverSupports,featSupported{e}] = self.solvers{e}.supports(ensemble{e});
500 bool = bool && solverSupports;
501 end
502 end
503 end
504
505 methods (Hidden)
506 buildLayers(self, lqn, resptproc, callservtproc);
507 buildLayersRecursive(self, idx, callers, ishostlayer);
508 updateLayers(self, it);
509 updatePopulations(self, it);
510 updateThinkTimes(self, it);
511 updateMetrics(self, it);
512 updateRoutingProbabilities(self, it);
513 svcmatrix = getEntryServiceMatrix(self)
514 prOt = overtake_prob(self, eidx); % Phase-2 overtaking probability
515
516 function delta = computeTaskDelta(self)
517 % COMPUTETASKDELTA Compute max queue-length change for task layers (MOL inner loop)
518 %
519 % Returns the maximum queue-length change across task layers,
520 % normalized by total jobs, similar to converged.m logic.
521 results = self.results;
522 it = size(results, 1);
523 if it < 2
524 delta = Inf;
525 return;
526 end
527
528 delta = 0;
529 for e = self.taskLayerIndices
530 metric = results{it, e}.QN;
531 metric_1 = results{it-1, e}.QN;
532 N = sum(self.ensemble{e}.getNumberOfJobs);
533 if N > 0
534 try
535 iterErr = max(abs(metric(:) - metric_1(:))) / N;
536 catch
537 iterErr = 0;
538 end
539 delta = max(delta, iterErr);
540 end
541 end
542 end
543
544 function delta = computeHostDelta(self)
545 % COMPUTEHOSTDELTA Compute max queue-length change for host layers (MOL outer loop)
546 %
547 % Returns the maximum queue-length change across host layers,
548 % normalized by total jobs, similar to converged.m logic.
549 results = self.results;
550 it = size(results, 1);
551 if it < 2
552 delta = Inf;
553 return;
554 end
555
556 delta = 0;
557 for e = self.hostLayerIndices
558 metric = results{it, e}.QN;
559 metric_1 = results{it-1, e}.QN;
560 N = sum(self.ensemble{e}.getNumberOfJobs);
561 if N > 0
562 try
563 iterErr = max(abs(metric(:) - metric_1(:))) / N;
564 catch
565 iterErr = 0;
566 end
567 delta = max(delta, iterErr);
568 end
569 end
570 end
571
572 function postTaskLayers(self, it)
573 % POSTTASKLAYERS Post-iteration updates for task layers only (MOL inner loop)
574 %
575 % Updates think times, layer parameters, and routing probabilities
576 % after task layer analysis, then resets task layer solvers.
577
578 self.updateThinkTimes(it);
579 if self.options.config.interlocking
580 self.updatePopulations(it);
581 end
582 self.updateLayers(it);
583 self.updateRoutingProbabilities(it);
584
585 % Reset task layer solvers
586 for e = self.taskLayerIndices
587 switch self.solvers{e}.name
588 case {'SolverMVA', 'SolverNC'}
589 switch self.options.method
590 case 'mol'
591 self.ensemble{e}.refreshRates();
592 otherwise
593 self.ensemble{e}.refreshRates();
594 end
595 otherwise
596 self.ensemble{e}.refreshProcesses();
597 end
598 self.solvers{e}.reset();
599 end
600
601 if self.options.config.interlocking
602 for e = self.taskLayerIndices
603 self.ensemble{e}.refreshJobs();
604 end
605 end
606 end
607
608 function postHostLayers(self, it)
609 % POSTHOSTLAYERS Post-iteration updates for host layers (MOL outer loop)
610 %
611 % Updates think times, layer parameters, and routing probabilities
612 % after host layer analysis, then resets host layer solvers.
613
614 self.updateThinkTimes(it);
615 if self.options.config.interlocking
616 self.updatePopulations(it);
617 end
618 self.updateLayers(it);
619 self.updateRoutingProbabilities(it);
620
621 % Reset host layer solvers
622 for e = self.hostLayerIndices
623 switch self.solvers{e}.name
624 case {'SolverMVA', 'SolverNC'}
625 self.ensemble{e}.refreshRates();
626 otherwise
627 self.ensemble{e}.refreshProcesses();
628 end
629 self.solvers{e}.reset();
630 end
631
632 if self.options.config.interlocking
633 for e = self.hostLayerIndices
634 self.ensemble{e}.refreshJobs();
635 end
636 end
637 end
638 end
639
640 methods
641 function state = get_state(self)
642 % GET_STATE Export current solver state for continuation
643 %
644 % STATE = GET_STATE() returns a struct containing the current
645 % solution state, which can be used to continue iteration with
646 % a different solver via set_state().
647 %
648 % The exported state includes:
649 % - Service time processes (servtproc)
650 % - Think time processes (thinktproc)
651 % - Call service time processes (callservtproc)
652 % - Throughput processes (tputproc)
653 % - Performance metrics (util, tput, servt, residt, etc.)
654 % - Relaxation state
655 % - Last iteration results
656 %
657 % Example:
658 % solver1 = SolverLN(model, @(m) SolverMVA(m));
659 % solver1.getEnsembleAvg();
660 % state = solver1.get_state();
661 %
662 % solver2 = SolverLN(model, @(m) SolverJMT(m));
663 % solver2.set_state(state);
664 % solver2.getEnsembleAvg(); % Continues from MVA solution
665
666 state = struct();
667
668 % Service/think time processes
669 state.servtproc = self.servtproc;
670 state.thinktproc = self.thinktproc;
671 state.callservtproc = self.callservtproc;
672 state.tputproc = self.tputproc;
673 state.entryproc = self.entryproc;
674
675 % Performance metrics
676 state.util = self.util;
677 state.tput = self.tput;
678 state.servt = self.servt;
679 state.residt = self.residt;
680 state.thinkt = self.thinkt;
681 state.callresidt = self.callresidt;
682 state.callservt = self.callservt;
683
684 % Relaxation state
685 state.relax_omega = self.relax_omega;
686 state.servt_prev = self.servt_prev;
687 state.residt_prev = self.residt_prev;
688 state.tput_prev = self.tput_prev;
689 state.thinkt_prev = self.thinkt_prev;
690 state.callservt_prev = self.callservt_prev;
691
692 % Results from last iteration
693 state.results = self.results;
694
695 % Interlock data
696 state.njobs = self.njobs;
697 state.ptaskcallers = self.ptaskcallers;
698 state.ilscaling = self.ilscaling;
699 end
700
701 function set_state(self, state)
702 % SET_STATE Import solution state for continuation
703 %
704 % SET_STATE(STATE) initializes the solver with a previously
705 % exported state, allowing iteration to continue from where
706 % a previous solver left off.
707 %
708 % This enables hybrid solving schemes where fast solvers (MVA)
709 % provide initial estimates and accurate solvers (JMT, DES)
710 % refine the solution.
711 %
712 % Example:
713 % solver1 = SolverLN(model, @(m) SolverMVA(m));
714 % solver1.getEnsembleAvg();
715 % state = solver1.get_state();
716 %
717 % solver2 = SolverLN(model, @(m) SolverJMT(m));
718 % solver2.set_state(state);
719 % solver2.getEnsembleAvg(); % Continues from MVA solution
720
721 % Service/think time processes
722 self.servtproc = state.servtproc;
723 self.thinktproc = state.thinktproc;
724 self.callservtproc = state.callservtproc;
725 self.tputproc = state.tputproc;
726 if isfield(state, 'entryproc')
727 self.entryproc = state.entryproc;
728 end
729
730 % Performance metrics
731 self.util = state.util;
732 self.tput = state.tput;
733 self.servt = state.servt;
734 if isfield(state, 'residt')
735 self.residt = state.residt;
736 end
737 if isfield(state, 'thinkt')
738 self.thinkt = state.thinkt;
739 end
740 if isfield(state, 'callresidt')
741 self.callresidt = state.callresidt;
742 end
743 if isfield(state, 'callservt')
744 self.callservt = state.callservt;
745 end
746
747 % Relaxation state
748 if isfield(state, 'relax_omega')
749 self.relax_omega = state.relax_omega;
750 end
751 if isfield(state, 'servt_prev')
752 self.servt_prev = state.servt_prev;
753 end
754 if isfield(state, 'residt_prev')
755 self.residt_prev = state.residt_prev;
756 end
757 if isfield(state, 'tput_prev')
758 self.tput_prev = state.tput_prev;
759 end
760 if isfield(state, 'thinkt_prev')
761 self.thinkt_prev = state.thinkt_prev;
762 end
763 if isfield(state, 'callservt_prev')
764 self.callservt_prev = state.callservt_prev;
765 end
766
767 % Results
768 if isfield(state, 'results')
769 self.results = state.results;
770 end
771
772 % Interlock data
773 if isfield(state, 'njobs')
774 self.njobs = state.njobs;
775 end
776 if isfield(state, 'ptaskcallers')
777 self.ptaskcallers = state.ptaskcallers;
778 end
779 if isfield(state, 'ilscaling')
780 self.ilscaling = state.ilscaling;
781 end
782
783 % Update layer models with imported state
784 it = 1;
785 if ~isempty(self.results)
786 it = size(self.results, 1);
787 end
788 self.updateLayers(it);
789
790 % Refresh all layer solvers with new parameters
791 for e = 1:self.nlayers
792 self.ensemble{e}.refreshChains();
793 switch self.solvers{e}.name
794 case {'SolverMVA', 'SolverNC'}
795 self.ensemble{e}.refreshRates();
796 otherwise
797 self.ensemble{e}.refreshProcesses();
798 end
799 self.solvers{e}.reset();
800 end
801 end
802
803 function update_solver(self, solverFactory)
804 % UPDATE_SOLVER Change the solver for all layers
805 %
806 % UPDATE_SOLVER(FACTORY) replaces all layer solvers with
807 % new solvers created by the given factory function.
808 %
809 % This allows switching between different solving methods
810 % (e.g., from MVA to JMT/DES) while preserving the current
811 % solution state.
812 %
813 % Example:
814 % solver = SolverLN(model, @(m) SolverMVA(m));
815 % solver.getEnsembleAvg(); % Fast initial solution
816 %
817 % solver.update_solver(@(m) SolverJMT(m, 'samples', 1e5));
818 % solver.getEnsembleAvg(); % Refine with simulation
819
820 self.solverFactory = solverFactory;
821
822 % Replace all layer solvers
823 for e = 1:self.nlayers
824 self.setSolver(solverFactory(self.ensemble{e}), e);
825 end
826 end
827
828 function [allMethods] = listValidMethods(self)
829 sn = self.model.getStruct();
830 % allMethods = LISTVALIDMETHODS()
831 % List valid methods for this solver
832 allMethods = {'default','moment3','mol'};
833 end
834 end
835
836 methods (Static)
837 function options = defaultOptions()
838 % OPTIONS = DEFAULTOPTIONS()
839 options = SolverOptions('LN');
840 end
841
842 function libs = getLibrariesUsed(sn, options)
843 % GETLIBRARIESUSED Get list of external libraries used by LN solver
844 % LN uses internal algorithms, no external library attribution needed
845 libs = {};
846 end
847 end
848end
849
850function solver = adaptiveSolverFactory(model, parentOptions)
851 % ADAPTIVESOLVERFACTORY - Select appropriate solver based on model characteristics
852 % Use JMT for models with open classes, MVA for pure closed networks
853 if nargin < 2
854 verbose = false;
855 else
856 verbose = parentOptions.verbose;
857 end
858
859 % Create MVA solver with reduced iter_max for sublayer stability (Python parity)
860 solver = SolverMVA(model, 'verbose', verbose);
861 solver.options.iter_max = 1000; % Cap sublayer MVA iterations
862end