LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_env.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_H
6#define LINE_SOLVERS_ENV_SOLVER_ENV_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * SolverENV: a queueing network in a random environment.
12 *
13 * Port of `matlab/src/solvers/@@SolverENV/SolverENV.m` driving
14 * `solver_env_meanfield_analyzer.m`, the default (and historical) coupling.
15 *
16 * THE FIXED POINT, in one paragraph. Each stage is solved TRANSIENTLY, not in
17 * steady state, because a stage does not last long enough to reach one -- the
18 * environment switches first. What the network carries across a switch is its
19 * queue lengths, so stage e must be started from the mean queue lengths it is
20 * ENTERED with, and those are the exit queue lengths of whichever stage
21 * preceded it. That is a fixed point over the entry vectors, and the iteration
22 * below is a Picard iteration on it:
23 *
24 * Qentry[e] = sum_h probOrig(h, e) * reset_{h->e}( Qexit[h][e] )
25 *
26 * where Qexit[h][e] is the transient of stage h AVERAGED OVER WHEN the h -> e
27 * switch happens -- the increments of that transition's CDF are the weights.
28 * The environment-averaged answer at the end weights each stage's transient
29 * over its own HOLDING time CDF instead (any exit, not a particular one), and
30 * blends the stages by their stationary probabilities probEnv.
31 *
32 * WHY THE WEIGHTS ARE CDF INCREMENTS AND NOT A DENSITY. The transient comes
33 * back on a grid, so the reference integrates the metric against the measure
34 * the transition induces on that same grid: w_j = F(t_j) - F(t_{j-1}), with
35 * w_1 = 0, normalized by their sum. That is a Riemann-Stieltjes sum, and it
36 * needs no density -- which matters, because a deterministic transition has
37 * none.
38 *
39 * THE MEAN-FIELD COLLAPSE is the approximation, and it is worth naming: only
40 * the MARGINAL MEAN queue lengths cross a switch, so any correlation between
41 * stations at the moment of the switch is discarded.
42 * `solver_env_statevec_analyzer.m` carries the whole joint distribution
43 * instead; it is ported in `solver_env_statevec.h`, as a separate class with a
44 * CTMC stage solver, and `env_dispatch.h` chooses between the two the way
45 * `SolverENV.init` does. `method = "statevec"` is refused HERE by name rather
46 * than silently served by the mean-field coupling.
47 *
48 * A LAYERED STAGE IS THE SAME FIXED POINT WITH A DIFFERENT STAGE SOLVER. When
49 * a stage holds a `lqn::LqnStruct` rather than a flat network, its transient
50 * comes from a `SolverLN` over the stage's own layers instead of from one fluid
51 * integration, and the (station, class) view the coupling blends over is the
52 * BLOCK-DIAGONAL UNION of those layers -- `LayeredNetwork.layerBlocks` in the
53 * reference, `SolverLN::layer_blocks` here. Nothing else changes: the same
54 * marginal mean queue lengths cross a switch, split into per-layer blocks on
55 * the way in (`SolverLN::init_from_marginal`) and reassembled on the way out.
56 * The one thing to know about the handoff is that it must be replayed AFTER the
57 * layered fixed point and BEFORE the layer transients, because the fixed point
58 * resets its layers as it converges; `run_layer_transient` is where that
59 * happens, and a warm start installed anywhere earlier is silently inert.
60 *
61 * WHAT IS PORTED, and what is refused by name:
62 * ported `meanfield` (the default), `smp` (the mean-field analyzer over the
63 * semi-Markov stage probabilities -- see `smp_stage_probabilities`), `statedep` (state-dependent environment rates,
64 * `resetEnvRates`), stochastic and deterministic sojourn,
65 * per-transition reset policies including the named `keep`/`clear`
66 * of a node breakdown, fluid stages, CTMC stages, and LayeredNetwork
67 * stages over fluid layers
68 * refused `statevec`/`blend` (this class is the mean-field coupling; use
69 * env::solver_env for them), `avg`/`dec` (the closed-form fast/slow
70 * limits of `solveEnvLimit`, ported as env::SolverEnvLimit and
71 * likewise reached through env::solver_env), and the cache
72 * aggregation of `aggregateCacheMeanfield_`.
73 */
74
75#include <algorithm>
76#include <cmath>
77#include <cstddef>
78#include <limits>
79#include <memory>
80#include <string>
81#include <vector>
82
93#include "line/util/error.h"
94#include "line/util/matrix.h"
95
96namespace line {
97namespace env {
98
99/**
100 * `LnOptions` as a LAYERED environment stage is solved with.
101 *
102 * Only the layer engine differs from the SolverLN default, and it differs
103 * because the mean-field coupling has no use for a stage it cannot integrate:
104 * see `EnvOptions::lqn`.
105 */
108 o.layer_solver = "fluid";
109 return o;
110}
111
112/** Options of SolverENV. Defaults are `Solver.defaultOptions`. */
114 int iter_max = 100;
115 double iter_tol = 1e-4;
116 /**
117 * Which solver runs each FLAT stage: the fluid transient or the enumerated
118 * CTMC. A LAYERED stage is run by SolverLN whatever this says, and its own
119 * layer engine is named by `lqn.layer_solver`; asking for `ctmc` alongside a
120 * layered stage is refused rather than reinterpreted, since an LQN has no
121 * single generator to enumerate.
122 */
123 std::string stage_solver = "fluid";
124 /** The inter-stage coupling: `meanfield` is the reference's default. */
125 std::string method = "meanfield";
126 /** `options.sojourn`: `stochastic` (default) or `deterministic`. */
127 std::string sojourn = "stochastic";
128 /** Options handed to each stage solver. */
130 /**
131 * `options.cutoff` of a CTMC stage, read only when `stage_solver` is ctmc.
132 * An open stage needs one; a closed one enumerates its own population.
133 */
134 double stage_cutoff = -1.0;
135 /** `options.timespan(2)` of the inner solver: the transient horizon. */
136 double timespan_end = 100.0;
137 /**
138 * Points on a UNIFORM transient grid, used only where `stage_grid` declines
139 * to build one (a stage whose holding time is the disabled 1 x 1 zero pair).
140 *
141 * It used to be the accuracy knob of the whole method, because the exit
142 * metrics are a Riemann-Stieltjes sum over whatever grid the stage was
143 * integrated on and a uniform grid resolves the HORIZON rather than the
144 * sojourn. Since 2026-08-11 each stage is integrated on the grid the
145 * sojourn asks for -- 90% of the points under `5*E[S]` -- so the answer no
146 * longer moves with this: on renv_node_breakdown the horizon may be 100 or
147 * 1000 and Server QLen is 0.458854 or 0.459191.
148 */
149 std::size_t tran_points = 1001;
150 /**
151 * Options of the `SolverLN` that runs a LAYERED stage.
152 *
153 * The reference names the stage solver by handing SolverENV a FACTORY --
154 * `ENV(env, @(m) LN(m, @(mm) FLD(mm), 'timespan', [0 T]))` -- and this field
155 * is that factory's argument list, because a C++ template cannot take a
156 * MATLAB function handle. `timespan_end`, `tran_points` and `tran_grid` are
157 * OVERWRITTEN by the environment for every stage: the horizon and the grid
158 * the exit average is summed on belong to the coupling, not to a stage.
159 *
160 * `layer_solver` defaults to `fluid` here where `LnOptions` alone defaults
161 * to `mva`, and the difference is not a preference: the coupling carries
162 * queue lengths across a switch and therefore needs each stage's
163 * TRANSIENT, which only the fluid layer engine produces. A caller who names
164 * another engine is refused by `init`, not quietly served the fluid one.
165 */
167};
168
169/** What SolverENV reports. */
171 /** Environment-averaged metrics, (nstations x nclasses). */
173 /** Per-stage, sojourn-averaged metrics. */
174 std::vector<Matrix<double>> QExit, UExit, TExit;
175 /** The entry queue lengths the fixed point converged to. */
176 std::vector<Matrix<double>> Qentry;
177 /**
178 * `meancov` only: the environment-wide queue-length COVARIANCE over the
179 * (station, class) pairs, (M*K)-by-(M*K) and indexed `ir = r*M + i`, and its
180 * diagonal reshaped to (nstations x nclasses). Empty under every other
181 * coupling, which carries a first moment alone.
182 */
184 int iterations = 0;
185 bool converged = false;
186};
187
188namespace detail {
189
190/**
191 * `maxpe(approx, exact)`: max |1 - approx/exact| over the entries where exact
192 * is nonzero, which is what the reference's convergence test compares.
193 */
194inline double env_maxpe(const std::vector<double>& approx, const std::vector<double>& exact) {
195 double worst = -1.0;
196 for (std::size_t i = 0; i < approx.size() && i < exact.size(); ++i) {
197 if (exact[i] == 0.0) continue;
198 const double e = std::fabs(1.0 - approx[i] / exact[i]);
199 if (e > worst) worst = e;
200 }
201 return worst; // negative means "no comparable entry", the reference's empty
202}
203
204/** Linear interpolation of a transient metric at `d`, clamped to the grid. */
205inline double env_det_eval(const std::vector<double>& t, const std::vector<double>& metric,
206 double d) {
207 if (t.empty()) return 0.0;
208 d = std::max(t.front(), std::min(d, t.back()));
209 for (std::size_t j = 1; j < t.size(); ++j) {
210 if (d <= t[j]) {
211 const double dt = t[j] - t[j - 1];
212 if (!(dt > 0.0)) return metric[j];
213 const double a = (d - t[j - 1]) / dt;
214 return metric[j - 1] * (1.0 - a) + metric[j] * a;
215 }
216 }
217 return metric.back();
218}
219
220} // namespace detail
221
222/**
223 * The environment solver.
224 *
225 * `envObj` must already carry a model per stage; `init()` is called here, as
226 * `SolverENV.init` does.
227 */
228template <class T>
230public:
231 SolverEnv(Environment<T>& e, const EnvOptions& o) : envObj(e), opt(o) { init(); }
232
234 const std::size_t E = envObj.nstages();
235 EnvSolution out;
236 out.QN = Matrix<double>(M, K, 0.0);
237 out.UN = Matrix<double>(M, K, 0.0);
238 out.TN = Matrix<double>(M, K, 0.0);
239
240 pre();
241 std::vector<std::vector<double>> qfirst_prev(E), qfirst_curr(E);
242 int it = 0;
243 for (it = 1; it <= opt.iter_max; ++it) {
244 for (std::size_t e = 0; e < E; ++e) analyze(e);
245 // The reference's convergence test compares the FIRST point of the
246 // transient -- the entry queue length -- across iterations.
247 qfirst_prev = qfirst_curr;
248 qfirst_curr.assign(E, std::vector<double>());
249 for (std::size_t e = 0; e < E; ++e) {
250 qfirst_curr[e].reserve(M * K);
251 for (std::size_t i = 0; i < M; ++i)
252 for (std::size_t r = 0; r < K; ++r) qfirst_curr[e].push_back(tranQ[e][i][r][0]);
253 }
254 bool conv = it > 1;
255 if (conv)
256 for (std::size_t e = 0; e < E && conv; ++e) {
257 const double d = detail::env_maxpe(qfirst_curr[e], qfirst_prev[e]);
258 if (d < 0.0) continue; // nothing comparable, treated as converged
259 if (!std::isfinite(d) || d >= opt.iter_tol) conv = false;
260 }
261 post();
262 if (conv) {
263 out.converged = true;
264 break;
265 }
266 }
267 out.iterations = std::min(it, opt.iter_max);
268 finish(out);
269 return out;
270 }
271
272private:
273 void init() {
274 if (opt.stage_solver != "fluid" && opt.stage_solver != "ctmc")
275 throw UnsupportedError(
276 "SolverENV: stage solver '" + opt.stage_solver +
277 "' is not available; the environment coupling needs a TRANSIENT stage solve and "
278 "only the fluid analyzer and the enumerated CTMC provide one in this port");
279 ctmc_stages = (opt.stage_solver == "ctmc");
280 if (opt.method == "statevec")
281 throw UnsupportedError(
282 "SolverENV: the state-vector analyzer (solver_env_statevec_analyzer.m) carries the "
283 "full joint distribution across a switch, which THIS class does not: it is the "
284 "mean-field coupling and carries the marginal means. The state-vector coupling is "
285 "ported as env::SolverEnvStatevec, with its own options and its CTMC stage solver; "
286 "reach it through env::solver_env (env_dispatch.h), which is the analyzer "
287 "selection of SolverENV.init");
288 if (opt.method == "avg" || opt.method == "dec")
289 throw UnsupportedError(
290 "SolverENV: the closed-form fast/slow environment limits (SolverENV.solveEnvLimit) "
291 "replace this fixed point rather than configure it -- neither carries anything "
292 "across a switch. They are ported as env::SolverEnvLimit, with their own result "
293 "type; reach them through env::solver_env (env_dispatch.h), which is the method "
294 "dispatch of SolverENV.runAnalyzer");
295 // `smp` selects no analyzer of its own: it runs the mean-field one after
296 // `smp_stage_probabilities` recomputes prob_env and prob_orig from the
297 // embedded jump chain of the arc CDFs, as the JAR's SolverENV.init does.
298 statedep = (opt.method == "statedep");
299 // "default" IS the reference's spelling of this coupling: SolverENV.m
300 // installs solver_env_meanfield_analyzer for every method that is not
301 // statevec, and its listValidMethods leads with 'default'. Only the CLI
302 // normalised it away, so a caller constructing SolverEnv directly with
303 // the reference's own name was refused. "blend" is a third spelling of
304 // it since 2026-09-13, when that name was repointed here from the
305 // state-vector coupling, and this whitelist is why it has to be named:
306 // falling past the statevec branch reaches the mean-field analyzer, but
307 // an unlisted name is refused before it gets there.
308 if (opt.method != "meanfield" && opt.method != "default" && opt.method != "mean"
309 && opt.method != "meancov" && opt.method != "blend" && opt.method != "blending"
310 && opt.method != "smp"
311 && !statedep)
312 throw UnsupportedError("SolverENV: unknown method '" + opt.method + "'");
313 // 'meancov' IS THIS COUPLING, carrying a COVARIANCE beside the mean: the
314 // propagation is the mean-field one and only the object crossing a switch
315 // is richer, so it is a flag here rather than an analyzer of its own.
316 meancov = (opt.method == "meancov");
317 // THE STAGE METHOD IS THE CALLER'S, as it is in the reference, where a
318 // stage is an ordinary SolverFluid the caller constructed: `kp` integrates
319 // the Ko-Pender fluid AND diffusion limits, and is the only route in this
320 // port that carries a second moment along a stage trajectory. Every other
321 // fluid method integrates the closing drift, which carries a first moment
322 // alone; `meancov` over such stages keeps the TIMING variance of the
323 // sojourn and the spread of the per-origin means, and nothing of the
324 // within-stage spread, exactly as the reference does for a stage solver
325 // that reports none.
326 {
327 std::string sm = opt.stage.method;
328 if (sm.size() > 4 && sm.compare(0, 4, "fld.") == 0) sm = sm.substr(4);
329 kp_stages = (!ctmc_stages && sm == "kp");
330 // `dae` is the OTHER route that carries a second moment along a stage
331 // trajectory: it integrates the linear-noise covariance beside the
332 // min-normal mean, and unlike `kp` its DRIFT reads that covariance, so
333 // a stage restarting at Sigma(0) = 0 gets the mean wrong as well as
334 // the variance. It needs a stage route of its own: `solver_fluid_transient`
335 // integrates the CLOSING drift whatever the method name says (only
336 // `solver_fluid_run_transient` dispatches, and it takes no output grid),
337 // so a `dae` stage sent there would be answered by a different method,
338 // report no covariance at all, and drop `init_qcov` in silence.
339 dae_stages = (!ctmc_stages && !kp_stages && sm == "dae");
340 }
341 // A CTMC STAGE IS NOT REFUSED under `meancov`: it takes a lattice STATE
342 // rather than a distribution, so the carried covariance simply does not
343 // seed it, which is what the reference does too. The coupling then keeps
344 // the timing variance of the sojourn and the spread of the per-origin
345 // means, and reports both.
346 if (!(opt.timespan_end > 0.0) || !std::isfinite(opt.timespan_end))
347 throw InputError(
348 "SolverENV: the stage transient needs a finite positive horizon, "
349 "options.timespan(2); the mean-field coupling integrates each stage's "
350 "TRAJECTORY against the holding-time CDF and has nothing to integrate over an "
351 "infinite one");
352 if (opt.tran_points < 2)
353 throw InputError("SolverENV: the transient grid needs at least two points");
354 if (!std::is_same<T, double>::value)
355 throw UnsupportedError(
356 "SolverENV: a fluid stage integrates its drift with LSODA, which is double only; "
357 "rerun with --arith double");
358
359 envObj.init();
360 if (opt.method == "smp") smp_stage_probabilities();
361 const std::size_t E = envObj.nstages();
362 build_lqn_stages();
363 M = stage_M(0);
364 K = stage_K(0);
365 for (std::size_t e = 1; e < E; ++e)
366 if (stage_M(e) != M || stage_K(e) != K)
367 throw InputError(
368 "SolverENV: every stage must have the same stations and classes; the metrics "
369 "are blended entrywise across them. A LAYERED stage counts the block-diagonal "
370 "union of the layers SolverLN builds for it, which is what layerBlocks "
371 "reports, so two layered stages agree exactly when they have the same layer "
372 "shape -- not merely the same LQN element count");
373 entry.assign(E, Matrix<double>(M, K, 0.0));
374 centry.assign(E, Matrix<double>(M * K, M * K, 0.0));
375 tranC.assign(E, std::vector<Matrix<double>>());
376 tranT.assign(E, std::vector<double>());
377 tranQ.assign(E, std::vector<std::vector<std::vector<double>>>());
378 tranU.assign(E, std::vector<std::vector<std::vector<double>>>());
379 tranTp.assign(E, std::vector<std::vector<std::vector<double>>>());
380 steady.assign(E, StageSteady());
381 steady_asked.assign(E, 0);
382 det_sojourn = (opt.sojourn == "deterministic");
383 dvals.assign(E, 0.0);
384 refresh_sojourn_means();
385 if (statedep) {
386 bool any = false;
387 for (std::size_t e = 0; e < E; ++e)
388 for (std::size_t h = 0; h < E; ++h)
389 if (envObj.arc(e, h).enabled && envObj.arc(e, h).reset_rates) any = true;
390 if (!any)
391 throw InputError(
392 "SolverENV: method 'statedep' updates each environment transition from the "
393 "state its stage is left in, and no arc carries a rate reset "
394 "(Environment::set_env_rate_reset, resetEnvRatesFun in the reference); the "
395 "run would be the mean-field fixed point under a different name");
396 }
397 }
398
399 // ---- layered (LayeredNetwork) stages ---------------------------------
400
401 /**
402 * Build one persistent `SolverLN` per LAYERED stage.
403 *
404 * ONE SOLVER FOR THE WHOLE RUN, not one per sweep, and that is the
405 * reference's arrangement too (`self.solvers{e}` outlives the iteration).
406 * It matters twice over: the layer ensemble is what carries the aggregate
407 * (station, class) SHAPE this coupling blends over, and it has to be known
408 * before any stage is solved; and the layered fixed point is solved ONCE,
409 * because a steady solve ignores the state it is started from -- only the
410 * transient reads it -- so re-running it every environment sweep would
411 * recompute the same numbers.
412 */
413 void build_lqn_stages() {
414 const std::size_t E = envObj.nstages();
415 lnsolv.assign(E, std::shared_ptr<ln::SolverLN<T>>());
416 lnblk.assign(E, ln::LnLayerBlocks());
417 if (!envObj.has_lqn_stages()) return;
418 if (ctmc_stages)
419 throw UnsupportedError(
420 "SolverENV: a LayeredNetwork stage is solved by SolverLN over its layers, not by "
421 "an enumerated CTMC over one stage generator -- an LQN has no single generator to "
422 "enumerate. Leave stage_solver at 'fluid', which names the engine each LAYER is "
423 "integrated with (EnvOptions::lqn.layer_solver)");
424 if (opt.lqn.layer_solver != "fluid")
425 throw UnsupportedError(
426 "SolverENV: a LayeredNetwork stage is carried across an environment switch by its "
427 "queue lengths, so the coupling needs the stage's TRANSIENT; among the layer "
428 "engines only the fluid one produces one, and this ensemble asks for '" +
429 opt.lqn.layer_solver + "' layers (EnvOptions::lqn.layer_solver)");
430 for (std::size_t e = 0; e < E; ++e) {
431 if (!envObj.is_lqn(e)) continue;
432 ln::LnOptions lo = opt.lqn;
433 // THE HORIZON AND THE GRID BELONG TO THE COUPLING. A stage lasts as
434 // long as the environment lets it, and the exit average is summed
435 // against the holding-time CDF, so neither is a property of the LQN
436 // and a caller's value for either would silently change what the
437 // Stieltjes sum integrates over. `tran_grid` is installed per sweep
438 // by `lqn_stage_transient`, since a state-dependent method may move
439 // the holding time between sweeps.
440 lo.timespan_end = opt.timespan_end;
441 lo.tran_points = opt.tran_points;
442 lo.tran_grid.clear();
443 lnsolv[e] = std::make_shared<ln::SolverLN<T>>(envObj.stage(e).lqn_model, lo);
444 lnblk[e] = lnsolv[e]->layer_blocks();
445 }
446 }
447
448 /** Stations of stage `e`: its own, or the block-diagonal union of its layers. */
449 std::size_t stage_M(std::size_t e) const {
450 return envObj.is_lqn(e) ? lnblk[e].M : envObj.stage(e).model.nstations;
451 }
452
453 /** Classes of stage `e`: its own, or the block-diagonal union of its layers. */
454 std::size_t stage_K(std::size_t e) const {
455 return envObj.is_lqn(e) ? lnblk[e].K : envObj.stage(e).model.nclasses;
456 }
457
458 /**
459 * One LAYERED stage's transient, assembled block-diagonally into the same
460 * `tranT`/`tranQ`/`tranU`/`tranTp` the flat path fills.
461 *
462 * `seed` says whether the fixed point has an entry vector yet, exactly as it
463 * does on the CTMC path: `pre()` runs before it has one, and warm-starting
464 * from an all-zero matrix would empty every closed layer rather than leave
465 * it on its default state.
466 *
467 * THE OFF-DIAGONAL BLOCKS ARE ZERO AND ARE ALLOCATED ANYWAY. They pair a
468 * station of one layer with a class of another and stand for nothing, which
469 * is why the reference leaves them as empty cells; here they must still be
470 * present and sized, because the convergence test reads the FIRST point of
471 * every (station, class) series and an absent series has no first point.
472 * Zero series contribute nothing to the blend and are skipped by `maxpe`,
473 * which ignores an exact value of zero -- so the two ports agree on the
474 * numbers as well as on the shape.
475 */
476 void lqn_stage_transient(std::size_t e, bool seed) {
477 ln::SolverLN<T>& s = *lnsolv[e];
478 s.init_from_marginal(seed ? entry[e] : Matrix<double>());
479 s.set_tran_grid(stage_grid(e));
480 const ln::LnTranSolution tr = s.get_tran_avg();
481 const ln::LnLayerBlocks& b = lnblk[e];
482
483 // Every layer was integrated on the same grid -- one horizon, one
484 // out_grid -- so layer one's is the stage's time base.
485 tranT[e].clear();
486 if (!tr.layers.empty()) tranT[e] = tr.layers[0].t;
487 const std::size_t P = std::max<std::size_t>(tranT[e].size(), 1);
488 tranQ[e].assign(M, std::vector<std::vector<double>>(K, std::vector<double>(P, 0.0)));
489 tranU[e].assign(M, std::vector<std::vector<double>>(K, std::vector<double>(P, 0.0)));
490 tranTp[e].assign(M, std::vector<std::vector<double>>(K, std::vector<double>(P, 0.0)));
491 if (tranT[e].empty()) return;
492
493 // `b` and the blocks were both derived from the SAME layer ensemble --
494 // it is built once, in SolverLN's constructor, and never resized -- so
495 // `msz`/`ksz` are the exact extents of each block and the offsets tile
496 // the aggregate exactly. Nothing here is a bounds guard.
497 for (std::size_t l = 0; l < tr.layers.size(); ++l) {
498 const ln::LnTranLayer& L = tr.layers[l];
499 for (std::size_t i = 0; i < b.msz[l]; ++i)
500 for (std::size_t r = 0; r < b.ksz[l]; ++r) {
501 const std::size_t row = b.roff[l] + i, col = b.coff[l] + r;
502 copy_series(L.QN[i][r], tranQ[e][row][col]);
503 copy_series(L.UN[i][r], tranU[e][row][col]);
504 copy_series(L.TN[i][r], tranTp[e][row][col]);
505 }
506 }
507 }
508
509 /**
510 * Copy a layer series onto the stage grid.
511 *
512 * The two lengths agree by construction -- every layer was integrated on the
513 * grid this stage's time base came from -- and the loop is written over
514 * whichever is shorter so that a layer whose integration was cut short
515 * leaves the tail at zero rather than reading past its own trajectory.
516 */
517 static void copy_series(const std::vector<double>& src, std::vector<double>& dst) {
518 for (std::size_t j = 0; j < dst.size() && j < src.size(); ++j) dst[j] = src[j];
519 }
520
521 /** F_eh(t) of one arc, `evalCDF` in the reference; zero for a disabled arc. */
522 double arc_cdf(std::size_t e, std::size_t h, double t) const {
523 const EnvArc<T>& a = envObj.arc(e, h);
524 if (!a.enabled) return 0.0;
525 return num_traits<T>::to_double(lang::dist_cdf(a.dist, num_traits<T>::from_double(t)));
526 }
527
528 /**
529 * Semi-Markov stage probabilities for method `smp`, as `SolverENV.init` does in the JAR and
530 * MATLAB. P(k,e) = int dF_ke(t) prod_{h!=k,e} (1 - F_kh(t)) is the embedded jump chain,
531 * integrated on N = max(1000, 100T) intervals with the survival at the midpoint, T doubling
532 * from 1 until F_ke(T) >= 1 - 1e-8. The mean holding time integrates the sojourn survival
533 * prod_{h!=k} (1 - F_kh(t)) by composite Simpson (N = 10000) over [0, U], U doubling from 10
534 * until the survival is <= 1e-8. prob_env(k) is then proportional to pie_dtmc(k) * hold(k),
535 * and prob_orig(k,e) = prob_env(k) E0(k,e) / sum_{h!=e} prob_env(h) E0(h,e), E0 the arc rates.
536 */
537 void smp_stage_probabilities() {
538 const std::size_t E = envObj.nstages();
539 const double eps = 1e-8;
540 Matrix<double> E0(E, E, 0.0);
541 for (std::size_t k = 0; k < E; ++k)
542 for (std::size_t h = 0; h < E; ++h) {
543 const EnvArc<T>& a = envObj.arc(k, h);
544 if (!a.enabled) continue;
545 const double m = num_traits<T>::to_double(lang::dist_moment(a.dist, 1));
546 E0(k, h) = (m > 0.0) ? 1.0 / m : 0.0;
547 }
548 Matrix<double> P(E, E, 0.0);
549 for (std::size_t k = 0; k < E; ++k)
550 for (std::size_t e = 0; e < E; ++e) {
551 if (k == e || !envObj.arc(k, e).enabled) continue;
552 double Tup = 1.0;
553 while (arc_cdf(k, e, Tup) < 1.0 - eps) {
554 Tup *= 2.0;
555 if (Tup > 1e6) break;
556 }
557 const std::size_t N = std::max<std::size_t>(1000, static_cast<std::size_t>(std::lround(Tup * 100.0)));
558 const double dt = Tup / static_cast<double>(N);
559 double sum = 0.0, Fprev = arc_cdf(k, e, 0.0);
560 for (std::size_t i = 0; i < N; ++i) {
561 const double t1 = static_cast<double>(i + 1) * dt;
562 const double Fnext = arc_cdf(k, e, t1);
563 const double tmid = t1 - 0.5 * dt;
564 double surv = 1.0;
565 for (std::size_t h = 0; h < E; ++h)
566 if (h != k && h != e && envObj.arc(k, h).enabled) surv *= 1.0 - arc_cdf(k, h, tmid);
567 sum += (Fnext - Fprev) * surv;
568 Fprev = Fnext;
569 }
570 P(k, e) = sum;
571 }
572 const std::vector<double> pie = mc::dtmc_solve(P);
573
574 std::vector<double> hold(E, 0.0);
575 const std::size_t Nh = 10000;
576 for (std::size_t k = 0; k < E; ++k) {
577 auto surv = [&](double t) {
578 double s = 1.0;
579 for (std::size_t h = 0; h < E; ++h)
580 if (h != k) s *= 1.0 - arc_cdf(k, h, t);
581 return s;
582 };
583 double U = 10.0;
584 while (surv(U) > eps) {
585 U *= 2.0;
586 if (U > 1e6) break;
587 }
588 const double dt = U / static_cast<double>(Nh);
589 double integral = 0.0;
590 for (std::size_t i = 0; i < Nh; ++i) {
591 const double t0 = static_cast<double>(i) * dt, t1 = t0 + dt;
592 integral += (surv(t0) + 4.0 * surv(0.5 * (t0 + t1)) + surv(t1)) * dt / 6.0;
593 }
594 hold[k] = integral;
595 }
596
597 double denom = 0.0;
598 for (std::size_t e = 0; e < E; ++e) denom += pie[e] * hold[e];
599 std::vector<double> pi(E, 0.0);
600 for (std::size_t k = 0; k < E; ++k) pi[k] = pie[k] * hold[k] / denom;
601 envObj.prob_env = pi;
602 Matrix<double> emb(E, E, 0.0);
603 for (std::size_t e = 0; e < E; ++e) {
604 double s = 0.0;
605 for (std::size_t h = 0; h < E; ++h)
606 if (h != e) s += pi[h] * E0(h, e);
607 if (s > 0.0)
608 for (std::size_t k = 0; k < E; ++k)
609 if (k != e) emb(k, e) = pi[k] * E0(k, e) / s;
610 }
611 envObj.prob_orig = emb;
612 }
613
614 /** The mean holding times the deterministic sojourn evaluates at. */
615 void refresh_sojourn_means() {
616 if (!det_sojourn) return;
617 for (std::size_t e = 0; e < envObj.nstages(); ++e)
618 dvals[e] = mam::map_mean(envObj.hold_time[e].map());
619 }
620
621 /**
622 * `pre_`: seed every stage from its own solve at the first iteration, so
623 * the fixed point starts somewhere the model could be.
624 *
625 * Which solve is the reference's branch on the STAGE horizon: a finite one
626 * takes the LAST POINT of the stage transient, and only an infinite one --
627 * which this coupling refuses in `init`, since it has no transient to
628 * integrate -- takes the steady state. The distinction is not cosmetic on a
629 * stage that is unstable on its own -- a broken server offered more than it
630 * can serve -- where the steady state is whatever the integrator's own
631 * horizon happened to reach and is far from the transient at
632 * `timespan_end`. The seed only moves by the drift per environment cycle,
633 * so a seed off by a factor of ten costs iterations proportionally.
634 */
635 void pre() {
636 const std::size_t E = envObj.nstages();
637 for (std::size_t e = 0; e < E; ++e) {
638 if (envObj.is_lqn(e)) {
639 // The layered seed is the reference's own: the LAST POINT of the
640 // stage transient run from the layers' default state, which for
641 // a finite horizon is what `pre_` takes for every stage solver.
642 lqn_stage_transient(e, false);
643 if (tranT[e].empty()) continue;
644 const std::size_t last = tranT[e].size() - 1;
645 for (std::size_t i = 0; i < M; ++i)
646 for (std::size_t r = 0; r < K; ++r) entry[e](i, r) = tranQ[e][i][r][last];
647 continue;
648 }
649 if (ctmc_stages) {
650 // A CTMC stage has no "seed from nowhere" solve to run: it
651 // starts from the model's OWN initial state, which is what
652 // `initDefault` put there, so the first sweep enters at that
653 // state's queue lengths rather than at zero.
654 const ctmc::CtmcTransient<T> tr = ctmc_stage_transient(e, false, std::vector<double>());
655 if (tr.t.empty()) continue;
656 for (std::size_t i = 0; i < M; ++i)
657 for (std::size_t r = 0; r < K; ++r)
658 entry[e](i, r) = num_traits<T>::to_double(tr.QNt[i][r].back());
659 continue;
660 }
661 if (kp_stages) {
662 kp_stage_transient(e, false);
663 if (tranT[e].empty()) continue;
664 const std::size_t last = tranT[e].size() - 1;
665 for (std::size_t i = 0; i < M; ++i)
666 for (std::size_t r = 0; r < K; ++r) entry[e](i, r) = tranQ[e][i][r][last];
667 continue;
668 }
669 fluid::FluidOptions fo = opt.stage;
670 fo.init_sol.clear();
671 // THE SAME METHOD AS `analyze`: the entry vector this seeds the fixed
672 // point with is read off a stage trajectory, so taking it from the
673 // closing drift and then iterating with `dae` would start the loop at
674 // another method's answer.
675 const std::vector<fluid::FluidTranPoint> tr =
676 dae_stages ? fluid::solver_fluid_dae_transient(envObj.stage(e).model, fo,
677 opt.timespan_end, opt.tran_points)
678 : fluid::solver_fluid_transient(envObj.stage(e).model, fo,
679 opt.timespan_end, opt.tran_points);
680 if (tr.empty()) continue;
681 for (std::size_t i = 0; i < M; ++i)
682 for (std::size_t r = 0; r < K; ++r) entry[e](i, r) = tr.back().QN(i, r);
683 }
684 }
685
686 /** `analyze_`: the transient of one stage from its entry queue lengths. */
687 void analyze(std::size_t e) {
688 if (envObj.is_lqn(e)) {
689 lqn_stage_transient(e, true);
690 return;
691 }
692 tranT[e].clear();
693 tranC[e].clear();
694 tranQ[e].assign(M, std::vector<std::vector<double>>(K));
695 tranU[e].assign(M, std::vector<std::vector<double>>(K));
696 tranTp[e].assign(M, std::vector<std::vector<double>>(K));
697 if (ctmc_stages) {
698 // THE REFINED GRID, and it was MEASURED against the alternative.
699 // The reference forms its Stieltjes sum on whatever points ode23
700 // stopped at, so reporting on those looks like the faithful choice;
701 // on renv_threestages_repairmen it is the worse one -- Queue1 QLen
702 // 0.8344 against the reference's 0.83053, where the refined grid
703 // gives 0.83092. The refined grid puts 90% of its points under
704 // 5*E[S], which is where the holding-time CDF puts its mass, so it
705 // resolves the integrand rather than the trajectory. The residual
706 // 4.7e-4 is what is left of this quadrature difference.
707 const ctmc::CtmcTransient<T> tr = ctmc_stage_transient(e, true, stage_grid(e));
708 for (std::size_t j = 0; j < tr.t.size(); ++j) {
709 tranT[e].push_back(num_traits<T>::to_double(tr.t[j]));
710 for (std::size_t i = 0; i < M; ++i)
711 for (std::size_t r = 0; r < K; ++r) {
712 tranQ[e][i][r].push_back(num_traits<T>::to_double(tr.QNt[i][r][j]));
713 tranU[e][i][r].push_back(num_traits<T>::to_double(tr.UNt[i][r][j]));
714 tranTp[e][i][r].push_back(num_traits<T>::to_double(tr.TNt[i][r][j]));
715 }
716 }
717 return;
718 }
719 const qn::NetworkStruct<T>& sn = envObj.stage(e).model;
720 if (kp_stages) {
721 kp_stage_transient(e, true);
722 return;
723 }
724 fluid::FluidOptions fo = opt.stage;
725 fo.init_sol = initsol_from_marginal(sn, entry[e]);
726 // A `dae` stage takes the entry covariance as well as the entry mean, and
727 // gives its own back. Only that method reads `init_qcov`: offering it to a
728 // closing stage would be dropped in silence, which is the failure this
729 // predicate exists to avoid.
730 if (meancov && dae_stages) fo.init_qcov = centry[e];
731 const std::vector<fluid::FluidTranPoint> tr =
732 dae_stages ? fluid::solver_fluid_dae_transient(sn, fo, opt.timespan_end,
733 opt.tran_points, stage_grid(e))
734 : fluid::solver_fluid_transient(sn, fo, opt.timespan_end,
735 opt.tran_points, stage_grid(e));
736 for (const fluid::FluidTranPoint& p : tr) {
737 tranT[e].push_back(p.t);
738 for (std::size_t i = 0; i < M; ++i)
739 for (std::size_t r = 0; r < K; ++r) {
740 tranQ[e][i][r].push_back(p.QN(i, r));
741 tranU[e][i][r].push_back(p.UN(i, r));
742 tranTp[e][i][r].push_back(p.TN(i, r));
743 }
744 if (meancov && dae_stages && p.QCov.rows() == M * K) tranC[e].push_back(p.QCov);
745 }
746 // ALL OR NOTHING: `exit_cov` reads tranC[e] only when it has one matrix per
747 // point, so a run where some points carried a covariance and others did not
748 // must contribute none rather than a ragged series.
749 if (tranC[e].size() != tranT[e].size()) tranC[e].clear();
750 }
751
752 /**
753 * One stage's transient under the KO-PENDER limits, mean AND covariance.
754 *
755 * THE ONLY STAGE ROUTE THAT CARRIES A SECOND MOMENT in this port: the
756 * closing drift integrates a first moment alone, so a `meancov` run over
757 * closing stages keeps the TIMING variance of the sojourn and nothing of the
758 * within-stage spread. `kp` integrates both on one grid, which is why the
759 * covariance is read off the same solve as the mean rather than asked for
760 * again. Selected by `EnvOptions::stage.method`, so a caller asks for the
761 * limit by name exactly as a standalone fluid solve does.
762 *
763 * `seed` says whether the fixed point has an entry vector yet, as on the
764 * layered and CTMC paths: `pre()` runs before it has one, and seeding from
765 * an all-zero mean and covariance would start every stage empty rather than
766 * at this method's own stationary arrival phase.
767 */
768 void kp_stage_transient(std::size_t e, bool seed) {
769 tranT[e].clear();
770 tranC[e].clear();
771 const qn::NetworkStruct<T>& sn = envObj.stage(e).model;
772 fluid::FluidOptions fo = opt.stage;
773 fo.method = "kp";
774 fo.init_sol.clear();
775 fo.timespan_end = opt.timespan_end;
776 if (seed) {
777 fo.init_qlen = entry[e];
778 if (meancov) fo.init_qcov = centry[e];
779 }
780 fluid::FluidKpTransient tr;
781 fluid::solver_fluid_kp_core(sn, fo, &tr, stage_grid(e));
782 tranT[e] = tr.t;
783 const std::size_t P = tr.t.size();
784 tranQ[e].assign(M, std::vector<std::vector<double>>(K, std::vector<double>(P, 0.0)));
785 tranU[e].assign(M, std::vector<std::vector<double>>(K, std::vector<double>(P, 0.0)));
786 tranTp[e].assign(M, std::vector<std::vector<double>>(K, std::vector<double>(P, 0.0)));
787 // Mean and covariance come off the SAME integration, on the same grid, so
788 // the two moments this coupling carries need no interpolation onto one
789 // another and the covariance costs nothing beyond the aggregation.
790 for (std::size_t n = 0; n < P; ++n)
791 for (std::size_t i = 0; i < M; ++i)
792 for (std::size_t r = 0; r < K; ++r) {
793 tranQ[e][i][r][n] = tr.QN[n](i, r);
794 tranU[e][i][r][n] = tr.UN[n](i, r);
795 tranTp[e][i][r][n] = tr.TN[n](i, r);
796 }
797 if (meancov) tranC[e] = tr.QCov;
798 }
799
800 /**
801 * One stage's transient under an ENUMERATED CTMC, from its entry marginal.
802 *
803 * THE MARGINAL IS ROUNDED HERE and is not for a fluid stage, which is the
804 * reference's own split (`roundMarginalForDiscreteSolver` runs for every
805 * stage solver except SolverFluid): a chain has no state holding 1.7 jobs,
806 * so the fixed point's continuous iterate has to be placed on the lattice
807 * before it can be a state at all. The rounding conserves each closed
808 * class's population, so the placed state is one the chain actually
809 * contains.
810 *
811 * The state is written onto a COPY of the stage struct as its one-row
812 * declared state space, which is the same channel `setState` reaches
813 * `solver_ctmc_transient_analyzer` through -- so this seeds the stage
814 * exactly as a caller would, rather than through a private door.
815 */
816 ctmc::CtmcTransient<T> ctmc_stage_transient(std::size_t e, bool seed,
817 const std::vector<double>& grid) const {
818 qn::NetworkStruct<T> sn = envObj.stage(e).model;
819 // `seed` is explicit and not inferred from the arguments: `pre()` runs
820 // BEFORE the fixed point has an entry vector, and its all-zero matrix
821 // would place every job nowhere -- a state the chain does not have.
822 if (seed) seed_ctmc_state(sn, entry[e]);
823 std::vector<T> g;
824 g.reserve(grid.size());
825 for (std::size_t j = 0; j < grid.size(); ++j) g.push_back(num_traits<T>::from_double(grid[j]));
827 sn, ctmc_stage_options(), num_traits<T>::from_int(0),
828 num_traits<T>::from_double(opt.timespan_end), g);
829 }
830
831 /** The CTMC options a stage is solved with; the cutoff is the caller's. */
832 ctmc::CtmcOptions ctmc_stage_options() const {
833 ctmc::CtmcOptions co;
834 co.cutoff = opt.stage_cutoff;
835 return co;
836 }
837
838 /**
839 * Write the rounded entry marginal onto `sn` as its declared state.
840 *
841 * A row that the encoding cannot realize leaves the station alone, so the
842 * analyzer falls back to that station's default marking rather than being
843 * handed a state the chain does not contain -- an error the coupling could
844 * not act on, where the default is at least a state of the same model.
845 */
846 void seed_ctmc_state(qn::NetworkStruct<T>& sn, const Matrix<double>& Q) const {
847 if (Q.empty()) return;
848 const std::vector<std::vector<std::size_t>> nir = round_marginal(sn, Q);
849 for (std::size_t i = 0; i < sn.nstations && i < nir.size(); ++i) {
850 const std::size_t ind = sn.station_to_node[i]; // 1-based node index
851 std::vector<std::size_t> ph(sn.nclasses, 1);
852 for (std::size_t r = 0; r < sn.nclasses; ++r) ph[r] = sn.phasessz_of(i + 1, r + 1);
853 std::vector<T> row;
854 if (!qn::from_marginal_node_first(sn, ind, nir[i], ph, row))
855 continue;
856 Matrix<T> space(1, row.size());
857 for (std::size_t c = 0; c < row.size(); ++c) space(0, c) = row[c];
858 sn.statespace[ind] = space;
859 sn.stateprior[ind] = std::vector<T>(1, num_traits<T>::from_int(1));
860 }
861 }
862
863 /**
864 * `roundMarginalForDiscreteSolver`: the continuous per-class marginal on the
865 * integer lattice, conserving each CLOSED class's population.
866 *
867 * Rounding entrywise does not conserve it -- three stations holding 0.5 jobs
868 * of a one-job class round to 0, 0, 0 and the class disappears from the
869 * chain -- so the residual is handed to the stations with the largest
870 * fractional parts, which is the reference's own rule.
871 */
872 std::vector<std::vector<std::size_t>> round_marginal(const qn::NetworkStruct<T>& sn,
873 const Matrix<double>& Q) const {
874 std::vector<std::vector<std::size_t>> n(sn.nstations, std::vector<std::size_t>(sn.nclasses, 0));
875 for (std::size_t r = 0; r < sn.nclasses; ++r) {
876 const double pop = sn.classes[r].population;
877 std::vector<double> frac(sn.nstations, 0.0);
878 long placed = 0;
879 for (std::size_t i = 0; i < sn.nstations; ++i) {
880 const double q = (i < Q.rows() && r < Q.cols()) ? std::max(0.0, Q(i, r)) : 0.0;
881 const double fl = std::floor(q);
882 n[i][r] = static_cast<std::size_t>(fl);
883 frac[i] = q - fl;
884 placed += static_cast<long>(fl);
885 }
886 if (!std::isfinite(pop)) continue; // an open class has no population to conserve
887 long want = static_cast<long>(std::llround(pop));
888 while (placed < want) {
889 std::size_t best = 0;
890 double bv = -1.0;
891 for (std::size_t i = 0; i < sn.nstations; ++i)
892 if (frac[i] > bv) { bv = frac[i]; best = i; }
893 ++n[best][r];
894 frac[best] = -1.0;
895 ++placed;
896 if (bv < 0.0) break; // nowhere left to place; the loop would not terminate
897 }
898 while (placed > want) {
899 std::size_t best = 0;
900 double bv = 2.0;
901 bool any = false;
902 for (std::size_t i = 0; i < sn.nstations; ++i)
903 if (n[i][r] > 0 && frac[i] < bv) { bv = frac[i]; best = i; any = true; }
904 if (!any) break;
905 --n[best][r];
906 frac[best] = 2.0;
907 --placed;
908 }
909 }
910 return n;
911 }
912
913 /** One stage's steady tables, in the (station, class) space of the blend. */
914 struct StageSteady {
915 Matrix<double> QN, UN, TN;
916 };
917
918 /**
919 * The stage's own STEADY tables, which are the exit value of every metric
920 * its transient carries no trajectory for.
921 *
922 * A METRIC WITH NO TRAJECTORY IS NOT A METRIC WORTH ZERO. A stage solver
923 * advances the quantities its integration actually carries; what it leaves
924 * out it holds CONSTANT over the stage, so the value at the exit instant is
925 * that constant whatever the sojourn was. `post()` and `finish()` used to
926 * skip such a stage outright and blend the zeros, reporting a stage whose
927 * metrics never move as ABSENT rather than as constant. That is the defect
928 * `stageSteady_` closes in the reference (`solver_env_meanfield_analyzer.m`)
929 * and `stageSteady`/`_stage_steady` close in the JAR and native python.
930 *
931 * THE HORIZON IS PUT ASIDE FOR THE ASK, and here it is not a refusal to
932 * dodge but a DIFFERENT ANSWER to avoid. The reference's `getAvg` rejects a
933 * finite `options.timespan` by name; `solver_fluid` instead ACCEPTS one and
934 * quietly stops its window iteration there, disarming both the fixed-point
935 * and the geometric-tail tests. Asked with the stage horizon still on the
936 * options it would hand back the state at `t = timespan_end` -- the very
937 * transient this seed exists to complement -- instead of the constant.
938 *
939 * THE ASK GOES TO THE ENGINE THAT PRODUCED THE TRAJECTORY, branch for branch
940 * with `analyze`: the closing drift's own fixed point for a closing stage
941 * (`solver_fluid_transient` integrates that drift whatever the method name
942 * says), `kp` and `dae` through their own steady entries, and the enumerated
943 * chain's stationary law for a CTMC stage. A seed taken from another closure
944 * would not be the constant the trajectory holds.
945 *
946 * A LAYERED STAGE IS NOT ASKED, exactly as the JAR's `stageSteadyAsk`
947 * refuses a non-`Network` stage by name: `SolverLN`'s steady table is over
948 * the LQN's OWN nodes -- hosts, tasks, entries, activities -- and not over
949 * the block-diagonal (station, class) aggregate this coupling blends in, so
950 * a leading-block copy would read LQN node k as class k. Its transient is
951 * already assembled in the right layout by `lqn_stage_transient`, and every
952 * cell it owns carries one.
953 *
954 * ASKED ONCE PER STAGE, AND ONLY WHERE IT IS READ. The stage networks do not
955 * change across sweeps -- `statedep` rewrites the environment arcs, not the
956 * models -- so the constants are the same every time; and a stage whose
957 * trajectory IS readable has every cell of the seed overwritten from it, so
958 * asking then would cost a steady solve per sweep to be discarded. That is
959 * this port's form of the reference's "read `result` first".
960 */
961 const StageSteady& stage_steady(std::size_t e) {
962 if (steady_asked[e]) return steady[e];
963 steady_asked[e] = 1;
964 StageSteady& out = steady[e];
965 out.QN = Matrix<double>(M, K, 0.0);
966 out.UN = Matrix<double>(M, K, 0.0);
967 out.TN = Matrix<double>(M, K, 0.0);
968 if (envObj.is_lqn(e)) return out;
969 const qn::NetworkStruct<T>& sn = envObj.stage(e).model;
970 if (ctmc_stages) {
971 const ctmc::CtmcAvg<T> a = ctmc::solver_ctmc_analyzer(sn, ctmc_stage_options()).avg;
972 copy_finite(a.QN, out.QN);
973 copy_finite(a.UN, out.UN);
974 copy_finite(a.TN, out.TN);
975 return out;
976 }
977 fluid::FluidOptions fo = opt.stage;
978 // NOTHING CARRIED IN: the constant is what the stage holds on its own,
979 // and the entry vector the handoff computes is the transient's business.
980 fo.init_sol.clear();
981 fo.init_qlen = Matrix<double>();
982 fo.init_qcov = Matrix<double>();
983 fo.timespan_end = std::numeric_limits<double>::infinity();
984 fluid::FluidSolution s;
985 if (kp_stages) {
986 fo.method = "kp";
987 s = fluid::solver_fluid_kp<T>(sn, fo);
988 } else if (dae_stages) {
989 fo.method = "dae";
990 s = fluid::solver_fluid_dae<T>(sn, fo);
991 } else {
992 fo.method = "closing";
993 s = fluid::solver_fluid<T>(sn, fo);
994 }
995 copy_finite(s.QN, out.QN);
996 copy_finite(s.UN, out.UN);
997 copy_finite(s.TN, out.TN);
998 return out;
999 }
1000
1001 /**
1002 * Copy `src` onto `dst`, leaving every non-finite entry at zero: a NaN is
1003 * ABSENT, not a number, and must not enter the probEnv blend.
1004 *
1005 * The two shapes agree by construction -- `init` refuses an environment
1006 * whose stages differ in stations or classes, and a flat stage's own tables
1007 * are (nstations, nclasses) -- so this is a copy and not a fit.
1008 */
1009 template <class S>
1010 static void copy_finite(const Matrix<S>& src, Matrix<double>& dst) {
1011 for (std::size_t i = 0; i < dst.rows(); ++i)
1012 for (std::size_t r = 0; r < dst.cols(); ++r) {
1013 const double v = num_traits<S>::to_double(src(i, r));
1014 if (std::isfinite(v)) dst(i, r) = v;
1015 }
1016 }
1017
1018 /**
1019 * The exit tables of stage `e`: its transient averaged over the sojourn on
1020 * the cells that carry one, and its steady constants on the cells that do
1021 * not. `want_all` asks for U and T beside Q, which only a state-dependent
1022 * rate reads in `post()` and which `finish()` always reports.
1023 *
1024 * THE WEIGHT IS THE STAGE SOJOURN, NOT THE e -> h CLOCK, and the reference
1025 * says so where it builds it: competing exponentials leave the exit TIME
1026 * independent of which destination won, so the exit average does not depend
1027 * on h at all -- h enters only through the reset applied on the way in.
1028 * Weighting by `proc[e][h]` instead read the transient over the mean of ONE
1029 * risk rather than of their minimum: on renv_twostages_repairmen, whose
1030 * Stage2 competes a 0.5 self arc with a 0.5 arc back, that is a mean of 2
1031 * against the sojourn's 1, and it reported Queue1 QLen 0.55882 against
1032 * MATLAB's 0.55550 with the two stations' throughputs 1.4% apart in a closed
1033 * cycle that admits one throughput.
1034 *
1035 * A TRAJECTORY THE COUPLING CANNOT AVERAGE IS NOT AN ANSWER OF ZERO. Two
1036 * cases reach `stage_steady` rather than a blend: a stage that reported no
1037 * transient at all, and one whose sojourn puts NO MASS on the grid, which is
1038 * this port's form of the reference's single-point transient (`tR = 1`, the
1039 * table `cdf_weights` can form no increment over). Both mean the same thing
1040 * -- nothing moved that the exit instant could be read off -- and the stage's
1041 * own constant is the exit value in both.
1042 */
1043 void stage_exit(std::size_t e, bool want_all, Matrix<double>& Qe, Matrix<double>& Ue,
1044 Matrix<double>& Te) {
1045 std::vector<double> w;
1046 double wsum = 0.0;
1047 if (!tranT[e].empty() && !det_sojourn) {
1048 w = cdf_weights(envObj.hold_time[e].map(), tranT[e]);
1049 for (double v : w) wsum += v;
1050 }
1051 if (tranT[e].empty() || (!det_sojourn && !(wsum > 0.0))) {
1052 const StageSteady& st = stage_steady(e);
1053 Qe = st.QN;
1054 if (want_all) {
1055 Ue = st.UN;
1056 Te = st.TN;
1057 }
1058 return;
1059 }
1060 for (std::size_t i = 0; i < M; ++i)
1061 for (std::size_t r = 0; r < K; ++r) {
1062 if (det_sojourn) {
1063 // Deterministic sojourn: the exit metrics are the transient
1064 // read at t = d_e.
1065 Qe(i, r) = detail::env_det_eval(tranT[e], tranQ[e][i][r], dvals[e]);
1066 if (!want_all) continue;
1067 Ue(i, r) = detail::env_det_eval(tranT[e], tranU[e][i][r], dvals[e]);
1068 Te(i, r) = detail::env_det_eval(tranT[e], tranTp[e][i][r], dvals[e]);
1069 continue;
1070 }
1071 Qe(i, r) = weighted(tranQ[e][i][r], w, wsum);
1072 if (!want_all) continue;
1073 Ue(i, r) = weighted(tranU[e][i][r], w, wsum);
1074 Te(i, r) = weighted(tranTp[e][i][r], w, wsum);
1075 }
1076 }
1077
1078 /**
1079 * `post_`: average each stage's transient over WHEN it hands over to each
1080 * destination, then re-seed every stage from its predecessors.
1081 */
1082 void post() {
1083 const std::size_t E = envObj.nstages();
1084 std::vector<std::vector<Matrix<double>>> Qexit(
1085 E, std::vector<Matrix<double>>(E, Matrix<double>(M, K, 0.0)));
1086 // The other two exit metrics are read only by a state-dependent rate,
1087 // which is given all three; without one they are never looked at, and
1088 // the reference computes them in the same loop regardless.
1089 std::vector<std::vector<Matrix<double>>> Uexit, Texit;
1090 if (statedep) {
1091 Uexit.assign(E, std::vector<Matrix<double>>(E, Matrix<double>(M, K, 0.0)));
1092 Texit.assign(E, std::vector<Matrix<double>>(E, Matrix<double>(M, K, 0.0)));
1093 }
1094 for (std::size_t e = 0; e < E; ++e) {
1095 // Seeded with the stage's own steady state rather than skipped: a
1096 // metric its transient carries no trajectory for is one it holds
1097 // CONSTANT, and calling that zero is what reported an open cache
1098 // model's Q, U and T as identically zero. see `stage_steady`
1099 Matrix<double> Qd(M, K, 0.0), Ud(M, K, 0.0), Td(M, K, 0.0);
1100 stage_exit(e, statedep, Qd, Ud, Td);
1101 for (std::size_t h = 0; h < E; ++h) {
1102 Qexit[e][h] = Qd;
1103 if (!statedep) continue;
1104 Uexit[e][h] = Ud;
1105 Texit[e][h] = Td;
1106 }
1107 }
1108
1109 // 'meancov' carries a COVARIANCE beside the mean, and it is taken over the
1110 // same sojourn weights the exit mean uses -- independent of the
1111 // destination, exactly as Qexit is, since competing exponentials leave the
1112 // exit time independent of where the switch goes.
1113 const std::size_t n2 = M * K;
1114 std::vector<Matrix<double>> Cexit;
1115 if (meancov) {
1116 Cexit.assign(E, Matrix<double>(n2, n2, 0.0));
1117 for (std::size_t e = 0; e < E; ++e)
1118 if (!tranT[e].empty()) Cexit[e] = stage_exit_cov(e);
1119 }
1120
1121 for (std::size_t e = 0; e < E; ++e) {
1122 if (tranT[e].empty()) continue;
1123 Matrix<double> Qe(M, K, 0.0);
1124 std::vector<double> ment(meancov ? n2 : 0, 0.0);
1125 Matrix<double> sent(meancov ? n2 : 0, meancov ? n2 : 0, 0.0);
1126 for (std::size_t h = 0; h < E; ++h) {
1127 const double p = envObj.prob_orig(h, e);
1128 if (!(p > 0.0)) continue;
1129 const ResetMarginal& f = envObj.arc(h, e).reset;
1130 const Matrix<double> reset = f ? f(Qexit[h][e]) : Qexit[h][e];
1131 if (reset.rows() != M || reset.cols() != K)
1132 throw InputError(
1133 "SolverENV: a reset policy returned a matrix of the wrong shape");
1134 for (std::size_t i = 0; i < M; ++i)
1135 for (std::size_t r = 0; r < K; ++r) Qe(i, r) += p * reset(i, r);
1136 if (!meancov) continue;
1137 // The reset is an arbitrary map on the means, so the covariance
1138 // crosses it by the delta method. The MIXTURE over origins is then
1139 // taken on the SECOND MOMENT, not on the covariances: a convex
1140 // combination of the C_h alone drops the spread of the per-origin
1141 // means, which is most of the variance when the stages differ.
1142 const Matrix<double> R = reset_jacobian(f, Qexit[h][e]);
1143 const Matrix<double> Ch = congruence(R, Cexit[h]);
1144 for (std::size_t a = 0; a < n2; ++a) {
1145 const double ma = reset(a % M, a / M);
1146 ment[a] += p * ma;
1147 for (std::size_t b = 0; b < n2; ++b)
1148 sent(a, b) += p * (Ch(a, b) + ma * reset(b % M, b / M));
1149 }
1150 }
1151 entry[e] = Qe;
1152 if (!meancov) continue;
1153 Matrix<double> Ce(n2, n2, 0.0);
1154 for (std::size_t a = 0; a < n2; ++a)
1155 for (std::size_t b = 0; b < n2; ++b)
1156 Ce(a, b) = 0.5 * ((sent(a, b) - ment[a] * ment[b]) +
1157 (sent(b, a) - ment[b] * ment[a]));
1158 centry[e] = Ce;
1159 }
1160
1161 // The state-dependent rates come AFTER the entry update, as they do in
1162 // the reference: the entries just computed used the probOrig of the
1163 // environment as it was during this iteration, and rewriting the arcs
1164 // first would blend them with weights from an environment the stage
1165 // transients were never solved under.
1166 if (!statedep) return;
1167 bool touched = false;
1168 for (std::size_t e = 0; e < E; ++e)
1169 for (std::size_t h = 0; h < E; ++h) {
1170 const EnvArc<T>& a = envObj.arc(e, h);
1171 if (!a.enabled || !a.reset_rates) continue;
1172 envObj.set_transition_dist(e, h,
1173 a.reset_rates(a.dist, Qexit[e][h], Uexit[e][h],
1174 Texit[e][h]));
1175 touched = true;
1176 }
1177 if (!touched) return;
1178 // Everything the analyzer integrates against -- the marked transition
1179 // processes, the superposed holding times, probEnv and probOrig -- is
1180 // derived from the arc distributions, so the environment is rebuilt
1181 // whole rather than patched.
1182 envObj.init();
1183 refresh_sojourn_means();
1184 }
1185
1186 /**
1187 * `finish_`: average each stage over its own holding time and blend the
1188 * stages by their stationary probabilities.
1189 */
1190 void finish(EnvSolution& out) {
1191 const std::size_t E = envObj.nstages();
1192 out.QExit.assign(E, Matrix<double>(M, K, 0.0));
1193 out.UExit.assign(E, Matrix<double>(M, K, 0.0));
1194 out.TExit.assign(E, Matrix<double>(M, K, 0.0));
1195 for (std::size_t e = 0; e < E; ++e)
1196 // Seeded with the stage's own steady state rather than skipped, for
1197 // the reason `post()` is: a metric with no trajectory is a constant
1198 // and not a zero, and here the zeros would be what the run REPORTS.
1199 // see `stage_steady`
1200 stage_exit(e, true, out.QExit[e], out.UExit[e], out.TExit[e]);
1201 for (std::size_t e = 0; e < E; ++e) {
1202 const double p = envObj.prob_env[e];
1203 for (std::size_t i = 0; i < M; ++i)
1204 for (std::size_t r = 0; r < K; ++r) {
1205 out.QN(i, r) += p * out.QExit[e](i, r);
1206 out.UN(i, r) += p * out.UExit[e](i, r);
1207 out.TN(i, r) += p * out.TExit[e](i, r);
1208 }
1209 }
1210 out.Qentry = entry;
1211 if (!meancov) return;
1212 // 'meancov' also REPORTS the second moment, mixed over the stages by the
1213 // same law of total variance the handoff uses. The term
1214 // sum_e p_e (m_e - m)(m_e - m)' is what the environment itself
1215 // contributes: two stages with identical within-stage variance but
1216 // different means still leave the queue length varying, and averaging the
1217 // per-stage covariances alone would report none of it.
1218 const std::size_t n = M * K;
1219 std::vector<double> mval(n, 0.0);
1220 Matrix<double> sval(n, n, 0.0);
1221 for (std::size_t e = 0; e < E; ++e) {
1222 const double p = envObj.prob_env[e];
1223 if (!(p > 0.0) || tranT[e].empty()) continue;
1224 const Matrix<double> Ce = stage_exit_cov(e);
1225 for (std::size_t a = 0; a < n; ++a) {
1226 const double ma = out.QExit[e](a % M, a / M);
1227 mval[a] += p * ma;
1228 for (std::size_t b = 0; b < n; ++b)
1229 sval(a, b) += p * (Ce(a, b) + ma * out.QExit[e](b % M, b / M));
1230 }
1231 }
1232 out.QCov = Matrix<double>(n, n, 0.0);
1233 out.QVar = Matrix<double>(M, K, 0.0);
1234 for (std::size_t a = 0; a < n; ++a) {
1235 for (std::size_t b = 0; b < n; ++b)
1236 out.QCov(a, b) = 0.5 * ((sval(a, b) - mval[a] * mval[b]) +
1237 (sval(b, a) - mval[b] * mval[a]));
1238 out.QVar(a % M, a / M) = std::max(0.0, out.QCov(a, a));
1239 }
1240 }
1241
1242 /**
1243 * The grid an exit average is summed on, refined where the SOJOURN WEIGHT
1244 * CANNOT SEE the integrator's own grid.
1245 *
1246 * The exit metric is `sum_k m(t_k) * [F(t_k) - F(t_{k-1})]` over the ODE
1247 * solver's OUTPUT grid, and that grid is chosen for the horizon rather than
1248 * for the sojourn: a stage integrated over [0,1e3] and read through an
1249 * Exp(1) clock puts almost every point where the weight is zero, so the
1250 * answer becomes an artifact of step placement.
1251 *
1252 * The grid is rebuilt UNCONDITIONALLY -- 90% of the points under `5*E[S]`
1253 * and the rest across the tail -- rather than only when the solver's own
1254 * grid looks too coarse. A "50 points inside the support is enough" escape
1255 * (native python's, before this) stops wherever the integrator's steps
1256 * happened to fall and does not converge: on renv_node_breakdown the sum
1257 * runs 0.462260, 0.460580, 0.459704, 0.459272, 0.459138, 0.459122 as the
1258 * point count goes 500 to 5e4, and this engine on a 1e5-point uniform grid
1259 * answers 0.459171. Rebuilding always is also what makes the four codebases
1260 * sum the SAME points, which is the property parity needs.
1261 */
1262 static constexpr std::size_t kCdfInterp = 5000;
1263
1264 static std::vector<double> refine_grid(const mam::Map<double>& m,
1265 const std::vector<double>& t) {
1266 if (t.size() < 2) return t;
1267 // A DISABLED ARC is the 1 x 1 zero pair, and `map_mean` refuses it by
1268 // name ("zero arrival rate") rather than returning an infinity. Its
1269 // weights are identically zero, so the grid it would be summed on
1270 // cannot matter; leave it alone, exactly as cdf_weights does.
1271 bool all_zero = true;
1272 for (std::size_t a = 0; a < m.D1.rows() && all_zero; ++a)
1273 for (std::size_t b = 0; b < m.D1.cols() && all_zero; ++b)
1274 if (m.D1(a, b) != 0.0) all_zero = false;
1275 if (all_zero) return t;
1276 const double t0 = t.front();
1277 const double tend = t.back();
1278 double mean_sojourn = mam::map_mean(m);
1279 if (!(mean_sojourn > 0.0) || !std::isfinite(mean_sojourn))
1280 mean_sojourn = (tend - t0) / 10.0;
1281 double tcdf = std::min(tend, 5.0 * mean_sojourn);
1282 if (tcdf <= t0) tcdf = tend;
1283 const std::size_t ndense = static_cast<std::size_t>(0.9 * kCdfInterp);
1284 const std::size_t ntail = kCdfInterp - ndense;
1285 const bool with_tail = tcdf < tend && ntail > 1;
1286 std::vector<double> fine;
1287 fine.reserve(with_tail ? ndense + ntail : ndense);
1288 for (std::size_t k = 0; k < ndense; ++k)
1289 fine.push_back(t0 + (tcdf - t0) * static_cast<double>(k) /
1290 static_cast<double>(ndense - 1));
1291 if (with_tail)
1292 for (std::size_t k = 1; k <= ntail; ++k)
1293 fine.push_back(tcdf + (tend - tcdf) * static_cast<double>(k) /
1294 static_cast<double>(ntail));
1295 return fine;
1296 }
1297
1298 /**
1299 * The OUTPUT GRID stage `e` is integrated on: the refined one, so the exit
1300 * average is summed over points the trajectory was actually evaluated at.
1301 *
1302 * Interpolating instead cannot recover resolution the trajectory never had
1303 * -- over [0,1e3] a uniform 1001-point grid carries SIX samples below
1304 * `5*E[S]` for an `Exp(1)` sojourn, and a piecewise-linear reading of six
1305 * samples is still six samples' worth of information; it reported a
1306 * throughput of 0.8099 for a source admitting 0.8. LSODA takes an arbitrary
1307 * increasing output vector, so the points are simply asked for.
1308 *
1309 * The scale is the HOLDING time, which is the sojourn regardless of which
1310 * destination fires (competing exponentials), so one grid serves post()'s
1311 * per-destination weights and finish()'s holding-time ones alike.
1312 */
1313 std::vector<double> stage_grid(std::size_t e) const {
1314 std::vector<double> ends(2);
1315 ends[0] = 0.0;
1316 ends[1] = opt.timespan_end;
1317 const std::vector<double> g = refine_grid(envObj.hold_time[e].map(), ends);
1318 // A disabled holding time leaves `refine_grid` with nothing to say; fall
1319 // back to the uniform grid the option asks for.
1320 if (g.size() < 3) return std::vector<double>();
1321 return g;
1322 }
1323
1324 /**
1325 * The covariance of the station-class queue lengths at the instant stage `e`
1326 * is left, by the law of total variance over the random sojourn T:
1327 *
1328 * Cov[Q(T)] = E_T[Cov(Q(t)|t)] + Cov_T[E(Q(t)|t)]
1329 * = sum_n w_n (C(t_n) + m(t_n) m(t_n)')/sum(w) - m_exit m_exit'
1330 *
1331 * with the SAME weights the exit mean uses, so the two are consistent by
1332 * construction. `C(t)` is the within-stage covariance the stage solver
1333 * integrated and is absent -- hence zero -- for a stage carrying a first
1334 * moment only; the timing term survives regardless. A deterministic sojourn
1335 * contributes no timing variance, so the answer is `C(d)` alone.
1336 */
1337 Matrix<double> stage_exit_cov(std::size_t e) const {
1338 const std::size_t n = M * K;
1339 Matrix<double> C(n, n, 0.0);
1340 const std::vector<double>& t = tranT[e];
1341 if (t.size() < 2) return C;
1342 const bool have_c = tranC[e].size() == t.size();
1343 if (det_sojourn) {
1344 if (!have_c) return C;
1345 // Clamped to the stage's own grid rather than extrapolated: linear
1346 // extrapolation of a covariance can leave the positive semidefinite
1347 // cone, while a convex combination of two members stays inside it.
1348 double d = std::max(t.front(), std::min(dvals[e], t.back()));
1349 std::size_t j = t.size() - 1;
1350 for (std::size_t k = 1; k < t.size(); ++k)
1351 if (d <= t[k]) {
1352 j = k;
1353 break;
1354 }
1355 const double dt = t[j] - t[j - 1];
1356 const double a = (dt > 0.0) ? (d - t[j - 1]) / dt : 0.0;
1357 for (std::size_t p = 0; p < n; ++p)
1358 for (std::size_t q = 0; q < n; ++q)
1359 C(p, q) = (1.0 - a) * tranC[e][j - 1](p, q) + a * tranC[e][j](p, q);
1360 return C;
1361 }
1362 const std::vector<double> w = cdf_weights(envObj.hold_time[e].map(), t);
1363 double wsum = 0.0;
1364 for (double v : w) wsum += v;
1365 if (!(wsum > 0.0)) return C;
1366 std::vector<double> mexit(n, 0.0);
1367 for (std::size_t a = 0; a < n; ++a)
1368 mexit[a] = weighted(tranQ[e][a % M][a / M], w, wsum);
1369 for (std::size_t a = 0; a < n; ++a) {
1370 const std::vector<double>& qa = tranQ[e][a % M][a / M];
1371 for (std::size_t b = 0; b < n; ++b) {
1372 const std::vector<double>& qb = tranQ[e][b % M][b / M];
1373 double acc = 0.0;
1374 for (std::size_t k = 0; k < w.size() && k < qa.size() && k < qb.size(); ++k)
1375 acc += w[k] * qa[k] * qb[k];
1376 if (have_c)
1377 for (std::size_t k = 0; k < w.size(); ++k) acc += w[k] * tranC[e][k](a, b);
1378 C(a, b) = acc / wsum - mexit[a] * mexit[b];
1379 }
1380 }
1381 for (std::size_t a = 0; a < n; ++a)
1382 for (std::size_t b = a + 1; b < n; ++b) {
1383 const double v = 0.5 * (C(a, b) + C(b, a)); // drop the rounding asymmetry
1384 C(a, b) = v;
1385 C(b, a) = v;
1386 }
1387 return C;
1388 }
1389
1390 /**
1391 * The Jacobian of a reset policy at the exit mean, so a covariance can cross
1392 * the switch as `R C R'`.
1393 *
1394 * A reset policy is an arbitrary map on the (station x class) mean queue
1395 * lengths, and there is no general way to push a second moment through one.
1396 * The delta method is the first-order image, which is the order the whole
1397 * mean-field coupling works to. The two NAMED policies are linear and R is
1398 * then EXACT: the identity for `keep`, zero for `clear`. Linear indexing is
1399 * column-major, `ir = r*M + i`, the same index space as the covariance.
1400 */
1401 Matrix<double> reset_jacobian(const ResetMarginal& f, const Matrix<double>& qexit) const {
1402 const std::size_t n = M * K;
1403 Matrix<double> R(n, n, 0.0);
1404 if (!f) { // the identity map: no reset on this arc
1405 for (std::size_t a = 0; a < n; ++a) R(a, a) = 1.0;
1406 return R;
1407 }
1408 const Matrix<double> base = f(qexit);
1409 if (base.rows() != M || base.cols() != K) return R;
1410 double scale = 1.0;
1411 for (std::size_t i = 0; i < M; ++i)
1412 for (std::size_t r = 0; r < K; ++r) scale = std::max(scale, std::fabs(qexit(i, r)));
1413 const double step = 1e-6 * scale;
1414 for (std::size_t j = 0; j < n; ++j) {
1415 Matrix<double> qp = qexit;
1416 qp(j % M, j / M) += step;
1417 const Matrix<double> pert = f(qp);
1418 if (pert.rows() != M || pert.cols() != K) continue;
1419 for (std::size_t a = 0; a < n; ++a)
1420 R(a, j) = (pert(a % M, a / M) - base(a % M, a / M)) / step;
1421 }
1422 return R;
1423 }
1424
1425 /** The congruence `R C R'`, the image of a covariance under a linear map. */
1426 static Matrix<double> congruence(const Matrix<double>& R, const Matrix<double>& C) {
1427 const std::size_t n = R.rows();
1428 Matrix<double> RC(n, n, 0.0), out(n, n, 0.0);
1429 for (std::size_t a = 0; a < n; ++a)
1430 for (std::size_t b = 0; b < n; ++b) {
1431 double acc = 0.0;
1432 for (std::size_t c = 0; c < n; ++c) acc += R(a, c) * C(c, b);
1433 RC(a, b) = acc;
1434 }
1435 for (std::size_t a = 0; a < n; ++a)
1436 for (std::size_t b = 0; b < n; ++b) {
1437 double acc = 0.0;
1438 for (std::size_t c = 0; c < n; ++c) acc += RC(a, c) * R(b, c);
1439 out(a, b) = acc;
1440 }
1441 return out;
1442 }
1443
1444 /** The Stieltjes weights of a transition over the transient grid. */
1445 std::vector<double> cdf_weights(const mam::Map<double>& m, const std::vector<double>& t) const {
1446 std::vector<double> w(t.size(), 0.0);
1447 if (t.size() < 2) return w;
1448 // A disabled arc is the 1 x 1 zero pair, whose CDF is identically zero.
1449 bool all_zero = true;
1450 for (std::size_t a = 0; a < m.D1.rows() && all_zero; ++a)
1451 for (std::size_t b = 0; b < m.D1.cols() && all_zero; ++b)
1452 if (m.D1(a, b) != 0.0) all_zero = false;
1453 if (all_zero) return w;
1454 const std::vector<double> F = mam::map_cdf(m, t);
1455 for (std::size_t j = 1; j < t.size(); ++j) w[j] = F[j] - F[j - 1];
1456 return w;
1457 }
1458
1459 static double weighted(const std::vector<double>& v, const std::vector<double>& w,
1460 double wsum) {
1461 double s = 0.0;
1462 for (std::size_t j = 0; j < v.size() && j < w.size(); ++j) s += v[j] * w[j];
1463 return s / wsum;
1464 }
1465
1466 /**
1467 * `initFromMarginal` for a fluid stage: the mean queue length of every
1468 * (station, class) enters in PHASE ONE.
1469 *
1470 * The reference does NOT round this for a fluid stage
1471 * (`roundMarginalForDiscreteSolver` is skipped when the stage solver is
1472 * SolverFluid), because a fluid state is continuous -- rounding it would
1473 * quantize the very quantity the fixed point is iterating on.
1474 */
1475 std::vector<double> initsol_from_marginal(const qn::NetworkStruct<T>& sn,
1476 const Matrix<double>& Q) const {
1477 const fluid::FluidLayout L = fluid::fluid_layout(sn);
1478 std::vector<double> y(L.nstates, 0.0);
1479 for (std::size_t i = 0; i < sn.nstations && i < Q.rows(); ++i)
1480 for (std::size_t r = 0; r < sn.nclasses && r < Q.cols(); ++r) {
1481 if (!L.enabled[i][r]) continue;
1482 y[L.qidx[i][r]] = std::max(0.0, Q(i, r));
1483 }
1484 return y;
1485 }
1486
1487 Environment<T>& envObj;
1488 EnvOptions opt;
1489 std::size_t M = 0, K = 0;
1490 bool det_sojourn = false;
1491 bool statedep = false; ///< `method = "statedep"`: the arcs are rewritten each sweep
1492 bool ctmc_stages = false; ///< `stage_solver = "ctmc"`: enumerate each stage's chain
1493 bool meancov = false; ///< `method = "meancov"`: a covariance crosses beside the mean
1494 bool kp_stages = false; ///< `stage.method = "kp"`: the Ko-Pender limits run each stage
1495 bool dae_stages = false; ///< `stage.method = "dae"`: the linear-noise DAE runs each stage
1496 std::vector<double> dvals;
1497 std::vector<Matrix<double>> entry; ///< per-stage entry queue lengths
1498 /**
1499 * `meancov` only: per-stage entry COVARIANCE over the (station, class)
1500 * pairs, the companion of `entry`, and the WITHIN-STAGE covariance the stage
1501 * solver integrated, one matrix per point of `tranT`. `tranC[e]` is empty
1502 * for a stage solver carrying a first moment only, and the coupling then
1503 * keeps the timing variance of the sojourn alone.
1504 */
1505 std::vector<Matrix<double>> centry;
1506 std::vector<std::vector<Matrix<double>>> tranC;
1507 std::vector<std::vector<double>> tranT;
1508 std::vector<std::vector<std::vector<std::vector<double>>>> tranQ, tranU, tranTp;
1509 /**
1510 * Per stage, its steady tables and whether they have been asked for: the
1511 * exit value of every metric its transient carries no trajectory for. Asked
1512 * at most once per run and only where one is read; see `stage_steady`.
1513 */
1514 std::vector<StageSteady> steady;
1515 std::vector<char> steady_asked;
1516 /** Per LAYERED stage, the SolverLN that runs it; null for a flat stage. */
1517 std::vector<std::shared_ptr<ln::SolverLN<T>>> lnsolv;
1518 /** Per LAYERED stage, where each of its layers sits in the aggregate view. */
1519 std::vector<ln::LnLayerBlocks> lnblk;
1520};
1521
1522} // namespace env
1523} // namespace line
1524
1525#endif // LINE_SOLVERS_ENV_SOLVER_ENV_H
InputError(const std::string &what)
Definition error.h:39
UnsupportedError(const std::string &what)
Definition error.h:51
SolverEnv(Environment< T > &e, const EnvOptions &o)
Definition solver_env.h:231
EnvSolution solve()
Definition solver_env.h:233
Equilibrium distribution of a discrete-time Markov chain, and stochastic complementation.
A random environment: a port of matlab/src/lang/Environment.m, restricted to what SolverENV reads out...
The exception types the port throws.
The min-normal closure as a DIFFERENTIAL-ALGEBRAIC system: solver_fluid_dae.m.
Port of solver_fluid_kp.m: the fluid AND diffusion limits of the (MAP_t/Ph_t/inf)^N network of Y.
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.
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....
CtmcTransient< T > solver_ctmc_transient_analyzer(const NetworkStruct< T > &sn, const CtmcOptions &opt, const T &t0, const T &t1, const std::vector< T > &grid=std::vector< T >())
Port of solver_ctmc_transient_analyzer.m.
std::function< Matrix< double >(const Matrix< double > &)> ResetMarginal
The reset policy of a transition, resetFun in the reference.
Definition environment.h:84
ln::LnOptions env_default_lqn_options()
LnOptions as a LAYERED environment stage is solved with.
Definition solver_env.h:106
FluidSolution solver_fluid_kp(const qn::NetworkStruct< T > &sn, const FluidOptions &opt)
Port of solver_fluid_kp.m: the steady table at the horizon.
Definition fluid_kp.h:1055
FluidLayout fluid_layout(const qn::NetworkStruct< T > &sn)
Port of the layout half of solver_fluid_odes.m.
Definition fluid_odes.h:282
FluidSolution solver_fluid_dae(const qn::NetworkStruct< T > &sn, const FluidOptions &opt, const FluidDaeOptions &dopt_in=FluidDaeOptions())
solver_fluid_dae.m: the min-normal closure solved as one system.
Definition fluid_dae.h:2153
std::vector< FluidTranPoint > solver_fluid_transient(const qn::NetworkStruct< T > &sn, const FluidOptions &opt, double t_end, std::size_t points=101, const std::vector< double > &out_grid=std::vector< double >())
Port of @@SolverFLD/getTranAvg: the metrics along the trajectory, not just at the fixed point.
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 ...
std::vector< FluidTranPoint > solver_fluid_dae_transient(const qn::NetworkStruct< T > &sn, const FluidOptions &opt, double t_end, std::size_t points=101, const std::vector< double > &out_grid=std::vector< double >(), const FluidDaeOptions &dopt_in=FluidDaeOptions())
@@SolverFLD/getTranAvg for the DAE route: the metrics ALONG the trajectory.
Definition fluid_dae.h:2611
FluidSolution solver_fluid_kp_core(const qn::NetworkStruct< T > &sn, const FluidOptions &opt, FluidKpTransient *tran, const std::vector< double > &out_grid=std::vector< double >())
The Ko-Pender solve, returning both the steady table and the covariance trajectory so that neither ha...
Definition fluid_kp.h:314
T dist_cdf(const Distrib< T > &d, const T &x)
F(x) = P{X <= x}, MATLAB's Distribution.evalCDF.
T dist_moment(const Distrib< T > &d, unsigned k)
The k-th raw moment.
T map_mean(const Map< T > &m)
Mean inter-arrival time, 1/lambda.
Definition map_moment.h:101
std::vector< T > map_cdf(const Map< T > &m, const std::vector< T > &points)
Cumulative distribution of the inter-arrival time at the given points.
Definition map_cdf.h:63
std::vector< T > dtmc_solve(const Matrix< T > &P)
Stationary distribution of a stochastic matrix P.
Definition dtmc_solve.h:106
bool from_marginal_node_first(const NetworkStruct< T > &sn, std::size_t ind, const std::vector< std::size_t > &n, const std::vector< std::size_t > &phases, std::vector< T > &out)
The FIRST row from_marginal_node emits, BUILT rather than enumerated.
Definition state.h:2022
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
Port of solver_ctmc_transient_analyzer.m: the time-dependent counterpart of solver_ctmc_analyzer,...
SolverFluid: the closing method, a port of solver_fluid.m, solver_fluid_iteration....
SolverLN: layered decomposition of a layered queueing network.
Options of SolverENV.
Definition solver_env.h:113
std::string method
The inter-stage coupling: meanfield is the reference's default.
Definition solver_env.h:125
std::string stage_solver
Which solver runs each FLAT stage: the fluid transient or the enumerated CTMC.
Definition solver_env.h:123
std::string sojourn
options.sojourn: stochastic (default) or deterministic.
Definition solver_env.h:127
fluid::FluidOptions stage
Options handed to each stage solver.
Definition solver_env.h:129
double stage_cutoff
options.cutoff of a CTMC stage, read only when stage_solver is ctmc.
Definition solver_env.h:134
double timespan_end
options.timespan(2) of the inner solver: the transient horizon.
Definition solver_env.h:136
ln::LnOptions lqn
Options of the SolverLN that runs a LAYERED stage.
Definition solver_env.h:166
std::size_t tran_points
Points on a UNIFORM transient grid, used only where stage_grid declines to build one (a stage whose h...
Definition solver_env.h:149
What SolverENV reports.
Definition solver_env.h:170
Matrix< double > UN
Definition solver_env.h:172
std::vector< Matrix< double > > Qentry
The entry queue lengths the fixed point converged to.
Definition solver_env.h:176
Matrix< double > QCov
meancov only: the environment-wide queue-length COVARIANCE over the (station, class) pairs,...
Definition solver_env.h:183
Matrix< double > TN
Definition solver_env.h:172
Matrix< double > QN
Environment-averaged metrics, (nstations x nclasses).
Definition solver_env.h:172
Matrix< double > QVar
Definition solver_env.h:183
std::vector< Matrix< double > > TExit
Definition solver_env.h:174
std::vector< Matrix< double > > UExit
Definition solver_env.h:174
std::vector< Matrix< double > > QExit
Per-stage, sojourn-averaged metrics.
Definition solver_env.h:174
Controls, defaulting to SolverOptions('Fluid') in the reference.
Options of SolverLN.
Definition solver_ln.h:266
std::string layer_solver
Which solver runs each layer: mva, nc, fluid or ssa.
Definition solver_ln.h:308