LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
qsys_mapg1.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_QSYS_MAPG1_H
6#define LINE_API_QSYS_QSYS_MAPG1_H
7
8/**
9 * @file
10 * @ingroup api_qsys
11 * The MAP/G/1 FCFS queue, by moment-matching the general service time to a
12 * phase-type distribution.
13 *
14 * Templated port of matlab/src/api/qsys/qsys_mapg1.m. The reference has two
15 * parts: a service fit, written in the .m file itself, and the queue solution,
16 * delegated to BUTools' MMAPPH1FCFS. This port transcribes the first and
17 * replaces the second with the port's own QBD route, exactly as
18 * qsys_mapph1.h already does -- MAP/PH/1 is a MAP/MAP/1 queue with a renewal
19 * service process, and qsys_mapph1 reproduces MMAPPH1FCFS to 1e-15 on that
20 * family (see the measured agreement table in qsys_mapph1.h).
21 *
22 * THE SERVICE FIT, branch by branch, is the reference's own fitServiceToPH:
23 *
24 * 3 or more moments : an acyclic PH matching (m1, m2, m3)
25 * exactly 2 moments : cv2 = m2/m1^2 - 1, and then
26 * cv2 <= 0 -> Erlang-k, k = max(1, round(1/max(cv2, 0.01)))
27 * cv2 < 1 -> Erlang-k, k = max(2, round(1/cv2))
28 * cv2 == 1 -> exponential
29 * cv2 > 1 -> balanced-means 2-phase hyperexponential,
30 * p = (1 + sqrt((cv2-1)/(cv2+1)))/2,
31 * rates 2p/m1 and 2(1-p)/m1
32 * 1 moment : exponential
33 *
34 * The Erlang branches match m1 exactly and m2 only through the rounded k, and
35 * the hyperexponential branch matches both m1 and cv2 exactly (its cv2 is
36 * 1/(2p(1-p)) - 1, which is the requested one for that p). Both are the
37 * reference's approximations, reproduced rather than improved.
38 *
39 * THE 3-MOMENT BRANCH IS THE ONE SUBSTITUTION. The reference calls BUTools'
40 * APHFrom3Moments, which this port does not transcribe (the same position
41 * qsys_mapph1.h takes on MMAPPH1FCFS). It uses line::mam::aph_fit instead, the
42 * m3a implementation of
43 * the same Bobbio-Horvath-Telek canonical APH. MEASURED: on
44 * (m1, m2, m3) = (1/3, 1/6, 1/9), the moments of Erlang(2) with mean 1/3, the
45 * two agree on the order (3), on the sub-generator
46 * ([-12 12 0; 0 -12 12; 0 0 -5]) and on the entry vector ([0.8 0 0.2]) to
47 * 1e-14, so the queue results agree as well. They are not guaranteed to pick
48 * the same representation everywhere -- the order search and the branch
49 * conditions are written differently -- and where they differ the queue
50 * results will differ too, because a queue depends on the whole service
51 * distribution and not on its first three moments. A caller who needs the
52 * reference's exact PH can pass it to qsys_mapph1 directly. The reference's
53 * fallback chain is reproduced: when the APH fit throws, BUTools'
54 * PH2From3Moments (transcribed below, with its feasibility bounds) is tried,
55 * and when that throws too the service is an exponential of mean m1.
56 *
57 * MOMENTS. MMAPPH1FCFS also returns higher queue-length and sojourn-time
58 * moments (ncMoms, stMoms). qsys_mapmap1 now provides both -- the queue-length
59 * moments in closed form from pi_0 and R, the sojourn-time moments from the
60 * phase-type sojourn law of the age process -- and they are passed through
61 * here as queueLengthMoments and sojournTimeMoments under the reference's
62 * numQLMoms / numSTMoms options (default 3 each).
63 *
64 * REFERENCE DEFECTS in qsys_mapg1.m:
65 *
66 * 1. THE cv2 <= 0 BRANCH SILENTLY BUILDS AN ERLANG-100. Line 128 computes
67 * k = max(1, round(1/max(cv2, 0.01))), and for any cv2 <= 0 the max pins
68 * the denominator at 0.01, so k = 100 whatever the moments were. Nothing
69 * warns. Deterministic service therefore enters the QBD with a 100-phase
70 * service process (cv2 = 0.01, not 0), which is both expensive and a
71 * silent modelling decision. MATLAB reproduction, from matlab/:
72 * r = qsys_mapg1(-2, 2, [1/3, (1/3)^2]) % cv2 = 0 exactly
73 * r.meanQueueLength -> 1.33999999999909
74 * against the exact M/D/1 value 4/3 + ... (the Erlang-100 answer is
75 * 1.3399999999 rather than the M/D/1 1.3333...). Reproduced here, since
76 * it is the reference's model choice, and pinned by a test that asserts
77 * the phase count is 100.
78 * 2. m2 and m3 are read without any feasibility check: a moment set that is
79 * not PH-representable reaches the fit and fails there.
80 * 3. The reference computes rho from the INPUT mean 1/serviceMoments(1) and
81 * not from the fitted PH, so a branch whose fit does not preserve m1 would
82 * report a utilization inconsistent with its own service process. Every
83 * branch does preserve m1, so this is latent rather than active; the port
84 * reproduces the reference's formula.
85 *
86 * ARITHMETIC. Gated on num_traits<T>::has_transcendental, inherited from
87 * qsys_mapmap1 (the cyclic reduction behind R) and from aph_fit.
88 */
89
90#include <cmath>
91#include <cstddef>
92#include <limits>
93#include <string>
94#include <vector>
95
100#include "line/num/number.h"
101#include "line/util/error.h"
102#include "line/util/matrix.h"
103
104namespace line {
105namespace qsys {
106
107/** Which branch of fitServiceToPH was taken, for the caller and for tests. */
108enum class MapG1ServiceFit {
109 Exponential, ///< one moment, or cv2 exactly 1
110 Erlang, ///< 0 < cv2 < 1, or the cv2 <= 0 branch
111 Hyperexponential, ///< cv2 > 1
112 Acyclic ///< three or more moments
113};
114
115/** Result of qsys_mapg1. */
116template <class T>
118 T meanQueueLength; ///< E[N], number in system
119 T meanWaitingTime; ///< max(0, E[W] - m1), as in the reference
120 T meanSojournTime; ///< E[W]
121 T utilization; ///< lambda m1
122 std::vector<T> queueLengthDist; ///< P(N = n), n = 0, 1, ...
123 std::vector<T> queueLengthMoments; ///< raw moments E[N^k], k = 1..numQLMoms
124 std::vector<T> sojournTimeMoments; ///< raw moments E[W^k], k = 1..numSTMoms
125 mam::Map<T> serviceFit; ///< the fitted PH as its renewal MAP
126 MapG1ServiceFit fitKind; ///< which branch produced it
127 std::size_t servicePhases; ///< order of the fitted PH
128 /** The reference's analyzer label ('BUTools:MMAPPH1FCFS'), kept for parity; the port computes the same quantities by the QBD route. */
129 std::string analyzer;
130};
131
132namespace detail {
133
134/** Exponential PH of the given mean, as its renewal MAP. */
135template <class T>
136mam::Map<T> mapg1_exponential(const T& mean) {
137 if (!(mean > num_traits<T>::from_int(0)))
138 throw InputError("qsys_mapg1: the mean service time must be positive");
139 mam::Map<T> m;
140 m.D0 = Matrix<T>(1, 1, T(-num_traits<T>::from_int(1) / mean));
141 m.D1 = Matrix<T>(1, 1, T(num_traits<T>::from_int(1) / mean));
142 return m;
143}
144
145/** Erlang-k PH of the given mean, as its renewal MAP (entry in phase 1). */
146template <class T>
147mam::Map<T> mapg1_erlang(const T& mean, std::size_t k) {
148 if (k == 0) throw InputError("qsys_mapg1: an Erlang fit needs at least one phase");
149 const T zero = num_traits<T>::from_int(0);
150 const T mu = T(num_traits<T>::from_int(static_cast<long>(k)) / mean);
151 mam::Map<T> m;
152 m.D0 = Matrix<T>(k, k, zero);
153 m.D1 = Matrix<T>(k, k, zero);
154 for (std::size_t i = 0; i < k; ++i) {
155 m.D0(i, i) = -mu;
156 if (i + 1 < k) m.D0(i, i + 1) = mu;
157 }
158 m.D1(k - 1, 0) = mu; // completion restarts in phase 1
159 return m;
160}
161
162/**
163 * Balanced-means two-phase hyperexponential with the given mean and squared
164 * coefficient of variation, as its renewal MAP.
165 */
166template <class T>
167mam::Map<T> mapg1_hyperexp2(const T& mean, const T& cv2) {
168 using std::sqrt;
169 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
170 const T two = num_traits<T>::from_int(2);
171 const T p = T(T(one + sqrt(T(T(cv2 - one) / T(cv2 + one)))) / two);
172 const T l1 = T(two * p / mean), l2 = T(two * T(one - p) / mean);
173 mam::Map<T> m;
174 m.D0 = Matrix<T>(2, 2, zero);
175 m.D1 = Matrix<T>(2, 2, zero);
176 m.D0(0, 0) = -l1;
177 m.D0(1, 1) = -l2;
178 m.D1(0, 0) = l1 * p;
179 m.D1(0, 1) = l1 * T(one - p);
180 m.D1(1, 0) = l2 * p;
181 m.D1(1, 1) = l2 * T(one - p);
182 return m;
183}
184
185/** BUTools APH2ndMomentLowerBound(m1, n). */
186inline double mapg1_aph_m2_lower(double m1, double n) { return m1 * m1 * (n + 1.0) / n; }
187
188/** BUTools APH3rdMomentLowerBound(m1, m2, n). */
189inline double mapg1_aph_m3_lower(double m1, double m2, double n) {
190 const double n2 = m2 / m1 / m1;
191 if (n2 < (n + 1.0) / n) return std::numeric_limits<double>::infinity();
192 if (n2 < (n + 4.0) / (n + 1.0)) {
193 const double p = ((n + 1.0) * (n2 - 2.0)) / (3.0 * n2 * (n - 1.0)) *
194 ((-2.0 * std::sqrt(n + 1.0)) / std::sqrt(-3.0 * n * n2 + 4.0 * n + 4.0) - 1.0);
195 const double a = (n2 - 2.0) / (p * (1.0 - n2) + std::sqrt(p * p + p * n * (n2 - 2.0) / (n - 1.0)));
196 const double l = ((3.0 + a) * (n - 1.0) + 2.0 * a) / ((n - 1.0) * (1.0 + a * p)) -
197 (2.0 * a * (n + 1.0)) / (2.0 * (n - 1.0) + a * p * (n * a + 2.0 * n - 2.0));
198 return l * m1 * m2;
199 }
200 return (n + 1.0) / n * n2 * m1 * m2;
201}
202
203/** BUTools APH3rdMomentUpperBound(m1, m2, n). */
204inline double mapg1_aph_m3_upper(double m1, double m2, double n) {
205 const double n2 = m2 / m1 / m1;
206 if (n2 < (n + 1.0) / n) return -std::numeric_limits<double>::infinity();
207 if (n2 <= n / (n - 1.0))
208 return m1 * m2 *
209 (2.0 * (n - 2.0) * (n * n2 - n - 1.0) * std::sqrt(1.0 + (n * (n2 - 2.0)) / (n - 1.0)) +
210 (n + 2.0) * (3.0 * n * n2 - 2.0 * n - 2.0)) /
211 (n * n * n2);
212 return std::numeric_limits<double>::infinity();
213}
214
215/**
216 * BUTools PH2From3Moments(moms, prec = 1e-14): the two-phase PH matching three
217 * moments, as its renewal MAP. Throws NumericError where BUTools errors (an
218 * infeasible moment set), which is what the reference's fallback chain catches.
219 */
220template <class T>
221mam::Map<T> mapg1_ph2_from_3moments(const T& m1T, const T& m2T, const T& m3T) {
222 using std::sqrt;
223 const double prec = 1e-14;
224 const double m1 = num_traits<T>::to_double(m1T), m2 = num_traits<T>::to_double(m2T),
225 m3 = num_traits<T>::to_double(m3T);
226 if (m2 < mapg1_aph_m2_lower(m1, 2.0))
227 throw NumericError(ErrorKind{"Infeasible"}, "PH2From3Moments: The given second moment is not feasible!");
228 if (m3 < mapg1_aph_m3_lower(m1, m2, 2.0))
229 throw NumericError(ErrorKind{"Infeasible"}, "PH2From3Moments: The given third moment is not feasible (too small)!");
230 if (m3 > mapg1_aph_m3_upper(m1, m2, 2.0))
231 throw NumericError(ErrorKind{"Infeasible"}, "PH2From3Moments: The given third moment is not feasible (too large)!");
232 if (std::fabs(m2 / m1 / m1 - 2.0) < prec) return mapg1_exponential(m1T);
233
234 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
235 const T two = num_traits<T>::from_int(2), three = num_traits<T>::from_int(3), six = num_traits<T>::from_int(6);
236 const T b = T(three * m1T * m2T - m3T);
237 const T c = T(three * m2T * m2T - two * m1T * m3T);
238 const T e = T(m2T - two * m1T * m1T);
239 T a = T(b * b + six * c * e);
240 if (a < zero) a = zero;
241 a = sqrt(a);
242 T lambda1, lambda2, p;
243 if (c > zero) {
244 lambda1 = T(T(b - a) / c);
245 lambda2 = T(T(b + a) / c);
246 p = T(T(-b - six * m1T * e + a) / T(b + a));
247 } else if (c < zero) {
248 lambda1 = T(T(b + a) / c);
249 lambda2 = T(T(b - a) / c);
250 p = T(T(b + six * m1T * e + a) / T(-b + a));
251 } else {
252 lambda1 = zero;
253 lambda2 = T(one / m1T);
254 p = zero;
255 }
256 // alpha = [p, 1-p], A = [-l1 l1; 0 -l2]; as a renewal MAP D1 = (-A e) alpha
257 mam::Map<T> m;
258 m.D0 = Matrix<T>(2, 2, zero);
259 m.D1 = Matrix<T>(2, 2, zero);
260 m.D0(0, 0) = -lambda1;
261 m.D0(0, 1) = lambda1;
262 m.D0(1, 1) = -lambda2;
263 m.D1(1, 0) = lambda2 * p;
264 m.D1(1, 1) = lambda2 * T(one - p);
265 return m;
266}
267
268} // namespace detail
269
270/**
271 * Fit a general service time to a PH, following qsys_mapg1.m's
272 * fitServiceToPH. Exposed separately because the branch selection is the part
273 * of the reference that is transcribed verbatim, and a caller may want the
274 * fitted process without solving a queue.
275 *
276 * @param moments the first one, two or three raw moments of the service time
277 * @param kind out: which branch was taken
278 */
279template <class T>
280mam::Map<T> qsys_mapg1_service_fit(const std::vector<T>& moments, MapG1ServiceFit& kind) {
282 "qsys_mapg1_service_fit requires transcendental arithmetic");
283 if (moments.empty()) throw InputError("qsys_mapg1: no service moments given");
284 const T one = num_traits<T>::from_int(1);
285 const T m1 = moments[0];
286
287 if (moments.size() >= 3) {
288 // APHFrom3Moments, then PH2From3Moments, then an exponential: the
289 // reference's try/catch chain.
290 try {
292 return mam::aph_fit(moments[0], moments[1], moments[2]).aph;
293 } catch (const Error&) {
294 }
295 try {
297 return detail::mapg1_ph2_from_3moments(moments[0], moments[1], moments[2]);
298 } catch (const Error&) {
299 }
301 return detail::mapg1_exponential(m1);
302 }
303 if (moments.size() == 2) {
304 const T cv2 = T(T(moments[1] / T(m1 * m1)) - one);
305 const T zero = num_traits<T>::from_int(0);
306 if (cv2 <= zero) {
307 // REFERENCE DEFECT 1: the floor at 0.01 makes this Erlang-100.
308 const T floored = num_traits<T>::from_double(0.01);
309 const T den = cv2 > floored ? cv2 : floored; // max(cv2, 0.01)
310 const double kd = std::round(1.0 / num_traits<T>::to_double(den));
311 const std::size_t k = kd < 1.0 ? 1u : static_cast<std::size_t>(kd);
313 return detail::mapg1_erlang(m1, k);
314 }
315 if (cv2 < one) {
316 const double kd = std::round(1.0 / num_traits<T>::to_double(cv2));
317 const std::size_t k = kd < 2.0 ? 2u : static_cast<std::size_t>(kd);
319 return detail::mapg1_erlang(m1, k);
320 }
321 if (cv2 == one) {
323 return detail::mapg1_exponential(m1);
324 }
326 return detail::mapg1_hyperexp2(m1, cv2);
327 }
329 return detail::mapg1_exponential(m1);
330}
331
332/**
333 * The MAP/G/1 FCFS queue.
334 *
335 * @param arrival arrival MAP (D0, D1)
336 * @param moments the first one, two or three raw moments of the service time
337 * @param dist_size how many entries of queueLengthDist to materialize
338 * (the reference's numQLProbs, default 100)
339 * @param numQLMoms how many raw queue-length moments to return (numQLMoms)
340 * @param numSTMoms how many raw sojourn-time moments to return (numSTMoms)
341 */
342template <class T>
343MapG1Result<T> qsys_mapg1(const mam::Map<T>& arrival, const std::vector<T>& moments,
344 std::size_t dist_size, std::size_t numQLMoms, std::size_t numSTMoms) {
346 "qsys_mapg1 requires transcendental arithmetic");
349 r.servicePhases = r.serviceFit.order();
350
351 const MapMap1Result<T> q = qsys_mapmap1(arrival, r.serviceFit, dist_size, numQLMoms, numSTMoms);
357 r.analyzer = "BUTools:MMAPPH1FCFS";
358
359 // The reference recomputes both of these from the INPUT mean rather than
360 // from the fitted process; see reference defect 3.
361 const T zero = num_traits<T>::from_int(0);
362 const T lambda = mam::map_lambda(arrival);
363 r.utilization = T(lambda * moments[0]);
364 const T wait = T(q.meanSojournTime - moments[0]);
365 r.meanWaitingTime = wait < zero ? zero : wait;
366 return r;
367}
368
369/** qsys_mapg1 with the reference's defaults numQLMoms = 3, numSTMoms = 3. */
370template <class T>
371MapG1Result<T> qsys_mapg1(const mam::Map<T>& arrival, const std::vector<T>& moments,
372 std::size_t dist_size) {
373 return qsys_mapg1(arrival, moments, dist_size, static_cast<std::size_t>(3),
374 static_cast<std::size_t>(3));
375}
376
377/** qsys_mapg1 with the reference's default of 100 materialized levels. */
378template <class T>
379MapG1Result<T> qsys_mapg1(const mam::Map<T>& arrival, const std::vector<T>& moments) {
380 return qsys_mapg1(arrival, moments, static_cast<std::size_t>(100));
381}
382
383} // namespace qsys
384} // namespace line
385
386#endif // LINE_API_QSYS_QSYS_MAPG1_H
Minimal-order acyclic phase-type fit of the first three moments (matlab/lib/kpctoolbox/aph/aph_fit....
Base error for the multiprecision C++ port.
Definition error.h:73
InputError(const std::string &what)
Definition error.h:105
NumericError(const std::string &what)
Definition error.h:112
The exception types the port throws.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Dense matrix and non-owning view.
AphFitResult< T > aph_fit(const T &e1, const T &e2, const T &e3, unsigned nmax, const T &tol)
Fit an APH(n) with n <= nmax to the raw moments e1, e2, e3.
Definition aph_fit.h:176
T map_lambda(const Map< T > &m)
Stationary arrival rate, lambda = pi D1 e.
Definition map_moment.h:79
mam::Map< T > qsys_mapg1_service_fit(const std::vector< T > &moments, MapG1ServiceFit &kind)
Fit a general service time to a PH, following qsys_mapg1.m's fitServiceToPH.
Definition qsys_mapg1.h:280
MapG1ServiceFit
Which branch of fitServiceToPH was taken, for the caller and for tests.
Definition qsys_mapg1.h:108
@ Acyclic
three or more moments
Definition qsys_mapg1.h:112
@ Exponential
one moment, or cv2 exactly 1
Definition qsys_mapg1.h:109
@ Erlang
0 < cv2 < 1, or the cv2 <= 0 branch
Definition qsys_mapg1.h:110
MapG1Result< T > qsys_mapg1(const mam::Map< T > &arrival, const std::vector< T > &moments, std::size_t dist_size, std::size_t numQLMoms, std::size_t numSTMoms)
The MAP/G/1 FCFS queue.
Definition qsys_mapg1.h:343
MapMap1Result< T > qsys_mapmap1(const mam::Map< T > &arrival, const mam::Map< T > &service, std::size_t dist_size, std::size_t numQLMoms, std::size_t numSTMoms)
MAP/MAP/1 as the reference solves it: the service MAP collapsed to its PH marginal (mapToPh),...
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
Number-type abstraction for the templated API port.
The MAP/MAP/1 FCFS queue: mean number in system, waiting time, sojourn time, utilization and the queu...
The MAP/PH/1 FCFS queue.
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Matrix< T > D1
Definition map_moment.h:55
Matrix< T > D0
Definition map_moment.h:54
Result of qsys_mapg1.
Definition qsys_mapg1.h:117
std::string analyzer
The reference's analyzer label ('BUTools:MMAPPH1FCFS'), kept for parity; the port computes the same q...
Definition qsys_mapg1.h:129
MapG1ServiceFit fitKind
which branch produced it
Definition qsys_mapg1.h:126
T meanWaitingTime
max(0, E[W] - m1), as in the reference
Definition qsys_mapg1.h:119
T meanQueueLength
E[N], number in system.
Definition qsys_mapg1.h:118
std::vector< T > sojournTimeMoments
raw moments E[W^k], k = 1..numSTMoms
Definition qsys_mapg1.h:124
std::size_t servicePhases
order of the fitted PH
Definition qsys_mapg1.h:127
std::vector< T > queueLengthMoments
raw moments E[N^k], k = 1..numQLMoms
Definition qsys_mapg1.h:123
mam::Map< T > serviceFit
the fitted PH as its renewal MAP
Definition qsys_mapg1.h:125
std::vector< T > queueLengthDist
P(N = n), n = 0, 1, ...
Definition qsys_mapg1.h:122
Return value of the MAP/MAP/1 family (qsys_mapmap1, qsys_mapph1, qsys_phph1), carrying the same quant...
std::vector< T > sojournTimeMoments
raw moments E[W^k], k = 1..numSTMoms
std::vector< T > queueLengthMoments
raw moments E[N^k], k = 1..numQLMoms
T meanQueueLength
E[N], number in system.
T meanSojournTime
E[W] = E[Wq] + E[S].
std::vector< T > queueLengthDist
P(N = n), n = 0, 1, ...