LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
qsys_mg1_srpt.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_QSYS_QSYS_MG1_SRPT_H
6#define LINE_API_QSYS_QSYS_MG1_SRPT_H
7
8/**
9 * @file
10 * @ingroup api_qsys
11 * M/G/1 under SRPT (shortest remaining processing time), by the
12 * Schrage-Miller formula.
13 *
14 * Templated port of matlab/src/api/qsys/qsys_mg1_srpt.m, cross-checked against
15 * jar/src/main/java/jline/api/qsys/Qsys_mg1_srpt.java.
16 *
17 * For a job of size x, with f the mixture size density and Fbar its tail,
18 *
19 * rho(x) = lambda int_0^x t f(t) dt
20 * m2(x) = int_0^x t^2 f(t) dt
21 * E[W(x)] = lambda (m2(x) + x^2 Fbar(x)) / (2 (1-rho(x))^2)
22 * E[R(x)] = int_0^x dt/(1-rho(t))
23 * E[T(x)] = E[W(x)] + E[R(x)]
24 *
25 * and the class mean is int_0^inf E[T(x)] f_r(x) dx. Because E[T(x)] depends
26 * on the size alone -- SRPT is size-based, not class-based -- that integral is
27 * exact given the size laws.
28 *
29 * Each class is matched to (mean 1/mu_r, scv cs_r^2) by an exponential when
30 * cs_r = 1, a balanced-means two-phase hyperexponential when cs_r > 1, and a
31 * Tijms mixture of Erlang-(k-1)/Erlang-k when cs_r < 1. The integrals are
32 * evaluated by cumulative trapezoid quadrature on the same fixed grid MATLAB
33 * builds: 40 e-foldings of the slowest phase, and at least 20000 points, or
34 * 200 per unit of phase-rate spread. That grid, not the formula, sets the
35 * accuracy -- the trapezoid rule on a uniform grid of N points is O(N^-2), so
36 * the reference itself carries an error of order 1e-6 relative, and the port
37 * matches it point for point rather than integrating better.
38 *
39 * ARITHMETIC. exp, log and the trapezoid rule make this transcendental.
40 */
41
42#include <algorithm>
43#include <cstddef>
44#include <vector>
45
49#include "line/num/number.h"
50#include "line/util/error.h"
51
52namespace line {
53namespace qsys {
54
55namespace detail {
56
57/** Job-size law matched to a mean and an SCV, as in MATLAB's local srpt_fit. */
58template <class T>
59struct SrptFit {
60 enum Kind { EXPONENTIAL, HYPEREXP2, ERLANG_MIX } kind = EXPONENTIAL;
61 T rate; ///< EXPONENTIAL and ERLANG_MIX
62 T p; ///< branch probability for HYPEREXP2 and ERLANG_MIX
63 T r1, r2; ///< HYPEREXP2 phase rates
64 unsigned k = 0; ///< ERLANG_MIX larger shape
65 T rate_min, rate_max;
66};
67
68template <class T>
69SrptFit<T> srpt_fit(const T& mu, const T& cs) {
70 const T one = num_traits<T>::from_int(1);
71 const T two = num_traits<T>::from_int(2);
72 const T half = num_traits<T>::from_rational(1, 2);
73 const T c2 = cs * cs;
74 SrptFit<T> f;
75 if (num_abs(T(c2 - one)) < T(num_traits<T>::from_double(1e-9))) {
76 f.kind = SrptFit<T>::EXPONENTIAL;
77 f.rate = mu;
78 f.rate_min = mu;
79 f.rate_max = mu;
80 } else if (c2 > one) {
81 const T pr = half * (one + num_sqrt(T((c2 - one) / (c2 + one))));
82 f.kind = SrptFit<T>::HYPEREXP2;
83 f.p = pr;
84 f.r1 = two * pr * mu;
85 f.r2 = two * (one - pr) * mu;
86 f.rate_min = num_min(f.r1, f.r2);
87 f.rate_max = f.r1 < f.r2 ? f.r2 : f.r1;
88 } else {
89 const double inv = 1.0 / num_traits<T>::to_double(c2);
90 const unsigned k = static_cast<unsigned>(std::ceil(inv));
91 const T kt = num_traits<T>::from_int(static_cast<long>(k));
92 const T pr = (one / (one + c2)) * (kt * c2 - num_sqrt(T(kt * (one + c2) - kt * kt * c2)));
93 f.kind = SrptFit<T>::ERLANG_MIX;
94 f.k = k;
95 f.p = pr;
96 f.rate = (kt - pr) * mu; // mean_x = 1/mu, so (k-p)/mean_x = (k-p) mu
97 f.rate_min = f.rate;
98 f.rate_max = f.rate;
99 }
100 return f;
101}
102
103/** log(m!) by summation, standing in for MATLAB's gammaln(m+1). */
104template <class T>
105T log_factorial(unsigned m) {
106 T s = num_traits<T>::from_int(0);
107 for (unsigned j = 2; j <= m; ++j) {
108 using std::log;
109 s += log(num_traits<T>::from_int(static_cast<long>(j)));
110 }
111 return s;
112}
113
114/** Erlang-n density with the given rate, in log space as in MATLAB. */
115template <class T>
116T erlang_pdf(unsigned n, const T& rate, const T& x) {
117 const T zero = num_traits<T>::from_int(0);
118 if (n == 0) return zero;
119 const T t = rate * x;
120 const unsigned m = n - 1;
121 if (t <= zero) return m == 0 ? rate : zero;
122 using std::log;
123 const T logp = num_traits<T>::from_int(static_cast<long>(m)) * log(t) - t - log_factorial<T>(m);
124 return rate * num_exp(logp);
125}
126
127/** Erlang-n complementary CDF, the upper Poisson tail, in log space. */
128template <class T>
129T erlang_tail(unsigned n, const T& rate, const T& x) {
130 const T zero = num_traits<T>::from_int(0);
131 if (n == 0) return zero;
132 const T t = rate * x;
133 if (t <= zero) return num_traits<T>::from_int(1);
134 using std::log;
135 T y = zero;
136 const T logt = log(t);
137 for (unsigned j = 0; j < n; ++j)
138 y += num_exp(T(num_traits<T>::from_int(static_cast<long>(j)) * logt - t -
139 log_factorial<T>(j)));
140 return y;
141}
142
143template <class T>
144T srpt_pdf(const SrptFit<T>& f, const T& x) {
145 const T one = num_traits<T>::from_int(1);
146 switch (f.kind) {
147 case SrptFit<T>::EXPONENTIAL:
148 return f.rate * num_exp(T(-f.rate * x));
149 case SrptFit<T>::HYPEREXP2:
150 return f.p * f.r1 * num_exp(T(-f.r1 * x)) +
151 (one - f.p) * f.r2 * num_exp(T(-f.r2 * x));
152 default:
153 return f.p * erlang_pdf(f.k - 1, f.rate, x) +
154 (one - f.p) * erlang_pdf(f.k, f.rate, x);
155 }
156}
157
158template <class T>
159T srpt_tail(const SrptFit<T>& f, const T& x) {
160 const T one = num_traits<T>::from_int(1);
161 switch (f.kind) {
162 case SrptFit<T>::EXPONENTIAL:
163 return num_exp(T(-f.rate * x));
164 case SrptFit<T>::HYPEREXP2:
165 return f.p * num_exp(T(-f.r1 * x)) + (one - f.p) * num_exp(T(-f.r2 * x));
166 default:
167 return f.p * erlang_tail(f.k - 1, f.rate, x) +
168 (one - f.p) * erlang_tail(f.k, f.rate, x);
169 }
170}
171
172} // namespace detail
173
174/**
175 * @brief M/G/1 under SRPT (shortest remaining processing time), by the
176 * Schrage-Miller formula.
177 *
178 * @param lambda per-class arrival rates
179 * @param mu per-class service rates
180 * @param cs per-class coefficients of variation of the service time
181 */
182template <class T>
183Mg1DisciplineResult<T> qsys_mg1_srpt(const std::vector<T>& lambda, const std::vector<T>& mu,
184 const std::vector<T>& cs) {
186 "qsys_mg1_srpt requires transcendental arithmetic");
187 detail::mg1_discipline_check("qsys_mg1_srpt", lambda, mu, cs);
188 const std::size_t K = lambda.size();
189 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
190 const T two = num_traits<T>::from_int(2);
191
192 T lambda_total = zero;
193 for (const T& v : lambda) lambda_total += v;
194 std::vector<T> p(K);
195 for (std::size_t i = 0; i < K; ++i) p[i] = lambda[i] / lambda_total;
196
197 std::vector<detail::SrptFit<T>> fits;
198 fits.reserve(K);
199 T rate_min = zero, rate_max = zero;
200 for (std::size_t r = 0; r < K; ++r) {
201 fits.push_back(detail::srpt_fit(mu[r], cs[r]));
202 if (r == 0 || fits[r].rate_min < rate_min) rate_min = fits[r].rate_min;
203 if (r == 0 || fits[r].rate_max > rate_max) rate_max = fits[r].rate_max;
204 }
205 if (rate_min <= zero) throw NumericError("qsys_mg1_srpt: degenerate phase rate");
206
207 // Grid: 40 e-foldings of the slowest phase, 200 points per rate ratio.
208 const T xmax = num_traits<T>::from_int(40) / rate_min;
209 const double ratio = num_traits<T>::to_double(rate_max) / num_traits<T>::to_double(rate_min);
210 std::size_t N = static_cast<std::size_t>(std::ceil(200.0 * ratio));
211 if (N < 20000u) N = 20000u;
212 if (N > 2000000u) N = 2000000u;
213
214 std::vector<T> x(N + 1), fmix(N + 1, zero), Fbar(N + 1, zero);
215 for (std::size_t i = 0; i <= N; ++i)
216 x[i] = xmax * num_traits<T>::from_int(static_cast<long>(i)) /
217 num_traits<T>::from_int(static_cast<long>(N));
218 for (std::size_t r = 0; r < K; ++r)
219 for (std::size_t i = 0; i <= N; ++i) {
220 fmix[i] += p[r] * detail::srpt_pdf(fits[r], x[i]);
221 Fbar[i] += p[r] * detail::srpt_tail(fits[r], x[i]);
222 }
223
224 std::vector<T> xf(N + 1), x2f(N + 1);
225 for (std::size_t i = 0; i <= N; ++i) {
226 xf[i] = x[i] * fmix[i];
227 x2f[i] = x[i] * x[i] * fmix[i];
228 }
229 std::vector<T> rho_x = detail::num_cumtrapz(x, xf);
230 for (T& v : rho_x) v *= lambda_total;
231 const std::vector<T> m2_x = detail::num_cumtrapz(x, x2f);
232
233 // Guard the (1-rho(x)) factors; rho(x) -> rho < 1 as x -> inf.
234 const T floor_ = T(num_traits<T>::from_double(1e-12));
235 std::vector<T> denom(N + 1), invden(N + 1), ET(N + 1);
236 for (std::size_t i = 0; i <= N; ++i) {
237 const T d = one - rho_x[i];
238 denom[i] = d > floor_ ? d : floor_;
239 invden[i] = one / denom[i];
240 }
241 const std::vector<T> Res = detail::num_cumtrapz(x, invden);
242 for (std::size_t i = 0; i <= N; ++i)
243 ET[i] = lambda_total * (m2_x[i] + x[i] * x[i] * Fbar[i]) / (two * denom[i] * denom[i]) +
244 Res[i];
245
247 r.W.assign(K, zero);
248 std::vector<T> integrand(N + 1);
249 for (std::size_t c = 0; c < K; ++c) {
250 for (std::size_t i = 0; i <= N; ++i) integrand[i] = ET[i] * detail::srpt_pdf(fits[c], x[i]);
251 r.W[c] = detail::num_trapz(x, integrand);
252 }
253 r.rhohat = detail::mg1_discipline_rhohat(lambda, r.W);
254 return r;
255}
256
257} // namespace qsys
258} // namespace line
259
260#endif // LINE_API_QSYS_QSYS_MG1_SRPT_H
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
Mg1DisciplineResult< T > qsys_mg1_srpt(const std::vector< T > &lambda, const std::vector< T > &mu, const std::vector< T > &cs)
M/G/1 under SRPT (shortest remaining processing time), by the Schrage-Miller formula.
T num_abs(const T &v)
Definition number.h:172
Number-type abstraction for the templated API port.
M/G/1 under SETF (shortest elapsed time first), the non-preemptive counterpart of FB/LAS.
Adaptive quadrature for the qsys functions whose MATLAB originals call integral(),...
Shared return type and arithmetic helpers for the templated qsys port.
std::vector< T > W
per-class mean response time
T rhohat
Q/(1+Q) with Q = sum_k lambda_k W_k.