LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_panaceald.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_PANACEALD_H
6#define LINE_API_PFQN_PFQN_PANACEALD_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * PANACEA normal-usage asymptotic expansion for LOAD-DEPENDENT closed networks
12 * (Mitra and McKenna, JACM 33(3):568-592, 1986).
13 *
14 * Templated port of matlab/src/api/pfqn/pfqn_panaceald.m. The expansion
15 * coefficients A_n are linear combinations of partition functions of a
16 * PSEUDONETWORK whose load dependence is the phi(n) transform of the original
17 * rate lattice {mu_i(n)}; the transform itself is carried by logpsi below, the
18 * partition functions by a direct load-dependent convolution over a lattice
19 * that never exceeds 2*(terms-1) jobs per class.
20 *
21 * WHERE THE INFINITE SERVERS GO. A type-3 (infinite-server) row is ABSENT from
22 * the pseudonetwork and enters only through the expansion parameter rho_j0.
23 * solver_ncld encodes such a row as mu(i,n) = n, so the rows whose rate lattice
24 * is exactly 1,2,...,Nt are detected here and folded into the think time, which
25 * is why the caller may pass the delay either in Z or as such a row of L.
26 *
27 * WHEN IT DOES NOT APPLY, and this is the part worth knowing. The expansion is
28 * an asymptotic series in the population and converges only in NORMAL USAGE,
29 * i.e. alpha_i = 1 - lambda_i / mu_i(Ntot) > 0 at every queueing centre. MATLAB
30 * returns NaN outside it and its caller turns that into an error. This port
31 * reports it through `normalUsage` plus a `reason`, as pfqn_panacea does, so a
32 * value that must not be used cannot be mistaken for one that may -- and so the
33 * caller can say WHICH of the four conditions declined.
34 *
35 * ACCURACY REGIME (measured, 2026-07-24). Relative error on lG falls with the
36 * population: 6.7e-4 at N = [10 10], 8.6e-8 at N = [400 400] at a fixed load
37 * margin alpha_min ~ 0.70, while the runtime stays flat because the
38 * pseudonetworks hold at most 4 jobs. At small N and heavy load it is dominated
39 * by pfqn_clw_lld, which is exact there and cheap. Do not calibrate it only on
40 * models small enough to have an exact reference.
41 *
42 * ARITHMETIC. A truncated asymptotic series reported as a logarithm, so gated
43 * on num_traits<T>::has_transcendental.
44 */
45
46#include <cmath>
47#include <cstddef>
48#include <limits>
49#include <vector>
50
52#include "line/num/number.h"
53#include "line/util/error.h"
54#include "line/util/matrix.h"
55
56namespace line {
57namespace pfqn {
58
59/** Return value of pfqn_panaceald, mirroring [Gn, lGn] plus why it declined. */
60template <class T>
62 T G;
63 T lG;
64 bool normalUsage; ///< false wherever the reference returns NaN
65 const char* reason; ///< which condition declined; nullptr when it applies
66};
67
68namespace detail {
69
70/** MATLAB xlogy(e,x): e*log(x), with the convention 0*log(0) = 0. */
71template <class T>
72T pald_xlogy(const T& e, const T& x) {
73 using std::log;
75 return T(e * log(x));
76}
77
78/**
79 * log psi(n) = log sum_{s>=n} [s!/(s-n)!] lambda^{s-n} / prod_{k<=s} mu(k),
80 * the mu-free part of the phi(n) transform of eq. (3.7)-(3.8a).
81 *
82 * The series is split into the exact head s <= K and a geometric tail summed in
83 * closed form through the Vandermonde identity; every term is positive, so the
84 * whole sum is taken by logsumexp with no cancellation.
85 *
86 * @param lPirow log prod_{k=1}^{s} mu(k) for s = 0..K, i.e. K+1 entries
87 */
88template <class T>
89T pald_logpsi(std::size_t n, const T& lambda, const std::vector<T>& lPirow, const T& muK,
90 const T& alpha, std::size_t K) {
91 using std::log;
92 std::vector<T> t;
93 t.reserve((K >= n ? K - n + 1 : 0) + n + 1);
94 const T nT = num_traits<T>::from_int(static_cast<long>(n));
95 for (std::size_t s = n; s <= K; ++s) {
96 const T sT = num_traits<T>::from_int(static_cast<long>(s));
97 t.push_back(T(num_factln<T>(sT) - num_factln<T>(T(sT - nT)) +
98 pald_xlogy(T(sT - nT), lambda) - lPirow[s]));
99 }
100 const std::size_t Tm = n > K + 1 ? n : K + 1;
101 const T TmT = num_traits<T>::from_int(static_cast<long>(Tm));
102 for (std::size_t i = 0; i <= n; ++i) {
103 const T iT = num_traits<T>::from_int(static_cast<long>(i));
104 t.push_back(T(num_factln<T>(nT) + num_factln<T>(TmT) - num_factln<T>(T(nT - iT)) -
105 num_factln<T>(T(TmT - nT + iT)) + pald_xlogy(T(TmT + iT - nT), lambda) +
106 T(num_traits<T>::from_int(static_cast<long>(K)) - TmT - iT) * log(muK) -
107 T(iT + num_traits<T>::from_int(1)) * log(alpha) - lPirow[K]));
108 }
109 return logsumexp(t);
110}
111
112/** Mixed-radix index (0-based) to population vector, MATLAB idx2vec. */
113inline void pald_idx2vec(std::size_t idx, const std::vector<int>& sizes, std::vector<int>& v) {
114 std::size_t t = idx;
115 for (std::size_t r = 0; r < sizes.size(); ++r) {
116 v[r] = static_cast<int>(t % static_cast<std::size_t>(sizes[r]));
117 t /= static_cast<std::size_t>(sizes[r]);
118 }
119}
120
121/** Population vector to mixed-radix index (0-based), MATLAB vec2idx. */
122inline std::size_t pald_vec2idx(const std::vector<int>& v, const std::vector<int>& sizes) {
123 std::size_t idx = 0, mult = 1;
124 for (std::size_t r = 0; r < sizes.size(); ++r) {
125 idx += mult * static_cast<std::size_t>(v[r]);
126 mult *= static_cast<std::size_t>(sizes[r]);
127 }
128 return idx;
129}
130
131/**
132 * Partition function of the pseudonetwork at population k, normalized to
133 * G(0) = 1 (MATLAB pseudonet).
134 *
135 * Populations never exceed 2*(terms-1) = 4, so the load-dependent convolution
136 * runs directly over the mixed-radix lattice rather than through pfqn_gld.
137 *
138 * @param gam (Mq x R) pseudonetwork demands
139 * @param k (R) population of the auxiliary lattice
140 * @param mups (Mq x >= sum k) pseudonetwork rate lattice
141 */
142template <class T>
143T pald_pseudonet(const Matrix<T>& gam, const std::vector<int>& k, const Matrix<T>& mups) {
144 using std::exp;
145 using std::log;
146 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
147 std::vector<std::size_t> nz;
148 for (std::size_t r = 0; r < k.size(); ++r)
149 if (k[r] > 0) nz.push_back(r);
150 const std::size_t Mq = gam.rows(), Rp = nz.size();
151 if (Rp == 0) return one;
152
153 std::vector<int> sizes(Rp), kk(Rp);
154 std::size_t npop = 1;
155 for (std::size_t a = 0; a < Rp; ++a) {
156 kk[a] = k[nz[a]];
157 sizes[a] = kk[a] + 1;
158 npop *= static_cast<std::size_t>(sizes[a]);
159 }
160
161 // Per-station balance terms over the whole lattice.
162 Matrix<T> sterm(Mq, npop, zero);
163 std::vector<int> m(Rp, 0);
164 for (std::size_t i = 0; i < Mq; ++i)
165 for (std::size_t jdx = 0; jdx < npop; ++jdx) {
166 pald_idx2vec(jdx, sizes, m);
167 int sm = 0;
168 for (std::size_t r = 0; r < Rp; ++r) sm += m[r];
169 T v = num_factln<T>(num_traits<T>::from_int(sm));
170 bool finite = true;
171 for (std::size_t r = 0; r < Rp && finite; ++r) {
172 if (m[r] == 0) continue;
173 const T g = gam(i, nz[r]);
174 if (!(g > zero)) {
175 finite = false;
176 break;
177 }
178 v = T(v + num_traits<T>::from_int(m[r]) * log(g) -
179 num_factln<T>(num_traits<T>::from_int(m[r])));
180 }
181 if (!finite) continue; // exp(-inf) = 0, the reference's break
182 if (sm > 0)
183 for (int s = 0; s < sm; ++s) v = T(v - log(mups(i, static_cast<std::size_t>(s))));
184 sterm(i, jdx) = exp(v);
185 }
186
187 // Load-dependent convolution, one station per pass.
188 std::vector<T> g(npop, zero), gnext(npop, zero);
189 g[0] = one;
190 std::vector<int> n(Rp, 0), diff(Rp, 0);
191 for (std::size_t i = 0; i < Mq; ++i) {
192 for (std::size_t idx = 0; idx < npop; ++idx) {
193 pald_idx2vec(idx, sizes, n);
194 T acc = zero;
195 for (std::size_t jdx = 0; jdx < npop; ++jdx) {
196 pald_idx2vec(jdx, sizes, m);
197 bool fits = true;
198 for (std::size_t r = 0; r < Rp; ++r) {
199 if (m[r] > n[r]) {
200 fits = false;
201 break;
202 }
203 diff[r] = n[r] - m[r];
204 }
205 if (!fits) continue;
206 acc += g[pald_vec2idx(diff, sizes)] * sterm(i, jdx);
207 }
208 gnext[idx] = acc;
209 }
210 g = gnext;
211 }
212 return g[npop - 1];
213}
214
215} // namespace detail
216
217/**
218 * @brief PANACEA normal-usage asymptotic expansion for LOAD-DEPENDENT closed
219 * networks (Mitra and McKenna, JACM 33(3):568-592, 1986).
220 *
221 * @param L (M x R) demands; an infinite-server row is recognized by its
222 * rate lattice and folded into the think time
223 * @param N (R) population
224 * @param Z (R) think times, already summed over the delay rows; empty for
225 * none
226 * @param mu (M x >= sum N) load-dependent rates; empty means all ones, and
227 * a short lattice is extended with its last column
228 * @param terms 1, 2 or 3 terms of the normal-usage series, as in pfqn_panacea
229 */
230template <class T>
231PanaceaLdResult<T> pfqn_panaceald(const Matrix<T>& L, const std::vector<int>& N,
232 const std::vector<T>& Z, const Matrix<T>& mu, int terms) {
234 "pfqn_panaceald requires transcendental arithmetic (asymptotic expansion of "
235 "log G through the phi(n) transform)");
236 using std::exp;
237 using std::log;
238 const std::size_t M = L.rows(), R = L.cols();
239 if (N.size() != R) throw InputError("pfqn_panaceald: L and N disagree on the class count");
240 if (terms < 1 || terms > 3)
241 throw InputError(
242 "pfqn_panaceald: the terms parameter must be 1, 2 or 3 (higher-order coefficients are "
243 "not implemented)");
244 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
245 std::vector<T> Zv = Z;
246 if (Zv.empty()) Zv.assign(R, zero);
247 if (Zv.size() != R) throw InputError("pfqn_panaceald: Z has the wrong length");
248
250 res.G = zero;
251 res.lG = zero;
252 res.normalUsage = true;
253 res.reason = nullptr;
254 const auto decline = [&](const char* why) {
255 res.normalUsage = false;
256 res.reason = why;
257 res.G = zero;
258 res.lG = zero;
259 return res;
260 };
261
262 long Ntl = 0;
263 for (std::size_t r = 0; r < R; ++r) {
264 if (N[r] < 0) throw InputError("pfqn_panaceald: negative population");
265 Ntl += N[r];
266 }
267 if (Ntl == 0) {
268 res.G = one;
269 res.lG = zero;
270 return res;
271 }
272 const std::size_t Nt = static_cast<std::size_t>(Ntl);
273
274 // The rate lattice, extended with its last column as the reference does.
275 Matrix<T> mux(M, Nt, one);
276 if (!mu.empty()) {
277 if (mu.rows() != M)
278 throw InputError("pfqn_panaceald: mu and L disagree on the station count");
279 if (mu.cols() == 0) throw InputError("pfqn_panaceald: mu has no rate column");
280 for (std::size_t i = 0; i < M; ++i)
281 for (std::size_t k = 0; k < Nt; ++k)
282 mux(i, k) = mu(i, k < mu.cols() ? k : mu.cols() - 1);
283 }
284
285 // Type-3 rows: rate lattice exactly 1, 2, ..., Nt.
286 const double fineTol = 1e-8; // GlobalConstants.FineTol
287 std::vector<bool> isIS(M, false);
288 for (std::size_t i = 0; i < M; ++i) {
289 bool all = true;
290 for (std::size_t k = 0; k < Nt && all; ++k)
291 if (std::fabs(num_traits<T>::to_double(mux(i, k)) - static_cast<double>(k + 1)) >=
292 fineTol)
293 all = false;
294 isIS[i] = all;
295 }
296
297 std::vector<T> Ztot = Zv;
298 std::vector<std::size_t> qrows;
299 for (std::size_t i = 0; i < M; ++i) {
300 if (isIS[i]) {
301 for (std::size_t r = 0; r < R; ++r) Ztot[r] += L(i, r);
302 } else {
303 qrows.push_back(i);
304 }
305 }
306 const std::size_t Mq = qrows.size();
307
308 for (std::size_t r = 0; r < R; ++r)
309 if (N[r] > 0 && !(Ztot[r] > zero))
310 // No infinite server on the route of a populated class: the
311 // expansion parameter rho_j0 is undefined and PANACEA does not
312 // apply at all, normal usage or not.
313 return decline(
314 "a populated class visits no infinite server, so the expansion parameter rho_j0 "
315 "is undefined");
316
317 const auto delay_only = [&]() {
318 T lG = zero;
319 for (std::size_t r = 0; r < R; ++r) {
320 const T nT = num_traits<T>::from_int(N[r]);
321 lG -= detail::num_factln<T>(nT);
322 lG += detail::pald_xlogy(nT, Ztot[r]);
323 }
324 res.lG = lG;
325 res.G = exp(lG);
326 return res;
327 };
328 if (Mq == 0) return delay_only();
329
330 Matrix<T> Lq(Mq, R), muq(Mq, Nt);
331 for (std::size_t a = 0; a < Mq; ++a) {
332 for (std::size_t r = 0; r < R; ++r) Lq(a, r) = L(qrows[a], r);
333 for (std::size_t k = 0; k < Nt; ++k) muq(a, k) = mux(qrows[a], k);
334 }
335 for (std::size_t a = 0; a < Mq; ++a)
336 for (std::size_t k = 0; k < Nt; ++k) {
337 const double v = num_traits<T>::to_double(muq(a, k));
338 if (!(v > 0.0) || !std::isfinite(v))
339 return decline("a load-dependent rate is not positive and finite");
340 }
341
342 // Offered load per queueing centre, sum_j K_j e_ji / rho_j0.
343 Matrix<T> r(Mq, R, zero);
344 for (std::size_t a = 0; a < Mq; ++a)
345 for (std::size_t j = 0; j < R; ++j)
346 if (Ztot[j] > zero) r(a, j) = T(Lq(a, j) / Ztot[j]);
347 std::vector<T> lambda(Mq, zero), muK(Mq, one), alpha(Mq, one);
348 for (std::size_t a = 0; a < Mq; ++a) {
349 T s = zero;
350 for (std::size_t j = 0; j < R; ++j) s += r(a, j) * num_traits<T>::from_int(N[j]);
351 lambda[a] = s;
352 muK[a] = muq(a, Nt - 1);
353 alpha[a] = T(one - lambda[a] / muK[a]);
354 if (!(alpha[a] > zero))
355 return decline(
356 "the model is not in normal usage (1 - lambda_i/mu_i(Ntot) <= 0 at some queueing "
357 "centre), so the {phi(n)} series diverges");
358 }
359
360 // log prod_{k=1}^{s} mu_i(k), s = 0..Nt.
361 std::vector<std::vector<T> > lPi(Mq, std::vector<T>(Nt + 1, zero));
362 for (std::size_t a = 0; a < Mq; ++a)
363 for (std::size_t s = 1; s <= Nt; ++s) lPi[a][s] = T(lPi[a][s - 1] + log(muq(a, s - 1)));
364
365 const std::size_t nmax = static_cast<std::size_t>(2 * (terms - 1));
366 Matrix<T> lpsi(Mq, nmax + 1, zero);
367 for (std::size_t a = 0; a < Mq; ++a)
368 for (std::size_t n = 0; n <= nmax; ++n)
369 lpsi(a, n) = detail::pald_logpsi(n, lambda[a], lPi[a], muK[a], alpha[a], Nt);
370
371 // Load dependence of the pseudonetwork centres:
372 // psi_i(n) = psi_i(0) n! / prod_{k=1}^{n} mups_i(k).
373 Matrix<T> mups(Mq, nmax > 0 ? nmax : 1, one);
374 for (std::size_t a = 0; a < Mq; ++a)
375 for (std::size_t n = 1; n <= nmax; ++n)
376 mups(a, n - 1) = exp(T(log(num_traits<T>::from_int(static_cast<long>(n))) +
377 lpsi(a, n - 1) - lpsi(a, n)));
378
379 // Expansion coefficients (5.4). The large parameter N cancels identically
380 // between beta_j = K_j/N, Gamma = N*r and the 1/N^n scaling, so the demands
381 // are taken as r and beta as N.
382 std::vector<T> A(3, zero);
383 A[0] = one;
384 if (terms >= 2) {
385 for (std::size_t j = 0; j < R; ++j) {
386 std::vector<int> m(R, 0);
387 m[j] = 2;
388 A[1] -= num_traits<T>::from_int(N[j]) * detail::pald_pseudonet(r, m, mups);
389 }
390 }
391 if (terms >= 3) {
392 for (std::size_t j = 0; j < R; ++j) {
393 std::vector<int> m(R, 0);
394 m[j] = 3;
396 detail::pald_pseudonet(r, m, mups);
397 m.assign(R, 0);
398 m[j] = 4;
399 A[2] += num_traits<T>::from_int(3) *
401 detail::pald_pseudonet(r, m, mups);
402 for (std::size_t k = 0; k < R; ++k) {
403 if (k == j) continue;
404 m.assign(R, 0);
405 m[j] = 2;
406 m[k] = 2;
408 num_traits<T>::from_int(N[k]) * detail::pald_pseudonet(r, m, mups);
409 }
410 }
411 }
412 T I = zero;
413 for (int t = 0; t < terms; ++t) I += A[t];
414 if (!(I > zero))
415 return decline("the truncated normal-usage series is not positive, so its logarithm is "
416 "undefined");
417
418 T lG = zero;
419 for (std::size_t j = 0; j < R; ++j) {
420 const T nT = num_traits<T>::from_int(N[j]);
421 lG -= detail::num_factln<T>(nT);
422 lG += detail::pald_xlogy(nT, Ztot[j]);
423 }
424 for (std::size_t a = 0; a < Mq; ++a) lG += lpsi(a, 0);
425 lG += log(I);
426 if (!std::isfinite(num_traits<T>::to_double(lG)))
427 return decline("the expansion evaluated to a non-finite logarithm");
428 res.lG = lG;
429 res.G = exp(lG);
430 return res;
431}
432
433/** Overload at the reference's default of three terms. */
434template <class T>
435PanaceaLdResult<T> pfqn_panaceald(const Matrix<T>& L, const std::vector<int>& N,
436 const std::vector<T>& Z, const Matrix<T>& mu) {
437 return pfqn_panaceald(L, N, Z, mu, 3);
438}
439
440} // namespace pfqn
441} // namespace line
442
443#endif // LINE_API_PFQN_PFQN_PANACEALD_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.
PanaceaLdResult< T > pfqn_panaceald(const Matrix< T > &L, const std::vector< int > &N, const std::vector< T > &Z, const Matrix< T > &mu, int terms)
PANACEA normal-usage asymptotic expansion for LOAD-DEPENDENT closed networks (Mitra and McKenna,...
Number-type abstraction for the templated API port.
Shared scalar machinery for the integration / asymptotic members of the pfqn family (pfqn_le,...
Return value of pfqn_panaceald, mirroring [Gn, lGn] plus why it declined.
bool normalUsage
false wherever the reference returns NaN
const char * reason
which condition declined; nullptr when it applies