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