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.
16 *
17 * The McKenna-Mitra integral form of the constant,
18 *
19 * 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,
20 *
21 * is estimated by importance sampling with an independent exponential proposal
22 * of rate gamma_i per station. Writing V ~ prod_i Exp(gamma_i), one draw
23 * contributes
24 *
25 * lZ = -sum_i (1 - gamma_i) V_i - sum_i log gamma_i - sum_r log N_r!
26 * + sum_r N_r log(sum_i V_i D(i,r) + Z_r),
27 *
28 * and lG = log mean exp(lZ). The proposal rates come from a Bard-Schweitzer
29 * pre-solve: gamma_i = max(0.01, 1 - U_i) in the IMCI variant, and the
30 * saturation-aware gamma_i = 1/sqrt(max N) when U_i > 0.9 in the plain MCI
31 * variant. Overshooting rates make the estimator heavy-tailed, which is why
32 * the reference clamps them.
33 *
34 * Deviations from the reference, both deliberate:
35 *
36 * - MATLAB caches the uniform matrix in a PERSISTENT variable VL and reuses
37 * it across calls (slicing VL(1:I,1:M)), so two successive calls with the
38 * same shape return the SAME estimate and a call with a larger I silently
39 * reuses the old columns. That is global hidden state; here the deviates
40 * come from the caller's generator, one draw per station per sample.
41 *
42 * - The 'rm' variant is REFUSED for M > 1 rather than reproduced. Its rate
43 * line, tput = N./(sum(D,1)+Z+max(D,1)*(sum(N)-1)), broadcasts a (1 x R)
44 * row against the (M x R) matrix max(D,1) and yields an (M x R) "throughput"
45 * whose product D*tput' is (M x M); the subsequent loop then reads gamma
46 * off the wrong axis. The expression is only dimensionally meaningful for a
47 * single station, which is the repairman model the variant is named for, so
48 * that is the only shape accepted here. See the report accompanying this
49 * port; nothing has been substituted for the M > 1 case.
50 *
51 * Arithmetic: INEXACT BY CONSTRUCTION. The value is a random variable, the
52 * integrand is evaluated through logs, and the proposal rates come from a
53 * tolerance-stopped Bard-Schweitzer solve; all three require transcendental
54 * arithmetic.
55 *
56 * RNG contract: see pfqn_mc_common.h. Comparable to MATLAB only in
57 * distribution, never stream for stream; reproducible within this port only
58 * when the generator is passed in the same state.
59 */
60
61#include <cmath>
62#include <cstddef>
63#include <vector>
64
68#include "line/num/number.h"
69#include "line/util/error.h"
70#include "line/util/matrix.h"
71
72namespace line {
73namespace pfqn {
74
75/** The proposal-rate rules the reference selects between. */
76enum class MciVariant {
77 Imci, ///< gamma = max(0.01, 1 - U), the MonteQueue 2.0 recommendation
78 Mci, ///< gamma = 1/sqrt(max N) where U > 0.9, else 1 - U
79 Rm ///< repairman: a single station, rates from the balanced bound
80};
81
82/**
83 * @brief Monte Carlo Integration estimate of the normalizing constant of a
84 * closed product-form network (Ross, Wang and Yao; MonteQueue 2.0).
85 *
86 * @param D (M x R) service demands
87 * @param N (R) population per class
88 * @param Z (R) think times; empty for none
89 * @param samples number of Monte Carlo samples
90 * @param variant proposal-rate rule
91 * @param rng explicit generator, advanced by the call
92 */
93template <class T>
94NcResult<T> pfqn_mci(const Matrix<T>& D, const std::vector<int>& N, const std::vector<T>& Z,
95 std::size_t samples, MciVariant variant, McRng& rng) {
97 "pfqn_mci requires transcendental arithmetic: it is a Monte Carlo estimator over "
98 "an exponential proposal, its integrand is evaluated in the log domain, and its "
99 "proposal rates come from a tolerance-stopped Bard-Schweitzer solve");
100
101 const std::size_t M = D.empty() ? 0 : D.rows();
102 const std::size_t R = N.size();
103 if (!D.empty() && D.cols() != R)
104 throw InputError("pfqn_mci: D and N disagree on the class count");
105 if (!Z.empty() && Z.size() != R) throw InputError("pfqn_mci: Z has the wrong length");
106 for (int n : N)
107 if (n < 0) throw InputError("pfqn_mci: negative population");
108 if (samples == 0) throw InputError("pfqn_mci: at least one sample is required");
109
110 const T zero = num_traits<T>::from_int(0);
111 std::vector<T> Zv(R, zero);
112 for (std::size_t r = 0; r < Z.size(); ++r) Zv[r] = Z[r];
113
114 // ---- degenerate model: pure delay ------------------------------------
115 T dsum = zero;
116 for (std::size_t i = 0; i < M; ++i)
117 for (std::size_t r = 0; r < R; ++r) dsum += D(i, r);
118 if (M == 0 || dsum < num_traits<T>::from_double(1e-4)) {
119 double lG = 0.0;
120 for (std::size_t r = 0; r < R; ++r) {
121 lG -= mc_log_factorial<T>(N[r]);
122 if (N[r] > 0) lG += N[r] * num_traits<T>::log_as_double(Zv[r]);
123 }
124 return {mc_exp<T>(lG), lG};
125 }
126
127 // ---- proposal rates ---------------------------------------------------
128 std::vector<double> gamma(M);
129 if (variant == MciVariant::Rm) {
130 if (M != 1)
131 throw UnsupportedError(
132 "pfqn_mci: the 'rm' variant is defined only for a single station; for M > 1 the "
133 "reference's rate expression broadcasts a (1 x R) row against an (M x R) matrix "
134 "and reads gamma off the wrong axis, so nothing faithful can be computed");
135 long Ntot = 0;
136 int Nmax = 0;
137 for (int n : N) {
138 Ntot += n;
139 if (n > Nmax) Nmax = n;
140 }
141 T util = zero;
142 for (std::size_t r = 0; r < R; ++r) {
143 const T dmax = D(0, r) > num_traits<T>::from_int(1) ? D(0, r) : num_traits<T>::from_int(1);
144 const T den = D(0, r) + Zv[r] + dmax * num_traits<T>::from_int(Ntot - 1);
145 if (den == zero) throw NumericError("pfqn_mci: zero denominator in the 'rm' bound");
146 util += D(0, r) * (num_traits<T>::from_int(N[r]) / den);
147 }
148 const double u = num_traits<T>::to_double(util);
149 gamma[0] = u > 0.9 ? 1.0 / std::sqrt(static_cast<double>(Nmax)) : 1.0 - u;
150 } else {
151 std::vector<T> Nt(R), Zt(R);
152 for (std::size_t r = 0; r < R; ++r) {
153 Nt[r] = num_traits<T>::from_int(N[r]);
154 Zt[r] = Zv[r];
155 }
156 const AmvaResult<T> bs = pfqn_bs(D, Nt, Zt);
157 int Nmax = 0;
158 for (int n : N)
159 if (n > Nmax) Nmax = n;
160 for (std::size_t i = 0; i < M; ++i) {
161 T util = zero;
162 for (std::size_t r = 0; r < R; ++r) util += D(i, r) * bs.XN[r];
163 const double u = num_traits<T>::to_double(util);
164 if (variant == MciVariant::Imci) {
165 gamma[i] = 1.0 - u > 0.01 ? 1.0 - u : 0.01;
166 } else {
167 gamma[i] = u > 0.9 ? 1.0 / std::sqrt(static_cast<double>(Nmax)) : 1.0 - u;
168 }
169 }
170 }
171 for (std::size_t i = 0; i < M; ++i)
172 if (!(gamma[i] > 0.0))
173 throw NumericError(
174 "pfqn_mci: a proposal rate is non-positive, the station is saturated beyond what "
175 "the variant's clamp covers");
176
177 // ---- constant part of every log-weight ---------------------------------
178 double lconst = 0.0;
179 for (std::size_t i = 0; i < M; ++i) lconst -= std::log(gamma[i]);
180 for (std::size_t r = 0; r < R; ++r) lconst -= mc_log_factorial<T>(N[r]);
181
182 std::vector<double> lZ(samples);
183 std::vector<T> V(M);
184 for (std::size_t s = 0; s < samples; ++s) {
185 double lz = lconst;
186 for (std::size_t i = 0; i < M; ++i) {
187 double u = mc_uniform01(rng);
188 if (u <= 0.0) u = 1.0 / 9007199254740992.0;
189 const double v = -std::log(u) / gamma[i];
191 lz -= (1.0 - gamma[i]) * v;
192 }
193 for (std::size_t r = 0; r < R; ++r) {
194 if (N[r] == 0) continue;
195 T inner = Zv[r];
196 for (std::size_t i = 0; i < M; ++i) inner += V[i] * D(i, r);
197 lz += N[r] * num_traits<T>::log_as_double(inner);
198 }
199 lZ[s] = lz;
200 }
201
202 double lG = mc_logmeanexp(lZ);
203 if (!std::isfinite(lG)) {
204 // Floating-point range exception: the reference falls back to the
205 // largest single log-weight, which is the Laplace-style lower bound.
206 double m = -std::numeric_limits<double>::infinity();
207 for (double v : lZ)
208 if (v > m) m = v;
209 lG = m;
210 }
211 return {mc_exp<T>(lG), lG};
212}
213
214/** Reference defaults: 1e5 samples, the IMCI proposal. */
215template <class T>
216NcResult<T> pfqn_mci(const Matrix<T>& D, const std::vector<int>& N, const std::vector<T>& Z,
217 McRng& rng) {
218 return pfqn_mci(D, N, Z, static_cast<std::size_t>(100000), MciVariant::Imci, rng);
219}
220
221} // namespace pfqn
222} // namespace line
223
224#endif // LINE_API_PFQN_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
bool empty() const
Definition matrix.h:92
NumericError(const std::string &what)
Definition error.h:45
UnsupportedError(const std::string &what)
Definition error.h:51
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:94
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.
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:76
@ Imci
gamma = max(0.01, 1 - U), the MonteQueue 2.0 recommendation
Definition pfqn_mci.h:77
@ Mci
gamma = 1/sqrt(max N) where U > 0.9, else 1 - U
Definition pfqn_mci.h:78
@ Rm
repairman: a single station, rates from the balanced bound
Definition pfqn_mci.h:79
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.
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,...
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