LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_gld.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_GLD_H
6#define LINE_API_PFQN_GLD_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Exact normalizing constant of a closed product-form network whose stations
12 * may be load dependent (generalized Buzen, Reiser-Kobayashi 1975).
13 *
14 * Templated port of matlab/src/api/pfqn/pfqn_gld.m (and of the single-class
15 * specialization matlab/src/api/pfqn/pfqn_gldsingle.m, which the recursion
16 * below subsumes), cross-checked term for term against mp_pfqn's
17 * gld/gld_multi.c, the exact GMP reference.
18 *
19 * Model. Station i serves at rate mu(i,k) when it holds k jobs, k = 1 ... Nt
20 * with Nt = sum_r N_r. The single-station balance function for a class vector
21 * k with j = sum_r k_r jobs is
22 *
23 * Y_i(k) = j! / prod_r k_r! * prod_r L(i,r)^{k_r} / prod_{a=1}^{j} mu(i,a)
24 *
25 * and the constant is the convolution of the M station factors,
26 *
27 * G_0(n) = [n == 0], G_i(n) = sum_{0 <= k <= n} Y_i(k) G_{i-1}(n - k),
28 * G(N) = G_M(N).
29 *
30 * Algorithm. MATLAB writes this as a recursion on (M, N, mu) with no
31 * memoization, whose cost is exponential in Nt; mp_pfqn replaced it by the
32 * station-by-station convolution above, which is what this port implements.
33 * The value is identical, the cost is O(M P^2 R) with P = prod_r (N_r + 1).
34 * A station whose rates are all 1 is load independent and is folded in by the
35 * classical in-place Buzen update
36 *
37 * G_i(n) = G_{i-1}(n) + sum_r L(i,r) G_i(n - e_r)
38 *
39 * in O(P R) instead, so a model with no load-dependent station reduces
40 * operation for operation to pfqn_ca on the same demands.
41 *
42 * NO pfqn_lld HERE, deliberately. MATLAB, python and the JAR carry a pfqn_lld
43 * alongside pfqn_gld: there the recursion is the unmemoised one described
44 * above, and saturating the rate shift at the LLD threshold s_k makes its
45 * state repeat, so a memo turns an exponential tree into a bounded one. That
46 * is a cure for an algorithm this port does not use. The convolution here is
47 * already polynomial and visits each station once, so there is nothing for the
48 * threshold to collapse; the LLD structure would have to be exploited by a
49 * different device, splitting a station's balance function into the s_k terms
50 * below the threshold and a geometric tail folded in by the Buzen update, and
51 * that is a change of algorithm rather than a port. pfqn_lldsingle IS ported,
52 * because the single-class kernel it accelerates is the same recursion in
53 * every language.
54 *
55 * Delay stations. MATLAB's pfqn_gld takes no think-time argument: a delay is
56 * an ordinary row of L whose rates are mu(i,k) = k, for which the factorials
57 * cancel and Y_i(k) collapses to prod_r Z_r^{k_r} / prod_r k_r!. The port
58 * keeps that convention, so an infinite-server station is expressed by giving
59 * it the rate row 1, 2, ..., Nt and needs no special case anywhere.
60 *
61 * Arithmetic. Every operation is an addition, a multiplication or a division
62 * in the field of the inputs, so the algorithm is exact in rational arithmetic
63 * with no reformulation; nothing here needs a transcendental function. As in
64 * pfqn_ca, IEEE double is the only arithmetic that can overflow, and it gets
65 * the same power-of-two rescaling of the demands: dividing every demand by
66 * 2^k divides every Y_i(k) of total degree j by 2^{jk}, hence divides G(N) by
67 * exactly 2^{Nt k}, and ldexp moves the exponent without touching a mantissa
68 * bit. The estimate that picks k accounts for the rates as well as the
69 * demands, since a delay station depresses G by Nt!.
70 */
71
72#include <algorithm>
73#include <cmath>
74#include <cstddef>
75#include <limits>
76#include <type_traits>
77#include <vector>
78
80#include "line/num/number.h"
81#include "line/util/error.h"
82#include "line/util/matrix.h"
84
85namespace line {
86namespace pfqn {
87
88namespace detail {
89
90/**
91 * Power-of-two scale exponent for the load-dependent recursion, from the
92 * largest single-station term log Y_i(N). Returns 0 for every arithmetic other
93 * than double, and also whenever a demand or a rate is non-positive, since the
94 * estimate is then unavailable and the recursion is run unscaled.
95 */
96template <class T>
97int gld_scale_exponent(const Matrix<T>& L, const std::vector<int>& N, const Matrix<T>& mu,
98 long Nt) {
99 if (!std::is_same<T, double>::value) return 0;
100 if (Nt <= 0) return 0;
101 const std::size_t M = L.rows(), R = L.cols();
102
103 double lmulti = std::lgamma(static_cast<double>(Nt) + 1.0);
104 for (std::size_t r = 0; r < R; ++r) lmulti -= std::lgamma(static_cast<double>(N[r]) + 1.0);
105
106 double lGest = -std::numeric_limits<double>::infinity();
107 for (std::size_t i = 0; i < M; ++i) {
108 double t = lmulti;
109 bool ok = true;
110 for (std::size_t r = 0; r < R && ok; ++r) {
111 if (N[r] > 0) {
112 const double lir = num_traits<T>::to_double(L(i, r));
113 if (lir > 0)
114 t += N[r] * std::log(lir);
115 else
116 ok = false;
117 }
118 }
119 for (long a = 0; a < Nt && ok; ++a) {
120 const double m = num_traits<T>::to_double(mu(i, static_cast<std::size_t>(a)));
121 if (m > 0)
122 t -= std::log(m);
123 else
124 ok = false;
125 }
126 if (ok && t > lGest) lGest = t;
127 }
128 if (!std::isfinite(lGest)) return 0;
129 return static_cast<int>(std::lround(lGest / (static_cast<double>(Nt) * std::log(2.0))));
130}
131
132} // namespace detail
133
134/**
135 * @brief Exact normalizing constant of a closed product-form network whose
136 * stations may be load dependent (generalized Buzen, Reiser-Kobayashi
137 * 1975).
138 *
139 * @param L (M x R) service demands, M stations and R closed classes
140 * @param N (R) population per class
141 * @param mu (M x Nt') load-dependent service rates, Nt' >= sum(N); mu(i,k-1)
142 * is the rate of station i while it holds k jobs. A row of all ones
143 * is a single server, the row 1, 2, ..., Nt is an infinite server.
144 *
145 * @throws InputError on a dimension mismatch, on a rate matrix with fewer
146 * columns than the total population, or on a zero rate (which would
147 * make the balance function undefined rather than infinite).
148 */
149template <class T>
150NcResult<T> pfqn_gld(const Matrix<T>& L, const std::vector<int>& N, const Matrix<T>& mu) {
151 const std::size_t M = L.rows();
152 const std::size_t R = N.size();
153 if (!L.empty() && L.cols() != R)
154 throw InputError("pfqn_gld: demand matrix and population vector disagree on the class count");
155
156 const T zero = num_traits<T>::from_int(0);
157 const T one = num_traits<T>::from_int(1);
158
159 long Nt = 0;
160 bool negative = false;
161 for (int v : N) {
162 if (v < 0) negative = true;
163 Nt += v;
164 }
165 // Same contract as pfqn_ca: an unreachable population has no states.
166 if (negative) return {zero, -std::numeric_limits<double>::infinity()};
167 if (Nt == 0) return {one, 0.0};
168 // MATLAB returns G = 0 for an empty demand matrix and a positive population.
169 if (M == 0) return {zero, -std::numeric_limits<double>::infinity()};
170
171 if (mu.rows() != M)
172 throw InputError("pfqn_gld: rate matrix and demand matrix disagree on the station count");
173 if (static_cast<long>(mu.cols()) < Nt)
174 throw InputError("pfqn_gld: rate matrix needs one column per job in the total population");
175 for (std::size_t i = 0; i < M; ++i)
176 for (long a = 0; a < Nt; ++a)
177 if (mu(i, static_cast<std::size_t>(a)) == zero)
178 throw InputError("pfqn_gld: load-dependent service rate must be nonzero");
179
180 const int kscale = detail::gld_scale_exponent(L, N, mu, Nt);
181 Matrix<T> Ls = L;
182 if constexpr (std::is_same<T, double>::value) {
183 if (kscale != 0) {
184 for (std::size_t i = 0; i < M; ++i)
185 for (std::size_t r = 0; r < R; ++r) Ls(i, r) = std::ldexp(Ls(i, r), -kscale);
186 }
187 }
188
189 const std::vector<std::size_t> prods = plane_sizes(N);
190 const std::size_t total = population_count(N);
191
192 // Running constant G_i over the population lattice, starting at G_0 = delta_0.
193 std::vector<T> Gv(total, zero);
194 Gv[0] = one;
195
196 std::vector<T> Y, Gs;
197 std::vector<T> muprod;
198 std::vector<int> n(R, 0), k(R, 0);
199
200 for (std::size_t m = 0; m < M; ++m) {
201 bool loadIndependent = true;
202 for (long a = 0; a < Nt && loadIndependent; ++a)
203 if (!(mu(m, static_cast<std::size_t>(a)) == one)) loadIndependent = false;
204
205 if (loadIndependent) {
206 // In-place Buzen, ascending: n - e_r is already updated for this
207 // station by the time n is reached, which is what the recursion asks.
208 std::fill(n.begin(), n.end(), 0);
209 while (next_pop(n, N)) {
210 const std::size_t idx = pop_index(n, prods);
211 T acc = Gv[idx];
212 for (std::size_t r = 0; r < R; ++r)
213 if (n[r] > 0) acc += Ls(m, r) * Gv[idx - prods[r]];
214 Gv[idx] = acc;
215 }
216 continue;
217 }
218
219 // Load-dependent station: build its balance function over the lattice,
220 // then convolve. muprod[j] = prod_{a=1}^{j} mu(m,a).
221 muprod.assign(static_cast<std::size_t>(Nt) + 1, one);
222 for (long j = 1; j <= Nt; ++j)
223 muprod[static_cast<std::size_t>(j)] =
224 muprod[static_cast<std::size_t>(j - 1)] * mu(m, static_cast<std::size_t>(j - 1));
225
226 Y.assign(total, zero);
227 std::fill(k.begin(), k.end(), 0);
228 bool more = true;
229 while (more) {
230 long j = 0;
231 for (int v : k) j += v;
232 const std::size_t ik = pop_index(k, prods);
233
234 bool vanishes = false;
235 T num = num_factorial<T>(static_cast<unsigned>(j));
236 for (std::size_t r = 0; r < R && !vanishes; ++r) {
237 if (k[r] > 0) {
238 if (Ls(m, r) == zero)
239 vanishes = true;
240 else
241 num *= num_pow_int(Ls(m, r), static_cast<unsigned>(k[r]));
242 }
243 }
244 if (!vanishes) {
245 T den = one;
246 for (std::size_t r = 0; r < R; ++r) den *= num_factorial<T>(static_cast<unsigned>(k[r]));
247 Y[ik] = num / den / muprod[static_cast<std::size_t>(j)];
248 }
249 more = next_pop(k, N);
250 }
251
252 Gs.assign(total, zero);
253 std::fill(n.begin(), n.end(), 0);
254 more = true;
255 while (more) {
256 const std::size_t idx = pop_index(n, prods);
257 T acc = zero;
258 std::fill(k.begin(), k.end(), 0);
259 while (true) {
260 std::size_t ik = 0, idiff = 0;
261 for (std::size_t r = 0; r < R; ++r) {
262 ik += prods[r] * static_cast<std::size_t>(k[r]);
263 idiff += prods[r] * static_cast<std::size_t>(n[r] - k[r]);
264 }
265 if (!(Y[ik] == zero) && !(Gv[idiff] == zero)) acc += Y[ik] * Gv[idiff];
266 bool carry = true;
267 for (std::size_t r = 0; r < R; ++r) {
268 if (k[r] < n[r]) {
269 ++k[r];
270 carry = false;
271 break;
272 }
273 k[r] = 0;
274 }
275 if (carry) break;
276 }
277 Gs[idx] = acc;
278 more = next_pop(n, N);
279 }
280 Gv.swap(Gs);
281 }
282
283 const T raw = Gv[total - 1];
284 const double lG =
285 num_traits<T>::log_as_double(raw) + static_cast<double>(Nt) * kscale * std::log(2.0);
286 T Gn = raw;
287 if constexpr (std::is_same<T, double>::value) {
288 if (kscale != 0) Gn = std::ldexp(raw, static_cast<int>(Nt * static_cast<long>(kscale)));
289 }
290 return {Gn, lG};
291}
292
293/** Overload with all rates equal to one, i.e. every station a single server. */
294template <class T>
295NcResult<T> pfqn_gld(const Matrix<T>& L, const std::vector<int>& N) {
296 long Nt = 0;
297 for (int v : N)
298 if (v > 0) Nt += v;
299 Matrix<T> mu(L.rows(), static_cast<std::size_t>(Nt > 0 ? Nt : 1), num_traits<T>::from_int(1));
300 return pfqn_gld(L, N, mu);
301}
302
303} // namespace pfqn
304} // namespace line
305
306#endif // LINE_API_PFQN_GLD_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_gld(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &mu)
Exact normalizing constant of a closed product-form network whose stations may be load dependent (gen...
Definition pfqn_gld.h:150
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.
Convolution algorithm for the exact normalizing constant of a closed product-form network (Buzen 1973...
Population-vector enumeration and combinatorics.
Return value of the normalizing-constant family, mirroring Ret.pfqnNc.
Definition pfqn_ca.h:44