LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_manjunath.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_MANJUNATH_H
6#define LINE_API_PFQN_MANJUNATH_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Exact normalizing constant of a closed multiclass product-form network whose
12 * state space carries arbitrary linear integer constraints (Manjunath-Sikdar).
13 *
14 * Templated port of matlab/src/api/pfqn/pfqn_manjunath.m and
15 * jar/src/main/java/jline/api/pfqn/nc/Pfqn_manjunath.java.
16 *
17 * This is the queueing-network half of the transform technique of which
18 * `lossn_manjunath` is the loss-network half. The two solve the same problem --
19 * sum a product form over an irregular integer state space -- from opposite ends
20 * of the paper: `lossn_manjunath` implements Section 2.2, a set of '<=' rows over
21 * the Poisson terms nu^n/n!, while this routine implements Section 3 together
22 * with Section 5.3, a MIXED set of '=', '<=' and '>' rows over the BCMP terms,
23 * where the population constraint of a closed network is itself one of the
24 * equalities.
25 *
26 * THE MODEL. M queueing stations (rows of L) and Mz delay stations (rows of Z)
27 * serve R closed classes with populations N. With n_i = sum_r n_ir,
28 *
29 * p(n) = (1/G) prod_{i queueing} n_i! prod_r L_ir^{n_ir}/n_ir!
30 * prod_{i delay} prod_r Z_ir^{n_ir}/n_ir!
31 *
32 * Every state obeys the R population equalities sum_i n_ir = N_r; the caller may
33 * impose any number of further rows sum_{i,r} A(j, i + S*r) n_ir {=,<=,>} b(j)
34 * with S = M + Mz, i.e. A acts on the (M+Mz) x R occupancy read column by column
35 * with the queueing stations first. With no extra rows the result is exactly
36 * `pfqn_ca`'s normalizing constant, which is the parity oracle used by the
37 * tests.
38 *
39 * WHY THE GENERATING FUNCTION IS A PRODUCT, AND WHERE THE n_i! GOES. Marking
40 * class r by z_r and row j by y_j, and writing u_ir = z_r prod_j y_j^{A(j,i+S r)},
41 * the sum over the occupancies of a single QUEUEING station is, by the
42 * multinomial theorem,
43 *
44 * sum_{n_i.} n_i! prod_r (L_ir u_ir)^{n_ir}/n_ir!
45 * = sum_k (sum_r L_ir u_ir)^k = 1 / (1 - sum_r L_ir u_ir),
46 *
47 * so the n_i! that couples the classes is exactly what turns the station's
48 * factor from an exponential into a geometric one. The paper reaches the same
49 * place through the Euler integral n! = int_0^inf e^-t t^n dt (Eqns 16-18),
50 * which is that geometric series evaluated; the closed form is used here because
51 * there is then no quadrature to discretize -- and, decisively for this port, no
52 * transcendental, so the whole computation stays rational.
53 *
54 * WHY THIS ONE RUNS AT EXACT ARITHMETIC. Every operation on the series is an
55 * addition or a multiplication of demands, plus a division by a small integer in
56 * the delay convolution, all of which are rational whenever L and Z are. G is
57 * therefore exact under Arith::Exact and only `lG` is transcendental, obtained
58 * through `num_traits<T>::log_as_double`. The double-only power-of-two rescaling
59 * of `pfqn_ca` is applied on the same terms and for the same reason: exact
60 * rationals have no exponent range to leave, and scaling them would only inflate
61 * their denominators.
62 *
63 * THE ELIMINATION ORDER IS THE MEMORY BOUND. Variable y_j is created when the
64 * first station its row touches is multiplied in and discharged immediately
65 * after the last, so peak memory is prod_r (N_r+1) times the product of (b_j+1)
66 * over the SIMULTANEOUSLY LIVE rows, not over all rows. The class axes are live
67 * throughout, so prod_r (N_r+1) is a floor -- the same lattice `pfqn_ca` walks.
68 * The peak is bounded by `PfqnManjunathOptions::max_live_states` and a model
69 * above it is refused by name rather than allowed to exhaust the machine.
70 *
71 * SCOPE. Load-dependent and multiserver stations are NOT covered: their
72 * per-station term is not geometric, and the multiclass n_i! coupling used above
73 * then breaks. Use `pfqn_gld` or `pfqn_conwayms`. A and b must be integer valued
74 * and A nonnegative, since the residue argument counts whole units; a fractional
75 * entry is refused rather than rounded.
76 *
77 * Reference: D. Manjunath and B. Sikdar, Integral Expressions for the Numerical
78 * Evaluation of Product Form Expressions Over Irregular Multidimensional Integer
79 * Spaces. Sections 3 and 5.3.
80 */
81
82#include <algorithm>
83#include <cmath>
84#include <cstddef>
85#include <cstdlib>
86#include <limits>
87#include <string>
88#include <type_traits>
89#include <vector>
90
91#include "line/num/number.h"
92#include "line/util/error.h"
93#include "line/util/matrix.h"
94
95namespace line {
96namespace pfqn {
97
98/** Controls of pfqn_manjunath. The reference has none; the fields are port-local. */
100 /**
101 * Cap on the number of series coefficients held at once. The default is 2^26
102 * coefficients, half a gigabyte at double. Raise it deliberately.
103 */
104 std::size_t max_live_states = static_cast<std::size_t>(1) << 26;
105 /**
106 * Also return the per-class decomposition. Requires the ONE configuration in
107 * which the truncated product form is the EXACT stationary law: a single
108 * queueing station inside the region and a SINGLE DELAY STATION OUTSIDE IT.
109 * Anything else is refused by name rather than answered wrongly.
110 */
111 bool stats = false;
112};
113
114/**
115 * Result of pfqn_manjunath.
116 *
117 * The decomposition fields are populated only when `PfqnManjunathOptions::stats`
118 * is set. They are `double` in every arithmetic, exactly as `lG` is, because
119 * each is a RATIO of normalizing constants taken in the log domain -- which is
120 * what keeps the four codebases comparable digit for digit and what removes the
121 * range problem from a ratio of two very large constants. `G` itself stays exact
122 * under Arith::Exact.
123 *
124 * `Q + think + blocked == N` exactly: a refused admission is a DELETED
125 * transition, so a blocked job never leaves the delay, and because the think
126 * time is exponential a held job is indistinguishable from one still thinking.
127 * Little's law is what separates the two.
128 */
129template <class T>
131 T G; ///< normalizing constant in the requested arithmetic
132 double lG = 0.0; ///< log of the constant, always a double
133 std::size_t peak_states = 0; ///< peak live series coefficients, the realised cost
134 bool has_stats = false; ///< whether the decomposition below was computed
135 std::vector<double> Q; ///< mean class r jobs at the queueing station
136 std::vector<double> X; ///< class r cycle throughput
137 std::vector<double> U; ///< class r utilization of the queueing station
138 std::vector<double> think; ///< class r jobs genuinely thinking, X_r * Z_r
139 std::vector<double> blocked; ///< class r jobs held at the delay by the constraint
140 std::vector<double> delay; ///< class r jobs at the delay, think + blocked
141};
142
143namespace detail {
144
145/** MATLAB's round: half away from zero, which std::lround also does. */
146inline int manjunath_round(double x) { return static_cast<int>(std::lround(x)); }
147
148
149/** Early exit, carrying an all-zero decomposition when one was asked for. */
150template <class T>
151PfqnManjunathResult<T> manjunath_empty(const T& G, double lG, std::size_t peak,
152 const PfqnManjunathOptions& options, std::size_t R) {
153 PfqnManjunathResult<T> out;
154 out.G = G;
155 out.lG = lG;
156 out.peak_states = peak;
157 if (options.stats) {
158 out.has_stats = true;
159 out.Q.assign(R, 0.0);
160 out.X.assign(R, 0.0);
161 out.U.assign(R, 0.0);
162 out.think.assign(R, 0.0);
163 out.blocked.assign(R, 0.0);
164 out.delay.assign(R, 0.0);
165 }
166 return out;
167}
168
169/** Forward declaration: the decomposition calls the entry point recursively. */
170template <class T>
171void manjunath_stats(PfqnManjunathResult<T>& out, const Matrix<T>& L, const std::vector<int>& N,
172 const Matrix<T>& Z, const std::vector<std::vector<long>>& A,
173 const std::vector<long>& b, const std::string& sense, double lG,
174 std::size_t M, std::size_t Mz, std::size_t S, std::size_t R,
175 const PfqnManjunathOptions& options);
176
177/**
178 * Coefficient-domain evaluation of the multiple contour integral of Eqn 9 for a
179 * set of '=' and '<=' rows. The series lives in a flat vector indexed
180 * column-major, its first R axes the class markers z_r of extent N_r+1
181 * throughout and its remaining J axes the row markers y_j, of extent 1 while row
182 * j is not live and b_j+1 while it is.
183 */
184template <class T>
185T pfqn_manjunath_series(const Matrix<T>& L, const Matrix<T>& Z, const std::vector<int>& N,
186 const std::vector<std::vector<long>>& A, const std::vector<long>& b,
187 const std::string& sense, const PfqnManjunathOptions& options,
188 std::size_t& peak) {
189 const std::size_t M = L.empty() ? 0 : L.rows();
190 const std::size_t Mz = Z.empty() ? 0 : Z.rows();
191 const std::size_t S = M + Mz;
192 const std::size_t R = N.size();
193 const std::size_t J = b.size();
194 const std::size_t D = R + J;
195 const T zero = num_traits<T>::from_int(0);
196
197 // Row j is created at the first station it touches and discharged after the
198 // last, so only an induced width of rows is ever live. A row that reached
199 // here touches at least one station, the trivial ones having been decided.
200 std::vector<std::size_t> first(J, 0), last(J, 0);
201 for (std::size_t j = 0; j < J; ++j) {
202 bool seen = false;
203 for (std::size_t i = 0; i < S; ++i) {
204 bool touch = false;
205 for (std::size_t r = 0; r < R; ++r)
206 if (A[j][i + S * r] != 0) touch = true;
207 if (!touch) continue;
208 if (!seen) {
209 first[j] = i;
210 seen = true;
211 }
212 last[j] = i;
213 }
214 if (!seen)
215 throw NumericError("pfqn_manjunath: a constraint row with no nonzero entry reached "
216 "the series; the rule was not reduced");
217 }
218
219 std::vector<std::size_t> dims(D, 1);
220 for (std::size_t r = 0; r < R; ++r) dims[r] = static_cast<std::size_t>(N[r]) + 1;
221 std::size_t P = 1;
222 for (std::size_t d = 0; d < D; ++d) P *= dims[d];
223 std::vector<T> ser(P, zero);
224 ser[0] = num_traits<T>::from_int(1);
225 if (P > peak) peak = P;
226
227 for (std::size_t i = 0; i < S; ++i) {
228 for (std::size_t j = 0; j < J; ++j) {
229 if (first[j] != i) continue;
230 const std::size_t newdim = static_cast<std::size_t>(b[j]) + 1;
231 std::size_t pre = 1, post = 1;
232 for (std::size_t d = 0; d < R + j; ++d) pre *= dims[d];
233 for (std::size_t d = R + j + 1; d < D; ++d) post *= dims[d];
234 if (newdim != 0 && pre * post > options.max_live_states / newdim)
235 throw UnsupportedError(
236 "pfqn_manjunath: the exact transform would hold more than " +
237 std::to_string(options.max_live_states) +
238 " series coefficients at once. The floor is the product of (N_r+1) over the "
239 "classes, on top of which each simultaneously live constraint row multiplies "
240 "by (b_j+1); raise PfqnManjunathOptions::max_live_states deliberately");
241 // The existing content keeps its coefficients and enters at degree
242 // zero in the new variable: nothing so far carries a power of it.
243 std::vector<T> grown(pre * newdim * post, zero);
244 for (std::size_t q = 0; q < post; ++q)
245 for (std::size_t p = 0; p < pre; ++p)
246 grown[p + q * pre * newdim] = ser[p + q * pre];
247 ser.swap(grown);
248 dims[R + j] = newdim;
249 if (ser.size() > peak) peak = ser.size();
250 }
251
252 std::vector<std::size_t> stride(D, 1);
253 for (std::size_t d = 1; d < D; ++d) stride[d] = stride[d - 1] * dims[d - 1];
254
255 // The monomial one class r job at station i contributes: z_r gains one
256 // degree and y_j gains A(j, i + S*r). A row not live at this station has
257 // a zero entry here by construction of first/last, so a dead axis is
258 // never shifted.
259 std::vector<std::vector<long>> delta(R, std::vector<long>(D, 0));
260 std::vector<std::size_t> off(R, 0);
261 std::vector<bool> fits(R, true);
262 for (std::size_t r = 0; r < R; ++r) {
263 delta[r][r] = 1;
264 for (std::size_t j = 0; j < J; ++j) delta[r][R + j] = A[j][i + S * r];
265 std::size_t o = 0;
266 for (std::size_t d = 0; d < D; ++d) {
267 if (delta[r][d] > static_cast<long>(dims[d]) - 1)
268 fits[r] = false; // a single job already breaks the cut
269 o += static_cast<std::size_t>(delta[r][d]) * stride[d];
270 }
271 off[r] = o;
272 }
273
274 if (i < M) {
275 // Queueing station: solve (1 - sum_r L_ir u_ir) x = ser in place.
276 // Every monomial of the operator raises the total class degree by
277 // one, so p - off[r] is always a strictly smaller flat index and a
278 // sweep in increasing flat index reads only final coefficients: the
279 // sweep IS the solve, not an iterate.
280 std::vector<std::size_t> sub(D, 0);
281 for (std::size_t p = 0; p < ser.size(); ++p) {
282 for (std::size_t r = 0; r < R; ++r) {
283 if (!fits[r] || L(i, r) == zero) continue;
284 bool ok = true;
285 for (std::size_t d = 0; d < D && ok; ++d)
286 if (static_cast<long>(sub[d]) < delta[r][d]) ok = false;
287 if (ok) ser[p] += T(L(i, r) * ser[p - off[r]]);
288 }
289 for (std::size_t d = 0; d < D; ++d) {
290 if (++sub[d] < dims[d]) break;
291 sub[d] = 0;
292 }
293 }
294 } else {
295 // Delay station: no n_i! coupling, so the factor is a product of
296 // exponentials, one per class, each convolved in term by term. There
297 // is no first-order recurrence to exploit here, which is why the
298 // delay costs a factor of the population that the queueing station
299 // does not.
300 for (std::size_t r = 0; r < R; ++r) {
301 if (!fits[r] || Z(i - M, r) == zero || N[r] == 0) continue;
302 std::vector<T> nxt(ser);
303 std::vector<T> term(ser);
304 for (int n = 1; n <= N[r]; ++n) {
305 std::vector<T> shifted(ser.size(), zero);
306 std::vector<std::size_t> sub(D, 0);
307 for (std::size_t p = 0; p < ser.size(); ++p) {
308 bool ok = true;
309 for (std::size_t d = 0; d < D && ok; ++d)
310 if (static_cast<long>(sub[d]) < delta[r][d]) ok = false;
311 if (ok) shifted[p] = term[p - off[r]];
312 for (std::size_t d = 0; d < D; ++d) {
313 if (++sub[d] < dims[d]) break;
314 sub[d] = 0;
315 }
316 }
317 const T c = Z(i - M, r) / num_traits<T>::from_int(n);
318 bool any = false;
319 for (std::size_t p = 0; p < shifted.size(); ++p) {
320 shifted[p] = T(c * shifted[p]);
321 if (!(shifted[p] == zero)) any = true;
322 }
323 term.swap(shifted);
324 if (!any) break;
325 for (std::size_t p = 0; p < nxt.size(); ++p) nxt[p] += term[p];
326 }
327 ser.swap(nxt);
328 }
329 }
330
331 for (std::size_t j = 0; j < J; ++j) {
332 if (last[j] != i) continue;
333 // Discharge marker j. The multiplier (y^{b+1}-1)/(y-1) of a '<=' row
334 // turns its residue into the partial sum of the coefficients of
335 // degrees 0..b, and the 1/y^{b+1} of an '=' row picks degree b.
336 std::size_t pre = 1, post = 1;
337 for (std::size_t d = 0; d < R + j; ++d) pre *= dims[d];
338 for (std::size_t d = R + j + 1; d < D; ++d) post *= dims[d];
339 const std::size_t dj = dims[R + j];
340 std::vector<T> out(pre * post, zero);
341 if (sense[j] == 'E') {
342 const std::size_t rhs = static_cast<std::size_t>(b[j]);
343 for (std::size_t q = 0; q < post; ++q)
344 for (std::size_t p = 0; p < pre; ++p)
345 out[p + q * pre] = ser[p + rhs * pre + q * pre * dj];
346 } else {
347 for (std::size_t q = 0; q < post; ++q)
348 for (std::size_t d = 0; d < dj; ++d)
349 for (std::size_t p = 0; p < pre; ++p)
350 out[p + q * pre] += ser[p + d * pre + q * pre * dj];
351 }
352 ser.swap(out);
353 dims[R + j] = 1;
354 }
355 }
356
357 std::size_t expect = 1;
358 for (std::size_t r = 0; r < R; ++r) expect *= static_cast<std::size_t>(N[r]) + 1;
359 if (ser.size() != expect)
360 throw NumericError("pfqn_manjunath: a constraint row was never discharged; the "
361 "elimination order is inconsistent");
362 // The closed network's own equalities: degree exactly N_r in every class.
363 std::size_t flat = 0, st = 1;
364 for (std::size_t r = 0; r < R; ++r) {
365 flat += static_cast<std::size_t>(N[r]) * st;
366 st *= static_cast<std::size_t>(N[r]) + 1;
367 }
368 return ser[flat];
369}
370
371} // namespace detail
372
373/**
374 * @brief Exact normalizing constant of a closed multiclass product-form
375 * network whose state space carries arbitrary linear integer
376 * constraints (Manjunath-Sikdar).
377 *
378 * @param L (M x R) service demands at the queueing stations; may be empty
379 * @param N (R) population per class
380 * @param Z (Mz x R) think times at the delay stations; may be empty
381 * @param A (J x (M+Mz)*R) extra constraint coefficients, nonnegative integers
382 * @param b (J) extra constraint right-hand sides, integers
383 * @param sense one character per row, 'E' (=), 'L' (<=) or 'G' (>); empty means all 'L'
384 */
385template <class T>
386PfqnManjunathResult<T> pfqn_manjunath(const Matrix<T>& L, const std::vector<int>& N,
387 const Matrix<T>& Z, const Matrix<T>& A,
388 const std::vector<long>& b, const std::string& sense,
389 const PfqnManjunathOptions& options = {}) {
390 const std::size_t R = N.size();
391 const std::size_t M = L.empty() ? 0 : L.rows();
392 const std::size_t Mz = Z.empty() ? 0 : Z.rows();
393 const std::size_t S = M + Mz;
394 if (!L.empty() && L.cols() != R)
395 throw InputError("pfqn_manjunath: L and N disagree on the class count");
396 if (!Z.empty() && Z.cols() != R)
397 throw InputError("pfqn_manjunath: Z and N disagree on the class count");
398
399 const std::size_t Jin = b.size();
400 if (Jin > 0 && (A.rows() != Jin || A.cols() != S * R))
401 throw InputError("pfqn_manjunath: A must be J x (M+Mz)*R, acting on the occupancy read "
402 "column by column with the queueing stations first");
403 std::string sn = sense;
404 if (sn.empty()) sn = std::string(Jin, 'L');
405 if (sn.size() != Jin)
406 throw InputError("pfqn_manjunath: sense must have one character per row of b");
407 for (std::size_t j = 0; j < Jin; ++j)
408 if (sn[j] != 'E' && sn[j] != 'L' && sn[j] != 'G')
409 throw InputError("pfqn_manjunath: sense must contain only 'E' (=), 'L' (<=) or "
410 "'G' (>)");
411
412 for (std::size_t r = 0; r < R; ++r)
413 if (N[r] < 0)
414 return detail::manjunath_empty<T>(num_traits<T>::from_int(0), -std::numeric_limits<double>::infinity(), 0, options, R);
415
416 // The integer view of the extra rows, refusing a fractional or negative
417 // entry by name: the residue argument counts whole units.
418 std::vector<std::vector<long>> Ai(Jin, std::vector<long>(S * R, 0));
419 for (std::size_t j = 0; j < Jin; ++j)
420 for (std::size_t c = 0; c < S * R; ++c) {
421 const double a = num_traits<T>::to_double(A(j, c));
422 if (a < 0.0 || std::fabs(a - std::round(a)) > 1e-9)
423 throw InputError("pfqn_manjunath: A must contain nonnegative integers; the "
424 "residue argument counts whole units");
425 Ai[j][c] = std::lround(a);
426 }
427
428 // A row of zeros constrains nothing, so it is decided here rather than
429 // carried as a one-coefficient dimension: 0 = b, 0 <= b and 0 > b are each
430 // settled by the sign of b alone. Same for a negative right-hand side, which
431 // no nonnegative combination can meet ('E', 'L') or can fail to beat ('G').
432 std::vector<std::vector<long>> Ak;
433 std::vector<long> bk;
434 std::string sk;
435 for (std::size_t j = 0; j < Jin; ++j) {
436 bool trivial = true;
437 for (std::size_t c = 0; c < S * R; ++c)
438 if (Ai[j][c] != 0) trivial = false;
439 if (sn[j] == 'E') {
440 if (b[j] < 0 || (trivial && b[j] != 0))
441 return detail::manjunath_empty<T>(num_traits<T>::from_int(0), -std::numeric_limits<double>::infinity(), 0, options, R);
442 if (trivial) continue;
443 } else if (sn[j] == 'L') {
444 if (b[j] < 0)
445 return detail::manjunath_empty<T>(num_traits<T>::from_int(0), -std::numeric_limits<double>::infinity(), 0, options, R);
446 if (trivial) continue;
447 } else {
448 if (b[j] < 0) continue; // 0 > negative always holds
449 if (trivial)
450 return detail::manjunath_empty<T>(num_traits<T>::from_int(0), -std::numeric_limits<double>::infinity(), 0, options, R);
451 }
452 Ak.push_back(Ai[j]);
453 bk.push_back(b[j]);
454 sk.push_back(sn[j]);
455 }
456 const std::size_t J = bk.size();
457
458 if (S == 0) {
459 // No station: the only state is empty, admissible when every class is too.
460 bool empty = true;
461 for (std::size_t r = 0; r < R; ++r)
462 if (N[r] != 0) empty = false;
463 if (empty) return detail::manjunath_empty<T>(num_traits<T>::from_int(1), 0.0, 1, options, R);
464 return detail::manjunath_empty<T>(num_traits<T>::from_int(0), -std::numeric_limits<double>::infinity(), 0, options, R);
465 }
466
467 long Nt = 0;
468 for (std::size_t r = 0; r < R; ++r) Nt += N[r];
469
470 // Every monomial that survives the extraction has total degree sum(N) in the
471 // demands, so a common power-of-two rescaling moves lG by a known amount and
472 // nothing else. Applied only under double, on the same grounds as pfqn_ca:
473 // exact rationals have no exponent range to leave.
474 int kscale = 0;
475 if (std::is_same<T, double>::value && Nt > 0) {
476 double lGest = -std::numeric_limits<double>::infinity();
477 for (std::size_t i = 0; i < M; ++i) {
478 double t = 0.0;
479 bool ok = true;
480 for (std::size_t r = 0; r < R && ok; ++r)
481 if (N[r] > 0) {
482 const double lir = num_traits<T>::to_double(L(i, r));
483 if (lir > 0)
484 t += N[r] * std::log(lir);
485 else
486 ok = false;
487 }
488 if (ok && t > lGest) lGest = t;
489 }
490 if (Mz > 0) {
491 double t = 0.0;
492 bool ok = true;
493 for (std::size_t r = 0; r < R && ok; ++r)
494 if (N[r] > 0) {
495 double zs = 0.0;
496 for (std::size_t k = 0; k < Mz; ++k) zs += num_traits<T>::to_double(Z(k, r));
497 if (zs > 0)
498 t += N[r] * std::log(zs) - std::lgamma(N[r] + 1.0);
499 else
500 ok = false;
501 }
502 if (ok && t > lGest) lGest = t;
503 }
504 if (std::isfinite(lGest))
505 kscale = detail::manjunath_round(lGest / (static_cast<double>(Nt) * std::log(2.0)));
506 }
507 Matrix<T> Ls = L;
508 Matrix<T> Zs = Z;
509 if (kscale != 0) {
510 const T c = num_traits<T>::from_int(1) /
511 num_pow_int(num_traits<T>::from_int(2), static_cast<unsigned>(std::abs(kscale)));
512 const T f = (kscale > 0) ? c : num_traits<T>::from_int(1) / c;
513 for (std::size_t i = 0; i < M; ++i)
514 for (std::size_t r = 0; r < R; ++r) Ls(i, r) = T(Ls(i, r) * f);
515 for (std::size_t k = 0; k < Mz; ++k)
516 for (std::size_t r = 0; r < R; ++r) Zs(k, r) = T(Zs(k, r) * f);
517 }
518
519 // A '>' row is the complement of a '<=' row at the same right-hand side,
520 // which is how the paper discharges it (Eqn 6). With several such rows the
521 // product of the complements expands by inclusion-exclusion, so the series is
522 // evaluated once per subset of them, with the subset's rows re-entered as
523 // '<=' and the rest dropped. Exact, and the only place the cost is
524 // exponential -- in the number of '>' rows, normally zero.
525 std::vector<std::size_t> gt;
526 for (std::size_t j = 0; j < J; ++j)
527 if (sk[j] == 'G') gt.push_back(j);
528 const std::size_t K = gt.size();
529 T Gs = num_traits<T>::from_int(0);
530 std::size_t peak = 0;
531 for (std::size_t mask = 0; mask < (static_cast<std::size_t>(1) << K); ++mask) {
532 std::vector<bool> on(J, false);
533 for (std::size_t j = 0; j < J; ++j) on[j] = (sk[j] != 'G');
534 std::size_t bits = 0;
535 for (std::size_t t = 0; t < K; ++t)
536 if (mask & (static_cast<std::size_t>(1) << t)) {
537 on[gt[t]] = true;
538 ++bits;
539 }
540 std::vector<std::vector<long>> As;
541 std::vector<long> bs;
542 std::string ss;
543 for (std::size_t j = 0; j < J; ++j)
544 if (on[j]) {
545 As.push_back(Ak[j]);
546 bs.push_back(bk[j]);
547 ss.push_back(sk[j] == 'G' ? 'L' : sk[j]);
548 }
549 const T g = detail::pfqn_manjunath_series<T>(Ls, Zs, N, As, bs, ss, options, peak);
550 if (bits % 2 == 0)
551 Gs += g;
552 else
553 Gs -= g;
554 }
555
556 if (!(Gs > num_traits<T>::from_int(0))) {
557 // Either the admissible set is empty or the '>' complements cancelled it.
558 return detail::manjunath_empty<T>(num_traits<T>::from_int(0), -std::numeric_limits<double>::infinity(), peak, options, R);
559 }
560 const double lG = num_traits<T>::log_as_double(Gs) +
561 static_cast<double>(Nt) * kscale * std::log(2.0);
562 T G = Gs;
563 if (kscale != 0) {
564 const T two = num_traits<T>::from_int(2);
565 const unsigned e = static_cast<unsigned>(std::abs(static_cast<long>(Nt) * kscale));
566 const T p = num_pow_int(two, e);
567 G = (kscale > 0) ? T(Gs * p) : T(Gs / p);
568 }
570 out.G = G;
571 out.lG = lG;
572 out.peak_states = peak;
573 if (options.stats)
574 detail::manjunath_stats<T>(out, L, N, Z, Ak, bk, sk, lG, M, Mz, S, R, options);
575 return out;
576}
577
578/** Overload without extra constraints: the plain closed-network constant. */
579template <class T>
580PfqnManjunathResult<T> pfqn_manjunath(const Matrix<T>& L, const std::vector<int>& N,
581 const Matrix<T>& Z,
582 const PfqnManjunathOptions& options = {}) {
583 return pfqn_manjunath<T>(L, N, Z, Matrix<T>(), std::vector<long>(), std::string(), options);
584}
585
586/** Overload without think times or extra constraints. */
587template <class T>
588PfqnManjunathResult<T> pfqn_manjunath(const Matrix<T>& L, const std::vector<int>& N,
589 const PfqnManjunathOptions& options = {}) {
590 return pfqn_manjunath<T>(L, N, Matrix<T>(), Matrix<T>(), std::vector<long>(), std::string(),
591 options);
592}
593
594namespace detail {
595
596/**
597 * Per-class decomposition, for the one configuration in which the truncated
598 * product form is the exact stationary law: a single queueing station inside the
599 * region and a single delay station outside it.
600 *
601 * WHY THE CONFIGURATION IS NOT A CONVENIENCE. With one queueing station the state
602 * is the queue occupancy alone (the delay holds the complement) and every
603 * transition moves one job of one class by one unit, so the chain is a
604 * multidimensional birth-death process. That process is reversible, and Kelly's
605 * truncation theorem then applies verbatim: restricting it to the
606 * coordinate-convex set A n <= b and renormalizing gives exactly the truncated
607 * product form. Add a second queueing station and the delay -> q1 -> q2 -> delay
608 * cycle destroys reversibility; truncation no longer preserves the product form,
609 * measured at 131% relative error on the stationary law of a 2-class, N = [2 2]
610 * instance. G and lG stay correct in every configuration; only the metrics are
611 * withheld.
612 *
613 * WHERE THE BLOCKED JOBS SIT. Nowhere special: a refused admission is a DELETED
614 * transition, so a blocked job never leaves the delay, and because the think time
615 * is exponential a held job is indistinguishable from one still thinking. The
616 * delay population carries both and Little's law separates them. This is NOT the
617 * WAITQ rule of SolverSSA/SolverCTMC/JMT, which moves a refused job out of the
618 * delay into a per-region FIFO counted at no station.
619 */
620template <class T>
621void manjunath_stats(PfqnManjunathResult<T>& out, const Matrix<T>& L, const std::vector<int>& N,
622 const Matrix<T>& Z, const std::vector<std::vector<long>>& A,
623 const std::vector<long>& b, const std::string& sense, double lG,
624 std::size_t M, std::size_t Mz, std::size_t S, std::size_t R,
625 const PfqnManjunathOptions& options) {
626 if (Mz != 1)
627 throw InputError("pfqn_manjunath: the per-class decomposition needs exactly one delay "
628 "station, got " + std::to_string(Mz) +
629 ". Pass Z as a 1xR row of think times");
630 if (M != 1)
631 throw UnsupportedError(
632 "pfqn_manjunath: the per-class decomposition needs exactly one queueing station, got " +
633 std::to_string(M) +
634 ". With two or more the delay->q1->q2->delay cycle makes the chain irreversible, "
635 "Kelly truncation no longer holds, and the truncated product form is not the "
636 "stationary law (measured at 131% error). G and lG are still returned and still "
637 "correct as a sum over the admissible set");
638 const std::size_t J = b.size();
639 // The delay must sit OUTSIDE the region: its columns are S*r + (S-1).
640 for (std::size_t r = 0; r < R; ++r) {
641 const std::size_t dcol = S * r + (S - 1);
642 for (std::size_t j = 0; j < J; ++j)
643 if (A[j][dcol] != 0)
644 throw InputError("pfqn_manjunath: constraint row(s) reference the delay station "
645 "in class " + std::to_string(r + 1) + " (column " +
646 std::to_string(dcol) + "). The delay must lie OUTSIDE the finite "
647 "capacity region, because the decomposition charges every held "
648 "job to it");
649 }
650
651 Matrix<T> Am(J, S * R);
652 for (std::size_t j = 0; j < J; ++j)
653 for (std::size_t c = 0; c < S * R; ++c) Am(j, c) = num_traits<T>::from_int(A[j][c]);
654
655 PfqnManjunathOptions sub = options;
656 sub.stats = false; // the recursion answers with constants only
657
658 // Ratios of normalizing constants are taken in the LOG domain, so the
659 // internal power-of-two rescaling cancels without ever being reconstructed.
660 out.Q.assign(R, 0.0);
661 out.X.assign(R, 0.0);
662 for (std::size_t r = 0; r < R; ++r) {
663 const std::size_t qcol = S * r; // column of (queueing station, class r)
664
665 // Throughput. One class r job removed from the queue leaves a state of
666 // population N - e_r whose admission rule is shifted by that job's own
667 // requirement column, exactly as the loss network's g(C - A e_r).
668 if (N[r] >= 1) {
669 std::vector<int> Nr(N);
670 Nr[r] -= 1;
671 std::vector<long> br(J);
672 for (std::size_t j = 0; j < J; ++j) br[j] = b[j] - A[j][qcol];
673 const PfqnManjunathResult<T> s = pfqn_manjunath<T>(L, Nr, Z, Am, br, sense, sub);
674 if (std::isfinite(s.lG)) out.X[r] = std::exp(s.lG - lG);
675 }
676
677 // Mean queue length from the marginal law. An '=' row is discharged by
678 // picking a single coefficient, so each call returns the mass of exactly
679 // that occupancy.
680 for (int k = 1; k <= N[r]; ++k) {
681 Matrix<T> Ak(J + 1, S * R);
682 for (std::size_t j = 0; j < J; ++j)
683 for (std::size_t c = 0; c < S * R; ++c) Ak(j, c) = Am(j, c);
684 for (std::size_t c = 0; c < S * R; ++c)
685 Ak(J, c) = num_traits<T>::from_int(c == qcol ? 1 : 0);
686 std::vector<long> bk(b);
687 bk.push_back(k);
688 const PfqnManjunathResult<T> s =
689 pfqn_manjunath<T>(L, N, Z, Ak, bk, sense + "E", sub);
690 if (std::isfinite(s.lG)) out.Q[r] += k * std::exp(s.lG - lG);
691 }
692 }
693
694 out.U.assign(R, 0.0);
695 out.think.assign(R, 0.0);
696 out.delay.assign(R, 0.0);
697 out.blocked.assign(R, 0.0);
698 for (std::size_t r = 0; r < R; ++r) {
699 out.U[r] = out.X[r] * num_traits<T>::to_double(L(0, r)); // one server, one visit
700 out.think[r] = out.X[r] * num_traits<T>::to_double(Z(0, r)); // Little's law
701 out.delay[r] = static_cast<double>(N[r]) - out.Q[r]; // not at the queue
702 out.blocked[r] = out.delay[r] - out.think[r]; // held there
703 }
704 out.has_stats = true;
705}
706
707} // namespace detail
708
709} // namespace pfqn
710} // namespace line
711
712#endif // LINE_API_PFQN_MANJUNATH_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
UnsupportedError(const std::string &what)
Definition error.h:51
The exception types the port throws.
Dense matrix and non-owning view.
PfqnManjunathResult< T > pfqn_manjunath(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const Matrix< T > &A, const std::vector< long > &b, const std::string &sense, const PfqnManjunathOptions &options={})
Exact normalizing constant of a closed multiclass product-form network whose state space carries arbi...
T num_pow_int(const T &base, unsigned e)
Integer power, valid in any field (no transcendental requirement).
Definition number.h:192
Number-type abstraction for the templated API port.
Controls of pfqn_manjunath.
std::size_t max_live_states
Cap on the number of series coefficients held at once.
bool stats
Also return the per-class decomposition.
Result of pfqn_manjunath.
T G
normalizing constant in the requested arithmetic
bool has_stats
whether the decomposition below was computed
std::vector< double > X
class r cycle throughput
std::vector< double > think
class r jobs genuinely thinking, X_r * Z_r
std::vector< double > delay
class r jobs at the delay, think + blocked
std::vector< double > U
class r utilization of the queueing station
std::size_t peak_states
peak live series coefficients, the realised cost
double lG
log of the constant, always a double
std::vector< double > blocked
class r jobs held at the delay by the constraint
std::vector< double > Q
mean class r jobs at the queueing station