LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
lossn_erlangfp.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_ERLANGFP_H
6#define LINE_API_LOSSN_LOSSN_ERLANGFP_H
7
8/**
9 * @file
10 * @ingroup api_lossn
11 * Erlang fixed-point (reduced-load) approximation for a loss network.
12 *
13 * Templated port of matlab/src/api/lossn/lossn_erlangfp.m. Link j sees class
14 * r offered at nu_r thinned by the blocking of r on every OTHER link,
15 * rho_(j,r) = nu_r prod_(i != j, A(i,r) > 0) (1 - B(i,r)),
16 * and its occupancy distribution is the Kaufman-Roberts one (Kaufman 1981,
17 * Roberts 1981), n q(n) = sum_r A(j,r) rho_(j,r) q(n - A(j,r)), so the
18 * blocking of class r on link j is B(j,r) = P(more than C_j - A(j,r) units
19 * busy). The (J x R) matrix B is the fixed point of that map, reached by the
20 * shared iteration in line::da::da_fpi. With unit requirements this is Kelly's
21 * Erlang fixed point; the earlier form fed the multi-unit load to Erlang B,
22 * which counted a call of size a as one unit.
23 *
24 * A fractional row is the same constraint scaled to integers: row j is
25 * multiplied by the least common denominator of A(j,:) and C_j (each entry
26 * read as the rational of MATLAB's rat(x, 1e-12)), which leaves the admissible
27 * set unchanged and gives the recursion whole units to count.
28 *
29 * The recursion itself is rational, but the fixed point around it stops on a
30 * tolerance, so the function stays transcendental-gated: an exact
31 * instantiation would promise more than the algorithm delivers.
32 */
33
34#include <algorithm>
35#include <cmath>
36#include <cstddef>
37#include <functional>
38#include <limits>
39#include <vector>
40
41#include "line/api/da/da_fpi.h"
42#include "line/num/number.h"
43#include "line/util/error.h"
44#include "line/util/matrix.h"
45
46namespace line {
47namespace lossn {
48
49template <class T>
51 std::vector<T> QLen; ///< carried traffic per class
52 std::vector<T> Loss; ///< blocking probability per class
53 Matrix<T> E; ///< (J x R) blocking of class r on link j (the fixed point)
54 std::size_t iterations = 0;
55 bool converged = false;
56};
57
58/**
59 * Erlang's loss formula B(nu, C), evaluated through logs as MATLAB does so the
60 * factorials stay in range for large C.
61 */
62template <class T>
63T erlang_b(const T& nu, int C) {
65 "erlang_b as ported evaluates through log/exp; use the rational recursion "
66 "B_k = nu B_{k-1} / (k + nu B_{k-1}) for an exact variant");
67 if (C < 0) throw InputError("erlang_b: negative capacity");
68 using std::exp;
69 using std::log;
70 const T lnu = log(nu);
71 T den = num_traits<T>::from_int(0);
72 for (int i = 0; i <= C; ++i)
73 den += exp(num_traits<T>::from_int(i) * lnu - log(num_factorial<T>(static_cast<unsigned>(i))));
74 const T lb = num_traits<T>::from_int(C) * lnu -
75 log(num_factorial<T>(static_cast<unsigned>(C))) - log(den);
76 return exp(lb);
77}
78
79/**
80 * Erlang B for a REAL capacity, the reference's ErlangB(nu, C) evaluated as it
81 * stands: the sum runs over i = 0..floor(C) and factln(C) is lgamma(C+1).
82 */
83template <class T>
84T erlang_b_real(const T& nu, const T& C) {
85 using std::exp;
86 using std::lgamma;
87 using std::log;
88 const double cd = num_traits<T>::to_double(C);
89 if (!(cd >= 0.0)) return num_traits<T>::from_double(std::numeric_limits<double>::quiet_NaN());
90 const T lnu = log(nu);
91 T den = num_traits<T>::from_int(0);
92 const long cfloor = static_cast<long>(std::floor(cd));
93 for (long i = 0; i <= cfloor; ++i)
94 den += exp(num_traits<T>::from_int(i) * lnu - log(num_factorial<T>(static_cast<unsigned>(i))));
95 const T lb = C * lnu - num_traits<T>::from_double(lgamma(cd + 1.0)) - log(den);
96 return exp(lb);
97}
98
99/**
100 * The reference's fixed-point settings: iter_max 10000, iter_tol 1e-10, the
101 * max-norm of the increment (`da_norm = @(d) max(abs(d(:)))`, the driver's
102 * default) and `da_nanstop = true`.
103 */
106 o.iter_max = 10000;
107 o.iter_tol = 1e-10;
108 o.nanstop = true;
109 return o;
110}
111
112namespace detail {
113
114/**
115 * Denominator of MATLAB's rat(x, tol): the continued-fraction convergent n/d
116 * of x is extended until |x - n/d| <= tol.
117 */
118inline long long erlangfp_rat_den(double x, double tol = 1e-12) {
119 if (!std::isfinite(x)) throw InputError("lossn_erlangfp: non-finite circuit requirement or capacity");
120 double y = x;
121 long long n0 = 1, d0 = 0, n1 = static_cast<long long>(std::floor(y)), d1 = 1;
122 double frac = y - std::floor(y);
123 for (int k = 0; k < 64 && std::fabs(x - static_cast<double>(n1) / static_cast<double>(d1)) > tol; ++k) {
124 if (frac == 0.0) break;
125 y = 1.0 / frac;
126 const long long a = static_cast<long long>(std::floor(y));
127 frac = y - std::floor(y);
128 const long long n2 = a * n1 + n0, d2 = a * d1 + d0;
129 n0 = n1; d0 = d1; n1 = n2; d1 = d2;
130 }
131 return d1 < 0 ? -d1 : d1;
132}
133
134inline long long erlangfp_gcd(long long a, long long b) {
135 while (b != 0) { const long long t = a % b; a = b; b = t; }
136 return a < 0 ? -a : a;
137}
138
139/**
140 * Per-class blocking on one link of capacity c, class r asking a[r] units at
141 * offered load rho[r]: the Kaufman-Roberts recursion n q(n) = sum_r a_r rho_r
142 * q(n - a_r), normalized, and B_r = sum_{n > c - a_r} q(n).
143 */
144template <class T>
145std::vector<T> kaufman_roberts_blocking(const std::vector<T>& rho, const std::vector<long long>& a, long long c) {
146 const std::size_t R = rho.size();
147 const T zero = num_traits<T>::from_int(0);
148 std::vector<T> q(static_cast<std::size_t>(c + 1), zero);
149 q[0] = num_traits<T>::from_int(1);
150 for (long long n = 1; n <= c; ++n) {
151 T s = zero;
152 for (std::size_t r = 0; r < R; ++r)
153 if (a[r] > 0 && n - a[r] >= 0)
154 s += num_traits<T>::from_int(static_cast<int>(a[r])) * rho[r] *
155 q[static_cast<std::size_t>(n - a[r])];
156 q[static_cast<std::size_t>(n)] = T(s / num_traits<T>::from_int(static_cast<int>(n)));
157 // rescale; only the normalized distribution is used
158 if (num_traits<T>::to_double(q[static_cast<std::size_t>(n)]) > 1e250) {
159 const T scale = q[static_cast<std::size_t>(n)];
160 for (long long m = 0; m <= n; ++m) q[static_cast<std::size_t>(m)] = T(q[static_cast<std::size_t>(m)] / scale);
161 }
162 }
163 T tot = zero;
164 for (const T& v : q) tot += v;
165 std::vector<T> b(R, zero);
166 for (std::size_t r = 0; r < R; ++r) {
167 if (a[r] <= 0) continue;
168 T acc = zero;
169 for (long long n = std::max<long long>(0, c - a[r] + 1); n <= c; ++n) acc += q[static_cast<std::size_t>(n)];
170 b[r] = T(acc / tot);
171 }
172 return b;
173}
174
175} // namespace detail
176
177/**
178 * @brief Erlang fixed-point (reduced-load) approximation for a loss network.
179 *
180 * @param nu offered load per class (R)
181 * @param A (J x R) capacity requirement of link j for class r; fractional
182 * entries are scaled to integers row by row (see the file comment)
183 * @param C (J) link capacities
184 * @param options fixed-point options; the default is the reference's
185 * (max-norm, iter_tol 1e-10, nanstop), see lossn_erlangfp_options()
186 *
187 * Returns the carried traffic QLen_r = nu_r prod_j (1 - B(j,r)), the loss
188 * 1 - prod_j (1 - B(j,r)) and the (J x R) blocking matrix E = B.
189 */
190template <class T>
191ErlangFpResult<T> lossn_erlangfp(const std::vector<T>& nu, const Matrix<T>& A,
192 const std::vector<T>& C,
193 const da::FpiOptions& options = lossn_erlangfp_options()) {
195 "lossn_erlangfp requires transcendental arithmetic (a tolerance-driven fixed "
196 "point around the Kaufman-Roberts recursion)");
197 const std::size_t R = nu.size();
198 const std::size_t J = C.size();
199 if (A.rows() != J || A.cols() != R)
200 throw InputError("lossn_erlangfp: the route matrix does not match nu and C");
201
202 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
203
204 // Integer rows: multiply row j by the lcm of the denominators of A(j,:), C_j.
205 std::vector<std::vector<long long>> Aint(J, std::vector<long long>(R, 0));
206 std::vector<long long> Cint(J, 0);
207 for (std::size_t j = 0; j < J; ++j) {
208 long long m = 1;
209 for (std::size_t r = 0; r <= R; ++r) {
210 const double x = (r < R) ? num_traits<T>::to_double(A(j, r)) : num_traits<T>::to_double(C[j]);
211 const long long d = detail::erlangfp_rat_den(x);
212 if (d > 0) m = m / detail::erlangfp_gcd(m, d) * d;
213 }
214 for (std::size_t r = 0; r < R; ++r)
215 Aint[j][r] = std::llround(static_cast<double>(m) * num_traits<T>::to_double(A(j, r)));
216 Cint[j] = static_cast<long long>(std::floor(static_cast<double>(m) * num_traits<T>::to_double(C[j]) + 1e-9));
217 if (Cint[j] < 0) throw InputError("lossn_erlangfp: negative link capacity");
218 }
219
220 // One sweep of the reduced-load map over the row-major (J x R) iterate.
221 const std::function<std::pair<std::vector<T>, std::vector<T>>(const std::vector<T>&, std::size_t)>
222 sweep = [&](const std::vector<T>& B, std::size_t) {
223 std::vector<T> Bnew = B;
224 for (std::size_t j = 0; j < J; ++j) {
225 std::vector<T> rho = nu;
226 for (std::size_t r = 0; r < R; ++r)
227 for (std::size_t i = 0; i < J; ++i)
228 if (i != j && Aint[i][r] > 0) rho[r] = T(rho[r] * T(one - B[i * R + r]));
229 const std::vector<T> bj = detail::kaufman_roberts_blocking<T>(rho, Aint[j], Cint[j]);
230 for (std::size_t r = 0; r < R; ++r) Bnew[j * R + r] = bj[r];
231 }
232 return std::make_pair(Bnew, B);
233 };
234
235 const std::vector<T> B0(J * R, zero);
236 da::FpiResult<T> fp = da::da_fpi<T>(sweep, B0, options);
237
239 r.E = Matrix<T>(J, R, zero);
240 for (std::size_t j = 0; j < J; ++j)
241 for (std::size_t c = 0; c < R; ++c) r.E(j, c) = fp.x[j * R + c];
242 r.iterations = fp.iterations;
243 r.converged = fp.converged;
244 r.QLen.assign(R, zero);
245 r.Loss.assign(R, zero);
246 for (std::size_t c = 0; c < R; ++c) {
247 T pass = one;
248 for (std::size_t j = 0; j < J; ++j) pass = T(pass * T(one - r.E(j, c)));
249 r.QLen[c] = T(nu[c] * pass);
250 r.Loss[c] = T(one - pass); // blocking probability = 1 - carried/offered
251 }
252 return r;
253}
254
255/** lossn_erlangfp with integer capacities. */
256template <class T>
257ErlangFpResult<T> lossn_erlangfp(const std::vector<T>& nu, const Matrix<T>& A,
258 const std::vector<int>& C,
259 const da::FpiOptions& options = lossn_erlangfp_options()) {
260 std::vector<T> Ct(C.size());
261 for (std::size_t j = 0; j < C.size(); ++j) Ct[j] = num_traits<T>::from_int(C[j]);
262 return lossn_erlangfp(nu, A, Ct, options);
263}
264
265} // namespace lossn
266} // namespace line
267
268#endif // LINE_API_LOSSN_LOSSN_ERLANGFP_H
InputError(const std::string &what)
Definition error.h:105
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
Damped fixed-point iteration, the shared driver of the decomposition algorithms.
The exception types the port throws.
Dense matrix and non-owning view.
FpiResult< T > da_fpi(const std::function< std::pair< std::vector< T >, std::vector< T > >(const std::vector< T > &, std::size_t)> &iterfun, const std::vector< T > &x0, const FpiOptions &options=FpiOptions())
Damped fixed-point iteration, the shared driver of the decomposition algorithms.
Definition da_fpi.h:102
T erlang_b(const T &nu, int C)
Erlang's loss formula B(nu, C), evaluated through logs as MATLAB does so the factorials stay in range...
ErlangFpResult< T > lossn_erlangfp(const std::vector< T > &nu, const Matrix< T > &A, const std::vector< T > &C, const da::FpiOptions &options=lossn_erlangfp_options())
Erlang fixed-point (reduced-load) approximation for a loss network.
T erlang_b_real(const T &nu, const T &C)
Erlang B for a REAL capacity, the reference's ErlangB(nu, C) evaluated as it stands: the sum runs ove...
da::FpiOptions lossn_erlangfp_options()
The reference's fixed-point settings: iter_max 10000, iter_tol 1e-10, the max-norm of the increment (...
double tot(const std::vector< double > &pops)
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
T num_factorial(unsigned n)
Factorial as a value of T.
Definition number.h:196
Number-type abstraction for the templated API port.
Options mirroring the fields MATLAB reads off the options struct.
Definition da_fpi.h:50
std::size_t iter_max
Definition da_fpi.h:51
bool nanstop
stop when the increment norm is NaN (the reference's isnan test)
Definition da_fpi.h:55
std::size_t iterations
Definition da_fpi.h:88
std::vector< T > x
Definition da_fpi.h:87
Matrix< T > E
(J x R) blocking of class r on link j (the fixed point)
std::vector< T > QLen
carried traffic per class
std::vector< T > Loss
blocking probability per class