LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
solver_env_meanfield_analyzer.m
1function varargout = solver_env_meanfield_analyzer(self, phase, it, e)
2% SOLVER_ENV_MEANFIELD_ANALYZER Mean-field (marginal mean queue-length) coupling for SolverENV.
3%
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').
10%
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
17%
18% Copyright (c) 2012-2026, Imperial College London
19% All rights reserved.
20
21switch phase
22 case 'pre'
23 pre_(self, it);
24 case 'analyze'
25 [varargout{1}, varargout{2}] = analyze_(self, it, e);
26 case 'post'
27 post_(self, it);
28 case 'finish'
29 finish_(self);
30 case 'converged'
31 varargout{1} = converged_(self, it);
32 otherwise
33 line_error(mfilename, sprintf('Unknown meanfield-analyzer phase: %s', phase));
34end
35end
36
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
41
42 bool = false;
43 if it <= 1
44 return
45 end
46
47 E = self.getNumberOfModels;
48 M = self.ensemble{1}.getNumberOfStations;
49 K = self.ensemble{1}.getNumberOfClasses;
50
51 % Check convergence per class (aligned with JAR structure)
52 for k = 1:K
53 % Build QEntry and QExit matrices (M x E) for this class
54 QEntry = zeros(M, E);
55 QExit = zeros(M, E);
56 for e = 1:E
57 % Skip stages where analysis failed
58 res_curr = self.results{it,e};
59 res_prev = self.results{it-1,e};
60 for i = 1:M
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);
66 end
67 end
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);
73 end
74 end
75 end
76 end
77
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(:));
82 if isempty(maxDiff)
83 maxDiff = 0; % Handle case where all QEntry values are zero
84 end
85 if isnan(maxDiff) || isinf(maxDiff)
86 return % Non-convergence on invalid values
87 end
88 if maxDiff >= self.options.iter_tol
89 return % Not converged
90 end
91 end
92 bool = true;
93end
94
95function pre_(self, it)
96 % PRE(IT)
97
98 if it==1
99 for e=list(self)
100 try
101 if isinf(self.getSolver(e).options.timespan(2))
102 [QN,~,~,~] = self.getSolver(e).getAvg();
103 else
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);
111 else
112 QN(i,k) = 0; % Default to 0 for NaN/invalid entries
113 end
114 end
115 end
116 end
117 if ~isa(self.solvers{e}, 'SolverFluid') && ~isa(self.ensemble{e}, 'LayeredNetwork')
118 QN = self.roundMarginalForDiscreteSolver(QN, self.sn{e});
119 end
120 self.ensemble{e}.initFromMarginal(QN);
121 catch
122 % Skip pre-initialization for stages where getAvg fails
123 end
124 end
125 end
126end
127
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') = [];
133 T0 = tic;
134 runtime = toc(T0);
135 %% initialize
136 try
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;
143 catch
144 % Skip analysis for stages where transient analysis fails
145 end
146end
147
148function post_(self, it)
149 % POST(IT)
150
151 E = self.getNumberOfModels;
152 M = self.ensemble{1}.getNumberOfStations;
153 K = self.ensemble{1}.getNumberOfClasses;
154 [detSojourn, dvals] = sojournConfig_(self, E);
155 for e=1: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')
158 for h = 1:E
159 Qexit{e,h} = zeros(M, K);
160 Uexit{e,h} = zeros(M, K);
161 Texit{e,h} = zeros(M, K);
162 end
163 continue
164 end
165 if detSojourn
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));
177 end
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));
181 end
182 end
183 end
184 end
185 for h = 1:E
186 Qexit{e,h} = Qd; Uexit{e,h} = Ud; Texit{e,h} = Td;
187 end
188 continue
189 end
190 for h = 1:E
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))]';
200 if ~isnan(w{e,h})
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});
205 end
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});
209 end
210 else
211 Qexit{e,h}(i,r) = 0;
212 Uexit{e,h}(i,r) = 0;
213 Texit{e,h}(i,r) = 0;
214 end
215 else
216 w{e,h} = 0;
217 end
218 end
219 end
220 end
221 end
222
223 Qentry = cell(1,E); % average entry queue-length
224 for e = 1:E
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')
228 continue
229 end
230 Qentry{e} = zeros(size(Qexit{e}));
231 for h=1: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});
235 end
236 end
237 if ~isa(self.solvers{e}, 'SolverFluid') && ~isa(self.ensemble{e}, 'LayeredNetwork')
238 Qentry{e} = self.roundMarginalForDiscreteSolver(Qentry{e}, self.sn{e});
239 end
240 self.solvers{e}.reset();
241 self.ensemble{e}.initFromMarginal(Qentry{e});
242 end
243
244 % Update transition rates between stages if state-dependent method
245 if isfield(self.options, 'method') && strcmp(self.options.method, 'statedep')
246 for e = 1:E
247 for h = 1:E
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});
251 end
252 end
253 end
254 % Reinitialize environment after rate updates
255 self.envObj.init();
256 end
257end
258
259function finish_(self)
260 % FINISH()
261
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);
267 for e=1: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)
278 if detSojourn
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));
284 else
285 UExit{e}(i,r) = 0;
286 end
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));
290 else
291 TExit{e}(i,r) = 0;
292 end
293 continue
294 end
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});
300 else
301 UExit{e}(i,r) = 0;
302 end
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});
306 else
307 TExit{e}(i,r) = 0;
308 end
309 else
310 QExit{e}(i,r) = 0;
311 UExit{e}(i,r) = 0;
312 TExit{e}(i,r) = 0;
313 end
314 end
315 end
316 end
317 % for h = 1: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))]';
322 % if ~isnan(w{e,h})
323 % QE{e,h}(i,r) = self.results{it,e}.Tran.Avg.Q{i,r}(:,1)'*w{e,h}/sum(w{e,h});
324 % else
325 % QE{e,h}(i,r) = 0;
326 % end
327 % end
328 % end
329 % end
330 end
331
332 Qval=0*QExit{e};
333 Uval=0*UExit{e};
334 Tval=0*TExit{e};
335 for e=1:E
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
339 end
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
348 % line_printf('\n');
349 %end
350
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);
355end
356
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;
368
369 % Only fluid stages expose the RMF cache transient used here.
370 for e = 1:E
371 if ~isa(self.solvers{e}, 'SolverFluid')
372 return
373 end
374 end
375 sn1 = self.sn{1};
376 if ~isfield(sn1, 'nodetype')
377 return
378 end
379 cacheNodes = find(sn1.nodetype == NodeType.Cache);
380 if isempty(cacheNodes)
381 return
382 end
383 ncaches = numel(cacheNodes);
384
385 % Finite integration window per stage (fall back to a few mean
386 % holding times when the inner solver left the timespan open).
387 tspan = cell(1, E);
388 for e = 1:E
389 ts = self.solvers{e}.options.timespan;
390 if isempty(ts) || any(~isfinite(ts))
391 ts = [0, 20 * map_mean(self.envObj.holdTime{e})];
392 end
393 tspan{e} = ts;
394 end
395
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;
401 prevEntryFlat = [];
402 for sweep = 1:maxSweep
403 for e = 1:E
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});
409 tt = tc{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);
415 for cc = 1:ncaches
416 cidx = find(cnodes == cacheNodes(cc), 1);
417 if isempty(cidx)
418 continue
419 end
420 Lam = sum(ar{e}(cidx, :));
421 if ~(Lam > 0)
422 continue
423 end
424 treal = tt / Lam;
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(:);
428 sw = sum(wm);
429 if ~isempty(xocc{cidx}) && sw > 0
430 exitOcc{e}{cc} = xocc{cidx} * wmass{e}{cc} / sw;
431 end
432 end
433 end
434 % Update each stage's entry occupancy from its predecessors.
435 newEntry = repmat({cell(1, ncaches)}, 1, E);
436 for e = 1:E
437 for cc = 1:ncaches
438 acc = [];
439 for h = 1:E
440 po = self.envObj.probOrig(h, e);
441 if po > 0 && ~isempty(exitOcc{h}{cc})
442 if isempty(acc)
443 acc = po * exitOcc{h}{cc};
444 else
445 acc = acc + po * exitOcc{h}{cc};
446 end
447 end
448 end
449 newEntry{e}{cc} = acc;
450 end
451 end
452 entryFlat = [];
453 for e = 1:E
454 for cc = 1:ncaches
455 entryFlat = [entryFlat; newEntry{e}{cc}(:)]; %#ok<AGROW>
456 end
457 end
458 entryOcc = newEntry;
459 if ~isempty(prevEntryFlat) && numel(prevEntryFlat) == numel(entryFlat)
460 if norm(entryFlat - prevEntryFlat, inf) < tol
461 prevEntryFlat = entryFlat;
462 break
463 end
464 end
465 prevEntryFlat = entryFlat;
466 end
467
468 % Aggregate the converged hit/miss throughputs and write results.
469 for cc = 1:ncaches
470 hitT = zeros(1, K); missT = zeros(1, K);
471 for e = 1:E
472 if numel(wmass{e}) < cc || isempty(wmass{e}{cc})
473 continue
474 end
475 w = wmass{e}{cc};
476 sw = sum(w);
477 if ~(sw > 0) || any(~isfinite(w))
478 continue
479 end
480 cidx = cc; % cnodes ordering matches cacheNodes across stages
481 pe = self.envObj.probEnv(e);
482 for k = 1:K
483 a = ar{e}(cidx, k);
484 if a <= 0
485 continue
486 end
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;
491 end
492 end
493 hitprob = NaN(1, K); missprob = NaN(1, K);
494 for k = 1:K
495 tot = hitT(k) + missT(k);
496 if tot > 0
497 hitprob(k) = hitT(k) / tot;
498 missprob(k) = missT(k) / tot;
499 end
500 end
501 node = ref.getNodeByIndex(cacheNodes(cc));
502 node.setResultHitProb(hitprob);
503 node.setResultMissProb(missprob);
504 end
505end
506
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');
512dvals = zeros(1, E);
513if detSojourn
514 for e = 1:E
515 dvals(e) = map_mean(self.envObj.holdTime{e});
516 end
517end
518end
519
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');
525end