LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
map_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_MAP_ENV_H
6#define LINE_SOLVERS_MAP_ENV_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `@NetworkSolver/mapEnvApprox.m`: the solver-agnostic
12 * random-environment approximation of a network whose arrival or service
13 * processes are non-renewal.
14 *
15 * `map2renv` turns each modulated process into a set of environment stages in
16 * which that process is exponential with the phase-conditional intensity, and
17 * SolverENV recombines the stages. The stage models are exponential and carry
18 * the base model's structure unchanged, so THE STAGE SOLVER IS THE CALLER
19 * ITSELF: nothing here reads a solver internal, and any runner that can answer
20 * `getAvg` on a struct can be driven through it.
21 *
22 * WHERE IT SITS. The reference's `runAnalyzer` still REFUSES a MAP model; only
23 * `getAvg` falls back (`@NetworkSolver/getAvg.m` lines 150-151). That placement
24 * is forced here as well as faithful: `env/solver_env.h` includes
25 * `ln/solver_ln.h`, which includes the MVA and NC runners, so the gate cannot
26 * live where the feature gate lives and has to stand in front of the runners
27 * instead. C++ has no `getAvg`, so the three ladders that play its part -- the
28 * facade's `avg_table`, the CLI's `run_avg_engine`, and the CLI's per-solver
29 * `-a avg` arms -- each wrap their runner in `run_avg` below. All three, or
30 * `line-cli -a avg` and `-a cache` would accept different models for one
31 * command line.
32 *
33 * WHAT IS DOUBLE-ONLY, and why the refusal stays. `SolverEnvLimit::init` and
34 * `SolverEnv::init` both refuse `T != double` -- a fluid stage integrates its
35 * drift with LSODA and a mean-field sojourn is a `double` quadrature -- while
36 * the MVA and NC runners this wraps are multiprecision. So `--arith rational`
37 * on a MAP model keeps the PLAIN rejection rather than falling back, and says
38 * so by name. See `_kb/14-cpp-multiprecision.md`.
39 */
40
41#include <cmath>
42#include <cstddef>
43#include <limits>
44#include <string>
45#include <type_traits>
46#include <vector>
47
49#include "line/io/map2renv.h"
54#include "line/util/error.h"
55
56namespace line {
57namespace solvers {
58
59namespace map_env_detail {
60
61/**
62 * `mapImageKind`: an MMPP image preserves the modulating chain exactly; a
63 * general MAP image aggregates the event-epoch phase jumps into the phase
64 * generator, and the correlation between the jumps and the event stream is lost.
65 */
66template <class T>
67const char* image_kind(const io::Map2RenvInfo<T>& info) {
68 return info.is_mmpp ? "exact-modulation" : "intensity-matched";
69}
70
71/** Longest mean stage sojourn of the environment, the mean-field horizon's scale. */
72template <class T>
74 double t = 0.0;
75 for (std::size_t s = 0; s < e.nstages(); ++s)
76 t = std::max(t, e.hold_time_mean(s));
77 if (!std::isfinite(t) || !(t > 0.0))
78 throw InputError(
79 "map_env: the environment image has no finite stage sojourn, so no integration "
80 "horizon can be set");
81 return t;
82}
83
84/**
85 * `selectEnvLimit`: the timescale test between the two closed-form limits.
86 *
87 * Compare the mean stage holding time of the environment with the relaxation
88 * time of the model, taken as the time the slowest station needs to clear the
89 * jobs it can hold. A stage that OUTLIVES the relaxation time lets each stage
90 * reach its own steady state, which is the quasi-stationary regime (`dec`); a
91 * stage that expires first leaves the model responding to the mean rate only,
92 * which is the rate-averaged regime (`avg`). The closed population enters the
93 * relaxation time because a closed queue drains in N services, so a populous
94 * model relaxes far more slowly than one service time.
95 */
96template <class T>
98 const std::size_t E = e.nstages();
99 std::vector<double> exit_rate;
100 for (std::size_t a = 0; a < E; ++a) {
101 double r = 0.0;
102 for (std::size_t b = 0; b < E; ++b) {
103 if (a == b) continue;
104 const env::EnvArc<T>& arc = e.arc(a, b);
105 if (!arc.enabled) continue;
106 // `1 / env{e,h}.getMean()`, the reference's `getRate()`: the arc's
107 // own rate, whatever family carries it.
108 const double m = num_traits<T>::to_double(arc.dist.mean);
109 if (m > 0.0) r += 1.0 / m;
110 }
111 if (r > 0.0) exit_rate.push_back(r);
112 }
113 // An ABSORBING environment never leaves the stage it is in, so every stage
114 // IS its own steady state and the quasi-stationary limit is exact.
115 if (exit_rate.empty()) return "dec";
116 double tau_env = 0.0;
117 for (std::size_t i = 0; i < exit_rate.size(); ++i) tau_env += 1.0 / exit_rate[i];
118 tau_env /= static_cast<double>(exit_rate.size());
119
120 double minrate = std::numeric_limits<double>::infinity();
121 for (std::size_t i = 0; i < sn.rates.rows(); ++i)
122 for (std::size_t r = 0; r < sn.rates.cols(); ++r) {
123 const double v = num_traits<T>::to_double(sn.rates(i, r));
124 if (std::isfinite(v) && v > 0.0) minrate = std::min(minrate, v);
125 }
126 if (!std::isfinite(minrate)) return "dec";
127 const std::vector<double> nj = sn.njobs();
128 double totn = 0.0;
129 for (std::size_t k = 0; k < nj.size(); ++k)
130 if (std::isfinite(nj[k])) totn += nj[k];
131 const double tau_sys = (1.0 + totn) / minrate;
132 return tau_env >= tau_sys ? "dec" : "avg";
133}
134
135/**
136 * The base-to-stage index correspondence `map_env_approx` relies on, asserted
137 * rather than assumed.
138 *
139 * `map2renv` copies the struct and rewrites only service entries, so station,
140 * class and node indices are identical between the base and every stage. Every
141 * column of the blend is read at the base's index, so a later change there
142 * would shift each one SILENTLY; this is what makes the correspondence an
143 * invariant rather than a coincidence.
144 */
145template <class T>
147 for (std::size_t s = 0; s < e.nstages(); ++s) {
148 const qn::NetworkStruct<T>& st = e.stage(s).model;
149 if (st.nstations != sn.nstations || st.nclasses != sn.nclasses ||
150 st.nodes.size() != sn.nodes.size())
151 throw InputError("map_env: stage " + std::to_string(s + 1) +
152 " of the environment image has a different station, class or node "
153 "count from the base model; every metric is blended at the base's "
154 "own index");
155 for (std::size_t i = 0; i < sn.nodes.size(); ++i)
156 if (st.nodes[i].name != sn.nodes[i].name)
157 throw InputError("map_env: node " + std::to_string(i + 1) + " of stage " +
158 std::to_string(s + 1) + " is '" + st.nodes[i].name +
159 "' where the base model has '" + sn.nodes[i].name +
160 "'; the image must keep the base's node order");
161 }
162}
163
164} // namespace map_env_detail
165
166/**
167 * `mapEnvApprox`: solve the model through the random-environment image of its
168 * non-renewal processes.
169 *
170 * @param sn the base model
171 * @param solver the calling solver's label ("SolverMVA", "SolverNC", ...),
172 * which decides the stage backend and the transient capability
173 * @param cfg the caller's map_env knobs
174 * @param stage_fn runs ONE stage with the calling solver and returns its
175 * Q/U/T and cache surface; the C++ spelling of the reference's
176 * `feval(class(self), stageModel, innerOptions)` factory
177 * @param requested_method the method the caller asked for, echoed in the result
178 *
179 * THE MEAN-FIELD COUPLING DOES NOT TAKE `stage_fn`, and that is not an omission:
180 * it carries the marginal means and the RMF cache transient across a switch,
181 * objects only its own backend produces, so it runs `SolverEnv`'s own fluid or
182 * ctmc stage instead. A solver with neither backend asking for `meanfield`
183 * EXPLICITLY is refused by name; `auto` never picks it for such a solver.
184 */
185template <class T, class StageFn>
186mva::AvgResult<T> map_env_approx(const qn::NetworkStruct<T>& sn, const std::string& solver,
187 const MapEnvConfig& cfg, StageFn stage_fn,
188 const std::string& requested_method = "default") {
189 if (!std::is_same<T, double>::value)
190 throw UnsupportedError(ErrorKind{"Arithmetic"},
191 "map_env: the random-environment fallback for a MAP/MMPP/MMAP/MPH process solves "
192 "each stage through SolverENV, whose couplings are double-only (a fluid stage "
193 "integrates with LSODA and a mean-field sojourn is a double quadrature). Rerun with "
194 "--arith double, or set map_env='off' to keep the plain rejection");
195
198 io::map2renv(sn, &info, cfg.max_stages > 0 ? cfg.max_stages : static_cast<std::size_t>(64));
200
201 const std::string backend = map_env_stage_backend(solver);
202 std::string method = cfg.method.empty() ? std::string("auto") : cfg.method;
203 if (method == "auto") {
204 // The mean-field coupling is the only one that MODELS the phase switch
205 // rather than taking a limit of it, so it is preferred wherever it can
206 // run. It can run only where BOTH hold: the solver produces transient
207 // averages, and this port has a stage backend for it.
208 method = (supports_transient_analysis(solver) && !backend.empty())
209 ? std::string("meanfield")
211 }
212 if (method != "dec" && method != "avg" && method != "meanfield")
213 throw UnsupportedError(ErrorKind{"Option"}, "map_env: map_env_method='" + method +
214 "' is not a supported environment recombination; use 'meanfield', "
215 "'dec', 'avg' or 'auto'");
216 if (method == "meanfield" && backend.empty())
217 throw UnsupportedError(ErrorKind{"Option"},
218 "map_env: the mean-field environment coupling integrates each stage over its sojourn, "
219 "so it needs a stage backend that produces transient averages, and this port wires "
220 "'fluid' and 'ctmc' only -- " +
221 solver +
222 " has neither. Use map_env_method='dec' or 'avg', which solve each stage in steady "
223 "state with the calling solver itself");
224
226 o.method = method == "meanfield" ? "meanfield" : method;
227 if (method == "meanfield") {
228 o.stage_solver = backend;
229 // The stage transients are weighted by the sojourn density over the
230 // integration grid, so the horizon must cover the sojourn distribution;
231 // beyond it the weights vanish and the extra span is inert.
232 o.timespan_end = 20.0 * map_env_detail::max_hold_time(e);
233 }
235 method == "meanfield" ? env::solver_env(e, o)
236 : env::solver_env(e, o, env::EnvStageAvgFn<T>(stage_fn));
237
239 out.QN = r.QN;
240 out.UN = r.UN;
241 out.TN = r.TN;
242 out.cache = r.cache;
243 out.method = requested_method;
244 out.actualmethod = "env." + method;
245
246 const std::size_t M = sn.nstations, K = sn.nclasses;
247 const T zero = num_traits<T>::from_int(0);
248 out.RN = Matrix<T>(M, K, zero);
249 // finalizeAvgResults derives AN from TN, and getAvg zeroes the Source rows; a NaN here printed ArvR=nan in every env-image table.
250 std::vector<std::vector<bool>> srcmask(M, std::vector<bool>(K, false));
251 for (std::size_t i = 0; i < M; ++i)
252 if (sn.stations[i].nodetype == qn::NodeType::Source)
253 for (std::size_t k = 0; k < K; ++k) srcmask[i][k] = true;
255 out.XN.assign(K, zero);
256 out.CN.assign(K, zero);
257 for (std::size_t i = 0; i < M; ++i)
258 for (std::size_t k = 0; k < K; ++k)
259 if (num_traits<T>::to_double(out.TN(i, k)) > 0.0)
260 out.RN(i, k) = T(out.QN(i, k) / out.TN(i, k));
261 for (std::size_t k = 0; k < K; ++k) {
262 const std::size_t ist = sn.classes[k].refstat;
263 if (ist >= 1 && ist <= M) out.XN[k] = out.TN(ist - 1, k);
264 if (num_traits<T>::to_double(out.XN[k]) > 0.0) {
265 T q = zero;
266 for (std::size_t i = 0; i < M; ++i) q = T(q + out.QN(i, k));
267 out.CN[k] = T(q / out.XN[k]);
268 }
269 }
271
272 // THE APPROXIMATION NOTICE IS NOT OPTIONAL. The caller asked for a solver
273 // that cannot consume this model's processes and is being answered by an
274 // approximation of a different model; `AvgResult::warning` exists for
275 // exactly this and is printed by the CLI and the avg table.
276 out.warning = "This solver has no native support for the non-renewal (MAP/MMPP) processes of "
277 "this model; the reported averages come from its " +
278 method + " random-environment approximation (" + std::to_string(info.nstages) +
279 " stages, " + map_env_detail::image_kind(info) +
280 " image). Set map_env='off' to reject the model instead.";
281
282 // The system metrics read the reference station, which for an open class is
283 // the Source. A stage solver whose transient does not report Source
284 // throughput leaves it at zero under the mean-field coupling, and the zero
285 // propagates into XN and CN. REPORT THAT rather than substituting the
286 // arrival rate, which would hide whose metric is missing.
287 const std::vector<double> nj = sn.njobs();
288 std::string openzero;
289 for (std::size_t k = 0; k < K; ++k) {
290 if (k < nj.size() && std::isfinite(nj[k])) continue;
291 if (num_traits<T>::to_double(out.XN[k]) != 0.0) continue;
292 bool any = false;
293 for (std::size_t i = 0; i < M; ++i)
294 if (num_traits<T>::to_double(out.QN(i, k)) > 0.0) any = true;
295 const std::size_t ist = sn.classes[k].refstat;
296 if (!any && ist >= 1 && ist <= sn.rates.rows() && k < sn.rates.cols()) {
297 const double lam = num_traits<T>::to_double(sn.rates(ist - 1, k));
298 any = std::isfinite(lam) && lam > 0.0;
299 }
300 if (!any) continue;
301 if (!openzero.empty()) openzero += ", ";
302 openzero += std::to_string(k + 1);
303 }
304 if (!openzero.empty())
305 out.warning += " The " + method +
306 " environment coupling returned no reference-station throughput for open "
307 "class(es) " +
308 openzero +
309 ", so their system throughput and system response time are reported as "
310 "zero; use map_env_method='dec' or 'avg' for system-level metrics.";
311 return out;
312}
313
314/**
315 * The `getAvg` funnel: run the model, or its environment image when the ONLY
316 * thing in the way is a non-renewal process.
317 *
318 * THREE OUTCOMES, AND THE THIRD IS THE SUBTLE ONE. Supported -> run. Needs the
319 * image -> `map_env_approx`. Otherwise -> RUN ANYWAY, and let the runner raise
320 * its own refusal in its own words. Raising a second copy of the refusal here
321 * would mean two messages for one condition, drifting apart as the feature sets
322 * move; `needs_map_env` is deliberately a question about whether the IMAGE
323 * helps, not a gate on whether the model is supported.
324 *
325 * NOT WIRED INTO THE `-s ba` ARM, deliberately. A bound request must be
326 * answered with a bound, and the environment image is an approximation of the
327 * model, so its bounds do not bracket the original one. The reference refuses
328 * `SolverBA` inside `needsMapEnv` itself; here the exclusion is the absence of
329 * this wrapper at the three BA call sites, each of which says so.
330 */
331template <class T, class Run, class StageFn>
332mva::AvgResult<T> run_avg(const qn::NetworkStruct<T>& sn, const std::string& solver,
333 const qn::FeatureSet& declared, const MapEnvConfig& cfg, Run run,
334 StageFn stage_fn, const std::string& requested_method = "default") {
335 const MapEnvDecision d = needs_map_env(declared, sn, cfg);
336 if (!d.needed) return run(sn);
337 return map_env_approx<T>(sn, solver, cfg, stage_fn, requested_method);
338}
339
340} // namespace solvers
341} // namespace line
342
343#endif // LINE_SOLVERS_MAP_ENV_H
InputError(const std::string &what)
Definition error.h:105
UnsupportedError(const std::string &what)
Definition error.h:119
const EnvStage< T > & stage(std::size_t e) const
std::size_t nstages() const
double hold_time_mean(std::size_t e) const
Environment.holdTimeMean(holdTime{e}): the mean holding time of stage e.
const EnvArc< T > & arc(std::size_t e, std::size_t h) const
A subset of the registry: MATLAB's SolverFeatureSet, whose list is a flag per field.
A network plus its refreshed NetworkStruct.
std::vector< NodeDef > nodes
every node, in creation order
The SolverENV entry surface: a port of the analyzer selection that @@SolverENV/SolverENV....
The exception types the port throws.
Port of matlab/src/io/map2renv.m and matlab/src/io/MAPQN2RENV.m (python twin in api/io/converters....
The map_env decision, as a predicate over a feature set.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
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,...
EnvAnalyzerSolution< T > solver_env(Environment< T > &e, const EnvOptions &o)
SolverENV.init's analyzer selection: solve the environment with the coupling o.method names.
env::Environment< T > map2renv(const qn::NetworkStruct< T > &base, Map2RenvInfo< T > *info=nullptr, std::size_t max_stages=64)
Definition map2renv.h:65
Matrix< T > sn_get_residt_from_respt(const qn::NetworkStruct< T > &L, const Matrix< T > &RN)
Port of sn_get_residt_from_respt: the per-JOB residence time.
Matrix< T > filter_metric(const qn::NetworkStruct< T > &L, const Matrix< T > &metric, MetricKind kind, const std::vector< std::vector< bool > > *zero_mask)
Port of filterMetric: what @@NetworkSolver/getAvg does between the analyzer and the caller.
Matrix< T > sn_get_arvr_from_tput(const qn::NetworkStruct< T > &L, const Matrix< T > &TN)
Port of sn_get_arvr_from_tput: the arrival rate each station sees, from the throughputs and the class...
double max_hold_time(const env::Environment< T > &e)
Longest mean stage sojourn of the environment, the mean-field horizon's scale.
Definition map_env.h:73
void assert_stage_correspondence(const qn::NetworkStruct< T > &sn, const env::Environment< T > &e)
The base-to-stage index correspondence map_env_approx relies on, asserted rather than assumed.
Definition map_env.h:146
const char * image_kind(const io::Map2RenvInfo< T > &info)
mapImageKind: an MMPP image preserves the modulating chain exactly; a general MAP image aggregates th...
Definition map_env.h:67
std::string select_env_limit(const qn::NetworkStruct< T > &sn, const env::Environment< T > &e)
selectEnvLimit: the timescale test between the two closed-form limits.
Definition map_env.h:97
MapEnvDecision needs_map_env(const qn::FeatureSet &declared, const qn::NetworkStruct< T > &sn, const MapEnvConfig &cfg=MapEnvConfig())
needsMapEnv: does this model need the environment image, and would the image make it solvable?
std::string map_env_stage_backend(const std::string &solver)
Which ENV stage backend a solver's name maps to, or empty when it has none.
mva::AvgResult< T > run_avg(const qn::NetworkStruct< T > &sn, const std::string &solver, const qn::FeatureSet &declared, const MapEnvConfig &cfg, Run run, StageFn stage_fn, const std::string &requested_method="default")
The getAvg funnel: run the model, or its environment image when the ONLY thing in the way is a non-re...
Definition map_env.h:332
mva::AvgResult< T > map_env_approx(const qn::NetworkStruct< T > &sn, const std::string &solver, const MapEnvConfig &cfg, StageFn stage_fn, const std::string &requested_method="default")
mapEnvApprox: solve the model through the random-environment image of its non-renewal processes.
Definition map_env.h:186
bool supports_transient_analysis(const std::string &solver)
supportsTransientAnalysis: does this solver produce transient averages?
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
The SolverMVA class surface: @@SolverMVA/runAnalyzer.m and the gates around it.
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...
Matrix< T > QN
Environment-averaged metrics, (nstations x nclasses).
solvers::CacheMetrics< T > cache
The environment-blended cache surface, whichever coupling produced it.
One arc of the environment process.
lang::Distrib< T > dist
the e -> h transition time
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
The INFO output of map2renv: how the stage set was assembled.
Definition map2renv.h:48
bool is_mmpp
True when every process was an MMPP, so the image is exact in structure.
Definition map2renv.h:53
The metrics getAvg returns, after filtering.
Matrix< T > TN
throughput
Matrix< T > RN
response time, per visit
std::string warning
The reference's own warning text, verbatim, empty when it did not warn.
Matrix< T > UN
utilization
Matrix< T > WN
residence time, per job
std::string method
the method asked for
std::string actualmethod
the algorithm that ran
Matrix< T > QN
queue length
std::vector< T > CN
system response time per class
std::vector< T > XN
system throughput per class
solvers::CacheMetrics< T > cache
What the cache branches observed, EMPTY on a model with no Cache node and on every solver that does n...
Matrix< T > AN
arrival rate
The caller-facing map_env knobs, options.config.map_env and friends.
std::size_t max_stages
0 = the transform's own default cap
What the gate decided, and what it decided it about.