LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_explicit_ld.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_LD_H
6#define LINE_API_PFQN_PFQN_EXPLICIT_LD_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Explicit closed-form normalizing constant of a multiclass LIMITED LOAD-DEPENDENT
12 * network.
13 *
14 * Templated port of matlab/src/api/pfqn/pfqn_explicit_ld.m. Load-dependent
15 * counterpart of pfqn_explicit: it evaluates the same divided-difference form of
16 * G. Casale, "Accelerating Performance Inference over Closed Systems by
17 * Asymptotic Methods", ACM SIGMETRICS 2017, Corollary 3.2,
18 *
19 * G(N) = sum_{0<=t<=N} (-1)^(|N|-|t|)/(N_1!...N_R!) prod_r C(N_r,t_r) h_t(|N|)
20 *
21 * but substitutes for the single-class constant h_t(|N|) the limited
22 * load-dependent closed form of G. Casale, P. G. Harrison, W. H. Ong,
23 * "Facilitating Load-Dependent Queueing Analysis Through Factorization",
24 * Perform. Eval. 2021, Theorem 1, Eq. (8),
25 *
26 * h_theta(N) = sum_{0<=v<s} g_sigma(N-|v|) prod_k phi_k(v_k)
27 * phi_k(v_k) = theta_k^v_k / prod_{t=1..v_k} alpha_k(t) * (1 - alpha_k(v_k)/alpha_k(s_k))
28 *
29 * at the induced demands theta_k(t) = sum_r t_r L(k,r). Here alpha_k(.) = mu(k,.)
30 * is the load-dependent scaling of station k, s_k the population past which it
31 * stays constant, sigma_k = theta_k/alpha_k(s_k) the SCALED demands, and g_sigma
32 * the FIXED-RATE single-class constant at those scaled demands, which is exactly
33 * what pfqn_explicit evaluates in closed form (Eqs. 15 and 16). The result is
34 * explicit throughout, with no recursion over population; pfqn_gldsingle is the
35 * same constant by an O(M|N|^2) recursion instead.
36 *
37 * TWO CONVENTIONS OF THEOREM 1 ARE NOT THOSE OF THE EQUILIBRIUM DISTRIBUTION.
38 * alpha_k(0) is taken as ZERO inside the bracket of phi_k, so that phi_k(0) = 1,
39 * even though the state probabilities use alpha_k(0) = 1; and g_sigma(n) = 0 for
40 * n < 0, which caps the outer sum at |v| <= |N|. With alpha_k(n) = min(n,s_k) the
41 * expression collapses to Gordon's multi-server formula, Oper. Res. 38(5), 1990,
42 * Eq. (29), but unlike that one it needs neither a multi-server shape nor
43 * distinct scaled demands.
44 *
45 * LIMITED LOAD DEPENDENCE. Theorem 1 holds for any s_k with
46 * alpha_k(n) = alpha_k(s_k) for all n >= s_k, and a LARGER s_k is always
47 * admissible, so s_k is detected here as the smallest index whose value the tail
48 * of mu(k,:) repeats to within tol. A station whose rates never settle (an
49 * infinite server, mu(k,n) = n) gets s_k = |N|, which is still exact: populations
50 * above |N| do not occur, so redefining alpha_k there changes nothing. It is
51 * merely expensive, since the inner sum costs prod_k s_k terms, capped by
52 * |v| <= |N|. Think time is not admissible: a delay would have to enter g_sigma,
53 * whose closed form covers queues only.
54 *
55 * ARITHMETIC. Both sums alternate in sign with terms far larger than the result,
56 * so they are evaluated as SIGNED log-sum-exps, and the routine is gated on
57 * num_traits<T>::has_transcendental exactly as pfqn_explicit is; a caller that
58 * wants the same constant in exact arithmetic wants pfqn_gld. phi_k is
59 * sign-definite when alpha_k increases, as a multi-server station does, and
60 * changes sign where alpha_k decreases, so a decreasing rate function costs
61 * digits in the inner sum too.
62 */
63
64#include <algorithm>
65#include <cmath>
66#include <cstddef>
67#include <limits>
68#include <string>
69#include <vector>
70
72#include "line/num/number.h"
73#include "line/util/error.h"
74#include "line/util/matrix.h"
75
76namespace line {
77namespace pfqn {
78
79namespace detail {
80
81/**
82 * Theorem 1 of Casale-Harrison-Ong (2021), Eq. (8): the single-class limited
83 * load-dependent constant at induced demands th and total population Nt, as the
84 * finite sum over 0 <= v < s of the fixed-rate constant at the scaled demands
85 * th/alpha(s), one population level lower for every job held back by v.
86 */
87template <class T>
88SignedLse<T> explicit_hlld(const std::vector<T>& th, std::size_t M, long Nt,
89 const std::vector<T>& alphaS, const std::vector<int>& vcap,
90 const std::vector<std::vector<T> >& lcum,
91 const std::vector<std::vector<T> >& lbr,
92 const std::vector<std::vector<double> >& sbr, const std::string& expr,
93 double tol) {
94 using std::log;
95 const T zero = num_traits<T>::from_int(0);
96 const double dinf = std::numeric_limits<double>::infinity();
97 std::vector<T> sigma(M), lth(M);
98 for (std::size_t i = 0; i < M; ++i) {
99 sigma[i] = T(th[i] / alphaS[i]);
100 lth[i] = (th[i] > zero) ? T(log(th[i])) : num_traits<T>::from_double(-dinf);
101 }
102 std::vector<T> lterm;
103 std::vector<double> sterm;
104 double lossDigits = 0.0;
105 std::vector<int> v(M, 0);
106 while (true) {
107 long nv = 0;
108 for (std::size_t i = 0; i < M; ++i) nv += v[i];
109 if (nv <= Nt) {
110 T lval = zero;
111 double sval = 1.0;
112 bool dead = false;
113 for (std::size_t i = 0; i < M; ++i) {
114 const int vi = v[i];
115 if (vi > 0) {
116 if (!std::isfinite(num_traits<T>::to_double(lth[i]))) {
117 // theta_k = 0 kills every v_k>0, and 0^0=1 keeps v_k=0
118 dead = true;
119 break;
120 }
121 // kept inside the guard because 0*(-Inf) is NaN, not 0
122 lval = T(lval + num_traits<T>::from_int(vi) * lth[i]);
123 }
124 lval = T(lval - lcum[i][vi] + lbr[i][vi]);
125 sval *= sbr[i][vi];
126 }
127 if (!dead && sval != 0.0 && std::isfinite(num_traits<T>::to_double(lval))) {
128 T lg = zero;
129 int sg = 1;
130 double dl = 0.0;
131 if (Nt - nv > 0) {
132 // g_sigma(0) = 1 by definition, so the Nt == nv case is left
133 // exact: reading it off the partial fraction instead would
134 // spend digits on an alternating sum of known value.
135 const SignedLse<T> g =
136 (expr == "distinct")
137 ? explicit_gdistinct(sigma, num_traits<T>::from_int(Nt - nv), M)
138 : explicit_grepeated(sigma, num_traits<T>::from_int(Nt - nv), M, tol);
139 lg = g.lS;
140 sg = g.sgn;
141 dl = g.lossDigits;
142 }
143 lossDigits = std::max(lossDigits, dl);
144 if (sg != 0) {
145 lterm.push_back(T(lval + lg));
146 sterm.push_back(sval * static_cast<double>(sg));
147 }
148 }
149 }
150 std::size_t i = M;
151 while (i > 0 && v[i - 1] == vcap[i - 1]) v[--i] = 0;
152 if (i == 0) break;
153 ++v[i - 1];
154 }
155 SignedLse<T> h = explicit_signed_logsumexp(lterm, sterm);
156 h.lossDigits = std::max(lossDigits, h.lossDigits);
157 return h;
158}
159
160} // namespace detail
161
162/**
163 * @brief Explicit closed-form normalizing constant of a multiclass LIMITED
164 * LOAD-DEPENDENT network.
165 *
166 * @param L (M x R) service demands
167 * @param N population per class
168 * @param mu (M x >= sum(N)) load-dependent rate lattice, alpha_i(j) = mu(i,j-1);
169 * an empty matrix means all ones
170 * @param tol relative tolerance declaring two scaled demands redundant, and the
171 * rate tail constant
172 * @param method "auto", "distinct" (force Eq. 15) or "repeated" (force Eq. 16)
173 * @param maxloss cancellation budget in decimal digits; a finite value turns the
174 * overrun into a silent REFUSAL (valid = false) for callers that
175 * hold a fallback, the default keeps the result whatever it costs
176 */
177template <class T>
178ExplicitResult<T> pfqn_explicit_ld(const Matrix<T>& L, const std::vector<int>& N,
179 const Matrix<T>& mu,
180 double tol = std::numeric_limits<double>::epsilon(),
181 const std::string& method = "auto",
182 double maxloss = std::numeric_limits<double>::infinity()) {
184 "pfqn_explicit_ld requires transcendental arithmetic: the alternating sums are "
185 "carried as signed log-sum-exps so that no intermediate can overflow. Use "
186 "pfqn_gld for the same constant in exact arithmetic");
187 using std::exp;
188 using std::log;
189 const T zero = num_traits<T>::from_int(0);
190 const T one = num_traits<T>::from_int(1);
191 const double dinf = std::numeric_limits<double>::infinity();
192 const std::size_t R = N.size();
193
195 res.method = "distinct";
196 res.lG = num_traits<T>::from_double(-dinf);
197 res.G = zero;
198
199 if (method != "auto" && method != "distinct" && method != "repeated")
200 throw InputError(
201 "pfqn_explicit_ld: unrecognized method, use 'auto', 'distinct' (Eq. 15) or 'repeated' "
202 "(Eq. 16)");
203 long Nsum = 0;
204 for (int v : N) Nsum += v;
205 if (Nsum < 0) return res;
206 if (Nsum == 0) {
207 res.lG = zero;
208 res.G = one;
209 return res;
210 }
211 if (L.rows() == 0 || L.cols() == 0) return res;
212 if (static_cast<std::size_t>(L.cols()) != R)
213 throw InputError("pfqn_explicit_ld: the demand matrix must have one column per class of N");
214 const std::size_t M = static_cast<std::size_t>(L.rows());
215 for (std::size_t i = 0; i < M; ++i)
216 for (std::size_t r = 0; r < R; ++r)
217 if (L(i, r) < zero)
218 throw InputError("pfqn_explicit_ld: the demand matrix must be nonnegative");
219 const std::size_t Nt = static_cast<std::size_t>(Nsum);
220
221 // ---- the rate lattice, defaulting to a fixed-rate model ----
222 std::vector<std::vector<T> > alpha(M, std::vector<T>(Nt, one));
223 if (mu.rows() != 0 && mu.cols() != 0) {
224 if (static_cast<std::size_t>(mu.rows()) != M)
225 throw InputError(
226 "pfqn_explicit_ld: the load-dependent rate matrix must have one row per station "
227 "of L");
228 if (static_cast<std::size_t>(mu.cols()) < Nt)
229 throw InputError(
230 "pfqn_explicit_ld: the load-dependent rate matrix must have at least sum(N) "
231 "columns");
232 for (std::size_t i = 0; i < M; ++i)
233 for (std::size_t k = 0; k < Nt; ++k) {
234 alpha[i][k] = mu(i, k);
235 if (!(alpha[i][k] > zero))
236 throw InputError(
237 "pfqn_explicit_ld: the load-dependent rates must be strictly positive");
238 }
239 }
240
241 // ---- s_k: the smallest index whose value the tail of the rate row repeats ----
242 // Any larger s_k also satisfies alpha_k(n)=alpha_k(s_k) for n>=s_k, so a missed
243 // tie only adds terms; a false tie would be a wrong answer, hence the strict tol.
244 std::vector<std::size_t> s(M, Nt);
245 std::vector<T> alphaS(M, one);
246 for (std::size_t i = 0; i < M; ++i) {
247 const T tail = alpha[i][Nt - 1];
248 const double atail = std::fabs(num_traits<T>::to_double(tail));
249 for (std::size_t n = Nt; n > 1; --n) {
250 const T d = T(alpha[i][n - 2] - tail);
251 if (std::fabs(num_traits<T>::to_double(d)) <= tol * std::max(atail, 1.0))
252 s[i] = n - 1;
253 else
254 break;
255 }
256 alphaS[i] = alpha[i][s[i] - 1];
257 }
258
259 // ---- per-station phi tables, in the log domain, indexed by v_k = 0..s_k-1 ----
260 std::vector<std::vector<T> > lcum(M), lbr(M);
261 std::vector<std::vector<double> > sbr(M);
262 std::vector<int> vcap(M, 0);
263 for (std::size_t i = 0; i < M; ++i) {
264 lcum[i].resize(s[i]);
265 lbr[i].resize(s[i]);
266 sbr[i].resize(s[i]);
267 for (std::size_t v = 0; v < s[i]; ++v) {
268 lcum[i][v] = (v == 0) ? zero : T(lcum[i][v - 1] + log(alpha[i][v - 1]));
269 const T br = T(one - (((v == 0) ? zero : alpha[i][v - 1]) / alphaS[i]));
270 if (br == zero) {
271 lbr[i][v] = num_traits<T>::from_double(-dinf);
272 sbr[i][v] = 0.0;
273 } else if (br > zero) {
274 lbr[i][v] = log(br);
275 sbr[i][v] = 1.0;
276 } else {
277 lbr[i][v] = log(T(zero - br));
278 sbr[i][v] = -1.0;
279 }
280 }
281 // g_sigma vanishes below zero population, Eq. (8) caps |v| <= |N|
282 vcap[i] = static_cast<int>(std::min<std::size_t>(s[i] - 1, Nt));
283 }
284
285 // ---- redundancy scan: are the SCALED induced demands pairwise distinct? ----
286 // The scan MUST form sigma exactly as explicit_hlld does, (L*t)/alphaS and
287 // not (L/alphaS)*t: the two orderings differ in the last ulp, so an exact
288 // tie can clear an eps-relative gap under one and not the other, and
289 // Eq. (15) would then divide by that ulp.
290 const auto induced = [&](const std::vector<int>& t) {
291 std::vector<T> th(M, zero);
292 for (std::size_t i = 0; i < M; ++i)
293 for (std::size_t r = 0; r < R; ++r)
294 th[i] = T(th[i] + L(i, r) * num_traits<T>::from_int(t[r]));
295 return th;
296 };
297 const auto scaled = [&](const std::vector<int>& t) {
298 std::vector<T> th = induced(t);
299 for (std::size_t i = 0; i < M; ++i) th[i] = T(th[i] / alphaS[i]);
300 return th;
301 };
302 const auto redundant_at = [&](std::vector<T> th) {
303 std::sort(th.begin(), th.end(), [](const T& a, const T& b) { return a < b; });
304 const T scale = th.back();
305 // scale == 0 leaves every scaled demand at zero, so the term takes no part
306 // in the sum
307 if (!(scale > zero)) return false;
308 const T gap = T(num_traits<T>::from_double(tol) * scale);
309 for (std::size_t i = 1; i < th.size(); ++i)
310 if (!(T(th[i] - th[i - 1]) > gap)) return true;
311 return false;
312 };
313 bool isRedundant = false;
314 if (R == 1) {
315 // the scaled demands at t are t*sigma, so both the tie structure and the
316 // relative tolerance are those of sigma itself, at every t at once
317 std::vector<T> th(M);
318 for (std::size_t i = 0; i < M; ++i) th[i] = T(L(i, 0) / alphaS[i]);
319 isRedundant = redundant_at(th);
320 } else {
321 std::vector<int> t(R, 0);
322 while (true) {
323 long ts = 0;
324 for (int val : t) ts += val;
325 if (ts > 0 && redundant_at(scaled(t))) {
326 isRedundant = true;
327 break;
328 }
329 std::size_t r = R;
330 while (r > 0 && t[r - 1] == N[r - 1]) t[--r] = 0;
331 if (r == 0) break;
332 ++t[r - 1];
333 }
334 }
335 std::string expr = method;
336 if (expr == "auto") {
337 expr = isRedundant ? "repeated" : "distinct";
338 } else if (expr == "distinct" && isRedundant) {
339 throw InputError(
340 "pfqn_explicit_ld: Eq. (15) requires pairwise distinct scaled demands, but two of them "
341 "agree to within tol. Use 'auto' or 'repeated'");
342 }
343 res.method = expr;
344
345 detail::SignedLse<T> total;
346 if (R == 1) {
347 // ---- single class: the divided difference is the identity ----
348 std::vector<T> th(M);
349 for (std::size_t i = 0; i < M; ++i) th[i] = L(i, 0);
350 total = detail::explicit_hlld(th, M, Nsum, alphaS, vcap, lcum, lbr, sbr, expr, tol);
351 } else {
352 // ---- outer divided-difference sum over 0 <= t <= N ----
353 std::vector<T> lterm;
354 std::vector<double> sterm;
355 double innerLoss = 0.0;
356 std::vector<int> t(R, 0);
357 while (true) {
358 long ts = 0;
359 for (int val : t) ts += val;
360 if (ts > 0) {
361 std::vector<T> th = induced(t);
362 T thmax = th[0];
363 for (const T& val : th)
364 if (val > thmax) thmax = val;
365 if (thmax > zero) {
366 const detail::SignedLse<T> h = detail::explicit_hlld(
367 th, M, Nsum, alphaS, vcap, lcum, lbr, sbr, expr, tol);
368 innerLoss = std::max(innerLoss, h.lossDigits);
369 if (h.sgn != 0) {
370 T l = h.lS;
371 for (std::size_t r = 0; r < R; ++r) {
372 l = T(l - detail::num_factln<T>(num_traits<T>::from_int(t[r])));
373 l = T(l -
374 detail::num_factln<T>(num_traits<T>::from_int(N[r] - t[r])));
375 }
376 lterm.push_back(l);
377 sterm.push_back(static_cast<double>(h.sgn) *
378 (((Nsum - ts) % 2 == 0) ? 1.0 : -1.0));
379 }
380 }
381 }
382 std::size_t r = R;
383 while (r > 0 && t[r - 1] == N[r - 1]) t[--r] = 0;
384 if (r == 0) break;
385 ++t[r - 1];
386 }
387 total = detail::explicit_signed_logsumexp(lterm, sterm);
388 total.lossDigits = std::max(total.lossDigits, innerLoss);
389 }
390 res.lossDigits = total.lossDigits;
391
392 // A caller that named a cancellation budget has a fallback and wants a verdict,
393 // not a warning: refuse quietly. lossDigits is infinite when the sum vanished
394 // identically, which is a total loss rather than a legitimate G = 0.
395 if (std::isfinite(maxloss) && (total.sgn < 0 || total.lossDigits > maxloss)) {
396 res.valid = false;
397 res.lG = num_traits<T>::from_double(std::numeric_limits<double>::quiet_NaN());
398 res.G = res.lG;
399 return res;
400 }
401 if (total.sgn == 0) {
402 res.lG = num_traits<T>::from_double(-dinf);
403 res.G = zero;
404 return res;
405 }
406 if (total.sgn < 0) {
407 // Double precision is exhausted by cancellation; the sign itself is wrong,
408 // so there is no result to hand back.
409 res.valid = false;
410 res.lG = num_traits<T>::from_double(std::numeric_limits<double>::quiet_NaN());
411 res.G = res.lG;
412 return res;
413 }
414 res.lG = total.lS;
415 res.G = exp(res.lG);
416 return res;
417}
418
419} // namespace pfqn
420} // namespace line
421
422#endif // LINE_API_PFQN_PFQN_EXPLICIT_LD_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_ld(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &mu, 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 LIMITED LOAD-DEPENDENT network.
Number-type abstraction for the templated API port.
Explicit closed-form normalizing constant of a multiclass closed network.
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 ...