LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_mmint2.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_PFQN_PFQN_MMINT2_H
6#define LINE_API_PFQN_PFQN_MMINT2_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * McKenna-Mitra integral form of the normalizing constant of a repairman
12 * model (one queueing station, R classes, per-class think time), in its three
13 * MATLAB quadratures.
14 *
15 * Templated port of matlab/src/api/pfqn/pfqn_mmint2.m,
16 * pfqn_mmint2_gausslegendre.m and pfqn_mmint2_gausslaguerre.m. All three
17 * evaluate
18 *
19 * G = 1/prod_r N_r! * int_0^inf u^{m-1} e^{-u} prod_r (Z_r + L_r u)^{N_r} du
20 *
21 * and differ only in the rule: adaptive Gauss-Kronrod on a truncated interval,
22 * a fixed Gauss-Legendre rule on [0, 1e6], or Gauss-Laguerre on [0, inf).
23 * The integrand is a polynomial times e^{-u}, so Gauss-Laguerre with enough
24 * nodes is exact up to rounding, which makes it the natural cross-check on the
25 * other two and on pfqn_ca.
26 *
27 * NODES. MATLAB loads a Julia-generated table (gausslegendre-data.mat,
28 * gausslaguerre-data.mat). A table cannot be carried across arithmetics -- it
29 * would pin every instantiation to the precision it was generated at -- so the
30 * rules are regenerated in T by pfqn_asympt_common.h.
31 *
32 * The Legendre form needs care. MATLAB's node count is
33 * n = max(300, min(tablesize, 2(sum N + m - 1) - 1)) and it takes the FIRST n
34 * entries of a 20000-point rule on [0, 1e6], which is not the same thing as a
35 * fresh n-point rule: the 20000-point prefix spans [0.0036, 557.8] and
36 * resolves the e^{-u} factor, whereas a genuine 300-point rule on [0, 1e6] has
37 * its first node at u = 13.7 and misses the mass entirely (it returns
38 * log G = -3.44 where the answer is 1.63). The port therefore generates the
39 * 20000-point rule and takes the same prefix, computing only the prefix since
40 * each Newton iteration is independent of the other nodes. `nodecap` is that
41 * table length and defaults to 20000, the length of MATLAB's
42 * gausslegendre-nodes.txt.
43 *
44 * TRUNCATION of the adaptive form. MATLAB integrates over
45 * [0, -log(1 - (1 - 1e-12))] = [0, 27.63...], i.e. the 1 - 10^-order quantile
46 * of the unit exponential, and asks for AbsTol 1e-12. The port keeps both
47 * constants. That truncation is the dominant error for large populations,
48 * where the polynomial factor pushes mass well beyond the cutoff; the tests
49 * record where it starts to bite.
50 *
51 * ARITHMETIC. Quadrature, so all three are gated on
52 * num_traits<T>::has_transcendental.
53 */
54
55#include <algorithm>
56#include <cmath>
57#include <cstddef>
58#include <vector>
59
62#include "line/num/number.h"
63#include "line/util/error.h"
64
65namespace line {
66namespace pfqn {
67
68/** Return value of the McKenna-Mitra quadratures, mirroring [G, lG]. */
69template <class T>
71 T G;
72 T lG;
73};
74
75/**
76 * Adaptive form (MATLAB pfqn_mmint2): Gauss-Kronrod on [0, 27.63] with
77 * absolute tolerance 1e-12.
78 *
79 * @param L (R) demand at the station, @param N (R) population,
80 * @param Z (R) think times
81 */
82template <class T>
83MmintResult<T> pfqn_mmint2(const std::vector<T>& L, const std::vector<T>& N,
84 const std::vector<T>& Z) {
86 "pfqn_mmint2 requires transcendental arithmetic (quadrature of e^{-u} p(u))");
87 using std::exp;
88 using std::log;
89 const std::size_t R = L.size();
90 if (N.size() != R || Z.size() != R)
91 throw InputError("pfqn_mmint2: L, N and Z must have the same length");
92 const T zero = num_traits<T>::from_int(0);
93
94 // The reference restricts the product to the classes with N_r > 0.
95 std::vector<std::size_t> nz;
96 for (std::size_t r = 0; r < R; ++r)
97 if (N[r] != zero) nz.push_back(r);
98
99 const auto f = [&](const T& u) {
100 using std::exp;
101 T p = exp(T(-u));
102 for (std::size_t k = 0; k < nz.size(); ++k) {
103 const std::size_t r = nz[k];
104 const double nd = num_traits<T>::to_double(N[r]);
105 p *= num_pow_int(T(Z[r] + L[r] * u), static_cast<unsigned>(nd));
106 }
107 return p;
108 };
109
110 const int order = 12;
111 const T hi = num_traits<T>::from_double(-std::log(1.0 - (1.0 - std::pow(10.0, -order))));
112 const T atol = num_traits<T>::from_double(std::pow(10.0, -order));
113 const T I = qsys::detail::num_integral<T>(f, zero, hi, num_traits<T>::from_double(1e-12), atol);
114 if (I <= zero) throw NumericError("pfqn_mmint2: non-positive integral");
115
116 MmintResult<T> res;
117 T lG = log(I);
118 for (std::size_t r = 0; r < R; ++r) lG -= detail::num_factln<T>(N[r]);
119 res.lG = lG;
120 res.G = exp(lG);
121 return res;
122}
123
124/**
125 * Gauss-Legendre form on [0, 1e6] (MATLAB pfqn_mmint2_gausslegendre).
126 *
127 * @param m station multiplicity, contributing the u^{m-1} factor
128 * @param nodecap size of the underlying tabulated rule, i.e. MATLAB's table
129 * length; the routine uses its first n nodes
130 * @param L (M) service demands
131 * @param N (1) population, single class
132 * @param Z (1) think time
133 */
134template <class T>
135MmintResult<T> pfqn_mmint2_gausslegendre(const std::vector<T>& L, const std::vector<T>& N,
136 const std::vector<T>& Z, int m, std::size_t nodecap) {
138 "pfqn_mmint2_gausslegendre requires transcendental arithmetic (quadrature)");
139 using std::exp;
140 using std::log;
141 const std::size_t R = L.size();
142 if (N.size() != R || Z.size() != R)
143 throw InputError("pfqn_mmint2_gausslegendre: L, N and Z must have the same length");
144 if (m < 1) throw InputError("pfqn_mmint2_gausslegendre: multiplicity must be at least one");
145 const T zero = num_traits<T>::from_int(0);
146 T Ntot = zero;
147 for (const T& v : N) Ntot += v;
148
149 const long want = 2 * (static_cast<long>(num_traits<T>::to_double(Ntot)) + m - 1) - 1;
150 std::size_t n = 300;
151 const std::size_t capped = std::min<std::size_t>(nodecap, want > 0 ? static_cast<std::size_t>(want) : 1);
152 if (capped > n) n = capped;
153 if (n > nodecap) n = nodecap;
154
155 // 20000-point table prefix rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
156 std::vector<T> x, w;
157 detail::gauss_legendre<T>(nodecap, zero, num_traits<T>::from_double(1e6), x, w, n);
158
159 std::vector<T> g(n);
160 for (std::size_t i = 0; i < n; ++i) {
161 T y = zero;
162 for (std::size_t r = 0; r < R; ++r) {
163 if (N[r] == zero) continue;
164 y += N[r] * log(T(Z[r] + L[r] * x[i]));
165 }
166 g[i] = T(log(w[i]) - x[i] + y);
167 if (m > 1) g[i] += num_traits<T>::from_int(m - 1) * log(x[i]);
168 }
169 T coeff = zero;
170 for (std::size_t r = 0; r < R; ++r) coeff -= detail::num_factln<T>(N[r]);
171 coeff -= detail::num_factln<T>(num_traits<T>::from_int(m - 1));
172
173 MmintResult<T> res;
174 // stable logsumexp rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
175 res.lG = T(detail::logsumexp(g) + coeff);
176 res.G = exp(res.lG);
177 return res;
178}
179
180template <class T>
181MmintResult<T> pfqn_mmint2_gausslegendre(const std::vector<T>& L, const std::vector<T>& N,
182 const std::vector<T>& Z) {
183 // 20000 is the length of MATLAB's gausslegendre-nodes.txt.
184 return pfqn_mmint2_gausslegendre(L, N, Z, 1, 20000);
185}
186
187/**
188 * Gauss-Laguerre form (MATLAB pfqn_mmint2_gausslaguerre).
189 *
190 * @param npts node count; MATLAB uses the length of its tabulated rule
191 * @param L (M) service demands
192 * @param N (1) population, single class
193 * @param Z (1) think time
194 * @param m multiplicity of the queueing station
195 */
196template <class T>
197MmintResult<T> pfqn_mmint2_gausslaguerre(const std::vector<T>& L, const std::vector<T>& N,
198 const std::vector<T>& Z, int m, std::size_t npts) {
200 "pfqn_mmint2_gausslaguerre requires transcendental arithmetic (quadrature)");
201 using std::exp;
202 using std::log;
203 const std::size_t R = L.size();
204 if (N.size() != R || Z.size() != R)
205 throw InputError("pfqn_mmint2_gausslaguerre: L, N and Z must have the same length");
206 if (m < 1) throw InputError("pfqn_mmint2_gausslaguerre: multiplicity must be at least one");
207 if (npts < 2) throw InputError("pfqn_mmint2_gausslaguerre: at least two nodes are required");
208 const T zero = num_traits<T>::from_int(0);
209
210 std::vector<T> x, w;
211 detail::gauss_laguerre<T>(npts, x, w);
212 std::vector<T> g(npts);
213 for (std::size_t i = 0; i < npts; ++i) {
214 T F = zero;
215 if (m > 1) F += num_traits<T>::from_int(m - 1) * log(x[i]);
216 for (std::size_t r = 0; r < R; ++r) {
217 if (N[r] == zero) continue;
218 F += N[r] * log(T(Z[r] + L[r] * x[i]));
219 }
220 g[i] = T(log(w[i]) + F);
221 }
222 T coeff = zero;
223 for (std::size_t r = 0; r < R; ++r) coeff -= detail::num_factln<T>(N[r]);
224 coeff -= detail::num_factln<T>(num_traits<T>::from_int(m - 1));
225
226 MmintResult<T> res;
227 res.lG = T(detail::logsumexp(g) + coeff);
228 res.G = exp(res.lG);
229 return res;
230}
231
232template <class T>
233MmintResult<T> pfqn_mmint2_gausslaguerre(const std::vector<T>& L, const std::vector<T>& N,
234 const std::vector<T>& Z) {
235 return pfqn_mmint2_gausslaguerre(L, N, Z, 1, 90);
236}
237
238} // namespace pfqn
239} // namespace line
240
241#endif // LINE_API_PFQN_PFQN_MMINT2_H
InputError(const std::string &what)
Definition error.h:39
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
MmintResult< T > pfqn_mmint2_gausslegendre(const std::vector< T > &L, const std::vector< T > &N, const std::vector< T > &Z, int m, std::size_t nodecap)
Gauss-Legendre form on [0, 1e6] (MATLAB pfqn_mmint2_gausslegendre).
MmintResult< T > pfqn_mmint2(const std::vector< T > &L, const std::vector< T > &N, const std::vector< T > &Z)
Adaptive form (MATLAB pfqn_mmint2): Gauss-Kronrod on [0, 27.63] with absolute tolerance 1e-12.
Definition pfqn_mmint2.h:83
MmintResult< T > pfqn_mmint2_gausslaguerre(const std::vector< T > &L, const std::vector< T > &N, const std::vector< T > &Z, int m, std::size_t npts)
Gauss-Laguerre form (MATLAB pfqn_mmint2_gausslaguerre).
T num_pow_int(const T &base, unsigned e)
Integer power, valid in any field (no transcendental requirement).
Definition number.h:192
Number-type abstraction for the templated API port.
Shared scalar machinery for the integration / asymptotic members of the pfqn family (pfqn_le,...
Adaptive quadrature for the qsys functions whose MATLAB originals call integral(),...
Return value of the McKenna-Mitra quadratures, mirroring [G, lG].
Definition pfqn_mmint2.h:70