LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_env_limit.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_LIMIT_H
6#define LINE_SOLVERS_ENV_SOLVER_ENV_LIMIT_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The two CLOSED-FORM environment limits: `SolverENV.solveEnvLimit`, reached by
12 * `options.method` in {`avg`, `dec`}.
13 *
14 * Both replace the fixed point with a single reading, and each is exact at one
15 * end of the time-scale separation between the environment and the network:
16 *
17 * `avg`, the FAST-environment limit. The environment switches so much faster
18 * than the network responds that the network only ever sees the AVERAGE of the
19 * modulated rates. One rate-averaged model is built -- every station-class
20 * rate that varies across stages replaced by its probEnv-weighted mean, as an
21 * exponential -- and solved once, in steady state. Exact as the stage-switch
22 * rate goes to infinity.
23 *
24 * `dec`, the SLOW-environment limit, i.e. quasi-stationary decomposition. The
25 * environment stays in a stage long enough for the network to reach that
26 * stage's own steady state, so each stage is solved independently and the
27 * metrics are averaged with weights probEnv. Exact as the stage-switch rate
28 * goes to zero.
29 *
30 * NEITHER CARRIES ANYTHING ACROSS A SWITCH, which is what makes them closed
31 * form and also what they give up: no entry state, no reset policy, no
32 * transient. A reset policy declared on an arc is therefore inert here, exactly
33 * as it is in the reference, whose `solveEnvLimit` never reads `resetFun`.
34 *
35 * WHAT IS AVERAGED, AND WHAT IS LEFT ALONE (`buildRateAveragedModel`). Only a
36 * Source, a Queue or a Delay carries a rate to average. A station-class pair
37 * that is disabled in any stage, or whose rate does not actually vary across
38 * them, keeps its ORIGINAL distribution rather than being rewritten as an
39 * exponential of its own mean -- so a non-modulated Erlang stays an Erlang, and
40 * the base model is preserved exactly outside the modulated rates.
41 *
42 * WHAT IS REFUSED. A stage holding a Cache: the reference aggregates the hit and
43 * miss ratios over the stages (`SolverENV.accumCacheMetric`) and writes them
44 * back onto the stage-1 model, and this port has no cache metric to aggregate,
45 * so it would report Q, U and T while silently dropping the answer a cache model
46 * is asked for.
47 */
48
49#include <algorithm>
50#include <cmath>
51#include <cstddef>
52#include <string>
53#include <type_traits>
54#include <vector>
55
58#include "line/num/number.h"
61#include "line/util/error.h"
62#include "line/util/matrix.h"
63
64namespace line {
65namespace env {
66
67/** What a limit solve reports. */
69 /** Environment-averaged metrics, (nstations x nclasses). */
71 /** The limit that ran: `avg` or `dec`. */
72 std::string method;
73 /** `dec` only: each stage's own steady state, before the probEnv blend. */
74 std::vector<Matrix<double> > QStage, UStage, TStage;
75 /** The stage probabilities the blend used. */
76 std::vector<double> prob_env;
77};
78
79/**
80 * The limit solver.
81 *
82 * `envObj` must already carry a model per stage; `init()` is called here, as
83 * `solveEnvLimit` calls `self.init()` before reading probEnv.
84 */
85template <class T>
87public:
88 SolverEnvLimit(Environment<T>& e, const EnvOptions& o) : envObj(e), opt(o) { init(); }
89
90 EnvLimitSolution solve() { return dec_ ? solve_dec() : solve_avg(); }
91
92private:
93 void init() {
94 if (opt.method != "avg" && opt.method != "dec")
95 throw UnsupportedError("SolverENV limit: '" + opt.method +
96 "' is not a closed-form environment limit; the two are 'avg' "
97 "(fast environment) and 'dec' (slow environment)");
98 dec_ = (opt.method == "dec");
99 if (opt.stage_solver != "fluid")
100 throw UnsupportedError(
101 "SolverENV limit: stage solver '" + opt.stage_solver +
102 "' is not available; the limits solve each stage in STEADY STATE and the fluid "
103 "analyzer is the one this port wires into the environment");
104 if (!std::is_same<T, double>::value)
105 throw UnsupportedError(
106 "SolverENV limit: a fluid stage integrates its drift with LSODA, which is double "
107 "only; rerun with --arith double");
108
109 // The two limits read the stage NETWORKS directly -- `dec` solves each
110 // in steady state, `avg` builds one network at the probEnv-weighted
111 // rates -- and a layered stage has no station rate table to weight. The
112 // reference has no branch for it either: `solveEnvLimit` reaches for
113 // `self.ensemble{1}.nodes`, which a LayeredNetwork does not carry.
114 envObj.reject_lqn_stages(
115 "SolverENV limit",
116 "the fast/slow limits read a stage's station rates directly -- 'dec' solves each "
117 "stage network in steady state and 'avg' builds one network at the probEnv-weighted "
118 "rates -- and a layered model has no such rate table, only the layers SolverLN "
119 "derives from it");
120 envObj.init();
121 const std::size_t E = envObj.nstages();
122 M = envObj.stage(0).model.nstations;
123 K = envObj.stage(0).model.nclasses;
124 for (std::size_t e = 1; e < E; ++e)
125 if (envObj.stage(e).model.nstations != M || envObj.stage(e).model.nclasses != K)
126 throw InputError(
127 "SolverENV limit: every stage must have the same stations and classes; the "
128 "metrics are blended entrywise across them");
129 for (std::size_t e = 0; e < E; ++e) {
130 const qn::NetworkStruct<T>& sn = envObj.stage(e).model;
131 for (std::size_t ind = 0; ind < sn.nodes.size(); ++ind)
132 if (sn.nodes[ind].nodetype == lang::NodeType::Cache)
133 throw UnsupportedError(
134 "SolverENV limit: stage " + std::to_string(e + 1) +
135 " holds a Cache, whose environment-blended hit and miss ratios come from "
136 "SolverENV.accumCacheMetric over the per-stage cache results; no cache "
137 "metric is reported by the stage solver here, so the blend would drop "
138 "the answer the model is asked for");
139 }
140 }
141
142 /** The slow-environment limit: each stage on its own, blended by probEnv. */
143 EnvLimitSolution solve_dec() {
144 const std::size_t E = envObj.nstages();
145 EnvLimitSolution out;
146 out.method = "dec";
147 out.prob_env = envObj.prob_env;
148 out.QN = Matrix<double>(M, K, 0.0);
149 out.UN = Matrix<double>(M, K, 0.0);
150 out.TN = Matrix<double>(M, K, 0.0);
151 out.QStage.assign(E, Matrix<double>(M, K, 0.0));
152 out.UStage.assign(E, Matrix<double>(M, K, 0.0));
153 out.TStage.assign(E, Matrix<double>(M, K, 0.0));
154 for (std::size_t e = 0; e < E; ++e) {
155 const fluid::FluidSolution s = stage_steady_state(envObj.stage(e).model);
156 const double p = envObj.prob_env[e];
157 for (std::size_t i = 0; i < M; ++i)
158 for (std::size_t r = 0; r < K; ++r) {
159 out.QStage[e](i, r) = s.QN(i, r);
160 out.UStage[e](i, r) = s.UN(i, r);
161 out.TStage[e](i, r) = s.TN(i, r);
162 out.QN(i, r) += p * s.QN(i, r);
163 out.UN(i, r) += p * s.UN(i, r);
164 out.TN(i, r) += p * s.TN(i, r);
165 }
166 }
167 return out;
168 }
169
170 /** The fast-environment limit: one rate-averaged model, solved once. */
171 EnvLimitSolution solve_avg() {
172 EnvLimitSolution out;
173 out.method = "avg";
174 out.prob_env = envObj.prob_env;
175 const qn::NetworkStruct<T> avg = rate_averaged_model();
176 const fluid::FluidSolution s = stage_steady_state(avg);
177 out.QN = Matrix<double>(M, K, 0.0);
178 out.UN = Matrix<double>(M, K, 0.0);
179 out.TN = Matrix<double>(M, K, 0.0);
180 for (std::size_t i = 0; i < M; ++i)
181 for (std::size_t r = 0; r < K; ++r) {
182 out.QN(i, r) = s.QN(i, r);
183 out.UN(i, r) = s.UN(i, r);
184 out.TN(i, r) = s.TN(i, r);
185 }
186 return out;
187 }
188
189 fluid::FluidSolution stage_steady_state(const qn::NetworkStruct<T>& sn) const {
190 fluid::FluidOptions fo = opt.stage;
191 // A limit starts from nothing carried over -- there is no entry state to
192 // seed with, which is the whole content of the approximation.
193 fo.init_sol.clear();
194 return fluid::solver_fluid(sn, fo);
195 }
196
197 /**
198 * `buildRateAveragedModel`: stage 1's network with every MODULATED rate
199 * replaced by its probEnv-weighted mean, as an exponential.
200 *
201 * The refresh chain is rerun because `rates` and everything derived from it
202 * are read off the service table; editing the table alone would leave the
203 * struct describing stage 1.
204 */
205 qn::NetworkStruct<T> rate_averaged_model() const {
206 const std::size_t E = envObj.nstages();
207 qn::NetworkStruct<T> sn = envObj.stage(0).model;
208 std::vector<double> r(E, 0.0);
209 for (std::size_t i = 0; i < M; ++i) {
210 const lang::NodeType nt = sn.stations[i].nodetype;
213 continue;
214 for (std::size_t k = 0; k < K; ++k) {
215 bool ok = true;
216 for (std::size_t e = 0; e < E && ok; ++e) {
217 const qn::NetworkStruct<T>& se = envObj.stage(e).model;
218 // The reference's `any(isnan(r)) || any(r<=0)`: a rate this
219 // port reports as disabled is MATLAB's NaN, and either way
220 // the pair is left as configured.
221 if (se.disabled[i][k]) {
222 ok = false;
223 break;
224 }
225 r[e] = num_traits<T>::to_double(se.rates(i, k));
226 if (!(r[e] > 0.0) || !std::isfinite(r[e])) ok = false;
227 }
228 if (!ok) continue;
229 double lo = r[0], hi = r[0], avg = 0.0;
230 for (std::size_t e = 0; e < E; ++e) {
231 lo = std::min(lo, r[e]);
232 hi = std::max(hi, r[e]);
233 avg += envObj.prob_env[e] * r[e];
234 }
235 // Not modulated: keep the original distribution, which may carry
236 // a shape the exponential of its mean would throw away.
237 if (hi - lo <= 1e-12 * std::max(1.0, hi)) continue;
238 sn.set_service(i + 1, k + 1,
239 lang::Distrib<T>::exp_rate(num_traits<T>::from_double(avg)));
240 }
241 }
242 sn.refresh_struct();
243 return sn;
244 }
245
246 Environment<T>& envObj;
247 EnvOptions opt;
248 std::size_t M = 0, K = 0;
249 bool dec_ = false;
250};
251
252/** `solveEnvLimit` on the original stages. */
253template <class T>
255 SolverEnvLimit<T> s(e, o);
256 return s.solve();
257}
258
259} // namespace env
260} // namespace line
261
262#endif // LINE_SOLVERS_ENV_SOLVER_ENV_LIMIT_H
InputError(const std::string &what)
Definition error.h:39
UnsupportedError(const std::string &what)
Definition error.h:51
SolverEnvLimit(Environment< T > &e, const EnvOptions &o)
A network plus its refreshed NetworkStruct.
A random environment: a port of matlab/src/lang/Environment.m, restricted to what SolverENV reads out...
The exception types the port throws.
Dense matrix and non-owning view.
EnvLimitSolution solver_env_limit(Environment< T > &e, const EnvOptions &o)
solveEnvLimit on the original stages.
FluidSolution solver_fluid(const qn::NetworkStruct< T > &sn_in, const FluidOptions &opt, qn::NetworkStruct< T > *sn_out=nullptr)
Port of solver_fluid_analyzer.m: dispatch on the method, refit the non-exponential FCFS stations the ...
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:324
A queueing network and its refreshed NetworkStruct.
Number-type abstraction for the templated API port.
SolverENV: a queueing network in a random environment.
SolverFluid: the closing method, a port of solver_fluid.m, solver_fluid_iteration....
What a limit solve reports.
std::vector< Matrix< double > > UStage
std::vector< double > prob_env
The stage probabilities the blend used.
Matrix< double > QN
Environment-averaged metrics, (nstations x nclasses).
std::vector< Matrix< double > > QStage
dec only: each stage's own steady state, before the probEnv blend.
std::vector< Matrix< double > > TStage
std::string method
The limit that ran: avg or dec.
Options of SolverENV.
Definition solver_env.h:110
static Distrib exp_rate(const T &r)
Definition lang_types.h:814