LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
cme.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_CME_H
6#define LINE_API_MAM_CME_H
7
8/**
9 * @file
10 * @ingroup api_mam
11 * Concentrated matrix exponentials, and the two-moment fit built on them.
12 *
13 * Templated port of matlab/src/lang/processes/CME.m and dist_fit_me.m (mirrored
14 * by jline.lang.processes.CME / MEFit and the native Python fit_me_mean_scv).
15 *
16 * A CME of order 2n+1 is the matrix-exponential law whose squared coefficient of
17 * variation is numerically minimal for that order, from the tables of Horvath,
18 * Horvath and Telek. Its SCV decays as O(1/n^2), so it reaches far below the
19 * Erlang bound 1/order that binds any phase-type of the same order: at 101
20 * phases a CME reaches 3.9e-4 where Erlang-101 stops at 9.9e-3. The unit-mean
21 * density with n harmonics is
22 * f(x) = mu1 e^{-mu1 x} ( c + sum_k a_k cos(k w mu1 x) + b_k sin(k w mu1 x) ),
23 * which is alpha exp(A x) (-A e) for the block-diagonal
24 * A = blkdiag( -mu1, mu1 [-1, -k w; k w, -1], k = 1..n ).
25 *
26 * The coefficients come from the SAME vendored `iltcme` table the CME inverse
27 * Laplace transform reads (api/mam/iltcme_table.h), so the two share one source
28 * of truth; a TU using this header must link `src/api/mam/iltcme_table.cpp`.
29 *
30 * IT IS NOT A PHASE-TYPE. The off-diagonal entries of A are not rates -- the
31 * rotation blocks carry a negative one -- so a CTMC assembled from a CME does
32 * not describe the model. `sn_is_phasetype` is the test every consumer applies,
33 * and `sn_nonmarkov_toph` tags the result ME rather than PH on the strength of
34 * it.
35 *
36 * ARITHMETIC: transcendental. The two-moment fit takes a square root and the
37 * table itself is a double table, so this does not instantiate under Rational.
38 */
39
40#include <algorithm>
41#include <cmath>
42#include <cstddef>
43#include <vector>
44
47#include "line/num/number.h"
48#include "line/util/error.h"
49#include "line/util/matrix.h"
50
51namespace line {
52namespace mam {
53
54/** The unit-mean (alpha, A) form of a CME, with the SCV it attains. */
55template <class T>
57 std::vector<T> alpha; ///< entry law, sums to one
58 Matrix<T> A; ///< (2n+1) square, generator-shaped but not a generator
59 double scv; ///< the tabulated cv2 of this order
60};
61
62/** Every phase count 2n+1 the vendored table realizes, ascending. */
63inline std::vector<std::size_t> cme_supported_orders() {
64 std::vector<std::size_t> orders;
65 for (std::size_t i = 0; i < iltcme::kTableSize; ++i)
66 orders.push_back(static_cast<std::size_t>(2 * iltcme::kTable[i].n + 1));
67 std::sort(orders.begin(), orders.end());
68 orders.erase(std::unique(orders.begin(), orders.end()), orders.end());
69 return orders;
70}
71
72/**
73 * The most concentrated table entry realizing `order` phases.
74 *
75 * Several entries share an n (the table's 'full' and 'approx' optimizations);
76 * the smallest cv2 wins, which is the selection rule matlab_ilt also applies.
77 */
78inline const iltcme::CmeEntry& cme_table_entry(std::size_t order) {
79 if (order < 3 || order % 2 == 0)
80 throw InputError("cme_table_entry: the order must be an odd integer 2n+1 with n >= 1");
81 if (iltcme::kTableSize == 0) throw InputError("cme_table_entry: the CME table is empty");
82 const int n = static_cast<int>((order - 1) / 2);
83 const iltcme::CmeEntry* best = 0;
84 for (std::size_t i = 0; i < iltcme::kTableSize; ++i)
85 if (iltcme::kTable[i].n == n && (best == 0 || iltcme::kTable[i].cv2 < best->cv2))
86 best = &iltcme::kTable[i];
87 if (best == 0)
88 throw InputError("cme_table_entry: no tabulated CME of order " + std::to_string(order));
89 return *best;
90}
91
92/** The minimal SCV a CME of this order attains. */
93inline double cme_min_scv(std::size_t order) { return cme_table_entry(order).cv2; }
94
95/**
96 * @param order an odd phase count 2n+1 present in the table
97 * @return the unit-mean representation of that order
98 */
99template <class T>
101 const iltcme::CmeEntry& e = cme_table_entry(order);
102 const std::size_t n = static_cast<std::size_t>(e.n), sz = 2 * n + 1;
103 const T zero = num_traits<T>::from_int(0);
104
106 r.scv = e.cv2;
107 r.A = Matrix<T>(sz, sz, zero);
108 r.alpha.assign(sz, zero);
109
110 const double mu1 = e.mu1, w = e.omega;
111 r.A(0, 0) = num_traits<T>::from_double(-mu1);
113 for (std::size_t k = 1; k <= n; ++k) {
114 const std::size_t i = 2 * k - 1; // 0-based: MATLAB's 2k
115 const double wk = static_cast<double>(k) * w;
116 r.A(i, i) = num_traits<T>::from_double(-mu1);
117 r.A(i, i + 1) = num_traits<T>::from_double(-wk * mu1);
118 r.A(i + 1, i) = num_traits<T>::from_double(wk * mu1);
119 r.A(i + 1, i + 1) = num_traits<T>::from_double(-mu1);
120 const double d = 2.0 * (1.0 + wk * wk);
121 r.alpha[i] = num_traits<T>::from_double(((1.0 + wk) * e.a[k - 1] - (1.0 - wk) * e.b[k - 1]) / d);
122 r.alpha[i + 1] = num_traits<T>::from_double(((1.0 - wk) * e.a[k - 1] + (1.0 + wk) * e.b[k - 1]) / d);
123 }
124 T s = zero;
125 for (std::size_t i = 0; i < sz; ++i) s += r.alpha[i];
126 if (!(num_traits<T>::to_double(s) > 0.0))
127 throw NumericError("cme_representation: the entry law does not normalize");
128 for (std::size_t i = 0; i < sz; ++i) r.alpha[i] = T(r.alpha[i] / s);
129 return r;
130}
131
132/** Assemble the renewal (D0, D1) of a matrix-exponential law (alpha, A). */
133template <class T>
134Map<T> me_to_map(const std::vector<T>& alpha, const Matrix<T>& A) {
135 const std::size_t n = A.rows();
136 const T zero = num_traits<T>::from_int(0);
137 Map<T> m;
138 m.D0 = A;
139 m.D1 = Matrix<T>(n, n, zero);
140 for (std::size_t i = 0; i < n; ++i) {
141 T rowsum = zero;
142 for (std::size_t j = 0; j < n; ++j) rowsum += A(i, j);
143 for (std::size_t j = 0; j < n; ++j) m.D1(i, j) = T(-rowsum * alpha[j]);
144 }
145 return m;
146}
147
148/**
149 * Two-moment matrix-exponential fit for 0 < scv < 1, a port of dist_fit_me.m.
150 *
151 * The fit is the convolution X = c Y + Z of a scaled unit-mean CME Y with an
152 * independent exponential Z. Matching c + d = mean and c^2 sY + d^2 = scv mean^2
153 * gives
154 * c = mean (1 - sqrt(1 - (1+sY)(1-scv))) / (1 + sY), d = mean - c,
155 * so every target in [sY/(1+sY), 1] is hit EXACTLY in 2n+2 phases. The
156 * exponential tail is what lets the convolution reach up to SCV 1; the
157 * concentrated part is what lets it reach far below the Erlang bound.
158 *
159 * BUDGET-LIMITED IS NOT AN ERROR. When maxPhases cannot buy an order whose reach
160 * covers the target, the most concentrated affordable member is returned and the
161 * caller gets the closest achievable SCV, rather than a silent Erlang.
162 *
163 * @param mean target mean, positive and finite
164 * @param scv target SCV, strictly inside (0,1)
165 * @param maxPhases cap on the phase count, 0 for no cap
166 */
167template <class T>
168Map<T> dist_fit_me(double mean, double scv, std::size_t maxPhases = 0) {
170 "dist_fit_me takes a square root of the moment discriminant");
171 if (!std::isfinite(mean) || mean <= 0.0)
172 throw InputError("dist_fit_me: the mean must be a positive finite number");
173 if (!std::isfinite(scv) || scv <= 0.0 || scv >= 1.0)
174 throw InputError(
175 "dist_fit_me: requires 0 < scv < 1; use a hyperexponential for scv >= 1 and a CME "
176 "for scv = 0");
177
178 const std::vector<std::size_t> orders = cme_supported_orders();
179 std::size_t bestOrder = 0;
180 for (std::size_t i = 0; i < orders.size(); ++i) {
181 const std::size_t order = orders[i];
182 if (maxPhases > 0 && order + 1 > maxPhases) continue;
183 const double sY = cme_min_scv(order);
184 bestOrder = order; // budget-limited: keep the most concentrated one that fits
185 if (sY / (1.0 + sY) <= scv) break;
186 }
187 if (bestOrder == 0)
188 throw InputError("dist_fit_me: no CME order fits a budget of " + std::to_string(maxPhases) +
189 " phases; the smallest is 3 phases plus one exponential");
190
191 const CmeRepresentation<T> rep = cme_representation<T>(bestOrder);
192 const double sY = rep.scv;
193 const double reach = sY / (1.0 + sY);
194 const double c = scv < reach ? mean / (1.0 + sY)
195 : mean * (1.0 - std::sqrt(1.0 - (1.0 + sY) * (1.0 - scv))) /
196 (1.0 + sY);
197 const double d = mean - c;
198 const std::size_t n = rep.alpha.size();
199 const T zero = num_traits<T>::from_int(0);
200
201 if (d <= mean * 1e-12) { // the whole mass is in the concentrated part
202 Matrix<T> A = rep.A;
203 const T inv = num_traits<T>::from_double(1.0 / mean);
204 for (std::size_t i = 0; i < n; ++i)
205 for (std::size_t j = 0; j < n; ++j) A(i, j) = T(A(i, j) * inv);
206 return me_to_map(rep.alpha, A);
207 }
208 if (c <= mean * 1e-12) { // degenerates to the exponential tail alone
209 Matrix<T> A(1, 1, num_traits<T>::from_double(-1.0 / mean));
210 std::vector<T> alpha(1, num_traits<T>::from_int(1));
211 return me_to_map(alpha, A);
212 }
213
214 // Convolution: the exit flow of the CME block feeds the exponential phase.
215 Matrix<T> A(n + 1, n + 1, zero);
216 std::vector<T> alpha(n + 1, zero);
217 const T invc = num_traits<T>::from_double(1.0 / c);
218 for (std::size_t i = 0; i < n; ++i) {
219 alpha[i] = rep.alpha[i];
220 T rowsum = zero;
221 for (std::size_t j = 0; j < n; ++j) {
222 A(i, j) = T(rep.A(i, j) * invc);
223 rowsum += A(i, j);
224 }
225 A(i, n) = -rowsum;
226 }
227 A(n, n) = num_traits<T>::from_double(-1.0 / d);
228 return me_to_map(alpha, A);
229}
230
231} // namespace mam
232} // namespace line
233
234#endif // LINE_API_MAM_CME_H
InputError(const std::string &what)
Definition error.h:39
std::size_t rows() const
Definition matrix.h:89
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
The vendored concentrated-matrix-exponential (CME) coefficient table that matlab_ilt reads from iltcm...
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Dense matrix and non-owning view.
const CmeEntry kTable[]
The reachable entries, in table order.
const std::size_t kTableSize
std::vector< std::size_t > cme_supported_orders()
Every phase count 2n+1 the vendored table realizes, ascending.
Definition cme.h:63
const iltcme::CmeEntry & cme_table_entry(std::size_t order)
The most concentrated table entry realizing order phases.
Definition cme.h:78
CmeRepresentation< T > cme_representation(std::size_t order)
Definition cme.h:100
Map< T > dist_fit_me(double mean, double scv, std::size_t maxPhases=0)
Two-moment matrix-exponential fit for 0 < scv < 1, a port of dist_fit_me.m.
Definition cme.h:168
Map< T > me_to_map(const std::vector< T > &alpha, const Matrix< T > &A)
Assemble the renewal (D0, D1) of a matrix-exponential law (alpha, A).
Definition cme.h:134
double cme_min_scv(std::size_t order)
The minimal SCV a CME of this order attains.
Definition cme.h:93
Number-type abstraction for the templated API port.
The unit-mean (alpha, A) form of a CME, with the SCV it attains.
Definition cme.h:56
double scv
the tabulated cv2 of this order
Definition cme.h:59
std::vector< T > alpha
entry law, sums to one
Definition cme.h:57
Matrix< T > A
(2n+1) square, generator-shaped but not a generator
Definition cme.h:58
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
One CME entry, carrying only the fields matlab_ilt reads.
double omega
angular frequency
const double * a
cosine coefficients, length n
int n
number of cosine/sine terms; the transform costs n+1 evaluations
double mu1
first moment scale
double cv2
squared coefficient of variation; smaller is steeper
double c
constant term
const double * b
sine coefficients, length n