LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
lossn_kr.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_LOSSN_LOSSN_KR_H
6#define LINE_API_LOSSN_LOSSN_KR_H
7
8/**
9 * @file
10 * @ingroup api_lossn
11 * The multirate single-link loss system (stochastic knapsack): exact blocking
12 * by the Kaufman-Roberts recursion, dimensioning against per-class blocking
13 * targets, and the critical-loading normal approximation.
14 *
15 * Templated ports of matlab/src/api/lossn/lossn_kr.m,
16 * lossn_kr_dim.m and lossn_kr_asy.m.
17 *
18 * A single link of C bandwidth units is offered K classes of calls. Class k
19 * arrives as a Poisson process with offered load rho_k (arrival rate times
20 * mean holding time) and holds b_k units for the whole call; a call that finds
21 * fewer than b_k free units is lost. The occupancy distribution
22 * q(n) = P(n units busy), n = 0..C, satisfies the Kaufman-Roberts recursion
23 *
24 * n q(n) = sum_k b_k rho_k q(n - b_k), q(n) = 0 for n < 0,
25 *
26 * and a class-k call is blocked when more than C - b_k units are busy,
27 * B_k = sum_{n = C-b_k+1}^{C} q(n).
28 *
29 * ARITHMETIC. The recursion and the dimensioning search are rational (the
30 * 1e250 rescale only compares a double image of the running term), so
31 * lossn_kr and lossn_kr_dim instantiate at every
32 * arithmetic. The critical-loading approximation evaluates the normal density
33 * and distribution function and is gated on num_traits<T>::has_transcendental.
34 *
35 * Reference: J. S. Kaufman, "Blocking in a Shared Resource Environment", IEEE
36 * Trans. Commun. 29(10), 1981. J. W. Roberts, "A Service System with
37 * Heterogeneous User Requirements", 1981. K. W. Ross, "Multiservice Loss Models
38 * for Broadband Telecommunication Networks", Springer, 1995, Sec. 2.2-2.7.
39 * M. I. Reiman, "A critically loaded multirate link with trunk reservation",
40 * Queueing Systems 9, 1991.
41 */
42
43#include <algorithm>
44#include <cmath>
45#include <cstddef>
46#include <string>
47#include <vector>
48
49#include "line/num/number.h"
50#include "line/util/error.h"
51
52namespace line {
53namespace lossn {
54
55/** Result of lossn_kr. */
56template <class T>
57struct KrResult {
58 std::vector<T> q; ///< occupancy distribution, q[n] = P(n units busy), n = 0..C
59 std::vector<T> B; ///< blocking probability of class k
60 std::vector<T> QLen; ///< mean number of class-k calls in progress, rho_k (1 - B_k)
61};
62
63/** Result of lossn_kr_dim. */
64template <class T>
66 long C = 0; ///< smallest capacity with B_k(C) <= Btarget_k for all k
67 std::vector<T> B; ///< blocking probabilities at that capacity
68};
69
70/** Result of lossn_kr_asy. */
71template <class T>
73 std::vector<T> B; ///< approximate blocking probability of class k
74 T alpha; ///< normalized spare capacity (C - sum b rho)/sqrt(C)
75 T sigma; ///< normalized occupancy standard deviation
76 T delta; ///< blocking per unit of call size, times sqrt(C)
77};
78
79namespace detail {
80
81/** The shared argument checks of the three functions: sizes and signs of rho and b. */
82template <class T>
83void kr_check_rho_b(const char* fn, const std::vector<T>& rho, const std::vector<T>& b) {
84 if (b.size() != rho.size())
85 throw InputError(std::string(fn) + ": b has " + std::to_string(b.size()) +
86 " entries but rho has " + std::to_string(rho.size()) + ".");
87 const T zero = num_traits<T>::from_int(0);
88 for (const T& r : rho)
89 if (r < zero) throw InputError(std::string(fn) + ": rho must be nonnegative.");
90}
91
92/** b checked to be nonnegative integers within 1e-9, then rounded. */
93template <class T>
94std::vector<long> kr_integral_b(const char* fn, const std::vector<T>& b) {
95 std::vector<long> out(b.size(), 0);
96 for (std::size_t k = 0; k < b.size(); ++k) {
97 const double x = num_traits<T>::to_double(b[k]);
98 if (!(x >= 0.0) || std::fabs(x - std::round(x)) > 1e-9)
99 throw InputError(std::string(fn) + ": b must contain nonnegative integers.");
100 out[k] = static_cast<long>(std::llround(x));
101 }
102 return out;
103}
104
105/** min(1, x), as MATLAB caps the blocking sums. */
106template <class T>
107T kr_cap1(const T& x) {
108 const T one = num_traits<T>::from_int(1);
109 return x > one ? one : x;
110}
111
112/**
113 * phi(x)/Phi(x). For x far in the lower tail both factors underflow, so the
114 * ratio is taken from the asymptotic Mills-ratio series instead.
115 */
116template <class T>
117T kr_normal_hazard(const T& x) {
118 using std::erfc;
119 using std::exp;
120 using std::sqrt;
121 if (x >= num_traits<T>::from_int(-35)) {
122 const T two = num_traits<T>::from_int(2);
123 const T pi = num_traits<T>::from_double(3.14159265358979323846);
124 return T(exp(T(-(x * x) / two)) / sqrt(T(two * pi)) /
125 T(num_traits<T>::from_double(0.5) * erfc(T(-x / sqrt(two)))));
126 }
127 const T one = num_traits<T>::from_int(1);
128 const T x2 = T(x * x), x4 = T(x2 * x2), x6 = T(x4 * x2);
129 return T(-x / T(one - one / x2 + num_traits<T>::from_int(3) / x4 -
130 num_traits<T>::from_int(15) / x6));
131}
132
133} // namespace detail
134
135/**
136 * @brief Exact occupancy distribution and per-class blocking of a stochastic knapsack.
137 *
138 * The result is insensitive to the holding-time distributions beyond their
139 * means. A class with b_k = 0 is never blocked; a class with b_k > C always is.
140 *
141 * @param rho offered load of class k (K, nonnegative)
142 * @param b bandwidth units held by a class-k call (K, nonnegative integers)
143 * @param C link capacity in bandwidth units (nonnegative integer)
144 * @return q (C+1), B (K) and QLen = rho (1 - B) (K)
145 */
146template <class T>
147KrResult<T> lossn_kr(const std::vector<T>& rho, const std::vector<T>& b,
148 const T& C) {
149 const char* fn = "lossn_kr";
150 detail::kr_check_rho_b(fn, rho, b);
151 const std::vector<long> bi = detail::kr_integral_b(fn, b);
152 const double cd = num_traits<T>::to_double(C);
153 if (!(cd >= 0.0) || std::fabs(cd - std::round(cd)) > 1e-9)
154 throw InputError(std::string(fn) + ": C must be a nonnegative integer.");
155 const long Ci = static_cast<long>(std::llround(cd));
156 const std::size_t K = rho.size();
157 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
158
159 KrResult<T> out;
160 std::vector<T>& q = out.q;
161 q.assign(static_cast<std::size_t>(Ci + 1), zero);
162 q[0] = one;
163 for (long n = 1; n <= Ci; ++n) {
164 T s = zero;
165 for (std::size_t k = 0; k < K; ++k)
166 if (bi[k] > 0 && n - bi[k] >= 0)
167 s += T(num_traits<T>::from_int(bi[k]) * rho[k] *
168 q[static_cast<std::size_t>(n - bi[k])]);
169 q[static_cast<std::size_t>(n)] = T(s / num_traits<T>::from_int(n));
170 if (num_traits<T>::to_double(q[static_cast<std::size_t>(n)]) > 1e250) {
171 // rescale; only the normalized distribution is used
172 const T scale = q[static_cast<std::size_t>(n)];
173 for (long m = 0; m <= n; ++m)
174 q[static_cast<std::size_t>(m)] = T(q[static_cast<std::size_t>(m)] / scale);
175 }
176 }
177 T tot = zero;
178 for (const T& v : q) tot += v;
179 for (T& v : q) v = T(v / tot);
180
181 out.B.assign(K, zero);
182 out.QLen.assign(K, zero);
183 for (std::size_t k = 0; k < K; ++k) {
184 if (bi[k] > 0) {
185 T acc = zero;
186 for (long n = std::max<long>(0, Ci - bi[k] + 1); n <= Ci; ++n)
187 acc += q[static_cast<std::size_t>(n)];
188 out.B[k] = detail::kr_cap1(acc);
189 }
190 out.QLen[k] = T(rho[k] * T(one - out.B[k]));
191 }
192 return out;
193}
194
195/** lossn_kr with an integer capacity. */
196template <class T>
197KrResult<T> lossn_kr(const std::vector<T>& rho, const std::vector<T>& b,
198 long C) {
199 return lossn_kr(rho, b, num_traits<T>::from_int(C));
200}
201
202/**
203 * @brief Smallest link capacity meeting per-class blocking targets.
204 *
205 * Returns the smallest integer capacity C such that every class of the
206 * stochastic knapsack has blocking B_k(C) no larger than its target. The
207 * unnormalized Kaufman-Roberts terms n q(n) = sum_k b_k rho_k q(n - b_k),
208 * q(0) = 1, do not depend on C, so the search extends a single recursion one
209 * unit at a time and evaluates B_k(C) = sum_{n>C-b_k} q(n) / sum_{n<=C} q(n) at
210 * each step, at a total cost of O(C K). The search scans every C upwards rather
211 * than bisecting because the blocking of a wide class need not decrease
212 * monotonically in C (Ross 1995, Sec. 2.6).
213 *
214 * @param rho offered load of class k (K, nonnegative)
215 * @param b bandwidth units held by a class-k call (K, nonnegative integers)
216 * @param Btarget blocking target per class (K), each in (0,1]
217 */
218template <class T>
219KrDimResult<T> lossn_kr_dim(const std::vector<T>& rho,
220 const std::vector<T>& b,
221 const std::vector<T>& Btarget) {
222 const char* fn = "lossn_kr_dim";
223 detail::kr_check_rho_b(fn, rho, b);
224 const std::vector<long> bi = detail::kr_integral_b(fn, b);
225 const std::size_t K = rho.size();
226 if (Btarget.size() != K)
227 throw InputError(std::string(fn) + ": Btarget has " + std::to_string(Btarget.size()) +
228 " entries but rho has " + std::to_string(K) + ".");
229 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
230 for (const T& t : Btarget)
231 if (!(t > zero) || t > one) throw InputError(std::string(fn) + ": Btarget must lie in (0,1].");
232
233 const long Cmax = 100000000L; // far beyond any knapsack the recursion can be asked to size
234 std::vector<T> q(1024, zero);
235 q[0] = one;
236 T total = one;
237 long C = 0;
238 KrDimResult<T> out;
239 while (true) {
240 std::vector<T> B(K, zero);
241 bool ok = true;
242 for (std::size_t k = 0; k < K; ++k) {
243 if (bi[k] > 0) {
244 T acc = zero;
245 for (long n = std::max<long>(0, C - bi[k] + 1); n <= C; ++n)
246 acc += q[static_cast<std::size_t>(n)];
247 B[k] = detail::kr_cap1(T(acc / total));
248 }
249 if (B[k] > Btarget[k]) ok = false;
250 }
251 if (ok) {
252 out.C = C;
253 out.B = B;
254 return out;
255 }
256 if (C >= Cmax)
257 throw NumericError(ErrorKind{"Capacity"},
258 std::string(fn) + ": No capacity up to " + std::to_string(Cmax) +
259 " meets the blocking targets.");
260 ++C;
261 if (static_cast<std::size_t>(C + 1) > q.size()) q.resize(2 * q.size(), zero);
262 T s = zero;
263 for (std::size_t k = 0; k < K; ++k)
264 if (bi[k] > 0 && C - bi[k] >= 0)
265 s += T(num_traits<T>::from_int(bi[k]) * rho[k] *
266 q[static_cast<std::size_t>(C - bi[k])]);
267 q[static_cast<std::size_t>(C)] = T(s / num_traits<T>::from_int(C));
268 total += q[static_cast<std::size_t>(C)];
269 if (num_traits<T>::to_double(q[static_cast<std::size_t>(C)]) > 1e250) {
270 // only ratios of the terms are used
271 const T scale = q[static_cast<std::size_t>(C)];
272 for (long m = 0; m <= C; ++m)
273 q[static_cast<std::size_t>(m)] = T(q[static_cast<std::size_t>(m)] / scale);
274 total = T(total / scale);
275 }
276 }
277}
278
279/** lossn_kr_dim with one blocking target shared by every class. */
280template <class T>
281KrDimResult<T> lossn_kr_dim(const std::vector<T>& rho,
282 const std::vector<T>& b, const T& Btarget) {
283 return lossn_kr_dim(rho, b, std::vector<T>(rho.size(), Btarget));
284}
285
286/**
287 * @brief Normal approximation of multirate single-link blocking for large C.
288 *
289 * In the critically loaded regime, where C and the offered loads grow together
290 * with sum_k b_k rho_k / C -> 1, the normalized occupancy of a stochastic
291 * knapsack converges to a truncated multivariate normal law (Kelly 1991; Ross
292 * 1995, Theorem 2.8) and blocking is asymptotically proportional to the call
293 * size (Ross 1995, Corollary 2.7). For a knapsack of fixed capacity C
294 *
295 * alpha = (C - sum_k b_k rho_k) / sqrt(C),
296 * sigma^2 = sum_k b_k^2 rho_k / C,
297 * delta = phi(alpha/sigma) / (sigma Phi(alpha/sigma)),
298 * B_k = b_k delta / sqrt(C),
299 *
300 * with phi and Phi the standard normal density and distribution function. B_k
301 * is capped at 1. The approximation is accurate when |alpha| is small (roughly
302 * below 1); far from critical loading use lossn_kr for the exact
303 * values, or lossn_asy for the fluid (Kelly) limit.
304 *
305 * @param rho offered load of class k (K, nonnegative)
306 * @param b bandwidth units held by a class-k call (K, nonnegative)
307 * @param C link capacity (positive)
308 */
309template <class T>
310KrAsyResult<T> lossn_kr_asy(const std::vector<T>& rho,
311 const std::vector<T>& b, const T& C) {
313 "lossn_kr_asy evaluates the normal density and distribution function");
314 using std::sqrt;
315 const char* fn = "lossn_kr_asy";
316 const std::size_t K = rho.size();
317 if (b.size() != K)
318 throw InputError(std::string(fn) + ": b has " + std::to_string(b.size()) +
319 " entries but rho has " + std::to_string(K) + ".");
320 const T zero = num_traits<T>::from_int(0);
321 for (std::size_t k = 0; k < K; ++k)
322 if (rho[k] < zero || b[k] < zero)
323 throw InputError(std::string(fn) + ": rho and b must be nonnegative.");
324 if (!(C > zero)) throw InputError(std::string(fn) + ": C must be a positive scalar.");
325
326 T sbr = zero, sb2r = zero;
327 for (std::size_t k = 0; k < K; ++k) {
328 sbr += T(b[k] * rho[k]);
329 sb2r += T(b[k] * b[k] * rho[k]);
330 }
331 const T sqC = sqrt(C);
332 KrAsyResult<T> out;
333 out.alpha = T(T(C - sbr) / sqC);
334 out.sigma = sqrt(T(sb2r / C));
335 out.B.assign(K, zero);
336 if (out.sigma == zero) {
337 // no class asks for any bandwidth: nothing is ever blocked
338 out.delta = zero;
339 return out;
340 }
341 out.delta = T(detail::kr_normal_hazard(T(out.alpha / out.sigma)) / out.sigma);
342 for (std::size_t k = 0; k < K; ++k) out.B[k] = detail::kr_cap1(T(b[k] * out.delta / sqC));
343 return out;
344}
345
346} // namespace lossn
347} // namespace line
348
349#endif // LINE_API_LOSSN_LOSSN_KR_H
InputError(const std::string &what)
Definition error.h:105
NumericError(const std::string &what)
Definition error.h:112
The exception types the port throws.
KrResult< T > lossn_kr(const std::vector< T > &rho, const std::vector< T > &b, const T &C)
Exact occupancy distribution and per-class blocking of a stochastic knapsack.
Definition lossn_kr.h:147
KrDimResult< T > lossn_kr_dim(const std::vector< T > &rho, const std::vector< T > &b, const std::vector< T > &Btarget)
Smallest link capacity meeting per-class blocking targets.
Definition lossn_kr.h:219
KrAsyResult< T > lossn_kr_asy(const std::vector< T > &rho, const std::vector< T > &b, const T &C)
Normal approximation of multirate single-link blocking for large C.
Definition lossn_kr.h:310
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
Number-type abstraction for the templated API port.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
Result of lossn_kr_asy.
Definition lossn_kr.h:72
T sigma
normalized occupancy standard deviation
Definition lossn_kr.h:75
std::vector< T > B
approximate blocking probability of class k
Definition lossn_kr.h:73
T delta
blocking per unit of call size, times sqrt(C)
Definition lossn_kr.h:76
T alpha
normalized spare capacity (C - sum b rho)/sqrt(C)
Definition lossn_kr.h:74
Result of lossn_kr_dim.
Definition lossn_kr.h:65
std::vector< T > B
blocking probabilities at that capacity
Definition lossn_kr.h:67
long C
smallest capacity with B_k(C) <= Btarget_k for all k
Definition lossn_kr.h:66
Result of lossn_kr.
Definition lossn_kr.h:57
std::vector< T > q
occupancy distribution, q[n] = P(n units busy), n = 0..C
Definition lossn_kr.h:58
std::vector< T > B
blocking probability of class k
Definition lossn_kr.h:59
std::vector< T > QLen
mean number of class-k calls in progress, rho_k (1 - B_k)
Definition lossn_kr.h:60