LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_env_meanfield.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_SOLVERS_ENV_SOLVER_ENV_MEANFIELD_H
6#define LINE_SOLVERS_ENV_SOLVER_ENV_MEANFIELD_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * SolverENV, the default mean-field path: ENVIRONMENT COMPRESSION.
12 *
13 * WHAT THIS FILE IS, and what it deliberately is not. The mean-field coupling
14 * itself -- `solver_env_meanfield_analyzer.m`, the `pre_`/`analyze_`/`post_`/
15 * `finish_`/`converged_` cycle over per-stage transient means -- is ALREADY
16 * ported, in `solver_env.h`. What was missing is the other half of the
17 * mean-field path, the half that lives in `@@SolverENV/SolverENV.m` rather than
18 * in the analyzer file: the environment rate matrix `E0`, its generator
19 * `Eutil`, `ctmc_decompose`, `findBestPartition`, `beamSearchPartition`,
20 * `computeMacroRate` and `applyCompression`. That is what this header adds, and
21 * it composes with `SolverEnv<T>` instead of duplicating it.
22 *
23 * WHAT COMPRESSION BUYS. The mean-field fixed point costs one transient stage
24 * solve per stage per iteration, so an environment with many stages is
25 * expensive in the number of stages and not in their size. When the environment
26 * is NEARLY COMPLETELY DECOMPOSABLE -- stages fall into groups that switch
27 * rapidly among themselves and rarely across groups -- each group behaves, on
28 * the slow time scale the network actually feels, like a single stage running
29 * at the group's conditionally-averaged rates. Aggregating the groups replaces
30 * E stage solves by E' < E of them, and the error is governed by the degree of
31 * coupling eps against the admissible epsMAX that the kernels report.
32 *
33 * THE KERNELS ARE NOT RE-DERIVED HERE. `ctmc_courtois`, `ctmc_kms`,
34 * `ctmc_takahashi` and `ctmc_multi` are already ported under `line/api/mc/`,
35 * with their reference-defect histories recorded in their own headers; this
36 * file is the dispatch `SolverENV.ctmc_decompose` puts in front of them and
37 * nothing more. The same discipline applies downstream: the metrics still come
38 * out of `SolverEnv<T>`, which is the single mean-field implementation, so
39 * there is no second copy of the Util computation to drift -- the failure mode
40 * `_kb/06-solver-catalog.md` records under "ENV state-vector Util had drifted
41 * from the CTMC analyzer".
42 *
43 * ONE DELIBERATE DIVERGENCE FROM THE REFERENCE, and it is a correctness fix
44 * rather than a preference. `applyCompression` replaces `self.ensemble`,
45 * `self.solvers` and `self.sn` with their E' macro versions but leaves
46 * `self.envObj` at its original E stages, so `envObj.proc{e}{h}` and
47 * `envObj.holdTime{e}` still describe MICRO transitions while every consumer
48 * now indexes them as macro ones. The analyzer's `post_` and `finish_` weight
49 * the macro transients by those stale micro CDFs, silently, and only when
50 * E' == E do the two agree. Here `env_compress` builds a genuinely compressed
51 * `Environment<T>`: E' stages, and arcs carrying the aggregated macro rates, so
52 * the holding times the analyzer integrates against are the ones its stages
53 * actually have.
54 *
55 * WHAT IS PORTED, and what is refused by name:
56 * ported `E0`/`Eutil`, `ctmc_decompose` over all four kernels,
57 * `findBestPartition`, `beamSearchPartition`, `computeMacroRate`,
58 * `applyCompression`, and the mean-field solve on top of the result
59 * ported `aggregateCacheMeanfield_` too, as of 2026-08-15: it is a SECOND
60 * mean-field fixed point, nested beside the queue-length one and
61 * carrying each Cache's own occupancy across a switch by prob_orig,
62 * because a cache's state is a per-item occupancy that no marginal
63 * queue length encodes. Its per-sweep transient is
64 * `solver_fld_cacheqn_tran`, whose grid runs in PER-REQUEST time --
65 * it is divided by the cache's total arrival rate before the
66 * holding-time CDF is evaluated on it, or every stage is weighted as
67 * though its cache saw the same request rate
68 * refused a Cache under a NON-FLUID stage solver (only the fluid stage
69 * exposes that RMF transient), compression of a non-exponential
70 * environment, and everything `solver_env.h` already refuses by name
71 */
72
73#include <algorithm>
74#include <cmath>
75#include <cstddef>
76#include <limits>
77#include <memory>
78#include <string>
79#include <vector>
80
88#include "line/num/number.h"
93#include "line/util/error.h"
94#include "line/util/matrix.h"
95
96namespace line {
97namespace env {
98
99/** A partition of the stage indices 0..E-1 into macro-states, MATLAB's `MS`. */
100using MacroPartition = std::vector<std::vector<std::size_t>>;
101
102/** The knobs `applyCompression` reads out of `options.config`. */
104 /** `options.config.da`: courtois (the reference's default), kms, takahashi, multi. */
105 std::string da = "courtois";
106 /** `options.config.da_iter`: sweeps for the two iterative kernels. */
107 std::size_t da_iter = 10;
108 /** `options.config.env_alpha`: the beam search's per-depth merge penalty. */
109 double env_alpha = 0.01;
110 /** Beam width, the reference's hard-coded B = 3. */
111 std::size_t beam_width = 3;
112 /**
113 * Stage count above which the reference switches from the pairwise search
114 * to the beam search. The two are genuinely different searches, not one
115 * search with a budget, so the threshold changes the answer and is exposed.
116 */
117 std::size_t beam_above_stages = 10;
118 /**
119 * A partition supplied by the caller, which SKIPS the search entirely.
120 * Worth having because both searches evaluate a decomposition per candidate
121 * pair, and a caller who already knows the group structure -- a repair model
122 * whose stages are (working, degraded) times (peak, offpeak), say -- should
123 * not pay for rediscovering it.
124 */
126};
127
128/** What `SolverENV.ctmc_decompose` returns: `[p, eps, epsMax, q]`. */
129template <class T>
130struct EnvDecomp {
131 std::vector<T> p; ///< approximate stationary vector of the environment
133};
134
135namespace meanfield_detail {
136
137/** Every kernel is seeded by Courtois, whose epsMAX is an eigenvalue modulus. */
138inline void refuse_inexact(const std::string& da) {
139 throw UnsupportedError(
140 "SolverENV compression: the '" + da +
141 "' decomposition needs transcendental arithmetic, because every one of ctmc_courtois, "
142 "ctmc_kms, ctmc_takahashi and ctmc_multi reports epsMAX, a subdominant eigenvalue "
143 "modulus with no rational closed form; rerun with --arith double or real");
144}
145
146/** MS must be a partition of 0..n-1, which every kernel assumes and none checks. */
147inline void check_partition(const MacroPartition& MS, std::size_t n) {
148 std::vector<bool> seen(n, false);
149 std::size_t total = 0;
150 for (const std::vector<std::size_t>& blk : MS) {
151 if (blk.empty()) throw InputError("SolverENV compression: a macro-state is empty");
152 for (std::size_t s : blk) {
153 if (s >= n)
154 throw InputError("SolverENV compression: a macro-state names stage " +
155 std::to_string(s + 1) + ", which does not exist");
156 if (seen[s])
157 throw InputError("SolverENV compression: stage " + std::to_string(s + 1) +
158 " appears in more than one macro-state");
159 seen[s] = true;
160 ++total;
161 }
162 }
163 if (total != n)
164 throw InputError(
165 "SolverENV compression: the macro-states must cover every stage; the partition "
166 "covers " +
167 std::to_string(total) + " of " + std::to_string(n));
168}
169
170/** Singletons, the starting point of both searches and the no-compression fallback. */
171inline MacroPartition singletons(std::size_t E) {
172 MacroPartition MS(E);
173 for (std::size_t i = 0; i < E; ++i) MS[i] = std::vector<std::size_t>{i};
174 return MS;
175}
176
177/** `trial`: merge blocks i and j of `ms`, the merged block taking i's position. */
178inline MacroPartition merge_blocks(const MacroPartition& ms, std::size_t i, std::size_t j) {
179 MacroPartition out;
180 out.reserve(ms.size() - 1);
181 for (std::size_t k = 0; k < ms.size(); ++k) {
182 if (k == i) {
183 std::vector<std::size_t> m = ms[i];
184 m.insert(m.end(), ms[j].begin(), ms[j].end());
185 out.push_back(m);
186 } else if (k != j) {
187 out.push_back(ms[k]);
188 }
189 }
190 return out;
191}
192
193} // namespace meanfield_detail
194
195/**
196 * Port of `SolverENV.ctmc_decompose`: one NCD decomposition, by whichever
197 * kernel `options.config.da` names.
198 *
199 * The uniformization rate the three iterative kernels report is the reference's
200 * `1.05 * max(max(abs(Q)))` rather than anything the kernel itself derived,
201 * which is why it is recomputed here instead of read back off the result.
202 */
203template <class T>
205 const EnvCompressOptions& opt) {
206 meanfield_detail::check_partition(MS, Q.rows());
207 EnvDecomp<T> out;
208 if constexpr (!num_traits<T>::has_transcendental) {
209 meanfield_detail::refuse_inexact(opt.da);
210 } else {
211 // 1.05 max|Q|, the rate the reference hands back for every kernel that
212 // does not report one of its own.
213 T qmax = num_traits<T>::from_int(0);
214 for (std::size_t i = 0; i < Q.rows(); ++i)
215 for (std::size_t j = 0; j < Q.cols(); ++j) {
216 const T a = num_abs(T(Q(i, j)));
217 if (a > qmax) qmax = a;
218 }
219 const T qdefault = T(num_traits<T>::from_rational(21, 20) * qmax);
220
221 if (opt.da == "courtois") {
223 out.p = r.p;
224 out.eps = r.eps;
225 out.epsMAX = r.epsMAX;
226 out.q = r.q;
227 } else if (opt.da == "kms") {
228 const mc::KmsResult<T> r = mc::ctmc_kms(Q, MS, opt.da_iter);
229 out.p = r.p;
230 out.eps = r.eps;
231 out.epsMAX = r.epsMAX;
232 out.q = qdefault;
233 } else if (opt.da == "takahashi") {
234 const mc::TakahashiResult<T> r = mc::ctmc_takahashi(Q, MS, opt.da_iter);
235 out.p = r.p;
236 out.eps = r.eps;
237 out.epsMAX = r.epsMAX;
238 out.q = qdefault;
239 } else if (opt.da == "multi") {
240 // The coarse partition defaults to singletons over the macro-states,
241 // so the second level decouples nothing: the reference exposes no
242 // way to supply a real macro-macro partition, and a two-level method
243 // whose coarse level is singletons is Courtois plus one extra solve.
244 MacroPartition MSS(MS.size());
245 for (std::size_t i = 0; i < MS.size(); ++i) MSS[i] = std::vector<std::size_t>{i};
246 const mc::MultiResult<T> r = mc::ctmc_multi(Q, MS, MSS);
247 out.p = r.p;
248 out.eps = r.eps;
249 out.epsMAX = r.epsMAX;
250 out.q = qdefault;
251 } else {
252 throw UnsupportedError(
253 "SolverENV compression: unknown decomposition '" + opt.da +
254 "'; options.config.da is one of courtois, kms, takahashi, multi");
255 }
256 }
257 return out;
258}
259
260/**
261 * `E0`, the environment's rate matrix: `E0(e,h) = env{e,h}.getRate()`.
262 *
263 * `getRate()` is the RECIPROCAL MEAN of the transition, so a general Markovian
264 * arc collapses to a single rate here and everything downstream treats the
265 * environment as a CTMC. That is the reference's own reading and it is why
266 * `env_compress` refuses a non-exponential environment by name: the collapse is
267 * harmless for the NCD diagnostics, which only ever look at Eutil, but it is
268 * not harmless once the macro arcs are rebuilt from it.
269 */
270template <class T>
272 const std::size_t E = e.nstages();
274 for (std::size_t a = 0; a < E; ++a)
275 for (std::size_t b = 0; b < E; ++b)
276 if (e.arc(a, b).enabled) E0(a, b) = e.arc(a, b).dist.rate();
277 return E0;
278}
279
280/**
281 * Port of `findBestPartition`, the small-environment search.
282 *
283 * ITS COMMENT CLAIMS AN EXHAUSTIVE SEARCH OVER ALL PARTITIONS AND THE CODE DOES
284 * NOT DO THAT. It evaluates the singletons and then every single pairwise merge
285 * of them, so it explores E(E-1)/2 + 1 partitions out of the Bell number of
286 * them and can never return a macro-state of more than two stages. The port is
287 * literal, because the alternative is a different method wearing the reference's
288 * name; a caller who wants deeper merging has `beam_above_stages` and
289 * `EnvCompressOptions::partition`.
290 */
291template <class T>
293 const std::size_t E = Eutil.rows();
294 MacroPartition best = meanfield_detail::singletons(E);
295 EnvDecomp<T> b = env_ctmc_decompose(Eutil, best, opt);
296 double best_eps = num_traits<T>::to_double(b.eps);
297 if (std::isnan(best_eps)) return best;
298
299 for (std::size_t i = 0; i < E; ++i)
300 for (std::size_t j = i + 1; j < E; ++j) {
301 const MacroPartition trial =
302 meanfield_detail::merge_blocks(meanfield_detail::singletons(E), i, j);
303 const EnvDecomp<T> t = env_ctmc_decompose(Eutil, trial, opt);
304 const double te = num_traits<T>::to_double(t.eps);
305 if (!std::isnan(te) && te < best_eps) {
306 best_eps = te;
307 best = trial;
308 }
309 }
310 return best;
311}
312
313/**
314 * Port of `beamSearchPartition`, the large-environment search: repeatedly merge
315 * two blocks, keeping the `beam_width` cheapest partitions at each depth.
316 *
317 * THE COST AND THE INCUMBENT ARE NOT THE SAME QUANTITY, in the reference. The
318 * incumbent `bestEps` is seeded with the raw eps of the singleton partition,
319 * and thereafter compared against `childEps - childEpsMax + alpha * depth`,
320 * which is a penalized score and not an eps at all. A merge is therefore
321 * adopted partly on the strength of its epsMAX and of how deep it sits, against
322 * a threshold that measured neither. This is ported literally rather than
323 * repaired: the search is a heuristic whose output is checked afterwards
324 * against eps <= epsMAX, so the comparison decides which candidate is tried and
325 * not whether the result is admissible.
326 */
327template <class T>
329 const std::size_t E = Eutil.rows();
330 std::vector<MacroPartition> beam{meanfield_detail::singletons(E)};
331 MacroPartition best = beam[0];
332 double best_cost = num_traits<T>::to_double(env_ctmc_decompose(Eutil, best, opt).eps);
333
334 for (std::size_t depth = 1; depth < E; ++depth) {
335 std::vector<std::pair<double, MacroPartition>> cand;
336 for (const MacroPartition& ms : beam) {
337 for (std::size_t i = 0; i < ms.size(); ++i)
338 for (std::size_t j = i + 1; j < ms.size(); ++j) {
339 const MacroPartition trial = meanfield_detail::merge_blocks(ms, i, j);
340 const EnvDecomp<T> t = env_ctmc_decompose(Eutil, trial, opt);
341 const double te = num_traits<T>::to_double(t.eps);
342 if (std::isnan(te) || !(te > 0.0)) continue;
343 const double cost = te - num_traits<T>::to_double(t.epsMAX) +
344 opt.env_alpha * static_cast<double>(depth);
345 cand.push_back(std::make_pair(cost, trial));
346 if (cost < best_cost) {
347 best_cost = cost;
348 best = trial;
349 }
350 }
351 }
352 if (cand.empty()) break;
353 // Stable, so that ties keep the order the merges were generated in and
354 // the search is reproducible across runs.
355 std::stable_sort(cand.begin(), cand.end(),
356 [](const std::pair<double, MacroPartition>& a,
357 const std::pair<double, MacroPartition>& b) { return a.first < b.first; });
358 beam.clear();
359 for (std::size_t i = 0; i < opt.beam_width && i < cand.size(); ++i)
360 beam.push_back(cand[i].second);
361 }
362 return best;
363}
364
365/** Everything `applyCompression` computes, plus the compressed environment. */
366template <class T>
369 /**
370 * The compressed environment. HELD BY SHARED POINTER because `SolverEnv<T>`
371 * stores a reference to the environment it solves, so the compressed one has
372 * to outlive the solver; returning it by value would make that the caller's
373 * problem to get right, and getting it wrong is a dangling reference rather
374 * than a wrong number.
375 */
376 std::shared_ptr<Environment<T>> env;
378 Matrix<T> macro_rate; ///< `computeMacroRate(i,j)`
379 std::vector<T> p; ///< micro stationary vector from the decomposition
380 std::vector<T> pmicro; ///< within-macro-state conditional probabilities
381 std::vector<T> pmacro; ///< `pMacro`, the macro-state probabilities
382 Matrix<double> prob_orig; ///< the macro embedding weights `newEmbweight`
384 /** eps <= epsMAX: below this the aggregation is meaningful, above it is not. */
385 bool compressible = false;
386};
387
388/**
389 * Port of `applyCompression`: pick a partition, decompose, and build the
390 * macro-state environment.
391 *
392 * WHY THE MACRO SERVICE RATES ARE A pmicro-WEIGHTED AVERAGE. Within a
393 * macro-state the environment switches fast compared with the network, so the
394 * network sees the group's rates averaged over the CONDITIONAL distribution of
395 * being in each micro-stage given the group -- which is exactly pmicro. That
396 * average is over rates and not over distributions, so a phase-type service
397 * collapses to an exponential of the same mean: the compression keeps the first
398 * moment and discards the SCV, as the reference's `Exp(rateSum)` does.
399 */
400template <class T>
402 const std::size_t E = e0.nstages();
403 const T zero = num_traits<T>::from_int(0);
404
405 // A MACRO-STATE IS A NETWORK AT AVERAGED RATES, so every stage merged into
406 // one has to have a station rate table to average. A layered stage does not:
407 // its stations are the layers SolverLN derives from it, and averaging those
408 // would aggregate an artifact of the layering rather than the model.
410 "SolverENV compression",
411 "a macro-state is built as one stage network carrying the pmicro-weighted average of "
412 "its members' station rates, and a layered model has no such rate table -- only the "
413 "layers SolverLN derives from it");
414
415 // The whole construction reads the environment as a CTMC, so a transition
416 // that is not exponential cannot survive it: the macro arc would be built as
417 // an Exp of the aggregated rate, and the analyzer would then integrate the
418 // stage transient against a holding-time CDF the model never had.
419 for (std::size_t a = 0; a < E; ++a)
420 for (std::size_t b = 0; b < E; ++b) {
421 if (!e0.arc(a, b).enabled) continue;
422 if (e0.arc(a, b).dist.type != lang::ProcessType::EXP)
423 throw UnsupportedError(
424 "SolverENV compression: the transition from stage " + std::to_string(a + 1) +
425 " to " + std::to_string(b + 1) +
426 " is not exponential, and the NCD decomposition reads the environment as a "
427 "CTMC through E0 = getRate(); aggregating it would silently replace the "
428 "transition by an exponential of the same mean, so it is refused instead");
429 }
430
432 c.E0 = env_rate_matrix(e0);
434
435 if (!opt.partition.empty()) {
436 meanfield_detail::check_partition(opt.partition, E);
437 c.MS = opt.partition;
438 } else if (E <= opt.beam_above_stages) {
440 } else {
442 }
443 const std::size_t Ec = c.MS.size();
444
445 const EnvDecomp<T> d = env_ctmc_decompose(c.Eutil, c.MS, opt);
446 c.p = d.p;
447 c.eps = d.eps;
448 c.epsMAX = d.epsMAX;
449 c.q = d.q;
450 // The reference warns and continues. The flag is reported rather than
451 // thrown for the same reason: an environment that does not decompose still
452 // has an answer, it is simply the answer to a model the aggregation moved.
454
455 c.pmacro.assign(Ec, zero);
456 for (std::size_t i = 0; i < Ec; ++i)
457 for (std::size_t s : c.MS[i]) c.pmacro[i] += c.p[s];
458 c.pmicro.assign(E, zero);
459 for (std::size_t i = 0; i < Ec; ++i) {
460 if (num_traits<T>::to_double(c.pmacro[i]) <= 0) continue;
461 for (std::size_t s : c.MS[i]) c.pmicro[s] = T(c.p[s] / c.pmacro[i]);
462 }
463
464 // computeMacroRate: the micro rates out of the block, weighted by the
465 // conditional probability of sitting in each of its micro-stages.
466 c.macro_rate = Matrix<T>(Ec, Ec, zero);
467 for (std::size_t i = 0; i < Ec; ++i)
468 for (std::size_t j = 0; j < Ec; ++j)
469 for (std::size_t mi : c.MS[i])
470 for (std::size_t mj : c.MS[j]) c.macro_rate(i, j) += T(c.pmicro[mi] * c.E0(mi, mj));
471
472 // newEmbweight: P(the previous macro-state was k | now entering e).
473 c.prob_orig = Matrix<double>(Ec, Ec, 0.0);
474 for (std::size_t x = 0; x < Ec; ++x) {
475 double tot = 0.0;
476 for (std::size_t h = 0; h < Ec; ++h)
477 if (h != x)
478 tot += num_traits<T>::to_double(c.pmacro[h]) *
480 if (!(tot > 0.0)) continue;
481 for (std::size_t k = 0; k < Ec; ++k) {
482 if (k == x) continue;
485 }
486 }
487
488 // The macro networks: the first micro-stage's structure, its rates replaced
489 // by the pmicro-weighted averages over the block.
490 c.env = std::make_shared<Environment<T>>(e0.name() + "-compressed", Ec);
491 for (std::size_t i = 0; i < Ec; ++i) {
492 const std::size_t first = c.MS[i][0];
493 qn::NetworkStruct<T> sn = e0.stage(first).model;
494 const std::size_t M = sn.nstations, K = sn.nclasses;
495 for (std::size_t s : c.MS[i])
496 if (e0.stage(s).model.nstations != M || e0.stage(s).model.nclasses != K)
497 throw InputError(
498 "SolverENV compression: macro-state " + std::to_string(i + 1) +
499 " merges stages with different stations or classes, whose rates cannot be "
500 "averaged entrywise");
501 for (std::size_t m = 0; m < M; ++m) {
502 const lang::NodeType nt = sn.stations[m].nodetype;
503 // Only a Queue or a Delay has a service rate to average; a Source
504 // carries an arrival process and a Join an infinite rate, and the
505 // reference skips both.
506 if (nt != lang::NodeType::Queue && nt != lang::NodeType::Delay) continue;
507 for (std::size_t k = 0; k < K; ++k) {
508 T acc = zero;
509 for (std::size_t s : c.MS[i])
510 acc += T(c.pmicro[s] * e0.stage(s).model.rates(m, k));
511 if (num_traits<T>::to_double(acc) > 0) sn.service[m][k] = lang::Distrib<T>::exp_rate(acc);
512 }
513 }
514 sn.refresh_struct();
515 c.env->set_stage(i, e0.stage(first).name + "+", e0.stage(first).type, sn);
516 }
517
518 for (std::size_t i = 0; i < Ec; ++i) {
519 bool any_out = false;
520 for (std::size_t j = 0; j < Ec; ++j) {
521 if (i == j) continue; // a self-loop would lengthen the holding time
522 if (!(num_traits<T>::to_double(c.macro_rate(i, j)) > 0)) continue;
523 // The reset policy of the representative micro arc. Two micro arcs
524 // folding into one macro arc may carry DIFFERENT resets and a
525 // std::function cannot be compared, so disagreement is detectable
526 // only in whether a reset is present at all; that much is refused,
527 // and beyond it the representative stands.
528 const std::size_t fi = c.MS[i][0], fj = c.MS[j][0];
529 const bool want = static_cast<bool>(e0.arc(fi, fj).reset);
530 for (std::size_t a : c.MS[i])
531 for (std::size_t b : c.MS[j])
532 if (e0.arc(a, b).enabled && static_cast<bool>(e0.arc(a, b).reset) != want)
533 throw UnsupportedError(
534 "SolverENV compression: the arcs folding into the macro transition " +
535 std::to_string(i + 1) + " -> " + std::to_string(j + 1) +
536 " do not agree on whether a reset policy applies, and one macro arc "
537 "can carry only one; split the partition so that reset policies are "
538 "uniform within it");
539 c.env->add_transition(i, j, lang::Distrib<T>::exp_rate(c.macro_rate(i, j)),
540 e0.arc(fi, fj).reset);
541 any_out = true;
542 }
543 if (!any_out)
544 throw InputError(
545 "SolverENV compression: macro-state " + std::to_string(i + 1) +
546 " has no outgoing transition, so the compressed environment is absorbing; the "
547 "partition merged a whole recurrent class into one block");
548 }
549 return c;
550}
551
552/**
553 * `probEnv = pMacro` and `probOrig = newEmbweight`, the two quantities
554 * `applyCompression` overwrites on the environment.
555 *
556 * ORDER MATTERS: `SolverEnv<T>`'s constructor calls `Environment::init()`,
557 * which recomputes both from the macro arcs, so this must be applied AFTER the
558 * solver is constructed and BEFORE `solve()` is called. `solver_env_meanfield`
559 * below does exactly that, and is the reason to prefer it over wiring the two
560 * calls by hand.
561 *
562 * The two are consistent rather than contradictory: for an exponential
563 * environment `Environment::init()` derives probEnv as the stationary law of
564 * the macro generator, and aggregating a chain by its exact conditional
565 * distributions reproduces the block sums of the original stationary law
566 * exactly. So this overwrite replaces one estimate of the same quantity by
567 * another, and the gap between them is a second reading of the decomposition
568 * error alongside eps.
569 */
570template <class T>
572 const std::size_t Ec = c.MS.size();
573 if (e.nstages() != Ec)
574 throw InputError(
575 "SolverENV compression: the macro probabilities do not match the environment they "
576 "are being applied to");
577 e.prob_env.assign(Ec, 0.0);
578 for (std::size_t i = 0; i < Ec; ++i) e.prob_env[i] = num_traits<T>::to_double(c.pmacro[i]);
579 e.prob_orig = c.prob_orig;
580}
581
582/** A mean-field solve, with the compression that produced it. */
583/** `aggregateCacheMeanfield_`'s output: one hit/miss vector per Cache node. */
584template <class T>
586 std::vector<std::size_t> nodes; ///< 0-based Cache node indices of stage 1
587 std::vector<std::vector<T> > hitprob; ///< [cache][class], NaN where no flow
588 std::vector<std::vector<T> > missprob; ///< [cache][class]
589};
590
591template <class T>
593 EnvSolution avg; ///< what SolverEnv reported
595 bool compressed = false; ///< false when the solve ran on the original stages
596 /**
597 * `aggregateCacheMeanfield_`: environment-blended hit and miss probabilities
598 * per Cache node. Empty when the model holds no Cache. The reference writes
599 * these onto the stage-one node objects with `setResultHitProb`; this port
600 * has no model-object layer at this level, so they ride in the result.
601 */
603};
604
605namespace meanfield_detail {
606
607/** 0-based Cache node indices of a stage, in `find(nodetype == Cache)` order. */
608template <class T>
609std::vector<std::size_t> cache_nodes_of(const qn::NetworkStruct<T>& sn) {
610 std::vector<std::size_t> out;
611 for (std::size_t ind = 0; ind < sn.nodes.size(); ++ind)
612 if (sn.nodes[ind].nodetype == lang::NodeType::Cache) out.push_back(ind);
613 return out;
614}
615
616/**
617 * `aggregateCacheMeanfield_`: the environment-blended hit and miss ratios of
618 * every Cache, by a mean-field fixed point over the caches' OWN occupancies.
619 *
620 * THIS IS A SECOND FIXED POINT, nested beside the queue-length one, and it
621 * exists because the two carry different objects. `SolverEnv`'s coupling hands
622 * each stage the marginal mean queue lengths its predecessors left; a cache's
623 * state is its per-item occupancy, which no queue length encodes. So this sweep
624 * integrates each stage's cache drift over its sojourn from an entry occupancy
625 * that mixes its predecessors' exit occupancies by `prob_orig`, exactly the way
626 * the queue-length handoff mixes means, and iterates the pair to convergence.
627 *
628 * THE RMF DRIFT RUNS IN PER-REQUEST TIME, NOT REAL TIME, which is the trap here.
629 * `solver_fld_cacheqn_tran` returns a grid in units of cache requests, so the
630 * holding-time CDF cannot be evaluated on it directly: the grid is divided by
631 * the cache's total arrival rate first (`treal = t / Lam`). Skipping that
632 * weights every stage as though its cache saw the same request rate, which
633 * silently favours the slow stages.
634 *
635 * THE RATIO IS TAKEN AFTER THE BLEND, as in the state-vector twin: a per-stage
636 * ratio weighted by prob_env averages ratios, which is not the ratio the
637 * environment exhibits unless every stage carries the same total rate.
638 */
639template <class T>
640CacheBlendResult<T> aggregate_cache_meanfield(Environment<T>& e, const EnvOptions& o) {
641 CacheBlendResult<T> out;
642 const std::size_t E = e.nstages();
643 if (E == 0) return out;
644 // The blend is over the Cache NODES of the stage networks; a layered
645 // environment has none, and reading an absent NetworkStruct would report an
646 // empty node list as though the model had been examined.
647 if (e.has_lqn_stages()) return out;
648 const qn::NetworkStruct<T>& sn1 = e.stage(0).model;
649 out.nodes = cache_nodes_of(sn1);
650 if (out.nodes.empty()) return out;
651 const std::size_t nc = out.nodes.size(), K = sn1.nclasses;
652
653 // Only a FLUID stage exposes the RMF cache transient this reads; the
654 // reference returns without writing anything when any stage is not one,
655 // rather than blending what it has.
656 if (o.stage_solver != "fluid") {
657 out.nodes.clear();
658 return out;
659 }
660
661 // A finite window per stage, falling back to a few mean holding times when
662 // the inner solver left the timespan open, as the reference does.
663 std::vector<double> tend(E, 0.0);
664 for (std::size_t s = 0; s < E; ++s) {
665 double t1 = o.timespan_end;
666 if (!std::isfinite(t1) || !(t1 > 0.0)) t1 = 20.0 * mam::map_mean(e.hold_time[s].map());
667 tend[s] = t1;
668 }
669
670 std::vector<std::vector<std::vector<T> > > entry(E, std::vector<std::vector<T> >(nc));
671 std::vector<std::vector<fluid::FluidCacheqnTranCache<T> > > tran(E);
672 std::vector<std::vector<std::vector<T> > > wmass(E, std::vector<std::vector<T> >(nc));
673 std::vector<std::vector<std::vector<T> > > exit_occ(E, std::vector<std::vector<T> >(nc));
674 std::vector<T> prev_flat;
675 const int sweeps = o.iter_max > 0 ? o.iter_max : 1;
676
677 for (int sweep = 0; sweep < sweeps; ++sweep) {
678 for (std::size_t s = 0; s < E; ++s) {
679 fluid::FluidOptions fo;
680 fo.method = "rmf";
681 tran[s] = fluid::solver_fld_cacheqn_tran(e.stage(s).model, fo, 0.0, tend[s], entry[s]);
682 for (std::size_t c = 0; c < nc; ++c) {
683 std::size_t idx = tran[s].size();
684 for (std::size_t q = 0; q < tran[s].size(); ++q)
685 if (tran[s][q].node == out.nodes[c]) idx = q;
686 if (idx == tran[s].size()) continue;
687 const fluid::FluidCacheqnTranCache<T>& tc = tran[s][idx];
688 double Lam = 0.0;
689 for (std::size_t k = 0; k < tc.arate.size(); ++k)
690 Lam += num_traits<T>::to_double(tc.arate[k]);
691 if (!(Lam > 0.0)) continue;
692
693 // PER-REQUEST TIME -> REAL TIME before the CDF is evaluated.
694 std::vector<double> treal(tc.t.size(), 0.0);
695 for (std::size_t j = 0; j < tc.t.size(); ++j)
696 treal[j] = num_traits<T>::to_double(tc.t[j]) / Lam;
697 const std::vector<double> F = mam::map_cdf(e.hold_time[s].map(), treal);
698 std::vector<T> w(treal.size(), num_traits<T>::from_int(0));
699 for (std::size_t j = 1; j < treal.size(); ++j)
700 w[j] = num_traits<T>::from_double(F[j] - F[j - 1]);
701 wmass[s][c] = w;
702
703 double sw = 0.0;
704 for (std::size_t j = 0; j < w.size(); ++j) sw += num_traits<T>::to_double(w[j]);
705 if (!(sw > 0.0) || tc.xocc.rows() == 0) continue;
706 std::vector<T> xo(tc.xocc.rows(), num_traits<T>::from_int(0));
707 for (std::size_t a = 0; a < tc.xocc.rows(); ++a) {
708 T acc = num_traits<T>::from_int(0);
709 for (std::size_t j = 0; j < w.size() && j < tc.xocc.cols(); ++j)
710 acc = T(acc + T(tc.xocc(a, j) * w[j]));
711 xo[a] = T(acc / num_traits<T>::from_double(sw));
712 }
713 exit_occ[s][c] = xo;
714 }
715 }
716
717 // Each stage's entry occupancy is its predecessors' exits, by prob_orig.
718 std::vector<std::vector<std::vector<T> > > next(E, std::vector<std::vector<T> >(nc));
719 for (std::size_t s = 0; s < E; ++s)
720 for (std::size_t c = 0; c < nc; ++c) {
721 std::vector<T> acc;
722 for (std::size_t h = 0; h < E; ++h) {
723 const double po = e.prob_orig(h, s);
724 if (!(po > 0.0) || exit_occ[h][c].empty()) continue;
725 if (acc.empty()) acc.assign(exit_occ[h][c].size(), num_traits<T>::from_int(0));
726 if (acc.size() != exit_occ[h][c].size()) continue;
727 for (std::size_t a = 0; a < acc.size(); ++a)
728 acc[a] = T(acc[a] + T(num_traits<T>::from_double(po) * exit_occ[h][c][a]));
729 }
730 next[s][c] = acc;
731 }
732
733 std::vector<T> flat;
734 for (std::size_t s = 0; s < E; ++s)
735 for (std::size_t c = 0; c < nc; ++c)
736 flat.insert(flat.end(), next[s][c].begin(), next[s][c].end());
737 entry = next;
738 if (!prev_flat.empty() && prev_flat.size() == flat.size()) {
739 double dmax = 0.0;
740 for (std::size_t a = 0; a < flat.size(); ++a)
741 dmax = std::max(dmax, std::fabs(num_traits<T>::to_double(flat[a]) -
742 num_traits<T>::to_double(prev_flat[a])));
743 prev_flat = flat;
744 if (dmax < o.iter_tol) break;
745 } else {
746 prev_flat = flat;
747 }
748 }
749
750 const T nan = num_traits<T>::from_double(std::numeric_limits<double>::quiet_NaN());
751 out.hitprob.assign(nc, std::vector<T>(K, nan));
752 out.missprob.assign(nc, std::vector<T>(K, nan));
753 for (std::size_t c = 0; c < nc; ++c) {
754 std::vector<T> hitT(K, num_traits<T>::from_int(0)), missT(K, num_traits<T>::from_int(0));
755 for (std::size_t s = 0; s < E; ++s) {
756 if (wmass[s][c].empty()) continue;
757 double sw = 0.0;
758 bool ok = true;
759 for (std::size_t j = 0; j < wmass[s][c].size(); ++j) {
760 const double v = num_traits<T>::to_double(wmass[s][c][j]);
761 if (!std::isfinite(v)) ok = false;
762 sw += v;
763 }
764 if (!ok || !(sw > 0.0)) continue;
765 std::size_t idx = tran[s].size();
766 for (std::size_t q = 0; q < tran[s].size(); ++q)
767 if (tran[s][q].node == out.nodes[c]) idx = q;
768 if (idx == tran[s].size()) continue;
769 const fluid::FluidCacheqnTranCache<T>& tc = tran[s][idx];
770 const T pe = num_traits<T>::from_double(e.prob_env[s]);
771 for (std::size_t k = 0; k < K && k < tc.arate.size(); ++k) {
772 if (!(num_traits<T>::to_double(tc.arate[k]) > 0.0)) continue;
773 T hbar = num_traits<T>::from_int(0), mbar = num_traits<T>::from_int(0);
774 for (std::size_t j = 0; j < wmass[s][c].size() && j < tc.hitprob_t.cols(); ++j) {
775 hbar = T(hbar + T(tc.hitprob_t(k, j) * wmass[s][c][j]));
776 mbar = T(mbar + T(tc.missprob_t(k, j) * wmass[s][c][j]));
777 }
778 const T swT = num_traits<T>::from_double(sw);
779 hitT[k] = T(hitT[k] + T(T(pe * tc.arate[k]) * T(hbar / swT)));
780 missT[k] = T(missT[k] + T(T(pe * tc.arate[k]) * T(mbar / swT)));
781 }
782 }
783 for (std::size_t k = 0; k < K; ++k) {
784 const T tot = T(hitT[k] + missT[k]);
785 if (num_traits<T>::to_double(tot) > 0.0) {
786 out.hitprob[c][k] = T(hitT[k] / tot);
787 out.missprob[c][k] = T(missT[k] / tot);
788 }
789 }
790 }
791 return out;
792}
793
794/** A Cache in a stage is only servable by the fluid backend; name the other case. */
795template <class T>
796void refuse_cache_stages(const Environment<T>& e, const EnvOptions& o) {
797 if (o.stage_solver == "fluid") return;
798 for (std::size_t s = 0; s < e.nstages(); ++s) {
799 // A layered stage carries no NetworkStruct at all, so there is no node
800 // table to scan; a Cache inside an LQN is a CacheTask, which lives in
801 // its host's LAYER and is SolverLN's to serve.
802 if (e.is_lqn(s)) continue;
803 const qn::NetworkStruct<T>& sn = e.stage(s).model;
804 for (std::size_t ind = 0; ind < sn.nodes.size(); ++ind)
805 if (sn.nodes[ind].nodetype == lang::NodeType::Cache)
806 throw UnsupportedError(
807 "SolverENV meanfield: stage " + std::to_string(s + 1) +
808 " holds a Cache, whose environment-blended hit and miss ratios come from "
809 "aggregateCacheMeanfield_ and its per-sweep solver_fld_cacheqn_tran RMF "
810 "transient; that transient exists only for a FLUID stage solver, and this "
811 "ensemble runs '" +
812 o.stage_solver + "' stages");
813 }
814}
815
816} // namespace meanfield_detail
817
818/** The mean-field solve on the original stages, with no compression. */
819template <class T>
821 meanfield_detail::refuse_cache_stages(e, o);
823 SolverEnv<T> s(e, o);
824 out.avg = s.solve();
825 out.cache = meanfield_detail::aggregate_cache_meanfield(e, o);
826 return out;
827}
828
829/**
830 * The compressed mean-field solve: aggregate the environment, then run the
831 * mean-field fixed point over the macro-states.
832 *
833 * The compressed environment is kept alive by the returned structure, which is
834 * what `SolverEnv` held a reference to; reading `.avg` out of the result and
835 * discarding the rest is safe, but the compression it came from travels with it
836 * so that eps and epsMAX can be checked against the numbers they produced.
837 */
838template <class T>
840 const EnvCompressOptions& c) {
841 meanfield_detail::refuse_cache_stages(e, o);
843 out.compression = env_compress(e, c);
844 out.compressed = true;
845 SolverEnv<T> s(*out.compression.env, o);
847 out.avg = s.solve();
848 out.cache = meanfield_detail::aggregate_cache_meanfield(*out.compression.env, o);
849 return out;
850}
851
852} // namespace env
853} // namespace line
854
855#endif // LINE_SOLVERS_ENV_SOLVER_ENV_MEANFIELD_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
bool empty() const
Definition matrix.h:92
UnsupportedError(const std::string &what)
Definition error.h:51
const std::string & name() const
const EnvStage< T > & stage(std::size_t e) const
Matrix< double > prob_orig
probOrig(h, e)
std::size_t nstages() const
void reject_lqn_stages(const std::string &who, const std::string &why) const
Refuse an environment carrying a LayeredNetwork stage, by name.
const EnvArc< T > & arc(std::size_t e, std::size_t h) const
std::vector< double > prob_env
probEnv
The environment solver.
Definition solver_env.h:219
EnvSolution solve()
Definition solver_env.h:223
A network plus its refreshed NetworkStruct.
std::vector< NodeDef > nodes
every node, in creation order
Courtois decomposition of a nearly completely decomposable (NCD) CTMC.
Koury-McAllister-Stewart aggregation-disaggregation for a nearly completely decomposable CTMC.
Two-level multigrid aggregation-disaggregation for a nearly completely decomposable CTMC.
Steady-state distribution of a continuous-time Markov chain.
Takahashi's aggregation-disaggregation for a nearly completely decomposable CTMC.
A random environment: a port of matlab/src/lang/Environment.m, restricted to what SolverENV reads out...
The exception types the port throws.
The INTEGRATED caching-queueing network under the fluid solver: ports of solver_fld_cacheqn_analyzer....
Cumulative distribution of the inter-arrival time of a MAP.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Dense matrix and non-owning view.
std::vector< std::vector< std::size_t > > MacroPartition
A partition of the stage indices 0..E-1 into macro-states, MATLAB's MS.
EnvDecomp< T > env_ctmc_decompose(const Matrix< T > &Q, const MacroPartition &MS, const EnvCompressOptions &opt)
Port of SolverENV.ctmc_decompose: one NCD decomposition, by whichever kernel options....
MacroPartition env_beam_search_partition(const Matrix< T > &Eutil, const EnvCompressOptions &opt)
Port of beamSearchPartition, the large-environment search: repeatedly merge two blocks,...
MacroPartition env_find_best_partition(const Matrix< T > &Eutil, const EnvCompressOptions &opt)
Port of findBestPartition, the small-environment search.
Matrix< T > env_rate_matrix(const Environment< T > &e)
E0, the environment's rate matrix: E0(e,h) = env{e,h}.getRate().
EnvMeanfieldSolution< T > solver_env_meanfield(Environment< T > &e, const EnvOptions &o)
The mean-field solve on the original stages, with no compression.
EnvCompression< T > env_compress(const Environment< T > &e0, const EnvCompressOptions &opt)
Port of applyCompression: pick a partition, decompose, and build the macro-state environment.
void env_apply_macro_probabilities(Environment< T > &e, const EnvCompression< T > &c)
probEnv = pMacro and probOrig = newEmbweight, the two quantities applyCompression overwrites on the e...
std::vector< FluidCacheqnTranCache< T > > solver_fld_cacheqn_tran(const qn::NetworkStruct< T > &sn, const FluidOptions &opt, double t0, double t1, const std::vector< std::vector< T > > &x0cell=std::vector< std::vector< T > >())
Port of solver_fld_cacheqn_tran.m: the transient counterpart of the analyzer above.
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:324
T map_mean(const Map< T > &m)
Mean inter-arrival time, 1/lambda.
Definition map_moment.h:101
std::vector< T > map_cdf(const Map< T > &m, const std::vector< T > &points)
Cumulative distribution of the inter-arrival time at the given points.
Definition map_cdf.h:63
Matrix< T > ctmc_makeinfgen(const Matrix< T > &Q)
Set the diagonal so that every row sums to zero (ctmc_makeinfgen).
Definition ctmc_solve.h:58
KmsResult< T > ctmc_kms(const Matrix< T > &Q, const std::vector< std::vector< std::size_t > > &MS, std::size_t numSteps)
Koury-McAllister-Stewart aggregation-disaggregation for a nearly completely decomposable CTMC.
Definition ctmc_kms.h:100
TakahashiResult< T > ctmc_takahashi(const Matrix< T > &Q, const std::vector< std::vector< std::size_t > > &MS, std::size_t numSteps, double massTol=1e-14)
Takahashi's aggregation-disaggregation for a nearly completely decomposable CTMC.
MultiResult< T > ctmc_multi(const Matrix< T > &Q, const std::vector< std::vector< std::size_t > > &MS, const std::vector< std::vector< std::size_t > > &MSS, const T &q)
Two-level multigrid aggregation-disaggregation for a nearly completely decomposable CTMC.
Definition ctmc_multi.h:61
CourtoisResult< T > ctmc_courtois(const Matrix< T > &Q, const std::vector< std::vector< std::size_t > > &MS, const T &q)
Courtois decomposition of a nearly completely decomposable (NCD) CTMC.
T num_abs(const T &v)
Definition number.h:172
A queueing network and its refreshed NetworkStruct.
Number-type abstraction for the templated API port.
SolverENV: a queueing network in a random environment.
A mean-field solve, with the compression that produced it.
std::vector< std::vector< T > > hitprob
[cache][class], NaN where no flow
std::vector< std::size_t > nodes
0-based Cache node indices of stage 1
std::vector< std::vector< T > > missprob
[cache][class]
The knobs applyCompression reads out of options.config.
MacroPartition partition
A partition supplied by the caller, which SKIPS the search entirely.
double env_alpha
options.config.env_alpha: the beam search's per-depth merge penalty.
std::size_t beam_above_stages
Stage count above which the reference switches from the pairwise search to the beam search.
std::string da
options.config.da: courtois (the reference's default), kms, takahashi, multi.
std::size_t beam_width
Beam width, the reference's hard-coded B = 3.
std::size_t da_iter
options.config.da_iter: sweeps for the two iterative kernels.
Everything applyCompression computes, plus the compressed environment.
std::vector< T > pmicro
within-macro-state conditional probabilities
std::vector< T > pmacro
pMacro, the macro-state probabilities
bool compressible
eps <= epsMAX: below this the aggregation is meaningful, above it is not.
Matrix< T > macro_rate
computeMacroRate(i,j)
std::vector< T > p
micro stationary vector from the decomposition
Matrix< double > prob_orig
the macro embedding weights newEmbweight
std::shared_ptr< Environment< T > > env
The compressed environment.
What SolverENV.ctmc_decompose returns: [p, eps, epsMax, q].
std::vector< T > p
approximate stationary vector of the environment
CacheBlendResult< T > cache
aggregateCacheMeanfield_: environment-blended hit and miss probabilities per Cache node.
bool compressed
false when the solve ran on the original stages
EnvSolution avg
what SolverEnv reported
Options of SolverENV.
Definition solver_env.h:110
What SolverENV reports.
Definition solver_env.h:167
static Distrib exp_rate(const T &r)
Definition lang_types.h:814
T q
uniformization rate used
T eps
NCD index: largest ROW sum of B, ||B||_inf (MATLAB and the JAR).
std::vector< T > p
approximate stationary vector, ORIGINAL state ordering
T epsMAX
(1 - max subdominant block eigenvalue modulus) / 2
T epsMAX
maximum admissible NCD index
Definition ctmc_kms.h:63
T eps
NCD index, as ctmc_courtois defines it.
Definition ctmc_kms.h:62
std::vector< T > p
estimate after numSteps sweeps, ORIGINAL ordering
Definition ctmc_kms.h:58
std::vector< T > p
approximate stationary vector, ORIGINAL ordering
Definition ctmc_multi.h:44
T eps
NCD index of the fine level.
Definition ctmc_multi.h:47
T epsMAX
maximum admissible NCD index of the fine level
Definition ctmc_multi.h:48
T eps
NCD index, as ctmc_courtois defines it.
T epsMAX
maximum admissible NCD index
std::vector< T > p
estimate after numSteps sweeps