LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_env_limit.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_LIMIT_H
6#define LINE_SOLVERS_ENV_SOLVER_ENV_LIMIT_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The two CLOSED-FORM environment limits: `SolverENV.solveEnvLimit`, reached by
12 * `options.method` in {`avg`, `dec`}.
13 *
14 * Both replace the fixed point with a single reading, and each is exact at one
15 * end of the time-scale separation between the environment and the network:
16 *
17 * `avg`, the FAST-environment limit. The environment switches so much faster
18 * than the network responds that the network only ever sees the AVERAGE of the
19 * modulated rates. One rate-averaged model is built -- every station-class
20 * rate that varies across stages replaced by its probEnv-weighted mean, as an
21 * exponential -- and solved once, in steady state. Exact as the stage-switch
22 * rate goes to infinity.
23 *
24 * `dec`, the SLOW-environment limit, i.e. quasi-stationary decomposition. The
25 * environment stays in a stage long enough for the network to reach that
26 * stage's own steady state, so each stage is solved independently and the
27 * metrics are averaged with weights probEnv. Exact as the stage-switch rate
28 * goes to zero.
29 *
30 * NEITHER CARRIES ANYTHING ACROSS A SWITCH, which is what makes them closed
31 * form and also what they give up: no entry state, no reset policy, no
32 * transient. A reset policy declared on an arc is therefore inert here, exactly
33 * as it is in the reference, whose `solveEnvLimit` never reads `resetFun`.
34 *
35 * WHAT IS AVERAGED, AND WHAT IS LEFT ALONE (`buildRateAveragedModel`). Only a
36 * Source, a Queue or a Delay carries a rate to average. A station-class pair
37 * that is disabled in any stage, or whose rate does not actually vary across
38 * them, keeps its ORIGINAL distribution rather than being rewritten as an
39 * exponential of its own mean -- so a non-modulated Erlang stays an Erlang, and
40 * the base model is preserved exactly outside the modulated rates.
41 *
42 * A STAGE HOLDING A CACHE is solved, not refused. The hit, miss and delayed
43 * fractions each stage reports are blended the way the reference's
44 * `accumCacheMetric` does after 2026-09-13: as RATES weighted by that stage's
45 * per-class arrival INTO THE CACHE, normalised once at the end. Rates and not
46 * ratios, because a stage's hit ratio is conditional on arriving in that stage,
47 * so a probEnv-weighted mean of ratios disagrees with the Sink-row hit
48 * throughput -- `sum_e p_e*lambda_e*h_e` -- in the SAME node table. It is also
49 * the convention `aggregate_cache_meanfield` and the statevec `cache_blend`
50 * already hold, each with a comment saying so.
51 *
52 * WHICH SOLVER RUNS A STAGE. The reference hands `SolverENV` a FACTORY and calls
53 * it for every stage, so "the stage solver is the calling solver" is literally
54 * true there. The second constructor here takes an `EnvStageAvgFn`, the same
55 * thing as a callable, and the no-factory path runs `stage_solver`: the fluid
56 * analyzer, with `ctmc` the enumerated chain at `stage_cutoff` (an ensemble built
57 * on SolverCTMC stages), with `mva` / `nc` the MVA or NC runner at `stage_mva` /
58 * `stage_nc` (an ensemble built on SolverMVA / SolverNC stages). The callable is
59 * what `map_env_approx` uses, so that the CALLING solver's own options reach
60 * every stage.
61 *
62 * `occupation`, `SolverENV.solveEnvOccupation`, is the `dec` twin for an
63 * environment that need not be ergodic. Each stage is solved in steady state
64 * as `dec` solves it, but weighted by the time the environment is expected to
65 * spend there from the initial stage law `EnvOptions::env_init` (default: stage
66 * 1), so the environment may be ABSORBING. Over a finite horizon [0,T] the
67 * occupation is int_0^T pi0 exp(A t) dt and the weights occupation/T, the
68 * time-averaged stage law, which needs exponential arcs to integrate. With
69 * T = Inf and absorbing stages the occupation is pi0_t (-A_tt)^-1 over the
70 * transient stages and the weights are the absorption probabilities; with no
71 * absorbing stage the weights are probEnv, as `dec`. The T = Inf cases read the
72 * embedded chain and the mean holding times only, so they hold for a
73 * semi-Markov environment too. A stage of weight zero is not solved.
74 *
75 * `occupation` is also the one limit that takes LAYERED stages, as the
76 * reference does: each is run by its own `SolverLN` (`EnvOptions::lqn`) to the
77 * fixed point and read over its LQN ELEMENTS (`SolverLN.getAvg`, an nidx x 1
78 * table), not over its layers: a zero-capacity stage shares the elements with
79 * the others while its layers do not (ln_zero_capacity drops the layers only a
80 * trapped reference task reaches). The stages must then be all layered with the
81 * same elements, which `Environment::set_lqn_stage` enforces.
82 * The reference solves stages sharing one model object once; a stage here holds
83 * its model by value, so there is no identity to share and each is solved.
84 */
85
86#include <algorithm>
87#include <cmath>
88#include <cstddef>
89#include <functional>
90#include <limits>
91#include <string>
92#include <type_traits>
93#include <vector>
94
98#include "line/num/number.h"
106#include "line/util/error.h"
107#include "line/util/expm.h"
108#include "line/util/linalg.h"
109#include "line/util/matrix.h"
110
111namespace line {
112namespace env {
113
114/**
115 * What one stage solve reports back to the limits.
116 *
117 * The literal translation of what the reference's `[Qe,Ue,~,Te] = se.getAvg()`
118 * plus `se.getAvgNode()` hand back, minus the node table: a Cache reports on the
119 * NODE, so `cache` carries what `getAvgNode`'s resync would have written there.
120 */
121template <class T>
126
127/**
128 * The stage solver, as a callable: the C++ spelling of the MATLAB function
129 * handle `SolverENV(renv, @(m) SolverX(m, opts))` passes.
130 *
131 * It is how a caller binds ITS OWN options into every stage (`map_env_approx`);
132 * the named stage solvers of `EnvOptions::stage_solver` cover the plain
133 * ensemble. There is no include cycle in the way of either: the MVA and NC
134 * runners reach no `env/` header (`ln/solver_ln.h` already includes both).
135 */
136template <class T>
137using EnvStageAvgFn = std::function<EnvStageAvg<T>(const qn::NetworkStruct<T>&)>;
138
139/** What a limit solve reports. */
141 /** Environment-averaged metrics, (nstations x nclasses). */
143 /** The limit that ran: `avg`, `dec` or `occupation`. */
144 std::string method;
145 /**
146 * `dec` and `occupation`: each stage's own steady state, before the blend.
147 * A stage `occupation` does not solve (weight zero) stays all zeros.
148 */
149 std::vector<Matrix<double> > QStage, UStage, TStage;
150 /**
151 * `occupation` only (`getStageOccupation`): the expected time spent in each
152 * stage over the horizon, Inf for a stage the environment ends in when the
153 * horizon is unbounded, and the weight of each stage in the blend.
154 */
155 std::vector<double> occupation, occupation_weight;
156 /** The stage probabilities the blend used: probEnv, or the occupation weights. */
157 std::vector<double> prob_env;
158 /**
159 * The environment-blended cache surface, one entry per Cache node.
160 *
161 * EMPTY on a model with no cache. Within an entry every field is optional
162 * and ABSENT MEANS NOT COMPUTED, never zero, exactly as `CacheNodeMetrics`
163 * states: a class no stage measured stays NaN rather than reading as "never
164 * hits". Kept `double`, since `init()` refuses `T != double` anyway.
165 */
167};
168
169/**
170 * The limit solver.
171 *
172 * `envObj` must already carry a model per stage; `init()` is called here, as
173 * `solveEnvLimit` calls `self.init()` before reading probEnv.
174 */
175template <class T>
177public:
178 SolverEnvLimit(Environment<T>& e, const EnvOptions& o) : envObj(e), opt(o) { init(); }
179
180 /**
181 * With an injected stage solver: `stage_fn` runs every stage, exactly as the
182 * reference calls its factory. `opt.stage_solver` is then not consulted,
183 * since the caller has already named the solver by handing one over.
184 */
186 : envObj(e), opt(o), stage_fn_(stage_fn) {
187 init();
188 }
189
191 if (occ_) return solve_occupation();
192 return dec_ ? solve_dec() : solve_avg();
193 }
194
195 /**
196 * `stageOccupation_`: the expected time spent in each stage over the
197 * horizon and the weight of each stage, read off the initialised
198 * environment without solving any stage. See the file header.
199 */
200 static void stage_occupation(const Environment<T>& env, const EnvOptions& o,
201 std::vector<double>& occ, std::vector<double>& w) {
202 const std::size_t E = env.nstages();
203 const std::vector<double> pi0 = env_init_prob(o, E);
204 const double inf = std::numeric_limits<double>::infinity();
205 const double hz = env_horizon(o);
206 occ.assign(E, 0.0);
207 w.assign(E, 0.0);
208 if (std::isfinite(hz)) {
209 // [A I; 0 0] * T, whose exponential carries int_0^T exp(A t) dt in its
210 // upper-right block; A is the generator of the exponential arcs.
211 Matrix<double> X(2 * E, 2 * E, 0.0);
212 for (std::size_t e = 0; e < E; ++e) {
213 double out = 0.0;
214 for (std::size_t h = 0; h < E; ++h) {
215 if (h == e) continue;
216 const EnvArc<T>& a = env.arc(e, h);
217 if (!a.enabled || a.dist.disabled) continue;
218 const double r = 1.0 / num_traits<T>::to_double(a.dist.mean);
219 X(e, h) = r * hz;
220 out += r;
221 }
222 X(e, e) = -out * hz;
223 X(e, E + e) = hz;
224 }
225 const Matrix<double> Z = expm(X);
226 for (std::size_t h = 0; h < E; ++h) {
227 double s = 0.0;
228 for (std::size_t e = 0; e < E; ++e) s += pi0[e] * Z(e, E + h);
229 occ[h] = s;
230 w[h] = s / hz;
231 }
232 return;
233 }
234 std::vector<char> absorb(E, 0);
235 bool any = false;
236 for (std::size_t e = 0; e < E; ++e) {
237 absorb[e] = !(env.rate[e] > 0.0);
238 any = any || absorb[e];
239 }
240 if (!any) {
241 for (std::size_t e = 0; e < E; ++e) {
242 w[e] = env.prob_env[e];
243 occ[e] = w[e] == 0.0 ? 0.0 : inf;
244 }
245 return;
246 }
247 // The mean-equivalent generator diag(lambda) (Pemb - I), restricted to
248 // the transient stages.
249 std::vector<std::size_t> tr;
250 for (std::size_t e = 0; e < E; ++e)
251 if (!absorb[e]) tr.push_back(e);
252 const std::size_t nt = tr.size();
253 Matrix<double> N(nt, nt, 0.0);
254 for (std::size_t a = 0; a < nt; ++a)
255 for (std::size_t b = 0; b < nt; ++b)
256 N(a, b) = -env.rate[tr[a]] * (env.pemb(tr[a], tr[b]) - (a == b ? 1.0 : 0.0));
257 Matrix<double> Ninv;
258 double rc = 0.0;
259 try {
260 Ninv = inverse(N);
261 const double n1 = norm1(N), ni = norm1(Ninv);
262 rc = (n1 > 0.0 && std::isfinite(ni)) ? 1.0 / (n1 * ni) : 0.0;
263 } catch (const NumericError&) {
264 rc = 0.0;
265 }
267 throw UnsupportedError(ErrorKind{"Structure"},
268 "SolverENV: method 'occupation' with timespan(2) = Inf needs every non-absorbing "
269 "stage to reach an absorbing one: some stages form a closed class. Set a finite "
270 "horizon, timespan [0,T].");
271 for (std::size_t b = 0; b < nt; ++b) {
272 double s = 0.0;
273 for (std::size_t a = 0; a < nt; ++a) s += pi0[tr[a]] * Ninv(a, b);
274 occ[tr[b]] = s;
275 }
276 for (std::size_t h = 0; h < E; ++h) {
277 if (!absorb[h]) continue;
278 double s = pi0[h];
279 for (std::size_t a = 0; a < nt; ++a)
280 s += occ[tr[a]] * env.rate[tr[a]] * env.pemb(tr[a], h);
281 w[h] = s;
282 if (s > 0.0) occ[h] = inf;
283 }
284 }
285
286 /**
287 * `occupationRefusal_`: a finite horizon integrates exp(A t), which only an
288 * environment of exponential arcs has; T = Inf reads the embedded chain and
289 * mean holding times, which any semi-Markov stage has. The initial stage
290 * law must be a distribution over the stages.
291 */
292 static void occupation_refusal(const Environment<T>& env, const EnvOptions& o) {
293 const std::size_t E = env.nstages();
294 if (std::isfinite(env_horizon(o))) {
295 for (std::size_t e = 0; e < E; ++e) {
296 if (env.is_kernel_stage(e))
297 throw UnsupportedError(ErrorKind{"Structure"},
298 "Stage " + std::to_string(e + 1) + " has kernel transitions, but method "
299 "'occupation' over a finite timespan needs exponential transitions to "
300 "integrate the stage law. Use timespan(2) = Inf.");
301 for (std::size_t h = 0; h < E; ++h) {
302 const EnvArc<T>& a = env.arc(e, h);
303 if (h == e || !a.enabled || a.dist.disabled ||
305 continue;
306 throw UnsupportedError(ErrorKind{"Structure"},
307 "The environment transition from stage " + std::to_string(e + 1) +
308 " to " + std::to_string(h + 1) + " is a " +
310 ": method 'occupation' over a finite timespan needs exponential "
311 "transitions. Expand it into exponential stages, or use timespan(2) = "
312 "Inf.");
313 }
314 }
315 }
316 const std::vector<double> pi0 = env_init_prob(o, E);
317 double tot = 0.0;
318 bool neg = false;
319 for (double v : pi0) {
320 tot += v;
321 if (!(v >= 0.0)) neg = true;
322 }
323 if (pi0.size() != E || neg || std::fabs(tot - 1.0) > lang::GlobalConstants::CoarseTol)
324 throw InputError("options.config.env_init must be a probability vector over the " +
325 std::to_string(E) + " stages of the environment.");
326 }
327
328 /** `envInitProb_`: `options.config.env_init`, default the first stage. */
329 static std::vector<double> env_init_prob(const EnvOptions& o, std::size_t E) {
330 if (!o.env_init.empty()) return o.env_init;
331 std::vector<double> pi0(E, 0.0);
332 pi0[0] = 1.0;
333 return pi0;
334 }
335
336 /** `envHorizon_`: the length of SolverENV's timespan, Inf when unbounded. */
337 static double env_horizon(const EnvOptions& o) {
338 if (std::isfinite(o.env_timespan_start) && std::isfinite(o.env_timespan_end))
340 return std::numeric_limits<double>::infinity();
341 }
342
343private:
344 /** The 1-norm, the column-sum maximum `rcond` is taken in. */
345 static double norm1(const Matrix<double>& A) {
346 double m = 0.0;
347 for (std::size_t j = 0; j < A.cols(); ++j) {
348 double s = 0.0;
349 for (std::size_t i = 0; i < A.rows(); ++i) s += std::fabs(A(i, j));
350 if (!std::isfinite(s)) return std::numeric_limits<double>::infinity();
351 m = std::max(m, s);
352 }
353 return m;
354 }
355
356 void init() {
357 if (opt.method != "avg" && opt.method != "dec" && opt.method != "occupation")
358 throw UnsupportedError(ErrorKind{"Method"}, "SolverENV limit: '" + opt.method +
359 "' is not a closed-form environment limit; the two are 'avg' "
360 "(fast environment) and 'dec' (slow environment), and "
361 "'occupation' is the occupation-weighted 'dec'");
362 dec_ = (opt.method == "dec");
363 occ_ = (opt.method == "occupation");
364 if (!stage_fn_ && opt.stage_solver != "fluid" && opt.stage_solver != "ctmc" &&
365 opt.stage_solver != "mva" && opt.stage_solver != "nc")
366 throw UnsupportedError(ErrorKind{"Option"},
367 "SolverENV limit: stage solver '" + opt.stage_solver +
368 "' is not available; the limits solve each stage in STEADY STATE, by the fluid "
369 "analyzer, the enumerated CTMC, MVA or NC. Hand a stage solver to the "
370 "EnvStageAvgFn constructor to run another one");
371 if (!std::is_same<T, double>::value)
372 throw UnsupportedError(ErrorKind{"Arithmetic"},
373 "SolverENV limit: a fluid stage integrates its drift with LSODA, which is double "
374 "only; rerun with --arith double");
375
376 // The two limits read the stage NETWORKS directly -- `dec` solves each
377 // in steady state, `avg` builds one network at the probEnv-weighted
378 // rates -- and a layered stage has no station rate table to weight. The
379 // reference has no branch for it either: `solveEnvLimit` reaches for
380 // `self.ensemble{1}.nodes`, which a LayeredNetwork does not carry.
381 // `occupation` reads a layered stage in its block layout instead.
382 if (!occ_) envObj.reject_lqn_stages(
383 "SolverENV limit",
384 "the fast/slow limits read a stage's station rates directly -- 'dec' solves each "
385 "stage network in steady state and 'avg' builds one network at the probEnv-weighted "
386 "rates -- and a layered model has no such rate table, only the layers SolverLN "
387 "derives from it");
388 if (occ_) {
389 occupation_refusal(envObj, opt);
390 } else {
391 envObj.refuse_semi_markov(opt.method);
392 envObj.refuse_absorbing(opt.method);
393 }
394 envObj.init();
395 const std::size_t E = envObj.nstages();
396 lqn_ = envObj.has_lqn_stages();
397 if (lqn_) {
398 for (std::size_t e = 0; e < E; ++e)
399 if (!envObj.is_lqn(e))
400 throw InputError(
401 "SolverENV occupation: stage " + std::to_string(e + 1) + " is a Network and "
402 "another stage a LayeredNetwork; the metrics are blended entrywise, so "
403 "every stage must be read in one layout");
404 M = envObj.stage(0).lqn_model.nidx;
405 K = 1;
406 return;
407 }
408 M = envObj.stage(0).model.nstations;
409 K = envObj.stage(0).model.nclasses;
410 for (std::size_t e = 1; e < E; ++e)
411 if (envObj.stage(e).model.nstations != M || envObj.stage(e).model.nclasses != K)
412 throw InputError(
413 "SolverENV limit: every stage must have the same stations and classes; the "
414 "metrics are blended entrywise across them");
415 }
416
417public:
418 /**
419 * The RATE accumulator behind the cache blend, `SolverENV.accumCacheMetric`. Public, since
420 * the mean-field coupling's `substituteCacheBlend_` weights by the same rule.
421 *
422 * Keyed by the Cache node's NAME, never by its position: the struct's node
423 * order is not the declaration order, and indexing by position is what once
424 * made a host write cache results onto a Sink (see `cache_metrics.h`).
425 */
427 public:
428 /** One stage's contribution, weighted by its arrival into each cache. */
429 void add(const qn::NetworkStruct<T>& sn, const EnvStageAvg<T>& stage, double w) {
430 const std::vector<double> arv_all = cache_arrival(sn, stage.TN);
431 for (std::size_t c = 0; c < stage.cache.caches.size(); ++c) {
432 const solvers::CacheNodeMetrics<T>& cm = stage.cache.caches[c];
433 Entry& en = entry(cm);
434 const std::size_t K = std::max(cm.hitprob.size(), cm.missprob.size());
435 std::vector<double> arv(std::max(K, en.arv.size()), 0.0);
436 for (std::size_t r = 0; r < arv.size(); ++r) {
437 const std::size_t ind = cm.node; // 1-based node index
438 const std::size_t off = (ind >= 1 ? (ind - 1) * sn.nclasses + r : arv_all.size());
439 if (off < arv_all.size()) arv[r] = w * arv_all[off];
440 }
441 grow(en.arv, arv.size());
442 for (std::size_t r = 0; r < arv.size(); ++r) en.arv[r] += arv[r];
443 accum(en.hit, cm.hitprob, arv);
444 accum(en.miss, cm.missprob, arv);
445 accum(en.dhit, cm.delayedprob, arv);
446 accum_rows(en.hitl, cm.hitproblist, arv);
447 }
448 }
449
450 /** Divide the accumulated rates by the accumulated arrivals, ONCE. */
453 for (std::size_t c = 0; c < order_.size(); ++c) {
454 const Entry& en = order_[c];
456 cm.node = en.node;
457 cm.name = en.name;
458 cm.itemcap = en.itemcap;
459 cm.nitems = en.nitems;
460 cm.hitprob = divide(en.hit, en.arv);
461 cm.missprob = divide(en.miss, en.arv);
462 cm.delayedprob = divide(en.dhit, en.arv);
463 if (en.hitl.rows() > 0) {
464 cm.hitproblist = Matrix<double>(en.hitl.rows(), en.hitl.cols(), nan_());
465 for (std::size_t r = 0; r < en.hitl.rows(); ++r) {
466 const double d = r < en.arv.size() ? en.arv[r] : 0.0;
467 for (std::size_t l = 0; l < en.hitl.cols(); ++l)
468 cm.hitproblist(r, l) = d > 0.0 ? en.hitl(r, l) / d : nan_();
469 }
470 }
471 out.caches.push_back(cm);
472 }
473 return out;
474 }
475
476 private:
477 struct Entry {
478 std::size_t node = 0;
479 std::string name;
480 std::vector<double> itemcap;
481 std::size_t nitems = 0;
482 std::vector<double> arv;
483 /** EMPTY stays empty: a quantity no stage measured must not become 0. */
484 std::vector<double> hit, miss, dhit;
485 Matrix<double> hitl;
486 };
487
488 static double nan_() { return std::numeric_limits<double>::quiet_NaN(); }
489
490 static void grow(std::vector<double>& v, std::size_t n) {
491 if (v.size() < n) v.resize(n, 0.0);
492 }
493
494 Entry& entry(const solvers::CacheNodeMetrics<T>& cm) {
495 for (std::size_t i = 0; i < order_.size(); ++i)
496 if (order_[i].name == cm.name) return order_[i];
497 Entry en;
498 en.node = cm.node;
499 en.name = cm.name;
500 en.itemcap = cm.itemcap;
501 en.nitems = cm.nitems;
502 order_.push_back(en);
503 return order_.back();
504 }
505
506 /** One quantity: an ABSENT ratio contributes nothing and leaves it absent. */
507 static void accum(std::vector<double>& a, const std::vector<T>& ratio,
508 const std::vector<double>& arv) {
509 if (ratio.empty()) return;
510 grow(a, ratio.size());
511 for (std::size_t r = 0; r < ratio.size() && r < arv.size(); ++r) {
512 const double v = num_traits<T>::to_double(ratio[r]);
513 if (std::isfinite(v)) a[r] += arv[r] * v;
514 }
515 }
516
517 static void accum_rows(Matrix<double>& a, const Matrix<T>& hl,
518 const std::vector<double>& arv) {
519 if (hl.rows() == 0 || hl.cols() == 0) return;
520 if (a.rows() == 0) a = Matrix<double>(hl.rows(), hl.cols(), 0.0);
521 for (std::size_t r = 0; r < hl.rows() && r < a.rows(); ++r) {
522 const double w = r < arv.size() ? arv[r] : 0.0;
523 for (std::size_t l = 0; l < hl.cols() && l < a.cols(); ++l) {
524 const double v = num_traits<T>::to_double(hl(r, l));
525 if (std::isfinite(v)) a(r, l) += w * v;
526 }
527 }
528 }
529
530 /**
531 * A class with no arrival anywhere divides by nothing and STAYS NaN: its
532 * ratio is undefined, and 0 would read as "never hits".
533 */
534 static std::vector<double> divide(const std::vector<double>& a,
535 const std::vector<double>& d) {
536 std::vector<double> r;
537 if (a.empty()) return r;
538 r.assign(a.size(), nan_());
539 for (std::size_t i = 0; i < a.size(); ++i)
540 if (i < d.size() && d[i] > 0.0) r[i] = a[i] / d[i];
541 return r;
542 }
543
544 /**
545 * The per-class arrival INTO EACH NODE, flattened node-major.
546 *
547 * The weight is the cache's own arrival and not the Source throughput,
548 * because the latter is 0 on a closed cache model. The station table
549 * handed in is all zeros on purpose: only the Cache rows are read, and
550 * those come from the chain reference throughput and the node visits,
551 * never from a station row.
552 */
553 static std::vector<double> cache_arrival(const qn::NetworkStruct<T>& sn,
554 const Matrix<double>& TN) {
555 const std::size_t I = sn.nodes.size(), R = sn.nclasses, M = sn.nstations;
556 std::vector<double> out(I * R, 0.0);
557 if (TN.rows() == 0) return out;
558 Matrix<T> TNt(TN.rows(), TN.cols(), num_traits<T>::from_int(0));
559 for (std::size_t i = 0; i < TN.rows(); ++i)
560 for (std::size_t r = 0; r < TN.cols(); ++r)
561 TNt(i, r) = num_traits<T>::from_double(TN(i, r));
562 const Matrix<T> AN(M, R, num_traits<T>::from_int(0));
563 const Matrix<T> ANn = api::sn_get_node_arvr_from_tput<T>(sn, TNt, AN);
564 if (ANn.rows() != I) return out;
565 for (std::size_t ind = 0; ind < I; ++ind)
566 for (std::size_t r = 0; r < R; ++r) {
567 const double v = num_traits<T>::to_double(ANn(ind, r));
568 out[ind * R + r] = std::isfinite(v) ? v : 0.0;
569 }
570 return out;
571 }
572
573 std::vector<Entry> order_;
574 };
575
576 /**
577 * One stage in steady state under the enumerated CTMC at `cutoff`, with its cache split,
578 * which is exact (read off the chain). An ensemble built on SolverCTMC stages.
579 */
582 co.cutoff = cutoff;
584 EnvStageAvg<T> out;
585 out.QN = to_double(r.QN);
586 out.UN = to_double(r.UN);
587 out.TN = to_double(r.TN);
588 out.cache = r.cache;
589 return out;
590 }
591
592private:
593 /** A stage table widened to double, which is what the limits blend in. */
594 static Matrix<double> to_double(const Matrix<T>& A) {
595 Matrix<double> out(A.rows(), A.cols(), 0.0);
596 for (std::size_t i = 0; i < A.rows(); ++i)
597 for (std::size_t j = 0; j < A.cols(); ++j) out(i, j) = num_traits<T>::to_double(A(i, j));
598 return out;
599 }
600
601 /** The slow-environment limit: each stage on its own, blended by probEnv. */
602 EnvLimitSolution solve_dec() {
603 const std::size_t E = envObj.nstages();
604 EnvLimitSolution out;
605 out.method = "dec";
606 out.prob_env = envObj.prob_env;
607 out.QN = Matrix<double>(M, K, 0.0);
608 out.UN = Matrix<double>(M, K, 0.0);
609 out.TN = Matrix<double>(M, K, 0.0);
610 out.QStage.assign(E, Matrix<double>(M, K, 0.0));
611 out.UStage.assign(E, Matrix<double>(M, K, 0.0));
612 out.TStage.assign(E, Matrix<double>(M, K, 0.0));
613 CacheAccum acc;
614 for (std::size_t e = 0; e < E; ++e) {
615 const EnvStageAvg<T> s = stage_steady_state(envObj.stage(e).model);
616 const double p = envObj.prob_env[e];
617 acc.add(envObj.stage(e).model, s, p);
618 for (std::size_t i = 0; i < M; ++i)
619 for (std::size_t r = 0; r < K; ++r) {
620 out.QStage[e](i, r) = s.QN(i, r);
621 out.UStage[e](i, r) = s.UN(i, r);
622 out.TStage[e](i, r) = s.TN(i, r);
623 out.QN(i, r) += p * s.QN(i, r);
624 out.UN(i, r) += p * s.UN(i, r);
625 out.TN(i, r) += p * s.TN(i, r);
626 }
627 }
628 out.cache = acc.finish();
629 return out;
630 }
631
632 /**
633 * `solveEnvOccupation`: each stage of nonzero weight in steady state, as
634 * `dec` solves it, blended by the occupation weights. A NaN or Inf cell is
635 * an ABSENT metric (`local_absent_zero`) and adds nothing to the blend.
636 */
637 EnvLimitSolution solve_occupation() {
638 const std::size_t E = envObj.nstages();
639 EnvLimitSolution out;
640 out.method = "occupation";
641 stage_occupation(envObj, opt, out.occupation, out.occupation_weight);
642 out.prob_env = out.occupation_weight;
643 out.QN = Matrix<double>(M, K, 0.0);
644 out.UN = Matrix<double>(M, K, 0.0);
645 out.TN = Matrix<double>(M, K, 0.0);
646 out.QStage.assign(E, Matrix<double>(M, K, 0.0));
647 out.UStage.assign(E, Matrix<double>(M, K, 0.0));
648 out.TStage.assign(E, Matrix<double>(M, K, 0.0));
649 for (std::size_t e = 0; e < E; ++e) {
650 const double w = out.occupation_weight[e];
651 if (w == 0.0) continue;
652 const EnvStageAvg<T> s = lqn_ ? lqn_stage_steady_state(e) : stage_steady_state(envObj.stage(e).model);
653 for (std::size_t i = 0; i < M; ++i)
654 for (std::size_t r = 0; r < K; ++r) {
655 out.QStage[e](i, r) = s.QN(i, r);
656 out.UStage[e](i, r) = s.UN(i, r);
657 out.TStage[e](i, r) = s.TN(i, r);
658 if (std::isfinite(s.QN(i, r))) out.QN(i, r) += w * s.QN(i, r);
659 if (std::isfinite(s.UN(i, r))) out.UN(i, r) += w * s.UN(i, r);
660 if (std::isfinite(s.TN(i, r))) out.TN(i, r) += w * s.TN(i, r);
661 }
662 }
663 return out;
664 }
665
666 /**
667 * A LAYERED stage in steady state: its own `SolverLN` run to the fixed point
668 * and read over its LQN elements, as the reference reads `SolverLN.getAvg`
669 * (row i is element i+1). An undefined cell is NaN, an absent metric that
670 * `solve_occupation` leaves out of the blend.
671 */
672 EnvStageAvg<T> lqn_stage_steady_state(std::size_t e) const {
673 const lqn::LqnStruct<T>& model = envObj.stage(e).lqn_model;
674 if (model.nidx != M)
675 throw InputError(
676 "SolverENV occupation: stage " + std::to_string(e + 1) + " has " +
677 std::to_string(model.nidx) + " LQN elements where stage 1 has " + std::to_string(M) +
678 "; the layered stages are blended element by element, so they must have the same "
679 "hosts, tasks, entries and activities");
680 ln::SolverLN<T> s(model, opt.lqn);
681 const ln::LnSolution<T> r = s.get_ensemble_avg();
682 const double nan = std::numeric_limits<double>::quiet_NaN();
683 EnvStageAvg<T> out;
684 out.QN = Matrix<double>(M, 1, nan);
685 out.UN = Matrix<double>(M, 1, nan);
686 out.TN = Matrix<double>(M, 1, nan);
687 for (std::size_t i = 1; i <= M; ++i) {
688 if (r.defined_Q[i]) out.QN(i - 1, 0) = num_traits<T>::to_double(r.QN[i]);
689 if (r.defined_U[i]) out.UN(i - 1, 0) = num_traits<T>::to_double(r.UN[i]);
690 if (r.defined_T[i]) out.TN(i - 1, 0) = num_traits<T>::to_double(r.TN[i]);
691 }
692 return out;
693 }
694
695 /** The fast-environment limit: one rate-averaged model, solved once. */
696 EnvLimitSolution solve_avg() {
697 EnvLimitSolution out;
698 out.method = "avg";
699 out.prob_env = envObj.prob_env;
700 const qn::NetworkStruct<T> avg = rate_averaged_model();
701 const EnvStageAvg<T> s = stage_steady_state(avg);
702 // One model, so the weight is the arrival itself and the normalisation
703 // returns exactly what the single solve reported.
704 CacheAccum acc;
705 acc.add(avg, s, 1.0);
706 out.cache = acc.finish();
707 out.QN = Matrix<double>(M, K, 0.0);
708 out.UN = Matrix<double>(M, K, 0.0);
709 out.TN = Matrix<double>(M, K, 0.0);
710 for (std::size_t i = 0; i < M; ++i)
711 for (std::size_t r = 0; r < K; ++r) {
712 out.QN(i, r) = s.QN(i, r);
713 out.UN(i, r) = s.UN(i, r);
714 out.TN(i, r) = s.TN(i, r);
715 }
716 return out;
717 }
718
719 /**
720 * One stage, in steady state: the injected solver when there is one, else
721 * the fluid analyzer, in the form that also REPORTS ITS CACHE SPLIT when the
722 * stage holds a Cache.
723 *
724 * WHICH ENTRY POINT IS A MODEL QUESTION AND NOT A PREFERENCE, because the
725 * two resolve `default` differently and therefore answer with DIFFERENT
726 * CLOSURES. `solver_fluid` is `solver_fluid_analyzer.m`: it hands the method
727 * to `fluid_dispatch`, which takes `default` to the matrix method (to
728 * `closing` on a DPS model). `solver_fluid_run_analyzer` is the port of
729 * `runAnalyzer`'s resolution and re-resolves `default` through the
730 * minnormal/dae ladder. On a stage sitting at its saturation knee the two
731 * disagree outright -- N/(Z+D) = 1/D is a continuum of first-order
732 * equilibria, and which point a closure picks is the whole answer -- so
733 * routing every stage through the runner silently moved the limits off the
734 * matrix closure they are pinned against.
735 *
736 * A CACHE STAGE IS THE ONE THAT MUST TAKE THE RUNNER, and for the reference's
737 * own reason rather than for a reporting convenience: `runAnalyzer` resolves
738 * a Cache model to `rmf`, which lives in `fluid_cacheqn.h` and which
739 * `fluid_dispatch` refuses BY NAME (it cannot call it without a cyclic
740 * include). That route is also the one that reports the split, which is the
741 * silence this file used to refuse a cache model over.
742 */
743 EnvStageAvg<T> stage_steady_state(const qn::NetworkStruct<T>& sn) const {
744 if (stage_fn_) return stage_fn_(sn);
745 if (opt.stage_solver == "ctmc") return ctmc_stage_steady(sn, opt.stage_cutoff);
746 if (opt.stage_solver == "mva" || opt.stage_solver == "nc") {
747 // `SolverENV(env, @SolverMVA)` / `@SolverNC`: the stage's own getAvg,
748 // at the options the ensemble was built with, from no warm start.
749 const mva::AvgResult<T> r =
750 opt.stage_solver == "mva"
751 ? mva::solver_mva_run_analyzer<T>(sn, opt.stage_mva, Matrix<T>())
752 : nc::solver_nc_run_analyzer<T>(sn, opt.stage_nc);
753 EnvStageAvg<T> out;
754 out.QN = to_double(r.QN);
755 out.UN = to_double(r.UN);
756 out.TN = to_double(r.TN);
757 out.cache = r.cache;
758 return out;
759 }
760 fluid::FluidOptions fo = opt.stage;
761 // A limit starts from nothing carried over -- there is no entry state to
762 // seed with, which is the whole content of the approximation.
763 fo.init_sol.clear();
764 EnvStageAvg<T> out;
765 const fluid::FluidSolution s =
766 fluid::detail::fluid_has_cache(sn)
767 ? fluid::solver_fluid_run_analyzer<T>(sn, fo, nullptr, nullptr, &out.cache)
768 : fluid::solver_fluid<T>(sn, fo);
769 out.QN = s.QN;
770 out.UN = s.UN;
771 out.TN = s.TN;
772 return out;
773 }
774
775 /**
776 * `buildRateAveragedModel`: stage 1's network with every MODULATED rate
777 * replaced by its probEnv-weighted mean, as an exponential.
778 *
779 * The refresh chain is rerun because `rates` and everything derived from it
780 * are read off the service table; editing the table alone would leave the
781 * struct describing stage 1.
782 */
783 qn::NetworkStruct<T> rate_averaged_model() const {
784 const std::size_t E = envObj.nstages();
785 qn::NetworkStruct<T> sn = envObj.stage(0).model;
786 std::vector<double> r(E, 0.0);
787 for (std::size_t i = 0; i < M; ++i) {
788 const lang::NodeType nt = sn.stations[i].nodetype;
791 continue;
792 for (std::size_t k = 0; k < K; ++k) {
793 bool ok = true;
794 for (std::size_t e = 0; e < E && ok; ++e) {
795 const qn::NetworkStruct<T>& se = envObj.stage(e).model;
796 // The reference's `any(isnan(r)) || any(r<=0)`: a rate this
797 // port reports as disabled is MATLAB's NaN, and either way
798 // the pair is left as configured.
799 if (se.disabled[i][k]) {
800 ok = false;
801 break;
802 }
803 r[e] = num_traits<T>::to_double(se.rates(i, k));
804 if (!(r[e] > 0.0) || !std::isfinite(r[e])) ok = false;
805 }
806 if (!ok) continue;
807 double lo = r[0], hi = r[0], avg = 0.0;
808 for (std::size_t e = 0; e < E; ++e) {
809 lo = std::min(lo, r[e]);
810 hi = std::max(hi, r[e]);
811 avg += envObj.prob_env[e] * r[e];
812 }
813 // Not modulated: keep the original distribution, which may carry
814 // a shape the exponential of its mean would throw away.
815 if (hi - lo <= 1e-12 * std::max(1.0, hi)) continue;
816 sn.set_service(i + 1, k + 1,
817 lang::Distrib<T>::exp_rate(num_traits<T>::from_double(avg)));
818 }
819 }
820 sn.refresh_struct();
821 return sn;
822 }
823
824 Environment<T>& envObj;
825 EnvOptions opt;
826 EnvStageAvgFn<T> stage_fn_;
827 std::size_t M = 0, K = 0;
828 bool dec_ = false;
829 bool occ_ = false;
830 bool lqn_ = false; ///< `occupation` over layered stages
831};
832
833/** `solveEnvLimit` on the original stages. */
834template <class T>
836 SolverEnvLimit<T> s(e, o);
837 return s.solve();
838}
839
840/**
841 * `getStageOccupation`: method `occupation`'s per-stage expected occupation and
842 * blend weight. It initialises the environment and solves no stage, so it is
843 * the cheap reading; `solver_env_limit` reports the same two vectors.
844 */
845template <class T>
846void stage_occupation(Environment<T>& e, const EnvOptions& o, std::vector<double>& occ,
847 std::vector<double>& w) {
848 if (o.method != "occupation")
849 throw InputError("getStageOccupation is defined for SolverENV method 'occupation'.");
851 e.init();
853}
854
855/** `solveEnvLimit` with the CALLING solver running every stage. */
856template <class T>
858 EnvStageAvgFn<T> stage_fn) {
859 SolverEnvLimit<T> s(e, o, stage_fn);
860 return s.solve();
861}
862
863} // namespace env
864} // namespace line
865
866#endif // LINE_SOLVERS_ENV_SOLVER_ENV_LIMIT_H
What a solver observed about the Cache nodes of a model.
InputError(const std::string &what)
Definition error.h:105
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
The algorithm cannot produce a trustworthy answer on this instance: LINE:numerical:<kind>.
Definition error.h:110
UnsupportedError(const std::string &what)
Definition error.h:119
void init()
Port of Environment.init().
The RATE accumulator behind the cache blend, SolverENV.accumCacheMetric.
solvers::CacheMetrics< double > finish() const
Divide the accumulated rates by the accumulated arrivals, ONCE.
void add(const qn::NetworkStruct< T > &sn, const EnvStageAvg< T > &stage, double w)
One stage's contribution, weighted by its arrival into each cache.
SolverEnvLimit(Environment< T > &e, const EnvOptions &o, EnvStageAvgFn< T > stage_fn)
With an injected stage solver: stage_fn runs every stage, exactly as the reference calls its factory.
static void occupation_refusal(const Environment< T > &env, const EnvOptions &o)
occupationRefusal_: a finite horizon integrates exp(A t), which only an environment of exponential ar...
static double env_horizon(const EnvOptions &o)
envHorizon_: the length of SolverENV's timespan, Inf when unbounded.
SolverEnvLimit(Environment< T > &e, const EnvOptions &o)
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,...
static std::vector< double > env_init_prob(const EnvOptions &o, std::size_t E)
envInitProb_: options.config.env_init, default the first stage.
static void stage_occupation(const Environment< T > &env, const EnvOptions &o, std::vector< double > &occ, std::vector< double > &w)
stageOccupation_: the expected time spent in each stage over the horizon and the weight of each stage...
A network plus its refreshed NetworkStruct.
A random environment: a port of matlab/src/lang/Environment.m, restricted to what SolverENV reads out...
The exception types the port throws.
Matrix exponential by scaling and squaring with a diagonal Pade approximant.
The fluid solver's outermost entry point: @@SolverFLD/runAnalyzer.m's method resolution over solver_f...
Dense linear algebra over the templated number type: products, identity, inverse, and powers.
Dense matrix and non-owning view.
Matrix< T > sn_get_node_arvr_from_tput(const qn::NetworkStruct< T > &sn, const Matrix< T > &TN, const Matrix< T > &AN)
Port of sn_get_node_arvr_from_tput.
mva::AvgResult< T > solver_ctmc_run_analyzer(const NetworkStruct< T > &sn, const CtmcOptions &opt)
Solve and format in one call, for a caller with no use for the chain.
EnvLimitSolution solver_env_limit(Environment< T > &e, const EnvOptions &o)
solveEnvLimit on the original stages.
std::function< EnvStageAvg< T >(const qn::NetworkStruct< T > &)> EnvStageAvgFn
The stage solver, as a callable: the C++ spelling of the MATLAB function handle SolverENV(renv,...
void stage_occupation(Environment< T > &e, const EnvOptions &o, std::vector< double > &occ, std::vector< double > &w)
getStageOccupation: method occupation's per-stage expected occupation and blend weight.
FluidSolution solver_fluid(const qn::NetworkStruct< T > &sn_in, const FluidOptions &opt, qn::NetworkStruct< T > *sn_out=nullptr)
Port of solver_fluid_analyzer.m: dispatch on the method, refit the non-exponential FCFS stations the ...
FluidSolution solver_fluid_run_analyzer(const qn::NetworkStruct< T > &sn, const FluidOptions &opt, qn::NetworkStruct< T > *sn_out=nullptr, qn::NetworkStruct< T > *refreshed_out=nullptr, solvers::CacheMetrics< T > *cache_out=nullptr)
Port of @@SolverFLD/runAnalyzer.m: resolve the method, route to the function the reference routes to,...
const char * process_to_text(ProcessType p)
The MATLAB ProcessType name, as sn.procid prints it.
Definition lang_types.h:601
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:331
AvgResult< T > solver_mva_run_analyzer(const qn::NetworkStruct< T > &L, const MvaOptions &opt_in, const Matrix< T > &init_sol)
Port of @@SolverMVA/runAnalyzer.m for the lang='matlab' path: gate, solve, convert,...
mva::AvgResult< T > solver_nc_run_analyzer(const qn::NetworkStruct< T > &L_in, const NcSolverOptions &opt_in)
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
Matrix< T > inverse(const Matrix< T > &A)
Inverse by LU with one factorization and n back substitutions.
Definition linalg.h:72
Matrix< T > expm(const Matrix< T > &A)
Matrix exponential exp(A).
Definition expm.h:141
A queueing network and its refreshed NetworkStruct.
Number-type abstraction for the templated API port.
Ports of matlab/src/api/sn/sn_get_node_arvr_from_tput.m and sn_get_node_tput_from_tput....
Port of solver_ctmc_analyzer.m and the parts of @@SolverCTMC/runAnalyzer.m that surround one solve: t...
SolverENV: a queueing network in a random environment.
SolverFluid: the closing method, a port of solver_fluid.m, solver_fluid_iteration....
The SolverMVA class surface: @@SolverMVA/runAnalyzer.m and the gates around it.
The SolverNC class surface: @@SolverNC/runAnalyzer.m and the gates around it.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
The SolverCTMC knobs this port honours.
double cutoff
< 0 = not given
One arc of the environment process.
lang::Distrib< T > dist
the e -> h transition time
What a limit solve reports.
std::vector< Matrix< double > > UStage
std::vector< double > prob_env
The stage probabilities the blend used: probEnv, or the occupation weights.
Matrix< double > QN
Environment-averaged metrics, (nstations x nclasses).
std::vector< Matrix< double > > QStage
dec and occupation: each stage's own steady state, before the blend.
std::vector< double > occupation
occupation only (getStageOccupation): the expected time spent in each stage over the horizon,...
std::vector< Matrix< double > > TStage
std::vector< double > occupation_weight
solvers::CacheMetrics< double > cache
The environment-blended cache surface, one entry per Cache node.
std::string method
The limit that ran: avg, dec or occupation.
Options of SolverENV.
Definition solver_env.h:120
std::string method
The inter-stage coupling: meanfield is the reference's default.
Definition solver_env.h:143
std::vector< double > env_init
Method occupation: options.config.env_init, the initial probability of each stage.
Definition solver_env.h:167
double env_timespan_start
Method occupation: SolverENV's OWN options.timespan, [t0, t1].
Definition solver_env.h:161
What one stage solve reports back to the limits.
solvers::CacheMetrics< T > cache
static Distrib exp_rate(const T &r)
Definition lang_types.h:955
static constexpr double FineTol
Definition lang_types.h:765
static constexpr double CoarseTol
Definition lang_types.h:766
The metrics getAvg returns, after filtering.
Matrix< T > TN
throughput
Matrix< T > UN
utilization
Matrix< T > QN
queue length
solvers::CacheMetrics< T > cache
What the cache branches observed, EMPTY on a model with no Cache node and on every solver that does n...
Every Cache node of the model, in node order; empty on a model with none.
std::vector< CacheNodeMetrics< T > > caches
One Cache node's measured behaviour.
std::vector< double > itemcap
(h) capacity of each list
std::vector< T > hitprob
(K) TRUE hit fraction, EMPTY = not computed
std::vector< T > delayedprob
(K) delayed-hit fraction, EMPTY off a retrieval system
Matrix< T > hitproblist
(K x h) per-list hit fraction, EMPTY = not computed
std::size_t node
1-based node index of the Cache
std::string name
The Cache node's NAME, which is what a cross-language payload must key on.