LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
infer_nhpp_ks.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_INFER_NHPP_KS_H
6#define LINE_API_INFER_NHPP_KS_H
7
8/**
9 * @file
10 * @ingroup api_infer
11 * Kolmogorov-Smirnov tests for a non-homogeneous Poisson arrival process.
12 *
13 * Templated port of matlab/src/api/infer/infer_nhpp_ks.m, cross-checked against
14 * jar/src/main/java/jline/api/infer/InferNhppKs.java.
15 *
16 * THE CONDITIONAL-UNIFORM TRANSFORMATION. Conditional on the number of arrivals
17 * in [T0,T], the arrival times of an NHPP are the order statistics of iid
18 * variables with cdf Lambda(t)/Lambda(T). Mapping the data through that cdf
19 * turns ANY NHPP, whatever its rate, into iid uniforms, so one KS test covers
20 * every rate function.
21 *
22 * WHY THE PLAIN TEST IS WEAK, AND WHAT FIXES IT. The CU KS test has "remarkably
23 * little power" against non-exponential interarrival times: it looks at the
24 * POSITIONS of the points, and those stay nearly uniform for many non-Poisson
25 * processes. Lewis (1965) applies the Durbin (1961) transformation first --
26 * reorder the GAPS ascending, rescale each by how many gaps remain, cumulate --
27 * which turns a difference in the gap DISTRIBUTION into a difference in
28 * position. Measured on 400 replications of an Erlang-4 renewal process, the CU
29 * test rejects at its own size while the Lewis test rejects essentially always.
30 *
31 * ARITHMETIC. exp and sqrt in the p-value: transcendental only.
32 *
33 * Reference: S.-H. Kim, W. Whitt (2014). Are call center and hospital arrivals
34 * well modeled by nonhomogeneous Poisson processes? M&SOM 16(3), 464-480;
35 * J. Durbin (1961), Biometrika 48, 41-55; P. A. W. Lewis (1965), JRSS B 27.
36 */
37
38#include <algorithm>
39#include <cmath>
40#include <cstddef>
41#include <functional>
42#include <string>
43#include <vector>
44
45#include "line/num/number.h"
46#include "line/util/error.h"
47
48namespace line {
49namespace infer {
50
51/** Which test to run on the conditional-uniform data. */
52enum class NhppKsMethod { Cu, Lewis };
53
54/** Outcome of the KS test. */
55template <class T>
57 T statistic; ///< the KS distance
58 T pvalue; ///< asymptotic p-value
59 std::size_t n = 0; ///< number of arrivals used
60 std::vector<T> uniforms; ///< after the conditional-uniform transformation
61 std::vector<T> transformed; ///< after the Durbin step, for the Lewis test
62};
63
64namespace detail {
65
66/** Two-sided KS distance between the sample and the uniform cdf. */
67template <class T>
68T nhpp_ks_stat(std::vector<T> u) {
69 std::sort(u.begin(), u.end());
70 const std::size_t n = u.size();
71 const T nT = num_traits<T>::from_int(static_cast<long>(n));
73 for (std::size_t i = 0; i < n; ++i) {
74 const T up = num_traits<T>::from_int(static_cast<long>(i + 1)) / nT - u[i];
75 const T lo = u[i] - num_traits<T>::from_int(static_cast<long>(i)) / nT;
76 if (up > d) d = up;
77 if (lo > d) d = lo;
78 }
79 return d;
80}
81
82/**
83 * Asymptotic Kolmogorov p-value with the small-sample correction of Stephens:
84 * the effective argument is (sqrt(n)+0.12+0.11/sqrt(n))D, accurate from n = 5.
85 */
86template <class T>
87T nhpp_ks_pvalue(const T& d, std::size_t n) {
88 using std::exp;
89 using std::sqrt;
90 const T one = num_traits<T>::from_int(1);
91 if (n == 0) return one;
92 const T nT = num_traits<T>::from_int(static_cast<long>(n));
93 const T x = (sqrt(nT) + num_traits<T>::from_double(0.12) +
94 num_traits<T>::from_double(0.11) / sqrt(nT)) * d;
95 if (x <= num_traits<T>::from_int(0)) return one;
97 for (int k = 1; k <= 100; ++k) {
98 const T term = exp(-num_traits<T>::from_int(2 * k * k) * x * x);
99 q += (k % 2 == 1) ? term : T(-term);
100 }
101 T p = num_traits<T>::from_int(2) * q;
102 if (p < num_traits<T>::from_int(0)) p = num_traits<T>::from_int(0);
103 if (p > one) p = one;
104 return p;
105}
106
107} // namespace detail
108
109/**
110 * @brief Kolmogorov-Smirnov tests for a non-homogeneous Poisson arrival
111 * process.
112 *
113 * @param times the arrival times, within [T0,T]
114 * @param T the right end of the observation interval
115 * @param cumRate the cumulative rate Lambda(t); a constant rate when empty
116 * @param method Cu for the plain test, Lewis for the Durbin-transformed one
117 * @param T0 the left end of the observation interval
118 */
119template <class Tv>
120NhppKsResult<Tv> infer_nhpp_ks(const std::vector<Tv>& times, const Tv& T,
121 const std::function<Tv(const Tv&)>& cumRate =
122 std::function<Tv(const Tv&)>(),
124 const Tv& T0 = num_traits<Tv>::from_int(0)) {
125 static_assert(num_traits<Tv>::has_transcendental, "infer_nhpp_ks needs exp for the p-value");
126 const Tv zero = num_traits<Tv>::from_int(0);
127 const Tv one = num_traits<Tv>::from_int(1);
128 std::vector<Tv> t;
129 for (const Tv& x : times)
130 if (x >= T0 && x <= T) t.push_back(x);
131 std::sort(t.begin(), t.end());
132 const std::size_t n = t.size();
133 if (n < 2) throw InputError("infer_nhpp_ks: at least two arrivals are needed to test");
134
135 std::vector<Tv> u(n);
136 if (!cumRate) {
137 for (std::size_t i = 0; i < n; ++i) u[i] = (t[i] - T0) / (T - T0);
138 } else {
139 const Tv lo = cumRate(T0), hi = cumRate(T);
140 if (hi <= lo)
141 throw InputError("infer_nhpp_ks: the cumulative rate must increase over the interval");
142 for (std::size_t i = 0; i < n; ++i) u[i] = (cumRate(t[i]) - lo) / (hi - lo);
143 }
144 for (Tv& v : u) {
145 if (v < zero) v = zero;
146 if (v > one) v = one;
147 }
148
150 r.n = n;
151 r.uniforms = u;
152 if (method == NhppKsMethod::Cu) {
153 r.transformed = u;
154 } else {
155 // The Durbin (1961) transformation: gaps, sorted ascending, each
156 // rescaled by how many gaps remain, then cumulated.
157 std::vector<Tv> v = u;
158 std::sort(v.begin(), v.end());
159 std::vector<Tv> gaps(n + 1);
160 gaps[0] = v[0];
161 for (std::size_t i = 1; i < n; ++i) gaps[i] = v[i] - v[i - 1];
162 gaps[n] = one - v[n - 1];
163 std::sort(gaps.begin(), gaps.end());
164 std::vector<Tv> c(n + 1);
165 Tv prev = zero;
166 for (std::size_t i = 0; i <= n; ++i) {
167 c[i] = num_traits<Tv>::from_int(static_cast<long>(n + 1 - i)) * (gaps[i] - prev);
168 prev = gaps[i];
169 }
170 r.transformed.resize(n);
171 Tv acc = zero;
172 for (std::size_t i = 0; i < n; ++i) {
173 acc += c[i];
174 Tv s = acc;
175 if (s < zero) s = zero;
176 if (s > one) s = one;
177 r.transformed[i] = s;
178 }
179 }
180 r.statistic = detail::nhpp_ks_stat(r.transformed);
181 r.pvalue = detail::nhpp_ks_pvalue(r.statistic, n);
182 return r;
183}
184
185} // namespace infer
186} // namespace line
187
188#endif // LINE_API_INFER_NHPP_KS_H
InputError(const std::string &what)
Definition error.h:39
The exception types the port throws.
NhppKsResult< Tv > infer_nhpp_ks(const std::vector< Tv > &times, const Tv &T, const std::function< Tv(const Tv &)> &cumRate=std::function< Tv(const Tv &)>(), NhppKsMethod method=NhppKsMethod::Lewis, const Tv &T0=num_traits< Tv >::from_int(0))
Kolmogorov-Smirnov tests for a non-homogeneous Poisson arrival process.
NhppKsMethod
Which test to run on the conditional-uniform data.
Number-type abstraction for the templated API port.
Outcome of the KS test.
T statistic
the KS distance
std::vector< T > uniforms
after the conditional-uniform transformation
std::size_t n
number of arrivals used
T pvalue
asymptotic p-value
std::vector< T > transformed
after the Durbin step, for the Lewis test