LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
qsys_mgisrgi_whitt.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_MGISRGI_WHITT_H
6#define LINE_API_QSYS_MGISRGI_WHITT_H
7
8/**
9 * @file
10 * @ingroup api_qsys
11 * Engineering solution of the call-center model M/GI/s/r+GI.
12 *
13 * Templated port of matlab/src/api/qsys/qsys_mgisrgi_whitt.m, cross-checked
14 * against jar/src/main/java/jline/api/qsys/Qsys_mgisrgi_whitt.java.
15 *
16 * Poisson arrivals at rate lambda, iid general service of mean 1/mu, s servers,
17 * r extra waiting spaces and iid general patience.
18 *
19 * TWO APPROXIMATIONS. The general patience law becomes STATE-DEPENDENT
20 * Markovian abandonment, a customer jth from the end of the queue abandoning at
21 * rate delta_j = h(j/lambda) for the patience hazard h (eq. 3.3), because such a
22 * customer has been waiting for about j/lambda; the total with k waiting is
23 * Delta_k = sum_{j<=k} delta_j (eq. 3.4). The general service law becomes an
24 * exponential of the same mean (Sec. 5). What is left is M/M/s/r+M(n), a
25 * birth-and-death process:
26 *
27 * mu_k = k mu 1 <= k <= s
28 * = s mu + Delta_{k-s} s+1 <= k <= s+r (7.1)
29 * x_s = 1, x_{s+k+1} = lambda x_{s+k}/mu_{s+k+1}, x_{k-1} = mu_k x_k/lambda
30 * p_k = x_k / sum_j x_j (7.4)-(7.7)
31 *
32 * and the customer experience follows from the kernel
33 *
34 * m_k(j) = 1/(s mu + Delta_k - Delta_{j-1}) (7.11)
35 * phi_k(j) = delta_j m_k(j) (7.10)
36 * sigma_k = prod_j (1 - phi_k(j)) (7.13)
37 *
38 * which is EXACT for M/M/s/r+M (eq. 7.12), i.e. for `qsys_erlanga`.
39 *
40 * ARITHMETIC. The birth-death leg and every moment are field operations, so the
41 * hazard and exponential forms instantiate at T = Rational. The ccdf form needs
42 * a logarithm and the waiting-time cdfs need a numerical Laplace inversion, so
43 * the first throws and the second is skipped unless T is double.
44 *
45 * DIVERGENCE from the printed eqs. (3.5)-(3.6): they read
46 * delta_j = int_{(j-1)/lambda}^{j/lambda} h(t) dt and Delta_k = -log F^c(k/lambda),
47 * which are cumulative hazards, i.e. dimensionless, while delta and Delta are
48 * rates everywhere else in the paper. They are the AVERAGE hazard over an
49 * interval of length 1/lambda, so the factor lambda is missing. Restoring it
50 * makes the ccdf form reduce to the exact Erlang A rates under exponential
51 * patience, which the paper states this approximation does (eq. 7.12).
52 *
53 * Reference: W. Whitt (2005). Engineering solution of a basic call-center model.
54 * Management Science 51(2), 221-235.
55 */
56
57#include <algorithm>
58#include <complex>
59#include <cstddef>
60#include <functional>
61#include <string>
62#include <type_traits>
63#include <vector>
64
67#include "line/num/number.h"
68#include "line/util/error.h"
69
70namespace line {
71namespace qsys {
72
73/** Which of the three accepted descriptions of the patience law is carried. */
75
76/**
77 * The patience (time-to-abandon) law, in the three forms the algorithm accepts.
78 *
79 * The engineering solution needs it only through its hazard rate near the
80 * origin, so these three carry exactly that.
81 */
82template <class T>
83struct Patience {
86 std::function<T(const T&)> fun;
87
88 /** Exponential patience of rate theta, h(t) = theta, the Erlang A case. */
89 static Patience exponential(const T& theta) {
91 throw InputError("Patience::exponential: the rate theta must be non-negative");
92 Patience p;
94 p.theta = theta;
95 return p;
96 }
97
98 /** Patience given by its hazard rate h = f/(1-F). */
99 static Patience hazard(std::function<T(const T&)> h) {
100 Patience p;
102 p.fun = std::move(h);
103 return p;
104 }
105
106 /** Patience given by its complementary cdf G(t) = 1-F(t). */
107 static Patience ccdf(std::function<T(const T&)> g) {
108 Patience p;
110 p.fun = std::move(g);
111 return p;
112 }
113};
114
115/** Steady-state measures of a multiserver queue with customer abandonment. */
116template <class T>
118 std::vector<T> queueLengthDist; ///< P(N = k), k = 0..s+r
119 T probLoss; ///< P(an arrival is blocked), 0 when r is infinite
120 T probNoWait; ///< P(W = 0) among entering customers
121 T probServed; ///< P(S), eventually served
122 T probAbandon; ///< P(A) = 1 - P(S)
123 T meanNumber; ///< E[N]
124 T varNumber; ///< Var[N]
125 T meanQueueLength; ///< E[Q], Q = (N-s)^+
126 T varQueueLength; ///< Var[Q]
127 T utilization; ///< E[min(N,s)]/s
128 T throughput; ///< lambda(1-P_loss)P(S)
129 T abandonRate; ///< lambda(1-P_loss)P(A)
130 T meanWaitServed; ///< E[W|S]
131 T varWaitServed; ///< Var[W|S]
132 T meanWaitAbandon; ///< E[W|A]
133 T varWaitAbandon; ///< Var[W|A]
134 T meanWait; ///< E[W] over entering customers
135 T secondMomentWait; ///< E[W^2] over entering customers
136 std::vector<T> abandonRates; ///< delta_j
137 std::vector<T> totalAbandonRates; ///< Delta_k, index k, Delta_0 = 0
138 std::size_t numWaitingSpaces = 0; ///< waiting spaces used, the truncation when r is infinite
139 bool exponentialPatience = false; ///< whether the answer is exact
140 T patienceRate; ///< the exponential rate, unset otherwise
141 std::vector<double> waitPoints; ///< times of the cdfs, empty when not requested
142 std::vector<double> cdfWaitServed; ///< P(W <= t | S)
143 std::vector<double> cdfWaitAbandon; ///< P(W <= t | A)
144 std::vector<double> cdfWait; ///< P(W <= t)
145};
146
147/** Options of `qsys_mgisrgi_whitt`, all with the MATLAB defaults. */
149 std::vector<double> wPoints; ///< times for the waiting-time cdfs
150 std::size_t maxQueue = 100000; ///< truncation level used when r is infinite
151 double tol = 1e-14; ///< relative tail tolerance for that truncation
152 std::string invMethod = "euler"; ///< Laplace inversion method
153 std::size_t invN = 41; ///< number of inversion nodes
154};
155
156namespace detail {
157
158/** One step of eqs. (3.3)-(3.4) (hazard form) or (3.5)-(3.6) (ccdf form). */
159template <class T>
160void mgisrgi_rate_step(std::size_t j, const T& lambda, const T& delta_prev,
161 const Patience<T>& patience, T& delta_j, T& delta_tot) {
162 const T t = num_traits<T>::from_int(static_cast<long>(j)) / lambda;
163 if (patience.form == PatienceForm::Ccdf) {
164 if constexpr (num_traits<T>::has_transcendental) {
165 const T g = patience.fun(t);
166 if (g <= num_traits<T>::from_int(0))
167 throw InputError(
168 "qsys_mgisrgi_whitt: the patience ccdf vanishes, so every customer has "
169 "abandoned by then; supply a hazard instead");
170 using std::log;
171 delta_tot = -lambda * log(g);
172 delta_j = delta_tot - delta_prev;
173 } else {
174 throw InputError(
175 "qsys_mgisrgi_whitt: the ccdf form of the patience law needs a logarithm, "
176 "which the exact arithmetic does not have; supply a hazard instead");
177 }
178 } else {
179 delta_j = patience.form == PatienceForm::Exponential ? patience.theta : patience.fun(t);
180 delta_tot = delta_prev + delta_j;
181 }
182 if (delta_j < num_traits<T>::from_int(0))
183 throw InputError("qsys_mgisrgi_whitt: the patience law produced a negative abandonment rate");
184}
185
186/**
187 * Eqs. (7.10)-(7.11): with k waiting, the total departure rate before the jth
188 * departure epoch is s mu + Delta_k - Delta_{j-1}, of which delta_j is the share
189 * belonging to the customer of interest. Fills rate, phi and the survival
190 * product prod_{l<j}(1-phi_k(l)).
191 */
192template <class T>
193void mgisrgi_kernel(std::size_t k, const T& smu, const std::vector<T>& dlt,
194 const std::vector<T>& delta, std::vector<T>& rate, std::vector<T>& phi,
195 std::vector<T>& surv) {
196 const T one = num_traits<T>::from_int(1);
197 rate.resize(k);
198 phi.resize(k);
199 surv.resize(k);
200 T running = one;
201 for (std::size_t j = 1; j <= k; ++j) {
202 rate[j - 1] = smu + dlt[k] - dlt[j - 1];
203 phi[j - 1] = delta[j - 1] / rate[j - 1];
204 surv[j - 1] = running;
205 running *= (one - phi[j - 1]);
206 }
207}
208
209/** A conditional moment is 0/0 when the conditioning event cannot happen. */
210template <class T>
211T mgisrgi_ratio(const T& num, const T& den) {
212 return den <= num_traits<T>::from_int(0) ? num_traits<T>::from_int(0) : T(num / den);
213}
214
215} // namespace detail
216
217/**
218 * @brief Engineering solution of the call-center model M/GI/s/r+GI.
219 *
220 * @param lambda arrival rate
221 * @param mu service rate of one server, the reciprocal of the mean service time
222 * @param s number of servers, s >= 1
223 * @param r extra waiting spaces; infinity for an unbounded queue
224 * @param patience the patience law
225 * @param opts cdf times, truncation controls and inversion settings
226 */
227template <class T>
228QsysAbandonResult<T> qsys_mgisrgi_whitt(const T& lambda, const T& mu, unsigned s, double r,
229 const Patience<T>& patience,
230 const MgisrgiOptions& opts = MgisrgiOptions()) {
231 const T zero = num_traits<T>::from_int(0);
232 const T one = num_traits<T>::from_int(1);
233 if (lambda <= zero) throw InputError("qsys_mgisrgi_whitt: the arrival rate lambda must be positive");
234 if (mu <= zero) throw InputError("qsys_mgisrgi_whitt: the service rate mu must be positive");
235 if (s < 1) throw InputError("qsys_mgisrgi_whitt: the number of servers s must be at least 1");
236 if (r < 0)
237 throw InputError("qsys_mgisrgi_whitt: the number of extra waiting spaces r must be non-negative");
238
239 const bool finiteR = std::isfinite(r);
240 std::size_t rr = finiteR ? static_cast<std::size_t>(r + 0.5) : opts.maxQueue;
241 const T smu = num_traits<T>::from_int(static_cast<long>(s)) * mu;
242
243 // The birth-death recursion of eqs. (7.4)-(7.7), unnormalized with x_s = 1.
244 std::vector<T> xUp(rr + 1, zero), dlt(rr + 1, zero), delta(rr, zero);
245 xUp[0] = one;
246 std::size_t kUsed = rr;
247 T peak = one;
248 const T tolT = num_traits<T>::from_double(opts.tol);
249 for (std::size_t k = 0; k < rr; ++k) {
250 const std::size_t j = k + 1;
251 detail::mgisrgi_rate_step(j, lambda, dlt[j - 1], patience, delta[j - 1], dlt[j]);
252 xUp[k + 1] = lambda * xUp[k] / (smu + dlt[j]);
253 if (xUp[k + 1] > peak) peak = xUp[k + 1];
254 if (!finiteR && xUp[k + 1] < tolT * peak && k >= 1) {
255 kUsed = j;
256 break;
257 }
258 }
259 if (!finiteR) {
260 if (kUsed == rr && rr > 0)
261 throw InputError("qsys_mgisrgi_whitt: the queue-length tail is still sizeable at the "
262 "truncation level; with r = Inf the patience law must make the chain "
263 "ergodic (raise maxQueue if the model is genuinely that large)");
264 xUp.resize(kUsed + 1);
265 dlt.resize(kUsed + 1);
266 delta.resize(kUsed);
267 rr = kUsed;
268 }
269
270 // The downward leg, eq. (7.5), over the states where not all servers are busy.
271 std::vector<T> x(s + rr + 1, zero);
272 T xk = one;
273 for (unsigned k = s; k >= 1; --k) {
274 xk = num_traits<T>::from_int(static_cast<long>(k)) * mu * xk / lambda;
275 x[k - 1] = xk;
276 }
277 for (std::size_t k = 0; k <= rr; ++k) x[s + k] = xUp[k];
278
279 T total = zero;
280 for (const T& v : x) total += v;
282 res.queueLengthDist.resize(x.size());
283 for (std::size_t i = 0; i < x.size(); ++i) res.queueLengthDist[i] = x[i] / total;
284 res.probLoss = finiteR ? res.queueLengthDist.back() : zero;
285 std::vector<T> pa(res.queueLengthDist.size());
286 for (std::size_t i = 0; i < pa.size(); ++i)
287 pa[i] = res.queueLengthDist[i] / (one - res.probLoss); // eq. (7.8), seen by an ENTERING customer
288
289 T meanNumber = zero, meanQueue = zero, busy = zero;
290 for (std::size_t k = 0; k < res.queueLengthDist.size(); ++k) {
291 const T pk = res.queueLengthDist[k];
292 const T kT = num_traits<T>::from_int(static_cast<long>(k));
293 meanNumber += kT * pk;
294 if (k > s) meanQueue += num_traits<T>::from_int(static_cast<long>(k - s)) * pk;
295 busy += num_traits<T>::from_int(static_cast<long>(std::min<std::size_t>(k, s))) * pk;
296 }
297 T varNumber = zero, varQueue = zero;
298 for (std::size_t k = 0; k < res.queueLengthDist.size(); ++k) {
299 const T pk = res.queueLengthDist[k];
300 const T dN = num_traits<T>::from_int(static_cast<long>(k)) - meanNumber;
301 const T q = k > s ? num_traits<T>::from_int(static_cast<long>(k - s)) : zero;
302 varNumber += dN * dN * pk;
303 varQueue += (q - meanQueue) * (q - meanQueue) * pk;
304 }
305
306 T probNoWait = zero; // eq. (7.9), states 0..s-1
307 for (unsigned k = 0; k < s; ++k) probNoWait += pa[k];
308
309 std::vector<T> sigma(rr, zero), mSum(rr, zero), vSum(rr, zero), ewa1(rr, zero), ewa2(rr, zero);
310 std::vector<std::vector<T>> rateK(rr), phiK(rr), survK(rr);
311 for (std::size_t k = 1; k <= rr; ++k) {
312 detail::mgisrgi_kernel(k, smu, dlt, delta, rateK[k - 1], phiK[k - 1], survK[k - 1]);
313 T prod = one, sm = zero, sv = zero, cumM = zero, cumV = zero, e1 = zero, e2 = zero;
314 for (std::size_t j = 0; j < k; ++j) {
315 const T m = one / rateK[k - 1][j];
316 prod *= (one - phiK[k - 1][j]);
317 sm += m;
318 sv += m * m;
319 // Eqs. (7.28)-(7.29): abandoning at the jth departure epoch costs the
320 // sum of the first j interdeparture times.
321 cumM += m;
322 cumV += m * m;
323 const T w = survK[k - 1][j] * phiK[k - 1][j];
324 e1 += w * cumM;
325 e2 += w * (cumV + cumM * cumM);
326 }
327 sigma[k - 1] = prod;
328 mSum[k - 1] = sm;
329 vSum[k - 1] = sv;
330 ewa1[k - 1] = e1;
331 ewa2[k - 1] = e2;
332 }
333
334 // Finding s+k in system puts the arrival in position k+1, so the weights are
335 // pa_{s+k} for k = 0..r-1.
336 std::vector<T> wArr(rr, zero);
337 for (std::size_t k = 0; k < rr; ++k) wArr[k] = pa[s + k];
338 T probServed = probNoWait, ews1 = zero, ews2 = zero, ewa1Tot = zero, ewa2Tot = zero;
339 for (std::size_t k = 0; k < rr; ++k) {
340 probServed += wArr[k] * sigma[k];
341 ews1 += wArr[k] * sigma[k] * mSum[k]; // eq. (7.16)
342 ews2 += wArr[k] * sigma[k] * (vSum[k] + mSum[k] * mSum[k]); // eq. (7.17)
343 ewa1Tot += wArr[k] * ewa1[k]; // eq. (7.26)
344 ewa2Tot += wArr[k] * ewa2[k]; // eq. (7.27)
345 }
346
347 res.probNoWait = probNoWait;
348 res.probServed = probServed;
349 res.probAbandon = one - probServed;
350 res.meanNumber = meanNumber;
351 res.varNumber = varNumber;
352 res.meanQueueLength = meanQueue;
353 res.varQueueLength = varQueue;
354 res.utilization = busy / num_traits<T>::from_int(static_cast<long>(s));
355 res.throughput = lambda * (one - res.probLoss) * probServed;
356 res.abandonRate = lambda * (one - res.probLoss) * res.probAbandon;
357 res.meanWaitServed = detail::mgisrgi_ratio(ews1, probServed);
358 res.varWaitServed = detail::mgisrgi_ratio(ews2, probServed) -
360 if (res.varWaitServed < zero) res.varWaitServed = zero;
361 res.meanWaitAbandon = detail::mgisrgi_ratio(ewa1Tot, res.probAbandon);
362 res.varWaitAbandon = detail::mgisrgi_ratio(ewa2Tot, res.probAbandon) -
364 if (res.varWaitAbandon < zero) res.varWaitAbandon = zero;
365 res.meanWait = ews1 + ewa1Tot;
366 res.secondMomentWait = ews2 + ewa2Tot;
367 res.abandonRates = delta;
368 res.totalAbandonRates = dlt;
369 res.numWaitingSpaces = rr;
371 res.patienceRate = patience.theta;
372
373 if (!opts.wPoints.empty()) {
374 if constexpr (std::is_same_v<T, double>) {
375 // Eqs. (7.22)-(7.23) served, (7.32)-(7.33) abandoning. Both fold the
376 // same kernel: the wait is a sum of exponentials with rates 1/m_k(j),
377 // truncated at the departure epoch that serves or loses the customer.
378 auto transform = [&](const lti::Cplx& z, bool served) {
379 lti::Cplx val(0.0, 0.0);
380 for (std::size_t k = 1; k <= rr; ++k) {
381 lti::Cplx chain(1.0, 0.0);
382 for (std::size_t j = 0; j < k; ++j) {
383 chain *= rateK[k - 1][j] / (rateK[k - 1][j] + z);
384 if (!served) val += chain * (wArr[k - 1] * survK[k - 1][j] * phiK[k - 1][j]);
385 }
386 if (served) val += chain * (wArr[k - 1] * sigma[k - 1]);
387 }
388 return val;
389 };
390 const lti::LaplaceMethod method = lti::laplace_method(opts.invMethod);
391 const double capS = std::max(probServed - probNoWait, 0.0);
392 const double capA = std::max(res.probAbandon, 0.0);
393 res.waitPoints = opts.wPoints;
394 res.cdfWaitServed.resize(opts.wPoints.size());
395 res.cdfWaitAbandon.resize(opts.wPoints.size());
396 res.cdfWait.resize(opts.wPoints.size());
397 for (std::size_t i = 0; i < opts.wPoints.size(); ++i) {
398 double fs = lti::laplace_invert(
399 [&](const lti::Cplx& z) { return transform(z, true) / z; }, opts.wPoints[i],
400 method, opts.invN);
401 double fa = lti::laplace_invert(
402 [&](const lti::Cplx& z) { return transform(z, false) / z; }, opts.wPoints[i],
403 method, opts.invN);
404 fs = std::min(std::max(fs, 0.0), capS);
405 fa = std::min(std::max(fa, 0.0), capA);
406 res.cdfWaitServed[i] = (probNoWait + fs) / std::max(probServed, 1e-300);
407 res.cdfWaitAbandon[i] = fa / std::max(res.probAbandon, 1e-300);
408 res.cdfWait[i] = probNoWait + fs + fa;
409 }
410 } else {
411 throw InputError("qsys_mgisrgi_whitt: the waiting-time cdfs need a numerical Laplace "
412 "inversion, which only the double instantiation carries");
413 }
414 }
415 return res;
416}
417
418/**
419 * Exact analysis of the Erlang A model M/M/s/r+M.
420 *
421 * The number in system is the birth-and-death process with death rate
422 * min(k,s) mu + (k-s)^+ theta, so every measure is exact: this is the case in
423 * which the approximation above reproduces the model (eq. 7.12). theta = 0
424 * recovers M/M/s/r, and then a finite r is required whenever lambda >= s mu.
425 *
426 * Port of matlab/src/api/qsys/qsys_erlanga.m.
427 *
428 * @param lambda arrival rate
429 * @param mu service rate of one server
430 * @param theta abandonment rate of a waiting customer
431 * @param s number of servers
432 * @param r extra waiting spaces; infinity for an unbounded queue
433 * @param opts cdf times, truncation controls and inversion settings
434 */
435template <class T>
436QsysAbandonResult<T> qsys_erlanga(const T& lambda, const T& mu, const T& theta, unsigned s,
437 double r = std::numeric_limits<double>::infinity(),
438 const MgisrgiOptions& opts = MgisrgiOptions()) {
439 if (theta <= num_traits<T>::from_int(0) && !std::isfinite(r) &&
440 lambda >= num_traits<T>::from_int(static_cast<long>(s)) * mu)
441 throw InputError("qsys_erlanga: without abandonment (theta = 0) and with an infinite "
442 "waiting room the queue is unstable at lambda >= s*mu; give a finite r "
443 "or a positive theta");
444 return qsys_mgisrgi_whitt(lambda, mu, s, r, Patience<T>::exponential(theta), opts);
445}
446
447} // namespace qsys
448} // namespace line
449
450#endif // LINE_API_QSYS_MGISRGI_WHITT_H
InputError(const std::string &what)
Definition error.h:39
The exception types the port throws.
Numerical inversion of a Laplace transform: Euler, Talbot, Gaver-Stehfest.
double laplace_invert(const LaplaceFn &F, double t, LaplaceMethod method=LaplaceMethod::Euler, std::size_t n=0)
Invert F at t by the named method.
LaplaceMethod
The methods laplace_invert accepts.
std::complex< double > Cplx
LaplaceMethod laplace_method(const std::string &s)
Parse the reference's method names, including its two Gaver spellings.
@ Exponential
one moment, or cv2 exactly 1
Definition qsys_mapg1.h:103
QsysAbandonResult< T > qsys_erlanga(const T &lambda, const T &mu, const T &theta, unsigned s, double r=std::numeric_limits< double >::infinity(), const MgisrgiOptions &opts=MgisrgiOptions())
Exact analysis of the Erlang A model M/M/s/r+M.
QsysAbandonResult< T > qsys_mgisrgi_whitt(const T &lambda, const T &mu, unsigned s, double r, const Patience< T > &patience, const MgisrgiOptions &opts=MgisrgiOptions())
Engineering solution of the call-center model M/GI/s/r+GI.
PatienceForm
Which of the three accepted descriptions of the patience law is carried.
Number-type abstraction for the templated API port.
Shared return type and arithmetic helpers for the templated qsys port.
Options of qsys_mgisrgi_whitt, all with the MATLAB defaults.
std::size_t invN
number of inversion nodes
double tol
relative tail tolerance for that truncation
std::size_t maxQueue
truncation level used when r is infinite
std::vector< double > wPoints
times for the waiting-time cdfs
std::string invMethod
Laplace inversion method.
The patience (time-to-abandon) law, in the three forms the algorithm accepts.
std::function< T(const T &)> fun
static Patience hazard(std::function< T(const T &)> h)
Patience given by its hazard rate h = f/(1-F).
static Patience ccdf(std::function< T(const T &)> g)
Patience given by its complementary cdf G(t) = 1-F(t).
static Patience exponential(const T &theta)
Exponential patience of rate theta, h(t) = theta, the Erlang A case.
Steady-state measures of a multiserver queue with customer abandonment.
T meanWait
E[W] over entering customers.
T abandonRate
lambda(1-P_loss)P(A)
bool exponentialPatience
whether the answer is exact
T probLoss
P(an arrival is blocked), 0 when r is infinite.
std::vector< T > queueLengthDist
P(N = k), k = 0..s+r.
T probNoWait
P(W = 0) among entering customers.
std::vector< T > abandonRates
delta_j
std::vector< double > cdfWaitAbandon
P(W <= t | A).
std::vector< double > cdfWaitServed
P(W <= t | S).
std::vector< double > waitPoints
times of the cdfs, empty when not requested
std::vector< double > cdfWait
P(W <= t).
T probServed
P(S), eventually served.
std::vector< T > totalAbandonRates
Delta_k, index k, Delta_0 = 0.
T patienceRate
the exponential rate, unset otherwise
T secondMomentWait
E[W^2] over entering customers.
std::size_t numWaitingSpaces
waiting spaces used, the truncation when r is infinite