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