LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_explicit.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_EXPLICIT_H
6#define LINE_API_PFQN_PFQN_EXPLICIT_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Explicit closed-form normalizing constant of a multiclass closed network.
12 *
13 * Templated port of matlab/src/api/pfqn/pfqn_explicit.m, i.e. Eqs. (15) and
14 * (16) of G. Casale, "Accelerating Performance Inference over Closed Systems by
15 * Asymptotic Methods", ACM SIGMETRICS 2017. Both instantiate the
16 * divided-difference form of Corollary 3.2,
17 *
18 * G(N) = sum_{0<=t<=N} (-1)^(|N|-|t|)/(N_1!...N_R!) prod_r C(N_r,t_r) g_t(|N|)
19 *
20 * by substituting a closed form for the single-class constant g_t(|N|) at the
21 * induced demands theta_k(t) = sum_r t_r L(k,r). Eq. (15) is Gordon's partial
22 * fraction and needs the induced demands PAIRWISE DISTINCT; Eq. (16) is the
23 * general partial-fraction expansion over the distinct values and their
24 * multiplicities, and reduces term by term to Eq. (15) when every multiplicity
25 * is one. The choice is automatic.
26 *
27 * SINGLE CLASS. At R=1 the multiclass constant IS the single-class constant at
28 * demands L, so the outer sum is skipped: g_t(N) = t^N g_1(N) and
29 * sum_t (-1)^(N-t) t^N/(t!(N-t)!) = S(N,N) = 1. Running the difference anyway
30 * would add N alternating terms, and their cancellation, to a closed form that
31 * carries none of them. What is left is O(K^2) work at any population, which is
32 * why the single-class route is the cheap one on large populations.
33 *
34 * ARITHMETIC. Both expressions alternate in sign with terms far larger than the
35 * result, so they are evaluated as SIGNED log-sum-exps: that removes the
36 * floating-point RANGE problem but not the cancellation, which is what makes
37 * multiprecision arithmetic necessary on all but small models. The routine is
38 * therefore gated on num_traits<T>::has_transcendental; a caller that wants the
39 * same constant in exact arithmetic wants pfqn_ca.
40 *
41 * ADMISSIBILITY. Single-server load-independent queues only: infinite servers
42 * need the integral form of Corollary 3.4 and load-dependent rates need
43 * pfqn_explicit_ld, which keeps this closed form as its inner kernel.
44 */
45
46#include <algorithm>
47#include <cmath>
48#include <cstddef>
49#include <limits>
50#include <string>
51#include <vector>
52
54#include "line/num/number.h"
55#include "line/util/error.h"
56#include "line/util/matrix.h"
57
58namespace line {
59namespace pfqn {
60
61/** Return value of pfqn_explicit, mirroring [lG, G, method, lossDigits]. */
62template <class T>
64 T lG; ///< logarithm of the normalizing constant
65 T G; ///< the normalizing constant
66 std::string method; ///< expression used, "distinct" (Eq. 15) or "repeated" (Eq. 16)
67 double lossDigits = 0.0; ///< decimal digits lost to cancellation
68 /// False when a caller's cancellation budget was exceeded: lG and G are then
69 /// meaningless and the caller is expected to fall back.
70 bool valid = true;
71};
72
73namespace detail {
74
75/** Logarithm of the binomial coefficient C(n,m); MATLAB nchoosekln.m. */
76template <class T>
77T explicit_nchoosekln(const T& n, const T& m) {
78 const T one = num_traits<T>::from_int(1);
79 return T(num_lgamma<T>(T(one + n)) - num_lgamma<T>(T(one + n - m)) -
80 num_lgamma<T>(T(one + m)));
81}
82
83/** Signed log-sum-exp of S = sum_i s_i exp(l_i): log|S|, sign(S), digits lost. */
84template <class T>
85struct SignedLse {
86 T lS;
87 int sgn;
88 double lossDigits;
89};
90
91template <class T>
92SignedLse<T> explicit_signed_logsumexp(const std::vector<T>& lterm,
93 const std::vector<double>& sterm) {
94 using std::exp;
95 using std::log;
96 const double dinf = std::numeric_limits<double>::infinity();
97 SignedLse<T> out;
98 out.lS = num_traits<T>::from_double(-dinf);
99 out.sgn = 0;
100 out.lossDigits = 0.0;
101 std::vector<std::size_t> keep;
102 for (std::size_t i = 0; i < lterm.size(); ++i) {
103 const double li = num_traits<T>::to_double(lterm[i]);
104 if (std::isfinite(li) && sterm[i] != 0.0) keep.push_back(i);
105 }
106 if (keep.empty()) return out;
107 T a = lterm[keep[0]];
108 for (std::size_t i : keep)
109 if (lterm[i] > a) a = lterm[i];
110 T s = num_traits<T>::from_int(0);
111 for (std::size_t i : keep)
112 s = T(s + num_traits<T>::from_double(sterm[i]) * exp(T(lterm[i] - a)));
113 const T zero = num_traits<T>::from_int(0);
114 if (s == zero) {
115 out.lossDigits = dinf;
116 return out;
117 }
118 out.sgn = (s > zero) ? 1 : -1;
119 const T abss = (s > zero) ? s : T(zero - s);
120 out.lS = T(a + log(abss));
121 // max(exp(l_i - a)) is 1, so -log10|s| is the shortfall of the sum against
122 // its largest term. Every one of the n terms carries a rounding error of
123 // order eps*max_term, so the digits actually lost are that shortfall PLUS
124 // log10(n); dropping the count understates the loss and lets a wrong answer
125 // past the guard.
126 out.lossDigits =
127 std::max(0.0, std::log10(static_cast<double>(keep.size()) /
128 std::fabs(num_traits<T>::to_double(abss))));
129 return out;
130}
131
132/**
133 * Eq. (14): single-class constant at pairwise distinct demands th, population
134 * Nt over K queues. A zero demand contributes nothing, which also realizes the
135 * 0/0 = 0 convention of Eq. (15) when the zero is repeated.
136 */
137template <class T>
138SignedLse<T> explicit_gdistinct(const std::vector<T>& th, const T& Nt, std::size_t K) {
139 using std::log;
140 const T zero = num_traits<T>::from_int(0);
141 const double dinf = std::numeric_limits<double>::infinity();
142 std::vector<T> lin(K, num_traits<T>::from_double(-dinf));
143 std::vector<double> sgv(K, 0.0);
144 for (std::size_t k = 0; k < K; ++k) {
145 if (!(th[k] > zero)) continue;
146 T acc = T(T(Nt + num_traits<T>::from_int(static_cast<long>(K) - 1)) * log(th[k]));
147 double sign = 1.0;
148 for (std::size_t i = 0; i < K; ++i) {
149 if (i == k) continue;
150 const T d = T(th[k] - th[i]);
151 const T ad = (d > zero) ? d : T(zero - d);
152 acc = T(acc - log(ad));
153 sign *= (d > zero) ? 1.0 : ((d < zero) ? -1.0 : 0.0);
154 }
155 lin[k] = acc;
156 sgv[k] = sign;
157 }
158 return explicit_signed_logsumexp(lin, sgv);
159}
160
161/** Every Kp-vector r >= 0 with sum(r) = k, i.e. MATLAB multichoose(Kp,k). */
162inline void explicit_multichoose(std::size_t Kp, int k, std::vector<int>& current,
163 std::size_t idx, std::vector<std::vector<int> >& out) {
164 if (idx + 1 == Kp) {
165 current[idx] = k;
166 out.push_back(current);
167 return;
168 }
169 for (int i = 0; i <= k; ++i) {
170 current[idx] = i;
171 explicit_multichoose(Kp, k - i, current, idx + 1, out);
172 }
173}
174
175inline std::vector<std::vector<int> > explicit_multichoose(std::size_t Kp, int k) {
176 std::vector<std::vector<int> > out;
177 if (Kp == 0 || k < 0) return out;
178 std::vector<int> current(Kp, 0);
179 explicit_multichoose(Kp, k, current, 0, out);
180 return out;
181}
182
183/**
184 * Eq. (16): single-class constant at demands th of arbitrary multiplicity,
185 * population Nt over K queues. Demands within tol of each other, relatively to
186 * the largest one, are merged into one distinct value carrying their count.
187 */
188template <class T>
189SignedLse<T> explicit_grepeated(const std::vector<T>& th, const T& Nt, std::size_t K,
190 double tol) {
191 using std::log;
192 const T zero = num_traits<T>::from_int(0);
193 const T one = num_traits<T>::from_int(1);
194 const double dinf = std::numeric_limits<double>::infinity();
195 std::vector<T> ths(th);
196 std::sort(ths.begin(), ths.end(), [](const T& a, const T& b) { return a < b; });
197 T scale = ths.back();
198 if (!(scale > zero)) scale = one;
199 const T gap = T(num_traits<T>::from_double(tol) * scale);
200 // clusters of consecutive near-ties, represented by their centroid
201 std::vector<T> thd;
202 std::vector<int> m;
203 for (std::size_t i = 0; i < ths.size();) {
204 std::size_t j = i + 1;
205 while (j < ths.size() && !(T(ths[j] - ths[j - 1]) > gap)) ++j;
206 T sum = zero;
207 for (std::size_t q = i; q < j; ++q) sum = T(sum + ths[q]);
208 thd.push_back(T(sum / num_traits<T>::from_int(static_cast<long>(j - i))));
209 m.push_back(static_cast<int>(j - i));
210 i = j;
211 }
212 const std::size_t Kp = thd.size();
213 std::vector<T> lin;
214 std::vector<double> sgv;
215 for (std::size_t j = 0; j < Kp; ++j) {
216 if (!(thd[j] > zero)) {
217 // the exponent Nt+K-m_j is at least Nt>=1, so a zero cluster
218 // contributes nothing
219 continue;
220 }
221 const T louter =
222 T(T(Nt + num_traits<T>::from_int(static_cast<long>(K) - m[j])) * log(thd[j]));
223 const double souter = ((m[j] - 1) % 2 == 0) ? 1.0 : -1.0;
224 const std::vector<std::vector<int> > rs = explicit_multichoose(Kp, m[j] - 1);
225 for (std::size_t i = 0; i < rs.size(); ++i) {
226 const std::vector<int>& r = rs[i];
227 T lval = T(louter + explicit_nchoosekln<T>(T(Nt + num_traits<T>::from_int(r[j])),
228 num_traits<T>::from_int(r[j])));
229 double sval = souter * ((r[j] % 2 == 0) ? 1.0 : -1.0);
230 bool vanished = false;
231 for (std::size_t k = 0; k < Kp; ++k) {
232 if (k == j) continue;
233 lval = T(lval + explicit_nchoosekln<T>(
234 num_traits<T>::from_int(m[k] + r[k] - 1),
235 num_traits<T>::from_int(r[k])));
236 if (r[k] > 0) {
237 if (!(thd[k] > zero)) {
238 // theta_k^r_k vanishes; 0^0 = 1 is the r_k = 0 case
239 vanished = true;
240 break;
241 }
242 lval = T(lval + num_traits<T>::from_int(r[k]) * log(thd[k]));
243 }
244 const T dd = T(thd[j] - thd[k]);
245 const T add = (dd > zero) ? dd : T(zero - dd);
246 lval = T(lval - num_traits<T>::from_int(m[k] + r[k]) * log(add));
247 if (!(dd > zero) && ((m[k] + r[k]) % 2 != 0)) sval = -sval;
248 }
249 if (vanished) {
250 lin.push_back(num_traits<T>::from_double(-dinf));
251 sgv.push_back(0.0);
252 } else {
253 lin.push_back(lval);
254 sgv.push_back(sval);
255 }
256 }
257 }
258 return explicit_signed_logsumexp(lin, sgv);
259}
260
261} // namespace detail
262
263/**
264 * @brief Explicit closed-form normalizing constant of a multiclass closed
265 * network.
266 *
267 * @param L (K x R) service demands of single-server load-independent queues
268 * @param N population per class
269 * @param tol relative tolerance declaring two induced demands redundant
270 * @param method "auto", "distinct" (force Eq. 15) or "repeated" (force Eq. 16)
271 * @param maxloss cancellation budget in decimal digits; a finite value turns the
272 * overrun into a silent REFUSAL (valid = false) for callers that
273 * hold a fallback, the default keeps the result whatever it costs
274 */
275template <class T>
276ExplicitResult<T> pfqn_explicit(const Matrix<T>& L, const std::vector<int>& N,
277 double tol = std::numeric_limits<double>::epsilon(),
278 const std::string& method = "auto",
279 double maxloss = std::numeric_limits<double>::infinity()) {
281 "pfqn_explicit requires transcendental arithmetic: the alternating sums are "
282 "carried as signed log-sum-exps so that no intermediate can overflow. Use "
283 "pfqn_ca for the same constant in exact arithmetic");
284 using std::exp;
285 using std::log;
286 const T zero = num_traits<T>::from_int(0);
287 const double dinf = std::numeric_limits<double>::infinity();
288 const std::size_t R = N.size();
289
291 res.method = "distinct";
292 res.lG = num_traits<T>::from_double(-dinf);
293 res.G = zero;
294
295 if (method != "auto" && method != "distinct" && method != "repeated")
296 throw InputError(
297 "pfqn_explicit: unrecognized method, use 'auto', 'distinct' (Eq. 15) or 'repeated' "
298 "(Eq. 16)");
299 long Nsum = 0;
300 for (int v : N) Nsum += v;
301 if (Nsum < 0) return res;
302 if (Nsum == 0) {
303 res.lG = zero;
304 res.G = num_traits<T>::from_int(1);
305 return res;
306 }
307 if (L.rows() == 0 || L.cols() == 0) return res;
308 if (static_cast<std::size_t>(L.cols()) != R)
309 throw InputError("pfqn_explicit: the demand matrix must have one column per class of N");
310 const std::size_t K = static_cast<std::size_t>(L.rows());
311 for (std::size_t i = 0; i < K; ++i)
312 for (std::size_t r = 0; r < R; ++r)
313 if (L(i, r) < zero)
314 throw InputError("pfqn_explicit: the demand matrix must be nonnegative");
315 const T Nt = num_traits<T>::from_int(Nsum);
316
317 // ---- redundancy scan: are the induced demands pairwise distinct at every t? ----
318 const auto induced = [&](const std::vector<int>& t) {
319 std::vector<T> th(K, zero);
320 for (std::size_t i = 0; i < K; ++i)
321 for (std::size_t r = 0; r < R; ++r)
322 th[i] = T(th[i] + L(i, r) * num_traits<T>::from_int(t[r]));
323 return th;
324 };
325 const auto redundant_at = [&](std::vector<T> th) {
326 std::sort(th.begin(), th.end(), [](const T& a, const T& b) { return a < b; });
327 const T scale = th.back();
328 // scale == 0 leaves every induced demand at zero, so g_t(|N|) = 0 at
329 // sum(N) > 0 and the term takes no part in the sum
330 if (!(scale > zero)) return false;
331 const T gap = T(num_traits<T>::from_double(tol) * scale);
332 for (std::size_t i = 1; i < th.size(); ++i)
333 if (!(T(th[i] - th[i - 1]) > gap)) return true;
334 return false;
335 };
336 bool isRedundant = false;
337 if (R == 1) {
338 // the induced demands at t are t*L, so both the tie structure and the
339 // relative tolerance are those of L itself, at every t at once
340 std::vector<T> th(K);
341 for (std::size_t i = 0; i < K; ++i) th[i] = L(i, 0);
342 isRedundant = redundant_at(th);
343 } else {
344 std::vector<int> t(R, 0);
345 while (true) {
346 long ts = 0;
347 for (int v : t) ts += v;
348 if (ts > 0 && redundant_at(induced(t))) {
349 isRedundant = true;
350 break;
351 }
352 std::size_t r = R;
353 while (r > 0 && t[r - 1] == N[r - 1]) t[--r] = 0;
354 if (r == 0) break;
355 ++t[r - 1];
356 }
357 }
358 std::string expr = method;
359 if (expr == "auto") {
360 expr = isRedundant ? "repeated" : "distinct";
361 } else if (expr == "distinct" && isRedundant) {
362 throw InputError(
363 "pfqn_explicit: Eq. (15) requires pairwise distinct induced demands, but two of them "
364 "agree to within tol. Use 'auto' or 'repeated'");
365 }
366 res.method = expr;
367
368 detail::SignedLse<T> total;
369 if (R == 1) {
370 // ---- single class: the divided difference is the identity, evaluate g ----
371 std::vector<T> th(K);
372 for (std::size_t i = 0; i < K; ++i) th[i] = L(i, 0);
373 total = (expr == "distinct") ? detail::explicit_gdistinct(th, Nt, K)
374 : detail::explicit_grepeated(th, Nt, K, tol);
375 } else {
376 // ---- outer divided-difference sum over 0 <= t <= N ----
377 std::vector<T> lterm;
378 std::vector<double> sterm;
379 double innerLoss = 0.0;
380 std::vector<int> t(R, 0);
381 while (true) {
382 long ts = 0;
383 for (int v : t) ts += v;
384 if (ts > 0) {
385 std::vector<T> th = induced(t);
386 T thmax = th[0];
387 for (const T& v : th)
388 if (v > thmax) thmax = v;
389 if (thmax > zero) {
390 const detail::SignedLse<T> g =
391 (expr == "distinct") ? detail::explicit_gdistinct(th, Nt, K)
392 : detail::explicit_grepeated(th, Nt, K, tol);
393 innerLoss = std::max(innerLoss, g.lossDigits);
394 if (g.sgn != 0) {
395 T l = g.lS;
396 for (std::size_t r = 0; r < R; ++r) {
397 l = T(l - detail::num_factln<T>(num_traits<T>::from_int(t[r])));
398 l = T(l - detail::num_factln<T>(
399 num_traits<T>::from_int(N[r] - t[r])));
400 }
401 lterm.push_back(l);
402 sterm.push_back(static_cast<double>(g.sgn) *
403 (((Nsum - ts) % 2 == 0) ? 1.0 : -1.0));
404 }
405 }
406 }
407 std::size_t r = R;
408 while (r > 0 && t[r - 1] == N[r - 1]) t[--r] = 0;
409 if (r == 0) break;
410 ++t[r - 1];
411 }
412 total = detail::explicit_signed_logsumexp(lterm, sterm);
413 total.lossDigits = std::max(total.lossDigits, innerLoss);
414 }
415 res.lossDigits = total.lossDigits;
416
417 // A caller that named a cancellation budget has a fallback and wants a
418 // verdict, not a warning: refuse quietly. lossDigits is infinite when the
419 // sum vanished identically, which is a total loss rather than a legitimate
420 // G = 0.
421 if (std::isfinite(maxloss) && (total.sgn < 0 || total.lossDigits > maxloss)) {
422 res.valid = false;
423 res.lG = num_traits<T>::from_double(std::numeric_limits<double>::quiet_NaN());
424 res.G = res.lG;
425 return res;
426 }
427 if (total.sgn == 0) {
428 res.lG = num_traits<T>::from_double(-dinf);
429 res.G = zero;
430 return res;
431 }
432 if (total.sgn < 0) {
433 // Double precision is exhausted by cancellation; the sign itself is
434 // wrong, so there is no result to hand back.
435 res.valid = false;
436 res.lG = num_traits<T>::from_double(std::numeric_limits<double>::quiet_NaN());
437 res.G = res.lG;
438 return res;
439 }
440 res.lG = total.lS;
441 res.G = exp(res.lG);
442 return res;
443}
444
445} // namespace pfqn
446} // namespace line
447
448#endif // LINE_API_PFQN_PFQN_EXPLICIT_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
The exception types the port throws.
Dense matrix and non-owning view.
ExplicitResult< T > pfqn_explicit(const Matrix< T > &L, const std::vector< int > &N, double tol=std::numeric_limits< double >::epsilon(), const std::string &method="auto", double maxloss=std::numeric_limits< double >::infinity())
Explicit closed-form normalizing constant of a multiclass closed network.
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_explicit, mirroring [lG, G, method, lossDigits].
std::string method
expression used, "distinct" (Eq. 15) or "repeated" (Eq. 16)
T G
the normalizing constant
T lG
logarithm of the normalizing constant
double lossDigits
decimal digits lost to cancellation
bool valid
False when a caller's cancellation budget was exceeded: lG and G are then meaningless and the caller ...