LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_linearizermx.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_LINEARIZERMX_H
6#define LINE_API_PFQN_LINEARIZERMX_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Linearizer for mixed open/closed queueing networks.
12 *
13 * Templated port of matlab/src/api/pfqn/pfqn_linearizermx.m, cross-checked
14 * against jar/src/main/java/jline/api/pfqn/mva/Pfqn_linearizermx.java.
15 *
16 * The open classes are solved in closed form from their fixed arrival rates:
17 * X_r = lambda_r and U(i,r) = lambda_r L(i,r). Their aggregate utilization
18 * U^o(i) = sum_{r open} U(i,r) then inflates the closed-class demands by the
19 * Bard-Schweitzer/Reiser open-class correction
20 *
21 * Dc(i,c) = L(i,c) / (1 - U^o(i)),
22 *
23 * and the resulting purely closed subnetwork is handed to one of the
24 * Linearizer variants: pfqn_linearizer, pfqn_gflinearizer with alpha = 2, or
25 * pfqn_egflinearizer with a Gompertz alpha per closed class, or
26 * pfqn_linearizerms when any station has more than one server. The open-class
27 * residence times are finally recovered from the closed-class queue lengths as
28 * W(i,r) = L(i,r) (1 + sum_c Qc(i,c)) / (1 - U^o(i)).
29 *
30 * Arithmetic: TRANSCENDENTAL-GATED, on two independent grounds. Every branch
31 * delegates to a Linearizer variant that stops on a tolerance, so the answer
32 * is a fixed point only to within tol; and the 'egflin' branch evaluates the
33 * Gompertz exponent 0.6 + 1.4 exp(-8 exp(-0.8 N_c)), a genuine transcendental
34 * of the closed population, which has no meaning in an exact field.
35 *
36 * Open classes. MATLAB marks them with N_r = Inf. There is no infinity in a
37 * general ordered field, so this port marks them with a negative entry
38 * (kOpenClass) in the otherwise non-negative population vector, and the
39 * `lambda(isnan) = 0` style scrubbing of the reference is dropped: NaN is a
40 * floating-point artefact, not a value of T, and silently rewriting a caller's
41 * input to zero would hide a modelling error rather than fix one.
42 *
43 * MATLAB-vs-JAR disagreement on the 'egflin' alpha, resolved in the JAR's
44 * favour. MATLAB builds the exponent vector in the GLOBAL class index space
45 *
46 * alphaM = zeros(1,R); % R = total class count
47 * for ridx = 1:length(closedClasses)
48 * r = closedClasses(ridx);
49 * alphaM(r) = 0.6 + 1.4*exp(-8*exp(-0.8*N(r)));
50 * end
51 *
52 * but pfqn_egflinearizer consumes it in the CLOSED-class index space, since it
53 * is called with Dc, which has only length(closedClasses) columns and whose
54 * class r is closedClasses(r). Whenever an open class precedes a closed one
55 * the two index spaces differ and the closed class silently receives the
56 * leading zero of alphaM, i.e. alpha = 0, so N_c^alpha_c collapses to 1 and
57 * the Gompertz scaling is switched off entirely. Verified on the two-station,
58 * two-class model of cpp/tests/test_pfqn_linearizer.cpp with class 1 open at
59 * lambda = 0.4 and class 2 closed at N = 3: MATLAB pfqn_linearizermx returns
60 * X_2 = 0.85177305755470, which reproduces exactly a direct
61 * pfqn_egflinearizer call with alpha = 0, whereas the intended exponent
62 * alpha = 1.2775503648739 gives X_2 = 0.84260134365230. The JAR indexes
63 * alphaM over Nclosed and is correct; this port follows the JAR. The defect is
64 * invisible in the single-class-per-index case where closedClasses == 1:R.
65 */
66
67#include <cmath>
68#include <cstddef>
69#include <vector>
70
76#include "line/num/number.h"
77#include "line/util/error.h"
78#include "line/util/matrix.h"
79
80namespace line {
81namespace pfqn {
82
83/** Population sentinel marking an open class, standing in for MATLAB's Inf. */
84constexpr int kOpenClass = -1;
85
86/** Which Linearizer variant solves the closed subnetwork. */
88
89/**
90 * @brief Linearizer for mixed open/closed queueing networks.
91 *
92 * @param lambda (R) per-class arrival rate; must be zero on closed classes
93 * @param L (M x R) service demands
94 * @param N (R) population per class, kOpenClass for an open class
95 * @param Z (K x R) think times, summed over rows; may be empty
96 * @param nservers (M) servers per station; all-one selects the single-server
97 * branch, anything larger routes to pfqn_linearizerms
98 * @param type (M) scheduling discipline; empty means all-PS, as in MATLAB
99 * @param tol convergence tolerance
100 * @param maxiter total inner-iteration budget
101 * @param method Linearizer variant for the closed subnetwork
102 * @param QN0 warm start, (M x R) or (M x closed count); may be empty
103 */
104template <class T>
105LinearizerResult<T> pfqn_linearizermx(const std::vector<T>& lambda, const Matrix<T>& L,
106 const std::vector<int>& N, const Matrix<T>& Z,
107 const std::vector<int>& nservers,
108 const std::vector<SchedStrategy>& type, double tol,
109 int maxiter, LinearizerMxMethod method,
110 const Matrix<T>& QN0) {
111 // runtime gating rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
112
113 const std::size_t M = L.rows();
114 const std::size_t R = N.size();
115 if (!L.empty() && L.cols() != R)
116 throw InputError(
117 "pfqn_linearizermx: demand matrix and population vector disagree on the class count");
118 if (lambda.size() != R)
119 throw InputError("pfqn_linearizermx: arrival-rate vector has the wrong class count");
120 if (nservers.size() != M)
121 throw InputError("pfqn_linearizermx: server-count vector has the wrong station count");
122 if (!type.empty() && type.size() != M)
123 throw InputError("pfqn_linearizermx: scheduling vector has the wrong station count");
124 for (int c : nservers)
125 if (c < 1) throw InputError("pfqn_linearizermx: server count below one");
126
127 const T zero = num_traits<T>::from_int(0);
128 const T one = num_traits<T>::from_int(1);
129
130 std::vector<std::size_t> openC, closedC;
131 for (std::size_t r = 0; r < R; ++r) {
132 if (N[r] == kOpenClass) {
133 openC.push_back(r);
134 } else {
135 if (N[r] < 0) throw InputError("pfqn_linearizermx: negative population");
136 // The reference refuses an arrival rate on a class that also has a
137 // finite positive population: it is neither open nor closed.
138 if (N[r] > 0 && lambda[r] != zero)
139 throw InputError(
140 "pfqn_linearizermx: arrival rate cannot be specified on closed classes");
141 closedC.push_back(r);
142 }
143 }
144
146 res.Q = Matrix<T>(M, R, zero);
147 res.U = Matrix<T>(M, R, zero);
148 res.W = Matrix<T>(M, R, zero);
149 res.C.assign(R, zero);
150 res.X.assign(R, zero);
151 res.totiter = 0;
152
153 for (std::size_t r : openC) {
154 res.X[r] = lambda[r];
155 for (std::size_t i = 0; i < M; ++i) res.U(i, r) = lambda[r] * L(i, r);
156 }
157 // Aggregate open-class utilization, before any closed class contributes.
158 std::vector<T> Ut(M, zero);
159 for (std::size_t i = 0; i < M; ++i)
160 for (std::size_t r : openC) Ut[i] += res.U(i, r);
161
162 const std::vector<T> Zs = sum_rows(Z, R);
163
164 const std::size_t Rc = closedC.size();
165 Matrix<T> Dc(M, Rc, zero);
166 for (std::size_t i = 0; i < M; ++i) {
167 const T slack = one - Ut[i];
168 // saturated-station rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
169 if (!(slack > zero))
170 throw NumericError(
171 "pfqn_linearizermx: open-class traffic saturates a station, the closed-class "
172 "demand correction 1/(1 - U_open) is not positive");
173 for (std::size_t k = 0; k < Rc; ++k) Dc(i, k) = L(i, closedC[k]) / slack;
174 }
175
176 std::vector<int> Nc(Rc, 0);
177 Matrix<T> Zc(1, Rc, zero);
178 for (std::size_t k = 0; k < Rc; ++k) {
179 Nc[k] = N[closedC[k]];
180 Zc(0, k) = Zs[closedC[k]];
181 }
182
183 // Warm start, accepted either over all classes or over the closed ones.
184 Matrix<T> QN0c;
185 if (!QN0.empty() && QN0.rows() == M) {
186 if (QN0.cols() == R) {
187 QN0c = Matrix<T>(M, Rc, zero);
188 for (std::size_t i = 0; i < M; ++i)
189 for (std::size_t k = 0; k < Rc; ++k) QN0c(i, k) = QN0(i, closedC[k]);
190 } else if (QN0.cols() == Rc) {
191 QN0c = QN0;
192 }
193 }
194
195 int cmax = 1;
196 for (int c : nservers)
197 if (c > cmax) cmax = c;
198
200 if (Rc == 0) {
201 sub.Q = Matrix<T>(M, 0, zero);
202 } else if (cmax == 1) {
203 switch (method) {
205 sub = pfqn_gflinearizer(Dc, Nc, Zc, type, tol, maxiter,
206 num_traits<T>::from_double(2.0), QN0c);
207 break;
209 // Gompertz exponent rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
210 std::vector<T> alphaM(Rc, zero);
211 for (std::size_t k = 0; k < Rc; ++k) {
212 if constexpr (num_traits<T>::has_transcendental) {
213 using std::exp;
214 const T n = num_traits<T>::from_int(Nc[k]);
215 const T inner = exp(T(num_traits<T>::from_double(-0.8) * n));
216 const T outer = exp(T(num_traits<T>::from_double(-8.0) * inner));
217 alphaM[k] = num_traits<T>::from_double(0.6) +
218 num_traits<T>::from_double(1.4) * outer;
219 } else {
220 const double n = static_cast<double>(Nc[k]);
221 alphaM[k] = num_traits<T>::from_double(
222 0.6 + 1.4 * std::exp(-8.0 * std::exp(-0.8 * n)));
223 }
224 }
225 sub = pfqn_egflinearizer(Dc, Nc, Zc, type, tol, maxiter, alphaM, QN0c);
226 break;
227 }
229 default:
230 sub = pfqn_linearizer(Dc, Nc, Zc, type, tol, maxiter, QN0c);
231 break;
232 }
233 } else {
234 sub = pfqn_linearizerms(Dc, Nc, Zc, nservers, type, tol, maxiter, QN0c);
235 }
236 res.totiter = sub.totiter;
237
238 for (std::size_t k = 0; k < Rc; ++k) {
239 const std::size_t r = closedC[k];
240 res.X[r] = sub.X[k];
241 res.C[r] = sub.C[k];
242 for (std::size_t i = 0; i < M; ++i) {
243 res.Q(i, r) = sub.Q(i, k);
244 res.W(i, r) = sub.W(i, k);
245 // Recomputed from the ORIGINAL demand L, not from the corrected
246 // Dc the subnetwork was solved with, exactly as in the reference.
247 res.U(i, r) = res.X[r] * L(i, r);
248 }
249 }
250
251 for (std::size_t i = 0; i < M; ++i) {
252 T qsum = one;
253 for (std::size_t k = 0; k < Rc; ++k) qsum += sub.Q(i, k);
254 for (std::size_t r : openC) {
255 res.W(i, r) = L(i, r) * qsum / (one - Ut[i]);
256 res.Q(i, r) = res.W(i, r) * res.X[r];
257 }
258 }
259 for (std::size_t r : openC) {
260 T c = zero;
261 for (std::size_t i = 0; i < M; ++i) c += res.W(i, r);
262 res.C[r] = c;
263 }
264 return res;
265}
266
267/** MATLAB defaults: all-PS, tol = 1e-8, maxiter = 1000, 'egflin', no warm start. */
268template <class T>
269LinearizerResult<T> pfqn_linearizermx(const std::vector<T>& lambda, const Matrix<T>& L,
270 const std::vector<int>& N, const Matrix<T>& Z,
271 const std::vector<int>& nservers,
273 return pfqn_linearizermx(lambda, L, N, Z, nservers, std::vector<SchedStrategy>(), 1e-8, 1000,
274 method, Matrix<T>());
275}
276
277} // namespace pfqn
278} // namespace line
279
280#endif // LINE_API_PFQN_LINEARIZERMX_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.
LinearizerResult< T > pfqn_gflinearizer(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const std::vector< SchedStrategy > &type, double tol, int maxiter, const T &alpha, const Matrix< T > &QN0)
Generalized fixed-point Linearizer with a single scaling exponent shared by every class (De Souza e S...
LinearizerMxMethod
Which Linearizer variant solves the closed subnetwork.
std::vector< T > sum_rows(const Matrix< T > &Z, std::size_t R)
Sum the rows of a think-time matrix into a length-R vector, the sum(Z,1) that every AMVA entry point ...
constexpr int kOpenClass
Population sentinel marking an open class, standing in for MATLAB's Inf.
LinearizerResult< T > pfqn_linearizermx(const std::vector< T > &lambda, const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const std::vector< int > &nservers, const std::vector< SchedStrategy > &type, double tol, int maxiter, LinearizerMxMethod method, const Matrix< T > &QN0)
Linearizer for mixed open/closed queueing networks.
LinearizerResult< T > pfqn_egflinearizer(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const std::vector< SchedStrategy > &type, double tol, int maxiter, const std::vector< T > &alpha, const Matrix< T > &QN0, int npasses=3)
Extended generalized fixed-point Linearizer (De Souza e Silva and Muntz's generalization of Chandy an...
LinearizerResult< T > pfqn_linearizerms(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const std::vector< int > &nservers, const std::vector< SchedStrategy > &type, double tol, int maxiter, const Matrix< T > &QN0)
Multiserver Linearizer (Krzesinski's Linearizer as described in Conway 1989, with De Souza e Silva an...
LinearizerResult< T > pfqn_linearizer(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const std::vector< SchedStrategy > &type, double tol, int maxiter, const Matrix< T > &QN0)
Chandy-Neuse Linearizer for single-server stations.
Number-type abstraction for the templated API port.
Scaffolding shared by the approximate-MVA family.
Extended generalized fixed-point Linearizer (De Souza e Silva and Muntz's generalization of Chandy an...
Generalized fixed-point Linearizer with a single scaling exponent shared by every class (De Souza e S...
Chandy-Neuse Linearizer for single-server stations.
Multiserver Linearizer (Krzesinski's Linearizer as described in Conway 1989, with De Souza e Silva an...
Return value of the Linearizer family, mirroring [Q,U,W,C,X,totiter].
Matrix< T > U
(M x R) utilization
std::vector< T > C
(R) cycle time, N_r/X_r - Z_r
std::vector< T > X
(R) per-class throughput
Matrix< T > W
(M x R) per-station residence time
Matrix< T > Q
(M x R) mean queue length
int totiter
total inner iterations across all Core calls