LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_mmsample2.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_MMSAMPLE2_H
6#define LINE_API_PFQN_MMSAMPLE2_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Sampled McKenna-Mitra integral form of the normalizing constant of a
12 * repairman (single-queue plus delay) model.
13 *
14 * Templated port of matlab/src/api/pfqn/pfqn_mmsample2.m, cross-checked
15 * against jar/src/main/java/jline/api/pfqn/nc/Pfqn_mmsample2.java.
16 *
17 * For a single queue the McKenna-Mitra form collapses to the one-dimensional
18 * integral
19 *
20 * G(N) = 1/prod_r N_r! int_0^inf e^{-v} prod_r ((Z_r + L_r) v)^{N_r} dv,
21 *
22 * whose log-integrand at the grid point v is
23 *
24 * f(v) = -v + sum(N) log v + sum_r N_r log(Z_r + L_r).
25 *
26 * The reference lays a grid of ceil(samples/2) uniform points on [0,1) plus
27 * ceil(samples/2) points log-spaced over [1, 1e5], rescaling the demands so
28 * that every coefficient is at least one, and reports
29 *
30 * lG = max_i (du_i + f(v_i)) - sum_r log N_r! + sum(N) log(scaleFactor).
31 *
32 * DEFECT IN THE REFERENCE, reproduced here rather than repaired. The estimator
33 * takes the MAXIMUM of the weighted log-integrand instead of summing the
34 * quadrature contributions log sum_i du_i exp(f(v_i)). Two consequences:
35 *
36 * - the width of the integrand is discarded, so the estimate is short by the
37 * Laplace factor sqrt(2 pi sum(N)): with the exact value being
38 * log((sum N)!) + sum_r N_r log(Z_r+L_r) - sum_r log N_r!, the returned
39 * value is Stirling's leading term without the sqrt(2 pi n) correction, a
40 * systematic relative shortfall of about 1/sqrt(2 pi sum(N)) in the log and
41 * a factor sqrt(2 pi sum(N)) in G itself;
42 * - the grid v is NOT sorted (uniform points first, then an increasing
43 * log-spaced block), so du = [0, diff(v)] alternates in sign and is not a
44 * quadrature weight at all; it merely perturbs the argmax.
45 *
46 * Because of this the routine does not converge to the exact constant as the
47 * sample count grows, and its test in this tree asserts the reference's own
48 * behaviour and the size of the shortfall, not agreement with pfqn_ca.
49 *
50 * Only the FIRST row of L is read, matching the reference: this is a
51 * single-queue model and additional rows are ignored.
52 *
53 * Arithmetic: INEXACT BY CONSTRUCTION. The grid is random, the integrand is
54 * evaluated in the log domain, and the log-spaced block is generated by
55 * exponentiation.
56 *
57 * RNG contract: see pfqn_mc_common.h. Comparable to MATLAB only in
58 * distribution, never stream for stream; reproducible within this port only
59 * when the generator is passed in the same state.
60 */
61
62#include <cmath>
63#include <cstddef>
64#include <limits>
65#include <vector>
66
69#include "line/num/number.h"
70#include "line/util/error.h"
71#include "line/util/matrix.h"
72
73namespace line {
74namespace pfqn {
75
76/**
77 * @brief Sampled McKenna-Mitra integral form of the normalizing constant of a
78 * repairman (single-queue plus delay) model.
79 *
80 * @param L (M x R) demands; only row 0 is read, as in the reference
81 * @param N (R) population per class
82 * @param Z (R) think times
83 * @param samples grid size; half uniform on [0,1), half log-spaced on [1,1e5]
84 * @param rng explicit generator, advanced by the call
85 */
86template <class T>
87NcResult<T> pfqn_mmsample2(const Matrix<T>& L, const std::vector<int>& N, const std::vector<T>& Z,
88 std::size_t samples, McRng& rng) {
90 "pfqn_mmsample2 requires transcendental arithmetic: it samples a random "
91 "quadrature grid and evaluates a log-domain integrand on it");
92
93 const std::size_t R = N.size();
94 if (L.empty()) throw InputError("pfqn_mmsample2: empty demand matrix");
95 if (L.cols() != R) throw InputError("pfqn_mmsample2: L and N disagree on the class count");
96 if (Z.size() != R) throw InputError("pfqn_mmsample2: Z has the wrong length");
97 for (int n : N)
98 if (n < 0) throw InputError("pfqn_mmsample2: negative population");
99 if (samples == 0) throw InputError("pfqn_mmsample2: at least one grid point is required");
100
101 // ---- rescale so that every coefficient is at least one -----------------
102 T mn = L(0, 0);
103 for (std::size_t i = 0; i < L.rows(); ++i)
104 for (std::size_t r = 0; r < R; ++r)
105 if (L(i, r) < mn) mn = L(i, r);
106 for (std::size_t r = 0; r < R; ++r)
107 if (Z[r] < mn) mn = Z[r];
108 const T scale = num_traits<T>::from_double(1e-7) + mn;
109 if (!(scale > num_traits<T>::from_int(0)))
110 throw NumericError("pfqn_mmsample2: non-positive scale factor, demands must be positive");
111
112 // sum_r N_r log((Z_r + L_r)/scale), the v-independent part of f
113 double lcoef = 0.0;
114 for (std::size_t r = 0; r < R; ++r) {
115 if (N[r] == 0) continue;
116 const T coef = (Z[r] + L(0, r)) / scale;
117 lcoef += N[r] * num_traits<T>::log_as_double(coef);
118 }
119 long Ntot = 0;
120 for (int n : N) Ntot += n;
121
122 // ---- the grid: uniform block, then the log-spaced block ----------------
123 const std::size_t nu = static_cast<std::size_t>(
124 std::ceil(0.5 * static_cast<double>(samples)));
125 const std::size_t nl = static_cast<std::size_t>(
126 std::ceil(0.5 * static_cast<double>(samples)));
127 std::vector<double> v;
128 v.reserve(nu + nl);
129 for (std::size_t i = 0; i < nu; ++i) v.push_back(mc_uniform01(rng));
130 for (std::size_t i = 0; i < nl; ++i) {
131 const double t = nl == 1 ? 0.0 : 5.0 * static_cast<double>(i) / static_cast<double>(nl - 1);
132 v.push_back(std::pow(10.0, t));
133 }
134
135 double best = -std::numeric_limits<double>::infinity();
136 for (std::size_t i = 0; i < v.size(); ++i) {
137 // du = [0, diff(v)]: the reference's (sign-indefinite) weight term.
138 const double du = i == 0 ? 0.0 : v[i] - v[i - 1];
139 if (v[i] <= 0.0) continue; // log(0) would be -inf and never wins the max
140 const double f = du - v[i] + static_cast<double>(Ntot) * std::log(v[i]) + lcoef;
141 if (f > best) best = f;
142 }
143
144 double lG = best;
145 for (std::size_t r = 0; r < R; ++r) lG -= mc_log_factorial<T>(N[r]);
146 lG += static_cast<double>(Ntot) * num_traits<T>::log_as_double(scale);
147 return {mc_exp<T>(lG), lG};
148}
149
150/** Reference call shape with an explicit grid size. */
151template <class T>
152NcResult<T> pfqn_mmsample2(const Matrix<T>& L, const std::vector<int>& N, const std::vector<T>& Z,
153 McRng& rng) {
154 return pfqn_mmsample2(L, N, Z, static_cast<std::size_t>(100000), rng);
155}
156
157} // namespace pfqn
158} // namespace line
159
160#endif // LINE_API_PFQN_MMSAMPLE2_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
The exception types the port throws.
Dense matrix and non-owning view.
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.
NcResult< T > pfqn_mmsample2(const Matrix< T > &L, const std::vector< int > &N, const std::vector< T > &Z, std::size_t samples, McRng &rng)
Sampled McKenna-Mitra integral form of the normalizing constant of a repairman (single-queue plus del...
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.
Number-type abstraction for the templated API port.
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,...
Return value of the normalizing-constant family, mirroring Ret.pfqnNc.
Definition pfqn_ca.h:44