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 % Key characteristics:
12 % - Multi-layer hierarchical model analysis
13 % - Iterative decomposition between layers
14 % - Software performance modeling support
15 % - Client-server architecture analysis
16 % - Task and entry modeling capabilities
17 % - Convergence-based iterative solution
18 %
19 % LN solver features:
20 % - Layered model decomposition
21 % - Service time propagation between layers
22 % - Utilization and throughput analysis per layer
23 % - Entry-level performance metrics
24 % - Think time and residence time computation
25 % - Interlocking analysis for dependencies
26 %
27 % SolverLN is ideal for:
28 % - Software performance modeling
29 % - Multi-tier application analysis
30 % - Client-server system evaluation
31 % - Hierarchical resource modeling
32 % - Service-oriented architectures
33 %
34 % Example:
35 % @code
36 % solver = SolverLN(layered_model, 'maxIter', 100);
37 % solver.runAnalyzer(); % Iterative layer analysis
38 % metrics = solver.getEnsembleAvg(); % Layer performance metrics
39 % @endcode
40 %
41 % Copyright (c) 2012-2026, Imperial College London
42 % All rights reserved.
43
44 properties %(Hidden) % registries of quantities to update at every iteration
45 nlayers; % number of model layers
46 lqn; % lqn data structure
47 hasconverged; % true if last iteration converged, false otherwise
48 averagingstart; % iteration at which result averaging started
49 idxhash; % ensemble model associated to host or task
50 servtmatrix; % auxiliary matrix to determine entry servt
51 ptaskcallers; % probability that a task is called by a given task, directly or indirectly (remotely)
52 ptaskcallers_step; % probability that a task is called by a given task, directly or indirectly (remotely) up to a given step distance
53 ilscaling; % interlock scalings
54 njobs; % number of jobs for each caller in a given submodel
55 njobsorig; % number of jobs for each caller at layer build time
56 routereset; % models that require hard reset of service chains
57 svcreset; % models that require hard reset of service process
58 maxitererr; % maximum error at current iteration over all layers
59 % Under-relaxation state for convergence improvement
60 relax_omega; % Current relaxation factor
61 relax_err_history; % Error history for adaptive mode
62 servt_prev; % Previous service times for relaxation
63 residt_prev; % Previous residence times for relaxation
64 tput_prev; % Previous throughputs for relaxation
65 thinkt_prev; % Previous think times for relaxation
66 callservt_prev; % Previous call service times for relaxation
67 % MOL (Method of Layers) properties for hierarchical iteration
68 hostLayerIndices; % Indices of host (processor) layers in ensemble
69 taskLayerIndices; % Indices of task layers in ensemble
70 util_prev_host; % Previous processor utilizations (for delta computation)
71 util_prev_task; % Previous task utilizations (for delta computation)
72 mol_it_host_outer; % MOL host outer iteration counter
73 mol_it_task_inner; % MOL task inner iteration counter
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 ignore; % elements to be ignored (e.g., components disconnected from a REF node)
95 end
96
97 properties %(Access = protected, Hidden) % registries of quantities to update at every iteration
98 arvproc_classes_updmap; % [modelidx, actidx, node, class]
99 thinkt_classes_updmap; % [modelidx, actidx, node, class]
100 actthinkt_classes_updmap; % [modelidx, actidx, node, class] for activity think-times
101 servt_classes_updmap; % [modelidx, actidx, node, class]
102 call_classes_updmap; % [modelidx, callidx, node, class]
103 route_prob_updmap; % [modelidx, actidxfrom, actidxto, nodefrom, nodeto, classfrom, classto]
104 unique_route_prob_updmap; % auxiliary cache of unique route_prob_updmap rows
105 solverFactory; % function handle to create layer solvers
106 end
107
108 methods
109 function self = SolverLN(lqnmodel, solverFactory, varargin)
110 % SELF = SOLVERLN(MODEL,SOLVERFACTORY,VARARGIN)
111 self@EnsembleSolver(lqnmodel, mfilename);
112
113 if any(cellfun(@(s) strcmpi(s,'java'), varargin))
114 self.obj = JLINE.SolverLN(JLINE.from_line_layered_network(lqnmodel));
115 self.obj.options.verbose = jline.VerboseLevel.SILENT;
116 else
117 % Default solver factory: Use JMT for open networks, MVA for closed networks
118 defaultSolverFactory = @(m) adaptiveSolverFactory(m, self.options);
119
120 if nargin == 1 %case SolverLN(model)
121 solverFactory = defaultSolverFactory;
122 self.setOptions(SolverLN.defaultOptions);
123 elseif nargin>1 && isstruct(solverFactory)
124 options = solverFactory;
125 self.setOptions(options);
126 solverFactory = defaultSolverFactory;
127 elseif nargin>2 % case SolverLN(model,'opt1',...)
128 if ischar(solverFactory)
129 inputvar = {solverFactory,varargin{:}}; %#ok<CCAT>
130 solverFactory = defaultSolverFactory;
131 else % case SolverLN(model, solverFactory, 'opt1',...)
132 inputvar = varargin;
133 end
134 self.setOptions(Solver.parseOptions(inputvar, SolverLN.defaultOptions));
135 else %case SolverLN(model,solverFactory)
136 self.setOptions(SolverLN.defaultOptions);
137 end
138 self.lqn = lqnmodel.getStruct();
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.callservt_prev = NaN(self.lqn.ncalls, 1);
254
255 % Initialize MOL-specific state
256 self.mol_it_host_outer = 0;
257 self.mol_it_task_inner = 0;
258 self.util_prev_host = zeros(self.lqn.nhosts, 1);
259 self.util_prev_task = zeros(self.lqn.ntasks, 1);
260 end
261
262
263 function pre(self, it) %#ok<INUSD> % operations before an iteration
264 % PRE(IT) % OPERATIONS BEFORE AN ITERATION
265 % no-op
266 end
267
268 function [result, runtime] = analyze(self, it, e) %#ok<INUSD>
269 % [RESULT, RUNTIME] = ANALYZE(IT, E)
270 T0 = tic;
271 result = struct();
272 %jresult = struct();
273 if e==1 && self.solvers{e}.options.verbose
274 line_printf('\n');
275 end
276
277 % Protection for unstable queues during LN iterations
278 % If a solver fails (e.g., due to queue instability with open arrivals),
279 % use results from previous iteration if available and continue
280 try
281 [result.QN, result.UN, result.RN, result.TN, result.AN, result.WN] = self.solvers{e}.getAvg();
282 catch ME
283 if it > 1 && ~isempty(self.results) && size(self.results, 1) >= (it-1) && size(self.results, 2) >= e
284 if self.solvers{e}.options.verbose
285 warning('LINE:SolverLN:Instability', ...
286 'Layer %d at iteration %d encountered instability (possibly due to high service demand with open arrivals). Using previous iteration values and continuing.', ...
287 e, it);
288 end
289 % Use results from previous iteration
290 prevResult = self.results{it-1, e};
291 result.QN = prevResult.QN;
292 result.UN = prevResult.UN;
293 result.RN = prevResult.RN;
294 result.TN = prevResult.TN;
295 result.AN = prevResult.AN;
296 result.WN = prevResult.WN;
297 else
298 % First iteration or no previous results, re-throw the exception
299 error('LINE:SolverLN:FirstIterationFailure', ...
300 'Layer %d failed at iteration %d with no previous iteration to fall back on: %s', ...
301 e, it, ME.message);
302 end
303 end
304 runtime = toc(T0);
305 end
306
307 function post(self, it) % operations after an iteration
308 % POST(IT) % OPERATIONS AFTER AN ITERATION
309 % convert the results of QNs into layer metrics
310
311 self.updateMetrics(it);
312
313 % recompute think times
314 self.updateThinkTimes(it);
315
316 if self.options.config.interlocking
317 % recompute layer populations
318 self.updatePopulations(it);
319 end
320
321 % update the model parameters
322 self.updateLayers(it);
323
324 % update entry selection and cache routing probabilities within callers
325 self.updateRoutingProbabilities(it);
326
327 % reset all layers with routing probability changes
328 for e= self.routereset
329 self.ensemble{e}.refreshChains();
330 self.solvers{e}.reset();
331 end
332
333 % refresh visits and network model parameters
334 for e= self.svcreset
335 switch self.solvers{e}.name
336 case {'SolverMVA', 'SolverNC'} %leaner than refreshProcesses, no need to refresh phases
337 % note: this does not refresh the sn.proc field, only sn.rates and sn.scv
338 switch self.options.method
339 case 'default'
340 self.ensemble{e}.refreshRates();
341 case 'moment3'
342 self.ensemble{e}.refreshProcesses();
343 end
344 otherwise
345 self.ensemble{e}.refreshProcesses();
346 end
347 self.solvers{e}.reset(); % commenting this out des not seem to produce a problem, but it goes faster with it
348 end
349
350 % this is required to handle population changes due to interlocking
351 if self.options.config.interlocking
352 for e=1:self.nlayers
353 self.ensemble{e}.refreshJobs();
354 end
355 end
356
357 if it==1
358 % now disable all solver support checks for future iterations
359 for e=1:length(self.ensemble)
360 self.solvers{e}.setChecks(false);
361 end
362 end
363 end
364
365
366 function finish(self) % operations after iterations are completed
367 % FINISH() % OPERATIONS AFTER INTERATIONS ARE COMPLETED
368 E = size(self.results,2);
369 for e=1:E
370 s = self.solvers{e};
371 s.getAvg();
372 self.solvers{e} = s;
373 end
374 self.model.ensemble = self.ensemble;
375 end
376
377 function [QNlqn_t, UNlqn_t, TNlqn_t] = getTranAvg(self)
378 self.getAvg;
379 QNclass_t = {};
380 UNclass_t = {};
381 TNclass_t = {};
382 QNlqn_t = cell(0,0);
383 for e=1:self.nlayers
384 [crows, ccols] = size(QNlqn_t);
385 [QNclass_t{e}, UNclass_t{e}, TNclass_t{e}] = self.solvers{e}.getTranAvg();
386 QNlqn_t(crows+1:crows+size(QNclass_t{e},1),ccols+1:ccols+size(QNclass_t{e},2)) = QNclass_t{e};
387 UNlqn_t(crows+1:crows+size(UNclass_t{e},1),ccols+1:ccols+size(UNclass_t{e},2)) = UNclass_t{e};
388 TNlqn_t(crows+1:crows+size(TNclass_t{e},1),ccols+1:ccols+size(TNclass_t{e},2)) = TNclass_t{e};
389 end
390 end
391
392 function varargout = getAvg(varargin)
393 % [QN,UN,RN,TN,AN,WN] = GETAVG(SELF,~,~,~,~,USELQNSNAMING)
394 [varargout{1:nargout}] = getEnsembleAvg( varargin{:} );
395 end
396
397 function [cdfRespT] = getCdfRespT(self)
398 if isempty(self.entrycdfrespt{1})
399 % save user-specified method to temporary variable
400 curMethod = self.getOptions.method;
401 % run with moment 3
402 self.options.method = 'moment3';
403 self.getAvg();
404 % restore user-specified method
405 self.options.method = curMethod;
406 end
407 cdfRespT = self.entrycdfrespt;
408 end
409
410 function [AvgTable,QT,UT,RT,WT,AT,TT] = getAvgTable(self)
411 % [AVGTABLE,QT,UT,RT,WT,TT] = GETAVGTABLE(USELQNSNAMING)
412 if (GlobalConstants.DummyMode)
413 [AvgTable, QT, UT, RT, TT, WT] = deal([]);
414 return
415 end
416
417 if ~isempty(self.obj)
418 avgTable = self.obj.getEnsembleAvg();
419 [QN,UN,RN,WN,AN,TN] = JLINE.arrayListToResults(avgTable);
420 else
421 [QN,UN,RN,TN,AN,WN] = getAvg(self);
422 end
423
424 % attempt to sanitize small numerical perturbations
425 variables = {QN, UN, RN, TN, AN, WN}; % Put all variables in a cell array
426 for i = 1:length(variables)
427 rVar = round(variables{i} * 10);
428 toRound = abs(variables{i} * 10 - rVar) < GlobalConstants.CoarseTol * variables{i} * 10;
429 variables{i}(toRound) = rVar(toRound) / 10;
430 variables{i}(variables{i}<=GlobalConstants.FineTol) = 0;
431 end
432 [QN, UN, RN, TN, AN, WN] = deal(variables{:}); % Assign the modified values back to the original variables
433
434 %%
435 Node = label(self.lqn.names);
436 O = length(Node);
437 NodeType = label(O,1);
438 for o = 1:O
439 switch self.lqn.type(o)
440 case LayeredNetworkElement.PROCESSOR
441 NodeType(o,1) = label({'Processor'});
442 case LayeredNetworkElement.TASK
443 if self.lqn.isref(o)
444 NodeType(o,1) = label({'RefTask'});
445 else
446 NodeType(o,1) = label({'Task'});
447 end
448 case LayeredNetworkElement.ENTRY
449 NodeType(o,1) = label({'Entry'});
450 case LayeredNetworkElement.ACTIVITY
451 NodeType(o,1) = label({'Activity'});
452 case LayeredNetworkElement.CALL
453 NodeType(o,1) = label({'Call'});
454 end
455 end
456 QLen = QN;
457 QT = Table(Node,QLen);
458 Util = UN;
459 UT = Table(Node,Util);
460 RespT = RN;
461 RT = Table(Node,RespT);
462 Tput = TN;
463 TT = Table(Node,Tput);
464 %SvcT = SN;
465 %ST = Table(Node,SvcT);
466 %ProcUtil = PN;
467 %PT = Table(Node,ProcUtil);
468 ResidT = WN;
469 WT = Table(Node,ResidT);
470 ArvR = AN;
471 AT = Table(Node,ArvR);
472 AvgTable = Table(Node, NodeType, QLen, Util, RespT, ResidT, ArvR, Tput);%, ProcUtil, SvcT);
473 end
474 end
475
476 methods
477 [QN,UN,RN,TN,AN,WN] = getEnsembleAvg(self);
478
479 function [bool, featSupported] = supports(model)
480 % [BOOL, FEATSUPPORTED] = SUPPORTS(MODEL)
481 % This method cannot be static as otherwise it cannot access self.solvers{e}
482 ensemble = model.getEnsemble;
483 featSupported = cell(length(ensemble),1);
484 bool = true;
485 for e = 1:length(ensemble)
486 [solverSupports,featSupported{e}] = self.solvers{e}.supports(ensemble{e});
487 bool = bool && solverSupports;
488 end
489 end
490 end
491
492 methods (Hidden)
493 buildLayers(self, lqn, resptproc, callservtproc);
494 buildLayersRecursive(self, idx, callers, ishostlayer);
495 updateLayers(self, it);
496 updatePopulations(self, it);
497 updateThinkTimes(self, it);
498 updateMetrics(self, it);
499 updateRoutingProbabilities(self, it);
500 svcmatrix = getEntryServiceMatrix(self)
501
502 function delta = computeTaskDelta(self)
503 % COMPUTETASKDELTA Compute max queue-length change for task layers (MOL inner loop)
504 %
505 % Returns the maximum queue-length change across task layers,
506 % normalized by total jobs, similar to converged.m logic.
507 results = self.results;
508 it = size(results, 1);
509 if it < 2
510 delta = Inf;
511 return;
512 end
513
514 delta = 0;
515 for e = self.taskLayerIndices
516 metric = results{it, e}.QN;
517 metric_1 = results{it-1, e}.QN;
518 N = sum(self.ensemble{e}.getNumberOfJobs);
519 if N > 0
520 try
521 iterErr = max(abs(metric(:) - metric_1(:))) / N;
522 catch
523 iterErr = 0;
524 end
525 delta = max(delta, iterErr);
526 end
527 end
528 end
529
530 function delta = computeHostDelta(self)
531 % COMPUTEHOSTDELTA Compute max queue-length change for host layers (MOL outer loop)
532 %
533 % Returns the maximum queue-length change across host layers,
534 % normalized by total jobs, similar to converged.m logic.
535 results = self.results;
536 it = size(results, 1);
537 if it < 2
538 delta = Inf;
539 return;
540 end
541
542 delta = 0;
543 for e = self.hostLayerIndices
544 metric = results{it, e}.QN;
545 metric_1 = results{it-1, e}.QN;
546 N = sum(self.ensemble{e}.getNumberOfJobs);
547 if N > 0
548 try
549 iterErr = max(abs(metric(:) - metric_1(:))) / N;
550 catch
551 iterErr = 0;
552 end
553 delta = max(delta, iterErr);
554 end
555 end
556 end
557
558 function postTaskLayers(self, it)
559 % POSTTASKLAYERS Post-iteration updates for task layers only (MOL inner loop)
560 %
561 % Updates think times, layer parameters, and routing probabilities
562 % after task layer analysis, then resets task layer solvers.
563
564 self.updateThinkTimes(it);
565 if self.options.config.interlocking
566 self.updatePopulations(it);
567 end
568 self.updateLayers(it);
569 self.updateRoutingProbabilities(it);
570
571 % Reset task layer solvers
572 for e = self.taskLayerIndices
573 switch self.solvers{e}.name
574 case {'SolverMVA', 'SolverNC'}
575 switch self.options.method
576 case 'mol'
577 self.ensemble{e}.refreshRates();
578 otherwise
579 self.ensemble{e}.refreshRates();
580 end
581 otherwise
582 self.ensemble{e}.refreshProcesses();
583 end
584 self.solvers{e}.reset();
585 end
586
587 if self.options.config.interlocking
588 for e = self.taskLayerIndices
589 self.ensemble{e}.refreshJobs();
590 end
591 end
592 end
593
594 function postHostLayers(self, it)
595 % POSTHOSTLAYERS Post-iteration updates for host layers (MOL outer loop)
596 %
597 % Updates think times, layer parameters, and routing probabilities
598 % after host layer analysis, then resets host layer solvers.
599
600 self.updateThinkTimes(it);
601 if self.options.config.interlocking
602 self.updatePopulations(it);
603 end
604 self.updateLayers(it);
605 self.updateRoutingProbabilities(it);
606
607 % Reset host layer solvers
608 for e = self.hostLayerIndices
609 switch self.solvers{e}.name
610 case {'SolverMVA', 'SolverNC'}
611 self.ensemble{e}.refreshRates();
612 otherwise
613 self.ensemble{e}.refreshProcesses();
614 end
615 self.solvers{e}.reset();
616 end
617
618 if self.options.config.interlocking
619 for e = self.hostLayerIndices
620 self.ensemble{e}.refreshJobs();
621 end
622 end
623 end
624 end
625
626 methods
627 function state = get_state(self)
628 % GET_STATE Export current solver state for continuation
629 %
630 % STATE = GET_STATE() returns a struct containing the current
631 % solution state, which can be used to continue iteration with
632 % a different solver via set_state().
633 %
634 % The exported state includes:
635 % - Service time processes (servtproc)
636 % - Think time processes (thinktproc)
637 % - Call service time processes (callservtproc)
638 % - Throughput processes (tputproc)
639 % - Performance metrics (util, tput, servt, residt, etc.)
640 % - Relaxation state
641 % - Last iteration results
642 %
643 % Example:
644 % solver1 = SolverLN(model, @(m) SolverMVA(m));
645 % solver1.getEnsembleAvg();
646 % state = solver1.get_state();
647 %
648 % solver2 = SolverLN(model, @(m) SolverJMT(m));
649 % solver2.set_state(state);
650 % solver2.getEnsembleAvg(); % Continues from MVA solution
651
652 state = struct();
653
654 % Service/think time processes
655 state.servtproc = self.servtproc;
656 state.thinktproc = self.thinktproc;
657 state.callservtproc = self.callservtproc;
658 state.tputproc = self.tputproc;
659 state.entryproc = self.entryproc;
660
661 % Performance metrics
662 state.util = self.util;
663 state.tput = self.tput;
664 state.servt = self.servt;
665 state.residt = self.residt;
666 state.thinkt = self.thinkt;
667 state.callresidt = self.callresidt;
668 state.callservt = self.callservt;
669
670 % Relaxation state
671 state.relax_omega = self.relax_omega;
672 state.servt_prev = self.servt_prev;
673 state.residt_prev = self.residt_prev;
674 state.tput_prev = self.tput_prev;
675 state.thinkt_prev = self.thinkt_prev;
676 state.callservt_prev = self.callservt_prev;
677
678 % Results from last iteration
679 state.results = self.results;
680
681 % Interlock data
682 state.njobs = self.njobs;
683 state.ptaskcallers = self.ptaskcallers;
684 state.ilscaling = self.ilscaling;
685 end
686
687 function set_state(self, state)
688 % SET_STATE Import solution state for continuation
689 %
690 % SET_STATE(STATE) initializes the solver with a previously
691 % exported state, allowing iteration to continue from where
692 % a previous solver left off.
693 %
694 % This enables hybrid solving schemes where fast solvers (MVA)
695 % provide initial estimates and accurate solvers (JMT, DES)
696 % refine the solution.
697 %
698 % Example:
699 % solver1 = SolverLN(model, @(m) SolverMVA(m));
700 % solver1.getEnsembleAvg();
701 % state = solver1.get_state();
702 %
703 % solver2 = SolverLN(model, @(m) SolverJMT(m));
704 % solver2.set_state(state);
705 % solver2.getEnsembleAvg(); % Continues from MVA solution
706
707 % Service/think time processes
708 self.servtproc = state.servtproc;
709 self.thinktproc = state.thinktproc;
710 self.callservtproc = state.callservtproc;
711 self.tputproc = state.tputproc;
712 if isfield(state, 'entryproc')
713 self.entryproc = state.entryproc;
714 end
715
716 % Performance metrics
717 self.util = state.util;
718 self.tput = state.tput;
719 self.servt = state.servt;
720 if isfield(state, 'residt')
721 self.residt = state.residt;
722 end
723 if isfield(state, 'thinkt')
724 self.thinkt = state.thinkt;
725 end
726 if isfield(state, 'callresidt')
727 self.callresidt = state.callresidt;
728 end
729 if isfield(state, 'callservt')
730 self.callservt = state.callservt;
731 end
732
733 % Relaxation state
734 if isfield(state, 'relax_omega')
735 self.relax_omega = state.relax_omega;
736 end
737 if isfield(state, 'servt_prev')
738 self.servt_prev = state.servt_prev;
739 end
740 if isfield(state, 'residt_prev')
741 self.residt_prev = state.residt_prev;
742 end
743 if isfield(state, 'tput_prev')
744 self.tput_prev = state.tput_prev;
745 end
746 if isfield(state, 'thinkt_prev')
747 self.thinkt_prev = state.thinkt_prev;
748 end
749 if isfield(state, 'callservt_prev')
750 self.callservt_prev = state.callservt_prev;
751 end
752
753 % Results
754 if isfield(state, 'results')
755 self.results = state.results;
756 end
757
758 % Interlock data
759 if isfield(state, 'njobs')
760 self.njobs = state.njobs;
761 end
762 if isfield(state, 'ptaskcallers')
763 self.ptaskcallers = state.ptaskcallers;
764 end
765 if isfield(state, 'ilscaling')
766 self.ilscaling = state.ilscaling;
767 end
768
769 % Update layer models with imported state
770 it = 1;
771 if ~isempty(self.results)
772 it = size(self.results, 1);
773 end
774 self.updateLayers(it);
775
776 % Refresh all layer solvers with new parameters
777 for e = 1:self.nlayers
778 self.ensemble{e}.refreshChains();
779 switch self.solvers{e}.name
780 case {'SolverMVA', 'SolverNC'}
781 self.ensemble{e}.refreshRates();
782 otherwise
783 self.ensemble{e}.refreshProcesses();
784 end
785 self.solvers{e}.reset();
786 end
787 end
788
789 function update_solver(self, solverFactory)
790 % UPDATE_SOLVER Change the solver for all layers
791 %
792 % UPDATE_SOLVER(FACTORY) replaces all layer solvers with
793 % new solvers created by the given factory function.
794 %
795 % This allows switching between different solving methods
796 % (e.g., from MVA to JMT/DES) while preserving the current
797 % solution state.
798 %
799 % Example:
800 % solver = SolverLN(model, @(m) SolverMVA(m));
801 % solver.getEnsembleAvg(); % Fast initial solution
802 %
803 % solver.update_solver(@(m) SolverJMT(m, 'samples', 1e5));
804 % solver.getEnsembleAvg(); % Refine with simulation
805
806 self.solverFactory = solverFactory;
807
808 % Replace all layer solvers
809 for e = 1:self.nlayers
810 self.setSolver(solverFactory(self.ensemble{e}), e);
811 end
812 end
813
814 function [allMethods] = listValidMethods(self)
815 sn = self.model.getStruct();
816 % allMethods = LISTVALIDMETHODS()
817 % List valid methods for this solver
818 allMethods = {'default','moment3','mol'};
819 end
820 end
821
822 methods (Static)
823 function options = defaultOptions()
824 % OPTIONS = DEFAULTOPTIONS()
825 options = SolverOptions('LN');
826 end
827 end
828end
829
830function solver = adaptiveSolverFactory(model, parentOptions)
831 % ADAPTIVESOLVERFACTORY - Select appropriate solver based on model characteristics
832 % Use JMT for models with open classes, MVA for pure closed networks
833 if nargin < 2
834 verbose = false;
835 else
836 verbose = parentOptions.verbose;
837 end
838
839 if model.hasOpenClasses()
840 solver = SolverJMT(model, 'verbose', verbose);
841 else
842 solver = SolverMVA(model, 'verbose', verbose);
843 end
844end