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