LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
SolverENV.m
1classdef SolverENV < EnsembleSolver
2 % ENV - Ensemble environment solver for models with random environment changes
3 %
4 % SolverENV analyzes queueing networks operating in random environments where
5 % system parameters (arrival rates, service rates, routing) change according
6 % to an underlying environmental process. It solves ensemble models by analyzing
7 % each environmental stage and computing environment-averaged performance metrics.
8 %
9 % @brief Environment solver for networks in random changing environments
10 %
11 % Key characteristics:
12 % - Random environment with multiple operational stages
13 % - Environmental process governing parameter changes
14 % - Ensemble model analysis across environment stages
15 % - Environment-averaged performance computation
16 % - Stage-dependent system behavior modeling
17 %
18 % Environment solver features:
19 % - Multi-stage environmental modeling
20 % - Stage transition matrix analysis
21 % - Weighted performance metric computation
22 % - Environmental ensemble solution
23 % - Adaptive parameter modeling
24 %
25 % SolverENV is ideal for:
26 % - Systems with time-varying parameters
27 % - Networks subject to environmental fluctuations
28 % - Multi-mode operational system analysis
29 % - Performance under uncertainty modeling
30 % - Adaptive system behavior analysis
31 %
32 % Example:
33 % @code
34 % env_model = Environment(stages, transitions); % Define environment
35 % solver = SolverENV(env_model, @SolverMVA, options);
36 % metrics = solver.getEnsembleAvg(); % Environment-averaged metrics
37 % @endcode
38 %
39 % Copyright (c) 2012-2026, Imperial College London
40 % All rights reserved.
41
42
43 properties
44 env; % user-supplied representation of each stage transition
45 envObj;
46 sn;
47 resetFromMarginal;
48 resetEnvRates; % function implementing the reset policy for environment rates
49 stateDepMethod = ''; % state-dependent method configuration
50 SMPMethod = false; % Use DTMC-based computation for Semi-Markov Processes
51 % Enhanced init properties (aligned with JAR)
52 ServerNum; % Cell array of server counts per class
53 SRates; % Cell array of service rates per class
54 E0; % Rate matrix
55 Eutil; % Infinitesimal generator
56 transitionCdfs; % Transition CDF functions
57 sojournCdfs; % Sojourn time CDF functions
58 dtmcP; % DTMC transition matrix
59 holdTimeMatrix; % Hold time matrix
60 newMethod = false; % Use DTMC-based computation
61 compression = false; % Use Courtois decomposition
62 compressionResult; % Results from Courtois decomposition
63 Ecompress; % Number of macro-states after compression
64 MS; % Macro-state partition
65 % Analyzer strategy: which inter-stage coupling to use.
66 analyzerMode = 'meanfield'; % 'meanfield' (default) | 'statevec'
67 analyzerFcn = @solver_env_meanfield_analyzer; % phase-dispatched analyzer handle
68 % State-vector analyzer working data (options.method='statevec' only)
69 Qgen; % Cell{E}: per-stage infinitesimal generator
70 SS; % Cell{E}: per-stage state space (rows = states)
71 SSaggr; % Cell{E}: per-stage aggregated (marginal) state space
72 piEnter; % Cell{E}: per-stage entry distribution (row vector)
73 piEnterPrev; % Cell{E}: previous-iteration entry distribution (convergence)
74 piExitDest; % Cell{E}{E}: per-stage exit distribution toward each destination
75 piTimeAvg; % Cell{E}: per-stage sojourn-end distribution used in the blend
76 statevecData;% Cell{E}: struct(arvRates,depRates,sn,options) per stage
77 resetStateFun; % Cell{E,E}: state-vector reset maps (default identity)
78 end
79
80 methods
81 function self = SolverENV(renv, solverFactory, options)
82 % SELF = SOLVERENV(ENV,SOLVERFACTORY,OPTIONS)
83 self@EnsembleSolver(renv, mfilename);
84 if nargin>=3 %exist('options','var')
85 self.setOptions(options);
86 else
87 self.setOptions(SolverENV.defaultOptions);
88 end
89
90 % Enable SMP method if specified in options
91 if isfield(self.options, 'method') && strcmpi(self.options.method, 'smp')
92 self.SMPMethod = true;
93 line_debug('ENV solver: SMP method enabled via options.method=''smp''');
94 end
95
96 self.envObj = renv;
97 self.ensemble = renv.getEnsemble;
98 env = renv.getEnv;
99 self.env = env;
100 for e=1:length(self.env)
101 self.sn{e} = self.ensemble{e}.getStruct;
102 self.setSolver(solverFactory(self.ensemble{e}),e);
103 end
104
105 for e=1:length(self.env)
106 for h=1:length(self.env)
107 self.resetFromMarginal{e,h} = renv.resetFun{e,h};
108 end
109 end
110
111 for e=1:length(self.env)
112 for h=1:length(self.env)
113 self.resetEnvRates{e,h} = renv.resetEnvRatesFun{e,h};
114 end
115 end
116
117 % State-vector reset maps (statevec analyzer). Default to identity
118 % for any (e,h) the environment left unset.
119 for e=1:length(self.env)
120 for h=1:length(self.env)
121 if ~isempty(renv.resetStateFun) && e<=size(renv.resetStateFun,1) ...
122 && h<=size(renv.resetStateFun,2) && ~isempty(renv.resetStateFun{e,h})
123 self.resetStateFun{e,h} = renv.resetStateFun{e,h};
124 else
125 self.resetStateFun{e,h} = @(pi) pi;
126 end
127 end
128 end
129
130 for e=1:length(self.env)
131 for h=1:length(self.env)
132 if isa(self.env{e,h},'Disabled')
133 self.env{e,h} = Exp(0);
134 elseif ~isa(self.env{e,h},'Markovian') && ~self.SMPMethod
135 line_error(mfilename,sprintf('The distribution of the environment transition from stage %d to %d is not supported by the %s solver. Use method=''smp'' for non-Markovian distributions.',e,h,self.getName));
136 end
137 end
138 end
139
140 for e=1:length(self.ensemble)
141 if ~self.solvers{e}.supports(self.ensemble{e})
142 line_error(mfilename,sprintf('Model in the environment stage %d is not supported by the %s solver.',e,self.getName));
143 end
144 end
145 end
146
147 function setStateDepMethod(self, method)
148 % SETSTATEDEPMETHOD(METHOD) Sets the state-dependent method
149 if isempty(method)
150 line_error(mfilename, 'State-dependent method cannot be null or empty.');
151 end
152 self.stateDepMethod = method;
153 end
154
155 function setNewMethod(self, flag)
156 % SETNEWMETHOD(FLAG) Enable/disable DTMC-based computation
157 self.newMethod = flag;
158 end
159
160 function setCompression(self, flag)
161 % SETCOMPRESSION(FLAG) Enable/disable Courtois decomposition
162 self.compression = flag;
163 end
164
165 function [p, eps, epsMax, q] = ctmc_decompose(self, Q, MS)
166 % CTMC_DECOMPOSE Perform CTMC decomposition using configured method
167 % [p, eps, epsMax, q] = CTMC_DECOMPOSE(Q, MS)
168 %
169 % Uses options.config.decomp to select the decomposition algorithm:
170 % 'courtois' - Courtois decomposition (default)
171 % 'kms' - Koury-McAllister-Stewart method
172 % 'takahashi' - Takahashi's method
173 % 'multi' - Multigrid method (requires MSS)
174 %
175 % Returns:
176 % p - steady-state probability vector
177 % eps - NCD index
178 % epsMax - max acceptable eps value
179 % q - randomization coefficient
180
181 % Get decomposition/aggregation method from options
182 if isfield(self.options, 'config') && isfield(self.options.config, 'da')
183 method = self.options.config.da;
184 else
185 method = 'courtois';
186 end
187
188 % Get numsteps for iterative methods
189 if isfield(self.options, 'config') && isfield(self.options.config, 'da_iter')
190 numsteps = self.options.config.da_iter;
191 else
192 numsteps = 10;
193 end
194
195 switch lower(method)
196 case 'courtois'
197 [p, ~, ~, eps, epsMax, ~, ~, ~, q] = ctmc_courtois(Q, MS);
198 case 'kms'
199 [p, ~, ~, eps, epsMax] = ctmc_kms(Q, MS, numsteps);
200 q = 1.05 * max(max(abs(Q)));
201 case 'takahashi'
202 [p, ~, ~, ~, eps, epsMax] = ctmc_takahashi(Q, MS, numsteps);
203 q = 1.05 * max(max(abs(Q)));
204 case 'multi'
205 % Multi requires MSS (macro-macro-states), default to singletons
206 nMacro = size(MS, 1);
207 MSS = cell(nMacro, 1);
208 for i = 1:nMacro
209 MSS{i} = i;
210 end
211 [p, ~, ~, ~, eps, epsMax] = ctmc_multi(Q, MS, MSS);
212 q = 1.05 * max(max(abs(Q)));
213 otherwise
214 line_error(mfilename, sprintf('Unknown decomposition method: %s', method));
215 end
216 end
217
218 function bool = converged(self, it)
219 % BOOL = CONVERGED(IT) Convergence test, delegated to the active analyzer.
220 bool = self.analyzerFcn(self, 'converged', it, []);
221 end
222
223
224 function runAnalyzer(self)
225 % RUNANALYZER()
226 % Run the ensemble solver iteration
227 line_debug('ENV solver starting: nstages=%d, method=%s', self.getNumberOfModels, self.options.method);
228
229 % Show library attribution if verbose and not yet shown
230 if self.options.verbose ~= VerboseLevel.SILENT && ~GlobalConstants.isLibraryAttributionShown()
231 libs = SolverENV.getLibrariesUsed([], self.options);
232 if ~isempty(libs)
233 line_printf('The solver will leverage %s.\n', strjoin(libs, ', '));
234 GlobalConstants.setLibraryAttributionShown(true);
235 end
236 end
237
238 % Closed-form fast/slow environment limits bypass the transient
239 % mean-field/statevec iteration (see solveEnvLimit).
240 if isfield(self.options,'method') && any(strcmpi(self.options.method,{'avg','dec'}))
241 self.solveEnvLimit();
242 return
243 end
244
245 iterate(self);
246 end
247
248 function init(self)
249 % INIT()
250 % Initialize the environment solver with enhanced data structures
251 % aligned with JAR SolverEnv implementation
252 line_debug('ENV solver init: initializing environment data structures');
253 options = self.options;
254 if isfield(options,'seed')
255 Solver.resetRandomGeneratorSeed(options.seed);
256 end
257 self.envObj.init();
258
259 % Initialize ServerNum and SRates matrices (aligned with JAR).
260 % These flat station x class server/rate tables are consumed only
261 % by the state-vector (CTMC) analyzer and are undefined for a
262 % LayeredNetworkStruct stage; skip them when the stage struct is
263 % not a flat NetworkStruct (e.g. LQN stages under the meanfield
264 % analyzer).
265 E = self.getNumberOfModels;
266 if isfield(self.sn{1}, 'nstations')
267 M = self.sn{1}.nstations;
268 K = self.sn{1}.nclasses;
269
270 self.ServerNum = cell(K, 1);
271 self.SRates = cell(K, 1);
272 for k = 1:K
273 self.ServerNum{k} = zeros(M, E);
274 self.SRates{k} = zeros(M, E);
275 for e = 1:E
276 for m = 1:M
277 self.ServerNum{k}(m, e) = self.sn{e}.nservers(m);
278 self.SRates{k}(m, e) = self.sn{e}.rates(m, k);
279 end
280 end
281 end
282 end
283
284 % Build rate matrix E0
285 self.E0 = zeros(E, E);
286 for e = 1:E
287 for h = 1:E
288 if ~isa(self.envObj.env{e,h}, 'Disabled')
289 self.E0(e, h) = self.envObj.env{e,h}.getRate();
290 end
291 end
292 end
293 self.Eutil = ctmc_makeinfgen(self.E0);
294
295 % Initialize transition CDFs
296 self.transitionCdfs = cell(E, E);
297 for e = 1:E
298 for h = 1:E
299 if ~isa(self.envObj.env{e,h}, 'Disabled')
300 envDist = self.envObj.env{e,h};
301 self.transitionCdfs{e,h} = @(t) envDist.evalCDF(t);
302 else
303 self.transitionCdfs{e,h} = @(t) 0;
304 end
305 end
306 end
307
308 % Initialize sojourn CDFs
309 self.sojournCdfs = cell(E, 1);
310 for e = 1:E
311 self.sojournCdfs{e} = @(t) self.computeSojournCdf(e, t);
312 end
313
314 % Select the inter-stage coupling analyzer. The state-vector
315 % analyzer carries the full per-stage distribution (no
316 % initFromMarginal) and requires a CTMC inner solver that can expose
317 % the stage generator; everything else uses the default mean-field
318 % (marginal mean-queue-length) coupling.
319 if isfield(self.options,'method') && any(strcmpi(self.options.method,{'statevec','blend'}))
320 % The state-vector analyzer (exposed also as 'blend') needs a
321 % single per-stage CTMC generator; an LQN stage decomposes into
322 % multiple layer submodels and exposes no single generator, so
323 % it is supported only by the mean-field analyzer.
324 for e=1:E
325 if isa(self.ensemble{e}, 'LayeredNetwork')
326 line_error(mfilename, sprintf(['The state-vector (statevec) analyzer does not support ' ...
327 'LayeredNetwork stages (stage %d): an LQN has no single stage generator. ' ...
328 'Use the default mean-field analyzer (omit method=''statevec'').'], e));
329 end
330 end
331 self.analyzerMode = 'statevec';
332 self.analyzerFcn = @solver_env_statevec_analyzer;
333 else
334 self.analyzerMode = 'meanfield';
335 self.analyzerFcn = @solver_env_meanfield_analyzer;
336 end
337
338 % newMethod: Use DTMC-based computation instead of CTMC
339 % Verified numerical integration for Semi-Markov Process DTMC transition probabilities
340 if self.newMethod
341 line_debug('ENV using DTMC-based computation (newMethod=true)');
342 self.dtmcP = zeros(E, E);
343 for k = 1:E
344 for e = 1:E
345 if k == e || isa(self.envObj.env{k,e}, 'Disabled')
346 self.dtmcP(k, e) = 0.0;
347 else
348 % Compute the upper limit of the sojourn time
349 epsilon = 1e-8;
350 T = 1;
351 while self.transitionCdfs{k,e}(T) < 1.0 - epsilon
352 T = T * 2;
353 if T > 1e6 % safety limit
354 break;
355 end
356 end
357 % Adaptive number of integration intervals based on T
358 N = max(1000, round(T * 100));
359 dt = T / N;
360 sumVal = 0;
361 for i = 0:(N-1)
362 t0 = i * dt;
363 t1 = t0 + dt;
364 deltaF = self.transitionCdfs{k,e}(t1) - self.transitionCdfs{k,e}(t0);
365 survival = 1;
366 for h = 1:E
367 if h ~= k && h ~= e && ~isa(self.envObj.env{k,h}, 'Disabled')
368 % Use midpoint for better accuracy
369 tmid = (t0 + t1) / 2.0;
370 survival = survival * (1.0 - self.envObj.env{k,h}.evalCDF(tmid));
371 end
372 end
373 sumVal = sumVal + deltaF * survival;
374 end
375 self.dtmcP(k, e) = sumVal;
376 end
377 end
378 end
379
380 % Solve DTMC for stationary distribution
381 dtmcPie = dtmc_solve(self.dtmcP);
382
383 % Calculate hold times using numerical integration
384 self.holdTimeMatrix = self.computeHoldTime(E);
385
386 % Compute steady-state probabilities
387 pi = zeros(1, E);
388 denomSum = 0;
389 for e = 1:E
390 denomSum = denomSum + dtmcPie(e) * self.holdTimeMatrix(e);
391 end
392 for k = 1:E
393 pi(k) = dtmcPie(k) * self.holdTimeMatrix(k) / denomSum;
394 end
395 self.envObj.probEnv = pi;
396
397 % Update embedding weights
398 newEmbweight = zeros(E, E);
399 for e = 1:E
400 sumVal = 0.0;
401 for h = 1:E
402 if h ~= e
403 sumVal = sumVal + pi(h) * self.E0(h, e);
404 end
405 end
406 for k = 1:E
407 if k == e
408 newEmbweight(k, e) = 0;
409 else
410 if sumVal > 0
411 newEmbweight(k, e) = pi(k) * self.E0(k, e) / sumVal;
412 end
413 end
414 end
415 end
416 self.envObj.probOrig = newEmbweight;
417 end
418
419 % Compression: Use Courtois decomposition for large environments
420 if self.compression
421 line_debug('ENV using compression (Courtois decomposition)');
422 self.applyCompression(E, M, K);
423 end
424 end
425
426 function applyCompression(self, E, M, K)
427 % APPLYCOMPRESSION Apply Courtois decomposition to reduce environment size
428 % This method finds a good partition of the environment states and
429 % creates compressed macro-state networks.
430
431 % Find best partition
432 if E <= 10
433 self.MS = self.findBestPartition(E);
434 else
435 % Beam search for large environments
436 self.MS = self.beamSearchPartition(E);
437 end
438
439 if isempty(self.MS)
440 % No compression possible, use singletons
441 self.MS = cell(E, 1);
442 for i = 1:E
443 self.MS{i} = i;
444 end
445 self.Ecompress = E;
446 return;
447 end
448
449 self.Ecompress = length(self.MS);
450
451 % Apply decomposition/aggregation
452 [p, eps, epsMax, q] = self.ctmc_decompose(self.Eutil, self.MS);
453
454 if eps > epsMax
455 line_warning(mfilename, 'Environment cannot be effectively compressed (eps > epsMax).');
456 end
457
458 % Store compression results
459 self.compressionResult.p = p;
460 self.compressionResult.eps = eps;
461 self.compressionResult.epsMax = epsMax;
462 self.compressionResult.q = q;
463
464 % Update probEnv with macro-state probabilities
465 pMacro = zeros(1, self.Ecompress);
466 for i = 1:self.Ecompress
467 pMacro(i) = sum(p(self.MS{i}));
468 end
469 self.envObj.probEnv = pMacro;
470
471 % Compute micro-state probabilities within each macro-state
472 pmicro = zeros(E, 1);
473 for i = 1:self.Ecompress
474 blockProb = p(self.MS{i});
475 if sum(blockProb) > 0
476 pmicro(self.MS{i}) = blockProb / sum(blockProb);
477 end
478 end
479 self.compressionResult.pmicro = pmicro;
480 self.compressionResult.pMacro = pMacro;
481
482 % Update embedding weights for macro-states
483 Ecomp = self.Ecompress;
484 newEmbweight = zeros(Ecomp, Ecomp);
485 for e = 1:Ecomp
486 sumVal = 0.0;
487 for h = 1:Ecomp
488 if h ~= e
489 sumVal = sumVal + pMacro(h) * self.computeMacroRate(h, e);
490 end
491 end
492 for k = 1:Ecomp
493 if k == e
494 newEmbweight(k, e) = 0;
495 elseif sumVal > 0
496 newEmbweight(k, e) = pMacro(k) * self.computeMacroRate(k, e) / sumVal;
497 end
498 end
499 end
500 self.envObj.probOrig = newEmbweight;
501
502 % Build macro-state networks with weighted-average rates
503 macroEnsemble = cell(self.Ecompress, 1);
504 macroSolvers = cell(self.Ecompress, 1);
505 macroSn = cell(self.Ecompress, 1);
506
507 for i = 1:self.Ecompress
508 % Copy the first micro-state network
509 firstMicro = self.MS{i}(1);
510 macroEnsemble{i} = self.ensemble{firstMicro}.copy();
511
512 % Compute weighted-average rates
513 for m = 1:M
514 for k = 1:K
515 rateSum = 0;
516 for r = 1:length(self.MS{i})
517 microIdx = self.MS{i}(r);
518 w = pmicro(microIdx);
519 rateSum = rateSum + w * self.sn{microIdx}.rates(m, k);
520 end
521
522 % Update service rate
523 jobclass = macroEnsemble{i}.classes{k};
524 station = macroEnsemble{i}.stations{m};
525 if isa(station, 'Queue') || isa(station, 'Delay')
526 if rateSum > 0
527 station.setService(jobclass, Exp(rateSum));
528 end
529 end
530 end
531 end
532
533 macroEnsemble{i}.refreshStruct(true);
534 macroSn{i} = macroEnsemble{i}.getStruct(true);
535
536 % Create solver for macro-state
537 % Use the same solver factory pattern as original
538 macroSolvers{i} = SolverFluid(macroEnsemble{i}, self.solvers{firstMicro}.options);
539 end
540
541 % Replace ensemble and solvers with compressed versions
542 self.ensemble = macroEnsemble;
543 self.solvers = macroSolvers;
544 self.sn = macroSn;
545 end
546
547 function rate = computeMacroRate(self, fromMacro, toMacro)
548 % COMPUTEMACRORATE Compute transition rate between macro-states
549 rate = 0;
550 for i = 1:length(self.MS{fromMacro})
551 mi = self.MS{fromMacro}(i);
552 for j = 1:length(self.MS{toMacro})
553 mj = self.MS{toMacro}(j);
554 rate = rate + self.compressionResult.pmicro(mi) * self.E0(mi, mj);
555 end
556 end
557 end
558
559 function MS = findBestPartition(self, E)
560 % FINDBESTPARTITION Find the best partition for small environments (E <= 10)
561 % Uses exhaustive search over all possible partitions
562
563 % Start with singletons
564 bestMS = cell(E, 1);
565 for i = 1:E
566 bestMS{i} = i;
567 end
568
569 [~, bestEps, bestEpsMax] = self.ctmc_decompose(self.Eutil, bestMS);
570 if isempty(bestEps) || isnan(bestEps)
571 MS = bestMS;
572 return;
573 end
574
575 % Try merging pairs
576 for i = 1:E
577 for j = (i+1):E
578 testMS = cell(E-1, 1);
579 idx = 1;
580 for k = 1:E
581 if k == i
582 testMS{idx} = [i, j];
583 idx = idx + 1;
584 elseif k ~= j
585 testMS{idx} = k;
586 idx = idx + 1;
587 end
588 end
589
590 [~, testEps, testEpsMax] = self.ctmc_decompose(self.Eutil, testMS);
591 if ~isempty(testEps) && ~isnan(testEps) && testEps < bestEps
592 bestEps = testEps;
593 bestEpsMax = testEpsMax;
594 bestMS = testMS;
595 end
596 end
597 end
598
599 MS = bestMS;
600 self.Ecompress = length(MS);
601 end
602
603 function MS = beamSearchPartition(self, E)
604 % BEAMSEARCHPARTITION Beam search for large environments (E > 10)
605
606 B = 3; % Beam width
607 alpha = 0.01; % Coupling threshold
608
609 if isfield(self.options, 'config') && isfield(self.options.config, 'env_alpha')
610 alpha = self.options.config.env_alpha;
611 end
612
613 % Initialize with singletons
614 singletons = cell(E, 1);
615 for i = 1:E
616 singletons{i} = i;
617 end
618 beam = {singletons};
619
620 bestSeen = singletons;
621 [~, bestEps] = self.ctmc_decompose(self.Eutil, bestSeen);
622
623 % Iteratively merge blocks
624 for depth = 1:(E-1)
625 candidates = {};
626
627 for b = 1:length(beam)
628 ms = beam{b};
629 nBlocks = length(ms);
630
631 % Try all pairwise merges
632 for i = 1:nBlocks
633 for j = (i+1):nBlocks
634 % Create merged partition
635 trial = cell(nBlocks - 1, 1);
636 idx = 1;
637 for k = 1:nBlocks
638 if k == i
639 trial{idx} = [ms{i}(:); ms{j}(:)];
640 idx = idx + 1;
641 elseif k ~= j
642 trial{idx} = ms{k};
643 idx = idx + 1;
644 end
645 end
646
647 [~, childEps, childEpsMax] = self.ctmc_decompose(self.Eutil, trial);
648
649 if ~isempty(childEps) && ~isnan(childEps) && childEps > 0
650 cost = childEps - childEpsMax + alpha * depth;
651 candidates{end+1} = {trial, cost};
652
653 if cost < bestEps
654 bestEps = cost;
655 bestSeen = trial;
656 end
657 end
658 end
659 end
660 end
661
662 if isempty(candidates)
663 break;
664 end
665
666 % Sort by cost and keep top B
667 costs = cellfun(@(x) x{2}, candidates);
668 [~, sortIdx] = sort(costs);
669 beam = {};
670 for i = 1:min(B, length(sortIdx))
671 beam{end+1} = candidates{sortIdx(i)}{1};
672 end
673 end
674
675 MS = bestSeen;
676 self.Ecompress = length(MS);
677 end
678
679 function holdTime = computeHoldTime(self, E)
680 % COMPUTEHOLDTIME Compute expected holding times using numerical integration
681 holdTime = zeros(1, E);
682 for k = 1:E
683 % Survival function: 1 - sojournCDF
684 surv = @(t) 1 - self.sojournCdfs{k}(t);
685
686 % Compute upper limit
687 upperLimit = 10;
688 while surv(upperLimit) > 1e-8
689 upperLimit = upperLimit * 2;
690 if upperLimit > 1e6 % safety limit
691 break;
692 end
693 end
694
695 % Simpson's rule integration
696 N = 10000;
697 dt = upperLimit / N;
698 integral = 0;
699 for i = 0:(N-1)
700 t0 = i * dt;
701 t1 = t0 + dt;
702 tmid = (t0 + t1) / 2;
703 % Simpson's rule: (f(a) + 4*f(mid) + f(b)) * h/6
704 integral = integral + (surv(t0) + 4*surv(tmid) + surv(t1)) * dt / 6;
705 end
706 holdTime(k) = integral;
707 end
708 end
709
710 function cdf = computeSojournCdf(self, e, t)
711 % COMPUTESOJOURNCDF Compute sojourn time CDF for environment stage e
712 E = self.getNumberOfModels;
713 surv = 1.0;
714 for h = 1:E
715 if h ~= e
716 surv = surv * (1 - self.transitionCdfs{e,h}(t));
717 end
718 end
719 cdf = 1 - surv;
720 end
721
722 function pre(self, it)
723 % PRE(IT) Delegated to the active analyzer (marginal | statevec).
724 self.analyzerFcn(self, 'pre', it, []);
725 end
726
727 % solves model in stage e
728 function [results_e, runtime] = analyze(self, it, e)
729 % [RESULTS_E, RUNTIME] = ANALYZE(IT, E) Delegated to the active analyzer.
730 [results_e, runtime] = self.analyzerFcn(self, 'analyze', it, e);
731 end
732
733
734 function post(self, it)
735 % POST(IT) Delegated to the active analyzer.
736 self.analyzerFcn(self, 'post', it, []);
737 end
738
739 function finish(self)
740 % FINISH() Delegated to the active analyzer.
741 self.analyzerFcn(self, 'finish', [], []);
742 end
743
744 function name = getName(self)
745 % NAME = GETNAME()
746
747 name = mfilename;
748 end
749
750 function [renvInfGen, stageInfGen, renvEventFilt, stageEventFilt, renvEvents, stageEvents] = getGenerator(self)
751 % [renvInfGen, stageInfGen, renvEventFilt, stageEventFilt, renvEvents, stageEvents] = getGenerator(self)
752 %
753 % Returns the infinitesimal generator matrices for the random environment model.
754 %
755 % Outputs:
756 % renvInfGen - Combined infinitesimal generator for the random environment (flattened)
757 % stageInfGen - Cell array of infinitesimal generators for each stage
758 % renvEventFilt - Cell array (E x E) of event filtration matrices for environment transitions
759 % stageEventFilt - Cell array of event filtration matrices for each stage
760 % renvEvents - Cell array of Event objects for environment transitions
761 % stageEvents - Cell array of synchronization maps for each stage
762
763 E = self.getNumberOfModels;
764 stageInfGen = cell(1,E);
765 stageEventFilt = cell(1,E);
766 stageEvents = cell(1,E);
767 for e=1:E
768 if isa(self.solvers{e},'SolverCTMC')
769 [stageInfGen{e}, stageEventFilt{e}, stageEvents{e}] = self.solvers{e}.getGenerator();
770 else
771 line_error(mfilename,'This method requires SolverENV to be instantiated with the CTMC solver.');
772 end
773 end
774
775 % Get number of states for each stage
776 nstates = cellfun(@(g) size(g, 1), stageInfGen);
777
778 % Get number of phases for each transition distribution
779 nphases = zeros(E, E);
780 for i=1:E
781 for j=1:E
782 if ~isempty(self.env{i,j}) && ~isa(self.env{i,j}, 'Disabled')
783 nphases(i,j) = self.env{i,j}.getNumberOfPhases();
784 else
785 nphases(i,j) = 1;
786 end
787 end
788 end
789 % Adjust diagonal (self-transitions have one less phase in the Kronecker expansion)
790 nphases = nphases - eye(E);
791
792 % Initialize block cell structure for the random environment generator
793 renvInfGen = cell(E,E);
794
795 for e=1:E
796 % Diagonal block: stage infinitesimal generator
797 renvInfGen{e,e} = stageInfGen{e};
798 for h=1:E
799 if h~=e
800 % Off-diagonal blocks: reset matrices (identity with appropriate dimensions)
801 minStates = min(nstates(e), nstates(h));
802 resetMatrix_eh = sparse(nstates(e), nstates(h));
803 for i=1:minStates
804 resetMatrix_eh(i,i) = 1;
805 end
806 renvInfGen{e,h} = resetMatrix_eh;
807 end
808 end
809 end
810
811 % Build environment transition events and expand generator with phase structure
812 renvEvents = cell(1,0);
813 for e=1:E
814 for h=1:E
815 if h~=e
816 % Get D0 (phase generator) for transition from e to h
817 if isempty(self.env{e,h}) || isa(self.env{e,h}, 'Disabled')
818 D0 = zeros(0);
819 else
820 proc = self.env{e,h}.getProcess();
821 D0 = proc{1};
822 end
823 % Kronecker sum with diagonal block
824 renvInfGen{e,e} = krons(renvInfGen{e,e}, D0);
825
826 % Get D1 (completion rate matrix) and initial probability vector pie
827 if isempty(self.env{h,e}) || isa(self.env{h,e}, 'Disabled') || any(isnan(map_pie(self.env{h,e}.getProcess())))
828 pie = ones(1, nphases(h,e));
829 else
830 pie = map_pie(self.env{h,e}.getProcess());
831 end
832
833 if isempty(self.env{e,h}) || isa(self.env{e,h}, 'Disabled')
834 D1 = zeros(0);
835 else
836 proc = self.env{e,h}.getProcess();
837 D1 = proc{2};
838 end
839
840 % Kronecker product for off-diagonal block
841 onePhase = ones(nphases(e,h), 1);
842 kronArg = D1 * onePhase * pie;
843 renvInfGen{e,h} = kron(renvInfGen{e,h}, sparse(kronArg));
844
845 % Create environment transition events
846 for i=1:self.ensemble{e}.getNumberOfNodes
847 renvEvents{1,end+1} = Event(EventType.STAGE, i, NaN, NaN, [e,h]); %#ok<AGROW>
848 end
849
850 % Handle other stages (f != e, f != h)
851 for f=1:E
852 if f~=h && f~=e
853 if isempty(self.env{f,h}) || isa(self.env{f,h}, 'Disabled') || any(isnan(map_pie(self.env{f,h}.getProcess())))
854 pie_fh = ones(1, nphases(f,h));
855 else
856 pie_fh = map_pie(self.env{f,h}.getProcess());
857 end
858 oneVec = ones(nphases(e,h), 1);
859 renvInfGen{e,f} = kron(renvInfGen{e,f}, oneVec * pie_fh);
860 end
861 end
862 end
863 end
864 end
865
866 % Build event filtration matrices for environment transitions
867 % Each renvEventFilt{e,h} isolates transitions from stage e to stage h
868 renvEventFilt = cell(E,E);
869 for e=1:E
870 for h=1:E
871 tmpCell = cell(E,E);
872 for e1=1:E
873 for h1=1:E
874 tmpCell{e1,h1} = renvInfGen{e1,h1};
875 end
876 end
877 % Zero out diagonal blocks (internal stage transitions)
878 for e1=1:E
879 tmpCell{e1,e1} = tmpCell{e1,e1} * 0;
880 end
881 % Zero out off-diagonal blocks that don't match (e,h) transition
882 for e1=1:E
883 for h1=1:E
884 if e~=e1 || h~=h1
885 if e1~=h1 % Only zero out off-diagonal entries
886 tmpCell{e1,h1} = tmpCell{e1,h1} * 0;
887 end
888 end
889 end
890 end
891 renvEventFilt{e,h} = cell2mat(tmpCell);
892 end
893 end
894
895 % Flatten block structure into single matrix and normalize
896 renvInfGen = cell2mat(renvInfGen);
897 renvInfGen = ctmc_makeinfgen(renvInfGen);
898 end
899
900 function varargout = getAvg(varargin)
901 % [QNCLASS, UNCLASS, TNCLASS] = GETAVG()
902 [varargout{1:nargout}] = getEnsembleAvg( varargin{:} );
903 end
904
905 function [QNclass, UNclass, RNclass, TNclass, ANclass, WNclass] = getEnsembleAvg(self)
906 % [QNCLASS, UNCLASS, TNCLASS] = GETENSEMBLEAVG()
907 RNclass=[];
908 ANclass=[];
909 WNclass=[];
910
911 if isfield(self.options,'lang') && strcmp(self.options.lang,'python')
912 [QNclass, UNclass, TNclass] = PYLINE.getEnvAvg(self.envObj, self.options);
913 WNclass = QNclass ./ TNclass;
914 RNclass = NaN*WNclass;
915 ANclass = NaN*TNclass;
916 self.result.Avg.Q = QNclass;
917 self.result.Avg.U = UNclass;
918 self.result.Avg.T = TNclass;
919 return
920 end
921
922 if isempty(self.result) || (isfield(self.options,'force') && self.options.force)
923 if isfield(self.options,'method') && any(strcmpi(self.options.method,{'avg','dec'}))
924 self.solveEnvLimit();
925 else
926 iterate(self);
927 end
928 if isempty(self.result)
929 QNclass=[];
930 UNclass=[];
931 TNclass=[];
932 return
933 end
934 end
935 QNclass = self.result.Avg.Q;
936 UNclass = self.result.Avg.U;
937 TNclass = self.result.Avg.T;
938 WNclass = QNclass ./ TNclass;
939 RNclass = NaN*WNclass;
940 ANclass = NaN*TNclass;
941 end
942
943 function solveEnvLimit(self)
944 % SOLVEENVLIMIT() Closed-form fast/slow random-environment limits.
945 %
946 % These treat the environment (stage) process as either infinitely
947 % fast or infinitely slow relative to the base-model dynamics, and
948 % therefore require no inter-stage coupling iteration:
949 %
950 % 'avg' (fast-environment limit): the base model sees the
951 % stationary-probability-weighted average of the modulated
952 % rates. A single rate-averaged model is built and solved
953 % once. Exact as the stage-switching rate -> Inf.
954 %
955 % 'dec' (slow-environment / quasi-stationary decomposition): each
956 % stage is solved independently in steady state and the
957 % per-stage metrics are averaged with weights probEnv(e).
958 % Exact as the stage-switching rate -> 0.
959 %
960 % Both populate self.result.Avg.{Q,U,T}; for stateful Cache nodes
961 % the aggregated hit/miss ratios are written back onto the stage-1
962 % reference model (self.ensemble{1}), so
963 % self.ensemble{1}.getNodeByName('Cache').getHitRatio() returns the
964 % environment-aggregated value.
965 self.init();
966 E = self.getNumberOfModels;
967 probEnv = self.envObj.probEnv(:);
968 M = self.ensemble{1}.getNumberOfStations;
969 K = self.ensemble{1}.getNumberOfClasses;
970 method = lower(self.options.method);
971
972 Qval = zeros(M,K); Uval = zeros(M,K); Tval = zeros(M,K);
973 nnodes = length(self.ensemble{1}.nodes);
974 cacheIdx = find(cellisa(self.ensemble{1}.nodes, 'Cache'))';
975 cacheHit = cell(1,nnodes);
976 cacheMiss = cell(1,nnodes);
977 cacheHitL = cell(1,nnodes);
978
979 switch method
980 case 'dec'
981 for e = 1:E
982 se = self.solvers{e};
983 se.reset();
984 [Qe,Ue,~,Te] = se.getAvg();
985 Qval = Qval + probEnv(e)*Qe;
986 Uval = Uval + probEnv(e)*Ue;
987 Tval = Tval + probEnv(e)*Te;
988 se.getAvgNodeTable(); % populate cache metrics on stage e
989 for c = cacheIdx
990 cacheNode = self.ensemble{e}.getNodeByIndex(c);
991 [cacheHit, cacheMiss, cacheHitL] = SolverENV.accumCacheMetric(...
992 cacheHit, cacheMiss, cacheHitL, c, cacheNode, probEnv(e));
993 end
994 end
995 case 'avg'
996 avgModel = self.buildRateAveragedModel(probEnv);
997 innerSolver = feval(class(self.solvers{1}), avgModel, self.solvers{1}.options);
998 [Qval,Uval,~,Tval] = innerSolver.getAvg();
999 innerSolver.getAvgNodeTable(); % populate cache metrics
1000 for c = cacheIdx
1001 cacheNode = avgModel.getNodeByIndex(c);
1002 [cacheHit, cacheMiss, cacheHitL] = SolverENV.accumCacheMetric(...
1003 cacheHit, cacheMiss, cacheHitL, c, cacheNode, 1.0);
1004 end
1005 otherwise
1006 line_error(mfilename, sprintf('solveEnvLimit called with unsupported method %s.', method));
1007 end
1008
1009 % Write aggregated cache metrics onto the stage-1 reference model
1010 for c = cacheIdx
1011 refCache = self.ensemble{1}.getNodeByIndex(c);
1012 if ~isempty(cacheHit{c}); refCache.setResultHitProb(cacheHit{c}); end
1013 if ~isempty(cacheMiss{c}); refCache.setResultMissProb(cacheMiss{c}); end
1014 if ~isempty(cacheHitL{c}); refCache.setResultHitProbList(cacheHitL{c}); end
1015 end
1016
1017 self.result.Avg.Q = Qval;
1018 self.result.Avg.U = Uval;
1019 self.result.Avg.T = Tval;
1020 end
1021
1022 function avgModel = buildRateAveragedModel(self, probEnv)
1023 % AVGMODEL = BUILDRATEAVERAGEDMODEL(PROBENV) Fast-environment model.
1024 % Replace every environment-modulated (i.e. stage-varying) station
1025 % rate by its probEnv-weighted average, represented as an
1026 % exponential. Non-modulated parameters keep their original
1027 % distribution, so the base model is preserved exactly outside the
1028 % modulated rates.
1029 E = self.getNumberOfModels;
1030 avgModel = self.ensemble{1}.copy();
1031 M = avgModel.getNumberOfStations;
1032 K = avgModel.getNumberOfClasses;
1033 for i = 1:M
1034 node = avgModel.stations{i};
1035 if isa(node,'Cache') || isa(node,'Sink')
1036 continue % stateful/absorbing nodes carry no service rate
1037 end
1038 for k = 1:K
1039 r = zeros(1,E);
1040 ok = true;
1041 for e = 1:E
1042 r(e) = self.sn{e}.rates(i,k);
1043 end
1044 if any(isnan(r)) || any(r<=0)
1045 continue % disabled for some stage: leave as configured
1046 end
1047 if (max(r)-min(r)) <= 1e-12*max(1,max(r))
1048 continue % not modulated: keep the original distribution
1049 end
1050 ravg = probEnv(:)'*r(:);
1051 if isa(node,'Source')
1052 node.setArrival(avgModel.classes{k}, Exp(ravg));
1053 elseif isa(node,'Queue') || isa(node,'Delay')
1054 node.setService(avgModel.classes{k}, Exp(ravg));
1055 end
1056 end
1057 end
1058 % The copy inherited a cached NetworkStruct from the stage model;
1059 % force a hard rebuild so the averaged rates take effect.
1060 avgModel.refreshStruct(true);
1061 end
1062
1063 function [AvgTable,QT,UT,TT] = getAvgTable(self,keepDisabled)
1064 % [AVGTABLE,QT,UT,TT] = GETAVGTABLE(SELF,KEEPDISABLED)
1065 % Return table of average station metrics
1066
1067 if nargin<2 %if ~exist('keepDisabled','var')
1068 keepDisabled = false;
1069 end
1070
1071 [QN,UN,~,TN] = getAvg(self);
1072 M = size(QN,1);
1073 K = size(QN,2);
1074 Q = self.result.Avg.Q;
1075 U = self.result.Avg.U;
1076 T = self.result.Avg.T;
1077 % Aggregate station/class labels for the stage-1 layout. For LQN
1078 % stages the flat NetworkStruct fields are absent, so labels come
1079 % from the block-diagonal layer networks instead.
1080 [stationLabels, classLabels] = self.aggregateStationClassNames(M, K);
1081 if isempty(QN)
1082 AvgTable = Table();
1083 QT = Table();
1084 UT = Table();
1085 TT = Table();
1086 elseif ~keepDisabled
1087 Qval = []; Uval = []; Tval = [];
1088 JobClass = {};
1089 Station = {};
1090 for ist=1:M
1091 for k=1:K
1092 if any(sum([QN(ist,k),UN(ist,k),TN(ist,k)])>0)
1093 JobClass{end+1,1} = classLabels{k};
1094 Station{end+1,1} = stationLabels{ist};
1095 Qval(end+1) = QN(ist,k);
1096 Uval(end+1) = UN(ist,k);
1097 Tval(end+1) = TN(ist,k);
1098 end
1099 end
1100 end
1101 QLen = Qval(:); % we need to save first in a variable named like the column
1102 QT = Table(Station,JobClass,QLen);
1103 Util = Uval(:); % we need to save first in a variable named like the column
1104 UT = Table(Station,JobClass,Util);
1105 Tput = Tval(:); % we need to save first in a variable named like the column
1106 Station = categorical(Station);
1107 JobClass = categorical(JobClass);
1108 TT = Table(Station,JobClass,Tput);
1109 RespT = QLen ./ Tput;
1110 AvgTable = Table(Station,JobClass,QLen,Util,RespT,Tput);
1111 else
1112 Qval = zeros(M,K); Uval = zeros(M,K);
1113 JobClass = cell(K*M,1);
1114 Station = cell(K*M,1);
1115 for ist=1:M
1116 for k=1:K
1117 JobClass{(ist-1)*K+k} = Q{ist,k}.class.name;
1118 Station{(ist-1)*K+k} = Q{ist,k}.station.name;
1119 Qval((ist-1)*K+k) = QN(ist,k);
1120 Uval((ist-1)*K+k) = UN(ist,k);
1121 Tval((ist-1)*K+k) = TN(ist,k);
1122 end
1123 end
1124 Station = categorical(Station);
1125 JobClass = categorical(JobClass);
1126 QLen = Qval(:); % we need to save first in a variable named like the column
1127 QT = Table(Station,JobClass,QLen);
1128 Util = Uval(:); % we need to save first in a variable named like the column
1129 UT = Table(Station,JobClass,Util);
1130 Tput = Tval(:); % we need to save first in a variable named like the column
1131 TT = Table(Station,JobClass,Tput);
1132 RespT = QLen ./ Tput;
1133 AvgTable = Table(Station,JobClass,QLen,Util,RespT,Tput);
1134 end
1135 end
1136
1137 function envsn = getStruct(self)
1138 E = self.getNumberOfModels;
1139 envsn = cell(1,E);
1140 for e=1:E
1141 envsn{e} = self.ensemble{e}.getStruct;
1142 end
1143 end
1144
1145 function [T, segmentResults] = getSamplePathTable(self, samplePath)
1146 % [T, SEGMENTRESULTS] = GETSAMPLEPATHTABLE(SELF, SAMPLEPATH)
1147 %
1148 % Compute transient performance metrics for a sample path through
1149 % environment states. The method runs transient analysis for each
1150 % segment and extracts initial and final metric values.
1151 %
1152 % Inputs:
1153 % samplePath - Cell array where each row is {stage, duration}
1154 % stage: string (name) or integer (1-based index)
1155 % duration: positive scalar (time spent in stage)
1156 %
1157 % Outputs:
1158 % T - Table with columns: Segment, Stage, Duration, Station, JobClass,
1159 % InitQLen, InitUtil, InitTput, FinalQLen, FinalUtil, FinalTput
1160 % segmentResults - Cell array with detailed transient results per segment
1161 %
1162 % Example:
1163 % samplePath = {'Fast', 5.0; 'Slow', 10.0; 'Fast', 3.0};
1164 % [T, results] = solver.getSamplePathTable(samplePath);
1165
1166 % Validate input
1167 if isempty(samplePath)
1168 line_error(mfilename, 'Sample path cannot be empty.');
1169 end
1170
1171 % Initialize if needed
1172 if isempty(self.envObj.probEnv)
1173 self.init();
1174 end
1175
1176 E = self.getNumberOfModels;
1177 M = self.sn{1}.nstations;
1178 K = self.sn{1}.nclasses;
1179 nSegments = size(samplePath, 1);
1180 segmentResults = cell(nSegments, 1);
1181
1182 % Initialize queue lengths (uniform distribution for closed classes)
1183 Q_current = zeros(M, K);
1184 for k = 1:K
1185 if self.sn{1}.njobs(k) > 0 % closed class
1186 Q_current(:, k) = self.sn{1}.njobs(k) / M;
1187 end
1188 end
1189
1190 % Process each segment
1191 for seg = 1:nSegments
1192 % Resolve stage
1193 stageSpec = samplePath{seg, 1};
1194 duration = samplePath{seg, 2};
1195
1196 if ischar(stageSpec) || isstring(stageSpec)
1197 e = self.envObj.envGraph.findnode(stageSpec);
1198 if e == 0
1199 line_error(mfilename, sprintf('Stage "%s" not found.', stageSpec));
1200 end
1201 stageName = char(stageSpec);
1202 else
1203 e = stageSpec;
1204 if e < 1 || e > E
1205 line_error(mfilename, sprintf('Stage index %d out of range [1, %d].', e, E));
1206 end
1207 stageName = self.envObj.envGraph.Nodes.Name{e};
1208 end
1209
1210 if duration <= 0
1211 line_error(mfilename, 'Duration must be positive.');
1212 end
1213
1214 % Initialize model from current queue lengths
1215 self.ensemble{e}.initFromMarginal(Q_current);
1216
1217 % Set solver timespan
1218 self.solvers{e}.options.timespan = [0, duration];
1219 self.solvers{e}.reset();
1220
1221 % Run transient analysis
1222 [Qt, Ut, Tt] = self.ensemble{e}.getTranHandles;
1223 [QNt, UNt, TNt] = self.solvers{e}.getTranAvg(Qt, Ut, Tt);
1224
1225 % Extract initial and final metrics
1226 initQ = zeros(M, K);
1227 initU = zeros(M, K);
1228 initT = zeros(M, K);
1229 finalQ = zeros(M, K);
1230 finalU = zeros(M, K);
1231 finalT = zeros(M, K);
1232
1233 for i = 1:M
1234 for r = 1:K
1235 Qir = QNt{i, r};
1236 Uir = UNt{i, r};
1237 Tir = TNt{i, r};
1238
1239 if isstruct(Qir) && isfield(Qir, 'metric') && ~isempty(Qir.metric)
1240 initQ(i, r) = Qir.metric(1);
1241 finalQ(i, r) = Qir.metric(end);
1242 end
1243 if isstruct(Uir) && isfield(Uir, 'metric') && ~isempty(Uir.metric)
1244 initU(i, r) = Uir.metric(1);
1245 finalU(i, r) = Uir.metric(end);
1246 end
1247 if isstruct(Tir) && isfield(Tir, 'metric') && ~isempty(Tir.metric)
1248 initT(i, r) = Tir.metric(1);
1249 finalT(i, r) = Tir.metric(end);
1250 end
1251 end
1252 end
1253
1254 % Store segment results
1255 segmentResults{seg}.stage = e;
1256 segmentResults{seg}.stageName = stageName;
1257 segmentResults{seg}.duration = duration;
1258 segmentResults{seg}.QNt = QNt;
1259 segmentResults{seg}.UNt = UNt;
1260 segmentResults{seg}.TNt = TNt;
1261 segmentResults{seg}.initQ = initQ;
1262 segmentResults{seg}.initU = initU;
1263 segmentResults{seg}.initT = initT;
1264 segmentResults{seg}.finalQ = finalQ;
1265 segmentResults{seg}.finalU = finalU;
1266 segmentResults{seg}.finalT = finalT;
1267
1268 % Update current queue lengths for next segment
1269 Q_current = finalQ;
1270 end
1271
1272 % Build output table
1273 Segment = [];
1274 Stage = {};
1275 Duration = [];
1276 Station = {};
1277 JobClass = {};
1278 InitQLen = [];
1279 InitUtil = [];
1280 InitTput = [];
1281 FinalQLen = [];
1282 FinalUtil = [];
1283 FinalTput = [];
1284
1285 for seg = 1:nSegments
1286 res = segmentResults{seg};
1287 for i = 1:M
1288 for r = 1:K
1289 % Only include rows with non-zero metrics
1290 if any([res.initQ(i,r), res.initU(i,r), res.initT(i,r), ...
1291 res.finalQ(i,r), res.finalU(i,r), res.finalT(i,r)] > 0)
1292 Segment(end+1, 1) = seg;
1293 Stage{end+1, 1} = res.stageName;
1294 Duration(end+1, 1) = res.duration;
1295 Station{end+1, 1} = self.sn{1}.nodenames{self.sn{1}.stationToNode(i)};
1296 JobClass{end+1, 1} = self.sn{1}.classnames{r};
1297 InitQLen(end+1, 1) = res.initQ(i, r);
1298 InitUtil(end+1, 1) = res.initU(i, r);
1299 InitTput(end+1, 1) = res.initT(i, r);
1300 FinalQLen(end+1, 1) = res.finalQ(i, r);
1301 FinalUtil(end+1, 1) = res.finalU(i, r);
1302 FinalTput(end+1, 1) = res.finalT(i, r);
1303 end
1304 end
1305 end
1306 end
1307
1308 Stage = categorical(Stage);
1309 Station = categorical(Station);
1310 JobClass = categorical(JobClass);
1311 T = Table(Segment, Stage, Duration, Station, JobClass, ...
1312 InitQLen, InitUtil, InitTput, FinalQLen, FinalUtil, FinalTput);
1313 end
1314 function [allMethods] = listValidMethods(self)
1315 % allMethods = LISTVALIDMETHODS()
1316 % List valid methods for this solver
1317 sn = self.model.getStruct();
1318 allMethods = {'default'};
1319 end
1320
1321 function [stationLabels, classLabels] = aggregateStationClassNames(self, M, K)
1322 % [STATIONLABELS, CLASSLABELS] = AGGREGATESTATIONCLASSNAMES(M,K)
1323 % Aggregate station/class label vectors for the stage-1 result
1324 % layout. For flat NetworkStruct stages these are the usual node
1325 % and class names; for LQN stages the layout is the block-diagonal
1326 % union of the layer networks, so labels are drawn from each layer
1327 % network (prefixed by the layer name to disambiguate).
1328 stationLabels = cell(M,1);
1329 classLabels = cell(K,1);
1330 if isfield(self.sn{1}, 'nstations')
1331 for ist=1:M
1332 stationLabels{ist} = self.sn{1}.nodenames{self.sn{1}.stationToNode(ist)};
1333 end
1334 for k=1:K
1335 classLabels{k} = self.sn{1}.classnames{k};
1336 end
1337 elseif isa(self.ensemble{1}, 'LayeredNetwork')
1338 model = self.ensemble{1};
1339 [Roff, Coff, Msz, Ksz] = model.layerBlocks;
1340 for e=1:length(model.ensemble)
1341 layer = model.ensemble{e};
1342 lname = layer.getName;
1343 for i=1:Msz(e)
1344 stationLabels{Roff(e)+i} = sprintf('%s.%s', lname, layer.stations{i}.name);
1345 end
1346 for r=1:Ksz(e)
1347 classLabels{Coff(e)+r} = sprintf('%s.%s', lname, layer.classes{r}.name);
1348 end
1349 end
1350 else
1351 for ist=1:M
1352 stationLabels{ist} = sprintf('Station%d', ist);
1353 end
1354 for k=1:K
1355 classLabels{k} = sprintf('Class%d', k);
1356 end
1357 end
1358 end
1359
1360 function Q = roundMarginalForDiscreteSolver(self, Q, snRef)
1361 % ROUNDMARGINALFORDISCRETESOLVER Round fractional queue lengths
1362 % to integers using the largest remainder method, preserving
1363 % closed chain populations exactly.
1364 for c = 1:snRef.nchains
1365 chainClasses = find(snRef.chains(c,:) > 0);
1366 njobs_chain = sum(snRef.njobs(chainClasses));
1367 if isinf(njobs_chain)
1368 % Open chain: simple rounding
1369 for idx = 1:length(chainClasses)
1370 k = chainClasses(idx);
1371 for i = 1:size(Q,1)
1372 Q(i,k) = round(Q(i,k));
1373 end
1374 end
1375 else
1376 % Closed chain: largest remainder method
1377 vals = [];
1378 indices = [];
1379 for i = 1:size(Q,1)
1380 for idx = 1:length(chainClasses)
1381 k = chainClasses(idx);
1382 vals(end+1) = Q(i,k);
1383 indices(end+1,:) = [i, k];
1384 end
1385 end
1386 floored = floor(vals);
1387 remainders = vals - floored;
1388 deficit = njobs_chain - sum(floored);
1389 deficit = round(deficit); % ensure integer
1390 if deficit > 0
1391 [~, sortIdx] = sort(remainders, 'descend');
1392 for d = 1:min(deficit, length(sortIdx))
1393 floored(sortIdx(d)) = floored(sortIdx(d)) + 1;
1394 end
1395 end
1396 for j = 1:length(vals)
1397 Q(indices(j,1), indices(j,2)) = floored(j);
1398 end
1399 end
1400 end
1401 end
1402 end
1403
1404 methods (Static)
1405
1406 function [bool, featSupported] = supports(model)
1407 % [BOOL, FEATSUPPORTED] = SUPPORTS(MODEL)
1408
1409 featUsed = model.getUsedLangFeatures();
1410
1411 featSupported = SolverFeatureSet;
1412
1413 % Nodes
1414 featSupported.setTrue('ClassSwitch');
1415 featSupported.setTrue('Delay');
1416 featSupported.setTrue('DelayStation');
1417 featSupported.setTrue('Queue');
1418 featSupported.setTrue('Sink');
1419 featSupported.setTrue('Source');
1420
1421 % Distributions
1422 featSupported.setTrue('Coxian');
1423 featSupported.setTrue('Cox2');
1424 featSupported.setTrue('Erlang');
1425 featSupported.setTrue('Exp');
1426 featSupported.setTrue('HyperExp');
1427
1428 % Sections
1429 featSupported.setTrue('StatelessClassSwitcher'); % Section
1430 featSupported.setTrue('InfiniteServer'); % Section
1431 featSupported.setTrue('SharedServer'); % Section
1432 featSupported.setTrue('Buffer'); % Section
1433 featSupported.setTrue('Dispatcher'); % Section
1434 featSupported.setTrue('Server'); % Section (Non-preemptive)
1435 featSupported.setTrue('JobSink'); % Section
1436 featSupported.setTrue('RandomSource'); % Section
1437 featSupported.setTrue('ServiceTunnel'); % Section
1438
1439 % Scheduling strategy
1440 featSupported.setTrue('SchedStrategy_INF');
1441 featSupported.setTrue('SchedStrategy_PS');
1442 featSupported.setTrue('SchedStrategy_FCFS');
1443 featSupported.setTrue('RoutingStrategy_PROB');
1444 featSupported.setTrue('RoutingStrategy_RAND');
1445 featSupported.setTrue('RoutingStrategy_RROBIN'); % with SolverJMT
1446
1447 % Customer Classes
1448 featSupported.setTrue('ClosedClass');
1449 featSupported.setTrue('OpenClass');
1450
1451 bool = SolverFeatureSet.supports(featSupported, featUsed);
1452 end
1453 end
1454
1455 methods (Static)
1456 % ensemble solver options
1457 function options = defaultOptions()
1458 % OPTIONS = DEFAULTOPTIONS()
1459 options = SolverOptions('ENV');
1460 end
1461
1462 function libs = getLibrariesUsed(sn, options)
1463 % GETLIBRARIESUSED Get list of external libraries used by ENV solver
1464 % ENV uses internal algorithms, no external library attribution needed
1465 libs = {};
1466 end
1467
1468 function [cacheHit, cacheMiss, cacheHitL] = accumCacheMetric(cacheHit, cacheMiss, cacheHitL, c, cacheNode, w)
1469 % Accumulate w-weighted cache hit/miss ratios of node index c
1470 % (used by solveEnvLimit for the 'avg'/'dec' environment limits).
1471 h = full(cacheNode.getHitRatio);
1472 if ~isempty(h)
1473 if isempty(cacheHit{c}); cacheHit{c} = zeros(size(h)); end
1474 cacheHit{c} = cacheHit{c} + w*h;
1475 end
1476 mval = full(cacheNode.getMissRatio);
1477 if ~isempty(mval)
1478 if isempty(cacheMiss{c}); cacheMiss{c} = zeros(size(mval)); end
1479 cacheMiss{c} = cacheMiss{c} + w*mval;
1480 end
1481 hl = cacheNode.getHitRatioByList;
1482 if ~isempty(hl)
1483 hl = full(hl);
1484 if isempty(cacheHitL{c}); cacheHitL{c} = zeros(size(hl)); end
1485 cacheHitL{c} = cacheHitL{c} + w*hl;
1486 end
1487 end
1488 end
1489end
1490
Definition fjtag.m:157
Definition Station.m:245