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 solver: 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 solver: 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 = 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
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);
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);
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);
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(:));
260 maxDiff = 0; % Handle case where all QEntry values are zero
262 if isnan(maxDiff) || isinf(maxDiff)
263 return % Non-convergence on invalid values
265 if maxDiff >= self.options.iter_tol
266 return % Not converged
273 function runAnalyzer(self)
275 % Run the ensemble solver iteration
276 line_debug('ENV solver starting: nstages=%d, method=%s', self.getNumberOfModels, self.options.method);
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);
282 line_printf('The solver will leverage %s.\n', strjoin(libs, ', '));
283 GlobalConstants.setLibraryAttributionShown(true);
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);
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;
306 self.ServerNum = cell(K, 1);
307 self.SRates = cell(K, 1);
309 self.ServerNum{k} = zeros(M, E);
310 self.SRates{k} = zeros(M, E);
313 self.ServerNum{k}(m, e) = self.sn{e}.nservers(m);
314 self.SRates{k}(m, e) = self.sn{e}.rates(m, k);
319 % Build rate matrix E0
320 self.E0 = zeros(E, E);
323 if ~isa(self.envObj.env{e,h},
'Disabled')
324 self.E0(e, h) = self.envObj.env{e,h}.getRate();
328 self.Eutil = ctmc_makeinfgen(self.E0);
330 % Initialize transition CDFs
331 self.transitionCdfs = cell(E, 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);
338 self.transitionCdfs{e,h} = @(t) 0;
343 % Initialize sojourn CDFs
344 self.sojournCdfs = cell(E, 1);
346 self.sojournCdfs{e} = @(t) self.computeSojournCdf(e, t);
349 % SMPMethod: Use DTMC-based computation instead of CTMC for Semi-Markov Processes
350 % Verified numerical integration for Semi-Markov Process DTMC transition probabilities
352 line_debug('ENV using DTMC-based computation for Semi-Markov Processes (SMPMethod=true)');
353 self.dtmcP = zeros(E, E);
356 if k == e || isa(self.envObj.env{k,e},
'Disabled')
357 self.dtmcP(k, e) = 0.0;
359 % Compute the upper limit of the sojourn time
362 while self.transitionCdfs{k,e}(T) < 1.0 - epsilon
364 if T > 1e6 % safety limit
368 % Adaptive number of integration intervals based on T
369 N = max(1000, round(T * 100));
375 deltaF = self.transitionCdfs{k,e}(t1) - self.transitionCdfs{k,e}(t0);
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));
384 sumVal = sumVal + deltaF * survival;
386 self.dtmcP(k, e) = sumVal;
391 % Solve DTMC
for stationary distribution
392 dtmcPie = dtmc_solve(self.dtmcP);
394 % Calculate hold times
using numerical integration
395 self.holdTimeMatrix = self.computeHoldTime(E);
397 % Compute steady-state probabilities
401 denomSum = denomSum + dtmcPie(e) * self.holdTimeMatrix(e);
404 pi(k) = dtmcPie(k) * self.holdTimeMatrix(k) / denomSum;
406 self.envObj.probEnv = pi;
408 % Update embedding weights
409 newEmbweight = zeros(E, E);
414 sumVal = sumVal + pi(h) * self.E0(h, e);
419 newEmbweight(k, e) = 0;
422 newEmbweight(k, e) = pi(k) * self.E0(k, e) / sumVal;
427 self.envObj.probOrig = newEmbweight;
430 % Compression: Use Courtois decomposition
for large environments
432 line_debug(
'ENV using compression (Courtois decomposition)');
433 self.applyCompression(E, M, K);
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.
442 % Find best partition
444 self.MS = self.findBestPartition(E);
446 % Beam search
for large environments
447 self.MS = self.beamSearchPartition(E);
451 % No compression possible, use singletons
452 self.MS = cell(E, 1);
460 self.Ecompress = length(self.MS);
462 % Apply decomposition/aggregation
463 [p, eps, epsMax, q] = self.ctmc_decompose(self.Eutil, self.MS);
466 line_warning(mfilename,
'Environment cannot be effectively compressed (eps > epsMax).');
469 % Store compression results
470 self.compressionResult.p = p;
471 self.compressionResult.eps = eps;
472 self.compressionResult.epsMax = epsMax;
473 self.compressionResult.q = q;
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}));
480 self.envObj.probEnv = pMacro;
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);
490 self.compressionResult.pmicro = pmicro;
491 self.compressionResult.pMacro = pMacro;
493 % Update embedding weights
for macro-states
494 Ecomp = self.Ecompress;
495 newEmbweight = zeros(Ecomp, Ecomp);
500 sumVal = sumVal + pMacro(h) * self.computeMacroRate(h, e);
505 newEmbweight(k, e) = 0;
507 newEmbweight(k, e) = pMacro(k) * self.computeMacroRate(k, e) / sumVal;
511 self.envObj.probOrig = newEmbweight;
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);
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();
523 % Compute weighted-average rates
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);
533 % Update service rate
534 jobclass = macroEnsemble{i}.classes{k};
535 station = macroEnsemble{i}.stations{m};
536 if isa(station,
'Queue') || isa(station,
'Delay')
538 station.setService(
jobclass, Exp(rateSum));
544 macroEnsemble{i}.refreshStruct(
true);
545 macroSn{i} = macroEnsemble{i}.getStruct(
true);
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);
552 % Replace ensemble and solvers with compressed versions
553 self.ensemble = macroEnsemble;
554 self.solvers = macroSolvers;
558 function rate = computeMacroRate(self, fromMacro, toMacro)
559 % COMPUTEMACRORATE Compute transition rate between macro-states
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);
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
574 % Start with singletons
580 [~, bestEps, bestEpsMax] = self.ctmc_decompose(self.Eutil, bestMS);
581 if isempty(bestEps) || isnan(bestEps)
589 testMS = cell(E-1, 1);
593 testMS{idx} = [i, j];
601 [~, testEps, testEpsMax] = self.ctmc_decompose(self.Eutil, testMS);
602 if ~isempty(testEps) && ~isnan(testEps) && testEps < bestEps
604 bestEpsMax = testEpsMax;
611 self.Ecompress = length(MS);
614 function MS = beamSearchPartition(self, E)
615 % BEAMSEARCHPARTITION Beam search
for large environments (E > 10)
618 alpha = 0.01; % Coupling threshold
620 if isfield(self.options, 'config') && isfield(self.options.config, 'env_alpha')
621 alpha = self.options.config.env_alpha;
624 % Initialize with singletons
625 singletons = cell(E, 1);
631 bestSeen = singletons;
632 [~, bestEps] = self.ctmc_decompose(self.Eutil, bestSeen);
634 % Iteratively merge blocks
638 for b = 1:length(beam)
640 nBlocks = length(ms);
642 % Try all pairwise merges
644 for j = (i+1):nBlocks
645 % Create merged partition
646 trial = cell(nBlocks - 1, 1);
650 trial{idx} = [ms{i}(:); ms{j}(:)];
658 [~, childEps, childEpsMax] = self.ctmc_decompose(self.Eutil, trial);
660 if ~isempty(childEps) && ~isnan(childEps) && childEps > 0
661 cost = childEps - childEpsMax + alpha * depth;
662 candidates{end+1} = {trial, cost};
673 if isempty(candidates)
677 % Sort by cost and keep top B
678 costs = cellfun(@(x) x{2}, candidates);
679 [~, sortIdx] = sort(costs);
681 for i = 1:min(B, length(sortIdx))
682 beam{end+1} = candidates{sortIdx(i)}{1};
687 self.Ecompress = length(MS);
690 function holdTime = computeHoldTime(self, E)
691 % COMPUTEHOLDTIME Compute expected holding times
using numerical integration
692 holdTime = zeros(1, E);
694 % Survival function: 1 - sojournCDF
695 surv = @(t) 1 - self.sojournCdfs{k}(t);
697 % Compute upper limit
699 while surv(upperLimit) > 1e-8
700 upperLimit = upperLimit * 2;
701 if upperLimit > 1e6 % safety limit
706 % Simpson
's rule integration
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;
717 holdTime(k) = integral;
721 function cdf = computeSojournCdf(self, e, t)
722 % COMPUTESOJOURNCDF Compute sojourn time CDF
for environment stage e
723 E = self.getNumberOfModels;
727 surv = surv * (1 - self.transitionCdfs{e,h}(t));
733 function pre(self, it)
738 if isinf(self.getSolver(e).options.timespan(2))
739 [QN,~,~,~] = self.getSolver(e).getAvg();
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);
749 QN(i,k) = 0; % Default to 0
for NaN/invalid entries
754 self.ensemble{e}.initFromMarginal(QN);
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') = [];
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;
777 function post(self, it)
780 E = self.getNumberOfModels;
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))]
';
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});
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});
815 Qentry = cell(1,E); % average entry queue-length
817 Qentry{e} = zeros(size(Qexit{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});
824 self.solvers{e}.reset();
825 self.ensemble{e}.initFromMarginal(Qentry{e});
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'))
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});
840 % Reinitialize environment after rate updates
845 function finish(self)
848 it = size(self.results,1); % use last iteration
849 E = self.getNumberOfModels;
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});
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});
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))]
';
888 % QE{e,h}(i,r) = self.results{it,e}.Tran.Avg.Q{i,r}(:,1)'*w{e,h}/sum(w{e,h});
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
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
917 function name = getName(self)
923 function [renvInfGen, stageInfGen, renvEventFilt, stageEventFilt, renvEvents, stageEvents] = getGenerator(self)
924 % [renvInfGen, stageInfGen, renvEventFilt, stageEventFilt, renvEvents, stageEvents] = getGenerator(self)
926 % Returns the infinitesimal generator matrices for the random environment model.
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
936 E = self.getNumberOfModels;
937 stageInfGen = cell(1,E);
938 stageEventFilt = cell(1,E);
939 stageEvents = cell(1,E);
941 if isa(self.solvers{e},
'SolverCTMC')
942 [stageInfGen{e}, stageEventFilt{e}, stageEvents{e}] = self.solvers{e}.getGenerator();
944 line_error(mfilename,
'This method requires SolverENV to be instantiated with the CTMC solver.');
948 % Get number of states
for each stage
949 nstates = cellfun(@(g) size(g, 1), stageInfGen);
951 % Get number of phases
for each transition distribution
952 nphases = zeros(E, E);
955 if ~isempty(self.env{i,j}) && ~isa(self.env{i,j},
'Disabled')
956 nphases(i,j) = self.env{i,j}.getNumberOfPhases();
962 % Adjust diagonal (self-transitions have one less phase in the Kronecker expansion)
963 nphases = nphases - eye(E);
965 % Initialize block cell structure
for the random environment generator
966 renvInfGen = cell(E,E);
969 % Diagonal block: stage infinitesimal generator
970 renvInfGen{e,e} = stageInfGen{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));
977 resetMatrix_eh(i,i) = 1;
979 renvInfGen{e,h} = resetMatrix_eh;
984 % Build environment transition events and expand generator with phase structure
985 renvEvents = cell(1,0);
989 % Get D0 (phase generator)
for transition from e to h
990 if isempty(self.env{e,h}) || isa(self.env{e,h},
'Disabled')
993 proc = self.env{e,h}.getProcess();
996 % Kronecker sum with diagonal block
997 renvInfGen{e,e} = krons(renvInfGen{e,e}, D0);
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));
1003 pie = map_pie(self.env{h,e}.getProcess());
1006 if isempty(self.env{e,h}) || isa(self.env{e,h},
'Disabled')
1009 proc = self.env{e,h}.getProcess();
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));
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>
1023 % Handle other stages (f != e, f != h)
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));
1029 pie_fh = map_pie(self.env{f,h}.getProcess());
1031 oneVec = ones(nphases(e,h), 1);
1032 renvInfGen{e,f} = kron(renvInfGen{e,f}, oneVec * pie_fh);
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);
1044 tmpCell = cell(E,E);
1047 tmpCell{e1,h1} = renvInfGen{e1,h1};
1050 % Zero out diagonal blocks (internal stage transitions)
1052 tmpCell{e1,e1} = tmpCell{e1,e1} * 0;
1054 % Zero out off-diagonal blocks that don't match (e,h) transition
1058 if e1~=h1 % Only zero out off-diagonal entries
1059 tmpCell{e1,h1} = tmpCell{e1,h1} * 0;
1064 renvEventFilt{e,h} = cell2mat(tmpCell);
1068 % Flatten block structure into single matrix and normalize
1069 renvInfGen = cell2mat(renvInfGen);
1070 renvInfGen = ctmc_makeinfgen(renvInfGen);
1073 function varargout = getAvg(varargin)
1074 % [QNCLASS, UNCLASS, TNCLASS] = GETAVG()
1075 [varargout{1:nargout}] = getEnsembleAvg( varargin{:} );
1078 function [QNclass, UNclass, RNclass, TNclass, ANclass, WNclass] = getEnsembleAvg(self)
1079 % [QNCLASS, UNCLASS, TNCLASS] = GETENSEMBLEAVG()
1084 if isempty(self.result) || (isfield(self.options,'force') && self.options.force)
1086 if isempty(self.result)
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;
1101 function [AvgTable,QT,UT,TT] = getAvgTable(self,keepDisabled)
1102 % [AVGTABLE,QT,UT,TT] = GETAVGTABLE(SELF,KEEPDISABLED)
1103 % Return table of average station metrics
1105 if nargin<2 %if ~exist('keepDisabled','var')
1106 keepDisabled = false;
1109 [QN,UN,~,TN] = getAvg(self);
1112 Q = self.result.Avg.Q;
1113 U = self.result.Avg.U;
1114 T = self.result.Avg.T;
1120 elseif ~keepDisabled
1121 Qval = []; Uval = []; Tval = [];
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);
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);
1146 Qval = zeros(M,K); Uval = zeros(M,K);
1147 JobClass = cell(K*M,1);
1148 Station = cell(K*M,1);
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);
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);
1171 function envsn = getStruct(self)
1172 E = self.getNumberOfModels;
1175 envsn{e} = self.ensemble{e}.getStruct;
1179 function [T, segmentResults] = getSamplePathTable(self, samplePath)
1180 % [T, SEGMENTRESULTS] = GETSAMPLEPATHTABLE(SELF, SAMPLEPATH)
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.
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)
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
1197 % samplePath = {
'Fast', 5.0;
'Slow', 10.0;
'Fast', 3.0};
1198 % [T, results] = solver.getSamplePathTable(samplePath);
1201 if isempty(samplePath)
1202 line_error(mfilename,
'Sample path cannot be empty.');
1205 % Initialize
if needed
1206 if isempty(self.envObj.probEnv)
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);
1216 % Initialize queue lengths (uniform distribution
for closed
classes)
1217 Q_current = zeros(M, K);
1219 if self.sn{1}.njobs(k) > 0 % closed
class
1220 Q_current(:, k) = self.sn{1}.njobs(k) / M;
1224 % Process each segment
1225 for seg = 1:nSegments
1227 stageSpec = samplePath{seg, 1};
1228 duration = samplePath{seg, 2};
1230 if ischar(stageSpec) || isstring(stageSpec)
1231 e = self.envObj.envGraph.findnode(stageSpec);
1233 line_error(mfilename, sprintf(
'Stage "%s" not found.', stageSpec));
1235 stageName = char(stageSpec);
1239 line_error(mfilename, sprintf(
'Stage index %d out of range [1, %d].', e, E));
1241 stageName = self.envObj.envGraph.Nodes.Name{e};
1245 line_error(mfilename,
'Duration must be positive.');
1248 % Initialize model from current queue lengths
1249 self.ensemble{e}.initFromMarginal(Q_current);
1251 % Set solver timespan
1252 self.solvers{e}.options.timespan = [0, duration];
1253 self.solvers{e}.reset();
1255 % Run transient analysis
1256 [Qt, Ut, Tt] = self.ensemble{e}.getTranHandles;
1257 [QNt, UNt, TNt] = self.solvers{e}.getTranAvg(Qt, Ut, Tt);
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);
1273 if isstruct(Qir) && isfield(Qir,
'metric') && ~isempty(Qir.metric)
1274 initQ(i, r) = Qir.metric(1);
1275 finalQ(i, r) = Qir.metric(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);
1281 if isstruct(Tir) && isfield(Tir, 'metric') && ~isempty(Tir.metric)
1282 initT(i, r) = Tir.metric(1);
1283 finalT(i, r) = Tir.metric(end);
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;
1302 % Update current queue lengths
for next segment
1306 % Build output table
1319 for seg = 1:nSegments
1320 res = segmentResults{seg};
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);
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);
1348 function [allMethods] = listValidMethods(self)
1349 % allMethods = LISTVALIDMETHODS()
1350 % List valid methods
for this solver
1351 sn = self.model.getStruct();
1352 allMethods = {
'default'};
1358 function [bool, featSupported] = supports(model)
1359 % [BOOL, FEATSUPPORTED] = SUPPORTS(MODEL)
1361 featUsed = model.getUsedLangFeatures();
1363 featSupported = SolverFeatureSet;
1366 featSupported.setTrue(
'ClassSwitch');
1367 featSupported.setTrue(
'Delay');
1368 featSupported.setTrue(
'DelayStation');
1369 featSupported.setTrue(
'Queue');
1370 featSupported.setTrue(
'Sink');
1371 featSupported.setTrue(
'Source');
1374 featSupported.setTrue(
'Coxian');
1375 featSupported.setTrue(
'Cox2');
1376 featSupported.setTrue(
'Erlang');
1377 featSupported.setTrue(
'Exp');
1378 featSupported.setTrue(
'HyperExp');
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
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
1400 featSupported.setTrue(
'ClosedClass');
1401 featSupported.setTrue(
'OpenClass');
1403 bool = SolverFeatureSet.supports(featSupported, featUsed);
1408 % ensemble solver options
1409 function options = defaultOptions()
1410 % OPTIONS = DEFAULTOPTIONS()
1411 options = SolverOptions('ENV');
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