LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_gerasimov.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_GERASIMOV_H
6#define LINE_API_PFQN_GERASIMOV_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Gerasimov's residue (closed-form) normalizing constant, generalized to R classes.
12 *
13 * Templated port of matlab/src/api/pfqn/pfqn_gerasimov.m and
14 * jar/src/main/java/jline/api/pfqn/nc/Pfqn_gerasimov.java.
15 *
16 * A. I. Gerasimov, "On Normalizing Constants in Multiclass Queueing Networks",
17 * Operations Research 43(4):704-711, 1995, evaluates
18 *
19 * G(N_1,...,N_R) = (2 pi i)^-R int_G1 ... int_GR
20 * prod_s z_s^{N_s-1} prod_i (1 - sum_s x_is/z_s)^-1
21 *
22 * by residues, and gives the resulting CLOSED FORM only for R = 1 (Thm 1-2) and
23 * R = 2 (Thm 3 for simple poles, Thm 4 for multiple ones), stating that "for
24 * three or more classes of customers, the normalizing constants can be found by
25 * numerical methods". This header implements the residue elimination itself, so
26 * the closed form is produced for ANY R; at R = 2 it reproduces Thm 3/4 term by
27 * term.
28 *
29 * Written as a coefficient of the u_s = 1/z_s series,
30 *
31 * G(N) = [prod_s u_s^{N_s}] exp(sum_s Z_s u_s) prod_i (1 - sum_s x_is u_s)^-1,
32 *
33 * every factor is AFFINE in u, so singling out u_r gives f = A - B u_r with A
34 * affine in the surviving variables. Partial fractions in u_r,
35 *
36 * [u_r^n] prod_j (A_j - B_j u_r)^-m_j
37 * = sum_j sum_{k=0}^{m_j-1} (-B_j)^-k C(n+m_j-k-1,n) B_j^n A_j^-(n+m_j-k)
38 * [t^k] prod_{l!=j} (C_jl - B_l t)^-m_l, C_jl = (A_l B_j - B_l A_j)/B_j,
39 *
40 * map a sum of products of affine powers into another one with one variable
41 * fewer, and R-1 such steps leave a univariate coefficient extraction. At R = 2
42 * the single step returns one term per station i, with outer factor
43 * x_i2^{N_2+M-1}/prod_{k!=i}(x_i2-x_k2), a pole of order N_2+1 at x_i1 and simple
44 * poles at the paper's z_1ik = (x_k1 x_i2 - x_i1 x_k2)/(x_i2 - x_k2): exactly
45 * Thm 3, with the multiple poles of Thm 4 (his xi_i < M) handled by the same
46 * step. Tied x_i2, vanishing x_i2 and identical station rows, all outside the
47 * paper's hypotheses, are ordinary cases here.
48 *
49 * Cost. Let M be the number of stations and order the populations
50 * N_(1) <= ... <= N_(R). The first elimination turns the single input term into
51 * M, and every later one multiplies the count by C(S+M-1,M-1) + M-1, where S is
52 * the total population already eliminated: a pole of order S+1 has to be
53 * differentiated against the M-1 remaining ones. The innermost extraction then
54 * convolves M series of length N_(1). Hence R = 1 costs O(M N), Buzen's own cost;
55 * R = 2 costs O(M^2 N_(1)^2), INDEPENDENT OF N_(2); and R >= 3 costs the same
56 * times prod_{r=3}^{R} C(N_(r)+M-1, M-1). The R = 2 line is the reason to reach
57 * for this method: a population removed by residues enters only as a pole ORDER,
58 * i.e. through binomial coefficients, so it costs nothing at all. For R >= 3 the
59 * term count is polynomial in the populations of degree (M-1)(R-2) and
60 * exponential in R, which is why the paper stops at two classes and why maxterms
61 * exists.
62 *
63 * Conditioning. The class left for the innermost extraction is the one with the
64 * SMALLEST population, because that population is the degree the final,
65 * sign-indefinite series is carried to, whereas the eliminated ones cancel
66 * nothing; basing on N = 100 instead of N = 6 in one four-station model cost 39
67 * nats of lG in double.
68 *
69 * Arithmetic: RATIONAL. Every operation is a product, a quotient or a binomial
70 * coefficient, so the exact backend runs the residue expansion with NO
71 * cancellation at all. That is the reason to have this method: in double the
72 * alternating sum loses accuracy (~1e-15 typically, ~1e-9 on ill-conditioned
73 * demand matrices), and the same expansion in exact arithmetic returns G to the
74 * last digit. Two affine forms count as one pole when they are proportional,
75 * which in the exact backend means proportional exactly, and in a floating one
76 * within the relative tolerance.
77 */
78
79#include <algorithm>
80#include <cmath>
81#include <limits>
82#include <type_traits>
83#include <vector>
84
87#include "line/num/number.h"
88#include "line/util/error.h"
89#include "line/util/matrix.h"
90
91namespace line {
92namespace pfqn {
93
94namespace detail {
95
96/** Poisson weight Z^p/p!, in the log domain wherever the type carries logs. */
97template <class T>
98T geras_poisw(const T& Z, unsigned p) {
99 if (p == 0) return num_traits<T>::from_int(1);
100 if constexpr (num_traits<T>::has_transcendental) {
101 using std::exp;
102 using std::log;
103 return T(exp(num_traits<T>::from_int(static_cast<long>(p)) * log(Z) -
104 num_lgamma<T>(num_traits<T>::from_int(static_cast<long>(p) + 1))));
105 } else {
106 return T(num_pow_int(Z, p) / num_factorial<T>(p));
107 }
108}
109
110
111/** A product of powers of affine forms, carrying a scalar coefficient. */
112template <class T>
113struct GerasTerm {
114 T c;
115 std::vector<std::vector<T>> F; ///< form j is F[j][0] + sum_s F[j][s] u_s
116 std::vector<int> m; ///< multiplicity of form j
117};
118
119/** base^e for a signed exponent. */
120template <class T>
121T geras_pow(const T& base, int e) {
122 if (e >= 0) return num_pow_int(base, static_cast<unsigned>(e));
123 return num_traits<T>::from_int(1) / num_pow_int(base, static_cast<unsigned>(-e));
124}
125
126/** Binomial coefficient. Exact in the exact backend, and in double corrected to
127 * the integer it is while that integer is representable. */
128template <class T>
129T geras_binom(int n, int k) {
130 if (k < 0 || n < 0 || k > n) return num_traits<T>::from_int(0);
131 const int kk = std::min(k, n - k);
132 T b = num_traits<T>::from_int(1);
133 for (int i = 1; i <= kk; ++i) {
134 b *= num_traits<T>::from_int(n - kk + i);
135 b /= num_traits<T>::from_int(i);
136 }
137 if constexpr (std::is_same<T, double>::value) {
138 if (b < 9007199254740992.0) b = std::rint(b);
139 }
140 return b;
141}
142
143/** All k-tuples of nonnegative integers summing to n. */
144inline std::vector<std::vector<int>> geras_compositions(int n, int k) {
145 std::vector<std::vector<int>> out;
146 if (k == 0) {
147 if (n == 0) out.push_back(std::vector<int>());
148 return out;
149 }
150 if (k == 1) {
151 out.push_back(std::vector<int>(1, n));
152 return out;
153 }
154 for (int a = 0; a <= n; ++a) {
155 std::vector<std::vector<int>> sub = geras_compositions(n - a, k - 1);
156 for (std::size_t i = 0; i < sub.size(); ++i) {
157 std::vector<int> row;
158 row.reserve(k);
159 row.push_back(a);
160 row.insert(row.end(), sub[i].begin(), sub[i].end());
161 out.push_back(row);
162 }
163 }
164 return out;
165}
166
167/**
168 * Merge proportional affine forms: f_k = lambda f_j is one pole of order
169 * m_j+m_k, not two nearby simple ones, and lambda^-m_k moves into the scalar.
170 */
171template <class T>
172void geras_merge(GerasTerm<T>& t, const T& tol) {
173 const std::size_t nf = t.F.size();
174 if (nf <= 1) return;
175 const std::size_t w = t.F[0].size();
176 std::vector<bool> keep(nf, true);
177 const T zero = num_traits<T>::from_int(0);
178 for (std::size_t j = 0; j < nf; ++j) {
179 if (!keep[j]) continue;
180 std::size_t pj = 0;
181 for (std::size_t s = 1; s < w; ++s)
182 if (num_abs(t.F[j][s]) > num_abs(t.F[j][pj])) pj = s;
183 if (t.F[j][pj] == zero) continue;
184 for (std::size_t k = j + 1; k < nf; ++k) {
185 if (!keep[k]) continue;
186 const T lam = t.F[k][pj] / t.F[j][pj];
187 if (lam == zero) continue;
188 T dev = zero, scale = zero;
189 for (std::size_t s = 0; s < w; ++s) {
190 dev = std::max(dev, num_abs(T(t.F[k][s] - lam * t.F[j][s])));
191 scale = std::max(scale, std::max(num_abs(t.F[k][s]), num_abs(t.F[j][s])));
192 }
193 if (dev <= tol * scale) {
194 t.c *= geras_pow(lam, -t.m[k]);
195 t.m[j] += t.m[k];
196 keep[k] = false;
197 }
198 }
199 }
200 std::vector<std::vector<T>> Fn;
201 std::vector<int> mn;
202 for (std::size_t j = 0; j < nf; ++j) {
203 if (keep[j]) {
204 Fn.push_back(t.F[j]);
205 mn.push_back(t.m[j]);
206 }
207 }
208 t.F.swap(Fn);
209 t.m.swap(mn);
210}
211
212/**
213 * One residue elimination: integrate out u_r and return the surviving sum of
214 * products of affine powers, each form narrowed from r+1 to r columns.
215 */
216template <class T>
217std::vector<GerasTerm<T>> geras_step(const std::vector<GerasTerm<T>>& terms, int r, int Nr,
218 const T& Zr, const T& tol, std::size_t maxterms) {
219 const T zero = num_traits<T>::from_int(0);
220 const T one = num_traits<T>::from_int(1);
221 std::vector<GerasTerm<T>> out;
222 for (std::size_t it = 0; it < terms.size(); ++it) {
223 GerasTerm<T> t = terms[it];
224 geras_merge(t, tol);
225 const std::size_t nf = t.F.size();
226 std::vector<std::vector<T>> A(nf, std::vector<T>(static_cast<std::size_t>(r), zero));
227 std::vector<T> B(nf, zero), scale(nf, zero);
228 for (std::size_t j = 0; j < nf; ++j) {
229 for (int s = 0; s < r; ++s) A[j][static_cast<std::size_t>(s)] = t.F[j][static_cast<std::size_t>(s)];
230 B[j] = -t.F[j][static_cast<std::size_t>(r)];
231 for (int s = 0; s <= r; ++s) scale[j] = std::max(scale[j], num_abs(t.F[j][static_cast<std::size_t>(s)]));
232 }
233 T c = t.c;
234 int shift = 0;
235 std::vector<bool> drop(nf, false);
236 for (std::size_t j = 0; j < nf; ++j) {
237 bool mono = true;
238 for (int s = 0; s < r && mono; ++s)
239 if (num_abs(A[j][static_cast<std::size_t>(s)]) > tol * scale[j]) mono = false;
240 if (!mono) continue;
241 if (num_abs(B[j]) <= tol * scale[j])
242 throw InputError("pfqn_gerasimov: identically zero factor, impossible after merging proportional ones");
243 // A factor -B u_r carries no finite pole: it only shifts the exponent.
244 c *= geras_pow(T(-B[j]), -t.m[j]);
245 shift += t.m[j];
246 drop[j] = true;
247 }
248 std::vector<std::size_t> S, P; // S carries a pole in u_r, P is free of u_r
249 for (std::size_t j = 0; j < nf; ++j) {
250 if (drop[j]) continue;
251 if (B[j] != zero) S.push_back(j); else P.push_back(j);
252 }
253 const int Ntot = Nr + shift;
254 const int pmax = (Zr > zero) ? Ntot : 0;
255 for (int p = 0; p <= pmax; ++p) {
256 T cz = c;
257 // Poisson weight Z_r^p/p!. In double the naive ratio overflows for
258 // p >~ 171, reachable when the eliminated class carries think time;
259 // an exact T cannot overflow and must keep the exact quotient.
260 if (p > 0) cz = c * geras_poisw<T>(Zr, static_cast<unsigned>(p));
261 const int Neff = Ntot - p;
262 if (S.empty()) {
263 if (Neff == 0) {
264 GerasTerm<T> nt;
265 nt.c = cz;
266 for (std::size_t q = 0; q < P.size(); ++q) {
267 nt.F.push_back(A[P[q]]);
268 nt.m.push_back(t.m[P[q]]);
269 }
270 out.push_back(nt);
271 }
272 continue;
273 }
274 for (std::size_t jj = 0; jj < S.size(); ++jj) {
275 const std::size_t j = S[jj];
276 std::vector<std::size_t> oth;
277 for (std::size_t q = 0; q < S.size(); ++q)
278 if (q != jj) oth.push_back(S[q]);
279 const std::size_t no = oth.size();
280 std::vector<std::vector<T>> Cjl(no, std::vector<T>(static_cast<std::size_t>(r), zero));
281 for (std::size_t l = 0; l < no; ++l) {
282 const std::size_t k = oth[l];
283 for (int s = 0; s < r; ++s) {
284 const std::size_t ss = static_cast<std::size_t>(s);
285 Cjl[l][ss] = (A[k][ss] * B[j] - B[k] * A[j][ss]) / B[j];
286 }
287 }
288 for (int k = 0; k < t.m[j]; ++k) {
289 const std::vector<std::vector<int>> comps = geras_compositions(k, static_cast<int>(no));
290 for (std::size_t ci = 0; ci < comps.size(); ++ci) {
291 const std::vector<int>& nk = comps[ci];
292 T coef = cz * geras_pow(T(-B[j]), -k) * geras_binom<T>(Neff + t.m[j] - k - 1, Neff) *
293 geras_pow(B[j], Neff);
294 for (std::size_t l = 0; l < no; ++l)
295 coef *= geras_binom<T>(t.m[oth[l]] + nk[l] - 1, nk[l]) * geras_pow(B[oth[l]], nk[l]);
296 if (coef == zero) continue;
297 GerasTerm<T> nt;
298 nt.c = coef;
299 nt.F.push_back(A[j]);
300 nt.m.push_back(Neff + t.m[j] - k);
301 for (std::size_t l = 0; l < no; ++l) {
302 nt.F.push_back(Cjl[l]);
303 nt.m.push_back(t.m[oth[l]] + nk[l]);
304 }
305 for (std::size_t q = 0; q < P.size(); ++q) {
306 nt.F.push_back(A[P[q]]);
307 nt.m.push_back(t.m[P[q]]);
308 }
309 out.push_back(nt);
310 }
311 }
312 }
313 }
314 if (out.size() > maxterms)
315 throw InputError("pfqn_gerasimov: residue expansion exceeded maxterms; use pfqn_ca or pfqn_nc");
316 }
317 (void)one;
318 return out;
319}
320
321/** Truncated convolution of two coefficient sequences, kept to degree n. */
322template <class T>
323std::vector<T> geras_conv(const std::vector<T>& a, const std::vector<T>& b, int n) {
324 const T zero = num_traits<T>::from_int(0);
325 std::vector<T> y(static_cast<std::size_t>(n) + 1, zero);
326 for (std::size_t i = 0; i < a.size() && static_cast<int>(i) <= n; ++i) {
327 if (a[i] == zero) continue;
328 for (std::size_t j = 0; j < b.size() && static_cast<int>(i + j) <= n; ++j)
329 y[i + j] += a[i] * b[j];
330 }
331 return y;
332}
333
334/**
335 * Last class: a univariate coefficient extraction. Summing the residues here too
336 * would repeat the step above, but convolving the series of each factor returns
337 * the same number without expanding the multiple poles.
338 */
339template <class T>
340T geras_base(const std::vector<GerasTerm<T>>& terms, int N1, const T& Z1, const T& tol) {
341 const T zero = num_traits<T>::from_int(0);
342 T G = zero;
343 for (std::size_t it = 0; it < terms.size(); ++it) {
344 GerasTerm<T> t = terms[it];
345 geras_merge(t, tol);
346 const std::size_t nf = t.F.size();
347 T c = t.c;
348 int shift = 0;
349 std::vector<bool> drop(nf, false);
350 for (std::size_t j = 0; j < nf; ++j) {
351 const T Aj = t.F[j][0];
352 const T Bj = -t.F[j][1];
353 const T scale = std::max(num_abs(t.F[j][0]), num_abs(t.F[j][1]));
354 if (num_abs(Aj) > tol * scale) continue;
355 if (num_abs(Bj) <= tol * scale)
356 throw InputError("pfqn_gerasimov: identically zero factor at the innermost coefficient extraction");
357 c *= geras_pow(T(-Bj), -t.m[j]);
358 shift += t.m[j];
359 drop[j] = true;
360 }
361 const int Ntot = N1 + shift;
362 const std::size_t len = static_cast<std::size_t>(Ntot) + 1;
363 std::vector<T> s(len, zero);
364 s[0] = num_traits<T>::from_int(1);
365 if (Z1 > zero) {
366 std::vector<T> pois(len, zero);
367 for (std::size_t n = 0; n < len; ++n)
368 pois[n] = geras_poisw<T>(Z1, static_cast<unsigned>(n));
369 s = geras_conv(s, pois, Ntot);
370 }
371 for (std::size_t j = 0; j < nf; ++j) {
372 if (drop[j]) continue;
373 const T Aj = t.F[j][0];
374 const T Bj = -t.F[j][1];
375 c *= geras_pow(Aj, -t.m[j]);
376 if (Bj == zero) continue;
377 const T ratio = Bj / Aj;
378 std::vector<T> seq(len, zero);
379 for (std::size_t n = 0; n < len; ++n)
380 seq[n] = geras_binom<T>(t.m[j] + static_cast<int>(n) - 1, static_cast<int>(n)) *
381 num_pow_int(ratio, static_cast<unsigned>(n));
382 s = geras_conv(s, seq, Ntot);
383 }
384 G += c * s[static_cast<std::size_t>(Ntot)];
385 }
386 return G;
387}
388
389} // namespace detail
390
391/**
392 * Exact normalizing constant of a closed multiclass product-form network by
393 * ITERATED RESIDUES of its rational generating function, one class at a time.
394 *
395 * @param L (M x R) service demands, M queueing stations, R classes
396 * @param N (R) population per class, nonnegative
397 * @param Z (K x R) think times, summed over rows; may be empty. A delay
398 * contributes the entire factor exp(sum_s Z_s u_s), handled exactly by
399 * convolving its Poisson coefficients into each elimination.
400 * @param tol relative tolerance for declaring two affine forms proportional,
401 * hence one pole rather than two. Ignored (taken as exactly zero) in
402 * the exact backend.
403 * @param maxterms cap on the residue terms carried between eliminations.
404 * Exceeding it is an error, not a truncation: a truncated residue sum
405 * is not a bound or an approximation of G, it is a wrong number.
406 */
407template <class T>
408NcResult<T> pfqn_gerasimov(const Matrix<T>& L, const std::vector<int>& N, const Matrix<T>& Z,
409 double tol = 1e-12, std::size_t maxterms = 200000) {
410 const std::size_t R0 = N.size();
411 if (!L.empty() && L.cols() != R0)
412 throw InputError("pfqn_gerasimov: L and N disagree on the class count");
413 const T zero = num_traits<T>::from_int(0);
414 std::vector<T> Zsum(R0, zero);
415 if (!Z.empty()) {
416 if (Z.cols() != R0) throw InputError("pfqn_gerasimov: Z and N disagree on the class count");
417 for (std::size_t k = 0; k < Z.rows(); ++k)
418 for (std::size_t r = 0; r < R0; ++r) Zsum[r] += Z(k, r);
419 }
420 for (std::size_t r = 0; r < R0; ++r) {
421 if (N[r] < 0 || Zsum[r] < zero) throw InputError("pfqn_gerasimov: L, N and Z must be nonnegative");
422 }
423 for (std::size_t i = 0; i < L.rows(); ++i)
424 for (std::size_t r = 0; r < R0; ++r)
425 if (L(i, r) < zero) throw InputError("pfqn_gerasimov: L, N and Z must be nonnegative");
426
427 // A class with no jobs is eliminated by evaluating the generating function at
428 // u_r = 0, i.e. by deleting its column outright.
429 std::vector<std::size_t> cls;
430 for (std::size_t r = 0; r < R0; ++r)
431 if (N[r] > 0) cls.push_back(r);
432 if (cls.empty()) return {num_traits<T>::from_int(1), 0.0};
433 // Class order, which decides both the cost and the accuracy.
434 // - The class left for the innermost extraction sets the CONDITIONING. Its
435 // population is the degree the final series is carried to, and the poles of
436 // the reduced problem have arbitrary sign, so that series cancels; the
437 // populations eliminated by residues enter only as pole ORDERS, through
438 // binomial coefficients, and cancel nothing. Basing on N = 100 rather than
439 // on N = 6 in one 4-station model cost 39 nats of lG in double. The SMALLEST
440 // population therefore goes to the base.
441 // - Eliminating class r leaves a pole of order N_r+1 that every LATER
442 // elimination has to differentiate, so the remaining classes are eliminated
443 // smallest-first to keep the multiplicities low for as long as possible.
444 // Eliminations run from index R down to 2, so indices 2..R hold the remaining
445 // populations in DECREASING order and index 1 holds the smallest.
446 std::stable_sort(cls.begin(), cls.end(),
447 [&N](std::size_t a, std::size_t b) { return N[a] < N[b]; });
448 if (cls.size() > 2) std::reverse(cls.begin() + 1, cls.end());
449 const std::size_t R = cls.size();
450
451 // Per-class scaling. The residue coefficients carry x_ir^{N_r+M-1}, which in
452 // double overflows well before G itself does: at x = 4 and N_r = 400 the factor
453 // alone is 1e240 while G is finite. Dividing column r by c_r divides G by
454 // exactly c_r^N_r (substitute u_r -> u_r/c_r in the generating function), so
455 // the scaling is exact in every backend and is undone at the end.
456 std::vector<T> cs(R, num_traits<T>::from_int(1));
457 for (std::size_t r = 0; r < R; ++r) {
458 T c = Zsum[cls[r]];
459 for (std::size_t i = 0; i < L.rows(); ++i) c = std::max(c, L(i, cls[r]));
460 if (c > zero) cs[r] = c;
461 }
462
463 // A station with no demand at all contributes the factor 1.
464 std::vector<std::vector<T>> rows;
465 for (std::size_t i = 0; i < L.rows(); ++i) {
466 std::vector<T> row(R + 1, zero);
467 row[0] = num_traits<T>::from_int(1);
468 bool any = false;
469 for (std::size_t r = 0; r < R; ++r) {
470 const T v = L(i, cls[r]);
471 row[r + 1] = -(v / cs[r]);
472 if (v > zero) any = true;
473 }
474 if (any) rows.push_back(row);
475 }
476
477 detail::GerasTerm<T> t0;
478 t0.c = num_traits<T>::from_int(1);
479 t0.F = rows;
480 t0.m.assign(rows.size(), 1);
481 std::vector<detail::GerasTerm<T>> terms(1, t0);
482
483 const T tolT = num_traits<T>::is_exact ? zero : num_traits<T>::from_double(tol);
484 // class s (1-based) sits in column s of F = [1, -L]; eliminate R, R-1, ..., 2
485 for (std::size_t r = R; r >= 2; --r) {
486 terms = detail::geras_step(terms, static_cast<int>(r), N[cls[r - 1]],
487 T(Zsum[cls[r - 1]] / cs[r - 1]), tolT, maxterms);
488 if (terms.empty()) return {zero, -std::numeric_limits<double>::infinity()};
489 }
490 const T Gs = detail::geras_base(terms, N[cls[0]], T(Zsum[cls[0]] / cs[0]), tolT);
491 if (Gs == zero) return {zero, -std::numeric_limits<double>::infinity()};
492 T unscale = num_traits<T>::from_int(1);
493 double lGscale = 0.0;
494 for (std::size_t r = 0; r < R; ++r) {
495 unscale *= num_pow_int(cs[r], static_cast<unsigned>(N[cls[r]]));
496 lGscale += N[cls[r]] * std::log(num_traits<T>::to_double(cs[r]));
497 }
498 const double lGout = num_traits<T>::log_as_double(Gs) + lGscale;
499 if constexpr (num_traits<T>::has_transcendental) {
500 // exp(lGout), not Gs*unscale: in double the product Prod cs_r^N_r
501 // overflows on its own whenever the scaling is large, even when G is
502 // inside the range. An exact T cannot overflow and keeps the product.
503 using std::exp;
504 return {T(exp(num_traits<T>::from_double(lGout))), lGout};
505 } else {
506 return {T(Gs * unscale), lGout};
507 }
508}
509
510/** Delay-free overload. */
511template <class T>
512NcResult<T> pfqn_gerasimov(const Matrix<T>& L, const std::vector<int>& N) {
513 return pfqn_gerasimov(L, N, Matrix<T>());
514}
515
516} // namespace pfqn
517} // namespace line
518
519#endif // LINE_API_PFQN_GERASIMOV_H
InputError(const std::string &what)
Definition error.h:39
std::size_t size() const
Definition matrix.h:91
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
bool empty() const
Definition matrix.h:92
The exception types the port throws.
Dense matrix and non-owning view.
NcResult< T > pfqn_gerasimov(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, double tol=1e-12, std::size_t maxterms=200000)
Exact normalizing constant of a closed multiclass product-form network by ITERATED RESIDUES of its ra...
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,...
Convolution algorithm for the exact normalizing constant of a closed product-form network (Buzen 1973...
Return value of the normalizing-constant family, mirroring Ret.pfqnNc.
Definition pfqn_ca.h:44