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:
the stage lasts exactly d_e, so
the
167 % exit metrics are
the transient evaluated at t=d_e and are
the
168 % same toward every destination h.
169 Qd = zeros(M, K); Ud = zeros(M, K); Td = zeros(M, K);
170 for i=1:size(self.results{it,e}.Tran.Avg.Q,1)
171 for r=1:size(self.results{it,e}.Tran.Avg.Q,2)
172 Qir = self.results{it,e}.Tran.Avg.Q{i,r};
173 if isstruct(Qir) && isfield(Qir,
't') && isfield(Qir,
'metric') && ~isempty(Qir.t)
174 Qd(i,r) = detEval_(Qir.t, Qir.metric, dvals(e));
175 Uir = self.results{it,e}.Tran.Avg.U{i,r};
176 if isstruct(Uir) && isfield(Uir,
'metric') && ~isempty(Uir.metric)
177 Ud(i,r) = detEval_(Qir.t, Uir.metric, dvals(e));
179 Tir = self.results{it,e}.Tran.Avg.T{i,r};
180 if isstruct(Tir) && isfield(Tir,
'metric') && ~isempty(Tir.metric)
181 Td(i,r) = detEval_(Qir.t, Tir.metric, dvals(e));
187 Qexit{e,h} = Qd; Uexit{e,h} = Ud; Texit{e,h} = Td;
192 Qexit{e,h} = zeros(size(self.results{it,e}.Tran.Avg.Q));
193 Uexit{e,h} = zeros(size(self.results{it,e}.Tran.Avg.U));
194 Texit{e,h} = zeros(size(self.results{it,e}.Tran.Avg.T));
195 for i=1:size(self.results{it,e}.Tran.Avg.Q,1)
196 for r=1:size(self.results{it,e}.Tran.Avg.Q,2)
197 Qir = self.results{it,e}.Tran.Avg.Q{i,r};
198 % Check
if result
is a valid
struct with required fields
199 if isstruct(Qir) && isfield(Qir,
't') && isfield(Qir,
'metric') && ~isempty(Qir.t)
200 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))]
';
202 Qexit{e,h}(i,r) = Qir.metric'*w{e,h}/sum(w{e,h});
203 Uir = self.results{it,e}.Tran.Avg.U{i,r};
204 if isstruct(Uir) && isfield(Uir,
'metric') && ~isempty(Uir.metric)
205 Uexit{e,h}(i,r) = Uir.metric
'*w{e,h}/sum(w{e,h});
207 Tir = self.results{it,e}.Tran.Avg.T{i,r};
208 if isstruct(Tir) && isfield(Tir, 'metric
') && ~isempty(Tir.metric)
209 Texit{e,h}(i,r) = Tir.metric'*w{e,h}/sum(w{e,h});
224 Qentry = cell(1,E); % average entry queue-length
226 % Skip stages where analysis failed (no valid Tran results)
227 if isempty(self.results{it,e}) || ~isfield(self.results{it,e},
'Tran') ...
228 || ~isstruct(self.results{it,e}.Tran.Avg) || ~isfield(self.results{it,e}.Tran.Avg,
'Q')
231 Qentry{e} = zeros(size(Qexit{e}));
233 % probability of coming from h to e \times resetFun(Qexit from h to e
234 if self.envObj.probOrig(h,e) > 0
235 Qentry{e} = Qentry{e} + self.envObj.probOrig(h,e) * self.resetFromMarginal{h,e}(Qexit{h,e});
238 if ~isa(self.solvers{e},
'SolverFluid') && ~isa(self.ensemble{e},
'LayeredNetwork')
239 Qentry{e} = self.roundMarginalForDiscreteSolver(Qentry{e}, self.sn{e});
241 self.solvers{e}.reset();
242 self.ensemble{e}.initFromMarginal(Qentry{e});
245 % Update transition rates between stages
if state-dependent method
246 if isfield(self.options,
'method') && strcmp(self.options.method,
'statedep')
249 if ~isa(self.envObj.env{e,h},
'Disabled') && ~isempty(self.resetEnvRates{e,h})
250 self.envObj.env{e,h} = self.resetEnvRates{e,h}(...
251 self.envObj.env{e,h}, Qexit{e,h}, Uexit{e,h}, Texit{e,h});
255 % Reinitialize environment after rate updates
260function finish_(self)
263 it = size(self.results,1); % use last iteration
264 E = self.getNumberOfModels;
265 M = self.ensemble{1}.getNumberOfStations;
266 K = self.ensemble{1}.getNumberOfClasses;
267 [detSojourn, dvals] = sojournConfig_(self, E);
269 QExit{e}=zeros(M, K);
270 UExit{e}=zeros(M, K);
271 TExit{e}=zeros(M, K);
272 if it>0 && ~isempty(self.results{it,e}) && isfield(self.results{it,e},
'Tran') ...
273 && isstruct(self.results{it,e}.Tran.Avg) && isfield(self.results{it,e}.Tran.Avg,
'Q')
274 for i=1:size(self.results{it,e}.Tran.Avg.Q,1)
275 for r=1:size(self.results{it,e}.Tran.Avg.Q,2)
276 Qir = self.results{it,e}.Tran.Avg.Q{i,r};
277 % Check
if result
is a valid
struct with required fields
278 if isstruct(Qir) && isfield(Qir,
't') && isfield(Qir,
'metric') && ~isempty(Qir.t)
280 % Deterministic sojourn: evaluate at t=d_e.
281 QExit{e}(i,r) = detEval_(Qir.t, Qir.metric, dvals(e));
282 Uir = self.results{it,e}.Tran.Avg.U{i,r};
283 if isstruct(Uir) && isfield(Uir,
'metric') && ~isempty(Uir.metric)
284 UExit{e}(i,r) = detEval_(Qir.t, Uir.metric, dvals(e));
288 Tir = self.results{it,e}.Tran.Avg.T{i,r};
289 if isstruct(Tir) && isfield(Tir,
'metric') && ~isempty(Tir.metric)
290 TExit{e}(i,r) = detEval_(Qir.t, Tir.metric, dvals(e));
296 w{e} = [0, map_cdf(self.envObj.holdTime{e}, Qir.t(2:end)) - map_cdf(self.envObj.holdTime{e}, Qir.t(1:end-1))]
';
297 QExit{e}(i,r) = Qir.metric'*w{e}/sum(w{e});
298 Uir = self.results{it,e}.Tran.Avg.U{i,r};
299 if isstruct(Uir) && isfield(Uir,
'metric') && ~isempty(Uir.metric)
300 UExit{e}(i,r) = Uir.metric
'*w{e}/sum(w{e});
304 Tir = self.results{it,e}.Tran.Avg.T{i,r};
305 if isstruct(Tir) && isfield(Tir, 'metric
') && ~isempty(Tir.metric)
306 TExit{e}(i,r) = Tir.metric'*w{e}/sum(w{e});
319 % QE{e,h} = zeros(size(self.results{it,e}.Tran.Avg.Q));
320 %
for i=1:size(self.results{it,e}.Tran.Avg.Q,1)
321 %
for r=1:size(self.results{it,e}.Tran.Avg.Q,2)
322 % 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))]
';
324 % QE{e,h}(i,r) = self.results{it,e}.Tran.Avg.Q{i,r}(:,1)'*w{e,h}/sum(w{e,h});
337 Qval = Qval + self.envObj.probEnv(e) * QExit{e}; % to check
338 Uval = Uval + self.envObj.probEnv(e) * UExit{e}; % to check
339 Tval = Tval + self.envObj.probEnv(e) * TExit{e}; % to check
341 self.result.Avg.Q = Qval;
342 % self.result.Avg.R = R;
343 % self.result.Avg.X = X;
344 self.result.Avg.U = Uval;
345 self.result.Avg.T = Tval;
346 % self.result.Avg.C = C;
347 %self.result.runtime = runtime;
348 %
if self.options.verbose
352 % Cache-hit aggregation across
the environment
for inner
solvers that
353 % expose a transient cache trajectory (FLD refined mean-field). The
354 % per-
class hit throughput
is the probEnv-weighted, sojourn-averaged
355 % (arrival x hit-prob) over stages;
the actual hit ratio
is
356 % hit/(hit+miss), written onto
the reference model
's cache nodes.
357 aggregateCacheMeanfield_(self, it, E);
360function aggregateCacheMeanfield_(self, it, E) %#ok<INUSD>
361 % Cache-hit aggregation for fluid inner solvers. Runs a self-
362 % contained mean-field fixed point that carries each cache's mean
363 % occupancy across environment switches (
the cache analog of
the
364 % queue-length initFromMarginal handoff): stage e
is integrated over
365 % its sojourn from an entry occupancy that mixes
the exit occupancy
366 % of its predecessors by probOrig. At convergence
the per-
class hit
367 % throughput
is the probEnv-weighted, sojourn-averaged arrival x
368 % hit-prob, and
the reported hit ratio
is hit/(hit+miss).
369 ref = self.ensemble{1};
370 K = ref.getNumberOfClasses;
372 % Only fluid stages expose
the RMF cache transient used here.
374 if ~isa(self.solvers{e},
'SolverFluid')
379 if ~isfield(sn1,
'nodetype')
382 cacheNodes = find(sn1.nodetype == NodeType.Cache);
383 if isempty(cacheNodes)
386 ncaches = numel(cacheNodes);
388 % Finite integration window per stage (fall back to a few mean
389 % holding times when
the inner solver left
the timespan open).
392 ts = self.
solvers{e}.options.timespan;
393 if isempty(ts) || any(~isfinite(ts))
394 ts = [0, 20 * map_mean(self.envObj.holdTime{e})];
399 entryOcc = repmat({cell(1, ncaches)}, 1, E); % entryOcc{e}{c}
400 hp = cell(1, E); mp = cell(1, E); ar = cell(1, E);
401 tc = cell(1, E); wmass = cell(1, E); exitOcc = cell(1, E);
402 maxSweep = max(1, self.options.iter_max);
403 tol = self.options.iter_tol;
405 for sweep = 1:maxSweep
407 opts_e = self.solvers{e}.options;
408 opts_e.method =
'rmf';
409 opts_e.timespan = tspan{e};
410 [tc{e}, hp{e}, mp{e}, cnodes, ar{e}, xocc] = ...
411 solver_fld_cacheqn_tran(self.sn{e}, opts_e, entryOcc{e});
413 % Sojourn-weighted exit occupancy per cache for the handoff.
414 % The RMF drift evolves in normalized (per-request) time; the
415 % cache's total request rate maps it to real time so
the
416 % holding-time weighting
is consistent across phases with
417 % different arrival rates.
418 exitOcc{e} = cell(1, ncaches);
419 wmass{e} = cell(1, ncaches);
421 cidx = find(cnodes == cacheNodes(cc), 1);
425 Lam = sum(ar{e}(cidx, :));
430 wm = [0, map_cdf(self.envObj.holdTime{e}, treal(2:end)) ...
431 - map_cdf(self.envObj.holdTime{e}, treal(1:end-1))];
432 wmass{e}{cc} = wm(:);
434 if ~isempty(xocc{cidx}) && sw > 0
435 exitOcc{e}{cc} = xocc{cidx} * wmass{e}{cc} / sw;
439 % Update each stage
's entry occupancy from its predecessors.
440 newEntry = repmat({cell(1, ncaches)}, 1, E);
445 po = self.envObj.probOrig(h, e);
446 if po > 0 && ~isempty(exitOcc{h}{cc})
448 acc = po * exitOcc{h}{cc};
450 acc = acc + po * exitOcc{h}{cc};
454 newEntry{e}{cc} = acc;
460 entryFlat = [entryFlat; newEntry{e}{cc}(:)]; %#ok<AGROW>
464 if ~isempty(prevEntryFlat) && numel(prevEntryFlat) == numel(entryFlat)
465 if norm(entryFlat - prevEntryFlat, inf) < tol
466 prevEntryFlat = entryFlat;
470 prevEntryFlat = entryFlat;
473 % Aggregate the converged hit/miss throughputs and write results.
475 hitT = zeros(1, K); missT = zeros(1, K);
477 if numel(wmass{e}) < cc || isempty(wmass{e}{cc})
482 if ~(sw > 0) || any(~isfinite(w))
485 cidx = cc; % cnodes ordering matches cacheNodes across stages
486 pe = self.envObj.probEnv(e);
492 hbar = reshape(hp{e}(cidx,k,:), 1, []) * w / sw;
493 mbar = reshape(mp{e}(cidx,k,:), 1, []) * w / sw;
494 hitT(k) = hitT(k) + pe * a * hbar;
495 missT(k) = missT(k) + pe * a * mbar;
498 hitprob = NaN(1, K); missprob = NaN(1, K);
500 tot = hitT(k) + missT(k);
502 hitprob(k) = hitT(k) / tot;
503 missprob(k) = missT(k) / tot;
506 node = ref.getNodeByIndex(cacheNodes(cc));
507 node.setResultHitProb(hitprob);
508 node.setResultMissProb(missprob);
512function [detSojourn, dvals] = sojournConfig_(self, E)
513% Sojourn option: 'stochastic
' (default) averages each stage's transient over
514%
the random holding-time CDF;
'deterministic' (off by
default) evaluates it at
515%
the fixed mean duration d_e = map_mean(holdTime{e}).
516detSojourn = isfield(self.options,
'sojourn') && strcmpi(self.options.sojourn,
'deterministic');
520 dvals(e) = map_mean(self.envObj.holdTime{e});
525function v = detEval_(t, metric, d)
526% Transient metric at
the deterministic sojourn time d (clamped to
the grid).
527t = t(:); metric = metric(:);
528d = max(t(1), min(d, t(end)));
529v = interp1(t, metric, d,
'linear');