LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_busyp_clw.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_CLW_H
6#define LINE_API_PFQN_PFQN_BUSYP_CLW_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Busy period of a subnetwork from point evaluations of the normalizing constant.
12 *
13 * Templated port of matlab/src/api/pfqn/pfqn_busyp_clw.m,
14 * jar/src/main/java/jline/api/pfqn/Pfqn_busyp_clw.java and
15 * python/line_solver/api/pfqn/busyp_clw.py.
16 *
17 * WHAT THIS BUYS OVER `pfqn_busyp` / `pfqn_busyp_multiclass`. Those walk the
18 * whole population ladder (the whole lattice, multichain) because the numerator
19 * sums over {|m| >= n}. The complement of that set is the SHELLS |m| <= n-1, and
20 * summing the product form over the WHOLE lattice is the full network's own
21 * normalizing constant, G_I and H convolving to it:
22 *
23 * sum_{m : |m| >= n} G_I(m) H(N-m) = G(N) - sum_{m : |m| <= n-1} G_I(m) H(N-m)
24 *
25 * so order n needs only the n lowest shells plus ONE evaluation of G(N). The
26 * ordinary busy period n=1 collapses to three constants,
27 *
28 * b(1,I) = [G(N) - H(N)] / sum_r A_r(I) H(N-e_r)
29 *
30 * all point evaluations at or near the full population, which is what the
31 * normalizing-constant methods are built for. This routine calls CLW
32 * (Choudhury-Leung-Whitt, J. ACM 42, 1995, numerical inversion of the generating
33 * function); any method returning lG(N) can take its place. The cost stops
34 * depending on N.
35 *
36 * THE OPEN CASE NEEDS NO INVERSION. The subnetwork's constant sequence has
37 * generating function g(z) = prod_{i in I} f_i(z) and the tail is g(1) minus a
38 * partial sum, with f_i(1) = 1/(1-rho_i) at a single server and exp(rho_i) at an
39 * infinite one. That removes the tail TRUNCATION of the ladder routine, not just
40 * its cost: the tail is exact, and the C++ test checks it at == against the
41 * closed form.
42 *
43 * ACCURACY: the numerator is a difference of two nearly equal quantities when the
44 * level set is unlikely, so the relative error grows with n -- 6e-12 at n=1
45 * against 2.7e-08 at n=N on a three-station closed model at N=20. Cost grows with
46 * n too, so the routine is most accurate where it is fastest.
47 *
48 * SCOPE: CLW's generating function covers single-server and infinite-server
49 * stations, so a general load-dependent scaling belongs to
50 * `pfqn_busyp_multiclass`. The identity is for the AGGREGATE level set: a
51 * per-class one has complement {m_r <= n-1}, the whole lattice in the other
52 * chains, which buys nothing.
53 *
54 * ARITHMETIC: log domain in double, as in the three reference implementations.
55 */
56
57#include <algorithm>
58#include <cmath>
59#include <cstddef>
60#include <limits>
61#include <string>
62#include <vector>
63
65// busyp_factln and the station function live with the multichain routine
68#include "line/num/number.h"
69#include "line/util/error.h"
70#include "line/util/matrix.h"
71
72namespace line {
73namespace pfqn {
74
75namespace detail {
76
77/**
78 * log G(k) of a set of nodes: the single servers go to the normalizing-constant
79 * method and the infinite servers into its aggregate think time.
80 */
81inline double busyp_clw_lognc(const std::vector<std::vector<double>>& L,
82 const std::vector<bool>& isdelay,
83 const std::vector<std::size_t>& nodes,
84 const std::vector<int>& k, const std::string& method) {
85 const std::size_t R = k.size();
86 bool all_zero = true;
87 for (std::size_t r = 0; r < R; ++r) {
88 if (k[r] < 0) return -std::numeric_limits<double>::infinity();
89 if (k[r] != 0) all_zero = false;
90 }
91 if (all_zero) return 0.0;
92 std::vector<std::size_t> queues;
93 std::vector<double> Z(R, 0.0);
94 for (std::size_t t = 0; t < nodes.size(); ++t) {
95 if (isdelay[nodes[t]]) {
96 for (std::size_t r = 0; r < R; ++r) Z[r] += L[nodes[t]][r];
97 } else {
98 queues.push_back(nodes[t]);
99 }
100 }
101 if (queues.empty()) {
102 // only infinite servers left: G(k) = prod_r Z_r^k_r / k_r!
103 double out = 0.0;
104 for (std::size_t r = 0; r < R; ++r) {
105 if (k[r] == 0) continue;
106 if (Z[r] <= 0) return -std::numeric_limits<double>::infinity();
107 out += k[r] * std::log(Z[r]) - busyp_factln(static_cast<std::size_t>(k[r]));
108 }
109 return out;
110 }
111 if (method != "clw")
112 throw InputError(
113 "pfqn_busyp_clw: only the clw method is wired here; the point evaluation is a "
114 "plug-in, so another token needs its own call rather than a silent substitution");
115 Matrix<double> Lq(queues.size(), R, 0.0);
116 for (std::size_t i = 0; i < queues.size(); ++i)
117 for (std::size_t r = 0; r < R; ++r) Lq(i, r) = L[queues[i]][r];
118 return static_cast<double>(pfqn_clw(Lq, k, Z).lG);
119}
120
121/** log X_i(k): multinomial at a single server, 1/prod k_r! at an infinite one. */
122inline double busyp_clw_node(const std::vector<double>& Li, bool isdelay,
123 const std::vector<std::size_t>& k) {
124 std::size_t tot = 0;
125 for (std::size_t r = 0; r < k.size(); ++r) tot += k[r];
126 if (tot == 0) return 0.0;
127 double v = isdelay ? 0.0 : busyp_factln(tot);
128 for (std::size_t r = 0; r < k.size(); ++r) {
129 if (k[r] == 0) continue;
130 if (Li[r] <= 0) return -std::numeric_limits<double>::infinity();
131 v += -busyp_factln(k[r]) + static_cast<double>(k[r]) * std::log(Li[r]);
132 }
133 return v;
134}
135
136/**
137 * log G_I(m) by direct enumeration of the splits of m across the subnetwork
138 * nodes, cheap because m is bounded by n-1 and n is small wherever this wins.
139 */
140inline double busyp_clw_station(const std::vector<std::vector<double>>& L,
141 const std::vector<bool>& isdelay,
142 const std::vector<std::size_t>& nodes, std::size_t from,
143 const std::vector<std::size_t>& m) {
144 const double neg_inf = -std::numeric_limits<double>::infinity();
145 const std::size_t R = m.size();
146 if (from >= nodes.size()) {
147 for (std::size_t r = 0; r < R; ++r)
148 if (m[r] > 0) return neg_inf;
149 return 0.0;
150 }
151 std::size_t total = 1;
152 for (std::size_t r = 0; r < R; ++r) total *= m[r] + 1;
153 std::vector<double> acc;
154 std::vector<std::size_t> head(R, 0), rest(R, 0);
155 for (std::size_t idx = 0; idx < total; ++idx) {
156 std::size_t t = idx;
157 for (std::size_t r = 0; r < R; ++r) {
158 head[r] = t % (m[r] + 1);
159 t /= m[r] + 1;
160 }
161 const double lterm = busyp_clw_node(L[nodes[from]], isdelay[nodes[from]], head);
162 if (lterm == neg_inf) continue;
163 for (std::size_t r = 0; r < R; ++r) rest[r] = m[r] - head[r];
164 const double lrest = busyp_clw_station(L, isdelay, nodes, from + 1, rest);
165 if (lrest == neg_inf) continue;
166 acc.push_back(lterm + lrest);
167 }
168 return busyp_lse(acc);
169}
170
171/** log G_I(0..kmax) of an OPEN subnetwork, convolving the per-node series. */
172inline std::vector<double> busyp_clw_open(const std::vector<double>& rho,
173 const std::vector<bool>& isdelay,
174 std::size_t kmax) {
175 const double neg_inf = -std::numeric_limits<double>::infinity();
176 std::vector<double> lg(kmax + 1, neg_inf);
177 lg[0] = 0.0;
178 for (std::size_t i = 0; i < rho.size(); ++i) {
179 std::vector<double> li(kmax + 1, 0.0);
180 double acc = 0.0;
181 for (std::size_t k = 1; k <= kmax; ++k) {
182 // 1/(1-rho z) has coefficients rho^k; exp(rho z) has rho^k/k!
183 acc += std::log(rho[i]) - (isdelay[i] ? std::log(static_cast<double>(k)) : 0.0);
184 li[k] = acc;
185 }
186 std::vector<double> lgnew(kmax + 1, neg_inf);
187 for (std::size_t m = 0; m <= kmax; ++m) {
188 std::vector<double> terms(m + 1, neg_inf);
189 for (std::size_t k = 0; k <= m; ++k) terms[k] = lg[m - k] + li[k];
190 lgnew[m] = busyp_lse(terms);
191 }
192 lg.swap(lgnew);
193 }
194 return lg;
195}
196
197} // namespace detail
198
199/**
200 * Mean busy period of order n for the subnetwork, via NC point evaluations.
201 *
202 * @param alpha (J x R) relative arrival rates, one column per chain
203 * @param mu (J x R) service rates, the chain-r rate at node j
204 * @param P routing matrices, one per chain (size 1 = shared by all chains)
205 * @param N population per chain, infinite entries for an open chain
206 * @param subnet zero-based node indexes forming the subnetwork
207 * @param n busy period orders, counting the jobs of every chain
208 * @param gamma (J x R) external arrival rates, empty for a closed network
209 * @param isdelay infinite-server nodes, empty meaning all single servers
210 * @param method method name of the normalizing-constant method ("clw")
211 */
212template <class T>
213std::vector<double> pfqn_busyp_clw(const Matrix<T>& alpha, const Matrix<T>& mu,
214 const std::vector<Matrix<T>>& P,
215 const std::vector<double>& N,
216 const std::vector<std::size_t>& subnet,
217 const std::vector<std::size_t>& n,
218 const Matrix<T>& gamma = Matrix<T>(),
219 const std::vector<bool>& isdelay = std::vector<bool>(),
220 const std::string& method = "clw") {
221 const std::size_t J = alpha.rows(), R = alpha.cols();
222 bool is_closed = true, is_open = true;
223 for (std::size_t r = 0; r < R; ++r) {
224 if (std::isinf(N[r]))
225 is_closed = false;
226 else
227 is_open = false;
228 }
229 if (!is_closed && !is_open)
230 throw InputError(
231 "pfqn_busyp_clw: a mixed model needs the lattice routine pfqn_busyp_multiclass");
232 std::vector<bool> delay = isdelay.empty() ? std::vector<bool>(J, false) : isdelay;
233
234 std::vector<std::size_t> target = subnet;
235 std::sort(target.begin(), target.end());
236 target.erase(std::unique(target.begin(), target.end()), target.end());
237 if (target.empty()) throw InputError("pfqn_busyp_clw: the subnetwork must be non-empty");
238 if (is_closed && target.size() >= J)
239 throw InputError(
240 "pfqn_busyp_clw: in a closed network the subnetwork must be a proper subset");
241 std::vector<bool> in_subnet(J, false);
242 for (std::size_t i = 0; i < target.size(); ++i) in_subnet[target[i]] = true;
243 std::vector<std::size_t> compl_nodes, all_nodes;
244 for (std::size_t j = 0; j < J; ++j) {
245 all_nodes.push_back(j);
246 if (!in_subnet[j]) compl_nodes.push_back(j);
247 }
248
249 // demands L(i,r) = alpha(i,r)/mu(i,r), zero where chain r does not visit node i
250 std::vector<std::vector<double>> L(J, std::vector<double>(R, 0.0));
251 for (std::size_t i = 0; i < J; ++i)
252 for (std::size_t r = 0; r < R; ++r) {
253 const double a = num_traits<T>::to_double(alpha(i, r));
254 const double m = num_traits<T>::to_double(mu(i, r));
255 L[i][r] = (a > 0 && m > 0) ? a / m : 0.0;
256 }
257
258 // A_r(I): the chain-r rate at which jobs enter the subnetwork from outside it
259 std::vector<double> A(R, 0.0);
260 double inflow = 0.0;
261 for (std::size_t r = 0; r < R; ++r) {
262 const Matrix<T>& Pr = (P.size() == 1) ? P[0] : P[r];
263 for (std::size_t i = 0; i < compl_nodes.size(); ++i)
264 for (std::size_t j = 0; j < target.size(); ++j)
265 A[r] += num_traits<T>::to_double(alpha(compl_nodes[i], r)) *
266 num_traits<T>::to_double(Pr(compl_nodes[i], target[j]));
267 if (gamma.rows() == J)
268 for (std::size_t j = 0; j < target.size(); ++j)
269 A[r] += num_traits<T>::to_double(gamma(target[j], r));
270 inflow += A[r];
271 }
272 if (inflow <= 0)
273 throw InputError(
274 "pfqn_busyp_clw: no job ever enters the subnetwork, its busy period is undefined");
275
276 std::size_t nmax = 1;
277 for (std::size_t t = 0; t < n.size(); ++t) nmax = std::max(nmax, n[t]);
278
279 if (is_open) {
280 // g_I(1) in closed form, so the tail is exact rather than truncated
281 std::vector<double> rho(target.size(), 0.0);
282 std::vector<bool> delayI(target.size(), false);
283 double lg1 = 0.0;
284 for (std::size_t t = 0; t < target.size(); ++t) {
285 for (std::size_t r = 0; r < R; ++r) rho[t] += L[target[t]][r];
286 delayI[t] = delay[target[t]];
287 if (delayI[t]) {
288 lg1 += rho[t];
289 } else {
290 if (rho[t] >= 1)
291 throw InputError(
292 "pfqn_busyp_clw: the subnetwork is not stable, its busy period is infinite");
293 lg1 += -std::log1p(-rho[t]);
294 }
295 }
296 const std::vector<double> lseq = detail::busyp_clw_open(rho, delayI, nmax);
297 std::vector<double> b(n.size(), 0.0);
298 for (std::size_t t = 0; t < n.size(); ++t) {
299 const std::vector<double> head(lseq.begin(),
300 lseq.begin() + static_cast<std::ptrdiff_t>(n[t]));
301 const double tail = lg1 + std::log1p(-std::exp(detail::busyp_lse(head) - lg1));
302 b[t] = std::exp(tail - lseq[n[t] - 1] - std::log(inflow));
303 }
304 return b;
305 }
306
307 std::vector<int> pop(R, 0);
308 std::vector<std::size_t> bound(R, 0);
309 for (std::size_t r = 0; r < R; ++r) {
310 pop[r] = static_cast<int>(std::llround(N[r]));
311 bound[r] = std::min<std::size_t>(static_cast<std::size_t>(pop[r]), nmax - 1);
312 }
313 std::vector<std::size_t> stride(R, 1);
314 for (std::size_t r = 1; r < R; ++r) stride[r] = stride[r - 1] * (bound[r - 1] + 1);
315 std::size_t size = 1;
316 for (std::size_t r = 0; r < R; ++r) size *= bound[r] + 1;
317 std::vector<std::vector<std::size_t>> mvec(size, std::vector<std::size_t>(R, 0));
318 for (std::size_t idx = 0; idx < size; ++idx)
319 for (std::size_t r = 0; r < R; ++r) mvec[idx][r] = (idx / stride[r]) % (bound[r] + 1);
320
321 // the low shells of the subnetwork, the only lattice this routine walks
322 std::vector<double> lGlow(size, 0.0);
323 for (std::size_t idx = 0; idx < size; ++idx)
324 lGlow[idx] = detail::busyp_clw_station(L, delay, target, 0, mvec[idx]);
325
326 const double lGfull = detail::busyp_clw_lognc(L, delay, all_nodes, pop, method);
327
328 std::vector<double> b(n.size(), 0.0);
329 for (std::size_t t = 0; t < n.size(); ++t) {
330 std::vector<double> corr, den;
331 for (std::size_t idx = 0; idx < size; ++idx) {
332 std::size_t level = 0;
333 for (std::size_t r = 0; r < R; ++r) level += mvec[idx][r];
334 if (level + 1 <= n[t]) {
335 // numerator: the full constant minus the shells the level set excludes
336 std::vector<int> left(R, 0);
337 for (std::size_t r = 0; r < R; ++r)
338 left[r] = pop[r] - static_cast<int>(mvec[idx][r]);
339 corr.push_back(lGlow[idx] +
340 detail::busyp_clw_lognc(L, delay, compl_nodes, left, method));
341 }
342 if (level + 1 != n[t]) continue;
343 // denominator: the flow out of the shell |m| = n-1
344 std::vector<double> terms;
345 for (std::size_t r = 0; r < R; ++r) {
346 if (A[r] <= 0) continue;
347 std::vector<int> left(R, 0);
348 bool ok = true;
349 for (std::size_t s = 0; s < R; ++s) {
350 left[s] = pop[s] - static_cast<int>(mvec[idx][s]) - (s == r ? 1 : 0);
351 if (left[s] < 0) ok = false;
352 }
353 if (!ok) continue;
354 terms.push_back(std::log(A[r]) +
355 detail::busyp_clw_lognc(L, delay, compl_nodes, left, method));
356 }
357 if (!terms.empty()) den.push_back(lGlow[idx] + detail::busyp_lse(terms));
358 }
359 const double num = lGfull + std::log1p(-std::exp(detail::busyp_lse(corr) - lGfull));
360 b[t] = std::exp(num - detail::busyp_lse(den));
361 }
362 return b;
363}
364
365} // namespace pfqn
366} // namespace line
367
368#endif // LINE_API_PFQN_PFQN_BUSYP_CLW_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.
std::vector< double > pfqn_busyp_clw(const Matrix< T > &alpha, const Matrix< T > &mu, const std::vector< Matrix< T > > &P, const std::vector< double > &N, const std::vector< std::size_t > &subnet, const std::vector< std::size_t > &n, const Matrix< T > &gamma=Matrix< T >(), const std::vector< bool > &isdelay=std::vector< bool >(), const std::string &method="clw")
Mean busy period of order n for the subnetwork, via NC point evaluations.
ClwResult< T > pfqn_clw(const Matrix< T > &L, const std::vector< int > &N, const std::vector< T > &Z, const std::vector< long > &m, const ClwOptions &opt)
Choudhury-Leung-Whitt normalization constant by numerical inversion of the generating function (JACM ...
Definition pfqn_clw.h:608
Number-type abstraction for the templated API port.
Mean busy period of order n for a subnetwork of a product-form network.
Multichain generalization of pfqn_busyp.
Choudhury-Leung-Whitt normalization constant by numerical inversion of the generating function (JACM ...