LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_nc_sanitize.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_NC_SANITIZE_H
6#define LINE_API_PFQN_NC_SANITIZE_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Preprocessing shared by the normalizing-constant solvers: drop the classes
12 * that cannot contribute, rescale the demands per class, and order the classes
13 * so that the zero-think-time ones come first.
14 *
15 * Templated port of matlab/src/api/pfqn/pfqn_nc_sanitize.m.
16 *
17 * The transformation is a change of variables on G, not an approximation:
18 * dividing every demand and think time of class r by Lmax_r divides G(N) by
19 * exactly Lmax_r^{N_r}, and removing a class whose demands are all zero
20 * factors out the delay term Z_r^{N_r}/N_r!. Both are returned, so the caller
21 * recovers G(N) of the original model as
22 *
23 * G_original(N) = Gremaind * G_sanitized(N_sanitized).
24 *
25 * MATLAB returns only the LOGARITHM lGremaind of that factor, which is what
26 * forces every downstream CoMoM routine into log space. Here the factor itself
27 * is returned as a value of T, so the rational path never leaves the field,
28 * and lGremaind is provided alongside for the double callers.
29 *
30 * Two invariants the reference once broke, kept here because both fail silently:
31 *
32 * 1. The delay term of a class confined to the think-time node is
33 * Z_r^{N_r}/N_r!, so its log subtracts factln(N_r), not log(N_r); the two
34 * agree only at N_r = 1 and 2.
35 *
36 * 2. The zero-demand and zero-think-time tests are PER CLASS, on the column
37 * sums of L and Z. find() on the matrix itself returns linear indices,
38 * which equal column indices only when L has a single row.
39 *
40 * Arithmetic: EXACT-CAPABLE. Every operation is a comparison, a division or a
41 * multiplication in the field of the inputs. Pass atol = 0 at T = Rational to
42 * get the exact "is identically zero" tests; a positive atol reproduces the
43 * reference's tolerant filtering in any arithmetic.
44 */
45
46#include <algorithm>
47#include <cstddef>
48#include <numeric>
49#include <vector>
50
51#include "line/num/number.h"
52#include "line/util/error.h"
53#include "line/util/matrix.h"
54
55namespace line {
56namespace pfqn {
57
58template <class T>
60 std::vector<T> lambda; ///< retained arrival rates, in the new class order
61 Matrix<T> L; ///< retained demands, rescaled and reordered
62 std::vector<int> N; ///< retained populations, reordered
63 Matrix<T> Z; ///< retained think times, rescaled and reordered
64 T Gremaind; ///< multiplicative factor removed from G
65 double lGremaind; ///< its logarithm, for the log-space callers
66 std::vector<std::size_t> classIndex; ///< original index of each retained class
67};
68
69/**
70 * @brief Preprocessing shared by the normalizing-constant solvers: drop the
71 * classes that cannot contribute, rescale the demands per class, and
72 * order the classes so that the zero-think-time ones come first.
73 *
74 * @param lambda (R) arrival rates; may be empty for a purely closed model
75 * @param L (M x R) service demands
76 * @param N (R) populations
77 * @param Z (K x R) think times; may be empty
78 * @param atol threshold below which a demand counts as zero; use 0 for exact
79 */
80template <class T>
81NcSanitizeResult<T> pfqn_nc_sanitize(const std::vector<T>& lambda, const Matrix<T>& L,
82 const std::vector<int>& N, const Matrix<T>& Z, const T& atol) {
83 const std::size_t R = N.size();
84 if (!L.empty() && L.cols() != R)
85 throw InputError("pfqn_nc_sanitize: L and N disagree on the class count");
86 if (!Z.empty() && Z.cols() != R)
87 throw InputError("pfqn_nc_sanitize: Z and N disagree on the class count");
88 if (!lambda.empty() && lambda.size() != R)
89 throw InputError("pfqn_nc_sanitize: lambda and N disagree on the class count");
90
91 const T zero = num_traits<T>::from_int(0);
92 const T one = num_traits<T>::from_int(1);
93 const std::size_t M = L.empty() ? 0 : L.rows();
94 const std::size_t D = Z.empty() ? 0 : Z.rows();
95
96 // Column sums, the per-class aggregates every test below is phrased on.
97 const auto colsum = [](const Matrix<T>& A, std::size_t r, const T& z) {
98 T s = z;
99 for (std::size_t i = 0; i < A.rows(); ++i) s += A(i, r);
100 return s;
101 };
102
104 res.Gremaind = one;
105
106 // ---- keep only the classes that have jobs and a well-defined demand ----
107 std::vector<std::size_t> keep;
108 for (std::size_t r = 0; r < R; ++r) {
109 if (N[r] == 0) continue; // empty class: contributes 1
110 const T tot = colsum(L, r, zero) + colsum(Z, r, zero);
111 if (tot < atol) continue; // ill-defined class
112 keep.push_back(r);
113 }
114
115 // ---- classes with no queueing demand at all live entirely in the delay ----
116 // Their contribution factors out exactly as Z_r^{N_r} / N_r!.
117 std::vector<std::size_t> retained;
118 for (std::size_t k = 0; k < keep.size(); ++k) {
119 const std::size_t r = keep[k];
120 const T ldem = colsum(L, r, zero);
121 if (M > 0 && ldem < atol) {
122 const T zr = colsum(Z, r, zero);
123 res.Gremaind *= num_pow_int(zr, static_cast<unsigned>(N[r])) /
124 num_factorial<T>(static_cast<unsigned>(N[r]));
125 continue;
126 }
127 retained.push_back(r);
128 }
129
130 const std::size_t Rk = retained.size();
131
132 // per-class rescaling rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
133 std::vector<T> scale(Rk, one);
134 for (std::size_t k = 0; k < Rk; ++k) {
135 const std::size_t r = retained[k];
136 if (M == 0) continue;
137 T mx = L(0, r);
138 for (std::size_t i = 1; i < M; ++i)
139 if (L(i, r) > mx) mx = L(i, r);
140 if (mx > zero) scale[k] = mx;
141 }
142 for (std::size_t k = 0; k < Rk; ++k)
143 res.Gremaind *= num_pow_int(scale[k], static_cast<unsigned>(N[retained[k]]));
144
145 // ---- order: ascending total think time, then zero-think-time first ----
146 std::vector<std::size_t> ord(Rk);
147 std::iota(ord.begin(), ord.end(), static_cast<std::size_t>(0));
148 std::vector<T> zsum(Rk, zero);
149 for (std::size_t k = 0; k < Rk; ++k) zsum[k] = colsum(Z, retained[k], zero) / scale[k];
150 // Stable, so classes with equal think time keep their relative order, as
151 // MATLAB's sort does.
152 std::stable_sort(ord.begin(), ord.end(),
153 [&](std::size_t a, std::size_t b) { return zsum[a] < zsum[b]; });
154 // zero-think-time reordering rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
155 std::stable_partition(ord.begin(), ord.end(),
156 [&](std::size_t a) { return !(zsum[a] >= atol); });
157
158 res.L = Matrix<T>(M, Rk);
159 res.Z = Matrix<T>(D, Rk);
160 res.N.assign(Rk, 0);
161 res.classIndex.assign(Rk, 0);
162 if (!lambda.empty()) res.lambda.assign(Rk, zero);
163 for (std::size_t k = 0; k < Rk; ++k) {
164 const std::size_t src = retained[ord[k]];
165 const T& sc = scale[ord[k]];
166 for (std::size_t i = 0; i < M; ++i) res.L(i, k) = L(i, src) / sc;
167 for (std::size_t i = 0; i < D; ++i) res.Z(i, k) = Z(i, src) / sc;
168 res.N[k] = N[src];
169 res.classIndex[k] = src;
170 if (!lambda.empty()) res.lambda[k] = lambda[src];
171 }
172
174 return res;
175}
176
177/** Overload with the exact (zero-tolerance) tests. */
178template <class T>
179NcSanitizeResult<T> pfqn_nc_sanitize(const Matrix<T>& L, const std::vector<int>& N,
180 const Matrix<T>& Z) {
181 return pfqn_nc_sanitize(std::vector<T>(), L, N, Z, num_traits<T>::from_int(0));
182}
183
184} // namespace pfqn
185} // namespace line
186
187#endif // LINE_API_PFQN_NC_SANITIZE_H
InputError(const std::string &what)
Definition error.h:105
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
The exception types the port throws.
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,...
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
T num_factorial(unsigned n)
Factorial as a value of T.
Definition number.h:196
T num_pow_int(const T &base, unsigned e)
Integer power, valid in any field (no transcendental requirement).
Definition number.h:204
Number-type abstraction for the templated API port.
std::vector< std::size_t > classIndex
original index of each retained class
T Gremaind
multiplicative factor removed from G
Matrix< T > Z
retained think times, rescaled and reordered
std::vector< T > lambda
retained arrival rates, in the new class order
Matrix< T > L
retained demands, rescaled and reordered
double lGremaind
its logarithm, for the log-space callers
std::vector< int > N
retained populations, reordered