1classdef SolverENV < EnsembleSolver
2 % ENV - Ensemble environment solver
for models with random environment changes
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.
9 % @brief Environment solver
for networks in random changing environments
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
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
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
34 % env_model = Environment(stages, transitions); % Define environment
35 % solver = SolverENV(env_model, @SolverMVA, options);
36 % metrics = solver.getEnsembleAvg(); % Environment-averaged metrics
39 % Copyright (c) 2012-2026, Imperial College London
40 % All rights reserved.
44 env; % user-supplied representation of each stage transition
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
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
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);
74 self.setOptions(SolverENV.defaultOptions);
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''');
84 self.ensemble = renv.getEnsemble;
87 for e=1:length(self.env)
88 self.sn{e} = self.ensemble{e}.getStruct;
89 self.setSolver(solverFactory(self.ensemble{e}),e);
92 for e=1:length(self.env)
93 for h=1:length(self.env)
94 self.resetFromMarginal{e,h} = renv.resetFun{e,h};
98 for e=1:length(self.env)
99 for h=1:length(self.env)
100 self.resetEnvRates{e,h} = renv.resetEnvRatesFun{e,h};
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));
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));
121 function setStateDepMethod(self, method)
122 % SETSTATEDEPMETHOD(METHOD) Sets the state-dependent method
124 line_error(mfilename, 'State-dependent method cannot be null or empty.');
126 self.stateDepMethod = method;
129 function setNewMethod(self, flag)
130 % SETNEWMETHOD(FLAG) Enable/disable DTMC-based computation
131 self.newMethod = flag;
134 function setCompression(self, flag)
135 % SETCOMPRESSION(FLAG) Enable/disable Courtois decomposition
136 self.compression = flag;
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)
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)
150 % p - steady-state probability vector
152 % epsMax - max acceptable eps value
153 % q - randomization coefficient
155 % Get decomposition/aggregation method from options
156 if isfield(self.options, 'config') && isfield(self.options.config, 'da')
157 method = self.options.config.da;
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;
171 [p, ~, ~, eps, epsMax, ~, ~, ~, q] = ctmc_courtois(Q, MS);
173 [p, ~, ~, eps, epsMax] = ctmc_kms(Q, MS, numsteps);
174 q = 1.05 * max(max(abs(Q)));
176 [p, ~, ~, ~, eps, epsMax] = ctmc_takahashi(Q, MS, numsteps);
177 q = 1.05 * max(max(abs(Q)));
179 % Multi requires MSS (macro-macro-states), default to singletons
180 nMacro = size(MS, 1);
181 MSS = cell(nMacro, 1);
185 [p, ~, ~, ~, eps, epsMax] = ctmc_multi(Q, MS, MSS);
186 q = 1.05 * max(max(abs(Q)));
188 line_error(mfilename, sprintf(
'Unknown decomposition method: %s', method));
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
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
206 % Check convergence per
class (aligned with JAR structure)
208 % Build QEntry and QExit matrices (M x E) for this class
209 QEntry = zeros(M, E);
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);
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);
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(:));
229 maxDiff = 0; % Handle case where all QEntry values are zero
231 if isnan(maxDiff) || isinf(maxDiff)
232 return % Non-convergence on invalid values
234 if maxDiff >= self.options.iter_tol
235 return % Not converged
242 function runAnalyzer(self)
244 % Run the ensemble solver iteration
245 line_debug('ENV solver starting: nstages=%d, method=%s', self.getNumberOfModels, self.options.method);
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);
251 line_printf('The solver will leverage %s.\n', strjoin(libs, ', '));
252 GlobalConstants.setLibraryAttributionShown(true);
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);
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;
275 self.ServerNum = cell(K, 1);
276 self.SRates = cell(K, 1);
278 self.ServerNum{k} = zeros(M, E);
279 self.SRates{k} = zeros(M, E);
282 self.ServerNum{k}(m, e) = self.sn{e}.nservers(m);
283 self.SRates{k}(m, e) = self.sn{e}.rates(m, k);
288 % Build rate matrix E0
289 self.E0 = zeros(E, E);
292 if ~isa(self.envObj.env{e,h},
'Disabled')
293 self.E0(e, h) = self.envObj.env{e,h}.getRate();
297 self.Eutil = ctmc_makeinfgen(self.E0);
299 % Initialize transition CDFs
300 self.transitionCdfs = cell(E, 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);
307 self.transitionCdfs{e,h} = @(t) 0;
312 % Initialize sojourn CDFs
313 self.sojournCdfs = cell(E, 1);
315 self.sojournCdfs{e} = @(t) self.computeSojournCdf(e, t);
318 % newMethod: Use DTMC-based computation instead of CTMC
319 % Verified numerical integration for Semi-Markov Process DTMC transition probabilities
321 line_debug('ENV using DTMC-based computation (newMethod=true)');
322 self.dtmcP = zeros(E, E);
325 if k == e || isa(self.envObj.env{k,e},
'Disabled')
326 self.dtmcP(k, e) = 0.0;
328 % Compute the upper limit of the sojourn time
331 while self.transitionCdfs{k,e}(T) < 1.0 - epsilon
333 if T > 1e6 % safety limit
337 % Adaptive number of integration intervals based on T
338 N = max(1000, round(T * 100));
344 deltaF = self.transitionCdfs{k,e}(t1) - self.transitionCdfs{k,e}(t0);
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));
353 sumVal = sumVal + deltaF * survival;
355 self.dtmcP(k, e) = sumVal;
360 % Solve DTMC
for stationary distribution
361 dtmcPie = dtmc_solve(self.dtmcP);
363 % Calculate hold times
using numerical integration
364 self.holdTimeMatrix = self.computeHoldTime(E);
366 % Compute steady-state probabilities
370 denomSum = denomSum + dtmcPie(e) * self.holdTimeMatrix(e);
373 pi(k) = dtmcPie(k) * self.holdTimeMatrix(k) / denomSum;
375 self.envObj.probEnv = pi;
377 % Update embedding weights
378 newEmbweight = zeros(E, E);
383 sumVal = sumVal + pi(h) * self.E0(h, e);
388 newEmbweight(k, e) = 0;
391 newEmbweight(k, e) = pi(k) * self.E0(k, e) / sumVal;
396 self.envObj.probOrig = newEmbweight;
399 % Compression: Use Courtois decomposition
for large environments
401 line_debug(
'ENV using compression (Courtois decomposition)');
402 self.applyCompression(E, M, K);
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.
411 % Find best partition
413 self.MS = self.findBestPartition(E);
415 % Beam search
for large environments
416 self.MS = self.beamSearchPartition(E);
420 % No compression possible, use singletons
421 self.MS = cell(E, 1);
429 self.Ecompress = length(self.MS);
431 % Apply decomposition/aggregation
432 [p, eps, epsMax, q] = self.ctmc_decompose(self.Eutil, self.MS);
435 line_warning(mfilename,
'Environment cannot be effectively compressed (eps > epsMax).');
438 % Store compression results
439 self.compressionResult.p = p;
440 self.compressionResult.eps = eps;
441 self.compressionResult.epsMax = epsMax;
442 self.compressionResult.q = q;
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}));
449 self.envObj.probEnv = pMacro;
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);
459 self.compressionResult.pmicro = pmicro;
460 self.compressionResult.pMacro = pMacro;
462 % Update embedding weights
for macro-states
463 Ecomp = self.Ecompress;
464 newEmbweight = zeros(Ecomp, Ecomp);
469 sumVal = sumVal + pMacro(h) * self.computeMacroRate(h, e);
474 newEmbweight(k, e) = 0;
476 newEmbweight(k, e) = pMacro(k) * self.computeMacroRate(k, e) / sumVal;
480 self.envObj.probOrig = newEmbweight;
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);
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();
492 % Compute weighted-average rates
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);
502 % Update service rate
503 jobclass = macroEnsemble{i}.classes{k};
504 station = macroEnsemble{i}.stations{m};
505 if isa(station,
'Queue') || isa(station,
'Delay')
507 station.setService(
jobclass, Exp(rateSum));
513 macroEnsemble{i}.refreshStruct(
true);
514 macroSn{i} = macroEnsemble{i}.getStruct(
true);
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);
521 % Replace ensemble and solvers with compressed versions
522 self.ensemble = macroEnsemble;
523 self.solvers = macroSolvers;
527 function rate = computeMacroRate(self, fromMacro, toMacro)
528 % COMPUTEMACRORATE Compute transition rate between macro-states
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);
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
543 % Start with singletons
549 [~, bestEps, bestEpsMax] = self.ctmc_decompose(self.Eutil, bestMS);
550 if isempty(bestEps) || isnan(bestEps)
558 testMS = cell(E-1, 1);
562 testMS{idx} = [i, j];
570 [~, testEps, testEpsMax] = self.ctmc_decompose(self.Eutil, testMS);
571 if ~isempty(testEps) && ~isnan(testEps) && testEps < bestEps
573 bestEpsMax = testEpsMax;
580 self.Ecompress = length(MS);
583 function MS = beamSearchPartition(self, E)
584 % BEAMSEARCHPARTITION Beam search
for large environments (E > 10)
587 alpha = 0.01; % Coupling threshold
589 if isfield(self.options, 'config') && isfield(self.options.config, 'env_alpha')
590 alpha = self.options.config.env_alpha;
593 % Initialize with singletons
594 singletons = cell(E, 1);
600 bestSeen = singletons;
601 [~, bestEps] = self.ctmc_decompose(self.Eutil, bestSeen);
603 % Iteratively merge blocks
607 for b = 1:length(beam)
609 nBlocks = length(ms);
611 % Try all pairwise merges
613 for j = (i+1):nBlocks
614 % Create merged partition
615 trial = cell(nBlocks - 1, 1);
619 trial{idx} = [ms{i}(:); ms{j}(:)];
627 [~, childEps, childEpsMax] = self.ctmc_decompose(self.Eutil, trial);
629 if ~isempty(childEps) && ~isnan(childEps) && childEps > 0
630 cost = childEps - childEpsMax + alpha * depth;
631 candidates{end+1} = {trial, cost};
642 if isempty(candidates)
646 % Sort by cost and keep top B
647 costs = cellfun(@(x) x{2}, candidates);
648 [~, sortIdx] = sort(costs);
650 for i = 1:min(B, length(sortIdx))
651 beam{end+1} = candidates{sortIdx(i)}{1};
656 self.Ecompress = length(MS);
659 function holdTime = computeHoldTime(self, E)
660 % COMPUTEHOLDTIME Compute expected holding times
using numerical integration
661 holdTime = zeros(1, E);
663 % Survival function: 1 - sojournCDF
664 surv = @(t) 1 - self.sojournCdfs{k}(t);
666 % Compute upper limit
668 while surv(upperLimit) > 1e-8
669 upperLimit = upperLimit * 2;
670 if upperLimit > 1e6 % safety limit
675 % Simpson
's rule integration
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;
686 holdTime(k) = integral;
690 function cdf = computeSojournCdf(self, e, t)
691 % COMPUTESOJOURNCDF Compute sojourn time CDF
for environment stage e
692 E = self.getNumberOfModels;
696 surv = surv * (1 - self.transitionCdfs{e,h}(t));
702 function pre(self, it)
707 if isinf(self.getSolver(e).options.timespan(2))
708 [QN,~,~,~] = self.getSolver(e).getAvg();
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);
718 QN(i,k) = 0; % Default to 0
for NaN/invalid entries
723 self.ensemble{e}.initFromMarginal(QN);
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') = [];
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;
746 function post(self, it)
749 E = self.getNumberOfModels;
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))]
';
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});
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});
784 Qentry = cell(1,E); % average entry queue-length
786 Qentry{e} = zeros(size(Qexit{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});
793 self.solvers{e}.reset();
794 self.ensemble{e}.initFromMarginal(Qentry{e});
797 % Update transition rates between stages
if state-dependent method
798 if isfield(self.options, 'method') && strcmp(self.options.method, 'statedep')
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});
807 % Reinitialize environment after rate updates
812 function finish(self)
815 it = size(self.results,1); % use last iteration
816 E = self.getNumberOfModels;
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});
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});
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))]
';
855 % QE{e,h}(i,r) = self.results{it,e}.Tran.Avg.Q{i,r}(:,1)'*w{e,h}/sum(w{e,h});
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
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
884 function name = getName(self)
890 function [renvInfGen, stageInfGen, renvEventFilt, stageEventFilt, renvEvents, stageEvents] = getGenerator(self)
891 % [renvInfGen, stageInfGen, renvEventFilt, stageEventFilt, renvEvents, stageEvents] = getGenerator(self)
893 % Returns the infinitesimal generator matrices for the random environment model.
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
903 E = self.getNumberOfModels;
904 stageInfGen = cell(1,E);
905 stageEventFilt = cell(1,E);
906 stageEvents = cell(1,E);
908 if isa(self.solvers{e},
'SolverCTMC')
909 [stageInfGen{e}, stageEventFilt{e}, stageEvents{e}] = self.solvers{e}.getGenerator();
911 line_error(mfilename,
'This method requires SolverENV to be instantiated with the CTMC solver.');
915 % Get number of states
for each stage
916 nstates = cellfun(@(g) size(g, 1), stageInfGen);
918 % Get number of phases
for each transition distribution
919 nphases = zeros(E, E);
922 if ~isempty(self.env{i,j}) && ~isa(self.env{i,j},
'Disabled')
923 nphases(i,j) = self.env{i,j}.getNumberOfPhases();
929 % Adjust diagonal (self-transitions have one less phase in the Kronecker expansion)
930 nphases = nphases - eye(E);
932 % Initialize block cell structure
for the random environment generator
933 renvInfGen = cell(E,E);
936 % Diagonal block: stage infinitesimal generator
937 renvInfGen{e,e} = stageInfGen{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));
944 resetMatrix_eh(i,i) = 1;
946 renvInfGen{e,h} = resetMatrix_eh;
951 % Build environment transition events and expand generator with phase structure
952 renvEvents = cell(1,0);
956 % Get D0 (phase generator)
for transition from e to h
957 if isempty(self.env{e,h}) || isa(self.env{e,h},
'Disabled')
960 proc = self.env{e,h}.getProcess();
963 % Kronecker sum with diagonal block
964 renvInfGen{e,e} = krons(renvInfGen{e,e}, D0);
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));
970 pie = map_pie(self.env{h,e}.getProcess());
973 if isempty(self.env{e,h}) || isa(self.env{e,h},
'Disabled')
976 proc = self.env{e,h}.getProcess();
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));
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>
990 % Handle other stages (f != e, f != h)
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));
996 pie_fh = map_pie(self.env{f,h}.getProcess());
998 oneVec = ones(nphases(e,h), 1);
999 renvInfGen{e,f} = kron(renvInfGen{e,f}, oneVec * pie_fh);
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);
1011 tmpCell = cell(E,E);
1014 tmpCell{e1,h1} = renvInfGen{e1,h1};
1017 % Zero out diagonal blocks (internal stage transitions)
1019 tmpCell{e1,e1} = tmpCell{e1,e1} * 0;
1021 % Zero out off-diagonal blocks that don't match (e,h) transition
1025 if e1~=h1 % Only zero out off-diagonal entries
1026 tmpCell{e1,h1} = tmpCell{e1,h1} * 0;
1031 renvEventFilt{e,h} = cell2mat(tmpCell);
1035 % Flatten block structure into single matrix and normalize
1036 renvInfGen = cell2mat(renvInfGen);
1037 renvInfGen = ctmc_makeinfgen(renvInfGen);
1040 function varargout = getAvg(varargin)
1041 % [QNCLASS, UNCLASS, TNCLASS] = GETAVG()
1042 [varargout{1:nargout}] = getEnsembleAvg( varargin{:} );
1045 function [QNclass, UNclass, RNclass, TNclass, ANclass, WNclass] = getEnsembleAvg(self)
1046 % [QNCLASS, UNCLASS, TNCLASS] = GETENSEMBLEAVG()
1051 if isempty(self.result) || (isfield(self.options,'force') && self.options.force)
1053 if isempty(self.result)
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;
1068 function [AvgTable,QT,UT,TT] = getAvgTable(self,keepDisabled)
1069 % [AVGTABLE,QT,UT,TT] = GETAVGTABLE(SELF,KEEPDISABLED)
1070 % Return table of average station metrics
1072 if nargin<2 %if ~exist('keepDisabled','var')
1073 keepDisabled = false;
1076 [QN,UN,~,TN] = getAvg(self);
1079 Q = self.result.Avg.Q;
1080 U = self.result.Avg.U;
1081 T = self.result.Avg.T;
1087 elseif ~keepDisabled
1088 Qval = []; Uval = []; Tval = [];
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);
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);
1113 Qval = zeros(M,K); Uval = zeros(M,K);
1114 JobClass = cell(K*M,1);
1115 Station = cell(K*M,1);
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);
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);
1138 function envsn = getStruct(self)
1139 E = self.getNumberOfModels;
1142 envsn{e} = self.ensemble{e}.getStruct;
1146 function [T, segmentResults] = getSamplePathTable(self, samplePath)
1147 % [T, SEGMENTRESULTS] = GETSAMPLEPATHTABLE(SELF, SAMPLEPATH)
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.
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)
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
1164 % samplePath = {
'Fast', 5.0;
'Slow', 10.0;
'Fast', 3.0};
1165 % [T, results] = solver.getSamplePathTable(samplePath);
1168 if isempty(samplePath)
1169 line_error(mfilename,
'Sample path cannot be empty.');
1172 % Initialize
if needed
1173 if isempty(self.envObj.probEnv)
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);
1183 % Initialize queue lengths (uniform distribution
for closed
classes)
1184 Q_current = zeros(M, K);
1186 if self.sn{1}.njobs(k) > 0 % closed
class
1187 Q_current(:, k) = self.sn{1}.njobs(k) / M;
1191 % Process each segment
1192 for seg = 1:nSegments
1194 stageSpec = samplePath{seg, 1};
1195 duration = samplePath{seg, 2};
1197 if ischar(stageSpec) || isstring(stageSpec)
1198 e = self.envObj.envGraph.findnode(stageSpec);
1200 line_error(mfilename, sprintf(
'Stage "%s" not found.', stageSpec));
1202 stageName = char(stageSpec);
1206 line_error(mfilename, sprintf(
'Stage index %d out of range [1, %d].', e, E));
1208 stageName = self.envObj.envGraph.Nodes.Name{e};
1212 line_error(mfilename,
'Duration must be positive.');
1215 % Initialize model from current queue lengths
1216 self.ensemble{e}.initFromMarginal(Q_current);
1218 % Set solver timespan
1219 self.solvers{e}.options.timespan = [0, duration];
1220 self.solvers{e}.reset();
1222 % Run transient analysis
1223 [Qt, Ut, Tt] = self.ensemble{e}.getTranHandles;
1224 [QNt, UNt, TNt] = self.solvers{e}.getTranAvg(Qt, Ut, Tt);
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);
1240 if isstruct(Qir) && isfield(Qir,
'metric') && ~isempty(Qir.metric)
1241 initQ(i, r) = Qir.metric(1);
1242 finalQ(i, r) = Qir.metric(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);
1248 if isstruct(Tir) && isfield(Tir, 'metric') && ~isempty(Tir.metric)
1249 initT(i, r) = Tir.metric(1);
1250 finalT(i, r) = Tir.metric(end);
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;
1269 % Update current queue lengths
for next segment
1273 % Build output table
1286 for seg = 1:nSegments
1287 res = segmentResults{seg};
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);
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);
1315 function [allMethods] = listValidMethods(self)
1316 % allMethods = LISTVALIDMETHODS()
1317 % List valid methods
for this solver
1318 sn = self.model.getStruct();
1319 allMethods = {
'default'};
1325 function [bool, featSupported] = supports(model)
1326 % [BOOL, FEATSUPPORTED] = SUPPORTS(MODEL)
1328 featUsed = model.getUsedLangFeatures();
1330 featSupported = SolverFeatureSet;
1333 featSupported.setTrue(
'ClassSwitch');
1334 featSupported.setTrue(
'Delay');
1335 featSupported.setTrue(
'DelayStation');
1336 featSupported.setTrue(
'Queue');
1337 featSupported.setTrue(
'Sink');
1338 featSupported.setTrue(
'Source');
1341 featSupported.setTrue(
'Coxian');
1342 featSupported.setTrue(
'Cox2');
1343 featSupported.setTrue(
'Erlang');
1344 featSupported.setTrue(
'Exp');
1345 featSupported.setTrue(
'HyperExp');
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
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
1367 featSupported.setTrue(
'ClosedClass');
1368 featSupported.setTrue(
'OpenClass');
1370 bool = SolverFeatureSet.supports(featSupported, featUsed);
1375 % ensemble solver options
1376 function options = defaultOptions()
1377 % OPTIONS = DEFAULTOPTIONS()
1378 options = SolverOptions('ENV');
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