LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
mtrace_bootstrap.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_TRACE_MTRACE_BOOTSTRAP_H
6#define LINE_API_TRACE_MTRACE_BOOTSTRAP_H
7
8/**
9 * @file
10 * @ingroup api_trace
11 * Block-bootstrap confidence intervals for the descriptors of a marked trace.
12 *
13 * Templated port of matlab/lib/m3a/m3a/mtrace/mtrace_bootstrap.m.
14 *
15 * The statistic is the whole descriptor vector the m3a fitters consume --
16 * [pc, backward moment, forward moment, sigma] -- and the resampling is by
17 * BLOCK, not by observation: the trace is cut into BN = floor(N / 50) contiguous
18 * blocks and the blocks are resampled with replacement. That is the point of the
19 * routine. An inter-arrival trace is autocorrelated, and resampling individual
20 * observations would destroy exactly the dependence the descriptors measure,
21 * giving intervals far too narrow for sigma and the forward moment.
22 *
23 * The intervals are BCa (bias-corrected and accelerated), which is what MATLAB's
24 * `bootci` computes by default:
25 * z0 = Phi^-1( #{theta* < theta_hat} / R ),
26 * a = sum(d^3) / (6 (sum d^2)^{3/2}) over the jackknife deviations d,
27 * alpha1,2 = Phi( z0 + (z0 -+ z_alpha) / (1 - a (z0 -+ z_alpha)) ),
28 * and the endpoints are the alpha1 and alpha2 quantiles of the replicates. The
29 * bias correction z0 and the acceleration a are what make BCa transformation
30 * respecting, which matters here because several descriptors are probabilities.
31 *
32 * THE THREE CODEBASES DISAGREE ON WHAT THIS FUNCTION IS. MATLAB returns
33 * confidence intervals as above. The JAR returns a statistics object with
34 * configurable block size and seed. Native Python returns a resampled (T, A)
35 * pair -- a sampler, not an estimator, and no interval at all. MATLAB is the
36 * reference and is what is ported; the divergence is recorded rather than
37 * papered over, because a caller reading the Python name expects a trace back.
38 *
39 * RANDOMNESS. `line::pfqn::McRng` by reference, the tree's convention. The
40 * stream is not comparable with MATLAB's, so the oracle is distributional:
41 * the interval must cover the point estimate and shrink as R grows.
42 *
43 * ARITHMETIC: transcendental, for the normal quantiles.
44 */
45
46#include <algorithm>
47#include <cmath>
48#include <cstddef>
49#include <vector>
50
56#include "line/num/number.h"
57#include "line/util/error.h"
58#include "line/util/matrix.h"
59
60namespace line {
61namespace trace {
62
63/** Lower and upper BCa endpoints of each descriptor, and the point estimate. */
64template <class T>
66 std::vector<T> estimate; ///< the statistic on the whole trace
67 std::vector<T> lower; ///< lower confidence limit, same layout
68 std::vector<T> upper; ///< upper confidence limit, same layout
69 std::size_t blocks = 0; ///< BN, the number of blocks resampled
70};
71
72namespace bootdetail {
73
74/**
75 * The descriptor vector the reference bootstraps: the class probabilities, the
76 * first backward moment, the first forward moment, then sigma flattened.
77 */
78template <class T>
79std::vector<T> stat_vector(const std::vector<T>& Tv, const std::vector<int>& A) {
80 const std::vector<unsigned> ord(1, 1u);
81 const std::vector<T> pc = mtrace_pc<T>(A);
82 const Matrix<T> B = mtrace_moment(Tv, A, ord, false, true);
83 const Matrix<T> F = mtrace_moment(Tv, A, ord, true, true);
84 const Matrix<T> S = mtrace_sigma<T>(A);
85 std::vector<T> out;
86 out.reserve(pc.size() + B.rows() + F.rows() + S.rows() * S.cols());
87 for (std::size_t i = 0; i < pc.size(); ++i) out.push_back(pc[i]);
88 for (std::size_t i = 0; i < B.rows(); ++i) out.push_back(B(i, 0));
89 for (std::size_t i = 0; i < F.rows(); ++i) out.push_back(F(i, 0));
90 // MATLAB's S(:) is column-major.
91 for (std::size_t j = 0; j < S.cols(); ++j)
92 for (std::size_t i = 0; i < S.rows(); ++i) out.push_back(S(i, j));
93 return out;
94}
95
96/** Concatenate the listed blocks, in the order given. */
97template <class T>
98void assemble(const std::vector<T>& Tv, const std::vector<int>& A,
99 const std::vector<std::size_t>& first, const std::vector<std::size_t>& len,
100 const std::vector<std::size_t>& pick, std::vector<T>* t, std::vector<int>* a) {
101 t->clear();
102 a->clear();
103 for (std::size_t i = 0; i < pick.size(); ++i) {
104 const std::size_t b = pick[i];
105 for (std::size_t k = 0; k < len[b]; ++k) {
106 t->push_back(Tv[first[b] + k]);
107 a->push_back(A[first[b] + k]);
108 }
109 }
110}
111
112/** The p-quantile of a sorted sample, by linear interpolation. */
113inline double quantile(const std::vector<double>& sorted, double p) {
114 if (sorted.empty()) return 0.0;
115 if (p <= 0.0) return sorted.front();
116 if (p >= 1.0) return sorted.back();
117 const double h = p * static_cast<double>(sorted.size() - 1);
118 const std::size_t lo = static_cast<std::size_t>(std::floor(h));
119 const std::size_t hi = std::min(lo + 1, sorted.size() - 1);
120 return sorted[lo] + (h - static_cast<double>(lo)) * (sorted[hi] - sorted[lo]);
121}
122
123} // namespace bootdetail
124
125/**
126 * @brief Block-bootstrap confidence intervals for the descriptors of a marked
127 * trace.
128 *
129 * @param Tv inter-arrival times
130 * @param A class labels, one per arrival
131 * @param rng generator, advanced by the call
132 * @param resamples number of bootstrap replicates; the reference default is 1000
133 * @param alpha two-sided level; 0.05 gives a 95% interval
134 * @param blockLen target block length; the reference uses 50
135 */
136template <class T>
137MtraceBootstrapResult<T> mtrace_bootstrap(const std::vector<T>& Tv, const std::vector<int>& A,
138 pfqn::McRng& rng, std::size_t resamples = 1000,
139 double alpha = 0.05, std::size_t blockLen = 50) {
141 "mtrace_bootstrap inverts the normal distribution");
142 if (Tv.empty() || Tv.size() != A.size())
143 throw InputError("mtrace_bootstrap: the trace and its labels must agree in length");
144 if (resamples < 2) throw InputError("mtrace_bootstrap: at least two replicates are required");
145 if (!(alpha > 0.0) || !(alpha < 1.0))
146 throw InputError("mtrace_bootstrap: the level must lie strictly inside (0,1)");
147 if (blockLen == 0) throw InputError("mtrace_bootstrap: the block length must be positive");
148
149 const std::size_t N = Tv.size();
150 const std::size_t BN = N / blockLen;
151 if (BN < 2)
152 throw InputError(
153 "mtrace_bootstrap: the trace is too short to cut into at least two blocks at this "
154 "block length; the block bootstrap has nothing to resample");
155
156 // The reference's block layout: BN blocks of floor(N/BN), the first
157 // mod(N, BN) of them one longer, so the blocks tile the trace exactly.
158 const std::size_t base = N / BN, extra = N % BN;
159 std::vector<std::size_t> len(BN, base), first(BN, 0);
160 for (std::size_t b = 0; b < extra; ++b) len[b] += 1;
161 for (std::size_t b = 1; b < BN; ++b) first[b] = first[b - 1] + len[b - 1];
162
164 out.blocks = BN;
165 out.estimate = bootdetail::stat_vector(Tv, A);
166 const std::size_t P = out.estimate.size();
167
168 // ---- the replicates -------------------------------------------------
169 std::vector<std::vector<double>> rep(P);
170 for (std::size_t i = 0; i < P; ++i) rep[i].reserve(resamples);
171 std::vector<std::size_t> pick(BN, 0);
172 std::vector<T> bt;
173 std::vector<int> ba;
174 for (std::size_t r = 0; r < resamples; ++r) {
175 for (std::size_t b = 0; b < BN; ++b)
176 pick[b] = static_cast<std::size_t>(pfqn::mc_uniform01(rng) * static_cast<double>(BN));
177 for (std::size_t b = 0; b < BN; ++b)
178 if (pick[b] >= BN) pick[b] = BN - 1;
179 bootdetail::assemble(Tv, A, first, len, pick, &bt, &ba);
180 std::vector<T> s;
181 try {
182 s = bootdetail::stat_vector(bt, ba);
183 } catch (const Error&) {
184 continue; // a replicate that lost a class has no descriptor vector
185 }
186 if (s.size() != P) continue; // ditto: the layout changed, so it is not comparable
187 for (std::size_t i = 0; i < P; ++i) rep[i].push_back(num_traits<T>::to_double(s[i]));
188 }
189
190 // ---- the jackknife, for the acceleration -----------------------------
191 std::vector<std::vector<double>> jack(P);
192 std::vector<std::size_t> all;
193 for (std::size_t b = 0; b < BN; ++b) all.push_back(b);
194 for (std::size_t drop = 0; drop < BN; ++drop) {
195 std::vector<std::size_t> keep;
196 for (std::size_t b = 0; b < BN; ++b)
197 if (b != drop) keep.push_back(b);
198 bootdetail::assemble(Tv, A, first, len, keep, &bt, &ba);
199 std::vector<T> s;
200 try {
201 s = bootdetail::stat_vector(bt, ba);
202 } catch (const Error&) {
203 continue;
204 }
205 if (s.size() != P) continue;
206 for (std::size_t i = 0; i < P; ++i) jack[i].push_back(num_traits<T>::to_double(s[i]));
207 }
208
209 out.lower.assign(P, num_traits<T>::from_int(0));
210 out.upper.assign(P, num_traits<T>::from_int(0));
211 const double za = sim::sim_norminv(alpha / 2.0);
212 for (std::size_t i = 0; i < P; ++i) {
213 std::vector<double> v = rep[i];
214 if (v.size() < 2) { // nothing usable: report the point estimate twice
215 out.lower[i] = out.estimate[i];
216 out.upper[i] = out.estimate[i];
217 continue;
218 }
219 std::sort(v.begin(), v.end());
220 const double theta = num_traits<T>::to_double(out.estimate[i]);
221
222 // Bias correction: the share of replicates below the point estimate.
223 std::size_t below = 0;
224 for (std::size_t k = 0; k < v.size(); ++k)
225 if (v[k] < theta) ++below;
226 double frac = static_cast<double>(below) / static_cast<double>(v.size());
227 // Guard the endpoints, where the normal quantile is infinite.
228 const double eps = 0.5 / static_cast<double>(v.size());
229 if (frac < eps) frac = eps;
230 if (frac > 1.0 - eps) frac = 1.0 - eps;
231 const double z0 = sim::sim_norminv(frac);
232
233 // Acceleration from the jackknife deviations.
234 double acc = 0.0;
235 if (jack[i].size() >= 2) {
236 double mean = 0.0;
237 for (std::size_t k = 0; k < jack[i].size(); ++k) mean += jack[i][k];
238 mean /= static_cast<double>(jack[i].size());
239 double s2 = 0.0, s3 = 0.0;
240 for (std::size_t k = 0; k < jack[i].size(); ++k) {
241 const double d = mean - jack[i][k];
242 s2 += d * d;
243 s3 += d * d * d;
244 }
245 if (s2 > 0.0) acc = s3 / (6.0 * std::pow(s2, 1.5));
246 }
247
248 auto endpoint = [&](double z) {
249 const double num = z0 + z;
250 const double den = 1.0 - acc * num;
251 if (!(std::fabs(den) > 0.0)) return 0.5;
252 return sim::sim_normcdf(z0 + num / den);
253 };
254 double a1 = endpoint(za), a2 = endpoint(-za);
255 if (a1 > a2) std::swap(a1, a2);
256 out.lower[i] = num_traits<T>::from_double(bootdetail::quantile(v, a1));
257 out.upper[i] = num_traits<T>::from_double(bootdetail::quantile(v, a2));
258 }
259 return out;
260}
261
262} // namespace trace
263} // namespace line
264
265#endif // LINE_API_TRACE_MTRACE_BOOTSTRAP_H
Base error for the multiprecision C++ port.
Definition error.h:31
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.
Dense matrix and non-owning view.
Empirical class-dependent moments of a marked trace.
Class probabilities of a marked trace, p_c = count_c / N.
One-step class transition frequencies of a marked trace,.
std::mt19937_64 McRng
The generator type every Monte Carlo entry point in this tree accepts.
double mc_uniform01(McRng &g)
Uniform deviate on [0,1) with 53 significant bits, as a double.
double sim_norminv(double p)
Standard normal quantile function.
Definition sim_dist.h:62
double sim_normcdf(double z)
Standard normal cumulative distribution function.
Definition sim_dist.h:52
MtraceBootstrapResult< T > mtrace_bootstrap(const std::vector< T > &Tv, const std::vector< int > &A, pfqn::McRng &rng, std::size_t resamples=1000, double alpha=0.05, std::size_t blockLen=50)
Block-bootstrap confidence intervals for the descriptors of a marked trace.
std::vector< T > mtrace_pc(const std::vector< int > &A)
Class probabilities of a marked trace, p_c = count_c / N.
Definition mtrace_pc.h:42
Matrix< T > mtrace_moment(const std::vector< T > &Tv, const std::vector< int > &A, const std::vector< unsigned > &orders, bool after=false, bool norm=false)
Empirical class-dependent moments of a marked trace.
Matrix< T > mtrace_sigma(const std::vector< int > &L)
One-step class transition frequencies of a marked trace, sigma(i,j) = #{t : A_t = i,...
Number-type abstraction for the templated API port.
Randomness scaffolding shared by the Monte Carlo normalizing-constant estimators (pfqn_mci,...
Normal and Student t quantiles used by the output-analysis routines.
Lower and upper BCa endpoints of each descriptor, and the point estimate.
std::vector< T > lower
lower confidence limit, same layout
std::vector< T > estimate
the statistic on the whole trace
std::size_t blocks
BN, the number of blocks resampled.
std::vector< T > upper
upper confidence limit, same layout