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 * ported `substituteCacheBlend_`: a Cache under a NON-FLUID (CTMC) stage solver,
69 * which exposes no RMF transient, blends each stage's own steady-state
70 * cache ratios by its arrival into the cache, without the occupancy
71 * handoff; the reference's warning rides in `cache_warning`
72 * refused compression of a non-exponential environment, and everything
73 * `solver_env.h` already refuses by name
74 */
75
76#include <algorithm>
77#include <cmath>
78#include <cstddef>
79#include <limits>
80#include <memory>
81#include <string>
82#include <vector>
83
91#include "line/num/number.h"
98#include "line/util/error.h"
99#include "line/util/matrix.h"
100
101namespace line {
102namespace env {
103
104/** A partition of the stage indices 0..E-1 into macro-states, MATLAB's `MS`. */
105using MacroPartition = std::vector<std::vector<std::size_t>>;
106
107/** The knobs `applyCompression` reads out of `options.config`. */
109 /** `options.config.da`: courtois (the reference's default), kms, takahashi, multi. */
110 std::string da = "courtois";
111 /** `options.config.da_iter`: sweeps for the two iterative kernels. */
112 std::size_t da_iter = 10;
113 /** `options.config.env_alpha`: the beam search's per-depth merge penalty. */
114 double env_alpha = 0.01;
115 /** Beam width, the reference's hard-coded B = 3. */
116 std::size_t beam_width = 3;
117 /**
118 * Stage count above which the reference switches from the pairwise search
119 * to the beam search. The two are genuinely different searches, not one
120 * search with a budget, so the threshold changes the answer and is exposed.
121 */
122 std::size_t beam_above_stages = 10;
123 /**
124 * A partition supplied by the caller, which SKIPS the search entirely.
125 * Worth having because both searches evaluate a decomposition per candidate
126 * pair, and a caller who already knows the group structure -- a repair model
127 * whose stages are (working, degraded) times (peak, offpeak), say -- should
128 * not pay for rediscovering it.
129 */
131};
132
133/** What `SolverENV.ctmc_decompose` returns: `[p, eps, epsMax, q]`. */
134template <class T>
135struct EnvDecomp {
136 std::vector<T> p; ///< approximate stationary vector of the environment
138};
139
140namespace meanfield_detail {
141
142/** Every kernel is seeded by Courtois, whose epsMAX is an eigenvalue modulus. */
143inline void refuse_inexact(const std::string& da) {
144 throw UnsupportedError(
145 "SolverENV compression: the '" + da +
146 "' decomposition needs transcendental arithmetic, because every one of ctmc_courtois, "
147 "ctmc_kms, ctmc_takahashi and ctmc_multi reports epsMAX, a subdominant eigenvalue "
148 "modulus with no rational closed form; rerun with --arith double or real");
149}
150
151/** MS must be a partition of 0..n-1, which every kernel assumes and none checks. */
152inline void check_partition(const MacroPartition& MS, std::size_t n) {
153 std::vector<bool> seen(n, false);
154 std::size_t total = 0;
155 for (const std::vector<std::size_t>& blk : MS) {
156 if (blk.empty()) throw InputError("SolverENV compression: a macro-state is empty");
157 for (std::size_t s : blk) {
158 if (s >= n)
159 throw InputError("SolverENV compression: a macro-state names stage " +
160 std::to_string(s + 1) + ", which does not exist");
161 if (seen[s])
162 throw InputError("SolverENV compression: stage " + std::to_string(s + 1) +
163 " appears in more than one macro-state");
164 seen[s] = true;
165 ++total;
166 }
167 }
168 if (total != n)
169 throw InputError(
170 "SolverENV compression: the macro-states must cover every stage; the partition "
171 "covers " +
172 std::to_string(total) + " of " + std::to_string(n));
173}
174
175/** Singletons, the starting point of both searches and the no-compression fallback. */
176inline MacroPartition singletons(std::size_t E) {
177 MacroPartition MS(E);
178 for (std::size_t i = 0; i < E; ++i) MS[i] = std::vector<std::size_t>{i};
179 return MS;
180}
181
182/** `trial`: merge blocks i and j of `ms`, the merged block taking i's position. */
183inline MacroPartition merge_blocks(const MacroPartition& ms, std::size_t i, std::size_t j) {
184 MacroPartition out;
185 out.reserve(ms.size() - 1);
186 for (std::size_t k = 0; k < ms.size(); ++k) {
187 if (k == i) {
188 std::vector<std::size_t> m = ms[i];
189 m.insert(m.end(), ms[j].begin(), ms[j].end());
190 out.push_back(m);
191 } else if (k != j) {
192 out.push_back(ms[k]);
193 }
194 }
195 return out;
196}
197
198} // namespace meanfield_detail
199
200/**
201 * Port of `SolverENV.ctmc_decompose`: one NCD decomposition, by whichever
202 * kernel `options.config.da` names.
203 *
204 * The uniformization rate the three iterative kernels report is the reference's
205 * `1.05 * max(max(abs(Q)))` rather than anything the kernel itself derived,
206 * which is why it is recomputed here instead of read back off the result.
207 */
208template <class T>
210 const EnvCompressOptions& opt) {
211 meanfield_detail::check_partition(MS, Q.rows());
212 EnvDecomp<T> out;
213 if constexpr (!num_traits<T>::has_transcendental) {
214 meanfield_detail::refuse_inexact(opt.da);
215 } else {
216 // 1.05 max|Q|, the rate the reference hands back for every kernel that
217 // does not report one of its own.
218 T qmax = num_traits<T>::from_int(0);
219 for (std::size_t i = 0; i < Q.rows(); ++i)
220 for (std::size_t j = 0; j < Q.cols(); ++j) {
221 const T a = num_abs(T(Q(i, j)));
222 if (a > qmax) qmax = a;
223 }
224 const T qdefault = T(num_traits<T>::from_rational(21, 20) * qmax);
225
226 if (opt.da == "courtois") {
228 out.p = r.p;
229 out.eps = r.eps;
230 out.epsMAX = r.epsMAX;
231 out.q = r.q;
232 } else if (opt.da == "kms") {
233 const mc::KmsResult<T> r = mc::ctmc_kms(Q, MS, opt.da_iter);
234 out.p = r.p;
235 out.eps = r.eps;
236 out.epsMAX = r.epsMAX;
237 out.q = qdefault;
238 } else if (opt.da == "takahashi") {
239 const mc::TakahashiResult<T> r = mc::ctmc_takahashi(Q, MS, opt.da_iter);
240 out.p = r.p;
241 out.eps = r.eps;
242 out.epsMAX = r.epsMAX;
243 out.q = qdefault;
244 } else if (opt.da == "multi") {
245 // The coarse partition defaults to singletons over the macro-states,
246 // so the second level decouples nothing: the reference exposes no
247 // way to supply a real macro-macro partition, and a two-level method
248 // whose coarse level is singletons is Courtois plus one extra solve.
249 MacroPartition MSS(MS.size());
250 for (std::size_t i = 0; i < MS.size(); ++i) MSS[i] = std::vector<std::size_t>{i};
251 const mc::MultiResult<T> r = mc::ctmc_multi(Q, MS, MSS);
252 out.p = r.p;
253 out.eps = r.eps;
254 out.epsMAX = r.epsMAX;
255 out.q = qdefault;
256 } else {
257 throw UnsupportedError(
258 "SolverENV compression: unknown decomposition '" + opt.da +
259 "'; options.config.da is one of courtois, kms, takahashi, multi");
260 }
261 }
262 return out;
263}
264
265/**
266 * `E0`, the environment's rate matrix: `E0(e,h) = env{e,h}.getRate()`.
267 *
268 * `getRate()` is the RECIPROCAL MEAN of the transition, so a general Markovian
269 * arc collapses to a single rate here and everything downstream treats the
270 * environment as a CTMC. That is the reference's own reading and it is why
271 * `env_compress` refuses a non-exponential environment by name: the collapse is
272 * harmless for the NCD diagnostics, which only ever look at Eutil, but it is
273 * not harmless once the macro arcs are rebuilt from it.
274 */
275template <class T>
277 const std::size_t E = e.nstages();
279 for (std::size_t a = 0; a < E; ++a)
280 for (std::size_t b = 0; b < E; ++b)
281 if (e.arc(a, b).enabled) E0(a, b) = e.arc(a, b).dist.rate();
282 return E0;
283}
284
285/**
286 * Port of `findBestPartition`, the small-environment search.
287 *
288 * ITS COMMENT CLAIMS AN EXHAUSTIVE SEARCH OVER ALL PARTITIONS AND THE CODE DOES
289 * NOT DO THAT. It evaluates the singletons and then every single pairwise merge
290 * of them, so it explores E(E-1)/2 + 1 partitions out of the Bell number of
291 * them and can never return a macro-state of more than two stages. The port is
292 * literal, because the alternative is a different method wearing the reference's
293 * name; a caller who wants deeper merging has `beam_above_stages` and
294 * `EnvCompressOptions::partition`.
295 */
296template <class T>
298 const std::size_t E = Eutil.rows();
299 MacroPartition best = meanfield_detail::singletons(E);
300 EnvDecomp<T> b = env_ctmc_decompose(Eutil, best, opt);
301 double best_eps = num_traits<T>::to_double(b.eps);
302 if (std::isnan(best_eps)) return best;
303
304 for (std::size_t i = 0; i < E; ++i)
305 for (std::size_t j = i + 1; j < E; ++j) {
306 const MacroPartition trial =
307 meanfield_detail::merge_blocks(meanfield_detail::singletons(E), i, j);
308 const EnvDecomp<T> t = env_ctmc_decompose(Eutil, trial, opt);
309 const double te = num_traits<T>::to_double(t.eps);
310 if (!std::isnan(te) && te < best_eps) {
311 best_eps = te;
312 best = trial;
313 }
314 }
315 return best;
316}
317
318/**
319 * Port of `beamSearchPartition`, the large-environment search: repeatedly merge
320 * two blocks, keeping the `beam_width` cheapest partitions at each depth.
321 *
322 * THE COST AND THE INCUMBENT ARE NOT THE SAME QUANTITY, in the reference. The
323 * incumbent `bestEps` is seeded with the raw eps of the singleton partition,
324 * and thereafter compared against `childEps - childEpsMax + alpha * depth`,
325 * which is a penalized score and not an eps at all. A merge is therefore
326 * adopted partly on the strength of its epsMAX and of how deep it sits, against
327 * a threshold that measured neither. This is ported literally rather than
328 * repaired: the search is a heuristic whose output is checked afterwards
329 * against eps <= epsMAX, so the comparison decides which candidate is tried and
330 * not whether the result is admissible.
331 */
332template <class T>
334 const std::size_t E = Eutil.rows();
335 std::vector<MacroPartition> beam{meanfield_detail::singletons(E)};
336 MacroPartition best = beam[0];
337 double best_cost = num_traits<T>::to_double(env_ctmc_decompose(Eutil, best, opt).eps);
338
339 for (std::size_t depth = 1; depth < E; ++depth) {
340 std::vector<std::pair<double, MacroPartition>> cand;
341 for (const MacroPartition& ms : beam) {
342 for (std::size_t i = 0; i < ms.size(); ++i)
343 for (std::size_t j = i + 1; j < ms.size(); ++j) {
344 const MacroPartition trial = meanfield_detail::merge_blocks(ms, i, j);
345 const EnvDecomp<T> t = env_ctmc_decompose(Eutil, trial, opt);
346 const double te = num_traits<T>::to_double(t.eps);
347 if (std::isnan(te) || !(te > 0.0)) continue;
348 const double cost = te - num_traits<T>::to_double(t.epsMAX) +
349 opt.env_alpha * static_cast<double>(depth);
350 cand.push_back(std::make_pair(cost, trial));
351 if (cost < best_cost) {
352 best_cost = cost;
353 best = trial;
354 }
355 }
356 }
357 if (cand.empty()) break;
358 // Stable, so that ties keep the order the merges were generated in and
359 // the search is reproducible across runs.
360 std::stable_sort(cand.begin(), cand.end(),
361 [](const std::pair<double, MacroPartition>& a,
362 const std::pair<double, MacroPartition>& b) { return a.first < b.first; });
363 beam.clear();
364 for (std::size_t i = 0; i < opt.beam_width && i < cand.size(); ++i)
365 beam.push_back(cand[i].second);
366 }
367 return best;
368}
369
370/** Everything `applyCompression` computes, plus the compressed environment. */
371template <class T>
374 /**
375 * The compressed environment. HELD BY SHARED POINTER because `SolverEnv<T>`
376 * stores a reference to the environment it solves, so the compressed one has
377 * to outlive the solver; returning it by value would make that the caller's
378 * problem to get right, and getting it wrong is a dangling reference rather
379 * than a wrong number.
380 */
381 std::shared_ptr<Environment<T>> env;
383 Matrix<T> macro_rate; ///< `computeMacroRate(i,j)`
384 std::vector<T> p; ///< micro stationary vector from the decomposition
385 std::vector<T> pmicro; ///< within-macro-state conditional probabilities
386 std::vector<T> pmacro; ///< `pMacro`, the macro-state probabilities
387 Matrix<double> prob_orig; ///< the macro embedding weights `newEmbweight`
389 /** eps <= epsMAX: below this the aggregation is meaningful, above it is not. */
390 bool compressible = false;
391};
392
393/**
394 * Port of `applyCompression`: pick a partition, decompose, and build the
395 * macro-state environment.
396 *
397 * WHY THE MACRO SERVICE RATES ARE A pmicro-WEIGHTED AVERAGE. Within a
398 * macro-state the environment switches fast compared with the network, so the
399 * network sees the group's rates averaged over the CONDITIONAL distribution of
400 * being in each micro-stage given the group -- which is exactly pmicro. That
401 * average is over rates and not over distributions, so a phase-type service
402 * collapses to an exponential of the same mean: the compression keeps the first
403 * moment and discards the SCV, as the reference's `Exp(rateSum)` does.
404 */
405template <class T>
407 const std::size_t E = e0.nstages();
408 const T zero = num_traits<T>::from_int(0);
409
410 // A MACRO-STATE IS A NETWORK AT AVERAGED RATES, so every stage merged into
411 // one has to have a station rate table to average. A layered stage does not:
412 // its stations are the layers SolverLN derives from it, and averaging those
413 // would aggregate an artifact of the layering rather than the model.
415 "SolverENV compression",
416 "a macro-state is built as one stage network carrying the pmicro-weighted average of "
417 "its members' station rates, and a layered model has no such rate table -- only the "
418 "layers SolverLN derives from it");
419
420 // The whole construction reads the environment as a CTMC, so a transition
421 // that is not exponential cannot survive it: the macro arc would be built as
422 // an Exp of the aggregated rate, and the analyzer would then integrate the
423 // stage transient against a holding-time CDF the model never had.
424 for (std::size_t a = 0; a < E; ++a)
425 for (std::size_t b = 0; b < E; ++b) {
426 if (!e0.arc(a, b).enabled) continue;
427 if (e0.arc(a, b).dist.type != lang::ProcessType::EXP)
428 throw UnsupportedError(
429 "SolverENV compression: the transition from stage " + std::to_string(a + 1) +
430 " to " + std::to_string(b + 1) +
431 " is not exponential, and the NCD decomposition reads the environment as a "
432 "CTMC through E0 = getRate(); aggregating it would silently replace the "
433 "transition by an exponential of the same mean, so it is refused instead");
434 }
435
437 c.E0 = env_rate_matrix(e0);
439
440 if (!opt.partition.empty()) {
441 meanfield_detail::check_partition(opt.partition, E);
442 c.MS = opt.partition;
443 } else if (E <= opt.beam_above_stages) {
445 } else {
447 }
448 const std::size_t Ec = c.MS.size();
449
450 const EnvDecomp<T> d = env_ctmc_decompose(c.Eutil, c.MS, opt);
451 c.p = d.p;
452 c.eps = d.eps;
453 c.epsMAX = d.epsMAX;
454 c.q = d.q;
455 // The reference warns and continues. The flag is reported rather than
456 // thrown for the same reason: an environment that does not decompose still
457 // has an answer, it is simply the answer to a model the aggregation moved.
459
460 c.pmacro.assign(Ec, zero);
461 for (std::size_t i = 0; i < Ec; ++i)
462 for (std::size_t s : c.MS[i]) c.pmacro[i] += c.p[s];
463 c.pmicro.assign(E, zero);
464 for (std::size_t i = 0; i < Ec; ++i) {
465 if (num_traits<T>::to_double(c.pmacro[i]) <= 0) continue;
466 for (std::size_t s : c.MS[i]) c.pmicro[s] = T(c.p[s] / c.pmacro[i]);
467 }
468
469 // computeMacroRate: the micro rates out of the block, weighted by the
470 // conditional probability of sitting in each of its micro-stages.
471 c.macro_rate = Matrix<T>(Ec, Ec, zero);
472 for (std::size_t i = 0; i < Ec; ++i)
473 for (std::size_t j = 0; j < Ec; ++j)
474 for (std::size_t mi : c.MS[i])
475 for (std::size_t mj : c.MS[j]) c.macro_rate(i, j) += T(c.pmicro[mi] * c.E0(mi, mj));
476
477 // newEmbweight: P(the previous macro-state was k | now entering e).
478 c.prob_orig = Matrix<double>(Ec, Ec, 0.0);
479 for (std::size_t x = 0; x < Ec; ++x) {
480 double tot = 0.0;
481 for (std::size_t h = 0; h < Ec; ++h)
482 if (h != x)
483 tot += num_traits<T>::to_double(c.pmacro[h]) *
485 if (!(tot > 0.0)) continue;
486 for (std::size_t k = 0; k < Ec; ++k) {
487 if (k == x) continue;
490 }
491 }
492
493 // The macro networks: the first micro-stage's structure, its rates replaced
494 // by the pmicro-weighted averages over the block.
495 c.env = std::make_shared<Environment<T>>(e0.name() + "-compressed", Ec);
496 for (std::size_t i = 0; i < Ec; ++i) {
497 const std::size_t first = c.MS[i][0];
498 qn::NetworkStruct<T> sn = e0.stage(first).model;
499 const std::size_t M = sn.nstations, K = sn.nclasses;
500 for (std::size_t s : c.MS[i])
501 if (e0.stage(s).model.nstations != M || e0.stage(s).model.nclasses != K)
502 throw InputError(
503 "SolverENV compression: macro-state " + std::to_string(i + 1) +
504 " merges stages with different stations or classes, whose rates cannot be "
505 "averaged entrywise");
506 for (std::size_t m = 0; m < M; ++m) {
507 const lang::NodeType nt = sn.stations[m].nodetype;
508 // Only a Queue or a Delay has a service rate to average; a Source
509 // carries an arrival process and a Join an infinite rate, and the
510 // reference skips both.
511 if (nt != lang::NodeType::Queue && nt != lang::NodeType::Delay) continue;
512 for (std::size_t k = 0; k < K; ++k) {
513 T acc = zero;
514 for (std::size_t s : c.MS[i])
515 acc += T(c.pmicro[s] * e0.stage(s).model.rates(m, k));
516 if (num_traits<T>::to_double(acc) > 0) sn.service[m][k] = lang::Distrib<T>::exp_rate(acc);
517 }
518 }
519 sn.refresh_struct();
520 c.env->set_stage(i, e0.stage(first).name + "+", e0.stage(first).type, sn);
521 }
522
523 for (std::size_t i = 0; i < Ec; ++i) {
524 bool any_out = false;
525 for (std::size_t j = 0; j < Ec; ++j) {
526 if (i == j) continue; // a self-loop would lengthen the holding time
527 if (!(num_traits<T>::to_double(c.macro_rate(i, j)) > 0)) continue;
528 // The reset policy of the representative micro arc. Two micro arcs
529 // folding into one macro arc may carry DIFFERENT resets and a
530 // std::function cannot be compared, so disagreement is detectable
531 // only in whether a reset is present at all; that much is refused,
532 // and beyond it the representative stands.
533 const std::size_t fi = c.MS[i][0], fj = c.MS[j][0];
534 const bool want = static_cast<bool>(e0.arc(fi, fj).reset);
535 for (std::size_t a : c.MS[i])
536 for (std::size_t b : c.MS[j])
537 if (e0.arc(a, b).enabled && static_cast<bool>(e0.arc(a, b).reset) != want)
538 throw UnsupportedError(
539 "SolverENV compression: the arcs folding into the macro transition " +
540 std::to_string(i + 1) + " -> " + std::to_string(j + 1) +
541 " do not agree on whether a reset policy applies, and one macro arc "
542 "can carry only one; split the partition so that reset policies are "
543 "uniform within it");
544 c.env->add_transition(i, j, lang::Distrib<T>::exp_rate(c.macro_rate(i, j)),
545 e0.arc(fi, fj).reset);
546 any_out = true;
547 }
548 if (!any_out)
549 throw InputError(
550 "SolverENV compression: macro-state " + std::to_string(i + 1) +
551 " has no outgoing transition, so the compressed environment is absorbing; the "
552 "partition merged a whole recurrent class into one block");
553 }
554 return c;
555}
556
557/**
558 * `probEnv = pMacro` and `probOrig = newEmbweight`, the two quantities
559 * `applyCompression` overwrites on the environment.
560 *
561 * ORDER MATTERS: `SolverEnv<T>`'s constructor calls `Environment::init()`,
562 * which recomputes both from the macro arcs, so this must be applied AFTER the
563 * solver is constructed and BEFORE `solve()` is called. `solver_env_meanfield`
564 * below does exactly that, and is the reason to prefer it over wiring the two
565 * calls by hand.
566 *
567 * The two are consistent rather than contradictory: for an exponential
568 * environment `Environment::init()` derives probEnv as the stationary law of
569 * the macro generator, and aggregating a chain by its exact conditional
570 * distributions reproduces the block sums of the original stationary law
571 * exactly. So this overwrite replaces one estimate of the same quantity by
572 * another, and the gap between them is a second reading of the decomposition
573 * error alongside eps.
574 */
575template <class T>
577 const std::size_t Ec = c.MS.size();
578 if (e.nstages() != Ec)
579 throw InputError(
580 "SolverENV compression: the macro probabilities do not match the environment they "
581 "are being applied to");
582 e.prob_env.assign(Ec, 0.0);
583 for (std::size_t i = 0; i < Ec; ++i) e.prob_env[i] = num_traits<T>::to_double(c.pmacro[i]);
584 e.prob_orig = c.prob_orig;
585}
586
587/** A mean-field solve, with the compression that produced it. */
588template <class T>
590 EnvSolution avg; ///< what SolverEnv reported
592 bool compressed = false; ///< false when the solve ran on the original stages
593 /**
594 * `aggregateCacheMeanfield_`: environment-blended hit and miss probabilities
595 * per Cache node. Empty when the model holds no Cache. The reference writes
596 * these onto the stage-one node objects with `setResultHitProb`; this port
597 * has no model-object layer at this level, so they ride in the result.
598 *
599 * KEYED BY NAME, in `CacheMetrics`' one shape rather than an ENV-private
600 * one: the statevec coupling and the closed-form limits report the same
601 * surface, and three shapes for one answer is how a host ends up writing
602 * cache results onto a Sink. A field no sweep produced stays EMPTY, and
603 * empty means not computed rather than zero.
604 */
606 /** The reference's `substituteCacheBlend_` warning, verbatim; empty when it did not run. */
607 std::string cache_warning;
608};
609
610namespace meanfield_detail {
611
612/** 0-based Cache node indices of a stage, in `find(nodetype == Cache)` order. */
613template <class T>
614std::vector<std::size_t> cache_nodes_of(const qn::NetworkStruct<T>& sn) {
615 std::vector<std::size_t> out;
616 for (std::size_t ind = 0; ind < sn.nodes.size(); ++ind)
617 if (sn.nodes[ind].nodetype == lang::NodeType::Cache) out.push_back(ind);
618 return out;
619}
620
621/**
622 * `local_hit_by_list`: the hit probability of class k resolved by cache list, from the
623 * sojourn-averaged occupancy `xbar` (flat DDPP state, item-major over lists 0..h) and the
624 * isolated per-item request rates `lam`. List 0 is outside the cache, so the rows are
625 * lists 1..h and sum to the class's total hit probability. Empty when it is undefined.
626 */
627template <class T>
628std::vector<T> hit_by_list(const std::vector<T>& xbar, const Matrix<T>& lam, std::size_t k) {
629 std::vector<T> hbl;
630 if (xbar.empty() || lam.rows() == 0 || k >= lam.rows()) return hbl;
631 const std::size_t nitems = lam.cols();
632 if (nitems == 0 || xbar.size() % nitems != 0) return hbl;
633 const std::size_t lists = xbar.size() / nitems;
634 if (lists < 2) return hbl;
635 const std::size_t h = lists - 1;
636 std::vector<double> wpop(nitems, 0.0);
637 double tot = 0.0;
638 for (std::size_t i = 0; i < nitems; ++i) {
639 const double v = num_traits<T>::to_double(lam(k, i));
640 wpop[i] = std::isfinite(v) ? v : 0.0;
641 tot += wpop[i];
642 }
643 if (!(tot > 0.0)) return hbl;
644 hbl.assign(h, num_traits<T>::from_int(0));
645 for (std::size_t l = 1; l <= h; ++l) {
646 T acc = num_traits<T>::from_int(0);
647 for (std::size_t i = 0; i < nitems; ++i)
648 acc = T(acc + T(num_traits<T>::from_double(wpop[i] / tot) * xbar[i + l * nitems]));
649 double v = num_traits<T>::to_double(acc);
650 v = std::max(0.0, std::min(1.0, v));
651 hbl[l - 1] = num_traits<T>::from_double(v);
652 }
653 return hbl;
654}
655
656/**
657 * `aggregateCacheMeanfield_`: the environment-blended hit and miss ratios of
658 * every Cache, by a mean-field fixed point over the caches' OWN occupancies.
659 *
660 * THIS IS A SECOND FIXED POINT, nested beside the queue-length one, and it
661 * exists because the two carry different objects. `SolverEnv`'s coupling hands
662 * each stage the marginal mean queue lengths its predecessors left; a cache's
663 * state is its per-item occupancy, which no queue length encodes. So this sweep
664 * integrates each stage's cache drift over its sojourn from an entry occupancy
665 * that mixes its predecessors' exit occupancies by `prob_orig`, exactly the way
666 * the queue-length handoff mixes means, and iterates the pair to convergence.
667 *
668 * THE RMF DRIFT RUNS IN PER-REQUEST TIME, NOT REAL TIME, which is the trap here.
669 * `solver_fld_cacheqn_tran` returns a grid in units of cache requests, so the
670 * holding-time CDF cannot be evaluated on it directly: the grid is divided by
671 * the cache's total arrival rate first (`treal = t / Lam`). Skipping that
672 * weights every stage as though its cache saw the same request rate, which
673 * silently favours the slow stages.
674 *
675 * THE RATIO IS TAKEN AFTER THE BLEND, as in the state-vector twin: a per-stage
676 * ratio weighted by prob_env averages ratios, which is not the ratio the
677 * environment exhibits unless every stage carries the same total rate.
678 */
679template <class T>
680solvers::CacheMetrics<T> aggregate_cache_meanfield(Environment<T>& e, const EnvOptions& o) {
681 solvers::CacheMetrics<T> out;
682 const std::size_t E = e.nstages();
683 if (E == 0) return out;
684 // The blend is over the Cache NODES of the stage networks; a layered
685 // environment has none, and reading an absent NetworkStruct would report an
686 // empty node list as though the model had been examined.
687 if (e.has_lqn_stages()) return out;
688 const qn::NetworkStruct<T>& sn1 = e.stage(0).model;
689 const std::vector<std::size_t> nodes = cache_nodes_of(sn1);
690 if (nodes.empty()) return out;
691 const std::size_t nc = nodes.size(), K = sn1.nclasses;
692
693 // Only a FLUID stage exposes the RMF cache transient this reads; the
694 // reference returns without writing anything when any stage is not one,
695 // rather than blending what it has. `meanfield_cache` sends a CTMC ensemble to
696 // `substitute_cache_blend` first, as `substituteCacheBlend_` is reached there.
697 if (o.stage_solver != "fluid") return out;
698
699 // A finite window per stage, falling back to a few mean holding times when
700 // the inner solver left the timespan open, as the reference does.
701 std::vector<double> tend(E, 0.0);
702 for (std::size_t s = 0; s < E; ++s) {
703 double t1 = o.timespan_end;
704 if (!std::isfinite(t1) || !(t1 > 0.0)) t1 = 20.0 * mam::map_mean(e.hold_time[s].map());
705 tend[s] = t1;
706 }
707
708 std::vector<std::vector<std::vector<T> > > entry(E, std::vector<std::vector<T> >(nc));
709 std::vector<std::vector<fluid::FluidCacheqnTranCache<T> > > tran(E);
710 std::vector<std::vector<std::vector<T> > > exit_occ(E, std::vector<std::vector<T> >(nc));
711 std::vector<T> prev_flat;
712 const int sweeps = o.iter_max > 0 ? o.iter_max : 1;
713
714 for (int sweep = 0; sweep < sweeps; ++sweep) {
715 for (std::size_t s = 0; s < E; ++s) {
716 fluid::FluidOptions fo;
717 fo.method = "rmf";
718 // The sojourn average int x dF(t/Lam) / int dF(t/Lam) is integrated WITH the
719 // drift (per-request time -> real time by the cache's request rate Lam), so it
720 // does not depend on the ODE output grid.
721 const auto hm = e.hold_time[s].map();
722 const auto hpie = mam::map_pie(hm);
723 Matrix<T> hD0(hm.D0.rows(), hm.D0.cols(), num_traits<T>::from_int(0));
724 for (std::size_t i = 0; i < hm.D0.rows(); ++i)
725 for (std::size_t j = 0; j < hm.D0.cols(); ++j)
726 hD0(i, j) = num_traits<T>::from_double(hm.D0(i, j));
727 std::vector<T> hpieT(hpie.size());
728 for (std::size_t i = 0; i < hpie.size(); ++i) hpieT[i] = num_traits<T>::from_double(hpie[i]);
729 tran[s] = fluid::solver_fld_cacheqn_tran(e.stage(s).model, fo, 0.0, tend[s], entry[s],
730 hD0, hpieT);
731 for (std::size_t c = 0; c < nc; ++c) {
732 exit_occ[s][c].clear();
733 std::size_t idx = tran[s].size();
734 for (std::size_t q = 0; q < tran[s].size(); ++q)
735 if (tran[s][q].node == nodes[c]) idx = q;
736 if (idx == tran[s].size()) continue;
737 const fluid::FluidCacheqnTranCache<T>& tc = tran[s][idx];
738 if (tc.sojourn.empty() || !(num_traits<T>::to_double(tc.sojourn.wtot) > 0.0)) continue;
739 exit_occ[s][c] = tc.sojourn.xbar;
740 }
741 }
742
743 // Each stage's entry occupancy is its predecessors' exits, by prob_orig.
744 std::vector<std::vector<std::vector<T> > > next(E, std::vector<std::vector<T> >(nc));
745 for (std::size_t s = 0; s < E; ++s)
746 for (std::size_t c = 0; c < nc; ++c) {
747 std::vector<T> acc;
748 for (std::size_t h = 0; h < E; ++h) {
749 const double po = e.prob_orig(h, s);
750 if (!(po > 0.0) || exit_occ[h][c].empty()) continue;
751 if (acc.empty()) acc.assign(exit_occ[h][c].size(), num_traits<T>::from_int(0));
752 if (acc.size() != exit_occ[h][c].size()) continue;
753 for (std::size_t a = 0; a < acc.size(); ++a)
754 acc[a] = T(acc[a] + T(num_traits<T>::from_double(po) * exit_occ[h][c][a]));
755 }
756 next[s][c] = acc;
757 }
758
759 std::vector<T> flat;
760 for (std::size_t s = 0; s < E; ++s)
761 for (std::size_t c = 0; c < nc; ++c)
762 flat.insert(flat.end(), next[s][c].begin(), next[s][c].end());
763 entry = next;
764 if (!prev_flat.empty() && prev_flat.size() == flat.size()) {
765 double dmax = 0.0;
766 for (std::size_t a = 0; a < flat.size(); ++a)
767 dmax = std::max(dmax, std::fabs(num_traits<T>::to_double(flat[a]) -
768 num_traits<T>::to_double(prev_flat[a])));
769 prev_flat = flat;
770 if (dmax < o.iter_tol) break;
771 } else {
772 prev_flat = flat;
773 }
774 }
775
776 const T nan = num_traits<T>::from_double(std::numeric_limits<double>::quiet_NaN());
777 std::vector<std::vector<T> > hitprob(nc, std::vector<T>(K, nan));
778 std::vector<std::vector<T> > missprob(nc, std::vector<T>(K, nan));
779 std::vector<Matrix<T> > hitproblist(nc);
780 for (std::size_t c = 0; c < nc; ++c) {
781 std::vector<T> hitT(K, num_traits<T>::from_int(0)), missT(K, num_traits<T>::from_int(0));
782 Matrix<T> hitLT; // (K x h) per-list hit throughput; empty until a stage resolves one
783 for (std::size_t s = 0; s < E; ++s) {
784 std::size_t idx = tran[s].size();
785 for (std::size_t q = 0; q < tran[s].size(); ++q)
786 if (tran[s][q].node == nodes[c]) idx = q;
787 if (idx == tran[s].size()) continue;
788 const fluid::FluidCacheqnTranCache<T>& tc = tran[s][idx];
789 const double sw = num_traits<T>::to_double(tc.sojourn.wtot);
790 if (tc.sojourn.empty() || !std::isfinite(sw) || !(sw > 0.0)) continue;
791 const T pe = num_traits<T>::from_double(e.prob_env[s]);
792 // Sojourn-averaged per-item, per-list occupancy of this stage, the same average
793 // hbar takes, so the list rows sum to it.
794 const std::vector<T>& xbar = tc.sojourn.xbar;
795 for (std::size_t k = 0; k < K && k < tc.arate.size(); ++k) {
796 if (!(num_traits<T>::to_double(tc.arate[k]) > 0.0)) continue;
797 // hit/miss are affine in the occupancy, so their sojourn averages are xbar's.
798 const T hbar = tc.hitprob_bar[k], mbar = tc.missprob_bar[k];
799 hitT[k] = T(hitT[k] + T(T(pe * tc.arate[k]) * hbar));
800 missT[k] = T(missT[k] + T(T(pe * tc.arate[k]) * mbar));
801 const std::vector<T> hbl = hit_by_list(xbar, tc.lam, k);
802 if (hbl.empty()) continue;
803 if (hitLT.rows() == 0) hitLT = Matrix<T>(K, hbl.size(), num_traits<T>::from_int(0));
804 for (std::size_t l = 0; l < hbl.size() && l < hitLT.cols(); ++l)
805 hitLT(k, l) = T(hitLT(k, l) + T(T(pe * tc.arate[k]) * hbl[l]));
806 }
807 }
808 if (hitLT.rows() > 0) hitproblist[c] = Matrix<T>(K, hitLT.cols(), nan);
809 for (std::size_t k = 0; k < K; ++k) {
810 const T tot = T(hitT[k] + missT[k]);
811 if (num_traits<T>::to_double(tot) > 0.0) {
812 hitprob[c][k] = T(hitT[k] / tot);
813 missprob[c][k] = T(missT[k] / tot);
814 for (std::size_t l = 0; l < hitLT.cols(); ++l)
815 hitproblist[c](k, l) = T(hitLT(k, l) / tot);
816 }
817 }
818 // The reference sets the list split only when some entry of it is defined.
819 bool any = false;
820 for (std::size_t k = 0; k < hitproblist[c].rows() && !any; ++k)
821 for (std::size_t l = 0; l < hitproblist[c].cols(); ++l)
822 if (std::isfinite(num_traits<T>::to_double(hitproblist[c](k, l)))) any = true;
823 if (!any) hitproblist[c] = Matrix<T>();
824 }
825
826 // The model's caches, in the same `find(nodetype == Cache)` order the sweep
827 // used, so entry c is `nodes[c]`; the caps and item counts come from the
828 // struct and the measured quantities from the blend above. DELAYED-HIT STAYS
829 // EMPTY and that is the answer, not a gap: the RMF drift models no retrieval
830 // sub-system, so there is no delayed fraction to report. The per-list split is
831 // `local_hit_by_list` of the reference, read off the sojourn-averaged occupancy.
832 out = solvers::cache_metrics_of(sn1, std::vector<T>(), std::vector<T>(), std::vector<T>(),
833 std::vector<T>(), Matrix<T>(), Matrix<T>(), std::vector<T>());
834 for (std::size_t c = 0; c < nc && c < out.caches.size(); ++c) {
835 out.caches[c].hitprob = hitprob[c];
836 out.caches[c].missprob = missprob[c];
837 if (hitproblist[c].rows() > 0) out.caches[c].hitproblist = hitproblist[c];
838 }
839 return out;
840}
841
842/** Whether any flat stage of `e` holds a Cache node. */
843template <class T>
844bool env_has_cache_stage(const Environment<T>& e) {
845 for (std::size_t s = 0; s < e.nstages(); ++s) {
846 // A layered stage carries no NetworkStruct; a Cache inside an LQN is a CacheTask,
847 // which lives in its host's LAYER and is SolverLN's to serve.
848 if (e.is_lqn(s)) continue;
849 if (!cache_nodes_of(e.stage(s).model).empty()) return true;
850 }
851 return false;
852}
853
854/** The text `substituteCacheBlend_` warns with. */
855inline const char* substitute_cache_blend_warning() {
856 return "The mean-field cache fixed point requires fluid stage solvers; blending each stage's "
857 "own cache ratios instead, without the occupancy handoff across environment switches.";
858}
859
860/**
861 * `substituteCacheBlend_`: the cache blend for stages the mean-field cache fixed point cannot
862 * integrate. Each stage is solved in its own steady state by the stage solver, and the hit,
863 * miss and delayed ratios are blended by the per-stage arrival into the cache times prob_env,
864 * the same rate weighting the 'avg'/'dec' limits use. The occupancy handoff across switches
865 * is NOT modelled; `cache_warning` says so, as the reference's warning does.
866 */
867template <class T>
868solvers::CacheMetrics<T> substitute_cache_blend(const Environment<T>& e, const EnvOptions& o) {
869 typename SolverEnvLimit<T>::CacheAccum acc;
870 for (std::size_t s = 0; s < e.nstages(); ++s) {
871 if (e.is_lqn(s)) continue;
872 const qn::NetworkStruct<T>& sn = e.stage(s).model;
873 const EnvStageAvg<T> st = SolverEnvLimit<T>::ctmc_stage_steady(sn, o.stage_cutoff);
874 acc.add(sn, st, e.prob_env[s]);
875 }
876 const solvers::CacheMetrics<double> c = acc.finish();
877 solvers::CacheMetrics<T> out;
878 for (std::size_t i = 0; i < c.caches.size(); ++i) {
879 const solvers::CacheNodeMetrics<double>& m = c.caches[i];
880 solvers::CacheNodeMetrics<T> w;
881 w.node = m.node;
882 w.name = m.name;
883 w.itemcap = m.itemcap;
884 w.nitems = m.nitems;
885 for (std::size_t k = 0; k < m.hitprob.size(); ++k)
886 w.hitprob.push_back(num_traits<T>::from_double(m.hitprob[k]));
887 for (std::size_t k = 0; k < m.missprob.size(); ++k)
888 w.missprob.push_back(num_traits<T>::from_double(m.missprob[k]));
889 for (std::size_t k = 0; k < m.delayedprob.size(); ++k)
890 w.delayedprob.push_back(num_traits<T>::from_double(m.delayedprob[k]));
891 if (m.hitproblist.rows() > 0) {
892 w.hitproblist = Matrix<T>(m.hitproblist.rows(), m.hitproblist.cols());
893 for (std::size_t r = 0; r < m.hitproblist.rows(); ++r)
894 for (std::size_t l = 0; l < m.hitproblist.cols(); ++l)
895 w.hitproblist(r, l) = num_traits<T>::from_double(m.hitproblist(r, l));
896 }
897 out.caches.push_back(w);
898 }
899 return out;
900}
901
902/**
903 * The cache surface of a mean-field solve: the RMF fixed point when every stage is fluid, the
904 * substitute blend otherwise, as `aggregateCacheMeanfield_` branches.
905 */
906template <class T>
907void meanfield_cache(Environment<T>& e, const EnvOptions& o, EnvMeanfieldSolution<T>& out) {
908 if (o.stage_solver == "fluid" || e.has_lqn_stages() || !env_has_cache_stage(e)) {
909 out.cache = aggregate_cache_meanfield(e, o);
910 return;
911 }
912 out.cache_warning = substitute_cache_blend_warning();
913 out.cache = substitute_cache_blend(e, o);
914}
915
916} // namespace meanfield_detail
917
918/** The mean-field solve on the original stages, with no compression. */
919template <class T>
922 SolverEnv<T> s(e, o);
923 out.avg = s.solve();
924 meanfield_detail::meanfield_cache(e, o, out);
925 return out;
926}
927
928/**
929 * The compressed mean-field solve: aggregate the environment, then run the
930 * mean-field fixed point over the macro-states.
931 *
932 * The compressed environment is kept alive by the returned structure, which is
933 * what `SolverEnv` held a reference to; reading `.avg` out of the result and
934 * discarding the rest is safe, but the compression it came from travels with it
935 * so that eps and epsMAX can be checked against the numbers they produced.
936 */
937template <class T>
939 const EnvCompressOptions& c) {
941 out.compression = env_compress(e, c);
942 out.compressed = true;
943 SolverEnv<T> s(*out.compression.env, o);
945 out.avg = s.solve();
946 meanfield_detail::meanfield_cache(*out.compression.env, o, out);
947 return out;
948}
949
950} // namespace env
951} // namespace line
952
953#endif // LINE_SOLVERS_ENV_SOLVER_ENV_MEANFIELD_H
What a solver observed about the Cache nodes of a model.
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
static EnvStageAvg< T > ctmc_stage_steady(const qn::NetworkStruct< T > &sn, double cutoff)
One stage in steady state under the enumerated CTMC at cutoff, with its cache split,...
The environment solver.
Definition solver_env.h:229
EnvSolution solve()
Definition solver_env.h:233
A network plus its refreshed NetworkStruct.
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 > >(), const Matrix< T > &hold_D0=Matrix< T >(), const std::vector< T > &hold_pie=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:326
T map_mean(const Map< T > &m)
Mean inter-arrival time, 1/lambda.
Definition map_moment.h:101
std::vector< T > map_pie(const Map< T > &m)
Phase distribution seen by an arriving job, pie = pi D1 / (pi D1 e).
Definition map_moment.h:89
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.
CacheMetrics< T > cache_metrics_of(const qn::NetworkStruct< T > &sn, const std::vector< T > &hitprob, const std::vector< T > &missprob, const std::vector< T > &delayedprob, const std::vector< T > &latency, const Matrix< T > &hitproblist, const Matrix< T > &itemprob, const std::vector< T > &listcost)
Assemble CacheMetrics from what a cache analyzer returned.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
T num_abs(const T &v)
Definition number.h:198
A queueing network and its refreshed NetworkStruct.
Number-type abstraction for the templated API port.
SolverENV: a queueing network in a random environment.
The two CLOSED-FORM environment limits: SolverENV.solveEnvLimit, reached by options....
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
A mean-field solve, with the compression that produced it.
std::string cache_warning
The reference's substituteCacheBlend_ warning, verbatim; empty when it did not run.
solvers::CacheMetrics< 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:113
What SolverENV reports.
Definition solver_env.h:170
static Distrib exp_rate(const T &r)
Definition lang_types.h:945
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
Every Cache node of the model, in node order; empty on a model with none.