LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
solver_env_statevec_analyzer.m
1function varargout = solver_env_statevec_analyzer(self, phase, it, e)
2% SOLVER_ENV_STATEVEC_ANALYZER Full state-vector coupling for SolverENV.
3%
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.
9%
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).
22%
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.
27%
28% Backend: requires a SolverCTMC inner solver (an explicit enumerated generator
29% and state space). MAM/LDQBD backends are not yet supported.
30%
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
37%
38% Copyright (c) 2012-2026, Imperial College London
39% All rights reserved.
40
41switch phase
42 case 'pre'
43 pre_(self, it);
44 case 'analyze'
45 [varargout{1}, varargout{2}] = analyze_(self, it, e);
46 case 'post'
47 post_(self, it);
48 case 'finish'
49 finish_(self);
50 case 'converged'
51 varargout{1} = converged_(self, it);
52 otherwise
53 line_error(mfilename, sprintf('Unknown statevec-analyzer phase: %s', phase));
54end
55end
56
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.
61if it ~= 1
62 return
63end
64E = self.getNumberOfModels;
65self.Qgen = cell(1, E);
66self.SS = 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);
72
73for e = 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));
79 end
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);
83 self.SS{e} = SSp;
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);
92 self.SS{e} = [];
93 self.SSaggr{e} = [];
94 self.statevecData{e} = struct('backend', 'mam', 'ld', ld, ...
95 'levelOf', levelOf, 'options', opts_e);
96 else
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)));
99 end
100 self.Qgen{e} = Q;
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);
104 pi0 = pi0(:)';
105 pi0(pi0 < 0) = 0;
106 if sum(pi0) > 0
107 pi0 = pi0 / sum(pi0);
108 end
109 self.piEnter{e} = pi0;
110end
111self.piEnterPrev = self.piEnter;
112end
113
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.
118results_e = struct();
119results_e.statevec = struct('ok', false);
120T0 = tic;
121
122E = self.getNumberOfModels;
123Q = self.Qgen{e};
124data = self.statevecData{e};
125opts = data.options;
126
127pi0 = self.piEnter{e}(:)';
128t0 = opts.timespan(1);
129t1 = opts.timespan(2);
130
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);
139 for h = 1:E
140 if self.E0(e, h) > 0
141 piExit_e{h} = piEx;
142 else
143 piExit_e{h} = [];
144 end
145 end
146 self.piExitDest{e} = piExit_e;
147 self.piTimeAvg{e} = piAvg;
148 results_e.statevec.ok = true;
149 runtime = toc(T0);
150 return
151end
152
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
155expSojourn = true;
156for h = 1:E
157 if self.E0(e, h) > 0 && ~isa(self.envObj.env{e, h}, 'Exp')
158 expSojourn = false;
159 break
160 end
161end
162if expSojourn
163 s_e = sum(self.E0(e, :)); % sojourn ~ Exp(s_e)
164 d = size(Q, 1);
165 piRes = s_e * (pi0 / (s_e * speye(d) - sparse(Q)));
166 piExit_e = cell(1, E);
167 for h = 1:E
168 if self.E0(e, h) > 0
169 piExit_e{h} = piRes;
170 else
171 piExit_e{h} = [];
172 end
173 end
174 self.piExitDest{e} = piExit_e;
175 self.piTimeAvg{e} = piRes;
176 results_e.statevec.ok = true;
177 runtime = toc(T0);
178 return
179end
180
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);
184t = t(:);
185
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);
189for h = 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));
192 w = [0; dF(:)];
193 sw = sum(w);
194 if sw > 0 && all(~isnan(w))
195 piExit_e{h} = (w' * pit) / sw;
196 else
197 piExit_e{h} = [];
198 end
199end
200self.piExitDest{e} = piExit_e;
201
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));
205wh = [0; dFh(:)];
206swh = sum(wh);
207if swh > 0 && all(~isnan(wh))
208 self.piTimeAvg{e} = (wh' * pit) / swh;
209else
210 self.piTimeAvg{e} = pit(end, :); % degenerate: use the terminal distribution
211end
212
213results_e.statevec.ok = true;
214runtime = toc(T0);
215end
216
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);
224
225for e = 1:E
226 nstates_e = size(self.Qgen{e}, 1);
227 acc = zeros(1, nstates_e);
228 wsum = 0;
229 for h = 1: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});
233 pex = pex(:)';
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));
238 end
239 acc = acc + po * pex;
240 wsum = wsum + po;
241 end
242 end
243 if wsum > 0
244 acc = acc / wsum;
245 else
246 acc = self.piEnter{e}; % no inflow this cycle: retain the current estimate
247 end
248 acc(acc < 0) = 0;
249 s = sum(acc);
250 if s > 0
251 acc = acc / s;
252 end
253 piEnterNew{e} = acc;
254end
255self.piEnter = piEnterNew;
256end
257
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.
262bool = false;
263if it < 1 || isempty(self.piEnterPrev) || isempty(self.piEnter)
264 return
265end
266E = self.getNumberOfModels;
267l1 = 0;
268for e = 1:E
269 a = self.piEnter{e};
270 b = self.piEnterPrev{e};
271 if isempty(a) || isempty(b) || numel(a) ~= numel(b)
272 return
273 end
274 l1 = max(l1, sum(abs(a(:) - b(:))));
275end
276if isnan(l1) || isinf(l1)
277 return
278end
279if l1 < self.options.iter_tol
280 bool = true;
281 line_debug('ENV statevec converged: iteration %d, max L1 entry change %e < iter_tol %e', ...
282 it, l1, self.options.iter_tol);
283end
284end
285
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;
293
294Qval = zeros(M, K);
295Uval = zeros(M, K);
296Tval = zeros(M, K);
297
298for e = 1:E
299 piF = self.piTimeAvg{e};
300 if isempty(piF)
301 continue
302 end
303 data = self.statevecData{e};
304 if strcmp(data.backend, 'mam')
305 [QN, UN, ~, TN] = solver_mam_ldqbd_avg(data.ld, piF, data.levelOf);
306 else
307 [QN, UN, ~, TN] = solver_ctmc_avg_from_pi(data.sn, piF, self.SS{e}, ...
308 self.SSaggr{e}, data.arvRates, data.depRates);
309 end
310 Qval = Qval + self.envObj.probEnv(e) * QN;
311 Uval = Uval + self.envObj.probEnv(e) * UN;
312 Tval = Tval + self.envObj.probEnv(e) * TN;
313end
314
315self.result.Avg.Q = Qval;
316self.result.Avg.U = Uval;
317self.result.Avg.T = Tval;
318
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);
322end
323
324% -------------------------------------------------------------------------
325function aggregateCacheBlend_(self, E, K)
326if strcmp(self.statevecData{1}.backend, 'mam')
327 return % MAM backend has no enumerated cache state
328end
329sn1 = self.statevecData{1}.sn;
330if ~isfield(sn1, 'nstateful')
331 return
332end
333cacheStateful = [];
334for isf = 1:sn1.nstateful
335 ind = sn1.statefulToNode(isf);
336 if sn1.nodetype(ind) == NodeType.Cache
337 cacheStateful(end+1) = isf; %#ok<AGROW>
338 end
339end
340if isempty(cacheStateful)
341 return
342end
343for isf = cacheStateful
344 hitT = zeros(1, K); missT = zeros(1, K);
345 for e = 1:E
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);
354 for k = 1:K
355 if length(np.hitclass) >= k
356 h = np.hitclass(k); mcl = np.missclass(k);
357 if h > 0 && mcl > 0
358 hitT(k) = hitT(k) + w * (pv' * dr(:, isf, h));
359 missT(k) = missT(k) + w * (pv' * dr(:, isf, mcl));
360 end
361 end
362 end
363 end
364 hitprob = NaN(1, K); missprob = NaN(1, K);
365 for k = 1:K
366 tot = hitT(k) + missT(k);
367 if tot > 0
368 hitprob(k) = hitT(k) / tot;
369 missprob(k) = missT(k) / tot;
370 end
371 end
372 node = self.ensemble{1}.getNodeByIndex(sn1.statefulToNode(isf));
373 node.setResultHitProb(hitprob);
374 node.setResultMissProb(missprob);
375end
376end