LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_ca.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_CA_H
6#define LINE_API_PFQN_CA_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Convolution algorithm for the exact normalizing constant of a closed
12 * product-form network (Buzen 1973, Reiser-Kobayashi 1975).
13 *
14 * Templated port of matlab/src/api/pfqn/pfqn_ca.m and
15 * jar/src/main/java/jline/api/pfqn/nc/Pfqn_ca.java, cross-checked against
16 * mp_pfqn's ca/convolution_multi_exact.c for the exact path.
17 *
18 * G_m(n) = G_{m-1}(n) + sum_r L(m,r) G_m(n - e_r), with G_0(n) the delay
19 * balance function prod_r Z_r^{n_r}/n_r!.
20 *
21 * Scaling: in IEEE double the recursion overflows as soon as G(N) leaves the
22 * double range, so the Lam (1982) dynamic scaling of the MATLAB implementation
23 * is applied there. No other number type needs it: exact rationals have no
24 * exponent range at all, and the high-precision binary floats have an exponent
25 * range wide enough that G never leaves it in practice. The scaling is exact
26 * either way, since dividing every demand by a power of two divides G(N) by
27 * exactly that power raised to sum(N).
28 */
29
30#include <cmath>
31#include <type_traits>
32#include <vector>
33
34#include "line/num/number.h"
35#include "line/util/error.h"
36#include "line/util/matrix.h"
38
39namespace line {
40namespace pfqn {
41
42/** Return value of the normalizing-constant family, mirroring Ret.pfqnNc. */
43template <class T>
44struct NcResult {
45 T G; ///< normalizing constant in the requested arithmetic
46 double lG; ///< log of the constant, always a double and always finite
47};
48
49namespace detail {
50
51/** Delay balance function F_Z(n) = prod_r Z_r^{n_r} / n_r!. */
52template <class T>
53T pff_delay(const std::vector<T>& Z, const std::vector<int>& n) {
54 int total = 0;
55 for (int v : n) total += v;
56 if (total == 0) return num_traits<T>::from_int(1);
58 for (std::size_t r = 0; r < n.size(); ++r) {
59 if (n[r] == 0) continue;
60 if (Z[r] == num_traits<T>::from_int(0)) return num_traits<T>::from_int(0);
61 f *= num_pow_int(Z[r], static_cast<unsigned>(n[r])) /
62 num_factorial<T>(static_cast<unsigned>(n[r]));
63 }
64 return f;
65}
66
67/**
68 * Power-of-two scale factor centring log G near zero, from the largest state
69 * term reachable by letting each class pick its own station or the delay.
70 * Returns 0 for every arithmetic other than double, which needs no scaling.
71 */
72template <class T>
73int scale_exponent(const Matrix<T>& L, const std::vector<int>& N, const std::vector<T>& Zsum) {
74 if (!std::is_same<T, double>::value) return 0;
75 const std::size_t M = L.rows(), R = L.cols();
76 long Nt = 0;
77 for (int v : N) Nt += v;
78 // Each class independently takes whichever station -- or the delay -- gives it
79 // its largest factor. The mixed state so named has term at least the product of
80 // those factors, because a station holding several classes carries a multinomial
81 // coefficient of at least one, so this is still a LOWER bound on log G. It
82 // dominates the per-configuration maximum it replaces, which asked ONE station
83 // (or the delay) to hold every class at once and so dropped the delay entirely
84 // as soon as a single class had no think time. That collapse is what made the
85 // scaling scale UP: on L=[1e-9,1], N=[99,1], Z=[1,0] the old estimate was the
86 // all-at-the-queue -2051.6 against a true log G of -359.1, giving kscale=-30,
87 // and Z/2^-30 = 1.07e9 overflowed the delay column Z^n/n! at n=[40,0].
88 double lGest = 0.0;
89 for (std::size_t r = 0; r < R; ++r) {
90 if (N[r] <= 0) continue;
91 double best = -std::numeric_limits<double>::infinity();
92 for (std::size_t i = 0; i < M; ++i) {
93 double lir = num_traits<T>::to_double(L(i, r));
94 if (lir > 0) best = std::max(best, N[r] * std::log(lir));
95 }
96 double zr = num_traits<T>::to_double(Zsum[r]);
97 if (zr > 0) best = std::max(best, N[r] * std::log(zr) - std::lgamma(N[r] + 1.0));
98 if (!std::isfinite(best)) {
99 // no station and no delay can hold class r, so G(N) is exactly zero
100 lGest = -std::numeric_limits<double>::infinity();
101 break;
102 }
103 lGest += best;
104 }
105 if (!std::isfinite(lGest) || Nt == 0) return 0;
106 return static_cast<int>(std::lround(lGest / (static_cast<double>(Nt) * std::log(2.0))));
107}
108
109} // namespace detail
110
111/**
112 * @brief Convolution algorithm for the exact normalizing constant of a closed
113 * product-form network (Buzen 1973, Reiser-Kobayashi 1975).
114 *
115 * @param L (M x R) service demands, M queueing stations, R classes
116 * @param N (R) population per class
117 * @param Z (K x R) think times, summed over rows; may be empty
118 */
119template <class T>
120NcResult<T> pfqn_ca(const Matrix<T>& L, const std::vector<int>& N, const Matrix<T>& Z) {
121 const std::size_t M = L.rows();
122 const std::size_t R = N.size();
123 if (!L.empty() && L.cols() != R) throw InputError("pfqn_ca: L and N disagree on the class count");
124
125 // Z summed over its rows, so a per-node think-time matrix is accepted.
126 std::vector<T> Zsum(R, num_traits<T>::from_int(0));
127 if (!Z.empty()) {
128 if (Z.cols() != R) throw InputError("pfqn_ca: Z and N disagree on the class count");
129 for (std::size_t k = 0; k < Z.rows(); ++k)
130 for (std::size_t r = 0; r < R; ++r) Zsum[r] += Z(k, r);
131 }
132
133 long Nt = 0;
134 bool negative = false;
135 for (int v : N) {
136 if (v < 0) negative = true;
137 Nt += v;
138 }
139 if (negative) return {num_traits<T>::from_int(0), -std::numeric_limits<double>::infinity()};
140
141 if (M == 0) {
142 // Delay-only network: G = prod_r Z_r^{N_r}/N_r!.
143 std::vector<int> n(N);
144 T G = detail::pff_delay(Zsum, n);
145 return {G, num_traits<T>::log_as_double(G)};
146 }
147 if (Nt == 0) return {num_traits<T>::from_int(1), 0.0};
148
149 const int kscale = detail::scale_exponent(L, N, Zsum);
150 Matrix<T> Ls = L;
151 std::vector<T> Zs = Zsum;
152 if constexpr (std::is_same<T, double>::value) {
153 // exponent-only rescaling rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
154 if (kscale != 0) {
155 for (std::size_t i = 0; i < M; ++i)
156 for (std::size_t r = 0; r < R; ++r) Ls(i, r) = std::ldexp(Ls(i, r), -kscale);
157 for (std::size_t r = 0; r < R; ++r) Zs[r] = std::ldexp(Zs[r], -kscale);
158 }
159 }
160
161 const std::vector<std::size_t> prods = plane_sizes(N);
162 const std::size_t total = population_count(N);
163
164 // G is (M+1) x total, laid out row-major with the station index outermost.
165 std::vector<T> G(static_cast<std::size_t>(M + 1) * total, num_traits<T>::from_int(1));
166 std::vector<int> n(R, 0);
167 bool more = true;
168 while (more) {
169 const std::size_t idxn = pop_index(n, prods);
170 G[idxn] = detail::pff_delay(Zs, n);
171 for (std::size_t m = 1; m <= M; ++m) {
172 T acc = G[(m - 1) * total + idxn];
173 for (std::size_t r = 0; r < R; ++r)
174 if (n[r] >= 1) acc += Ls(m - 1, r) * G[m * total + (idxn - prods[r])];
175 G[m * total + idxn] = acc;
176 }
177 more = next_pop(n, N);
178 }
179
180 const T raw = G[M * total + (total - 1)];
181 const double lG =
182 num_traits<T>::log_as_double(raw) + static_cast<double>(Nt) * kscale * std::log(2.0);
183 T Gn = raw;
184 if constexpr (std::is_same<T, double>::value) {
185 // exact exponent-adjustment recovery rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
186 if (kscale != 0) Gn = std::ldexp(raw, static_cast<int>(static_cast<long>(Nt) * kscale));
187 }
188 return {Gn, lG};
189}
190
191/** Overload without think times. */
192template <class T>
193NcResult<T> pfqn_ca(const Matrix<T>& L, const std::vector<int>& N) {
194 return pfqn_ca(L, N, Matrix<T>());
195}
196
197} // namespace pfqn
198} // namespace line
199
200#endif // LINE_API_PFQN_CA_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
bool empty() const
Definition matrix.h:92
The exception types the port throws.
Dense matrix and non-owning view.
NcResult< T > pfqn_ca(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z)
Convolution algorithm for the exact normalizing constant of a closed product-form network (Buzen 1973...
Definition pfqn_ca.h:120
T num_factorial(unsigned n)
Factorial as a value of T.
Definition number.h:184
std::size_t population_count(const std::vector< int > &N)
Number of population vectors n with 0 <= n <= N.
Definition population.h:38
std::vector< std::size_t > plane_sizes(const std::vector< int > &N)
Mixed-radix plane sizes: prods[r] = prod_{s<r} (N[s]+1).
Definition population.h:27
bool next_pop(std::vector< int > &n, const std::vector< int > &N)
Advance n to the next population vector in the lattice 0 <= n <= N, odometer order with the last clas...
Definition population.h:56
T num_pow_int(const T &base, unsigned e)
Integer power, valid in any field (no transcendental requirement).
Definition number.h:192
std::size_t pop_index(const std::vector< int > &n, const std::vector< std::size_t > &prods)
Index of n in the lattice, 0-based (MATLAB hashpop is 1-based).
Definition population.h:45
Number-type abstraction for the templated API port.
Population-vector enumeration and combinatorics.
Return value of the normalizing-constant family, mirroring Ret.pfqnNc.
Definition pfqn_ca.h:44
T G
normalizing constant in the requested arithmetic
Definition pfqn_ca.h:45
double lG
log of the constant, always a double and always finite
Definition pfqn_ca.h:46