LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_mc_common.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_MC_COMMON_H
6#define LINE_API_PFQN_MC_COMMON_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Randomness scaffolding shared by the Monte Carlo normalizing-constant
12 * estimators (pfqn_mci, pfqn_is, pfqn_ld_is, pfqn_oi_is, pfqn_pas_is, pfqn_ls,
13 * pfqn_mmsample2) and by the perfect sampler pfqn_cftp.
14 *
15 * This header is NOT a port of a MATLAB function. It exists because the MATLAB
16 * references draw from the global MATLAB stream (rand / randi / mvnrnd, seeded
17 * out of band by `rng(options.seed)`), and a library must not carry a hidden
18 * global stream. Every estimator in this tree therefore takes a
19 * `std::mt19937_64&` as an explicit argument and consumes it through the
20 * helpers below.
21 *
22 * REPRODUCIBILITY CONTRACT, which every estimator's own header repeats:
23 *
24 * - The estimate is comparable to MATLAB only IN DISTRIBUTION, never stream
25 * for stream. MATLAB's Mersenne Twister, its uniform-to-integer mapping and
26 * its normal transform all differ from the ones here, so the same seed does
27 * NOT produce the same sample path and the two estimates agree only up to
28 * Monte Carlo error. What IS comparable is the estimand: both target the
29 * exact constant of pfqn_ca, and both converge to it at the 1/sqrt(n) rate.
30 *
31 * - Within this port, reproducibility requires passing a generator in the
32 * same state. Two calls with generators seeded identically, on identical
33 * inputs, in the same build, produce bit-identical output; the generator is
34 * advanced by the call, so a second call on the same generator object does
35 * not repeat the first. No estimator seeds, re-seeds or copies the
36 * generator internally.
37 *
38 * - The uniform-to-integer map here is rejection based, not modulo based, so
39 * it is unbiased and identical on every platform and standard-library
40 * version. std::uniform_int_distribution and std::normal_distribution are
41 * deliberately avoided: their output is implementation defined, which would
42 * make a fixed-seed regression test non-portable.
43 *
44 * Arithmetic: everything here is inherently inexact (uniform deviates are
45 * dyadic approximations of a continuous law, the normal transform needs
46 * log/cos), so each consumer gates on num_traits<T>::has_transcendental.
47 */
48
49#include <cmath>
50#include <cstdint>
51#include <limits>
52#include <random>
53#include <vector>
54
55#include "line/num/number.h"
56#include "line/util/error.h"
57
58namespace line {
59namespace pfqn {
60
61/** The generator type every Monte Carlo entry point in this tree accepts. */
62using McRng = std::mt19937_64;
63
64/**
65 * Uniform deviate on [0,1) with 53 significant bits, as a double. The top 53
66 * bits of one 64-bit draw are used, so exactly one generator step is consumed
67 * per deviate and the mapping is fully specified.
68 */
69inline double mc_uniform01(McRng& g) {
70 return static_cast<double>(g() >> 11) * (1.0 / 9007199254740992.0);
71}
72
73/** The same deviate materialized in the working arithmetic. */
74template <class T>
78
79/**
80 * Uniform integer on [0, n), unbiased by rejection. Consumes one generator
81 * step per attempt; the rejection probability is below 2^-64 * n, so for the
82 * class counts these estimators use it never rejects in practice.
83 */
84inline std::uint64_t mc_uniform_int(McRng& g, std::uint64_t n) {
85 if (n == 0) throw InputError("mc_uniform_int: empty range");
86 if (n == 1) return 0;
87 const std::uint64_t threshold = (0u - n) % n; // 2^64 mod n
88 std::uint64_t r;
89 do {
90 r = g();
91 } while (r < threshold);
92 return r % n;
93}
94
95/**
96 * Standard normal deviate by the Box-Muller transform. Two uniforms are drawn
97 * and only the cosine branch is kept, so the routine holds no state between
98 * calls: a generator handed to two different estimators cannot be
99 * cross-contaminated by a cached second variate.
100 */
101inline double mc_normal01(McRng& g) {
102 double u1 = mc_uniform01(g);
103 const double u2 = mc_uniform01(g);
104 // log(0) would be -inf; the smallest representable positive deviate keeps
105 // the transform finite without perturbing the distribution measurably.
106 if (u1 <= 0.0) u1 = 1.0 / 9007199254740992.0;
107 return std::sqrt(-2.0 * std::log(u1)) * std::cos(6.283185307179586476925286766559 * u2);
108}
109
110/**
111 * log(mean(exp(v))), computed by factoring out the maximum so that the
112 * exponentials stay in range. MATLAB's logmeanexp, which every estimator that
113 * averages log-weights calls.
114 */
115inline double mc_logmeanexp(const std::vector<double>& v) {
116 if (v.empty()) return -std::numeric_limits<double>::infinity();
117 double m = -std::numeric_limits<double>::infinity();
118 for (double x : v)
119 if (x > m) m = x;
120 if (!std::isfinite(m)) return m;
121 double acc = 0.0;
122 for (double x : v) acc += std::exp(x - m);
123 return m + std::log(acc / static_cast<double>(v.size()));
124}
125
126/**
127 * exp of a log-domain value, materialized in the working arithmetic. Overflows
128 * to infinity in double exactly where the references do, and stays in range
129 * for the high-precision backends.
130 */
131template <class T>
132T mc_exp(double lv) {
133 using std::exp;
134 return exp(num_traits<T>::from_double(lv));
135}
136
137/**
138 * log(n!) for a non-negative integer n, the factln / gammaln(1+n) of the
139 * references, accumulated in the working arithmetic so the high-precision
140 * backends do not lose the digits a double lgamma would drop.
141 */
142template <class T>
143double mc_log_factorial(long n) {
144 if (n < 0) throw InputError("mc_log_factorial: negative argument");
145 if (n < 2) return 0.0;
146 return num_traits<T>::log_as_double(num_factorial<T>(static_cast<unsigned>(n)));
147}
148
149} // namespace pfqn
150} // namespace line
151
152#endif // LINE_API_PFQN_MC_COMMON_H
InputError(const std::string &what)
Definition error.h:39
The exception types the port throws.
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.
T mc_exp(double lv)
exp of a log-domain value, materialized in the working arithmetic.
double mc_normal01(McRng &g)
Standard normal deviate by the Box-Muller transform.
T mc_uniform(McRng &g)
The same deviate materialized in the working arithmetic.
T num_factorial(unsigned n)
Factorial as a value of T.
Definition number.h:184
Number-type abstraction for the templated API port.