LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
laplace_invert.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_LTI_LAPLACE_INVERT_H
6#define LINE_API_LTI_LAPLACE_INVERT_H
7
8/**
9 * @file
10 * @ingroup api_lti
11 * Numerical inversion of a Laplace transform: Euler, Talbot, Gaver-Stehfest.
12 *
13 * Port of python/line_solver/api/lti/__init__.py. THIS IS PYTHON-ONLY: MATLAB
14 * carries only the CME method (`matlab/lib/thirdparty/iltcme/matlab_ilt.m`,
15 * already ported as `api/mam/matlab_ilt.h`), so native Python is the reference
16 * for the other three.
17 *
18 * ALL FOUR ARE THE SAME FRAMEWORK. Abate-Whitt writes
19 *
20 * f(t) ~ (1/t) sum_k Re[ omega_k F(alpha_k / t) ],
21 *
22 * and a method IS its (alpha, omega) pair -- nothing else differs, which is why
23 * they share one evaluator here. What differs is where the nodes sit:
24 *
25 * - EULER puts them on a vertical line and accelerates an alternating series
26 * with binomial (Euler) weights. Odd `n` only; the reference silently rounds
27 * an even `n` up, and so does this. ITS DEFAULT IS NOT THE REFERENCE'S --
28 * see the note on `laplace_invert_euler`.
29 * - TALBOT deforms the contour into the left half plane, where the transform
30 * decays, so it needs far fewer nodes -- 32 against Euler's 99. It requires
31 * F to be analytic there, which a rational transform is and a transform with
32 * a branch cut is not.
33 * - GAVER-STEHFEST samples F on the REAL axis only, which is what makes it the
34 * one usable method when the transform cannot be evaluated at complex
35 * argument. It pays for that in conditioning: the weights alternate in sign
36 * and grow, so it needs high precision and `n` even (again rounded).
37 * - CME is `api/mam/matlab_ilt.h`, whose coefficient table is vendored; it is
38 * not duplicated here, and `laplace_invert` dispatches to it.
39 *
40 * The defaults are the reference's own for Talbot (32) and Gaver-Stehfest (12).
41 * EULER'S IS NOT: the old default was 99, which is unusable in double
42 * precision; both this port and native Python now default to 41. The measurement is at
43 * `laplace_invert_euler`.
44 *
45 * ARITHMETIC: double. Every one of these is a floating-point quadrature.
46 */
47
48#include <algorithm>
49#include <cmath>
50#include <complex>
51#include <cstddef>
52#include <functional>
53#include <string>
54#include <vector>
55
57#include "line/util/error.h"
58
59namespace line {
60namespace lti {
61
62using Cplx = std::complex<double>;
63
64/** The transform, evaluated at complex argument. */
65using LaplaceFn = std::function<Cplx(Cplx)>;
66/** A transform that can only be evaluated on the real axis. */
67using RealLaplaceFn = std::function<double(double)>;
68
69namespace ltidetail {
70
71/** Binomial coefficient, exactly, for the small n these methods use. */
72inline double binom(std::size_t n, std::size_t k) {
73 if (k > n) return 0.0;
74 double v = 1.0;
75 for (std::size_t i = 0; i < k; ++i)
76 v = v * static_cast<double>(n - i) / static_cast<double>(i + 1);
77 return v;
78}
79
80} // namespace ltidetail
81
82/** Euler nodes: a vertical line at Re = (n-1) log(10) / 6. */
83inline std::vector<Cplx> euler_get_alpha(std::size_t n) {
84 std::vector<Cplx> a(n);
85 for (std::size_t i = 0; i < n; ++i)
86 a[i] = Cplx(static_cast<double>(n - 1) * std::log(10.0) / 6.0,
87 M_PI * static_cast<double>(i));
88 return a;
89}
90
91/**
92 * Euler weights before the alternating sign and the scale.
93 *
94 * The tail is the binomial partial sums that give the Euler acceleration; it is
95 * filled BACKWARDS from the last entry, which is what makes the running sum
96 * correct.
97 */
98inline std::vector<double> euler_get_eta(std::size_t n) {
99 if (n < 3) throw InputError("euler_get_eta: at least three terms are required");
100 std::vector<double> res(n, 0.0);
101 res[0] = 0.5;
102 for (std::size_t i = 1; i < (n + 1) / 2; ++i) res[i] = 1.0;
103 res[n - 1] = 1.0 / std::pow(2.0, (static_cast<double>(n) - 1.0) / 2.0);
104 for (std::size_t i = 1; i < (n - 1) / 2; ++i)
105 res[n - i - 1] = res[n - i] + std::pow(2.0, (1.0 - static_cast<double>(n)) / 2.0) *
106 ltidetail::binom((n - 1) / 2, i);
107 return res;
108}
109
110/** Euler weights: eta, alternating in sign, scaled by 10^((n-1)/6). */
111inline std::vector<Cplx> euler_get_omega(std::size_t n) {
112 const std::vector<double> eta = euler_get_eta(n);
113 std::vector<Cplx> res(n);
114 const double scale = std::pow(10.0, (static_cast<double>(n) - 1.0) / 6.0);
115 for (std::size_t i = 0; i < n; ++i)
116 res[i] = Cplx(scale * ((i % 2 == 0) ? 1.0 : -1.0) * eta[i], 0.0);
117 return res;
118}
119
120/** Talbot nodes: the cotangent contour, bending into the left half plane. */
121inline std::vector<Cplx> talbot_get_alpha(std::size_t n) {
122 if (n == 0) throw InputError("talbot_get_alpha: at least one term is required");
123 std::vector<Cplx> a(n);
124 a[0] = Cplx(2.0 * static_cast<double>(n) / 5.0, 0.0);
125 for (std::size_t i = 1; i < n; ++i) {
126 const double th = static_cast<double>(i) * M_PI / static_cast<double>(n);
127 a[i] = Cplx(2.0 * static_cast<double>(i) * M_PI / 5.0 * (1.0 / std::tan(th)),
128 2.0 * static_cast<double>(i) * M_PI / 5.0);
129 }
130 return a;
131}
132
133/** Talbot weights, which carry the contour's own derivative. */
134inline std::vector<Cplx> talbot_get_omega(std::size_t n, const std::vector<Cplx>& alpha) {
135 if (alpha.size() != n) throw InputError("talbot_get_omega: alpha has the wrong length");
136 std::vector<Cplx> w(n);
137 w[0] = std::exp(alpha[0]) / 5.0;
138 for (std::size_t i = 1; i < n; ++i) {
139 const double th = static_cast<double>(i) * M_PI / static_cast<double>(n);
140 const double cot = 1.0 / std::tan(th);
141 const Cplx mult(1.0, th * (1.0 + cot * cot) - cot);
142 w[i] = 2.0 * std::exp(alpha[i]) / 5.0 * mult;
143 }
144 return w;
145}
146
147/** Gaver-Stehfest nodes: k log 2, on the REAL axis. */
148inline std::vector<double> gaver_stehfest_get_alpha(std::size_t n) {
149 if (n % 2 == 1) --n; // the method is defined for even n only
150 std::vector<double> a(n);
151 for (std::size_t k = 1; k <= n; ++k) a[k - 1] = static_cast<double>(k) * std::log(2.0);
152 return a;
153}
154
155/**
156 * Gaver-Stehfest weights.
157 *
158 * They alternate in sign and grow rapidly with n, which is why the method needs
159 * more precision than the others rather than more terms.
160 */
161inline std::vector<double> gaver_stehfest_get_omega(std::size_t n) {
162 if (n % 2 == 1) --n;
163 if (n == 0) throw InputError("gaver_stehfest_get_omega: at least two terms are required");
164 const std::size_t h = n / 2;
165 double fact = 1.0;
166 for (std::size_t i = 2; i <= h; ++i) fact *= static_cast<double>(i);
167
168 std::vector<double> res(n, 0.0);
169 for (std::size_t k = 1; k <= n; ++k) {
170 double sum = 0.0;
171 for (std::size_t j = (k + 1) / 2; j <= std::min(k, h); ++j)
172 sum += std::pow(static_cast<double>(j), static_cast<double>(h + 1)) / fact *
173 ltidetail::binom(h, j) * ltidetail::binom(2 * j, j) *
174 ltidetail::binom(j, k - j);
175 res[k - 1] = (((h + k) % 2 == 0) ? 1.0 : -1.0) * std::log(2.0) * sum;
176 }
177 return res;
178}
179
180/**
181 * Euler inversion of F at t. `n` is rounded UP to odd, as the reference does.
182 *
183 * THE DEFAULT IS 41, NOT THE OLD 99, AND THAT IS A CORRECTION RATHER THAN A
184 * PREFERENCE. The weights carry a factor 10^((n-1)/6), and the sum they
185 * multiply alternates in sign, so the method's accuracy is a race between the
186 * series converging and the cancellation eating the mantissa. Measured on
187 * F(s) = 2/(s+2), whose inverse is 2 exp(-2t), as the worst relative error over
188 * t in {0.1, 0.5, 1, 2}:
189 *
190 * n = 11 21 31 41 51 71 99
191 * err = 4.4e-3 2.1e-6 1.6e-9 1.6e-10 4.7e-8 1.7e-4 1.4e+0
192 *
193 * At the reference's 99 the scale is 2.2e16, past what a double resolves, and
194 * the answer is 140 per cent wrong -- negative at some t. Native Python has the
195 * same default and the same behaviour (measured: 1.997 against an exact 1.637
196 * at t = 0.1, and -0.033 at t = 0.5), and `api/lti` has no MATLAB twin, so
197 * nothing else in the tree catches it. Shipping a default that returns noise
198 * is not a convention worth preserving; the reference's own defect is recorded
199 * for its maintainer rather than reproduced here.
200 */
201inline double laplace_invert_euler(const LaplaceFn& F, double t, std::size_t n = 41) {
202 if (!(t > 0.0)) throw InputError("laplace_invert_euler: t must be positive");
203 if (n % 2 == 0) ++n;
204 const std::vector<Cplx> a = euler_get_alpha(n), w = euler_get_omega(n);
205 double r = 0.0;
206 for (std::size_t i = 0; i < n; ++i) r += (w[i] * F(a[i] / t)).real();
207 return r / t;
208}
209
210/** Talbot inversion of F at t. */
211inline double laplace_invert_talbot(const LaplaceFn& F, double t, std::size_t n = 32) {
212 if (!(t > 0.0)) throw InputError("laplace_invert_talbot: t must be positive");
213 if (n == 0) throw InputError("laplace_invert_talbot: at least one term is required");
214 const std::vector<Cplx> a = talbot_get_alpha(n);
215 const std::vector<Cplx> w = talbot_get_omega(n, a);
216 double r = 0.0;
217 for (std::size_t i = 0; i < n; ++i) r += (w[i] * F(a[i] / t)).real();
218 return r / t;
219}
220
221/**
222 * Gaver-Stehfest inversion of F at t.
223 *
224 * The transform is sampled on the REAL axis only, which is the whole reason to
225 * choose this method. `n` is rounded DOWN to even.
226 */
227inline double laplace_invert_gaver_stehfest(const RealLaplaceFn& F, double t,
228 std::size_t n = 12) {
229 if (!(t > 0.0)) throw InputError("laplace_invert_gaver_stehfest: t must be positive");
230 if (n % 2 == 1) --n;
231 const std::vector<double> a = gaver_stehfest_get_alpha(n);
232 const std::vector<double> w = gaver_stehfest_get_omega(n);
233 double r = 0.0;
234 for (std::size_t i = 0; i < a.size(); ++i) r += w[i] * F(a[i] / t);
235 return r / t;
236}
237
238// ---------------------------------------------------------------------------
239// Weeks / Laguerre (Weeks, JACM 13, 1966; Abate, Choudhury and Whitt, INFORMS
240// J. Computing 8(4), 1996; Harrison and Knottenbelt 2002, Sec. 4.1-4.3)
241// ---------------------------------------------------------------------------
242
243/** A Laguerre expansion: the damping, the scaling and the coefficients. */
245 double sigma = 0.0;
246 double b = 1.0;
247 std::vector<double> q;
248};
249
250/**
251 * Laguerre coefficients q_n, n = 0..2*p0-1, of f_{sigma,b}(t) =
252 * exp(-sigma t) f(t/b), whose generating function is
253 *
254 * Q_{sigma,b}(z) = b/(1-z) * L( b(1+z)/(2(1-z)) + b*sigma ).
255 *
256 * NOTE ON THE PAPER. Eq. 10 as printed carries the factor (1-z) rather than
257 * 1/(1-z). The scaled form above, printed later in the same section, carries
258 * 1/(1-z) and is the correct one: with l_n(t) = exp(-t/2) L_n(t) the transform
259 * of l_n is (s-1/2)^n/(s+1/2)^{n+1}, so L(s) = Q(z)/(s+1/2) with
260 * z = (s-1/2)/(s+1/2) and s+1/2 = 1/(1-z). Implementing the printed (1-z) is
261 * wrong at every t (163 per cent at t = 0.1 on Exp(2)).
262 *
263 * Sec. 4.3 fixes the trapezoid count at 2*p0 and the radius at r = 0.1^(4/p0)
264 * for every n, so the quadrature is one discrete Fourier transform of Q sampled
265 * on the circle and the transform is evaluated 2*p0 times IN TOTAL rather than
266 * per coefficient. The DFT is evaluated directly: at 2*p0 = 400 points that is
267 * 160k complex multiplies, which is not worth a dependency.
268 */
269inline std::vector<double> laplace_weeks_coeffs(const LaplaceFn& F, double sigma = 0.0,
270 double b = 1.0, std::size_t p0 = 200) {
271 if (!(b > 0.0)) throw InputError("laplace_weeks_coeffs: b must be positive");
272 if (p0 == 0) throw InputError("laplace_weeks_coeffs: p0 must be positive");
273 const std::size_t N = 2 * p0;
274 const double r = std::pow(0.1, 4.0 / static_cast<double>(p0));
275 const double twopi = 2.0 * 3.14159265358979323846;
276
277 std::vector<Cplx> Q(N);
278 for (std::size_t j = 0; j < N; ++j) {
279 const double u = twopi * static_cast<double>(j) / static_cast<double>(N);
280 const Cplx z = r * Cplx(std::cos(u), std::sin(u));
281 const Cplx s = b * (Cplx(1.0, 0.0) + z) / (2.0 * (Cplx(1.0, 0.0) - z)) + b * sigma;
282 Q[j] = b / (Cplx(1.0, 0.0) - z) * F(s);
283 }
284
285 std::vector<double> q(N, 0.0);
286 double rpow = 1.0;
287 for (std::size_t n = 0; n < N; ++n) {
288 Cplx acc(0.0, 0.0);
289 for (std::size_t j = 0; j < N; ++j) {
290 const double u = -twopi * static_cast<double>(n) * static_cast<double>(j) /
291 static_cast<double>(N);
292 acc += Q[j] * Cplx(std::cos(u), std::sin(u));
293 }
294 q[n] = acc.real() / static_cast<double>(N) / rpow;
295 rpow *= r;
296 }
297 return q;
298}
299
300/**
301 * The automatic (sigma, b) search of Fig. 1: accept the first pair at which the
302 * coefficients have decayed by term p0, doubling sigma from 0.001 and stepping
303 * b by 4 whenever sigma passes 0.2.
304 *
305 * REFUSES BY NAME when the box is exhausted. Raising b further is
306 * counterproductive and excessive damping is unstable in finite precision, and
307 * a density with a discontinuity in itself or its derivatives has no usable
308 * Laguerre representation at all (Sec. 4.2). Returning the last iterate would
309 * report noise as an answer; Euler handles those cases instead.
310 */
311inline WeeksParams laplace_weeks_scaling(const LaplaceFn& F, std::size_t p0 = 200,
312 double tol = 1e-10) {
313 WeeksParams w;
314 w.sigma = 0.0;
315 w.b = 1.0;
316 for (;;) {
317 w.q = laplace_weeks_coeffs(F, w.sigma, w.b, p0);
318 if (std::abs(w.q[p0]) <= tol && std::abs(w.q[p0 + 1]) <= tol) return w;
319 w.sigma = (w.sigma == 0.0) ? 0.001 : 2.0 * w.sigma;
320 if (w.sigma > 0.2) {
321 w.b += 4.0;
322 if (w.b > 10.0)
323 throw NumericError(
324 "laplace_weeks_scaling: no suitable scaling parameters were found for the "
325 "Laguerre inversion: the transform's density is not smooth enough for a "
326 "Laguerre series. Use the euler method instead.");
327 w.sigma = 0.0;
328 }
329 }
330}
331
332namespace weeks_detail {
333
334/**
335 * Truncate at the FIRST index where the coefficients have decayed, never the
336 * last. The quadrature divides by r^n with r < 1, so past the genuine decay the
337 * entries are rounding noise amplified by r^-n: at n = 2*p0 that factor is 1e8,
338 * and scanning for the last entry above a threshold sums 1e-8 of pure noise
339 * (worst error on Exp(2) 2.7e-09 instead of 1.9e-14).
340 */
341inline std::size_t nterms(const std::vector<double>& q) {
342 const std::size_t p0 = q.size() / 2;
343 for (std::size_t n = 1; n + 1 < p0; ++n)
344 if (std::abs(q[n]) <= 1e-13 && std::abs(q[n + 1]) <= 1e-13) return n;
345 return p0;
346}
347
348/** l_n(t) = exp(-t/2) L_n(t) by the stable recursion of Sec. 4.1. */
349inline std::vector<double> functions(double t, std::size_t N) {
350 std::vector<double> l(N, 0.0);
351 if (N == 0) return l;
352 l[0] = std::exp(-t / 2.0);
353 if (N > 1) l[1] = (1.0 - t) * l[0];
354 for (std::size_t n = 2; n < N; ++n) {
355 const double dn = static_cast<double>(n);
356 l[n] = ((2.0 * dn - 1.0 - t) / dn) * l[n - 1] - ((dn - 1.0) / dn) * l[n - 2];
357 }
358 return l;
359}
360
361} // namespace weeks_detail
362
363/**
364 * Invert by the Laguerre series f(t) = sum_n q_n l_n(t), recovered as
365 * exp(sigma*b*t) f_{sigma,b}(b*t).
366 *
367 * Unlike Euler and Talbot the coefficients do not depend on t, so ONE parameter
368 * set serves an arbitrary number of time points: the transform is evaluated
369 * 2*p0 times in total, not 2*p0 times per t. That is the property this method
370 * is here for, so build the WeeksParams once and reuse it on a grid.
371 */
372inline double laplace_invert_weeks(const WeeksParams& w, double t) {
373 if (!(t > 0.0)) return 0.0;
374 const std::size_t n = weeks_detail::nterms(w.q);
375 const std::vector<double> l = weeks_detail::functions(w.b * t, n);
376 double acc = 0.0;
377 for (std::size_t i = 0; i < n; ++i) acc += w.q[i] * l[i];
378 return std::exp(w.sigma * w.b * t) * acc;
379}
380
381/** Convenience overload: build the parameters, then invert at one point. */
382inline double laplace_invert_weeks(const LaplaceFn& F, double t, std::size_t p0 = 200) {
384}
385
386/** The methods `laplace_invert` accepts. */
388
389/** Parse the reference's method names, including its two Gaver spellings. */
390inline LaplaceMethod laplace_method(const std::string& s) {
391 if (s == "euler") return LaplaceMethod::Euler;
392 if (s == "talbot") return LaplaceMethod::Talbot;
393 if (s == "gaver-stehfest" || s == "gaver_stehfest" || s == "gaver")
395 if (s == "cme") return LaplaceMethod::Cme;
396 if (s == "weeks" || s == "laguerre") return LaplaceMethod::Weeks;
397 throw InputError("laplace_invert: unknown method '" + s +
398 "', expected euler, talbot, gaver-stehfest, cme or weeks");
399}
400
401/**
402 * Invert F at t by the named method.
403 *
404 * @param n 0 takes the method's own default: 41 Euler (see
405 * `laplace_invert_euler`), 32 Talbot, 12 Gaver-Stehfest, 25 CME
406 */
407inline double laplace_invert(const LaplaceFn& F, double t,
408 LaplaceMethod method = LaplaceMethod::Euler, std::size_t n = 0) {
409 switch (method) {
410 case LaplaceMethod::Euler: return laplace_invert_euler(F, t, n ? n : 41);
411 case LaplaceMethod::Talbot: return laplace_invert_talbot(F, t, n ? n : 32);
413 // The real-axis method is fed the same transform restricted to the
414 // real axis; a transform that cannot be evaluated there will say so
415 // itself rather than being silently approximated.
417 [&F](double s) { return F(Cplx(s, 0.0)).real(); }, t, n ? n : 12);
418 case LaplaceMethod::Cme: {
419 std::vector<double> tv(1, t);
420 return mam::matlab_ilt([&F](const Cplx& s) { return F(s); }, tv, n ? n : 25,
422 }
424 // Rebuilding the expansion for a single point wastes the one
425 // property this method has; the grid overloads below do it once.
426 return laplace_invert_weeks(F, t, n ? n : 200);
427 }
428 throw InputError("laplace_invert: unreachable method");
429}
430
431/**
432 * The DENSITY on a grid: the inversion clamped at zero.
433 *
434 * A density cannot be negative, and a numerical inversion can undershoot near
435 * the origin or in a tail; the reference clamps, and so does this.
436 */
437inline std::vector<double> laplace_invert_pdf(const LaplaceFn& F, const std::vector<double>& t,
439 std::size_t n = 0) {
440 std::vector<double> out(t.size(), 0.0);
441 if (method == LaplaceMethod::Weeks) {
442 // One expansion serves the whole grid; this is the point of Weeks.
443 const WeeksParams w = laplace_weeks_scaling(F, n ? n : 200);
444 for (std::size_t i = 0; i < t.size(); ++i)
445 out[i] = std::max(0.0, laplace_invert_weeks(w, t[i]));
446 return out;
447 }
448 for (std::size_t i = 0; i < t.size(); ++i) {
449 if (!(t[i] > 0.0)) continue;
450 out[i] = std::max(0.0, laplace_invert(F, t[i], method, n));
451 }
452 return out;
453}
454
455/**
456 * The DISTRIBUTION on a grid, from the transform of the DENSITY.
457 *
458 * F(s)/s is the transform of the CDF, so that is what is inverted -- passing
459 * the CDF's own transform here would invert it twice. The result is clamped
460 * into [0,1] and made monotone by a running maximum, because a numerical
461 * inversion is pointwise and nothing in it enforces either property; a
462 * non-monotone "CDF" then yields negative probabilities downstream.
463 */
464inline std::vector<double> laplace_invert_cdf(const LaplaceFn& F, const std::vector<double>& t,
466 std::size_t n = 0) {
467 const LaplaceFn Fc = [&F](Cplx s) {
468 if (std::abs(s) < 1e-15) return Cplx(1.0, 0.0);
469 return F(s) / s;
470 };
471 std::vector<double> out(t.size(), 0.0);
472 if (method == LaplaceMethod::Weeks) {
473 const WeeksParams w = laplace_weeks_scaling(Fc, n ? n : 200);
474 for (std::size_t i = 0; i < t.size(); ++i) {
475 double v = laplace_invert_weeks(w, t[i]);
476 out[i] = std::min(1.0, std::max(0.0, v));
477 }
478 for (std::size_t i = 1; i < out.size(); ++i) out[i] = std::max(out[i], out[i - 1]);
479 return out;
480 }
481 for (std::size_t i = 0; i < t.size(); ++i) {
482 if (!(t[i] > 0.0)) continue;
483 double v = laplace_invert(Fc, t[i], method, n);
484 if (v < 0.0) v = 0.0;
485 if (v > 1.0) v = 1.0;
486 out[i] = v;
487 }
488 for (std::size_t i = 1; i < out.size(); ++i) out[i] = std::max(out[i], out[i - 1]);
489 return out;
490}
491
492} // namespace lti
493} // namespace line
494
495#endif // LINE_API_LTI_LAPLACE_INVERT_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.
Numerical inverse Laplace transform in the Abate-Whitt framework, the port of matlab/lib/thirdparty/i...
double laplace_invert_euler(const LaplaceFn &F, double t, std::size_t n=41)
Euler inversion of F at t.
std::vector< double > laplace_invert_cdf(const LaplaceFn &F, const std::vector< double > &t, LaplaceMethod method=LaplaceMethod::Euler, std::size_t n=0)
The DISTRIBUTION on a grid, from the transform of the DENSITY.
double laplace_invert(const LaplaceFn &F, double t, LaplaceMethod method=LaplaceMethod::Euler, std::size_t n=0)
Invert F at t by the named method.
std::function< double(double)> RealLaplaceFn
A transform that can only be evaluated on the real axis.
std::vector< Cplx > talbot_get_alpha(std::size_t n)
Talbot nodes: the cotangent contour, bending into the left half plane.
std::vector< double > laplace_invert_pdf(const LaplaceFn &F, const std::vector< double > &t, LaplaceMethod method=LaplaceMethod::Euler, std::size_t n=0)
The DENSITY on a grid: the inversion clamped at zero.
double laplace_invert_weeks(const WeeksParams &w, double t)
Invert by the Laguerre series f(t) = sum_n q_n l_n(t), recovered as exp(sigma*b*t) f_{sigma,...
double laplace_invert_talbot(const LaplaceFn &F, double t, std::size_t n=32)
Talbot inversion of F at t.
std::vector< double > gaver_stehfest_get_omega(std::size_t n)
Gaver-Stehfest weights.
std::vector< Cplx > talbot_get_omega(std::size_t n, const std::vector< Cplx > &alpha)
Talbot weights, which carry the contour's own derivative.
std::vector< Cplx > euler_get_omega(std::size_t n)
Euler weights: eta, alternating in sign, scaled by 10^((n-1)/6).
LaplaceMethod
The methods laplace_invert accepts.
std::complex< double > Cplx
std::vector< double > euler_get_eta(std::size_t n)
Euler weights before the alternating sign and the scale.
LaplaceMethod laplace_method(const std::string &s)
Parse the reference's method names, including its two Gaver spellings.
std::vector< double > gaver_stehfest_get_alpha(std::size_t n)
Gaver-Stehfest nodes: k log 2, on the REAL axis.
WeeksParams laplace_weeks_scaling(const LaplaceFn &F, std::size_t p0=200, double tol=1e-10)
The automatic (sigma, b) search of Fig.
std::function< Cplx(Cplx)> LaplaceFn
The transform, evaluated at complex argument.
std::vector< Cplx > euler_get_alpha(std::size_t n)
Euler nodes: a vertical line at Re = (n-1) log(10) / 6.
double laplace_invert_gaver_stehfest(const RealLaplaceFn &F, double t, std::size_t n=12)
Gaver-Stehfest inversion of F at t.
std::vector< double > laplace_weeks_coeffs(const LaplaceFn &F, double sigma=0.0, double b=1.0, std::size_t p0=200)
Laguerre coefficients q_n, n = 0..2*p0-1, of f_{sigma,b}(t) = exp(-sigma t) f(t/b),...
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
A Laguerre expansion: the damping, the scaling and the coefficients.
std::vector< double > q