1classdef SolverLN < EnsembleSolver
2 % SolverLN Layered network solver
for hierarchical performance models
4 % SolverLN implements analysis of layered queueing networks (LQNs) which model
5 % hierarchical software systems with clients, application servers, and resource
6 % layers. It uses iterative decomposition to solve multi-layer models by
7 % analyzing each layer separately and propagating service demands between layers.
9 % @brief Layered network solver
for hierarchical software performance models
13 % solver = SolverLN(layered_model,
'maxIter', 100);
14 % solver.runAnalyzer(); % Iterative layer analysis
15 % metrics = solver.getEnsembleAvg(); % Layer performance metrics
18 % Copyright (c) 2012-2026, Imperial College London
19 % All rights reserved.
21 properties %(Hidden) % registries of quantities to update at every iteration
22 nlayers; % number of model layers
23 lqn; % lqn data structure
24 hasconverged; %
true if last iteration converged,
false otherwise
25 averagingstart; % iteration at which result averaging started
26 idxhash; % ensemble model associated to host or task
27 servtmatrix; % auxiliary matrix to determine entry servt
28 ilscaling; % interlock scalings
29 % LQNS V5-style interlock data structures (built once at init)
30 il_table_all; % (nentries x nentries) reachability probability, all phases
31 il_table_ph1; % (nentries x nentries) reachability probability, phase-1 only
32 il_common_entries; % cell(nhosts+ntasks,1) common parent entry indices per server
33 il_source_tasks_all; % cell(nhosts+ntasks,1) all-phase source tasks per server
34 il_source_tasks_ph2; % cell(nhosts+ntasks,1) phase-2 source tasks per server
35 il_num_sources; % (nhosts+ntasks,1) total source multiplicity per server
36 njobs; % number of jobs for each caller in a given submodel
37 njobsorig; % number of jobs for each caller at layer build time
38 routereset; % models that require hard reset of service chains
39 svcreset; % models that require hard reset of service process
40 maxitererr; % maximum error at current iteration over all layers
41 % Under-relaxation state for convergence improvement
42 relax_omega; % Current relaxation factor
43 relax_err_history; % Error history for adaptive mode
44 servt_prev; % Previous service times for relaxation
45 residt_prev; % Previous residence times for relaxation
46 tput_prev; % Previous throughputs for relaxation
47 thinkt_prev; % Previous think times for relaxation
48 callservt_prev; % Previous call service times for relaxation
49 callresidt_prev; % Previous call residence times for growth rate capping
50 singleReplicaTasks; % Task indices modeled as single representative replica (fan-out)
51 % MOL (Method of Layers) properties for hierarchical iteration
52 hostLayerIndices; % Indices of host (processor) layers in ensemble
53 taskLayerIndices; % Indices of task layers in ensemble
54 util_prev_host; % Previous processor utilizations (for delta computation)
55 util_prev_task; % Previous task utilizations (for delta computation)
56 mol_it_host_outer; % MOL host outer iteration counter
57 mol_it_task_inner; % MOL task inner iteration counter
58 % Phase-2 support properties
59 hasPhase2; % Flag: model has phase-2 activities
60 servt_ph1; % Phase-1 service time per activity (nidx x 1)
61 servt_ph2; % Phase-2 service time per activity (nidx x 1)
62 util_ph1; % Phase-1 utilization per entry
63 util_ph2; % Phase-2 utilization per entry
64 prOvertake; % Overtaking probability per entry (nentries x 1)
67 properties %(Hidden) % performance metrics and related processes
69 util_ilock; % interlock matrix (ntask x ntask), element (i,j) says how much the utilization of task i
is imputed to task j
72 servt; % this
is the mean service time of an activity, which
is the response time at the lower layer (if applicable)
73 residt; % this
is the residence time at the lower layer (if applicable)
74 servtproc; % this
is the service time process with mean fitted to the servt value
75 servtcdf; % this
is the cdf of the service time process
85 joint; % join times at AND-Join activities (synchronization delay)
86 ignore; % elements to be ignored (e.g., components disconnected from a REF node)
89 properties %(Access = protected, Hidden) % registries of quantities to update at every iteration
90 arvproc_classes_updmap; % [modelidx, actidx, node, class]
91 thinkt_classes_updmap; % [modelidx, actidx, node, class]
92 actthinkt_classes_updmap; % [modelidx, actidx, node, class] for activity think-times
93 servt_classes_updmap; % [modelidx, actidx, node, class]
94 call_classes_updmap; % [modelidx, callidx, node, class]
95 route_prob_updmap; % [modelidx, actidxfrom, actidxto, nodefrom, nodeto, classfrom, classto]
96 unique_route_prob_updmap; % auxiliary cache of unique route_prob_updmap rows
97 solverFactory; % function handle to create layer solvers
101 function self = SolverLN(lqnmodel, solverFactory, varargin)
102 % SELF = SOLVERLN(MODEL,SOLVERFACTORY,VARARGIN)
103 self@EnsembleSolver(lqnmodel, mfilename);
105 if any(cellfun(@(s) strcmpi(s,'java'), varargin))
106 self.obj = JLINE.SolverLN(JLINE.from_line_layered_network(lqnmodel));
107 self.obj.options.verbose = jline.VerboseLevel.SILENT;
109 % Default solver factory: Use JMT for open networks, MVA for closed networks
110 defaultSolverFactory = @(m) adaptiveSolverFactory(m, self.options);
112 if nargin == 1 %case SolverLN(model)
113 solverFactory = defaultSolverFactory;
114 self.setOptions(SolverLN.defaultOptions);
115 elseif nargin>1 && isstruct(solverFactory)
116 options = solverFactory;
117 self.setOptions(options);
118 solverFactory = defaultSolverFactory;
119 elseif nargin>2 % case SolverLN(model,'opt1',...)
120 if ischar(solverFactory)
121 inputvar = {solverFactory,varargin{:}}; %#ok<CCAT>
122 solverFactory = defaultSolverFactory;
123 else %
case SolverLN(model, solverFactory,
'opt1',...)
126 self.setOptions(Solver.parseOptions(inputvar, SolverLN.defaultOptions));
127 else %case SolverLN(model,solverFactory)
128 self.setOptions(SolverLN.defaultOptions);
130 self.lqn = lqnmodel.getStruct();
132 % Detect and initialize phase-2 support
133 if isfield(self.lqn, 'actphase') && any(self.lqn.actphase > 1)
134 self.hasPhase2 = true;
135 self.servt_ph1 = zeros(self.lqn.nidx, 1);
136 self.servt_ph2 = zeros(self.lqn.nidx, 1);
137 self.util_ph1 = zeros(self.lqn.nidx, 1);
138 self.util_ph2 = zeros(self.lqn.nidx, 1);
139 self.prOvertake = zeros(self.lqn.nentries, 1);
141 self.hasPhase2 = false;
145 line_debug('LN: solver factory=%s, constructing layers', func2str(solverFactory));
146 for e=1:self.getNumberOfModels
147 if numel(find(self.lqn.isfunction == 1))
148 if ~isempty(self.ensemble{e}.stations{2}.setupTime)
149 solverFactory = @(m) SolverMAM(m,
'verbose',
false,
'method',
'dec.poisson');
151 solverFactory = @(m) SolverAuto(m,
'verbose',
false);
153 self.setSolver(solverFactory(self.ensemble{e}),e);
155 self.setSolver(solverFactory(self.ensemble{e}),e);
158 self.solverFactory = solverFactory; % Store
for later use
162 function runtime = runAnalyzer(self, options) %#ok<INUSD> %
generic method to run the solver
163 line_error(mfilename,
'Use getEnsembleAvg instead.');
166 function sn = getStruct(self)
169 % Get data structure summarizing the model
170 sn = self.model.getStruct();
173 function construct(self)
174 % mark down to ignore unreachable disconnected components
175 self.ignore =
false(self.lqn.nidx,1);
176 [~,wccs] = weaklyconncomp(self.lqn.graph
'+self.lqn.graph);
177 uwccs = unique(wccs);
179 % the model has disconnected submodels
180 wccref = false(1,length(uwccs));
181 for t=1:self.lqn.ntasks
182 tidx = self.lqn.tshift+t;
183 if self.lqn.sched(tidx) == SchedStrategy.REF
184 wccref(wccs(tidx)) = true;
187 if any(wccref==false)
188 for dw=find(wccref==false) % disconnected component
189 self.ignore(find(wccs==dw)) = true;
194 % initialize internal data structures
195 self.entrycdfrespt = cell(length(self.lqn.nentries),1);
196 self.hasconverged = false;
198 % initialize svc and think times
199 self.servtproc = self.lqn.hostdem;
200 self.thinkproc = self.lqn.think;
201 self.callservtproc = cell(self.lqn.ncalls,1);
202 for cidx = 1:self.lqn.ncalls
203 self.callservtproc{cidx} = self.lqn.hostdem{self.lqn.callpair(cidx,2)};
207 self.njobs = zeros(self.lqn.tshift + self.lqn.ntasks, self.lqn.tshift + self.lqn.ntasks);
208 buildLayers(self); % build layers
209 line_debug('LN construct: built %d layers from LQN model (%d hosts, %d tasks, %d entries, %d activities)
', ...
210 length(self.ensemble), self.lqn.nhosts, self.lqn.ntasks, self.lqn.nentries, self.lqn.nacts);
211 self.njobsorig = self.njobs;
212 self.nlayers = length(self.ensemble);
214 % interlock data structures are built in init() via initInterlock()
216 % layering generates update maps that we use here to cache the elements that need reset
217 self.routereset = unique(self.idxhash(self.route_prob_updmap(:,1)))';
218 self.svcreset = unique(self.idxhash(self.thinkt_classes_updmap(:,1)))
';
219 self.svcreset = union(self.svcreset,unique(self.idxhash(self.call_classes_updmap(:,1)))');
222 function self = reset(self)
226 bool = converged(self, it); % convergence test at iteration it
228 function init(self) % operations before starting to iterate
229 % INIT() % OPERATIONS BEFORE STARTING TO ITERATE
230 self.unique_route_prob_updmap = unique(self.route_prob_updmap(:,1))
';
231 self.tput = zeros(self.lqn.nidx,1);
232 self.tputproc = cell(self.lqn.nidx,1);
233 self.util = zeros(self.lqn.nidx,1);
234 self.servt = zeros(self.lqn.nidx,1);
235 self.servtmatrix = getEntryServiceMatrix(self);
237 for e= 1:self.nlayers
238 self.solvers{e}.enableChecks=false;
241 % Initialize under-relaxation state
242 relax_mode = self.options.config.relax;
245 self.relax_omega = 1.0; % Start without relaxation
246 case {'fixed
', 'adaptive
'}
247 self.relax_omega = self.options.config.relax_factor;
248 otherwise % 'none
' or unrecognized
249 self.relax_omega = 1.0; % No relaxation
251 self.relax_err_history = [];
252 line_debug('LN init: %d layers, relaxation=%s (omega=%.3f)
', ...
253 self.nlayers, self.options.config.relax, self.relax_omega);
254 self.servt_prev = NaN(self.lqn.nidx, 1);
255 self.residt_prev = NaN(self.lqn.nidx, 1);
256 self.tput_prev = NaN(self.lqn.nidx, 1);
257 self.thinkt_prev = NaN(self.lqn.nidx, 1);
258 self.thinkt = zeros(self.lqn.nidx, 1); % Initialize to zeros (Python parity)
259 self.callservt_prev = NaN(self.lqn.ncalls, 1);
260 self.callresidt_prev = NaN(self.lqn.ncalls, 1);
262 % Build interlock tables (LQNS V5 static analysis)
263 if self.options.config.interlocking
264 self.initInterlock();
267 % Initialize MOL-specific state
268 self.mol_it_host_outer = 0;
269 self.mol_it_task_inner = 0;
270 self.util_prev_host = zeros(self.lqn.nhosts, 1);
271 self.util_prev_task = zeros(self.lqn.ntasks, 1);
275 function pre(self, it) %#ok<INUSD> % operations before an iteration
276 % PRE(IT) % OPERATIONS BEFORE AN ITERATION
280 function [result, runtime] = analyze(self, it, e) %#ok<INUSD>
281 % [RESULT, RUNTIME] = ANALYZE(IT, E)
283 line_debug('LN analyze: iteration %d, layer %d (%s)
', it, e, class(self.solvers{e}));
286 if e==1 && self.solvers{e}.options.verbose
290 % Protection for unstable queues during LN iterations
291 % If a solver fails (e.g., due to queue instability with open arrivals),
292 % use results from previous iteration if available and continue
294 [result.QN, result.UN, result.RN, result.TN, result.AN, result.WN] = self.solvers{e}.getAvg();
296 if it > 1 && ~isempty(self.results) && size(self.results, 1) >= (it-1) && size(self.results, 2) >= e
297 if self.solvers{e}.options.verbose
298 warning('LINE:SolverLN:Instability
', ...
299 'Layer %d at iteration %d encountered instability (possibly due to high service demand with open arrivals). Using previous iteration values and continuing.
', ...
302 % Use results from previous iteration
303 prevResult = self.results{it-1, e};
304 result.QN = prevResult.QN;
305 result.UN = prevResult.UN;
306 result.RN = prevResult.RN;
307 result.TN = prevResult.TN;
308 result.AN = prevResult.AN;
309 result.WN = prevResult.WN;
311 % First iteration or no previous results, re-throw the exception
312 error('LINE:SolverLN:FirstIterationFailure
', ...
313 'Layer %d failed at iteration %d with no previous iteration to fall back on: %s
', ...
320 function post(self, it) % operations after an iteration
321 % POST(IT) % OPERATIONS AFTER AN ITERATION
322 line_debug('LN post: iteration %d, updating metrics and layer parameters
', it);
323 % convert the results of QNs into layer metrics
325 self.updateMetrics(it);
327 if self.options.config.interlocking
328 % apply interlock correction to call residence times
329 self.updatePopulations(it);
332 % recompute think times
333 self.updateThinkTimes(it);
335 % update the model parameters
336 self.updateLayers(it);
338 % update entry selection and cache routing probabilities within callers
339 self.updateRoutingProbabilities(it);
341 % reset all layers with routing probability changes
342 for e= self.routereset
343 self.ensemble{e}.refreshChains();
344 self.solvers{e}.reset();
347 % refresh visits and network model parameters
349 switch self.solvers{e}.name
350 case {'SolverMVA
', 'SolverNC
'} %leaner than refreshProcesses, no need to refresh phases
351 % note: this does not refresh the sn.proc field, only sn.rates and sn.scv
352 switch self.options.method
354 self.ensemble{e}.refreshRates();
356 self.ensemble{e}.refreshProcesses();
359 self.ensemble{e}.refreshProcesses();
361 self.solvers{e}.reset(); % commenting this out des not seem to produce a problem, but it goes faster with it
364 % Note: interlock correction is done via callresidt adjustment
365 % in updatePopulations, no population changes needed
368 % now disable all solver support checks for future iterations
369 for e=1:length(self.ensemble)
370 self.solvers{e}.setChecks(false);
376 function finish(self) % operations after iterations are completed
377 % FINISH() % OPERATIONS AFTER INTERATIONS ARE COMPLETED
378 line_debug('LN finish: final analysis of %d layers
', size(self.results,2));
379 E = size(self.results,2);
385 self.model.ensemble = self.ensemble;
388 function [QNlqn_t, UNlqn_t, TNlqn_t] = getTranAvg(self)
395 [crows, ccols] = size(QNlqn_t);
396 [QNclass_t{e}, UNclass_t{e}, TNclass_t{e}] = self.solvers{e}.getTranAvg();
397 QNlqn_t(crows+1:crows+size(QNclass_t{e},1),ccols+1:ccols+size(QNclass_t{e},2)) = QNclass_t{e};
398 UNlqn_t(crows+1:crows+size(UNclass_t{e},1),ccols+1:ccols+size(UNclass_t{e},2)) = UNclass_t{e};
399 TNlqn_t(crows+1:crows+size(TNclass_t{e},1),ccols+1:ccols+size(TNclass_t{e},2)) = TNclass_t{e};
403 function varargout = getAvg(varargin)
404 % [QN,UN,RN,TN,AN,WN] = GETAVG(SELF,~,~,~,~,USELQNSNAMING)
405 [varargout{1:nargout}] = getEnsembleAvg( varargin{:} );
408 function [cdfRespT] = getCdfRespT(self)
409 if isempty(self.entrycdfrespt{1})
410 % save user-specified method to temporary variable
411 curMethod = self.getOptions.method;
413 self.options.method = 'moment3
';
415 % restore user-specified method
416 self.options.method = curMethod;
418 cdfRespT = self.entrycdfrespt;
421 function [AvgTable,QT,UT,RT,WT,AT,TT] = getAvgTable(self)
422 % [AVGTABLE,QT,UT,RT,WT,TT] = GETAVGTABLE(USELQNSNAMING)
423 if (GlobalConstants.DummyMode)
424 [AvgTable, QT, UT, RT, TT, WT] = deal([]);
428 if ~isempty(self.obj)
429 avgTable = self.obj.getEnsembleAvg();
430 [QN,UN,RN,WN,AN,TN] = JLINE.arrayListToResults(avgTable);
432 [QN,UN,RN,TN,AN,WN] = getAvg(self);
435 % attempt to sanitize small numerical perturbations
436 variables = {QN, UN, RN, TN, AN, WN}; % Put all variables in a cell array
437 for i = 1:length(variables)
438 rVar = round(variables{i} * 10);
439 toRound = abs(variables{i} * 10 - rVar) < GlobalConstants.CoarseTol * variables{i} * 10;
440 variables{i}(toRound) = rVar(toRound) / 10;
441 variables{i}(variables{i}<=GlobalConstants.FineTol) = 0;
443 [QN, UN, RN, TN, AN, WN] = deal(variables{:}); % Assign the modified values back to the original variables
446 Node = label(self.lqn.names);
448 NodeType = label(O,1);
450 switch self.lqn.type(o)
451 case LayeredNetworkElement.PROCESSOR
452 NodeType(o,1) = label({'Processor
'});
453 case LayeredNetworkElement.TASK
455 NodeType(o,1) = label({'RefTask
'});
457 NodeType(o,1) = label({'Task
'});
459 case LayeredNetworkElement.ENTRY
460 NodeType(o,1) = label({'Entry
'});
461 case LayeredNetworkElement.ACTIVITY
462 NodeType(o,1) = label({'Activity
'});
463 case LayeredNetworkElement.CALL
464 NodeType(o,1) = label({'Call
'});
468 QT = Table(Node,QLen);
470 UT = Table(Node,Util);
472 RT = Table(Node,RespT);
474 TT = Table(Node,Tput);
476 %ST = Table(Node,SvcT);
478 %PT = Table(Node,ProcUtil);
480 WT = Table(Node,ResidT);
482 AT = Table(Node,ArvR);
483 AvgTable = Table(Node, NodeType, QLen, Util, RespT, ResidT, ArvR, Tput);%, ProcUtil, SvcT);
486 function [AvgTable,QT,UT,RT,WT,AT,TT] = avgTable(self)
487 % AVGTABLE Alias for getAvgTable
488 [AvgTable,QT,UT,RT,WT,AT,TT] = self.getAvgTable();
491 function [AvgTable,QT,UT,RT,WT,AT,TT] = avgT(self)
492 % AVGT Short alias for getAvgTable
493 [AvgTable,QT,UT,RT,WT,AT,TT] = self.getAvgTable();
496 function [AvgTable,QT,UT,RT,WT,AT,TT] = aT(self)
497 % AT Short alias for getAvgTable (MATLAB-compatible)
498 [AvgTable,QT,UT,RT,WT,AT,TT] = self.getAvgTable();
503 [QN,UN,RN,TN,AN,WN] = getEnsembleAvg(self);
505 function [bool, featSupported] = supports(model)
506 % [BOOL, FEATSUPPORTED] = SUPPORTS(MODEL)
507 % This method cannot be static as otherwise it cannot access self.solvers{e}
508 ensemble = model.getEnsemble;
509 featSupported = cell(length(ensemble),1);
511 for e = 1:length(ensemble)
512 [solverSupports,featSupported{e}] = self.solvers{e}.supports(ensemble{e});
513 bool = bool && solverSupports;
519 buildLayers(self, lqn, resptproc, callservtproc);
520 buildLayersRecursive(self, idx, callers, ishostlayer);
522 updateLayers(self, it);
523 updatePopulations(self, it);
524 updateThinkTimes(self, it);
525 updateMetrics(self, it);
526 updateRoutingProbabilities(self, it);
527 svcmatrix = getEntryServiceMatrix(self)
528 prOt = overtake_prob(self, eidx); % Phase-2 overtaking probability
530 function delta = computeTaskDelta(self)
531 % COMPUTETASKDELTA Compute max queue-length change for task layers (MOL inner loop)
533 % Returns the maximum queue-length change across task layers,
534 % normalized by total jobs, similar to converged.m logic.
535 results = self.results;
536 it = size(results, 1);
543 for e = self.taskLayerIndices
544 metric = results{it, e}.QN;
545 metric_1 = results{it-1, e}.QN;
546 N = sum(self.ensemble{e}.getNumberOfJobs);
549 iterErr = max(abs(metric(:) - metric_1(:))) / N;
553 delta = max(delta, iterErr);
558 function delta = computeHostDelta(self)
559 % COMPUTEHOSTDELTA Compute max queue-length change for host layers (MOL outer loop)
561 % Returns the maximum queue-length change across host layers,
562 % normalized by total jobs, similar to converged.m logic.
563 results = self.results;
564 it = size(results, 1);
571 for e = self.hostLayerIndices
572 metric = results{it, e}.QN;
573 metric_1 = results{it-1, e}.QN;
574 N = sum(self.ensemble{e}.getNumberOfJobs);
577 iterErr = max(abs(metric(:) - metric_1(:))) / N;
581 delta = max(delta, iterErr);
586 function postTaskLayers(self, it)
587 % POSTTASKLAYERS Post-iteration updates for task layers only (MOL inner loop)
589 % Updates think times, layer parameters, and routing probabilities
590 % after task layer analysis, then resets task layer solvers.
592 if self.options.config.interlocking
593 self.updatePopulations(it);
595 self.updateThinkTimes(it);
596 self.updateLayers(it);
597 self.updateRoutingProbabilities(it);
599 % Reset task layer solvers
600 for e = self.taskLayerIndices
601 switch self.solvers{e}.name
602 case {'SolverMVA
', 'SolverNC
'}
603 switch self.options.method
605 self.ensemble{e}.refreshRates();
607 self.ensemble{e}.refreshRates();
610 self.ensemble{e}.refreshProcesses();
612 self.solvers{e}.reset();
617 function postHostLayers(self, it)
618 % POSTHOSTLAYERS Post-iteration updates for host layers (MOL outer loop)
620 % Updates think times, layer parameters, and routing probabilities
621 % after host layer analysis, then resets host layer solvers.
623 if self.options.config.interlocking
624 self.updatePopulations(it);
626 self.updateThinkTimes(it);
627 self.updateLayers(it);
628 self.updateRoutingProbabilities(it);
630 % Reset host layer solvers
631 for e = self.hostLayerIndices
632 switch self.solvers{e}.name
633 case {'SolverMVA
', 'SolverNC
'}
634 self.ensemble{e}.refreshRates();
636 self.ensemble{e}.refreshProcesses();
638 self.solvers{e}.reset();
644 function state = get_state(self)
645 % GET_STATE Export current solver state for continuation
647 % STATE = GET_STATE() returns a struct containing the current
648 % solution state, which can be used to continue iteration with
649 % a different solver via set_state().
651 % The exported state includes:
652 % - Service time processes (servtproc)
653 % - Think time processes (thinktproc)
654 % - Call service time processes (callservtproc)
655 % - Throughput processes (tputproc)
656 % - Performance metrics (util, tput, servt, residt, etc.)
658 % - Last iteration results
661 % solver1 = SolverLN(model, @(m) SolverMVA(m));
662 % solver1.getEnsembleAvg();
663 % state = solver1.get_state();
665 % solver2 = SolverLN(model, @(m) SolverJMT(m));
666 % solver2.set_state(state);
667 % solver2.getEnsembleAvg(); % Continues from MVA solution
671 % Service/think time processes
672 state.servtproc = self.servtproc;
673 state.thinktproc = self.thinktproc;
674 state.callservtproc = self.callservtproc;
675 state.tputproc = self.tputproc;
676 state.entryproc = self.entryproc;
678 % Performance metrics
679 state.util = self.util;
680 state.tput = self.tput;
681 state.servt = self.servt;
682 state.residt = self.residt;
683 state.thinkt = self.thinkt;
684 state.callresidt = self.callresidt;
685 state.callservt = self.callservt;
688 state.relax_omega = self.relax_omega;
689 state.servt_prev = self.servt_prev;
690 state.residt_prev = self.residt_prev;
691 state.tput_prev = self.tput_prev;
692 state.thinkt_prev = self.thinkt_prev;
693 state.callservt_prev = self.callservt_prev;
694 state.callresidt_prev = self.callresidt_prev;
696 % Results from last iteration
697 state.results = self.results;
700 state.njobs = self.njobs;
701 state.ilscaling = self.ilscaling;
704 function set_state(self, state)
705 % SET_STATE Import solution state for continuation
707 % SET_STATE(STATE) initializes the solver with a previously
708 % exported state, allowing iteration to continue from where
709 % a previous solver left off.
711 % This enables hybrid solving schemes where fast solvers (MVA)
712 % provide initial estimates and accurate solvers (JMT, LDES)
713 % refine the solution.
716 % solver1 = SolverLN(model, @(m) SolverMVA(m));
717 % solver1.getEnsembleAvg();
718 % state = solver1.get_state();
720 % solver2 = SolverLN(model, @(m) SolverJMT(m));
721 % solver2.set_state(state);
722 % solver2.getEnsembleAvg(); % Continues from MVA solution
724 % Service/think time processes
725 self.servtproc = state.servtproc;
726 self.thinktproc = state.thinktproc;
727 self.callservtproc = state.callservtproc;
728 self.tputproc = state.tputproc;
729 if isfield(state, 'entryproc
')
730 self.entryproc = state.entryproc;
733 % Performance metrics
734 self.util = state.util;
735 self.tput = state.tput;
736 self.servt = state.servt;
737 if isfield(state, 'residt
')
738 self.residt = state.residt;
740 if isfield(state, 'thinkt
')
741 self.thinkt = state.thinkt;
743 if isfield(state, 'callresidt
')
744 self.callresidt = state.callresidt;
746 if isfield(state, 'callservt
')
747 self.callservt = state.callservt;
751 if isfield(state, 'relax_omega
')
752 self.relax_omega = state.relax_omega;
754 if isfield(state, 'servt_prev
')
755 self.servt_prev = state.servt_prev;
757 if isfield(state, 'residt_prev
')
758 self.residt_prev = state.residt_prev;
760 if isfield(state, 'tput_prev
')
761 self.tput_prev = state.tput_prev;
763 if isfield(state, 'thinkt_prev
')
764 self.thinkt_prev = state.thinkt_prev;
766 if isfield(state, 'callservt_prev
')
767 self.callservt_prev = state.callservt_prev;
769 if isfield(state, 'callresidt_prev
')
770 self.callresidt_prev = state.callresidt_prev;
774 if isfield(state, 'results
')
775 self.results = state.results;
779 if isfield(state, 'njobs
')
780 self.njobs = state.njobs;
782 if isfield(state, 'ilscaling
')
783 self.ilscaling = state.ilscaling;
786 % Update layer models with imported state
788 if ~isempty(self.results)
789 it = size(self.results, 1);
791 self.updateLayers(it);
793 % Refresh all layer solvers with new parameters
794 for e = 1:self.nlayers
795 % Ensure layer model struct is fully built before refresh
796 self.ensemble{e}.getStruct(false);
797 self.ensemble{e}.refreshChains();
798 switch self.solvers{e}.name
799 case {'SolverMVA
', 'SolverNC
'}
800 self.ensemble{e}.refreshRates();
802 self.ensemble{e}.refreshProcesses();
804 self.solvers{e}.reset();
808 function update_solver(self, solverFactory)
809 % UPDATE_SOLVER Change the solver for all layers
811 % UPDATE_SOLVER(FACTORY) replaces all layer solvers with
812 % new solvers created by the given factory function.
814 % This allows switching between different solving methods
815 % (e.g., from MVA to JMT/LDES) while preserving the current
819 % solver = SolverLN(model, @(m) SolverMVA(m));
820 % solver.getEnsembleAvg(); % Fast initial solution
822 % solver.update_solver(@(m) SolverJMT(m, 'samples
', 1e5));
823 % solver.getEnsembleAvg(); % Refine with simulation
825 self.solverFactory = solverFactory;
827 % Replace all layer solvers
828 for e = 1:self.nlayers
829 self.setSolver(solverFactory(self.ensemble{e}), e);
833 function [allMethods] = listValidMethods(self)
834 sn = self.model.getStruct();
835 % allMethods = LISTVALIDMETHODS()
836 % List valid methods for this solver
837 allMethods = {'default
','moment3
','mol
'};
842 function options = defaultOptions()
843 % OPTIONS = DEFAULTOPTIONS()
844 options = SolverOptions('LN
');
847 function libs = getLibrariesUsed(sn, options)
848 % GETLIBRARIESUSED Get list of external libraries used by LN solver
849 % LN uses internal algorithms, no external library attribution needed
855function solver = adaptiveSolverFactory(model, parentOptions)
856 % ADAPTIVESOLVERFACTORY - Select appropriate solver based on model characteristics
857 % Use JMT for models with open classes, MVA for pure closed networks
861 verbose = parentOptions.verbose;
864 % Create MVA solver with reduced iter_max for sublayer stability (Python parity)
865 solver = SolverMVA(model, 'verbose
', verbose);
866 solver.options.iter_max = 1000; % Cap sublayer MVA iterations