LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_clwjd.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_CLWJD_H
6#define LINE_API_PFQN_CLWJD_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Normalizing constant of a closed network of LIMITED JOINT-DEPENDENT (LJD)
12 * stations 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_clwjd.m. This stands to
16 * pfqn_clwoi as pfqn_clw_lld stands to pfqn_clw: a per-station cutoff beyond
17 * which the rate stops changing turns an infinite series into a rational
18 * function of the same denominators.
19 *
20 * Station i has a rate mu_i(n) that reads the whole per-class occupancy but
21 * saturates coordinatewise: with a cutoff vector l_i,
22 *
23 * mu_i(n) = c_{i,t}, t = ( min(n_1,l_{i,1}), ..., min(n_R,l_{i,R}) ),
24 *
25 * so past l_{i,r} further class-r jobs no longer change the rate. Order
26 * independence is l_i = 1 (t is the support indicator); a multiserver station
27 * with c servers is l_i = c, min(sum n, c) being a function of the clipped
28 * vector once every l_{i,r} >= c.
29 *
30 * Splitting the count lattice by clipped region, on which mu_i is constant, and
31 * writing F_{i,t} for the part of F_i carried by the states with t_i(n) = t,
32 *
33 * ( mu_{i,t} - sum_{r: t_r = l_{i,r}} v_{i,r} z_r ) F_{i,t}(z)
34 * = sum_{r: t_r >= 1} v_{i,r} z_r F_{i,t-e_r}(z), F_{i,0} = 1,
35 * F_i(z) = sum_t F_{i,t}(z),
36 *
37 * the two sides differing because removing a class-r job leaves the region only
38 * on an UNsaturated coordinate: for t_r < l_{i,r} the region pins n_r = t_r so
39 * n - e_r lands in t - e_r, while for t_r = l_{i,r} the region is n_r >= l and
40 * n - e_r lands in t or in t - e_r. The singularities are therefore the
41 * hyperplanes sum_{r in S} v_{i,r} z_r = mu_{i,t} over the SATURATED sets
42 * S = {r : t_r = l_{i,r}}: at most 2^R per station, however large the cutoffs
43 * are. Setting l_i = 1 reduces this to the support recursion of pfqn_clwoi and
44 * R = 1 reduces it to Bertozzi-McKenna eq. 2.19.
45 *
46 * The restrictive static scaling of eqs. 5.41-5.46 runs on one row per
47 * (station, saturated set), carrying v_{i,r}/min{mu_{i,t} : saturated set of t
48 * is S}, the smallest rate over regions sharing a saturated set being the
49 * binding one; rows dominated by a superset of no larger rate are dropped.
50 *
51 * SCOPE. The rate must be constant on each clipped region, which is checked on
52 * probe states. Any rate is admissible with an all-N cutoff (the default), the
53 * clipping being vacuous on the reachable lattice.
54 *
55 * Arithmetic: TRANSCENDENTAL, double and Real only, as for the rest of the CLW
56 * family.
57 *
58 * COST. prod_r 2 l_r N_r contour points, each costing
59 * O(M R prod_r (l_{i,r}+1)), against O(M prod_r (N_r+1)(N_r+2)/2) for
60 * pfqn_ncjd: the inversion is linear rather than quadratic in each population,
61 * but the per-point region box grows with the cutoff, so it pays off exactly
62 * when the joint dependence saturates early and loses outright at cutoff N.
63 */
64
65#include <algorithm>
66#include <cmath>
67#include <cstddef>
68#include <limits>
69#include <vector>
70
73#include "line/num/number.h"
74#include "line/util/error.h"
75#include "line/util/matrix.h"
76
77namespace line {
78namespace pfqn {
79
80namespace detail {
81
82/**
83 * The rate of one clipped region, with the constancy check. The region pins
84 * every unsaturated coordinate and leaves the saturated ones free above the
85 * cutoff, so the rate is probed at the region representative and above it.
86 */
87template <class T>
88T clwjd_region_rate(const OiRate<T>& murate, const std::vector<int>& t,
89 const std::vector<std::size_t>& keep, std::size_t R,
90 const std::vector<int>& N, const std::vector<int>& lrow) {
91 const T zero = num_traits<T>::from_int(0);
92 std::vector<int> nrep(R, 0);
93 for (std::size_t b = 0; b < t.size(); ++b) nrep[keep[b]] = t[b];
94 const T rate = murate(nrep);
95 if (!(rate > zero))
96 throw InputError("pfqn_clwjd: a station has a non-positive rate on a reachable region");
97 for (int pass = 0; pass < 2; ++pass) {
98 std::vector<int> probe = nrep;
99 bool differs = false;
100 for (std::size_t b = 0; b < t.size(); ++b) {
101 if (t[b] != lrow[b]) continue;
102 const int nr = N[keep[b]];
103 probe[keep[b]] = (pass == 0) ? nr : std::max(t[b], (t[b] + nr) / 2);
104 if (probe[keep[b]] != nrep[keep[b]]) differs = true;
105 }
106 if (!differs) continue;
107 const T rt = murate(probe);
108 T diff = T(rt - rate);
109 if (diff < zero) diff = T(-diff);
110 const T scale = (rate > num_traits<T>::from_int(1)) ? rate : num_traits<T>::from_int(1);
111 if (diff > num_traits<T>::from_double(1e-9) * scale)
112 throw InputError(
113 "pfqn_clwjd: a station has a rate that varies within a clipped region; raise the "
114 "cutoff or use pfqn_ncjd");
115 }
116 return rate;
117}
118
119} // namespace detail
120
121/**
122 * @brief Normalizing constant of a closed network of LIMITED JOINT-DEPENDENT
123 * (LJD) stations plus one aggregated delay, by numerical inversion of
124 * the multichain generating function (Choudhury-Leung-Whitt, J. ACM
125 * 42(5):935-970, 1995).
126 *
127 * @param Z (R) think-time demand of the aggregated delay node
128 * @param N (R) closed population, finite
129 * @param mu one rate handle per joint-dependent station; empty for a pure delay
130 * @param visits (M x R) per-station class visit ratios; empty for unit visits
131 * @param lcut (M x R) per-station per-class saturation cutoffs l_{i,r} >= 1,
132 * clipped to N_r (exact: a rate difference at n_r > N_r can only
133 * move coefficients with n_r > N_r); empty for the all-N cutoff
134 * @param opt lattice and aliasing parameters
135 */
136template <class T>
137ClwResult<T> pfqn_clwjd(const std::vector<T>& Z, const std::vector<int>& N,
138 const std::vector<OiRate<T>>& mu, const Matrix<T>& visits,
139 const Matrix<int>& lcut, const ClwOptions& opt) {
141 "pfqn_clwjd requires transcendental arithmetic (contour integration of a "
142 "generating function)");
143 using std::exp;
144 using std::log;
145 const std::size_t R = N.size();
146 const std::size_t M = mu.size();
147 if (!Z.empty() && Z.size() != R)
148 throw InputError("pfqn_clwjd: Z and N disagree on the chain count");
149 if (!visits.empty() && (visits.rows() != M || visits.cols() != R))
150 throw InputError("pfqn_clwjd: visits must be M x R");
151 if (!lcut.empty() && (lcut.rows() != M || lcut.cols() != R))
152 throw InputError("pfqn_clwjd: lcut must be M x R");
153 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
154
155 ClwResult<T> res;
156 long Ntot = 0;
157 for (std::size_t r = 0; r < R; ++r) {
158 if (N[r] < 0) {
159 res.G = zero;
160 res.lG = T(-std::numeric_limits<T>::infinity());
161 return res;
162 }
163 Ntot += N[r];
164 }
165 if (Ntot == 0) {
166 res.G = one;
167 res.lG = zero;
168 return res;
169 }
170
171 std::vector<int> lfull;
172 std::vector<double> gfull;
173 detail::clw_defaults(R, opt, lfull, gfull);
174
175 // drop zero-population chains: the coefficient of z_r^0 is the generating
176 // function at z_r = 0, which kills every F_{i,t} with t_r >= 1
177 std::vector<std::size_t> keep;
178 for (std::size_t r = 0; r < R; ++r)
179 if (N[r] > 0) keep.push_back(r);
180 const std::size_t p = keep.size();
181 std::vector<int> Nk(p, 0), l(p, 1);
182 std::vector<T> Zk(p, zero);
183 std::vector<double> gam(p, 0.0);
184 for (std::size_t j = 0; j < p; ++j) {
185 Nk[j] = N[keep[j]];
186 Zk[j] = Z.empty() ? zero : Z[keep[j]];
187 l[j] = lfull[keep[j]];
188 gam[j] = gfull[keep[j]];
189 }
190
191 // saturation cutoffs, broadcast and clipped to the reachable lattice
192 std::vector<std::vector<int>> Lk(M, std::vector<int>(p, 1));
193 for (std::size_t i = 0; i < M; ++i)
194 for (std::size_t b = 0; b < p; ++b) {
195 int li = lcut.empty() ? Nk[b] : lcut(i, keep[b]);
196 if (li < 1) li = 1;
197 if (li > Nk[b]) li = Nk[b];
198 Lk[i][b] = li;
199 }
200
201 // per-station region tables over the clipped box prod_r {0,...,l_{i,r}}, in
202 // mixed radix so that t - e_r always precedes t
203 std::vector<std::size_t> ntreg(M, 1);
204 std::vector<std::vector<T>> muT(M);
205 std::vector<std::vector<std::vector<std::size_t>>> regDec(M), regSat(M);
206 std::vector<std::vector<std::size_t>> satMask(M);
207 for (std::size_t i = 0; i < M; ++i) {
208 std::vector<std::size_t> st(p, 1);
209 std::size_t nt = 1;
210 for (std::size_t b = 0; b < p; ++b) {
211 st[b] = nt;
212 nt *= static_cast<std::size_t>(Lk[i][b] + 1);
213 }
214 ntreg[i] = nt;
215 muT[i].assign(nt, one);
216 regDec[i].assign(nt, std::vector<std::size_t>());
217 regSat[i].assign(nt, std::vector<std::size_t>());
218 satMask[i].assign(nt, 0);
219 std::vector<int> t(p, 0);
220 for (std::size_t tl = 0; tl < nt; ++tl) {
221 for (std::size_t b = 0; b < p; ++b)
222 t[b] = static_cast<int>((tl / st[b]) % static_cast<std::size_t>(Lk[i][b] + 1));
223 for (std::size_t b = 0; b < p; ++b) {
224 if (t[b] >= 1) {
225 regDec[i][tl].push_back(b);
226 regDec[i][tl].push_back(tl - st[b]);
227 }
228 if (t[b] == Lk[i][b]) {
229 regSat[i][tl].push_back(b);
230 satMask[i][tl] |= (static_cast<std::size_t>(1) << b);
231 }
232 }
233 if (tl > 0) muT[i][tl] = detail::clwjd_region_rate<T>(mu[i], t, keep, R, N, Lk[i]);
234 }
235 }
236
237 // per-station visit vectors restricted to the retained chains
238 Matrix<T> V(M ? M : 1, p, one);
239 for (std::size_t i = 0; i < M; ++i)
240 for (std::size_t b = 0; b < p; ++b) V(i, b) = visits.empty() ? one : visits(i, keep[b]);
241
242 std::vector<T> r(p, one);
243 for (std::size_t j = 0; j < p; ++j)
244 r[j] = detail::clw_pow_real(
246 T(num_traits<T>::from_double(-gam[j]) /
247 num_traits<T>::from_int(2 * static_cast<long>(l[j]) * Nk[j])));
248
249 // binding rate of each saturated set: regions sharing a saturated set share the
250 // hyperplane sum_{r in S} v_r z_r = mu, so the smallest rate constrains
251 const std::size_t nmask = static_cast<std::size_t>(1) << p;
252 std::vector<std::vector<T>> muS(M, std::vector<T>(nmask, zero));
253 std::vector<std::vector<bool>> muSet(M, std::vector<bool>(nmask, false));
254 for (std::size_t i = 0; i < M; ++i)
255 for (std::size_t tl = 0; tl < ntreg[i]; ++tl) {
256 const std::size_t sm = satMask[i][tl];
257 if (sm == 0) continue;
258 if (!muSet[i][sm] || muT[i][tl] < muS[i][sm]) {
259 muS[i][sm] = muT[i][tl];
260 muSet[i][sm] = true;
261 }
262 }
263
264 // one constraint row per (station, saturated set), dominated rows dropped:
265 // set S is implied by a superset S' with mu_{i,S'} <= mu_{i,S}
266 std::vector<std::vector<T>> rows;
267 for (std::size_t i = 0; i < M; ++i)
268 for (std::size_t mask = 1; mask < nmask; ++mask) {
269 if (!muSet[i][mask]) continue;
270 bool dominated = false;
271 for (std::size_t mask2 = 1; mask2 < nmask && !dominated; ++mask2)
272 if (mask2 != mask && (mask & mask2) == mask && muSet[i][mask2] &&
273 muS[i][mask2] <= muS[i][mask] * num_traits<T>::from_double(1 + 1e-12))
274 dominated = true;
275 if (dominated) continue;
276 std::vector<T> row(p, zero);
277 for (std::size_t b = 0; b < p; ++b)
278 if (mask & (static_cast<std::size_t>(1) << b)) row[b] = V(i, b) / muS[i][mask];
279 rows.push_back(row);
280 }
281 const std::size_t nrow = rows.size();
282 Matrix<T> Lt(nrow ? nrow : 1, p, zero);
283 for (std::size_t i = 0; i < nrow; ++i)
284 for (std::size_t j = 0; j < p; ++j) Lt(i, j) = rows[i][j];
285
286 const std::vector<long> mult(nrow ? nrow : 1, 1);
287 const std::vector<T> alpha = detail::clw_scaling(Lt, Lt, Nk, Zk, l, r, mult);
288
289 std::vector<T> arho0(p, zero);
290 Matrix<T> vs(M ? M : 1, p, zero);
291 for (std::size_t j = 0; j < p; ++j) {
292 arho0[j] = alpha[j] * Zk[j];
293 for (std::size_t i = 0; i < M; ++i) vs(i, j) = V(i, j) * alpha[j];
294 }
295
296 std::size_t ntmax = 1;
297 for (std::size_t i = 0; i < M; ++i) ntmax = std::max(ntmax, ntreg[i]);
298 std::vector<detail::Cx<T>> FT(ntmax, detail::Cx<T>(zero, zero));
299 const auto gbar = [&](const std::vector<detail::Cx<T>>& w) {
300 detail::Cx<T> expo(zero, zero);
301 for (std::size_t j = 0; j < p; ++j)
302 expo = detail::cx_add(
303 expo, detail::cx_scale(detail::Cx<T>(T(w[j].re - one), w[j].im), arho0[j]));
304 detail::Cx<T> logF(zero, zero);
305 for (std::size_t i = 0; i < M; ++i) {
306 FT[0] = detail::Cx<T>(one, zero);
307 detail::Cx<T> tot(one, zero);
308 for (std::size_t tl = 1; tl < ntreg[i]; ++tl) {
309 detail::Cx<T> num(zero, zero);
310 const std::vector<std::size_t>& dec = regDec[i][tl];
311 for (std::size_t k = 0; k < dec.size(); k += 2)
312 num = detail::cx_add(
313 num, detail::cx_mul(detail::cx_scale(w[dec[k]], vs(i, dec[k])),
314 FT[dec[k + 1]]));
315 detail::Cx<T> den(muT[i][tl], zero);
316 const std::vector<std::size_t>& sat = regSat[i][tl];
317 for (std::size_t k = 0; k < sat.size(); ++k)
318 den = detail::cx_sub(den, detail::cx_scale(w[sat[k]], vs(i, sat[k])));
319 FT[tl] = detail::cx_div(num, den);
320 tot = detail::cx_add(tot, FT[tl]);
321 }
322 logF = detail::cx_add(logF, detail::cx_log(tot));
323 }
324 return detail::cx_exp(detail::cx_add(expo, logF));
325 };
326
327 std::vector<detail::Cx<T>> w(p);
328 const detail::Cx<T> gv = detail::clw_invert(0, w, Nk, l, r, p, gbar);
329 if (!(gv.re > zero))
330 throw NumericError("pfqn_clwjd: the inverted generating function is not positive");
331
332 T lG = log(gv.re);
333 for (std::size_t j = 0; j < p; ++j)
334 lG += arho0[j] - num_traits<T>::from_int(Nk[j]) * log(alpha[j]);
335 res.lG = lG;
336 res.G = (lG > num_traits<T>::from_int(709)) ? T(std::numeric_limits<T>::infinity()) : T(exp(lG));
337 return res;
338}
339
340/** Overload with the CLW default parameters. */
341template <class T>
342ClwResult<T> pfqn_clwjd(const std::vector<T>& Z, const std::vector<int>& N,
343 const std::vector<OiRate<T>>& mu, const Matrix<T>& visits,
344 const Matrix<int>& lcut) {
345 return pfqn_clwjd(Z, N, mu, visits, lcut, ClwOptions());
346}
347
348/** Overload with the all-N cutoff, i.e. no truncation of the joint dependence. */
349template <class T>
350ClwResult<T> pfqn_clwjd(const std::vector<T>& Z, const std::vector<int>& N,
351 const std::vector<OiRate<T>>& mu, const Matrix<T>& visits) {
352 return pfqn_clwjd(Z, N, mu, visits, Matrix<int>(), ClwOptions());
353}
354
355/** Overload with unit visits and the all-N cutoff. */
356template <class T>
357ClwResult<T> pfqn_clwjd(const std::vector<T>& Z, const std::vector<int>& N,
358 const std::vector<OiRate<T>>& mu) {
359 return pfqn_clwjd(Z, N, mu, Matrix<T>(), Matrix<int>(), ClwOptions());
360}
361
362} // namespace pfqn
363} // namespace line
364
365#endif // LINE_API_PFQN_CLWJD_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.
ClwResult< T > pfqn_clwjd(const std::vector< T > &Z, const std::vector< int > &N, const std::vector< OiRate< T > > &mu, const Matrix< T > &visits, const Matrix< int > &lcut, const ClwOptions &opt)
Normalizing constant of a closed network of LIMITED JOINT-DEPENDENT (LJD) stations plus one aggregate...
Definition pfqn_clwjd.h:137
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
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