LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
lossn_mci.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_MCI_H
6#define LINE_API_LOSSN_LOSSN_MCI_H
7
8/**
9 * @file
10 * @ingroup api_lossn
11 * Monte Carlo importance-sampling summation for product-form loss networks.
12 *
13 * Templated port of matlab/src/api/lossn/lossn_mci.m, implementing Ross and
14 * Wang, "Monte Carlo Summation Applied to Product-Form Loss Networks",
15 * Probability in the Engineering and Informational Sciences 6 (1992), 323-348.
16 *
17 * Links j = 1..J carry capacity C(j), routes r = 1..R carry offered load nu(r)
18 * and need A(j, r) circuits on link j. The state n is feasible iff A n <= C,
19 * the equilibrium law is product form with normalizing constant
20 * g(C) = sum_{n feasible} prod_r nu_r^{n_r} / n_r!, and the class-r acceptance
21 * probability is g(C - A(:, r)) / g(C). States are drawn from the truncated
22 * Poisson importance law (Eq. 6) over {0..N_1} x ... x {0..N_R} with
23 * N_r = min_j floor(C_j / A_jr), and the ratio estimators (Eq. 8) give g and
24 * the blocking probabilities with delta-method confidence intervals.
25 *
26 * RANDOMNESS. There is no global generator here and no hidden seeding. The
27 * caller passes its own engine by reference, or a seed, and the uniforms are
28 * drawn as (gen() >> 11) * 2^-53 rather than through a distribution object, so
29 * a given engine and seed reproduce the same estimate on every standard
30 * library. The draw order mirrors the reference (all S samples of route 1,
31 * then of route 2, and so on) so that the two implementations traverse the
32 * same importance law in the same order, though their engines differ and their
33 * sample paths therefore cannot be compared point by point. What CAN be
34 * compared, and is, is convergence to a closed form: a single link with unit
35 * circuit requirements is an Erlang loss system and its blocking probability
36 * is Erlang B.
37 *
38 * ARITHMETIC. The importance weights are formed in log space, exactly as in
39 * the reference, so log, exp, lgamma and the inverse error function are all
40 * unavoidable; the header is gated on num_traits<T>::has_transcendental and is
41 * instantiated at double and Real only. An exact instantiation would be
42 * meaningless in any case: the estimate is a random variable.
43 *
44 * REFERENCE NOTE. MATLAB's var normalizes by S - 1 and so does the port, and
45 * the covariance is likewise the S - 1 form; with S below 2 the estimator is
46 * undefined and the port rejects it rather than dividing by zero, which the
47 * reference does silently.
48 */
49
50#include <cmath>
51#include <cstddef>
52#include <cstdint>
53#include <limits>
54#include <random>
55#include <vector>
56
57#include "line/num/number.h"
58#include "line/util/error.h"
59#include "line/util/matrix.h"
60
61namespace line {
62namespace lossn {
63
64/** Options of lossn_mci, MATLAB's options struct. */
65template <class T>
67 std::size_t samples = 100000; ///< S
68 double alpha = 0.05; ///< 1 - confidence level
69 std::vector<T> gamma; ///< importance parameters, empty = heuristic
70};
71
72/** Result of lossn_mci. */
73template <class T>
75 std::vector<T> QLen; ///< mean carried load per route
76 std::vector<T> Loss; ///< blocking probability per route
77 double lG = 0.0; ///< log of the estimated normalizing constant
78 Matrix<T> acceptCI; ///< (R x 2) acceptance confidence interval
79 Matrix<T> lossCI; ///< (R x 2) blocking confidence interval
80 std::vector<T> acceptPoint;
81 std::vector<T> lossPoint;
82 T level = num_traits<T>::from_int(0); ///< 1 - alpha
83 std::size_t nsamples = 0;
84};
85
86namespace detail {
87
88/** log(sum exp(x)), shifted by the maximum. */
89template <class T>
90T logsumexp(const std::vector<T>& x) {
91 using std::exp;
92 using std::log;
93 if (x.empty()) throw InputError("logsumexp: empty argument");
94 T m = x[0];
95 for (std::size_t i = 1; i < x.size(); ++i)
96 if (x[i] > m) m = x[i];
98 for (std::size_t i = 0; i < x.size(); ++i) s += exp(T(x[i] - m));
99 return T(m + log(s));
100}
101
102/**
103 * log(l!) accumulated term by term, which is what MATLAB's gammaln(l + 1)
104 * evaluates to at the integer arguments this algorithm uses. Summing the logs
105 * rather than taking the log of a factorial keeps the working precision and
106 * cannot overflow, and it removes any dependency on a special-function
107 * library: Boost.Math's lgamma promotes through __float128 for its precision
108 * policy, which drags libquadmath into every binary that links this header.
109 */
110template <class T>
111std::vector<T> log_factorials(std::size_t upTo) {
112 using std::log;
113 std::vector<T> lf(upTo + 1, num_traits<T>::from_int(0));
114 for (std::size_t l = 1; l <= upTo; ++l)
115 lf[l] = T(lf[l - 1] + log(num_traits<T>::from_int(static_cast<long>(l))));
116 return lf;
117}
118
119/**
120 * The standard-normal 1 - alpha/2 quantile, MATLAB's sqrt(2) erfinv(1 - alpha).
121 *
122 * Computed in double by bisection on std::erf, and only then converted to T.
123 * That is not a precision compromise in disguise: alpha reaches this function
124 * as a double, so the quantile it determines carries double information and no
125 * more, whatever the working type of the estimator is. It is also a random
126 * estimator's confidence half width, whose own statistical error dwarfs any
127 * arithmetic one.
128 */
129inline double normal_quantile(double alpha) {
130 if (!(alpha > 0.0) || !(alpha < 1.0))
131 throw InputError("lossn_mci: the confidence level alpha must lie in (0, 1)");
132 const double target = 1.0 - alpha;
133 double lo = 0.0, hi = 10.0;
134 for (int it = 0; it < 200; ++it) {
135 const double mid = 0.5 * (lo + hi);
136 if (std::erf(mid) < target)
137 lo = mid;
138 else
139 hi = mid;
140 }
141 return std::sqrt(2.0) * 0.5 * (lo + hi);
142}
143
144/** A uniform on [0, 1) built from the engine directly, for reproducibility. */
145template <class Rng>
146double uniform01(Rng& gen) {
147 return static_cast<double>(gen() >> 11) * (1.0 / 9007199254740992.0);
148}
149
150} // namespace detail
151
152/**
153 * Estimate the normalizing constant and the blocking probabilities.
154 *
155 * @param nu (R) offered load per route
156 * @param A (J x R) circuit requirement of link j for route r
157 * @param C (J) link capacity
158 * @param opt options
159 * @param gen the caller's random engine, advanced in place
160 */
161template <class T, class Rng>
162LossnMciResult<T> lossn_mci(const std::vector<T>& nu, const Matrix<T>& A, const std::vector<T>& C,
163 const LossnMciOptions<T>& opt, Rng& gen) {
165 "lossn_mci requires transcendental arithmetic (log-space importance weights)");
166 using std::exp;
167 using std::log;
168 using std::pow;
169 using std::sqrt;
170
171 const std::size_t R = nu.size(), J = C.size();
172 if (R == 0 || J == 0) throw InputError("lossn_mci: empty loss network");
173 if (A.rows() != J || A.cols() != R) throw InputError("lossn_mci: A must be J x R");
174 const std::size_t S = opt.samples;
175 if (S < 2) throw InputError("lossn_mci: at least two samples are required");
176 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
177
178 // per-route maximum feasible occupancy
179 std::vector<std::size_t> N(R, 0);
180 for (std::size_t k = 0; k < R; ++k) {
181 bool any = false;
182 T best = zero;
183 for (std::size_t j = 0; j < J; ++j) {
184 if (!(A(j, k) > zero)) continue;
185 const T ratio = C[j] / A(j, k);
186 if (!any || ratio < best) best = ratio;
187 any = true;
188 }
189 if (!any) continue;
190 const double bd = num_traits<T>::to_double(best);
191 N[k] = bd <= 0.0 ? 0 : static_cast<std::size_t>(std::floor(bd));
192 }
193
194 // importance parameters, Section 3.4 heuristic
195 std::vector<T> gamma(opt.gamma);
196 if (gamma.empty()) {
197 T delta = zero;
198 for (std::size_t j = 0; j < J; ++j) {
199 if (C[j] == zero) throw InputError("lossn_mci: a link has zero capacity");
200 T load = zero;
201 for (std::size_t k = 0; k < R; ++k) load += A(j, k) * nu[k];
202 load = T(load / C[j]);
203 if (j == 0 || load > delta) delta = load;
204 }
205 T base = T(one - num_traits<T>::from_double(0.15) * T(one - delta));
206 const T floorBase = num_traits<T>::from_double(1e-6);
207 if (base < floorBase) base = floorBase;
208 gamma.assign(R, zero);
209 for (std::size_t k = 0; k < R; ++k) {
210 T b = zero;
211 for (std::size_t j = 0; j < J; ++j)
212 if (A(j, k) > b) b = A(j, k);
213 gamma[k] = nu[k] * pow(base, b);
214 }
215 }
216 if (gamma.size() != R) throw InputError("lossn_mci: gamma must have one entry per route");
217 const T gammaFloor = num_traits<T>::from_double(1e-300);
218 for (std::size_t k = 0; k < R; ++k)
219 if (gamma[k] < gammaFloor) gamma[k] = gammaFloor;
220
221 // normalization constant of the importance law, and its per-route cdfs
222 T log_c = zero;
223 std::vector<std::vector<T>> cdf(R);
224 for (std::size_t k = 0; k < R; ++k) {
225 std::vector<T> logterms(N[k] + 1);
226 const std::vector<T> lf = detail::log_factorials<T>(N[k]);
227 for (std::size_t l = 0; l <= N[k]; ++l)
228 logterms[l] =
229 T(num_traits<T>::from_int(static_cast<long>(l)) * log(gamma[k]) - lf[l]);
230 const T lse = detail::logsumexp(logterms);
231 log_c += lse;
232 cdf[k].assign(N[k] + 1, zero);
233 T acc = zero;
234 for (std::size_t l = 0; l <= N[k]; ++l) {
235 acc += exp(T(logterms[l] - lse));
236 cdf[k][l] = acc;
237 }
238 cdf[k][N[k]] = one; // guard rounding, as in the reference
239 }
240
241 // S i.i.d. draws, column by column
242 std::vector<std::size_t> V(S * R, 0);
243 for (std::size_t k = 0; k < R; ++k)
244 for (std::size_t s = 0; s < S; ++s) {
245 const T u = num_traits<T>::from_double(detail::uniform01(gen));
246 std::size_t v = 0;
247 for (std::size_t l = 0; l <= N[k]; ++l)
248 if (u > cdf[k][l]) ++v;
249 V[s * R + k] = v;
250 }
251
252 // feasibility indicators and log likelihood ratios
253 std::vector<T> logratio(R);
254 for (std::size_t k = 0; k < R; ++k) {
255 if (!(nu[k] > zero)) throw InputError("lossn_mci: a route has non-positive offered load");
256 logratio[k] = T(log(nu[k]) - log(gamma[k]));
257 }
258 std::vector<T> log_alpha(S, zero);
259 std::vector<char> inOmega(S, 0);
260 std::vector<char> inOmegaK(S * R, 0);
261 std::vector<T> AV(J, zero);
262 for (std::size_t s = 0; s < S; ++s) {
263 for (std::size_t j = 0; j < J; ++j) {
264 T t = zero;
265 for (std::size_t k = 0; k < R; ++k)
266 t += A(j, k) * num_traits<T>::from_int(static_cast<long>(V[s * R + k]));
267 AV[j] = t;
268 }
269 bool ok = true;
270 for (std::size_t j = 0; j < J && ok; ++j)
271 if (AV[j] > C[j]) ok = false;
272 inOmega[s] = ok ? 1 : 0;
273 for (std::size_t k = 0; k < R; ++k) {
274 bool okk = true;
275 for (std::size_t j = 0; j < J && okk; ++j)
276 if (AV[j] > T(C[j] - A(j, k))) okk = false;
277 inOmegaK[s * R + k] = okk ? 1 : 0;
278 }
279 T la = zero;
280 for (std::size_t k = 0; k < R; ++k)
281 la += num_traits<T>::from_int(static_cast<long>(V[s * R + k])) * logratio[k];
282 log_alpha[s] = la;
283 }
284
286 res.nsamples = S;
287 res.level = T(one - num_traits<T>::from_double(opt.alpha));
288
289 // normalizing constant
290 std::vector<T> laO;
291 for (std::size_t s = 0; s < S; ++s)
292 if (inOmega[s]) laO.push_back(log_alpha[s]);
293 if (laO.empty()) {
294 res.lG = -std::numeric_limits<double>::infinity();
295 } else {
296 const T lse = detail::logsumexp(laO);
297 res.lG = num_traits<T>::to_double(T(log_c + lse)) -
298 std::log(static_cast<double>(S));
299 }
300
301 // ratio estimators with shifted weights
302 T M = zero;
303 bool anyOmega = false;
304 for (std::size_t s = 0; s < S; ++s)
305 if (inOmega[s]) {
306 if (!anyOmega || log_alpha[s] > M) M = log_alpha[s];
307 anyOmega = true;
308 }
309 std::vector<T> w(S), Z(S);
310 T meanZ = zero;
311 for (std::size_t s = 0; s < S; ++s) {
312 w[s] = exp(T(log_alpha[s] - M));
313 Z[s] = inOmega[s] ? w[s] : zero;
314 meanZ += Z[s];
315 }
316 meanZ = T(meanZ / num_traits<T>::from_int(static_cast<long>(S)));
317
318 const T crit = num_traits<T>::from_double(detail::normal_quantile(opt.alpha));
319 res.acceptPoint.assign(R, zero);
320 res.acceptCI = Matrix<T>(R, 2, zero);
321 res.lossCI = Matrix<T>(R, 2, zero);
322 res.Loss.assign(R, zero);
323 res.QLen.assign(R, zero);
324 if (!(meanZ > zero))
325 throw NumericError("lossn_mci: no sampled state was feasible, the estimator is undefined");
326
327 T varZ = zero;
328 for (std::size_t s = 0; s < S; ++s) varZ += T(Z[s] - meanZ) * T(Z[s] - meanZ);
329 varZ = T(varZ / num_traits<T>::from_int(static_cast<long>(S - 1)));
330
331 for (std::size_t k = 0; k < R; ++k) {
332 std::vector<T> Y(S);
333 T meanY = zero;
334 for (std::size_t s = 0; s < S; ++s) {
335 Y[s] = inOmegaK[s * R + k] ? w[s] : zero;
336 meanY += Y[s];
337 }
338 meanY = T(meanY / num_traits<T>::from_int(static_cast<long>(S)));
339 const T phi = T(meanY / meanZ);
340 T varY = zero, covYZ = zero;
341 for (std::size_t s = 0; s < S; ++s) {
342 varY += T(Y[s] - meanY) * T(Y[s] - meanY);
343 covYZ += T(Y[s] - meanY) * T(Z[s] - meanZ);
344 }
345 const T denom = num_traits<T>::from_int(static_cast<long>(S - 1));
346 varY = T(varY / denom);
347 covYZ = T(covYZ / denom);
348 T sig2 = T((varY - num_traits<T>::from_int(2) * phi * covYZ + phi * phi * varZ) /
349 (num_traits<T>::from_int(static_cast<long>(S)) * meanZ * meanZ));
350 if (sig2 < zero) sig2 = zero;
351 const T half = T(crit * sqrt(sig2));
352 res.acceptPoint[k] = phi;
353 res.acceptCI(k, 0) = T(phi - half);
354 res.acceptCI(k, 1) = T(phi + half);
355 res.Loss[k] = T(one - phi);
356 res.QLen[k] = nu[k] * phi;
357 res.lossCI(k, 0) = T(one - res.acceptCI(k, 1));
358 res.lossCI(k, 1) = T(one - res.acceptCI(k, 0));
359 }
360 res.lossPoint = res.Loss;
361 return res;
362}
363
364/**
365 * Overload seeding a local engine. Deterministic in the seed and independent
366 * of any global state; the engine is created and destroyed here.
367 */
368template <class T>
369LossnMciResult<T> lossn_mci(const std::vector<T>& nu, const Matrix<T>& A, const std::vector<T>& C,
370 const LossnMciOptions<T>& opt, std::uint64_t seed) {
371 std::mt19937_64 gen(seed);
372 return lossn_mci(nu, A, C, opt, gen);
373}
374
375} // namespace lossn
376} // namespace line
377
378#endif // LINE_API_LOSSN_LOSSN_MCI_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
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
Dense matrix and non-owning view.
LossnMciResult< T > lossn_mci(const std::vector< T > &nu, const Matrix< T > &A, const std::vector< T > &C, const LossnMciOptions< T > &opt, Rng &gen)
Estimate the normalizing constant and the blocking probabilities.
Definition lossn_mci.h:162
Number-type abstraction for the templated API port.
Options of lossn_mci, MATLAB's options struct.
Definition lossn_mci.h:66
std::vector< T > gamma
importance parameters, empty = heuristic
Definition lossn_mci.h:69
double alpha
1 - confidence level
Definition lossn_mci.h:68
Result of lossn_mci.
Definition lossn_mci.h:74
std::vector< T > QLen
mean carried load per route
Definition lossn_mci.h:75
std::vector< T > Loss
blocking probability per route
Definition lossn_mci.h:76
Matrix< T > acceptCI
(R x 2) acceptance confidence interval
Definition lossn_mci.h:78
std::vector< T > lossPoint
Definition lossn_mci.h:81
double lG
log of the estimated normalizing constant
Definition lossn_mci.h:77
std::vector< T > acceptPoint
Definition lossn_mci.h:80
Matrix< T > lossCI
(R x 2) blocking confidence interval
Definition lossn_mci.h:79