LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_recal.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_RECAL_H
6#define LINE_API_PFQN_RECAL_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * RECAL (REcursive CALculation) for the exact normalizing constant of a closed
12 * product-form network (Conway and Georganas 1986).
13 *
14 * Templated port of matlab/src/api/pfqn/pfqn_recal.m, cross-checked against
15 * mp_pfqn's recal/recal-multi-exact.c for the exact path.
16 *
17 * Where convolution recurses on stations over the population lattice, RECAL
18 * recurses on jobs over the space of station multiplicity vectors. Jobs are
19 * added one at a time, class 0 first, and the state carried between steps is a
20 * function g_n indexed by a multiplicity vector m with sum(m) = Ntot - n:
21 *
22 * g_0(m) = 1 for all sum(m) = Ntot
23 * g_n(m) = ( Z_r g_{n-1}(m + e_delay)
24 * + sum_j (m_j + 1 + m0_j - 1) L(j,r) g_{n-1}(m + e_j) ) / n_r
25 *
26 * where r is the class of the n-th job, n_r its index within that class, and
27 * m0(j) the multiplicity of station j. The answer is g_Ntot(0), the single
28 * state with an empty multiplicity vector. Every operation is a field
29 * operation (the only division is by the small integer n_r), so the recursion
30 * is exact in rational arithmetic with no reformulation, which is what makes
31 * RECAL usable as an exactness oracle for the rest of the pfqn family.
32 *
33 * Delay column: think time enters as one extra column of m, appended after the
34 * M queueing stations. It is allocated only when some class has a non-zero
35 * think time, following mp_pfqn. MATLAB always carries the column; with Z = 0
36 * the column is a spectator (g never reads across it and the level-0 value is
37 * 1 everywhere), so the returned constant is identical and only the size of
38 * the intermediate arrays differs.
39 *
40 * Station consolidation: stations with identical demand rows are merged and
41 * their multiplicities added, which is exactly what the (m_j + m0_j - 1)
42 * coefficient is for. This mirrors pfqn_unique in the MATLAB reference and
43 * shrinks the state space from multichoose(M+1, Ntot) to multichoose(M'+1,
44 * Ntot). Rows are compared for exact equality rather than with the MATLAB
45 * 1e-14 tolerance: merging rows that only nearly agree would perturb the
46 * constant, which an exact-capable algorithm must not do.
47 *
48 * Scaling: as for convolution, in IEEE double the recursion can leave the
49 * exponent range, so the same power-of-two rescaling of pfqn_ca is applied
50 * there and nowhere else. G is homogeneous of degree Ntot in (L, Z), so
51 * dividing every demand and think time by 2^k divides G by exactly 2^(k*Ntot),
52 * and the recovery is an exponent adjustment rather than an exp().
53 */
54
55#include <cmath>
56#include <cstddef>
57#include <limits>
58#include <type_traits>
59#include <vector>
60
62#include "line/num/number.h"
63#include "line/util/error.h"
64#include "line/util/matrix.h"
66
67namespace line {
68namespace pfqn {
69
70namespace detail {
71
72/** Largest number of multiplicity vectors the port will allocate g over. */
73constexpr unsigned long long RECAL_MAX_STATES = 100000000ULL;
74
75/**
76 * Binomial coefficient as an exact integer. The running product is a binomial
77 * at every step, so the division is exact and no rounding is possible; an
78 * overflow of the accumulator is reported rather than wrapped.
79 */
80inline unsigned long long binom_exact(unsigned long long n, unsigned long long k) {
81 if (k > n) return 0ULL;
82 if (k > n - k) k = n - k;
83 unsigned long long r = 1ULL;
84 for (unsigned long long i = 1ULL; i <= k; ++i) {
85 const unsigned long long a = n - k + i;
86 if (r > std::numeric_limits<unsigned long long>::max() / a)
87 throw NumericError("pfqn_recal: multiplicity state space overflows a 64-bit count");
88 r = r * a / i;
89 }
90 return r;
91}
92
93/** Number of vectors of ncols non-negative integers summing to k. */
94inline unsigned long long multichoose_count(std::size_t ncols, int k) {
95 if (k < 0) return 0ULL;
96 if (ncols == 0) return k == 0 ? 1ULL : 0ULL;
97 return binom_exact(static_cast<unsigned long long>(ncols) + static_cast<unsigned long long>(k) - 1ULL,
98 static_cast<unsigned long long>(k));
99}
100
101/**
102 * Position of m in the enumeration of all ncols-vectors summing to ksum,
103 * ordered by the first component ascending and then recursively on the tail.
104 * O(ncols + ksum), replacing the linear row scan of the MATLAB matchrow; the
105 * enumeration it ranks is the one recal_next_composition walks.
106 */
107inline std::size_t recal_rank(const std::vector<int>& m, std::size_t ncols, int ksum) {
108 unsigned long long idx = 0ULL;
109 std::size_t pos = 0;
110 while (ncols > 1) {
111 const int c = m[pos];
112 for (int i = 0; i < c; ++i) idx += multichoose_count(ncols - 1, ksum - i);
113 ksum -= c;
114 --ncols;
115 ++pos;
116 }
117 return static_cast<std::size_t>(idx);
118}
119
120/**
121 * Advance m to the next vector of the same length and sum, in the order that
122 * recal_rank ranks. Returns false once the enumeration is exhausted.
123 */
124inline bool recal_next_composition(std::vector<int>& m) {
125 const std::size_t n = m.size();
126 if (n < 2) return false;
127 const std::size_t last = n - 1;
128 long rest = 0;
129 for (long p = static_cast<long>(n) - 2; p >= 0; --p) {
130 rest += m[static_cast<std::size_t>(p) + 1];
131 if (rest > 0) {
132 m[static_cast<std::size_t>(p)] += 1;
133 for (std::size_t j = static_cast<std::size_t>(p) + 1; j < last; ++j) m[j] = 0;
134 m[last] = static_cast<int>(rest - 1);
135 return true;
136 }
137 }
138 return false;
139}
140
141/**
142 * Merge stations whose demand rows are identical, summing their
143 * multiplicities. Mirrors pfqn_unique followed by the m0 accumulation of the
144 * MATLAB reference, with exact row equality as the merge test.
145 */
146template <class T>
147void consolidate_stations(const Matrix<T>& L, const std::vector<int>& m0, Matrix<T>& Lu,
148 std::vector<int>& m0u) {
149 const std::size_t M = L.rows(), R = L.cols();
150 std::vector<std::size_t> keep;
151 std::vector<std::size_t> mapping(M, 0);
152 for (std::size_t i = 0; i < M; ++i) {
153 std::size_t hit = keep.size();
154 for (std::size_t u = 0; u < keep.size(); ++u) {
155 bool same = true;
156 for (std::size_t r = 0; r < R; ++r)
157 if (!(L(i, r) == L(keep[u], r))) {
158 same = false;
159 break;
160 }
161 if (same) {
162 hit = u;
163 break;
164 }
165 }
166 if (hit == keep.size()) keep.push_back(i);
167 mapping[i] = hit;
168 }
169 Lu = Matrix<T>(keep.size(), R);
170 for (std::size_t u = 0; u < keep.size(); ++u)
171 for (std::size_t r = 0; r < R; ++r) Lu(u, r) = L(keep[u], r);
172 m0u.assign(keep.size(), 0);
173 for (std::size_t i = 0; i < M; ++i) m0u[mapping[i]] += m0[i];
174}
175
176} // namespace detail
177
178/**
179 * @brief RECAL (REcursive CALculation) for the exact normalizing constant of
180 * a closed product-form network (Conway and Georganas 1986).
181 *
182 * @param L (M x R) service demands, M queueing stations, R classes
183 * @param N (R) population per class, non-negative
184 * @param Z (K x R) think times, summed over rows; may be empty
185 * @param m0 (M) station multiplicities, each at least one; empty for all ones
186 */
187template <class T>
188NcResult<T> pfqn_recal(const Matrix<T>& L, const std::vector<int>& N, const Matrix<T>& Z,
189 const std::vector<int>& m0) {
190 const std::size_t M = L.rows();
191 const std::size_t R = N.size();
192 if (!L.empty() && L.cols() != R)
193 throw InputError("pfqn_recal: demand matrix and population vector disagree on the class count");
194 if (!m0.empty() && m0.size() != M)
195 throw InputError("pfqn_recal: multiplicity vector has the wrong length");
196 for (std::size_t i = 0; i < m0.size(); ++i)
197 if (m0[i] < 1) throw InputError("pfqn_recal: station multiplicity below one");
198
199 const T zero = num_traits<T>::from_int(0);
200 const T one = num_traits<T>::from_int(1);
201
202 // Z summed over its rows, so a per-node think-time matrix is accepted.
203 std::vector<T> Zsum(R, zero);
204 if (!Z.empty()) {
205 if (Z.cols() != R) throw InputError("pfqn_recal: Z and N disagree on the class count");
206 for (std::size_t k = 0; k < Z.rows(); ++k)
207 for (std::size_t r = 0; r < R; ++r) Zsum[r] += Z(k, r);
208 }
209
210 // negative-population rejection rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
211 long Nt = 0;
212 for (int v : N) {
213 if (v < 0) throw InputError("pfqn_recal: negative population");
214 Nt += v;
215 }
216
217 if (M == 0) {
218 // Delay-only network: G = prod_r Z_r^{N_r} / N_r!.
219 const T G = detail::pff_delay(Zsum, N);
220 return {G, num_traits<T>::log_as_double(G)};
221 }
222 if (Nt == 0) return {one, 0.0};
223
224 std::vector<int> mult(M, 1);
225 for (std::size_t i = 0; i < m0.size(); ++i) mult[i] = m0[i];
227 std::vector<int> m0c;
228 detail::consolidate_stations(L, mult, Lc, m0c);
229 const std::size_t Mq = Lc.rows();
230
231 const int kscale = detail::scale_exponent(Lc, N, Zsum);
232 if constexpr (std::is_same<T, double>::value) {
233 // exponent-only rescaling rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
234 if (kscale != 0) {
235 for (std::size_t i = 0; i < Mq; ++i)
236 for (std::size_t r = 0; r < R; ++r) Lc(i, r) = std::ldexp(Lc(i, r), -kscale);
237 for (std::size_t r = 0; r < R; ++r) Zsum[r] = std::ldexp(Zsum[r], -kscale);
238 }
239 }
240
241 bool hasZ = false;
242 for (std::size_t r = 0; r < R; ++r)
243 if (!(Zsum[r] == zero)) {
244 hasZ = true;
245 break;
246 }
247 // Columns of the multiplicity vector: the queueing stations, plus one
248 // delay column when any class thinks.
249 const std::size_t Mz = hasZ ? Mq + 1 : Mq;
250 const std::size_t delay = Mq; // column index of the delay slot, if present
251
252 const unsigned long long states = detail::multichoose_count(Mz, static_cast<int>(Nt));
253 if (states > detail::RECAL_MAX_STATES)
254 throw NumericError("pfqn_recal: multiplicity state space too large for this model");
255
256 // g at the previous and the current job count. Level 0 is the largest, so a
257 // single pair of buffers of that size serves every level.
258 std::vector<T> gprev(static_cast<std::size_t>(states), one);
259 std::vector<T> gcur(static_cast<std::size_t>(states), zero);
260
261 std::vector<int> m(Mz, 0);
262 int n = 0;
263 for (std::size_t r = 0; r < R; ++r) {
264 for (int nr = 1; nr <= N[r]; ++nr) {
265 ++n;
266 const int k = static_cast<int>(Nt) - n; // sum of the current level
267 const int kprev = k + 1; // sum of the previous level
268 const unsigned long long ncfg = detail::multichoose_count(Mz, k);
269 const T nrv = num_traits<T>::from_int(nr);
270 const bool thinks = hasZ && !(Zsum[r] == zero);
271
272 m.assign(Mz, 0);
273 m[Mz - 1] = k;
274 for (unsigned long long i = 0; i < ncfg; ++i) {
275 T acc = zero;
276 if (thinks) {
277 m[delay] += 1;
278 acc += Zsum[r] * gprev[detail::recal_rank(m, Mz, kprev)];
279 m[delay] -= 1;
280 }
281 for (std::size_t j = 0; j < Mq; ++j) {
282 if (Lc(j, r) == zero) continue; // the term is exactly zero
283 m[j] += 1;
284 acc += num_traits<T>::from_int(m[j] + m0c[j] - 1) * Lc(j, r) *
285 gprev[detail::recal_rank(m, Mz, kprev)];
286 m[j] -= 1;
287 }
288 gcur[static_cast<std::size_t>(i)] = acc / nrv;
289 if (i + 1 < ncfg) detail::recal_next_composition(m);
290 }
291 gprev.swap(gcur);
292 }
293 }
294
295 // The final level holds the single empty multiplicity vector.
296 const T raw = gprev[0];
297 const double lG =
298 num_traits<T>::log_as_double(raw) + static_cast<double>(Nt) * kscale * std::log(2.0);
299 T G = raw;
300 if constexpr (std::is_same<T, double>::value) {
301 if (kscale != 0) G = std::ldexp(raw, static_cast<int>(Nt * kscale));
302 }
303 return {G, lG};
304}
305
306/** Overload with unit station multiplicities. */
307template <class T>
308NcResult<T> pfqn_recal(const Matrix<T>& L, const std::vector<int>& N, const Matrix<T>& Z) {
309 return pfqn_recal(L, N, Z, std::vector<int>());
310}
311
312/** Overload without think times. */
313template <class T>
314NcResult<T> pfqn_recal(const Matrix<T>& L, const std::vector<int>& N) {
315 return pfqn_recal(L, N, Matrix<T>(), std::vector<int>());
316}
317
318} // namespace pfqn
319} // namespace line
320
321#endif // LINE_API_PFQN_RECAL_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
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
Dense matrix and non-owning view.
NcResult< T > pfqn_recal(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const std::vector< int > &m0)
RECAL (REcursive CALculation) for the exact normalizing constant of a closed product-form network (Co...
Definition pfqn_recal.h:188
@ Lc
Birman-Kogan Algorithm 2, single chain subproblems by MVA.
Definition pfqn_nc.h:117
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