LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_busyp.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_BUSYP_H
6#define LINE_API_PFQN_PFQN_BUSYP_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Mean busy period of order n for a subnetwork of a product-form network.
12 *
13 * Templated port of matlab/src/api/pfqn/pfqn_busyp.m,
14 * jar/src/main/java/jline/api/pfqn/Pfqn_busyp.java and
15 * python/line_solver/api/pfqn/busyp.py. Implements H. Daduna, "Busy Periods for
16 * Subnetworks in Stochastic Networks: Mean Value Analysis", J. ACM 35(3), 1988:
17 * Theorem 1 for a closed Gordon-Newell network and Theorem 3 for an open
18 * Jackson network.
19 *
20 * The busy period of order n for a set of nodes I is the interval from the
21 * instant a job entering I finds n-1 jobs in it until fewer than n remain. With
22 * G(m,I) the normalizing constant of I at population m, H(m,I) that of the
23 * complement, and A(I) the total rate at which jobs enter I from outside it,
24 *
25 * closed: b(n,I) = sum_{m=n}^{N} G(m,I) H(N-m,I) / [G(n-1,I) H(N-n,I) A(I)]
26 * open: b(n,I) = sum_{m>=n} G(m,I) / [G(n-1,I) A(I)]
27 *
28 * The paper is single-chain: alpha solves x*P = x for a closed network and
29 * x = gamma + x*P for an open one, and every node is a state-dependent
30 * single-server FCFS station. By the insensitivity noted in Section 5 the
31 * result depends on the service processes only through the rates mu.
32 *
33 * ARITHMETIC: the sums are accumulated in the log domain in double, exactly as
34 * the three reference implementations do, which is what keeps G(m,I) from
35 * overflowing on its own well before the ratio does. The templated inputs are
36 * therefore read through num_traits<T>::to_double; there is no exact-rational
37 * path, since a logarithm has none.
38 */
39
40#include <algorithm>
41#include <cmath>
42#include <cstddef>
43#include <functional>
44#include <limits>
45#include <vector>
46
47#include "line/num/number.h"
48#include "line/util/error.h"
49#include "line/util/matrix.h"
50
51namespace line {
52namespace pfqn {
53
54/** Default relative tolerance of the open-network tail truncation. */
55inline constexpr double PFQN_BUSYP_DEFAULT_TOL = 1e-12;
56
57namespace detail {
58
59/** log-sum-exp, stable when every entry is -infinity. */
60inline double busyp_lse(const std::vector<double>& v) {
61 double m = -std::numeric_limits<double>::infinity();
62 for (std::size_t i = 0; i < v.size(); ++i)
63 if (v[i] > m) m = v[i];
64 if (!std::isfinite(m)) return m;
65 double s = 0.0;
66 for (std::size_t i = 0; i < v.size(); ++i) s += std::exp(v[i] - m);
67 return m + std::log(s);
68}
69
70/**
71 * Rates of the selected nodes for populations 1..K. A rate table shorter than K
72 * keeps its last rate, the saturated-server convention; a node whose rate does
73 * not saturate (an infinite server) must be supplied through the callable
74 * overload instead.
75 */
76template <class T>
77std::vector<std::vector<double>> busyp_rates(const Matrix<T>& mu,
78 const std::vector<std::size_t>& idx,
79 std::size_t K) {
80 std::vector<std::vector<double>> out(idx.size(), std::vector<double>(K, 0.0));
81 const std::size_t cols = mu.cols();
82 for (std::size_t i = 0; i < idx.size(); ++i)
83 for (std::size_t k = 0; k < K; ++k)
84 out[i][k] = num_traits<T>::to_double(mu(idx[i], std::min(k, cols - 1)));
85 return out;
86}
87
88/** Same, for a rate law given as a callable rate(node, population). */
89inline std::vector<std::vector<double>> busyp_rates(
90 const std::function<double(std::size_t, std::size_t)>& mu,
91 const std::vector<std::size_t>& idx, std::size_t K) {
92 std::vector<std::vector<double>> out(idx.size(), std::vector<double>(K, 0.0));
93 for (std::size_t i = 0; i < idx.size(); ++i)
94 for (std::size_t k = 1; k <= K; ++k) out[i][k - 1] = mu(idx[i], k);
95 return out;
96}
97
98/**
99 * Log normalizing constants of orders 0..K of a set of nodes: lg[m] is the log
100 * of the sum over the compositions n_1+...+n_L = m of the product over the
101 * nodes of prod_{k=1}^{n_i} alpha_i/mu_i(k), the G(m,I) and H(m,I) of the
102 * paper. The nodes are convolved one at a time in the log domain.
103 */
104inline std::vector<double> busyp_lgvec(const std::vector<double>& alpha,
105 const std::vector<std::vector<double>>& mu,
106 std::size_t K) {
107 const double neg_inf = -std::numeric_limits<double>::infinity();
108 std::vector<double> lg(K + 1, neg_inf);
109 lg[0] = 0.0;
110 for (std::size_t i = 0; i < alpha.size(); ++i) {
111 std::vector<double> li(K + 1, 0.0);
112 double acc = 0.0;
113 for (std::size_t m = 1; m <= K; ++m) {
114 acc += std::log(alpha[i]) - std::log(mu[i][m - 1]);
115 li[m] = acc;
116 }
117 std::vector<double> lgnew(K + 1, neg_inf);
118 std::vector<double> terms;
119 for (std::size_t m = 0; m <= K; ++m) {
120 terms.assign(m + 1, neg_inf);
121 for (std::size_t k = 0; k <= m; ++k) terms[k] = lg[m - k] + li[k];
122 lgnew[m] = busyp_lse(terms);
123 }
124 lg.swap(lgnew);
125 }
126 return lg;
127}
128
129/**
130 * Truncation order of the open-network sum sum_{m>=n} G(m,I). The subnetwork
131 * terms decay geometrically once the rates saturate, so the truncation grows
132 * until the geometric tail estimate is negligible against the partial sum.
133 */
134template <class RateSource>
135std::size_t busyp_trunc(const std::vector<double>& alpha, const RateSource& mu,
136 const std::vector<std::size_t>& subnet, std::size_t nmax,
137 double tol) {
138 std::size_t K = std::max<std::size_t>(nmax + 8, 16);
139 std::vector<std::vector<double>> rows = busyp_rates(mu, subnet, K);
140 double rho_max = 0.0;
141 for (std::size_t i = 0; i < alpha.size(); ++i)
142 rho_max = std::max(rho_max, alpha[i] / rows[i][K - 1]);
143 if (rho_max >= 1)
144 throw InputError("pfqn_busyp: the subnetwork is not stable, its busy period is infinite");
145 while (true) {
146 const std::vector<double> lg = busyp_lgvec(alpha, busyp_rates(mu, subnet, K), K);
147 // decay rate read off the last two orders, the exact ratio for a
148 // saturated single-server subnetwork and an upper estimate otherwise
149 double r = std::exp(lg[K] - lg[K - 1]);
150 if (!(r < 1)) r = rho_max;
151 const double ltail = lg[K] + std::log(r) - std::log1p(-r);
152 std::vector<double> partial(lg.begin() + static_cast<std::ptrdiff_t>(nmax), lg.end());
153 if (ltail - busyp_lse(partial) < std::log(tol)) return K;
154 K = 2 * K;
155 if (K > 1000000)
156 throw NumericError(
157 "pfqn_busyp: the open busy period sum did not converge, the subnetwork "
158 "is nearly saturated");
159 }
160}
161
162} // namespace detail
163
164/** What pfqn_busyp returns: the durations and the two constant sequences. */
166 std::vector<double> b; ///< mean duration per requested order
167 std::vector<double> lG; ///< log normalizing constants of the subnetwork
168 std::vector<double> lH; ///< log constants of the complement, empty when open
169};
170
171/**
172 * Mean busy period of order n for the subnetwork.
173 *
174 * @param alpha relative arrival rates, one per node
175 * @param mu load-dependent rates, either a (J x K) matrix mu(j,k-1) with k
176 * jobs at node j, or a callable mu(j, k) when the rates do not
177 * saturate (an infinite server)
178 * @param P (J x J) routing matrix
179 * @param N population, infinity for an open network
180 * @param subnet zero-based node indexes forming the subnetwork
181 * @param n busy period orders, 1 <= n <= N
182 * @param gamma external arrival rates, empty for a closed network
183 * @param tol relative tolerance of the open-network tail truncation
184 */
185template <class T, class RateSource>
186BusyPeriodResult pfqn_busyp(const std::vector<double>& alpha, const RateSource& mu,
187 const Matrix<T>& P, double N,
188 const std::vector<std::size_t>& subnet,
189 const std::vector<std::size_t>& n,
190 const std::vector<double>& gamma = {},
191 double tol = PFQN_BUSYP_DEFAULT_TOL) {
192 const std::size_t J = alpha.size();
193 const bool is_closed = std::isfinite(N);
194
195 std::vector<std::size_t> target = subnet;
196 std::sort(target.begin(), target.end());
197 target.erase(std::unique(target.begin(), target.end()), target.end());
198 if (target.empty()) throw InputError("pfqn_busyp: the subnetwork must be non-empty");
199 if (is_closed && target.size() >= J)
200 // a closed network needs jobs outside the subnetwork to start a busy period
201 throw InputError(
202 "pfqn_busyp: in a closed network the subnetwork must be a proper subset of "
203 "the nodes");
204 if (target.back() >= J)
205 throw InputError("pfqn_busyp: the subnetwork indexes are out of range");
206
207 std::vector<bool> in_subnet(J, false);
208 for (std::size_t i = 0; i < target.size(); ++i) in_subnet[target[i]] = true;
209 std::vector<std::size_t> compl_nodes;
210 for (std::size_t j = 0; j < J; ++j)
211 if (!in_subnet[j]) compl_nodes.push_back(j);
212
213 std::size_t nmax = 0;
214 for (std::size_t t = 0; t < n.size(); ++t) {
215 if (n[t] < 1)
216 throw InputError("pfqn_busyp: the busy period order must be a positive integer");
217 if (is_closed && static_cast<double>(n[t]) > N)
218 throw InputError("pfqn_busyp: the busy period order must be an integer in 1..N");
219 nmax = std::max(nmax, n[t]);
220 }
221 if (!is_closed && gamma.empty())
222 throw InputError("pfqn_busyp: an open network requires the external arrival rates gamma");
223
224 // A(I) for a closed network, C(I) for an open one: both are the total rate at
225 // which jobs enter the subnetwork from outside it, which is what starts a busy
226 // period. The closed network has no external stream.
227 double inflow = 0.0;
228 for (std::size_t i = 0; i < compl_nodes.size(); ++i)
229 for (std::size_t j = 0; j < target.size(); ++j)
230 inflow += alpha[compl_nodes[i]] *
231 num_traits<T>::to_double(P(compl_nodes[i], target[j]));
232 if (!gamma.empty())
233 for (std::size_t j = 0; j < target.size(); ++j) inflow += gamma[target[j]];
234 if (inflow <= 0)
235 throw InputError(
236 "pfqn_busyp: no job ever enters the subnetwork, its busy period is undefined");
237
238 std::vector<double> alpha_sub, alpha_compl;
239 for (std::size_t i = 0; i < target.size(); ++i) alpha_sub.push_back(alpha[target[i]]);
240 for (std::size_t i = 0; i < compl_nodes.size(); ++i)
241 alpha_compl.push_back(alpha[compl_nodes[i]]);
242
243 BusyPeriodResult out;
244 out.b.assign(n.size(), 0.0);
245 if (is_closed) {
246 const std::size_t pop = static_cast<std::size_t>(std::llround(N));
247 out.lG = detail::busyp_lgvec(alpha_sub, detail::busyp_rates(mu, target, pop), pop);
248 out.lH = detail::busyp_lgvec(alpha_compl,
249 detail::busyp_rates(mu, compl_nodes, pop), pop);
250 for (std::size_t t = 0; t < n.size(); ++t) {
251 // Theorem 1
252 std::vector<double> terms;
253 for (std::size_t m = n[t]; m <= pop; ++m)
254 terms.push_back(out.lG[m] + out.lH[pop - m]);
255 out.b[t] = std::exp(detail::busyp_lse(terms) - out.lG[n[t] - 1] -
256 out.lH[pop - n[t]] - std::log(inflow));
257 }
258 } else {
259 const std::size_t K = detail::busyp_trunc(alpha_sub, mu, target, nmax, tol);
260 out.lG = detail::busyp_lgvec(alpha_sub, detail::busyp_rates(mu, target, K), K);
261 for (std::size_t t = 0; t < n.size(); ++t) {
262 // Theorem 3, the tail summed up to the truncation order
263 std::vector<double> terms;
264 for (std::size_t m = n[t]; m <= K; ++m) terms.push_back(out.lG[m]);
265 out.b[t] = std::exp(detail::busyp_lse(terms) - out.lG[n[t] - 1] -
266 std::log(inflow));
267 }
268 }
269 return out;
270}
271
272} // namespace pfqn
273} // namespace line
274
275#endif // LINE_API_PFQN_PFQN_BUSYP_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
Dense matrix and non-owning view.
BusyPeriodResult pfqn_busyp(const std::vector< double > &alpha, const RateSource &mu, const Matrix< T > &P, double N, const std::vector< std::size_t > &subnet, const std::vector< std::size_t > &n, const std::vector< double > &gamma={}, double tol=PFQN_BUSYP_DEFAULT_TOL)
Mean busy period of order n for the subnetwork.
Definition pfqn_busyp.h:186
constexpr double PFQN_BUSYP_DEFAULT_TOL
Default relative tolerance of the open-network tail truncation.
Definition pfqn_busyp.h:55
Number-type abstraction for the templated API port.
What pfqn_busyp returns: the durations and the two constant sequences.
Definition pfqn_busyp.h:165
std::vector< double > lH
log constants of the complement, empty when open
Definition pfqn_busyp.h:168
std::vector< double > lG
log normalizing constants of the subnetwork
Definition pfqn_busyp.h:167
std::vector< double > b
mean duration per requested order
Definition pfqn_busyp.h:166