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` (which selects no analyzer of its
63 * own -- see `init`), `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
90#include "line/util/error.h"
91#include "line/util/matrix.h"
92
93namespace line {
94namespace env {
95
96/**
97 * `LnOptions` as a LAYERED environment stage is solved with.
98 *
99 * Only the layer engine differs from the SolverLN default, and it differs
100 * because the mean-field coupling has no use for a stage it cannot integrate:
101 * see `EnvOptions::lqn`.
102 */
105 o.layer_solver = "fluid";
106 return o;
107}
108
109/** Options of SolverENV. Defaults are `Solver.defaultOptions`. */
111 int iter_max = 100;
112 double iter_tol = 1e-4;
113 /**
114 * Which solver runs each FLAT stage: the fluid transient or the enumerated
115 * CTMC. A LAYERED stage is run by SolverLN whatever this says, and its own
116 * layer engine is named by `lqn.layer_solver`; asking for `ctmc` alongside a
117 * layered stage is refused rather than reinterpreted, since an LQN has no
118 * single generator to enumerate.
119 */
120 std::string stage_solver = "fluid";
121 /** The inter-stage coupling: `meanfield` is the reference's default. */
122 std::string method = "meanfield";
123 /** `options.sojourn`: `stochastic` (default) or `deterministic`. */
124 std::string sojourn = "stochastic";
125 /** Options handed to each stage solver. */
127 /**
128 * `options.cutoff` of a CTMC stage, read only when `stage_solver` is ctmc.
129 * An open stage needs one; a closed one enumerates its own population.
130 */
131 double stage_cutoff = -1.0;
132 /** `options.timespan(2)` of the inner solver: the transient horizon. */
133 double timespan_end = 100.0;
134 /**
135 * Points on a UNIFORM transient grid, used only where `stage_grid` declines
136 * to build one (a stage whose holding time is the disabled 1 x 1 zero pair).
137 *
138 * It used to be the accuracy knob of the whole method, because the exit
139 * metrics are a Riemann-Stieltjes sum over whatever grid the stage was
140 * integrated on and a uniform grid resolves the HORIZON rather than the
141 * sojourn. Since 2026-08-11 each stage is integrated on the grid the
142 * sojourn asks for -- 90% of the points under `5*E[S]` -- so the answer no
143 * longer moves with this: on renv_node_breakdown the horizon may be 100 or
144 * 1000 and Server QLen is 0.458854 or 0.459191.
145 */
146 std::size_t tran_points = 1001;
147 /**
148 * Options of the `SolverLN` that runs a LAYERED stage.
149 *
150 * The reference names the stage solver by handing SolverENV a FACTORY --
151 * `ENV(env, @(m) LN(m, @(mm) FLD(mm), 'timespan', [0 T]))` -- and this field
152 * is that factory's argument list, because a C++ template cannot take a
153 * MATLAB function handle. `timespan_end`, `tran_points` and `tran_grid` are
154 * OVERWRITTEN by the environment for every stage: the horizon and the grid
155 * the exit average is summed on belong to the coupling, not to a stage.
156 *
157 * `layer_solver` defaults to `fluid` here where `LnOptions` alone defaults
158 * to `mva`, and the difference is not a preference: the coupling carries
159 * queue lengths across a switch and therefore needs each stage's
160 * TRANSIENT, which only the fluid layer engine produces. A caller who names
161 * another engine is refused by `init`, not quietly served the fluid one.
162 */
164};
165
166/** What SolverENV reports. */
168 /** Environment-averaged metrics, (nstations x nclasses). */
170 /** Per-stage, sojourn-averaged metrics. */
171 std::vector<Matrix<double>> QExit, UExit, TExit;
172 /** The entry queue lengths the fixed point converged to. */
173 std::vector<Matrix<double>> Qentry;
174 int iterations = 0;
175 bool converged = false;
176};
177
178namespace detail {
179
180/**
181 * `maxpe(approx, exact)`: max |1 - approx/exact| over the entries where exact
182 * is nonzero, which is what the reference's convergence test compares.
183 */
184inline double env_maxpe(const std::vector<double>& approx, const std::vector<double>& exact) {
185 double worst = -1.0;
186 for (std::size_t i = 0; i < approx.size() && i < exact.size(); ++i) {
187 if (exact[i] == 0.0) continue;
188 const double e = std::fabs(1.0 - approx[i] / exact[i]);
189 if (e > worst) worst = e;
190 }
191 return worst; // negative means "no comparable entry", the reference's empty
192}
193
194/** Linear interpolation of a transient metric at `d`, clamped to the grid. */
195inline double env_det_eval(const std::vector<double>& t, const std::vector<double>& metric,
196 double d) {
197 if (t.empty()) return 0.0;
198 d = std::max(t.front(), std::min(d, t.back()));
199 for (std::size_t j = 1; j < t.size(); ++j) {
200 if (d <= t[j]) {
201 const double dt = t[j] - t[j - 1];
202 if (!(dt > 0.0)) return metric[j];
203 const double a = (d - t[j - 1]) / dt;
204 return metric[j - 1] * (1.0 - a) + metric[j] * a;
205 }
206 }
207 return metric.back();
208}
209
210} // namespace detail
211
212/**
213 * The environment solver.
214 *
215 * `envObj` must already carry a model per stage; `init()` is called here, as
216 * `SolverENV.init` does.
217 */
218template <class T>
220public:
221 SolverEnv(Environment<T>& e, const EnvOptions& o) : envObj(e), opt(o) { init(); }
222
224 const std::size_t E = envObj.nstages();
225 EnvSolution out;
226 out.QN = Matrix<double>(M, K, 0.0);
227 out.UN = Matrix<double>(M, K, 0.0);
228 out.TN = Matrix<double>(M, K, 0.0);
229
230 pre();
231 std::vector<std::vector<double>> qfirst_prev(E), qfirst_curr(E);
232 int it = 0;
233 for (it = 1; it <= opt.iter_max; ++it) {
234 for (std::size_t e = 0; e < E; ++e) analyze(e);
235 // The reference's convergence test compares the FIRST point of the
236 // transient -- the entry queue length -- across iterations.
237 qfirst_prev = qfirst_curr;
238 qfirst_curr.assign(E, std::vector<double>());
239 for (std::size_t e = 0; e < E; ++e) {
240 qfirst_curr[e].reserve(M * K);
241 for (std::size_t i = 0; i < M; ++i)
242 for (std::size_t r = 0; r < K; ++r) qfirst_curr[e].push_back(tranQ[e][i][r][0]);
243 }
244 bool conv = it > 1;
245 if (conv)
246 for (std::size_t e = 0; e < E && conv; ++e) {
247 const double d = detail::env_maxpe(qfirst_curr[e], qfirst_prev[e]);
248 if (d < 0.0) continue; // nothing comparable, treated as converged
249 if (!std::isfinite(d) || d >= opt.iter_tol) conv = false;
250 }
251 post();
252 if (conv) {
253 out.converged = true;
254 break;
255 }
256 }
257 out.iterations = std::min(it, opt.iter_max);
258 finish(out);
259 return out;
260 }
261
262private:
263 void init() {
264 if (opt.stage_solver != "fluid" && opt.stage_solver != "ctmc")
265 throw UnsupportedError(
266 "SolverENV: stage solver '" + opt.stage_solver +
267 "' is not available; the environment coupling needs a TRANSIENT stage solve and "
268 "only the fluid analyzer and the enumerated CTMC provide one in this port");
269 ctmc_stages = (opt.stage_solver == "ctmc");
270 if (opt.method == "statevec" || opt.method == "blend")
271 throw UnsupportedError(
272 "SolverENV: the state-vector analyzer (solver_env_statevec_analyzer.m) carries the "
273 "full joint distribution across a switch, which THIS class does not: it is the "
274 "mean-field coupling and carries the marginal means. The state-vector coupling is "
275 "ported as env::SolverEnvStatevec, with its own options and its CTMC stage solver; "
276 "reach it through env::solver_env (env_dispatch.h), which is the analyzer "
277 "selection of SolverENV.init");
278 if (opt.method == "avg" || opt.method == "dec")
279 throw UnsupportedError(
280 "SolverENV: the closed-form fast/slow environment limits (SolverENV.solveEnvLimit) "
281 "replace this fixed point rather than configure it -- neither carries anything "
282 "across a switch. They are ported as env::SolverEnvLimit, with their own result "
283 "type; reach them through env::solver_env (env_dispatch.h), which is the method "
284 "dispatch of SolverENV.runAnalyzer");
285 // `smp` is the reference's escape hatch for a NON-MARKOVIAN environment
286 // transition, and it selects no analyzer of its own: it only lifts the
287 // constructor check that every arc is Markovian, after which the
288 // mean-field analyzer runs unchanged. An arc here is a (D0, D1) pair by
289 // construction, so there is no check to lift and nothing else to do.
290 statedep = (opt.method == "statedep");
291 // "default" IS the reference's spelling of this coupling: SolverENV.m
292 // installs solver_env_meanfield_analyzer for every method that is not
293 // statevec/blend, and its listValidMethods leads with 'default'. Only
294 // the CLI normalised it away, so a caller constructing SolverEnv
295 // directly with the reference's own name was refused.
296 if (opt.method != "meanfield" && opt.method != "default" && opt.method != "smp"
297 && !statedep)
298 throw UnsupportedError("SolverENV: unknown method '" + opt.method + "'");
299 if (!(opt.timespan_end > 0.0) || !std::isfinite(opt.timespan_end))
300 throw InputError(
301 "SolverENV: the stage transient needs a finite positive horizon, "
302 "options.timespan(2); the mean-field coupling integrates each stage's "
303 "TRAJECTORY against the holding-time CDF and has nothing to integrate over an "
304 "infinite one");
305 if (opt.tran_points < 2)
306 throw InputError("SolverENV: the transient grid needs at least two points");
307 if (!std::is_same<T, double>::value)
308 throw UnsupportedError(
309 "SolverENV: a fluid stage integrates its drift with LSODA, which is double only; "
310 "rerun with --arith double");
311
312 envObj.init();
313 const std::size_t E = envObj.nstages();
314 build_lqn_stages();
315 M = stage_M(0);
316 K = stage_K(0);
317 for (std::size_t e = 1; e < E; ++e)
318 if (stage_M(e) != M || stage_K(e) != K)
319 throw InputError(
320 "SolverENV: every stage must have the same stations and classes; the metrics "
321 "are blended entrywise across them. A LAYERED stage counts the block-diagonal "
322 "union of the layers SolverLN builds for it, which is what layerBlocks "
323 "reports, so two layered stages agree exactly when they have the same layer "
324 "shape -- not merely the same LQN element count");
325 entry.assign(E, Matrix<double>(M, K, 0.0));
326 tranT.assign(E, std::vector<double>());
327 tranQ.assign(E, std::vector<std::vector<std::vector<double>>>());
328 tranU.assign(E, std::vector<std::vector<std::vector<double>>>());
329 tranTp.assign(E, std::vector<std::vector<std::vector<double>>>());
330 det_sojourn = (opt.sojourn == "deterministic");
331 dvals.assign(E, 0.0);
332 refresh_sojourn_means();
333 if (statedep) {
334 bool any = false;
335 for (std::size_t e = 0; e < E; ++e)
336 for (std::size_t h = 0; h < E; ++h)
337 if (envObj.arc(e, h).enabled && envObj.arc(e, h).reset_rates) any = true;
338 if (!any)
339 throw InputError(
340 "SolverENV: method 'statedep' updates each environment transition from the "
341 "state its stage is left in, and no arc carries a rate reset "
342 "(Environment::set_env_rate_reset, resetEnvRatesFun in the reference); the "
343 "run would be the mean-field fixed point under a different name");
344 }
345 }
346
347 // ---- layered (LayeredNetwork) stages ---------------------------------
348
349 /**
350 * Build one persistent `SolverLN` per LAYERED stage.
351 *
352 * ONE SOLVER FOR THE WHOLE RUN, not one per sweep, and that is the
353 * reference's arrangement too (`self.solvers{e}` outlives the iteration).
354 * It matters twice over: the layer ensemble is what carries the aggregate
355 * (station, class) SHAPE this coupling blends over, and it has to be known
356 * before any stage is solved; and the layered fixed point is solved ONCE,
357 * because a steady solve ignores the state it is started from -- only the
358 * transient reads it -- so re-running it every environment sweep would
359 * recompute the same numbers.
360 */
361 void build_lqn_stages() {
362 const std::size_t E = envObj.nstages();
363 lnsolv.assign(E, std::shared_ptr<ln::SolverLN<T>>());
364 lnblk.assign(E, ln::LnLayerBlocks());
365 if (!envObj.has_lqn_stages()) return;
366 if (ctmc_stages)
367 throw UnsupportedError(
368 "SolverENV: a LayeredNetwork stage is solved by SolverLN over its layers, not by "
369 "an enumerated CTMC over one stage generator -- an LQN has no single generator to "
370 "enumerate. Leave stage_solver at 'fluid', which names the engine each LAYER is "
371 "integrated with (EnvOptions::lqn.layer_solver)");
372 if (opt.lqn.layer_solver != "fluid")
373 throw UnsupportedError(
374 "SolverENV: a LayeredNetwork stage is carried across an environment switch by its "
375 "queue lengths, so the coupling needs the stage's TRANSIENT; among the layer "
376 "engines only the fluid one produces one, and this ensemble asks for '" +
377 opt.lqn.layer_solver + "' layers (EnvOptions::lqn.layer_solver)");
378 for (std::size_t e = 0; e < E; ++e) {
379 if (!envObj.is_lqn(e)) continue;
380 ln::LnOptions lo = opt.lqn;
381 // THE HORIZON AND THE GRID BELONG TO THE COUPLING. A stage lasts as
382 // long as the environment lets it, and the exit average is summed
383 // against the holding-time CDF, so neither is a property of the LQN
384 // and a caller's value for either would silently change what the
385 // Stieltjes sum integrates over. `tran_grid` is installed per sweep
386 // by `lqn_stage_transient`, since a state-dependent method may move
387 // the holding time between sweeps.
388 lo.timespan_end = opt.timespan_end;
389 lo.tran_points = opt.tran_points;
390 lo.tran_grid.clear();
391 lnsolv[e] = std::make_shared<ln::SolverLN<T>>(envObj.stage(e).lqn_model, lo);
392 lnblk[e] = lnsolv[e]->layer_blocks();
393 }
394 }
395
396 /** Stations of stage `e`: its own, or the block-diagonal union of its layers. */
397 std::size_t stage_M(std::size_t e) const {
398 return envObj.is_lqn(e) ? lnblk[e].M : envObj.stage(e).model.nstations;
399 }
400
401 /** Classes of stage `e`: its own, or the block-diagonal union of its layers. */
402 std::size_t stage_K(std::size_t e) const {
403 return envObj.is_lqn(e) ? lnblk[e].K : envObj.stage(e).model.nclasses;
404 }
405
406 /**
407 * One LAYERED stage's transient, assembled block-diagonally into the same
408 * `tranT`/`tranQ`/`tranU`/`tranTp` the flat path fills.
409 *
410 * `seed` says whether the fixed point has an entry vector yet, exactly as it
411 * does on the CTMC path: `pre()` runs before it has one, and warm-starting
412 * from an all-zero matrix would empty every closed layer rather than leave
413 * it on its default state.
414 *
415 * THE OFF-DIAGONAL BLOCKS ARE ZERO AND ARE ALLOCATED ANYWAY. They pair a
416 * station of one layer with a class of another and stand for nothing, which
417 * is why the reference leaves them as empty cells; here they must still be
418 * present and sized, because the convergence test reads the FIRST point of
419 * every (station, class) series and an absent series has no first point.
420 * Zero series contribute nothing to the blend and are skipped by `maxpe`,
421 * which ignores an exact value of zero -- so the two ports agree on the
422 * numbers as well as on the shape.
423 */
424 void lqn_stage_transient(std::size_t e, bool seed) {
425 ln::SolverLN<T>& s = *lnsolv[e];
426 s.init_from_marginal(seed ? entry[e] : Matrix<double>());
427 s.set_tran_grid(stage_grid(e));
428 const ln::LnTranSolution tr = s.get_tran_avg();
429 const ln::LnLayerBlocks& b = lnblk[e];
430
431 // Every layer was integrated on the same grid -- one horizon, one
432 // out_grid -- so layer one's is the stage's time base.
433 tranT[e].clear();
434 if (!tr.layers.empty()) tranT[e] = tr.layers[0].t;
435 const std::size_t P = std::max<std::size_t>(tranT[e].size(), 1);
436 tranQ[e].assign(M, std::vector<std::vector<double>>(K, std::vector<double>(P, 0.0)));
437 tranU[e].assign(M, std::vector<std::vector<double>>(K, std::vector<double>(P, 0.0)));
438 tranTp[e].assign(M, std::vector<std::vector<double>>(K, std::vector<double>(P, 0.0)));
439 if (tranT[e].empty()) return;
440
441 // `b` and the blocks were both derived from the SAME layer ensemble --
442 // it is built once, in SolverLN's constructor, and never resized -- so
443 // `msz`/`ksz` are the exact extents of each block and the offsets tile
444 // the aggregate exactly. Nothing here is a bounds guard.
445 for (std::size_t l = 0; l < tr.layers.size(); ++l) {
446 const ln::LnTranLayer& L = tr.layers[l];
447 for (std::size_t i = 0; i < b.msz[l]; ++i)
448 for (std::size_t r = 0; r < b.ksz[l]; ++r) {
449 const std::size_t row = b.roff[l] + i, col = b.coff[l] + r;
450 copy_series(L.QN[i][r], tranQ[e][row][col]);
451 copy_series(L.UN[i][r], tranU[e][row][col]);
452 copy_series(L.TN[i][r], tranTp[e][row][col]);
453 }
454 }
455 }
456
457 /**
458 * Copy a layer series onto the stage grid.
459 *
460 * The two lengths agree by construction -- every layer was integrated on the
461 * grid this stage's time base came from -- and the loop is written over
462 * whichever is shorter so that a layer whose integration was cut short
463 * leaves the tail at zero rather than reading past its own trajectory.
464 */
465 static void copy_series(const std::vector<double>& src, std::vector<double>& dst) {
466 for (std::size_t j = 0; j < dst.size() && j < src.size(); ++j) dst[j] = src[j];
467 }
468
469 /** The mean holding times the deterministic sojourn evaluates at. */
470 void refresh_sojourn_means() {
471 if (!det_sojourn) return;
472 for (std::size_t e = 0; e < envObj.nstages(); ++e)
473 dvals[e] = mam::map_mean(envObj.hold_time[e].map());
474 }
475
476 /**
477 * `pre_`: seed every stage from its own solve at the first iteration, so
478 * the fixed point starts somewhere the model could be.
479 *
480 * Which solve is the reference's branch on the STAGE horizon: a finite one
481 * takes the LAST POINT of the stage transient, and only an infinite one --
482 * which this coupling refuses in `init`, since it has no transient to
483 * integrate -- takes the steady state. The distinction is not cosmetic on a
484 * stage that is unstable on its own -- a broken server offered more than it
485 * can serve -- where the steady state is whatever the integrator's own
486 * horizon happened to reach and is far from the transient at
487 * `timespan_end`. The seed only moves by the drift per environment cycle,
488 * so a seed off by a factor of ten costs iterations proportionally.
489 */
490 void pre() {
491 const std::size_t E = envObj.nstages();
492 for (std::size_t e = 0; e < E; ++e) {
493 if (envObj.is_lqn(e)) {
494 // The layered seed is the reference's own: the LAST POINT of the
495 // stage transient run from the layers' default state, which for
496 // a finite horizon is what `pre_` takes for every stage solver.
497 lqn_stage_transient(e, false);
498 if (tranT[e].empty()) continue;
499 const std::size_t last = tranT[e].size() - 1;
500 for (std::size_t i = 0; i < M; ++i)
501 for (std::size_t r = 0; r < K; ++r) entry[e](i, r) = tranQ[e][i][r][last];
502 continue;
503 }
504 if (ctmc_stages) {
505 // A CTMC stage has no "seed from nowhere" solve to run: it
506 // starts from the model's OWN initial state, which is what
507 // `initDefault` put there, so the first sweep enters at that
508 // state's queue lengths rather than at zero.
509 const ctmc::CtmcTransient<T> tr = ctmc_stage_transient(e, false, std::vector<double>());
510 if (tr.t.empty()) continue;
511 for (std::size_t i = 0; i < M; ++i)
512 for (std::size_t r = 0; r < K; ++r)
513 entry[e](i, r) = num_traits<T>::to_double(tr.QNt[i][r].back());
514 continue;
515 }
516 fluid::FluidOptions fo = opt.stage;
517 fo.init_sol.clear();
518 const std::vector<fluid::FluidTranPoint> tr = fluid::solver_fluid_transient(
519 envObj.stage(e).model, fo, opt.timespan_end, opt.tran_points);
520 if (tr.empty()) continue;
521 for (std::size_t i = 0; i < M; ++i)
522 for (std::size_t r = 0; r < K; ++r) entry[e](i, r) = tr.back().QN(i, r);
523 }
524 }
525
526 /** `analyze_`: the transient of one stage from its entry queue lengths. */
527 void analyze(std::size_t e) {
528 if (envObj.is_lqn(e)) {
529 lqn_stage_transient(e, true);
530 return;
531 }
532 tranT[e].clear();
533 tranQ[e].assign(M, std::vector<std::vector<double>>(K));
534 tranU[e].assign(M, std::vector<std::vector<double>>(K));
535 tranTp[e].assign(M, std::vector<std::vector<double>>(K));
536 if (ctmc_stages) {
537 // THE REFINED GRID, and it was MEASURED against the alternative.
538 // The reference forms its Stieltjes sum on whatever points ode23
539 // stopped at, so reporting on those looks like the faithful choice;
540 // on renv_threestages_repairmen it is the worse one -- Queue1 QLen
541 // 0.8344 against the reference's 0.83053, where the refined grid
542 // gives 0.83092. The refined grid puts 90% of its points under
543 // 5*E[S], which is where the holding-time CDF puts its mass, so it
544 // resolves the integrand rather than the trajectory. The residual
545 // 4.7e-4 is what is left of this quadrature difference.
546 const ctmc::CtmcTransient<T> tr = ctmc_stage_transient(e, true, stage_grid(e));
547 for (std::size_t j = 0; j < tr.t.size(); ++j) {
548 tranT[e].push_back(num_traits<T>::to_double(tr.t[j]));
549 for (std::size_t i = 0; i < M; ++i)
550 for (std::size_t r = 0; r < K; ++r) {
551 tranQ[e][i][r].push_back(num_traits<T>::to_double(tr.QNt[i][r][j]));
552 tranU[e][i][r].push_back(num_traits<T>::to_double(tr.UNt[i][r][j]));
553 tranTp[e][i][r].push_back(num_traits<T>::to_double(tr.TNt[i][r][j]));
554 }
555 }
556 return;
557 }
558 const qn::NetworkStruct<T>& sn = envObj.stage(e).model;
559 fluid::FluidOptions fo = opt.stage;
560 fo.init_sol = initsol_from_marginal(sn, entry[e]);
561 const std::vector<fluid::FluidTranPoint> tr = fluid::solver_fluid_transient(
562 sn, fo, opt.timespan_end, opt.tran_points, stage_grid(e));
563 for (const fluid::FluidTranPoint& p : tr) {
564 tranT[e].push_back(p.t);
565 for (std::size_t i = 0; i < M; ++i)
566 for (std::size_t r = 0; r < K; ++r) {
567 tranQ[e][i][r].push_back(p.QN(i, r));
568 tranU[e][i][r].push_back(p.UN(i, r));
569 tranTp[e][i][r].push_back(p.TN(i, r));
570 }
571 }
572 }
573
574 /**
575 * One stage's transient under an ENUMERATED CTMC, from its entry marginal.
576 *
577 * THE MARGINAL IS ROUNDED HERE and is not for a fluid stage, which is the
578 * reference's own split (`roundMarginalForDiscreteSolver` runs for every
579 * stage solver except SolverFluid): a chain has no state holding 1.7 jobs,
580 * so the fixed point's continuous iterate has to be placed on the lattice
581 * before it can be a state at all. The rounding conserves each closed
582 * class's population, so the placed state is one the chain actually
583 * contains.
584 *
585 * The state is written onto a COPY of the stage struct as its one-row
586 * declared state space, which is the same channel `setState` reaches
587 * `solver_ctmc_transient_analyzer` through -- so this seeds the stage
588 * exactly as a caller would, rather than through a private door.
589 */
590 ctmc::CtmcTransient<T> ctmc_stage_transient(std::size_t e, bool seed,
591 const std::vector<double>& grid) const {
592 qn::NetworkStruct<T> sn = envObj.stage(e).model;
593 // `seed` is explicit and not inferred from the arguments: `pre()` runs
594 // BEFORE the fixed point has an entry vector, and its all-zero matrix
595 // would place every job nowhere -- a state the chain does not have.
596 if (seed) seed_ctmc_state(sn, entry[e]);
597 std::vector<T> g;
598 g.reserve(grid.size());
599 for (std::size_t j = 0; j < grid.size(); ++j) g.push_back(num_traits<T>::from_double(grid[j]));
601 sn, ctmc_stage_options(), num_traits<T>::from_int(0),
602 num_traits<T>::from_double(opt.timespan_end), g);
603 }
604
605 /** The CTMC options a stage is solved with; the cutoff is the caller's. */
606 ctmc::CtmcOptions ctmc_stage_options() const {
607 ctmc::CtmcOptions co;
608 co.cutoff = opt.stage_cutoff;
609 return co;
610 }
611
612 /**
613 * Write the rounded entry marginal onto `sn` as its declared state.
614 *
615 * A row that the encoding cannot realize leaves the station alone, so the
616 * analyzer falls back to that station's default marking rather than being
617 * handed a state the chain does not contain -- an error the coupling could
618 * not act on, where the default is at least a state of the same model.
619 */
620 void seed_ctmc_state(qn::NetworkStruct<T>& sn, const Matrix<double>& Q) const {
621 if (Q.empty()) return;
622 const std::vector<std::vector<std::size_t>> nir = round_marginal(sn, Q);
623 for (std::size_t i = 0; i < sn.nstations && i < nir.size(); ++i) {
624 const std::size_t ind = sn.station_to_node[i]; // 1-based node index
625 std::vector<std::size_t> ph(sn.nclasses, 1);
626 for (std::size_t r = 0; r < sn.nclasses; ++r) ph[r] = sn.phasessz_of(i + 1, r + 1);
627 std::vector<T> row;
628 if (!qn::from_marginal_node_first(sn, ind, nir[i], ph, row))
629 continue;
630 Matrix<T> space(1, row.size());
631 for (std::size_t c = 0; c < row.size(); ++c) space(0, c) = row[c];
632 sn.statespace[ind] = space;
633 sn.stateprior[ind] = std::vector<T>(1, num_traits<T>::from_int(1));
634 }
635 }
636
637 /**
638 * `roundMarginalForDiscreteSolver`: the continuous per-class marginal on the
639 * integer lattice, conserving each CLOSED class's population.
640 *
641 * Rounding entrywise does not conserve it -- three stations holding 0.5 jobs
642 * of a one-job class round to 0, 0, 0 and the class disappears from the
643 * chain -- so the residual is handed to the stations with the largest
644 * fractional parts, which is the reference's own rule.
645 */
646 std::vector<std::vector<std::size_t>> round_marginal(const qn::NetworkStruct<T>& sn,
647 const Matrix<double>& Q) const {
648 std::vector<std::vector<std::size_t>> n(sn.nstations, std::vector<std::size_t>(sn.nclasses, 0));
649 for (std::size_t r = 0; r < sn.nclasses; ++r) {
650 const double pop = sn.classes[r].population;
651 std::vector<double> frac(sn.nstations, 0.0);
652 long placed = 0;
653 for (std::size_t i = 0; i < sn.nstations; ++i) {
654 const double q = (i < Q.rows() && r < Q.cols()) ? std::max(0.0, Q(i, r)) : 0.0;
655 const double fl = std::floor(q);
656 n[i][r] = static_cast<std::size_t>(fl);
657 frac[i] = q - fl;
658 placed += static_cast<long>(fl);
659 }
660 if (!std::isfinite(pop)) continue; // an open class has no population to conserve
661 long want = static_cast<long>(std::llround(pop));
662 while (placed < want) {
663 std::size_t best = 0;
664 double bv = -1.0;
665 for (std::size_t i = 0; i < sn.nstations; ++i)
666 if (frac[i] > bv) { bv = frac[i]; best = i; }
667 ++n[best][r];
668 frac[best] = -1.0;
669 ++placed;
670 if (bv < 0.0) break; // nowhere left to place; the loop would not terminate
671 }
672 while (placed > want) {
673 std::size_t best = 0;
674 double bv = 2.0;
675 bool any = false;
676 for (std::size_t i = 0; i < sn.nstations; ++i)
677 if (n[i][r] > 0 && frac[i] < bv) { bv = frac[i]; best = i; any = true; }
678 if (!any) break;
679 --n[best][r];
680 frac[best] = 2.0;
681 --placed;
682 }
683 }
684 return n;
685 }
686
687 /**
688 * `post_`: average each stage's transient over WHEN it hands over to each
689 * destination, then re-seed every stage from its predecessors.
690 */
691 void post() {
692 const std::size_t E = envObj.nstages();
693 std::vector<std::vector<Matrix<double>>> Qexit(
694 E, std::vector<Matrix<double>>(E, Matrix<double>(M, K, 0.0)));
695 // The other two exit metrics are read only by a state-dependent rate,
696 // which is given all three; without one they are never looked at, and
697 // the reference computes them in the same loop regardless.
698 std::vector<std::vector<Matrix<double>>> Uexit, Texit;
699 if (statedep) {
700 Uexit.assign(E, std::vector<Matrix<double>>(E, Matrix<double>(M, K, 0.0)));
701 Texit.assign(E, std::vector<Matrix<double>>(E, Matrix<double>(M, K, 0.0)));
702 }
703 for (std::size_t e = 0; e < E; ++e) {
704 if (tranT[e].empty()) continue;
705 if (det_sojourn) {
706 Matrix<double> Qd(M, K, 0.0), Ud(M, K, 0.0), Td(M, K, 0.0);
707 for (std::size_t i = 0; i < M; ++i)
708 for (std::size_t r = 0; r < K; ++r) {
709 Qd(i, r) = detail::env_det_eval(tranT[e], tranQ[e][i][r], dvals[e]);
710 if (!statedep) continue;
711 Ud(i, r) = detail::env_det_eval(tranT[e], tranU[e][i][r], dvals[e]);
712 Td(i, r) = detail::env_det_eval(tranT[e], tranTp[e][i][r], dvals[e]);
713 }
714 for (std::size_t h = 0; h < E; ++h) {
715 Qexit[e][h] = Qd;
716 if (!statedep) continue;
717 Uexit[e][h] = Ud;
718 Texit[e][h] = Td;
719 }
720 continue;
721 }
722 // THE WEIGHT IS THE STAGE SOJOURN, NOT THE e -> h CLOCK, and the
723 // reference says so where it builds it: competing exponentials leave
724 // the exit TIME independent of which destination won, so the exit
725 // average does not depend on h at all -- h enters only through the
726 // reset applied on the way in. Weighting by `proc[e][h]` instead read
727 // the transient over the mean of ONE risk rather than of their
728 // minimum: on renv_twostages_repairmen, whose Stage2 competes a 0.5
729 // self arc with a 0.5 arc back, that is a mean of 2 against the
730 // sojourn's 1, and it reported Queue1 QLen 0.55882 against MATLAB's
731 // 0.55550 with the two stations' throughputs 1.4% apart in a closed
732 // cycle that admits one throughput.
733 const std::vector<double> w = cdf_weights(envObj.hold_time[e].map(), tranT[e]);
734 double wsum = 0.0;
735 for (double v : w) wsum += v;
736 if (!(wsum > 0.0)) continue; // the stage never leaves
737 for (std::size_t i = 0; i < M; ++i)
738 for (std::size_t r = 0; r < K; ++r) {
739 const double q = weighted(tranQ[e][i][r], w, wsum);
740 double u = 0.0, t = 0.0;
741 if (statedep) {
742 u = weighted(tranU[e][i][r], w, wsum);
743 t = weighted(tranTp[e][i][r], w, wsum);
744 }
745 for (std::size_t h = 0; h < E; ++h) {
746 Qexit[e][h](i, r) = q;
747 if (!statedep) continue;
748 Uexit[e][h](i, r) = u;
749 Texit[e][h](i, r) = t;
750 }
751 }
752 }
753
754 for (std::size_t e = 0; e < E; ++e) {
755 if (tranT[e].empty()) continue;
756 Matrix<double> Qe(M, K, 0.0);
757 for (std::size_t h = 0; h < E; ++h) {
758 const double p = envObj.prob_orig(h, e);
759 if (!(p > 0.0)) continue;
760 const ResetMarginal& f = envObj.arc(h, e).reset;
761 const Matrix<double> reset = f ? f(Qexit[h][e]) : Qexit[h][e];
762 if (reset.rows() != M || reset.cols() != K)
763 throw InputError(
764 "SolverENV: a reset policy returned a matrix of the wrong shape");
765 for (std::size_t i = 0; i < M; ++i)
766 for (std::size_t r = 0; r < K; ++r) Qe(i, r) += p * reset(i, r);
767 }
768 entry[e] = Qe;
769 }
770
771 // The state-dependent rates come AFTER the entry update, as they do in
772 // the reference: the entries just computed used the probOrig of the
773 // environment as it was during this iteration, and rewriting the arcs
774 // first would blend them with weights from an environment the stage
775 // transients were never solved under.
776 if (!statedep) return;
777 bool touched = false;
778 for (std::size_t e = 0; e < E; ++e)
779 for (std::size_t h = 0; h < E; ++h) {
780 const EnvArc<T>& a = envObj.arc(e, h);
781 if (!a.enabled || !a.reset_rates) continue;
782 envObj.set_transition_dist(e, h,
783 a.reset_rates(a.dist, Qexit[e][h], Uexit[e][h],
784 Texit[e][h]));
785 touched = true;
786 }
787 if (!touched) return;
788 // Everything the analyzer integrates against -- the marked transition
789 // processes, the superposed holding times, probEnv and probOrig -- is
790 // derived from the arc distributions, so the environment is rebuilt
791 // whole rather than patched.
792 envObj.init();
793 refresh_sojourn_means();
794 }
795
796 /**
797 * `finish_`: average each stage over its own holding time and blend the
798 * stages by their stationary probabilities.
799 */
800 void finish(EnvSolution& out) {
801 const std::size_t E = envObj.nstages();
802 out.QExit.assign(E, Matrix<double>(M, K, 0.0));
803 out.UExit.assign(E, Matrix<double>(M, K, 0.0));
804 out.TExit.assign(E, Matrix<double>(M, K, 0.0));
805 for (std::size_t e = 0; e < E; ++e) {
806 if (tranT[e].empty()) continue;
807 if (det_sojourn) {
808 for (std::size_t i = 0; i < M; ++i)
809 for (std::size_t r = 0; r < K; ++r) {
810 out.QExit[e](i, r) = detail::env_det_eval(tranT[e], tranQ[e][i][r], dvals[e]);
811 out.UExit[e](i, r) = detail::env_det_eval(tranT[e], tranU[e][i][r], dvals[e]);
812 out.TExit[e](i, r) = detail::env_det_eval(tranT[e], tranTp[e][i][r], dvals[e]);
813 }
814 continue;
815 }
816 const std::vector<double> w = cdf_weights(envObj.hold_time[e].map(), tranT[e]);
817 double wsum = 0.0;
818 for (double v : w) wsum += v;
819 if (!(wsum > 0.0)) continue;
820 for (std::size_t i = 0; i < M; ++i)
821 for (std::size_t r = 0; r < K; ++r) {
822 out.QExit[e](i, r) = weighted(tranQ[e][i][r], w, wsum);
823 out.UExit[e](i, r) = weighted(tranU[e][i][r], w, wsum);
824 out.TExit[e](i, r) = weighted(tranTp[e][i][r], w, wsum);
825 }
826 }
827 for (std::size_t e = 0; e < E; ++e) {
828 const double p = envObj.prob_env[e];
829 for (std::size_t i = 0; i < M; ++i)
830 for (std::size_t r = 0; r < K; ++r) {
831 out.QN(i, r) += p * out.QExit[e](i, r);
832 out.UN(i, r) += p * out.UExit[e](i, r);
833 out.TN(i, r) += p * out.TExit[e](i, r);
834 }
835 }
836 out.Qentry = entry;
837 }
838
839 /**
840 * The grid an exit average is summed on, refined where the SOJOURN WEIGHT
841 * CANNOT SEE the integrator's own grid.
842 *
843 * The exit metric is `sum_k m(t_k) * [F(t_k) - F(t_{k-1})]` over the ODE
844 * solver's OUTPUT grid, and that grid is chosen for the horizon rather than
845 * for the sojourn: a stage integrated over [0,1e3] and read through an
846 * Exp(1) clock puts almost every point where the weight is zero, so the
847 * answer becomes an artifact of step placement.
848 *
849 * The grid is rebuilt UNCONDITIONALLY -- 90% of the points under `5*E[S]`
850 * and the rest across the tail -- rather than only when the solver's own
851 * grid looks too coarse. A "50 points inside the support is enough" escape
852 * (native python's, before this) stops wherever the integrator's steps
853 * happened to fall and does not converge: on renv_node_breakdown the sum
854 * runs 0.462260, 0.460580, 0.459704, 0.459272, 0.459138, 0.459122 as the
855 * point count goes 500 to 5e4, and this engine on a 1e5-point uniform grid
856 * answers 0.459171. Rebuilding always is also what makes the four codebases
857 * sum the SAME points, which is the property parity needs.
858 */
859 static constexpr std::size_t kCdfInterp = 5000;
860
861 static std::vector<double> refine_grid(const mam::Map<double>& m,
862 const std::vector<double>& t) {
863 if (t.size() < 2) return t;
864 // A DISABLED ARC is the 1 x 1 zero pair, and `map_mean` refuses it by
865 // name ("zero arrival rate") rather than returning an infinity. Its
866 // weights are identically zero, so the grid it would be summed on
867 // cannot matter; leave it alone, exactly as cdf_weights does.
868 bool all_zero = true;
869 for (std::size_t a = 0; a < m.D1.rows() && all_zero; ++a)
870 for (std::size_t b = 0; b < m.D1.cols() && all_zero; ++b)
871 if (m.D1(a, b) != 0.0) all_zero = false;
872 if (all_zero) return t;
873 const double t0 = t.front();
874 const double tend = t.back();
875 double mean_sojourn = mam::map_mean(m);
876 if (!(mean_sojourn > 0.0) || !std::isfinite(mean_sojourn))
877 mean_sojourn = (tend - t0) / 10.0;
878 double tcdf = std::min(tend, 5.0 * mean_sojourn);
879 if (tcdf <= t0) tcdf = tend;
880 const std::size_t ndense = static_cast<std::size_t>(0.9 * kCdfInterp);
881 const std::size_t ntail = kCdfInterp - ndense;
882 const bool with_tail = tcdf < tend && ntail > 1;
883 std::vector<double> fine;
884 fine.reserve(with_tail ? ndense + ntail : ndense);
885 for (std::size_t k = 0; k < ndense; ++k)
886 fine.push_back(t0 + (tcdf - t0) * static_cast<double>(k) /
887 static_cast<double>(ndense - 1));
888 if (with_tail)
889 for (std::size_t k = 1; k <= ntail; ++k)
890 fine.push_back(tcdf + (tend - tcdf) * static_cast<double>(k) /
891 static_cast<double>(ntail));
892 return fine;
893 }
894
895 /**
896 * The OUTPUT GRID stage `e` is integrated on: the refined one, so the exit
897 * average is summed over points the trajectory was actually evaluated at.
898 *
899 * Interpolating instead cannot recover resolution the trajectory never had
900 * -- over [0,1e3] a uniform 1001-point grid carries SIX samples below
901 * `5*E[S]` for an `Exp(1)` sojourn, and a piecewise-linear reading of six
902 * samples is still six samples' worth of information; it reported a
903 * throughput of 0.8099 for a source admitting 0.8. LSODA takes an arbitrary
904 * increasing output vector, so the points are simply asked for.
905 *
906 * The scale is the HOLDING time, which is the sojourn regardless of which
907 * destination fires (competing exponentials), so one grid serves post()'s
908 * per-destination weights and finish()'s holding-time ones alike.
909 */
910 std::vector<double> stage_grid(std::size_t e) const {
911 std::vector<double> ends(2);
912 ends[0] = 0.0;
913 ends[1] = opt.timespan_end;
914 const std::vector<double> g = refine_grid(envObj.hold_time[e].map(), ends);
915 // A disabled holding time leaves `refine_grid` with nothing to say; fall
916 // back to the uniform grid the option asks for.
917 if (g.size() < 3) return std::vector<double>();
918 return g;
919 }
920
921 /** The Stieltjes weights of a transition over the transient grid. */
922 std::vector<double> cdf_weights(const mam::Map<double>& m, const std::vector<double>& t) const {
923 std::vector<double> w(t.size(), 0.0);
924 if (t.size() < 2) return w;
925 // A disabled arc is the 1 x 1 zero pair, whose CDF is identically zero.
926 bool all_zero = true;
927 for (std::size_t a = 0; a < m.D1.rows() && all_zero; ++a)
928 for (std::size_t b = 0; b < m.D1.cols() && all_zero; ++b)
929 if (m.D1(a, b) != 0.0) all_zero = false;
930 if (all_zero) return w;
931 const std::vector<double> F = mam::map_cdf(m, t);
932 for (std::size_t j = 1; j < t.size(); ++j) w[j] = F[j] - F[j - 1];
933 return w;
934 }
935
936 static double weighted(const std::vector<double>& v, const std::vector<double>& w,
937 double wsum) {
938 double s = 0.0;
939 for (std::size_t j = 0; j < v.size() && j < w.size(); ++j) s += v[j] * w[j];
940 return s / wsum;
941 }
942
943 /**
944 * `initFromMarginal` for a fluid stage: the mean queue length of every
945 * (station, class) enters in PHASE ONE.
946 *
947 * The reference does NOT round this for a fluid stage
948 * (`roundMarginalForDiscreteSolver` is skipped when the stage solver is
949 * SolverFluid), because a fluid state is continuous -- rounding it would
950 * quantize the very quantity the fixed point is iterating on.
951 */
952 std::vector<double> initsol_from_marginal(const qn::NetworkStruct<T>& sn,
953 const Matrix<double>& Q) const {
954 const fluid::FluidLayout L = fluid::fluid_layout(sn);
955 std::vector<double> y(L.nstates, 0.0);
956 for (std::size_t i = 0; i < sn.nstations && i < Q.rows(); ++i)
957 for (std::size_t r = 0; r < sn.nclasses && r < Q.cols(); ++r) {
958 if (!L.enabled[i][r]) continue;
959 y[L.qidx[i][r]] = std::max(0.0, Q(i, r));
960 }
961 return y;
962 }
963
964 Environment<T>& envObj;
965 EnvOptions opt;
966 std::size_t M = 0, K = 0;
967 bool det_sojourn = false;
968 bool statedep = false; ///< `method = "statedep"`: the arcs are rewritten each sweep
969 bool ctmc_stages = false; ///< `stage_solver = "ctmc"`: enumerate each stage's chain
970 std::vector<double> dvals;
971 std::vector<Matrix<double>> entry; ///< per-stage entry queue lengths
972 std::vector<std::vector<double>> tranT;
973 std::vector<std::vector<std::vector<std::vector<double>>>> tranQ, tranU, tranTp;
974 /** Per LAYERED stage, the SolverLN that runs it; null for a flat stage. */
975 std::vector<std::shared_ptr<ln::SolverLN<T>>> lnsolv;
976 /** Per LAYERED stage, where each of its layers sits in the aggregate view. */
977 std::vector<ln::LnLayerBlocks> lnblk;
978};
979
980} // namespace env
981} // namespace line
982
983#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:221
EnvSolution solve()
Definition solver_env.h:223
A random environment: a port of matlab/src/lang/Environment.m, restricted to what SolverENV reads out...
The exception types the port throws.
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.
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:103
FluidLayout fluid_layout(const qn::NetworkStruct< T > &sn)
Port of the layout half of solver_fluid_odes.m.
Definition fluid_odes.h:282
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.
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
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:2005
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:110
std::string method
The inter-stage coupling: meanfield is the reference's default.
Definition solver_env.h:122
std::string stage_solver
Which solver runs each FLAT stage: the fluid transient or the enumerated CTMC.
Definition solver_env.h:120
std::string sojourn
options.sojourn: stochastic (default) or deterministic.
Definition solver_env.h:124
fluid::FluidOptions stage
Options handed to each stage solver.
Definition solver_env.h:126
double stage_cutoff
options.cutoff of a CTMC stage, read only when stage_solver is ctmc.
Definition solver_env.h:131
double timespan_end
options.timespan(2) of the inner solver: the transient horizon.
Definition solver_env.h:133
ln::LnOptions lqn
Options of the SolverLN that runs a LAYERED stage.
Definition solver_env.h:163
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:146
What SolverENV reports.
Definition solver_env.h:167
Matrix< double > UN
Definition solver_env.h:169
std::vector< Matrix< double > > Qentry
The entry queue lengths the fixed point converged to.
Definition solver_env.h:173
Matrix< double > TN
Definition solver_env.h:169
Matrix< double > QN
Environment-averaged metrics, (nstations x nclasses).
Definition solver_env.h:169
std::vector< Matrix< double > > TExit
Definition solver_env.h:171
std::vector< Matrix< double > > UExit
Definition solver_env.h:171
std::vector< Matrix< double > > QExit
Per-stage, sojourn-averaged metrics.
Definition solver_env.h:171
Controls, defaulting to SolverOptions('Fluid') in the reference.
Options of SolverLN.
Definition solver_ln.h:264
std::string layer_solver
Which solver runs each layer: mva, nc, fluid or ssa.
Definition solver_ln.h:288
double timespan_end
options.timespan(2): the transient horizon; infinite means none is set.
Definition solver_ln.h:325