LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
fluid_odes_statedep.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_FLUID_FLUID_ODES_STATEDEP_H
6#define LINE_SOLVERS_FLUID_FLUID_ODES_STATEDEP_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The state-dependent fluid drifts: ports of `ode_statedep.m`, `ode_softmin.m`
12 * and `ode_pnorm.m`.
13 *
14 * HOW THESE DIFFER FROM `closing`. The closing drift factors every rate into a
15 * constant times one state entry, which is why it can precompute `rateBase`
16 * and evaluate the drift as a sum over events. These three cannot: the server
17 * share a job receives depends on the whole station's occupancy, and for FCFS
18 * on the MEAN SERVICE TIME OF THE PHASE the job is in, so the multiplier
19 * changes per (class, phase) at every step. They are therefore written the way
20 * the reference writes them -- accumulate directly into dx, station by station
21 * -- and are correspondingly slower. That is the trade the reference names in
22 * its own comment: "slower than ODE_RATES_STATEINDEP, but allows rates that
23 * are more complex functions of x".
24 *
25 * WHAT THE THREE VARY. Only how a saturated station's capacity enters:
26 *
27 * statedep the hard `min(ni, c)`, which has a kink at ni = c
28 * softmin `softmin(ni, c, alpha)`, the weighted average
29 * (x e^-ax + y e^-ay)/(e^-ax + e^-ay), smooth everywhere
30 * pnorm `ghat = 1/(1 + (ni/c)^p)^(1/p)`, a smooth stand-in for
31 * min(1, c/ni) from Ruuskanen et al., PEVA 151 (2021)
32 *
33 * The kink is what makes the closing/statedep drift stiff near saturation, and
34 * smoothing it is what lets the integrator take longer steps. The fixed point
35 * moves slightly in exchange, which is why these are separate methods rather
36 * than a faster way to compute the same answer.
37 *
38 * OPEN MODELS ARE REFUSED. `ode_statedep.m` errors on an EXT station -- the
39 * family has no source term -- and so does this port, by name.
40 *
41 * A NOTE ON TWO REFERENCE ASYMMETRIES, both reproduced deliberately.
42 * 1. At an INF station the completion loop SKIPS j == i, so a delay that
43 * routes to itself contributes no flow; the PS, FCFS and DPS branches do
44 * not skip it. This is `ode_statedep.m`'s `if j~=i`.
45 * 2. `ode_pnorm.m`'s DPS branch scales by `ghat * w/wni` where statedep
46 * scales by `nservers * w/wni` -- ghat where the others have the server
47 * count. Faithful to the reference; noted because it does not follow from
48 * the smoothing argument.
49 */
50
51#include <cmath>
52#include <cstddef>
53#include <functional>
54#include <string>
55#include <vector>
56
59#include "line/util/error.h"
60
61namespace line {
62namespace fluid {
63
64/** Which smoothing the drift applies at a saturated station. */
66
67/** Everything the state-dependent drifts read, lowered once to doubles. */
70 std::vector<std::vector<std::vector<double>>> mu, phi, pie;
71 std::vector<std::vector<Matrix<double>>> d0; ///< the PH generator per (i,c)
72 std::vector<std::vector<std::vector<std::vector<double>>>> rt; ///< rt[i][c][j][l]
73 std::vector<lang::SchedStrategy> sched;
74 std::vector<double> nservers;
75 std::vector<std::vector<double>> weight;
77 double alpha = 20.0; ///< softmin sharpness
78 double pstar = 20.0; ///< p-norm exponent
79};
80
81namespace detail {
82
83/** Port of `util/softmin.m`, including its overflow guard. */
84inline double fluid_softmin(double x, double y, double alpha) {
85 const double lo = std::min(x, y), hi = std::max(x, y);
86 const double gap = hi - lo;
87 // exp(-alpha*gap) underflows past 745/alpha; there the min IS the answer.
88 if (!(gap < 745.0 / alpha)) return lo;
89 const double w = std::exp(-alpha * gap);
90 return lo + gap * w / (1.0 + w);
91}
92
93/** Port of `util/pnorm_smooth.m`: a smooth stand-in for min(1, c/x). */
94inline double fluid_pnorm_smooth(double x, double c, double p) {
95 if (x <= 0.0 || c <= 0.0) return 0.0;
96 const double ratio = x / c;
97 double g;
98 if (p <= 0.0) {
99 g = std::min(1.0, c / x); // the reference's hard-min fallback
100 } else {
101 // Above capacity the same value as (1/r)/(1+r^-p)^(1/p): r^p overflows for large p
102 g = ratio > 1.0 ? (1.0 / ratio) / std::pow(1.0 + std::pow(1.0 / ratio, p), 1.0 / p)
103 : 1.0 / std::pow(1.0 + std::pow(ratio, p), 1.0 / p);
104 }
105 return std::isnan(g) ? 0.0 : g;
106}
107
108} // namespace detail
109
110/** Assemble what the state-dependent drifts need from `sn`. */
111template <class T>
113 double alpha = 20.0, double pstar = 20.0) {
114 const std::size_t M = sn.nstations, K = sn.nclasses;
116 s.kind = kind;
117 s.alpha = alpha;
118 s.pstar = pstar;
120 const FluidLayout& L = s.layout;
121
122 s.mu.assign(M, std::vector<std::vector<double>>(K));
123 s.phi.assign(M, std::vector<std::vector<double>>(K));
124 s.pie.assign(M, std::vector<std::vector<double>>(K));
125 s.d0.assign(M, std::vector<Matrix<double>>(K));
126 for (std::size_t i = 0; i < M; ++i)
127 for (std::size_t r = 0; r < K; ++r) {
128 if (!L.enabled[i][r]) {
129 s.pie[i][r] = std::vector<double>{1.0};
130 continue;
131 }
132 detail::fluid_mu_phi(sn.service[i][r], s.mu[i][r], s.phi[i][r]);
133 s.pie[i][r] = detail::fluid_pie(sn.service[i][r]);
134 const std::size_t n = sn.service[i][r].D0.rows();
135 s.d0[i][r] = Matrix<double>(n, n, 0.0);
136 for (std::size_t a = 0; a < n; ++a)
137 for (std::size_t b = 0; b < n; ++b)
138 s.d0[i][r](a, b) = num_traits<T>::to_double(sn.service[i][r].D0(a, b));
139 }
140
141 const std::size_t S = sn.nof_stateful();
142 const bool have_rt = sn.rt.rows() == S * K;
143 std::vector<std::size_t> sf(M, 0);
144 for (std::size_t i = 0; i < M; ++i) sf[i] = sn.stateful_of_station(i + 1) - 1;
145 s.rt.assign(M, std::vector<std::vector<std::vector<double>>>(
146 K, std::vector<std::vector<double>>(M, std::vector<double>(K, 0.0))));
147 for (std::size_t i = 0; i < M && have_rt; ++i)
148 for (std::size_t c = 0; c < K; ++c)
149 for (std::size_t j = 0; j < M; ++j)
150 for (std::size_t l = 0; l < K; ++l)
151 s.rt[i][c][j][l] = num_traits<T>::to_double(sn.rt(sf[i] * K + c, sf[j] * K + l));
152
153 double closed_pop = 0.0;
154 for (std::size_t r = 0; r < K; ++r)
155 if (std::isfinite(sn.classes[r].population)) closed_pop += sn.classes[r].population;
156 s.sched.resize(M);
157 s.nservers.resize(M);
158 s.weight.assign(M, std::vector<double>(K, 1.0));
159 for (std::size_t i = 0; i < M; ++i) {
160 s.sched[i] = sn.stations[i].sched;
161 const double c = sn.stations[i].nservers;
162 s.nservers[i] = std::isfinite(c) ? c : closed_pop;
163 if (sn.stations[i].sched == lang::SchedStrategy::DPS)
164 for (std::size_t r = 0; r < K && r < sn.stations[i].schedparam.size(); ++r)
165 s.weight[i][r] = num_traits<T>::to_double(sn.stations[i].schedparam[r]);
166 if (sn.stations[i].sched == lang::SchedStrategy::EXT)
167 throw UnsupportedError(ErrorKind{"OpenClass"},
168 "fluid: the 'statedep', 'softmin' and 'pnorm' methods have no source term and are "
169 "refused on an open model (station '" + sn.stations[i].name +
170 "' is a Source); use method 'closing' or 'matrix'");
171 }
172 return s;
173}
174
175/**
176 * The drift dx/dt for the state-dependent family.
177 *
178 * Written as the reference writes it: for each station, first the phase
179 * changes, then the completions, each moving `x[from] * rate` of mass.
180 */
181inline std::function<void(double, const double*, double*)> fluid_drift_statedep(
182 const FluidStateDepSystem& s) {
183 const std::size_t n = s.layout.nstates;
184 return [s, n](double, const double* x, double* dx) {
185 const FluidLayout& L = s.layout;
186 const std::size_t M = L.qidx.size();
187 const std::size_t K = M ? L.qidx[0].size() : 0;
188 for (std::size_t i = 0; i < n; ++i) dx[i] = 0.0;
189
190 for (std::size_t i = 0; i < M; ++i) {
191 const lang::SchedStrategy sc = s.sched[i];
192 const double c = s.nservers[i];
193
194 // The station's total occupancy, which every branch but INF needs.
195 double ni = 0.0;
196 for (std::size_t r = 0; r < K; ++r)
197 for (std::size_t k = 0; k < L.kic[i][r]; ++k) ni += x[L.qidx[i][r] + k];
198
199 // FCFS weights the share by the mean duration of the phase a job
200 // is in: w = -1/D0(k,k). DPS weights it by the class weight.
201 std::vector<std::vector<double>> wfcfs;
202 std::vector<double> wdps;
204 if (sc == lang::SchedStrategy::FCFS) {
205 wfcfs.assign(K, std::vector<double>());
206 for (std::size_t r = 0; r < K; ++r) {
207 wfcfs[r].assign(L.kic[i][r], 0.0);
208 if (!L.enabled[i][r]) continue;
209 for (std::size_t k = 0; k < L.kic[i][r]; ++k) {
210 const double d = s.d0[i][r](k, k);
211 wfcfs[r][k] = (d != 0.0) ? -1.0 / d : 0.0;
212 wni += wfcfs[r][k] * x[L.qidx[i][r] + k];
213 }
214 }
215 } else if (sc == lang::SchedStrategy::DPS) {
216 double wsum = 0.0;
217 for (std::size_t r = 0; r < K; ++r) wsum += s.weight[i][r];
218 wdps.assign(K, 0.0);
219 for (std::size_t r = 0; r < K; ++r)
220 wdps[r] = (wsum > 0.0) ? s.weight[i][r] / wsum : 0.0;
221 for (std::size_t r = 0; r < K; ++r) {
222 double blk = 0.0;
223 for (std::size_t k = 0; k < L.kic[i][r]; ++k) blk += x[L.qidx[i][r] + k];
224 wni += wdps[r] * blk;
225 }
226 }
227
228 // The smoothed capacity, shared by every rate at this station.
229 double ghat = 1.0;
230 if (s.kind == StateDepKind::PNorm)
231 ghat = (ni > 0.0 && c > 0.0) ? detail::fluid_pnorm_smooth(ni, c, s.pstar) : 1.0;
232 const double capped = (s.kind == StateDepKind::SoftMin)
233 ? detail::fluid_softmin(ni, c, s.alpha)
234 : std::min(ni, c);
235
236 // The multiplier applied to a rate of class r in phase k.
237 const auto factor = [&](std::size_t r, std::size_t k) -> double {
238 switch (sc) {
240 return 1.0;
242 // statedep/softmin: min-or-softmin times the phase share.
243 // pnorm: ghat in place of the capacity.
244 if (s.kind == StateDepKind::PNorm) return ghat * wfcfs[r][k] / wni;
245 return capped * wfcfs[r][k] / wni;
247 if (s.kind == StateDepKind::PNorm) return ghat * wdps[r] / wni;
248 return (ni > c) ? c * wdps[r] / wni : 1.0;
249 default: // PS and anything else the reference treats as PS
250 if (s.kind == StateDepKind::PNorm) return ghat;
251 return (ni > c && ni > 0.0) ? c / ni : 1.0;
252 }
253 };
254
255 // ---- phase changes within (i,r) --------------------------------
256 for (std::size_t r = 0; r < K; ++r) {
257 if (!L.enabled[i][r]) continue;
258 const std::size_t b = L.qidx[i][r];
259 for (std::size_t k = 0; k + 1 < L.kic[i][r]; ++k)
260 for (std::size_t kp = 0; kp < L.kic[i][r]; ++kp) {
261 if (kp == k) continue;
262 const double flow = x[b + k] * s.d0[i][r](k, kp) * factor(r, k);
263 dx[b + k] -= flow;
264 dx[b + kp] += flow;
265 }
266 }
267
268 // ---- service completions ---------------------------------------
269 for (std::size_t r = 0; r < K; ++r) {
270 if (!L.enabled[i][r]) continue;
271 const std::size_t b = L.qidx[i][r];
272 for (std::size_t j = 0; j < M; ++j) {
273 // The reference's `if j~=i`, INF only: see the header.
274 if (sc == lang::SchedStrategy::INF && j == i) continue;
275 for (std::size_t l = 0; l < K; ++l) {
276 if (!L.enabled[j][l]) continue;
277 const double p = s.rt[i][r][j][l];
278 if (!(p > 0.0)) continue;
279 const std::size_t bj = L.qidx[j][l];
280 for (std::size_t k = 0; k < L.kic[i][r]; ++k) {
281 const double base = s.phi[i][r][k] * s.mu[i][r][k] * p * factor(r, k);
282 for (std::size_t kj = 0; kj < L.kic[j][l]; ++kj) {
283 const double flow = x[b + k] * base * s.pie[j][l][kj];
284 dx[b + k] -= flow;
285 dx[bj + kj] += flow;
286 }
287 }
288 }
289 }
290 }
291 }
292 };
293}
294
295} // namespace fluid
296} // namespace line
297
298#endif // LINE_SOLVERS_FLUID_FLUID_ODES_STATEDEP_H
UnsupportedError(const std::string &what)
Definition error.h:119
A network plus its refreshed NetworkStruct.
The exception types the port throws.
The fluid drift: a port of solver_fluid_odes.m and the ode_jumps_new / ode_rate_base / ode_rates_clos...
FluidLayout fluid_layout(const qn::NetworkStruct< T > &sn)
Port of the layout half of solver_fluid_odes.m.
Definition fluid_odes.h:282
StateDepKind
Which smoothing the drift applies at a saturated station.
std::function< void(double, const double *, double *)> fluid_drift_statedep(const FluidStateDepSystem &s)
The drift dx/dt for the state-dependent family.
FluidStateDepSystem fluid_statedep_system(const qn::NetworkStruct< T > &sn, StateDepKind kind, double alpha=20.0, double pstar=20.0)
Assemble what the state-dependent drifts need from sn.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:185
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
Where each (station, class) block sits in the state vector.
Definition fluid_odes.h:86
std::vector< std::vector< std::size_t > > qidx
0-based first index of (i,r)
Definition fluid_odes.h:88
std::size_t nstates
length of the state vector
Definition fluid_odes.h:87
std::vector< std::vector< bool > > enabled
whether (i,r) is served at all
Definition fluid_odes.h:90
std::vector< std::vector< std::size_t > > kic
phases held by (i,r); 0 when disabled
Definition fluid_odes.h:89
Everything the state-dependent drifts read, lowered once to doubles.
std::vector< std::vector< std::vector< double > > > mu
std::vector< std::vector< std::vector< std::vector< double > > > > rt
rt[i][c][j][l]
std::vector< std::vector< Matrix< double > > > d0
the PH generator per (i,c)
std::vector< std::vector< double > > weight
std::vector< std::vector< std::vector< double > > > phi
std::vector< lang::SchedStrategy > sched
std::vector< std::vector< std::vector< double > > > pie
static constexpr double FineTol
Definition lang_types.h:765