LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_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_PFQN_MCI_H
6#define LINE_API_PFQN_MCI_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Monte Carlo Integration estimate of the normalizing constant of a closed
12 * product-form network (Ross, Wang and Yao; MonteQueue 2.0).
13 *
14 * Templated port of matlab/src/api/pfqn/pfqn_mci.m, cross-checked against
15 * jar/src/main/java/jline/api/pfqn/nc/Pfqn_mci.java and
16 * python/line_solver/api/pfqn/asymptotic.py. Variants: mci, imci, amci,
17 * lhsmci, rm.
18 *
19 * The McKenna-Mitra integral form of the constant,
20 *
21 * G(N) = 1/prod_r N_r! int_{R_+^M} e^{-sum_i v_i} prod_r (sum_i v_i D(i,r) + Z_r)^{N_r} dv,
22 *
23 * is estimated by importance sampling with an independent exponential proposal
24 * of rate gamma_i per station. Writing V ~ prod_i Exp(gamma_i), one draw
25 * contributes
26 *
27 * lZ = -sum_i (1 - gamma_i) V_i - sum_i log gamma_i - sum_r log N_r!
28 * + sum_r N_r log(sum_i V_i D(i,r) + Z_r),
29 *
30 * and lG = log mean exp(lZ). The proposal rates come from a Bard-Schweitzer
31 * pre-solve: gamma_i = max(0.01, 1 - U_i) in the IMCI variant, and the
32 * saturation-aware gamma_i = 1/sqrt(max N) when U_i > 0.9 in the plain MCI
33 * variant. Overshooting rates make the estimator heavy-tailed, which is why
34 * the reference clamps them.
35 *
36 * The deviates come from the caller's generator, one draw per station per
37 * sample. (MATLAB used to cache its uniforms in a persistent across calls; that
38 * cache was removed on 2026-07-28, so MATLAB now redraws per call as this port,
39 * the JAR and python do.)
40 *
41 * Variant notes:
42 *
43 * - The 'rm' variant reads the balanced-job-bound rate with the COLUMN maximum
44 * of the demands, tput_r = N_r / (sum_i D(i,r) + Z_r + max_i D(i,r) (sum N - 1)),
45 * for every M, as MATLAB (max(D,[],1)), the JAR and python do. Before
46 * 2026-10-21 MATLAB wrote max(D,1), the elementwise maximum with the scalar 1,
47 * which for M > 1 built the rates from the FIRST station's clamped demands
48 * only, and the JAR copied those numbers. Only the proposal, hence the
49 * variance, was affected: every reading targets the same constant.
50 *
51 * - 'amci' and 'lhsmci' use the 'imci' tilt and differ only in the sampler.
52 * 'amci' draws antithetic pairs (u, 1-u) in rows s and ceil(I/2)+s, and
53 * 'lhsmci' stratifies each station's uniforms by a Latin hypercube. Both fill
54 * the uniforms STATION-MAJOR (all draws for station 1, then station 2, ...),
55 * in the order of MATLAB's column-major rand(Ih,M) and of the JAR; the
56 * hypercube draws its Fisher-Yates permutation (as the JAR does, by
57 * mc_uniform_int) before the I jitters of that station. The plain samplers
58 * ('mci', 'imci', 'rm') keep the sample-major order this port has always
59 * used, so their seeded outputs are unchanged.
60 *
61 * Arithmetic: INEXACT BY CONSTRUCTION. The value is a random variable, the
62 * integrand is evaluated through logs, and the proposal rates come from a
63 * tolerance-stopped Bard-Schweitzer solve; all three require transcendental
64 * arithmetic.
65 *
66 * RNG contract: see pfqn_mc_common.h. Comparable to MATLAB, the JAR and python
67 * only in distribution, never stream for stream (MATLAB's rand/randperm,
68 * java.util.Random and numpy's legacy generator are three different streams);
69 * reproducible within this port only when the generator is passed in the same
70 * state. The cross-codebase check is therefore statistical: the mean of
71 * independent replications must sit within its own standard error of the
72 * exact constant (line-test.git cpp/tests/test_pfqn_mci_variants.cpp).
73 */
74
75#include <cmath>
76#include <cstddef>
77#include <limits>
78#include <vector>
79
83#include "line/num/number.h"
84#include "line/util/error.h"
85#include "line/util/matrix.h"
86
87namespace line {
88namespace pfqn {
89
90/** The proposal-rate rules the reference selects between. */
91enum class MciVariant {
92 Imci, ///< gamma = max(0.01, 1 - U), the MonteQueue 2.0 recommendation
93 Mci, ///< gamma = 1/sqrt(max N) where U > 0.9, else 1 - U
94 Rm, ///< repairman: rates from the balanced job bound, column-max reading
95 Amci, ///< the Imci tilt, antithetic uniform pairs (u, 1-u) (Ross and Wang)
96 Lhsmci ///< the Imci tilt, Latin hypercube stratified uniforms per station
97};
98
99/**
100 * @brief Monte Carlo Integration estimate of the normalizing constant of a
101 * closed product-form network (Ross, Wang and Yao; MonteQueue 2.0).
102 *
103 * @param D (M x R) service demands
104 * @param N (R) population per class
105 * @param Z (R) think times; empty for none
106 * @param samples number of Monte Carlo samples
107 * @param variant proposal-rate rule
108 * @param rng explicit generator, advanced by the call
109 */
110template <class T>
111NcResult<T> pfqn_mci(const Matrix<T>& D, const std::vector<int>& N, const std::vector<T>& Z,
112 std::size_t samples, MciVariant variant, McRng& rng) {
114 "pfqn_mci requires transcendental arithmetic: it is a Monte Carlo estimator over "
115 "an exponential proposal, its integrand is evaluated in the log domain, and its "
116 "proposal rates come from a tolerance-stopped Bard-Schweitzer solve");
117
118 const std::size_t M = D.empty() ? 0 : D.rows();
119 const std::size_t R = N.size();
120 if (!D.empty() && D.cols() != R)
121 throw InputError("pfqn_mci: D and N disagree on the class count");
122 if (!Z.empty() && Z.size() != R) throw InputError("pfqn_mci: Z has the wrong length");
123 for (int n : N)
124 if (n < 0) throw InputError("pfqn_mci: negative population");
125 if (samples == 0) throw InputError("pfqn_mci: at least one sample is required");
126
127 const T zero = num_traits<T>::from_int(0);
128 std::vector<T> Zv(R, zero);
129 for (std::size_t r = 0; r < Z.size(); ++r) Zv[r] = Z[r];
130
131 // ---- degenerate model: pure delay ------------------------------------
132 T dsum = zero;
133 for (std::size_t i = 0; i < M; ++i)
134 for (std::size_t r = 0; r < R; ++r) dsum += D(i, r);
135 if (M == 0 || dsum < num_traits<T>::from_double(1e-4)) {
136 double lG = 0.0;
137 for (std::size_t r = 0; r < R; ++r) {
138 lG -= mc_log_factorial<T>(N[r]);
139 if (N[r] > 0) lG += N[r] * num_traits<T>::log_as_double(Zv[r]);
140 }
141 return {mc_exp<T>(lG), lG};
142 }
143
144 // ---- proposal rates ---------------------------------------------------
145 std::vector<double> gamma(M);
146 if (variant == MciVariant::Rm) {
147 long Ntot = 0;
148 int Nmax = 0;
149 for (int n : N) {
150 Ntot += n;
151 if (n > Nmax) Nmax = n;
152 }
153 std::vector<T> tput(R, zero);
154 for (std::size_t r = 0; r < R; ++r) {
155 T dsumr = zero;
156 T dmax = D(0, r);
157 for (std::size_t i = 0; i < M; ++i) {
158 dsumr += D(i, r);
159 if (D(i, r) > dmax) dmax = D(i, r);
160 }
161 const T den = dsumr + Zv[r] + dmax * num_traits<T>::from_int(Ntot - 1);
162 if (den == zero) throw NumericError(ErrorKind{"Degenerate"}, "pfqn_mci: zero denominator in the 'rm' bound");
163 tput[r] = num_traits<T>::from_int(N[r]) / den;
164 }
165 for (std::size_t i = 0; i < M; ++i) {
166 T util = zero;
167 for (std::size_t r = 0; r < R; ++r) util += D(i, r) * tput[r];
168 const double u = num_traits<T>::to_double(util);
169 gamma[i] = u > 0.9 ? 1.0 / std::sqrt(static_cast<double>(Nmax)) : 1.0 - u;
170 }
171 } else {
172 std::vector<T> Nt(R), Zt(R);
173 for (std::size_t r = 0; r < R; ++r) {
174 Nt[r] = num_traits<T>::from_int(N[r]);
175 Zt[r] = Zv[r];
176 }
177 const AmvaResult<T> bs = pfqn_bs(D, Nt, Zt);
178 int Nmax = 0;
179 for (int n : N)
180 if (n > Nmax) Nmax = n;
181 for (std::size_t i = 0; i < M; ++i) {
182 T util = zero;
183 for (std::size_t r = 0; r < R; ++r) util += D(i, r) * bs.XN[r];
184 const double u = num_traits<T>::to_double(util);
185 if (variant != MciVariant::Mci) { // Imci, Amci and Lhsmci share the tilt
186 gamma[i] = 1.0 - u > 0.01 ? 1.0 - u : 0.01;
187 } else {
188 gamma[i] = u > 0.9 ? 1.0 / std::sqrt(static_cast<double>(Nmax)) : 1.0 - u;
189 }
190 }
191 }
192 for (std::size_t i = 0; i < M; ++i)
193 if (!(gamma[i] > 0.0))
194 throw NumericError(ErrorKind{"Instability"},
195 "pfqn_mci: a proposal rate is non-positive, the station is saturated beyond what "
196 "the variant's clamp covers");
197
198 // ---- constant part of every log-weight ---------------------------------
199 double lconst = 0.0;
200 for (std::size_t i = 0; i < M; ++i) lconst -= std::log(gamma[i]);
201 for (std::size_t r = 0; r < R; ++r) lconst -= mc_log_factorial<T>(N[r]);
202
203 // ---- exponential draws V(s,i) = -log(u)/gamma_i -------------------------
204 const double tiny = 1.0 / 9007199254740992.0; // keeps log(u) finite at u = 0
205 std::vector<double> Vd(samples * M); // row-major: sample s, station i
206 if (variant == MciVariant::Amci) {
207 const std::size_t Ih = (samples + 1) / 2;
208 for (std::size_t i = 0; i < M; ++i) {
209 for (std::size_t s = 0; s < Ih; ++s) {
210 double u = mc_uniform01(rng);
211 if (u <= 0.0) u = tiny;
212 Vd[s * M + i] = -std::log(u) / gamma[i];
213 if (Ih + s < samples) Vd[(Ih + s) * M + i] = -std::log(1.0 - u) / gamma[i];
214 }
215 }
216 } else if (variant == MciVariant::Lhsmci) {
217 std::vector<std::size_t> perm(samples);
218 for (std::size_t i = 0; i < M; ++i) {
219 for (std::size_t s = 0; s < samples; ++s) perm[s] = s;
220 for (std::size_t s = samples - 1; s > 0; --s) {
221 const std::size_t k = static_cast<std::size_t>(mc_uniform_int(rng, s + 1));
222 const std::size_t t = perm[s];
223 perm[s] = perm[k];
224 perm[k] = t;
225 }
226 for (std::size_t s = 0; s < samples; ++s) {
227 double u = (static_cast<double>(perm[s]) + mc_uniform01(rng)) /
228 static_cast<double>(samples);
229 if (u <= 0.0) u = tiny;
230 Vd[s * M + i] = -std::log(u) / gamma[i];
231 }
232 }
233 } else {
234 for (std::size_t s = 0; s < samples; ++s) {
235 for (std::size_t i = 0; i < M; ++i) {
236 double u = mc_uniform01(rng);
237 if (u <= 0.0) u = tiny;
238 Vd[s * M + i] = -std::log(u) / gamma[i];
239 }
240 }
241 }
242
243 std::vector<double> lZ(samples);
244 std::vector<T> V(M);
245 for (std::size_t s = 0; s < samples; ++s) {
246 double lz = lconst;
247 for (std::size_t i = 0; i < M; ++i) {
248 const double v = Vd[s * M + i];
250 lz -= (1.0 - gamma[i]) * v;
251 }
252 for (std::size_t r = 0; r < R; ++r) {
253 if (N[r] == 0) continue;
254 T inner = Zv[r];
255 for (std::size_t i = 0; i < M; ++i) inner += V[i] * D(i, r);
256 lz += N[r] * num_traits<T>::log_as_double(inner);
257 }
258 lZ[s] = lz;
259 }
260
261 double lG = mc_logmeanexp(lZ);
262 if (!std::isfinite(lG)) {
263 // Floating-point range exception: the reference falls back to the
264 // largest single log-weight, which is the Laplace-style lower bound.
265 double m = -std::numeric_limits<double>::infinity();
266 for (double v : lZ)
267 if (v > m) m = v;
268 lG = m;
269 }
270 return {mc_exp<T>(lG), lG};
271}
272
273/** Reference defaults: 1e5 samples, the IMCI proposal. */
274template <class T>
275NcResult<T> pfqn_mci(const Matrix<T>& D, const std::vector<int>& N, const std::vector<T>& Z,
276 McRng& rng) {
277 return pfqn_mci(D, N, Z, static_cast<std::size_t>(100000), MciVariant::Imci, rng);
278}
279
280} // namespace pfqn
281} // namespace line
282
283#endif // LINE_API_PFQN_MCI_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
bool empty() const
Definition matrix.h:92
NumericError(const std::string &what)
Definition error.h:112
The exception types the port throws.
Dense matrix and non-owning view.
NcResult< T > pfqn_mci(const Matrix< T > &D, const std::vector< int > &N, const std::vector< T > &Z, std::size_t samples, MciVariant variant, McRng &rng)
Monte Carlo Integration estimate of the normalizing constant of a closed product-form network (Ross,...
Definition pfqn_mci.h:111
double mc_log_factorial(long n)
log(n!) for a non-negative integer n, the factln / gammaln(1+n) of the references,...
std::mt19937_64 McRng
The generator type every Monte Carlo entry point in this tree accepts.
double mc_logmeanexp(const std::vector< double > &v)
log(mean(exp(v))), computed by factoring out the maximum so that the exponentials stay in range.
std::uint64_t mc_uniform_int(McRng &g, std::uint64_t n)
Uniform integer on [0, n), unbiased by rejection.
double mc_uniform01(McRng &g)
Uniform deviate on [0,1) with 53 significant bits, as a double.
MciVariant
The proposal-rate rules the reference selects between.
Definition pfqn_mci.h:91
@ Imci
gamma = max(0.01, 1 - U), the MonteQueue 2.0 recommendation
Definition pfqn_mci.h:92
@ Amci
the Imci tilt, antithetic uniform pairs (u, 1-u) (Ross and Wang)
Definition pfqn_mci.h:95
@ Lhsmci
the Imci tilt, Latin hypercube stratified uniforms per station
Definition pfqn_mci.h:96
@ Mci
gamma = 1/sqrt(max N) where U > 0.9, else 1 - U
Definition pfqn_mci.h:93
@ Rm
repairman: rates from the balanced job bound, column-max reading
Definition pfqn_mci.h:94
AmvaResult< T > pfqn_bs(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, const std::vector< AmvaSched > &type, double tol=1e-6, std::size_t maxiter=1000, const Matrix< T > &QN0=Matrix< T >())
Bard-Schweitzer approximate MVA.
Definition pfqn_bs.h:73
T mc_exp(double lv)
exp of a log-domain value, materialized in the working arithmetic.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
Number-type abstraction for the templated API port.
Bard-Schweitzer approximate MVA.
Convolution algorithm for the exact normalizing constant of a closed product-form network (Buzen 1973...
Randomness scaffolding shared by the Monte Carlo normalizing-constant estimators (pfqn_mci,...
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
std::vector< T > XN
(R) throughput
Definition pfqn_bs.h:49
Return value of the normalizing-constant family, mirroring Ret.pfqnNc.
Definition pfqn_ca.h:44