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: 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));
178 end
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));
182 end
183 end
184 end
185 end
186 for h = 1:E
187 Qexit{e,h} = Qd; Uexit{e,h} = Ud; Texit{e,h} = Td;
188 end
189 continue
190 end
191 for h = 1:E
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))]';
201 if ~isnan(w{e,h})
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});
206 end
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});
210 end
211 else
212 Qexit{e,h}(i,r) = 0;
213 Uexit{e,h}(i,r) = 0;
214 Texit{e,h}(i,r) = 0;
215 end
216 else
217 w{e,h} = 0;
218 end
219 end
220 end
221 end
222 end
223
224 Qentry = cell(1,E); % average entry queue-length
225 for e = 1:E
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')
229 continue
230 end
231 Qentry{e} = zeros(size(Qexit{e}));
232 for h=1: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});
236 end
237 end
238 if ~isa(self.solvers{e}, 'SolverFluid') && ~isa(self.ensemble{e}, 'LayeredNetwork')
239 Qentry{e} = self.roundMarginalForDiscreteSolver(Qentry{e}, self.sn{e});
240 end
241 self.solvers{e}.reset();
242 self.ensemble{e}.initFromMarginal(Qentry{e});
243 end
244
245 % Update transition rates between stages if state-dependent method
246 if isfield(self.options, 'method') && strcmp(self.options.method, 'statedep')
247 for e = 1:E
248 for h = 1:E
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});
252 end
253 end
254 end
255 % Reinitialize environment after rate updates
256 self.envObj.init();
257 end
258end
259
260function finish_(self)
261 % FINISH()
262
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);
268 for e=1: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)
279 if detSojourn
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));
285 else
286 UExit{e}(i,r) = 0;
287 end
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));
291 else
292 TExit{e}(i,r) = 0;
293 end
294 continue
295 end
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});
301 else
302 UExit{e}(i,r) = 0;
303 end
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});
307 else
308 TExit{e}(i,r) = 0;
309 end
310 else
311 QExit{e}(i,r) = 0;
312 UExit{e}(i,r) = 0;
313 TExit{e}(i,r) = 0;
314 end
315 end
316 end
317 end
318 % for h = 1: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))]';
323 % if ~isnan(w{e,h})
324 % QE{e,h}(i,r) = self.results{it,e}.Tran.Avg.Q{i,r}(:,1)'*w{e,h}/sum(w{e,h});
325 % else
326 % QE{e,h}(i,r) = 0;
327 % end
328 % end
329 % end
330 % end
331 end
332
333 Qval=0*QExit{e};
334 Uval=0*UExit{e};
335 Tval=0*TExit{e};
336 for e=1:E
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
340 end
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
349 % line_printf('\n');
350 %end
351
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);
358end
359
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;
371
372 % Only fluid stages expose the RMF cache transient used here.
373 for e = 1:E
374 if ~isa(self.solvers{e}, 'SolverFluid')
375 return
376 end
377 end
378 sn1 = self.sn{1};
379 if ~isfield(sn1, 'nodetype')
380 return
381 end
382 cacheNodes = find(sn1.nodetype == NodeType.Cache);
383 if isempty(cacheNodes)
384 return
385 end
386 ncaches = numel(cacheNodes);
387
388 % Finite integration window per stage (fall back to a few mean
389 % holding times when the inner solver left the timespan open).
390 tspan = cell(1, E);
391 for e = 1:E
392 ts = self.solvers{e}.options.timespan;
393 if isempty(ts) || any(~isfinite(ts))
394 ts = [0, 20 * map_mean(self.envObj.holdTime{e})];
395 end
396 tspan{e} = ts;
397 end
398
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;
404 prevEntryFlat = [];
405 for sweep = 1:maxSweep
406 for e = 1:E
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});
412 tt = tc{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);
420 for cc = 1:ncaches
421 cidx = find(cnodes == cacheNodes(cc), 1);
422 if isempty(cidx)
423 continue
424 end
425 Lam = sum(ar{e}(cidx, :));
426 if ~(Lam > 0)
427 continue
428 end
429 treal = tt / Lam;
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(:);
433 sw = sum(wm);
434 if ~isempty(xocc{cidx}) && sw > 0
435 exitOcc{e}{cc} = xocc{cidx} * wmass{e}{cc} / sw;
436 end
437 end
438 end
439 % Update each stage's entry occupancy from its predecessors.
440 newEntry = repmat({cell(1, ncaches)}, 1, E);
441 for e = 1:E
442 for cc = 1:ncaches
443 acc = [];
444 for h = 1:E
445 po = self.envObj.probOrig(h, e);
446 if po > 0 && ~isempty(exitOcc{h}{cc})
447 if isempty(acc)
448 acc = po * exitOcc{h}{cc};
449 else
450 acc = acc + po * exitOcc{h}{cc};
451 end
452 end
453 end
454 newEntry{e}{cc} = acc;
455 end
456 end
457 entryFlat = [];
458 for e = 1:E
459 for cc = 1:ncaches
460 entryFlat = [entryFlat; newEntry{e}{cc}(:)]; %#ok<AGROW>
461 end
462 end
463 entryOcc = newEntry;
464 if ~isempty(prevEntryFlat) && numel(prevEntryFlat) == numel(entryFlat)
465 if norm(entryFlat - prevEntryFlat, inf) < tol
466 prevEntryFlat = entryFlat;
467 break
468 end
469 end
470 prevEntryFlat = entryFlat;
471 end
472
473 % Aggregate the converged hit/miss throughputs and write results.
474 for cc = 1:ncaches
475 hitT = zeros(1, K); missT = zeros(1, K);
476 for e = 1:E
477 if numel(wmass{e}) < cc || isempty(wmass{e}{cc})
478 continue
479 end
480 w = wmass{e}{cc};
481 sw = sum(w);
482 if ~(sw > 0) || any(~isfinite(w))
483 continue
484 end
485 cidx = cc; % cnodes ordering matches cacheNodes across stages
486 pe = self.envObj.probEnv(e);
487 for k = 1:K
488 a = ar{e}(cidx, k);
489 if a <= 0
490 continue
491 end
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;
496 end
497 end
498 hitprob = NaN(1, K); missprob = NaN(1, K);
499 for k = 1:K
500 tot = hitT(k) + missT(k);
501 if tot > 0
502 hitprob(k) = hitT(k) / tot;
503 missprob(k) = missT(k) / tot;
504 end
505 end
506 node = ref.getNodeByIndex(cacheNodes(cc));
507 node.setResultHitProb(hitprob);
508 node.setResultMissProb(missprob);
509 end
510end
511
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');
517dvals = zeros(1, E);
518if detSojourn
519 for e = 1:E
520 dvals(e) = map_mean(self.envObj.holdTime{e});
521 end
522end
523end
524
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');
530end
Definition Station.m:245