LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
hyperexp_fit_longtail.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_MAM_HYPEREXP_FIT_LONGTAIL_H
6#define LINE_API_MAM_HYPEREXP_FIT_LONGTAIL_H
7
8/**
9 * @file
10 * @ingroup api_mam
11 * Fitting a hyperexponential to a long-tail distribution.
12 *
13 * Templated port of matlab/src/api/mam/hyperexp_fit_longtail.m, cross-checked
14 * against jar/src/main/java/jline/api/mam/HyperexpFitLongtail.java.
15 *
16 * WHY MOMENTS ARE THE WRONG HANDLE. A Pareto law with tail index below 2 has
17 * infinite variance, so no two- or three-moment fit exists at all; and even when
18 * the moments are finite, matching them says nothing about the several ORDERS OF
19 * MAGNITUDE of time scale over which a long-tail law acts. This procedure
20 * matches the CCDF ITSELF at points spread across those decades.
21 *
22 * THE RECURSION, with lambda_1 < ... < lambda_k. In the far tail only the
23 * slowest component survives, so it can be fitted there alone:
24 *
25 * lambda_1 = ln(F^c(c_1)/F^c(b c_1))/((b-1)c_1) (4.4)
26 * p_1 = F^c(c_1) exp(lambda_1 c_1) (4.5)
27 *
28 * subtract it and repeat one decade lower (4.6)-(4.11); the last component takes
29 * the remaining probability, p_k = 1 - sum_{j<k} p_j, and its rate follows from
30 * the ccdf at c_k (4.12)-(4.14). This is Prony's method applied to a ccdf.
31 *
32 * DEFAULTS. (b, decade) = (1.5, 4) rather than the paper's illustrative (2, 10):
33 * the fit is exact AT the fitting arguments and free between them, and measured
34 * on a Weibull(0.3) the tighter grid cuts the worst between-point error from
35 * about 54% to 12%, at the cost of more components.
36 *
37 * ARITHMETIC. Logarithms and exponentials throughout: transcendental only.
38 *
39 * Reference: A. Feldmann, W. Whitt (1998). Fitting mixtures of exponentials to
40 * long-tail distributions to analyze network performance models. Performance
41 * Evaluation 31, 245-279, Section 4.
42 */
43
44#include <cmath>
45#include <cstddef>
46#include <functional>
47#include <vector>
48
49#include "line/num/number.h"
50#include "line/util/error.h"
51
52namespace line {
53namespace mam {
54
55/** Outcome of the long-tail hyperexponential fit. */
56template <class T>
58 std::vector<T> p; ///< mixing probabilities, summing to 1
59 std::vector<T> lambda; ///< rates, increasing
60 std::vector<T> points; ///< the fitting arguments c_i
61 T mean; ///< mean of the fitted law
62 T targetMean; ///< mean of the original law over the covered range
63 T coverageLow; ///< c_k, the smallest constrained argument
64 T coverageHigh; ///< b c_1, the largest
65 T maxRelError; ///< worst relative error at the fitting arguments
66 T maxRelErrorGrid; ///< worst relative error on a log grid across the coverage
67};
68
69namespace detail {
70
71/** Smallest t with F^c(t) <= prob, by doubling then bisection. */
72template <class T, class Ccdf>
73T hefit_quantile(Ccdf&& ccdf, const T& prob) {
74 const T two = num_traits<T>::from_int(2);
76 while (ccdf(hi) > prob) {
77 hi *= two;
78 if (hi > num_traits<T>::from_double(1e15))
79 throw InputError("hyperexp_fit_longtail: the ccdf does not decay, so there is no tail "
80 "to fit");
81 }
83 for (int i = 0; i < 200; ++i) {
84 const T mid = (lo + hi) / two;
85 if (ccdf(mid) > prob) {
86 lo = mid;
87 } else {
88 hi = mid;
89 }
90 }
91 return (lo + hi) / two;
92}
93
94} // namespace detail
95
96/**
97 * The recursion at a fixed component count.
98 *
99 * @param ccdf F^c(t) = P(X > t)
100 * @param k number of exponential components
101 * @param c1 the largest fitting argument
102 * @param b the within-scale spacing, 1 < b < decade
103 * @param decade the ratio between successive fitting arguments
104 */
105template <class T, class Ccdf>
106HyperexpLongtailResult<T> hyperexp_fit_longtail_k(Ccdf&& ccdf, std::size_t k, const T& c1,
107 const T& b, const T& decade) {
109 "hyperexp_fit_longtail needs logarithms and exponentials");
110 using std::exp;
111 using std::log;
112 const T zero = num_traits<T>::from_int(0);
113 const T one = num_traits<T>::from_int(1);
114 if (k < 1) throw InputError("hyperexp_fit_longtail: at least one component is required");
115 if (b <= one) throw InputError("hyperexp_fit_longtail: the spacing b must exceed 1");
116 if (decade <= b)
117 throw InputError("hyperexp_fit_longtail: the decade ratio must exceed the spacing b, or "
118 "the fitting arguments would interleave");
119
120 std::vector<T> cs(k);
121 cs[0] = c1;
122 for (std::size_t i = 1; i < k; ++i) cs[i] = cs[i - 1] / decade;
123
124 std::vector<T> p(k, zero), lam(k, zero);
125 for (std::size_t i = 0; i < k; ++i) {
126 const T ci = cs[i];
127 // Eqs. (4.6)-(4.7): what the already-fitted, slower components leave.
128 T residC = ccdf(ci), residBC = ccdf(T(b * ci));
129 for (std::size_t j = 0; j < i; ++j) {
130 residC -= p[j] * exp(-lam[j] * ci);
131 residBC -= p[j] * exp(-lam[j] * b * ci);
132 }
133 if (i + 1 < k) {
134 if (residC <= zero || residBC <= zero || residC <= residBC)
135 throw InputError("hyperexp_fit_longtail: the residual ccdf is not positive and "
136 "decreasing at a fitting argument. The recursion needs the "
137 "arguments well separated, c_i/c_(i+1) >> b; widen decade, lower "
138 "k, or move c1 further into the tail");
139 lam[i] = log(residC / residBC) / ((b - one) * ci); // eq. (4.10)
140 p[i] = residC * exp(lam[i] * ci); // eq. (4.11)
141 } else {
142 // Eqs. (4.12)-(4.14): the last component takes the rest of the mass.
143 T rest = one;
144 for (std::size_t j = 0; j < i; ++j) rest -= p[j];
145 if (rest <= zero)
146 throw InputError("hyperexp_fit_longtail: the fitted components already carry all "
147 "the probability, so the last one has none left");
148 if (residC <= zero)
149 throw InputError("hyperexp_fit_longtail: the residual ccdf has gone non-positive "
150 "at the last fitting argument");
151 p[i] = rest;
152 lam[i] = log(p[i] / residC) / ci; // eq. (4.14)
153 }
154 if (lam[i] <= zero)
155 throw InputError("hyperexp_fit_longtail: a non-positive rate came out of the fit; the "
156 "ccdf is not decaying fast enough for this many components");
157 }
158
159 auto fitted = [&](const T& t) {
160 T v = zero;
161 for (std::size_t j = 0; j < k; ++j) v += p[j] * exp(-lam[j] * t);
162 return v;
163 };
165 r.p = p;
166 r.lambda = lam;
167 r.points = cs;
168 r.mean = zero;
169 for (std::size_t j = 0; j < k; ++j) r.mean += p[j] / lam[j];
170 r.coverageLow = cs[k - 1];
171 r.coverageHigh = cs[0] * b;
172 // The target mean over the covered range, by the trapezoid rule: a k too
173 // small to reach the body shows up here and nowhere else.
174 const std::size_t gn = 20000;
175 const T h = r.coverageHigh / num_traits<T>::from_int(static_cast<long>(gn));
176 T acc = (ccdf(zero) + ccdf(r.coverageHigh)) / num_traits<T>::from_int(2);
177 for (std::size_t i = 1; i < gn; ++i)
178 acc += ccdf(T(num_traits<T>::from_int(static_cast<long>(i)) * h));
179 r.targetMean = acc * h;
180 r.maxRelError = zero;
181 for (std::size_t i = 0; i < k; ++i) {
182 for (int j = 0; j < 2; ++j) {
183 const T t = j == 0 ? cs[i] : T(b * cs[i]);
184 const T target = ccdf(t);
185 if (target > zero) {
186 const T e = num_abs(T(fitted(t) - target)) / target;
187 if (e > r.maxRelError) r.maxRelError = e;
188 }
189 }
190 }
191 // The fit is exact at the fitting arguments by construction; this says
192 // whether it also holds BETWEEN them.
193 r.maxRelErrorGrid = zero;
194 const T loLog = log(r.coverageLow), hiLog = log(r.coverageHigh);
195 for (int i = 0; i < 200; ++i) {
196 const T t = exp(loLog + (hiLog - loLog) * num_traits<T>::from_rational(i, 199));
197 const T target = ccdf(t);
198 if (target > num_traits<T>::from_double(1e-300)) {
199 const T e = num_abs(T(fitted(t) - target)) / target;
200 if (e > r.maxRelErrorGrid) r.maxRelErrorGrid = e;
201 }
202 }
203 return r;
204}
205
206/**
207 * The fit with the component count chosen automatically: one per decade between
208 * the 0.9 quantile and the 1e-6 quantile, retrying with fewer when the
209 * recursion runs out of probability near the body.
210 *
211 * @param ccdf F^c(t) = P(X > t)
212 * @param b the within-scale spacing
213 * @param decade the ratio between successive fitting arguments
214 */
215template <class T, class Ccdf>
217 const T& b = num_traits<T>::from_rational(3, 2),
218 const T& decade = num_traits<T>::from_int(4)) {
219 using std::log;
220 const T top = detail::hefit_quantile<T>(ccdf, num_traits<T>::from_double(1e-6));
221 const T body = detail::hefit_quantile<T>(ccdf, num_traits<T>::from_rational(9, 10));
222 if (body <= num_traits<T>::from_int(0) || top <= body)
223 throw InputError("hyperexp_fit_longtail: the ccdf gives no usable range of time scales");
224 long k0 = static_cast<long>(std::llround(num_traits<T>::to_double(T(log(top / body) / log(decade))))) + 1;
225 if (k0 < 2) k0 = 2;
226 // The recursion needs each component to dominate at its own scale. Near the
227 // body of a law with a lot of mass there (a Pareto, say) that fails and the
228 // remaining probability runs out; back off one component at a time.
229 for (long k = k0; k >= 2; --k) {
230 try {
231 return hyperexp_fit_longtail_k<T>(ccdf, static_cast<std::size_t>(k), top, b, decade);
232 } catch (const InputError&) {
233 continue;
234 }
235 }
236 throw InputError("hyperexp_fit_longtail: no component count admits the recursion; the ccdf may "
237 "not be long-tailed enough for this scheme");
238}
239
240} // namespace mam
241} // namespace line
242
243#endif // LINE_API_MAM_HYPEREXP_FIT_LONGTAIL_H
Malformed or inconsistent input (dimensions, negative populations, ...).
Definition error.h:37
InputError(const std::string &what)
Definition error.h:39
The exception types the port throws.
HyperexpLongtailResult< T > hyperexp_fit_longtail_k(Ccdf &&ccdf, std::size_t k, const T &c1, const T &b, const T &decade)
The recursion at a fixed component count.
HyperexpLongtailResult< T > hyperexp_fit_longtail(Ccdf &&ccdf, const T &b=num_traits< T >::from_rational(3, 2), const T &decade=num_traits< T >::from_int(4))
The fit with the component count chosen automatically: one per decade between the 0....
T num_abs(const T &v)
Definition number.h:172
Number-type abstraction for the templated API port.
Outcome of the long-tail hyperexponential fit.
std::vector< T > lambda
rates, increasing
T coverageLow
c_k, the smallest constrained argument
std::vector< T > points
the fitting arguments c_i
T maxRelError
worst relative error at the fitting arguments
std::vector< T > p
mixing probabilities, summing to 1
T maxRelErrorGrid
worst relative error on a log grid across the coverage
T targetMean
mean of the original law over the covered range