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 * REFERENCE DEFECT, reproduced as a rejection rather than as a wrong number.
25 * pfqn_grnmol.m sets S = ceil(sum(N)-1)/2, which MATLAB parses as
26 * (ceil(sum(N)-1))/2 and NOT as ceil((sum(N)-1)/2), the value pfqn_cub.m uses.
27 * For an EVEN total population S is therefore a half-integer, c_i is a
28 * half-integer, and the weight calls factorial(i + c_i) on a non-integer,
29 * which MATLAB rejects outright ("N must be a matrix of non-negative
30 * integers"). pfqn_grnmol is thus callable only for an ODD total population,
31 * and the port throws InputError for an even one instead of silently choosing
32 * one of the two readings of the expression. Use pfqn_cub for even
33 * populations; it computes the same integral with the correct degree.
34 *
35 * ARITHMETIC. Every ingredient -- the barycentric points, the binomial-like
36 * weights, the integer powers -- is rational, and the factorial prefactor is
37 * formed as an exact factorial rather than through gammaln. The routine is
38 * therefore EXACT in rational arithmetic and is deliberately left ungated:
39 * at the reference degree it returns the exact normalizing constant, and
40 * checking that against pfqn_ca is the sharpest test the rule admits.
41 */
42
43#include <cstddef>
44#include <vector>
45
46#include "line/num/number.h"
47#include "line/util/error.h"
48#include "line/util/matrix.h"
49
50namespace line {
51namespace pfqn {
52
53namespace detail {
54
55/**
56 * Advance b to the next M-vector of non-negative integers with a fixed sum,
57 * in the order matlab/util/sprod.m produces through multichoose. Returns
58 * false once the enumeration is exhausted.
59 */
60inline bool next_composition(std::vector<long>& b, long total) {
61 const std::size_t M = b.size();
62 if (M < 2) return false;
63 // Find the rightmost position that can be decremented with something to
64 // its right to absorb the unit.
65 for (std::size_t i = M - 1; i-- > 0;) {
66 if (b[i] > 0) {
67 b[i] -= 1;
68 long rest = total;
69 for (std::size_t k = 0; k <= i; ++k) rest -= b[k];
70 for (std::size_t k = i + 1; k < M; ++k) b[k] = 0;
71 b[i + 1] = rest;
72 return true;
73 }
74 }
75 return false;
76}
77
78} // namespace detail
79
80/**
81 * @brief Normalizing constant by the closed-form Grundmann-Moeller rule.
82 *
83 * @param L (M x R) demands, @param N (R) population with an ODD total
84 * @return the normalizing constant
85 */
86template <class T>
87T pfqn_grnmol(const Matrix<T>& L, const std::vector<int>& N) {
88 const std::size_t M = L.rows(), R = L.cols();
89 if (N.size() != R) throw InputError("pfqn_grnmol: L and N disagree on the class count");
90 if (M == 0) throw InputError("pfqn_grnmol: empty demand matrix");
91 long Nt = 0;
92 for (int v : N) {
93 if (v < 0) throw InputError("pfqn_grnmol: negative population");
94 Nt += v;
95 }
96 if (Nt == 0) return num_traits<T>::from_int(1);
97 if (Nt % 2 == 0)
98 throw InputError(
99 "pfqn_grnmol: the reference is only callable for an odd total population "
100 "(S = ceil(sum(N)-1)/2 is a half-integer otherwise); use pfqn_cub");
101 const long S = (Nt - 1) / 2;
102 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
103
104 T G = zero;
105 for (long i = 0; i <= S; ++i) {
106 const long ci = 2 * (S - i) + static_cast<long>(M);
107 T w = T(one / num_pow_int(num_traits<T>::from_int(2), static_cast<unsigned>(2 * S)));
108 if (i % 2 == 1) w = T(-w);
109 w *= num_pow_int(num_traits<T>::from_int(ci), static_cast<unsigned>(2 * S + 1));
110 w /= num_factorial<T>(static_cast<unsigned>(i));
111 w /= num_factorial<T>(static_cast<unsigned>(i + ci));
112
113 const long tot = S - i;
114 std::vector<long> b(M, 0);
115 b[0] = tot;
116 T H = zero;
117 while (true) {
118 T prod = one;
119 for (std::size_t r = 0; r < R; ++r) {
120 if (N[r] == 0) continue;
121 T uL = zero;
122 for (std::size_t m = 0; m < M; ++m)
123 uL += T(num_traits<T>::from_int(2 * b[m] + 1) / num_traits<T>::from_int(ci)) *
124 L(m, r);
125 prod *= num_pow_int(uL, static_cast<unsigned>(N[r]));
126 }
127 H += prod;
128 if (!detail::next_composition(b, tot)) break;
129 }
130 G += w * H;
131 }
132
133 G *= num_factorial<T>(static_cast<unsigned>(Nt + static_cast<long>(M) - 1));
134 for (std::size_t r = 0; r < R; ++r) G /= num_factorial<T>(static_cast<unsigned>(N[r]));
135 return G;
136}
137
138} // namespace pfqn
139} // namespace line
140
141#endif // LINE_API_PFQN_PFQN_GRNMOL_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.
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:87
T num_factorial(unsigned n)
Factorial as a value of T.
Definition number.h:184
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.