LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_procomom.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_PROCOMOM_H
6#define LINE_API_PFQN_PROCOMOM_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * ProCoMoM: marginal queue-length probabilities of a closed multiclass
12 * product-form network by the class-oriented method of moments.
13 *
14 * Templated port of matlab/src/api/pfqn/pfqn_procomom.m and
15 * matlab/src/api/pfqn/pfqn_procomom2.m.
16 *
17 * pfqn_procomom carries, instead of the plain moments of pfqn_comom, the
18 * generating coefficients of ONE station's marginal distribution: pk(:, j+1)
19 * holds the basis coefficients of P(n_station = j). The basis is the same
20 * Dn = multichoose(R,M) layout as pfqn_comom, with the reference's UP shifts
21 * (Dn + e_s) where pfqn_comom uses down shifts, and the station of interest is
22 * rotated into the last row of L so the two extra couplings, DC (same level,
23 * one job less at the station) and DD (previous level), always refer to row M.
24 * Every station is solved in turn and its distribution normalized to sum 1.
25 *
26 * pfqn_procomom2 is the two-node special case, a queue plus a delay, where the
27 * whole recursion collapses to a product of bidiagonal transfer matrices,
28 *
29 * F = prod_r T_r^{N_r} / N_r!, T_r(row,row) = Z_r,
30 * T_r(row,row+1) = (n + m - 1) L_r / mu(n),
31 *
32 * and the unnormalized marginal is F e_last. No linear system is solved at all.
33 *
34 * SYSTEM SHAPE. The ProCoMoM matrix is NOT square: countrows(r) contributes
35 * M rows per basis vector in the propagation branch but M + r - 1 (or 1) in the
36 * equation branch, so for M = R = 2 the class-1 step is 6 x 6 and the class-2
37 * step is 7 x 6. The reference solves it with an economy QR when full rank and
38 * with a truncated SVD when not. Both are done here by util/lstsq.h in exact
39 * arithmetic: the normal equations give the identical least-squares vector, and
40 * a full-rank factorization gives the identical minimum-norm vector.
41 *
42 * NOT PORTED, deliberately: the reference's rank-deficiency remedy, which
43 * re-solves the whole problem with the demands randomly perturbed by
44 * 1e-10..1e-4 of their scale (`rng(23000,'twister')`) and keeps whichever
45 * attempt looks best behaved. It is a floating-point workaround for a
46 * floating-point rank test, it changes the answer, and reproducing it would
47 * require MATLAB's Mersenne-Twister stream bit for bit. This port returns the
48 * exact pseudoinverse solution, which is what the perturbation approximates,
49 * and reports the rank deficiency in `rankdef` instead of hiding it.
50 *
51 * Arithmetic: EXACT-CAPABLE. Field operations and least-squares solves only.
52 *
53 * REFERENCE DEFECTS in pfqn_procomom2.m, all reproducible, none corrected in
54 * MATLAB by this port (it does not edit MATLAB):
55 *
56 * 1. `pfqn_procomom2(L,N,Z)` raises "Not enough input arguments". The
57 * nargin < 4 branch evaluates `ones(m, sum(N)+1)`, but `m` is assigned only
58 * by the nargin < 5 branch BELOW it. Reproduce with
59 * pfqn_procomom2([0.4 0.6],[2 1],[1 2]).
60 * 2. Debug scaffolding runs on EVERY call: `QN=double(Q)` and a full
61 * `pfqn_mvald(...)` are executed without semicolons, so the routine prints
62 * QN, XNMVA, QNMVA and pik to the console and pays for an O(prod(N+1))
63 * load-dependent MVA it does not use. Visible in the output of the call
64 * above.
65 * 3. Dead branch: `if any(~isfinite(pk(1,1))) || ~isfinite(G) ... elseif
66 * ~isfinite(G)` -- the elseif is subsumed by the first test and can never
67 * be taken.
68 *
69 * This port implements the transfer-matrix method only, with no printing and
70 * no MVA call, and accepts the three-argument form.
71 */
72
73#include <cstddef>
74#include <vector>
75
78#include "line/num/number.h"
79#include "line/util/error.h"
80#include "line/util/lstsq.h"
81#include "line/util/matrix.h"
82
83namespace line {
84namespace pfqn {
85
86/** Return value of pfqn_procomom, mirroring [Pr, Q]. */
87template <class T>
89 Matrix<T> Pr; ///< (M x sumN+1) marginals, Pr(k, j) = P(n_k = j)
90 std::vector<T> Q; ///< (M) mean queue lengths
91 bool rankdef; ///< true when some ProCoMoM system was rank deficient
92};
93
94/**
95 * Marginal queue-length distributions of every station.
96 *
97 * @param L (M x R) demand matrix
98 * @param N (R) populations
99 * @param Z (R) think times
100 * @param atol tolerance below which a class demand counts as zero
101 */
102template <class T>
103ProcomomResult<T> pfqn_procomom(const Matrix<T>& L, const std::vector<int>& N,
104 const std::vector<T>& Z, const T& atol) {
105 const std::size_t M = L.rows(), R = L.cols();
106 if (M == 0 || R == 0) throw InputError("pfqn_procomom: empty demand matrix");
107 if (N.size() != R) throw InputError("pfqn_procomom: L and N disagree on the class count");
108 if (Z.size() != R) throw InputError("pfqn_procomom: L and Z disagree on the class count");
109
110 const T zero = num_traits<T>::from_int(0);
111 const T one = num_traits<T>::from_int(1);
112 int sumN = 0;
113 for (std::size_t r = 0; r < R; ++r) {
114 if (N[r] < 0) throw InputError("pfqn_procomom: negative population");
115 sumN += N[r];
116 }
117 const std::size_t NP = static_cast<std::size_t>(sumN) + 1;
118
119 // ---- per-class rescaling; normalized marginals are invariant to it -------------
120 Matrix<T> Ls(M, R, zero);
121 std::vector<T> Zs(R, zero);
122 for (std::size_t r = 0; r < R; ++r) {
123 T mx = L(0, r);
124 for (std::size_t i = 1; i < M; ++i)
125 if (L(i, r) > mx) mx = L(i, r);
126 if (mx < atol) mx = one;
127 for (std::size_t i = 0; i < M; ++i) Ls(i, r) = L(i, r) / mx;
128 Zs[r] = Z[r] / mx;
129 }
130
131 const std::vector<std::vector<int>> Dn =
132 detail::comom_basis(static_cast<int>(R), static_cast<int>(M));
133 const std::size_t numDn = Dn.size();
134 const std::size_t basisSize = numDn * M;
135 const std::vector<int> zeroDn(R, 0);
136
137 // zero-based basis column of (dn, i) with i = 1..M
138 const auto phash = [&](const std::vector<int>& dn, std::size_t i) -> long {
139 const int pos = matchrow(Dn, dn);
140 if (pos < 0) return -1;
141 return static_cast<long>(pos) * static_cast<long>(M) + static_cast<long>(i) - 1;
142 };
143
145 res.Pr = Matrix<T>(M, NP, zero);
146 res.Q.assign(M, zero);
147 res.rankdef = false;
148
149 for (std::size_t station = 0; station < M; ++station) {
150 // rotate the station of interest into the last row
151 Matrix<T> Lr = Ls;
152 for (std::size_t r = 0; r < R; ++r) {
153 const T tmp = Lr(station, r);
154 Lr(station, r) = Lr(M - 1, r);
155 Lr(M - 1, r) = tmp;
156 }
157
158 Matrix<T> pk(basisSize, NP, zero);
159 for (std::size_t kk = 1; kk <= M; ++kk) pk(static_cast<std::size_t>(phash(zeroDn, kk)), 0) = one;
160
161 std::vector<int> Ncur(R, 0);
162 for (std::size_t r = 0; r < R; ++r) {
163 for (int Nr = 1; Nr <= N[r]; ++Nr) {
164 Ncur[r] = Nr;
165 const Matrix<T> pklast = pk;
166 pk = Matrix<T>(basisSize, NP, zero);
167
168 // ---- genpmatrix ------------------------------------------------
169 std::size_t numRows = 0;
170 for (std::size_t d = 0; d < numDn; ++d) {
171 int s1 = 0;
172 if (r + 1 <= R - 1)
173 for (std::size_t c = r; c + 1 < R; ++c) s1 += Dn[d][c];
174 if (r + 1 <= R - 1 && s1 > 0) {
175 numRows += M;
176 } else {
177 int s3 = 0;
178 for (std::size_t c = 0; c <= r; ++c) s3 += Dn[d][c];
179 numRows += (s3 < static_cast<int>(M)) ? (M + r) : 1;
180 }
181 }
182 Matrix<T> Ag(numRows, basisSize, zero), Bg(numRows, basisSize, zero),
183 DCg(numRows, basisSize, zero), DDg(numRows, basisSize, zero);
184 std::size_t row = 0;
185 for (std::size_t d = 0; d < numDn; ++d) {
186 const std::vector<int>& dv = Dn[d];
187 int s1 = 0;
188 if (r + 1 <= R - 1)
189 for (std::size_t c = r; c + 1 < R; ++c) s1 += dv[c];
190 if (r + 1 <= R - 1 && s1 > 0) {
191 int s2 = 0;
192 for (std::size_t c = r + 1; c + 1 < R; ++c) s2 += dv[c];
193 for (std::size_t k = 1; k <= M; ++k) {
194 const long cA = phash(dv, k);
195 Ag(row, static_cast<std::size_t>(cA)) = one;
196 if (s2 > 0) {
197 Bg(row, static_cast<std::size_t>(cA)) = one;
198 } else {
199 std::vector<int> sh = dv;
200 sh[r] -= 1;
201 const long cB = phash(sh, k);
202 if (cB >= 0) Bg(row, static_cast<std::size_t>(cB)) = one;
203 }
204 ++row;
205 }
206 } else {
207 int s3 = 0;
208 for (std::size_t c = 0; c <= r; ++c) s3 += dv[c];
209 if (s3 < static_cast<int>(M)) {
210 for (std::size_t k = 1; k + 1 <= M; ++k) {
211 Ag(row, static_cast<std::size_t>(phash(dv, k + 1))) = one;
212 Ag(row, static_cast<std::size_t>(phash(dv, 1))) = -one;
213 for (std::size_t s = 0; s < r; ++s) {
214 std::vector<int> sh = dv;
215 sh[s] += 1; // UP shift
216 const long c = phash(sh, k + 1);
217 if (c >= 0) Ag(row, static_cast<std::size_t>(c)) = -Lr(k - 1, s);
218 }
219 Bg(row, static_cast<std::size_t>(phash(dv, k + 1))) = Lr(k - 1, r);
220 ++row;
221 }
222 for (std::size_t s = 0; s < r; ++s) {
223 Ag(row, static_cast<std::size_t>(phash(dv, 1))) =
224 num_traits<T>::from_int(Ncur[s] - dv[s]);
225 std::vector<int> sh = dv;
226 sh[s] += 1; // UP shift
227 const long cb = phash(sh, 1);
228 if (cb >= 0) {
229 Ag(row, static_cast<std::size_t>(cb)) = -Zs[s];
230 DCg(row, static_cast<std::size_t>(cb)) = Lr(M - 1, s);
231 }
232 for (std::size_t k = 1; k + 1 <= M; ++k) {
233 const long c = phash(sh, k + 1);
234 if (c >= 0) Ag(row, static_cast<std::size_t>(c)) = -Lr(k - 1, s);
235 }
236 ++row;
237 }
238 }
239 // extra population constraint of class r, always present
240 Ag(row, static_cast<std::size_t>(phash(dv, 1))) =
241 num_traits<T>::from_int(Ncur[r] - dv[r]);
242 Bg(row, static_cast<std::size_t>(phash(dv, 1))) = Zs[r];
243 for (std::size_t k = 1; k + 1 <= M; ++k)
244 Bg(row, static_cast<std::size_t>(phash(dv, k + 1))) = Lr(k - 1, r);
245 DDg(row, static_cast<std::size_t>(phash(dv, 1))) = Lr(M - 1, r);
246 ++row;
247 }
248 }
249 if (row != numRows) throw NumericError("pfqn_procomom: row count mismatch");
250
251 // ---- level recursion -------------------------------------------
252 int sumNcur = 0;
253 for (int x : Ncur) sumNcur += x;
254 const T tol = line::detail::lstsq_tolerance(Ag);
255 for (int n = 0; n <= sumNcur; ++n) {
256 std::vector<T> rhs(numRows, zero);
257 for (std::size_t i = 0; i < numRows; ++i) {
258 T s = zero;
259 for (std::size_t j = 0; j < basisSize; ++j)
260 s += Bg(i, j) * pklast(j, static_cast<std::size_t>(n));
261 if (n >= 1) {
262 const T nn = num_traits<T>::from_int(n);
263 T sc = zero, sd = zero;
264 for (std::size_t j = 0; j < basisSize; ++j) {
265 sc += DCg(i, j) * pk(j, static_cast<std::size_t>(n) - 1);
266 sd += DDg(i, j) * pklast(j, static_cast<std::size_t>(n) - 1);
267 }
268 s += nn * sc + nn * sd;
269 }
270 rhs[i] = s;
271 }
272 const LstsqResult<T> sol = lstsq(Ag, rhs, tol);
273 if (sol.rankdef) res.rankdef = true;
274 for (std::size_t j = 0; j < basisSize; ++j)
275 pk(j, static_cast<std::size_t>(n)) = sol.x[j];
276 }
277 }
278 }
279
280 const std::size_t outRow = static_cast<std::size_t>(phash(zeroDn, 1));
281 T total = zero;
282 for (std::size_t j = 0; j < NP; ++j) total += pk(outRow, j);
283 if (total != zero)
284 for (std::size_t j = 0; j < NP; ++j) res.Pr(station, j) = pk(outRow, j) / total;
285 }
286
287 for (std::size_t i = 0; i < M; ++i) {
288 T q = zero;
289 for (std::size_t j = 0; j < NP; ++j) q += num_traits<T>::from_int(static_cast<long>(j)) * res.Pr(i, j);
290 res.Q[i] = q;
291 }
292 return res;
293}
294
295/** Overload with the reference's default tolerance. */
296template <class T>
297ProcomomResult<T> pfqn_procomom(const Matrix<T>& L, const std::vector<int>& N,
298 const std::vector<T>& Z) {
299 return pfqn_procomom(L, N, Z, num_traits<T>::from_double(1e-14));
300}
301
302/** Return value of pfqn_procomom2, mirroring [pk, lG, G, T, F, B]. */
303template <class T>
305 std::vector<T> pk; ///< (sumN+1) marginal, pk[n] = P(n jobs at the queue)
306 T G; ///< normalizing constant
307 double lG; ///< its logarithm
308 std::vector<Matrix<T>> Tr; ///< per-class transfer matrices
309 Matrix<T> F; ///< prod_r Tr^{N_r} / N_r!
310 Matrix<T> B; ///< prod_r Tr
311};
312
313/**
314 * Queue-plus-delay marginal by the transfer-matrix form of ProCoMoM.
315 *
316 * @param L (R) demands at the single queueing station
317 * @param N (R) populations
318 * @param Z (R) think times
319 * @param mu (sumN) load-dependent rates of the queue, mu[n-1] with n jobs;
320 * empty for the load-independent default
321 * @param m multiplicity of the queueing station
322 */
323template <class T>
324Procomom2Result<T> pfqn_procomom2(const std::vector<T>& L, const std::vector<int>& N,
325 const std::vector<T>& Z, const std::vector<T>& mu, int m) {
326 const std::size_t R = L.size();
327 if (R == 0) throw InputError("pfqn_procomom2: empty demand vector");
328 if (N.size() != R) throw InputError("pfqn_procomom2: L and N disagree on the class count");
329 if (Z.size() != R) throw InputError("pfqn_procomom2: L and Z disagree on the class count");
330 if (m < 1) throw InputError("pfqn_procomom2: the multiplicity must be at least one");
331
332 const T zero = num_traits<T>::from_int(0);
333 const T one = num_traits<T>::from_int(1);
334 int sumN = 0;
335 for (std::size_t r = 0; r < R; ++r) {
336 if (N[r] < 0) throw InputError("pfqn_procomom2: negative population");
337 sumN += N[r];
338 }
339 const std::size_t dim = static_cast<std::size_t>(sumN) + 1;
340
341 // mu_[n] is the reference's mu(1+n): mu_[0] = 1, mu_[n] the rate with n jobs.
342 std::vector<T> mu_(dim, one);
343 if (!mu.empty()) {
344 if (mu.size() < static_cast<std::size_t>(sumN))
345 throw InputError("pfqn_procomom2: mu must supply a rate for every population up to sum(N)");
346 for (std::size_t n = 1; n < dim; ++n) {
347 if (mu[n - 1] <= zero)
348 throw InputError("pfqn_procomom2: the load-dependent rates must be positive");
349 mu_[n] = mu[n - 1];
350 }
351 }
352
354 res.Tr.reserve(R);
355 for (std::size_t r = 0; r < R; ++r) {
356 Matrix<T> Tm(dim, dim, zero);
357 for (int n = sumN; n >= 1; --n) {
358 const std::size_t row = static_cast<std::size_t>(sumN - n);
359 Tm(row, row) = Z[r];
360 Tm(row, row + 1) =
361 num_traits<T>::from_int(n + m - 1) * L[r] / mu_[static_cast<std::size_t>(n)];
362 }
363 Tm(dim - 1, dim - 1) = Z[r];
364 res.Tr.push_back(Tm);
365 }
366
367 const auto matmul = [&](const Matrix<T>& A, const Matrix<T>& Bm) {
368 Matrix<T> C(dim, dim, zero);
369 for (std::size_t i = 0; i < dim; ++i)
370 for (std::size_t k = 0; k < dim; ++k) {
371 if (A(i, k) == zero) continue;
372 for (std::size_t j = 0; j < dim; ++j) C(i, j) += A(i, k) * Bm(k, j);
373 }
374 return C;
375 };
376
377 Matrix<T> F(dim, dim, zero), B(dim, dim, zero);
378 for (std::size_t i = 0; i < dim; ++i) {
379 F(i, i) = one;
380 B(i, i) = one;
381 }
382 for (std::size_t r = 0; r < R; ++r) {
383 Matrix<T> P(dim, dim, zero);
384 for (std::size_t i = 0; i < dim; ++i) P(i, i) = one;
385 for (int e = 0; e < N[r]; ++e) P = matmul(P, res.Tr[r]);
386 const T fac = num_factorial<T>(static_cast<unsigned>(N[r]));
387 for (std::size_t i = 0; i < dim; ++i)
388 for (std::size_t j = 0; j < dim; ++j) P(i, j) /= fac;
389 F = matmul(F, P);
390 B = matmul(B, res.Tr[r]);
391 }
392 res.F = F;
393 res.B = B;
394
395 // pk = (F e_last)', reversed so that pk[n] = P(n jobs at the queue)
396 std::vector<T> v(dim, zero);
397 for (std::size_t i = 0; i < dim; ++i) v[i] = F(i, dim - 1);
398 T G = zero;
399 for (std::size_t i = 0; i < dim; ++i) G += v[i];
400 res.G = G;
402 res.pk.assign(dim, zero);
403 if (G != zero)
404 for (std::size_t i = 0; i < dim; ++i) res.pk[dim - 1 - i] = v[i] / G;
405 return res;
406}
407
408/** Overload with the load-independent, unit-multiplicity defaults. */
409template <class T>
410Procomom2Result<T> pfqn_procomom2(const std::vector<T>& L, const std::vector<int>& N,
411 const std::vector<T>& Z) {
412 return pfqn_procomom2(L, N, Z, std::vector<T>(), 1);
413}
414
415} // namespace pfqn
416} // namespace line
417
418#endif // LINE_API_PFQN_PROCOMOM_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.
Least squares for a rectangular system, exact-capable.
Dense matrix and non-owning view.
ProcomomResult< T > pfqn_procomom(const Matrix< T > &L, const std::vector< int > &N, const std::vector< T > &Z, const T &atol)
Marginal queue-length distributions of every station.
int matchrow(const std::vector< std::vector< int > > &rows, const std::vector< int > &row)
Position of row in rows, or -1 when absent.
Procomom2Result< T > pfqn_procomom2(const std::vector< T > &L, const std::vector< int > &N, const std::vector< T > &Z, const std::vector< T > &mu, int m)
Queue-plus-delay marginal by the transfer-matrix form of ProCoMoM.
LstsqResult< T > lstsq(const Matrix< T > &A, const std::vector< T > &b, const T &tol)
Least-squares solution of A x = b, minimum-norm when A is rank deficient.
Definition lstsq.h:152
T num_factorial(unsigned n)
Factorial as a value of T.
Definition number.h:184
Matrix< T > matmul(const Matrix< T > &A, const Matrix< T > &B)
Matrix product A B.
Definition linalg.h:36
Number-type abstraction for the templated API port.
Integer-composition enumeration shared by the CoMoM and MVAC ports.
CoMoM (class-oriented method of moments), the general basis formulation, and the original repairman-m...
Outcome of lstsq: the solution and whether the system was rank deficient.
Definition lstsq.h:58
std::vector< T > x
Definition lstsq.h:59
Return value of pfqn_procomom2, mirroring [pk, lG, G, T, F, B].
Matrix< T > B
prod_r Tr
std::vector< T > pk
(sumN+1) marginal, pk[n] = P(n jobs at the queue)
T G
normalizing constant
std::vector< Matrix< T > > Tr
per-class transfer matrices
Matrix< T > F
prod_r Tr^{N_r} / N_r!
Return value of pfqn_procomom, mirroring [Pr, Q].
std::vector< T > Q
(M) mean queue lengths
bool rankdef
true when some ProCoMoM system was rank deficient
Matrix< T > Pr
(M x sumN+1) marginals, Pr(k, j) = P(n_k = j)