LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_cyclet_ofree.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_CYCLET_OFREE_H
6#define LINE_API_PFQN_PFQN_CYCLET_OFREE_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Exact passage-time law along an OVERTAKE-FREE PATH of a closed single-chain
12 * tree-like product-form network.
13 *
14 * Port of matlab/src/api/pfqn/pfqn_cyclet_ofree.m, which is the reference.
15 * Source: P. G. Harrison and W. J. Knottenbelt, "Passage Time Distributions in
16 * Large Markov Chains", 2002, Sec. 7.1, Theorems 1 and 2, after P. G. Harrison,
17 * J. Appl. Prob. 27, 1990 and H. Duduna, Adv. Appl. Prob. 14, 1982. The
18 * underlying sojourn-time result for overtake-free paths is F. Kelly and
19 * P. Pollett, Adv. Appl. Prob. 15, 1983.
20 *
21 * THE ONE FACT THAT MAKES ALL THREE ROUTES WORK. Conditional on the path,
22 *
23 * T | z = sum_{j in z} Erlang(u_{z_j} + 1, mu_{z_j})
24 *
25 * with u distributed as the network's equilibrium population vector AT N-1 (the
26 * arrival theorem). Hence the transform of Theorem 1 collapses to
27 *
28 * L(s|z) = prod_{j in z} mu_j/(s+mu_j) * G(y(s), N-1) / G(x, N-1)
29 *
30 * where x_i = v_i/mu_i and y_i(s) = x_i mu_i/(s+mu_i) on the path, x_i off it.
31 * One Buzen convolution per value of s.
32 *
33 * MOMENTS ARE NEVER TAKEN FROM THE DENSITY. They come from running the same
34 * Buzen convolution in the ring of truncated power series in s, so they are
35 * exact to machine precision, are unaffected by the time grid, and stay valid
36 * when the rates coincide and Theorem 2 does not apply.
37 *
38 * NOTE ON THE PAPER. The inner sum of Theorem 2 reads (v_j t)^(c-i)/(c-i)! and
39 * that is CORRECT as printed, however odd the visit ratio looks against a time:
40 * substituting the service rate instead returns negative densities. Verified
41 * against a direct mixture-of-Erlangs oracle to 1e-15, and at the paper's own
42 * N = 18 example against the transform route to 1e-11.
43 *
44 * ARITHMETIC: double. The density needs exp and an incomplete gamma.
45 */
46
47#include <algorithm>
48#include <cmath>
49#include <complex>
50#include <cstddef>
51#include <string>
52#include <vector>
53
55#include "line/util/error.h"
56
57namespace line {
58namespace pfqn {
59
60/** One path's outcome: which route ran, and the network constant it used. */
62 std::string method;
63 double lG = 0.0;
64 std::vector<std::size_t> path;
65};
66
67/** Density, distribution and moments of the passage time along a path. */
69 std::vector<double> f;
70 std::vector<double> F;
71 std::vector<double> mom;
72 std::vector<CycletPathInfo> info;
73};
74
75namespace cyclet_detail {
76
77/**
78 * Buzen's convolution: g[k] = G at population k for the node set y, k = 0..n.
79 * This is the k(y,a,b) recursion of Sec. 7.1 with the node index rolled up,
80 * k(y,a,b) = k(y,a-1,b) + y_a k(y,a,b-1), k(y,a,0) = 1, k(y,0,b>0) = 0.
81 */
82template <class S>
83std::vector<S> buzen(const std::vector<S>& y, std::size_t n) {
84 std::vector<S> g(n + 1, S(0.0));
85 g[0] = S(1.0);
86 for (std::size_t i = 0; i < y.size(); ++i)
87 for (std::size_t k = 1; k <= n; ++k) g[k] = g[k] + y[i] * g[k - 1];
88 return g;
89}
90
91inline std::vector<double> series_mul(const std::vector<double>& a, const std::vector<double>& b,
92 std::size_t K) {
93 std::vector<double> c(K + 1, 0.0);
94 for (std::size_t i = 0; i <= K; ++i) {
95 if (a[i] == 0.0) continue;
96 for (std::size_t j = 0; j + i <= K; ++j) c[i + j] += a[i] * b[j];
97 }
98 return c;
99}
100
101/**
102 * The regularized lower incomplete gamma P(a, x) for INTEGER a, which is all
103 * this file needs: P(k+1, x) = 1 - exp(-x) sum_{j<k+1} x^j/j!. The finite sum is
104 * exact for the integer shapes the Erlang terms produce, so no series/continued
105 * fraction switch is needed.
106 */
107inline double gammainc_int(std::size_t k, double x) {
108 if (!(x > 0.0)) return 0.0;
109 double term = std::exp(-x);
110 double acc = term;
111 for (std::size_t j = 1; j <= k; ++j) {
112 term *= x / double(j);
113 acc += term;
114 }
115 return std::min(1.0, std::max(0.0, 1.0 - acc));
116}
117
118inline double factorial_d(std::size_t k) {
119 double r = 1.0;
120 for (std::size_t i = 2; i <= k; ++i) r *= double(i);
121 return r;
122}
123
124} // namespace cyclet_detail
125
126/**
127 * Exact passage-time density, CDF and moments along the overtake-free paths
128 * `paths`, mixed by `pathprob`.
129 *
130 * @param method "auto" (default) uses "exact" when the path rates are separated
131 * and "lt" otherwise; "exact" is Theorem 2 in closed form and
132 * REQUIRES DISTINCT RATES on the path, since its partial fractions
133 * divide by prod_{i!=j}(mu_i - mu_j)
134 */
135inline CycletResult pfqn_cyclet_ofree(const std::vector<double>& v, const std::vector<double>& mu,
136 std::size_t N,
137 const std::vector<std::vector<std::size_t>>& paths,
138 const std::vector<double>& tset,
139 const std::string& method = "auto", std::size_t nmom = 3,
140 const std::vector<double>& pathprob = {},
141 const std::string& lti_method = "euler",
142 double tol = 1e-8) {
143 const std::size_t M = v.size();
144 if (mu.size() != M)
145 throw InputError("pfqn_cyclet_ofree: v and mu must name the same number of nodes");
146 for (std::size_t i = 0; i < M; ++i)
147 if (!(mu[i] > 0.0))
148 throw InputError("pfqn_cyclet_ofree: every service rate must be positive");
149 if (N < 1) throw InputError("pfqn_cyclet_ofree: the population N must be positive");
150 if (paths.empty()) throw InputError("pfqn_cyclet_ofree: no path was given");
151
152 std::vector<double> pp = pathprob;
153 if (pp.empty()) pp.assign(paths.size(), 1.0 / double(paths.size()));
154 if (pp.size() != paths.size())
155 throw InputError("pfqn_cyclet_ofree: pathprob must carry one probability per path");
156
157 std::vector<double> x(M);
158 for (std::size_t i = 0; i < M; ++i) x[i] = v[i] / mu[i];
159 const double Gn1 = cyclet_detail::buzen(x, N - 1).back();
160 if (!(Gn1 > 0.0))
161 throw NumericError(
162 "pfqn_cyclet_ofree: the network normalizing constant at population N-1 vanished; "
163 "check v and mu");
164
165 CycletResult out;
166 out.f.assign(tset.size(), 0.0);
167 out.F.assign(tset.size(), 0.0);
168 out.mom.assign(nmom, 0.0);
169
170 for (std::size_t ip = 0; ip < paths.size(); ++ip) {
171 const std::vector<std::size_t>& z = paths[ip];
172 if (z.empty())
173 throw InputError(
174 "pfqn_cyclet_ofree: an overtake-free path must contain at least the root node");
175 for (std::size_t a = 0; a < z.size(); ++a) {
176 if (z[a] >= M)
177 throw InputError("pfqn_cyclet_ofree: a path node is outside the network");
178 for (std::size_t b = a + 1; b < z.size(); ++b)
179 if (z[a] == z[b])
180 throw InputError("pfqn_cyclet_ofree: a path must have distinct nodes");
181 }
182 const std::size_t m = z.size();
183 std::vector<bool> onpath(M, false);
184 for (std::size_t j : z) onpath[j] = true;
185
186 std::string mth = method;
187 if (mth == "auto") {
188 if (m == 1) {
189 mth = "exact";
190 } else {
191 double sep = std::numeric_limits<double>::infinity();
192 double mx = 0.0;
193 for (std::size_t a = 0; a < m; ++a) {
194 mx = std::max(mx, mu[z[a]]);
195 for (std::size_t b = a + 1; b < m; ++b)
196 sep = std::min(sep, std::abs(mu[z[a]] - mu[z[b]]));
197 }
198 mth = (sep > tol * mx) ? "exact" : "lt";
199 }
200 }
201
202 std::vector<double> fi(tset.size(), 0.0), Fi(tset.size(), 0.0);
203 if (mth == "exact") {
204 std::vector<double> xoff;
205 for (std::size_t i = 0; i < M; ++i)
206 if (!onpath[i]) xoff.push_back(x[i]);
207 const std::vector<double> Gm = cyclet_detail::buzen(xoff, N - 1);
208
209 // coef[j][k] multiplies t^k exp(-mu_j t)
210 std::vector<std::vector<double>> coef(m, std::vector<double>(N, 0.0));
211 for (std::size_t j = 0; j < m; ++j) {
212 double den = 1.0;
213 for (std::size_t i = 0; i < m; ++i)
214 if (i != j) den *= (mu[z[i]] - mu[z[j]]);
215 if (den == 0.0)
216 throw InputError(
217 "pfqn_cyclet_ofree: Theorem 2 needs distinct service rates on the path; "
218 "two coincide. Use method 'lt'");
219 std::vector<double> w;
220 for (std::size_t i = 0; i < m; ++i)
221 if (i != j) w.push_back((v[z[i]] - v[z[j]]) / (mu[z[i]] - mu[z[j]]));
222 const std::vector<double> K = cyclet_detail::buzen(w, N - 1);
223 for (std::size_t c = 0; c < N; ++c) {
224 const double Gmc = Gm[N - 1 - c];
225 if (Gmc == 0.0) continue;
226 for (std::size_t i = 0; i <= c; ++i) coef[j][c - i] += Gmc * K[i] / den;
227 }
228 }
229
230 double pref = 1.0 / Gn1;
231 for (std::size_t j = 0; j < m; ++j) pref *= mu[z[j]];
232 for (std::size_t j = 0; j < m; ++j) {
233 const double mj = mu[z[j]], vj = v[z[j]];
234 for (std::size_t k = 0; k < N; ++k) {
235 const double c = coef[j][k];
236 if (c == 0.0) continue;
237 const double vk = std::pow(vj, double(k));
238 const double kf = cyclet_detail::factorial_d(k);
239 for (std::size_t it = 0; it < tset.size(); ++it) {
240 const double t = tset[it];
241 if (t < 0.0) continue;
242 fi[it] += pref * c * vk * std::pow(t, double(k)) / kf * std::exp(-mj * t);
243 // int_0^t s^k exp(-mu s) ds = k!/mu^(k+1) P(k+1, mu t)
244 Fi[it] += pref * c * vk / std::pow(mj, double(k + 1)) *
245 cyclet_detail::gammainc_int(k, mj * t);
246 }
247 }
248 }
249 } else if (mth == "lt") {
250 using C = std::complex<double>;
251 const lti::LaplaceFn L = [&](C s) {
252 std::vector<C> y(M);
253 for (std::size_t i = 0; i < M; ++i) y[i] = C(x[i], 0.0);
254 for (std::size_t j : z) y[j] = y[j] * (C(mu[j], 0.0) / (s + C(mu[j], 0.0)));
255 C acc = cyclet_detail::buzen(y, N - 1).back() / C(Gn1, 0.0);
256 for (std::size_t j : z) acc = acc * (C(mu[j], 0.0) / (s + C(mu[j], 0.0)));
257 return acc;
258 };
259 const lti::LaplaceMethod lm = lti::laplace_method(lti_method);
260 fi = lti::laplace_invert_pdf(L, tset, lm);
261 Fi = lti::laplace_invert_cdf(L, tset, lm);
262 } else {
263 throw InputError("pfqn_cyclet_ofree: unknown method '" + method +
264 "', expected auto, exact or lt");
265 }
266
267 // Moments by the series-ring Buzen convolution.
268 const std::size_t K = nmom;
269 std::vector<std::vector<double>> Y(M, std::vector<double>(K + 1, 0.0));
270 for (std::size_t i = 0; i < M; ++i) {
271 if (onpath[i]) {
272 double p = 1.0;
273 for (std::size_t k = 0; k <= K; ++k) {
274 Y[i][k] = x[i] * p;
275 p *= (-1.0 / mu[i]);
276 }
277 } else {
278 Y[i][0] = x[i];
279 }
280 }
281 std::vector<std::vector<double>> G(N, std::vector<double>(K + 1, 0.0));
282 G[0][0] = 1.0;
283 for (std::size_t i = 0; i < M; ++i)
284 for (std::size_t nn = 1; nn < N; ++nn) {
285 const std::vector<double> add = cyclet_detail::series_mul(Y[i], G[nn - 1], K);
286 for (std::size_t k = 0; k <= K; ++k) G[nn][k] += add[k];
287 }
288 std::vector<double> L(K + 1, 0.0);
289 for (std::size_t k = 0; k <= K; ++k) L[k] = G[N - 1][k] / Gn1;
290 for (std::size_t j : z) {
291 std::vector<double> e(K + 1, 0.0);
292 double p = 1.0;
293 for (std::size_t k = 0; k <= K; ++k) {
294 e[k] = p;
295 p *= (-1.0 / mu[j]);
296 }
297 L = cyclet_detail::series_mul(L, e, K);
298 }
299 std::vector<double> momi(nmom, 0.0);
300 for (std::size_t q = 1; q <= nmom; ++q)
301 momi[q - 1] = ((q % 2) ? -1.0 : 1.0) * cyclet_detail::factorial_d(q) * L[q];
302
303 for (std::size_t it = 0; it < tset.size(); ++it) {
304 out.f[it] += pp[ip] * fi[it];
305 out.F[it] += pp[ip] * Fi[it];
306 }
307 for (std::size_t q = 0; q < nmom; ++q) out.mom[q] += pp[ip] * momi[q];
308
310 pi.method = mth;
311 pi.lG = std::log(Gn1);
312 pi.path = z;
313 out.info.push_back(pi);
314 }
315
316 for (std::size_t it = 0; it < tset.size(); ++it) {
317 out.f[it] = std::max(0.0, out.f[it]);
318 out.F[it] = std::min(1.0, std::max(0.0, out.F[it]));
319 }
320 return out;
321}
322
323} // namespace pfqn
324} // namespace line
325
326#endif // LINE_API_PFQN_PFQN_CYCLET_OFREE_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.
Numerical inversion of a Laplace transform: Euler, Talbot, Gaver-Stehfest.
std::vector< double > laplace_invert_cdf(const LaplaceFn &F, const std::vector< double > &t, LaplaceMethod method=LaplaceMethod::Euler, std::size_t n=0)
The DISTRIBUTION on a grid, from the transform of the DENSITY.
std::vector< double > laplace_invert_pdf(const LaplaceFn &F, const std::vector< double > &t, LaplaceMethod method=LaplaceMethod::Euler, std::size_t n=0)
The DENSITY on a grid: the inversion clamped at zero.
LaplaceMethod
The methods laplace_invert accepts.
LaplaceMethod laplace_method(const std::string &s)
Parse the reference's method names, including its two Gaver spellings.
std::function< Cplx(Cplx)> LaplaceFn
The transform, evaluated at complex argument.
@ Gm
the reference's alias of 'cub'
Definition pfqn_nc.h:111
CycletResult pfqn_cyclet_ofree(const std::vector< double > &v, const std::vector< double > &mu, std::size_t N, const std::vector< std::vector< std::size_t > > &paths, const std::vector< double > &tset, const std::string &method="auto", std::size_t nmom=3, const std::vector< double > &pathprob={}, const std::string &lti_method="euler", double tol=1e-8)
Exact passage-time density, CDF and moments along the overtake-free paths paths, mixed by pathprob.
One path's outcome: which route ran, and the network constant it used.
std::vector< std::size_t > path
Density, distribution and moments of the passage time along a path.
std::vector< CycletPathInfo > info
std::vector< double > F
std::vector< double > mom
std::vector< double > f