LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_dnc.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_DNC_H
6#define LINE_API_PFQN_DNC_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Distinct-load Normalizing Constant (DNC) at a nonintegral population.
12 *
13 * Templated port of matlab/src/api/pfqn/pfqn_dnc.m.
14 *
15 * Normalizing constant and throughput of a single-class closed product-form
16 * network at a REAL-VALUED population, by partial-fraction inversion of the
17 * network generating function (Dowdy and Gordon 1984). With distinct loads
18 * x_1..x_G of multiplicities m_1..m_G the generating function
19 * prod_g (1 - x_g u)^{-m_g} expands as
20 *
21 * G(n) = sum_g sum_{j = 1..m_g} A_gj C(n+j-1, j-1) x_g^n,
22 *
23 * every term of which is an analytic function of n. Evaluating it at a real n
24 * therefore interpolates the integral normalizing constants exactly (it
25 * reproduces them at every integer) and gives a smooth throughput curve
26 * X(N) = G(N-1)/G(N) through the integral points, rather than the rounding or
27 * linear interpolation the paper compares against. For all-distinct loads the
28 * coefficients have the closed form A_g = prod_{l != g} x_g / (x_g - x_l), used
29 * directly; with repeated loads they are recovered from the M integral
30 * constants G(0..M-1), which determine them uniquely.
31 *
32 * Only the queueing part admits this continuation: the delay sequence Z^n / n!
33 * is entire and has no partial-fraction expansion, so a think time is rejected
34 * here. Use pfqn_nintmva for nonintegral populations with a delay.
35 *
36 * Reference: L. W. Dowdy, K. D. Gordon, "Algorithms for Nonintegral Degrees of
37 * Multiprogramming in Closed Queuing Networks", Performance Evaluation
38 * 4(1):19-28, 1984.
39 *
40 * Arithmetic: TRANSCENDENTAL. The partial-fraction series is evaluated in the
41 * log domain through gamma functions, so the exact backend is refused at
42 * compile time.
43 */
44
45#include <algorithm>
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/lu.h"
55#include "line/util/matrix.h"
56
57namespace line {
58namespace pfqn {
59
60/** Normalizing constant and throughput at a real-valued population. */
61template <class T>
62struct DncResult {
63 T X; ///< throughput G(N-1)/G(N); NaN where the continuation does not apply
64 T G; ///< normalizing constant at population N
65 T lG; ///< log of the normalizing constant
66};
67
68namespace detail {
69
70/**
71 * Partial-fraction series evaluated at a real population n. The continuation is
72 * analytic for n > -1; below that the binomial factor changes sign and the
73 * log-domain evaluation would lose it, so it is not extended there.
74 */
75template <class T>
76T dnc_eval(const T& n, const std::vector<T>& A, const std::vector<T>& u,
77 const std::vector<std::size_t>& node, const std::vector<long>& j) {
78 using std::exp;
79 using std::log;
80 const T minus_one = num_traits<T>::from_int(-1);
81 if (n <= minus_one) return std::numeric_limits<T>::quiet_NaN();
82 const T one = num_traits<T>::from_int(1);
84 for (std::size_t k = 0; k < A.size(); ++k) {
85 const T jT = num_traits<T>::from_int(j[k]);
86 const T e = T(num_lgamma<T>(T(n + jT)) - num_lgamma<T>(jT) - num_lgamma<T>(T(n + one)) +
87 n * log(u[node[k]]));
88 g += A[k] * exp(e);
89 }
90 return g;
91}
92
93} // namespace detail
94
95/**
96 * @brief Distinct-load Normalizing Constant (DNC) at a nonintegral
97 * population.
98 *
99 * @param L (M) service demands of the queueing stations
100 * @param N population, real and nonnegative (may be fractional)
101 */
102template <class T>
103DncResult<T> pfqn_dnc(const std::vector<T>& L, const T& N) {
105 "pfqn_dnc needs logarithms and is not available in exact arithmetic");
106 using std::exp;
107 using std::log;
108 const T zero = num_traits<T>::from_int(0);
109 const T one = num_traits<T>::from_int(1);
110
111 std::vector<T> Lp;
112 for (std::size_t i = 0; i < L.size(); ++i)
113 if (L[i] > zero) Lp.push_back(L[i]);
114 if (Lp.empty())
115 throw InputError("pfqn_dnc requires at least one station with positive demand");
116 if (N < zero) throw InputError("pfqn_dnc requires a nonnegative population");
117
118 const std::size_t M = Lp.size();
119 T xmax = Lp[0];
120 for (std::size_t i = 1; i < M; ++i)
121 if (Lp[i] > xmax) xmax = Lp[i];
122 std::vector<T> y(M);
123 for (std::size_t i = 0; i < M; ++i) y[i] = T(Lp[i] / xmax);
124
125 // Distinct loads and multiplicities, merged under a relative tolerance so
126 // that numerically coincident loads take the multiplicity branch rather
127 // than a near-singular partial-fraction denominator.
128 std::vector<T> ys = y;
129 std::sort(ys.begin(), ys.end(), [](const T& a, const T& b) { return b < a; });
130 const T near = num_traits<T>::from_double(1.0 - 1e-9);
131 std::vector<T> u;
132 std::vector<long> mult;
133 u.push_back(ys[0]);
134 mult.push_back(1);
135 for (std::size_t i = 1; i < M; ++i) {
136 if (ys[i] > T(u.back() * near)) {
137 ++mult.back();
138 } else {
139 u.push_back(ys[i]);
140 mult.push_back(1);
141 }
142 }
143 const std::size_t Gd = u.size();
144
145 std::vector<T> A;
146 std::vector<std::size_t> node;
147 std::vector<long> j;
148 bool all_simple = true;
149 for (std::size_t g = 0; g < Gd; ++g)
150 if (mult[g] != 1) all_simple = false;
151
152 if (all_simple) {
153 A.assign(Gd, one);
154 node.resize(Gd);
155 j.assign(Gd, 1);
156 for (std::size_t g = 0; g < Gd; ++g) {
157 node[g] = g;
158 T prod = one;
159 for (std::size_t l = 0; l < Gd; ++l) {
160 if (l == g) continue;
161 const T den = T(u[g] - u[l]);
162 if (den == zero) throw NumericError("pfqn_dnc: coincident loads in the simple branch");
163 prod *= T(u[g] / den);
164 }
165 A[g] = prod;
166 }
167 } else {
168 // Repeated loads: recover the coefficients from G(0..M-1), computed by
169 // convolution on the scaled loads (bounded by construction, max u = 1).
170 std::vector<T> gint(M, zero);
171 gint[0] = one;
172 std::size_t len = 1;
173 for (std::size_t i = 0; i < M; ++i) {
174 std::vector<T> gi(M);
175 gi[0] = one;
176 for (std::size_t k = 1; k < M; ++k) gi[k] = T(gi[k - 1] * y[i]);
177 std::vector<T> out(M, zero);
178 const std::size_t newlen = std::min(M, len + M - 1);
179 for (std::size_t a = 0; a < len; ++a)
180 for (std::size_t b = 0; b + a < M; ++b) out[a + b] += gint[a] * gi[b];
181 gint = out;
182 len = newlen;
183 }
184 node.resize(M);
185 j.resize(M);
186 std::size_t c = 0;
187 for (std::size_t g = 0; g < Gd; ++g)
188 for (long jj = 1; jj <= mult[g]; ++jj) {
189 node[c] = g;
190 j[c] = jj;
191 ++c;
192 }
193 Matrix<T> F(M, M);
194 for (std::size_t n = 0; n < M; ++n) {
195 const T nT = num_traits<T>::from_int(static_cast<long>(n));
196 for (std::size_t k = 0; k < M; ++k) {
197 const T jT = num_traits<T>::from_int(j[k]);
198 F(n, k) = exp(T(detail::num_lgamma<T>(T(nT + jT)) - detail::num_lgamma<T>(jT) -
199 detail::num_lgamma<T>(T(nT + one)) + nT * log(u[node[k]])));
200 }
201 }
202 A = solve(F, gint);
203 }
204
205 DncResult<T> res;
206 const T GN = detail::dnc_eval<T>(N, A, u, node, j);
207 const T GN1 = detail::dnc_eval<T>(T(N - one), A, u, node, j);
208 res.lG = T(log(GN) + N * log(xmax));
209 res.G = exp(res.lG);
210 const bool gn1_nan = !(GN1 == GN1);
211 if (N <= zero || GN <= zero || gn1_nan) {
212 res.X = std::numeric_limits<T>::quiet_NaN();
213 } else {
214 res.X = T(T(GN1 / GN) / xmax);
215 }
216 return res;
217}
218
219} // namespace pfqn
220} // namespace line
221
222#endif // LINE_API_PFQN_DNC_H
InputError(const std::string &what)
Definition error.h:39
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
DncResult< T > pfqn_dnc(const std::vector< T > &L, const T &N)
Distinct-load Normalizing Constant (DNC) at a nonintegral population.
Definition pfqn_dnc.h:103
std::vector< T > solve(const Matrix< T > &A, const std::vector< T > &b)
Convenience: solve Ax = b, leaving A and b untouched.
Definition lu.h:158
Number-type abstraction for the templated API port.
Shared scalar machinery for the integration / asymptotic members of the pfqn family (pfqn_le,...
Normalizing constant and throughput at a real-valued population.
Definition pfqn_dnc.h:62
T X
throughput G(N-1)/G(N); NaN where the continuation does not apply
Definition pfqn_dnc.h:63
T lG
log of the normalizing constant
Definition pfqn_dnc.h:65
T G
normalizing constant at population N
Definition pfqn_dnc.h:64