LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
map_gamma.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_MAP_GAMMA_H
6#define LINE_API_MAM_MAP_GAMMA_H
7
8/**
9 * @file
10 * @ingroup api_mam
11 * Autocorrelation decay rate of a MAP: the gamma of the geometric model
12 * rho(k) = rho0 * gamma^k with rho0 = (1 - 1/scv)/2.
13 *
14 * Templated port of matlab/lib/kpctoolbox/map/map_gamma.m, cross-checked
15 * against jar/src/main/java/jline/api/mam/Map_gamma.java.
16 *
17 * Order one is Poisson and has decay rate zero. Order two has a genuinely
18 * geometric acf, so the rate is the exact ratio acf(2)/acf(1); a vanishing
19 * acf(1) means the MAP degenerates to a phase-type renewal process and the rate
20 * is again zero. Only above order two is a fit needed, and there both references
21 * evaluate the acf on the ten lags 1, 1+limit/10, ..., and regress.
22 *
23 * WHY THE FIT IS ROBUST AND NOT PLAIN LEAST SQUARES: MATLAB uses nlinfit with
24 * RobustWgtFun 'fair'. On a higher-order MAP the acf is a sum of geometric
25 * terms, so the short lags sit far off any single geometric and would otherwise
26 * dominate the fit and bias gamma low. The reproduction here is the JAR's: an
27 * ordinary fit, the leverage of its Jacobian held fixed, then iteratively
28 * reweighted fits with w = 1/(1 + |r_adj|/(1.4 sigma)) and sigma the MAD
29 * estimate floored against the spread of the response. Dropping the robustness
30 * is not a simplification, it changes the answer.
31 *
32 * ARITHMETIC: unlike most of api/mam this is a floating-point algorithm. The
33 * lags enter as real exponents and the MAD needs an order statistic, so the
34 * regression runs in double whatever T is; only the acf, mean and second moment
35 * that feed it are computed in T. Orders one and two return an exact T.
36 */
37
38#include <algorithm>
39#include <cmath>
40#include <cstddef>
41#include <vector>
42
44#include "line/num/number.h"
45#include "line/util/error.h"
46#include "line/util/levmar.h"
47#include "line/util/matrix.h"
48
49namespace line {
50namespace mam {
51
52namespace detail {
53
54/** One weighted Levenberg-Marquardt fit of rho_k = rho0 gamma^k. */
55inline double map_gamma_ls(const std::vector<double>& lag, const std::vector<double>& rho,
56 double rho0, double start, const std::vector<double>& weights) {
57 const std::size_t m = lag.size();
58 std::vector<double> sw(m, 1.0);
59 if (!weights.empty())
60 for (std::size_t i = 0; i < m; ++i) sw[i] = std::sqrt(weights[i]);
61 // The optimizer minimises sqrt(w) * (model - rho), so both sides carry sqrt(w)
62 const std::vector<double>* plag = &lag;
63 const std::vector<double>* prho = &rho;
64 auto res = [plag, prho, &sw, rho0, m](const std::vector<double>& x) {
65 std::vector<double> r(m);
66 for (std::size_t i = 0; i < m; ++i)
67 r[i] = sw[i] * (rho0 * std::pow(x[0], (*plag)[i]) - (*prho)[i]);
68 return r;
69 };
70 auto jac = [plag, &sw, rho0, m](const std::vector<double>& x) {
71 Matrix<double> J(m, 1, 0.0);
72 for (std::size_t i = 0; i < m; ++i)
73 J(i, 0) = sw[i] * rho0 * (*plag)[i] * std::pow(x[0], (*plag)[i] - 1.0);
74 return J;
75 };
76 LevmarOptions<double> opt = levmar_defaults<double>();
77 opt.max_iter = 500;
78 const LevmarResult<double> out = levmar_jac(res, jac, std::vector<double>(1, start), m, opt);
79 return out.x[0];
80}
81
82/** Median of a copy of the sample, the MAD ingredient of the robust fit. */
83inline double map_gamma_median(std::vector<double> v) {
84 std::sort(v.begin(), v.end());
85 const std::size_t n = v.size();
86 if (n % 2 == 1) return v[n / 2];
87 return 0.5 * (v[n / 2 - 1] + v[n / 2]);
88}
89
90/** Robust nonlinear fit of the geometric acf model, nlinfit with the fair weight. */
91inline double map_gamma_fit(const std::vector<double>& lag, const std::vector<double>& rho,
92 double rho0, double start) {
93 const std::size_t m = lag.size();
94 double gamma = map_gamma_ls(lag, rho, rho0, start, std::vector<double>());
95
96 // Leverage of the least-squares Jacobian, held fixed across the reweighting
97 // as nlinfit does; for one parameter the QR reduces to normalising it.
98 double norm2 = 0.0;
99 for (std::size_t i = 0; i < m; ++i) {
100 const double j = rho0 * lag[i] * std::pow(gamma, lag[i] - 1.0);
101 norm2 += j * j;
102 }
103 std::vector<double> adjust(m);
104 for (std::size_t i = 0; i < m; ++i) {
105 const double j = rho0 * lag[i] * std::pow(gamma, lag[i] - 1.0);
106 const double h = norm2 > 0.0 ? std::min(0.9999, j * j / norm2) : 0.0;
107 adjust[i] = 1.0 / std::sqrt(1.0 - h);
108 }
109
110 // A near-perfect fit drives the MAD to zero and makes every point an
111 // outlier, so the scale is floored against the spread of the response
112 double mean = 0.0;
113 for (std::size_t i = 0; i < m; ++i) mean += rho[i];
114 mean /= static_cast<double>(m);
115 double var = 0.0;
116 for (std::size_t i = 0; i < m; ++i) var += (rho[i] - mean) * (rho[i] - mean);
117 var /= static_cast<double>(m - 1);
118 double tiny = 1e-6 * std::sqrt(var);
119 if (tiny == 0.0) tiny = 1.0;
120
121 const double tune = 1.4; // fair
122 const double delta = std::sqrt(std::numeric_limits<double>::epsilon());
123 std::vector<double> weights(m);
124 for (unsigned iter = 0; iter < 200; ++iter) {
125 const double previous = gamma;
126 std::vector<double> radj(m), absr(m);
127 for (std::size_t i = 0; i < m; ++i) {
128 radj[i] = (rho[i] - rho0 * std::pow(gamma, lag[i])) * adjust[i];
129 absr[i] = std::fabs(radj[i]);
130 }
131 const double sigma = map_gamma_median(absr) / 0.6745;
132 const double scale = std::max(sigma, tiny) * tune;
133 for (std::size_t i = 0; i < m; ++i) weights[i] = 1.0 / (1.0 + std::fabs(radj[i] / scale));
134 gamma = map_gamma_ls(lag, rho, rho0, previous, weights);
135 if (std::fabs(gamma - previous) <
136 delta * std::max(std::fabs(gamma), std::fabs(previous)))
137 break;
138 }
139 return gamma;
140}
141
142} // namespace detail
143
144/** Result of map_gamma, mirroring the MATLAB [GAMMA, RHO0] pair. */
145template <class T>
147 T gamma; ///< the fitted geometric decay rate of the acf
148 T rho0; ///< (1 - 1/scv)/2, the lag-0 amplitude; zero below order three
149};
150
151/**
152 * @param m the MAP
153 * @param limit largest lag considered; the reference default is 1000
154 */
155template <class T>
156MapGammaResult<T> map_gamma_full(const Map<T>& m, long limit = 1000) {
157 if (limit < 1) throw InputError("map_gamma: the lag limit must be positive");
158 const T zero = num_traits<T>::from_int(0);
160 out.gamma = zero;
161 out.rho0 = zero;
162 const std::size_t n = m.order();
163 if (n == 1) return out;
164 if (n == 2) {
165 const std::vector<T> a = map_acf(m, std::vector<unsigned>{1u, 2u});
166 if (num_abs(a[0]) < num_traits<T>::from_double(1e-8)) return out;
167 out.gamma = a[1] / a[0];
168 return out;
169 }
170 const long step = std::max<long>(1, limit / 10);
171 std::vector<unsigned> lags;
172 for (long l = 1; l <= limit; l += step) lags.push_back(static_cast<unsigned>(l));
173 const T m1 = map_mean(m);
174 const T m2 = map_moment(m, 2u);
175 const T scv = (m2 - m1 * m1) / (m1 * m1);
176 if (scv == zero) throw NumericError("map_gamma: the MAP has zero scv, rho0 is undefined");
178 num_traits<T>::from_int(1) / scv);
179 const std::vector<T> acf = map_acf(m, lags);
180 std::vector<double> dl(lags.size()), dr(lags.size());
181 for (std::size_t i = 0; i < lags.size(); ++i) {
182 dl[i] = static_cast<double>(lags[i]);
183 dr[i] = num_traits<T>::to_double(acf[i]);
184 }
186 detail::map_gamma_fit(dl, dr, num_traits<T>::to_double(out.rho0), 0.99));
187 return out;
188}
189
190/** Autocorrelation decay rate of a MAP (map_gamma.m). */
191template <class T>
192T map_gamma(const Map<T>& m, long limit = 1000) {
193 return map_gamma_full(m, limit).gamma;
194}
195
196} // namespace mam
197} // namespace line
198
199#endif // LINE_API_MAM_MAP_GAMMA_H
InputError(const std::string &what)
Definition error.h:39
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
Levenberg-Marquardt for nonlinear least squares.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Dense matrix and non-owning view.
std::vector< T > map_acf(const Map< T > &m, const std::vector< unsigned > &lags)
Autocorrelation coefficients of the inter-arrival times at the given lags,.
Definition map_moment.h:168
T map_mean(const Map< T > &m)
Mean inter-arrival time, 1/lambda.
Definition map_moment.h:101
MapGammaResult< T > map_gamma_full(const Map< T > &m, long limit=1000)
Definition map_gamma.h:156
T map_moment(const Map< T > &m, unsigned k)
Raw moment of order k of the inter-arrival time: k!
Definition map_moment.h:118
T map_gamma(const Map< T > &m, long limit=1000)
Autocorrelation decay rate of a MAP (map_gamma.m).
Definition map_gamma.h:192
T num_abs(const T &v)
Definition number.h:172
LevmarResult< T > levmar_jac(F f, J jac, const std::vector< T > &x0, std::size_t m, const LevmarOptions< T > &opt)
Levenberg-Marquardt with a caller-supplied Jacobian.
Definition levmar.h:168
LevmarOptions< T > levmar_defaults()
MINPACK-like defaults, with a central-difference step of eps^(1/3).
Definition levmar.h:77
Number-type abstraction for the templated API port.
Result of map_gamma, mirroring the MATLAB [GAMMA, RHO0] pair.
Definition map_gamma.h:146
T gamma
the fitted geometric decay rate of the acf
Definition map_gamma.h:147
T rho0
(1 - 1/scv)/2, the lag-0 amplitude; zero below order three
Definition map_gamma.h:148
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
std::size_t order() const
Definition map_moment.h:57