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 % see _kb/06-solver-catalog.md
for rationale
90 [~, ~, ~, ~, ~, ~, ~, ld] = solver_mam_ldqbd(self.sn{e}, opts_e);
91 [Q, levelOf] = solver_mam_ldqbd_flatten(ld);
94 self.statevecData{e} =
struct(
'backend',
'mam',
'ld', ld, ...
95 'levelOf', levelOf,
'options', opts_e);
97 line_error(mfilename, sprintf([
'The statevec analyzer requires a SolverCTMC or SolverMAM ' ...
98 'inner solver, but environment stage %d uses %s.'], e,
class(solver_e)));
101 % Warm-start each entry distribution from the stage
's own stationary
102 % distribution (a valid probability vector over its state space).
103 pi0 = ctmc_solve_reducible(Q);
107 pi0 = pi0 / sum(pi0);
109 self.piEnter{e} = pi0;
111self.piEnterPrev = self.piEnter;
114% -------------------------------------------------------------------------
115function [results_e, runtime] = analyze_(self, it, e)
116% Propagate the entry distribution of stage e through its sojourn and store
117% the per-destination exit distributions and the sojourn-end distribution.
119results_e.statevec =
struct(
'ok',
false);
122E = self.getNumberOfModels;
124data = self.statevecData{e};
127pi0 = self.piEnter{e}(:)
';
128t0 = opts.timespan(1);
129t1 = opts.timespan(2);
131% Deterministic sojourn option (off by default): exit is pi0*exp(Q*d_e), same
132% toward every destination. see _kb/06-solver-catalog.md for rationale
133if isfield(self.options,'sojourn
') && strcmpi(self.options.sojourn,'deterministic
')
134 d_e = max(map_mean(self.envObj.holdTime{e}), eps);
135 % Exact deterministic sojourn via uniformization: the exit is pi0*exp(Q*d_e)
136 % and the blend uses the time-average (1/d_e) * \int_0^{d_e} exp(Q t) dt.
137 [piAvg, piEx] = ctmc_timeaverage(pi0, Q, d_e);
138 piExit_e = cell(1, E);
146 self.piExitDest{e} = piExit_e;
147 self.piTimeAvg{e} = piAvg;
148 results_e.statevec.ok = true;
153% Exponential environment sojourn: exit equals the time-average via the
154% resolvent s*pi*(sI-Q)^{-1}. see _kb/06-solver-catalog.md for rationale
157 if self.E0(e, h) > 0 && ~isa(self.envObj.env{e, h}, 'Exp
')
163 s_e = sum(self.E0(e, :)); % sojourn ~ Exp(s_e)
165 piRes = s_e * (pi0 / (s_e * speye(d) - sparse(Q)));
166 piExit_e = cell(1, E);
174 self.piExitDest{e} = piExit_e;
175 self.piTimeAvg{e} = piRes;
176 results_e.statevec.ok = true;
181% General Markovian (PH/Erlang) sojourn: transient pi(t) = pi0 * exp(Q t) on the
182% adaptive ode23 grid, averaged over the random holding-time / transition CDFs.
183[pit, t] = ctmc_transient(Q, pi0, t0, t1);
186% Per-destination exit distributions: E[ pi(T_{e->h}) ] weighted by the
187% increments of the e->h transition CDF proc{e}{h}.
188piExit_e = cell(1, E);
190 proc_eh = self.envObj.proc{e}{h};
191 dF = map_cdf(proc_eh, t(2:end)) - map_cdf(proc_eh, t(1:end-1));
194 if sw > 0 && all(~isnan(w))
195 piExit_e{h} = (w' * pit) / sw;
200self.piExitDest{e} = piExit_e;
202% Sojourn-end distribution: E[ pi(T_sojourn) ] weighted by holdTime{e}.
203holdT = self.envObj.holdTime{e};
204dFh = map_cdf(holdT, t(2:end)) - map_cdf(holdT, t(1:end-1));
207if swh > 0 && all(~isnan(wh))
208 self.piTimeAvg{e} = (wh
' * pit) / swh;
210 self.piTimeAvg{e} = pit(end, :); % degenerate: use the terminal distribution
213results_e.statevec.ok = true;
217% -------------------------------------------------------------------------
218function post_(self, it)
219% Chain the entry distributions: carry each stage's exit distributions into
220% the stages they feed, weighted by the origin probabilities probOrig.
221E = self.getNumberOfModels;
222self.piEnterPrev = self.piEnter;
223piEnterNew = cell(1, E);
226 nstates_e = size(self.Qgen{e}, 1);
227 acc = zeros(1, nstates_e);
230 po = self.envObj.probOrig(h, e);
231 if po > 0 && ~isempty(self.piExitDest{h}) && ~isempty(self.piExitDest{h}{e})
232 pex = self.resetStateFun{h, e}(self.piExitDest{h}{e});
234 if numel(pex) ~= nstates_e
235 line_error(mfilename, sprintf(['resetStateFun{%d,%d} returned a %d-element vector but
' ...
236 'stage %d has %d states. Supply a resetStateFun{%d,%d} that maps the state space of
' ...
237 'stage %d onto that of stage %d.
'], h, e, numel(pex), e, nstates_e, h, e, h, e));
239 acc = acc + po * pex;
246 acc = self.piEnter{e}; % no inflow this cycle: retain the current estimate
255self.piEnter = piEnterNew;
258% -------------------------------------------------------------------------
259function bool = converged_(self, it)
260% Converged when the max L1 change across all entry distributions over a full
261% cycle falls below iter_tol.
263if it < 1 || isempty(self.piEnterPrev) || isempty(self.piEnter)
266E = self.getNumberOfModels;
270 b = self.piEnterPrev{e};
271 if isempty(a) || isempty(b) || numel(a) ~= numel(b)
274 l1 = max(l1, sum(abs(a(:) - b(:))));
276if isnan(l1) || isinf(l1)
279if l1 < self.options.iter_tol
281 line_debug('ENV statevec converged: iteration %d, max L1 entry change %e < iter_tol %e
', ...
282 it, l1, self.options.iter_tol);
286% -------------------------------------------------------------------------
287function finish_(self)
288% Environment-averaged blend: map each stage's sojourn-end distribution to
289% marginal metrics and weight by the stage probability probEnv.
290E = self.getNumberOfModels;
291M = self.ensemble{1}.getNumberOfStations;
292K = self.ensemble{1}.getNumberOfClasses;
299 piF = self.piTimeAvg{e};
303 data = self.statevecData{e};
304 if strcmp(data.backend,
'mam')
305 [QN, UN, ~, TN] = solver_mam_ldqbd_avg(data.ld, piF, data.levelOf);
307 [QN, UN, ~, TN] = solver_ctmc_avg_from_pi(data.sn, piF, self.SS{e}, ...
308 self.SSaggr{e}, data.arvRates, data.depRates);
310 Qval = Qval + self.envObj.probEnv(e) * QN;
311 Uval = Uval + self.envObj.probEnv(e) * UN;
312 Tval = Tval + self.envObj.probEnv(e) * TN;
315self.result.Avg.Q = Qval;
316self.result.Avg.U = Uval;
317self.result.Avg.T = Tval;
319% Environment-blended cache hit/miss ratios written onto the stage-1 reference
320% model. see _kb/06-solver-catalog.md
for rationale
321aggregateCacheBlend_(self, E, K);
324% -------------------------------------------------------------------------
325function aggregateCacheBlend_(self, E, K)
326if strcmp(self.statevecData{1}.backend,
'mam')
327 return % MAM backend has no enumerated cache state
329sn1 = self.statevecData{1}.sn;
330if ~isfield(sn1,
'nstateful')
334for isf = 1:sn1.nstateful
335 ind = sn1.statefulToNode(isf);
336 if sn1.nodetype(ind) == NodeType.Cache
337 cacheStateful(end+1) = isf; %
#ok<AGROW>
340if isempty(cacheStateful)
343for isf = cacheStateful
344 hitT = zeros(1, K); missT = zeros(1, K);
346 piF = self.piTimeAvg{e};
347 if isempty(piF);
continue; end
348 pv = piF(:); pv(pv < 0) = 0;
349 if sum(pv) > 0; pv = pv / sum(pv); end
350 dr = self.statevecData{e}.depRates;
351 sne = self.statevecData{e}.sn;
352 np = sne.nodeparam{sne.statefulToNode(isf)};
353 w = self.envObj.probEnv(e);
355 if length(np.hitclass) >= k
356 h = np.hitclass(k); mcl = np.missclass(k);
358 hitT(k) = hitT(k) + w * (pv
' * dr(:, isf, h));
359 missT(k) = missT(k) + w * (pv' * dr(:, isf, mcl));
364 hitprob = NaN(1, K); missprob = NaN(1, K);
366 tot = hitT(k) + missT(k);
368 hitprob(k) = hitT(k) / tot;
369 missprob(k) = missT(k) / tot;
372 node = self.ensemble{1}.getNodeByIndex(sn1.statefulToNode(isf));
373 node.setResultHitProb(hitprob);
374 node.setResultMissProb(missprob);