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. The mean-field one and the
41 * `occupation` limit (each layered stage solved by SolverLN and read over its
42 * LQN elements), and that is the reference's split rather than this port's:
43 * a `statevec` run needs
44 * one generator per stage to propagate a joint law across, which an LQN does not
45 * have (it decomposes into a network per layer), and the `avg`/`dec` limits read
46 * a stage's station rate table, which an LQN does not carry. Both refuse a
47 * LayeredNetwork stage by name, in their own `init`, through
48 * `Environment::reject_lqn_stages`; so does the compression, whose macro-state
49 * is a network at averaged rates. The one thing still refused everywhere is the
50 * cache aggregation of the two analyzers.
51 *
52 * WHAT COMPRESSION IS, AND WHY IT IS A SEPARATE OVERLOAD. `setCompression` is
53 * an explicit opt-in flag in the reference, defaulting to false, and the knobs
54 * it reads live in `options.config` rather than beside `method`. Passing the
55 * `EnvCompressOptions` is that opt-in here: there is no way to ask for the
56 * compressed solve without saying which decomposition kernel and which
57 * partition search it should use. The reference applies compression inside
58 * `init`, i.e. BEFORE the analyzer runs and independently of which analyzer was
59 * installed, so both couplings can run on a compressed environment and both do
60 * here.
61 */
62
63#include <algorithm>
64#include <cstddef>
65#include <string>
66#include <vector>
67
69#include "line/num/number.h"
74#include "line/util/matrix.h"
76
77namespace line {
78namespace env {
79
80/**
81 * What the ENV entry reports: the environment-blended metrics, and the whole
82 * result of whichever coupling produced them.
83 *
84 * The sub-result is kept rather than reduced away because the two couplings
85 * report different things beside the means -- the mean-field one the per-stage
86 * exit metrics and the entry queue lengths its fixed point converged to, the
87 * state-vector one the entry and sojourn-end DISTRIBUTIONS and the cache blend
88 * -- and a caller who chose a coupling chose it for those.
89 */
90template <class T>
92 /** Environment-averaged metrics, (nstations x nclasses). */
94 /** What ran: `meanfield`, `statevec`, or the limit `avg` / `dec`. */
95 std::string method;
96 /** True when the environment was aggregated before the coupling ran. */
97 bool compressed = false;
98 /** The aggregation, when there was one; its `eps` against `epsMAX` says whether it was meaningful. */
100 /** Populated on the mean-field path. */
102 /** Populated on the state-vector path. */
104 /**
105 * Populated on the closed-form limit path (`avg`, `dec`, `occupation`);
106 * `limit.occupation` / `limit.occupation_weight` are `getStageOccupation`.
107 */
109 /**
110 * The environment-blended cache surface, whichever coupling produced it.
111 *
112 * A Cache does not report in (station, class) and so cannot ride in QN/UN/
113 * TN; all three couplings now report it in `CacheMetrics`' one shape, keyed
114 * by the Cache's NAME. EMPTY on a model with no cache, and within an entry
115 * every field is optional: ABSENT MEANS NOT COMPUTED, never zero.
116 */
118 /**
119 * ZERO ON THE LIMIT PATH, and that is the answer rather than a gap: a limit
120 * reads the environment once and iterates nothing, so reporting an
121 * iteration count would describe a fixed point that never ran.
122 */
123 int iterations = 0;
124 /**
125 * True on the limit path: a closed form has converged by construction. The
126 * flag says whether the numbers can be trusted as the method's own answer,
127 * not whether a loop ended, and a limit's answer is always its own.
128 */
129 bool converged = false;
130};
131
132namespace dispatch_detail {
133
134/** The mean-field metrics, which are double by construction, in the caller's arithmetic. */
135template <class T>
136Matrix<T> env_widen(const Matrix<double>& A) {
138 for (std::size_t i = 0; i < A.rows(); ++i)
139 for (std::size_t j = 0; j < A.cols(); ++j) B(i, j) = num_traits<T>::from_double(A(i, j));
140 return B;
141}
142
143/**
144 * The limit path's cache surface, which is double by construction, in the
145 * caller's arithmetic.
146 *
147 * `EnvLimitSolution` is deliberately not templated -- `SolverEnvLimit::init`
148 * refuses `T != double` -- so the widening happens here rather than by
149 * templating a struct that can only ever hold one arithmetic.
150 */
151template <class T>
154 for (std::size_t i = 0; i < c.caches.size(); ++i) {
157 w.node = m.node;
158 w.name = m.name;
159 w.itemcap = m.itemcap;
160 w.itemsize = m.itemsize;
161 w.nitems = m.nitems;
162 for (std::size_t k = 0; k < m.hitprob.size(); ++k)
163 w.hitprob.push_back(num_traits<T>::from_double(m.hitprob[k]));
164 for (std::size_t k = 0; k < m.missprob.size(); ++k)
166 for (std::size_t k = 0; k < m.delayedprob.size(); ++k)
168 for (std::size_t k = 0; k < m.latency.size(); ++k)
169 w.latency.push_back(num_traits<T>::from_double(m.latency[k]));
170 for (std::size_t k = 0; k < m.listcost.size(); ++k)
172 w.hitproblist = env_widen<T>(m.hitproblist);
173 w.itemprob = env_widen<T>(m.itemprob);
174 out.caches.push_back(w);
175 }
176 return out;
177}
178
179/**
180 * `EnvOptions` as the state-vector coupling reads it.
181 *
182 * `stage_solver` is carried through UNTRANSLATED, so a caller who left it at
183 * the mean-field default of `fluid` is refused by name inside
184 * `SolverEnvStatevec::init` -- which is the reference's own error, "this method
185 * requires SolverENV to be instantiated with the CTMC solver"
186 * (`@@SolverENV/SolverENV.m` line 763). Silently substituting `ctmc` would run a
187 * different inner solver from the one asked for.
188 *
189 * TWO THINGS `EnvOptions` CANNOT CARRY, and neither is invented here: the
190 * per-transition state reset maps (functions on a state vector, which have no
191 * counterpart among the mean-field resets, as `ResetStateVec` explains) and the
192 * per-stage horizon overrides. A caller who needs either builds an
193 * `EnvStatevecOptions` and calls `solver_env_statevec` directly. The horizon
194 * that IS carried is `timespan_end`, and it should be a few mean holding times
195 * rather than a large safe number, for the ode23 reason `EnvStatevecOptions`
196 * gives at length.
197 */
198template <class T>
199EnvStatevecOptions<T> env_statevec_options(const EnvOptions& o) {
200 EnvStatevecOptions<T> s;
201 s.iter_max = o.iter_max;
202 s.iter_tol = o.iter_tol;
203 s.stage_solver = o.stage_solver;
204 s.sojourn = o.sojourn;
205 s.timespan_end = o.timespan_end;
206 // `stage_cutoff` bounds an OPEN stage's level count under either backend:
207 // it truncates the enumerated chain for the CTMC one and the LD-QBD level
208 // recursion for the MAM one. A closed stage takes its population instead
209 // and ignores it in both.
210 if (o.stage_cutoff > 0) s.mam_cutoff = static_cast<std::size_t>(o.stage_cutoff);
211 return s;
212}
213
214/** Read the state-vector result into the common shape. */
215template <class T>
216void env_take_statevec(EnvAnalyzerSolution<T>& out) {
217 out.method = "statevec";
218 out.QN = out.statevec.QN;
219 out.UN = out.statevec.UN;
220 out.TN = out.statevec.TN;
221 out.iterations = out.statevec.iterations;
222 out.converged = out.statevec.converged;
223 out.cache = out.statevec.cache;
224}
225
226/**
227 * Read the mean-field result into the common shape.
228 *
229 * `asked` is reported rather than the bare analyzer name, because `smp` and
230 * `statedep` also run this analyzer and are not the same run: `statedep`
231 * rewrote the environment as it went, and a result labelled `meanfield` would
232 * describe an environment that was an input when it was an output.
233 */
234template <class T>
235void env_take_meanfield(EnvAnalyzerSolution<T>& out, const std::string& asked = "meanfield") {
236 out.method = asked.empty() ? "meanfield" : asked;
237 out.QN = env_widen<T>(out.meanfield.avg.QN);
238 out.UN = env_widen<T>(out.meanfield.avg.UN);
239 out.TN = env_widen<T>(out.meanfield.avg.TN);
240 out.iterations = out.meanfield.avg.iterations;
241 out.converged = out.meanfield.avg.converged;
242 out.cache = out.meanfield.cache;
243}
244
245/** Read a closed-form limit result into the common shape. */
246template <class T>
247void env_take_limit(EnvAnalyzerSolution<T>& out) {
248 out.method = out.limit.method;
249 out.QN = env_widen<T>(out.limit.QN);
250 out.UN = env_widen<T>(out.limit.UN);
251 out.TN = env_widen<T>(out.limit.TN);
252 out.iterations = 0;
253 out.converged = true;
254 out.cache = env_widen_cache<T>(out.limit.cache);
255}
256
257/** True for the one name the reference's `init` installs the state-vector analyzer on. */
258inline bool env_is_statevec(const std::string& m) { return m == "statevec"; }
259
260/**
261 * True for the names `runAnalyzer` sends to `solveEnvLimit`, plus `occupation`
262 * (`solveEnvOccupation`), which `SolverEnvLimit` serves as the weighted `dec`.
263 */
264inline bool env_is_limit(const std::string& m) {
265 return m == "avg" || m == "dec" || m == "occupation";
266}
267
268} // namespace dispatch_detail
269
270/**
271 * Port of `SolverENV.listValidMethods`.
272 *
273 * Every name here selects a COUPLING -- what crosses an environment switch --
274 * and each is dispatched by this file or by `SolverEnv`'s own ladder:
275 * `default`/`meanfield`/`mean`/`blend` carry the marginal means, `meancov` the
276 * same coupling carrying a COVARIANCE beside the mean (the exit second moment
277 * over the sojourn law, mixed over the origins of a switch by the law of total
278 * variance, handed to the next stage as `FluidOptions::init_qlen`/`init_qcov`),
279 * `statevec` the whole joint distribution, `smp` the marginal means over the
280 * semi-Markov stage probabilities of the embedded jump chain, `statedep` makes the transition depend on the
281 * state it leaves, and `avg`/`dec` are the closed-form fast/slow limits of
282 * `solveEnvLimit`.
283 *
284 * `default` is the reference's spelling and `meanfield` this port's; both name
285 * the mean-field coupling and `SolverEnv::init` accepts either, as it does
286 * `blend`.
287 *
288 * `blend` NAMES THE MEAN-FIELD COUPLING SINCE 2026-09-13 and named the
289 * state-vector one before that. It is kept as a spelling of `default` so an
290 * existing script still runs, but it no longer reaches `SolverEnvStatevec` and
291 * the numbers it returns move: ask for `statevec` by name to carry the joint
292 * distribution. The word survives inside the analyzers as the
293 * environment-averaged BLEND they both compute, which is what it described.
294 */
295inline std::vector<std::string> env_list_valid_methods() {
296 // the names and their order are this solver's row of the method registry (doc/line-methods.json)
297 return ::line::method_registry::methods("ENV");
298}
299
300/** Port of `runAnalyzerChecks`' method gate: an unlisted method is refused. */
301inline void env_check_method(const std::string& method) {
302 const std::vector<std::string> valid = env_list_valid_methods();
303 if (std::find(valid.begin(), valid.end(), method) != valid.end()) return;
304 throw UnsupportedError(ErrorKind{"Method"}, "SolverENV: the '" + method + "' method is unsupported by this solver");
305}
306
307/**
308 * `SolverENV.init`'s analyzer selection: solve the environment with the
309 * coupling `o.method` names.
310 *
311 * An unknown method is refused by `SolverEnv`'s ladder, which is reached
312 * because everything that is not the state-vector coupling IS the mean-field
313 * one in the reference -- the `else` of its `if`, not a separate case.
314 */
315template <class T>
318 if (dispatch_detail::env_is_limit(o.method)) {
319 out.limit = solver_env_limit(e, o);
320 dispatch_detail::env_take_limit(out);
321 return out;
322 }
323 if (dispatch_detail::env_is_statevec(o.method)) {
324 out.statevec = solver_env_statevec(e, dispatch_detail::env_statevec_options<T>(o));
325 dispatch_detail::env_take_statevec(out);
326 return out;
327 }
329 dispatch_detail::env_take_meanfield(out, o.method);
330 return out;
331}
332
333/**
334 * The same, with the stage solver supplied by the caller.
335 *
336 * WHAT THIS IS FOR. The reference's `SolverENV(renv, @(m) SolverX(m, opts))`
337 * takes a FACTORY and runs every stage with whatever it returns, and
338 * `mapEnvApprox` hands it `feval(class(self), ...)` so that the environment
339 * image is solved by the solver the user called. Without an injected callable
340 * this port could only ever run fluid stages, and "the stage solver is the
341 * calling solver" would simply be untrue here.
342 *
343 * WHY ONLY THE LIMITS TAKE IT, and why the other two refuse by name rather than
344 * ignoring it. The two couplings carry an object across an environment switch
345 * that only their own backend can produce -- the mean-field one the RMF cache
346 * transient and the marginal means `initFromMarginal` reads, the state-vector
347 * one a per-stage generator and the joint law over it -- so an arbitrary
348 * `getAvg` callable cannot drive either. Accepting the function and quietly
349 * solving with the built-in backend would answer a different question from the
350 * one asked, which is the silent substitution this port refuses everywhere.
351 *
352 * The limits have no such object: `solveEnvLimit` asks each stage for its
353 * steady state and blends, so any solver that can answer `getAvg` will do.
354 */
355template <class T>
357 EnvStageAvgFn<T> stage_fn) {
358 if (!stage_fn) return solver_env(e, o);
359 if (!dispatch_detail::env_is_limit(o.method))
360 throw UnsupportedError(ErrorKind{"Option"},
361 "SolverENV: the '" + o.method +
362 "' coupling cannot run an injected stage solver; it carries a per-stage object across "
363 "an environment switch that only its own backend produces (the mean-field coupling "
364 "the RMF transient and the marginal means, the state-vector one a generator and the "
365 "joint law over it). The closed-form limits 'avg' and 'dec' take one");
367 SolverEnvLimit<T> s(e, o, stage_fn);
368 out.limit = s.solve();
369 dispatch_detail::env_take_limit(out);
370 return out;
371}
372
373/**
374 * The same, on a COMPRESSED environment: aggregate the stages first, then run
375 * the coupling over the macro-states.
376 *
377 * The mean-field path delegates to `solver_env_meanfield`, which already owns
378 * the construct-then-apply-then-solve order that `env_apply_macro_probabilities`
379 * requires. The state-vector path repeats that order here rather than reaching
380 * for a wrapper of its own: `SolverEnvStatevec`'s constructor calls
381 * `Environment::init()`, which recomputes probEnv and probOrig from the macro
382 * arcs, so the macro probabilities have to be written back AFTER it and BEFORE
383 * `solve()`, exactly as on the mean-field side.
384 */
385template <class T>
387 const EnvCompressOptions& c) {
389 out.compressed = true;
390 // A limit reads probEnv and the stage models, both of which the aggregation
391 // rewrites, so it runs on the macro-states like the couplings do; the macro
392 // probabilities must still be written back after `SolverEnvLimit`'s own
393 // `Environment::init()` and before it reads them, which is why the construct
394 // and the solve are separated here.
395 if (dispatch_detail::env_is_limit(o.method)) {
396 out.compression = env_compress(e, c);
397 SolverEnvLimit<T> s(*out.compression.env, o);
399 out.limit = s.solve();
400 dispatch_detail::env_take_limit(out);
401 return out;
402 }
403 if (dispatch_detail::env_is_statevec(o.method)) {
404 out.compression = env_compress(e, c);
405 SolverEnvStatevec<T> s(*out.compression.env, dispatch_detail::env_statevec_options<T>(o));
407 out.statevec = s.solve();
408 dispatch_detail::env_take_statevec(out);
409 return out;
410 }
411 out.meanfield = solver_env_meanfield(e, o, c);
412 out.compression = out.meanfield.compression;
413 dispatch_detail::env_take_meanfield(out, o.method);
414 return out;
415}
416
417} // namespace env
418} // namespace line
419
420#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:119
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.
The method names of each solver, in listing order.
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....
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
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, occupation); limit.occupation / limit....
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:120
std::string method
The inter-stage coupling: meanfield is the reference's default.
Definition solver_env.h:143
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 latency per read request (hits wait 0), 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.