LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
qsys_ggisgi_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_GGISGI_FLUID_H
6#define LINE_API_QSYS_GGISGI_FLUID_H
7
8/**
9 * @file
10 * @ingroup api_qsys
11 * Steady state of the G/GI/s+GI fluid model.
12 *
13 * Templated port of matlab/src/api/qsys/qsys_ggisgi_fluid.m, cross-checked
14 * against jar/src/main/java/jline/api/qsys/Qsys_ggisgi_fluid.java.
15 *
16 * Scale the content by s and let s grow. Customers become quanta of fluid but
17 * their sojourns do not shrink, so the ages survive the limit: the state is the
18 * density b(x) of fluid in service of age x and the density q(x) of fluid
19 * waiting of age x. With rho = lambda/(s mu),
20 *
21 * rho <= 1 b(x) = rho G^c(x), q = 0 (3.2)
22 * rho > 1 b(x) = G^c(x), q(x) = rho F^c(x) on [0,w] (3.4)-(3.5)
23 *
24 * with the queue boundary w solving F^c(w) = 1/rho (3.6): fluid that survives
25 * its patience for w enters service, so the surviving fraction must equal the
26 * fraction 1/rho the servers can absorb. Then
27 *
28 * P(abandon) = 1 - 1/rho, W = int_0^w F^c = m_a F_e(w), Q = lambda W.
29 *
30 * WHAT THE DISTRIBUTIONS CONTRIBUTE (Corollary 3.1): the rates and the number in
31 * service depend on G and F only through their means; w, Q and the queue age
32 * profile depend on F beyond its mean but on G only through its mean. Neither s
33 * nor anything about the arrival process beyond its rate appears.
34 *
35 * ARITHMETIC. The boundary w comes out of a bisection against a tolerance and
36 * the mean wait out of a Simpson quadrature, so this is an approximation of an
37 * approximation and there is nothing to gain from exact arithmetic; the
38 * instantiation is therefore restricted to the transcendental types.
39 *
40 * Reference: W. Whitt (2006). Fluid models for multiserver queues with
41 * abandonments. Operations Research 54(1), 37-54.
42 */
43
44#include <algorithm>
45#include <cmath>
46#include <cstddef>
47#include <functional>
48#include <limits>
49#include <string>
50#include <vector>
51
53#include "line/num/number.h"
54#include "line/util/error.h"
55
56namespace line {
57namespace qsys {
58
59/** Steady state of the G/GI/s+GI fluid model. */
60template <class T>
62 std::string regime; ///< "underloaded", "balanced" or "overloaded"
63 T trafficIntensity; ///< rho = lambda/(s mu)
64 T offeredWait; ///< w, the wait of every served customer
65 T meanWait; ///< E[W] over all customers
66 T meanWaitServed; ///< w again, the fluid wait being deterministic
67 T meanWaitAbandon; ///< E[patience | patience <= w]
68 T probAbandon; ///< 1 - 1/rho when overloaded
69 T meanQueueLength; ///< Q = lambda W, in customers
70 T meanNumberInService; ///< B = min(lambda/mu, s), in customers
71 T meanNumber; ///< B + Q
72 T utilization; ///< min(rho,1)
73 T throughput; ///< min(lambda, s mu)
74 T abandonRate; ///< lambda - throughput
75 std::vector<T> agePoints; ///< ages of the densities, empty when not requested
76 std::vector<T> serviceAgeDensity;///< b(x) per server
77 std::vector<T> queueAgeDensity; ///< q(x) per server
78};
79
80namespace detail {
81
82/**
83 * Smallest w with F^c(w) = target, by doubling then bisection. F^c is
84 * non-increasing, so the doubling either brackets the crossing or proves that
85 * the patience law never decays that far.
86 */
87template <class T, class Ccdf>
88T fluid_inv_ccdf(Ccdf&& ccdf, const T& target, double tol, double maxTime) {
89 const T zero = num_traits<T>::from_int(0);
90 if (ccdf(zero) < target)
91 throw InputError("qsys_ggisgi_fluid: the patience ccdf is below 1/rho at t = 0, so it is "
92 "not a ccdf");
93 T lo = zero;
94 T hi;
95 if (std::isnan(maxTime)) {
97 while (ccdf(hi) > target) {
99 if (hi > num_traits<T>::from_double(1e12))
100 throw InputError("qsys_ggisgi_fluid: the patience ccdf never falls to 1/rho, so "
101 "the overloaded fluid model has no equilibrium: too little of the "
102 "fluid is willing to abandon");
103 }
104 } else {
105 hi = num_traits<T>::from_double(maxTime);
106 if (ccdf(hi) > target)
107 throw InputError("qsys_ggisgi_fluid: the patience ccdf is still above 1/rho at maxTime");
108 }
109 const T two = num_traits<T>::from_int(2);
110 const T tolT = num_traits<T>::from_double(tol);
111 const T one = num_traits<T>::from_int(1);
112 while (hi - lo > tolT * (hi > one ? hi : one)) {
113 const T mid = (lo + hi) / two;
114 if (ccdf(mid) > target) {
115 lo = mid;
116 } else {
117 hi = mid;
118 }
119 }
120 return (lo + hi) / two;
121}
122
123/**
124 * Composite Simpson rule on a fixed fine grid: the integrand is a ccdf, hence
125 * monotone and bounded, so a fixed grid is enough and is reproducible.
126 */
127template <class T, class Fn>
128T fluid_integral(Fn&& f, const T& a, const T& b) {
129 const T zero = num_traits<T>::from_int(0);
130 if (b <= a) return zero;
131 const std::size_t n = 2000;
132 const T h = (b - a) / num_traits<T>::from_int(static_cast<long>(n));
133 T sum = f(a) + f(b);
134 for (std::size_t i = 1; i < n; ++i) {
135 const T w = num_traits<T>::from_int((i % 2 == 1) ? 4 : 2);
136 sum += w * f(T(a + num_traits<T>::from_int(static_cast<long>(i)) * h));
137 }
138 return h / num_traits<T>::from_int(3) * sum;
139}
140
141} // namespace detail
142
143/**
144 * @brief Steady state of the G/GI/s+GI fluid model.
145 *
146 * @param lambda arrival rate
147 * @param mu service rate of one server
148 * @param s number of servers, s >= 1
149 * @param patienceCcdf F^c(t) = P(patience > t)
150 * @param servingCcdf G^c(x) = P(service > x), used only for the in-service age
151 * density; an empty callable takes the exponential of rate mu
152 * @param agePoints ages at which to return the densities
153 * @param tol bisection tolerance for w
154 * @param maxTime largest age searched for w; NaN grows the search
155 */
156template <class T>
158 const T& lambda, const T& mu, unsigned s, const std::function<T(const T&)>& patienceCcdf,
159 const std::function<T(const T&)>& servingCcdf = std::function<T(const T&)>(),
160 const std::vector<T>& agePoints = std::vector<T>(), double tol = 1e-12,
161 double maxTime = std::numeric_limits<double>::quiet_NaN()) {
163 "qsys_ggisgi_fluid bisects against a tolerance, so it needs inexact arithmetic");
164 const T zero = num_traits<T>::from_int(0);
165 const T one = num_traits<T>::from_int(1);
166 if (lambda <= zero) throw InputError("qsys_ggisgi_fluid: the arrival rate lambda must be positive");
167 if (mu <= zero) throw InputError("qsys_ggisgi_fluid: the service rate mu must be positive");
168 if (s < 1) throw InputError("qsys_ggisgi_fluid: the number of servers s must be at least 1");
169 if (!patienceCcdf) throw InputError("qsys_ggisgi_fluid: the patience ccdf must be callable");
170
171 std::function<T(const T&)> gc = servingCcdf;
172 if (!gc) {
173 const T fmu = mu;
174 gc = [fmu](const T& x) {
175 using std::exp;
176 return exp(-fmu * x);
177 };
178 }
179
180 const T sT = num_traits<T>::from_int(static_cast<long>(s));
181 const T rho = lambda / (sT * mu);
183 res.trafficIntensity = rho;
184
185 T w = zero, meanWait = zero, probAbandon = zero, meanWaitAbandon = zero;
186 if (rho <= one) {
187 // Underloaded and balanced, eq. (3.2): the queue is empty and the model
188 // is the infinite-server fluid model.
189 res.regime = (rho == one) ? "balanced" : "underloaded";
190 } else {
191 res.regime = "overloaded";
192 w = detail::fluid_inv_ccdf<T>(patienceCcdf, T(one / rho), tol, maxTime); // eq. (3.6)
193 // Eq. (3.14): W = int_0^w F^c(t) dt = m_a F_e(w), over ALL fluid.
194 meanWait = detail::fluid_integral<T>(patienceCcdf, zero, w);
195 probAbandon = one - one / rho;
196 // E[T | T <= w] = (W - w F^c(w)) / F(w) by parts, F^c(w) = 1/rho.
197 meanWaitAbandon = (meanWait - w / rho) / probAbandon;
198 }
199
200 res.offeredWait = w;
201 res.meanWait = meanWait;
202 res.meanWaitServed = w;
203 res.meanWaitAbandon = meanWaitAbandon;
204 res.probAbandon = probAbandon;
205 res.meanQueueLength = lambda * meanWait; // eq. (3.11), Little's law
206 res.meanNumberInService = detail::num_min(T(lambda / mu), sT);
208 res.utilization = detail::num_min(rho, one);
209 res.throughput = detail::num_min(lambda, T(sT * mu));
210 res.abandonRate = lambda - res.throughput;
211
212 if (!agePoints.empty()) {
213 const T sigma = detail::num_min(rho, one); // rate into service, per server
214 res.agePoints = agePoints;
215 res.serviceAgeDensity.resize(agePoints.size());
216 res.queueAgeDensity.resize(agePoints.size());
217 for (std::size_t i = 0; i < agePoints.size(); ++i) {
218 res.serviceAgeDensity[i] = sigma * gc(agePoints[i]);
219 res.queueAgeDensity[i] = (rho > one && agePoints[i] <= w)
220 ? T(rho * patienceCcdf(agePoints[i]))
221 : zero;
222 }
223 }
224 return res;
225}
226
227} // namespace qsys
228} // namespace line
229
230#endif // LINE_API_QSYS_GGISGI_FLUID_H
InputError(const std::string &what)
Definition error.h:39
The exception types the port throws.
QsysFluidAbandonResult< T > qsys_ggisgi_fluid(const T &lambda, const T &mu, unsigned s, const std::function< T(const T &)> &patienceCcdf, const std::function< T(const T &)> &servingCcdf=std::function< T(const T &)>(), const std::vector< T > &agePoints=std::vector< T >(), double tol=1e-12, double maxTime=std::numeric_limits< double >::quiet_NaN())
Steady state of the G/GI/s+GI fluid model.
Number-type abstraction for the templated API port.
Shared return type and arithmetic helpers for the templated qsys port.
Steady state of the G/GI/s+GI fluid model.
T offeredWait
w, the wait of every served customer
std::vector< T > serviceAgeDensity
b(x) per server
T probAbandon
1 - 1/rho when overloaded
T meanWait
E[W] over all customers.
std::vector< T > agePoints
ages of the densities, empty when not requested
T meanWaitServed
w again, the fluid wait being deterministic
T meanWaitAbandon
E[patience | patience <= w].
std::vector< T > queueAgeDensity
q(x) per server
T meanQueueLength
Q = lambda W, in customers.
std::string regime
"underloaded", "balanced" or "overloaded"
T meanNumberInService
B = min(lambda/mu, s), in customers.