LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
qsys_ldps_workload.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_LDPS_WORKLOAD_H
6#define LINE_API_QSYS_QSYS_LDPS_WORKLOAD_H
7
8/**
9 * @file
10 * @ingroup api_qsys
11 * Stationary distribution of the unfinished work in a single-stage
12 * load-dependent generalized-processor-sharing station with Poisson arrivals
13 * and blocking. Port of matlab/src/api/qsys/qsys_ldps_workload.m, which is the
14 * model of J.W. Cohen, "The multiple phase service network with generalized
15 * processor sharing", Acta Informatica 12, 245-284 (1979), Sect. 9.
16 *
17 * MODEL. Poisson arrivals of rate lambda; blocking capacity N, an arrival
18 * finding N requests present being lost without trace; each of x present
19 * requests accrues service at rate f(x), so the stage completes work at total
20 * rate x f(x); required service times iid with an absolutely continuous law B
21 * of finite mean beta. The port is parametrized, as the reference is, by the
22 * LINE load-dependent TOTAL rate scaling alpha(x) = x f(x), which is the
23 * argument of setLoadDependence at a PS station.
24 *
25 * Cohen's eqs. (9.1)-(9.3):
26 * P{psi_t < psi} = sum_{h=0}^{N} p_h Psi^{h*}(psi)
27 * p_h propto rho^h/prod_{k=1}^{h} alpha(k), rho = lambda beta
28 * Psi(psi) = int_0^psi (1 - B(v))/beta dv
29 * with Psi^{h*} the h-fold convolution and Psi^{0*} degenerate at zero, so the
30 * workload has an ATOM of size p_0 at the origin. Substituting f(k) = alpha(k)/k
31 * cancels the factorial that appears in Cohen's phi(h), leaving the familiar
32 * load-dependent birth-death form for p. The state probabilities depend on B
33 * only through beta; the workload depends on its shape only through the
34 * equilibrium residual law Psi.
35 *
36 * WHAT THE PORT TAKES INSTEAD OF A Distribution OBJECT. The reference takes a
37 * LINE Distribution and reads getMean, getSCV and evalCDF off it. The C++ port
38 * has no model layer, so it takes exactly those three things: the mean, the
39 * squared coefficient of variation (used only to size the default grid through
40 * the mean equilibrium residual life beta(1+SCV)/2) and the CDF as a callable.
41 * Nothing else of the Distribution interface is used by the reference either,
42 * so this is the same function with its dependency made explicit.
43 *
44 * ACCURACY. The convolutions are formed on a uniform grid with the TRAPEZOIDAL
45 * rule, not the rectangle rule that a bare convolution implies. The correction
46 * matters because the equilibrium density does not vanish at the origin,
47 * e(0) = 1/beta: without it each convolution over-counts by dt e(0) g_i, which
48 * accumulates over h and drives the mixture CDF above one. Convergence is
49 * second order in the step for an absolutely continuous B, the case Cohen
50 * assumes, and falls to first order when B has an atom so that 1 - B is
51 * discontinuous, Det being the extreme case; the reference measures 4.00x per
52 * grid doubling for Exp and about 2x for Det, and the port reproduces both
53 * regimes (see the tests).
54 *
55 * ARITHMETIC. Gated on num_traits<T>::has_transcendental. The birth-death
56 * weights are accumulated in LOGS, exactly as the reference does, so a large
57 * rho or a large N cannot overflow before normalization; that alone needs
58 * log/exp. The quadrature is a tolerance-free fixed grid, so it introduces no
59 * further requirement, but it does introduce a discretization error, which is
60 * why the grid size is an explicit argument rather than hidden.
61 *
62 * NOT the weighted GPS/DPS discipline of SchedStrategy.GPS: this formula has no
63 * per-class weights and does not represent them.
64 */
65
66#include <cmath>
67#include <cstddef>
68#include <functional>
69#include <vector>
70
72#include "line/num/number.h"
73#include "line/util/error.h"
74
75namespace line {
76namespace qsys {
77
78/**
79 * The three things Cohen's formula needs from the required-service-time law:
80 * its mean, its squared coefficient of variation, and its CDF.
81 */
82template <class T>
85 T scv;
86 std::function<T(const T&)> cdf;
87};
88
89/** Return value of qsys_ldps_workload, mirroring the three MATLAB outputs. */
90template <class T>
92 std::vector<T> F; ///< F(j) = P{psi <= t(j)}; F(0) = p(0) when t(0) = 0
93 std::vector<T> t; ///< grid at which F is reported
94 std::vector<T> p; ///< p(h) = P{x = h}, h = 0..N, the number in system
95};
96
97namespace ldps_detail {
98
99/**
100 * Convolution of two densities sampled on a uniform grid, trapezoidal rather
101 * than rectangular:
102 * (f*g)(t_i) ~ dt [ sum_{j=0}^{i} f_j g_{i-j} - (f_0 g_i + f_i g_0)/2 ].
103 */
104template <class T>
105std::vector<T> convtrap(const std::vector<T>& f, const std::vector<T>& g, const T& dt,
106 std::size_t n) {
107 const T zero = num_traits<T>::from_int(0);
108 const T half = num_traits<T>::from_rational(1, 2);
109 std::vector<T> c(n, zero);
110 for (std::size_t i = 0; i < n; ++i) {
111 T s = zero;
112 for (std::size_t j = 0; j <= i; ++j) s += f[j] * g[i - j];
113 c[i] = dt * (s - half * (f[0] * g[i] + f[i] * g[0]));
114 }
115 return c;
116}
117
118} // namespace ldps_detail
119
120/**
121 * Workload distribution of the load-dependent PS station with blocking.
122 *
123 * @param lambda Poisson arrival rate
124 * @param B required service time: mean, SCV and CDF
125 * @param alpha rate scaling alpha(n) = n f(n) for n = 1..N, at least N entries
126 * @param N blocking capacity
127 * @param t grid at which the CDF is wanted; empty for an automatic grid
128 * @param ngrid points of the internal uniform quadrature grid
129 */
130template <class T>
132 const std::vector<T>& alpha, std::size_t N,
133 const std::vector<T>& t, std::size_t ngrid) {
135 "qsys_ldps_workload accumulates the birth-death weights in logs");
136 using std::exp;
137 using std::log;
138 using std::sqrt;
139 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
140 const T two = num_traits<T>::from_int(2);
141
142 if (lambda <= zero)
143 throw InputError("qsys_ldps_workload: lambda must be a positive arrival rate");
144 if (B.mean <= zero)
145 throw InputError("qsys_ldps_workload: the service law must have a finite positive mean");
146 if (!B.cdf) throw InputError("qsys_ldps_workload: the service law has no CDF");
147 if (N < 1) throw InputError("qsys_ldps_workload: N must be a positive blocking capacity");
148 if (alpha.size() < N)
149 throw InputError(
150 "qsys_ldps_workload: alpha must supply the rate scaling for n = 1..N");
151 for (std::size_t k = 0; k < N; ++k)
152 if (alpha[k] <= zero)
153 throw InputError(
154 "qsys_ldps_workload: alpha(n) must be strictly positive, since every request in a "
155 "busy stage is served at a positive rate");
156 if (ngrid < 2) throw InputError("qsys_ldps_workload: ngrid must be at least 2");
157
158 // Stationary number in system, eq. (9.1), accumulated in logs.
159 const T beta = B.mean;
160 const T rho = lambda * beta;
161 std::vector<T> logw(N + 1, zero);
162 for (std::size_t h = 1; h <= N; ++h) logw[h] = logw[h - 1] + log(rho) - log(alpha[h - 1]);
163 T lmax = logw[0];
164 for (const T& v : logw)
165 if (v > lmax) lmax = v;
166 std::vector<T> p(N + 1);
167 T wsum = zero;
168 for (std::size_t h = 0; h <= N; ++h) {
169 p[h] = exp(logw[h] - lmax);
170 wsum += p[h];
171 }
172 for (T& v : p) v /= wsum;
173
174 // Grid. Psi has mean m1e, the mean equilibrium residual life of B, so the
175 // largest h that carries non-negligible mass sizes the support.
176 const T m1e = beta * (one + B.scv) / two;
177 if (m1e <= zero)
178 throw InputError(
179 "qsys_ldps_workload: the equilibrium residual service time needs a finite second "
180 "moment");
181 const bool userGrid = !t.empty();
182 T tmax;
183 if (userGrid) {
184 tmax = zero;
185 for (const T& v : t) {
186 if (v < zero) throw InputError("qsys_ldps_workload: t must be non-negative");
187 if (v > tmax) tmax = v;
188 }
189 if (tmax <= zero) tmax = m1e;
190 } else {
191 std::size_t hmax = 0;
192 const T floor_ = num_traits<T>::from_double(1e-12);
193 for (std::size_t h = 0; h <= N; ++h)
194 if (p[h] > floor_) hmax = h;
195 if (hmax < 1) hmax = 1;
196 const T hT = num_traits<T>::from_int(static_cast<long>(hmax));
197 tmax = m1e * (hT + num_traits<T>::from_int(8) * sqrt(hT));
198 const T floorT = num_traits<T>::from_int(8) * m1e;
199 if (tmax < floorT) tmax = floorT;
200 }
201
202 std::vector<T> tg(ngrid);
203 const T span = num_traits<T>::from_int(static_cast<long>(ngrid - 1));
204 for (std::size_t i = 0; i < ngrid; ++i)
205 tg[i] = tmax * num_traits<T>::from_int(static_cast<long>(i)) / span;
206 const T dt = tg[1] - tg[0];
207
208 // Equilibrium residual service density, eq. (9.3).
209 std::vector<T> e(ngrid);
210 for (std::size_t i = 0; i < ngrid; ++i) e[i] = (one - B.cdf(tg[i])) / beta;
211
212 // Workload distribution, eq. (9.2). The h = 0 term is degenerate at zero
213 // and contributes the atom p_0 across the whole non-negative grid.
214 std::vector<T> Fg(ngrid, p[0]);
215 std::vector<T> dens = e;
216 for (std::size_t h = 1; h <= N; ++h) {
217 if (h > 1) dens = ldps_detail::convtrap(dens, e, dt, ngrid);
218 const std::vector<T> C = detail::num_cumtrapz(tg, dens);
219 for (std::size_t i = 0; i < ngrid; ++i) Fg[i] += p[h] * C[i];
220 }
221
223 out.p = p;
224 if (userGrid) {
225 out.t = t;
226 out.F.resize(t.size());
227 for (std::size_t j = 0; j < t.size(); ++j) {
228 // grid interpolation rationale: see _kb/03-api-layer.md (cpp port notes: qsys)
229 const T x = t[j];
230 if (x >= tg[ngrid - 1]) {
231 out.F[j] = Fg[ngrid - 1];
232 continue;
233 }
234 const double pos = num_traits<T>::to_double(T(x / dt));
235 std::size_t i = static_cast<std::size_t>(pos);
236 if (i + 1 >= ngrid) i = ngrid - 2;
237 const T w = (x - tg[i]) / dt;
238 out.F[j] = Fg[i] + w * (Fg[i + 1] - Fg[i]);
239 }
240 } else {
241 out.t = tg;
242 out.F = Fg;
243 }
244 return out;
245}
246
247/** qsys_ldps_workload on the automatic grid with the reference default ngrid = 2001. */
248template <class T>
250 const std::vector<T>& alpha, std::size_t N) {
251 return qsys_ldps_workload(lambda, B, alpha, N, std::vector<T>(),
252 static_cast<std::size_t>(2001));
253}
254
255} // namespace qsys
256} // namespace line
257
258#endif // LINE_API_QSYS_QSYS_LDPS_WORKLOAD_H
InputError(const std::string &what)
Definition error.h:39
The exception types the port throws.
LdpsWorkloadResult< T > qsys_ldps_workload(const T &lambda, const WorkloadServiceLaw< T > &B, const std::vector< T > &alpha, std::size_t N, const std::vector< T > &t, std::size_t ngrid)
Workload distribution of the load-dependent PS station with blocking.
Number-type abstraction for the templated API port.
Adaptive quadrature for the qsys functions whose MATLAB originals call integral(),...
Return value of qsys_ldps_workload, mirroring the three MATLAB outputs.
std::vector< T > F
F(j) = P{psi <= t(j)}; F(0) = p(0) when t(0) = 0.
std::vector< T > p
p(h) = P{x = h}, h = 0..N, the number in system
std::vector< T > t
grid at which F is reported
The three things Cohen's formula needs from the required-service-time law: its mean,...
std::function< T(const T &)> cdf