LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
dist_fitters.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_LANG_DIST_FITTERS_H
6#define LINE_LANG_DIST_FITTERS_H
7
8/**
9 * @file
10 * @ingroup line_lang
11 * The moment fitters the reference distributions carry as STATIC FACTORIES:
12 * `Erlang.fitMeanAndOrder`, `HyperExp.fitMeanAndSCV`, `Coxian.fitMeanAndSCV`,
13 * `Cox2.fitCentral`, `APH.fitMeanAndSCV`, `APH.fitCentral`,
14 * `Gamma.fitMeanAndSCV`, `Pareto.fitMeanAndSCV`.
15 *
16 * They live BESIDE `Distrib` rather than inside it because they are the only
17 * part of the distribution layer that needs `line::mam`: fitting an APH is
18 * `aph_fit`, fitting a two-phase hyperexponential is `map_hyperexp`, and
19 * `lang/lang_types.h` sits UNDER `api/` in the include order. A model script
20 * that only constructs distributions by parameter never pays for this header.
21 *
22 * WHAT IS A PORT AND WHAT IS A SUBSTITUTION. Every branch below is the
23 * reference's arithmetic transcribed, with one documented exception:
24 * `APH.fitCentral`/`fitMeanAndSCV` call BUTools' `APHFrom3Moments`, which this
25 * port does not transcribe, so they go through
26 * `mam::aph_fit` -- the same Bobbio-Horvath-Telek canonical APH, and the same
27 * substitution `api/qsys/qsys_mapg1.h:37-45` already documents.
28 *
29 * EVERY FITTER IS GATED ON TRANSCENDENTAL ARITHMETIC. Each one takes a square
30 * root of a moment discriminant, which has no exact rational counterpart; the
31 * gate is `mam::map_hyperexp`'s and is stated here rather than discovered as a
32 * link error in the exact instantiation.
33 */
34
35#include <cmath>
36#include <cstddef>
37#include <vector>
38
43#include "line/num/number.h"
44#include "line/util/error.h"
45
46namespace line {
47namespace lang {
48
49namespace fitdetail {
50
51/** sqrt and pow found by ADL, so a Boost.Multiprecision T resolves its own. */
52template <class T>
53T fsqrt(const T& v) {
54 using std::sqrt;
55 return sqrt(v);
56}
57
58template <class T>
59T fpow(const T& b, const T& e) {
60 using std::pow;
61 return pow(b, e);
62}
63
64} // namespace fitdetail
65
66/**
67 * A PH distribution from a fitted (D0, D1) pair.
68 *
69 * The initial vector is recovered from D1 rather than carried alongside it:
70 * D1 = (-D0 e) alpha by construction, so row i of D1 is alpha scaled by phase
71 * i's exit rate and ANY row with a positive exit rate recovers it.
72 *
73 * IT CANNOT BE ROW 0. The canonical APH `aph_fit` returns is BIDIAGONAL: phase
74 * 1 moves to phase 2 and never completes, so its exit rate is exactly zero and
75 * its D1 row is all zeros. Reading alpha off row 0 threw on every APH fit.
76 */
77template <class T>
78Distrib<T> ph_from_map(const mam::Map<T>& m, bool acyclic) {
79 const std::size_t n = m.D0.rows();
80 for (std::size_t i = 0; i < n; ++i) {
81 T out = num_traits<T>::from_int(0);
82 for (std::size_t j = 0; j < n; ++j) out += m.D1(i, j);
83 if (!(out > num_traits<T>::from_int(0))) continue;
84 std::vector<T> alpha(n);
85 for (std::size_t j = 0; j < n; ++j) alpha[j] = T(m.D1(i, j) / out);
86 return Distrib<T>::phase_type(alpha, m.D0, acyclic);
87 }
88 throw NumericError("ph_from_map: no phase completes, so the fitted PH has no alpha");
89}
90
91// ---------------------------------------------------------------------------
92// Erlang
93// ---------------------------------------------------------------------------
94
95/** `Erlang.fitMeanAndOrder(MEAN, k)`: k phases, each of rate k / MEAN. */
96template <class T>
97Distrib<T> erlang_fit_mean_order(const T& mean, std::size_t k) {
98 if (k == 0) throw InputError("Erlang.fitMeanAndOrder: the order must be positive");
99 if (!(mean > num_traits<T>::from_int(0)))
100 throw InputError("Erlang.fitMeanAndOrder: the mean must be positive");
101 return Distrib<T>::erlang(T(num_traits<T>::from_int(static_cast<int>(k)) / mean), k);
102}
103
104// ---------------------------------------------------------------------------
105// HyperExp
106// ---------------------------------------------------------------------------
107
108/**
109 * `HyperExp.fitMeanAndSCV(MEAN, SCV)`, which is `map_hyperexp` at p = 0.99
110 * read back as (p, mu1, mu2).
111 */
112template <class T>
113Distrib<T> hyperexp_fit_mean_scv(const T& mean, const T& scv) {
115 "HyperExp.fitMeanAndSCV needs a square root of the moment discriminant");
116 const mam::Map<T> m = mam::map_hyperexp(mean, scv, num_traits<T>::from_double(0.99));
117 // map_hyperexp returns D0 = diag(-mu1, -mu2) and D1(i, j) = mu_i * p_j, so
118 // the branch probability is read off row 0 and the rates off the diagonal.
119 const T mu1 = T(-m.D0(0, 0)), mu2 = T(-m.D0(1, 1));
120 const T p = T(m.D1(0, 0) / mu1);
121 return Distrib<T>::hyperexp(p, mu1, mu2);
122}
123
124/**
125 * `HyperExp.fitMeanAndSCVBalanced(MEAN, SCV)`: the balanced-means branch,
126 * p / mu1 = (1 - p) / mu2. Both roots are tried in the reference's order.
127 */
128template <class T>
129Distrib<T> hyperexp_fit_mean_scv_balanced(const T& mean, const T& scv) {
131 "HyperExp.fitMeanAndSCVBalanced needs a square root");
132 const T one = num_traits<T>::from_int(1), two = num_traits<T>::from_int(2);
133 const T root = fitdetail::fsqrt<T>(T((scv - one) / (scv + one)));
134 T p = T(one / two - root / two);
135 T mu1 = T(-(two * (root / two - one / two)) / mean);
136 if (!(mu1 > num_traits<T>::from_int(0)) || !(p > num_traits<T>::from_int(0)) || !(p < one)) {
137 p = T(root / two + one / two);
138 mu1 = T((two * (root / two + one / two)) / mean);
139 }
140 const T mu2 = T((one - p) / p * mu1);
141 return Distrib<T>::hyperexp(p, mu1, mu2);
142}
143
144// ---------------------------------------------------------------------------
145// Coxian
146// ---------------------------------------------------------------------------
147
148/**
149 * `Coxian.fitMeanAndSCV(MEAN, SCV)`, branch for branch.
150 *
151 * SCV below 1/2 is matched by an ERLANG of order ceil(1/SCV) expressed as a
152 * Coxian with every phi zero but the last -- the reference's own choice, and
153 * the reason this cannot be routed through `Distrib::erlang`: the order it
154 * picks matches the mean exactly and the SCV only from below.
155 */
156template <class T>
157Distrib<T> coxian_fit_mean_scv(const T& mean, const T& scv) {
158 static_assert(num_traits<T>::has_transcendental, "Coxian.fitMeanAndSCV needs a square root");
159 const double c2 = num_traits<T>::to_double(scv);
160 const double tol = GlobalConstants::CoarseTol;
161 const T one = num_traits<T>::from_int(1), two = num_traits<T>::from_int(2);
162 std::vector<T> mu, phi;
163 if (c2 >= 1.0 - tol && c2 <= 1.0 + tol) {
164 mu.push_back(T(one / mean));
165 phi.push_back(one);
166 } else if (c2 > 0.5 + tol && c2 < 1.0 - tol) {
167 const T r = fitdetail::fsqrt<T>(T(one + two * (scv - one)));
168 mu.push_back(T(two / mean / (one + r)));
169 mu.push_back(T(two / mean / (one - r)));
170 phi.push_back(num_traits<T>::from_int(0));
171 phi.push_back(one);
172 } else if (c2 <= 0.5 + tol) {
173 const std::size_t n = static_cast<std::size_t>(std::ceil(1.0 / c2));
174 const T lambda = T(num_traits<T>::from_int(static_cast<int>(n)) / mean);
175 mu.assign(n, lambda);
176 phi.assign(n, num_traits<T>::from_int(0));
177 phi[n - 1] = one;
178 } else {
179 // SCV > 1: the two-phase hyperexponential written as a Coxian.
180 mu.push_back(T(two / mean));
181 mu.push_back(T(mu[0] / (two * scv)));
182 phi.push_back(T(one - mu[1] / mu[0]));
183 phi.push_back(one);
184 }
185 return Distrib<T>::coxian(mu, phi);
186}
187
188/**
189 * `Cox2.fitCentral(MEAN, VAR, SKEW)`: the two-phase Coxian matching three
190 * central moments exactly when the moment set admits one.
191 *
192 * Both roots of the moment condition are tried in the reference's order, and
193 * the fallback when neither is feasible is the reference's: `fitMeanAndSCV`
194 * above SCV = 1/2 and the exponential of that mean below it.
195 */
196template <class T>
197Distrib<T> cox2_fit_central(const T& mean, const T& var, const T& skew) {
198 static_assert(num_traits<T>::has_transcendental, "Cox2.fitCentral needs a square root");
199 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
200 const T scv = T(var / (mean * mean));
201 const T e1 = mean;
202 const T e2 = T((one + scv) * e1 * e1);
203 const T e3 = T(-(num_traits<T>::from_int(2) * e1 * e1 * e1 -
204 num_traits<T>::from_int(3) * e1 * e2 -
205 skew * fitdetail::fpow<T>(T(e2 - e1 * e1), num_traits<T>::from_double(1.5))));
206
207 const T disc = T(num_traits<T>::from_int(24) * e1 * e1 * e1 * e3 -
208 num_traits<T>::from_int(27) * e1 * e1 * e2 * e2 -
209 num_traits<T>::from_int(18) * e1 * e2 * e3 +
210 num_traits<T>::from_int(18) * e2 * e2 * e2 + e3 * e3);
211 const T den = T(num_traits<T>::from_int(-3) * e2 * e2 + num_traits<T>::from_int(2) * e1 * e3);
212 if (num_traits<T>::to_double(disc) >= 0.0 && den != zero) {
213 const T s = fitdetail::fsqrt<T>(disc);
214 const T a = T(num_traits<T>::from_int(3) * e1 * e2);
215 T mu1[2], mu2[2];
216 mu1[0] = T(num_traits<T>::from_int(2) * (e3 - a) / den + (a - e3 + s) / den);
217 mu1[1] = T(num_traits<T>::from_int(2) * (e3 - a) / den - (e3 - a + s) / den);
218 mu2[0] = T(-(a - e3 + s) / den);
219 mu2[1] = T((e3 - a + s) / den);
220 for (int k = 0; k < 2; ++k) {
221 const T phi = T(one - mu2[k] * e1 + mu2[k] / mu1[k]);
222 if (num_traits<T>::to_double(phi) >= 0.0 && num_traits<T>::to_double(phi) <= 1.0 &&
223 num_traits<T>::to_double(mu1[k]) >= 0.0 && num_traits<T>::to_double(mu2[k]) >= 0.0)
224 return Distrib<T>::cox2(mu1[k], mu2[k], phi);
225 }
226 }
227 if (num_traits<T>::to_double(scv) >= 0.5) return coxian_fit_mean_scv(mean, scv);
228 return Distrib<T>::exp_mean(mean);
229}
230
231/** `Coxian.fitCentral`, which the reference forwards to `Cox2.fitCentral`. */
232template <class T>
233Distrib<T> coxian_fit_central(const T& mean, const T& var, const T& skew) {
234 return cox2_fit_central(mean, var, skew);
235}
236
237// ---------------------------------------------------------------------------
238// APH
239// ---------------------------------------------------------------------------
240
241/** `APH.fitMeanAndSCV(MEAN, SCV)`, through `mam::aph_fit_mean_scv`. */
242template <class T>
243Distrib<T> aph_fit_mean_scv(const T& mean, const T& scv) {
244 return ph_from_map(mam::aph_fit_mean_scv(mean, scv), true);
245}
246
247/**
248 * `APH.fitCentral(MEAN, VAR, SKEW)`: the three central moments converted to
249 * raw ones and matched by a canonical APH.
250 */
251template <class T>
252Distrib<T> aph_fit_central(const T& mean, const T& var, const T& skew) {
253 static_assert(num_traits<T>::has_transcendental, "APH.fitCentral needs a square root");
254 const T one = num_traits<T>::from_int(1);
255 const T scv = T(var / (mean * mean));
256 const T e1 = mean;
257 const T e2 = T((one + scv) * e1 * e1);
258 const T e3 = T(-(num_traits<T>::from_int(2) * e1 * e1 * e1 -
259 num_traits<T>::from_int(3) * e1 * e2 -
260 skew * fitdetail::fpow<T>(T(e2 - e1 * e1), num_traits<T>::from_double(1.5))));
261 return ph_from_map(mam::aph_fit(e1, e2, e3).aph, true);
262}
263
264// ---------------------------------------------------------------------------
265// Gamma and Pareto
266// ---------------------------------------------------------------------------
267
268/** `Gamma.fitMeanAndSCV(MEAN, SCV)`: shape 1/SCV, scale MEAN * SCV. */
269template <class T>
270Distrib<T> gamma_fit_mean_scv(const T& mean, const T& scv) {
271 const T shape = T(num_traits<T>::from_int(1) / scv);
272 return Distrib<T>::gamma_dist(shape, T(mean / shape));
273}
274
275/**
276 * `Pareto.fitMeanAndSCV(MEAN, SCV)`: alpha = 1 + sqrt(1 + 1/SCV) and
277 * k = MEAN (alpha - 1) / alpha.
278 */
279template <class T>
280Distrib<T> pareto_fit_mean_scv(const T& mean, const T& scv) {
281 static_assert(num_traits<T>::has_transcendental, "Pareto.fitMeanAndSCV needs a square root");
282 const T one = num_traits<T>::from_int(1);
283 const T shape = T(one + fitdetail::fsqrt<T>(T(one + one / scv)));
284 return Distrib<T>::pareto(shape, T(mean * (shape - one) / shape));
285}
286
287} // namespace lang
288} // namespace line
289
290#endif // LINE_LANG_DIST_FITTERS_H
Minimal-order acyclic phase-type fit of the first three moments (matlab/lib/kpctoolbox/aph/aph_fit....
Acyclic phase-type fitters from the first two moments.
InputError(const std::string &what)
Definition error.h:39
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
MAP constructors and structural transformations.
Distrib< T > hyperexp_fit_mean_scv_balanced(const T &mean, const T &scv)
HyperExp.fitMeanAndSCVBalanced(MEAN, SCV): the balanced-means branch, p / mu1 = (1 - p) / mu2.
Distrib< T > aph_fit_central(const T &mean, const T &var, const T &skew)
APH.fitCentral(MEAN, VAR, SKEW): the three central moments converted to raw ones and matched by a can...
Distrib< T > erlang_fit_mean_order(const T &mean, std::size_t k)
Erlang.fitMeanAndOrder(MEAN, k): k phases, each of rate k / MEAN.
Distrib< T > ph_from_map(const mam::Map< T > &m, bool acyclic)
A PH distribution from a fitted (D0, D1) pair.
Distrib< T > coxian_fit_central(const T &mean, const T &var, const T &skew)
Coxian.fitCentral, which the reference forwards to Cox2.fitCentral.
Distrib< T > aph_fit_mean_scv(const T &mean, const T &scv)
APH.fitMeanAndSCV(MEAN, SCV), through mam::aph_fit_mean_scv.
Distrib< T > gamma_fit_mean_scv(const T &mean, const T &scv)
Gamma.fitMeanAndSCV(MEAN, SCV): shape 1/SCV, scale MEAN * SCV.
Distrib< T > pareto_fit_mean_scv(const T &mean, const T &scv)
Pareto.fitMeanAndSCV(MEAN, SCV): alpha = 1 + sqrt(1 + 1/SCV) and k = MEAN (alpha - 1) / alpha.
Distrib< T > coxian_fit_mean_scv(const T &mean, const T &scv)
Coxian.fitMeanAndSCV(MEAN, SCV), branch for branch.
Distrib< T > hyperexp_fit_mean_scv(const T &mean, const T &scv)
HyperExp.fitMeanAndSCV(MEAN, SCV), which is map_hyperexp at p = 0.99 read back as (p,...
Distrib< T > cox2_fit_central(const T &mean, const T &var, const T &skew)
Cox2.fitCentral(MEAN, VAR, SKEW): the two-phase Coxian matching three central moments exactly when th...
Map< T > aph_fit_mean_scv(const T &mean, const T &scv)
Port of APH.fitMeanAndSCV, the entry point the analyzers fit arrivals with.
Map< T > map_hyperexp(const T &mean, const T &scv, const T &p_in)
Two-phase hyperexponential renewal MAP matching a mean and an SCV >= 1, with branching probability p ...
AphFitResult< T > aph_fit(const T &e1, const T &e2, const T &e3, unsigned nmax, const T &tol)
Fit an APH(n) with n <= nmax to the raw moments e1, e2, e3.
Definition aph_fit.h:176
Number-type abstraction for the templated API port.
static Distrib phase_type(const std::vector< T > &alpha, const Matrix< T > &A, bool acyclic)
PH / APH given by (alpha, A): D0 = A and D1 = (-A e) alpha.
static Distrib pareto(const T &shape, const T &scale)
Pareto(shape, scale), with the MATLAB parameter order (alpha, k).
static Distrib gamma_dist(const T &shape, const T &scale)
Gamma(shape, scale), Weibull(scale, shape) and Lognormal(mu, sigma).
static Distrib cox2(const T &mu1, const T &mu2, const T &phi1)
Cox2(mu1, mu2, phi1), MATLAB's two-phase Coxian constructor.
static Distrib hyperexp(const T &p, const T &lambda1, const T &lambda2)
Definition lang_types.h:956
static Distrib erlang(const T &phase_rate, std::size_t r)
Erlang(alpha, r): r phases of rate alpha, as MATLAB's Erlang(phaseRate, nphases).
Definition lang_types.h:873
static Distrib exp_mean(const T &m)
Definition lang_types.h:799
static Distrib coxian(const std::vector< T > &mu, const std::vector< T > &phi)
Coxian(mu, phi): phase i completes with probability phi(i) and otherwise moves to phase i+1.
Definition lang_types.h:987
static constexpr double CoarseTol
Definition lang_types.h:669
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Matrix< T > D1
Definition map_moment.h:55
Matrix< T > D0
Definition map_moment.h:54