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 * pfqn_lld (pfqn_lld.h) is the port of MATLAB's memoised recursion with the
43 * rate shift saturated at the LLD threshold s_k, and returns the same constant
44 * as this convolution together with the detected thresholds. The convolution
45 * here is already polynomial and visits each station once, so pfqn_gld itself
46 * has nothing for the threshold to collapse. pfqn_lldsingle is the single-class
47 * kernel both share.
48 *
49 * Delay stations. MATLAB's pfqn_gld takes no think-time argument: a delay is
50 * an ordinary row of L whose rates are mu(i,k) = k, for which the factorials
51 * cancel and Y_i(k) collapses to prod_r Z_r^{k_r} / prod_r k_r!. The port
52 * keeps that convention, so an infinite-server station is expressed by giving
53 * it the rate row 1, 2, ..., Nt and needs no special case anywhere.
54 *
55 * Arithmetic. Every operation is an addition, a multiplication or a division
56 * in the field of the inputs, so the algorithm is exact in rational arithmetic
57 * with no reformulation; nothing here needs a transcendental function. As in
58 * pfqn_ca, IEEE double is the only arithmetic that can overflow, and it gets
59 * the same power-of-two rescaling of the demands: dividing every demand by
60 * 2^k divides every Y_i(k) of total degree j by 2^{jk}, hence divides G(N) by
61 * exactly 2^{Nt k}, and ldexp moves the exponent without touching a mantissa
62 * bit. The estimate that picks k accounts for the rates as well as the
63 * demands, since a delay station depresses G by Nt!.
64 */
65
66#include <algorithm>
67#include <cmath>
68#include <cstddef>
69#include <limits>
70#include <type_traits>
71#include <vector>
72
74#include "line/num/number.h"
75#include "line/util/error.h"
76#include "line/util/matrix.h"
78
79namespace line {
80namespace pfqn {
81
82namespace detail {
83
84/**
85 * Power-of-two scale exponent for the load-dependent recursion, from the
86 * largest single-station term log Y_i(N). Returns 0 for every arithmetic other
87 * than double, and also whenever a demand or a rate is non-positive, since the
88 * estimate is then unavailable and the recursion is run unscaled.
89 */
90template <class T>
91int gld_scale_exponent(const Matrix<T>& L, const std::vector<int>& N, const Matrix<T>& mu,
92 long Nt) {
93 if (!std::is_same<T, double>::value) return 0;
94 if (Nt <= 0) return 0;
95 const std::size_t M = L.rows(), R = L.cols();
96
97 double lmulti = std::lgamma(static_cast<double>(Nt) + 1.0);
98 for (std::size_t r = 0; r < R; ++r) lmulti -= std::lgamma(static_cast<double>(N[r]) + 1.0);
99
100 double lGest = -std::numeric_limits<double>::infinity();
101 for (std::size_t i = 0; i < M; ++i) {
102 double t = lmulti;
103 bool ok = true;
104 for (std::size_t r = 0; r < R && ok; ++r) {
105 if (N[r] > 0) {
106 const double lir = num_traits<T>::to_double(L(i, r));
107 if (lir > 0)
108 t += N[r] * std::log(lir);
109 else
110 ok = false;
111 }
112 }
113 for (long a = 0; a < Nt && ok; ++a) {
114 const double m = num_traits<T>::to_double(mu(i, static_cast<std::size_t>(a)));
115 if (m > 0)
116 t -= std::log(m);
117 else
118 ok = false;
119 }
120 if (ok && t > lGest) lGest = t;
121 }
122 if (!std::isfinite(lGest)) return 0;
123 return static_cast<int>(std::lround(lGest / (static_cast<double>(Nt) * std::log(2.0))));
124}
125
126} // namespace detail
127
128/**
129 * @brief Exact normalizing constant of a closed product-form network whose
130 * stations may be load dependent (generalized Buzen, Reiser-Kobayashi
131 * 1975).
132 *
133 * @param L (M x R) service demands, M stations and R closed classes
134 * @param N (R) population per class
135 * @param mu (M x Nt') load-dependent service rates, Nt' >= sum(N); mu(i,k-1)
136 * is the rate of station i while it holds k jobs. A row of all ones
137 * is a single server, the row 1, 2, ..., Nt is an infinite server.
138 *
139 * @throws InputError on a dimension mismatch, on a rate matrix with fewer
140 * columns than the total population, or on a zero rate (which would
141 * make the balance function undefined rather than infinite).
142 */
143template <class T>
144NcResult<T> pfqn_gld(const Matrix<T>& L, const std::vector<int>& N, const Matrix<T>& mu) {
145 const std::size_t M = L.rows();
146 const std::size_t R = N.size();
147 if (!L.empty() && L.cols() != R)
148 throw InputError("pfqn_gld: demand matrix and population vector disagree on the class count");
149
150 const T zero = num_traits<T>::from_int(0);
151 const T one = num_traits<T>::from_int(1);
152
153 long Nt = 0;
154 bool negative = false;
155 for (int v : N) {
156 if (v < 0) negative = true;
157 Nt += v;
158 }
159 // Same contract as pfqn_ca: an unreachable population has no states.
160 if (negative) return {zero, -std::numeric_limits<double>::infinity()};
161 if (Nt == 0) return {one, 0.0};
162 // MATLAB returns G = 0 for an empty demand matrix and a positive population.
163 if (M == 0) return {zero, -std::numeric_limits<double>::infinity()};
164
165 if (mu.rows() != M)
166 throw InputError("pfqn_gld: rate matrix and demand matrix disagree on the station count");
167 if (static_cast<long>(mu.cols()) < Nt)
168 throw InputError("pfqn_gld: rate matrix needs one column per job in the total population");
169 for (std::size_t i = 0; i < M; ++i)
170 for (long a = 0; a < Nt; ++a)
171 if (mu(i, static_cast<std::size_t>(a)) == zero)
172 throw InputError("pfqn_gld: load-dependent service rate must be nonzero");
173
174 const int kscale = detail::gld_scale_exponent(L, N, mu, Nt);
175 Matrix<T> Ls = L;
176 if constexpr (std::is_same<T, double>::value) {
177 if (kscale != 0) {
178 for (std::size_t i = 0; i < M; ++i)
179 for (std::size_t r = 0; r < R; ++r) Ls(i, r) = std::ldexp(Ls(i, r), -kscale);
180 }
181 }
182
183 const std::vector<std::size_t> prods = plane_sizes(N);
184 const std::size_t total = population_count(N);
185
186 // Running constant G_i over the population lattice, starting at G_0 = delta_0.
187 std::vector<T> Gv(total, zero);
188 Gv[0] = one;
189
190 std::vector<T> Y, Gs;
191 std::vector<T> muprod;
192 std::vector<int> n(R, 0), k(R, 0);
193
194 for (std::size_t m = 0; m < M; ++m) {
195 bool loadIndependent = true;
196 for (long a = 0; a < Nt && loadIndependent; ++a)
197 if (!(mu(m, static_cast<std::size_t>(a)) == one)) loadIndependent = false;
198
199 if (loadIndependent) {
200 // In-place Buzen, ascending: n - e_r is already updated for this
201 // station by the time n is reached, which is what the recursion asks.
202 std::fill(n.begin(), n.end(), 0);
203 while (next_pop(n, N)) {
204 const std::size_t idx = pop_index(n, prods);
205 T acc = Gv[idx];
206 for (std::size_t r = 0; r < R; ++r)
207 if (n[r] > 0) acc += Ls(m, r) * Gv[idx - prods[r]];
208 Gv[idx] = acc;
209 }
210 continue;
211 }
212
213 // Load-dependent station: build its balance function over the lattice,
214 // then convolve. muprod[j] = prod_{a=1}^{j} mu(m,a).
215 muprod.assign(static_cast<std::size_t>(Nt) + 1, one);
216 for (long j = 1; j <= Nt; ++j)
217 muprod[static_cast<std::size_t>(j)] =
218 muprod[static_cast<std::size_t>(j - 1)] * mu(m, static_cast<std::size_t>(j - 1));
219
220 Y.assign(total, zero);
221 std::fill(k.begin(), k.end(), 0);
222 bool more = true;
223 while (more) {
224 long j = 0;
225 for (int v : k) j += v;
226 const std::size_t ik = pop_index(k, prods);
227
228 bool vanishes = false;
229 T num = num_factorial<T>(static_cast<unsigned>(j));
230 for (std::size_t r = 0; r < R && !vanishes; ++r) {
231 if (k[r] > 0) {
232 if (Ls(m, r) == zero)
233 vanishes = true;
234 else
235 num *= num_pow_int(Ls(m, r), static_cast<unsigned>(k[r]));
236 }
237 }
238 if (!vanishes) {
239 T den = one;
240 for (std::size_t r = 0; r < R; ++r) den *= num_factorial<T>(static_cast<unsigned>(k[r]));
241 Y[ik] = num / den / muprod[static_cast<std::size_t>(j)];
242 }
243 more = next_pop(k, N);
244 }
245
246 Gs.assign(total, zero);
247 std::fill(n.begin(), n.end(), 0);
248 more = true;
249 while (more) {
250 const std::size_t idx = pop_index(n, prods);
251 T acc = zero;
252 std::fill(k.begin(), k.end(), 0);
253 while (true) {
254 std::size_t ik = 0, idiff = 0;
255 for (std::size_t r = 0; r < R; ++r) {
256 ik += prods[r] * static_cast<std::size_t>(k[r]);
257 idiff += prods[r] * static_cast<std::size_t>(n[r] - k[r]);
258 }
259 if (!(Y[ik] == zero) && !(Gv[idiff] == zero)) acc += Y[ik] * Gv[idiff];
260 bool carry = true;
261 for (std::size_t r = 0; r < R; ++r) {
262 if (k[r] < n[r]) {
263 ++k[r];
264 carry = false;
265 break;
266 }
267 k[r] = 0;
268 }
269 if (carry) break;
270 }
271 Gs[idx] = acc;
272 more = next_pop(n, N);
273 }
274 Gv.swap(Gs);
275 }
276
277 const T raw = Gv[total - 1];
278 const double lG =
279 num_traits<T>::log_as_double(raw) + static_cast<double>(Nt) * kscale * std::log(2.0);
280 T Gn = raw;
281 if constexpr (std::is_same<T, double>::value) {
282 if (kscale != 0) Gn = std::ldexp(raw, static_cast<int>(Nt * static_cast<long>(kscale)));
283 }
284 return {Gn, lG};
285}
286
287/** Overload with all rates equal to one, i.e. every station a single server. */
288template <class T>
289NcResult<T> pfqn_gld(const Matrix<T>& L, const std::vector<int>& N) {
290 long Nt = 0;
291 for (int v : N)
292 if (v > 0) Nt += v;
293 Matrix<T> mu(L.rows(), static_cast<std::size_t>(Nt > 0 ? Nt : 1), num_traits<T>::from_int(1));
294 return pfqn_gld(L, N, mu);
295}
296
297} // namespace pfqn
298} // namespace line
299
300#endif // LINE_API_PFQN_GLD_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
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:144
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
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:204
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