LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
qsys_mmk_qed.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_MMK_QED_H
6#define LINE_API_QSYS_MMK_QED_H
7
8/**
9 * @file
10 * @ingroup api_qsys
11 * Halfin-Whitt QED approximation for the M/M/s queue, and the square-root
12 * staffing rule that inverts it.
13 *
14 * Templated port of matlab/src/api/qsys/qsys_mmk_qed.m, qsys_mmk_qed_alpha.m and
15 * qsys_mmk_qed_staffing.m, cross-checked against
16 * jar/src/main/java/jline/api/qsys/Qsys_mmk_qed.java.
17 *
18 * Let s grow with the offered load a = lambda/mu so that the SERVER SLACK stays
19 * of order sqrt(s), i.e. beta = (1-rho) sqrt(s) = (s-a)/sqrt(s) is held fixed.
20 * The delay probability then has the non-degenerate limit
21 *
22 * alpha(beta) = [ 1 + beta Phi(beta)/phi(beta) ]^(-1)
23 *
24 * with phi and Phi the standard normal density and cdf. Servers are busy a
25 * fraction 1 - beta/sqrt(s) of the time, so efficiency tends to 1, and yet the
26 * delay probability tends to a constant strictly between 0 and 1, so quality
27 * does not collapse. Staffing inverts that: s = ceil(a + beta sqrt(a)).
28 *
29 * ARITHMETIC. erfc and exp put this in the transcendental family; the staffing
30 * search is a bisection against a tolerance, so there is nothing exact to
31 * preserve.
32 *
33 * NUMERICS. alpha is evaluated as phi/(phi + beta Phi) rather than as the
34 * reciprocal of 1 + beta Phi/phi: the two agree, but the quotient overflows once
35 * phi underflows (beta beyond about 38), whereas this form degrades to
36 * 0/(0+beta) = 0, which is the correct limit. The exact Erlang C used by the
37 * refinement goes through the Erlang B recursion for the same reason, a^j/j!
38 * being infinite well below the s these rules propose.
39 *
40 * Reference: S. Halfin, W. Whitt (1981). Heavy-traffic limits for queues with
41 * many exponential servers. Operations Research 29(3), 567-588.
42 */
43
44#include <cmath>
45#include <cstddef>
46#include <string>
47
49#include "line/num/number.h"
50#include "line/util/error.h"
51
52namespace line {
53namespace qsys {
54
55/** QED measures of the M/M/s queue. */
56template <class T>
58 T offeredLoad; ///< a = lambda/mu, in erlangs
59 T trafficIntensity; ///< rho = a/s
60 T beta; ///< the QED server slack (s-a)/sqrt(s)
61 T probDelay; ///< alpha(beta)
62 T meanWaitDelayed; ///< E[W | W > 0] = 1/(s mu - lambda), exact for M/M/s
63 T meanWait; ///< E[W] = alpha(beta)/(s mu - lambda)
64 T meanQueueLength; ///< E[Q] = lambda E[W]
65 T meanNumber; ///< E[N] = a + E[Q]
66 T utilization; ///< rho
67};
68
69/** Outcome of the square-root staffing rule. */
70template <class T>
72 unsigned numServers; ///< the recommended s
73 T beta; ///< the slack achieved, (s-a)/sqrt(s)
74 T betaTarget; ///< the slack the target asks for, before rounding s up
75 T offeredLoad; ///< a = lambda/mu
76 T probDelay; ///< the QED delay probability at the recommended s
77 T meanWait; ///< the QED mean wait at the recommended s
78 T serviceLevel; ///< P(W <= deadline), for the service-level criterion
79 bool exactUsed; ///< whether the exact Erlang C refinement was applied
80};
81
82/** Which target the staffing rule is asked to meet. */
84
85/**
86 * The Halfin-Whitt delay-probability function alpha(beta); 1 at beta <= 0.
87 *
88 * @param beta the QED server-slack parameter
89 */
90template <class T>
91T qsys_mmk_qed_alpha(const T& beta) {
93 "qsys_mmk_qed_alpha needs erfc and exp");
94 const T zero = num_traits<T>::from_int(0);
95 const T one = num_traits<T>::from_int(1);
96 if (beta <= zero) return one;
97 const T two = num_traits<T>::from_int(2);
98 using std::erfc;
99 using std::exp;
100 using std::sqrt;
101 const T phi = exp(-beta * beta / two) / sqrt(two * num_traits<T>::from_double(M_PI));
102 const T Phi = erfc(T(-beta / sqrt(two))) / two;
103 return phi / (phi + beta * Phi);
104}
105
106/**
107 * Erlang C by the Erlang B recursion B_j = a B_{j-1}/(j + a B_{j-1}), which
108 * never forms a^j/j! and so never overflows.
109 *
110 * @param s number of servers
111 * @param lambda arrival rate
112 * @param mu service rate of one server
113 */
114template <class T>
115T qsys_mmk_qed_erlangc(unsigned s, const T& lambda, const T& mu) {
116 const T one = num_traits<T>::from_int(1);
117 const T a = lambda / mu;
118 T b = one;
119 for (unsigned j = 1; j <= s; ++j)
120 b = a * b / (num_traits<T>::from_int(static_cast<long>(j)) + a * b);
121 const T rho = a / num_traits<T>::from_int(static_cast<long>(s));
122 return rho >= one ? one : T(b / (one - rho * (one - b)));
123}
124
125/**
126 * @brief Halfin-Whitt QED approximation for the M/M/s queue, and the
127 * square-root staffing rule that inverts it.
128 *
129 * @param lambda arrival rate
130 * @param mu service rate of one server
131 * @param s number of servers, s >= 1
132 */
133template <class T>
134QsysQedResult<T> qsys_mmk_qed(const T& lambda, const T& mu, unsigned s) {
135 static_assert(num_traits<T>::has_transcendental, "qsys_mmk_qed needs erfc and exp");
136 const T zero = num_traits<T>::from_int(0);
137 if (lambda <= zero) throw InputError("qsys_mmk_qed: the arrival rate lambda must be positive");
138 if (mu <= zero) throw InputError("qsys_mmk_qed: the service rate mu must be positive");
139 if (s < 1) throw InputError("qsys_mmk_qed: the number of servers s must be at least 1");
140 using std::sqrt;
141 const T sT = num_traits<T>::from_int(static_cast<long>(s));
143 r.offeredLoad = lambda / mu;
144 r.trafficIntensity = r.offeredLoad / sT;
145 r.beta = (sT - r.offeredLoad) / sqrt(sT);
147 if (r.beta <= zero) {
148 // Not a QED model: every arrival is delayed and there is no steady state.
150 const T inf = num_traits<T>::from_double(std::numeric_limits<double>::infinity());
152 return r;
153 }
155 r.meanWaitDelayed = num_traits<T>::from_int(1) / (sT * mu - lambda);
157 r.meanQueueLength = lambda * r.meanWait;
159 return r;
160}
161
162namespace detail {
163
164/** Bisection for a root of an increasing f on (0, hi]; the bracket grows. */
165template <class T, class Fn>
166T qed_solve(Fn&& f) {
167 const T lo0 = num_traits<T>::from_double(1e-9);
168 T lo = lo0, hi = num_traits<T>::from_int(1);
169 if (f(lo) > num_traits<T>::from_int(0)) return lo;
170 while (f(hi) < num_traits<T>::from_int(0)) {
172 if (hi > num_traits<T>::from_double(1e6))
173 throw InputError("qsys_mmk_qed_staffing: no server slack meets the target");
174 }
175 const T two = num_traits<T>::from_int(2);
176 for (int i = 0; i < 200; ++i) {
177 const T mid = (lo + hi) / two;
178 if (f(mid) < num_traits<T>::from_int(0)) {
179 lo = mid;
180 } else {
181 hi = mid;
182 }
183 }
184 return (lo + hi) / two;
185}
186
187/** The exact M/M/s measure against the target. */
188template <class T>
189bool qed_meets(const T& lambda, const T& mu, unsigned s, const T& target, QedCriterion crit,
190 const T& deadline, const T& level) {
191 const T sT = num_traits<T>::from_int(static_cast<long>(s));
192 if (sT * mu <= lambda) return false;
193 const T c = qsys_mmk_qed_erlangc(s, lambda, mu);
194 const T wq = c / (sT * mu - lambda);
195 switch (crit) {
196 case QedCriterion::Delay: return c <= target;
197 case QedCriterion::MeanWait: return wq <= target;
199 using std::exp;
200 return (num_traits<T>::from_int(1) - c * exp(-(sT * mu - lambda) * deadline)) >= level;
201 }
202 }
203 return false;
204}
205
206} // namespace detail
207
208/**
209 * Square-root staffing of the M/M/s queue.
210 *
211 * @param lambda arrival rate
212 * @param mu service rate of one server
213 * @param target the largest acceptable P(W>0) for Delay, the largest
214 * acceptable E[W] for MeanWait, unused for ServiceLevel
215 * @param crit which target to meet
216 * @param deadline the deadline of the service-level criterion
217 * @param level the probability that deadline must be met with
218 * @param exact walk s until the EXACT Erlang C measure meets the target
219 */
220template <class T>
222 const T& lambda, const T& mu, const T& target, QedCriterion crit = QedCriterion::Delay,
223 const T& deadline = num_traits<T>::from_int(0), const T& level = num_traits<T>::from_int(0),
224 bool exact = false) {
225 static_assert(num_traits<T>::has_transcendental, "qsys_mmk_qed_staffing needs erfc and exp");
226 const T zero = num_traits<T>::from_int(0);
227 const T one = num_traits<T>::from_int(1);
228 if (lambda <= zero)
229 throw InputError("qsys_mmk_qed_staffing: the arrival rate lambda must be positive");
230 if (mu <= zero) throw InputError("qsys_mmk_qed_staffing: the service rate mu must be positive");
231 using std::ceil;
232 using std::exp;
233 using std::floor;
234 using std::sqrt;
235 const T a = lambda / mu;
236 T betaTarget;
237 switch (crit) {
239 if (!(target > zero && target < one))
240 throw InputError(
241 "qsys_mmk_qed_staffing: for the delay criterion the target must be in (0,1)");
242 betaTarget = detail::qed_solve<T>([&](const T& b) { return target - qsys_mmk_qed_alpha(b); });
243 break;
245 if (!(target > zero))
246 throw InputError(
247 "qsys_mmk_qed_staffing: for the meanwait criterion the target must be positive");
248 // The residual is written target - E[W] so that it increases in beta.
249 betaTarget = detail::qed_solve<T>(
250 [&](const T& b) { return target - qsys_mmk_qed_alpha(b) / (mu * b * sqrt(a)); });
251 break;
253 if (!(level > zero && level < one) || !(deadline > zero))
254 throw InputError("qsys_mmk_qed_staffing: the service level must be in (0,1) and "
255 "the deadline positive");
256 betaTarget = detail::qed_solve<T>([&](const T& b) {
257 const T sApprox = a + b * sqrt(a);
258 return (one - qsys_mmk_qed_alpha(b) * exp(-mu * b * sqrt(sApprox) * deadline)) - level;
259 });
260 break;
261 }
262
263 long sl = static_cast<long>(num_traits<T>::to_double(T(ceil(a + betaTarget * sqrt(a)))));
264 if (sl < 1) sl = 1;
265 unsigned s = static_cast<unsigned>(sl);
266 if (num_traits<T>::from_int(static_cast<long>(s)) * mu <= lambda)
267 s = static_cast<unsigned>(num_traits<T>::to_double(T(floor(a)))) + 1;
268 if (exact) {
269 while (!detail::qed_meets(lambda, mu, s, target, crit, deadline, level)) {
270 ++s;
271 if (s > 10000000u)
272 throw InputError("qsys_mmk_qed_staffing: the exact refinement passed 10^7 servers "
273 "without meeting the target");
274 }
275 while (s > 1 && detail::qed_meets(lambda, mu, s - 1, target, crit, deadline, level)) --s;
276 }
277
278 const QsysQedResult<T> qed = qsys_mmk_qed(lambda, mu, s);
280 res.numServers = s;
281 res.beta = qed.beta;
282 res.betaTarget = betaTarget;
283 res.offeredLoad = a;
284 res.probDelay = qed.probDelay;
285 res.meanWait = qed.meanWait;
286 res.exactUsed = exact;
287 res.serviceLevel = zero;
288 if (crit == QedCriterion::ServiceLevel)
289 res.serviceLevel =
290 one - qed.probDelay *
291 exp(-(num_traits<T>::from_int(static_cast<long>(s)) * mu - lambda) * deadline);
292 return res;
293}
294
295} // namespace qsys
296} // namespace line
297
298#endif // LINE_API_QSYS_MMK_QED_H
Delay(model, name): the infinite-server station.
Definition nodes.h:147
InputError(const std::string &what)
Definition error.h:39
The exception types the port throws.
QsysQedResult< T > qsys_mmk_qed(const T &lambda, const T &mu, unsigned s)
Halfin-Whitt QED approximation for the M/M/s queue, and the square-root staffing rule that inverts it...
T qsys_mmk_qed_erlangc(unsigned s, const T &lambda, const T &mu)
Erlang C by the Erlang B recursion B_j = a B_{j-1}/(j + a B_{j-1}), which never forms a^j/j!
QsysQedStaffingResult< T > qsys_mmk_qed_staffing(const T &lambda, const T &mu, const T &target, QedCriterion crit=QedCriterion::Delay, const T &deadline=num_traits< T >::from_int(0), const T &level=num_traits< T >::from_int(0), bool exact=false)
Square-root staffing of the M/M/s queue.
QedCriterion
Which target the staffing rule is asked to meet.
T qsys_mmk_qed_alpha(const T &beta)
The Halfin-Whitt delay-probability function alpha(beta); 1 at beta <= 0.
Number-type abstraction for the templated API port.
Shared return type and arithmetic helpers for the templated qsys port.
QED measures of the M/M/s queue.
T meanWaitDelayed
E[W | W > 0] = 1/(s mu - lambda), exact for M/M/s.
T meanQueueLength
E[Q] = lambda E[W].
T offeredLoad
a = lambda/mu, in erlangs
T beta
the QED server slack (s-a)/sqrt(s)
T meanWait
E[W] = alpha(beta)/(s mu - lambda).
T meanNumber
E[N] = a + E[Q].
Outcome of the square-root staffing rule.
T meanWait
the QED mean wait at the recommended s
T betaTarget
the slack the target asks for, before rounding s up
T serviceLevel
P(W <= deadline), for the service-level criterion.
T probDelay
the QED delay probability at the recommended s
unsigned numServers
the recommended s
bool exactUsed
whether the exact Erlang C refinement was applied
T beta
the slack achieved, (s-a)/sqrt(s)