LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_schmidt_ext.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_EXT_H
6#define LINE_API_PFQN_SCHMIDT_EXT_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Extended Schmidt MVA with queue-aware alpha corrections.
12 *
13 * Templated port of matlab/src/api/pfqn/pfqn_schmidt_ext.m, cross-checked
14 * against the second half of jar/src/main/java/jline/api/pfqn/mva/
15 * Pfqn_schmidt_amva.java (that file carries both the plain and the extended
16 * recursion). The Java entry point takes SERVICE RATES and inverts them to
17 * demands internally, where MATLAB and this port take demands; that is an API
18 * divergence, not an algorithmic one. Reference:
19 * R. Schmidt, "An approximate MVA algorithm for exponential, class-dependent
20 * multiple server stations", Performance Evaluation 29(4), 1997.
21 *
22 * The population recursion is the one of pfqn_schmidt. What the extension adds
23 * is the mean service time B_c(n) used at a class-dependent multiserver FCFS
24 * station. Plain Schmidt weights the composition n by the demands themselves,
25 *
26 * B_c(n) = D(i,c) + max(0, |n| - s) / (s (|n| - 1)) (sum_t n_t D(i,t) - D(i,c));
27 *
28 * the extension replaces the second term by max(0, |n| - s) times a mean
29 * INTERDEPARTURE time read off an auxiliary solve. For each class r a tagged
30 * single-job class R+1 is appended at station i with that station's class-r
31 * demand, the population is dropped to N - e_r, plain pfqn_schmidt is run on
32 * the (R+1)-class model, and its utilizations u give
33 *
34 * alpha(i) = sum_{s<=R} u(i,s) - u(i,R+1),
35 * 1/interdep = s sum_{s: D(i,s)>0} (u(i,s)/alpha(i)) / D(i,s),
36 *
37 * i.e. a demand-weighted harmonic mean over the classes actually competing for
38 * the servers, with the tagged job's own utilization removed. That is the
39 * "queue-aware" correction: the departure rate seen by a waiting job reflects
40 * the class mix at the station rather than its own demand.
41 *
42 * Reference behaviour preserved verbatim: the alphas are computed only for
43 * FCFS stations whose demands are class dependent, and used only where
44 * N_c > 1; the class-independent multiserver marginal is the binomial form of
45 * the reference, not the recursive one of plain Schmidt; the idle-state
46 * probability is floored at 1e-12 (2.2e-16 for the PS branch); and the servers
47 * used in the queue-length update block are read with the class index left
48 * over from the preceding loop, which for an (M x R) server matrix is the LAST
49 * class. That last item is a reference quirk, reproduced because it selects
50 * the numbers the reference produces; it is inert whenever the server counts
51 * do not vary by class.
52 *
53 * The extra SchedStrategy.FCFS that the reference appends to `sched` before
54 * the auxiliary solve is dropped here: it lengthens the vector to M + 1 for an
55 * M-station model and pfqn_schmidt never reads past M, so it is inert.
56 *
57 * The auxiliary solve widens the model to R + 1 classes, so a per-class server
58 * matrix cannot be carried into it; the reference indexes S(ist,c) with
59 * c = R + 1 and would raise an out-of-bounds error. This port therefore
60 * requires a per-station (M x 1) server vector whenever any alpha is needed,
61 * and says so rather than inventing a widening rule.
62 *
63 * Arithmetic: INEXACT BY CONSTRUCTION. The correction is an approximation
64 * whose alpha factors come from an auxiliary approximate solve, and the
65 * marginal probabilities use non-integer binomial powers; it is gated on
66 * has_transcendental accordingly. Plain pfqn_schmidt, being a finite rational
67 * recursion, stays instantiable at Rational and is not gated.
68 */
69
70#include <cstddef>
71#include <vector>
72
75#include "line/num/number.h"
76#include "line/util/error.h"
77#include "line/util/matrix.h"
79
80namespace line {
81namespace pfqn {
82
83/** Return value of pfqn_schmidt_ext, mirroring [XN,QN,UN,CN]. */
84template <class T>
86 std::vector<T> XN; ///< (R) per-class throughput
87 Matrix<T> QN; ///< (M x R) mean queue length
88 Matrix<T> UN; ///< (M x R) utilization, D X / s
89 Matrix<T> CN; ///< (M x R) residence time
90};
91
92namespace detail {
93
94/** matlab getBcn: the plain Schmidt composition-weighted mean service time. */
95template <class T>
96T schmidt_ext_bcn(const Matrix<T>& D, std::size_t i, std::size_t c, const std::vector<int>& nvec,
97 int ns) {
98 T bcn = D(i, c);
99 long nsum = 0;
100 for (int t : nvec) nsum += t;
101 if (nsum > 1) {
102 const T eps = num_traits<T>::from_double(1e-12);
103 T sumVal = num_traits<T>::from_int(0);
104 for (std::size_t t = 0; t < nvec.size(); ++t)
105 sumVal += num_traits<T>::from_int(nvec[t]) * D(i, t);
106 const T num = num_traits<T>::from_int(nsum - ns > 0 ? nsum - ns : 0);
107 const T den0 = num_traits<T>::from_int(ns * (nsum - 1));
108 const T den = den0 > eps ? den0 : eps;
109 bcn += num / den * (sumVal - D(i, c));
110 }
111 return bcn;
112}
113
114/** matlab getBcnExt: the queue-aware mean interdeparture correction. */
115template <class T>
116T schmidt_ext_bcn_ext(const Matrix<T>& u, const Matrix<T>& D, std::size_t i, std::size_t c,
117 const std::vector<int>& nvec, std::size_t C, int ns) {
118 const T zero = num_traits<T>::from_int(0);
119 T nonPinned = zero;
120 for (std::size_t s = 0; s < C; ++s) nonPinned += u(i, s);
121 nonPinned -= u(i, C); // the tagged class sits in column C (0-based)
122
123 T weighted = zero;
124 if (nonPinned > zero)
125 for (std::size_t s = 0; s < C; ++s)
126 if (D(i, s) > zero) weighted += (u(i, s) / nonPinned) / D(i, s);
127
128 const T interdep =
129 weighted > zero ? T(num_traits<T>::from_int(1) / (num_traits<T>::from_int(ns) * weighted))
130 : zero;
131 T bcn = D(i, c);
132 long nsum = 0;
133 for (int t : nvec) nsum += t;
134 if (nsum > 1) bcn += num_traits<T>::from_int(nsum - ns > 0 ? nsum - ns : 0) * interdep;
135 return bcn;
136}
137
138} // namespace detail
139
140/**
141 * @brief Extended Schmidt MVA with queue-aware alpha corrections.
142 *
143 * @param D (M x R) service demands
144 * @param N (R) population per class
145 * @param S (M x 1) or (M x R) server counts; (M x 1) is required when any
146 * class-dependent FCFS multiserver station needs an alpha
147 * @param sched (M) scheduling discipline per station
148 */
149template <class T>
150SchmidtExtResult<T> pfqn_schmidt_ext(const Matrix<T>& D, const std::vector<int>& N,
151 const Matrix<int>& S,
152 const std::vector<SchedStrategy>& sched) {
154 "pfqn_schmidt_ext requires transcendental arithmetic: its alpha correction is "
155 "an approximation drawn from an auxiliary approximate solve, and its marginal "
156 "probabilities use non-integer binomial powers");
157
158 const std::size_t M = D.rows();
159 const std::size_t R = N.size();
160 if (!D.empty() && D.cols() != R)
161 throw InputError("pfqn_schmidt_ext: D and N disagree on the class count");
162 if (sched.size() != M) throw InputError("pfqn_schmidt_ext: sched has the wrong station count");
163 if (S.rows() != M || (S.cols() != R && S.cols() != 1))
164 throw InputError("pfqn_schmidt_ext: server-count matrix has the wrong shape");
165 for (int n : N)
166 if (n < 0) throw InputError("pfqn_schmidt_ext: negative population");
167
168 const T zero = num_traits<T>::from_int(0);
169 const T one = num_traits<T>::from_int(1);
170 const T tiny = num_traits<T>::from_double(1e-12);
171 const T epsT = num_traits<T>::from_double(2.220446049250313e-16);
172
174 res.XN.assign(R, zero);
175 res.QN = Matrix<T>(M, R, zero);
176 res.UN = Matrix<T>(M, R, zero);
177 res.CN = Matrix<T>(M, R, zero);
178 if (M == 0 || R == 0) return res;
179
180 const auto nserv = [&](std::size_t i, std::size_t c) {
181 return S.cols() == 1 ? S(i, 0) : S(i, c);
182 };
183 std::vector<bool> classIndep(M, true);
184 for (std::size_t i = 0; i < M; ++i)
185 for (std::size_t c = 1; c < R; ++c)
186 if (D(i, c) != D(i, 0)) classIndep[i] = false;
187
188 // ---- alphas: one auxiliary Schmidt solve per class-dependent FCFS pair --
189 std::vector<Matrix<T>> alphas(M * R);
190 for (std::size_t i = 0; i < M; ++i) {
191 if (sched[i] != SchedStrategy::FCFS || classIndep[i]) continue;
192 if (S.cols() != 1)
193 throw InputError(
194 "pfqn_schmidt_ext: a class-dependent FCFS station needs the alpha correction, "
195 "whose auxiliary solve widens the model to R+1 classes; pass a per-station "
196 "(M x 1) server vector");
197 for (std::size_t r = 0; r < R; ++r) {
198 Matrix<T> Dmod(M, R + 1, zero);
199 std::vector<int> Nmod(R + 1, 0);
200 for (std::size_t k = 0; k < R; ++k) {
201 Nmod[k] = k == r ? N[k] - 1 : N[k];
202 for (std::size_t j = 0; j < M; ++j) {
203 Dmod(j, k) = D(j, k);
204 Dmod(j, R) = j == i ? D(j, r) : zero;
205 }
206 }
207 Nmod[R] = 1;
208 if (Nmod[r] < 0)
209 throw InputError(
210 "pfqn_schmidt_ext: a class-dependent FCFS station needs the alpha correction "
211 "at N - e_r, which is negative for an empty class");
212 alphas[i * R + r] = pfqn_schmidt(Dmod, Nmod, S, sched).UN;
213 }
214 }
215
216 // ---- population recursion ---------------------------------------------
217 const std::vector<std::size_t> prods = plane_sizes(N);
218 const std::size_t total = population_count(N);
219 long Ntot = 0;
220 for (int n : N) Ntot += n;
221
222 std::vector<detail::SchmidtPc> kind(M, detail::SchmidtPc::None);
223 for (std::size_t i = 0; i < M; ++i) {
224 bool single = true;
225 for (std::size_t c = 0; c < (S.cols() == 1 ? std::size_t(1) : R); ++c)
226 if (nserv(i, c) != 1) single = false;
227 switch (sched[i]) {
229 break;
231 if (!single) kind[i] = detail::SchmidtPc::Scalar;
232 break;
234 if (classIndep[i]) {
235 if (!single) kind[i] = detail::SchmidtPc::Scalar;
236 } else {
237 kind[i] = detail::SchmidtPc::Vector;
238 }
239 break;
240 }
241 }
242
243 std::vector<Matrix<T>> Lq(M, Matrix<T>(R, total, zero));
244 std::vector<Matrix<T>> Pc(M);
245 for (std::size_t i = 0; i < M; ++i) {
246 if (kind[i] == detail::SchmidtPc::Scalar)
247 Pc[i] = Matrix<T>(static_cast<std::size_t>(1 + Ntot), total, zero);
248 else if (kind[i] == detail::SchmidtPc::Vector)
249 Pc[i] = Matrix<T>(total, total, zero);
250 if (kind[i] != detail::SchmidtPc::None) Pc[i](0, 0) = one;
251 }
252
253 Matrix<T> xtab(R, total, zero);
254 std::vector<T> w(M * R * total, zero);
255
256 std::vector<int> kvec(R, 0);
257 std::size_t hlast = 0;
258 bool more = true;
259 while (more) {
260 const std::size_t hk = pop_index(kvec, prods);
261 hlast = hk;
262 long kpop = 0;
263 for (int t : kvec) kpop += t;
264
265 for (std::size_t i = 0; i < M; ++i)
266 for (std::size_t c = 0; c < R; ++c) {
267 if (kvec[c] <= 0) continue;
268 const std::size_t hkc = hk - prods[c];
269 const int ns = nserv(i, c);
270 T& wi = w[(i * R + c) * total + hk];
271 if (sched[i] == SchedStrategy::INF) {
272 wi = D(i, c);
273 continue;
274 }
275 const bool vectorPc = kind[i] == detail::SchmidtPc::Vector;
276 if (!vectorPc || ns == 1) {
277 T qtot = zero;
278 for (std::size_t r = 0; r < R; ++r) qtot += Lq[i](r, hkc);
279 if (ns == 1) {
280 wi = D(i, c) * (one + qtot);
281 } else {
282 const T nsT = num_traits<T>::from_int(ns);
283 wi = D(i, c) / nsT * (one + qtot);
284 for (int j = 1; j <= ns - 1; ++j)
285 wi += num_traits<T>::from_int(ns - j) *
286 Pc[i](static_cast<std::size_t>(j - 1), hkc) * (D(i, c) / nsT);
287 }
288 } else {
289 std::vector<int> nvec(R, 0);
290 bool more_n = true;
291 while (more_n) {
292 if (nvec[c] > 0) {
293 const std::size_t hnc = pop_index(nvec, prods) - prods[c];
294 const Matrix<T>& al = alphas[i * R + c];
295 const T Bcn = (N[c] > 1 && !al.empty())
296 ? detail::schmidt_ext_bcn_ext(al, D, i, c, nvec, R, ns)
297 : detail::schmidt_ext_bcn(D, i, c, nvec, ns);
298 wi += Bcn * Pc[i](hnc, hkc);
299 }
300 more_n = next_pop(nvec, kvec);
301 }
302 }
303 }
304
305 for (std::size_t c = 0; c < R; ++c) {
306 T denom = zero;
307 for (std::size_t i = 0; i < M; ++i) denom += w[(i * R + c) * total + hk];
308 xtab(c, hk) = denom > zero ? T(num_traits<T>::from_int(kvec[c]) / denom) : zero;
309 }
310
311 for (std::size_t i = 0; i < M; ++i) {
312 for (std::size_t c = 0; c < R; ++c)
313 Lq[i](c, hk) = xtab(c, hk) * w[(i * R + c) * total + hk];
314
315 // The reference reads the server count with the class index left
316 // over from the loop above, which is the last class.
317 const int nsLast = nserv(i, R - 1);
318 const int s0 = nserv(i, 0);
319
320 if (sched[i] == SchedStrategy::PS) {
321 if (nsLast > 1 && kind[i] == detail::SchmidtPc::Scalar) {
322 const long top = s0 < kpop ? s0 : kpop;
323 for (long n = 1; n <= top; ++n)
324 for (std::size_t c = 0; c < R; ++c) {
325 if (kvec[c] <= 0) continue;
326 const std::size_t hkc = hk - prods[c];
327 Pc[i](static_cast<std::size_t>(n), hk) +=
328 D(i, c) / num_traits<T>::from_int(n) * xtab(c, hk) *
329 Pc[i](static_cast<std::size_t>(n - 1), hkc);
330 }
331 if (top >= 1) {
332 T acc = zero;
333 for (long n = 1; n <= top; ++n)
334 acc += Pc[i](static_cast<std::size_t>(n), hk);
335 const T p0 = one - acc;
336 Pc[i](0, hk) = p0 > epsT ? p0 : epsT;
337 }
338 }
339 } else if (sched[i] == SchedStrategy::FCFS) {
340 if (kind[i] == detail::SchmidtPc::Vector) {
341 T sumAll = zero;
342 std::vector<int> nvec(R, 0);
343 bool more_n = next_pop(nvec, kvec); // skip the zero vector
344 while (more_n) {
345 const std::size_t hn = pop_index(nvec, prods);
346 long nsum = 0;
347 for (int t : nvec) nsum += t;
348 T prob = zero;
349 for (std::size_t r = 0; r < R; ++r) {
350 if (nvec[r] <= 0) continue;
351 const std::size_t hnc = hn - prods[r];
352 const std::size_t hkc = hk - prods[r];
353 const int nsr = nserv(i, r);
354 const Matrix<T>& al = alphas[i * R + r];
355 const T Bcn =
356 (N[r] > 1 && !al.empty())
357 ? detail::schmidt_ext_bcn_ext(al, D, i, r, nvec, R, nsr)
358 : detail::schmidt_ext_bcn(D, i, r, nvec, nsr);
359 prob += Bcn / num_traits<T>::from_int(nsum) * xtab(r, hk) *
360 Pc[i](hnc, hkc);
361 }
362 Pc[i](hn, hk) = prob;
363 sumAll += prob;
364 more_n = next_pop(nvec, kvec);
365 }
366 const T p0 = one - sumAll;
367 Pc[i](0, hk) = p0 > tiny ? p0 : tiny;
368 } else if (nsLast > 1 && kind[i] == detail::SchmidtPc::Scalar) {
369 // Class-independent multiserver: binomial marginal.
370 long Kj = 0;
371 for (std::size_t r = 0; r < R; ++r)
372 if (D(i, r) > zero) Kj += N[r];
373 T meanQ = zero;
374 for (std::size_t r = 0; r < R; ++r) meanQ += Lq[i](r, hk);
375 const long top = (nsLast < kpop ? nsLast : kpop) - 1;
376 for (long n = 1; n <= top; ++n) {
377 if (Kj <= 0 || n > Kj) continue;
378 const T frac = meanQ / num_traits<T>::from_int(Kj);
379 Pc[i](static_cast<std::size_t>(n), hk) =
380 num_nck<T>(static_cast<int>(Kj), static_cast<int>(n)) *
381 num_pow_int(frac, static_cast<unsigned>(n)) *
382 num_pow_int(T(one - frac), static_cast<unsigned>(Kj - n));
383 }
384 T sum1 = zero, sum2 = zero;
385 for (std::size_t r = 0; r < R; ++r) sum1 += D(i, r) * xtab(r, hk);
386 for (long n = 0; n <= nsLast - 1; ++n)
387 sum2 += num_traits<T>::from_int(nsLast - n) *
388 Pc[i](static_cast<std::size_t>(n), hk);
389 const T p0 = one - (sum1 + sum2) / num_traits<T>::from_int(nsLast);
390 Pc[i](0, hk) = p0 > tiny ? p0 : tiny;
391 }
392 }
393 }
394
395 more = next_pop(kvec, N);
396 }
397
398 for (std::size_t c = 0; c < R; ++c) res.XN[c] = xtab(c, hlast);
399 for (std::size_t i = 0; i < M; ++i)
400 for (std::size_t c = 0; c < R; ++c) {
401 res.UN(i, c) = D(i, c) * res.XN[c] / num_traits<T>::from_int(nserv(i, 0));
402 res.CN(i, c) = w[(i * R + c) * total + hlast];
403 res.QN(i, c) = Lq[i](c, hlast);
404 }
405 return res;
406}
407
408} // namespace pfqn
409} // namespace line
410
411#endif // LINE_API_PFQN_SCHMIDT_EXT_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.
SchmidtExtResult< T > pfqn_schmidt_ext(const Matrix< T > &D, const std::vector< int > &N, const Matrix< int > &S, const std::vector< SchedStrategy > &sched)
Extended Schmidt MVA with queue-aware alpha corrections.
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
T num_pow_int(const T &base, unsigned e)
Integer power, valid in any field (no transcendental requirement).
Definition number.h:192
T num_nck(int n, int k)
Binomial coefficient as a value of T, by the Pascal recurrence.
Definition population.h:87
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.
Schmidt's MVA for closed networks with general scheduling disciplines and class-dependent multiserver...
Population-vector enumeration and combinatorics.
Return value of pfqn_schmidt_ext, mirroring [XN,QN,UN,CN].
Matrix< T > UN
(M x R) utilization, D X / s
Matrix< T > CN
(M x R) residence time
Matrix< T > QN
(M x R) mean queue length
std::vector< T > XN
(R) per-class throughput