LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_busyp_multiclass.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_MULTICLASS_H
6#define LINE_API_PFQN_PFQN_BUSYP_MULTICLASS_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Multichain generalization of `pfqn_busyp`.
12 *
13 * Templated port of matlab/src/api/pfqn/pfqn_busyp_multiclass.m,
14 * jar/src/main/java/jline/api/pfqn/Pfqn_busyp_multiclass.java and
15 * python/line_solver/api/pfqn/busyp_multiclass.py.
16 *
17 * Daduna (J. ACM 35(3), 1988) states Theorems 1 and 3 for a single chain and
18 * notes in Section 5 that they carry over to the whole product-form class. The
19 * proof uses only that the stationary law is product form and that the busy
20 * period is Keilson's mean ergodic sojourn time on a level set, neither of which
21 * is single-chain, so replacing the scalar population by a per-chain vector m
22 * gives, for a closed network,
23 *
24 * sum_{m : |m| >= n} G_I(m) H(N-m)
25 * b(n,I) = --------------------------------------------------
26 * sum_{m : |m| = n-1} G_I(m) sum_r A_r(I) H(N-m-e_r)
27 *
28 * with G_I and H the normalizing constants of the subnetwork and of its
29 * complement at a population VECTOR and A_r(I) the chain-r arrival flow into I.
30 * The denominator is the exact chain-r flow across the cut: a chain-r departure
31 * from the complement at population k occurs at rate alpha_ir H(k-e_r)/H(k),
32 * and the H(k) cancels the state weight. At R=1 the inner sum holds the single
33 * term m=n-1 and H(N-m-e_1)=H(N-n), so it collapses to Theorem 1 exactly, which
34 * is the regression the test runs.
35 *
36 * THE OPEN CASE NEEDS NO LATTICE. In an open product-form network the stations
37 * are independent and the total occupancy of a node depends on the AGGREGATE
38 * load sum_r alpha_ir/mu_ir alone, since summing the station function over the
39 * compositions of t collapses the multinomial to (sum_r rho_ir)^t. It is
40 * therefore reduced here to the single-chain routine on aggregated demands.
41 *
42 * PER CLASS: with jobclass = r the level set becomes {m_r >= n}, the jobs of
43 * chain r alone. Only chain-r arrivals move that level, so the flow sum loses its
44 * sum over r and the same two lattices serve every class.
45 *
46 * A MIXED MODEL keeps the closed lattice with its OPEN dimensions TRUNCATED. The
47 * closed chains are conserved between the subnetwork and its complement, the open
48 * ones are not: the complement's open count is free, so its open dimensions are
49 * summed out and no e_r shift applies to an open chain, removing one job from an
50 * unbounded dimension leaving the same sum. The truncation grows until the answer
51 * stops moving, and is the only approximation in that branch. For a per-class
52 * query on a CLOSED chain there is an exact shortcut: marginalizing the open
53 * chains leaves a closed network with the demands deflated by 1/(1-rho_i^open),
54 * where the RATES and not the visits must be deflated, since A_r is built from
55 * the visit ratios.
56 *
57 * ARITHMETIC: log domain in double, as in the three reference implementations.
58 */
59
60#include <algorithm>
61#include <cmath>
62#include <cstddef>
63#include <limits>
64#include <vector>
65
67#include "line/num/number.h"
68#include "line/util/error.h"
69#include "line/util/matrix.h"
70
71namespace line {
72namespace pfqn {
73
74namespace detail {
75
76/** log(k!). */
77inline double busyp_factln(std::size_t k) {
78 return (k < 2) ? 0.0 : std::lgamma(static_cast<double>(k) + 1.0);
79}
80
81/**
82 * log X_i(m) over the lattice for one node.
83 *
84 * X_i(m) = multinomial(|m|; m) prod_r L(i,r)^m_r / prod_{k=1}^{|m|} phi_i(k),
85 * which at R=1 is the prod_k alpha_i/mu_i(k) of the single-chain routine and at
86 * phi(k)=k the infinite-server form prod_r L^m_r/m_r!.
87 */
88inline std::vector<double> busyp_station(const std::vector<double>& Li,
89 const std::vector<double>& phii,
90 const std::vector<std::vector<std::size_t>>& mvec) {
91 const std::size_t size = mvec.size(), R = mvec.empty() ? 0 : mvec[0].size();
92 std::vector<double> out(size, 0.0);
93 for (std::size_t idx = 0; idx < size; ++idx) {
94 std::size_t tot = 0;
95 for (std::size_t r = 0; r < R; ++r) tot += mvec[idx][r];
96 double v = busyp_factln(tot);
97 bool ok = true;
98 for (std::size_t r = 0; r < R && ok; ++r) {
99 if (mvec[idx][r] == 0) continue;
100 if (Li[r] <= 0)
101 ok = false;
102 else
103 v += -busyp_factln(mvec[idx][r]) +
104 static_cast<double>(mvec[idx][r]) * std::log(Li[r]);
105 }
106 if (!ok) {
107 out[idx] = -std::numeric_limits<double>::infinity();
108 continue;
109 }
110 for (std::size_t k = 1; k <= tot; ++k)
111 v -= std::log(phii[std::min(k, phii.size()) - 1]);
112 out[idx] = v;
113 }
114 return out;
115}
116
117/**
118 * Log normalizing constants over the whole lattice of a set of nodes. A node
119 * whose scaling row is all ones takes the Buzen recursion, O(R) per lattice
120 * point; any other node needs the full sub-lattice convolution.
121 */
122inline std::vector<double> busyp_lgvec_multi(
123 const std::vector<std::vector<double>>& L, const std::vector<std::vector<double>>& phi,
124 const std::vector<std::vector<std::size_t>>& mvec, const std::vector<std::size_t>& stride,
125 const std::vector<std::size_t>& pop) {
126 const double neg_inf = -std::numeric_limits<double>::infinity();
127 const std::size_t nodes = L.size(), R = pop.size(), size = mvec.size();
128 std::size_t totalN = 0;
129 for (std::size_t r = 0; r < R; ++r) totalN += pop[r];
130 std::vector<double> lg(size, neg_inf);
131 lg[0] = 0.0;
132 for (std::size_t i = 0; i < nodes; ++i) {
133 bool is_li = true;
134 for (std::size_t k = 0; k < std::min(phi[i].size(), std::max<std::size_t>(1, totalN)); ++k)
135 if (phi[i][k] != 1.0) is_li = false;
136 if (is_li) {
137 std::vector<double> lgnew = lg;
138 for (std::size_t idx = 0; idx < size; ++idx) {
139 double acc = lgnew[idx];
140 for (std::size_t r = 0; r < R; ++r)
141 if (mvec[idx][r] > 0 && L[i][r] > 0) {
142 const double alt = std::log(L[i][r]) + lgnew[idx - stride[r]];
143 acc = busyp_lse(std::vector<double>{acc, alt});
144 }
145 lgnew[idx] = acc;
146 }
147 lg.swap(lgnew);
148 } else {
149 const std::vector<double> lX = busyp_station(L[i], phi[i], mvec);
150 std::vector<double> lgnew(size, neg_inf);
151 for (std::size_t a = 0; a < size; ++a) {
152 if (lg[a] == neg_inf) continue;
153 for (std::size_t c = 0; c < size; ++c) {
154 if (lX[c] == neg_inf) continue;
155 bool fits = true;
156 std::size_t j = 0;
157 for (std::size_t r = 0; r < R && fits; ++r) {
158 const std::size_t s = mvec[a][r] + mvec[c][r];
159 if (s > pop[r])
160 fits = false;
161 else
162 j += s * stride[r];
163 }
164 if (fits) lgnew[j] = busyp_lse(std::vector<double>{lgnew[j], lg[a] + lX[c]});
165 }
166 }
167 lg.swap(lgnew);
168 }
169 }
170 return lg;
171}
172
173/**
174 * The lattice evaluation shared by the closed and the mixed branch. `pop` bounds
175 * every chain: the population of a closed one, the truncation of an open one;
176 * `open_chain` names the dimensions that are NOT conserved, whose complement
177 * counts are summed out rather than read at N-m.
178 */
179inline std::vector<double> busyp_lattice(const std::vector<std::vector<double>>& L,
180 const std::vector<std::vector<double>>& scaling,
181 const std::vector<std::size_t>& target,
182 const std::vector<std::size_t>& compl_nodes,
183 const std::vector<std::size_t>& pop,
184 const std::vector<std::size_t>& n,
185 const std::vector<double>& A, int jobclass,
186 const std::vector<bool>& open_chain) {
187 const double neg_inf = -std::numeric_limits<double>::infinity();
188 const std::size_t R = pop.size();
189 std::vector<std::size_t> stride(R, 1);
190 for (std::size_t r = 1; r < R; ++r) stride[r] = stride[r - 1] * (pop[r - 1] + 1);
191 std::size_t size = 1;
192 for (std::size_t r = 0; r < R; ++r) size *= pop[r] + 1;
193 std::vector<std::vector<std::size_t>> mvec(size, std::vector<std::size_t>(R, 0));
194 for (std::size_t idx = 0; idx < size; ++idx)
195 for (std::size_t r = 0; r < R; ++r) mvec[idx][r] = (idx / stride[r]) % (pop[r] + 1);
196
197 std::vector<std::vector<double>> Lsub, Lcompl, Psub, Pcompl;
198 for (std::size_t i = 0; i < target.size(); ++i) {
199 Lsub.push_back(L[target[i]]);
200 Psub.push_back(scaling[target[i]]);
201 }
202 for (std::size_t i = 0; i < compl_nodes.size(); ++i) {
203 Lcompl.push_back(L[compl_nodes[i]]);
204 Pcompl.push_back(scaling[compl_nodes[i]]);
205 }
206 const std::vector<double> lG = busyp_lgvec_multi(Lsub, Psub, mvec, stride, pop);
207 const std::vector<double> lH = busyp_lgvec_multi(Lcompl, Pcompl, mvec, stride, pop);
208
209 // Hbar sums the complement over its unconserved dimensions, so it is indexed by
210 // the CLOSED components alone; with no open chain it is lH itself.
211 bool any_open = false;
212 for (std::size_t r = 0; r < R; ++r) any_open = any_open || open_chain[r];
213 std::vector<double> lHbar = lH;
214 if (any_open) {
215 lHbar.assign(size, neg_inf);
216 for (std::size_t idx = 0; idx < size; ++idx) {
217 std::size_t j = 0;
218 for (std::size_t r = 0; r < R; ++r)
219 if (!open_chain[r]) j += mvec[idx][r] * stride[r];
220 lHbar[j] = busyp_lse(std::vector<double>{lHbar[j], lH[idx]});
221 }
222 }
223
224 std::vector<double> b(n.size(), 0.0);
225 for (std::size_t t = 0; t < n.size(); ++t) {
226 const std::size_t nt = n[t];
227 std::vector<double> num, den;
228 for (std::size_t idx = 0; idx < size; ++idx) {
229 // the level set is |m| for the aggregate busy period and m_r for the
230 // class-r one; only chain-r arrivals move m_r
231 std::size_t level = 0;
232 if (jobclass < 0)
233 for (std::size_t r = 0; r < R; ++r) level += mvec[idx][r];
234 else
235 level = mvec[idx][static_cast<std::size_t>(jobclass)];
236 if (level >= nt) {
237 std::size_t j = 0;
238 for (std::size_t r = 0; r < R; ++r)
239 if (!open_chain[r]) j += (pop[r] - mvec[idx][r]) * stride[r];
240 num.push_back(lG[idx] + lHbar[j]);
241 }
242 if (level + 1 != nt) continue;
243 std::vector<double> terms;
244 for (std::size_t r = 0; r < R; ++r) {
245 if (jobclass >= 0 && r != static_cast<std::size_t>(jobclass)) continue;
246 if (A[r] <= 0) continue;
247 if (!open_chain[r] && pop[r] == mvec[idx][r]) continue;
248 std::size_t j = 0;
249 for (std::size_t s = 0; s < R; ++s) {
250 if (open_chain[s]) continue;
251 // a closed chain conserves jobs, so the departing one is removed
252 j += (pop[s] - mvec[idx][s] - (s == r ? 1 : 0)) * stride[s];
253 }
254 terms.push_back(std::log(A[r]) + lHbar[j]);
255 }
256 if (!terms.empty()) den.push_back(lG[idx] + busyp_lse(terms));
257 }
258 b[t] = std::exp(busyp_lse(num) - busyp_lse(den));
259 }
260 return b;
261}
262
263} // namespace detail
264
265/**
266 * Mean busy period of order n for the subnetwork, multichain.
267 *
268 * @param alpha (J x R) relative arrival rates, one column per chain
269 * @param mu (J x R) service rates, the chain-r rate at node j
270 * @param P routing matrices, one per chain (size 1 = shared by all chains)
271 * @param N population per chain, infinite entries for an open chain
272 * @param subnet zero-based node indexes forming the subnetwork
273 * @param n busy period orders, counting the jobs of every chain
274 * @param gamma (J x R) external arrival rates, empty for a closed network
275 * @param phi (J x K) dimensionless load-dependent scaling, empty = single server
276 * @param tol relative tolerance of the open-network tail truncation
277 * @param jobclass zero-based chain whose own jobs are counted, -1 for every chain
278 */
279template <class T>
280std::vector<double> pfqn_busyp_multiclass(const Matrix<T>& alpha, const Matrix<T>& mu,
281 const std::vector<Matrix<T>>& P,
282 const std::vector<double>& N,
283 const std::vector<std::size_t>& subnet,
284 const std::vector<std::size_t>& n,
285 const Matrix<T>& gamma = Matrix<T>(),
286 const Matrix<T>& phi = Matrix<T>(),
287 double tol = PFQN_BUSYP_DEFAULT_TOL,
288 int jobclass = -1) {
289 const std::size_t J = alpha.rows(), R = alpha.cols();
290 if (N.size() != R)
291 throw InputError(
292 "pfqn_busyp_multiclass: the population vector must have one entry per chain");
293 bool is_closed = true, is_open = true;
294 for (std::size_t r = 0; r < R; ++r) {
295 if (std::isinf(N[r]))
296 is_closed = false;
297 else
298 is_open = false;
299 }
300 const bool is_mixed = !is_closed && !is_open;
301
302 std::vector<std::size_t> target = subnet;
303 std::sort(target.begin(), target.end());
304 target.erase(std::unique(target.begin(), target.end()), target.end());
305 if (target.empty())
306 throw InputError("pfqn_busyp_multiclass: the subnetwork must be non-empty");
307 if (is_closed && target.size() >= J)
308 throw InputError(
309 "pfqn_busyp_multiclass: in a closed network the subnetwork must be a proper "
310 "subset of the nodes");
311 if (target.back() >= J)
312 throw InputError("pfqn_busyp_multiclass: the subnetwork indexes are out of range");
313 std::vector<bool> in_subnet(J, false);
314 for (std::size_t i = 0; i < target.size(); ++i) in_subnet[target[i]] = true;
315 std::vector<std::size_t> compl_nodes;
316 for (std::size_t j = 0; j < J; ++j)
317 if (!in_subnet[j]) compl_nodes.push_back(j);
318
319 std::size_t totalN = 0;
320 for (std::size_t r = 0; r < R; ++r)
321 if (!std::isinf(N[r])) totalN += static_cast<std::size_t>(std::llround(N[r]));
322
323 std::vector<std::vector<double>> scaling(J);
324 for (std::size_t i = 0; i < J; ++i) {
325 if (phi.rows() == J && phi.cols() > 0) {
326 scaling[i].resize(phi.cols());
327 for (std::size_t k = 0; k < phi.cols(); ++k)
328 scaling[i][k] = num_traits<T>::to_double(phi(i, k));
329 } else {
330 scaling[i].assign(std::max<std::size_t>(1, totalN), 1.0);
331 }
332 }
333
334 // demands L(i,r) = alpha(i,r)/mu(i,r), zero where chain r does not visit node i
335 std::vector<std::vector<double>> L(J, std::vector<double>(R, 0.0));
336 for (std::size_t i = 0; i < J; ++i)
337 for (std::size_t r = 0; r < R; ++r) {
338 const double a = num_traits<T>::to_double(alpha(i, r));
339 const double m = num_traits<T>::to_double(mu(i, r));
340 L[i][r] = (a > 0 && m > 0) ? a / m : 0.0;
341 }
342
343 // A_r(I): the chain-r rate at which jobs enter the subnetwork from outside it
344 std::vector<double> A(R, 0.0);
345 double inflow = 0.0;
346 for (std::size_t r = 0; r < R; ++r) {
347 const Matrix<T>& Pr = (P.size() == 1) ? P[0] : P[r];
348 for (std::size_t i = 0; i < compl_nodes.size(); ++i)
349 for (std::size_t j = 0; j < target.size(); ++j)
350 A[r] += num_traits<T>::to_double(alpha(compl_nodes[i], r)) *
351 num_traits<T>::to_double(Pr(compl_nodes[i], target[j]));
352 if (gamma.rows() == J)
353 for (std::size_t j = 0; j < target.size(); ++j)
354 A[r] += num_traits<T>::to_double(gamma(target[j], r));
355 inflow += A[r];
356 }
357 if (inflow <= 0)
358 throw InputError(
359 "pfqn_busyp_multiclass: no job ever enters the subnetwork, its busy period is "
360 "undefined");
361 if (jobclass >= static_cast<int>(R))
362 throw InputError("pfqn_busyp_multiclass: the job class index is out of range");
363 if (jobclass >= 0 && A[static_cast<std::size_t>(jobclass)] <= 0)
364 throw InputError(
365 "pfqn_busyp_multiclass: no job of that class ever enters the subnetwork");
366
367 if (is_open && !is_mixed) {
368 // exact reduction to the single-chain routine on a per-station scalar; the
369 // synthetic problem carries no routing, the whole inflow riding on gamma
370 std::vector<double> rho(J, 0.0);
371 for (std::size_t i = 0; i < J; ++i)
372 for (std::size_t r = 0; r < R; ++r) rho[i] += L[i][r];
373 Matrix<double> zeroP(J, J, 0.0);
374 std::vector<double> gsyn(J, 0.0);
375 if (jobclass < 0) {
376 gsyn[target[0]] = inflow;
377 } else {
378 // the class-r marginal is geometric in rho_ir/(1-rho_i+rho_ir), NOT in
379 // rho_ir: the other classes inflate the queue the class-r jobs sit in.
380 // That collapse assumes a load-INDEPENDENT station.
381 const std::size_t rr = static_cast<std::size_t>(jobclass);
382 for (std::size_t t = 0; t < target.size(); ++t)
383 for (std::size_t k = 0; k < scaling[target[t]].size(); ++k)
384 if (scaling[target[t]][k] != 1.0)
385 throw InputError(
386 "pfqn_busyp_multiclass: a per-class busy period of an open "
387 "subnetwork requires load-independent stations");
388 for (std::size_t i = 0; i < J; ++i) {
389 const double den = 1.0 - rho[i] + L[i][rr];
390 rho[i] = (den > 0) ? L[i][rr] / den : 0.0;
391 }
392 gsyn[target[0]] = A[rr];
393 }
394 const std::function<double(std::size_t, std::size_t)> rates =
395 [&](std::size_t j, std::size_t k) {
396 return scaling[j][std::min(k, scaling[j].size()) - 1];
397 };
398 return pfqn_busyp(rho, rates, zeroP, std::numeric_limits<double>::infinity(), target,
399 n, gsyn, tol)
400 .b;
401 }
402
403 if (!is_mixed) {
404 const std::size_t bound = (jobclass < 0)
405 ? totalN
406 : static_cast<std::size_t>(std::llround(N[static_cast<std::size_t>(jobclass)]));
407 for (std::size_t t = 0; t < n.size(); ++t)
408 if (n[t] < 1 || n[t] > bound)
409 throw InputError(
410 "pfqn_busyp_multiclass: the busy period order must be an integer in "
411 "1..sum(N), or in 1..N(r) for the busy period of class r alone");
412 std::vector<std::size_t> pop(R, 0);
413 for (std::size_t r = 0; r < R; ++r)
414 pop[r] = static_cast<std::size_t>(std::llround(N[r]));
415 const std::vector<bool> open_chain(R, false);
416 return detail::busyp_lattice(L, scaling, target, compl_nodes, pop, n, A, jobclass,
417 open_chain);
418 }
419
420 // A mixed model grows the truncation of its open dimensions until the answer stops
421 // moving; that truncation is the only approximation in this branch.
422 std::vector<bool> open_chain(R, false);
423 for (std::size_t r = 0; r < R; ++r) open_chain[r] = std::isinf(N[r]);
424 std::size_t nmax = 1;
425 for (std::size_t t = 0; t < n.size(); ++t) nmax = std::max(nmax, n[t]);
426 std::size_t trunc = 8 + 2 * nmax;
427 std::vector<double> prev;
428 while (true) {
429 std::vector<std::size_t> pop(R, 0);
430 for (std::size_t r = 0; r < R; ++r)
431 pop[r] = open_chain[r] ? trunc : static_cast<std::size_t>(std::llround(N[r]));
432 const std::vector<double> b = detail::busyp_lattice(L, scaling, target, compl_nodes,
433 pop, n, A, jobclass, open_chain);
434 if (!prev.empty()) {
435 bool settled = true;
436 for (std::size_t t = 0; t < b.size(); ++t)
437 if (std::fabs(b[t] - prev[t]) > 1e-10 * std::fabs(b[t])) settled = false;
438 if (settled) return b;
439 }
440 prev = b;
441 trunc *= 2;
442 if (trunc > 4096)
443 throw NumericError(
444 "pfqn_busyp_multiclass: the mixed busy period did not converge, so some "
445 "station of the subnetwork is nearly saturated");
446 }
447}
448
449} // namespace pfqn
450} // namespace line
451
452#endif // LINE_API_PFQN_PFQN_BUSYP_MULTICLASS_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
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
std::vector< double > pfqn_busyp_multiclass(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 Matrix< T > &phi=Matrix< T >(), double tol=PFQN_BUSYP_DEFAULT_TOL, int jobclass=-1)
Mean busy period of order n for the subnetwork, multichain.
Number-type abstraction for the templated API port.
Mean busy period of order n for a subnetwork of a product-form network.
std::vector< double > b
mean duration per requested order
Definition pfqn_busyp.h:166