LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
matlab_ilt.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_MATLAB_ILT_H
6#define LINE_API_MAM_MATLAB_ILT_H
7
8/**
9 * @file
10 * @ingroup api_mam
11 * Numerical inverse Laplace transform in the Abate-Whitt framework, the port of
12 * `matlab/lib/thirdparty/iltcme/matlab_ilt.m`.
13 *
14 * Every variant evaluates the same quadrature
15 *
16 * f(t) ~= (1/t) sum_k Re( eta_k F(beta_k / t) )
17 *
18 * and differs only in the (eta, beta) pair:
19 *
20 * - **cme** (the default, and the only one `solver_mam_transient_qbd` uses):
21 * concentrated matrix exponential weights read from the vendored ILT-CME
22 * table. The entry chosen is the steepest -- smallest cv2 -- whose n+1 does
23 * not exceed the evaluation budget, which is exactly the reference's scan.
24 * - **euler**: binomial (Euler) weights, no table.
25 * - **gaver**: Gaver-Stehfest weights, no table.
26 *
27 * WHY cme IS NOT INTERCHANGEABLE WITH THE OTHER TWO, since it is tempting to
28 * reach for a table-free variant: they are different quadratures with different
29 * error behaviour, so substituting one for another changes the answer. The
30 * reference defaults to cme; a port that quietly used euler would produce
31 * numbers that are not the reference's while reporting the same method. That is
32 * why the table is vendored rather than avoided.
33 *
34 * ARITHMETIC. Double only, and deliberately: the transform is evaluated at
35 * COMPLEX arguments, and the whole route is transcendental. The signature takes
36 * a `std::function` over `std::complex<double>` so a caller supplies its own
37 * transform.
38 */
39
40#include <algorithm>
41#include <cmath>
42#include <complex>
43#include <cstddef>
44#include <functional>
45#include <vector>
46
48#include "line/util/error.h"
49
50namespace line {
51namespace mam {
52
53/** Which Abate-Whitt weights to use. */
54enum class IltMethod { Cme, Euler, Gaver };
55
56/**
57 * Invert a Laplace transform at the requested time points.
58 *
59 * @param fun the transform F(s), evaluated at complex s
60 * @param times the points to invert at; each must be positive
61 * @param maxFnEvals evaluation budget per point, which selects the CME entry
62 * @param method the weight family; the reference's default is Cme
63 */
64inline std::vector<double> matlab_ilt(
65 const std::function<std::complex<double>(const std::complex<double>&)>& fun,
66 const std::vector<double>& times, std::size_t maxFnEvals,
67 IltMethod method = IltMethod::Cme) {
68 if (times.empty()) return {};
69 for (double t : times)
70 if (!(t > 0.0))
71 throw InputError("matlab_ilt: every evaluation time must be strictly positive");
72
73 std::vector<std::complex<double>> eta, beta;
74
75 if (method == IltMethod::Cme) {
76 // The reference's scan: start at entry 0 REGARDLESS of its cost, then
77 // take any entry that is steeper and affordable. Seeding with entry 0
78 // before testing the bound is why it is always reachable.
79 if (iltcme::kTableSize == 0)
80 throw InputError("matlab_ilt: the vendored ILT-CME table is empty");
81 const iltcme::CmeEntry* best = &iltcme::kTable[0];
82 for (std::size_t i = 1; i < iltcme::kTableSize; ++i) {
83 const iltcme::CmeEntry& e = iltcme::kTable[i];
84 if (e.cv2 < best->cv2 && static_cast<std::size_t>(e.n) + 1 <= maxFnEvals) best = &e;
85 }
86 const double mu1 = best->mu1;
87 eta.reserve(static_cast<std::size_t>(best->n) + 1);
88 beta.reserve(static_cast<std::size_t>(best->n) + 1);
89 eta.emplace_back(best->c * mu1, 0.0);
90 beta.emplace_back(mu1, 0.0);
91 for (int k = 0; k < best->n; ++k) {
92 eta.emplace_back(best->a[k] * mu1, best->b[k] * mu1);
93 beta.emplace_back(mu1, mu1 * static_cast<double>(k + 1) * best->omega);
94 }
95 } else {
96 // The reference also carries table-free 'euler' and 'gaver' weights.
97 // Neither is ported: `solver_mam_transient_qbd` is the only caller and
98 // it takes the default, so those branches are unreachable here, and an
99 // unreachable branch is an untested one. They are NOT a substitute for
100 // cme in any case -- different quadratures with different error -- which
101 // is exactly why the cme table was vendored rather than avoided.
102 throw UnsupportedError(
103 "matlab_ilt: only the CME weights are ported; the reference's 'euler' and 'gaver' "
104 "variants are unreachable from this tree and are different quadratures, not "
105 "substitutes for cme");
106 }
107
108 std::vector<double> out(times.size(), 0.0);
109 for (std::size_t i = 0; i < times.size(); ++i) {
110 const double t = times[i];
111 double acc = 0.0;
112 for (std::size_t k = 0; k < eta.size(); ++k)
113 acc += std::real(eta[k] * fun(beta[k] / t));
114 out[i] = acc / t;
115 }
116 return out;
117}
118
119} // namespace mam
120} // namespace line
121
122#endif // LINE_API_MAM_MATLAB_ILT_H
InputError(const std::string &what)
Definition error.h:39
UnsupportedError(const std::string &what)
Definition error.h:51
The exception types the port throws.
The vendored concentrated-matrix-exponential (CME) coefficient table that matlab_ilt reads from iltcm...
const CmeEntry kTable[]
The reachable entries, in table order.
const std::size_t kTableSize
IltMethod
Which Abate-Whitt weights to use.
Definition matlab_ilt.h:54
std::vector< double > matlab_ilt(const std::function< std::complex< double >(const std::complex< double > &)> &fun, const std::vector< double > &times, std::size_t maxFnEvals, IltMethod method=IltMethod::Cme)
Invert a Laplace transform at the requested time points.
Definition matlab_ilt.h:64
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