LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_rd.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_RD_H
6#define LINE_API_PFQN_RD_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Reduction heuristic (RD) for the normalizing constant of a closed
12 * LOAD-DEPENDENT product-form network.
13 *
14 * Templated port of matlab/src/api/pfqn/pfqn_rd.m. MATLAB is the ONLY usable
15 * reference here: jar/src/main/java/jline/api/pfqn/nc/Pfqn_rd.java carries four
16 * recorded defects (the load-independent flag is not reset per station, the
17 * rate comparison runs over the wrong axis, the demand division skips class 0,
18 * and the rate matrix is not reset), so it was not used to adjudicate anything
19 * in this port.
20 *
21 * Method. Each station's rate profile mu_i(k) is split into a constant part
22 * and a residual. Let s_i be the first population at which the rate reaches its
23 * terminal value mu_i(sum N), so that mu_i(k) = mu_i(s_i) for k >= s_i. The
24 * demands are rescaled by that terminal rate, y = L / mu_i(s_i), which turns
25 * the station load independent above s_i, and the residual profile
26 *
27 * gamma_i(k) = mu_i(k) / mu_i(s_i),
28 * beta_i(1) = gamma_i(1) / (1 - gamma_i(1)),
29 * beta_i(j) = (1 - gamma_i(j-1)) gamma_i(j) / (1 - gamma_i(j))
30 *
31 * carries what the rescaling threw away. The heuristic then writes
32 *
33 * G(N) = G_LI(y, N, Z) * Cgamma,
34 * Cgamma = sum_{v=0}^{vmax} (sum(N) - max(0, v-1))/sum(N) * E_v,
35 *
36 * with E_v the single-class load-dependent constant pfqn_lldsingle(rho, v,
37 * beta) evaluated at the single-class utilizations rho = y X, X the exact MVA
38 * throughput of the rescaled model, and vmax = min(sum_i (s_i - 1), sum(N))
39 * over the stations that are genuinely load dependent. A station whose rate is
40 * already constant is folded into the demands up front and contributes
41 * nothing.
42 *
43 * Reference behaviour preserved verbatim: a not-a-number rate becomes
44 * infinite, an infinite terminal rate pushes s_i back to the last finite
45 * column, a not-a-number beta becomes infinite (this is the 0 * Inf that every
46 * column past s_i produces), and an all-infinite beta means the residual is
47 * empty and the plain load-independent constant is returned unchanged. The
48 * reference also relies on MATLAB auto-growing lEN so that its first entry is
49 * zero, i.e. E_0 = 1; that is written explicitly here. THAT RELIANCE HOLDS ONLY
50 * FOR vmax >= 1, where the first assignment lands at index 2 and index 1 is
51 * filled with zero as a side effect. At vmax = 0 the assigning loop never runs,
52 * so nothing auto-grows and the read raises "Unrecognized function or variable
53 * 'lEN'"; the explicit zeros here are what make that case defined, and the
54 * reference has been corrected to preallocate the same way.
55 *
56 * pfqn_lldsingle is called on beta, which can be negative, so its linear
57 * (non-logarithmic) recursion is the one that applies; the reference wraps the
58 * call in real() for exactly that reason. The port has only the linear
59 * recursion, so no wrapper is needed.
60 *
61 * Arithmetic: INEXACT BY CONSTRUCTION. This is a heuristic reduction, not an
62 * identity: Cgamma is a truncated and reweighted correction series, the
63 * comparison that locates s_i is against a tolerance, and the result is
64 * reported as a log. Gated on has_transcendental accordingly.
65 */
66
67#include <cmath>
68#include <cstddef>
69#include <limits>
70#include <vector>
71
75#include "line/num/number.h"
76#include "line/util/error.h"
77#include "line/util/matrix.h"
78
79namespace line {
80namespace pfqn {
81
82/** Return value of pfqn_rd, mirroring [lGN, Cgamma]. */
83template <class T>
84struct RdResult {
85 double lGN; ///< log of the normalizing constant
86 T Cgamma; ///< the correction factor, one when the residual is empty
87};
88
89/**
90 * @brief Reduction heuristic (RD) for the normalizing constant of a closed
91 * LOAD-DEPENDENT product-form network.
92 *
93 * @param L0 (M x R) service demands
94 * @param N (R) population per class
95 * @param Z (K x R) think times, summed over rows; empty for none
96 * @param mu0 (M x >= sum N) load-dependent rates
97 * @param tol tolerance used to locate the terminal rate (reference 1e-6)
98 * @param method the load-independent constant algorithm to reduce to
99 */
100template <class T>
101RdResult<T> pfqn_rd(const Matrix<T>& L0, const std::vector<int>& N, const Matrix<T>& Z,
102 const Matrix<T>& mu0, double tol, NcMethod method) {
104 "pfqn_rd requires transcendental arithmetic: it is a heuristic reduction whose "
105 "correction series is truncated and reweighted, it locates the terminal rate by "
106 "a tolerance comparison, and it reports a logarithm");
107
108 const std::size_t M = L0.rows();
109 const std::size_t R = N.size();
110 if (!L0.empty() && L0.cols() != R)
111 throw InputError("pfqn_rd: L and N disagree on the class count");
112
113 const T zero = num_traits<T>::from_int(0);
114 const T one = num_traits<T>::from_int(1);
115 const T inf = num_traits<T>::from_double(std::numeric_limits<double>::infinity());
116 const T tolT = num_traits<T>::from_double(tol);
117 const auto is_nan = [](const T& v) { return !(v == v); };
118 const auto is_finite = [&](const T& v) { return !is_nan(v) && v < inf && v > T(-inf); };
119
120 long Ntot = 0;
121 for (int n : N) Ntot += n;
122
123 RdResult<T> res;
124 res.Cgamma = one;
125 if (Ntot < 0) {
126 res.lGN = -std::numeric_limits<double>::infinity();
127 return res;
128 }
129
130 Matrix<T> L = L0;
131 Matrix<T> mu = mu0;
132 // ---- fold the load-independent stations into the demands ---------------
133 for (std::size_t i = 0; i < M; ++i) {
134 bool constant = true;
135 for (std::size_t k = 1; k < mu.cols(); ++k)
136 if (mu(i, k) != mu(i, 0)) constant = false;
137 if (!constant) continue;
138 for (std::size_t r = 0; r < R; ++r) L(i, r) = L(i, r) / mu(i, 0);
139 for (std::size_t k = 0; k < mu.cols(); ++k) mu(i, k) = one;
140 }
141 if (Ntot == 0) {
142 res.lGN = 0.0;
143 return res;
144 }
145 const std::size_t Nt = static_cast<std::size_t>(Ntot);
146 if (mu.rows() != M) throw InputError("pfqn_rd: mu has the wrong station count");
147 if (mu.cols() < Nt) throw InputError("pfqn_rd: mu has fewer rate columns than the population");
148
149 // ---- truncate the rate table and normalize the missing entries ---------
150 Matrix<T> muT(M, Nt, zero);
151 for (std::size_t i = 0; i < M; ++i)
152 for (std::size_t k = 0; k < Nt; ++k) muT(i, k) = is_nan(mu(i, k)) ? inf : mu(i, k);
153
154 // ---- s_i: first column at which the terminal rate is reached -----------
155 std::vector<std::size_t> s(M, Nt);
156 for (std::size_t i = 0; i < M; ++i) {
157 if (!is_finite(muT(i, Nt - 1))) {
158 s[i] = Nt; // 1-based sum(N)
159 continue;
160 }
161 std::size_t found = Nt;
162 for (std::size_t k = 0; k < Nt; ++k)
163 if (num_abs(T(muT(i, k) - muT(i, Nt - 1))) < tolT) {
164 found = k + 1; // 1-based, as in the reference
165 break;
166 }
167 s[i] = found;
168 }
169
170 // ---- rescale the demands by the terminal rate --------------------------
171 Matrix<T> y = L;
172 for (std::size_t i = 0; i < M; ++i) {
173 if (!is_finite(muT(i, s[i] - 1))) {
174 std::size_t lastfinite = 0;
175 for (std::size_t k = 0; k < Nt; ++k)
176 if (is_finite(muT(i, k))) lastfinite = k + 1;
177 if (lastfinite == 0)
178 throw NumericError("pfqn_rd: a station has no finite load-dependent rate");
179 s[i] = lastfinite;
180 }
181 for (std::size_t r = 0; r < R; ++r) y(i, r) = y(i, r) / muT(i, s[i] - 1);
182 }
183
184 // ---- residual profile and its beta transform ---------------------------
185 Matrix<T> gamma(M, Nt, one);
186 for (std::size_t i = 0; i < M; ++i)
187 for (std::size_t k = 0; k < Nt; ++k) gamma(i, k) = muT(i, k) / muT(i, s[i] - 1);
188
189 Matrix<T> beta(M, Nt, one);
190 for (std::size_t i = 0; i < M; ++i) {
191 beta(i, 0) = gamma(i, 0) / (one - gamma(i, 0));
192 for (std::size_t j = 1; j < Nt; ++j)
193 beta(i, j) = (one - gamma(i, j - 1)) * (gamma(i, j) / (one - gamma(i, j)));
194 }
195 bool allInf = true;
196 for (std::size_t i = 0; i < M; ++i)
197 for (std::size_t j = 0; j < Nt; ++j) {
198 if (is_nan(beta(i, j))) beta(i, j) = inf;
199 if (beta(i, j) != inf) allInf = false;
200 }
201
202 const std::vector<T> lambda;
203 if (allInf) {
204 // The residual is empty: the rescaled model IS the model.
205 res.lGN = pfqn_nc(lambda, L, N, Z, method, zero).lG;
206 return res;
207 }
208
209 // ---- the correction series --------------------------------------------
210 long vmax_l = 0;
211 for (std::size_t i = 0; i < M; ++i)
212 if (s[i] > 1) vmax_l += static_cast<long>(s[i]) - 1;
213 if (vmax_l > Ntot) vmax_l = Ntot;
214 const std::size_t vmax = static_cast<std::size_t>(vmax_l < 0 ? 0 : vmax_l);
215
216 const MvaResult<T> Y = pfqn_mva(y, N, Matrix<T>());
217 Matrix<T> rhoN(M, 1, zero);
218 for (std::size_t i = 0; i < M; ++i) {
219 T acc = zero;
220 for (std::size_t r = 0; r < R; ++r) acc += y(i, r) * Y.XN[r];
221 rhoN(i, 0) = acc;
222 }
223
224 // lEN[0] = 0, i.e. E_0 = 1: the reference gets this from MATLAB's
225 // auto-growth of an unassigned array slot.
226 std::vector<double> lEN(vmax + 1, 0.0);
227 for (std::size_t v = 1; v <= vmax; ++v) {
228 const NcResult<T> e = pfqn_lldsingle(rhoN, static_cast<int>(v), beta);
229 // negative-beta real() rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
230 lEN[v] = e.G < zero ? num_traits<T>::log_as_double(T(-e.G)) : e.lG;
231 }
232
233 T Cgamma = zero;
234 for (std::size_t v = 0; v <= vmax; ++v) {
235 const T EN = num_traits<T>::from_double(std::exp(lEN[v]));
236 const long shift = v >= 1 ? static_cast<long>(v) - 1 : 0;
237 Cgamma += num_traits<T>::from_int(Ntot - (shift > 0 ? shift : 0)) /
238 num_traits<T>::from_int(Ntot) * EN;
239 }
240 res.Cgamma = Cgamma;
241 res.lGN = pfqn_nc(lambda, y, N, Z, method, zero).lG + num_traits<T>::log_as_double(Cgamma);
242 return res;
243}
244
245/**
246 * Reference defaults: tol 1e-6, and the exact convolution for the reduced
247 * load-independent constant. The reference sets options.method = 'default',
248 * whose multi-station branch in this tree dispatches to the cub / le family
249 * that is not ported; 'ca' is what MATLAB's 'default' itself selects for
250 * models of the size this heuristic targets, and it is exact.
251 */
252template <class T>
253RdResult<T> pfqn_rd(const Matrix<T>& L, const std::vector<int>& N, const Matrix<T>& Z,
254 const Matrix<T>& mu) {
255 return pfqn_rd(L, N, Z, mu, 1e-6, NcMethod::Ca);
256}
257
258} // namespace pfqn
259} // namespace line
260
261#endif // LINE_API_PFQN_RD_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.
Dense matrix and non-owning view.
RdResult< T > pfqn_rd(const Matrix< T > &L0, const std::vector< int > &N, const Matrix< T > &Z, const Matrix< T > &mu0, double tol, NcMethod method)
Reduction heuristic (RD) for the normalizing constant of a closed LOAD-DEPENDENT product-form network...
Definition pfqn_rd.h:101
MvaResult< T > pfqn_mva(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const std::vector< int > &mi)
Exact Mean Value Analysis for closed product-form networks (Reiser and Lavenberg 1980).
Definition pfqn_mva.h:71
NcResult< T > pfqn_lldsingle(const Matrix< T > &L, int N, const Matrix< T > &mu)
Exact normalizing constant of a SINGLE-CLASS closed network whose stations are LIMITED load dependent...
NcMethod
The methods this port dispatches, one per compute_norm_const case.
Definition pfqn_nc.h:101
NcDispatchResult< T > pfqn_nc(const std::vector< T > &lambda, const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, NcMethod method, const T &atol, const NcOptions &nopt)
Normalizing constant of a product-form queueing network: the dispatcher.
Definition pfqn_nc.h:276
T num_abs(const T &v)
Definition number.h:172
Number-type abstraction for the templated API port.
Exact normalizing constant of a SINGLE-CLASS closed network whose stations are LIMITED load dependent...
Exact Mean Value Analysis for closed product-form networks (Reiser and Lavenberg 1980).
Normalizing constant of a product-form queueing network: the dispatcher.
std::vector< T > XN
(R) per-class throughput
Definition pfqn_mva.h:45
Return value of the normalizing-constant family, mirroring Ret.pfqnNc.
Definition pfqn_ca.h:44
T G
normalizing constant in the requested arithmetic
Definition pfqn_ca.h:45
double lG
log of the constant, always a double and always finite
Definition pfqn_ca.h:46
Return value of pfqn_rd, mirroring [lGN, Cgamma].
Definition pfqn_rd.h:84
T Cgamma
the correction factor, one when the residual is empty
Definition pfqn_rd.h:86
double lGN
log of the normalizing constant
Definition pfqn_rd.h:85