LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_env_statevec.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_STATEVEC_H
6#define LINE_SOLVERS_ENV_SOLVER_ENV_STATEVEC_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * SolverENV, `method = "statevec"`: a port of
12 * `matlab/src/solvers/ENV/solver_env_statevec_analyzer.m`.
13 *
14 * WHAT IT DOES DIFFERENTLY FROM THE MEAN-FIELD COUPLING. `solver_env.h` carries
15 * only the MARGINAL MEAN queue lengths across an environment switch, so any
16 * correlation between stations at the moment of the switch is thrown away. This
17 * analyzer carries the whole JOINT distribution instead: each stage is an
18 * explicitly enumerated CTMC, and what crosses a switch is the full state
19 * probability vector. The two agree exactly when the marginal collapse happens
20 * to be lossless and diverge when it is not, which is the entire reason the
21 * reference keeps both.
22 *
23 * THE FIXED POINT, and it is the same shape as the mean-field one. For stage e
24 * with generator Q_e and entry distribution pi_enter[e], propagate the transient
25 * pi(t) = pi_enter[e] exp(Q_e t) over the stage's time span, then read off
26 * pi_exit[e][h] the distribution AT the e -> h switch, the expectation of
27 * pi(t) under the e -> h transition time,
28 * pi_timeavg[e] the distribution at the END of the sojourn, under the
29 * superposed holding time,
30 * and chain the entries as
31 * pi_enter[e] = sum_h prob_orig(h, e) reset_{h->e}( pi_exit[h][e] ),
32 * renormalized, iterated to an L1 fixed point. The blend at the end weights each
33 * stage's pi_timeavg by prob_env and maps it to means.
34 *
35 * THREE SOJOURN REGIMES, and only the third one integrates anything. When every
36 * outgoing transition of a stage is exponential the exit distribution equals the
37 * time-average and both are the RESOLVENT s pi_0 (sI - Q)^{-1}, a single linear
38 * solve with no quadrature -- so that regime stays exact even in rational
39 * arithmetic. A deterministic sojourn is uniformization at one point. Only a
40 * general phase-type sojourn needs the adaptive transient, and that branch is
41 * gated below.
42 *
43 * WHY THIS FILE DOES NOT DECODE MARGINALS ITSELF. There is a recorded defect
44 * here: `_kb/06-solver-catalog.md`, "ENV state-vector Util had drifted from the
45 * CTMC analyzer". MATLAB, Java and Python each grew a THIRD copy of the
46 * state-to-means reduction beside `solver_ctmc_analyzer`, all three drifted
47 * identically in the lld/cd branch, and no parity check saw it because they
48 * drifted together -- Util was overstated by 44% on a closed lld model. The
49 * reduction here is therefore `ctmc::solver_ctmc_avg_from_pi`, the same function
50 * the CTMC analyzer calls, invoked on the same `CtmcResult`. Nothing about the
51 * discipline, the load dependence or the loss guards is re-derived.
52 *
53 * THE ORACLE THAT NEEDS NO EXTERNAL REFERENCE, named in that same entry: an
54 * environment whose stages are IDENTICAL cannot change anything the network
55 * does, so this solver must reproduce the plain SolverCTMC solution of that one
56 * model. It does so EXACTLY here, not approximately, because the stationary
57 * distribution is a fixed point of all three sojourn regimes: pi Q = 0 makes the
58 * resolvent, the uniformized time-average and the transient all return pi
59 * unchanged, and `pre()` seeds from pi.
60 *
61 * WHAT IS PORTED, and what is refused by name:
62 * ported the CTMC backend, all three sojourn regimes, per-transition state
63 * reset policies, the cache hit/miss blend of `aggregateCacheBlend_`
64 * ported both stage backends -- the enumerated CTMC and the MAM/LDQBD one
65 * (`solver_mam_ldqbd` + `solver_mam_ldqbd_flatten`, reduced by
66 * `solver_mam_ldqbd_avg`) -- all three sojourn regimes, per-transition
67 * state reset policies, the cache hit/miss blend of
68 * `aggregateCacheBlend_`, with its exact delayed-hit split on a
69 * retrieval-aware cache (CTMC backend, `ctmc::cache_delayed_hit_rate`)
70 * refused an infinite inner-solver timespan
71 *
72 * THE TWO BACKENDS DIFFER IN WHAT A STATE IS, and everything downstream of that
73 * is shared. A CTMC stage's state is an enumerated row of `sn.space`; a MAM
74 * stage's is a (level, phase) pair of the flattened QBD, with no marginal to
75 * decode and no cache content in it. So the propagation, the fixed point and the
76 * reset policies are backend-agnostic -- they act on a probability vector and a
77 * generator -- while the two ENDS are not: `pre()` builds the generator
78 * differently and `finish()` reduces it differently
79 * (`solver_ctmc_avg_from_pi` against `solver_mam_ldqbd_avg`).
80 */
81
82#include <algorithm>
83#include <cmath>
84#include <cstddef>
85#include <functional>
86#include <limits>
87#include <map>
88#include <string>
89#include <vector>
90
97#include "line/num/number.h"
103#include "line/util/error.h"
104#include "line/util/lu.h"
105#include "line/util/matrix.h"
106
107namespace line {
108namespace env {
109
110/**
111 * `resetStateFun{h,e}` of the reference: the state distribution of stage h at
112 * the h -> e switch, mapped onto the state space of stage e.
113 *
114 * This is NOT `ResetMarginal`, which acts on the (nstations x nclasses) mean
115 * queue lengths the mean-field coupling carries. The two coexist because the
116 * couplings carry different objects, and a policy expressed on means has no
117 * canonical lift to a joint distribution.
118 */
119template <class T>
120using ResetStateVec = std::function<std::vector<T>(const std::vector<T>&)>;
121
122/** Options of the state-vector coupling. */
123template <class T>
125 int iter_max = 100;
126 double iter_tol = 1e-4;
127 /**
128 * Which solver runs each stage: `"ctmc"`, the enumerated chain, or `"mam"`,
129 * which flattens a level-dependent QBD into a generator over its
130 * level/phase states. The MAM backend only accepts what `solver_mam_ldqbd`
131 * accepts -- one class and either Delay+Queue or Source+Queue -- and it
132 * carries NO cache state, so the hit/miss blend is skipped there, as the
133 * reference skips it.
134 */
135 std::string stage_solver = "ctmc";
136 /**
137 * `cutoff` handed to `solver_mam_ldqbd` for an OPEN stage, which is where
138 * the level count comes from; a closed stage takes its population instead
139 * and ignores this.
140 */
141 std::size_t mam_cutoff = 10;
142 /** `options.sojourn`: `stochastic` (default) or `deterministic`. */
143 std::string sojourn = "stochastic";
144 /** Options handed to each stage's SolverCTMC. */
146 /**
147 * `options.timespan` of the inner solver. The end DEFAULTS TO INFINITY so
148 * that a caller who never set one is refused by name rather than served a
149 * silently invented horizon -- the reference's own check, and the reason
150 * `CTMC(model,'timespan',[0,T])` is spelled out in its error message.
151 *
152 * SET IT TO A FEW MEAN HOLDING TIMES, NOT TO A LARGE SAFE NUMBER. Only the
153 * general phase-type branch reads it, and that branch integrates with
154 * `ctmc_transient`, i.e. with ode23, whose maximum step is one tenth of the
155 * span. An explicit Runge-Kutta pair is unstable once that step exceeds the
156 * chain's fastest time scale, and the local error estimate CANNOT see it:
157 * near an equilibrium the estimate vanishes with the derivative, so every
158 * oversized step is accepted and roundoff is amplified instead. On the
159 * three-job closed model of `test_env_statevec.cpp` (max |Q_ii| = 4) a span
160 * of 5 reproduces the stationary law to the last bit while a span of 50
161 * drifts to an L1 error of 7e-4. The holding-time CDF has decayed long
162 * before the horizon anyway, so a longer one buys nothing.
163 */
164 double timespan_start = 0.0;
165 double timespan_end = std::numeric_limits<double>::infinity();
166 /** Per-stage override of `timespan_end`, or empty for the global one. */
167 std::vector<double> stage_timespan_end;
168 /** `resetStateFun[h][e]`; an empty entry, or an empty table, is identity. */
169 std::vector<std::vector<ResetStateVec<T>>> reset_state;
170};
171
172/** What the state-vector coupling reports. */
173template <class T>
175 /** Environment-averaged metrics, (nstations x nclasses). */
177 /** Per-stage, sojourn-averaged metrics. */
178 std::vector<Matrix<T>> QStage, UStage, TStage;
179 /** The entry distributions the fixed point converged to, per stage. */
180 std::vector<std::vector<T>> pi_enter;
181 /** The sojourn-end distributions the blend was taken over, per stage. */
182 std::vector<std::vector<T>> pi_timeavg;
183 /**
184 * `aggregateCacheBlend_`: the environment-blended cache surface. The
185 * reference writes it onto the stage-one node objects with
186 * `setResultHitProb`; a struct-level port has no node object to write to,
187 * so it is reported here instead, and a class the cache never serves stays
188 * NaN rather than reading as "never hits".
189 *
190 * KEYED BY NAME, in `CacheMetrics`' one shape rather than the node-index
191 * map this used to be: the mean-field coupling and the closed-form limits
192 * report the same surface, and indexing a cache result by position is what
193 * once made a host write it onto a Sink (see `cache_metrics.h`).
194 */
196 int iterations = 0;
197 bool converged = false;
198};
199
200/**
201 * The state-vector environment solver.
202 *
203 * `envObj` must already carry a model per stage; `init()` is called here, as
204 * `SolverENV.init` does.
205 */
206template <class T>
208public:
209 SolverEnvStatevec(Environment<T>& e, const EnvStatevecOptions<T>& o) : envObj(e), opt(o) {
210 init();
211 }
212
214 const std::size_t E = envObj.nstages();
216
217 pre();
218 int it = 0;
219 for (it = 1; it <= opt.iter_max; ++it) {
220 for (std::size_t e = 0; e < E; ++e) analyze(e);
221 post();
222 if (converged()) {
223 out.converged = true;
224 break;
225 }
226 }
227 out.iterations = std::min(it, opt.iter_max);
228 finish(out);
229 return out;
230 }
231
232private:
233 void init() {
234 mam_backend = (opt.stage_solver == "mam");
235 if (!mam_backend && opt.stage_solver != "ctmc")
236 throw UnsupportedError(ErrorKind{"Option"},
237 "SolverENV statevec: stage solver '" + opt.stage_solver +
238 "' is not available; the state-vector coupling needs an EXPLICIT generator over a "
239 "state space it can propagate a distribution across, which SolverCTMC exposes by "
240 "enumeration and SolverMAM by flattening its level-dependent QBD blocks");
241 if (opt.sojourn != "stochastic" && opt.sojourn != "deterministic")
242 throw InputError("SolverENV statevec: unknown sojourn '" + opt.sojourn + "'");
243
244 // A LAYERED STAGE HAS NO SINGLE GENERATOR to propagate a distribution
245 // across -- an LQN decomposes into one network per layer, and the joint
246 // law over the union of them is not what any layer solver produces. The
247 // reference refuses it in the same place and for the same reason
248 // (`@@SolverENV/SolverENV.m`, where `method` selects the state-vector
249 // analyzer), so this is its error and not a limit of this port.
250 envObj.reject_lqn_stages(
251 "SolverENV statevec",
252 "the state-vector coupling propagates a joint distribution over ONE stage "
253 "generator, which a layered model does not have -- it decomposes into a network "
254 "per layer");
255 envObj.refuse_semi_markov("statevec");
256 envObj.refuse_absorbing("statevec");
257 envObj.init();
258 const std::size_t E = envObj.nstages();
259 M = envObj.stage(0).model.nstations;
260 K = envObj.stage(0).model.nclasses;
261 for (std::size_t e = 1; e < E; ++e)
262 if (envObj.stage(e).model.nstations != M || envObj.stage(e).model.nclasses != K)
263 throw InputError(
264 "SolverENV statevec: every stage must have the same stations and classes; the "
265 "metrics are blended entrywise across them");
266
267 // The reference checks the horizon PER STAGE and names the stage that
268 // is missing one, because each stage carries its own inner solver.
269 tspan_end.assign(E, opt.timespan_end);
270 for (std::size_t e = 0; e < E; ++e) {
271 if (e < opt.stage_timespan_end.size()) tspan_end[e] = opt.stage_timespan_end[e];
272 if (!std::isfinite(tspan_end[e]) || !(tspan_end[e] > opt.timespan_start))
273 throw InputError(
274 "SolverENV statevec: the statevec analyzer requires a finite inner-solver "
275 "timespan for stage " +
276 std::to_string(e + 1) + ", e.g. CTMC(model,'timespan',[0,T])");
277 }
278
279 det_sojourn = (opt.sojourn == "deterministic");
280 stages.assign(E, ctmc::CtmcSolution<T>());
281 mam_ld.assign(E, mam::LdqbdBlocks<T>());
282 mam_flat.assign(E, mam::LdqbdFlat<T>());
283 pi_enter.assign(E, std::vector<T>());
284 pi_enter_prev.assign(E, std::vector<T>());
285 pi_exit.assign(E, std::vector<std::vector<T>>());
286 pi_timeavg.assign(E, std::vector<T>());
287 dvals.assign(E, 0.0);
288 if (det_sojourn)
289 for (std::size_t e = 0; e < E; ++e)
290 dvals[e] = std::max(envObj.hold_time_mean(e),
291 std::numeric_limits<double>::epsilon());
292 }
293
294 /**
295 * `pre_`: build each stage's chain once and warm-start its entry
296 * distribution from that stage's own stationary law, which is a valid
297 * probability vector over its state space whatever the environment does.
298 *
299 * THE CHAIN COMES FROM THE ANALYZER, not from a bare `solver_ctmc` call as
300 * the reference's `pre_` makes. The analyzer restricts a reducible generator
301 * to the weakly connected component of the model's initial state; solving
302 * the unrestricted generator instead spreads mass over states the model can
303 * never occupy. That restriction is also what makes the identical-stage
304 * oracle hold EXACTLY rather than up to the mass on unreachable states.
305 */
306 void pre() {
307 const std::size_t E = envObj.nstages();
308 for (std::size_t e = 0; e < E; ++e) {
309 if (mam_backend) {
310 // The MAM backend has no enumerated space: the stage is a
311 // level-dependent QBD, and flattening its blocks is what turns
312 // it into something a distribution can be propagated across.
313 // `Nlev` is finite by construction here -- the closed population,
314 // or the open truncation `cutoff` -- so the flat matrix exists.
315 mam::MamOptions mo;
316 mo.method = "default";
317 mo.cutoff = opt.mam_cutoff;
318 mam_ld[e] = mam::solver_mam_ldqbd(envObj.stage(e).model, mo).ld;
319 mam_flat[e] = mam::solver_mam_ldqbd_flatten(mam_ld[e]);
320 } else {
321 stages[e] = ctmc::solver_ctmc_analyzer(envObj.stage(e).model, opt.stage);
322 }
323 }
324 seed_entry_distributions();
325 pi_enter_prev = pi_enter;
326 }
327
328 /**
329 * Warm start. A stage's OWN stationary law is not a usable seed: a stage
330 * that is individually unstable or critical (arrival rate >= its own
331 * service rate) has no stationary law at all, and the reducible solver then
332 * returns the stationary law of the TRUNCATED generator, which piles mass
333 * against the truncation wall and whose mean grows linearly with the cutoff
334 * -- for a critical M/M/1 truncated at N it is uniform, with mean N/2.
335 *
336 * The fixed point chained in `post` is EXACT: it is the stationary equation
337 * of the joint (queue,stage) chain, phi_e = (sum_h phi_h q_he)(s_e I-Q_e)^-1.
338 * It does contract to the right answer from that seed, but the number of
339 * sweeps it needs grows with the cutoff, so at a finite `iter_max` the
340 * reported result drifts FURTHER from the truth as the cutoff is RAISED --
341 * the natural response to a suspect number makes it worse.
342 *
343 * Seed instead from the environment-averaged generator sum_e prob_env[e]*Q_e,
344 * positive recurrent exactly when the model is stable on average, which is
345 * the regime in which the answer exists at all; its stationary law is
346 * therefore cutoff-independent. Averaging needs one common state space, so
347 * when the stages differ in size fall back to the per-stage law, which is
348 * the best available and no worse than before.
349 */
350 void seed_entry_distributions() {
351 const std::size_t E = envObj.nstages();
352
353 // The per-stage law, which is what the seed falls back to and also what
354 // selects the recurrent class below. THE CHAIN COMES FROM THE ANALYZER,
355 // not from a bare solve: the analyzer restricts a reducible generator to
356 // the weakly connected component of the model's initial state AND uses
357 // that state to pick among the BSCCs that survive, so `stages[e].pi` is
358 // not reproducible from `gen(e)` alone. Re-deriving it here is what
359 // would spread mass over states the model can never occupy.
360 std::vector<std::vector<T>> own(E);
361 for (std::size_t e = 0; e < E; ++e) {
362 // The MAM backend has no analyzer law, and the REDUCIBLE solver:
363 // the flat generator of a truncated open QBD need not be irreducible.
364 own[e] = mam_backend ? normalized(mc::ctmc_solve_reducible(mam_flat[e].Q).pi)
365 : normalized(stages[e].pi);
366 }
367
368 bool sameSpace = E > 1;
369 for (std::size_t e = 1; e < E && sameSpace; ++e)
370 sameSpace = (state_count(e) == state_count(0));
371
372 std::vector<T> shared;
373 if (sameSpace) {
374 std::vector<double> w(E, 1.0 / static_cast<double>(E));
375 double wsum = 0.0;
376 bool usable = envObj.prob_env.size() == E;
377 for (std::size_t e = 0; e < E && usable; ++e) {
378 if (!std::isfinite(envObj.prob_env[e]) || envObj.prob_env[e] < 0.0) usable = false;
379 else wsum += envObj.prob_env[e];
380 }
381 // Stage probabilities unavailable or degenerate: weight stages equally.
382 if (usable && wsum > 0.0)
383 for (std::size_t e = 0; e < E; ++e) w[e] = envObj.prob_env[e] / wsum;
384
385 const std::size_t n = state_count(0);
386 Matrix<T> Qbar(n, n, num_traits<T>::from_double(0.0));
387 for (std::size_t e = 0; e < E; ++e) {
388 const Matrix<T>& Qe = gen(e);
389 const T we = num_traits<T>::from_double(w[e]);
390 for (std::size_t i = 0; i < n; ++i)
391 for (std::size_t j = 0; j < n; ++j) Qbar(i, j) += we * Qe(i, j);
392 }
393 // Seeded with the blended per-stage law so the recurrent class the
394 // ANALYZER chose is the one carried forward. This is also what keeps
395 // the identical-stage oracle EXACT: with equal stages Qbar == Q_0 and
396 // pi0 == pi, pi is already stationary for Q_0, so the solve returns
397 // it unchanged and the fixed point is still reached at sweep one.
398 std::vector<T> pi0(n, num_traits<T>::from_double(0.0));
399 for (std::size_t e = 0; e < E; ++e) {
400 const T we = num_traits<T>::from_double(w[e]);
401 for (std::size_t i = 0; i < n; ++i) pi0[i] += we * own[e][i];
402 }
403 shared = normalized(mc::ctmc_solve_reducible(Qbar, pi0).pi);
404 }
405
406 for (std::size_t e = 0; e < E; ++e) {
407 pi_enter[e] = shared.empty() ? own[e] : shared;
408 }
409 }
410
411 /** The stage's generator, whichever backend built it. */
412 const Matrix<T>& gen(std::size_t e) const {
413 return mam_backend ? mam_flat[e].Q : stages[e].chain.Q;
414 }
415
416 /** The size of the stage's state space, whichever backend built it. */
417 std::size_t state_count(std::size_t e) const {
418 return mam_backend ? mam_flat[e].levelOf.size() : stages[e].chain.space.size();
419 }
420
421 /**
422 * `analyze_`: propagate the entry distribution of stage e through its
423 * sojourn, recording where it lands at each destination and at the end.
424 */
425 void analyze(std::size_t e) {
426 const std::size_t E = envObj.nstages();
427 const Matrix<T>& Q = gen(e);
428 const std::vector<T> pi0 = pi_enter[e];
429
430 if (det_sojourn) {
431 // A deterministic sojourn has no CDF to integrate against: the exit
432 // is pi0 exp(Q d) and the blend is the exact time-average over
433 // [0, d], both from uniformization, and both the same toward every
434 // destination since the switch instant does not depend on where it
435 // goes.
436 std::vector<T> avg, ex;
437 time_average(pi0, Q, dvals[e], avg, ex);
438 pi_exit[e].assign(E, std::vector<T>());
439 for (std::size_t h = 0; h < E; ++h)
440 if (envObj.arc(e, h).enabled) pi_exit[e][h] = ex;
441 pi_timeavg[e] = avg;
442 return;
443 }
444
445 double s_e = 0.0;
446 if (exp_sojourn(e, s_e)) {
447 // Every outgoing transition exponential: the memorylessness makes
448 // the distribution at the switch equal to the time-average, and both
449 // are the resolvent s pi0 (sI - Q)^{-1}. One linear solve, no
450 // quadrature, and no dependence on the horizon at all.
451 const std::vector<T> res = resolvent(pi0, Q, s_e);
452 pi_exit[e].assign(E, std::vector<T>());
453 for (std::size_t h = 0; h < E; ++h)
454 if (envObj.arc(e, h).enabled) pi_exit[e][h] = res;
455 pi_timeavg[e] = res;
456 return;
457 }
458
459 // General phase-type sojourn: the transient on the integrator's own
460 // adaptive grid, averaged against the transition and holding-time CDFs.
461 if constexpr (!num_traits<T>::has_transcendental) {
462 throw UnsupportedError(ErrorKind{"Arithmetic"},
463 "SolverENV statevec: a non-exponential environment sojourn needs the transient "
464 "pi0 exp(Qt) from ctmc_transient, which is an adaptive approximation governed by "
465 "a tolerance and has no exact value in rational arithmetic; rerun with "
466 "--arith double, or use exponential transitions, whose resolvent is exact");
467 } else {
468 const mc::TransientResult<T> tr =
469 mc::ctmc_transient(Q, pi0, num_traits<T>::from_double(opt.timespan_start),
470 num_traits<T>::from_double(tspan_end[e]));
471 std::vector<double> t(tr.t.size());
472 for (std::size_t j = 0; j < tr.t.size(); ++j) t[j] = num_traits<T>::to_double(tr.t[j]);
473
474 pi_exit[e].assign(E, std::vector<T>());
475 for (std::size_t h = 0; h < E; ++h)
476 pi_exit[e][h] = stieltjes(tr.pi, cdf_weights(envObj.proc[e][h].map(), t));
477
478 const std::vector<T> avg =
479 stieltjes(tr.pi, cdf_weights(envObj.hold_time[e].map(), t));
480 if (!avg.empty()) {
481 pi_timeavg[e] = avg;
482 } else {
483 // Degenerate holding time on this grid: fall back to the
484 // terminal distribution, as the reference does.
485 const std::size_t last = tr.pi.rows() - 1, n = tr.pi.cols();
486 std::vector<T> tail(n);
487 for (std::size_t j = 0; j < n; ++j) tail[j] = tr.pi(last, j);
488 pi_timeavg[e] = tail;
489 }
490 }
491 }
492
493 /**
494 * `post_`: chain the entry distributions, carrying each stage's exit
495 * distributions into the stages they feed, weighted by prob_orig.
496 */
497 void post() {
498 const std::size_t E = envObj.nstages();
499 pi_enter_prev = pi_enter;
500 std::vector<std::vector<T>> next(E);
501
502 for (std::size_t e = 0; e < E; ++e) {
503 const std::size_t n = state_count(e);
504 std::vector<T> acc(n, num_traits<T>::from_int(0));
505 double wsum = 0.0;
506 for (std::size_t h = 0; h < E; ++h) {
507 const double po = envObj.prob_orig(h, e);
508 if (!(po > 0.0)) continue;
509 if (pi_exit[h].size() <= e || pi_exit[h][e].empty()) continue;
510 const std::vector<T> pex = reset_apply(h, e, pi_exit[h][e]);
511 if (pex.size() != n)
512 throw InputError(
513 "SolverENV statevec: reset_state[" + std::to_string(h) + "][" +
514 std::to_string(e) + "] returned a " + std::to_string(pex.size()) +
515 "-element vector but stage " + std::to_string(e + 1) + " has " +
516 std::to_string(n) +
517 " states; supply a reset that maps the state space of stage " +
518 std::to_string(h + 1) + " onto that of stage " + std::to_string(e + 1));
519 const T w = num_traits<T>::from_double(po);
520 for (std::size_t s = 0; s < n; ++s) acc[s] += T(w * pex[s]);
521 wsum += po;
522 }
523 if (wsum > 0.0) {
524 const T w = num_traits<T>::from_double(wsum);
525 for (std::size_t s = 0; s < n; ++s) acc[s] = T(acc[s] / w);
526 } else {
527 acc = pi_enter[e]; // no inflow this cycle: retain the estimate
528 }
529 next[e] = normalized(acc);
530 }
531 pi_enter = next;
532 }
533
534 /**
535 * `converged_`: the max L1 change of any entry distribution over a full
536 * cycle. It is the DISTRIBUTIONS that are compared and not the means,
537 * because two different joint laws can share every marginal mean.
538 */
539 bool converged() const {
540 const std::size_t E = envObj.nstages();
541 double l1 = 0.0;
542 for (std::size_t e = 0; e < E; ++e) {
543 const std::vector<T>& a = pi_enter[e];
544 const std::vector<T>& b = pi_enter_prev[e];
545 if (a.empty() || b.empty() || a.size() != b.size()) return false;
546 double d = 0.0;
547 for (std::size_t s = 0; s < a.size(); ++s)
548 d += std::fabs(num_traits<T>::to_double(a[s]) - num_traits<T>::to_double(b[s]));
549 l1 = std::max(l1, d);
550 }
551 if (!std::isfinite(l1)) return false;
552 return l1 < opt.iter_tol;
553 }
554
555 /**
556 * `finish_`: map each stage's sojourn-end distribution to means with the
557 * CTMC analyzer's own reduction, and blend by prob_env.
558 */
559 void finish(EnvStatevecSolution<T>& out) {
560 const std::size_t E = envObj.nstages();
561 const T zero = num_traits<T>::from_int(0);
562 out.QN = Matrix<T>(M, K, zero);
563 out.UN = Matrix<T>(M, K, zero);
564 out.TN = Matrix<T>(M, K, zero);
565 out.QStage.assign(E, Matrix<T>(M, K, zero));
566 out.UStage.assign(E, Matrix<T>(M, K, zero));
567 out.TStage.assign(E, Matrix<T>(M, K, zero));
568
569 for (std::size_t e = 0; e < E; ++e) {
570 if (pi_timeavg[e].empty()) continue;
571 Matrix<T> QNe, UNe, TNe;
572 if (mam_backend) {
573 // The LD-QBD reduction reports one column, the model being
574 // single-class by construction; K is 1 here for the same reason.
575 const mam::LdqbdAvg<T> a =
576 mam::solver_mam_ldqbd_avg(mam_ld[e], pi_timeavg[e], mam_flat[e].levelOf);
577 QNe = a.QN;
578 UNe = a.UN;
579 TNe = a.TN;
580 } else {
581 // stationary=false: pi_timeavg is a SOJOURN-AVERAGED TRANSIENT
582 // law, so the arrival-rate utilization reading does not balance
583 // the departure-rate one and max() of the two biases upward.
584 const ctmc::CtmcAvg<T> a = ctmc::solver_ctmc_avg_from_pi(
585 envObj.stage(e).model, stages[e].chain, pi_timeavg[e], false);
586 QNe = a.QN;
587 UNe = a.UN;
588 TNe = a.TN;
589 }
590 out.QStage[e] = QNe;
591 out.UStage[e] = UNe;
592 out.TStage[e] = TNe;
593 const T p = num_traits<T>::from_double(envObj.prob_env[e]);
594 for (std::size_t i = 0; i < M; ++i)
595 for (std::size_t r = 0; r < K && r < QNe.cols(); ++r) {
596 out.QN(i, r) += T(p * QNe(i, r));
597 out.UN(i, r) += T(p * UNe(i, r));
598 out.TN(i, r) += T(p * TNe(i, r));
599 }
600 }
601 out.pi_enter = pi_enter;
602 out.pi_timeavg = pi_timeavg;
603 cache_blend(out);
604 }
605
606 /**
607 * `aggregateCacheBlend_`: hit and miss probabilities as the ratio of the
608 * environment-blended hit and miss THROUGHPUTS.
609 *
610 * The ratio has to be taken after the blend and not before it: a per-stage
611 * ratio weighted by prob_env would be an average of ratios, which is not
612 * the ratio the environment actually exhibits unless every stage carries
613 * the same total rate.
614 */
615 void cache_blend(EnvStatevecSolution<T>& out) const {
616 // A MAM stage's state is a (level, phase) pair of the flattened QBD: it
617 // carries no cache content and no departure-rate table to read hit and
618 // miss throughputs off, so there is nothing to blend. The reference
619 // returns here for the same reason.
620 if (mam_backend) return;
621 const std::size_t E = envObj.nstages();
622 const qn::NetworkStruct<T>& sn1 = envObj.stage(0).model;
623 const T zero = num_traits<T>::from_int(0);
624
625 out.cache = solvers::cache_metrics_of(sn1, std::vector<T>(), std::vector<T>(),
626 std::vector<T>(), std::vector<T>(), Matrix<T>(),
627 Matrix<T>(), std::vector<T>());
628 for (std::size_t c = 0; c < out.cache.caches.size(); ++c) {
629 const std::size_t ind = out.cache.caches[c].node;
630 const std::size_t isf = sn1.stateful_index(ind);
631 if (isf == 0) continue;
632 std::vector<T> hitT(K, zero), missT(K, zero), dhitT(K, zero);
633 bool any_delayed = false;
634 for (std::size_t e = 0; e < E; ++e) {
635 if (pi_timeavg[e].empty()) continue;
636 const std::vector<T> pv = normalized(pi_timeavg[e]);
637 const qn::NetworkStruct<T>& sne = envObj.stage(e).model;
638 const auto it = sne.nodeparam.find(ind);
639 if (it == sne.nodeparam.end()) continue;
640 const qn::CacheParam<T>& np = it->second;
641 const T w = num_traits<T>::from_double(envObj.prob_env[e]);
642 const auto& dr = stages[e].chain.dep_rates;
643 // Delayed hits depart in the HIT class, so the hit-class departure rate is (true
644 // hits + delayed hits) and the exact transition reward splits it.
645 const std::vector<double> drate =
646 ctmc::analyzer_detail::cache_delayed_hit_rate(sne, ind, stages[e].chain, pv);
647 for (std::size_t k = 0; k < K && k < np.hitclass.size(); ++k) {
648 const std::size_t hc = np.hitclass[k];
649 const std::size_t mc = k < np.missclass.size() ? np.missclass[k] : 0;
650 if (hc == 0 || mc == 0) continue;
651 T hit_rate = zero, miss_rate = zero;
652 for (std::size_t s = 0; s < pv.size(); ++s) {
653 hit_rate += T(pv[s] * dr[s][isf - 1][hc - 1]);
654 miss_rate += T(pv[s] * dr[s][isf - 1][mc - 1]);
655 }
656 T dk = zero;
657 if (k < drate.size())
658 dk = num_traits<T>::from_double(
659 std::min(drate[k], num_traits<T>::to_double(hit_rate)));
660 if (num_traits<T>::to_double(dk) > 0) any_delayed = true;
661 hitT[k] += T(w * T(hit_rate - dk));
662 dhitT[k] += T(w * dk);
663 missT[k] += T(w * miss_rate);
664 }
665 }
666 const T nan = num_traits<T>::from_double(std::numeric_limits<double>::quiet_NaN());
667 std::vector<T> hp(K, nan), mp(K, nan), dp(K, nan);
668 for (std::size_t k = 0; k < K; ++k) {
669 const T tot = T(hitT[k] + missT[k] + dhitT[k]);
670 if (num_traits<T>::to_double(tot) > 0) {
671 hp[k] = T(hitT[k] / tot);
672 mp[k] = T(missT[k] / tot);
673 dp[k] = T(dhitT[k] / tot);
674 }
675 }
676 out.cache.caches[c].hitprob = hp;
677 out.cache.caches[c].missprob = mp;
678 if (any_delayed) out.cache.caches[c].delayedprob = dp;
679 }
680 }
681
682 // ---- the three sojourn regimes ---------------------------------------
683
684 /** True when every enabled transition out of e is exponential; `s` is their total rate. */
685 bool exp_sojourn(std::size_t e, double& s) const {
686 const std::size_t E = envObj.nstages();
687 s = 0.0;
688 bool any = false;
689 for (std::size_t h = 0; h < E; ++h) {
690 const EnvArc<T>& a = envObj.arc(e, h);
691 if (!a.enabled) continue;
692 if (a.dist.type != lang::ProcessType::EXP) return false;
693 s += num_traits<T>::to_double(a.dist.D1(0, 0));
694 any = true;
695 }
696 return any && s > 0.0;
697 }
698
699 /** `s pi0 (sI - Q)^{-1}`, solved as `(sI - Q)^T x = s pi0` on the columns. */
700 static std::vector<T> resolvent(const std::vector<T>& pi0, const Matrix<T>& Q, double s) {
701 const std::size_t n = Q.rows();
702 const T sT = num_traits<T>::from_double(s);
703 Matrix<T> A(n, n, num_traits<T>::from_int(0));
704 for (std::size_t i = 0; i < n; ++i)
705 for (std::size_t j = 0; j < n; ++j)
706 A(j, i) = T((i == j ? sT : num_traits<T>::from_int(0)) - Q(i, j));
707 std::vector<T> b(n);
708 for (std::size_t i = 0; i < n; ++i) b[i] = T(sT * pi0[i]);
709 return line::solve(A, b);
710 }
711
712 /** `ctmc_timeaverage` at the mean holding time, gated on transcendentals. */
713 static void time_average(const std::vector<T>& pi0, const Matrix<T>& Q, double d,
714 std::vector<T>& avg, std::vector<T>& ex) {
715 if constexpr (!num_traits<T>::has_transcendental) {
716 throw UnsupportedError(ErrorKind{"Arithmetic"},
717 "SolverENV statevec: a deterministic sojourn needs ctmc_timeaverage, whose "
718 "uniformization carries Poisson weights exp(-qt) that are not rational; rerun "
719 "with --arith double");
720 } else {
721 const mc::TimeAverageResult<T> r =
722 mc::ctmc_timeaverage(pi0, Q, num_traits<T>::from_double(d));
723 avg = r.piTimeAvg;
724 ex = r.piExit;
725 }
726 }
727
728 /**
729 * The Stieltjes weights of a transition over the transient grid, w_j =
730 * F(t_j) - F(t_{j-1}) with w_1 = 0.
731 *
732 * NO DENSITY IS INVOLVED, deliberately: the increments of the CDF are the
733 * measure the transition induces on this grid, and a deterministic
734 * transition has increments but no density.
735 */
736 static std::vector<double> cdf_weights(const mam::Map<double>& m,
737 const std::vector<double>& t) {
738 std::vector<double> w(t.size(), 0.0);
739 if (t.size() < 2) return w;
740 // A disabled arc is the 1 x 1 zero pair, whose CDF is identically zero.
741 bool all_zero = true;
742 for (std::size_t a = 0; a < m.D1.rows() && all_zero; ++a)
743 for (std::size_t b = 0; b < m.D1.cols() && all_zero; ++b)
744 if (m.D1(a, b) != 0.0) all_zero = false;
745 if (all_zero) return w;
746 const std::vector<double> F = mam::map_cdf(m, t);
747 for (std::size_t j = 1; j < t.size(); ++j) w[j] = F[j] - F[j - 1];
748 return w;
749 }
750
751 /** `(w' * pit) / sum(w)`; empty when the weights carry no mass or a NaN. */
752 static std::vector<T> stieltjes(const Matrix<T>& pit, const std::vector<double>& w) {
753 double sw = 0.0;
754 for (double v : w) {
755 if (std::isnan(v)) return std::vector<T>();
756 sw += v;
757 }
758 if (!(sw > 0.0)) return std::vector<T>();
759 const std::size_t n = pit.cols();
760 std::vector<T> out(n, num_traits<T>::from_int(0));
761 for (std::size_t j = 0; j < w.size() && j < pit.rows(); ++j) {
762 if (w[j] == 0.0) continue;
763 const T wj = num_traits<T>::from_double(w[j] / sw);
764 for (std::size_t s = 0; s < n; ++s) out[s] += T(wj * pit(j, s));
765 }
766 return out;
767 }
768
769 // ---- small helpers ----------------------------------------------------
770
771 std::vector<T> reset_apply(std::size_t h, std::size_t e, const std::vector<T>& p) const {
772 if (h < opt.reset_state.size() && e < opt.reset_state[h].size() && opt.reset_state[h][e])
773 return opt.reset_state[h][e](p);
774 return p;
775 }
776
777 /** Clamp the numerical dust below zero and renormalize to a distribution. */
778 static std::vector<T> normalized(const std::vector<T>& p) {
779 const T zero = num_traits<T>::from_int(0);
780 std::vector<T> q = p;
781 T tot = zero;
782 for (std::size_t s = 0; s < q.size(); ++s) {
783 if (num_traits<T>::to_double(q[s]) < 0) q[s] = zero;
784 tot += q[s];
785 }
786 if (num_traits<T>::to_double(tot) > 0)
787 for (std::size_t s = 0; s < q.size(); ++s) q[s] = T(q[s] / tot);
788 return q;
789 }
790
791 Environment<T>& envObj;
792 EnvStatevecOptions<T> opt;
793 std::size_t M = 0, K = 0;
794 bool det_sojourn = false;
795 std::vector<double> dvals, tspan_end;
796 std::vector<ctmc::CtmcSolution<T>> stages;
797 bool mam_backend = false;
798 std::vector<mam::LdqbdBlocks<T>> mam_ld;
799 std::vector<mam::LdqbdFlat<T>> mam_flat;
800 std::vector<std::vector<T>> pi_enter, pi_enter_prev, pi_timeavg;
801 std::vector<std::vector<std::vector<T>>> pi_exit; ///< pi_exit[e][h]
802};
803
804/** Solve in one call, for a caller with no use for the solver object. */
805template <class T>
809
810} // namespace env
811} // namespace line
812
813#endif // LINE_SOLVERS_ENV_SOLVER_ENV_STATEVEC_H
What a solver observed about the Cache nodes of a model.
InputError(const std::string &what)
Definition error.h:105
UnsupportedError(const std::string &what)
Definition error.h:119
SolverEnvStatevec(Environment< T > &e, const EnvStatevecOptions< T > &o)
EnvStatevecSolution< T > solve()
Limiting distribution of a CTMC whose generator may be reducible.
Transient distribution of a CTMC over a time interval, by integrating the forward equations d pi/dt =...
Transient distribution of a CTMC by uniformization (Jensen's method), and the time-averaged distribut...
A random environment: a port of matlab/src/lang/Environment.m, restricted to what SolverENV reads out...
The exception types the port throws.
LU factorization with partial pivoting, templated on the number type.
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.
CtmcAvg< T > solver_ctmc_avg_from_pi(const NetworkStruct< T > &sn, const CtmcResult< T > &r, const std::vector< T > &pivec, bool stationary=true)
Port of solver_ctmc_avg_from_pi: map a state distribution to mean metrics.
CtmcSolution< T > solver_ctmc_analyzer(const NetworkStruct< T > &sn_in, const CtmcOptions &opt)
Port of solver_ctmc_analyzer.m plus the fork-join wrapper of @@SolverCTMC/runAnalyzer....
EnvStatevecSolution< T > solver_env_statevec(Environment< T > &e, const EnvStatevecOptions< T > &o)
Solve in one call, for a caller with no use for the solver object.
std::function< std::vector< T >(const std::vector< T > &)> ResetStateVec
resetStateFun{h,e} of the reference: the state distribution of stage h at the h -> e switch,...
double tot(const std::vector< double > &pops)
LdqbdSolution< T > solver_mam_ldqbd(const qn::NetworkStruct< T > &L, const MamOptions &opt)
Port of solver_mam_ldqbd.m.
LdqbdFlat< T > solver_mam_ldqbd_flatten(const LdqbdBlocks< T > &ld)
Port of solver_mam_ldqbd_flatten.m.
std::vector< T > map_cdf(const Map< T > &m, const std::vector< T > &points)
Cumulative distribution of the inter-arrival time at the given points.
Definition map_cdf.h:63
LdqbdAvg< T > solver_mam_ldqbd_avg(const LdqbdBlocks< T > &ld, const std::vector< T > &piflat_in, const std::vector< std::size_t > &levelOf)
Port of solver_mam_ldqbd_avg.m: map a distribution over the flat state space to means.
ReducibleResult< T > ctmc_solve_reducible(const Matrix< T > &Q, const std::vector< T > &pi0, double zeroColTol=1e-12)
Limiting distribution of a CTMC whose generator may be reducible.
TimeAverageResult< T > ctmc_timeaverage(const std::vector< T > &pi0, const Matrix< T > &Q, const T &t, double tol=1e-12, long maxiter=-1)
Time-averaged distribution (1/t) int_0^t pi(u) du, plus pi(t) itself.
TransientResult< T > ctmc_transient(const Matrix< T > &Q, const std::vector< T > &pi0, const T &t0, const T &t1, double rtol=1e-3, double atol=1e-6, bool stiff=false)
Transient distribution of a CTMC over a time interval, by integrating the forward equations d pi/dt =...
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
std::vector< T > solve(const Matrix< T > &A, const std::vector< T > &b)
Convenience: solve Ax = b, leaving A and b untouched.
Definition lu.h:158
A queueing network and its refreshed NetworkStruct.
Number-type abstraction for the templated API port.
Port of solver_ctmc.m: the infinitesimal generator of a queueing network, assembled from the enumerat...
Port of solver_ctmc_analyzer.m and the parts of @@SolverCTMC/runAnalyzer.m that surround one solve: t...
The two reductions that let an LD-QBD stand in for an enumerated CTMC.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
The SolverCTMC knobs this port honours.
Options of the state-vector coupling.
std::string sojourn
options.sojourn: stochastic (default) or deterministic.
ctmc::CtmcOptions stage
Options handed to each stage's SolverCTMC.
std::vector< double > stage_timespan_end
Per-stage override of timespan_end, or empty for the global one.
std::vector< std::vector< ResetStateVec< T > > > reset_state
resetStateFun[h][e]; an empty entry, or an empty table, is identity.
std::string stage_solver
Which solver runs each stage: "ctmc", the enumerated chain, or "mam", which flattens a level-dependen...
double timespan_start
options.timespan of the inner solver.
std::size_t mam_cutoff
cutoff handed to solver_mam_ldqbd for an OPEN stage, which is where the level count comes from; a clo...
What the state-vector coupling reports.
std::vector< Matrix< T > > QStage
Per-stage, sojourn-averaged metrics.
std::vector< Matrix< T > > UStage
solvers::CacheMetrics< T > cache
aggregateCacheBlend_: the environment-blended cache surface.
std::vector< Matrix< T > > TStage
Matrix< T > QN
Environment-averaged metrics, (nstations x nclasses).
std::vector< std::vector< T > > pi_enter
The entry distributions the fixed point converged to, per stage.
std::vector< std::vector< T > > pi_timeavg
The sojourn-end distributions the blend was taken over, per stage.
Every Cache node of the model, in node order; empty on a model with none.