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 on GIVEN fitting arguments, the reference's
98 * hyperexp_fit_longtail_core with its `points` input: cs strictly decreasing,
99 * one component per argument.
100 *
101 * @param ccdf F^c(t) = P(X > t)
102 * @param cs the fitting arguments c_1 > c_2 > ... > c_k
103 * @param b the within-scale spacing, b > 1
104 */
105template <class T, class Ccdf>
106HyperexpLongtailResult<T> hyperexp_fit_longtail_points(Ccdf&& ccdf, const std::vector<T>& cs,
107 const T& b) {
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 const std::size_t k = cs.size();
115 if (k < 1) throw InputError("hyperexp_fit_longtail: at least one component is required");
116 if (b <= one) throw InputError("hyperexp_fit_longtail: the spacing b must exceed 1");
117 for (std::size_t i = 1; i < k; ++i)
118 if (!(cs[i] < cs[i - 1]))
119 throw InputError("hyperexp_fit_longtail: the fitting arguments must be strictly "
120 "decreasing");
121
122 std::vector<T> p(k, zero), lam(k, zero);
123 for (std::size_t i = 0; i < k; ++i) {
124 const T ci = cs[i];
125 // Eqs. (4.6)-(4.7): what the already-fitted, slower components leave.
126 T residC = ccdf(ci), residBC = ccdf(T(b * ci));
127 for (std::size_t j = 0; j < i; ++j) {
128 residC -= p[j] * exp(-lam[j] * ci);
129 residBC -= p[j] * exp(-lam[j] * b * ci);
130 }
131 if (i + 1 < k) {
132 if (residC <= zero || residBC <= zero || residC <= residBC)
133 throw InputError("hyperexp_fit_longtail: the residual ccdf is not positive and "
134 "decreasing at a fitting argument. The recursion needs the "
135 "arguments well separated, c_i/c_(i+1) >> b; widen decade, lower "
136 "k, or move c1 further into the tail");
137 lam[i] = log(residC / residBC) / ((b - one) * ci); // eq. (4.10)
138 p[i] = residC * exp(lam[i] * ci); // eq. (4.11)
139 } else {
140 // Eqs. (4.12)-(4.14): the last component takes the rest of the mass.
141 T rest = one;
142 for (std::size_t j = 0; j < i; ++j) rest -= p[j];
143 if (rest <= zero)
144 throw InputError("hyperexp_fit_longtail: the fitted components already carry all "
145 "the probability, so the last one has none left");
146 if (residC <= zero)
147 throw InputError("hyperexp_fit_longtail: the residual ccdf has gone non-positive "
148 "at the last fitting argument");
149 p[i] = rest;
150 lam[i] = log(p[i] / residC) / ci; // eq. (4.14)
151 }
152 if (lam[i] <= zero)
153 throw InputError("hyperexp_fit_longtail: a non-positive rate came out of the fit; the "
154 "ccdf is not decaying fast enough for this many components");
155 }
156
157 auto fitted = [&](const T& t) {
158 T v = zero;
159 for (std::size_t j = 0; j < k; ++j) v += p[j] * exp(-lam[j] * t);
160 return v;
161 };
163 r.p = p;
164 r.lambda = lam;
165 r.points = cs;
166 r.mean = zero;
167 for (std::size_t j = 0; j < k; ++j) r.mean += p[j] / lam[j];
168 r.coverageLow = cs[k - 1];
169 r.coverageHigh = cs[0] * b;
170 // The target mean over the covered range, by the trapezoid rule: a k too
171 // small to reach the body shows up here and nowhere else.
172 const std::size_t gn = 20000;
173 const T h = r.coverageHigh / num_traits<T>::from_int(static_cast<long>(gn));
174 T acc = (ccdf(zero) + ccdf(r.coverageHigh)) / num_traits<T>::from_int(2);
175 for (std::size_t i = 1; i < gn; ++i)
176 acc += ccdf(T(num_traits<T>::from_int(static_cast<long>(i)) * h));
177 r.targetMean = acc * h;
178 r.maxRelError = zero;
179 for (std::size_t i = 0; i < k; ++i) {
180 for (int j = 0; j < 2; ++j) {
181 const T t = j == 0 ? cs[i] : T(b * cs[i]);
182 const T target = ccdf(t);
183 if (target > zero) {
184 const T e = num_abs(T(fitted(t) - target)) / target;
185 if (e > r.maxRelError) r.maxRelError = e;
186 }
187 }
188 }
189 // The fit is exact at the fitting arguments by construction; this says
190 // whether it also holds BETWEEN them.
191 r.maxRelErrorGrid = zero;
192 const T loLog = log(r.coverageLow), hiLog = log(r.coverageHigh);
193 for (int i = 0; i < 200; ++i) {
194 const T t = exp(loLog + (hiLog - loLog) * num_traits<T>::from_rational(i, 199));
195 const T target = ccdf(t);
196 if (target > num_traits<T>::from_double(1e-300)) {
197 const T e = num_abs(T(fitted(t) - target)) / target;
198 if (e > r.maxRelErrorGrid) r.maxRelErrorGrid = e;
199 }
200 }
201 return r;
202}
203
204/**
205 * The recursion at a fixed component count.
206 *
207 * @param ccdf F^c(t) = P(X > t)
208 * @param k number of exponential components
209 * @param c1 the largest fitting argument
210 * @param b the within-scale spacing, 1 < b < decade
211 * @param decade the ratio between successive fitting arguments
212 */
213template <class T, class Ccdf>
214HyperexpLongtailResult<T> hyperexp_fit_longtail_k(Ccdf&& ccdf, std::size_t k, const T& c1,
215 const T& b, const T& decade) {
216 const T one = num_traits<T>::from_int(1);
217 if (k < 1) throw InputError("hyperexp_fit_longtail: at least one component is required");
218 if (b <= one) throw InputError("hyperexp_fit_longtail: the spacing b must exceed 1");
219 if (decade <= b)
220 throw InputError("hyperexp_fit_longtail: the decade ratio must exceed the spacing b, or "
221 "the fitting arguments would interleave");
222 std::vector<T> cs(k);
223 cs[0] = c1;
224 for (std::size_t i = 1; i < k; ++i) cs[i] = cs[i - 1] / decade;
225 return hyperexp_fit_longtail_points<T>(ccdf, cs, b);
226}
227
228/**
229 * The recursion at a fixed component count with the reference's default c1,
230 * the 1e-6 quantile of the ccdf (an empty 'c1' option).
231 */
232template <class T, class Ccdf>
234 const T& b = num_traits<T>::from_rational(3, 2),
235 const T& decade = num_traits<T>::from_int(4)) {
236 const T c1 = detail::hefit_quantile<T>(ccdf, num_traits<T>::from_double(1e-6));
237 return hyperexp_fit_longtail_k<T>(ccdf, k, c1, b, decade);
238}
239
240/**
241 * The fit with the component count chosen automatically: one per decade between
242 * the 0.9 quantile and the top argument c1 (the 1e-6 quantile unless given),
243 * retrying with fewer when the recursion runs out of probability near the body.
244 *
245 * @param ccdf F^c(t) = P(X > t)
246 * @param b the within-scale spacing
247 * @param decade the ratio between successive fitting arguments
248 * @param c1 the largest fitting argument; non-positive selects the 1e-6
249 * quantile, the reference's empty 'c1' option
250 */
251template <class T, class Ccdf>
253 const T& b = num_traits<T>::from_rational(3, 2),
254 const T& decade = num_traits<T>::from_int(4),
255 const T& c1 = num_traits<T>::from_int(0)) {
256 using std::log;
257 const T one = num_traits<T>::from_int(1);
258 if (b <= one) throw InputError("hyperexp_fit_longtail: the spacing b must exceed 1");
259 if (decade <= b)
260 throw InputError("hyperexp_fit_longtail: the decade ratio must exceed the spacing b, or "
261 "the fitting arguments would interleave");
262 const T top = c1 > num_traits<T>::from_int(0)
263 ? c1
264 : detail::hefit_quantile<T>(ccdf, num_traits<T>::from_double(1e-6));
265 const T body = detail::hefit_quantile<T>(ccdf, num_traits<T>::from_rational(9, 10));
266 if (body <= num_traits<T>::from_int(0) || top <= body)
267 throw InputError("hyperexp_fit_longtail: the ccdf gives no usable range of time scales");
268 long k0 = static_cast<long>(std::llround(num_traits<T>::to_double(T(log(top / body) / log(decade))))) + 1;
269 if (k0 < 2) k0 = 2;
270 // The recursion needs each component to dominate at its own scale. Near the
271 // body of a law with a lot of mass there (a Pareto, say) that fails and the
272 // remaining probability runs out; back off one component at a time.
273 for (long k = k0; k >= 2; --k) {
274 try {
275 return hyperexp_fit_longtail_k<T>(ccdf, static_cast<std::size_t>(k), top, b, decade);
276 } catch (const InputError&) {
277 continue;
278 }
279 }
280 throw InputError("hyperexp_fit_longtail: no component count admits the recursion; the ccdf may "
281 "not be long-tailed enough for this scheme");
282}
283
284/** The reference's name-value options: k, c1, b, decade, points (empty = unset). */
285template <class T>
287 std::size_t k = 0; ///< 'k': component count, 0 = automatic
288 T c1 = num_traits<T>::from_int(0); ///< 'c1': largest argument, <= 0 = 1e-6 quantile
289 T b = num_traits<T>::from_rational(3, 2); ///< 'b'
290 T decade = num_traits<T>::from_int(4); ///< 'decade'
291 std::vector<T> points; ///< 'points': explicit fitting arguments
292};
293
294/**
295 * hyperexp_fit_longtail(ccdf, 'name', value, ...): 'points' fixes the fitting
296 * arguments (and k); else 'k' fixes the count with c1 the 1e-6 quantile unless
297 * given; else the automatic path, with 'c1' overriding its top argument.
298 */
299template <class T, class Ccdf>
301 const T one = num_traits<T>::from_int(1);
302 if (opt.b <= one) throw InputError("hyperexp_fit_longtail: the spacing b must exceed 1");
303 if (opt.decade <= opt.b)
304 throw InputError("hyperexp_fit_longtail: the decade ratio must exceed the spacing b, or "
305 "the fitting arguments would interleave");
306 if (!opt.points.empty()) return hyperexp_fit_longtail_points<T>(ccdf, opt.points, opt.b);
307 if (opt.k > 0) {
308 const T c1 = opt.c1 > num_traits<T>::from_int(0)
309 ? opt.c1
310 : detail::hefit_quantile<T>(ccdf, num_traits<T>::from_double(1e-6));
311 return hyperexp_fit_longtail_k<T>(ccdf, opt.k, c1, opt.b, opt.decade);
312 }
313 return hyperexp_fit_longtail<T>(ccdf, opt.b, opt.decade, opt.c1);
314}
315
316} // namespace mam
317} // namespace line
318
319#endif // LINE_API_MAM_HYPEREXP_FIT_LONGTAIL_H
Malformed or inconsistent input (dimensions, negative populations, ...): LINE:input:<kind>.
Definition error.h:103
InputError(const std::string &what)
Definition error.h:105
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_points(Ccdf &&ccdf, const std::vector< T > &cs, const T &b)
The recursion on GIVEN fitting arguments, the reference's hyperexp_fit_longtail_core with its points ...
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), const T &c1=num_traits< T >::from_int(0))
The fit with the component count chosen automatically: one per decade between the 0....
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
T num_abs(const T &v)
Definition number.h:184
Number-type abstraction for the templated API port.
The reference's name-value options: k, c1, b, decade, points (empty = unset).
std::size_t k
'k': component count, 0 = automatic
T c1
'c1': largest argument, <= 0 = 1e-6 quantile
std::vector< T > points
'points': explicit fitting arguments
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