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 g = 1.0 / std::pow(1.0 + std::pow(ratio, p), 1.0 / p);
102 }
103 return std::isnan(g) ? 0.0 : g;
104}
105
106} // namespace detail
107
108/** Assemble what the state-dependent drifts need from `sn`. */
109template <class T>
111 double alpha = 20.0, double pstar = 20.0) {
112 const std::size_t M = sn.nstations, K = sn.nclasses;
114 s.kind = kind;
115 s.alpha = alpha;
116 s.pstar = pstar;
118 const FluidLayout& L = s.layout;
119
120 s.mu.assign(M, std::vector<std::vector<double>>(K));
121 s.phi.assign(M, std::vector<std::vector<double>>(K));
122 s.pie.assign(M, std::vector<std::vector<double>>(K));
123 s.d0.assign(M, std::vector<Matrix<double>>(K));
124 for (std::size_t i = 0; i < M; ++i)
125 for (std::size_t r = 0; r < K; ++r) {
126 if (!L.enabled[i][r]) {
127 s.pie[i][r] = std::vector<double>{1.0};
128 continue;
129 }
130 detail::fluid_mu_phi(sn.service[i][r], s.mu[i][r], s.phi[i][r]);
131 s.pie[i][r] = detail::fluid_pie(sn.service[i][r]);
132 const std::size_t n = sn.service[i][r].D0.rows();
133 s.d0[i][r] = Matrix<double>(n, n, 0.0);
134 for (std::size_t a = 0; a < n; ++a)
135 for (std::size_t b = 0; b < n; ++b)
136 s.d0[i][r](a, b) = num_traits<T>::to_double(sn.service[i][r].D0(a, b));
137 }
138
139 const std::size_t S = sn.nof_stateful();
140 const bool have_rt = sn.rt.rows() == S * K;
141 std::vector<std::size_t> sf(M, 0);
142 for (std::size_t i = 0; i < M; ++i) sf[i] = sn.stateful_of_station(i + 1) - 1;
143 s.rt.assign(M, std::vector<std::vector<std::vector<double>>>(
144 K, std::vector<std::vector<double>>(M, std::vector<double>(K, 0.0))));
145 for (std::size_t i = 0; i < M && have_rt; ++i)
146 for (std::size_t c = 0; c < K; ++c)
147 for (std::size_t j = 0; j < M; ++j)
148 for (std::size_t l = 0; l < K; ++l)
149 s.rt[i][c][j][l] = num_traits<T>::to_double(sn.rt(sf[i] * K + c, sf[j] * K + l));
150
151 double closed_pop = 0.0;
152 for (std::size_t r = 0; r < K; ++r)
153 if (std::isfinite(sn.classes[r].population)) closed_pop += sn.classes[r].population;
154 s.sched.resize(M);
155 s.nservers.resize(M);
156 s.weight.assign(M, std::vector<double>(K, 1.0));
157 for (std::size_t i = 0; i < M; ++i) {
158 s.sched[i] = sn.stations[i].sched;
159 const double c = sn.stations[i].nservers;
160 s.nservers[i] = std::isfinite(c) ? c : closed_pop;
161 if (sn.stations[i].sched == lang::SchedStrategy::DPS)
162 for (std::size_t r = 0; r < K && r < sn.stations[i].schedparam.size(); ++r)
163 s.weight[i][r] = num_traits<T>::to_double(sn.stations[i].schedparam[r]);
164 if (sn.stations[i].sched == lang::SchedStrategy::EXT)
165 throw UnsupportedError(
166 "fluid: the 'statedep', 'softmin' and 'pnorm' methods have no source term and are "
167 "refused on an open model (station '" + sn.stations[i].name +
168 "' is a Source); use method 'closing' or 'matrix'");
169 }
170 return s;
171}
172
173/**
174 * The drift dx/dt for the state-dependent family.
175 *
176 * Written as the reference writes it: for each station, first the phase
177 * changes, then the completions, each moving `x[from] * rate` of mass.
178 */
179inline std::function<void(double, const double*, double*)> fluid_drift_statedep(
180 const FluidStateDepSystem& s) {
181 const std::size_t n = s.layout.nstates;
182 return [s, n](double, const double* x, double* dx) {
183 const FluidLayout& L = s.layout;
184 const std::size_t M = L.qidx.size();
185 const std::size_t K = M ? L.qidx[0].size() : 0;
186 for (std::size_t i = 0; i < n; ++i) dx[i] = 0.0;
187
188 for (std::size_t i = 0; i < M; ++i) {
189 const lang::SchedStrategy sc = s.sched[i];
190 const double c = s.nservers[i];
191
192 // The station's total occupancy, which every branch but INF needs.
193 double ni = 0.0;
194 for (std::size_t r = 0; r < K; ++r)
195 for (std::size_t k = 0; k < L.kic[i][r]; ++k) ni += x[L.qidx[i][r] + k];
196
197 // FCFS weights the share by the mean duration of the phase a job
198 // is in: w = -1/D0(k,k). DPS weights it by the class weight.
199 std::vector<std::vector<double>> wfcfs;
200 std::vector<double> wdps;
202 if (sc == lang::SchedStrategy::FCFS) {
203 wfcfs.assign(K, std::vector<double>());
204 for (std::size_t r = 0; r < K; ++r) {
205 wfcfs[r].assign(L.kic[i][r], 0.0);
206 if (!L.enabled[i][r]) continue;
207 for (std::size_t k = 0; k < L.kic[i][r]; ++k) {
208 const double d = s.d0[i][r](k, k);
209 wfcfs[r][k] = (d != 0.0) ? -1.0 / d : 0.0;
210 wni += wfcfs[r][k] * x[L.qidx[i][r] + k];
211 }
212 }
213 } else if (sc == lang::SchedStrategy::DPS) {
214 double wsum = 0.0;
215 for (std::size_t r = 0; r < K; ++r) wsum += s.weight[i][r];
216 wdps.assign(K, 0.0);
217 for (std::size_t r = 0; r < K; ++r)
218 wdps[r] = (wsum > 0.0) ? s.weight[i][r] / wsum : 0.0;
219 for (std::size_t r = 0; r < K; ++r) {
220 double blk = 0.0;
221 for (std::size_t k = 0; k < L.kic[i][r]; ++k) blk += x[L.qidx[i][r] + k];
222 wni += wdps[r] * blk;
223 }
224 }
225
226 // The smoothed capacity, shared by every rate at this station.
227 double ghat = 1.0;
228 if (s.kind == StateDepKind::PNorm)
229 ghat = (ni > 0.0 && c > 0.0) ? detail::fluid_pnorm_smooth(ni, c, s.pstar) : 1.0;
230 const double capped = (s.kind == StateDepKind::SoftMin)
231 ? detail::fluid_softmin(ni, c, s.alpha)
232 : std::min(ni, c);
233
234 // The multiplier applied to a rate of class r in phase k.
235 const auto factor = [&](std::size_t r, std::size_t k) -> double {
236 switch (sc) {
238 return 1.0;
240 // statedep/softmin: min-or-softmin times the phase share.
241 // pnorm: ghat in place of the capacity.
242 if (s.kind == StateDepKind::PNorm) return ghat * wfcfs[r][k] / wni;
243 return capped * wfcfs[r][k] / wni;
245 if (s.kind == StateDepKind::PNorm) return ghat * wdps[r] / wni;
246 return (ni > c) ? c * wdps[r] / wni : 1.0;
247 default: // PS and anything else the reference treats as PS
248 if (s.kind == StateDepKind::PNorm) return ghat;
249 return (ni > c && ni > 0.0) ? c / ni : 1.0;
250 }
251 };
252
253 // ---- phase changes within (i,r) --------------------------------
254 for (std::size_t r = 0; r < K; ++r) {
255 if (!L.enabled[i][r]) continue;
256 const std::size_t b = L.qidx[i][r];
257 for (std::size_t k = 0; k + 1 < L.kic[i][r]; ++k)
258 for (std::size_t kp = 0; kp < L.kic[i][r]; ++kp) {
259 if (kp == k) continue;
260 const double flow = x[b + k] * s.d0[i][r](k, kp) * factor(r, k);
261 dx[b + k] -= flow;
262 dx[b + kp] += flow;
263 }
264 }
265
266 // ---- service completions ---------------------------------------
267 for (std::size_t r = 0; r < K; ++r) {
268 if (!L.enabled[i][r]) continue;
269 const std::size_t b = L.qidx[i][r];
270 for (std::size_t j = 0; j < M; ++j) {
271 // The reference's `if j~=i`, INF only: see the header.
272 if (sc == lang::SchedStrategy::INF && j == i) continue;
273 for (std::size_t l = 0; l < K; ++l) {
274 if (!L.enabled[j][l]) continue;
275 const double p = s.rt[i][r][j][l];
276 if (!(p > 0.0)) continue;
277 const std::size_t bj = L.qidx[j][l];
278 for (std::size_t k = 0; k < L.kic[i][r]; ++k) {
279 const double base = s.phi[i][r][k] * s.mu[i][r][k] * p * factor(r, k);
280 for (std::size_t kj = 0; kj < L.kic[j][l]; ++kj) {
281 const double flow = x[b + k] * base * s.pie[j][l][kj];
282 dx[b + k] -= flow;
283 dx[bj + kj] += flow;
284 }
285 }
286 }
287 }
288 }
289 }
290 };
291}
292
293} // namespace fluid
294} // namespace line
295
296#endif // LINE_SOLVERS_FLUID_FLUID_ODES_STATEDEP_H
UnsupportedError(const std::string &what)
Definition error.h:51
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:181
A queueing network and its refreshed NetworkStruct.
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:668