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 divergences from the reference, both deliberate.
31 *
32 * 1. REFERENCE DEFECT, corrected here. MATLAB's zero-demand branch reads
33 *
34 * lGremaind = lGremaind + N(zerodemands)*log(Z(zerodemands))' ...
35 * - sum(log(N(zerodemands)));
36 *
37 * The delay balance function of a class confined to the think-time node is
38 * Z_r^{N_r}/N_r!, whose log is N_r log Z_r - log(N_r!), i.e. -factln(N_r).
39 * The reference subtracted log(N_r) instead of log(N_r!), which agrees only
40 * at N_r = 1 and 2 (1! = 1, 2! = 2) and diverges like log((N_r-1)!) after
41 * that. Measured on L = [0 0.5], N = [3 2], Z = [1 0]: MATLAB reconstructed
42 * lG = -2.484906649788 against the exact -3.178053830348, short by exactly
43 * log 2. FIXED IN MATLAB (commit "m fix: pfqn_nc_sanitize delay term uses
44 * factln and column selection"), which now reconstructs the exact constant
45 * to 0. `Pfqn_nc_sanitize.java` still carries the old form.
46 *
47 * 2. MATLAB indexes the zero-demand and zero-think-time tests with find() on a
48 * MATRIX, e.g. `zerodemands = find(L < atol)`, and then uses the resulting
49 * LINEAR indices as COLUMN indices. That is only equivalent to the intended
50 * per-class test when L has a single row, which is exactly the case for the
51 * repairman callers this routine was written for. This port applies the test
52 * to the column sums; MATLAB now does the same, and the M = 2 case
53 * reconstructs the exact constant to 0 where it previously could not.
54 *
55 * Arithmetic: EXACT-CAPABLE. Every operation is a comparison, a division or a
56 * multiplication in the field of the inputs. Pass atol = 0 at T = Rational to
57 * get the exact "is identically zero" tests; a positive atol reproduces the
58 * reference's tolerant filtering in any arithmetic.
59 */
60
61#include <algorithm>
62#include <cstddef>
63#include <numeric>
64#include <vector>
65
66#include "line/num/number.h"
67#include "line/util/error.h"
68#include "line/util/matrix.h"
69
70namespace line {
71namespace pfqn {
72
73template <class T>
75 std::vector<T> lambda; ///< retained arrival rates, in the new class order
76 Matrix<T> L; ///< retained demands, rescaled and reordered
77 std::vector<int> N; ///< retained populations, reordered
78 Matrix<T> Z; ///< retained think times, rescaled and reordered
79 T Gremaind; ///< multiplicative factor removed from G
80 double lGremaind; ///< its logarithm, for the log-space callers
81 std::vector<std::size_t> classIndex; ///< original index of each retained class
82};
83
84/**
85 * @brief Preprocessing shared by the normalizing-constant solvers: drop the
86 * classes that cannot contribute, rescale the demands per class, and
87 * order the classes so that the zero-think-time ones come first.
88 *
89 * @param lambda (R) arrival rates; may be empty for a purely closed model
90 * @param L (M x R) service demands
91 * @param N (R) populations
92 * @param Z (K x R) think times; may be empty
93 * @param atol threshold below which a demand counts as zero; use 0 for exact
94 */
95template <class T>
96NcSanitizeResult<T> pfqn_nc_sanitize(const std::vector<T>& lambda, const Matrix<T>& L,
97 const std::vector<int>& N, const Matrix<T>& Z, const T& atol) {
98 const std::size_t R = N.size();
99 if (!L.empty() && L.cols() != R)
100 throw InputError("pfqn_nc_sanitize: L and N disagree on the class count");
101 if (!Z.empty() && Z.cols() != R)
102 throw InputError("pfqn_nc_sanitize: Z and N disagree on the class count");
103 if (!lambda.empty() && lambda.size() != R)
104 throw InputError("pfqn_nc_sanitize: lambda and N disagree on the class count");
105
106 const T zero = num_traits<T>::from_int(0);
107 const T one = num_traits<T>::from_int(1);
108 const std::size_t M = L.empty() ? 0 : L.rows();
109 const std::size_t D = Z.empty() ? 0 : Z.rows();
110
111 // Column sums, the per-class aggregates every test below is phrased on.
112 const auto colsum = [](const Matrix<T>& A, std::size_t r, const T& z) {
113 T s = z;
114 for (std::size_t i = 0; i < A.rows(); ++i) s += A(i, r);
115 return s;
116 };
117
119 res.Gremaind = one;
120
121 // ---- keep only the classes that have jobs and a well-defined demand ----
122 std::vector<std::size_t> keep;
123 for (std::size_t r = 0; r < R; ++r) {
124 if (N[r] == 0) continue; // empty class: contributes 1
125 const T tot = colsum(L, r, zero) + colsum(Z, r, zero);
126 if (tot < atol) continue; // ill-defined class
127 keep.push_back(r);
128 }
129
130 // ---- classes with no queueing demand at all live entirely in the delay ----
131 // Their contribution factors out exactly as Z_r^{N_r} / N_r!.
132 std::vector<std::size_t> retained;
133 for (std::size_t k = 0; k < keep.size(); ++k) {
134 const std::size_t r = keep[k];
135 const T ldem = colsum(L, r, zero);
136 if (M > 0 && ldem < atol) {
137 const T zr = colsum(Z, r, zero);
138 res.Gremaind *= num_pow_int(zr, static_cast<unsigned>(N[r])) /
139 num_factorial<T>(static_cast<unsigned>(N[r]));
140 continue;
141 }
142 retained.push_back(r);
143 }
144
145 const std::size_t Rk = retained.size();
146
147 // per-class rescaling rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
148 std::vector<T> scale(Rk, one);
149 for (std::size_t k = 0; k < Rk; ++k) {
150 const std::size_t r = retained[k];
151 if (M == 0) continue;
152 T mx = L(0, r);
153 for (std::size_t i = 1; i < M; ++i)
154 if (L(i, r) > mx) mx = L(i, r);
155 if (mx > zero) scale[k] = mx;
156 }
157 for (std::size_t k = 0; k < Rk; ++k)
158 res.Gremaind *= num_pow_int(scale[k], static_cast<unsigned>(N[retained[k]]));
159
160 // ---- order: ascending total think time, then zero-think-time first ----
161 std::vector<std::size_t> ord(Rk);
162 std::iota(ord.begin(), ord.end(), static_cast<std::size_t>(0));
163 std::vector<T> zsum(Rk, zero);
164 for (std::size_t k = 0; k < Rk; ++k) zsum[k] = colsum(Z, retained[k], zero) / scale[k];
165 // Stable, so classes with equal think time keep their relative order, as
166 // MATLAB's sort does.
167 std::stable_sort(ord.begin(), ord.end(),
168 [&](std::size_t a, std::size_t b) { return zsum[a] < zsum[b]; });
169 // zero-think-time reordering rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
170 std::stable_partition(ord.begin(), ord.end(),
171 [&](std::size_t a) { return !(zsum[a] >= atol); });
172
173 res.L = Matrix<T>(M, Rk);
174 res.Z = Matrix<T>(D, Rk);
175 res.N.assign(Rk, 0);
176 res.classIndex.assign(Rk, 0);
177 if (!lambda.empty()) res.lambda.assign(Rk, zero);
178 for (std::size_t k = 0; k < Rk; ++k) {
179 const std::size_t src = retained[ord[k]];
180 const T& sc = scale[ord[k]];
181 for (std::size_t i = 0; i < M; ++i) res.L(i, k) = L(i, src) / sc;
182 for (std::size_t i = 0; i < D; ++i) res.Z(i, k) = Z(i, src) / sc;
183 res.N[k] = N[src];
184 res.classIndex[k] = src;
185 if (!lambda.empty()) res.lambda[k] = lambda[src];
186 }
187
189 return res;
190}
191
192/** Overload with the exact (zero-tolerance) tests. */
193template <class T>
194NcSanitizeResult<T> pfqn_nc_sanitize(const Matrix<T>& L, const std::vector<int>& N,
195 const Matrix<T>& Z) {
196 return pfqn_nc_sanitize(std::vector<T>(), L, N, Z, num_traits<T>::from_int(0));
197}
198
199} // namespace pfqn
200} // namespace line
201
202#endif // LINE_API_PFQN_NC_SANITIZE_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
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,...
T num_factorial(unsigned n)
Factorial as a value of T.
Definition number.h:184
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.
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