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, mam::map_mean(e.hold_time[s].map()));
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(
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("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(
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.
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 out.AN = Matrix<T>(M, K, num_traits<T>::from_double(std::numeric_limits<double>::quiet_NaN()));
250 out.XN.assign(K, zero);
251 out.CN.assign(K, zero);
252 for (std::size_t i = 0; i < M; ++i)
253 for (std::size_t k = 0; k < K; ++k)
254 if (num_traits<T>::to_double(out.TN(i, k)) > 0.0)
255 out.RN(i, k) = T(out.QN(i, k) / out.TN(i, k));
256 for (std::size_t k = 0; k < K; ++k) {
257 const std::size_t ist = sn.classes[k].refstat;
258 if (ist >= 1 && ist <= M) out.XN[k] = out.TN(ist - 1, k);
259 if (num_traits<T>::to_double(out.XN[k]) > 0.0) {
260 T q = zero;
261 for (std::size_t i = 0; i < M; ++i) q = T(q + out.QN(i, k));
262 out.CN[k] = T(q / out.XN[k]);
263 }
264 }
266
267 // THE APPROXIMATION NOTICE IS NOT OPTIONAL. The caller asked for a solver
268 // that cannot consume this model's processes and is being answered by an
269 // approximation of a different model; `AvgResult::warning` exists for
270 // exactly this and is printed by the CLI and the avg table.
271 out.warning = "This solver has no native support for the non-renewal (MAP/MMPP) processes of "
272 "this model; the reported averages come from its " +
273 method + " random-environment approximation (" + std::to_string(info.nstages) +
274 " stages, " + map_env_detail::image_kind(info) +
275 " image). Set map_env='off' to reject the model instead.";
276
277 // The system metrics read the reference station, which for an open class is
278 // the Source. A stage solver whose transient does not report Source
279 // throughput leaves it at zero under the mean-field coupling, and the zero
280 // propagates into XN and CN. REPORT THAT rather than substituting the
281 // arrival rate, which would hide whose metric is missing.
282 const std::vector<double> nj = sn.njobs();
283 std::string openzero;
284 for (std::size_t k = 0; k < K; ++k) {
285 if (k < nj.size() && std::isfinite(nj[k])) continue;
286 if (num_traits<T>::to_double(out.XN[k]) != 0.0) continue;
287 bool any = false;
288 for (std::size_t i = 0; i < M; ++i)
289 if (num_traits<T>::to_double(out.QN(i, k)) > 0.0) any = true;
290 const std::size_t ist = sn.classes[k].refstat;
291 if (!any && ist >= 1 && ist <= sn.rates.rows() && k < sn.rates.cols()) {
292 const double lam = num_traits<T>::to_double(sn.rates(ist - 1, k));
293 any = std::isfinite(lam) && lam > 0.0;
294 }
295 if (!any) continue;
296 if (!openzero.empty()) openzero += ", ";
297 openzero += std::to_string(k + 1);
298 }
299 if (!openzero.empty())
300 out.warning += " The " + method +
301 " environment coupling returned no reference-station throughput for open "
302 "class(es) " +
303 openzero +
304 ", so their system throughput and system response time are reported as "
305 "zero; use map_env_method='dec' or 'avg' for system-level metrics.";
306 return out;
307}
308
309/**
310 * The `getAvg` funnel: run the model, or its environment image when the ONLY
311 * thing in the way is a non-renewal process.
312 *
313 * THREE OUTCOMES, AND THE THIRD IS THE SUBTLE ONE. Supported -> run. Needs the
314 * image -> `map_env_approx`. Otherwise -> RUN ANYWAY, and let the runner raise
315 * its own refusal in its own words. Raising a second copy of the refusal here
316 * would mean two messages for one condition, drifting apart as the feature sets
317 * move; `needs_map_env` is deliberately a question about whether the IMAGE
318 * helps, not a gate on whether the model is supported.
319 *
320 * NOT WIRED INTO THE `-s ba` ARM, deliberately. A bound request must be
321 * answered with a bound, and the environment image is an approximation of the
322 * model, so its bounds do not bracket the original one. The reference refuses
323 * `SolverBA` inside `needsMapEnv` itself; here the exclusion is the absence of
324 * this wrapper at the three BA call sites, each of which says so.
325 */
326template <class T, class Run, class StageFn>
327mva::AvgResult<T> run_avg(const qn::NetworkStruct<T>& sn, const std::string& solver,
328 const qn::FeatureSet& declared, const MapEnvConfig& cfg, Run run,
329 StageFn stage_fn, const std::string& requested_method = "default") {
330 const MapEnvDecision d = needs_map_env(declared, sn, cfg);
331 if (!d.needed) return run(sn);
332 return map_env_approx<T>(sn, solver, cfg, stage_fn, requested_method);
333}
334
335} // namespace solvers
336} // namespace line
337
338#endif // LINE_SOLVERS_MAP_ENV_H
InputError(const std::string &what)
Definition error.h:39
UnsupportedError(const std::string &what)
Definition error.h:51
const EnvStage< T > & stage(std::size_t e) const
std::size_t nstages() const
std::vector< mam::Mmap< double > > hold_time
holdTime[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
T map_mean(const Map< T > &m)
Mean inter-arrival time, 1/lambda.
Definition map_moment.h:101
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.
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:327
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.
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:113
std::string method
The inter-stage coupling: meanfield is the reference's default.
Definition solver_env.h:125
std::string stage_solver
Which solver runs each FLAT stage: the fluid transient or the enumerated CTMC.
Definition solver_env.h:123
double timespan_end
options.timespan(2) of the inner solver: the transient horizon.
Definition solver_env.h:136
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
std::size_t nstages
Definition map2renv.h:49
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.