LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_cub.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_CUB_H
6#define LINE_API_PFQN_PFQN_CUB_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Normalizing constant by Grundmann-Moeller cubature over the simplex.
12 *
13 * Templated port of matlab/src/api/pfqn/pfqn_cub.m together with its two local
14 * functions simplexquad and grnmol (Grundmann and Moller, SIAM J. Numer. Anal.
15 * 15 (1978) 282-290). With Z = 0 the constant is a single integral of
16 * prod_r (u' L(:,r))^{N_r} over the (M-1)-simplex; the degree-(2s+1) rule is
17 * EXACT once s >= ceil((sum N - 1)/2), because the integrand is then a
18 * polynomial of degree sum(N) <= 2s+1. With Z > 0 an outer integral over the
19 * McKenna-Mitra scale variable v is added on a uniform grid, which is where
20 * the method stops being exact.
21 *
22 * The `grnmol` rule is exported, because it is the only working
23 * Grundmann-Moeller implementation in the reference tree; see the note on
24 * pfqn_grnmol in the report accompanying this port.
25 *
26 * ARITHMETIC. The rule itself is a weighted sum of integrand values at
27 * rational barycentric points, so at Z = 0 and full degree the whole
28 * computation would be exact in a field -- were it not for the
29 * exp(gammaln(1+sum N+M-1) - sum gammaln(1+N)) prefactor, which MATLAB forms in
30 * logarithms. Since that prefactor is a ratio of factorials it could be formed
31 * exactly, but the reference does not, and reproducing the reference is the
32 * contract; the routine is therefore gated on
33 * num_traits<T>::has_transcendental, and the exactness claim above is about
34 * the cubature, not about the returned scalar.
35 */
36
37#include <cmath>
38#include <cstddef>
39#include <functional>
40#include <vector>
41
43#include "line/num/number.h"
44#include "line/util/error.h"
45#include "line/util/matrix.h"
46
47namespace line {
48namespace pfqn {
49
50/** Return value of pfqn_cub, mirroring [Gn, lGn]. */
51template <class T>
52struct CubResult {
53 T G;
54 T lG;
55};
56
57/**
58 * Grundmann-Moeller rule of degrees 1, 3, ..., 2s+1 over the n-simplex with
59 * vertices the columns of the identity (MATLAB's grnmol on V = eye(n,n+1)).
60 *
61 * @param f integrand, evaluated on the n free barycentric coordinates
62 * @param n simplex dimension
63 * @param s maximum rule order
64 * @param tol relative stopping tolerance between consecutive degrees
65 * @return the successive estimates, the last of which is the answer
66 */
67template <class T>
68std::vector<T> grnmol(const std::function<T(const std::vector<T>&)>& f, std::size_t n, int s,
69 const T& tol) {
70 // exactness-in-any-field rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
71 if (n == 0) throw InputError("grnmol: zero-dimensional simplex");
72 const T zero = num_traits<T>::from_int(0);
73 std::vector<T> Q, Qv;
74 const T Vol = T(num_traits<T>::from_int(1) / num_factorial<T>(static_cast<unsigned>(n)));
75 int d = 0;
76 while (true) {
77 const long m = static_cast<long>(n) + 2 * d + 1;
78 std::vector<long> al(n, 1);
79 long alz = 2 * d + 1;
80 T Qs = zero;
81 while (true) {
82 // barycentric evaluation point rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
83 std::vector<T> x(n);
85 for (std::size_t j = 1; j < n; ++j)
86 x[j] = T(num_traits<T>::from_int(al[j - 1]) / num_traits<T>::from_int(m));
87 Qs += f(x);
88 for (std::size_t j = 0; j < n; ++j) {
89 alz -= 2;
90 if (alz > 0) {
91 al[j] += 2;
92 break;
93 }
94 alz += al[j] + 1;
95 al[j] = 1;
96 }
97 if (alz == 2 * d + 1) break;
98 }
99 ++d;
100 Qv.push_back(T(Vol * Qs));
101 T q = zero;
103 for (long k = static_cast<long>(n) + 1; k <= m; ++k) p /= num_traits<T>::from_int(2 * k);
104 for (int i = 1; i <= d; ++i) {
105 q += num_pow_int(num_traits<T>::from_int(m + 2 - 2 * i), static_cast<unsigned>(2 * d - 1)) *
106 p * Qv[static_cast<std::size_t>(d - i)];
107 p = T(-p * num_traits<T>::from_int(m + 1 - i) / num_traits<T>::from_int(i));
108 }
109 Q.push_back(q);
110 // MATLAB's test is abs(Q(d)-Q(d-1)) < tol*Q(d-1), with Q(d-1) SIGNED,
111 // so a negative previous estimate never stops the loop. Kept as is.
112 if (d > s || (d > 1 && num_abs(T(Q[static_cast<std::size_t>(d) - 1] -
113 Q[static_cast<std::size_t>(d) - 2])) <
114 T(tol * Q[static_cast<std::size_t>(d) - 2])))
115 break;
116 }
117 return Q;
118}
119
120/**
121 * @brief Normalizing constant by Grundmann-Moeller cubature over the simplex.
122 *
123 * @param L (M x R) demands
124 * @param N (R) population
125 * @param Z (R) think times, empty or all zero for the exact branch
126 * @param order rule degree; the default ceil((sum N - 1)/2) makes the Z = 0
127 * branch exact
128 * @param atol absolute tolerance, also the zero test on sum(Z)
129 */
130template <class T>
131CubResult<T> pfqn_cub(const Matrix<T>& L, const std::vector<int>& N, const std::vector<T>& Z,
132 int order, const T& atol) {
134 "pfqn_cub requires transcendental arithmetic (the factorial prefactor is formed "
135 "in logarithms, and the Z > 0 branch is a quadrature)");
136 using std::exp;
137 using std::log;
138 const std::size_t M = L.rows(), R = L.cols();
139 if (N.size() != R) throw InputError("pfqn_cub: L and N disagree on the class count");
140 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
141 CubResult<T> res;
142
143 long Nt = 0;
144 for (int v : N) {
145 if (v < 0) throw InputError("pfqn_cub: negative population");
146 Nt += v;
147 }
148 if (M == 0 || N.empty() || Nt == 0) {
149 res.G = one;
150 res.lG = zero;
151 return res;
152 }
153 if (M == 1) throw InputError("pfqn_cub: the simplex is degenerate for a single station");
154
155 T Zsum = zero;
156 for (const T& v : Z) Zsum += v;
157
158 if (Z.empty() || Zsum < atol) {
159 // Integrand prod_r (u' L(:,r))^{N_r} on the (M-1)-simplex.
160 const std::function<T(const std::vector<T>&)> f = [&](const std::vector<T>& x) {
161 T last = one;
162 for (const T& v : x) last -= v;
163 T prod = one;
164 for (std::size_t r = 0; r < R; ++r) {
165 if (N[r] == 0) continue;
166 T uL = zero;
167 for (std::size_t i = 0; i + 1 < M; ++i) uL += x[i] * L(i, r);
168 uL += last * L(M - 1, r);
169 prod *= num_pow_int(uL, static_cast<unsigned>(N[r]));
170 }
171 return prod;
172 };
173 const std::vector<T> Q = grnmol<T>(f, M - 1, order, atol);
174 T coeff = detail::num_factln<T>(num_traits<T>::from_int(Nt + static_cast<long>(M) - 1));
175 for (std::size_t r = 0; r < R; ++r)
176 coeff -= detail::num_factln<T>(num_traits<T>::from_int(N[r]));
177 res.G = T(Q.back() * exp(coeff));
178 res.lG = log(res.G);
179 return res;
180 }
181
182 // Z > 0: outer McKenna-Mitra integral on a uniform grid of 1e4 steps.
183 const long steps = 10000;
184 const T vmax = num_traits<T>::from_int(10 * Nt);
185 const T dv = T(vmax / num_traits<T>::from_int(steps));
186 T Gn = zero;
187 for (long k = 0; k <= steps; ++k) {
188 const T v = T(dv * num_traits<T>::from_int(k));
189 Matrix<T> Lv(M, R);
190 for (std::size_t i = 0; i < M; ++i)
191 for (std::size_t r = 0; r < R; ++r) Lv(i, r) = T(L(i, r) * v + Z[r]);
192 const std::function<T(const std::vector<T>&)> f = [&](const std::vector<T>& x) {
193 T last = one;
194 for (const T& val : x) last -= val;
195 T s = zero;
196 for (std::size_t r = 0; r < R; ++r) {
197 if (N[r] == 0) continue;
198 T uL = zero;
199 for (std::size_t i = 0; i + 1 < M; ++i) uL += x[i] * Lv(i, r);
200 uL += last * Lv(M - 1, r);
201 s += num_traits<T>::from_int(N[r]) * log(uL);
202 }
203 return T(exp(s));
204 };
205 const std::vector<T> Q = grnmol<T>(f, M - 1, order, atol);
206 const T dG =
207 T(exp(T(-v)) * num_pow_int(v, static_cast<unsigned>(M - 1)) * Q.back() * dv);
208 Gn += dG;
209 if (k > 0 && Gn > zero && T(dG / Gn) < atol) break;
210 }
211 T coeff = zero;
212 for (std::size_t r = 0; r < R; ++r) coeff -= detail::num_factln<T>(num_traits<T>::from_int(N[r]));
213 res.G = T(Gn * exp(coeff));
214 res.lG = log(res.G);
215 return res;
216}
217
218template <class T>
219CubResult<T> pfqn_cub(const Matrix<T>& L, const std::vector<int>& N, const std::vector<T>& Z) {
220 long Nt = 0;
221 for (int v : N) Nt += v;
222 const int order = static_cast<int>((Nt - 1 + 1) / 2); // ceil((Nt-1)/2)
223 return pfqn_cub(L, N, Z, order, num_traits<T>::from_double(1e-8));
224}
225
226} // namespace pfqn
227} // namespace line
228
229#endif // LINE_API_PFQN_PFQN_CUB_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
The exception types the port throws.
Dense matrix and non-owning view.
std::vector< T > grnmol(const std::function< T(const std::vector< T > &)> &f, std::size_t n, int s, const T &tol)
Grundmann-Moeller rule of degrees 1, 3, ..., 2s+1 over the n-simplex with vertices the columns of the...
Definition pfqn_cub.h:68
CubResult< T > pfqn_cub(const Matrix< T > &L, const std::vector< int > &N, const std::vector< T > &Z, int order, const T &atol)
Normalizing constant by Grundmann-Moeller cubature over the simplex.
Definition pfqn_cub.h:131
T num_factorial(unsigned n)
Factorial as a value of T.
Definition number.h:184
T num_abs(const T &v)
Definition number.h:172
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,...
Return value of pfqn_cub, mirroring [Gn, lGn].
Definition pfqn_cub.h:52