1function varargout = solver_env_statevec_analyzer(self, phase, it, e)
2% SOLVER_ENV_STATEVEC_ANALYZER Full state-vector coupling
for SolverENV.
4% Alternative to
the mean-field analyzer (solver_env_meanfield_analyzer). Instead
5% of collapsing each stage to marginal mean queue lengths and re-seeding
the
6% next stage with initFromMarginal,
this analyzer carries
the entire state
7% probability vector across environment switches and propagates it with
the
8% CTMC transient (matrix-exponential action) on
the per-stage generator.
10% For each stage e with infinitesimal generator Q_e (supplied by a SolverCTMC
11% inner solver) and entry distribution pi_enter{e}, one iteration computes
the
12% transient pi(t) = pi_enter{e} * exp(Q_e t) over
the stage time span, then:
13% -
the exit distribution toward each destination h, pi_exit{e}{h}, as
the
14% expectation of pi(t) at
the (random) e->h transition time, weighted by
the
15% increments of
the e->h transition CDF proc{e}{h};
16% -
the sojourn-end distribution pi_timeavg{e}, weighted by
the overall stage
17% holding-time CDF holdTime{e}, used in
the environment-averaged blend.
18% Entry distributions are chained as
19% pi_enter{e} = sum_h probOrig(h,e) * resetStateFun{h,e}(pi_exit{h}{e}),
20% renormalised, and iterated to an L1 fixed point. The blend reuses
the
21% discipline-aware CTMC marginal mapping (solver_ctmc_avg_from_pi).
23% This weighting scheme
is identical to solver_env_meanfield_analyzer;
the sole
24% difference
is that
the full joint distribution
is propagated and chained
25% rather than its marginal means, so
the two agree when
the marginal collapse
26%
is exact and differ when inter-
class/inter-station correlations matter.
28% Backend:
requires a SolverCTMC inner solver (an
explicit enumerated generator
29% and state space). MAM/LDQBD backends are not yet supported.
31% Phase dispatch (called by
the SolverENV EnsembleSolver hooks):
32%
'pre' pre_(self,it) -> []
33%
'analyze' analyze_(self,it,e) -> [results_e, runtime]
34%
'post' post_(self,it) -> []
35%
'finish' finish_(self) -> []
36%
'converged' converged_(self,it) -> bool
38% Copyright (c) 2012-2026, Imperial College London
45 [varargout{1}, varargout{2}] = analyze_(self, it, e);
51 varargout{1} = converged_(self, it);
53 line_error(mfilename, sprintf(
'Unknown statevec-analyzer phase: %s', phase));
57% -------------------------------------------------------------------------
58function pre_(self, it)
59% Build
the per-stage generator, state space and metric-mapping data once,
60% and initialise
the per-stage entry distributions.
64E = self.getNumberOfModels;
65self.Qgen = cell(1, E);
67self.SSaggr = cell(1, E);
68self.statevecData = cell(1, E);
69self.piEnter = cell(1, E);
70self.piExitDest = cell(1, E);
71self.piTimeAvg = cell(1, E);
74 solver_e = self.solvers{e};
75 opts_e = solver_e.getOptions;
76 if ~isfield(opts_e,
'timespan') || ~isfinite(opts_e.timespan(2))
77 line_error(mfilename, sprintf(['The statevec analyzer requires a finite inner-solver timespan ' ...
78 'for stage %d, e.g. CTMC(model,''timespan'',[0,T]).'], e));
80 if isa(solver_e, 'SolverCTMC')
81 % CTMC backend: explicit enumerated generator + state space.
82 [Q, SSp, SSaggr, ~, arvRates, depRates, sn_e] = solver_ctmc(self.sn{e}, opts_e);
84 self.SSaggr{e} = SSaggr;
85 self.statevecData{e} =
struct(
'backend',
'ctmc',
'arvRates', arvRates, ...
86 'depRates', depRates,
'sn', sn_e,
'options', opts_e);
87 elseif isa(solver_e,
'SolverMAM')
88 % MAM backend: level-dependent QBD blocks flattened to a generator
89 % (single-
class Delay+Queue closed or Source+Queue open; exact
90 % multiserver and load-dependence; open handled by truncation).
91 [~, ~, ~, ~, ~, ~, ~, ld] = solver_mam_ldqbd(self.sn{e}, opts_e);
92 [Q, levelOf] = solver_mam_ldqbd_flatten(ld);
95 self.statevecData{e} =
struct(
'backend',
'mam',
'ld', ld, ...
96 'levelOf', levelOf,
'options', opts_e);
98 line_error(mfilename, sprintf([
'The statevec analyzer requires a SolverCTMC or SolverMAM ' ...
99 'inner solver, but environment stage %d uses %s.'], e,
class(solver_e)));
102 % Warm-start each entry distribution from
the stage
's own stationary
103 % distribution (a valid probability vector over its state space).
104 pi0 = ctmc_solve_reducible(Q);
108 pi0 = pi0 / sum(pi0);
110 self.piEnter{e} = pi0;
112self.piEnterPrev = self.piEnter;
115% -------------------------------------------------------------------------
116function [results_e, runtime] = analyze_(self, it, e)
117% Propagate
the entry distribution of stage e through its sojourn and store
118%
the per-destination exit distributions and
the sojourn-end distribution.
120results_e.statevec =
struct(
'ok',
false);
123E = self.getNumberOfModels;
125data = self.statevecData{e};
128pi0 = self.piEnter{e}(:)
';
129t0 = opts.timespan(1);
130t1 = opts.timespan(2);
132% Deterministic sojourn option (off by default): the stage lasts exactly its
133% mean duration d_e, so the exit distribution is pi0*exp(Q*d_e) (matrix
134% exponential at the fixed time) and is the same toward every destination.
135if isfield(self.options,'sojourn
') && strcmpi(self.options.sojourn,'deterministic
')
136 d_e = max(map_mean(self.envObj.holdTime{e}), eps);
137 % Exact deterministic sojourn via uniformization: the exit is pi0*exp(Q*d_e)
138 % and the blend uses the time-average (1/d_e) * \int_0^{d_e} exp(Q t) dt.
139 [piAvg, piEx] = ctmc_timeaverage(pi0, Q, d_e);
140 piExit_e = cell(1, E);
148 self.piExitDest{e} = piExit_e;
149 self.piTimeAvg{e} = piAvg;
150 results_e.statevec.ok = true;
155% Exponential environment sojourn (every enabled transition from e is Exp): the
156% exit equals the time-average and is given exactly by the resolvent
157% s*pi*(sI - Q)^{-1}, s = total exit rate. The same value goes toward every
158% destination; the destination split is handled by probOrig in post_. General
159% Markovian (PH/Erlang) transitions fall through to the transient below.
162 if self.E0(e, h) > 0 && ~isa(self.envObj.env{e, h}, 'Exp
')
168 s_e = sum(self.E0(e, :)); % sojourn ~ Exp(s_e)
170 piRes = s_e * (pi0 / (s_e * speye(d) - sparse(Q)));
171 piExit_e = cell(1, E);
179 self.piExitDest{e} = piExit_e;
180 self.piTimeAvg{e} = piRes;
181 results_e.statevec.ok = true;
186% General Markovian (PH/Erlang) sojourn: transient pi(t) = pi0 * exp(Q t) on the
187% adaptive ode23 grid, averaged over the random holding-time / transition CDFs.
188[pit, t] = ctmc_transient(Q, pi0, t0, t1);
191% Per-destination exit distributions: E[ pi(T_{e->h}) ] weighted by the
192% increments of the e->h transition CDF proc{e}{h}.
193piExit_e = cell(1, E);
195 proc_eh = self.envObj.proc{e}{h};
196 dF = map_cdf(proc_eh, t(2:end)) - map_cdf(proc_eh, t(1:end-1));
199 if sw > 0 && all(~isnan(w))
200 piExit_e{h} = (w' * pit) / sw;
205self.piExitDest{e} = piExit_e;
207% Sojourn-end distribution: E[ pi(T_sojourn) ] weighted by holdTime{e}.
208holdT = self.envObj.holdTime{e};
209dFh = map_cdf(holdT, t(2:end)) - map_cdf(holdT, t(1:end-1));
212if swh > 0 && all(~isnan(wh))
213 self.piTimeAvg{e} = (wh
' * pit) / swh;
215 self.piTimeAvg{e} = pit(end, :); % degenerate: use the terminal distribution
218results_e.statevec.ok = true;
222% -------------------------------------------------------------------------
223function post_(self, it)
224% Chain the entry distributions: carry each stage's exit distributions into
225%
the stages they feed, weighted by
the origin probabilities probOrig.
226E = self.getNumberOfModels;
227self.piEnterPrev = self.piEnter;
228piEnterNew = cell(1, E);
231 nstates_e = size(self.Qgen{e}, 1);
232 acc = zeros(1, nstates_e);
235 po = self.envObj.probOrig(h, e);
236 if po > 0 && ~isempty(self.piExitDest{h}) && ~isempty(self.piExitDest{h}{e})
237 pex = self.resetStateFun{h, e}(self.piExitDest{h}{e});
239 if numel(pex) ~= nstates_e
240 line_error(mfilename, sprintf(['resetStateFun{%d,%d} returned a %d-element vector but
' ...
241 'stage %d has %d states. Supply a resetStateFun{%d,%d} that maps
the state space of
' ...
242 'stage %d onto that of stage %d.
'], h, e, numel(pex), e, nstates_e, h, e, h, e));
244 acc = acc + po * pex;
251 acc = self.piEnter{e}; % no inflow this cycle: retain the current estimate
260self.piEnter = piEnterNew;
263% -------------------------------------------------------------------------
264function bool = converged_(self, it)
265% Converged when the max L1 change across all entry distributions over a full
266% cycle falls below iter_tol.
268if it < 1 || isempty(self.piEnterPrev) || isempty(self.piEnter)
271E = self.getNumberOfModels;
275 b = self.piEnterPrev{e};
276 if isempty(a) || isempty(b) || numel(a) ~= numel(b)
279 l1 = max(l1, sum(abs(a(:) - b(:))));
281if isnan(l1) || isinf(l1)
284if l1 < self.options.iter_tol
286 line_debug('ENV statevec converged: iteration %d, max L1 entry change %e < iter_tol %e
', ...
287 it, l1, self.options.iter_tol);
291% -------------------------------------------------------------------------
292function finish_(self)
293% Environment-averaged blend: map each stage's sojourn-end distribution to
294% marginal metrics and weight by
the stage probability probEnv.
295E = self.getNumberOfModels;
296M = self.ensemble{1}.getNumberOfStations;
297K = self.ensemble{1}.getNumberOfClasses;
304 piF = self.piTimeAvg{e};
308 data = self.statevecData{e};
309 if strcmp(data.backend,
'mam')
310 [QN, UN, ~, TN] = solver_mam_ldqbd_avg(data.ld, piF, data.levelOf);
312 [QN, UN, ~, TN] = solver_ctmc_avg_from_pi(data.sn, piF, self.SS{e}, ...
313 self.SSaggr{e}, data.arvRates, data.depRates);
315 Qval = Qval + self.envObj.probEnv(e) * QN;
316 Uval = Uval + self.envObj.probEnv(e) * UN;
317 Tval = Tval + self.envObj.probEnv(e) * TN;
320self.result.Avg.Q = Qval;
321self.result.Avg.U = Uval;
322self.result.Avg.T = Tval;
324% Environment-blended cache hit/miss ratios. Each Cache node
's hit/miss
325% throughput is the sojourn-averaged distribution piTimeAvg{e} weighted by the
326% class-departure rates depRates(:,isf,hit/missClass), accumulated over stages
327% with weight probEnv(e). The blend is written back onto the stage-1 reference
328% model so ensemble{1}.getNodeByName('Cache
').getHitRatio() returns it.
329aggregateCacheBlend_(self, E, K);
332% -------------------------------------------------------------------------
333function aggregateCacheBlend_(self, E, K)
334if strcmp(self.statevecData{1}.backend, 'mam
')
335 return % MAM backend has no enumerated cache state
337sn1 = self.statevecData{1}.sn;
338if ~isfield(sn1, 'nstateful
')
342for isf = 1:sn1.nstateful
343 ind = sn1.statefulToNode(isf);
344 if sn1.nodetype(ind) == NodeType.Cache
345 cacheStateful(end+1) = isf; %#ok<AGROW>
348if isempty(cacheStateful)
351for isf = cacheStateful
352 hitT = zeros(1, K); missT = zeros(1, K);
354 piF = self.piTimeAvg{e};
355 if isempty(piF); continue; end
356 pv = piF(:); pv(pv < 0) = 0;
357 if sum(pv) > 0; pv = pv / sum(pv); end
358 dr = self.statevecData{e}.depRates;
359 sne = self.statevecData{e}.sn;
360 np = sne.nodeparam{sne.statefulToNode(isf)};
361 w = self.envObj.probEnv(e);
363 if length(np.hitclass) >= k
364 h = np.hitclass(k); mcl = np.missclass(k);
366 hitT(k) = hitT(k) + w * (pv' * dr(:, isf, h));
367 missT(k) = missT(k) + w * (pv
' * dr(:, isf, mcl));
372 hitprob = NaN(1, K); missprob = NaN(1, K);
374 tot = hitT(k) + missT(k);
376 hitprob(k) = hitT(k) / tot;
377 missprob(k) = missT(k) / tot;
380 node = self.ensemble{1}.getNodeByIndex(sn1.statefulToNode(isf));
381 node.setResultHitProb(hitprob);
382 node.setResultMissProb(missprob);