LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
sim_firquest.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_SIM_FIRQUEST_H
6#define LINE_API_SIM_SIM_FIRQUEST_H
7
8/**
9 * @file
10 * @ingroup api_sim
11 * Fixed-sample-size quantile interval from independent replications.
12 *
13 * Port of matlab/src/api/sim/sim_firquest.m. FIRQUEST is the replicated
14 * counterpart of FQUEST and differs from sim_fquest in four places:
15 *
16 * The warmup randomness test runs independently on each replicate path, and
17 * the batch size it settles on may differ between replications.
18 *
19 * Truncation removes the LARGEST of those batch sizes from the front of every
20 * replication, not just from one path. This is more aggressive than FQUEST on
21 * purpose: an untruncated transient common to all replications biases every
22 * replicate estimate the same way, and averaging cannot remove it.
23 *
24 * The four stage tests act on the R*b signed areas and R*b replicate batched
25 * quantile estimators POOLED IN REPLICATION-MAJOR ORDER, i.e. all b statistics
26 * of replication 1, then those of replication 2, and so on, which is MATLAB's
27 * column-major areas(:). Both stage tests are order-sensitive, so pooling in
28 * any other order silently changes which batch count is selected.
29 *
30 * The delivered interval is
31 * ytilde_p(N*) +- t_{1-alpha/2, 2Rb-1} sqrt(Vtilde_p(w;R,b,m)/N*),
32 * N* = R*b*m, with the pooled combined variance-parameter estimator
33 * A_p(w;R,b,m) = (Rb)^{-1} sum_j A_p(w;j,m)^2
34 * Ntilde_p(R,b,m) = m (Rb-1)^{-1} sum_j (yhat_p(j,m) - ytilde_p(N*))^2
35 * Vtilde_p = [Rb A_p + (Rb-1) Ntilde_p] / (2Rb-1).
36 * The heuristic fallback drops FQUEST's residual-autocorrelation correction,
37 * since the pooled batch quantiles come from independent paths.
38 *
39 * DEFAULTS DIFFER FROM FQUEST and that is why the options argument is a pointer
40 * here: b0 = 25 rather than 50, and s is chosen from R rather than fixed,
41 * because the stage tests act on the R*b pooled statistics and so need fewer
42 * batches per replication. Passing nullptr takes both; passing a default
43 * constructed QuestOptions takes the FQUEST values instead, which is a
44 * different procedure, not a formality. Start from sim_firquest_options(R) when
45 * overriding one field.
46 *
47 * Independent replications shorten the correlation the estimator has to fight,
48 * and they parallelize, but they reintroduce initialization bias in every path,
49 * so a short run length per replication is worse here than in sim_fquest. The
50 * reference reports slight undercoverage at p = 0.99 when the total sample is
51 * under 500000, down to 90.8%.
52 *
53 * Reference: A. Lolos, C. Alexopoulos, D. Goldsman, K. D. Dingec, A. C. Mokashi,
54 * J. R. Wilson, "A Fixed-Sample-Size Procedure for Estimating Steady-State
55 * Quantiles Based on Independent Replications", Proc. Winter Simulation
56 * Conference, 2025.
57 */
58
59#include <algorithm>
60#include <cmath>
61#include <cstddef>
62#include <string>
63#include <vector>
64
73#include "line/num/number.h"
74#include "line/util/error.h"
75
76namespace line {
77namespace sim {
78
79/**
80 * The article's batch counts as a function of the replication count, chosen so
81 * that R*b pooled statistics remain enough to test while every replication still
82 * contributes at least one batch.
83 *
84 * @param R number of replications
85 * @return the descending batch-count ladder
86 */
87inline std::vector<long> sim_firquest_batchcounts(std::size_t R) {
88 if (R == 2) return std::vector<long>{14, 11, 8, 5};
89 if (R == 3) return std::vector<long>{10, 8, 6, 4};
90 if (R == 4) return std::vector<long>{6, 5, 4, 3};
91 if (R < 10) return std::vector<long>{5, 4, 3, 2};
92 if (R < 17) return std::vector<long>{4, 3, 2, 1};
93 if (R < 23) return std::vector<long>{3, 2, 1};
94 if (R < 33) return std::vector<long>{2, 1};
95 return std::vector<long>{1};
96}
97
98/**
99 * The FIRQUEST defaults at R replications: b0 = 25 and the R-dependent ladder,
100 * every other constant as in FQUEST.
101 *
102 * @param R number of replications
103 * @return the option set sim_firquest uses when none is supplied
104 */
105inline QuestOptions sim_firquest_options(std::size_t R) {
107 opt.b0 = 25;
109 return opt;
110}
111
112namespace detail {
113
114/**
115 * Pooled statistics over the last b*m observations of every replication. The
116 * areas and batch quantiles come back in replication-major order, see the note
117 * at the top of this header.
118 */
119template <class T>
120StsQuantileStats<T> firquest_pool(const std::vector<std::vector<T>>& Yt, long b, long m, double p,
121 double weight) {
122 const std::size_t R = Yt.size();
123 const std::size_t bm = static_cast<std::size_t>(b) * static_cast<std::size_t>(m);
124
125 StsQuantileStats<T> pooled;
126 pooled.b = static_cast<std::size_t>(b);
127 pooled.m = static_cast<std::size_t>(m);
128 pooled.n = R * bm;
129 pooled.areas.reserve(R * static_cast<std::size_t>(b));
130 pooled.bqe.reserve(R * static_cast<std::size_t>(b));
131
132 std::vector<T> all;
133 all.reserve(pooled.n);
134 for (std::size_t r = 0; r < R; ++r) {
135 const std::vector<T> kept(Yt[r].end() - static_cast<std::ptrdiff_t>(bm), Yt[r].end());
136 const StsQuantileStats<T> st = sim_sts_quantile_areas<T>(
137 kept, static_cast<std::size_t>(b), static_cast<std::size_t>(m), p, weight);
138 pooled.areas.insert(pooled.areas.end(), st.areas.begin(), st.areas.end());
139 pooled.bqe.insert(pooled.bqe.end(), st.bqe.begin(), st.bqe.end());
140 all.insert(all.end(), kept.begin(), kept.end());
141 }
142
143 std::sort(all.begin(), all.end());
144 pooled.quantile =
145 all[static_cast<std::size_t>(std::ceil(static_cast<double>(pooled.n) * p)) - 1];
146
147 const std::size_t K = R * static_cast<std::size_t>(b);
148 T sumSq = num_traits<T>::from_int(0);
149 for (std::size_t i = 0; i < K; ++i) sumSq += T(pooled.areas[i] * pooled.areas[i]);
150 pooled.Ap = T(sumSq / num_traits<T>::from_int(static_cast<long>(K)));
151
152 T sd = num_traits<T>::from_int(0);
153 for (std::size_t i = 0; i < K; ++i) {
154 const T d = T(pooled.bqe[i] - pooled.quantile);
155 sd += T(d * d);
156 }
157 pooled.Np = T(num_traits<T>::from_int(m) * sd /
158 num_traits<T>::from_int(static_cast<long>(K - 1)));
159 pooled.Vp = T((num_traits<T>::from_int(static_cast<long>(K)) * pooled.Ap +
160 num_traits<T>::from_int(static_cast<long>(K - 1)) * pooled.Np) /
161 num_traits<T>::from_int(static_cast<long>(2 * K - 1)));
162 return pooled;
163}
164
165} // namespace detail
166
167/**
168 * @brief Fixed-sample-size quantile interval from independent replications.
169 *
170 * @param Y R replicate sample paths of equal length, finite
171 * @param p quantile order in (0,1)
172 * @param alpha nominal non-coverage, 0.05 by default
173 * @param options procedure constants, nullptr for the FIRQUEST defaults at this R
174 */
175template <class T>
176QuestResult<T> sim_firquest(const std::vector<std::vector<T>>& Y, double p, double alpha = 0.05,
177 const QuestOptions* options = nullptr) {
179 "sim_firquest: the interval is a t quantile times a square root, so exact "
180 "arithmetic is refused");
181 const std::size_t R = Y.size();
182 if (R < 2)
183 throw InputError("sim_firquest: at least 2 replications are required, use sim_fquest for a "
184 "single path");
185 const std::size_t nRep = Y[0].size();
186 if (nRep == 0)
187 throw InputError("sim_firquest: the replicate paths must be nonempty and of equal length");
188 for (std::size_t r = 0; r < R; ++r) {
189 if (Y[r].size() != nRep)
190 throw InputError("sim_firquest: the replicate paths must be nonempty and of equal "
191 "length");
192 for (std::size_t i = 0; i < nRep; ++i)
193 if (!detail::num_isfinite(Y[r][i]))
194 throw InputError("sim_firquest: the sample paths must be finite");
195 }
196
197 const QuestOptions opt =
198 sim_quest_options(options == nullptr ? sim_firquest_options(R) : *options);
199
200 if (!(p > 0.0) || !(p < 1.0))
201 throw InputError("sim_firquest: p must be a real scalar in (0,1)");
202 if (!(alpha > 0.0) || !(alpha < 1.0))
203 throw InputError("sim_firquest: alpha must be a real scalar in (0,1)");
204 if (static_cast<long>(R) * opt.s.back() < 3)
205 throw InputError("sim_firquest: R*min(s) pooled batches is below the 3 the stage tests "
206 "need");
207
208 QuestResult<T> res;
209 res.R = R;
210
211 // ---- warmup: one randomness loop per replicate path
212 const long n = static_cast<long>(nRep);
213 const long b0 = opt.b0;
214 long mStart = opt.m0;
215 if (n < b0 * mStart) mStart = n / b0;
216 if (mStart < 1)
217 throw InputError("sim_firquest: each replication is too short for the initial batch count "
218 "b0");
219
220 long mMax = 0;
221 bool failed = false;
222 for (std::size_t r = 0; r < R; ++r) {
223 long m = mStart;
224 long ell = 1;
225 bool atMax = false, passed = false;
226 while (true) {
227 const std::vector<T> head(Y[r].begin(),
228 Y[r].begin() + static_cast<std::ptrdiff_t>(b0 * m));
230 head, static_cast<std::size_t>(b0), static_cast<std::size_t>(m), p, opt.weight);
231 const double sig =
232 opt.beta * std::exp(-opt.eta * std::pow(static_cast<double>(ell - 1), opt.theta));
233 if (!sim_vonneumann<T>(st.areas, sig).reject) {
234 passed = true;
235 break;
236 }
237 if (atMax) break;
238 ++ell;
239 m = detail::warmup_next_m(n, b0, m, atMax);
240 if (m < 1) break;
241 }
242 failed = failed || !passed;
243 mMax = std::max(mMax, m);
244 }
245 if (failed)
246 res.warnings.push_back("the warmup randomness test could not be passed in every "
247 "replication, the replicate paths are too short");
248
249 // ---- truncation: delete the longest warmup batch from every replication
250 const long truncated = mMax > 0 ? mMax : 0;
251 if (truncated >= n)
252 throw InputError("sim_firquest: the warmup batch size exhausts the replication length");
253 std::vector<std::vector<T>> Yt(R);
254 for (std::size_t r = 0; r < R; ++r)
255 Yt[r].assign(Y[r].begin() + static_cast<std::ptrdiff_t>(truncated), Y[r].end());
256 const long nstarRep = static_cast<long>(Yt[0].size());
257
258 // ---- batch-count selection on the pooled statistics
259 std::size_t v = 0;
260 long b = opt.s[v];
261 long m = nstarRep / b;
262 bool ok = true, havePooled = false;
263 StsQuantileStats<T> pooled;
264 for (int stage = 1; stage <= 4; ++stage) {
265 while (true) {
266 if (m < 1) {
267 ok = false;
268 break;
269 }
270 pooled = detail::firquest_pool<T>(Yt, b, m, p, opt.weight);
271 havePooled = true;
272 const std::vector<T>& sample = stage <= 2 ? pooled.areas : pooled.bqe;
273 const bool reject = (stage % 2 == 1) ? sim_vonneumann<T>(sample, opt.beta).reject
274 : sim_shapirowilk<T>(sample, opt.beta).reject;
275 if (!reject) break;
276 ++v;
277 if (v >= opt.s.size()) {
278 ok = false;
279 break;
280 }
281 b = opt.s[v];
282 m = nstarRep / b;
283 }
284 if (!ok) break;
285 }
286
287 if (!havePooled || m < 1)
288 throw InputError("sim_firquest: the replicate paths are too short to form min(s) batches "
289 "each");
290
291 res.b = static_cast<std::size_t>(b);
292 res.m = static_cast<std::size_t>(m);
293 res.n = pooled.n;
294 res.truncated = static_cast<std::size_t>(truncated);
295 res.estimate = pooled.quantile;
296 res.Ap = pooled.Ap;
297 res.Np = pooled.Np;
298 res.Vp = pooled.Vp;
299
300 const T nst = num_traits<T>::from_int(static_cast<long>(pooled.n));
301 if (ok) {
302 const T t = num_traits<T>::from_double(
303 sim_tinv(1.0 - alpha / 2.0, static_cast<double>(2 * static_cast<long>(R) * b - 1)));
304 const T half = T(t * detail::num_sqrt(T(pooled.Vp / nst)));
305 res.lower = T(res.estimate - half);
306 res.upper = T(res.estimate + half);
307 res.halfwidth = half;
308 res.heuristic = false;
309 } else {
310 res.warnings.push_back("a randomness or normality test failed at b = " +
311 std::to_string(opt.s.back()) +
312 " per replication, the delivered interval is heuristic");
313 res.heuristic = true;
314 if (opt.force) {
316 pooled.bqe, res.estimate, pooled.Ap, pooled.Np, pooled.n, alpha, false);
317 res.lower = ci.lower;
318 res.upper = ci.upper;
319 res.halfwidth = T(T(ci.upper - ci.lower) / num_traits<T>::from_int(2));
320 } else {
321 res.lower = detail::num_nan<T>();
322 res.upper = detail::num_nan<T>();
323 res.halfwidth = detail::num_nan<T>();
324 }
325 }
326 return res;
327}
328
329} // namespace sim
330} // namespace line
331
332#endif // LINE_API_SIM_SIM_FIRQUEST_H
InputError(const std::string &what)
Definition error.h:39
The exception types the port throws.
QuestResult< T > sim_firquest(const std::vector< std::vector< T > > &Y, double p, double alpha=0.05, const QuestOptions *options=nullptr)
Fixed-sample-size quantile interval from independent replications.
std::vector< long > sim_firquest_batchcounts(std::size_t R)
The article's batch counts as a function of the replication count, chosen so that R*b pooled statisti...
StsQuantileStats< T > sim_sts_quantile_areas(const std::vector< T > &Y, std::size_t b, std::size_t m, double p, double weight=std::sqrt(12.0))
Standardized time series areas of the batched quantile process.
VonNeumannResult< T > sim_vonneumann(const std::vector< T > &x, double alpha=0.05)
Von Neumann ratio test for randomness of a sequence.
double sim_tinv(double p, double nu)
Quantile function of Student's t distribution.
Definition sim_dist.h:77
QuestOptions sim_firquest_options(std::size_t R)
The FIRQUEST defaults at R replications: b0 = 25 and the R-dependent ladder, every other constant as ...
QuestOptions sim_quest_options(const QuestOptions &options=QuestOptions())
Validates an option set and returns it.
ShapiroWilkResult< T > sim_shapirowilk(const std::vector< T > &x, double alpha=0.05)
Shapiro-Wilk test for univariate normality.
QuestInterval< T > sim_quest_heuristic_ci(const std::vector< T > &bqe, const T &centre, const T &Ap, const T &Np, std::size_t nstar, double alpha, bool useAutocorr)
Fallback interval used when a QUEST stage test fails.
Number-type abstraction for the templated API port.
Normal and Student t quantiles used by the output-analysis routines.
Fixed-sample-size confidence interval for a steady-state quantile.
Fallback interval used when a QUEST stage test fails.
Options of the QUEST procedures, with the published FQUEST defaults.
Shapiro-Wilk test for univariate normality.
Standardized time series areas of the batched quantile process.
Shared arithmetic helpers for the templated simulation output-analysis port.
Von Neumann ratio test for randomness of a sequence.
A confidence interval, asymmetric about the point estimate in general.
Procedure constants shared by sim_fquest and sim_firquest.
Point estimate and interval delivered by the QUEST procedures.
Definition sim_fquest.h:84
T halfwidth
(upper-lower)/2, attained only on average when asymmetric
Definition sim_fquest.h:88
T estimate
Full-sample empirical p-quantile of the truncated path.
Definition sim_fquest.h:85
std::size_t truncated
Observations deleted from the front of each path.
Definition sim_fquest.h:93
T upper
Upper confidence limit, NaN if refused.
Definition sim_fquest.h:87
T Vp
Combined variance-parameter estimator.
Definition sim_fquest.h:96
T Ap
STS area variance-parameter estimator.
Definition sim_fquest.h:94
std::size_t b
Final batch count, per replication in sim_firquest.
Definition sim_fquest.h:89
std::size_t R
Replications, 1 for sim_fquest.
Definition sim_fquest.h:92
std::size_t n
Observations the interval rests on.
Definition sim_fquest.h:91
bool heuristic
True when a stage test failed.
Definition sim_fquest.h:97
T lower
Lower confidence limit, NaN if refused.
Definition sim_fquest.h:86
std::size_t m
Final batch size.
Definition sim_fquest.h:90
std::vector< std::string > warnings
Diagnostics, empty on a clean run.
Definition sim_fquest.h:98
T Np
NBQ variance-parameter estimator.
Definition sim_fquest.h:95
Batched-quantile statistics of one sample path.
std::size_t n
Number of observations used, b*m.
T Ap
Batched STS area estimator A_p(w;b,m).
std::vector< T > areas
b signed STS areas A_p(w;j,m)
T Np
NBQ variance-parameter estimator N_p(b,m), NaN at b = 1.
std::vector< T > bqe
b batched quantile estimators yhat_p(j,m)
T Vp
Combined variance-parameter estimator, NaN at b = 1.
T quantile
Full-sample empirical p-quantile ytilde_p(n).