LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_schmidt.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_SCHMIDT_H
6#define LINE_API_PFQN_SCHMIDT_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Schmidt's MVA for closed networks with general scheduling disciplines and
12 * class-dependent multiserver FCFS stations.
13 *
14 * Templated port of matlab/src/api/pfqn/pfqn_schmidt.m, cross-checked against
15 * jar/src/main/java/jline/api/pfqn/ld/Pfqn_schmidt.java.
16 *
17 * The recursion walks the population lattice 0 <= k <= N and, at each k,
18 * computes the residence time by the arrival theorem. Three station kinds are
19 * distinguished, as in the reference:
20 *
21 * - INF: w = D(i,c).
22 * - PS, and FCFS with class-independent demands: the standard
23 * w = D(i,c)/s (1 + sum_r L(i,r | k - e_c)) plus, for s > 1, the
24 * idle-server correction sum_{j=1}^{s-1} (s-j) Pr(j-1 busy | k - e_c) D/s.
25 * Both need the scalar busy-server distribution Pr(j | k).
26 * - FCFS with class-dependent demands and s > 1: the full per-class state
27 * distribution Pr(nvec | k) is carried, and
28 * w = sum_{nvec <= k, nvec_c > 0} B_c(nvec) Pr(nvec - e_c | k - e_c),
29 * with B_c the queue-composition-weighted mean service time.
30 *
31 * Pure service times. The B_c terms need per-visit service times S = D/v, not
32 * demands, so the visit ratios enter explicitly; with v == 1 the two coincide.
33 *
34 * Arithmetic: EXACT-CAPABLE, no transcendental gate. Every step is a finite
35 * sum, product or quotient over the population lattice, so the whole recursion
36 * stays in the field of the inputs and is exact in rational arithmetic. The
37 * reference's three floating-point guards (max(v, 1e-12) on the visit ratios,
38 * max(s (sum(nvec) - 1), 1e-12) on the B_c denominator, and the max(eps, .)
39 * and max(1e-12, .) floors on the idle-state probability) are carried over
40 * verbatim as constants of the algorithm, since removing them would change the
41 * numbers the reference produces; they are the only place a magic constant
42 * enters, and none of them makes the arithmetic inexact.
43 */
44
45#include <cstddef>
46#include <vector>
47
49#include "line/num/number.h"
50#include "line/util/error.h"
51#include "line/util/matrix.h"
53
54namespace line {
55namespace pfqn {
56
57/** Return value of pfqn_schmidt, mirroring [XN,QN,UN,CN]. */
58template <class T>
60 std::vector<T> XN; ///< (R) per-class throughput
61 Matrix<T> QN; ///< (M x R) mean queue length
62 Matrix<T> UN; ///< (M x R) utilization, D X / s
63 Matrix<T> CN; ///< (M x R) residence time
64};
65
66namespace detail {
67
68/** Which marginal distribution a station needs, if any. */
69enum class SchmidtPc { None, Scalar, Vector };
70
71} // namespace detail
72
73/**
74 * @brief Schmidt's MVA for closed networks with general scheduling
75 * disciplines and class-dependent multiserver FCFS stations.
76 *
77 * @param D (M x R) service demands
78 * @param N (R) population per class
79 * @param S (M x R) or (M x 1) server counts
80 * @param sched (M) scheduling discipline per station
81 * @param v (M x R) visit ratios; empty for all ones
82 */
83template <class T>
84SchmidtResult<T> pfqn_schmidt(const Matrix<T>& D, const std::vector<int>& N, const Matrix<int>& S,
85 const std::vector<SchedStrategy>& sched, const Matrix<T>& v) {
86 const std::size_t M = D.rows();
87 const std::size_t R = N.size();
88 if (!D.empty() && D.cols() != R)
89 throw InputError("pfqn_schmidt: demand matrix and population vector disagree on the class count");
90 if (sched.size() != M)
91 throw InputError("pfqn_schmidt: scheduling vector has the wrong station count");
92 if (S.rows() != M || (S.cols() != R && S.cols() != 1))
93 throw InputError("pfqn_schmidt: server-count matrix has the wrong shape");
94 if (!v.empty() && (v.rows() != M || v.cols() != R))
95 throw InputError("pfqn_schmidt: visit-ratio matrix has the wrong shape");
96 for (int n : N)
97 if (n < 0) throw InputError("pfqn_schmidt: negative population");
98 for (std::size_t i = 0; i < M; ++i)
99 for (std::size_t c = 0; c < S.cols(); ++c)
100 if (S(i, c) < 1) throw InputError("pfqn_schmidt: server count below one");
101
102 const T zero = num_traits<T>::from_int(0);
103 const T one = num_traits<T>::from_int(1);
104 const T tiny = num_traits<T>::from_double(1e-12);
105 const T epsT = num_traits<T>::from_double(2.220446049250313e-16);
106
108 res.XN.assign(R, zero);
109 res.QN = Matrix<T>(M, R, zero);
110 res.UN = Matrix<T>(M, R, zero);
111 res.CN = Matrix<T>(M, R, zero);
112 if (M == 0) return res;
113
114 Matrix<T> vis(M, R, one);
115 if (!v.empty()) vis = v;
116 // Pure per-visit service times, S_pure = D / max(v, 1e-12).
117 Matrix<T> Sp(M, R, zero);
118 for (std::size_t i = 0; i < M; ++i)
119 for (std::size_t c = 0; c < R; ++c) {
120 const T den = vis(i, c) > tiny ? vis(i, c) : tiny;
121 Sp(i, c) = D(i, c) / den;
122 }
123
124 const std::vector<std::size_t> prods = plane_sizes(N);
125 const std::size_t total = population_count(N);
126 long Ntot = 0;
127 for (int n : N) Ntot += n;
128
129 auto nserv = [&](std::size_t i, std::size_t c) {
130 return S.cols() == 1 ? S(i, 0) : S(i, static_cast<std::size_t>(c));
131 };
132 // A station has class-independent demands when every class shares D(i,0).
133 std::vector<bool> classIndep(M, true);
134 for (std::size_t i = 0; i < M; ++i)
135 for (std::size_t c = 1; c < R; ++c)
136 if (D(i, c) != D(i, 0)) classIndep[i] = false;
137
138 std::vector<detail::SchmidtPc> kind(M, detail::SchmidtPc::None);
139 for (std::size_t i = 0; i < M; ++i) {
140 bool single = true;
141 for (std::size_t c = 0; c < (S.cols() == 1 ? std::size_t(1) : R); ++c)
142 if (nserv(i, c) != 1) single = false;
143 switch (sched[i]) {
145 break;
147 if (!single) kind[i] = detail::SchmidtPc::Scalar;
148 break;
150 if (classIndep[i]) {
151 if (!single) kind[i] = detail::SchmidtPc::Scalar;
152 } else {
153 kind[i] = detail::SchmidtPc::Vector;
154 }
155 break;
156 }
157 }
158
159 std::vector<Matrix<T>> Lq(M, Matrix<T>(R, total, zero));
160 std::vector<Matrix<T>> Pc(M);
161 for (std::size_t i = 0; i < M; ++i) {
162 if (kind[i] == detail::SchmidtPc::Scalar)
163 Pc[i] = Matrix<T>(static_cast<std::size_t>(1 + Ntot), total, zero);
164 else if (kind[i] == detail::SchmidtPc::Vector)
165 Pc[i] = Matrix<T>(total, total, zero);
166 if (kind[i] != detail::SchmidtPc::None) Pc[i](0, 0) = one; // Pr(0 | 0) = 1
167 }
168
169 // x[(i*R + c)*total + h] and w likewise.
170 std::vector<T> x(M * R * total, zero), w(M * R * total, zero);
171
172 std::vector<int> kvec(R, 0);
173 std::size_t hlast = 0;
174 bool more = true;
175 while (more) {
176 const std::size_t hk = pop_index(kvec, prods);
177 hlast = hk;
178 long kpop = 0;
179 for (int t : kvec) kpop += t;
180
181 for (std::size_t i = 0; i < M; ++i)
182 for (std::size_t c = 0; c < R; ++c) {
183 if (kvec[c] <= 0) continue;
184 const std::size_t hkc = hk - prods[c];
185 const int ns = nserv(i, c);
186 T& wi = w[(i * R + c) * total + hk];
187 if (sched[i] == SchedStrategy::INF) {
188 wi = D(i, c);
189 continue;
190 }
191 const bool vectorPc = kind[i] == detail::SchmidtPc::Vector;
192 if (!vectorPc || ns == 1) {
193 T qtot = zero;
194 for (std::size_t r = 0; r < R; ++r) qtot += Lq[i](r, hkc);
195 if (ns == 1) {
196 wi = D(i, c) * (one + qtot);
197 } else {
198 const T nsT = num_traits<T>::from_int(ns);
199 wi = D(i, c) / nsT * (one + qtot);
200 for (int j = 1; j <= ns - 1; ++j)
201 wi += num_traits<T>::from_int(ns - j) *
202 Pc[i](static_cast<std::size_t>(j - 1), hkc) * (D(i, c) / nsT);
203 }
204 } else {
205 // Class-dependent multiserver FCFS: sum over the joint state.
206 const T nsT = num_traits<T>::from_int(ns);
207 std::vector<int> nvec(R, 0);
208 bool more_n = true;
209 while (more_n) {
210 if (nvec[c] > 0) {
211 long nsum = 0;
212 for (int t : nvec) nsum += t;
213 const std::size_t hnc = pop_index(nvec, prods) - prods[c];
214 T Bcn = Sp(i, c);
215 if (nsum > ns) {
216 T sumVal = zero;
217 for (std::size_t r = 0; r < R; ++r)
218 sumVal += num_traits<T>::from_int(nvec[r]) * Sp(i, r);
219 const T den0 = nsT * num_traits<T>::from_int(nsum - 1);
220 const T den = den0 > tiny ? den0 : tiny;
221 Bcn += num_traits<T>::from_int(nsum - ns) / den *
222 (sumVal - Sp(i, c));
223 }
224 wi += Bcn * Pc[i](hnc, hkc);
225 }
226 more_n = next_pop(nvec, kvec);
227 }
228 }
229 }
230
231 for (std::size_t c = 0; c < R; ++c) {
232 T denom = zero;
233 for (std::size_t i = 0; i < M; ++i) denom += vis(i, c) * w[(i * R + c) * total + hk];
234 for (std::size_t i = 0; i < M; ++i)
235 x[(i * R + c) * total + hk] =
236 denom > zero ? vis(i, c) * num_traits<T>::from_int(kvec[c]) / denom : zero;
237 }
238
239 for (std::size_t i = 0; i < M; ++i) {
240 for (std::size_t c = 0; c < R; ++c)
241 Lq[i](c, hk) = x[(i * R + c) * total + hk] * w[(i * R + c) * total + hk];
242
243 if (kind[i] == detail::SchmidtPc::Scalar) {
244 // Pr(j busy | k) from Pr(j-1 busy | k - e_c).
245 const int s0 = nserv(i, 0);
246 const long jmax = sched[i] == SchedStrategy::PS
247 ? (s0 < kpop ? s0 : kpop)
248 : (s0 < kpop ? s0 : kpop) - 1;
249 for (long n = 1; n <= jmax; ++n)
250 for (std::size_t c = 0; c < R; ++c) {
251 if (kvec[c] <= 0) continue;
252 const std::size_t hkc = hk - prods[c];
253 Pc[i](static_cast<std::size_t>(n), hk) +=
254 D(i, c) / num_traits<T>::from_int(n) * x[(i * R + c) * total + hk] *
255 Pc[i](static_cast<std::size_t>(n - 1), hkc);
256 }
257 if (jmax >= 1) {
258 const long top = sched[i] == SchedStrategy::PS ? (s0 < kpop ? s0 : kpop)
259 : (s0 < kpop ? s0 : kpop);
260 T acc = zero;
261 for (long n = 1; n <= top; ++n) acc += Pc[i](static_cast<std::size_t>(n), hk);
262 const T p0 = one - acc;
263 Pc[i](0, hk) = p0 > epsT ? p0 : epsT;
264 }
265 } else if (kind[i] == detail::SchmidtPc::Vector) {
266 const int ns = nserv(i, 0);
267 const T nsT = num_traits<T>::from_int(ns);
268 T sumAll = zero;
269 std::vector<int> nvec(R, 0);
270 bool more_n = next_pop(nvec, kvec); // skip the zero vector
271 while (more_n) {
272 const std::size_t hn = pop_index(nvec, prods);
273 long nsum = 0;
274 for (int t : nvec) nsum += t;
275 T prob = zero;
276 for (std::size_t c = 0; c < R; ++c) {
277 if (nvec[c] <= 0 || kvec[c] <= 0) continue;
278 const std::size_t hnc = hn - prods[c];
279 const std::size_t hkc = hk - prods[c];
280 T Bcn = Sp(i, c);
281 if (nsum > 1) {
282 T sumVal = zero;
283 for (std::size_t r = 0; r < R; ++r)
284 sumVal += num_traits<T>::from_int(nvec[r]) * Sp(i, r);
285 const T den0 = nsT * num_traits<T>::from_int(nsum - 1);
286 const T den = den0 > tiny ? den0 : tiny;
287 Bcn += num_traits<T>::from_int(nsum - ns > 0 ? nsum - ns : 0) / den *
288 (sumVal - Sp(i, c));
289 }
290 prob += Bcn / num_traits<T>::from_int(nsum) *
291 x[(i * R + c) * total + hk] * Pc[i](hnc, hkc);
292 }
293 Pc[i](hn, hk) = prob;
294 sumAll += prob;
295 more_n = next_pop(nvec, kvec);
296 }
297 const T p0 = one - sumAll;
298 Pc[i](0, hk) = p0 > tiny ? p0 : tiny;
299 }
300 }
301
302 more = next_pop(kvec, N);
303 }
304
305 for (std::size_t c = 0; c < R; ++c) {
306 T tot = zero;
307 for (std::size_t i = 0; i < M; ++i) tot += w[(i * R + c) * total + hlast];
308 res.XN[c] = tot > zero ? num_traits<T>::from_int(N[c]) / tot : zero;
309 }
310 for (std::size_t i = 0; i < M; ++i)
311 for (std::size_t c = 0; c < R; ++c) {
312 res.UN(i, c) = D(i, c) * res.XN[c] / num_traits<T>::from_int(nserv(i, c));
313 res.CN(i, c) = w[(i * R + c) * total + hlast];
314 res.QN(i, c) = Lq[i](c, hlast);
315 }
316 return res;
317}
318
319/** Unit visit ratios, the MATLAB default. */
320template <class T>
321SchmidtResult<T> pfqn_schmidt(const Matrix<T>& D, const std::vector<int>& N, const Matrix<int>& S,
322 const std::vector<SchedStrategy>& sched) {
323 return pfqn_schmidt(D, N, S, sched, Matrix<T>());
324}
325
326} // namespace pfqn
327} // namespace line
328
329#endif // LINE_API_PFQN_SCHMIDT_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.
SchmidtResult< T > pfqn_schmidt(const Matrix< T > &D, const std::vector< int > &N, const Matrix< int > &S, const std::vector< SchedStrategy > &sched, const Matrix< T > &v)
Schmidt's MVA for closed networks with general scheduling disciplines and class-dependent multiserver...
std::size_t population_count(const std::vector< int > &N)
Number of population vectors n with 0 <= n <= N.
Definition population.h:38
std::vector< std::size_t > plane_sizes(const std::vector< int > &N)
Mixed-radix plane sizes: prods[r] = prod_{s<r} (N[s]+1).
Definition population.h:27
bool next_pop(std::vector< int > &n, const std::vector< int > &N)
Advance n to the next population vector in the lattice 0 <= n <= N, odometer order with the last clas...
Definition population.h:56
std::size_t pop_index(const std::vector< int > &n, const std::vector< std::size_t > &prods)
Index of n in the lattice, 0-based (MATLAB hashpop is 1-based).
Definition population.h:45
Number-type abstraction for the templated API port.
Scaffolding shared by the approximate-MVA family.
Population-vector enumeration and combinatorics.
Return value of pfqn_schmidt, mirroring [XN,QN,UN,CN].
std::vector< T > XN
(R) per-class throughput
Matrix< T > CN
(M x R) residence time
Matrix< T > UN
(M x R) utilization, D X / s
Matrix< T > QN
(M x R) mean queue length