LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
qsys_gtmtst_fluid.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_API_QSYS_GTMTST_FLUID_H
6#define LINE_API_QSYS_GTMTST_FLUID_H
7
8/**
9 * @file
10 * @ingroup api_qsys
11 * The Gt/Mt/st+GI many-server fluid queue, and the network of them.
12 *
13 * Templated port of matlab/src/api/qsys/qsys_gtmtst_fluid.m and
14 * matlab/src/api/npfqn/npfqn_gtmtst_fluid.m, cross-checked against
15 * jar/src/main/java/jline/api/qsys/Qsys_gtmtst_fluid.java.
16 *
17 * Time-varying arrival rate lambda(t), staffing s(t), exponential service at
18 * rate mu(t), general patience with ccdf F^c, unlimited waiting room.
19 *
20 * THE MODEL ALTERNATES BETWEEN TWO REGIMES and the algorithm is the bookkeeping
21 * of that alternation:
22 *
23 * UNDERLOADED the queue is empty, every arrival enters service at once, and
24 * B' = lambda(t) - mu(t)B(t) (18, Mt form)
25 * ends when B reaches s while lambda > Gamma (15)
26 * OVERLOADED B(t) = s(t), fluid enters service at exactly
27 * Gamma(t) = s'(t) + s(t)mu(t) (13)
28 * q(t,x) = lambda(t-x)F^c(x) for x <= w(t) (20)
29 * w'(t) = 1 - Gamma(t)/[lambda(t-w(t))F^c(w(t))] (21)
30 * ends when w returns to 0 with lambda <= Gamma (14)
31 *
32 * WHY w AND NOT Q. The queue content is a functional of w, but not the other way
33 * round: two systems with the same Q and different age profiles abandon at
34 * different rates. Tracking the boundary keeps the age profile exact, which is
35 * what makes a general patience law admissible at all.
36 *
37 * THE NETWORK IS A FIXED POINT. lambda_j = lambda_j^0 + sum_i sigma_i P_ij with
38 * sigma_i = mu_i B_i (23)-(24), iterated from the external rates; the nth
39 * iterate is the fluid that has made n transitions, and the map is a monotone
40 * contraction, so the rates increase to the fixed point. Only the SERVICE
41 * COMPLETION flow is routed: abandoning fluid leaves the network, which is what
42 * makes the traffic equations linear in sigma.
43 *
44 * ARITHMETIC. RK4 on a grid against a tolerance, so nothing is exact; the
45 * instantiation is restricted to the transcendental types.
46 *
47 * Reference: Y. Liu, W. Whitt (2012). The Gt/GI/st+GI many-server fluid queue.
48 * Queueing Systems 71, 405-444; Y. Liu, W. Whitt (2014). Algorithms for
49 * time-varying networks of many-server fluid queues. INFORMS J. on Computing
50 * 26(1), 59-73.
51 */
52
53#include <algorithm>
54#include <cmath>
55#include <cstddef>
56#include <functional>
57#include <vector>
58
60#include "line/num/number.h"
61#include "line/util/error.h"
62
63namespace line {
64namespace qsys {
65
66/** Trajectory of the Gt/Mt/st+GI fluid queue; every vector is on the time grid. */
67template <class T>
69 std::vector<T> times; ///< the time grid
70 std::vector<int> regime; ///< 1 overloaded, 0 underloaded
71 std::vector<T> B; ///< fluid in service
72 std::vector<T> Q; ///< fluid in queue
73 std::vector<T> X; ///< B + Q
74 std::vector<T> w; ///< boundary waiting time
75 std::vector<T> v; ///< potential waiting time
76 std::vector<T> sigma; ///< service completion rate mu B
77 std::vector<T> alpha; ///< abandonment rate
78 std::vector<T> utilization; ///< B/s
79 std::vector<T> arrivalRate; ///< lambda on the grid
80 std::vector<T> staffing; ///< s on the grid
81 std::vector<T> capacityRate; ///< Gamma = s' + s mu
82};
83
84/** Options of `qsys_gtmtst_fluid`, all with the MATLAB defaults. */
85template <class T>
87 T dt = num_traits<T>::from_int(0); ///< grid step; non-positive takes T/2000
88 T B0 = num_traits<T>::from_int(0); ///< fluid in service at time 0
89 T w0 = num_traits<T>::from_int(0); ///< boundary waiting time at time 0
90 std::function<T(const T&)> sPrime; ///< s'(t); differentiated numerically when empty
91 std::function<T(const T&)> pdf; ///< patience density; differenced when empty
92 std::function<T(const T&)> lambdaPast; ///< arrival rate before time 0
93};
94
95namespace detail {
96
97/** Linear interpolation on an increasing grid, clamped at both ends. */
98template <class T>
99T tv_interp(const std::vector<T>& xs, const std::vector<T>& ys, const T& x) {
100 const std::size_t n = xs.size();
101 if (x <= xs[0]) return ys[0];
102 if (x >= xs[n - 1]) return ys[n - 1];
103 std::size_t lo = 0, hi = n - 1;
104 while (hi - lo > 1) {
105 const std::size_t mid = (lo + hi) / 2;
106 if (xs[mid] <= x) {
107 lo = mid;
108 } else {
109 hi = mid;
110 }
111 }
112 const T f = (x - xs[lo]) / (xs[hi] - xs[lo]);
113 return ys[lo] + f * (ys[hi] - ys[lo]);
114}
115
116/**
117 * int_0^w lambda(t-x) WEIGHT(x) dx by Simpson: the ccdf gives the queue content
118 * that has not abandoned, the density gives the abandonment rate.
119 */
120template <class T, class LamOf, class Weight>
121T tv_integrate(LamOf&& lamOf, Weight&& weight, const T& ti, const T& wi, const T& dt) {
122 const T zero = num_traits<T>::from_int(0);
123 if (wi <= zero) return zero;
124 std::size_t m = static_cast<std::size_t>(
125 std::max(8.0, std::ceil(num_traits<T>::to_double(wi) / num_traits<T>::to_double(dt)) + 1));
126 if (m % 2 == 1) ++m;
127 const T h = wi / num_traits<T>::from_int(static_cast<long>(m));
128 T sum = lamOf(ti) * weight(zero) + lamOf(T(ti - wi)) * weight(wi);
129 for (std::size_t j = 1; j < m; ++j) {
130 const T xx = num_traits<T>::from_int(static_cast<long>(j)) * h;
131 sum += num_traits<T>::from_int(j % 2 == 1 ? 4 : 2) * lamOf(T(ti - xx)) * weight(xx);
132 }
133 return h / num_traits<T>::from_int(3) * sum;
134}
135
136} // namespace detail
137
138/**
139 * @brief The Gt/Mt/st+GI many-server fluid queue, and the network of them.
140 *
141 * @param lambdaFun arrival rate lambda(t)
142 * @param sFun staffing s(t), positive
143 * @param muFun service rate mu(t), positive
144 * @param patienceCcdf F^c(x) = P(patience > x)
145 * @param T horizon; the model is solved on [0,T]
146 * @param opts grid step, initial condition and the optional derivatives
147 */
148template <class Tv>
149QsysTvFluidResult<Tv> qsys_gtmtst_fluid(const std::function<Tv(const Tv&)>& lambdaFun,
150 const std::function<Tv(const Tv&)>& sFun,
151 const std::function<Tv(const Tv&)>& muFun,
152 const std::function<Tv(const Tv&)>& patienceCcdf,
153 const Tv& T,
154 const TvFluidOptions<Tv>& opts = TvFluidOptions<Tv>()) {
156 "qsys_gtmtst_fluid integrates on a grid, so it needs inexact arithmetic");
157 const Tv zero = num_traits<Tv>::from_int(0);
158 const Tv one = num_traits<Tv>::from_int(1);
159 const Tv two = num_traits<Tv>::from_int(2);
160 if (T <= zero) throw InputError("qsys_gtmtst_fluid: the horizon T must be positive");
161 Tv dt = opts.dt;
162 if (dt <= zero) dt = T / num_traits<Tv>::from_int(2000);
163 const std::size_t n =
164 static_cast<std::size_t>(std::llround(num_traits<Tv>::to_double(T / dt))) + 1;
165 if (n < 2) throw InputError("qsys_gtmtst_fluid: the grid needs at least two points");
166
168 r.times.resize(n);
169 for (std::size_t i = 0; i < n; ++i)
170 r.times[i] = T * num_traits<Tv>::from_int(static_cast<long>(i)) /
171 num_traits<Tv>::from_int(static_cast<long>(n - 1));
172 const Tv step = r.times[1] - r.times[0];
173
174 std::vector<Tv> lam(n), s(n), mu(n), sp(n), gamma(n);
175 for (std::size_t i = 0; i < n; ++i) {
176 lam[i] = lambdaFun(r.times[i]);
177 s[i] = sFun(r.times[i]);
178 mu[i] = muFun(r.times[i]);
179 if (s[i] <= zero) throw InputError("qsys_gtmtst_fluid: the staffing must be positive");
180 if (mu[i] <= zero) throw InputError("qsys_gtmtst_fluid: the service rate must be positive");
181 }
182 if (opts.sPrime) {
183 for (std::size_t i = 0; i < n; ++i) sp[i] = opts.sPrime(r.times[i]);
184 } else {
185 for (std::size_t i = 0; i < n; ++i) {
186 if (i == 0) {
187 sp[i] = (s[1] - s[0]) / step;
188 } else if (i + 1 == n) {
189 sp[i] = (s[n - 1] - s[n - 2]) / step;
190 } else {
191 sp[i] = (s[i + 1] - s[i - 1]) / (two * step);
192 }
193 }
194 }
195 for (std::size_t i = 0; i < n; ++i) gamma[i] = sp[i] + s[i] * mu[i]; // Gamma(t), eq. (13)
196
197 const std::function<Tv(const Tv&)> past = opts.lambdaPast ? opts.lambdaPast : lambdaFun;
198 auto lamOf = [&](const Tv& u) { return u < zero ? past(u) : lambdaFun(u); };
199 std::function<Tv(const Tv&)> pdf = opts.pdf;
200 if (!pdf) {
201 pdf = [&patienceCcdf](const Tv& x) {
202 const Tv h = num_traits<Tv>::from_double(1e-6);
203 const Tv lo = x - h < num_traits<Tv>::from_int(0) ? num_traits<Tv>::from_int(0) : Tv(x - h);
204 const Tv d = (patienceCcdf(lo) - patienceCcdf(Tv(x + h))) / (num_traits<Tv>::from_int(2) * h);
205 return d < num_traits<Tv>::from_int(0) ? num_traits<Tv>::from_int(0) : d;
206 };
207 }
208
209 r.regime.assign(n, 0);
210 r.B.assign(n, zero);
211 r.Q.assign(n, zero);
212 r.w.assign(n, zero);
213 r.alpha.assign(n, zero);
214 r.B[0] = opts.B0;
215 r.w[0] = opts.w0;
216 const bool over0 =
217 opts.w0 > zero || (opts.B0 >= s[0] - num_traits<Tv>::from_double(1e-12) && lam[0] > gamma[0]);
218 r.regime[0] = over0 ? 1 : 0;
219 if (over0) r.B[0] = s[0];
220 r.Q[0] = detail::tv_integrate<Tv>(lamOf, patienceCcdf, r.times[0], r.w[0], step);
221 r.alpha[0] = detail::tv_integrate<Tv>(lamOf, pdf, r.times[0], r.w[0], step);
222
223 auto wdot = [&](const Tv& tt, const Tv& ww) {
224 const Tv den = lamOf(Tv(tt - ww)) * patienceCcdf(ww);
225 // No fluid of that age survives, so the boundary can only advance with
226 // the clock.
227 if (den <= zero) return one;
228 return Tv(one - detail::tv_interp(r.times, gamma, tt) / den);
229 };
230
231 for (std::size_t i = 0; i + 1 < n; ++i) {
232 Tv bNext, wNext;
233 if (r.regime[i] == 0) {
234 // Underloaded: B' = lambda - mu B, by RK4 on the grid step.
235 auto f = [&](const Tv& tt, const Tv& bb) {
236 return detail::tv_interp(r.times, lam, tt) - detail::tv_interp(r.times, mu, tt) * bb;
237 };
238 const Tv k1 = f(r.times[i], r.B[i]);
239 const Tv k2 = f(Tv(r.times[i] + step / two), Tv(r.B[i] + step * k1 / two));
240 const Tv k3 = f(Tv(r.times[i] + step / two), Tv(r.B[i] + step * k2 / two));
241 const Tv k4 = f(Tv(r.times[i] + step), Tv(r.B[i] + step * k3));
242 bNext = r.B[i] + step * (k1 + two * k2 + two * k3 + k4) / num_traits<Tv>::from_int(6);
243 wNext = zero;
244 if (bNext >= s[i + 1] && lam[i + 1] > gamma[i + 1]) {
245 // The servers just filled and the input outruns the freed
246 // capacity: eq. (15), the underloaded interval ends here.
247 bNext = s[i + 1];
248 r.regime[i + 1] = 1;
249 } else {
250 r.regime[i + 1] = 0;
251 if (bNext > s[i + 1]) bNext = s[i + 1];
252 }
253 } else {
254 // Overloaded: B = s and the boundary moves by eq. (21).
255 const Tv k1 = wdot(r.times[i], r.w[i]);
256 const Tv w2 = r.w[i] + step * k1 / two;
257 const Tv k2 = wdot(Tv(r.times[i] + step / two), w2 < zero ? zero : w2);
258 const Tv w3 = r.w[i] + step * k2 / two;
259 const Tv k3 = wdot(Tv(r.times[i] + step / two), w3 < zero ? zero : w3);
260 const Tv w4 = r.w[i] + step * k3;
261 const Tv k4 = wdot(Tv(r.times[i] + step), w4 < zero ? zero : w4);
262 wNext = r.w[i] + step * (k1 + two * k2 + two * k3 + k4) / num_traits<Tv>::from_int(6);
263 bNext = s[i + 1];
264 if (wNext <= zero && lam[i + 1] <= gamma[i + 1]) {
265 // The queue has drained and the input no longer outruns the
266 // freed capacity: eq. (14), the overloaded interval ends here.
267 wNext = zero;
268 r.regime[i + 1] = 0;
269 } else {
270 if (wNext < zero) wNext = zero;
271 r.regime[i + 1] = 1;
272 }
273 }
274 r.B[i + 1] = bNext;
275 r.w[i + 1] = wNext;
276 if (r.regime[i + 1] == 1) {
277 r.Q[i + 1] = detail::tv_integrate<Tv>(lamOf, patienceCcdf, r.times[i + 1], wNext, step);
278 r.alpha[i + 1] = detail::tv_integrate<Tv>(lamOf, pdf, r.times[i + 1], wNext, step);
279 }
280 }
281
282 r.X.resize(n);
283 r.sigma.resize(n);
284 r.utilization.resize(n);
285 std::vector<Tv> entry(n);
286 for (std::size_t i = 0; i < n; ++i) {
287 r.sigma[i] = mu[i] * r.B[i]; // service completion rate, eq. (3)
288 r.utilization[i] = r.B[i] / s[i];
289 r.X[i] = r.B[i] + r.Q[i];
290 entry[i] = r.times[i] - r.w[i];
291 }
292 // The potential waiting time of an arrival at t is the u-t at which the
293 // boundary reaches it, i.e. the solution of u - w(u) = t. That map is
294 // non-decreasing, so one interpolation inverts it.
295 r.v.resize(n);
296 for (std::size_t i = 0; i < n; ++i) {
297 const Tv u = detail::tv_interp(entry, r.times, r.times[i]);
298 r.v[i] = u - r.times[i] < zero ? zero : Tv(u - r.times[i]);
299 }
300 r.arrivalRate = lam;
301 r.staffing = s;
302 r.capacityRate = gamma;
303 return r;
304}
305
306/** Result of the network solve. */
307template <class T>
309 std::vector<T> times; ///< the time grid
310 std::vector<QsysTvFluidResult<T>> queues; ///< the per-queue trajectories
311 std::vector<std::vector<T>> arrivalRates; ///< converged total rates, one row per queue
312 std::size_t iterations = 0; ///< iterations of the traffic-rate fixed point
313 T residual; ///< sup-norm change at the last iteration
314};
315
316/**
317 * A time-varying open network of many-server fluid queues with abandonment.
318 *
319 * @param lambdaFuns external arrival rate of each queue
320 * @param sFuns staffing of each queue
321 * @param muFuns service rate of each queue
322 * @param patienceCcdfs patience ccdf of each queue
323 * @param P routing proportions, substochastic
324 * @param T horizon
325 * @param dt grid step; non-positive takes T/2000
326 * @param B0 initial fluid in service, empty for an empty network
327 * @param w0 initial boundary waiting times, empty for an empty network
328 * @param tol sup-norm tolerance on the arrival-rate iteration
329 * @param maxIter cap on the iterations
330 */
331template <class Tv>
333 const std::vector<std::function<Tv(const Tv&)>>& lambdaFuns,
334 const std::vector<std::function<Tv(const Tv&)>>& sFuns,
335 const std::vector<std::function<Tv(const Tv&)>>& muFuns,
336 const std::vector<std::function<Tv(const Tv&)>>& patienceCcdfs,
337 const std::vector<std::vector<Tv>>& P, const Tv& T, const Tv& dt = num_traits<Tv>::from_int(0),
338 const std::vector<Tv>& B0 = std::vector<Tv>(), const std::vector<Tv>& w0 = std::vector<Tv>(),
339 double tol = 1e-6, std::size_t maxIter = 100) {
341 "npfqn_gtmtst_fluid integrates on a grid, so it needs inexact arithmetic");
342 const Tv zero = num_traits<Tv>::from_int(0);
343 const Tv one = num_traits<Tv>::from_int(1);
344 const std::size_t m = lambdaFuns.size();
345 if (sFuns.size() != m || muFuns.size() != m || patienceCcdfs.size() != m)
346 throw InputError("npfqn_gtmtst_fluid: every queue needs an arrival rate, a staffing, a "
347 "service rate and a patience law");
348 if (T <= zero) throw InputError("npfqn_gtmtst_fluid: the horizon T must be positive");
349 Tv step = dt;
350 if (step <= zero) step = T / num_traits<Tv>::from_int(2000);
351 const std::size_t n =
352 static_cast<std::size_t>(std::llround(num_traits<Tv>::to_double(T / step))) + 1;
353 if (P.size() != m) throw InputError("npfqn_gtmtst_fluid: the routing matrix must be m x m");
354 for (std::size_t i = 0; i < m; ++i) {
355 if (P[i].size() != m)
356 throw InputError("npfqn_gtmtst_fluid: the routing matrix must be m x m");
357 Tv row = zero;
358 for (std::size_t j = 0; j < m; ++j) {
359 if (P[i][j] < -num_traits<Tv>::from_double(1e-12))
360 throw InputError("npfqn_gtmtst_fluid: the routing matrix must be non-negative");
361 row += P[i][j];
362 }
363 if (row > one + num_traits<Tv>::from_double(1e-9))
364 throw InputError("npfqn_gtmtst_fluid: the routing matrix must be substochastic");
365 }
366
368 out.times.resize(n);
369 for (std::size_t i = 0; i < n; ++i)
370 out.times[i] = T * num_traits<Tv>::from_int(static_cast<long>(i)) /
371 num_traits<Tv>::from_int(static_cast<long>(n - 1));
372
373 std::vector<std::vector<Tv>> ext(m, std::vector<Tv>(n, zero));
374 for (std::size_t i = 0; i < m; ++i)
375 for (std::size_t k = 0; k < n; ++k) ext[i][k] = lambdaFuns[i](out.times[k]);
376 std::vector<std::vector<Tv>> lam = ext;
377
379 for (std::size_t iter = 1; iter <= maxIter; ++iter) {
380 out.iterations = iter;
381 out.queues.clear();
382 std::vector<std::vector<Tv>> sigma(m, std::vector<Tv>(n, zero));
383 for (std::size_t i = 0; i < m; ++i) {
384 const std::vector<Tv>& row = lam[i];
385 const std::vector<Tv>& grid = out.times;
386 std::function<Tv(const Tv&)> fun = [&grid, &row](const Tv& u) {
387 return detail::tv_interp(grid, row, u);
388 };
390 o.dt = step;
391 o.B0 = B0.empty() ? zero : B0[i];
392 o.w0 = w0.empty() ? zero : w0[i];
394 qsys_gtmtst_fluid<Tv>(fun, sFuns[i], muFuns[i], patienceCcdfs[i], T, o);
395 sigma[i] = res.sigma;
396 out.queues.push_back(std::move(res));
397 }
398 // lambda_j = lambda_j^0 + sum_i sigma_i P_ij, eqs. (23)-(24).
399 Tv diff = zero;
400 std::vector<std::vector<Tv>> newlam = ext;
401 for (std::size_t j = 0; j < m; ++j) {
402 for (std::size_t k = 0; k < n; ++k) {
403 for (std::size_t i = 0; i < m; ++i) newlam[j][k] += sigma[i][k] * P[i][j];
404 const Tv d = newlam[j][k] - lam[j][k];
405 const Tv ad = d < zero ? Tv(-d) : d;
406 if (ad > diff) diff = ad;
407 }
408 }
409 lam = newlam;
410 out.residual = diff;
411 if (num_traits<Tv>::to_double(diff) < tol) break;
412 }
413 out.arrivalRates = lam;
414 return out;
415}
416
417} // namespace qsys
418} // namespace line
419
420#endif // LINE_API_QSYS_GTMTST_FLUID_H
InputError(const std::string &what)
Definition error.h:39
The exception types the port throws.
QsysTvFluidResult< Tv > qsys_gtmtst_fluid(const std::function< Tv(const Tv &)> &lambdaFun, const std::function< Tv(const Tv &)> &sFun, const std::function< Tv(const Tv &)> &muFun, const std::function< Tv(const Tv &)> &patienceCcdf, const Tv &T, const TvFluidOptions< Tv > &opts=TvFluidOptions< Tv >())
The Gt/Mt/st+GI many-server fluid queue, and the network of them.
NpfqnTvFluidResult< Tv > npfqn_gtmtst_fluid(const std::vector< std::function< Tv(const Tv &)> > &lambdaFuns, const std::vector< std::function< Tv(const Tv &)> > &sFuns, const std::vector< std::function< Tv(const Tv &)> > &muFuns, const std::vector< std::function< Tv(const Tv &)> > &patienceCcdfs, const std::vector< std::vector< Tv > > &P, const Tv &T, const Tv &dt=num_traits< Tv >::from_int(0), const std::vector< Tv > &B0=std::vector< Tv >(), const std::vector< Tv > &w0=std::vector< Tv >(), double tol=1e-6, std::size_t maxIter=100)
A time-varying open network of many-server fluid queues with abandonment.
Number-type abstraction for the templated API port.
Shared return type and arithmetic helpers for the templated qsys port.
Result of the network solve.
std::vector< std::vector< T > > arrivalRates
converged total rates, one row per queue
T residual
sup-norm change at the last iteration
std::size_t iterations
iterations of the traffic-rate fixed point
std::vector< QsysTvFluidResult< T > > queues
the per-queue trajectories
std::vector< T > times
the time grid
Trajectory of the Gt/Mt/st+GI fluid queue; every vector is on the time grid.
std::vector< T > alpha
abandonment rate
std::vector< T > utilization
B/s.
std::vector< T > Q
fluid in queue
std::vector< T > arrivalRate
lambda on the grid
std::vector< T > v
potential waiting time
std::vector< int > regime
1 overloaded, 0 underloaded
std::vector< T > capacityRate
Gamma = s' + s mu.
std::vector< T > sigma
service completion rate mu B
std::vector< T > B
fluid in service
std::vector< T > w
boundary waiting time
std::vector< T > times
the time grid
std::vector< T > staffing
s on the grid
Options of qsys_gtmtst_fluid, all with the MATLAB defaults.
T B0
fluid in service at time 0
T dt
grid step; non-positive takes T/2000
std::function< T(const T &)> lambdaPast
arrival rate before time 0
T w0
boundary waiting time at time 0
std::function< T(const T &)> sPrime
s'(t); differentiated numerically when empty
std::function< T(const T &)> pdf
patience density; differenced when empty