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 % Enhanced init properties (aligned with JAR)
51 ServerNum; % Cell
array of server counts per
class
52 SRates; % Cell
array of service rates per
class
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
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);
73 self.setOptions(SolverENV.defaultOptions);
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: SMP method enabled via options.method=''smp''');
83 self.ensemble = renv.getEnsemble;
86 for e=1:length(self.env)
87 self.sn{e} = self.ensemble{e}.getStruct;
88 self.setSolver(solverFactory(self.ensemble{e}),e);
91 for e=1:length(self.env)
92 for h=1:length(self.env)
93 self.resetFromMarginal{e,h} = renv.resetFun{e,h};
97 for e=1:length(self.env)
98 for h=1:length(self.env)
99 self.resetEnvRates{e,h} = renv.resetEnvRatesFun{e,h};
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;
117 if hasStateDependentRates
122 if hasStateDependentRates
123 self.stateDepMethod = 'statedep
';
124 line_debug('ENV: auto-detected state-dependent environment rates
');
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.
']);
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));
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));
152 function setStateDepMethod(self, method)
153 % SETSTATEDEPMETHOD(METHOD) Sets the state-dependent method
155 line_error(mfilename, 'State-dependent method cannot be null or empty.
');
157 self.stateDepMethod = method;
160 function setSMPMethod(self, flag)
161 % SETSMPMETHOD(FLAG) Enable/disable DTMC-based computation for Semi-Markov Processes
162 self.SMPMethod = flag;
165 function setCompression(self, flag)
166 % SETCOMPRESSION(FLAG) Enable/disable Courtois decomposition
167 self.compression = flag;
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)
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)
181 % p - steady-state probability vector
183 % epsMax - max acceptable eps value
184 % q - randomization coefficient
186 % Get decomposition/aggregation method from options
187 if isfield(self.options,
'config') && isfield(self.options.config,
'da')
188 method = self.options.config.da;
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;
202 [p, ~, ~, eps, epsMax, ~, ~, ~, q] = ctmc_courtois(Q, MS);
204 [p, ~, ~, eps, epsMax] = ctmc_kms(Q, MS, numsteps);
205 q = 1.05 * max(max(abs(Q)));
207 [p, ~, ~, ~, eps, epsMax] = ctmc_takahashi(Q, MS, numsteps);
208 q = 1.05 * max(max(abs(Q)));
210 % Multi requires MSS (macro-macro-states), default to singletons
211 nMacro = size(MS, 1);
212 MSS = cell(nMacro, 1);
216 [p, ~, ~, ~, eps, epsMax] = ctmc_multi(Q, MS, MSS);
217 q = 1.05 * max(max(abs(Q)));
219 line_error(mfilename, sprintf(
'Unknown decomposition method: %s', method));
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
233 E = self.getNumberOfModels;
234 M = self.ensemble{1}.getNumberOfStations;
235 K = self.ensemble{1}.getNumberOfClasses;
237 % Check convergence per
class (aligned with JAR structure)
239 % Build QEntry and QExit matrices (M x E) for this class
240 QEntry = zeros(M, E);
243 % Skip stages where analysis failed
244 res_curr = self.results{it,e};
245 res_prev = self.results{it-1,e};
247 if ~isempty(res_curr) && isfield(res_curr,
'Tran') ...
248 && isstruct(res_curr.Tran.Avg) && isfield(res_curr.Tran.Avg,
'Q')
249 Qik_curr = res_curr.Tran.Avg.Q{i,k};
250 if isstruct(Qik_curr) && isfield(Qik_curr,
'metric') && ~isempty(Qik_curr.metric)
251 QExit(i,e) = Qik_curr.metric(1);
254 if ~isempty(res_prev) && isfield(res_prev, 'Tran') ...
255 && isstruct(res_prev.Tran.Avg) && isfield(res_prev.Tran.Avg, 'Q')
256 Qik_prev = res_prev.Tran.Avg.Q{i,k};
257 if isstruct(Qik_prev) && isfield(Qik_prev,
'metric') && ~isempty(Qik_prev.metric)
258 QEntry(i,e) = Qik_prev.metric(1);
264 % Compute max relative absolute difference using maxpe
265 % maxpe computes max(abs(1 - approx./exact)) = max(abs((approx-exact)./exact))
266 % This matches JAR's Matrix.maxAbsDiff() implementation
267 maxDiff = maxpe(QExit(:), QEntry(:));
269 maxDiff = 0; % Handle case where all QEntry values are zero
271 if isnan(maxDiff) || isinf(maxDiff)
272 return % Non-convergence on invalid values
274 if maxDiff >= self.options.iter_tol
275 return % Not converged
278 line_debug('ENV converged: iteration %d, all
classes within tolerance (iter_tol=%e)', it, self.options.iter_tol);
283 function runAnalyzer(self)
285 % Run the ensemble solver iteration
286 line_debug('ENV: runAnalyzer starting (nstages=%d, method=%s)', self.getNumberOfModels, self.options.method);
288 % Show library attribution if verbose and not yet shown
289 if self.options.verbose ~= VerboseLevel.SILENT && ~GlobalConstants.isLibraryAttributionShown()
290 libs = SolverENV.getLibrariesUsed([], self.options);
292 line_printf('The solver will leverage %s.\n', strjoin(libs, ', '));
293 GlobalConstants.setLibraryAttributionShown(true);
302 % Initialize the environment solver with enhanced data structures
303 % aligned with JAR SolverEnv implementation
304 line_debug('ENV init: initializing environment data structures');
305 options = self.options;
306 if isfield(options,'seed')
307 Solver.resetRandomGeneratorSeed(options.seed);
311 % Initialize ServerNum and SRates matrices (aligned with JAR)
312 E = self.getNumberOfModels;
313 M = self.sn{1}.nstations;
314 K = self.sn{1}.nclasses;
316 self.ServerNum = cell(K, 1);
317 self.SRates = cell(K, 1);
319 self.ServerNum{k} = zeros(M, E);
320 self.SRates{k} = zeros(M, E);
323 self.ServerNum{k}(m, e) = self.sn{e}.nservers(m);
324 self.SRates{k}(m, e) = self.sn{e}.rates(m, k);
329 line_debug(
'ENV init: %d stages, %d stations, %d classes', E, M, K);
331 % Build rate matrix E0
332 self.E0 = zeros(E, E);
335 if ~isa(self.envObj.env{e,h},
'Disabled')
336 self.E0(e, h) = self.envObj.env{e,h}.getRate();
340 self.Eutil = ctmc_makeinfgen(self.E0);
342 % Initialize transition CDFs
343 self.transitionCdfs = cell(E, E);
346 if ~isa(self.envObj.env{e,h},
'Disabled')
347 envDist = self.envObj.env{e,h};
348 self.transitionCdfs{e,h} = @(t) envDist.evalCDF(t);
350 self.transitionCdfs{e,h} = @(t) 0;
355 % Initialize sojourn CDFs
356 self.sojournCdfs = cell(E, 1);
358 self.sojournCdfs{e} = @(t) self.computeSojournCdf(e, t);
361 % SMPMethod: Use DTMC-based computation instead of CTMC for Semi-Markov Processes
362 % Verified numerical integration for Semi-Markov Process DTMC transition probabilities
364 line_debug('ENV init: using DTMC-based computation for Semi-Markov Processes (SMPMethod=true)');
365 self.dtmcP = zeros(E, E);
368 if k == e || isa(self.envObj.env{k,e},
'Disabled')
369 self.dtmcP(k, e) = 0.0;
371 % Compute the upper limit of the sojourn time
374 while self.transitionCdfs{k,e}(T) < 1.0 - epsilon
376 if T > 1e6 % safety limit
380 % Adaptive number of integration intervals based on T
381 N = max(1000, round(T * 100));
387 deltaF = self.transitionCdfs{k,e}(t1) - self.transitionCdfs{k,e}(t0);
390 if h ~= k && h ~= e && ~isa(self.envObj.env{k,h},
'Disabled')
391 % Use midpoint
for better accuracy
392 tmid = (t0 + t1) / 2.0;
393 survival = survival * (1.0 - self.envObj.env{k,h}.evalCDF(tmid));
396 sumVal = sumVal + deltaF * survival;
398 self.dtmcP(k, e) = sumVal;
403 line_debug(
'ENV init: DTMC transition matrix computed, solving for stationary distribution');
404 % Solve DTMC
for stationary distribution
405 dtmcPie = dtmc_solve(self.dtmcP);
407 % Calculate hold times
using numerical integration
408 self.holdTimeMatrix = self.computeHoldTime(E);
410 % Compute steady-state probabilities
414 denomSum = denomSum + dtmcPie(e) * self.holdTimeMatrix(e);
417 pi(k) = dtmcPie(k) * self.holdTimeMatrix(k) / denomSum;
419 self.envObj.probEnv = pi;
421 % Update embedding weights
422 newEmbweight = zeros(E, E);
427 sumVal = sumVal + pi(h) * self.E0(h, e);
432 newEmbweight(k, e) = 0;
435 newEmbweight(k, e) = pi(k) * self.E0(k, e) / sumVal;
440 self.envObj.probOrig = newEmbweight;
443 % Compression: Use Courtois decomposition
for large environments
445 line_debug(
'ENV init: using compression (Courtois decomposition) for %d stages', E);
446 self.applyCompression(E, M, K);
450 function applyCompression(self, E, M, K)
451 % APPLYCOMPRESSION Apply Courtois decomposition to reduce environment size
452 % This method finds a good partition of the environment states and
453 % creates compressed macro-state networks.
455 % Find best partition
457 self.MS = self.findBestPartition(E);
459 % Beam search
for large environments
460 self.MS = self.beamSearchPartition(E);
464 % No compression possible, use singletons
465 self.MS = cell(E, 1);
473 self.Ecompress = length(self.MS);
474 line_debug(
'ENV compression: %d stages compressed to %d macro-states', E, self.Ecompress);
476 % Apply decomposition/aggregation
477 [p, eps, epsMax, q] = self.ctmc_decompose(self.Eutil, self.MS);
480 line_warning(mfilename,
'Environment cannot be effectively compressed (eps > epsMax).');
483 % Store compression results
484 self.compressionResult.p = p;
485 self.compressionResult.eps = eps;
486 self.compressionResult.epsMax = epsMax;
487 self.compressionResult.q = q;
489 % Update probEnv with macro-state probabilities
490 pMacro = zeros(1, self.Ecompress);
491 for i = 1:self.Ecompress
492 pMacro(i) = sum(p(self.MS{i}));
494 self.envObj.probEnv = pMacro;
496 % Compute micro-state probabilities within each macro-state
497 pmicro = zeros(E, 1);
498 for i = 1:self.Ecompress
499 blockProb = p(self.MS{i});
500 if sum(blockProb) > 0
501 pmicro(self.MS{i}) = blockProb / sum(blockProb);
504 self.compressionResult.pmicro = pmicro;
505 self.compressionResult.pMacro = pMacro;
507 % Update embedding weights
for macro-states
508 Ecomp = self.Ecompress;
509 newEmbweight = zeros(Ecomp, Ecomp);
514 sumVal = sumVal + pMacro(h) * self.computeMacroRate(h, e);
519 newEmbweight(k, e) = 0;
521 newEmbweight(k, e) = pMacro(k) * self.computeMacroRate(k, e) / sumVal;
525 self.envObj.probOrig = newEmbweight;
527 % Build macro-state networks with weighted-average rates
528 macroEnsemble = cell(self.Ecompress, 1);
529 macroSolvers = cell(self.Ecompress, 1);
530 macroSn = cell(self.Ecompress, 1);
532 for i = 1:self.Ecompress
533 % Copy the first micro-state network
534 firstMicro = self.MS{i}(1);
535 macroEnsemble{i} = self.ensemble{firstMicro}.copy();
537 % Compute weighted-average rates
541 for r = 1:length(self.MS{i})
542 microIdx = self.MS{i}(r);
543 w = pmicro(microIdx);
544 rateSum = rateSum + w * self.sn{microIdx}.rates(m, k);
547 % Update service rate
548 jobclass = macroEnsemble{i}.classes{k};
549 station = macroEnsemble{i}.stations{m};
550 if isa(station,
'Queue') || isa(station,
'Delay')
552 station.setService(
jobclass, Exp(rateSum));
558 macroEnsemble{i}.refreshStruct(
true);
559 macroSn{i} = macroEnsemble{i}.getStruct(
true);
561 % Create solver
for macro-state
562 % Use the same solver factory pattern as original
563 macroSolvers{i} = SolverFluid(macroEnsemble{i}, self.solvers{firstMicro}.options);
566 % Replace ensemble and solvers with compressed versions
567 self.ensemble = macroEnsemble;
568 self.solvers = macroSolvers;
572 function rate = computeMacroRate(self, fromMacro, toMacro)
573 % COMPUTEMACRORATE Compute transition rate between macro-states
575 for i = 1:length(self.MS{fromMacro})
576 mi = self.MS{fromMacro}(i);
577 for j = 1:length(self.MS{toMacro})
578 mj = self.MS{toMacro}(j);
579 rate = rate + self.compressionResult.pmicro(mi) * self.E0(mi, mj);
584 function MS = findBestPartition(self, E)
585 % FINDBESTPARTITION Find the best partition
for small environments (E <= 10)
586 % Uses exhaustive search over all possible partitions
588 % Start with singletons
594 [~, bestEps, bestEpsMax] = self.ctmc_decompose(self.Eutil, bestMS);
595 if isempty(bestEps) || isnan(bestEps)
603 testMS = cell(E-1, 1);
607 testMS{idx} = [i, j];
615 [~, testEps, testEpsMax] = self.ctmc_decompose(self.Eutil, testMS);
616 if ~isempty(testEps) && ~isnan(testEps) && testEps < bestEps
618 bestEpsMax = testEpsMax;
625 self.Ecompress = length(MS);
628 function MS = beamSearchPartition(self, E)
629 % BEAMSEARCHPARTITION Beam search
for large environments (E > 10)
632 alpha = 0.01; % Coupling threshold
634 if isfield(self.options, 'config') && isfield(self.options.config, 'env_alpha')
635 alpha = self.options.config.env_alpha;
638 % Initialize with singletons
639 singletons = cell(E, 1);
645 bestSeen = singletons;
646 [~, bestEps] = self.ctmc_decompose(self.Eutil, bestSeen);
648 % Iteratively merge blocks
652 for b = 1:length(beam)
654 nBlocks = length(ms);
656 % Try all pairwise merges
658 for j = (i+1):nBlocks
659 % Create merged partition
660 trial = cell(nBlocks - 1, 1);
664 trial{idx} = [ms{i}(:); ms{j}(:)];
672 [~, childEps, childEpsMax] = self.ctmc_decompose(self.Eutil, trial);
674 if ~isempty(childEps) && ~isnan(childEps) && childEps > 0
675 cost = childEps - childEpsMax + alpha * depth;
676 candidates{end+1} = {trial, cost};
687 if isempty(candidates)
691 % Sort by cost and keep top B
692 costs = cellfun(@(x) x{2}, candidates);
693 [~, sortIdx] = sort(costs);
695 for i = 1:min(B, length(sortIdx))
696 beam{end+1} = candidates{sortIdx(i)}{1};
701 self.Ecompress = length(MS);
704 function holdTime = computeHoldTime(self, E)
705 % COMPUTEHOLDTIME Compute expected holding times
using numerical integration
706 holdTime = zeros(1, E);
708 % Survival function: 1 - sojournCDF
709 surv = @(t) 1 - self.sojournCdfs{k}(t);
711 % Compute upper limit
713 while surv(upperLimit) > 1e-8
714 upperLimit = upperLimit * 2;
715 if upperLimit > 1e6 % safety limit
720 % Simpson
's rule integration
727 tmid = (t0 + t1) / 2;
728 % Simpson's rule: (f(a) + 4*f(mid) + f(b)) * h/6
729 integral = integral + (surv(t0) + 4*surv(tmid) + surv(t1)) * dt / 6;
731 holdTime(k) = integral;
735 function cdf = computeSojournCdf(self, e, t)
736 % COMPUTESOJOURNCDF Compute sojourn time CDF
for environment stage e
737 E = self.getNumberOfModels;
741 surv = surv * (1 - self.transitionCdfs{e,h}(t));
747 function pre(self, it)
749 line_debug(
'ENV pre: iteration %d', it);
753 if isinf(self.getSolver(e).options.timespan(2))
754 [QN,~,~,~] = self.getSolver(e).getAvg();
756 [QNt,~,~] = self.getSolver(e).getTranAvg();
757 % Handle
case where getTranAvg returns NaN
for disabled/empty results
758 QN = zeros(size(QNt));
759 for i = 1:size(QNt,1)
760 for k = 1:size(QNt,2)
761 if isstruct(QNt{i,k}) && isfield(QNt{i,k},
'metric')
762 QN(i,k) = QNt{i,k}.metric(end);
764 QN(i,k) = 0; % Default to 0
for NaN/invalid entries
769 if ~isa(self.solvers{e},
'SolverFluid')
770 QN = self.roundMarginalForDiscreteSolver(QN, self.sn{e});
772 self.ensemble{e}.initFromMarginal(QN);
774 % Skip pre-initialization for stages where getAvg fails
780 % solves model in stage e
781 function [results_e, runtime] = analyze(self, it, e)
782 % [RESULTS_E, RUNTIME] = ANALYZE(IT, E)
783 line_debug('ENV analyze: iteration %d, stage %d (%s)', it, e, class(self.solvers{e}));
784 results_e =
struct();
785 results_e.(
'Tran') =
struct();
786 results_e.Tran.(
'Avg') = [];
791 [Qt,Ut,Tt] = self.ensemble{e}.getTranHandles;
792 self.solvers{e}.reset();
793 [QNt,UNt,TNt] = self.solvers{e}.getTranAvg(Qt,Ut,Tt);
794 results_e.Tran.Avg.Q = QNt;
795 results_e.Tran.Avg.U = UNt;
796 results_e.Tran.Avg.T = TNt;
798 % Skip analysis
for stages where transient analysis fails
803 function post(self, it)
805 line_debug(
'ENV post: iteration %d, computing exit metrics and entry marginals', it);
806 E = self.getNumberOfModels;
807 M = self.ensemble{1}.getNumberOfStations;
808 K = self.ensemble{1}.getNumberOfClasses;
810 if isempty(self.results{it,e}) || ~isfield(self.results{it,e},
'Tran') ...
811 || ~isstruct(self.results{it,e}.Tran.Avg) || ~isfield(self.results{it,e}.Tran.Avg,
'Q')
813 Qexit{e,h} = zeros(M, K);
814 Uexit{e,h} = zeros(M, K);
815 Texit{e,h} = zeros(M, K);
820 Qexit{e,h} = zeros(size(self.results{it,e}.Tran.Avg.Q));
821 Uexit{e,h} = zeros(size(self.results{it,e}.Tran.Avg.U));
822 Texit{e,h} = zeros(size(self.results{it,e}.Tran.Avg.T));
823 for i=1:size(self.results{it,e}.Tran.Avg.Q,1)
824 for r=1:size(self.results{it,e}.Tran.Avg.Q,2)
825 Qir = self.results{it,e}.Tran.Avg.Q{i,r};
826 % Check
if result
is a valid
struct with required fields
827 if isstruct(Qir) && isfield(Qir,
't') && isfield(Qir,
'metric') && ~isempty(Qir.t)
828 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))]
';
830 Qexit{e,h}(i,r) = Qir.metric'*w{e,h}/sum(w{e,h});
831 Uir = self.results{it,e}.Tran.Avg.U{i,r};
832 if isstruct(Uir) && isfield(Uir,
'metric') && ~isempty(Uir.metric)
833 Uexit{e,h}(i,r) = Uir.metric
'*w{e,h}/sum(w{e,h});
835 Tir = self.results{it,e}.Tran.Avg.T{i,r};
836 if isstruct(Tir) && isfield(Tir, 'metric
') && ~isempty(Tir.metric)
837 Texit{e,h}(i,r) = Tir.metric'*w{e,h}/sum(w{e,h});
852 Qentry = cell(1,E); % average entry queue-length
854 % Skip stages where analysis failed (no valid Tran results)
855 if isempty(self.results{it,e}) || ~isfield(self.results{it,e}, 'Tran') ...
856 || ~isstruct(self.results{it,e}.Tran.Avg) || ~isfield(self.results{it,e}.Tran.Avg, 'Q')
859 Qentry{e} = zeros(size(Qexit{e}));
861 % probability of coming from h to e \times resetFun(Qexit from h to e
862 if self.envObj.probOrig(h,e) > 0
863 Qentry{e} = Qentry{e} + self.envObj.probOrig(h,e) * self.resetFromMarginal{h,e}(Qexit{h,e});
866 if ~isa(self.solvers{e},
'SolverFluid')
867 Qentry{e} = self.roundMarginalForDiscreteSolver(Qentry{e}, self.sn{e});
869 self.solvers{e}.reset();
870 self.ensemble{e}.initFromMarginal(Qentry{e});
873 % Update transition rates between stages
if state-dependent method
874 % Auto-detected or manually specified via options.method='statedep'
875 if strcmp(self.stateDepMethod, 'statedep') || ...
876 (isfield(self.options, 'method') && strcmp(self.options.method, 'statedep'))
877 line_debug('ENV post: updating state-dependent environment transition rates');
880 if ~isa(self.envObj.env{e,h},
'Disabled') && ~isempty(self.resetEnvRates{e,h})
881 self.envObj.env{e,h} = self.resetEnvRates{e,h}(...
882 self.envObj.env{e,h}, Qexit{e,h}, Uexit{e,h}, Texit{e,h});
886 % Reinitialize environment after rate updates
891 function finish(self)
893 line_debug('ENV finish: computing environment-averaged performance metrics');
894 it = size(self.results,1); % use last iteration
895 E = self.getNumberOfModels;
896 M = self.ensemble{1}.getNumberOfStations;
897 K = self.ensemble{1}.getNumberOfClasses;
899 QExit{e}=zeros(M, K);
900 UExit{e}=zeros(M, K);
901 TExit{e}=zeros(M, K);
902 if it>0 && ~isempty(self.results{it,e}) && isfield(self.results{it,e},
'Tran') ...
903 && isstruct(self.results{it,e}.Tran.Avg) && isfield(self.results{it,e}.Tran.Avg,
'Q')
904 for i=1:size(self.results{it,e}.Tran.Avg.Q,1)
905 for r=1:size(self.results{it,e}.Tran.Avg.Q,2)
906 Qir = self.results{it,e}.Tran.Avg.Q{i,r};
907 % Check
if result
is a valid
struct with required fields
908 if isstruct(Qir) && isfield(Qir,
't') && isfield(Qir,
'metric') && ~isempty(Qir.t)
909 w{e} = [0, map_cdf(self.envObj.holdTime{e}, Qir.t(2:end)) - map_cdf(self.envObj.holdTime{e}, Qir.t(1:end-1))]
';
910 QExit{e}(i,r) = Qir.metric'*w{e}/sum(w{e});
911 Uir = self.results{it,e}.Tran.Avg.U{i,r};
912 if isstruct(Uir) && isfield(Uir,
'metric') && ~isempty(Uir.metric)
913 UExit{e}(i,r) = Uir.metric
'*w{e}/sum(w{e});
917 Tir = self.results{it,e}.Tran.Avg.T{i,r};
918 if isstruct(Tir) && isfield(Tir, 'metric
') && ~isempty(Tir.metric)
919 TExit{e}(i,r) = Tir.metric'*w{e}/sum(w{e});
932 % QE{e,h} = zeros(size(self.results{it,e}.Tran.Avg.Q));
933 %
for i=1:size(self.results{it,e}.Tran.Avg.Q,1)
934 %
for r=1:size(self.results{it,e}.Tran.Avg.Q,2)
935 % 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))]
';
937 % QE{e,h}(i,r) = self.results{it,e}.Tran.Avg.Q{i,r}(:,1)'*w{e,h}/sum(w{e,h});
950 Qval = Qval + self.envObj.probEnv(e) * QExit{e}; % to check
951 Uval = Uval + self.envObj.probEnv(e) * UExit{e}; % to check
952 Tval = Tval + self.envObj.probEnv(e) * TExit{e}; % to check
954 self.result.Avg.Q = Qval;
955 % self.result.Avg.R = R;
956 % self.result.Avg.X = X;
957 self.result.Avg.U = Uval;
958 self.result.Avg.T = Tval;
959 % self.result.Avg.C = C;
960 %self.result.runtime = runtime;
961 %
if self.options.verbose
966 function name = getName(self)
972 function [renvInfGen, stageInfGen, renvEventFilt, stageEventFilt, renvEvents, stageEvents] = getGenerator(self)
973 % [renvInfGen, stageInfGen, renvEventFilt, stageEventFilt, renvEvents, stageEvents] = getGenerator(self)
975 % Returns the infinitesimal generator matrices for the random environment model.
978 % renvInfGen - Combined infinitesimal generator for the random environment (flattened)
979 % stageInfGen - Cell
array of infinitesimal generators for each stage
980 % renvEventFilt - Cell
array (E x E) of event filtration matrices for environment transitions
981 % stageEventFilt - Cell
array of event filtration matrices for each stage
982 % renvEvents - Cell
array of Event objects for environment transitions
983 % stageEvents - Cell
array of synchronization maps for each stage
985 E = self.getNumberOfModels;
986 stageInfGen = cell(1,E);
987 stageEventFilt = cell(1,E);
988 stageEvents = cell(1,E);
990 if isa(self.solvers{e},
'SolverCTMC')
991 [stageInfGen{e}, stageEventFilt{e}, stageEvents{e}] = self.solvers{e}.getGenerator();
993 line_error(mfilename,
'This method requires SolverENV to be instantiated with the CTMC solver.');
997 % Get number of states
for each stage
998 nstates = cellfun(@(g) size(g, 1), stageInfGen);
1000 % Get number of phases
for each transition distribution
1001 nphases = zeros(E, E);
1004 if ~isempty(self.env{i,j}) && ~isa(self.env{i,j},
'Disabled')
1005 nphases(i,j) = self.env{i,j}.getNumberOfPhases();
1011 % Adjust diagonal (self-transitions have one less phase in the Kronecker expansion)
1012 nphases = nphases - eye(E);
1014 % Initialize block cell structure
for the random environment generator
1015 renvInfGen = cell(E,E);
1018 % Diagonal block: stage infinitesimal generator
1019 renvInfGen{e,e} = stageInfGen{e};
1022 % Off-diagonal blocks: reset matrices (identity with appropriate dimensions)
1023 minStates = min(nstates(e), nstates(h));
1024 resetMatrix_eh = sparse(nstates(e), nstates(h));
1026 resetMatrix_eh(i,i) = 1;
1028 renvInfGen{e,h} = resetMatrix_eh;
1033 % Build environment transition events and expand generator with phase structure
1034 renvEvents = cell(1,0);
1038 % Get D0 (phase generator)
for transition from e to h
1039 if isempty(self.env{e,h}) || isa(self.env{e,h},
'Disabled')
1042 proc = self.env{e,h}.getProcess();
1045 % Kronecker sum with diagonal block
1046 renvInfGen{e,e} = krons(renvInfGen{e,e}, D0);
1048 % Get D1 (completion rate matrix) and initial probability vector pie
1049 if isempty(self.env{h,e}) || isa(self.env{h,e},
'Disabled') || any(isnan(map_pie(self.env{h,e}.getProcess())))
1050 pie = ones(1, nphases(h,e));
1052 pie = map_pie(self.env{h,e}.getProcess());
1055 if isempty(self.env{e,h}) || isa(self.env{e,h},
'Disabled')
1058 proc = self.env{e,h}.getProcess();
1062 % Kronecker product
for off-diagonal block
1063 onePhase = ones(nphases(e,h), 1);
1064 kronArg = D1 * onePhase * pie;
1065 renvInfGen{e,h} = kron(renvInfGen{e,h}, sparse(kronArg));
1067 % Create environment transition events
1068 for i=1:self.ensemble{e}.getNumberOfNodes
1069 renvEvents{1,end+1} = Event(EventType.STAGE, i, NaN, NaN, [e,h]); %#ok<AGROW>
1072 % Handle other stages (f != e, f != h)
1075 if isempty(self.env{f,h}) || isa(self.env{f,h},
'Disabled') || any(isnan(map_pie(self.env{f,h}.getProcess())))
1076 pie_fh = ones(1, nphases(f,h));
1078 pie_fh = map_pie(self.env{f,h}.getProcess());
1080 oneVec = ones(nphases(e,h), 1);
1081 renvInfGen{e,f} = kron(renvInfGen{e,f}, oneVec * pie_fh);
1088 % Build
event filtration matrices
for environment transitions
1089 % Each renvEventFilt{e,h} isolates transitions from stage e to stage h
1090 renvEventFilt = cell(E,E);
1093 tmpCell = cell(E,E);
1096 tmpCell{e1,h1} = renvInfGen{e1,h1};
1099 % Zero out diagonal blocks (internal stage transitions)
1101 tmpCell{e1,e1} = tmpCell{e1,e1} * 0;
1103 % Zero out off-diagonal blocks that don't match (e,h) transition
1107 if e1~=h1 % Only zero out off-diagonal entries
1108 tmpCell{e1,h1} = tmpCell{e1,h1} * 0;
1113 renvEventFilt{e,h} = cell2mat(tmpCell);
1117 % Flatten block structure into single matrix and normalize
1118 renvInfGen = cell2mat(renvInfGen);
1119 renvInfGen = ctmc_makeinfgen(renvInfGen);
1122 function varargout = getAvg(varargin)
1123 % [QNCLASS, UNCLASS, TNCLASS] = GETAVG()
1124 [varargout{1:nargout}] = getEnsembleAvg( varargin{:} );
1127 function [QNclass, UNclass, RNclass, TNclass, ANclass, WNclass] = getEnsembleAvg(self)
1128 % [QNCLASS, UNCLASS, TNCLASS] = GETENSEMBLEAVG()
1133 if isempty(self.result) || (isfield(self.options,'force') && self.options.force)
1135 if isempty(self.result)
1142 QNclass = self.result.Avg.Q;
1143 UNclass = self.result.Avg.U;
1144 TNclass = self.result.Avg.T;
1145 WNclass = QNclass ./ TNclass;
1146 RNclass = NaN*WNclass;
1147 ANclass = NaN*TNclass;
1150 function [AvgTable,QT,UT,TT] = getAvgTable(self,keepDisabled)
1151 % [AVGTABLE,QT,UT,TT] = GETAVGTABLE(SELF,KEEPDISABLED)
1152 % Return table of average station metrics
1154 if nargin<2 %if ~exist('keepDisabled','var')
1155 keepDisabled = false;
1158 [QN,UN,~,TN] = getAvg(self);
1161 Q = self.result.Avg.Q;
1162 U = self.result.Avg.U;
1163 T = self.result.Avg.T;
1169 elseif ~keepDisabled
1170 Qval = []; Uval = []; Tval = [];
1175 if any(sum([QN(ist,k),UN(ist,k),TN(ist,k)])>0)
1176 JobClass{end+1,1} = self.sn{1}.classnames{k};
1177 Station{end+1,1} = self.sn{1}.nodenames{self.sn{1}.stationToNode(ist)};
1178 Qval(end+1) = QN(ist,k);
1179 Uval(end+1) = UN(ist,k);
1180 Tval(end+1) = TN(ist,k);
1184 QLen = Qval(:); % we need to save first in a variable named like the column
1185 QT = Table(Station,JobClass,QLen);
1186 Util = Uval(:); % we need to save first in a variable named like the column
1187 UT = Table(Station,JobClass,Util);
1188 Tput = Tval(:); % we need to save first in a variable named like the column
1189 Station = categorical(Station);
1190 JobClass = categorical(JobClass);
1191 TT = Table(Station,JobClass,Tput);
1192 RespT = QLen ./ Tput;
1193 AvgTable = Table(Station,JobClass,QLen,Util,RespT,Tput);
1195 Qval = zeros(M,K); Uval = zeros(M,K);
1196 JobClass = cell(K*M,1);
1197 Station = cell(K*M,1);
1200 JobClass{(ist-1)*K+k} = Q{ist,k}.class.name;
1201 Station{(ist-1)*K+k} = Q{ist,k}.station.name;
1202 Qval((ist-1)*K+k) = QN(ist,k);
1203 Uval((ist-1)*K+k) = UN(ist,k);
1204 Tval((ist-1)*K+k) = TN(ist,k);
1207 Station = categorical(Station);
1208 JobClass = categorical(JobClass);
1209 QLen = Qval(:); % we need to save first in a variable named like the column
1210 QT = Table(Station,JobClass,QLen);
1211 Util = Uval(:); % we need to save first in a variable named like the column
1212 UT = Table(Station,JobClass,Util);
1213 Tput = Tval(:); % we need to save first in a variable named like the column
1214 TT = Table(Station,JobClass,Tput);
1215 RespT = QLen ./ Tput;
1216 AvgTable = Table(Station,JobClass,QLen,Util,RespT,Tput);
1220 function envsn = getStruct(self)
1221 E = self.getNumberOfModels;
1224 envsn{e} = self.ensemble{e}.getStruct;
1228 function [T, segmentResults] = getSamplePathTable(self, samplePath)
1229 % [T, SEGMENTRESULTS] = GETSAMPLEPATHTABLE(SELF, SAMPLEPATH)
1231 % Compute transient performance metrics
for a sample path through
1232 % environment states. The method runs transient analysis
for each
1233 % segment and extracts initial and
final metric values.
1236 % samplePath - Cell
array where each row
is {stage, duration}
1237 % stage: string (name) or integer (1-based index)
1238 % duration: positive scalar (time spent in stage)
1241 % T - Table with columns: Segment, Stage, Duration, Station, JobClass,
1242 % InitQLen, InitUtil, InitTput, FinalQLen, FinalUtil, FinalTput
1243 % segmentResults - Cell array with detailed transient results per segment
1246 % samplePath = {
'Fast', 5.0;
'Slow', 10.0;
'Fast', 3.0};
1247 % [T, results] = solver.getSamplePathTable(samplePath);
1250 if isempty(samplePath)
1251 line_error(mfilename,
'Sample path cannot be empty.');
1254 % Initialize
if needed
1255 if isempty(self.envObj.probEnv)
1259 E = self.getNumberOfModels;
1260 M = self.sn{1}.nstations;
1261 K = self.sn{1}.nclasses;
1262 nSegments = size(samplePath, 1);
1263 segmentResults = cell(nSegments, 1);
1265 % Initialize queue lengths (uniform distribution
for closed
classes)
1266 Q_current = zeros(M, K);
1268 if self.sn{1}.njobs(k) > 0 % closed
class
1269 Q_current(:, k) = self.sn{1}.njobs(k) / M;
1273 % Process each segment
1274 for seg = 1:nSegments
1276 stageSpec = samplePath{seg, 1};
1277 duration = samplePath{seg, 2};
1279 if ischar(stageSpec) || isstring(stageSpec)
1280 e = self.envObj.envGraph.findnode(stageSpec);
1282 line_error(mfilename, sprintf(
'Stage "%s" not found.', stageSpec));
1284 stageName = char(stageSpec);
1288 line_error(mfilename, sprintf(
'Stage index %d out of range [1, %d].', e, E));
1290 stageName = self.envObj.envGraph.Nodes.Name{e};
1294 line_error(mfilename,
'Duration must be positive.');
1297 % Initialize model from current queue lengths
1298 self.ensemble{e}.initFromMarginal(Q_current);
1300 % Set solver timespan
1301 self.solvers{e}.options.timespan = [0, duration];
1302 self.solvers{e}.reset();
1304 % Run transient analysis
1305 [Qt, Ut, Tt] = self.ensemble{e}.getTranHandles;
1306 [QNt, UNt, TNt] = self.solvers{e}.getTranAvg(Qt, Ut, Tt);
1308 % Extract initial and
final metrics
1309 initQ = zeros(M, K);
1310 initU = zeros(M, K);
1311 initT = zeros(M, K);
1312 finalQ = zeros(M, K);
1313 finalU = zeros(M, K);
1314 finalT = zeros(M, K);
1322 if isstruct(Qir) && isfield(Qir,
'metric') && ~isempty(Qir.metric)
1323 initQ(i, r) = Qir.metric(1);
1324 finalQ(i, r) = Qir.metric(end);
1326 if isstruct(Uir) && isfield(Uir, 'metric') && ~isempty(Uir.metric)
1327 initU(i, r) = Uir.metric(1);
1328 finalU(i, r) = Uir.metric(end);
1330 if isstruct(Tir) && isfield(Tir, 'metric') && ~isempty(Tir.metric)
1331 initT(i, r) = Tir.metric(1);
1332 finalT(i, r) = Tir.metric(end);
1337 % Store segment results
1338 segmentResults{seg}.stage = e;
1339 segmentResults{seg}.stageName = stageName;
1340 segmentResults{seg}.duration = duration;
1341 segmentResults{seg}.QNt = QNt;
1342 segmentResults{seg}.UNt = UNt;
1343 segmentResults{seg}.TNt = TNt;
1344 segmentResults{seg}.initQ = initQ;
1345 segmentResults{seg}.initU = initU;
1346 segmentResults{seg}.initT = initT;
1347 segmentResults{seg}.finalQ = finalQ;
1348 segmentResults{seg}.finalU = finalU;
1349 segmentResults{seg}.finalT = finalT;
1351 % Update current queue lengths
for next segment
1355 % Build output table
1368 for seg = 1:nSegments
1369 res = segmentResults{seg};
1372 % Only include rows with non-zero metrics
1373 if any([res.initQ(i,r), res.initU(i,r), res.initT(i,r), ...
1374 res.finalQ(i,r), res.finalU(i,r), res.finalT(i,r)] > 0)
1375 Segment(end+1, 1) = seg;
1376 Stage{end+1, 1} = res.stageName;
1377 Duration(end+1, 1) = res.duration;
1378 Station{end+1, 1} = self.sn{1}.nodenames{self.sn{1}.stationToNode(i)};
1379 JobClass{end+1, 1} = self.sn{1}.classnames{r};
1380 InitQLen(end+1, 1) = res.initQ(i, r);
1381 InitUtil(end+1, 1) = res.initU(i, r);
1382 InitTput(end+1, 1) = res.initT(i, r);
1383 FinalQLen(end+1, 1) = res.finalQ(i, r);
1384 FinalUtil(end+1, 1) = res.finalU(i, r);
1385 FinalTput(end+1, 1) = res.finalT(i, r);
1391 Stage = categorical(Stage);
1392 Station = categorical(Station);
1393 JobClass = categorical(JobClass);
1394 T = Table(Segment, Stage, Duration, Station, JobClass, ...
1395 InitQLen, InitUtil, InitTput, FinalQLen, FinalUtil, FinalTput);
1397 function [allMethods] = listValidMethods(self)
1398 % allMethods = LISTVALIDMETHODS()
1399 % List valid methods
for this solver
1400 sn = self.model.getStruct();
1401 allMethods = {
'default'};
1404 function Q = roundMarginalForDiscreteSolver(self, Q, snRef)
1405 % ROUNDMARGINALFORDISCRETESOLVER Round fractional queue lengths
1406 % to integers
using the largest remainder method, preserving
1407 % closed chain populations exactly.
1408 for c = 1:snRef.nchains
1409 chainClasses = find(snRef.chains(c,:) > 0);
1410 njobs_chain = sum(snRef.njobs(chainClasses));
1411 if isinf(njobs_chain)
1412 % Open chain: simple rounding
1413 for idx = 1:length(chainClasses)
1414 k = chainClasses(idx);
1416 Q(i,k) = round(Q(i,k));
1420 % Closed chain: largest remainder method
1424 for idx = 1:length(chainClasses)
1425 k = chainClasses(idx);
1426 vals(end+1) = Q(i,k);
1427 indices(end+1,:) = [i, k];
1430 floored = floor(vals);
1431 remainders = vals - floored;
1432 deficit = njobs_chain - sum(floored);
1433 deficit = round(deficit); % ensure integer
1435 [~, sortIdx] = sort(remainders, 'descend');
1436 for d = 1:min(deficit, length(sortIdx))
1437 floored(sortIdx(d)) = floored(sortIdx(d)) + 1;
1440 for j = 1:length(vals)
1441 Q(indices(j,1), indices(j,2)) = floored(j);
1450 function [
bool, featSupported] = supports(model)
1451 % [BOOL, FEATSUPPORTED] = SUPPORTS(MODEL)
1453 featUsed = model.getUsedLangFeatures();
1455 featSupported = SolverFeatureSet;
1458 featSupported.setTrue('ClassSwitch');
1459 featSupported.setTrue('Delay');
1460 featSupported.setTrue('DelayStation');
1461 featSupported.setTrue('Queue');
1462 featSupported.setTrue('Sink');
1463 featSupported.setTrue('Source');
1466 featSupported.setTrue('Coxian');
1467 featSupported.setTrue('Cox2');
1468 featSupported.setTrue('Erlang');
1469 featSupported.setTrue('Exp');
1470 featSupported.setTrue('HyperExp');
1473 featSupported.setTrue('StatelessClassSwitcher'); % Section
1474 featSupported.setTrue('InfiniteServer'); % Section
1475 featSupported.setTrue('SharedServer'); % Section
1476 featSupported.setTrue('Buffer'); % Section
1477 featSupported.setTrue('Dispatcher'); % Section
1478 featSupported.setTrue('Server'); % Section (Non-preemptive)
1479 featSupported.setTrue('JobSink'); % Section
1480 featSupported.setTrue('RandomSource'); % Section
1481 featSupported.setTrue('ServiceTunnel'); % Section
1483 % Scheduling strategy
1484 featSupported.setTrue('SchedStrategy_INF');
1485 featSupported.setTrue('SchedStrategy_PS');
1486 featSupported.setTrue('SchedStrategy_FCFS');
1487 featSupported.setTrue('RoutingStrategy_PROB');
1488 featSupported.setTrue('RoutingStrategy_RAND');
1489 featSupported.setTrue('RoutingStrategy_RROBIN'); % with SolverJMT
1492 featSupported.setTrue('ClosedClass');
1493 featSupported.setTrue('OpenClass');
1495 bool = SolverFeatureSet.supports(featSupported, featUsed);
1500 % ensemble solver options
1501 function options = defaultOptions()
1502 % OPTIONS = DEFAULTOPTIONS()
1503 options = SolverOptions('ENV');
1506 function libs = getLibrariesUsed(sn, options)
1507 % GETLIBRARIESUSED Get list of external libraries used by ENV solver
1508 % ENV uses internal algorithms, no external library attribution needed