LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_comom.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_COMOM_H
6#define LINE_API_PFQN_COMOM_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * CoMoM (class-oriented method of moments), the general basis formulation, and
12 * the original repairman-model implementation that preceded pfqn_comomrm.
13 *
14 * Templated port of matlab/src/api/pfqn/pfqn_comom.m and
15 * matlab/src/api/pfqn/pfqn_comomrm_orig.m. Both accept at most ONE queueing
16 * station (plus a delay); the reference raises an error for M > 1 and so does
17 * this port.
18 *
19 * WHERE THIS DIFFERS FROM pfqn_comomrm. pfqn_comomrm (already ported) writes
20 * C^{-1} out in closed form and never forms a linear system. pfqn_comom builds
21 * the full (numDn (M+1)) x (numDn (M+1)) matrices A, B and DA of the CoMoM
22 * basis from scratch on the first job of each class, updates A by DA on every
23 * later job, and SOLVES A h = B h n_r / (sum(n) + M - 1) at each step. It is
24 * the slower but structurally general form, and it is a genuinely independent
25 * route to the same normalizing constant, which is what makes it worth
26 * carrying as a cross-check.
27 *
28 * BASIS LAYOUT. Dn = multichoose(R, M) with its LAST column zeroed and the
29 * rows reordered by sort_by_nnz_pos. For M = 1 that is the R-row set
30 * {0, e_1, ..., e_{R-1}} with the zero row first. The basis position of the
31 * moment G(n - d) with "level" index i (i = 1 is the plain constant, i > 1 the
32 * per-station moment) is
33 *
34 * col(d, 1) = numDn M + pos(d), col(d, i>1) = pos(d) M + i - 2
35 *
36 * in zero-based columns, which is the reference's hash() with its 1-based
37 * indexing removed. Call sites index into that layout by POSITION, so the
38 * enumeration order of multichoose and the bubble sort that follows it are
39 * reproduced verbatim in pfqn_comb_common.h rather than replaced.
40 *
41 * SCALING. The reference renormalizes h by |sum(h)| after every job and folds
42 * the discarded factors into a log accumulator, then also takes abs(h). The
43 * basis entries are normalizing constants and are positive, so the abs() is a
44 * no-op and the scale factors cancel in the final lG. This port carries the
45 * UNSCALED basis, exactly as pfqn_comomrm.h does, which is what makes
46 *
47 * G(N) = h[matrixDim - R] * (sum(N) + M - 1)! / prod_r N_r! * prod_r Lmax_r^{N_r}
48 *
49 * an exact identity in the field of the inputs.
50 *
51 * SORTING IN pfqn_comomrm_orig. The reference sorts the classes by ascending
52 * think time AFTER calling pfqn_nc_sanitize, but permutes only L and Z, leaving
53 * N in place. That would desynchronize N from its demands, except that
54 * pfqn_nc_sanitize has ALREADY ordered the classes by ascending think time, so
55 * the second sort is the identity on every input. This port permutes L, Z and N
56 * together, which agrees with the reference wherever the reference is defined
57 * and is correct if the ordering guarantee were ever to change. Verified
58 * identical on L = [0.5 0.25 0.125], N = [1 2 1], Z = [4 0.25 0.5], whose
59 * post-sanitize think times [8 1 4] are NOT sorted before the second pass:
60 * MATLAB returns lG = -0.439231970578982 against the exact -0.43923197057898189.
61 *
62 * Arithmetic: EXACT-CAPABLE. Additions, multiplications, divisions and one
63 * linear solve per job, all in the field of the inputs. The reference's logs
64 * are only the scale bookkeeping and the factln seeding, both of which are
65 * ratios of factorials formed directly here.
66 *
67 * REFERENCE DEFECTS: none found in either routine. Both reproduce pfqn_ca to
68 * the last few ulps on every model tried, including the non-identity think-time
69 * ordering above.
70 */
71
72#include <algorithm>
73#include <cstddef>
74#include <vector>
75
79#include "line/num/number.h"
80#include "line/util/error.h"
81#include "line/util/lu.h"
82#include "line/util/matrix.h"
83
84namespace line {
85namespace pfqn {
86
87namespace detail {
88
89/** Dn basis of pfqn_comom / pfqn_procomom: multichoose(R,M), last column zeroed, sorted. */
90inline std::vector<std::vector<int>> comom_basis(int R, int M) {
91 std::vector<std::vector<int>> Dn = multichoose_rows(R, M);
92 for (std::size_t d = 0; d < Dn.size(); ++d) Dn[d][static_cast<std::size_t>(R) - 1] = 0;
94 return Dn;
95}
96
97} // namespace detail
98
99/**
100 * CoMoM on the general basis (matlab pfqn_comom.m).
101 *
102 * @param L (1 x R) demands at the single queueing station
103 * @param N (R) populations
104 * @param Z (R) think times
105 * @param atol tolerance below which a demand counts as zero
106 */
107template <class T>
108ComomResult<T> pfqn_comom(const Matrix<T>& L, const std::vector<int>& N, const std::vector<T>& Z,
109 const T& atol) {
110 const std::size_t M = L.rows();
111 const std::size_t R = L.cols();
112 if (M > 1)
113 throw InputError(
114 "pfqn_comom: at most one queueing station is supported (repairman models with a "
115 "delay); use pfqn_ca or pfqn_recal for M > 1");
116 if (M == 0 || R == 0) throw InputError("pfqn_comom: empty demand matrix");
117 if (N.size() != R) throw InputError("pfqn_comom: L and N disagree on the class count");
118 if (Z.size() != R) throw InputError("pfqn_comom: L and Z disagree on the class count");
119
120 const T zero = num_traits<T>::from_int(0);
121 const T one = num_traits<T>::from_int(1);
122
123 // ---- per-class rescaling (reference: Lmax with the sub-atol entries taken from Z) --
124 std::vector<T> Lmax(R, one);
125 for (std::size_t r = 0; r < R; ++r) {
126 T mx = (L(0, r) < atol) ? Z[r] : L(0, r);
127 for (std::size_t i = 1; i < M; ++i) {
128 const T v = (L(i, r) < atol) ? Z[r] : L(i, r);
129 if (v > mx) mx = v;
130 }
131 if (mx <= zero) {
132 if (N[r] > 0)
133 throw InputError(
134 "pfqn_comom: a populated class has neither service demand nor think time");
135 mx = one;
136 }
137 Lmax[r] = mx;
138 }
139 Matrix<T> Ls(M, R, zero);
140 std::vector<T> Zs(R, zero);
141 for (std::size_t r = 0; r < R; ++r) {
142 for (std::size_t i = 0; i < M; ++i) Ls(i, r) = L(i, r) / Lmax[r];
143 Zs[r] = Z[r] / Lmax[r];
144 }
145
146 // ---- basis layout ---------------------------------------------------------------
147 const std::vector<std::vector<int>> Dn =
148 detail::comom_basis(static_cast<int>(R), static_cast<int>(M));
149 const std::size_t numDn = Dn.size();
150 const std::size_t dim = numDn * (M + 1);
151
152 // zero-based column of moment (d, level i), i = 1..M+1 in the reference's numbering
153 const auto col = [&](const std::vector<int>& d, std::size_t i) {
154 const int pos = matchrow(Dn, d);
155 if (pos < 0) throw NumericError("pfqn_comom: basis vector outside the Dn set");
156 const std::size_t p = static_cast<std::size_t>(pos);
157 return (i == 1) ? (numDn * M + p) : (p * M + i - 2);
158 };
159
160 std::vector<int> zeroRow(R, 0);
161 std::vector<T> h(dim, zero);
162 for (std::size_t i = 0; i <= M; ++i) h[col(zeroRow, i + 1)] = one;
163
164 // ---- iterate one job at a time ---------------------------------------------------
165 std::vector<int> nvec(R, 0);
166 Matrix<T> A(dim, dim, zero), B(dim, dim, zero), DA(dim, dim, zero);
167 for (std::size_t r = 0; r < R; ++r) {
168 for (int Nr = 1; Nr <= N[r]; ++Nr) {
169 nvec[r] += 1;
170 if (Nr == 1) {
171 A = Matrix<T>(dim, dim, zero);
172 B = Matrix<T>(dim, dim, zero);
173 DA = Matrix<T>(dim, dim, zero);
174 std::size_t row = 0;
175 for (std::size_t d = 0; d < numDn; ++d) {
176 const std::vector<int>& dv = Dn[d];
177 int s1 = 0; // sum of dv over the reference's columns r..R-1 (1-based)
178 for (std::size_t c = r; c + 1 < R; ++c) s1 += dv[c];
179 if (s1 > 0) {
180 int s2 = 0;
181 for (std::size_t c = r + 1; c + 1 < R; ++c) s2 += dv[c];
182 for (std::size_t k = 0; k <= M; ++k) {
183 const std::size_t c1 = col(dv, k + 1);
184 A(row, c1) = one;
185 if (s2 > 0) {
186 B(row, c1) = one;
187 } else {
188 std::vector<int> dm = dv;
189 dm[r] -= 1;
190 B(row, col(dm, k + 1)) = one;
191 }
192 ++row;
193 }
194 } else {
195 int s3 = 0;
196 for (std::size_t c = 0; c <= r; ++c) s3 += dv[c];
197 if (s3 < static_cast<int>(M)) {
198 for (std::size_t k = 1; k <= M; ++k) {
199 A(row, col(dv, k + 1)) = one;
200 A(row, col(dv, 1)) = -one;
201 for (std::size_t s = 0; s < r; ++s) {
202 std::vector<int> dp = dv;
203 dp[s] += 1;
204 A(row, col(dp, k + 1)) = -Ls(k - 1, s);
205 }
206 B(row, col(dv, k + 1)) = Ls(k - 1, r);
207 ++row;
208 }
209 for (std::size_t s = 0; s < r; ++s) {
210 A(row, col(dv, 1)) = num_traits<T>::from_int(nvec[s] - dv[s]);
211 std::vector<int> dp = dv;
212 dp[s] += 1;
213 A(row, col(dp, 1)) = -Zs[s];
214 for (std::size_t k = 1; k <= M; ++k)
215 A(row, col(dp, k + 1)) = -Ls(k - 1, s);
216 ++row;
217 }
218 }
219 }
220 }
221 // population constraint of the class being filled
222 for (std::size_t d = 0; d < numDn; ++d) {
223 const std::vector<int>& dv = Dn[d];
224 int s1 = 0;
225 for (std::size_t c = r; c + 1 < R; ++c) s1 += dv[c];
226 if (s1 > 0) continue;
227 const std::size_t c0 = col(dv, 1);
228 A(row, c0) = num_traits<T>::from_int(nvec[r] - dv[r]);
229 DA(row, c0) = one;
230 B(row, c0) = Zs[r];
231 for (std::size_t k = 1; k <= M; ++k) B(row, col(dv, k + 1)) = Ls(k - 1, r);
232 ++row;
233 }
234 if (row != dim)
235 throw NumericError("pfqn_comom: the CoMoM system is not square");
236 } else {
237 for (std::size_t i = 0; i < dim; ++i)
238 for (std::size_t j = 0; j < dim; ++j) A(i, j) += DA(i, j);
239 }
240 int nt = 0;
241 for (int x : nvec) nt += x;
242 const T fac = num_traits<T>::from_int(nvec[r]) /
243 num_traits<T>::from_int(nt + static_cast<long>(M) - 1);
244 std::vector<T> b(dim, zero);
245 for (std::size_t i = 0; i < dim; ++i) {
246 T s = zero;
247 for (std::size_t j = 0; j < dim; ++j) s += B(i, j) * h[j];
248 b[i] = s * fac;
249 }
250 h = solve(A, b);
251 }
252 }
253
254 // ---- undo the scaling ------------------------------------------------------------
255 int Ntot = 0;
256 for (int x : N) Ntot += x;
257 T fact = num_factorial<T>(static_cast<unsigned>(Ntot + static_cast<long>(M) - 1));
258 for (std::size_t r = 0; r < R; ++r) {
259 fact /= num_factorial<T>(static_cast<unsigned>(N[r]));
260 fact *= num_pow_int(Lmax[r], static_cast<unsigned>(N[r]));
261 }
262
263 ComomResult<T> res;
264 res.basis = h;
265 res.G = fact * h[dim - R];
267 return res;
268}
269
270/** Overload with the reference's default tolerance. */
271template <class T>
272ComomResult<T> pfqn_comom(const Matrix<T>& L, const std::vector<int>& N, const std::vector<T>& Z) {
273 return pfqn_comom(L, N, Z, num_traits<T>::from_double(1e-14));
274}
275
276/**
277 * Original CoMoM for the finite repairman model (matlab pfqn_comomrm_orig.m).
278 *
279 * @param L (1 x R) demands at the single queueing station
280 * @param N (R) populations
281 * @param Z (K x R) think times
282 * @param atol tolerance passed to pfqn_nc_sanitize
283 */
284template <class T>
285ComomResult<T> pfqn_comomrm_orig(const Matrix<T>& L, const std::vector<int>& N, const Matrix<T>& Z,
286 const T& atol) {
287 if (!L.empty() && L.rows() != 1)
288 throw InputError("pfqn_comomrm_orig: the solver accepts at most a single queueing station");
289
290 const T zero = num_traits<T>::from_int(0);
291 const T one = num_traits<T>::from_int(1);
292 // basis multiplicity rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
293 const T mT = one;
294
295 const NcSanitizeResult<T> san = pfqn_nc_sanitize(std::vector<T>(), L, N, Z, atol);
296 const std::size_t R = san.N.size();
297
298 ComomResult<T> res;
299 if (R == 0) {
300 res.G = san.Gremaind;
301 res.lG = san.lGremaind;
302 res.basis.assign(1, one);
303 return res;
304 }
305
306 // ---- second rescaling, an identity after pfqn_nc_sanitize but reproduced ---------
307 std::vector<T> Lv(R, zero), Zv(R, zero), Lmax(R, one);
308 for (std::size_t r = 0; r < R; ++r) {
309 Lv[r] = san.L.empty() ? zero : san.L(0, r);
310 for (std::size_t k = 0; k < san.Z.rows(); ++k) Zv[r] += san.Z(k, r);
311 T mx = (Lv[r] < atol) ? Zv[r] : Lv[r];
312 if (mx <= zero) {
313 if (san.N[r] > 0)
314 throw InputError(
315 "pfqn_comomrm_orig: a populated class has neither demand nor think time");
316 mx = one;
317 }
318 Lmax[r] = mx;
319 }
320 std::vector<int> Nv(R, 0);
321 for (std::size_t r = 0; r < R; ++r) {
322 Lv[r] /= Lmax[r];
323 Zv[r] /= Lmax[r];
324 Nv[r] = san.N[r];
325 }
326
327 // ---- ascending think time; identity given pfqn_nc_sanitize's ordering -----------
328 std::vector<std::size_t> ord(R);
329 for (std::size_t r = 0; r < R; ++r) ord[r] = r;
330 std::stable_sort(ord.begin(), ord.end(),
331 [&](std::size_t a, std::size_t b) { return Zv[a] < Zv[b]; });
332 std::vector<T> Lp(R, zero), Zp(R, zero);
333 std::vector<int> Np(R, 0);
334 for (std::size_t r = 0; r < R; ++r) {
335 Lp[r] = Lv[ord[r]];
336 Zp[r] = Zv[ord[r]];
337 Np[r] = Nv[ord[r]];
338 }
339
340 // ---- the transfer-matrix recursion on the unscaled basis -------------------------
341 std::vector<int> nvec(R, 0);
342 std::vector<T> h(2, one), hprev(2, one);
343 Matrix<T> F1, F2;
344 for (std::size_t r = 0; r < R; ++r) {
345 const std::size_t rr = r + 1; // the reference's 1-based class index
346 for (int Nr = 1; Nr <= Np[r]; ++Nr) {
347 nvec[r] += 1;
348 int nt = 0;
349 for (int x : nvec) nt += x;
350 if (Nr == 1) {
351 if (rr > 1) {
352 // basis expansion rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
353 const std::size_t p = rr - 1;
354 const T w = num_traits<T>::from_int(Np[r - 1]) /
356 std::vector<T> hn(2 * rr, zero);
357 for (std::size_t i = 0; i < p; ++i) hn[i] = h[i];
358 for (std::size_t i = 0; i < p; ++i) hn[rr + i] = h[p + i];
359 hn[p] = hprev[0] * w;
360 hn[2 * rr - 1] = hprev[p] * w;
361 h.swap(hn);
362 }
363 // C, A12, B2r of the reference, then F1r and F2r.
364 Matrix<T> C(rr, rr, zero), A12(rr, rr, zero), B1r(rr, 2 * rr, zero),
365 B2r(rr, 2 * rr, zero);
366 C(0, 0) = one;
367 for (std::size_t s = 0; s + 1 < rr; ++s) C(0, 1 + s) = -Lp[s];
368 A12(0, 0) = -one;
369 B1r(0, 0) = Lp[r];
370 for (std::size_t s = 0; s + 1 < rr; ++s) {
371 A12(1 + s, 0) = num_traits<T>::from_int(Np[s]);
372 A12(1 + s, 1 + s) = -Zp[s];
373 C(1 + s, 1 + s) = -mT * Lp[s];
374 }
375 for (std::size_t i = 0; i < rr; ++i) {
376 B2r(i, i) = mT * Lp[r];
377 B2r(i, rr + i) = Zp[r];
378 }
379 // F1r = [ C^{-1} B1r ; 0 ], F2r = [ -C^{-1} A12 B2r ; B2r ]
380 Matrix<T> LU = C;
381 const std::vector<std::size_t> piv = lu_factor(LU);
382 Matrix<T> iCB1(rr, 2 * rr, zero), iCA12(rr, rr, zero);
383 for (std::size_t j = 0; j < 2 * rr; ++j) {
384 std::vector<T> rhs(rr, zero);
385 for (std::size_t i = 0; i < rr; ++i) rhs[i] = B1r(i, j);
386 lu_solve(LU, piv, rhs);
387 for (std::size_t i = 0; i < rr; ++i) iCB1(i, j) = rhs[i];
388 }
389 for (std::size_t j = 0; j < rr; ++j) {
390 std::vector<T> rhs(rr, zero);
391 for (std::size_t i = 0; i < rr; ++i) rhs[i] = A12(i, j);
392 lu_solve(LU, piv, rhs);
393 for (std::size_t i = 0; i < rr; ++i) iCA12(i, j) = rhs[i];
394 }
395 F1 = Matrix<T>(2 * rr, 2 * rr, zero);
396 F2 = Matrix<T>(2 * rr, 2 * rr, zero);
397 for (std::size_t i = 0; i < rr; ++i)
398 for (std::size_t j = 0; j < 2 * rr; ++j) {
399 F1(i, j) = iCB1(i, j);
400 T s = zero;
401 for (std::size_t k = 0; k < rr; ++k) s += iCA12(i, k) * B2r(k, j);
402 F2(i, j) = -s;
403 F2(rr + i, j) = B2r(i, j);
404 }
405 }
406 hprev = h;
407 const T nr = num_traits<T>::from_int(nvec[r]);
408 const T den = num_traits<T>::from_int(nt); // sum(nvec) + M - 1 with M = 1
409 std::vector<T> hn(2 * rr, zero);
410 for (std::size_t i = 0; i < 2 * rr; ++i) {
411 T s = zero;
412 for (std::size_t j = 0; j < 2 * rr; ++j) s += (nr * F1(i, j) + F2(i, j)) * hprev[j];
413 hn[i] = s / den;
414 }
415 h.swap(hn);
416 }
417 }
418
419 int Ntot = 0;
420 for (std::size_t r = 0; r < R; ++r) Ntot += Np[r];
421 T fact = num_factorial<T>(static_cast<unsigned>(Ntot)); // (sum N + M - 1)! with M = 1
422 for (std::size_t r = 0; r < R; ++r) {
423 fact /= num_factorial<T>(static_cast<unsigned>(Np[r]));
424 fact *= num_pow_int(Lmax[r], static_cast<unsigned>(Nv[r]));
425 }
426
427 res.basis = h;
428 res.G = san.Gremaind * fact * h[h.size() - R];
430 return res;
431}
432
433/** Overload with the exact (zero-tolerance) tests. */
434template <class T>
435ComomResult<T> pfqn_comomrm_orig(const Matrix<T>& L, const std::vector<int>& N,
436 const Matrix<T>& Z) {
437 return pfqn_comomrm_orig(L, N, Z, num_traits<T>::from_int(0));
438}
439
440} // namespace pfqn
441} // namespace line
442
443#endif // LINE_API_PFQN_COMOM_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.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
NcSanitizeResult< T > pfqn_nc_sanitize(const std::vector< T > &lambda, const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const T &atol)
Preprocessing shared by the normalizing-constant solvers: drop the classes that cannot contribute,...
ComomResult< T > pfqn_comomrm_orig(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const T &atol)
Original CoMoM for the finite repairman model (matlab pfqn_comomrm_orig.m).
Definition pfqn_comom.h:285
int matchrow(const std::vector< std::vector< int > > &rows, const std::vector< int > &row)
Position of row in rows, or -1 when absent.
std::vector< std::vector< int > > multichoose_rows(int n, int k)
All n-vectors of nonnegative integers summing to k, in MATLAB multichoose(n,k) order.
void sort_by_nnz_pos(std::vector< std::vector< int > > &I)
MATLAB's sortbynnzpos: a stable bubble sort putting the rows with FEWER nonzeros first and,...
ComomResult< T > pfqn_comom(const Matrix< T > &L, const std::vector< int > &N, const std::vector< T > &Z, const T &atol)
CoMoM on the general basis (matlab pfqn_comom.m).
Definition pfqn_comom.h:108
void lu_solve(const Matrix< T > &LU, const std::vector< std::size_t > &piv, std::vector< T > &b)
Solve LUx = Pb in place on b, using the factors from lu_factor.
Definition lu.h:94
T num_factorial(unsigned n)
Factorial as a value of T.
Definition number.h:184
std::vector< std::size_t > lu_factor(Matrix< T > &A)
In-place LU of A (n x n).
Definition lu.h:48
T num_pow_int(const T &base, unsigned e)
Integer power, valid in any field (no transcendental requirement).
Definition number.h:192
std::vector< T > solve(const Matrix< T > &A, const std::vector< T > &b)
Convenience: solve Ax = b, leaving A and b untouched.
Definition lu.h:158
Number-type abstraction for the templated API port.
Integer-composition enumeration shared by the CoMoM and MVAC ports.
CoMoM (class-oriented method of moments) for the finite repairman model: one queueing station of mult...
Preprocessing shared by the normalizing-constant solvers: drop the classes that cannot contribute,...
std::vector< T > basis
the final unscaled basis h
T G
normalizing constant
double lG
its logarithm
T Gremaind
multiplicative factor removed from G
Matrix< T > Z
retained think times, rescaled and reordered
Matrix< T > L
retained demands, rescaled and reordered
double lGremaind
its logarithm, for the log-space callers
std::vector< int > N
retained populations, reordered