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 % (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);
93 self.SS{e} = [];
94 self.SSaggr{e} = [];
95 self.statevecData{e} = struct('backend', 'mam', 'ld', ld, ...
96 'levelOf', levelOf, 'options', opts_e);
97 else
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)));
100 end
101 self.Qgen{e} = Q;
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);
105 pi0 = pi0(:)';
106 pi0(pi0 < 0) = 0;
107 if sum(pi0) > 0
108 pi0 = pi0 / sum(pi0);
109 end
110 self.piEnter{e} = pi0;
111end
112self.piEnterPrev = self.piEnter;
113end
114
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.
119results_e = struct();
120results_e.statevec = struct('ok', false);
121T0 = tic;
122
123E = self.getNumberOfModels;
124Q = self.Qgen{e};
125data = self.statevecData{e};
126opts = data.options;
127
128pi0 = self.piEnter{e}(:)';
129t0 = opts.timespan(1);
130t1 = opts.timespan(2);
131
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);
141 for h = 1:E
142 if self.E0(e, h) > 0
143 piExit_e{h} = piEx;
144 else
145 piExit_e{h} = [];
146 end
147 end
148 self.piExitDest{e} = piExit_e;
149 self.piTimeAvg{e} = piAvg;
150 results_e.statevec.ok = true;
151 runtime = toc(T0);
152 return
153end
154
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.
160expSojourn = true;
161for h = 1:E
162 if self.E0(e, h) > 0 && ~isa(self.envObj.env{e, h}, 'Exp')
163 expSojourn = false;
164 break
165 end
166end
167if expSojourn
168 s_e = sum(self.E0(e, :)); % sojourn ~ Exp(s_e)
169 d = size(Q, 1);
170 piRes = s_e * (pi0 / (s_e * speye(d) - sparse(Q)));
171 piExit_e = cell(1, E);
172 for h = 1:E
173 if self.E0(e, h) > 0
174 piExit_e{h} = piRes;
175 else
176 piExit_e{h} = [];
177 end
178 end
179 self.piExitDest{e} = piExit_e;
180 self.piTimeAvg{e} = piRes;
181 results_e.statevec.ok = true;
182 runtime = toc(T0);
183 return
184end
185
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);
189t = t(:);
190
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);
194for h = 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));
197 w = [0; dF(:)];
198 sw = sum(w);
199 if sw > 0 && all(~isnan(w))
200 piExit_e{h} = (w' * pit) / sw;
201 else
202 piExit_e{h} = [];
203 end
204end
205self.piExitDest{e} = piExit_e;
206
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));
210wh = [0; dFh(:)];
211swh = sum(wh);
212if swh > 0 && all(~isnan(wh))
213 self.piTimeAvg{e} = (wh' * pit) / swh;
214else
215 self.piTimeAvg{e} = pit(end, :); % degenerate: use the terminal distribution
216end
217
218results_e.statevec.ok = true;
219runtime = toc(T0);
220end
221
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);
229
230for e = 1:E
231 nstates_e = size(self.Qgen{e}, 1);
232 acc = zeros(1, nstates_e);
233 wsum = 0;
234 for h = 1: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});
238 pex = pex(:)';
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));
243 end
244 acc = acc + po * pex;
245 wsum = wsum + po;
246 end
247 end
248 if wsum > 0
249 acc = acc / wsum;
250 else
251 acc = self.piEnter{e}; % no inflow this cycle: retain the current estimate
252 end
253 acc(acc < 0) = 0;
254 s = sum(acc);
255 if s > 0
256 acc = acc / s;
257 end
258 piEnterNew{e} = acc;
259end
260self.piEnter = piEnterNew;
261end
262
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.
267bool = false;
268if it < 1 || isempty(self.piEnterPrev) || isempty(self.piEnter)
269 return
270end
271E = self.getNumberOfModels;
272l1 = 0;
273for e = 1:E
274 a = self.piEnter{e};
275 b = self.piEnterPrev{e};
276 if isempty(a) || isempty(b) || numel(a) ~= numel(b)
277 return
278 end
279 l1 = max(l1, sum(abs(a(:) - b(:))));
280end
281if isnan(l1) || isinf(l1)
282 return
283end
284if l1 < self.options.iter_tol
285 bool = true;
286 line_debug('ENV statevec converged: iteration %d, max L1 entry change %e < iter_tol %e', ...
287 it, l1, self.options.iter_tol);
288end
289end
290
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;
298
299Qval = zeros(M, K);
300Uval = zeros(M, K);
301Tval = zeros(M, K);
302
303for e = 1:E
304 piF = self.piTimeAvg{e};
305 if isempty(piF)
306 continue
307 end
308 data = self.statevecData{e};
309 if strcmp(data.backend, 'mam')
310 [QN, UN, ~, TN] = solver_mam_ldqbd_avg(data.ld, piF, data.levelOf);
311 else
312 [QN, UN, ~, TN] = solver_ctmc_avg_from_pi(data.sn, piF, self.SS{e}, ...
313 self.SSaggr{e}, data.arvRates, data.depRates);
314 end
315 Qval = Qval + self.envObj.probEnv(e) * QN;
316 Uval = Uval + self.envObj.probEnv(e) * UN;
317 Tval = Tval + self.envObj.probEnv(e) * TN;
318end
319
320self.result.Avg.Q = Qval;
321self.result.Avg.U = Uval;
322self.result.Avg.T = Tval;
323
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);
330end
331
332% -------------------------------------------------------------------------
333function aggregateCacheBlend_(self, E, K)
334if strcmp(self.statevecData{1}.backend, 'mam')
335 return % MAM backend has no enumerated cache state
336end
337sn1 = self.statevecData{1}.sn;
338if ~isfield(sn1, 'nstateful')
339 return
340end
341cacheStateful = [];
342for isf = 1:sn1.nstateful
343 ind = sn1.statefulToNode(isf);
344 if sn1.nodetype(ind) == NodeType.Cache
345 cacheStateful(end+1) = isf; %#ok<AGROW>
346 end
347end
348if isempty(cacheStateful)
349 return
350end
351for isf = cacheStateful
352 hitT = zeros(1, K); missT = zeros(1, K);
353 for e = 1:E
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);
362 for k = 1:K
363 if length(np.hitclass) >= k
364 h = np.hitclass(k); mcl = np.missclass(k);
365 if h > 0 && mcl > 0
366 hitT(k) = hitT(k) + w * (pv' * dr(:, isf, h));
367 missT(k) = missT(k) + w * (pv' * dr(:, isf, mcl));
368 end
369 end
370 end
371 end
372 hitprob = NaN(1, K); missprob = NaN(1, K);
373 for k = 1:K
374 tot = hitT(k) + missT(k);
375 if tot > 0
376 hitprob(k) = hitT(k) / tot;
377 missprob(k) = missT(k) / tot;
378 end
379 end
380 node = self.ensemble{1}.getNodeByIndex(sn1.statefulToNode(isf));
381 node.setResultHitProb(hitprob);
382 node.setResultMissProb(missprob);
383end
384end
Definition Station.m:245