LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
fluid_diffusion.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_DIFFUSION_H
6#define LINE_SOLVERS_FLUID_FLUID_DIFFUSION_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The `diffusion` method: a port of `solver_fluid_diffusion.m`.
12 *
13 * WHAT IT COMPUTES. The fluid limit follows the MEAN drift and says nothing
14 * about fluctuation. The diffusion approximation simulates the diffusion limit
15 * by Euler-Maruyama. Every routing arc a -> b carries the flux phi_a(x) P(a,b),
16 * with phi = mu*x at an INF station and mu*x/max(1,n_i) at a single server
17 * (n_i the station total), plus its own Brownian noise sqrt(phi_a P(a,b) dt) Z
18 * moved from a to b, so each step conserves the chain populations exactly. A
19 * transfer that would drive a coordinate below zero is scaled down until it
20 * lands there, which keeps the population exact without re-projecting.
21 *
22 * QN is the time average over the SECOND HALF of the trajectory (the first is
23 * warm-up). TN is X_c * V(i,r) per chain, with X_c the mean departure rate
24 * E[phi]/V at the chain's bottleneck (largest V/mu, single servers before INF
25 * stations), so the throughputs are flow-balanced by construction; UN is TN/mu
26 * at a single server and QN at an INF station.
27 *
28 * THE HORIZON IS NOT iter_max. `timestep` is the step and a finite
29 * `timespan_end` sets the horizon, ceil(tend/dt) steps; otherwise 100000 steps
30 * are taken. iter_max is the closing-iteration count every FLD run carries a
31 * default for, and sizing the SDE with it once capped the run at ~200 steps.
32 * That, together with a drift that served the whole queue at a single server
33 * and unit noise per coordinate, made the method several-fold wrong.
34 *
35 * PARITY IS STATISTICAL, NOT EXACT. The trajectory is driven by pseudorandom
36 * normals, and MATLAB's `randn` and this port's Mersenne Twister produce
37 * different streams from the same seed. Two runs therefore agree in
38 * distribution and not digit for digit -- the same situation the SSA solvers
39 * are in across codebases. Tests must assert on averages and invariants
40 * (population conservation, ordering), never on a specific trajectory.
41 */
42
43#include <algorithm>
44#include <cmath>
45#include <cstddef>
46#include <random>
47#include <vector>
48
52#include "line/util/error.h"
53#include "line/util/matrix.h"
54
55namespace line {
56namespace fluid {
57
58/** Controls of the Euler-Maruyama trajectory. */
60 std::size_t steps = 100000; ///< number of steps (from timespan_end / dt when finite)
61 double dt = 0.01; ///< step size (the reference's timestep)
62 unsigned long seed = 23000; ///< RNG seed; see the header on parity
63};
64
65/** Steady-state averages of the diffusion trajectory. */
69
70/**
71 * Run the diffusion approximation of `sn`.
72 *
73 * Refuses by name every model shape the reference refuses: open classes, a
74 * Source, a discipline outside {PS, FCFS, INF, SIRO}, and any finite
75 * multiserver station.
76 */
77template <class T>
79 const std::size_t M = sn.nstations, K = sn.nclasses;
80 for (std::size_t r = 0; r < K; ++r)
81 if (!std::isfinite(sn.classes[r].population))
82 throw UnsupportedError(ErrorKind{"OpenClass"},
83 "fluid diffusion: the method supports closed networks only; class '" +
84 sn.classes[r].name + "' is open");
85 for (std::size_t i = 0; i < M; ++i) {
86 const lang::SchedStrategy sc = sn.stations[i].sched;
88 throw UnsupportedError(ErrorKind{"OpenClass"}, "fluid diffusion: a Source is not supported (station '" +
89 sn.stations[i].name + "'); the method is for closed networks");
92 throw UnsupportedError(ErrorKind{"SchedStrategy"},
93 "fluid diffusion: scheduling at station '" + sn.stations[i].name +
94 "' is outside the supported set {PS, FCFS, INF, SIRO}");
95 const double c = sn.stations[i].nservers;
96 if (std::isfinite(c) && c > 1.0)
97 throw UnsupportedError(ErrorKind{"MultiServer"},
98 "fluid diffusion: only single-server or infinite-server stations are supported; "
99 "station '" + sn.stations[i].name + "' has more than one server");
100 }
101
102 // Service rate per (station, class); 0 marks "not served here".
103 Matrix<double> mu(M, K, 0.0);
104 std::vector<bool> inf_server(M, false);
105 for (std::size_t i = 0; i < M; ++i) {
106 inf_server[i] = !std::isfinite(sn.stations[i].nservers);
107 for (std::size_t r = 0; r < K; ++r) {
108 if (sn.disabled[i][r] || sn.service[i][r].D0.rows() == 0) continue;
109 const double rate = num_traits<T>::to_double(sn.rates(i, r));
110 if (rate > 0.0 && std::isfinite(rate)) mu(i, r) = rate;
111 }
112 }
113
114 // Station-space routing, with the non-station stateful nodes folded out.
115 const std::size_t S = sn.nof_stateful();
116 std::vector<std::size_t> keep;
117 keep.reserve(M * K);
118 for (std::size_t i = 0; i < M; ++i) {
119 const std::size_t isf = sn.stateful_of_station(i + 1) - 1;
120 for (std::size_t r = 0; r < K; ++r) keep.push_back(isf * K + r);
121 }
122 Matrix<double> rt_full(S * K, S * K, 0.0);
123 if (sn.rt.rows() == S * K)
124 for (std::size_t a = 0; a < S * K; ++a)
125 for (std::size_t b = 0; b < S * K; ++b)
126 rt_full(a, b) = num_traits<T>::to_double(sn.rt(a, b));
127 const Matrix<double> P = mc::dtmc_stochcomp(rt_full, keep);
128
129 // Arcs between coordinates a = i*K + r; a self-loop moves no job.
130 const std::size_t MK = M * K;
131 std::vector<std::size_t> asrc, adst;
132 std::vector<double> aprob;
133 for (std::size_t a = 0; a < MK; ++a)
134 for (std::size_t b = 0; b < MK; ++b)
135 if (a != b && P(a, b) > 0.0) {
136 asrc.push_back(a);
137 adst.push_back(b);
138 aprob.push_back(P(a, b));
139 }
140 const std::size_t narc = asrc.size();
141
142 auto phi = [&](const std::vector<double>& x, std::vector<double>& f) {
143 for (std::size_t i = 0; i < M; ++i) {
144 double n = 0.0;
145 for (std::size_t r = 0; r < K; ++r) n += x[i * K + r];
146 const double den = inf_server[i] ? 1.0 : std::max(1.0, n);
147 for (std::size_t r = 0; r < K; ++r) f[i * K + r] = mu(i, r) * x[i * K + r] / den;
148 }
149 };
150
151 const double dt = opt.dt > 0.0 ? opt.dt : 0.01;
152 const std::size_t steps = std::max<std::size_t>(2, opt.steps);
153 std::mt19937_64 rng(opt.seed);
154 std::normal_distribution<double> gauss(0.0, 1.0);
155
156 // Each class starts spread evenly over the stations that serve it.
157 std::vector<double> x(MK, 0.0), f(MK, 0.0);
158 for (std::size_t r = 0; r < K; ++r) {
159 std::size_t nsv = 0;
160 for (std::size_t i = 0; i < M; ++i)
161 if (mu(i, r) > 0.0) ++nsv;
162 for (std::size_t i = 0; i < M; ++i)
163 if (mu(i, r) > 0.0) x[i * K + r] = sn.classes[r].population / static_cast<double>(nsv);
164 }
165 phi(x, f);
166
167 const std::size_t burn = steps / 2;
168 std::vector<double> qsum(MK, 0.0), fsum(MK, 0.0), gain(MK), loss(MK), xn(MK), theta(MK);
169 std::vector<std::size_t> loser(narc), gainer(narc);
170 std::vector<double> amt(narc);
171 for (std::size_t step = 1; step < steps; ++step) {
172 for (std::size_t e = 0; e < narc; ++e) {
173 const double fl = f[asrc[e]] * aprob[e] * dt;
174 const double inc = fl + std::sqrt(fl) * gauss(rng);
175 // a transfer drains its source when positive and its destination when negative
176 loser[e] = inc > 0.0 ? asrc[e] : adst[e];
177 gainer[e] = inc > 0.0 ? adst[e] : asrc[e];
178 amt[e] = std::fabs(inc);
179 }
180 for (int it = 0; it < 100; ++it) {
181 std::fill(gain.begin(), gain.end(), 0.0);
182 std::fill(loss.begin(), loss.end(), 0.0);
183 for (std::size_t e = 0; e < narc; ++e) {
184 gain[gainer[e]] += amt[e];
185 loss[loser[e]] += amt[e];
186 }
187 bool neg = false;
188 for (std::size_t a = 0; a < MK; ++a) {
189 xn[a] = x[a] + gain[a] - loss[a];
190 theta[a] = 1.0;
191 if (xn[a] < -lang::GlobalConstants::FineTol) {
192 theta[a] = (x[a] + gain[a]) / loss[a];
193 neg = true;
194 }
195 }
196 if (!neg) break;
197 for (std::size_t e = 0; e < narc; ++e) amt[e] *= theta[loser[e]];
198 }
199 for (std::size_t a = 0; a < MK; ++a) x[a] = std::max(xn[a], 0.0);
200 phi(x, f);
201 if (step >= burn)
202 for (std::size_t a = 0; a < MK; ++a) {
203 qsum[a] += x[a];
204 fsum[a] += f[a];
205 }
206 }
207 const double navg = static_cast<double>(steps - std::max<std::size_t>(burn, 1));
208
209 DiffusionResult out;
210 out.QN = Matrix<double>(M, K, 0.0);
211 out.UN = Matrix<double>(M, K, 0.0);
212 out.RN = Matrix<double>(M, K, 0.0);
213 out.TN = Matrix<double>(M, K, 0.0);
214 for (std::size_t a = 0; a < MK; ++a) out.QN(a / K, a % K) = qsum[a] / navg;
215
216 // Per chain: the mean rate at its bottleneck, spread over the visits.
217 for (std::size_t c = 0; c < sn.nchains; ++c) {
218 const Matrix<T>& V = sn.visits[c];
219 auto vis = [&](std::size_t i, std::size_t r) {
220 return num_traits<T>::to_double(V(sn.stateful_of_station(i + 1) - 1, r));
221 };
222 bool found = false, bfin = false;
223 std::size_t bi = 0, br = 0;
224 double bD = -1.0;
225 for (std::size_t i = 0; i < M; ++i)
226 for (std::size_t r = 0; r < K; ++r) {
227 if (!sn.chains[c][r] || mu(i, r) <= 0.0) continue;
228 const double v = vis(i, r);
229 if (!(v > 0.0)) continue;
230 // a single server outranks every INF station: its rate is bounded by mu, so the
231 // boundary distorts it least, and only there does TN = X*V keep sum_r U <= 1
232 const bool fin = !inf_server[i];
233 const double D = v / mu(i, r);
234 if (!found || (fin && !bfin) || (fin == bfin && D > bD)) {
235 found = true;
236 bi = i;
237 br = r;
238 bfin = fin;
239 bD = D;
240 }
241 }
242 if (!found) continue;
243 const double Xc = (fsum[bi * K + br] / navg) / vis(bi, br);
244 for (std::size_t i = 0; i < M; ++i)
245 for (std::size_t r = 0; r < K; ++r)
246 if (sn.chains[c][r] && mu(i, r) > 0.0) out.TN(i, r) = Xc * vis(i, r);
247 }
248 for (std::size_t i = 0; i < M; ++i)
249 for (std::size_t r = 0; r < K; ++r) {
250 out.UN(i, r) = inf_server[i] ? out.QN(i, r) : (mu(i, r) > 0.0 ? out.TN(i, r) / mu(i, r) : 0.0);
251 // TN is zero only to the integrator's accuracy.
252 if (out.TN(i, r) > lang::GlobalConstants::Zero) out.RN(i, r) = out.QN(i, r) / out.TN(i, r);
253 }
254 return out;
255}
256
257} // namespace fluid
258} // namespace line
259
260#endif // LINE_SOLVERS_FLUID_FLUID_DIFFUSION_H
UnsupportedError(const std::string &what)
Definition error.h:119
A network plus its refreshed NetworkStruct.
Stochastic complement of a DTMC partition, a port of matlab/lib/kpctoolbox/mc/dtmc_stochcomp....
The exception types the port throws.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
Dense matrix and non-owning view.
DiffusionResult fluid_diffusion(const qn::NetworkStruct< T > &sn, const DiffusionOptions &opt)
Run the diffusion approximation of sn.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:185
Matrix< T > dtmc_stochcomp(const Matrix< T > &P, const std::vector< std::size_t > &keep)
Stochastic complement of a DTMC partition, a port of matlab/lib/kpctoolbox/mc/dtmc_stochcomp....
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
Controls of the Euler-Maruyama trajectory.
unsigned long seed
RNG seed; see the header on parity.
std::size_t steps
number of steps (from timespan_end / dt when finite)
double dt
step size (the reference's timestep)
Steady-state averages of the diffusion trajectory.
static constexpr double FineTol
Definition lang_types.h:765
static constexpr double Zero
Definition lang_types.h:767