LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
dpfqn_nc.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_DPFQN_DPFQN_NC_H
6#define LINE_API_DPFQN_DPFQN_NC_H
7
8/**
9 * @file
10 * @ingroup api_dpfqn
11 * Normalizing constants of a discrete-time closed cycle of Bernoulli servers.
12 *
13 * Templated port of matlab/src/api/dpfqn/dpfqn_nc.m and dpfqn_ncld.m. With
14 * q_j = 1 - p_j the queue length vector has the product form of Daduna (2001),
15 * corollary 3.4,
16 *
17 * pi(n_1,...,n_J) = prod_j (q_j/p_j)^n_j (1/q_j)^{1{n_j>0}} / G(N,J),
18 *
19 * whose extra factor on the busy nodes is what separates it from the
20 * continuous-time Gordon-Newell form: a homogeneous cycle is uniform on the
21 * state space in continuous time and is not here.
22 *
23 * `dpfqn_nc` runs the three-term recursion of proposition 3.18,
24 *
25 * G(k,j) = G(k,j-1) + (q_j/p_j) G(k-1,j) + G(k-1,j-1),
26 *
27 * together with the arrival constants of proposition 3.19,
28 *
29 * G1(k,J) = G(k-1,J-1) + (q_J/p_J) G1(k-1,J), k >= 3.
30 *
31 * Unlike the continuous-time convolution algorithm neither recursion is
32 * invariant to the numbering of the nodes. By lemma 7.3 the arrival constant is
33 * the same at every node, so one family suffices and
34 *
35 * throughput per slot X = G1(N,J) / G(N,J) (equal at every node),
36 * utilization U_j = X / p_j,
37 * tail probability P(X_j >= k)
38 * = (q_j/p_j)^k (1/q_j) G1(N-k+1,J) / G(N,J).
39 *
40 * The last identity is corollary 3.20(a) with its index corrected: as printed
41 * there the right-hand side evaluates to P(X_j >= k+1). Corollary 3.20(c),
42 * which transfers the tail from node 1 to node j, holds for k >= 1 only; at
43 * k = 0 both tails are 1 while the stated ratio is q_1/q_j.
44 *
45 * `dpfqn_ncld` takes the state dependent case of theorem 3.2 by truncated
46 * convolution of the per-node weights
47 *
48 * w_j(n) = prod_{h=1}^{n-1} q_j(h) / prod_{h=1}^{n} p_j(h),
49 *
50 * with the complement constants (the cycle without node j) from a prefix/suffix
51 * pass, so that P(X_j = n) = W[j][n] * Gc[j][N-n] / G[N]. Deconvolution is
52 * never used, so a node with a near-unit service probability does not spoil the
53 * accuracy of the other marginals.
54 *
55 * Everything here is a rational function of the service probabilities, so the
56 * exact instantiation returns the constants with no rounding. That matters in
57 * the interesting regime: a slow node makes (q/p)^N large, and the double
58 * evaluation of the ratio G1(N-k+1)/G loses digits exactly there. No
59 * transcendental appears in any metric: every quantity the analyzer reads is a
60 * ratio of constants in the same common scale, and `lG` is a double-valued
61 * diagnostic obtained through `to_double`, as in `solver_nc.h`.
62 */
63
64#include <cmath>
65#include <cstddef>
66#include <vector>
67
68#include "line/num/number.h"
69#include "line/util/error.h"
70
71namespace line {
72namespace dpfqn {
73
74/** Constants of the state independent cycle, in one common scale. */
75template <class T>
76struct DtNcResult {
77 double lG; ///< true log of G(N,J), in double whatever T is
78 T G; ///< G(N,J), in the common scale
79 std::vector<T> G1; ///< G1[k] = G_1(k,J), k = 0..N, same scale
80
81 /** Throughput per slot, equal at every node of the cycle. */
82 T throughput() const { return G1[G1.size() - 1] / G; }
83};
84
85/** Constants of the state dependent cycle, in one common scale. */
86template <class T>
88 double lG; ///< true log of G(N,J), in double whatever T is
89 std::vector<T> G; ///< G(k), k = 0..N
90 std::vector<std::vector<T> > W; ///< time-stationary node weights w_j(n)
91 std::vector<std::vector<T> > Gc; ///< cycle without node j, at population k
92 std::vector<std::vector<T> > Wa; ///< arrival weights v_j(n) = w_j(n) q_j(n)
93
94 /** Marginal queue length law of node j, P(X_j = n) for n = 0..N. */
95 std::vector<T> marginal(std::size_t j) const {
96 const std::size_t N = G.size() - 1;
97 std::vector<T> marg(N + 1);
98 for (std::size_t n = 0; n <= N; ++n) {
99 marg[n] = W[j][n] * Gc[j][N - n] / G[N];
100 }
101 return marg;
102 }
103};
104
105/**
106 * Propositions 3.18 and 3.19 for a cycle with state independent service
107 * probabilities.
108 *
109 * @param p per-slot service completion probabilities p_j in (0,1)
110 * @param N number of customers cycling in the J nodes
111 */
112template <class T>
113DtNcResult<T> dpfqn_nc(const std::vector<T>& p, std::size_t N) {
114 if (p.empty()) {
115 throw InputError("dpfqn_nc: the cycle must contain at least one node");
116 }
117 const T zero = num_traits<T>::from_int(0);
118 const T one = num_traits<T>::from_int(1);
119 for (std::size_t j = 0; j < p.size(); ++j) {
120 if (!(p[j] > zero) || !(p[j] < one)) {
121 throw InputError("dpfqn_nc: service probabilities must be in the open interval (0,1)");
122 }
123 }
124 const std::size_t J = p.size();
125 std::vector<T> x(J);
126 for (std::size_t j = 0; j < J; ++j) {
127 x[j] = (one - p[j]) / p[j];
128 }
129
130 // The recursion is linear and homogeneous in the whole table, so rescaling
131 // every entry at once preserves it; that is what keeps (q/p)^N from
132 // overflowing for a slow node and a large population. G1 is rescaled in
133 // step so the two families stay in one common scale.
134 std::vector<std::vector<T> > Gt(N + 1, std::vector<T>(J + 1, zero));
135 for (std::size_t j = 0; j <= J; ++j) {
136 Gt[0][j] = one;
137 }
138 std::vector<T> G1(N + 1, zero);
139 double lscale = 0.0;
140 const T limit = num_traits<T>::from_double(1e250);
141 for (std::size_t k = 1; k <= N; ++k) {
142 for (std::size_t j = 1; j <= J; ++j) {
143 Gt[k][j] = Gt[k][j - 1] + x[j - 1] * Gt[k - 1][j] + Gt[k - 1][j - 1];
144 }
145 if (k == 1) {
146 G1[1] = Gt[0][0];
147 } else if (k == 2) {
148 T s = x[0];
149 for (std::size_t j = 1; j < J; ++j) {
150 s = s + one / p[j];
151 }
152 G1[2] = s * Gt[0][0];
153 } else {
154 G1[k] = Gt[k - 1][J - 1] + x[J - 1] * G1[k - 1];
155 }
156 T mx = zero;
157 for (std::size_t j = 0; j <= J; ++j) {
158 if (Gt[k][j] > mx) {
159 mx = Gt[k][j];
160 }
161 }
162 if (mx > limit) {
163 for (std::size_t a = 0; a <= N; ++a) {
164 for (std::size_t b = 0; b <= J; ++b) {
165 Gt[a][b] = Gt[a][b] / mx;
166 }
167 G1[a] = G1[a] / mx;
168 }
169 lscale += std::log(num_traits<T>::to_double(mx));
170 }
171 }
172 DtNcResult<T> out;
173 out.G = Gt[N][J];
174 out.lG = std::log(num_traits<T>::to_double(out.G)) + lscale;
175 out.G1 = G1;
176 return out;
177}
178
179namespace detail {
180
181/** Convolution of two population tables truncated at N. */
182template <class T>
183std::vector<T> dt_conv(const std::vector<T>& a, const std::vector<T>& b, std::size_t N) {
184 std::vector<T> c(N + 1, num_traits<T>::from_int(0));
185 for (std::size_t k = 0; k <= N; ++k) {
187 for (std::size_t m = 0; m <= k; ++m) {
188 s = s + a[m] * b[k - m];
189 }
190 c[k] = s;
191 }
192 return c;
193}
194
195} // namespace detail
196
197/**
198 * Theorem 3.2 for a cycle whose service probabilities depend on the local
199 * queue length.
200 *
201 * @param P service probabilities, P[j][n-1] = p_j(n) in (0,1]
202 * @param N number of customers cycling in the J nodes
203 */
204template <class T>
205DtNcLdResult<T> dpfqn_ncld(const std::vector<std::vector<T> >& P, std::size_t N) {
206 if (P.empty()) {
207 throw InputError("dpfqn_ncld: P must be a non-empty matrix of service probabilities");
208 }
209 const std::size_t J = P.size();
210 const T zero = num_traits<T>::from_int(0);
211 const T one = num_traits<T>::from_int(1);
212 DtNcLdResult<T> out;
213 if (N == 0) {
214 out.lG = 0.0;
215 out.G.assign(1, one);
216 out.W.assign(J, std::vector<T>(1, one));
217 out.Gc = out.W;
218 out.Wa = out.W;
219 return out;
220 }
221 for (std::size_t j = 0; j < J; ++j) {
222 if (P[j].size() < N) {
223 throw InputError("dpfqn_ncld: P must supply p_j(n) for every n = 1..N");
224 }
225 for (std::size_t n = 0; n < N; ++n) {
226 if (!(P[j][n] > zero) || P[j][n] > one) {
227 throw InputError("dpfqn_ncld: service probabilities must be in the interval (0,1]");
228 }
229 // p_j(n)=1 is admissible only at the last reachable population,
230 // where the missing q_j(n) never multiplies any weight.
231 if (n + 1 < N && !(P[j][n] < one)) {
232 throw InputError("dpfqn_ncld: service probabilities below the population bound must be "
233 "strictly less than 1");
234 }
235 }
236 }
237
238 // Per-node weights of theorem 3.2 by their exact recurrence,
239 // w_j(0) = 1 and w_j(n) = w_j(n-1) q_j(n-1) / p_j(n) with q_j(0) := 1, then
240 // a per-node division by the largest entry. The recurrence keeps every
241 // weight in the field, so nothing here is transcendental; the divisions
242 // cancel in every ratio below and are tracked only for lG.
243 std::vector<double> shift(J, 0.0);
244 out.W.assign(J, std::vector<T>(N + 1, zero));
245 out.Wa.assign(J, std::vector<T>(N + 1, zero));
246 for (std::size_t j = 0; j < J; ++j) {
247 out.W[j][0] = one;
248 for (std::size_t n = 1; n <= N; ++n) {
249 const T q = (n >= 2) ? (one - P[j][n - 2]) : one;
250 out.W[j][n] = out.W[j][n - 1] * q / P[j][n - 1];
251 }
252 T mx = out.W[j][0];
253 for (std::size_t n = 1; n <= N; ++n) {
254 if (out.W[j][n] > mx) {
255 mx = out.W[j][n];
256 }
257 }
258 shift[j] = std::log(num_traits<T>::to_double(mx));
259 for (std::size_t n = 0; n <= N; ++n) {
260 out.W[j][n] = out.W[j][n] / mx;
261 }
262 out.Wa[j][0] = out.W[j][0];
263 for (std::size_t n = 1; n <= N; ++n) {
264 out.Wa[j][n] = out.W[j][n] * (one - P[j][n - 1]);
265 }
266 }
267
268 // Prefix/suffix convolution: pre[j] covers nodes 0..j-1 and suf[j] covers
269 // nodes j..J-1, so the complement of node j is their convolution.
270 std::vector<std::vector<T> > pre(J + 1, std::vector<T>(N + 1, zero));
271 pre[0][0] = one;
272 for (std::size_t j = 0; j < J; ++j) {
273 pre[j + 1] = detail::dt_conv(pre[j], out.W[j], N);
274 }
275 std::vector<std::vector<T> > suf(J + 1, std::vector<T>(N + 1, zero));
276 suf[J][0] = one;
277 for (std::size_t j = J; j-- > 0;) {
278 suf[j] = detail::dt_conv(suf[j + 1], out.W[j], N);
279 }
280 out.G = pre[J];
281 out.Gc.assign(J, std::vector<T>(N + 1, zero));
282 for (std::size_t j = 0; j < J; ++j) {
283 out.Gc[j] = detail::dt_conv(pre[j], suf[j + 1], N);
284 }
285
286 double total = 0.0;
287 for (std::size_t j = 0; j < J; ++j) {
288 total += shift[j];
289 }
290 out.lG = std::log(num_traits<T>::to_double(out.G[N])) + total;
291 return out;
292}
293
294} // namespace dpfqn
295} // namespace line
296
297#endif // LINE_API_DPFQN_DPFQN_NC_H
InputError(const std::string &what)
Definition error.h:39
The exception types the port throws.
DtNcLdResult< T > dpfqn_ncld(const std::vector< std::vector< T > > &P, std::size_t N)
Theorem 3.2 for a cycle whose service probabilities depend on the local queue length.
Definition dpfqn_nc.h:205
DtNcResult< T > dpfqn_nc(const std::vector< T > &p, std::size_t N)
Propositions 3.18 and 3.19 for a cycle with state independent service probabilities.
Definition dpfqn_nc.h:113
Number-type abstraction for the templated API port.
Constants of the state dependent cycle, in one common scale.
Definition dpfqn_nc.h:87
double lG
true log of G(N,J), in double whatever T is
Definition dpfqn_nc.h:88
std::vector< std::vector< T > > Gc
cycle without node j, at population k
Definition dpfqn_nc.h:91
std::vector< T > marginal(std::size_t j) const
Marginal queue length law of node j, P(X_j = n) for n = 0..N.
Definition dpfqn_nc.h:95
std::vector< T > G
G(k), k = 0..N.
Definition dpfqn_nc.h:89
std::vector< std::vector< T > > Wa
arrival weights v_j(n) = w_j(n) q_j(n)
Definition dpfqn_nc.h:92
std::vector< std::vector< T > > W
time-stationary node weights w_j(n)
Definition dpfqn_nc.h:90
Constants of the state independent cycle, in one common scale.
Definition dpfqn_nc.h:76
T throughput() const
Throughput per slot, equal at every node of the cycle.
Definition dpfqn_nc.h:82
T G
G(N,J), in the common scale.
Definition dpfqn_nc.h:78
std::vector< T > G1
G1[k] = G_1(k,J), k = 0..N, same scale.
Definition dpfqn_nc.h:79
double lG
true log of G(N,J), in double whatever T is
Definition dpfqn_nc.h:77