LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ldes_stats.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_SOLVERS_LDES_LDES_STATS_H
6#define LINE_SOLVERS_LDES_LDES_STATS_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The estimators of the native LDES engine: running integrals, the MSER-5
12 * warmup filter, and the batch-means confidence intervals.
13 *
14 * THE INTEGRALS ARE LAZY. `tot_qlen(i,r)` is the time integral of the class-r
15 * queue length at station i, advanced only when that pair CHANGES. Advancing
16 * every pair on every event would be O(M*K) per event on a hot loop where the
17 * event touches one pair; the reference does the same thing with
18 * `lastQueueUpdateTime`, and the invariant is that `update_qlen` is called
19 * BEFORE any write to `qlen`, never after.
20 *
21 * THE WARMUP FILTER IS MSER-5 over EVENT-SPACED observations, not time-spaced
22 * ones. That matters for what the estimators may then do with the series: an
23 * unweighted mean of per-interval averages overweights congested epochs,
24 * because a congested epoch contains more events and therefore more intervals
25 * per unit time. So the means below are integral DIFFERENCES over elapsed
26 * time, and only the CI -- which needs a sequence of comparable observations,
27 * not a mean -- reads the per-interval series directly.
28 */
29
30#include <algorithm>
31#include <cmath>
32#include <cstddef>
33#include <cstdint>
34#include <limits>
35#include <string>
36#include <vector>
37
39#include "line/util/matrix.h"
40
41namespace line {
42namespace ldes {
43namespace engine {
44
45/** The running per-(station, class) integrals and tallies. */
46struct Accum {
47 Accum(std::size_t M, std::size_t K)
48 : qlen(M, std::vector<double>(K, 0.0)),
49 held(M, std::vector<double>(K, 0.0)),
50 busy(M, std::vector<double>(K, 0.0)),
51 tot_qlen(M, std::vector<double>(K, 0.0)),
52 tot_busy(M, std::vector<double>(K, 0.0)),
53 last_qlen(M, std::vector<double>(K, 0.0)),
54 last_busy(M, std::vector<double>(K, 0.0)),
55 busy_scale(M, 1.0),
56 util_peak(M, 1.0),
57 resp_sum(M, std::vector<double>(K, 0.0)),
58 resp_cnt(M, std::vector<double>(K, 0.0)),
59 completed(M, std::vector<double>(K, 0.0)),
60 arrived(M, std::vector<double>(K, 0.0)),
61 join_dropped(M, std::vector<double>(K, 0.0)) {}
62
63 /** Advance the queue-length integral of (i,r) to `now`. Call BEFORE writing qlen. */
64 void update_qlen(std::size_t i, std::size_t r, double now) {
65 const double dt = now - last_qlen[i][r];
66 if (dt > 0.0) tot_qlen[i][r] += (qlen[i][r] + held[i][r]) * dt;
67 last_qlen[i][r] = now;
68 }
69 /**
70 * Advance the busy-server integral of (i,r) to `now`. Call BEFORE writing busy.
71 *
72 * The integrand is the WORK being delivered, `busy * busy_scale`, not the
73 * head count: a load-dependent server running alpha(n) times faster does the
74 * same work in less time, so plain busy TIME reads it as no busier than one
75 * at its nominal rate. `busy_scale` is 1 unless the station is load
76 * dependent, so this is the identity everywhere else.
77 */
78 void update_busy(std::size_t i, std::size_t r, double now) {
79 const double dt = now - last_busy[i][r];
80 if (dt > 0.0) tot_busy[i][r] += busy[i][r] * busy_scale[i] * dt;
81 last_busy[i][r] = now;
82 }
83 /**
84 * Install the load-dependent speed station `i` runs at from `now` on.
85 *
86 * Flushes every class FIRST, so the interval that just ended is credited at
87 * the scale that was actually in force over it. The engine calls this after
88 * the population is written, from the same chokepoint that re-times the
89 * departures.
90 */
91 void set_busy_scale(std::size_t i, double v, double now) {
92 if (v == busy_scale[i]) return;
93 for (std::size_t r = 0; r < busy[i].size(); ++r) update_busy(i, r, now);
94 busy_scale[i] = v;
95 }
96
97 std::vector<std::vector<double>> qlen;
98 /**
99 * Callers parked on their server awaiting a synchronous REPLY (sn.syncreply), per (station, class).
100 *
101 * They are counted in the QLen INTEGRAL, as the reference counts currentBlockedServers in
102 * Solver_ssj.updateQueueStats, but kept OUT of `qlen`, which every decision reads: capacity,
103 * load-dependent rates, JSQ and setup all use currentQueueLength there, which excludes them.
104 * Written only after update_qlen, like qlen.
105 */
106 std::vector<std::vector<double>> held;
107 std::vector<std::vector<double>> busy;
108 std::vector<std::vector<double>> tot_qlen, tot_busy;
109 std::vector<std::vector<double>> last_qlen, last_busy;
110 /**
111 * The speed each station is running at right now (load dependence only),
112 * and the peak capacity that normalizes its utilization,
113 * max(c, max(alpha)).
114 *
115 * `util_peak` is CTMC's own `ceff`, which is what makes LDES report the same
116 * utilization as the analytic solvers on a load-dependent station. Both are
117 * 1 and c respectively when the station is not load dependent, leaving every
118 * other model's numbers untouched.
119 */
120 std::vector<double> busy_scale, util_peak;
121 std::vector<std::vector<double>> resp_sum, resp_cnt, completed;
122 /**
123 * Jobs that ARRIVED at each (station, class), which is not what `completed`
124 * counts: the reference reports AN as arrivedCustomers / simTime, and the
125 * two differ by whatever is still in the station at the end and by anything
126 * dropped, balked or reneged. Reporting completions as the arrival rate
127 * would hide exactly the loss those features exist to measure.
128 */
129 std::vector<std::vector<double>> arrived;
130 /**
131 * Siblings a QUORUM Join discarded, per (station, class): a sibling that
132 * reaches the Join after its parent already fired lost the race and is
133 * thrown away. It is a completion of nothing, so it belongs neither in
134 * `completed` nor in `arrived`, and it is what a Join row's loss rate is.
135 * Cumulative, and differenced against the truncation point like `completed`.
136 */
137 std::vector<std::vector<double>> join_dropped;
138};
139
140/**
141 * MSER-5 truncation point over a series of observations, in BATCHES.
142 *
143 * Returns the batch index d minimising var(Z_{d..N-1}) / (N-d)^2 over the
144 * batch means Z, and 0 when there are fewer than four batches -- below that
145 * the criterion is estimated from too few terms to be a truncation rule.
146 * Transcribes `computeMSER5TruncationPoint`.
147 */
148inline int mser5_truncation(const std::vector<double>& obs, int batch_size) {
149 const std::size_t n = obs.size();
150 if (batch_size <= 0 || n < static_cast<std::size_t>(batch_size) * 4) return 0;
151 const int num_batches = static_cast<int>(n / static_cast<std::size_t>(batch_size));
152 std::vector<double> means(static_cast<std::size_t>(num_batches), 0.0);
153 for (int j = 0; j < num_batches; ++j) {
154 double sum = 0.0;
155 for (int i = 0; i < batch_size; ++i)
156 sum += obs[static_cast<std::size_t>(j * batch_size + i)];
157 means[static_cast<std::size_t>(j)] = sum / batch_size;
158 }
159 double min_mser = std::numeric_limits<double>::max();
160 int optimal_d = 0;
161 const int max_d = num_batches / 2;
162 for (int d = 0; d < max_d; ++d) {
163 const int remaining = num_batches - d;
164 if (remaining < 2) break;
165 double sum = 0.0;
166 for (int j = d; j < num_batches; ++j) sum += means[static_cast<std::size_t>(j)];
167 const double mean = sum / remaining;
168 double variance = 0.0;
169 for (int j = d; j < num_batches; ++j) {
170 const double diff = means[static_cast<std::size_t>(j)] - mean;
171 variance += diff * diff;
172 }
173 variance /= (remaining - 1);
174 const double mser = variance / (static_cast<double>(remaining) * remaining);
175 if (mser < min_mser) {
176 min_mser = mser;
177 optimal_d = d;
178 }
179 }
180 return optimal_d;
181}
182
183/** Where the warmup ended, and whether a truncation was applied at all. */
185 std::size_t index = 0; ///< observation index of the truncation point
186 double warmup_end = 0.0; ///< the instant of that observation
187 bool applied = false;
188};
189
190/** The event-spaced observation series MSER-5 and the CI both read. */
192 Observations(std::size_t M, std::size_t K, bool mser_on, int batch)
193 : enabled(mser_on),
194 batch_size(batch),
195 qlen(M, std::vector<std::vector<double>>(K)),
196 qt(M, std::vector<std::vector<double>>(K)),
197 bt(M, std::vector<std::vector<double>>(K)),
198 cmp(M, std::vector<std::vector<double>>(K)),
199 drp(M, std::vector<std::vector<double>>(K)),
200 last_qt(M, std::vector<double>(K, 0.0)),
201 in_mser(M, static_cast<char>(1)),
202 nstations(M),
203 nclasses(K) {}
204
205 /**
206 * Record one observation.
207 *
208 * The queue-length entry is the INTERVAL TIME-AVERAGE, not the instantaneous
209 * count: it is the integral delivered since the previous observation over
210 * the interval length, which is what makes a sequence of them comparable
211 * even though the intervals differ in duration. The other three are
212 * CUMULATIVE, because the estimators difference them against the truncation
213 * point rather than averaging them.
214 */
215 void collect(const Accum& acc, double now) {
216 const double dt = now - last_time;
217 time.push_back(now);
218 for (std::size_t i = 0; i < nstations; ++i)
219 for (std::size_t r = 0; r < nclasses; ++r) {
220 if (dt > 0.0) {
221 qlen[i][r].push_back((acc.tot_qlen[i][r] - last_qt[i][r]) / dt);
222 last_qt[i][r] = acc.tot_qlen[i][r];
223 } else {
224 qlen[i][r].push_back(acc.qlen[i][r]);
225 }
226 qt[i][r].push_back(acc.tot_qlen[i][r]);
227 bt[i][r].push_back(acc.tot_busy[i][r]);
228 cmp[i][r].push_back(acc.completed[i][r]);
229 drp[i][r].push_back(acc.join_dropped[i][r]);
230 }
231 last_time = now;
232 }
233
234 /**
235 * The truncation point, on the AGGREGATE queue length first.
236 *
237 * The per-series fallback is not a refinement, it is the closed-network
238 * case: a closed model holds a constant total population, so the aggregate
239 * series is flat and carries no transient signal at all, however long the
240 * warmup actually was. The most conservative per-series point is taken then.
241 *
242 * BOTH RUN OVER THE SERVICE STATIONS ONLY, which is the range
243 * `applyMSER5Truncation` walks. A Join is measured like a station but is
244 * not one of them, and folding its series in would move the truncation
245 * point of every fork-join model away from the reference's.
246 */
248 Truncation t;
249 if (!enabled || time.empty()) return t;
250 std::vector<double> aggregate(time.size(), 0.0);
251 for (std::size_t k = 0; k < time.size(); ++k) {
252 double tot = 0.0;
253 for (std::size_t i = 0; i < nstations; ++i) {
254 if (!in_mser[i]) continue;
255 for (std::size_t r = 0; r < nclasses; ++r)
256 if (k < qlen[i][r].size()) tot += qlen[i][r][k];
257 }
258 aggregate[k] = tot;
259 }
260 int batch = mser5_truncation(aggregate, batch_size);
261 if (batch == 0) {
262 for (std::size_t i = 0; i < nstations; ++i) {
263 if (!in_mser[i]) continue;
264 for (std::size_t r = 0; r < nclasses; ++r) {
265 const int b = mser5_truncation(qlen[i][r], batch_size);
266 if (b > batch) batch = b;
267 }
268 }
269 }
270 const std::size_t idx =
271 static_cast<std::size_t>(batch) * static_cast<std::size_t>(batch_size);
272 if (idx < time.size()) {
273 t.index = idx;
274 t.warmup_end = time[idx];
275 t.applied = true;
276 }
277 return t;
278 }
279
280 bool enabled = true;
281 int batch_size = 5;
282 std::vector<double> time;
283 std::vector<std::vector<std::vector<double>>> qlen, qt, bt, cmp, drp;
284 std::vector<std::vector<double>> last_qt;
285 /// Whether station i feeds the truncation criterion; every one is still recorded.
286 std::vector<char> in_mser;
287 double last_time = 0.0;
288 std::size_t nstations = 0, nclasses = 0;
289};
290
291/** Grand mean, standard error and degrees of freedom of a batch-means estimate. */
293 double mean = 0.0;
294 double stderr_ = 0.0;
295 int df = 0;
296 bool ok = false;
297};
298
299/**
300 * The variance inflation of OVERLAPPING batch means.
301 *
302 * Overlapping batches are positively correlated, so the naive sample variance
303 * of their means underestimates the true one; 4/3 is the standard asymptotic
304 * correction at 50% overlap, interpolated linearly below it and reducing to 1
305 * at zero overlap, where the batches are disjoint and no correction is due.
306 * Transcribes `computeOverlapAdjustmentFactor`.
307 */
308inline double overlap_adjustment(double overlap) {
309 if (overlap <= 0.0) return 1.0;
310 if (overlap >= 0.5) return 4.0 / 3.0;
311 return 1.0 + (overlap / 0.5) * (4.0 / 3.0 - 1.0);
312}
313
314/** Non-overlapping batch means. Transcribes `computeBMStatisticsInternal`. */
315inline StatTriple bm_statistics(const std::vector<double>& obs, int batch_size) {
316 StatTriple s;
317 const std::size_t n = obs.size();
318 const int nb = (batch_size > 0) ? static_cast<int>(n / static_cast<std::size_t>(batch_size)) : 0;
319 if (nb < 2) return s;
320 std::vector<double> means(static_cast<std::size_t>(nb), 0.0);
321 for (int i = 0; i < nb; ++i) {
322 double sum = 0.0;
323 for (int j = 0; j < batch_size; ++j)
324 sum += obs[static_cast<std::size_t>(i * batch_size + j)];
325 means[static_cast<std::size_t>(i)] = sum / batch_size;
326 }
327 double grand = 0.0;
328 for (int i = 0; i < nb; ++i) grand += means[static_cast<std::size_t>(i)];
329 grand /= nb;
330 double ss = 0.0;
331 for (int i = 0; i < nb; ++i) {
332 const double d = means[static_cast<std::size_t>(i)] - grand;
333 ss += d * d;
334 }
335 s.mean = grand;
336 s.stderr_ = std::sqrt((ss / (nb - 1)) / nb);
337 s.df = nb - 1;
338 s.ok = true;
339 return s;
340}
341
342/** Overlapping batch means. Transcribes `computeOBMStatisticsInternal`. */
343inline StatTriple obm_statistics(const std::vector<double>& obs, int batch_size, double overlap) {
344 StatTriple s;
345 const std::size_t n = obs.size();
346 if (batch_size <= 0 || n < static_cast<std::size_t>(batch_size) * 2) return s;
347 int step = static_cast<int>(batch_size * (1.0 - overlap));
348 if (step < 1) step = 1;
349 const int nb = static_cast<int>((n - static_cast<std::size_t>(batch_size)) /
350 static_cast<std::size_t>(step)) + 1;
351 if (nb < 2) return s;
352 std::vector<double> means(static_cast<std::size_t>(nb), 0.0);
353 for (int i = 0; i < nb; ++i) {
354 const std::size_t start = static_cast<std::size_t>(i) * static_cast<std::size_t>(step);
355 double sum = 0.0;
356 for (int j = 0; j < batch_size; ++j)
357 if (start + static_cast<std::size_t>(j) < n) sum += obs[start + static_cast<std::size_t>(j)];
358 means[static_cast<std::size_t>(i)] = sum / batch_size;
359 }
360 double grand = 0.0;
361 for (int i = 0; i < nb; ++i) grand += means[static_cast<std::size_t>(i)];
362 grand /= nb;
363 double ss = 0.0;
364 for (int i = 0; i < nb; ++i) {
365 const double d = means[static_cast<std::size_t>(i)] - grand;
366 ss += d * d;
367 }
368 const double adj = overlap_adjustment(overlap);
369 s.mean = grand;
370 s.stderr_ = std::sqrt((adj * ss / (nb - 1)) / nb);
371 s.df = static_cast<int>((nb - 1) / adj);
372 if (s.df < 1) s.df = 1;
373 s.ok = true;
374 return s;
375}
376
377/**
378 * The two-sided t critical value, from the REFERENCE'S TABLE.
379 *
380 * Deliberately the table and not an exact quantile: this port must report the
381 * same interval WIDTH as the Java engine on the same series, and the reference
382 * rounds its own critical values to three decimals and saturates at df = 30.
383 * Substituting an exact `sim_tinv` here would move every half-width by a few
384 * parts in a thousand and make a cross-engine comparison of CI widths fail for
385 * a reason that has nothing to do with the simulation.
386 */
387inline double t_critical(double level, int df) {
388 static const double t90[30] = {
389 6.314, 2.920, 2.353, 2.132, 2.015, 1.943, 1.895, 1.860, 1.833, 1.812,
390 1.796, 1.782, 1.771, 1.761, 1.753, 1.746, 1.740, 1.734, 1.729, 1.725,
391 1.721, 1.717, 1.714, 1.711, 1.708, 1.706, 1.703, 1.701, 1.699, 1.697};
392 static const double t95[30] = {
393 12.706, 4.303, 3.182, 2.776, 2.571, 2.447, 2.365, 2.306, 2.262, 2.228,
394 2.201, 2.179, 2.160, 2.145, 2.131, 2.120, 2.110, 2.101, 2.093, 2.086,
395 2.080, 2.074, 2.069, 2.064, 2.060, 2.056, 2.052, 2.048, 2.045, 2.042};
396 static const double t99[30] = {
397 63.657, 9.925, 5.841, 4.604, 4.032, 3.707, 3.499, 3.355, 3.250, 3.169,
398 3.106, 3.055, 3.012, 2.977, 2.947, 2.921, 2.898, 2.878, 2.861, 2.845,
399 2.831, 2.819, 2.807, 2.797, 2.787, 2.779, 2.771, 2.763, 2.756, 2.750};
400 const int idx = std::min(df, 30) - 1;
401 if (idx < 0) return 1.96;
402 if (level >= 0.99) return t99[idx];
403 if (level >= 0.95) return t95[idx];
404 return t90[idx];
405}
406
407
408/**
409 * Heidelberger-Welch spectral estimate of the variance of the sample mean.
410 *
411 * The variance of a mean over a correlated series is 2*pi*S(0)/m, with S(0)
412 * the spectral density at frequency zero. S(0) cannot be read off the
413 * periodogram directly -- the ordinate at f=0 is exactly the quantity being
414 * estimated and carries all the bias -- so the LOG-periodogram over the lowest
415 * frequencies is fitted with a quadratic and the intercept extrapolated back
416 * to zero. That is what makes this estimator different from batch means rather
417 * than a re-spelling of it: it models the correlation instead of trying to
418 * batch it away.
419 *
420 * FALLS BACK TO PLAIN BATCH MEANS, not to an error, whenever the fit cannot be
421 * trusted: too few batches, too few regression points, a zero periodogram
422 * ordinate (log undefined) or a non-positive intercept. A spectral estimate
423 * from a singular fit is not conservative, it is arbitrary. Transcribes
424 * `computeSpectralStatisticsInternal`.
425 */
426inline StatTriple spectral_statistics(const std::vector<double>& obs, int batch_size,
427 double low_freq_frac) {
428 const std::size_t n = obs.size();
429 const int m = (batch_size > 0) ? static_cast<int>(n / static_cast<std::size_t>(batch_size)) : 0;
430 if (m < 4) return bm_statistics(obs, batch_size);
431
432 std::vector<double> means(static_cast<std::size_t>(m), 0.0);
433 for (int i = 0; i < m; ++i) {
434 double sum = 0.0;
435 for (int j = 0; j < batch_size; ++j)
436 sum += obs[static_cast<std::size_t>(i * batch_size + j)];
437 means[static_cast<std::size_t>(i)] = sum / batch_size;
438 }
439 double grand = 0.0;
440 for (int i = 0; i < m; ++i) grand += means[static_cast<std::size_t>(i)];
441 grand /= m;
442
443 const int nf = m / 2;
444 if (nf < 2) return bm_statistics(obs, batch_size);
445 std::vector<double> per(static_cast<std::size_t>(nf), 0.0);
446 for (int k = 1; k <= nf; ++k) {
447 double re = 0.0, im = 0.0;
448 const double f = 2.0 * 3.14159265358979323846 * k / m;
449 for (int t = 0; t < m; ++t) {
450 const double c = means[static_cast<std::size_t>(t)] - grand;
451 re += c * std::cos(f * t);
452 im += c * std::sin(f * t);
453 }
454 per[static_cast<std::size_t>(k - 1)] = (re * re + im * im) / m;
455 }
456
457 int npts = static_cast<int>(nf * low_freq_frac);
458 if (npts < 3) npts = 3;
459 if (npts > nf) return bm_statistics(obs, batch_size);
460 for (int k = 0; k < npts; ++k)
461 if (!(per[static_cast<std::size_t>(k)] > 0.0)) return bm_statistics(obs, batch_size);
462
463 // Ordinary least squares for log I(f) = b0 + b1 f + b2 f^2, solved through
464 // the 3x3 normal equations by Gaussian elimination: the design is tiny and
465 // pulling in a general solver here would only add a failure mode.
466 double A[3][4];
467 for (int r = 0; r < 3; ++r)
468 for (int c = 0; c < 4; ++c) A[r][c] = 0.0;
469 for (int k = 0; k < npts; ++k) {
470 const double f = 2.0 * 3.14159265358979323846 * (k + 1) / m;
471 const double x[3] = {1.0, f, f * f};
472 const double y = std::log(per[static_cast<std::size_t>(k)]);
473 for (int r = 0; r < 3; ++r) {
474 for (int c = 0; c < 3; ++c) A[r][c] += x[r] * x[c];
475 A[r][3] += x[r] * y;
476 }
477 }
478 for (int i = 0; i < 3; ++i) {
479 int piv = i;
480 for (int r = i + 1; r < 3; ++r)
481 if (std::fabs(A[r][i]) > std::fabs(A[piv][i])) piv = r;
482 if (!(std::fabs(A[piv][i]) > 1e-300)) return bm_statistics(obs, batch_size);
483 if (piv != i)
484 for (int c = 0; c < 4; ++c) std::swap(A[i][c], A[piv][c]);
485 for (int r = 0; r < 3; ++r) {
486 if (r == i) continue;
487 const double fct = A[r][i] / A[i][i];
488 for (int c = i; c < 4; ++c) A[r][c] -= fct * A[i][c];
489 }
490 }
491 const double b0 = A[0][3] / A[0][0];
492 const double s0 = std::exp(b0);
493 if (!(s0 > 0.0) || !std::isfinite(s0)) return bm_statistics(obs, batch_size);
494
495 StatTriple st;
496 st.mean = grand;
497 st.stderr_ = std::sqrt(2.0 * 3.14159265358979323846 * s0 / m);
498 st.df = npts - 3;
499 if (st.df < 1) st.df = 1;
500 st.ok = true;
501 return st;
502}
503
504/** Dispatch on `cimethod`. */
505inline StatTriple ci_statistics(const std::vector<double>& obs, int batch_size,
506 const LdesOptions& o) {
507 if (o.cimethod == "bm") return bm_statistics(obs, batch_size);
508 if (o.cimethod == "spectral")
509 return spectral_statistics(obs, batch_size, o.spectral_low_freq_frac);
510 return obm_statistics(obs, batch_size, o.obmoverlap);
511}
512
513
514/**
515 * Convergence-based stopping: batch the four metrics and stop once EVERY
516 * active (station, class) pair has reached the requested relative precision.
517 *
518 * THE CONJUNCTION IS THE POINT. A run stops when the WORST metric at the WORST
519 * pair is precise enough, not when the aggregate is: stopping on an average
520 * relative precision leaves the lightly loaded stations, whose estimates
521 * converge slowest, arbitrarily wrong while the report looks converged.
522 *
523 * A pair whose mean is effectively zero counts as converged: a relative
524 * precision against zero is not a bound, it is a division. Transcribes
525 * `checkConvergence` and `isMetricConverged`.
526 */
528public:
529 void init(std::size_t M, std::size_t K, const LdesOptions& o, std::uint64_t max_events) {
530 enabled_ = o.cnvgon;
531 if (!enabled_) return;
532 tol_ = o.cnvgtol;
533 min_batches_ = o.cnvgbatch;
534 interval_ = (o.cnvgchk > 0) ? static_cast<std::uint64_t>(o.cnvgchk)
535 : std::max<std::uint64_t>(1, max_events / 50);
536 level_ = (o.confint > 0.0) ? o.confint : 0.95;
537 q_.assign(M, std::vector<std::vector<double>>(K));
538 u_.assign(M, std::vector<std::vector<double>>(K));
539 r_.assign(M, std::vector<std::vector<double>>(K));
540 t_.assign(M, std::vector<std::vector<double>>(K));
541 start_qt_.assign(M, std::vector<double>(K, 0.0));
542 start_bt_.assign(M, std::vector<double>(K, 0.0));
543 start_cmp_.assign(M, std::vector<double>(K, 0.0));
544 start_rsum_.assign(M, std::vector<double>(K, 0.0));
545 start_rcnt_.assign(M, std::vector<double>(K, 0.0));
546 nstations_ = M;
547 nclasses_ = K;
548 }
549
550 bool enabled() const { return enabled_; }
551 std::uint64_t interval() const { return interval_; }
552
553 /** Close the current batch at `now` and record one batch mean per metric. */
554 void finalize_batch(const Accum& acc, const std::vector<std::size_t>& nservers, double now) {
555 if (!enabled_) return;
556 const double dur = now - batch_start_;
557 if (!(dur > 0.0)) {
558 reset_batch(acc, now);
559 return;
560 }
561 for (std::size_t i = 0; i < nstations_; ++i)
562 for (std::size_t k = 0; k < nclasses_; ++k) {
563 const double ql = (acc.tot_qlen[i][k] - start_qt_[i][k]) / dur;
564 q_[i][k].push_back(ql);
565 const double c = acc.util_peak[i] > 0.0
566 ? acc.util_peak[i]
567 : static_cast<double>(nservers[i] > 0 ? nservers[i] : 1);
568 u_[i][k].push_back((acc.tot_busy[i][k] - start_bt_[i][k]) / (dur * c));
569 t_[i][k].push_back((acc.completed[i][k] - start_cmp_[i][k]) / dur);
570 const double dn = acc.resp_cnt[i][k] - start_rcnt_[i][k];
571 r_[i][k].push_back(dn > 0.0 ? (acc.resp_sum[i][k] - start_rsum_[i][k]) / dn : 0.0);
572 }
573 reset_batch(acc, now);
574 }
575
576 /** True once every active pair has reached the tolerance on all four metrics. */
577 bool converged(const std::vector<std::vector<bool>>& off) const {
578 if (!enabled_) return false;
579 if (nstations_ == 0 || nclasses_ == 0) return false;
580 if (static_cast<int>(q_[0][0].size()) < min_batches_) return false;
581 for (std::size_t i = 0; i < nstations_; ++i)
582 for (std::size_t k = 0; k < nclasses_; ++k) {
583 if (off[i][k]) continue;
584 if (!metric_ok(q_[i][k])) return false;
585 if (!metric_ok(u_[i][k])) return false;
586 if (!metric_ok(t_[i][k])) return false;
587 bool any_positive = false;
588 for (double v : r_[i][k])
589 if (v > 0.0) {
590 any_positive = true;
591 break;
592 }
593 if (any_positive && !metric_ok(r_[i][k])) return false;
594 }
595 return true;
596 }
597
598 int batches() const { return (nstations_ && nclasses_) ? static_cast<int>(q_[0][0].size()) : 0; }
599
600private:
601 void reset_batch(const Accum& acc, double now) {
602 batch_start_ = now;
603 for (std::size_t i = 0; i < nstations_; ++i)
604 for (std::size_t k = 0; k < nclasses_; ++k) {
605 start_qt_[i][k] = acc.tot_qlen[i][k];
606 start_bt_[i][k] = acc.tot_busy[i][k];
607 start_cmp_[i][k] = acc.completed[i][k];
608 start_rsum_[i][k] = acc.resp_sum[i][k];
609 start_rcnt_[i][k] = acc.resp_cnt[i][k];
610 }
611 }
612
613 bool metric_ok(const std::vector<double>& b) const {
614 const std::size_t n = b.size();
615 if (n < 2) return false;
616 double sum = 0.0;
617 for (double x : b) sum += x;
618 const double mean = sum / n;
619 // A relative precision against a zero mean is a division, not a bound.
620 if (std::fabs(mean) < 1e-12) return true;
621 double var = 0.0;
622 for (double x : b) var += (x - mean) * (x - mean);
623 var /= (n - 1);
624 const double se = std::sqrt(var / n);
625 const double half = t_critical(level_, static_cast<int>(n) - 1) * se;
626 return half / std::fabs(mean) <= tol_;
627 }
628
629 bool enabled_ = false;
630 double tol_ = 0.05, level_ = 0.95, batch_start_ = 0.0;
631 int min_batches_ = 20;
632 std::uint64_t interval_ = 1;
633 std::size_t nstations_ = 0, nclasses_ = 0;
634 std::vector<std::vector<std::vector<double>>> q_, u_, r_, t_;
635 std::vector<std::vector<double>> start_qt_, start_bt_, start_cmp_, start_rsum_, start_rcnt_;
636};
637
638/**
639 * Fill the half-width matrices of `res` from the post-warmup observation
640 * series. Transcribes `computeOBMConfidenceIntervals`.
641 *
642 * The three series are not treated alike, and that is the reference's design:
643 *
644 * - QUEUE LENGTH has one observation per interval already, so it batches
645 * directly;
646 * - THROUGHPUT is stored CUMULATIVE, so it is differenced into per-interval
647 * rates first -- batching the cumulative counts would estimate the variance
648 * of a random walk;
649 * - UTILIZATION is not measured separately at all. Its interval is the
650 * throughput's, carried through the utilization law U = T/(mu*c), which is
651 * exact for the mean and is what keeps the two intervals consistent.
652 *
653 * A series shorter than `ciminobs` yields NO interval rather than a wide one:
654 * a half-width computed from a handful of batches is not conservative, it is
655 * arbitrary.
656 */
657inline void batch_means_ci(const Observations& obs, const Truncation& tr, const LdesOptions& o,
658 LdesResult& res) {
659 if (o.cimethod == "none" || !(o.confint > 0.0) || obs.time.empty()) return;
660 const std::size_t M = obs.nstations, K = obs.nclasses;
661 res.QNCI = Matrix<double>(M, K, 0.0);
662 res.UNCI = Matrix<double>(M, K, 0.0);
663 res.RNCI = Matrix<double>(M, K, 0.0);
664 res.TNCI = Matrix<double>(M, K, 0.0);
665 const std::size_t t0 = tr.applied ? tr.index : 0;
666
667 for (std::size_t i = 0; i < M; ++i) {
668 for (std::size_t r = 0; r < K; ++r) {
669 if (t0 >= obs.qlen[i][r].size()) continue;
670 const std::vector<double> q(obs.qlen[i][r].begin() + static_cast<std::ptrdiff_t>(t0),
671 obs.qlen[i][r].end());
672 if (q.size() >= static_cast<std::size_t>(o.ciminobs)) {
673 const int b = std::max(o.ciminbatch, static_cast<int>(std::sqrt(
674 static_cast<double>(q.size()))));
675 const StatTriple s = ci_statistics(q, b, o);
676 if (s.ok) res.QNCI(i, r) = t_critical(o.confint, s.df) * s.stderr_;
677 }
678
679 std::vector<double> rates;
680 for (std::size_t k = t0 + 1; k < obs.cmp[i][r].size(); ++k) {
681 const double dt = obs.time[k] - obs.time[k - 1];
682 if (dt > 0.0) rates.push_back((obs.cmp[i][r][k] - obs.cmp[i][r][k - 1]) / dt);
683 }
684 if (rates.size() >= static_cast<std::size_t>(o.ciminobs)) {
685 const int b = std::max(o.ciminbatch, static_cast<int>(std::sqrt(
686 static_cast<double>(rates.size()))));
687 const StatTriple s = ci_statistics(rates, b, o);
688 if (s.ok) {
689 const double half = t_critical(o.confint, s.df) * s.stderr_;
690 res.TNCI(i, r) = half;
691 if (res.TN.rows() > i && res.RN.rows() > i && res.UN(i, r) > 0.0 &&
692 res.TN(i, r) > 0.0)
693 res.UNCI(i, r) = half * res.UN(i, r) / res.TN(i, r);
694 }
695 }
696 }
697 }
698
699 // HOW LONG THE RUN SHOULD HAVE BEEN, when the caller asked for it. The
700 // half-widths just computed pin the ASYMPTOTIC variance of each estimator,
701 // which is the quantity a run length is planned from -- not the stationary
702 // variance, which on M/M/1 differs from it by a factor blowing up like
703 // (1-rho)^-2.
704 if (o.run_length_plan_precision > 0.0) {
705 // The ACTUAL number of events simulated where the result carries it,
706 // not the budget: LDES stops early on convergence, and planning from a
707 // budget it never spent would overstate N and so overstate sigma^2.
708 const double used = res.total_simulated_events > 0
709 ? static_cast<double>(res.total_simulated_events)
710 : static_cast<double>(o.events > 0 ? o.events : o.samples);
711 if (used > 0.0) {
713 res.QN, res.QNCI, used, o.run_length_plan_precision, o.confint);
714 res.has_run_length_plan = true;
715 }
716 }
717}
718
719} // namespace engine
720} // namespace ldes
721} // namespace line
722
723#endif // LINE_SOLVERS_LDES_LDES_STATS_H
std::size_t rows() const
Definition matrix.h:89
Convergence-based stopping: batch the four metrics and stop once EVERY active (station,...
Definition ldes_stats.h:527
bool converged(const std::vector< std::vector< bool > > &off) const
True once every active pair has reached the tolerance on all four metrics.
Definition ldes_stats.h:577
std::uint64_t interval() const
Definition ldes_stats.h:551
void init(std::size_t M, std::size_t K, const LdesOptions &o, std::uint64_t max_events)
Definition ldes_stats.h:529
void finalize_batch(const Accum &acc, const std::vector< std::size_t > &nservers, double now)
Close the current batch at now and record one batch mean per metric.
Definition ldes_stats.h:554
The option and result records of SolverLDES, the discrete-event simulator.
Dense matrix and non-owning view.
StatTriple bm_statistics(const std::vector< double > &obs, int batch_size)
Non-overlapping batch means.
Definition ldes_stats.h:315
StatTriple spectral_statistics(const std::vector< double > &obs, int batch_size, double low_freq_frac)
Heidelberger-Welch spectral estimate of the variance of the sample mean.
Definition ldes_stats.h:426
void batch_means_ci(const Observations &obs, const Truncation &tr, const LdesOptions &o, LdesResult &res)
Fill the half-width matrices of res from the post-warmup observation series.
Definition ldes_stats.h:657
StatTriple ci_statistics(const std::vector< double > &obs, int batch_size, const LdesOptions &o)
Dispatch on cimethod.
Definition ldes_stats.h:505
StatTriple obm_statistics(const std::vector< double > &obs, int batch_size, double overlap)
Overlapping batch means.
Definition ldes_stats.h:343
double t_critical(double level, int df)
The two-sided t critical value, from the REFERENCE'S TABLE.
Definition ldes_stats.h:387
double overlap_adjustment(double overlap)
The variance inflation of OVERLAPPING batch means.
Definition ldes_stats.h:308
int mser5_truncation(const std::vector< double > &obs, int batch_size)
MSER-5 truncation point over a series of observations, in BATCHES.
Definition ldes_stats.h:148
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.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
The knobs of one LDES run.
int ciminbatch
–ciminbatch
double spectral_low_freq_frac
–spectrallowfreqfrac
double cnvgtol
–cnvgtol
double confint
Confidence level of the reported half-widths.
double obmoverlap
–obmoverlap; 0 reduces OBM to plain batch means
double run_length_plan_precision
options.config.runLengthPlan: ask for the run length this run SHOULD have had, for a target relative ...
std::string cimethod
–cimethod: obm | bm | spectral | none
int cnvgchk
–cnvgchk, events between checks; 0 = samples/50
int ciminobs
–ciminobs, below which no CI is reported
std::size_t events
0 = not given; overrides samples when set
int cnvgbatch
–cnvgbatch, batches before the first check
std::size_t samples
-s, service-completion budget
One ldes-result document, parsed.
sim::RunLengthPlan< double > run_length_plan
Matrix< double > TNCI
bool has_run_length_plan
options.config.runLengthPlan: the run length the caller would need for the precision they asked for,...
Matrix< double > UN
Matrix< double > TN
Matrix< double > UNCI
Matrix< double > RN
Matrix< double > QNCI
long long total_simulated_events
Matrix< double > RNCI
Matrix< double > QN
The running per-(station, class) integrals and tallies.
Definition ldes_stats.h:46
std::vector< std::vector< double > > held
Callers parked on their server awaiting a synchronous REPLY (sn.syncreply), per (station,...
Definition ldes_stats.h:106
Accum(std::size_t M, std::size_t K)
Definition ldes_stats.h:47
void update_busy(std::size_t i, std::size_t r, double now)
Advance the busy-server integral of (i,r) to now.
Definition ldes_stats.h:78
std::vector< std::vector< double > > busy
Definition ldes_stats.h:107
std::vector< double > busy_scale
The speed each station is running at right now (load dependence only), and the peak capacity that nor...
Definition ldes_stats.h:120
std::vector< std::vector< double > > last_qlen
Definition ldes_stats.h:109
std::vector< std::vector< double > > resp_cnt
Definition ldes_stats.h:121
std::vector< std::vector< double > > qlen
Definition ldes_stats.h:97
std::vector< std::vector< double > > tot_qlen
Definition ldes_stats.h:108
std::vector< std::vector< double > > completed
Definition ldes_stats.h:121
std::vector< std::vector< double > > resp_sum
Definition ldes_stats.h:121
std::vector< std::vector< double > > join_dropped
Siblings a QUORUM Join discarded, per (station, class): a sibling that reaches the Join after its par...
Definition ldes_stats.h:137
void update_qlen(std::size_t i, std::size_t r, double now)
Advance the queue-length integral of (i,r) to now.
Definition ldes_stats.h:64
void set_busy_scale(std::size_t i, double v, double now)
Install the load-dependent speed station i runs at from now on.
Definition ldes_stats.h:91
std::vector< std::vector< double > > tot_busy
Definition ldes_stats.h:108
std::vector< std::vector< double > > arrived
Jobs that ARRIVED at each (station, class), which is not what completed counts: the reference reports...
Definition ldes_stats.h:129
std::vector< std::vector< double > > last_busy
Definition ldes_stats.h:109
std::vector< double > util_peak
Definition ldes_stats.h:120
The event-spaced observation series MSER-5 and the CI both read.
Definition ldes_stats.h:191
Truncation truncate() const
The truncation point, on the AGGREGATE queue length first.
Definition ldes_stats.h:247
std::vector< std::vector< double > > last_qt
Definition ldes_stats.h:284
std::vector< std::vector< std::vector< double > > > bt
Definition ldes_stats.h:283
std::vector< std::vector< std::vector< double > > > qlen
Definition ldes_stats.h:283
std::vector< std::vector< std::vector< double > > > cmp
Definition ldes_stats.h:283
std::vector< char > in_mser
Whether station i feeds the truncation criterion; every one is still recorded.
Definition ldes_stats.h:286
std::vector< double > time
Definition ldes_stats.h:282
void collect(const Accum &acc, double now)
Record one observation.
Definition ldes_stats.h:215
std::vector< std::vector< std::vector< double > > > drp
Definition ldes_stats.h:283
std::vector< std::vector< std::vector< double > > > qt
Definition ldes_stats.h:283
Observations(std::size_t M, std::size_t K, bool mser_on, int batch)
Definition ldes_stats.h:192
Grand mean, standard error and degrees of freedom of a batch-means estimate.
Definition ldes_stats.h:292
Where the warmup ended, and whether a truncation was applied at all.
Definition ldes_stats.h:184
double warmup_end
the instant of that observation
Definition ldes_stats.h:186
std::size_t index
observation index of the truncation point
Definition ldes_stats.h:185