LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
env_dispatch.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_ENV_DISPATCH_H
6#define LINE_SOLVERS_ENV_ENV_DISPATCH_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The SolverENV entry surface: a port of the analyzer selection that
12 * `@@SolverENV/SolverENV.m`'s `init` performs (lines 311-327) and of the
13 * compression switch `setCompression` arms (line 160, applied at line 412).
14 *
15 * WHAT SELECTS WHAT. The reference holds ONE solver object and swaps the
16 * function handle it delegates `pre`/`analyze`/`post`/`finish` to:
17 * `method` == statevec installs `solver_env_statevec_analyzer`, and every other
18 * method installs `solver_env_meanfield_analyzer` -- `blend` among them since
19 * 2026-09-13, when that name was repointed from the state-vector coupling to
20 * the mean-field one. This port has two solver CLASSES instead of one object
21 * with two handles, because the two couplings carry different objects across
22 * an environment switch -- the mean-field one carries the marginal mean queue
23 * lengths, the state-vector one the whole joint distribution -- and so they
24 * need different options, different stage solvers and different result types.
25 * This file is the switch that stands in front of them, and it is the only
26 * place that knows both exist.
27 *
28 * WHY `SolverEnv` STILL REFUSES `statevec` BY NAME. It is the mean-field
29 * coupling and nothing else; asked for the state-vector one it cannot answer,
30 * and answering with the mean-field collapse would be the silent fallback the
31 * whole port avoids. The refusal is therefore kept where it is and this entry
32 * routes AROUND it rather than through it.
33 *
34 * THE THIRD THING THIS SWITCH SELECTS is neither coupling: `avg` and `dec`, the
35 * closed-form fast/slow limits of `solveEnvLimit`, which bypass the fixed point
36 * entirely and are ported as `SolverEnvLimit`. The reference routes them the
37 * same way, at the TOP of `runAnalyzer` and before `iterate`, because there is
38 * no inter-stage coupling to choose when nothing crosses a switch.
39 *
40 * WHICH COUPLINGS TAKE A LAYERED STAGE. Only the mean-field one, and that is
41 * the reference's split rather than this port's: a `statevec` run needs
42 * one generator per stage to propagate a joint law across, which an LQN does not
43 * have (it decomposes into a network per layer), and the `avg`/`dec` limits read
44 * a stage's station rate table, which an LQN does not carry. Both refuse a
45 * LayeredNetwork stage by name, in their own `init`, through
46 * `Environment::reject_lqn_stages`; so does the compression, whose macro-state
47 * is a network at averaged rates. The one thing still refused everywhere is the
48 * cache aggregation of the two analyzers.
49 *
50 * WHAT COMPRESSION IS, AND WHY IT IS A SEPARATE OVERLOAD. `setCompression` is
51 * an explicit opt-in flag in the reference, defaulting to false, and the knobs
52 * it reads live in `options.config` rather than beside `method`. Passing the
53 * `EnvCompressOptions` is that opt-in here: there is no way to ask for the
54 * compressed solve without saying which decomposition kernel and which
55 * partition search it should use. The reference applies compression inside
56 * `init`, i.e. BEFORE the analyzer runs and independently of which analyzer was
57 * installed, so both couplings can run on a compressed environment and both do
58 * here.
59 */
60
61#include <algorithm>
62#include <cstddef>
63#include <string>
64#include <vector>
65
67#include "line/num/number.h"
72#include "line/util/matrix.h"
73
74namespace line {
75namespace env {
76
77/**
78 * What the ENV entry reports: the environment-blended metrics, and the whole
79 * result of whichever coupling produced them.
80 *
81 * The sub-result is kept rather than reduced away because the two couplings
82 * report different things beside the means -- the mean-field one the per-stage
83 * exit metrics and the entry queue lengths its fixed point converged to, the
84 * state-vector one the entry and sojourn-end DISTRIBUTIONS and the cache blend
85 * -- and a caller who chose a coupling chose it for those.
86 */
87template <class T>
89 /** Environment-averaged metrics, (nstations x nclasses). */
91 /** What ran: `meanfield`, `statevec`, or the limit `avg` / `dec`. */
92 std::string method;
93 /** True when the environment was aggregated before the coupling ran. */
94 bool compressed = false;
95 /** The aggregation, when there was one; its `eps` against `epsMAX` says whether it was meaningful. */
97 /** Populated on the mean-field path. */
99 /** Populated on the state-vector path. */
101 /** Populated on the closed-form limit path (`avg`, `dec`). */
103 /**
104 * The environment-blended cache surface, whichever coupling produced it.
105 *
106 * A Cache does not report in (station, class) and so cannot ride in QN/UN/
107 * TN; all three couplings now report it in `CacheMetrics`' one shape, keyed
108 * by the Cache's NAME. EMPTY on a model with no cache, and within an entry
109 * every field is optional: ABSENT MEANS NOT COMPUTED, never zero.
110 */
112 /**
113 * ZERO ON THE LIMIT PATH, and that is the answer rather than a gap: a limit
114 * reads the environment once and iterates nothing, so reporting an
115 * iteration count would describe a fixed point that never ran.
116 */
117 int iterations = 0;
118 /**
119 * True on the limit path: a closed form has converged by construction. The
120 * flag says whether the numbers can be trusted as the method's own answer,
121 * not whether a loop ended, and a limit's answer is always its own.
122 */
123 bool converged = false;
124};
125
126namespace dispatch_detail {
127
128/** The mean-field metrics, which are double by construction, in the caller's arithmetic. */
129template <class T>
130Matrix<T> env_widen(const Matrix<double>& A) {
132 for (std::size_t i = 0; i < A.rows(); ++i)
133 for (std::size_t j = 0; j < A.cols(); ++j) B(i, j) = num_traits<T>::from_double(A(i, j));
134 return B;
135}
136
137/**
138 * The limit path's cache surface, which is double by construction, in the
139 * caller's arithmetic.
140 *
141 * `EnvLimitSolution` is deliberately not templated -- `SolverEnvLimit::init`
142 * refuses `T != double` -- so the widening happens here rather than by
143 * templating a struct that can only ever hold one arithmetic.
144 */
145template <class T>
148 for (std::size_t i = 0; i < c.caches.size(); ++i) {
151 w.node = m.node;
152 w.name = m.name;
153 w.itemcap = m.itemcap;
154 w.itemsize = m.itemsize;
155 w.nitems = m.nitems;
156 for (std::size_t k = 0; k < m.hitprob.size(); ++k)
157 w.hitprob.push_back(num_traits<T>::from_double(m.hitprob[k]));
158 for (std::size_t k = 0; k < m.missprob.size(); ++k)
160 for (std::size_t k = 0; k < m.delayedprob.size(); ++k)
162 for (std::size_t k = 0; k < m.latency.size(); ++k)
163 w.latency.push_back(num_traits<T>::from_double(m.latency[k]));
164 for (std::size_t k = 0; k < m.listcost.size(); ++k)
166 w.hitproblist = env_widen<T>(m.hitproblist);
167 w.itemprob = env_widen<T>(m.itemprob);
168 out.caches.push_back(w);
169 }
170 return out;
171}
172
173/**
174 * `EnvOptions` as the state-vector coupling reads it.
175 *
176 * `stage_solver` is carried through UNTRANSLATED, so a caller who left it at
177 * the mean-field default of `fluid` is refused by name inside
178 * `SolverEnvStatevec::init` -- which is the reference's own error, "this method
179 * requires SolverENV to be instantiated with the CTMC solver"
180 * (`@@SolverENV/SolverENV.m` line 763). Silently substituting `ctmc` would run a
181 * different inner solver from the one asked for.
182 *
183 * TWO THINGS `EnvOptions` CANNOT CARRY, and neither is invented here: the
184 * per-transition state reset maps (functions on a state vector, which have no
185 * counterpart among the mean-field resets, as `ResetStateVec` explains) and the
186 * per-stage horizon overrides. A caller who needs either builds an
187 * `EnvStatevecOptions` and calls `solver_env_statevec` directly. The horizon
188 * that IS carried is `timespan_end`, and it should be a few mean holding times
189 * rather than a large safe number, for the ode23 reason `EnvStatevecOptions`
190 * gives at length.
191 */
192template <class T>
193EnvStatevecOptions<T> env_statevec_options(const EnvOptions& o) {
194 EnvStatevecOptions<T> s;
195 s.iter_max = o.iter_max;
196 s.iter_tol = o.iter_tol;
197 s.stage_solver = o.stage_solver;
198 s.sojourn = o.sojourn;
199 s.timespan_end = o.timespan_end;
200 // `stage_cutoff` bounds an OPEN stage's level count under either backend:
201 // it truncates the enumerated chain for the CTMC one and the LD-QBD level
202 // recursion for the MAM one. A closed stage takes its population instead
203 // and ignores it in both.
204 if (o.stage_cutoff > 0) s.mam_cutoff = static_cast<std::size_t>(o.stage_cutoff);
205 return s;
206}
207
208/** Read the state-vector result into the common shape. */
209template <class T>
210void env_take_statevec(EnvAnalyzerSolution<T>& out) {
211 out.method = "statevec";
212 out.QN = out.statevec.QN;
213 out.UN = out.statevec.UN;
214 out.TN = out.statevec.TN;
215 out.iterations = out.statevec.iterations;
216 out.converged = out.statevec.converged;
217 out.cache = out.statevec.cache;
218}
219
220/**
221 * Read the mean-field result into the common shape.
222 *
223 * `asked` is reported rather than the bare analyzer name, because `smp` and
224 * `statedep` also run this analyzer and are not the same run: `statedep`
225 * rewrote the environment as it went, and a result labelled `meanfield` would
226 * describe an environment that was an input when it was an output.
227 */
228template <class T>
229void env_take_meanfield(EnvAnalyzerSolution<T>& out, const std::string& asked = "meanfield") {
230 out.method = asked.empty() ? "meanfield" : asked;
231 out.QN = env_widen<T>(out.meanfield.avg.QN);
232 out.UN = env_widen<T>(out.meanfield.avg.UN);
233 out.TN = env_widen<T>(out.meanfield.avg.TN);
234 out.iterations = out.meanfield.avg.iterations;
235 out.converged = out.meanfield.avg.converged;
236 out.cache = out.meanfield.cache;
237}
238
239/** Read a closed-form limit result into the common shape. */
240template <class T>
241void env_take_limit(EnvAnalyzerSolution<T>& out) {
242 out.method = out.limit.method;
243 out.QN = env_widen<T>(out.limit.QN);
244 out.UN = env_widen<T>(out.limit.UN);
245 out.TN = env_widen<T>(out.limit.TN);
246 out.iterations = 0;
247 out.converged = true;
248 out.cache = env_widen_cache<T>(out.limit.cache);
249}
250
251/** True for the one name the reference's `init` installs the state-vector analyzer on. */
252inline bool env_is_statevec(const std::string& m) { return m == "statevec"; }
253
254/** True for the two names `runAnalyzer` sends to `solveEnvLimit`. */
255inline bool env_is_limit(const std::string& m) { return m == "avg" || m == "dec"; }
256
257} // namespace dispatch_detail
258
259/**
260 * Port of `SolverENV.listValidMethods`.
261 *
262 * Every name here selects a COUPLING -- what crosses an environment switch --
263 * and each is dispatched by this file or by `SolverEnv`'s own ladder:
264 * `default`/`meanfield`/`mean`/`blend` carry the marginal means, `meancov` the
265 * same coupling carrying a COVARIANCE beside the mean (the exit second moment
266 * over the sojourn law, mixed over the origins of a switch by the law of total
267 * variance, handed to the next stage as `FluidOptions::init_qlen`/`init_qcov`),
268 * `statevec` the whole joint distribution, `smp` the marginal means over the
269 * semi-Markov stage probabilities of the embedded jump chain, `statedep` makes the transition depend on the
270 * state it leaves, and `avg`/`dec` are the closed-form fast/slow limits of
271 * `solveEnvLimit`.
272 *
273 * `default` is the reference's spelling and `meanfield` this port's; both name
274 * the mean-field coupling and `SolverEnv::init` accepts either, as it does
275 * `blend`.
276 *
277 * `blend` NAMES THE MEAN-FIELD COUPLING SINCE 2026-09-13 and named the
278 * state-vector one before that. It is kept as a spelling of `default` so an
279 * existing script still runs, but it no longer reaches `SolverEnvStatevec` and
280 * the numbers it returns move: ask for `statevec` by name to carry the joint
281 * distribution. The word survives inside the analyzers as the
282 * environment-averaged BLEND they both compute, which is what it described.
283 */
284inline std::vector<std::string> env_list_valid_methods() {
285 return {"default", "meanfield", "mean", "meancov", "blend",
286 "blending", "smp", "statedep", "statevec", "avg", "dec"};
287}
288
289/** Port of `runAnalyzerChecks`' method gate: an unlisted method is refused. */
290inline void env_check_method(const std::string& method) {
291 const std::vector<std::string> valid = env_list_valid_methods();
292 if (std::find(valid.begin(), valid.end(), method) != valid.end()) return;
293 throw UnsupportedError("SolverENV: the '" + method + "' method is unsupported by this solver");
294}
295
296/**
297 * `SolverENV.init`'s analyzer selection: solve the environment with the
298 * coupling `o.method` names.
299 *
300 * An unknown method is refused by `SolverEnv`'s ladder, which is reached
301 * because everything that is not the state-vector coupling IS the mean-field
302 * one in the reference -- the `else` of its `if`, not a separate case.
303 */
304template <class T>
307 if (dispatch_detail::env_is_limit(o.method)) {
308 out.limit = solver_env_limit(e, o);
309 dispatch_detail::env_take_limit(out);
310 return out;
311 }
312 if (dispatch_detail::env_is_statevec(o.method)) {
313 out.statevec = solver_env_statevec(e, dispatch_detail::env_statevec_options<T>(o));
314 dispatch_detail::env_take_statevec(out);
315 return out;
316 }
318 dispatch_detail::env_take_meanfield(out, o.method);
319 return out;
320}
321
322/**
323 * The same, with the stage solver supplied by the caller.
324 *
325 * WHAT THIS IS FOR. The reference's `SolverENV(renv, @(m) SolverX(m, opts))`
326 * takes a FACTORY and runs every stage with whatever it returns, and
327 * `mapEnvApprox` hands it `feval(class(self), ...)` so that the environment
328 * image is solved by the solver the user called. Without an injected callable
329 * this port could only ever run fluid stages, and "the stage solver is the
330 * calling solver" would simply be untrue here.
331 *
332 * WHY ONLY THE LIMITS TAKE IT, and why the other two refuse by name rather than
333 * ignoring it. The two couplings carry an object across an environment switch
334 * that only their own backend can produce -- the mean-field one the RMF cache
335 * transient and the marginal means `initFromMarginal` reads, the state-vector
336 * one a per-stage generator and the joint law over it -- so an arbitrary
337 * `getAvg` callable cannot drive either. Accepting the function and quietly
338 * solving with the built-in backend would answer a different question from the
339 * one asked, which is the silent substitution this port refuses everywhere.
340 *
341 * The limits have no such object: `solveEnvLimit` asks each stage for its
342 * steady state and blends, so any solver that can answer `getAvg` will do.
343 */
344template <class T>
346 EnvStageAvgFn<T> stage_fn) {
347 if (!stage_fn) return solver_env(e, o);
348 if (!dispatch_detail::env_is_limit(o.method))
349 throw UnsupportedError(
350 "SolverENV: the '" + o.method +
351 "' coupling cannot run an injected stage solver; it carries a per-stage object across "
352 "an environment switch that only its own backend produces (the mean-field coupling "
353 "the RMF transient and the marginal means, the state-vector one a generator and the "
354 "joint law over it). The closed-form limits 'avg' and 'dec' take one");
356 SolverEnvLimit<T> s(e, o, stage_fn);
357 out.limit = s.solve();
358 dispatch_detail::env_take_limit(out);
359 return out;
360}
361
362/**
363 * The same, on a COMPRESSED environment: aggregate the stages first, then run
364 * the coupling over the macro-states.
365 *
366 * The mean-field path delegates to `solver_env_meanfield`, which already owns
367 * the construct-then-apply-then-solve order that `env_apply_macro_probabilities`
368 * requires. The state-vector path repeats that order here rather than reaching
369 * for a wrapper of its own: `SolverEnvStatevec`'s constructor calls
370 * `Environment::init()`, which recomputes probEnv and probOrig from the macro
371 * arcs, so the macro probabilities have to be written back AFTER it and BEFORE
372 * `solve()`, exactly as on the mean-field side.
373 */
374template <class T>
376 const EnvCompressOptions& c) {
378 out.compressed = true;
379 // A limit reads probEnv and the stage models, both of which the aggregation
380 // rewrites, so it runs on the macro-states like the couplings do; the macro
381 // probabilities must still be written back after `SolverEnvLimit`'s own
382 // `Environment::init()` and before it reads them, which is why the construct
383 // and the solve are separated here.
384 if (dispatch_detail::env_is_limit(o.method)) {
385 out.compression = env_compress(e, c);
386 SolverEnvLimit<T> s(*out.compression.env, o);
388 out.limit = s.solve();
389 dispatch_detail::env_take_limit(out);
390 return out;
391 }
392 if (dispatch_detail::env_is_statevec(o.method)) {
393 out.compression = env_compress(e, c);
394 SolverEnvStatevec<T> s(*out.compression.env, dispatch_detail::env_statevec_options<T>(o));
396 out.statevec = s.solve();
397 dispatch_detail::env_take_statevec(out);
398 return out;
399 }
400 out.meanfield = solver_env_meanfield(e, o, c);
401 out.compression = out.meanfield.compression;
402 dispatch_detail::env_take_meanfield(out, o.method);
403 return out;
404}
405
406} // namespace env
407} // namespace line
408
409#endif // LINE_SOLVERS_ENV_ENV_DISPATCH_H
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
UnsupportedError(const std::string &what)
Definition error.h:51
The state-vector environment solver.
EnvStatevecSolution< T > solve()
A random environment: a port of matlab/src/lang/Environment.m, restricted to what SolverENV reads out...
Dense matrix and non-owning view.
EnvStatevecSolution< T > solver_env_statevec(Environment< T > &e, const EnvStatevecOptions< T > &o)
Solve in one call, for a caller with no use for the solver object.
EnvLimitSolution solver_env_limit(Environment< T > &e, const EnvOptions &o)
solveEnvLimit on the original stages.
std::function< EnvStageAvg< T >(const qn::NetworkStruct< T > &)> EnvStageAvgFn
The stage solver, as a callable: the C++ spelling of the MATLAB function handle SolverENV(renv,...
void env_check_method(const std::string &method)
Port of runAnalyzerChecks' method gate: an unlisted method is refused.
EnvMeanfieldSolution< T > solver_env_meanfield(Environment< T > &e, const EnvOptions &o)
The mean-field solve on the original stages, with no compression.
EnvCompression< T > env_compress(const Environment< T > &e0, const EnvCompressOptions &opt)
Port of applyCompression: pick a partition, decompose, and build the macro-state environment.
std::vector< std::string > env_list_valid_methods()
Port of SolverENV.listValidMethods.
void env_apply_macro_probabilities(Environment< T > &e, const EnvCompression< T > &c)
probEnv = pMacro and probOrig = newEmbweight, the two quantities applyCompression overwrites on the e...
EnvAnalyzerSolution< T > solver_env(Environment< T > &e, const EnvOptions &o)
SolverENV.init's analyzer selection: solve the environment with the coupling o.method names.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
Number-type abstraction for the templated API port.
SolverENV: a queueing network in a random environment.
The two CLOSED-FORM environment limits: SolverENV.solveEnvLimit, reached by options....
SolverENV, the default mean-field path: ENVIRONMENT COMPRESSION.
SolverENV, method = "statevec": a port of matlab/src/solvers/ENV/solver_env_statevec_analyzer....
What the ENV entry reports: the environment-blended metrics, and the whole result of whichever coupli...
EnvMeanfieldSolution< T > meanfield
Populated on the mean-field path.
std::string method
What ran: meanfield, statevec, or the limit avg / dec.
EnvLimitSolution limit
Populated on the closed-form limit path (avg, dec).
EnvStatevecSolution< T > statevec
Populated on the state-vector path.
bool converged
True on the limit path: a closed form has converged by construction.
Matrix< T > QN
Environment-averaged metrics, (nstations x nclasses).
EnvCompression< T > compression
The aggregation, when there was one; its eps against epsMAX says whether it was meaningful.
bool compressed
True when the environment was aggregated before the coupling ran.
solvers::CacheMetrics< T > cache
The environment-blended cache surface, whichever coupling produced it.
int iterations
ZERO ON THE LIMIT PATH, and that is the answer rather than a gap: a limit reads the environment once ...
The knobs applyCompression reads out of options.config.
Everything applyCompression computes, plus the compressed environment.
What a limit solve reports.
A mean-field solve, with the compression that produced it.
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
What the state-vector coupling reports.
Every Cache node of the model, in node order; empty on a model with none.
std::vector< CacheNodeMetrics< T > > caches
One Cache node's measured behaviour.
std::vector< double > itemcap
(h) capacity of each list
std::vector< double > itemsize
(n) storage cost per item, EMPTY without setItemSizes
std::vector< T > hitprob
(K) TRUE hit fraction, EMPTY = not computed
std::vector< T > delayedprob
(K) delayed-hit fraction, EMPTY off a retrieval system
Matrix< T > hitproblist
(K x h) per-list hit fraction, EMPTY = not computed
std::size_t node
1-based node index of the Cache
std::vector< T > latency
(K) expected retrieval latency, EMPTY = not computed
std::vector< T > listcost
(h) mean storage cost held by each list
Matrix< T > itemprob
(n x h+1), column 0 = miss; EMPTY = not computed
std::string name
The Cache node's NAME, which is what a cross-language payload must key on.