LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
sim_runlength.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_SIM_RUNLENGTH_H
6#define LINE_API_SIM_RUNLENGTH_H
7
8/**
9 * @file
10 * @ingroup api_sim
11 * Run-length planning for steady-state simulation.
12 *
13 * Templated port of matlab/src/api/sim/sim_runlength.m, sim_asymvar_mm1.m and
14 * sim_asymvar_ctmc.m, cross-checked against
15 * jar/src/main/java/jline/api/sim/SimRunlength.java.
16 *
17 * THE QUANTITY THAT MATTERS is not the variance of the process but its
18 * ASYMPTOTIC VARIANCE sigma^2 = lim t Var(time-average over [0,t]), twice the
19 * integral of the autocovariance: a time average of a positively correlated
20 * process converges at rate sigma^2/t, not Var(X)/t. Then
21 *
22 * t* = (z/eps)^2 sigma^2 / mean^2
23 *
24 * is the run needed for relative precision eps at confidence 1-alpha.
25 *
26 * For M/M/1, sigma^2 = 2 rho(1+rho)/(mu (1-rho)^4) in closed form; the FOURTH
27 * power is the whole story, and dividing by the squared mean leaves a run length
28 * growing like (1-rho)^-2. Checked here against the general CTMC deviation-vector
29 * computation, which agrees to 1e-6 at rho up to 0.9.
30 *
31 * ARITHMETIC. The normal quantile needs erfc, so the planner is transcendental;
32 * the CTMC asymptotic variance is a linear solve and stays exact.
33 *
34 * Reference: W. Whitt (1989). Planning queueing simulations. Management Science
35 * 35(11), 1341-1366.
36 */
37
38#include <cmath>
39#include <limits>
40#include <cstddef>
41#include <vector>
42
43#include "line/num/number.h"
44#include "line/util/error.h"
45#include "line/util/lu.h"
46#include "line/util/matrix.h"
47
48namespace line {
49namespace sim {
50
51/** Second-order description of a steady-state estimator. */
52template <class T>
54 T mean; ///< the steady-state mean
55 T variance; ///< Var of the process itself
56 T asymptoticVariance; ///< sigma^2, what the run length depends on
57 T relaxationTime; ///< sigma^2/Var, the correlation time scale
58 std::vector<T> deviation; ///< the deviation vector, for the CTMC form
59};
60
61/** Outcome of the run-length plan. */
62template <class T>
65 T z; ///< the two-sided normal quantile used
66 T halfWidth; ///< the half-width a supplied run buys
67 T achievedRelPrecision; ///< that half-width over the mean
68 bool hasRun = false; ///< whether a run length was supplied
69};
70
71/**
72 * Asymptotic variance of the M/M/1 number-in-system process.
73 *
74 * @param lambda arrival rate
75 * @param mu service rate
76 */
77template <class T>
78AsymVarResult<T> sim_asymvar_mm1(const T& lambda, const T& mu) {
79 const T zero = num_traits<T>::from_int(0);
80 const T one = num_traits<T>::from_int(1);
81 const T two = num_traits<T>::from_int(2);
82 if (lambda <= zero || mu <= zero)
83 throw InputError("sim_asymvar_mm1: the arrival and service rates must be positive");
84 const T rho = lambda / mu;
85 if (rho >= one) throw InputError("sim_asymvar_mm1: the queue must be stable, rho < 1");
87 r.mean = rho / (one - rho);
88 r.variance = rho / ((one - rho) * (one - rho));
89 const T d = (one - rho) * (one - rho) * (one - rho) * (one - rho);
90 r.asymptoticVariance = two * rho * (one + rho) / (mu * d);
91 r.relaxationTime = r.variance > zero ? T(r.asymptoticVariance / r.variance) : zero;
92 return r;
93}
94
95/**
96 * Asymptotic variance of a reward on a CTMC: 2 sum_x pi(x)g(x)d(x) with
97 * g = f - E_pi[f] and A d = -g, pi d = 0. The normalization is what pins d: A
98 * alone is singular, since a constant may be added without changing sigma^2.
99 *
100 * @param A the generator, rows summing to zero
101 * @param f the reward attached to each state
102 * @param pi the stationary distribution; solved for when empty
103 */
104template <class T>
105AsymVarResult<T> sim_asymvar_ctmc(const Matrix<T>& A, const std::vector<T>& f,
106 const std::vector<T>& pi = std::vector<T>()) {
107 const T zero = num_traits<T>::from_int(0);
108 const T one = num_traits<T>::from_int(1);
109 const std::size_t n = A.rows();
110 if (A.cols() != n) throw InputError("sim_asymvar_ctmc: the generator must be square");
111 if (f.size() != n) throw InputError("sim_asymvar_ctmc: one reward per state is required");
112 for (std::size_t i = 0; i < n; ++i) {
113 T row = zero;
114 for (std::size_t j = 0; j < n; ++j) row += A(i, j);
115 if (num_abs(row) > num_traits<T>::from_double(1e-8))
116 throw InputError("sim_asymvar_ctmc: the generator rows must sum to zero");
117 }
118 std::vector<T> p = pi;
119 if (p.empty()) {
120 // pi A = 0 with sum pi = 1, as a square system.
121 Matrix<T> M(n, n);
122 std::vector<T> b(n, zero);
123 for (std::size_t j = 0; j + 1 < n; ++j)
124 for (std::size_t i = 0; i < n; ++i) M(j, i) = A(i, j);
125 for (std::size_t i = 0; i < n; ++i) M(n - 1, i) = one;
126 b[n - 1] = one;
127 p = solve(M, b);
128 }
129 T mean = zero;
130 for (std::size_t i = 0; i < n; ++i) mean += p[i] * f[i];
131 std::vector<T> g(n);
132 for (std::size_t i = 0; i < n; ++i) g[i] = f[i] - mean;
133 // A d = -g pins d only up to a constant, so one equation of A is redundant
134 // and one normalization replaces it. WHICH equation is dropped matters: the
135 // rows of A are related by pi A = 0, so a row whose pi is tiny is only
136 // nominally redundant, and dropping it loses real information -- on a queue
137 // truncated where pi has underflowed, that alone puts sigma^2 out by orders
138 // of magnitude. Dropping the row with the LARGEST pi is the well-conditioned
139 // choice.
140 std::size_t drop = 0;
141 for (std::size_t i = 1; i < n; ++i)
142 if (p[i] > p[drop]) drop = i;
143 Matrix<T> M2(n, n);
144 std::vector<T> b2(n, zero);
145 std::size_t r0 = 0;
146 for (std::size_t i = 0; i < n; ++i) {
147 if (i == drop) continue;
148 for (std::size_t j = 0; j < n; ++j) M2(r0, j) = A(i, j);
149 b2[r0] = -g[i];
150 ++r0;
151 }
152 for (std::size_t j = 0; j < n; ++j) M2(n - 1, j) = p[j];
153 const std::vector<T> d = solve(M2, b2);
155 r.mean = mean;
156 r.variance = zero;
157 r.asymptoticVariance = zero;
158 for (std::size_t i = 0; i < n; ++i) {
159 r.variance += p[i] * g[i] * g[i];
160 r.asymptoticVariance += p[i] * g[i] * d[i];
161 }
163 r.relaxationTime = r.variance > zero ? T(r.asymptoticVariance / r.variance) : zero;
164 r.deviation = d;
165 return r;
166}
167
168/**
169 * Run length for a steady-state estimate of a given relative precision.
170 *
171 * @param mean the steady-state mean being estimated
172 * @param asymVar sigma^2 of that estimator
173 * @param relPrecision the target half-width as a fraction of the mean
174 * @param confidence the confidence level of the interval
175 * @param runLength an actual run length, to report the precision it buys;
176 * non-positive to skip
177 */
178template <class T>
179RunLengthResult<T> sim_runlength(const T& mean, const T& asymVar,
180 const T& relPrecision = num_traits<T>::from_rational(1, 20),
181 const T& confidence = num_traits<T>::from_rational(19, 20),
182 const T& runLength = num_traits<T>::from_int(0)) {
183 static_assert(num_traits<T>::has_transcendental, "sim_runlength needs erfc for the quantile");
184 using std::erfc;
185 using std::sqrt;
186 const T zero = num_traits<T>::from_int(0);
187 const T one = num_traits<T>::from_int(1);
188 const T two = num_traits<T>::from_int(2);
189 if (mean == zero)
190 throw InputError("sim_runlength: a relative precision is meaningless for a zero mean");
191 if (asymVar < zero) throw InputError("sim_runlength: the asymptotic variance cannot be negative");
192 if (relPrecision <= zero) throw InputError("sim_runlength: the relative precision must be positive");
193 if (confidence <= zero || confidence >= one)
194 throw InputError("sim_runlength: the confidence must lie in (0,1)");
195 // Two-sided normal quantile, by bisection on erfc.
196 T lo = zero, hi = num_traits<T>::from_int(40);
197 const T target = one - confidence;
198 for (int i = 0; i < 200; ++i) {
199 const T mid = (lo + hi) / two;
200 if (erfc(mid / sqrt(two)) > target) {
201 lo = mid;
202 } else {
203 hi = mid;
204 }
205 }
207 r.z = (lo + hi) / two;
208 r.requiredRunLength = (r.z / relPrecision) * (r.z / relPrecision) * asymVar / (mean * mean);
209 r.halfWidth = zero;
210 r.achievedRelPrecision = zero;
211 if (runLength > zero) {
212 r.hasRun = true;
213 r.halfWidth = r.z * sqrt(asymVar / runLength);
215 }
216 return r;
217}
218
219/** The plan of `sim_runlength_plan`: what the run should have been. */
220template <class T>
222 T relPrecision; ///< the precision planned for
223 T confidence; ///< the level the half-widths were computed at
224 T samplesUsed; ///< the run length they came from
225 Matrix<T> asymptoticVariance; ///< sigma^2 per (station, class), NaN where unplannable
226 Matrix<T> requiredSamples; ///< the run length that reaches relPrecision
227};
228
229/**
230 * How long a simulation run should have been, from the one it already did.
231 *
232 * A batch-means half-width H at confidence 1-alpha over a run of N samples pins
233 * the ASYMPTOTIC variance of the estimator,
234 *
235 * sigma^2 = (H/z)^2 N, z = Phi^-1((1+confidence)/2),
236 *
237 * and that is the quantity a run length is planned from -- NOT the stationary
238 * variance, which on M/M/1 differs from it by a factor blowing up like
239 * (1-rho)^-2. `sim_runlength` then turns it into the sample count that reaches
240 * a requested RELATIVE precision.
241 *
242 * An entry with a non-positive mean or half-width is left NaN, since there is
243 * nothing to plan from there.
244 *
245 * Reference: W. Whitt (1989). Planning queueing simulations. Management Science
246 * 35(11), 1341-1366.
247 */
248template <class T>
250 const T& samplesUsed,
251 const T& relPrecision = num_traits<T>::from_rational(1, 20),
252 const T& confidence = num_traits<T>::from_rational(19, 20)) {
254 "sim_runlength_plan needs erfc for the quantile");
255 const T zero = num_traits<T>::from_int(0);
256 if (samplesUsed <= zero)
257 throw InputError("sim_runlength_plan: the number of samples already used must be positive");
258 const T nan = num_traits<T>::from_double(std::numeric_limits<double>::quiet_NaN());
260 out.relPrecision = relPrecision;
261 out.confidence = confidence;
262 out.samplesUsed = samplesUsed;
263 out.asymptoticVariance = Matrix<T>(means.rows(), means.cols(), nan);
264 out.requiredSamples = Matrix<T>(means.rows(), means.cols(), nan);
265 // The same z the interval itself was built with; `sim_runlength` computes
266 // it inline from erfc, so it is read back from there rather than recomputed
267 // by a second rule.
269 confidence)
270 .z;
271 for (std::size_t i = 0; i < means.rows(); ++i)
272 for (std::size_t r = 0; r < means.cols(); ++r) {
273 if (i >= ciHalfWidth.rows() || r >= ciHalfWidth.cols()) continue;
274 const double h = num_traits<T>::to_double(ciHalfWidth(i, r));
275 const double m = num_traits<T>::to_double(means(i, r));
276 if (!std::isfinite(h) || h <= 0.0 || !std::isfinite(m) || m <= 0.0) continue;
277 const T av = T(ciHalfWidth(i, r) / z * (ciHalfWidth(i, r) / z) * samplesUsed);
278 out.asymptoticVariance(i, r) = av;
279 out.requiredSamples(i, r) =
280 sim_runlength<T>(means(i, r), av, relPrecision, confidence).requiredRunLength;
281 }
282 return out;
283}
284
285} // namespace sim
286} // namespace line
287
288#endif // LINE_API_SIM_RUNLENGTH_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
The exception types the port throws.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
RunLengthPlan< T > sim_runlength_plan(const Matrix< T > &means, const Matrix< T > &ciHalfWidth, const T &samplesUsed, const T &relPrecision=num_traits< T >::from_rational(1, 20), const T &confidence=num_traits< T >::from_rational(19, 20))
How long a simulation run should have been, from the one it already did.
AsymVarResult< T > sim_asymvar_ctmc(const Matrix< T > &A, const std::vector< T > &f, const std::vector< T > &pi=std::vector< T >())
Asymptotic variance of a reward on a CTMC: 2 sum_x pi(x)g(x)d(x) with g = f - E_pi[f] and A d = -g,...
AsymVarResult< T > sim_asymvar_mm1(const T &lambda, const T &mu)
Asymptotic variance of the M/M/1 number-in-system process.
RunLengthResult< T > sim_runlength(const T &mean, const T &asymVar, const T &relPrecision=num_traits< T >::from_rational(1, 20), const T &confidence=num_traits< T >::from_rational(19, 20), const T &runLength=num_traits< T >::from_int(0))
Run length for a steady-state estimate of a given relative precision.
T num_abs(const T &v)
Definition number.h:172
std::vector< T > solve(const Matrix< T > &A, const std::vector< T > &b)
Convenience: solve Ax = b, leaving A and b untouched.
Definition lu.h:158
Number-type abstraction for the templated API port.
Second-order description of a steady-state estimator.
T asymptoticVariance
sigma^2, what the run length depends on
std::vector< T > deviation
the deviation vector, for the CTMC form
T relaxationTime
sigma^2/Var, the correlation time scale
T variance
Var of the process itself.
T mean
the steady-state mean
The plan of sim_runlength_plan: what the run should have been.
Matrix< T > asymptoticVariance
sigma^2 per (station, class), NaN where unplannable
Matrix< T > requiredSamples
the run length that reaches relPrecision
T relPrecision
the precision planned for
T confidence
the level the half-widths were computed at
T samplesUsed
the run length they came from
Outcome of the run-length plan.
bool hasRun
whether a run length was supplied
T halfWidth
the half-width a supplied run buys
T z
the two-sided normal quantile used
T achievedRelPrecision
that half-width over the mean