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 adds a noise term and
15 * integrates the resulting stochastic differential equation by
16 * Euler-Maruyama,
17 *
18 * x <- max(0, x + drift(x) dt + sqrt(dt) Z), Z standard normal,
19 *
20 * renormalising each class back to its population after every step so the
21 * closed network stays closed. The reported queue lengths are the TIME AVERAGE
22 * over the whole trajectory, not the end state: a single noisy path is
23 * meaningless at its last instant and informative in the mean.
24 *
25 * THE DRIFT HERE IS SIMPLER THAN THE FLUID ONE. It is flow in minus flow out
26 * with the service rate applied to the whole queue -- x/mu_inv -- and no
27 * server-sharing term at all. That is why the reference restricts the method
28 * to single-server and infinite-server stations: with c = 1 the fluid rate
29 * min(x, c)/mu_inv and this x/mu_inv differ, and the reference chooses the
30 * latter, so the two methods answer slightly different questions. All the
31 * restrictions below are the reference's and are enforced by name.
32 *
33 * PARITY IS STATISTICAL, NOT EXACT. The trajectory is driven by pseudorandom
34 * normals, and MATLAB's `randn` and this port's Mersenne Twister produce
35 * different streams from the same seed. Two runs therefore agree in
36 * distribution and not digit for digit -- the same situation the SSA solvers
37 * are in across codebases. Tests must assert on averages and invariants
38 * (population conservation, ordering), never on a specific trajectory.
39 */
40
41#include <cmath>
42#include <cstddef>
43#include <random>
44#include <vector>
45
48#include "line/util/error.h"
49#include "line/util/matrix.h"
50
51namespace line {
52namespace fluid {
53
54/** Controls of the Euler-Maruyama trajectory. */
56 std::size_t steps = 10000; ///< number of steps (the reference's iter_max)
57 double dt = 0.01; ///< step size (the reference's timestep)
58 unsigned long seed = 23000; ///< RNG seed; see the header on parity
59};
60
61/** Time-averaged queue lengths of the diffusion trajectory. */
65
66/**
67 * Run the diffusion approximation of `sn`.
68 *
69 * Refuses by name every model shape the reference refuses: open classes, a
70 * Source, a discipline outside {PS, FCFS, INF, SIRO}, and any finite
71 * multiserver station.
72 */
73template <class T>
75 const std::size_t M = sn.nstations, K = sn.nclasses;
76 for (std::size_t r = 0; r < K; ++r)
77 if (!std::isfinite(sn.classes[r].population))
78 throw UnsupportedError(
79 "fluid diffusion: the method supports closed networks only; class '" +
80 sn.classes[r].name + "' is open");
81 for (std::size_t i = 0; i < M; ++i) {
82 const lang::SchedStrategy sc = sn.stations[i].sched;
84 throw UnsupportedError("fluid diffusion: a Source is not supported (station '" +
85 sn.stations[i].name + "'); the method is for closed networks");
88 throw UnsupportedError(
89 "fluid diffusion: scheduling at station '" + sn.stations[i].name +
90 "' is outside the supported set {PS, FCFS, INF, SIRO}");
91 const double c = sn.stations[i].nservers;
92 if (std::isfinite(c) && c > 1.0)
93 throw UnsupportedError(
94 "fluid diffusion: only single-server or infinite-server stations are supported; "
95 "station '" + sn.stations[i].name + "' has more than one server");
96 }
97
98 // Mean service time per (station, class); Inf marks "not served here".
99 Matrix<double> mu_inv(M, K, std::numeric_limits<double>::infinity());
100 for (std::size_t i = 0; i < M; ++i)
101 for (std::size_t r = 0; r < K; ++r) {
102 if (sn.disabled[i][r] || sn.service[i][r].D0.rows() == 0) continue;
103 const double rate = num_traits<T>::to_double(sn.rates(i, r));
104 if (rate > 0.0 && std::isfinite(rate)) mu_inv(i, r) = 1.0 / rate;
105 }
106
107 // Station-space routing, as the reference builds it.
108 const std::size_t S = sn.nof_stateful();
109 std::vector<std::size_t> keep;
110 keep.reserve(M * K);
111 for (std::size_t i = 0; i < M; ++i) {
112 const std::size_t isf = sn.stateful_of_station(i + 1) - 1;
113 for (std::size_t r = 0; r < K; ++r) keep.push_back(isf * K + r);
114 }
115 Matrix<double> rt_full(S * K, S * K, 0.0);
116 if (sn.rt.rows() == S * K)
117 for (std::size_t a = 0; a < S * K; ++a)
118 for (std::size_t b = 0; b < S * K; ++b)
119 rt_full(a, b) = num_traits<T>::to_double(sn.rt(a, b));
120 const Matrix<double> P = mc::dtmc_stochcomp(rt_full, keep);
121
122 const std::size_t steps = std::max<std::size_t>(2, opt.steps);
123 std::mt19937_64 rng(opt.seed);
124 std::normal_distribution<double> gauss(0.0, 1.0);
125
126 // Start with each class spread evenly over the stations, as the reference.
127 Matrix<double> x(M, K, 0.0), avg(M, K, 0.0);
128 for (std::size_t r = 0; r < K; ++r)
129 for (std::size_t i = 0; i < M; ++i)
130 x(i, r) = sn.classes[r].population / static_cast<double>(M);
131 for (std::size_t i = 0; i < M; ++i)
132 for (std::size_t r = 0; r < K; ++r) avg(i, r) = x(i, r) / static_cast<double>(steps);
133
134 Matrix<double> xn(M, K, 0.0);
135 for (std::size_t step = 1; step < steps; ++step) {
136 for (std::size_t i = 0; i < M; ++i)
137 for (std::size_t r = 0; r < K; ++r) {
138 // drift = inflow - outflow, both at the full queue rate
139 const double out =
140 std::isfinite(mu_inv(i, r)) && mu_inv(i, r) > 0.0 ? x(i, r) / mu_inv(i, r) : 0.0;
141 double in = 0.0;
142 for (std::size_t j = 0; j < M; ++j)
143 for (std::size_t q = 0; q < K; ++q) {
144 if (!(std::isfinite(mu_inv(j, q)) && mu_inv(j, q) > 0.0)) continue;
145 in += (x(j, q) / mu_inv(j, q)) * P(j * K + q, i * K + r);
146 }
147 const double dW = std::sqrt(opt.dt) * gauss(rng);
148 double v = x(i, r) + (in - out) * opt.dt + dW;
149 if (v < 0.0) v = 0.0;
150 xn(i, r) = v;
151 }
152 // Renormalise each class back to its population: the network is closed.
153 for (std::size_t r = 0; r < K; ++r) {
154 double tot = 0.0;
155 for (std::size_t i = 0; i < M; ++i) tot += xn(i, r);
156 for (std::size_t i = 0; i < M; ++i)
157 xn(i, r) = (tot > 0.0) ? xn(i, r) * sn.classes[r].population / tot
158 : sn.classes[r].population / static_cast<double>(M);
159 }
160 for (std::size_t i = 0; i < M; ++i)
161 for (std::size_t r = 0; r < K; ++r) {
162 x(i, r) = xn(i, r);
163 avg(i, r) += x(i, r) / static_cast<double>(steps);
164 }
165 }
166
167 DiffusionResult out;
168 out.QN = avg;
169 return out;
170}
171
172} // namespace fluid
173} // namespace line
174
175#endif // LINE_SOLVERS_FLUID_FLUID_DIFFUSION_H
UnsupportedError(const std::string &what)
Definition error.h:51
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.
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:181
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....
A queueing network and its refreshed NetworkStruct.
Controls of the Euler-Maruyama trajectory.
unsigned long seed
RNG seed; see the header on parity.
std::size_t steps
number of steps (the reference's iter_max)
double dt
step size (the reference's timestep)
Time-averaged queue lengths of the diffusion trajectory.