LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_grnmol.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_GRNMOL_H
6#define LINE_API_PFQN_PFQN_GRNMOL_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Normalizing constant by the closed-form Grundmann-Moeller rule.
12 *
13 * Templated port of matlab/src/api/pfqn/pfqn_grnmol.m, which applies the rule
14 * directly rather than through the successive-degree recursion of pfqn_cub:
15 *
16 * G = [ (sum N + M - 1)! / prod_r N_r! ] * sum_{i=0}^{S} w_i H_i
17 * w_i = 2^{-2S} (-1)^i c_i^{2S+1} / (i! (i + c_i)!), c_i = 2(S-i) + M
18 * H_i = sum_{|b| = S-i} prod_r ( ((2b+1)/c_i)' L(:,r) )^{N_r}
19 *
20 * with b ranging over the M-vectors of non-negative integers summing to S-i,
21 * which is what the reference's call to matlab/util/sprod.m enumerates. The
22 * point (2b+1)/c_i is barycentric by construction, since sum_m (2b_m+1) = c_i.
23 *
24 * DEGREE. pfqn_grnmol.m sets S = ceil((sum(N)-1)/2), the value pfqn_cub.m
25 * uses, so the rule is exact for every total population, odd or even. (An
26 * earlier reference wrote ceil(sum(N)-1)/2, a half-integer S for even sum(N)
27 * that made the function uncallable there; MATLAB has been corrected and this
28 * port follows it, accepting even populations.)
29 *
30 * ARITHMETIC. Every ingredient -- the barycentric points, the binomial-like
31 * weights, the integer powers -- is rational, and the factorial prefactor is
32 * formed as an exact factorial rather than through gammaln. The routine is
33 * therefore EXACT in rational arithmetic and is deliberately left ungated:
34 * at the reference degree it returns the exact normalizing constant, and
35 * checking that against pfqn_ca is the sharpest test the rule admits.
36 */
37
38#include <cstddef>
39#include <vector>
40
41#include "line/num/number.h"
42#include "line/util/error.h"
43#include "line/util/matrix.h"
44
45namespace line {
46namespace pfqn {
47
48namespace detail {
49
50/**
51 * Advance b to the next M-vector of non-negative integers with a fixed sum,
52 * in the order matlab/util/sprod.m produces through multichoose. Returns
53 * false once the enumeration is exhausted.
54 */
55inline bool next_composition(std::vector<long>& b, long total) {
56 const std::size_t M = b.size();
57 if (M < 2) return false;
58 // Find the rightmost position that can be decremented with something to
59 // its right to absorb the unit.
60 for (std::size_t i = M - 1; i-- > 0;) {
61 if (b[i] > 0) {
62 b[i] -= 1;
63 long rest = total;
64 for (std::size_t k = 0; k <= i; ++k) rest -= b[k];
65 for (std::size_t k = i + 1; k < M; ++k) b[k] = 0;
66 b[i + 1] = rest;
67 return true;
68 }
69 }
70 return false;
71}
72
73} // namespace detail
74
75/**
76 * @brief Normalizing constant by the closed-form Grundmann-Moeller rule.
77 *
78 * @param L (M x R) demands, @param N (R) population
79 * @return the normalizing constant
80 */
81template <class T>
82T pfqn_grnmol(const Matrix<T>& L, const std::vector<int>& N) {
83 const std::size_t M = L.rows(), R = L.cols();
84 if (N.size() != R) throw InputError("pfqn_grnmol: L and N disagree on the class count");
85 if (M == 0) throw InputError("pfqn_grnmol: empty demand matrix");
86 long Nt = 0;
87 for (int v : N) {
88 if (v < 0) throw InputError("pfqn_grnmol: negative population");
89 Nt += v;
90 }
91 if (Nt == 0) return num_traits<T>::from_int(1);
92 // S = ceil((sum(N)-1)/2), i.e. floor(Nt/2): (Nt-1)/2 for odd Nt, Nt/2 for even Nt
93 const long S = Nt / 2;
94 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
95
96 T G = zero;
97 for (long i = 0; i <= S; ++i) {
98 const long ci = 2 * (S - i) + static_cast<long>(M);
99 T w = T(one / num_pow_int(num_traits<T>::from_int(2), static_cast<unsigned>(2 * S)));
100 if (i % 2 == 1) w = T(-w);
101 w *= num_pow_int(num_traits<T>::from_int(ci), static_cast<unsigned>(2 * S + 1));
102 w /= num_factorial<T>(static_cast<unsigned>(i));
103 w /= num_factorial<T>(static_cast<unsigned>(i + ci));
104
105 const long tot = S - i;
106 std::vector<long> b(M, 0);
107 b[0] = tot;
108 T H = zero;
109 while (true) {
110 T prod = one;
111 for (std::size_t r = 0; r < R; ++r) {
112 if (N[r] == 0) continue;
113 T uL = zero;
114 for (std::size_t m = 0; m < M; ++m)
115 uL += T(num_traits<T>::from_int(2 * b[m] + 1) / num_traits<T>::from_int(ci)) *
116 L(m, r);
117 prod *= num_pow_int(uL, static_cast<unsigned>(N[r]));
118 }
119 H += prod;
120 if (!detail::next_composition(b, tot)) break;
121 }
122 G += w * H;
123 }
124
125 G *= num_factorial<T>(static_cast<unsigned>(Nt + static_cast<long>(M) - 1));
126 for (std::size_t r = 0; r < R; ++r) G /= num_factorial<T>(static_cast<unsigned>(N[r]));
127 return G;
128}
129
130} // namespace pfqn
131} // namespace line
132
133#endif // LINE_API_PFQN_PFQN_GRNMOL_H
InputError(const std::string &what)
Definition error.h:105
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.
T pfqn_grnmol(const Matrix< T > &L, const std::vector< int > &N)
Normalizing constant by the closed-form Grundmann-Moeller rule.
Definition pfqn_grnmol.h:82
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
T num_factorial(unsigned n)
Factorial as a value of T.
Definition number.h:196
T num_pow_int(const T &base, unsigned e)
Integer power, valid in any field (no transcendental requirement).
Definition number.h:204
Number-type abstraction for the templated API port.