1function varargout = solver_env_meanfield_analyzer(self, phase, it, e)
2% SOLVER_ENV_MEANFIELD_ANALYZER Mean-field (marginal mean queue-length) coupling
for SolverENV.
4% This analyzer implements the
default (and historical) SolverENV coupling:
5% every stage
is solved transiently and reduced to its marginal mean queue
6% lengths, which are carried across environment switches via initFromMarginal.
7% This mean-field collapse discards the joint state distribution at the handoff;
8% see solver_env_statevec_analyzer
for the full state-vector alternative. It
is
9% the
default analyzer (options.method other than
'statevec').
11% Phase dispatch (called by the SolverENV EnsembleSolver hooks):
12%
'pre' pre_(self,it) -> []
13%
'analyze' analyze_(self,it,e) -> [results_e, runtime]
14%
'post' post_(self,it) -> []
15%
'finish' finish_(self) -> []
16%
'converged' converged_(self,it) -> bool
18% Copyright (c) 2012-2026, Imperial College London
25 [varargout{1}, varargout{2}] = analyze_(self, it, e);
31 varargout{1} = converged_(self, it);
33 line_error(mfilename, sprintf(
'Unknown meanfield-analyzer phase: %s', phase));
37function
bool = converged_(self, it) % convergence test at iteration it
38 % BOOL = CONVERGED(IT) % CONVERGENCE TEST AT ITERATION IT
39 % Computes max relative absolute difference of queue lengths between iterations
40 % Aligned with JAR SolverEnv.converged() implementation
47 E = self.getNumberOfModels;
48 M = self.ensemble{1}.getNumberOfStations;
49 K = self.ensemble{1}.getNumberOfClasses;
51 % Check convergence per
class (aligned with JAR structure)
53 % Build QEntry and QExit matrices (M x E) for this class
57 % Skip stages where analysis failed
58 res_curr = self.results{it,e};
59 res_prev = self.results{it-1,e};
61 if ~isempty(res_curr) && isfield(res_curr,
'Tran') ...
62 && isstruct(res_curr.Tran.Avg) && isfield(res_curr.Tran.Avg,
'Q')
63 Qik_curr = res_curr.Tran.Avg.Q{i,k};
64 if isstruct(Qik_curr) && isfield(Qik_curr,
'metric') && ~isempty(Qik_curr.metric)
65 QExit(i,e) = Qik_curr.metric(1);
68 if ~isempty(res_prev) && isfield(res_prev, 'Tran') ...
69 && isstruct(res_prev.Tran.Avg) && isfield(res_prev.Tran.Avg, 'Q')
70 Qik_prev = res_prev.Tran.Avg.Q{i,k};
71 if isstruct(Qik_prev) && isfield(Qik_prev,
'metric') && ~isempty(Qik_prev.metric)
72 QEntry(i,e) = Qik_prev.metric(1);
78 % Compute max relative absolute difference using maxpe
79 % maxpe computes max(abs(1 - approx./exact)) = max(abs((approx-exact)./exact))
80 % This matches JAR's Matrix.maxAbsDiff() implementation
81 maxDiff = maxpe(QExit(:), QEntry(:));
83 maxDiff = 0; % Handle case where all QEntry values are zero
85 if isnan(maxDiff) || isinf(maxDiff)
86 return % Non-convergence on invalid values
88 if maxDiff >= self.options.iter_tol
89 return % Not converged
95function pre_(self, it)
101 if isinf(self.getSolver(e).options.timespan(2))
102 [QN,~,~,~] = self.getSolver(e).getAvg();
104 [QNt,~,~] = self.getSolver(e).getTranAvg();
105 % Handle case where getTranAvg returns NaN for disabled/empty results
106 QN = zeros(size(QNt));
107 for i = 1:size(QNt,1)
108 for k = 1:size(QNt,2)
109 if isstruct(QNt{i,k}) && isfield(QNt{i,k},
'metric')
110 QN(i,k) = QNt{i,k}.metric(end);
112 QN(i,k) = 0; % Default to 0
for NaN/invalid entries
117 if ~isa(self.solvers{e},
'SolverFluid') && ~isa(self.ensemble{e},
'LayeredNetwork')
118 QN = self.roundMarginalForDiscreteSolver(QN, self.sn{e});
120 self.ensemble{e}.initFromMarginal(QN);
122 % Skip pre-initialization
for stages where getAvg fails
128function [results_e, runtime] = analyze_(self, it, e)
129 % [RESULTS_E, RUNTIME] = ANALYZE(IT, E)
130 results_e =
struct();
131 results_e.(
'Tran') =
struct();
132 results_e.Tran.(
'Avg') = [];
137 [Qt,Ut,Tt] = self.ensemble{e}.getTranHandles;
138 self.solvers{e}.reset();
139 [QNt,UNt,TNt] = self.solvers{e}.getTranAvg(Qt,Ut,Tt);
140 results_e.Tran.Avg.Q = QNt;
141 results_e.Tran.Avg.U = UNt;
142 results_e.Tran.Avg.T = TNt;
144 % Skip analysis
for stages where transient analysis fails
148function post_(self, it)
151 E = self.getNumberOfModels;
152 M = self.ensemble{1}.getNumberOfStations;
153 K = self.ensemble{1}.getNumberOfClasses;
154 [detSojourn, dvals] = sojournConfig_(self, E);
156 if isempty(self.results{it,e}) || ~isfield(self.results{it,e},
'Tran') ...
157 || ~isstruct(self.results{it,e}.Tran.Avg) || ~isfield(self.results{it,e}.Tran.Avg,
'Q')
159 Qexit{e,h} = zeros(M, K);
160 Uexit{e,h} = zeros(M, K);
161 Texit{e,h} = zeros(M, K);
166 % Deterministic sojourn: exit metrics are the transient at
167 % t=d_e. see _kb/06-solver-catalog.md
for rationale
168 Qd = zeros(M, K); Ud = zeros(M, K); Td = zeros(M, K);
169 for i=1:size(self.results{it,e}.Tran.Avg.Q,1)
170 for r=1:size(self.results{it,e}.Tran.Avg.Q,2)
171 Qir = self.results{it,e}.Tran.Avg.Q{i,r};
172 if isstruct(Qir) && isfield(Qir,
't') && isfield(Qir,
'metric') && ~isempty(Qir.t)
173 Qd(i,r) = detEval_(Qir.t, Qir.metric, dvals(e));
174 Uir = self.results{it,e}.Tran.Avg.U{i,r};
175 if isstruct(Uir) && isfield(Uir,
'metric') && ~isempty(Uir.metric)
176 Ud(i,r) = detEval_(Qir.t, Uir.metric, dvals(e));
178 Tir = self.results{it,e}.Tran.Avg.T{i,r};
179 if isstruct(Tir) && isfield(Tir,
'metric') && ~isempty(Tir.metric)
180 Td(i,r) = detEval_(Qir.t, Tir.metric, dvals(e));
186 Qexit{e,h} = Qd; Uexit{e,h} = Ud; Texit{e,h} = Td;
191 Qexit{e,h} = zeros(size(self.results{it,e}.Tran.Avg.Q));
192 Uexit{e,h} = zeros(size(self.results{it,e}.Tran.Avg.U));
193 Texit{e,h} = zeros(size(self.results{it,e}.Tran.Avg.T));
194 for i=1:size(self.results{it,e}.Tran.Avg.Q,1)
195 for r=1:size(self.results{it,e}.Tran.Avg.Q,2)
196 Qir = self.results{it,e}.Tran.Avg.Q{i,r};
197 % Check
if result
is a valid
struct with required fields
198 if isstruct(Qir) && isfield(Qir,
't') && isfield(Qir,
'metric') && ~isempty(Qir.t)
199 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))]
';
201 Qexit{e,h}(i,r) = Qir.metric'*w{e,h}/sum(w{e,h});
202 Uir = self.results{it,e}.Tran.Avg.U{i,r};
203 if isstruct(Uir) && isfield(Uir,
'metric') && ~isempty(Uir.metric)
204 Uexit{e,h}(i,r) = Uir.metric
'*w{e,h}/sum(w{e,h});
206 Tir = self.results{it,e}.Tran.Avg.T{i,r};
207 if isstruct(Tir) && isfield(Tir, 'metric
') && ~isempty(Tir.metric)
208 Texit{e,h}(i,r) = Tir.metric'*w{e,h}/sum(w{e,h});
223 Qentry = cell(1,E); % average entry queue-length
225 % Skip stages where analysis failed (no valid Tran results)
226 if isempty(self.results{it,e}) || ~isfield(self.results{it,e},
'Tran') ...
227 || ~isstruct(self.results{it,e}.Tran.Avg) || ~isfield(self.results{it,e}.Tran.Avg,
'Q')
230 Qentry{e} = zeros(size(Qexit{e}));
232 % probability of coming from h to e \times resetFun(Qexit from h to e
233 if self.envObj.probOrig(h,e) > 0
234 Qentry{e} = Qentry{e} + self.envObj.probOrig(h,e) * self.resetFromMarginal{h,e}(Qexit{h,e});
237 if ~isa(self.solvers{e},
'SolverFluid') && ~isa(self.ensemble{e},
'LayeredNetwork')
238 Qentry{e} = self.roundMarginalForDiscreteSolver(Qentry{e}, self.sn{e});
240 self.solvers{e}.reset();
241 self.ensemble{e}.initFromMarginal(Qentry{e});
244 % Update transition rates between stages
if state-dependent method
245 if isfield(self.options,
'method') && strcmp(self.options.method,
'statedep')
248 if ~isa(self.envObj.env{e,h},
'Disabled') && ~isempty(self.resetEnvRates{e,h})
249 self.envObj.env{e,h} = self.resetEnvRates{e,h}(...
250 self.envObj.env{e,h}, Qexit{e,h}, Uexit{e,h}, Texit{e,h});
254 % Reinitialize environment after rate updates
259function finish_(self)
262 it = size(self.results,1); % use last iteration
263 E = self.getNumberOfModels;
264 M = self.ensemble{1}.getNumberOfStations;
265 K = self.ensemble{1}.getNumberOfClasses;
266 [detSojourn, dvals] = sojournConfig_(self, E);
268 QExit{e}=zeros(M, K);
269 UExit{e}=zeros(M, K);
270 TExit{e}=zeros(M, K);
271 if it>0 && ~isempty(self.results{it,e}) && isfield(self.results{it,e},
'Tran') ...
272 && isstruct(self.results{it,e}.Tran.Avg) && isfield(self.results{it,e}.Tran.Avg,
'Q')
273 for i=1:size(self.results{it,e}.Tran.Avg.Q,1)
274 for r=1:size(self.results{it,e}.Tran.Avg.Q,2)
275 Qir = self.results{it,e}.Tran.Avg.Q{i,r};
276 % Check
if result
is a valid
struct with required fields
277 if isstruct(Qir) && isfield(Qir,
't') && isfield(Qir,
'metric') && ~isempty(Qir.t)
279 % Deterministic sojourn: evaluate at t=d_e.
280 QExit{e}(i,r) = detEval_(Qir.t, Qir.metric, dvals(e));
281 Uir = self.results{it,e}.Tran.Avg.U{i,r};
282 if isstruct(Uir) && isfield(Uir,
'metric') && ~isempty(Uir.metric)
283 UExit{e}(i,r) = detEval_(Qir.t, Uir.metric, dvals(e));
287 Tir = self.results{it,e}.Tran.Avg.T{i,r};
288 if isstruct(Tir) && isfield(Tir,
'metric') && ~isempty(Tir.metric)
289 TExit{e}(i,r) = detEval_(Qir.t, Tir.metric, dvals(e));
295 w{e} = [0, map_cdf(self.envObj.holdTime{e}, Qir.t(2:end)) - map_cdf(self.envObj.holdTime{e}, Qir.t(1:end-1))]
';
296 QExit{e}(i,r) = Qir.metric'*w{e}/sum(w{e});
297 Uir = self.results{it,e}.Tran.Avg.U{i,r};
298 if isstruct(Uir) && isfield(Uir,
'metric') && ~isempty(Uir.metric)
299 UExit{e}(i,r) = Uir.metric
'*w{e}/sum(w{e});
303 Tir = self.results{it,e}.Tran.Avg.T{i,r};
304 if isstruct(Tir) && isfield(Tir, 'metric
') && ~isempty(Tir.metric)
305 TExit{e}(i,r) = Tir.metric'*w{e}/sum(w{e});
318 % QE{e,h} = zeros(size(self.results{it,e}.Tran.Avg.Q));
319 %
for i=1:size(self.results{it,e}.Tran.Avg.Q,1)
320 %
for r=1:size(self.results{it,e}.Tran.Avg.Q,2)
321 % 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))]
';
323 % QE{e,h}(i,r) = self.results{it,e}.Tran.Avg.Q{i,r}(:,1)'*w{e,h}/sum(w{e,h});
336 Qval = Qval + self.envObj.probEnv(e) * QExit{e}; % to check
337 Uval = Uval + self.envObj.probEnv(e) * UExit{e}; % to check
338 Tval = Tval + self.envObj.probEnv(e) * TExit{e}; % to check
340 self.result.Avg.Q = Qval;
341 % self.result.Avg.R = R;
342 % self.result.Avg.X = X;
343 self.result.Avg.U = Uval;
344 self.result.Avg.T = Tval;
345 % self.result.Avg.C = C;
346 %self.result.runtime = runtime;
347 %
if self.options.verbose
351 % Cache-hit aggregation across the environment (probEnv-weighted,
352 % sojourn-averaged), written onto the reference model
's cache nodes.
353 % see _kb/06-solver-catalog.md for rationale
354 aggregateCacheMeanfield_(self, it, E);
357function aggregateCacheMeanfield_(self, it, E) %#ok<INUSD>
358 % Cache-hit aggregation for fluid inner solvers. Runs a self-
359 % contained mean-field fixed point that carries each cache's mean
360 % occupancy across environment switches (the cache analog of the
361 % queue-length initFromMarginal handoff): stage e
is integrated over
362 % its sojourn from an entry occupancy that mixes the exit occupancy
363 % of its predecessors by probOrig. At convergence the per-class hit
364 % throughput
is the probEnv-weighted, sojourn-averaged arrival x
365 % hit-prob, and the reported hit ratio
is hit/(hit+miss).
366 ref = self.ensemble{1};
367 K = ref.getNumberOfClasses;
369 % Only fluid stages expose the RMF cache transient used here.
371 if ~isa(self.solvers{e},
'SolverFluid')
376 if ~isfield(sn1,
'nodetype')
379 cacheNodes = find(sn1.nodetype == NodeType.Cache);
380 if isempty(cacheNodes)
383 ncaches = numel(cacheNodes);
385 % Finite integration window per stage (fall back to a few mean
386 % holding times when the inner solver left the timespan open).
389 ts = self.solvers{e}.options.timespan;
390 if isempty(ts) || any(~isfinite(ts))
391 ts = [0, 20 * map_mean(self.envObj.holdTime{e})];
396 entryOcc = repmat({cell(1, ncaches)}, 1, E); % entryOcc{e}{c}
397 hp = cell(1, E); mp = cell(1, E); ar = cell(1, E);
398 tc = cell(1, E); wmass = cell(1, E); exitOcc = cell(1, E);
399 maxSweep = max(1, self.options.iter_max);
400 tol = self.options.iter_tol;
402 for sweep = 1:maxSweep
404 opts_e = self.solvers{e}.options;
405 opts_e.method =
'rmf';
406 opts_e.timespan = tspan{e};
407 [tc{e}, hp{e}, mp{e}, cnodes, ar{e}, xocc] = ...
408 solver_fld_cacheqn_tran(self.sn{e}, opts_e, entryOcc{e});
410 % Sojourn-weighted exit occupancy per cache for the handoff
411 % (RMF drift mapped from per-request to real time).
412 % see _kb/06-solver-catalog.md for rationale
413 exitOcc{e} = cell(1, ncaches);
414 wmass{e} = cell(1, ncaches);
416 cidx = find(cnodes == cacheNodes(cc), 1);
420 Lam = sum(ar{e}(cidx, :));
425 wm = [0, map_cdf(self.envObj.holdTime{e}, treal(2:end)) ...
426 - map_cdf(self.envObj.holdTime{e}, treal(1:end-1))];
427 wmass{e}{cc} = wm(:);
429 if ~isempty(xocc{cidx}) && sw > 0
430 exitOcc{e}{cc} = xocc{cidx} * wmass{e}{cc} / sw;
434 % Update each stage's entry occupancy from its predecessors.
435 newEntry = repmat({cell(1, ncaches)}, 1, E);
440 po = self.envObj.probOrig(h, e);
441 if po > 0 && ~isempty(exitOcc{h}{cc})
443 acc = po * exitOcc{h}{cc};
445 acc = acc + po * exitOcc{h}{cc};
449 newEntry{e}{cc} = acc;
455 entryFlat = [entryFlat; newEntry{e}{cc}(:)]; %#ok<AGROW>
459 if ~isempty(prevEntryFlat) && numel(prevEntryFlat) == numel(entryFlat)
460 if norm(entryFlat - prevEntryFlat, inf) < tol
461 prevEntryFlat = entryFlat;
465 prevEntryFlat = entryFlat;
468 % Aggregate the converged hit/miss throughputs and write results.
470 hitT = zeros(1, K); missT = zeros(1, K);
472 if numel(wmass{e}) < cc || isempty(wmass{e}{cc})
477 if ~(sw > 0) || any(~isfinite(w))
480 cidx = cc; % cnodes ordering matches cacheNodes across stages
481 pe = self.envObj.probEnv(e);
487 hbar = reshape(hp{e}(cidx,k,:), 1, []) * w / sw;
488 mbar = reshape(mp{e}(cidx,k,:), 1, []) * w / sw;
489 hitT(k) = hitT(k) + pe * a * hbar;
490 missT(k) = missT(k) + pe * a * mbar;
493 hitprob = NaN(1, K); missprob = NaN(1, K);
495 tot = hitT(k) + missT(k);
497 hitprob(k) = hitT(k) / tot;
498 missprob(k) = missT(k) / tot;
501 node = ref.getNodeByIndex(cacheNodes(cc));
502 node.setResultHitProb(hitprob);
503 node.setResultMissProb(missprob);
507function [detSojourn, dvals] = sojournConfig_(self, E)
508% Sojourn option:
'stochastic' (
default) averages each stage
's transient over
509% the random holding-time CDF; 'deterministic
' (off by default) evaluates it at
510% the fixed mean duration d_e = map_mean(holdTime{e}).
511detSojourn = isfield(self.options,'sojourn
') && strcmpi(self.options.sojourn,'deterministic
');
515 dvals(e) = map_mean(self.envObj.holdTime{e});
520function v = detEval_(t, metric, d)
521% Transient metric at the deterministic sojourn time d (clamped to the grid).
522t = t(:); metric = metric(:);
523d = max(t(1), min(d, t(end)));
524v = interp1(t, metric, d, 'linear
');