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.
53 *
54 * WHAT IS NOT RETURNED. MMAPPH1FCFS also returns higher queue-length and
55 * sojourn-time moments (ncMoms, stMoms beyond the first). The QBD route gives
56 * the means and the queue-length distribution, and no higher moments are
57 * fabricated from the truncated distribution: queueLengthMoments and
58 * sojournTimeMoments simply have no counterpart here.
59 *
60 * REFERENCE DEFECTS in qsys_mapg1.m:
61 *
62 * 1. THE cv2 <= 0 BRANCH SILENTLY BUILDS AN ERLANG-100. Line 128 computes
63 * k = max(1, round(1/max(cv2, 0.01))), and for any cv2 <= 0 the max pins
64 * the denominator at 0.01, so k = 100 whatever the moments were. Nothing
65 * warns. Deterministic service therefore enters the QBD with a 100-phase
66 * service process (cv2 = 0.01, not 0), which is both expensive and a
67 * silent modelling decision. MATLAB reproduction, from matlab/:
68 * r = qsys_mapg1(-2, 2, [1/3, (1/3)^2]) % cv2 = 0 exactly
69 * r.meanQueueLength -> 1.33999999999909
70 * against the exact M/D/1 value 4/3 + ... (the Erlang-100 answer is
71 * 1.3399999999 rather than the M/D/1 1.3333...). Reproduced here, since
72 * it is the reference's model choice, and pinned by a test that asserts
73 * the phase count is 100.
74 * 2. m2 and m3 are read without any feasibility check: a moment set that is
75 * not PH-representable reaches the fit and fails there.
76 * 3. The reference computes rho from the INPUT mean 1/serviceMoments(1) and
77 * not from the fitted PH, so a branch whose fit does not preserve m1 would
78 * report a utilization inconsistent with its own service process. Every
79 * branch does preserve m1, so this is latent rather than active; the port
80 * reproduces the reference's formula.
81 *
82 * ARITHMETIC. Gated on num_traits<T>::has_transcendental, inherited from
83 * qsys_mapmap1 (the cyclic reduction behind R) and from aph_fit.
84 */
85
86#include <cmath>
87#include <cstddef>
88#include <vector>
89
94#include "line/num/number.h"
95#include "line/util/error.h"
96#include "line/util/matrix.h"
97
98namespace line {
99namespace qsys {
100
101/** Which branch of fitServiceToPH was taken, for the caller and for tests. */
102enum class MapG1ServiceFit {
103 Exponential, ///< one moment, or cv2 exactly 1
104 Erlang, ///< 0 < cv2 < 1, or the cv2 <= 0 branch
105 Hyperexponential, ///< cv2 > 1
106 Acyclic ///< three or more moments
107};
108
109/** Result of qsys_mapg1. */
110template <class T>
112 T meanQueueLength; ///< E[N], number in system
113 T meanWaitingTime; ///< max(0, E[W] - m1), as in the reference
114 T meanSojournTime; ///< E[W]
115 T utilization; ///< lambda m1
116 std::vector<T> queueLengthDist; ///< P(N = n), n = 0, 1, ...
117 mam::Map<T> serviceFit; ///< the fitted PH as its renewal MAP
118 MapG1ServiceFit fitKind; ///< which branch produced it
119 std::size_t servicePhases; ///< order of the fitted PH
120};
121
122namespace detail {
123
124/** Exponential PH of the given mean, as its renewal MAP. */
125template <class T>
126mam::Map<T> mapg1_exponential(const T& mean) {
127 if (!(mean > num_traits<T>::from_int(0)))
128 throw InputError("qsys_mapg1: the mean service time must be positive");
129 mam::Map<T> m;
130 m.D0 = Matrix<T>(1, 1, T(-num_traits<T>::from_int(1) / mean));
131 m.D1 = Matrix<T>(1, 1, T(num_traits<T>::from_int(1) / mean));
132 return m;
133}
134
135/** Erlang-k PH of the given mean, as its renewal MAP (entry in phase 1). */
136template <class T>
137mam::Map<T> mapg1_erlang(const T& mean, std::size_t k) {
138 if (k == 0) throw InputError("qsys_mapg1: an Erlang fit needs at least one phase");
139 const T zero = num_traits<T>::from_int(0);
140 const T mu = T(num_traits<T>::from_int(static_cast<long>(k)) / mean);
141 mam::Map<T> m;
142 m.D0 = Matrix<T>(k, k, zero);
143 m.D1 = Matrix<T>(k, k, zero);
144 for (std::size_t i = 0; i < k; ++i) {
145 m.D0(i, i) = -mu;
146 if (i + 1 < k) m.D0(i, i + 1) = mu;
147 }
148 m.D1(k - 1, 0) = mu; // completion restarts in phase 1
149 return m;
150}
151
152/**
153 * Balanced-means two-phase hyperexponential with the given mean and squared
154 * coefficient of variation, as its renewal MAP.
155 */
156template <class T>
157mam::Map<T> mapg1_hyperexp2(const T& mean, const T& cv2) {
158 using std::sqrt;
159 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
160 const T two = num_traits<T>::from_int(2);
161 const T p = T(T(one + sqrt(T(T(cv2 - one) / T(cv2 + one)))) / two);
162 const T l1 = T(two * p / mean), l2 = T(two * T(one - p) / mean);
163 mam::Map<T> m;
164 m.D0 = Matrix<T>(2, 2, zero);
165 m.D1 = Matrix<T>(2, 2, zero);
166 m.D0(0, 0) = -l1;
167 m.D0(1, 1) = -l2;
168 m.D1(0, 0) = l1 * p;
169 m.D1(0, 1) = l1 * T(one - p);
170 m.D1(1, 0) = l2 * p;
171 m.D1(1, 1) = l2 * T(one - p);
172 return m;
173}
174
175} // namespace detail
176
177/**
178 * Fit a general service time to a PH, following qsys_mapg1.m's
179 * fitServiceToPH. Exposed separately because the branch selection is the part
180 * of the reference that is transcribed verbatim, and a caller may want the
181 * fitted process without solving a queue.
182 *
183 * @param moments the first one, two or three raw moments of the service time
184 * @param kind out: which branch was taken
185 */
186template <class T>
187mam::Map<T> qsys_mapg1_service_fit(const std::vector<T>& moments, MapG1ServiceFit& kind) {
189 "qsys_mapg1_service_fit requires transcendental arithmetic");
190 if (moments.empty()) throw InputError("qsys_mapg1: no service moments given");
191 const T one = num_traits<T>::from_int(1);
192 const T m1 = moments[0];
193
194 if (moments.size() >= 3) {
196 return mam::aph_fit(moments[0], moments[1], moments[2]).aph;
197 }
198 if (moments.size() == 2) {
199 const T cv2 = T(T(moments[1] / T(m1 * m1)) - one);
200 const T zero = num_traits<T>::from_int(0);
201 if (cv2 <= zero) {
202 // REFERENCE DEFECT 1: the floor at 0.01 makes this Erlang-100.
203 const T floored = num_traits<T>::from_double(0.01);
204 const T den = cv2 > floored ? cv2 : floored; // max(cv2, 0.01)
205 const double kd = std::round(1.0 / num_traits<T>::to_double(den));
206 const std::size_t k = kd < 1.0 ? 1u : static_cast<std::size_t>(kd);
208 return detail::mapg1_erlang(m1, k);
209 }
210 if (cv2 < one) {
211 const double kd = std::round(1.0 / num_traits<T>::to_double(cv2));
212 const std::size_t k = kd < 2.0 ? 2u : static_cast<std::size_t>(kd);
214 return detail::mapg1_erlang(m1, k);
215 }
216 if (cv2 == one) {
218 return detail::mapg1_exponential(m1);
219 }
221 return detail::mapg1_hyperexp2(m1, cv2);
222 }
224 return detail::mapg1_exponential(m1);
225}
226
227/**
228 * The MAP/G/1 FCFS queue.
229 *
230 * @param arrival arrival MAP (D0, D1)
231 * @param moments the first one, two or three raw moments of the service time
232 * @param dist_size how many entries of queueLengthDist to materialize
233 * (the reference's numQLProbs, default 100)
234 */
235template <class T>
236MapG1Result<T> qsys_mapg1(const mam::Map<T>& arrival, const std::vector<T>& moments,
237 std::size_t dist_size) {
239 "qsys_mapg1 requires transcendental arithmetic");
242 r.servicePhases = r.serviceFit.order();
243
244 const MapMap1Result<T> q = qsys_mapmap1(arrival, r.serviceFit, dist_size);
248
249 // The reference recomputes both of these from the INPUT mean rather than
250 // from the fitted process; see reference defect 3.
251 const T zero = num_traits<T>::from_int(0);
252 const T lambda = mam::map_lambda(arrival);
253 r.utilization = T(lambda * moments[0]);
254 const T wait = T(q.meanSojournTime - moments[0]);
255 r.meanWaitingTime = wait < zero ? zero : wait;
256 return r;
257}
258
259/** qsys_mapg1 with the reference's default of 100 materialized levels. */
260template <class T>
261MapG1Result<T> qsys_mapg1(const mam::Map<T>& arrival, const std::vector<T>& moments) {
262 return qsys_mapg1(arrival, moments, static_cast<std::size_t>(100));
263}
264
265} // namespace qsys
266} // namespace line
267
268#endif // LINE_API_QSYS_QSYS_MAPG1_H
Minimal-order acyclic phase-type fit of the first three moments (matlab/lib/kpctoolbox/aph/aph_fit....
InputError(const std::string &what)
Definition error.h:39
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:187
MapG1ServiceFit
Which branch of fitServiceToPH was taken, for the caller and for tests.
Definition qsys_mapg1.h:102
@ Acyclic
three or more moments
Definition qsys_mapg1.h:106
@ Exponential
one moment, or cv2 exactly 1
Definition qsys_mapg1.h:103
@ Erlang
0 < cv2 < 1, or the cv2 <= 0 branch
Definition qsys_mapg1.h:104
MapMap1Result< T > qsys_mapmap1(const mam::Map< T > &arrival, const mam::Map< T > &service, std::size_t dist_size)
MAP/MAP/1 by the exact QBD solution.
MapG1Result< T > qsys_mapg1(const mam::Map< T > &arrival, const std::vector< T > &moments, std::size_t dist_size)
The MAP/G/1 FCFS queue.
Definition qsys_mapg1.h:236
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:111
MapG1ServiceFit fitKind
which branch produced it
Definition qsys_mapg1.h:118
T meanWaitingTime
max(0, E[W] - m1), as in the reference
Definition qsys_mapg1.h:113
T meanQueueLength
E[N], number in system.
Definition qsys_mapg1.h:112
std::size_t servicePhases
order of the fitted PH
Definition qsys_mapg1.h:119
mam::Map< T > serviceFit
the fitted PH as its renewal MAP
Definition qsys_mapg1.h:117
std::vector< T > queueLengthDist
P(N = n), n = 0, 1, ...
Definition qsys_mapg1.h:116
Return value of the MAP/MAP/1 family (qsys_mapmap1, qsys_mapph1, qsys_phph1), carrying the same quant...
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, ...