LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_clwoi.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_CLWOI_H
6#define LINE_API_PFQN_CLWOI_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Normalizing constant of a closed network of ORDER-INDEPENDENT (OI) stations
12 * plus one aggregated delay, by numerical inversion of the multichain
13 * generating function (Choudhury-Leung-Whitt, J. ACM 42(5):935-970, 1995).
14 *
15 * Templated port of matlab/src/api/pfqn/pfqn_clwoi.m. It is the OI counterpart
16 * of pfqn_clw_lld and the transform counterpart of the convolution routine
17 * pfqn_ncoi; both return the same G(N) and differ only in cost.
18 *
19 * G(z) = exp( sum_r Z_r z_r ) prod_i F_i(z), F_i(z) = sum_n Phi_i(n) z^n,
20 *
21 * with Phi_i the v-weighted balanced-fairness balance function of station i,
22 * mu_i(n) Phi_i(n) = sum_{r: n_r>0} v_{i,r} Phi_i(n - e_r).
23 *
24 * Unlike a load-dependent station, whose factor collapses to a function of the
25 * single argument sum_r rho_{ri} z_r, an OI station factor depends on the whole
26 * vector z, because mu_i(n) depends on the occupancy only through its SUPPORT
27 * supp(n) = {r : n_r > 0}. It is nevertheless rational and available in closed
28 * form. Splitting the count lattice by support, on which mu_i(n) = mu_{i,S} is
29 * constant, and writing F_{i,S} for the part of F_i carried by the states of
30 * support S, the balance recursion gives
31 *
32 * ( mu_{i,S} - sum_{r in S} v_{i,r} z_r ) F_{i,S}(z)
33 * = sum_{r in S} v_{i,r} z_r F_{i,S minus r}(z), F_{i,{}} = 1,
34 * F_i(z) = sum_S F_{i,S}(z),
35 *
36 * since removing a class-r job from a state of support S lands on support S
37 * when n_r >= 2 and on S minus r when n_r = 1. The singularities are the hyperplanes
38 * sum_{r in S} v_{i,r} z_r = mu_{i,S}, one per support, in place of the single
39 * pole x = c_i of the load-dependent case; a load-independent single-server
40 * queue (mu_{i,S} = 1) gives back 1/(1 - sum_r v_{i,r} z_r).
41 *
42 * The restrictive static scaling of eqs. 5.41-5.46 is reused verbatim on the
43 * EXPANDED constraint matrix that lists one row per (station, nonempty support)
44 * pair with unit-pole intensities v_{i,r}/mu_{i,S}, after dropping the rows
45 * dominated by a superset of no larger rate. Each surviving row is a binding
46 * singular hyperplane, so the contour stays inside the domain of analyticity
47 * exactly as the single-pole normalization does for pfqn_clw_lld.
48 *
49 * SCOPE. The rates must be support-only, mu_i(n) = mu_i(supp(n)), which is the
50 * defining property of an OI station and what makes the transform a finite
51 * rational function. Every rate handle is verified EXHAUSTIVELY on the count
52 * lattice 0 < n <= N before the inversion, at prod_r (N_r+1) evaluations per
53 * station (below the contour points spent afterwards), and a state whose rate
54 * differs from that of its support is an error naming that state: a rate that
55 * varies inside a support is a general balanced-fairness station and belongs to
56 * pfqn_ncoi. It is refused rather than warned-and-inverted because the
57 * inversion would otherwise return a plausible but wrong G(N). A non-empty swap
58 * graph breaks the closure of Phi on the count vector altogether and requires
59 * the microstate routine pfqn_pas_nc.
60 *
61 * Arithmetic: TRANSCENDENTAL, double and Real only, for the same reason as the
62 * rest of the CLW family (the contour radius 10^{-gamma/(2 l K)} is not in the
63 * field of the inputs). Accuracy against the exact pfqn_ncoi is ~1e-9 for two
64 * chains and ~1e-8 for three, i.e. the accuracy the method itself has.
65 *
66 * COST. prod_r 2 l_r N_r contour points, each costing O(M R 2^R), against
67 * O(M prod_r (N_r+1)(N_r+2)/2) for pfqn_ncoi: linear rather than quadratic in
68 * each population, so it wins on large populations with few chains and loses as
69 * the chain count grows. Unlike pfqn_ncoi it returns G at the single population
70 * N; throughputs need the R additional inversions at N - e_r.
71 */
72
73#include <algorithm>
74#include <cmath>
75#include <cstddef>
76#include <limits>
77#include <string>
78#include <vector>
79
82#include "line/num/number.h"
83#include "line/util/error.h"
84#include "line/util/matrix.h"
85
86namespace line {
87namespace pfqn {
88
89namespace detail {
90
91/**
92 * The rate of one support, read at its 0/1 indicator. The indicator is itself a
93 * lattice point of that support, every retained chain having N_r >= 1;
94 * constancy over the support is verified by clwoi_check_support.
95 */
96template <class T>
97T clwoi_support_rate(const OiRate<T>& murate, const std::vector<int>& chi) {
98 const T zero = num_traits<T>::from_int(0);
99 const T rate = murate(chi);
100 if (!(rate > zero))
101 throw InputError("pfqn_clwoi: a station has a non-positive rate on a reachable support");
102 return rate;
103}
104
105/** Formats a count vector as "[n1 n2 ...]" for the diagnostics below. */
106inline std::string clwoi_state_str(const std::vector<int>& n) {
107 std::string s = "[";
108 for (std::size_t r = 0; r < n.size(); ++r) {
109 if (r) s += " ";
110 s += std::to_string(n[r]);
111 }
112 return s + "]";
113}
114
115/**
116 * Exhaustive support-only check: every state 0 < n <= N is compared against the
117 * rate of its own support. The transform is exact only if mu_i is constant on
118 * each support, and a rate that violates that yields a wrong G with no other
119 * symptom, so it is refused rather than inverted. The scan costs
120 * prod_r (N_r+1) rate evaluations per station, below the prod_r 2 l_r N_r
121 * contour points the inversion itself spends (l_r >= 1 gives 2 l_r N_r >= N_r+1).
122 */
123template <class T>
124void clwoi_check_support(const std::vector<OiRate<T>>& mu, const Matrix<T>& muS,
125 const std::vector<std::size_t>& keep, const std::vector<int>& Nk,
126 std::size_t R) {
127 const std::size_t M = mu.size(), p = keep.size();
128 if (M == 0) return;
129 std::size_t L = 1;
130 for (std::size_t j = 0; j < p; ++j) L *= static_cast<std::size_t>(Nk[j]) + 1;
131 for (std::size_t i = 0; i < M; ++i) {
132 std::vector<int> n(R, 0);
133 for (std::size_t idx = 1; idx < L; ++idx) { // idx 0 is the empty support, unused
134 std::size_t rem = idx, mask = 0;
135 for (std::size_t j = 0; j < p; ++j) {
136 const std::size_t base = static_cast<std::size_t>(Nk[j]) + 1;
137 const std::size_t nj = rem % base;
138 rem /= base;
139 n[keep[j]] = static_cast<int>(nj);
140 if (nj > 0) mask |= static_cast<std::size_t>(1) << j;
141 }
142 const T rate = mu[i](n);
143 const T ref = muS(i, mask);
144 T diff = T(rate - ref);
145 if (diff < num_traits<T>::from_int(0)) diff = T(-diff);
146 const T scale = (ref > num_traits<T>::from_int(1)) ? ref : num_traits<T>::from_int(1);
147 if (diff > num_traits<T>::from_double(1e-9) * scale) {
148 std::vector<int> chi(R, 0);
149 for (std::size_t j = 0; j < p; ++j)
150 if (mask & (static_cast<std::size_t>(1) << j)) chi[keep[j]] = 1;
151 throw InputError(
152 "pfqn_clwoi: station " + std::to_string(i) +
153 " has a rate that varies within a support (state " + clwoi_state_str(n) +
154 " against the indicator " + clwoi_state_str(chi) +
155 "); pfqn_clwoi requires order-independent (support-only) rates, "
156 "mu(n)=mu(supp(n)), use pfqn_ncoi for a general balanced-fairness station");
157 }
158 }
159 }
160}
161
162} // namespace detail
163
164/**
165 * @brief Normalizing constant of a closed network of ORDER-INDEPENDENT (OI)
166 * stations plus one aggregated delay, by numerical inversion of the
167 * multichain generating function (Choudhury-Leung-Whitt, J. ACM
168 * 42(5):935-970, 1995).
169 *
170 * @param Z (R) think-time demand of the aggregated delay node
171 * @param N (R) closed population, finite
172 * @param mu one rate handle per OI station, mapping a per-class count vector
173 * to the total service rate; must depend on the count vector only
174 * through its support. Empty for a pure delay network
175 * @param visits (M x R) per-station class visit ratios weighting the balance
176 * recursion; empty for unit visits
177 * @param opt lattice and aliasing parameters
178 */
179template <class T>
180ClwResult<T> pfqn_clwoi(const std::vector<T>& Z, const std::vector<int>& N,
181 const std::vector<OiRate<T>>& mu, const Matrix<T>& visits,
182 const ClwOptions& opt) {
184 "pfqn_clwoi requires transcendental arithmetic (contour integration of a "
185 "generating function)");
186 using std::exp;
187 using std::log;
188 const std::size_t R = N.size();
189 const std::size_t M = mu.size();
190 if (!Z.empty() && Z.size() != R)
191 throw InputError("pfqn_clwoi: Z and N disagree on the chain count");
192 if (!visits.empty() && (visits.rows() != M || visits.cols() != R))
193 throw InputError("pfqn_clwoi: visits must be M x R");
194 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
195
196 ClwResult<T> res;
197 long Ntot = 0;
198 for (std::size_t r = 0; r < R; ++r) {
199 if (N[r] < 0) {
200 res.G = zero;
201 res.lG = T(-std::numeric_limits<T>::infinity());
202 return res;
203 }
204 Ntot += N[r];
205 }
206 if (Ntot == 0) {
207 res.G = one;
208 res.lG = zero;
209 return res;
210 }
211
212 std::vector<int> lfull;
213 std::vector<double> gfull;
214 detail::clw_defaults(R, opt, lfull, gfull);
215
216 // drop zero-population chains: the coefficient of z_r^0 is the generating
217 // function at z_r = 0, which kills every F_{i,S} with r in S
218 std::vector<std::size_t> keep;
219 for (std::size_t r = 0; r < R; ++r)
220 if (N[r] > 0) keep.push_back(r);
221 const std::size_t p = keep.size();
222 std::vector<int> Nk(p, 0), l(p, 1);
223 std::vector<T> Zk(p, zero);
224 std::vector<double> gam(p, 0.0);
225 for (std::size_t j = 0; j < p; ++j) {
226 Nk[j] = N[keep[j]];
227 Zk[j] = Z.empty() ? zero : Z[keep[j]];
228 l[j] = lfull[keep[j]];
229 gam[j] = gfull[keep[j]];
230 }
231 const std::size_t nmask = static_cast<std::size_t>(1) << p;
232
233 // support rate table mu_{i,S}, S a bitmask over the retained chains
234 Matrix<T> muS(M ? M : 1, nmask, zero);
235 for (std::size_t i = 0; i < M; ++i)
236 for (std::size_t mask = 1; mask < nmask; ++mask) {
237 std::vector<int> chi(R, 0);
238 for (std::size_t b = 0; b < p; ++b)
239 if (mask & (static_cast<std::size_t>(1) << b)) chi[keep[b]] = 1;
240 muS(i, mask) = detail::clwoi_support_rate<T>(mu[i], chi);
241 }
242 detail::clwoi_check_support<T>(mu, muS, keep, Nk, R);
243
244 // per-mask chain lists and the S minus r column indices of the recursion
245 std::vector<std::vector<std::size_t>> bits(nmask), subcol(nmask);
246 for (std::size_t mask = 1; mask < nmask; ++mask)
247 for (std::size_t b = 0; b < p; ++b)
248 if (mask & (static_cast<std::size_t>(1) << b)) {
249 bits[mask].push_back(b);
250 subcol[mask].push_back(mask & ~(static_cast<std::size_t>(1) << b));
251 }
252
253 // per-station visit vectors restricted to the retained chains
254 Matrix<T> V(M ? M : 1, p, one);
255 for (std::size_t i = 0; i < M; ++i)
256 for (std::size_t b = 0; b < p; ++b) V(i, b) = visits.empty() ? one : visits(i, keep[b]);
257
258 std::vector<T> r(p, one);
259 for (std::size_t j = 0; j < p; ++j)
260 r[j] = detail::clw_pow_real(
262 T(num_traits<T>::from_double(-gam[j]) /
263 num_traits<T>::from_int(2 * static_cast<long>(l[j]) * Nk[j])));
264
265 // One constraint row per (station, nonempty support), holding the unit-pole
266 // intensities v_{i,r}/mu_{i,S} of that singular hyperplane. Support S is
267 // dominated by a superset S' with mu_{i,S'} <= mu_{i,S}, since then
268 // v/mu_{S'} >= v/mu_S on all of S; keeping such slack rows would perturb the
269 // group averages of eq. 5.44 (for load-independent stations only the
270 // full-support pole survives, reproducing the pfqn_clw_lld scaling).
271 std::vector<std::vector<T>> rows;
272 for (std::size_t i = 0; i < M; ++i)
273 for (std::size_t mask = 1; mask < nmask; ++mask) {
274 bool dominated = false;
275 for (std::size_t mask2 = 1; mask2 < nmask && !dominated; ++mask2)
276 if (mask2 != mask && (mask & mask2) == mask &&
277 muS(i, mask2) <= muS(i, mask) * num_traits<T>::from_double(1 + 1e-12))
278 dominated = true;
279 if (dominated) continue;
280 std::vector<T> row(p, zero);
281 for (std::size_t t = 0; t < bits[mask].size(); ++t)
282 row[bits[mask][t]] = V(i, bits[mask][t]) / muS(i, mask);
283 rows.push_back(row);
284 }
285 const std::size_t nrow = rows.size();
286 Matrix<T> Lt(nrow ? nrow : 1, p, zero);
287 for (std::size_t i = 0; i < nrow; ++i)
288 for (std::size_t j = 0; j < p; ++j) Lt(i, j) = rows[i][j];
289 if (nrow == 0)
290 for (std::size_t j = 0; j < p; ++j) Lt(0, j) = zero; // pure delay network
291
292 const std::vector<long> mult(nrow ? nrow : 1, 1);
293 const std::vector<T> alpha = detail::clw_scaling(Lt, Lt, Nk, Zk, l, r, mult);
294
295 std::vector<T> arho0(p, zero);
296 Matrix<T> vs(M ? M : 1, p, zero);
297 for (std::size_t j = 0; j < p; ++j) {
298 arho0[j] = alpha[j] * Zk[j];
299 for (std::size_t i = 0; i < M; ++i) vs(i, j) = V(i, j) * alpha[j];
300 }
301
302 // Gbar(w) = exp(sum_r alpha_r Z_r (w_r - 1)) prod_i F_i(alpha_r v_{i,r} w_r).
303 // Summing logs is legitimate for the principal complex log because
304 // exp(log a + log b) = a b.
305 std::vector<detail::Cx<T>> FS(nmask, detail::Cx<T>(zero, zero));
306 const auto gbar = [&](const std::vector<detail::Cx<T>>& w) {
307 detail::Cx<T> expo(zero, zero);
308 for (std::size_t j = 0; j < p; ++j)
309 expo = detail::cx_add(
310 expo, detail::cx_scale(detail::Cx<T>(T(w[j].re - one), w[j].im), arho0[j]));
311 detail::Cx<T> logF(zero, zero);
312 for (std::size_t i = 0; i < M; ++i) {
313 FS[0] = detail::Cx<T>(one, zero);
314 detail::Cx<T> tot(one, zero);
315 for (std::size_t mask = 1; mask < nmask; ++mask) {
316 detail::Cx<T> num(zero, zero);
317 detail::Cx<T> den(muS(i, mask), zero);
318 for (std::size_t t = 0; t < bits[mask].size(); ++t) {
319 const std::size_t b = bits[mask][t];
320 const detail::Cx<T> x = detail::cx_scale(w[b], vs(i, b));
321 num = detail::cx_add(num, detail::cx_mul(x, FS[subcol[mask][t]]));
322 den = detail::cx_sub(den, x);
323 }
324 FS[mask] = detail::cx_div(num, den);
325 tot = detail::cx_add(tot, FS[mask]);
326 }
327 logF = detail::cx_add(logF, detail::cx_log(tot));
328 }
329 return detail::cx_exp(detail::cx_add(expo, logF));
330 };
331
332 std::vector<detail::Cx<T>> w(p);
333 const detail::Cx<T> gv = detail::clw_invert(0, w, Nk, l, r, p, gbar);
334 if (!(gv.re > zero))
335 throw NumericError("pfqn_clwoi: the inverted generating function is not positive");
336
337 T lG = log(gv.re);
338 for (std::size_t j = 0; j < p; ++j)
339 lG += arho0[j] - num_traits<T>::from_int(Nk[j]) * log(alpha[j]);
340 res.lG = lG;
341 res.G = (lG > num_traits<T>::from_int(709)) ? T(std::numeric_limits<T>::infinity()) : T(exp(lG));
342 return res;
343}
344
345/** Overload with the CLW default parameters. */
346template <class T>
347ClwResult<T> pfqn_clwoi(const std::vector<T>& Z, const std::vector<int>& N,
348 const std::vector<OiRate<T>>& mu, const Matrix<T>& visits) {
349 return pfqn_clwoi(Z, N, mu, visits, ClwOptions());
350}
351
352/** Overload with unit visits. */
353template <class T>
354ClwResult<T> pfqn_clwoi(const std::vector<T>& Z, const std::vector<int>& N,
355 const std::vector<OiRate<T>>& mu) {
356 return pfqn_clwoi(Z, N, mu, Matrix<T>(), ClwOptions());
357}
358
359} // namespace pfqn
360} // namespace line
361
362#endif // LINE_API_PFQN_CLWOI_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
bool empty() const
Definition matrix.h:92
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
Dense matrix and non-owning view.
std::function< T(const std::vector< int > &)> OiRate
An OI station's total service rate as a function of the occupancy vector.
Definition pfqn_ncoi.h:61
ClwResult< T > pfqn_clwoi(const std::vector< T > &Z, const std::vector< int > &N, const std::vector< OiRate< T > > &mu, const Matrix< T > &visits, const ClwOptions &opt)
Normalizing constant of a closed network of ORDER-INDEPENDENT (OI) stations plus one aggregated delay...
Definition pfqn_clwoi.h:180
Number-type abstraction for the templated API port.
Choudhury-Leung-Whitt normalization constant by numerical inversion of the generating function (JACM ...
Normalizing constant of a closed network of ORDER-INDEPENDENT (OI) / pass-and-swap stations with empt...
Optional lattice and aliasing parameters; empty means "use the CLW defaults".
Definition pfqn_clw.h:104
Return value of pfqn_clw and pfqn_clw_lld, mirroring [G, lG].
Definition pfqn_clw.h:98
T G
normalization constant, +infinity when it overflows the range of T
Definition pfqn_clw.h:99
T lG
its natural logarithm, always finite
Definition pfqn_clw.h:100