974 lines
35 KiB
Python
974 lines
35 KiB
Python
|
# contributed to mpmath by Kristopher L. Kuhlman, February 2017
|
||
|
# contributed to mpmath by Guillermo Navas-Palencia, February 2022
|
||
|
|
||
|
class InverseLaplaceTransform(object):
|
||
|
r"""
|
||
|
Inverse Laplace transform methods are implemented using this
|
||
|
class, in order to simplify the code and provide a common
|
||
|
infrastructure.
|
||
|
|
||
|
Implement a custom inverse Laplace transform algorithm by
|
||
|
subclassing :class:`InverseLaplaceTransform` and implementing the
|
||
|
appropriate methods. The subclass can then be used by
|
||
|
:func:`~mpmath.invertlaplace` by passing it as the *method*
|
||
|
argument.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, ctx):
|
||
|
self.ctx = ctx
|
||
|
|
||
|
def calc_laplace_parameter(self, t, **kwargs):
|
||
|
r"""
|
||
|
Determine the vector of Laplace parameter values needed for an
|
||
|
algorithm, this will depend on the choice of algorithm (de
|
||
|
Hoog is default), the algorithm-specific parameters passed (or
|
||
|
default ones), and desired time.
|
||
|
"""
|
||
|
raise NotImplementedError
|
||
|
|
||
|
def calc_time_domain_solution(self, fp):
|
||
|
r"""
|
||
|
Compute the time domain solution, after computing the
|
||
|
Laplace-space function evaluations at the abscissa required
|
||
|
for the algorithm. Abscissa computed for one algorithm are
|
||
|
typically not useful for another algorithm.
|
||
|
"""
|
||
|
raise NotImplementedError
|
||
|
|
||
|
|
||
|
class FixedTalbot(InverseLaplaceTransform):
|
||
|
|
||
|
def calc_laplace_parameter(self, t, **kwargs):
|
||
|
r"""The "fixed" Talbot method deforms the Bromwich contour towards
|
||
|
`-\infty` in the shape of a parabola. Traditionally the Talbot
|
||
|
algorithm has adjustable parameters, but the "fixed" version
|
||
|
does not. The `r` parameter could be passed in as a parameter,
|
||
|
if you want to override the default given by (Abate & Valko,
|
||
|
2004).
|
||
|
|
||
|
The Laplace parameter is sampled along a parabola opening
|
||
|
along the negative imaginary axis, with the base of the
|
||
|
parabola along the real axis at
|
||
|
`p=\frac{r}{t_\mathrm{max}}`. As the number of terms used in
|
||
|
the approximation (degree) grows, the abscissa required for
|
||
|
function evaluation tend towards `-\infty`, requiring high
|
||
|
precision to prevent overflow. If any poles, branch cuts or
|
||
|
other singularities exist such that the deformed Bromwich
|
||
|
contour lies to the left of the singularity, the method will
|
||
|
fail.
|
||
|
|
||
|
**Optional arguments**
|
||
|
|
||
|
:class:`~mpmath.calculus.inverselaplace.FixedTalbot.calc_laplace_parameter`
|
||
|
recognizes the following keywords
|
||
|
|
||
|
*tmax*
|
||
|
maximum time associated with vector of times
|
||
|
(typically just the time requested)
|
||
|
*degree*
|
||
|
integer order of approximation (M = number of terms)
|
||
|
*r*
|
||
|
abscissa for `p_0` (otherwise computed using rule
|
||
|
of thumb `2M/5`)
|
||
|
|
||
|
The working precision will be increased according to a rule of
|
||
|
thumb. If 'degree' is not specified, the working precision and
|
||
|
degree are chosen to hopefully achieve the dps of the calling
|
||
|
context. If 'degree' is specified, the working precision is
|
||
|
chosen to achieve maximum resulting precision for the
|
||
|
specified degree.
|
||
|
|
||
|
.. math ::
|
||
|
|
||
|
p_0=\frac{r}{t}
|
||
|
|
||
|
.. math ::
|
||
|
|
||
|
p_i=\frac{i r \pi}{Mt_\mathrm{max}}\left[\cot\left(
|
||
|
\frac{i\pi}{M}\right) + j \right] \qquad 1\le i <M
|
||
|
|
||
|
where `j=\sqrt{-1}`, `r=2M/5`, and `t_\mathrm{max}` is the
|
||
|
maximum specified time.
|
||
|
|
||
|
"""
|
||
|
|
||
|
# required
|
||
|
# ------------------------------
|
||
|
# time of desired approximation
|
||
|
self.t = self.ctx.convert(t)
|
||
|
|
||
|
# optional
|
||
|
# ------------------------------
|
||
|
# maximum time desired (used for scaling) default is requested
|
||
|
# time.
|
||
|
self.tmax = self.ctx.convert(kwargs.get('tmax', self.t))
|
||
|
|
||
|
# empirical relationships used here based on a linear fit of
|
||
|
# requested and delivered dps for exponentially decaying time
|
||
|
# functions for requested dps up to 512.
|
||
|
|
||
|
if 'degree' in kwargs:
|
||
|
self.degree = kwargs['degree']
|
||
|
self.dps_goal = self.degree
|
||
|
else:
|
||
|
self.dps_goal = int(1.72*self.ctx.dps)
|
||
|
self.degree = max(12, int(1.38*self.dps_goal))
|
||
|
|
||
|
M = self.degree
|
||
|
|
||
|
# this is adjusting the dps of the calling context hopefully
|
||
|
# the caller doesn't monkey around with it between calling
|
||
|
# this routine and calc_time_domain_solution()
|
||
|
self.dps_orig = self.ctx.dps
|
||
|
self.ctx.dps = self.dps_goal
|
||
|
|
||
|
# Abate & Valko rule of thumb for r parameter
|
||
|
self.r = kwargs.get('r', self.ctx.fraction(2, 5)*M)
|
||
|
|
||
|
self.theta = self.ctx.linspace(0.0, self.ctx.pi, M+1)
|
||
|
|
||
|
self.cot_theta = self.ctx.matrix(M, 1)
|
||
|
self.cot_theta[0] = 0 # not used
|
||
|
|
||
|
# all but time-dependent part of p
|
||
|
self.delta = self.ctx.matrix(M, 1)
|
||
|
self.delta[0] = self.r
|
||
|
|
||
|
for i in range(1, M):
|
||
|
self.cot_theta[i] = self.ctx.cot(self.theta[i])
|
||
|
self.delta[i] = self.r*self.theta[i]*(self.cot_theta[i] + 1j)
|
||
|
|
||
|
self.p = self.ctx.matrix(M, 1)
|
||
|
self.p = self.delta/self.tmax
|
||
|
|
||
|
# NB: p is complex (mpc)
|
||
|
|
||
|
def calc_time_domain_solution(self, fp, t, manual_prec=False):
|
||
|
r"""The fixed Talbot time-domain solution is computed from the
|
||
|
Laplace-space function evaluations using
|
||
|
|
||
|
.. math ::
|
||
|
|
||
|
f(t,M)=\frac{2}{5t}\sum_{k=0}^{M-1}\Re \left[
|
||
|
\gamma_k \bar{f}(p_k)\right]
|
||
|
|
||
|
where
|
||
|
|
||
|
.. math ::
|
||
|
|
||
|
\gamma_0 = \frac{1}{2}e^{r}\bar{f}(p_0)
|
||
|
|
||
|
.. math ::
|
||
|
|
||
|
\gamma_k = e^{tp_k}\left\lbrace 1 + \frac{jk\pi}{M}\left[1 +
|
||
|
\cot \left( \frac{k \pi}{M} \right)^2 \right] - j\cot\left(
|
||
|
\frac{k \pi}{M}\right)\right \rbrace \qquad 1\le k<M.
|
||
|
|
||
|
Again, `j=\sqrt{-1}`.
|
||
|
|
||
|
Before calling this function, call
|
||
|
:class:`~mpmath.calculus.inverselaplace.FixedTalbot.calc_laplace_parameter`
|
||
|
to set the parameters and compute the required coefficients.
|
||
|
|
||
|
**References**
|
||
|
|
||
|
1. Abate, J., P. Valko (2004). Multi-precision Laplace
|
||
|
transform inversion. *International Journal for Numerical
|
||
|
Methods in Engineering* 60:979-993,
|
||
|
http://dx.doi.org/10.1002/nme.995
|
||
|
2. Talbot, A. (1979). The accurate numerical inversion of
|
||
|
Laplace transforms. *IMA Journal of Applied Mathematics*
|
||
|
23(1):97, http://dx.doi.org/10.1093/imamat/23.1.97
|
||
|
"""
|
||
|
|
||
|
# required
|
||
|
# ------------------------------
|
||
|
self.t = self.ctx.convert(t)
|
||
|
|
||
|
# assume fp was computed from p matrix returned from
|
||
|
# calc_laplace_parameter(), so is already a list or matrix of
|
||
|
# mpmath 'mpc' types
|
||
|
|
||
|
# these were computed in previous call to
|
||
|
# calc_laplace_parameter()
|
||
|
theta = self.theta
|
||
|
delta = self.delta
|
||
|
M = self.degree
|
||
|
p = self.p
|
||
|
r = self.r
|
||
|
|
||
|
ans = self.ctx.matrix(M, 1)
|
||
|
ans[0] = self.ctx.exp(delta[0])*fp[0]/2
|
||
|
|
||
|
for i in range(1, M):
|
||
|
ans[i] = self.ctx.exp(delta[i])*fp[i]*(
|
||
|
1 + 1j*theta[i]*(1 + self.cot_theta[i]**2) -
|
||
|
1j*self.cot_theta[i])
|
||
|
|
||
|
result = self.ctx.fraction(2, 5)*self.ctx.fsum(ans)/self.t
|
||
|
|
||
|
# setting dps back to value when calc_laplace_parameter was
|
||
|
# called, unless flag is set.
|
||
|
if not manual_prec:
|
||
|
self.ctx.dps = self.dps_orig
|
||
|
|
||
|
return result.real
|
||
|
|
||
|
|
||
|
# ****************************************
|
||
|
|
||
|
class Stehfest(InverseLaplaceTransform):
|
||
|
|
||
|
def calc_laplace_parameter(self, t, **kwargs):
|
||
|
r"""
|
||
|
The Gaver-Stehfest method is a discrete approximation of the
|
||
|
Widder-Post inversion algorithm, rather than a direct
|
||
|
approximation of the Bromwich contour integral.
|
||
|
|
||
|
The method abscissa along the real axis, and therefore has
|
||
|
issues inverting oscillatory functions (which have poles in
|
||
|
pairs away from the real axis).
|
||
|
|
||
|
The working precision will be increased according to a rule of
|
||
|
thumb. If 'degree' is not specified, the working precision and
|
||
|
degree are chosen to hopefully achieve the dps of the calling
|
||
|
context. If 'degree' is specified, the working precision is
|
||
|
chosen to achieve maximum resulting precision for the
|
||
|
specified degree.
|
||
|
|
||
|
.. math ::
|
||
|
|
||
|
p_k = \frac{k \log 2}{t} \qquad 1 \le k \le M
|
||
|
"""
|
||
|
|
||
|
# required
|
||
|
# ------------------------------
|
||
|
# time of desired approximation
|
||
|
self.t = self.ctx.convert(t)
|
||
|
|
||
|
# optional
|
||
|
# ------------------------------
|
||
|
|
||
|
# empirical relationships used here based on a linear fit of
|
||
|
# requested and delivered dps for exponentially decaying time
|
||
|
# functions for requested dps up to 512.
|
||
|
|
||
|
if 'degree' in kwargs:
|
||
|
self.degree = kwargs['degree']
|
||
|
self.dps_goal = int(1.38*self.degree)
|
||
|
else:
|
||
|
self.dps_goal = int(2.93*self.ctx.dps)
|
||
|
self.degree = max(16, self.dps_goal)
|
||
|
|
||
|
# _coeff routine requires even degree
|
||
|
if self.degree % 2 > 0:
|
||
|
self.degree += 1
|
||
|
|
||
|
M = self.degree
|
||
|
|
||
|
# this is adjusting the dps of the calling context
|
||
|
# hopefully the caller doesn't monkey around with it
|
||
|
# between calling this routine and calc_time_domain_solution()
|
||
|
self.dps_orig = self.ctx.dps
|
||
|
self.ctx.dps = self.dps_goal
|
||
|
|
||
|
self.V = self._coeff()
|
||
|
self.p = self.ctx.matrix(self.ctx.arange(1, M+1))*self.ctx.ln2/self.t
|
||
|
|
||
|
# NB: p is real (mpf)
|
||
|
|
||
|
def _coeff(self):
|
||
|
r"""Salzer summation weights (aka, "Stehfest coefficients")
|
||
|
only depend on the approximation order (M) and the precision"""
|
||
|
|
||
|
M = self.degree
|
||
|
M2 = int(M/2) # checked earlier that M is even
|
||
|
|
||
|
V = self.ctx.matrix(M, 1)
|
||
|
|
||
|
# Salzer summation weights
|
||
|
# get very large in magnitude and oscillate in sign,
|
||
|
# if the precision is not high enough, there will be
|
||
|
# catastrophic cancellation
|
||
|
for k in range(1, M+1):
|
||
|
z = self.ctx.matrix(min(k, M2)+1, 1)
|
||
|
for j in range(int((k+1)/2), min(k, M2)+1):
|
||
|
z[j] = (self.ctx.power(j, M2)*self.ctx.fac(2*j)/
|
||
|
(self.ctx.fac(M2-j)*self.ctx.fac(j)*
|
||
|
self.ctx.fac(j-1)*self.ctx.fac(k-j)*
|
||
|
self.ctx.fac(2*j-k)))
|
||
|
V[k-1] = self.ctx.power(-1, k+M2)*self.ctx.fsum(z)
|
||
|
|
||
|
return V
|
||
|
|
||
|
def calc_time_domain_solution(self, fp, t, manual_prec=False):
|
||
|
r"""Compute time-domain Stehfest algorithm solution.
|
||
|
|
||
|
.. math ::
|
||
|
|
||
|
f(t,M) = \frac{\log 2}{t} \sum_{k=1}^{M} V_k \bar{f}\left(
|
||
|
p_k \right)
|
||
|
|
||
|
where
|
||
|
|
||
|
.. math ::
|
||
|
|
||
|
V_k = (-1)^{k + N/2} \sum^{\min(k,N/2)}_{i=\lfloor(k+1)/2 \rfloor}
|
||
|
\frac{i^{\frac{N}{2}}(2i)!}{\left(\frac{N}{2}-i \right)! \, i! \,
|
||
|
\left(i-1 \right)! \, \left(k-i\right)! \, \left(2i-k \right)!}
|
||
|
|
||
|
As the degree increases, the abscissa (`p_k`) only increase
|
||
|
linearly towards `\infty`, but the Stehfest coefficients
|
||
|
(`V_k`) alternate in sign and increase rapidly in sign,
|
||
|
requiring high precision to prevent overflow or loss of
|
||
|
significance when evaluating the sum.
|
||
|
|
||
|
**References**
|
||
|
|
||
|
1. Widder, D. (1941). *The Laplace Transform*. Princeton.
|
||
|
2. Stehfest, H. (1970). Algorithm 368: numerical inversion of
|
||
|
Laplace transforms. *Communications of the ACM* 13(1):47-49,
|
||
|
http://dx.doi.org/10.1145/361953.361969
|
||
|
|
||
|
"""
|
||
|
|
||
|
# required
|
||
|
self.t = self.ctx.convert(t)
|
||
|
|
||
|
# assume fp was computed from p matrix returned from
|
||
|
# calc_laplace_parameter(), so is already
|
||
|
# a list or matrix of mpmath 'mpf' types
|
||
|
|
||
|
result = self.ctx.fdot(self.V, fp)*self.ctx.ln2/self.t
|
||
|
|
||
|
# setting dps back to value when calc_laplace_parameter was called
|
||
|
if not manual_prec:
|
||
|
self.ctx.dps = self.dps_orig
|
||
|
|
||
|
# ignore any small imaginary part
|
||
|
return result.real
|
||
|
|
||
|
|
||
|
# ****************************************
|
||
|
|
||
|
class deHoog(InverseLaplaceTransform):
|
||
|
|
||
|
def calc_laplace_parameter(self, t, **kwargs):
|
||
|
r"""the de Hoog, Knight & Stokes algorithm is an
|
||
|
accelerated form of the Fourier series numerical
|
||
|
inverse Laplace transform algorithms.
|
||
|
|
||
|
.. math ::
|
||
|
|
||
|
p_k = \gamma + \frac{jk}{T} \qquad 0 \le k < 2M+1
|
||
|
|
||
|
where
|
||
|
|
||
|
.. math ::
|
||
|
|
||
|
\gamma = \alpha - \frac{\log \mathrm{tol}}{2T},
|
||
|
|
||
|
`j=\sqrt{-1}`, `T = 2t_\mathrm{max}` is a scaled time,
|
||
|
`\alpha=10^{-\mathrm{dps\_goal}}` is the real part of the
|
||
|
rightmost pole or singularity, which is chosen based on the
|
||
|
desired accuracy (assuming the rightmost singularity is 0),
|
||
|
and `\mathrm{tol}=10\alpha` is the desired tolerance, which is
|
||
|
chosen in relation to `\alpha`.`
|
||
|
|
||
|
When increasing the degree, the abscissa increase towards
|
||
|
`j\infty`, but more slowly than the fixed Talbot
|
||
|
algorithm. The de Hoog et al. algorithm typically does better
|
||
|
with oscillatory functions of time, and less well-behaved
|
||
|
functions. The method tends to be slower than the Talbot and
|
||
|
Stehfest algorithsm, especially so at very high precision
|
||
|
(e.g., `>500` digits precision).
|
||
|
|
||
|
"""
|
||
|
|
||
|
# required
|
||
|
# ------------------------------
|
||
|
self.t = self.ctx.convert(t)
|
||
|
|
||
|
# optional
|
||
|
# ------------------------------
|
||
|
self.tmax = kwargs.get('tmax', self.t)
|
||
|
|
||
|
# empirical relationships used here based on a linear fit of
|
||
|
# requested and delivered dps for exponentially decaying time
|
||
|
# functions for requested dps up to 512.
|
||
|
|
||
|
if 'degree' in kwargs:
|
||
|
self.degree = kwargs['degree']
|
||
|
self.dps_goal = int(1.38*self.degree)
|
||
|
else:
|
||
|
self.dps_goal = int(self.ctx.dps*1.36)
|
||
|
self.degree = max(10, self.dps_goal)
|
||
|
|
||
|
# 2*M+1 terms in approximation
|
||
|
M = self.degree
|
||
|
|
||
|
# adjust alpha component of abscissa of convergence for higher
|
||
|
# precision
|
||
|
tmp = self.ctx.power(10.0, -self.dps_goal)
|
||
|
self.alpha = self.ctx.convert(kwargs.get('alpha', tmp))
|
||
|
|
||
|
# desired tolerance (here simply related to alpha)
|
||
|
self.tol = self.ctx.convert(kwargs.get('tol', self.alpha*10.0))
|
||
|
self.np = 2*self.degree+1 # number of terms in approximation
|
||
|
|
||
|
# this is adjusting the dps of the calling context
|
||
|
# hopefully the caller doesn't monkey around with it
|
||
|
# between calling this routine and calc_time_domain_solution()
|
||
|
self.dps_orig = self.ctx.dps
|
||
|
self.ctx.dps = self.dps_goal
|
||
|
|
||
|
# scaling factor (likely tun-able, but 2 is typical)
|
||
|
self.scale = kwargs.get('scale', 2)
|
||
|
self.T = self.ctx.convert(kwargs.get('T', self.scale*self.tmax))
|
||
|
|
||
|
self.p = self.ctx.matrix(2*M+1, 1)
|
||
|
self.gamma = self.alpha - self.ctx.log(self.tol)/(self.scale*self.T)
|
||
|
self.p = (self.gamma + self.ctx.pi*
|
||
|
self.ctx.matrix(self.ctx.arange(self.np))/self.T*1j)
|
||
|
|
||
|
# NB: p is complex (mpc)
|
||
|
|
||
|
def calc_time_domain_solution(self, fp, t, manual_prec=False):
|
||
|
r"""Calculate time-domain solution for
|
||
|
de Hoog, Knight & Stokes algorithm.
|
||
|
|
||
|
The un-accelerated Fourier series approach is:
|
||
|
|
||
|
.. math ::
|
||
|
|
||
|
f(t,2M+1) = \frac{e^{\gamma t}}{T} \sum_{k=0}^{2M}{}^{'}
|
||
|
\Re\left[\bar{f}\left( p_k \right)
|
||
|
e^{i\pi t/T} \right],
|
||
|
|
||
|
where the prime on the summation indicates the first term is halved.
|
||
|
|
||
|
This simplistic approach requires so many function evaluations
|
||
|
that it is not practical. Non-linear acceleration is
|
||
|
accomplished via Pade-approximation and an analytic expression
|
||
|
for the remainder of the continued fraction. See the original
|
||
|
paper (reference 2 below) a detailed description of the
|
||
|
numerical approach.
|
||
|
|
||
|
**References**
|
||
|
|
||
|
1. Davies, B. (2005). *Integral Transforms and their
|
||
|
Applications*, Third Edition. Springer.
|
||
|
2. de Hoog, F., J. Knight, A. Stokes (1982). An improved
|
||
|
method for numerical inversion of Laplace transforms. *SIAM
|
||
|
Journal of Scientific and Statistical Computing* 3:357-366,
|
||
|
http://dx.doi.org/10.1137/0903022
|
||
|
|
||
|
"""
|
||
|
|
||
|
M = self.degree
|
||
|
np = self.np
|
||
|
T = self.T
|
||
|
|
||
|
self.t = self.ctx.convert(t)
|
||
|
|
||
|
# would it be useful to try re-using
|
||
|
# space between e&q and A&B?
|
||
|
e = self.ctx.zeros(np, M+1)
|
||
|
q = self.ctx.matrix(2*M, M)
|
||
|
d = self.ctx.matrix(np, 1)
|
||
|
A = self.ctx.zeros(np+1, 1)
|
||
|
B = self.ctx.ones(np+1, 1)
|
||
|
|
||
|
# initialize Q-D table
|
||
|
e[:, 0] = 0.0 + 0j
|
||
|
q[0, 0] = fp[1]/(fp[0]/2)
|
||
|
for i in range(1, 2*M):
|
||
|
q[i, 0] = fp[i+1]/fp[i]
|
||
|
|
||
|
# rhombus rule for filling triangular Q-D table (e & q)
|
||
|
for r in range(1, M+1):
|
||
|
# start with e, column 1, 0:2*M-2
|
||
|
mr = 2*(M-r) + 1
|
||
|
e[0:mr, r] = q[1:mr+1, r-1] - q[0:mr, r-1] + e[1:mr+1, r-1]
|
||
|
if not r == M:
|
||
|
rq = r+1
|
||
|
mr = 2*(M-rq)+1 + 2
|
||
|
for i in range(mr):
|
||
|
q[i, rq-1] = q[i+1, rq-2]*e[i+1, rq-1]/e[i, rq-1]
|
||
|
|
||
|
# build up continued fraction coefficients (d)
|
||
|
d[0] = fp[0]/2
|
||
|
for r in range(1, M+1):
|
||
|
d[2*r-1] = -q[0, r-1] # even terms
|
||
|
d[2*r] = -e[0, r] # odd terms
|
||
|
|
||
|
# seed A and B for recurrence
|
||
|
A[0] = 0.0 + 0.0j
|
||
|
A[1] = d[0]
|
||
|
B[0:2] = 1.0 + 0.0j
|
||
|
|
||
|
# base of the power series
|
||
|
z = self.ctx.expjpi(self.t/T) # i*pi is already in fcn
|
||
|
|
||
|
# coefficients of Pade approximation (A & B)
|
||
|
# using recurrence for all but last term
|
||
|
for i in range(1, 2*M):
|
||
|
A[i+1] = A[i] + d[i]*A[i-1]*z
|
||
|
B[i+1] = B[i] + d[i]*B[i-1]*z
|
||
|
|
||
|
# "improved remainder" to continued fraction
|
||
|
brem = (1 + (d[2*M-1] - d[2*M])*z)/2
|
||
|
# powm1(x,y) computes x^y - 1 more accurately near zero
|
||
|
rem = brem*self.ctx.powm1(1 + d[2*M]*z/brem,
|
||
|
self.ctx.fraction(1, 2))
|
||
|
|
||
|
# last term of recurrence using new remainder
|
||
|
A[np] = A[2*M] + rem*A[2*M-1]
|
||
|
B[np] = B[2*M] + rem*B[2*M-1]
|
||
|
|
||
|
# diagonal Pade approximation
|
||
|
# F=A/B represents accelerated trapezoid rule
|
||
|
result = self.ctx.exp(self.gamma*self.t)/T*(A[np]/B[np]).real
|
||
|
|
||
|
# setting dps back to value when calc_laplace_parameter was called
|
||
|
if not manual_prec:
|
||
|
self.ctx.dps = self.dps_orig
|
||
|
|
||
|
return result
|
||
|
|
||
|
|
||
|
# ****************************************
|
||
|
|
||
|
class Cohen(InverseLaplaceTransform):
|
||
|
|
||
|
def calc_laplace_parameter(self, t, **kwargs):
|
||
|
r"""The Cohen algorithm accelerates the convergence of the nearly
|
||
|
alternating series resulting from the application of the trapezoidal
|
||
|
rule to the Bromwich contour inversion integral.
|
||
|
|
||
|
.. math ::
|
||
|
|
||
|
p_k = \frac{\gamma}{2 t} + \frac{\pi i k}{t} \qquad 0 \le k < M
|
||
|
|
||
|
where
|
||
|
|
||
|
.. math ::
|
||
|
|
||
|
\gamma = \frac{2}{3} (d + \log(10) + \log(2 t)),
|
||
|
|
||
|
`d = \mathrm{dps\_goal}`, which is chosen based on the desired
|
||
|
accuracy using the method developed in [1] to improve numerical
|
||
|
stability. The Cohen algorithm shows robustness similar to the de Hoog
|
||
|
et al. algorithm, but it is faster than the fixed Talbot algorithm.
|
||
|
|
||
|
**Optional arguments**
|
||
|
|
||
|
*degree*
|
||
|
integer order of the approximation (M = number of terms)
|
||
|
*alpha*
|
||
|
abscissa for `p_0` (controls the discretization error)
|
||
|
|
||
|
The working precision will be increased according to a rule of
|
||
|
thumb. If 'degree' is not specified, the working precision and
|
||
|
degree are chosen to hopefully achieve the dps of the calling
|
||
|
context. If 'degree' is specified, the working precision is
|
||
|
chosen to achieve maximum resulting precision for the
|
||
|
specified degree.
|
||
|
|
||
|
**References**
|
||
|
|
||
|
1. P. Glasserman, J. Ruiz-Mata (2006). Computing the credit loss
|
||
|
distribution in the Gaussian copula model: a comparison of methods.
|
||
|
*Journal of Credit Risk* 2(4):33-66, 10.21314/JCR.2006.057
|
||
|
|
||
|
"""
|
||
|
self.t = self.ctx.convert(t)
|
||
|
|
||
|
if 'degree' in kwargs:
|
||
|
self.degree = kwargs['degree']
|
||
|
self.dps_goal = int(1.5 * self.degree)
|
||
|
else:
|
||
|
self.dps_goal = int(self.ctx.dps * 1.74)
|
||
|
self.degree = max(22, int(1.31 * self.dps_goal))
|
||
|
|
||
|
M = self.degree + 1
|
||
|
|
||
|
# this is adjusting the dps of the calling context hopefully
|
||
|
# the caller doesn't monkey around with it between calling
|
||
|
# this routine and calc_time_domain_solution()
|
||
|
self.dps_orig = self.ctx.dps
|
||
|
self.ctx.dps = self.dps_goal
|
||
|
|
||
|
ttwo = 2 * self.t
|
||
|
tmp = self.ctx.dps * self.ctx.log(10) + self.ctx.log(ttwo)
|
||
|
tmp = self.ctx.fraction(2, 3) * tmp
|
||
|
self.alpha = self.ctx.convert(kwargs.get('alpha', tmp))
|
||
|
|
||
|
# all but time-dependent part of p
|
||
|
a_t = self.alpha / ttwo
|
||
|
p_t = self.ctx.pi * 1j / self.t
|
||
|
|
||
|
self.p = self.ctx.matrix(M, 1)
|
||
|
self.p[0] = a_t
|
||
|
|
||
|
for i in range(1, M):
|
||
|
self.p[i] = a_t + i * p_t
|
||
|
|
||
|
def calc_time_domain_solution(self, fp, t, manual_prec=False):
|
||
|
r"""Calculate time-domain solution for Cohen algorithm.
|
||
|
|
||
|
The accelerated nearly alternating series is:
|
||
|
|
||
|
.. math ::
|
||
|
|
||
|
f(t, M) = \frac{e^{\gamma / 2}}{t} \left[\frac{1}{2}
|
||
|
\Re\left(\bar{f}\left(\frac{\gamma}{2t}\right) \right) -
|
||
|
\sum_{k=0}^{M-1}\frac{c_{M,k}}{d_M}\Re\left(\bar{f}
|
||
|
\left(\frac{\gamma + 2(k+1) \pi i}{2t}\right)\right)\right],
|
||
|
|
||
|
where coefficients `\frac{c_{M, k}}{d_M}` are described in [1].
|
||
|
|
||
|
1. H. Cohen, F. Rodriguez Villegas, D. Zagier (2000). Convergence
|
||
|
acceleration of alternating series. *Experiment. Math* 9(1):3-12
|
||
|
|
||
|
"""
|
||
|
self.t = self.ctx.convert(t)
|
||
|
|
||
|
n = self.degree
|
||
|
M = n + 1
|
||
|
|
||
|
A = self.ctx.matrix(M, 1)
|
||
|
for i in range(M):
|
||
|
A[i] = fp[i].real
|
||
|
|
||
|
d = (3 + self.ctx.sqrt(8)) ** n
|
||
|
d = (d + 1 / d) / 2
|
||
|
b = -self.ctx.one
|
||
|
c = -d
|
||
|
s = 0
|
||
|
|
||
|
for k in range(n):
|
||
|
c = b - c
|
||
|
s = s + c * A[k + 1]
|
||
|
b = 2 * (k + n) * (k - n) * b / ((2 * k + 1) * (k + self.ctx.one))
|
||
|
|
||
|
result = self.ctx.exp(self.alpha / 2) / self.t * (A[0] / 2 - s / d)
|
||
|
|
||
|
# setting dps back to value when calc_laplace_parameter was
|
||
|
# called, unless flag is set.
|
||
|
if not manual_prec:
|
||
|
self.ctx.dps = self.dps_orig
|
||
|
|
||
|
return result
|
||
|
|
||
|
|
||
|
# ****************************************
|
||
|
|
||
|
class LaplaceTransformInversionMethods(object):
|
||
|
def __init__(ctx, *args, **kwargs):
|
||
|
ctx._fixed_talbot = FixedTalbot(ctx)
|
||
|
ctx._stehfest = Stehfest(ctx)
|
||
|
ctx._de_hoog = deHoog(ctx)
|
||
|
ctx._cohen = Cohen(ctx)
|
||
|
|
||
|
def invertlaplace(ctx, f, t, **kwargs):
|
||
|
r"""Computes the numerical inverse Laplace transform for a
|
||
|
Laplace-space function at a given time. The function being
|
||
|
evaluated is assumed to be a real-valued function of time.
|
||
|
|
||
|
The user must supply a Laplace-space function `\bar{f}(p)`,
|
||
|
and a desired time at which to estimate the time-domain
|
||
|
solution `f(t)`.
|
||
|
|
||
|
A few basic examples of Laplace-space functions with known
|
||
|
inverses (see references [1,2]) :
|
||
|
|
||
|
.. math ::
|
||
|
|
||
|
\mathcal{L}\left\lbrace f(t) \right\rbrace=\bar{f}(p)
|
||
|
|
||
|
.. math ::
|
||
|
|
||
|
\mathcal{L}^{-1}\left\lbrace \bar{f}(p) \right\rbrace = f(t)
|
||
|
|
||
|
.. math ::
|
||
|
|
||
|
\bar{f}(p) = \frac{1}{(p+1)^2}
|
||
|
|
||
|
.. math ::
|
||
|
|
||
|
f(t) = t e^{-t}
|
||
|
|
||
|
>>> from mpmath import *
|
||
|
>>> mp.dps = 15; mp.pretty = True
|
||
|
>>> tt = [0.001, 0.01, 0.1, 1, 10]
|
||
|
>>> fp = lambda p: 1/(p+1)**2
|
||
|
>>> ft = lambda t: t*exp(-t)
|
||
|
>>> ft(tt[0]),ft(tt[0])-invertlaplace(fp,tt[0],method='talbot')
|
||
|
(0.000999000499833375, 8.57923043561212e-20)
|
||
|
>>> ft(tt[1]),ft(tt[1])-invertlaplace(fp,tt[1],method='talbot')
|
||
|
(0.00990049833749168, 3.27007646698047e-19)
|
||
|
>>> ft(tt[2]),ft(tt[2])-invertlaplace(fp,tt[2],method='talbot')
|
||
|
(0.090483741803596, -1.75215800052168e-18)
|
||
|
>>> ft(tt[3]),ft(tt[3])-invertlaplace(fp,tt[3],method='talbot')
|
||
|
(0.367879441171442, 1.2428864009344e-17)
|
||
|
>>> ft(tt[4]),ft(tt[4])-invertlaplace(fp,tt[4],method='talbot')
|
||
|
(0.000453999297624849, 4.04513489306658e-20)
|
||
|
|
||
|
The methods also work for higher precision:
|
||
|
|
||
|
>>> mp.dps = 100; mp.pretty = True
|
||
|
>>> nstr(ft(tt[0]),15),nstr(ft(tt[0])-invertlaplace(fp,tt[0],method='talbot'),15)
|
||
|
('0.000999000499833375', '-4.96868310693356e-105')
|
||
|
>>> nstr(ft(tt[1]),15),nstr(ft(tt[1])-invertlaplace(fp,tt[1],method='talbot'),15)
|
||
|
('0.00990049833749168', '1.23032291513122e-104')
|
||
|
|
||
|
.. math ::
|
||
|
|
||
|
\bar{f}(p) = \frac{1}{p^2+1}
|
||
|
|
||
|
.. math ::
|
||
|
|
||
|
f(t) = \mathrm{J}_0(t)
|
||
|
|
||
|
>>> mp.dps = 15; mp.pretty = True
|
||
|
>>> fp = lambda p: 1/sqrt(p*p + 1)
|
||
|
>>> ft = lambda t: besselj(0,t)
|
||
|
>>> ft(tt[0]),ft(tt[0])-invertlaplace(fp,tt[0],method='dehoog')
|
||
|
(0.999999750000016, -6.09717765032273e-18)
|
||
|
>>> ft(tt[1]),ft(tt[1])-invertlaplace(fp,tt[1],method='dehoog')
|
||
|
(0.99997500015625, -5.61756281076169e-17)
|
||
|
|
||
|
.. math ::
|
||
|
|
||
|
\bar{f}(p) = \frac{\log p}{p}
|
||
|
|
||
|
.. math ::
|
||
|
|
||
|
f(t) = -\gamma -\log t
|
||
|
|
||
|
>>> mp.dps = 15; mp.pretty = True
|
||
|
>>> fp = lambda p: log(p)/p
|
||
|
>>> ft = lambda t: -euler-log(t)
|
||
|
>>> ft(tt[0]),ft(tt[0])-invertlaplace(fp,tt[0],method='stehfest')
|
||
|
(6.3305396140806, -1.92126634837863e-16)
|
||
|
>>> ft(tt[1]),ft(tt[1])-invertlaplace(fp,tt[1],method='stehfest')
|
||
|
(4.02795452108656, -4.81486093200704e-16)
|
||
|
|
||
|
**Options**
|
||
|
|
||
|
:func:`~mpmath.invertlaplace` recognizes the following optional
|
||
|
keywords valid for all methods:
|
||
|
|
||
|
*method*
|
||
|
Chooses numerical inverse Laplace transform algorithm
|
||
|
(described below).
|
||
|
*degree*
|
||
|
Number of terms used in the approximation
|
||
|
|
||
|
**Algorithms**
|
||
|
|
||
|
Mpmath implements four numerical inverse Laplace transform
|
||
|
algorithms, attributed to: Talbot, Stehfest, and de Hoog,
|
||
|
Knight and Stokes. These can be selected by using
|
||
|
*method='talbot'*, *method='stehfest'*, *method='dehoog'* or
|
||
|
*method='cohen'* or by passing the classes *method=FixedTalbot*,
|
||
|
*method=Stehfest*, *method=deHoog*, or *method=Cohen*. The functions
|
||
|
:func:`~mpmath.invlaptalbot`, :func:`~mpmath.invlapstehfest`,
|
||
|
:func:`~mpmath.invlapdehoog`, and :func:`~mpmath.invlapcohen`
|
||
|
are also available as shortcuts.
|
||
|
|
||
|
All four algorithms implement a heuristic balance between the
|
||
|
requested precision and the precision used internally for the
|
||
|
calculations. This has been tuned for a typical exponentially
|
||
|
decaying function and precision up to few hundred decimal
|
||
|
digits.
|
||
|
|
||
|
The Laplace transform converts the variable time (i.e., along
|
||
|
a line) into a parameter given by the right half of the
|
||
|
complex `p`-plane. Singularities, poles, and branch cuts in
|
||
|
the complex `p`-plane contain all the information regarding
|
||
|
the time behavior of the corresponding function. Any numerical
|
||
|
method must therefore sample `p`-plane "close enough" to the
|
||
|
singularities to accurately characterize them, while not
|
||
|
getting too close to have catastrophic cancellation, overflow,
|
||
|
or underflow issues. Most significantly, if one or more of the
|
||
|
singularities in the `p`-plane is not on the left side of the
|
||
|
Bromwich contour, its effects will be left out of the computed
|
||
|
solution, and the answer will be completely wrong.
|
||
|
|
||
|
*Talbot*
|
||
|
|
||
|
The fixed Talbot method is high accuracy and fast, but the
|
||
|
method can catastrophically fail for certain classes of time-domain
|
||
|
behavior, including a Heaviside step function for positive
|
||
|
time (e.g., `H(t-2)`), or some oscillatory behaviors. The
|
||
|
Talbot method usually has adjustable parameters, but the
|
||
|
"fixed" variety implemented here does not. This method
|
||
|
deforms the Bromwich integral contour in the shape of a
|
||
|
parabola towards `-\infty`, which leads to problems
|
||
|
when the solution has a decaying exponential in it (e.g., a
|
||
|
Heaviside step function is equivalent to multiplying by a
|
||
|
decaying exponential in Laplace space).
|
||
|
|
||
|
*Stehfest*
|
||
|
|
||
|
The Stehfest algorithm only uses abscissa along the real axis
|
||
|
of the complex `p`-plane to estimate the time-domain
|
||
|
function. Oscillatory time-domain functions have poles away
|
||
|
from the real axis, so this method does not work well with
|
||
|
oscillatory functions, especially high-frequency ones. This
|
||
|
method also depends on summation of terms in a series that
|
||
|
grows very large, and will have catastrophic cancellation
|
||
|
during summation if the working precision is too low.
|
||
|
|
||
|
*de Hoog et al.*
|
||
|
|
||
|
The de Hoog, Knight, and Stokes method is essentially a
|
||
|
Fourier-series quadrature-type approximation to the Bromwich
|
||
|
contour integral, with non-linear series acceleration and an
|
||
|
analytical expression for the remainder term. This method is
|
||
|
typically one of the most robust. This method also involves the
|
||
|
greatest amount of overhead, so it is typically the slowest of the
|
||
|
four methods at high precision.
|
||
|
|
||
|
*Cohen*
|
||
|
|
||
|
The Cohen method is a trapezoidal rule approximation to the Bromwich
|
||
|
contour integral, with linear acceleration for alternating
|
||
|
series. This method is as robust as the de Hoog et al method and the
|
||
|
fastest of the four methods at high precision, and is therefore the
|
||
|
default method.
|
||
|
|
||
|
**Singularities**
|
||
|
|
||
|
All numerical inverse Laplace transform methods have problems
|
||
|
at large time when the Laplace-space function has poles,
|
||
|
singularities, or branch cuts to the right of the origin in
|
||
|
the complex plane. For simple poles in `\bar{f}(p)` at the
|
||
|
`p`-plane origin, the time function is constant in time (e.g.,
|
||
|
`\mathcal{L}\left\lbrace 1 \right\rbrace=1/p` has a pole at
|
||
|
`p=0`). A pole in `\bar{f}(p)` to the left of the origin is a
|
||
|
decreasing function of time (e.g., `\mathcal{L}\left\lbrace
|
||
|
e^{-t/2} \right\rbrace=1/(p+1/2)` has a pole at `p=-1/2`), and
|
||
|
a pole to the right of the origin leads to an increasing
|
||
|
function in time (e.g., `\mathcal{L}\left\lbrace t e^{t/4}
|
||
|
\right\rbrace = 1/(p-1/4)^2` has a pole at `p=1/4`). When
|
||
|
singularities occur off the real `p` axis, the time-domain
|
||
|
function is oscillatory. For example `\mathcal{L}\left\lbrace
|
||
|
\mathrm{J}_0(t) \right\rbrace=1/\sqrt{p^2+1}` has a branch cut
|
||
|
starting at `p=j=\sqrt{-1}` and is a decaying oscillatory
|
||
|
function, This range of behaviors is illustrated in Duffy [3]
|
||
|
Figure 4.10.4, p. 228.
|
||
|
|
||
|
In general as `p \rightarrow \infty` `t \rightarrow 0` and
|
||
|
vice-versa. All numerical inverse Laplace transform methods
|
||
|
require their abscissa to shift closer to the origin for
|
||
|
larger times. If the abscissa shift left of the rightmost
|
||
|
singularity in the Laplace domain, the answer will be
|
||
|
completely wrong (the effect of singularities to the right of
|
||
|
the Bromwich contour are not included in the results).
|
||
|
|
||
|
For example, the following exponentially growing function has
|
||
|
a pole at `p=3`:
|
||
|
|
||
|
.. math ::
|
||
|
|
||
|
\bar{f}(p)=\frac{1}{p^2-9}
|
||
|
|
||
|
.. math ::
|
||
|
|
||
|
f(t)=\frac{1}{3}\sinh 3t
|
||
|
|
||
|
>>> mp.dps = 15; mp.pretty = True
|
||
|
>>> fp = lambda p: 1/(p*p-9)
|
||
|
>>> ft = lambda t: sinh(3*t)/3
|
||
|
>>> tt = [0.01,0.1,1.0,10.0]
|
||
|
>>> ft(tt[0]),invertlaplace(fp,tt[0],method='talbot')
|
||
|
(0.0100015000675014, 0.0100015000675014)
|
||
|
>>> ft(tt[1]),invertlaplace(fp,tt[1],method='talbot')
|
||
|
(0.101506764482381, 0.101506764482381)
|
||
|
>>> ft(tt[2]),invertlaplace(fp,tt[2],method='talbot')
|
||
|
(3.33929164246997, 3.33929164246997)
|
||
|
>>> ft(tt[3]),invertlaplace(fp,tt[3],method='talbot')
|
||
|
(1781079096920.74, -1.61331069624091e-14)
|
||
|
|
||
|
**References**
|
||
|
|
||
|
1. [DLMF]_ section 1.14 (http://dlmf.nist.gov/1.14T4)
|
||
|
2. Cohen, A.M. (2007). Numerical Methods for Laplace Transform
|
||
|
Inversion, Springer.
|
||
|
3. Duffy, D.G. (1998). Advanced Engineering Mathematics, CRC Press.
|
||
|
|
||
|
**Numerical Inverse Laplace Transform Reviews**
|
||
|
|
||
|
1. Bellman, R., R.E. Kalaba, J.A. Lockett (1966). *Numerical
|
||
|
inversion of the Laplace transform: Applications to Biology,
|
||
|
Economics, Engineering, and Physics*. Elsevier.
|
||
|
2. Davies, B., B. Martin (1979). Numerical inversion of the
|
||
|
Laplace transform: a survey and comparison of methods. *Journal
|
||
|
of Computational Physics* 33:1-32,
|
||
|
http://dx.doi.org/10.1016/0021-9991(79)90025-1
|
||
|
3. Duffy, D.G. (1993). On the numerical inversion of Laplace
|
||
|
transforms: Comparison of three new methods on characteristic
|
||
|
problems from applications. *ACM Transactions on Mathematical
|
||
|
Software* 19(3):333-359, http://dx.doi.org/10.1145/155743.155788
|
||
|
4. Kuhlman, K.L., (2013). Review of Inverse Laplace Transform
|
||
|
Algorithms for Laplace-Space Numerical Approaches, *Numerical
|
||
|
Algorithms*, 63(2):339-355.
|
||
|
http://dx.doi.org/10.1007/s11075-012-9625-3
|
||
|
|
||
|
"""
|
||
|
|
||
|
rule = kwargs.get('method', 'cohen')
|
||
|
if type(rule) is str:
|
||
|
lrule = rule.lower()
|
||
|
if lrule == 'talbot':
|
||
|
rule = ctx._fixed_talbot
|
||
|
elif lrule == 'stehfest':
|
||
|
rule = ctx._stehfest
|
||
|
elif lrule == 'dehoog':
|
||
|
rule = ctx._de_hoog
|
||
|
elif rule == 'cohen':
|
||
|
rule = ctx._cohen
|
||
|
else:
|
||
|
raise ValueError("unknown invlap algorithm: %s" % rule)
|
||
|
else:
|
||
|
rule = rule(ctx)
|
||
|
|
||
|
# determine the vector of Laplace-space parameter
|
||
|
# needed for the requested method and desired time
|
||
|
rule.calc_laplace_parameter(t, **kwargs)
|
||
|
|
||
|
# compute the Laplace-space function evalutations
|
||
|
# at the required abscissa.
|
||
|
fp = [f(p) for p in rule.p]
|
||
|
|
||
|
# compute the time-domain solution from the
|
||
|
# Laplace-space function evaluations
|
||
|
return rule.calc_time_domain_solution(fp, t)
|
||
|
|
||
|
# shortcuts for the above function for specific methods
|
||
|
def invlaptalbot(ctx, *args, **kwargs):
|
||
|
kwargs['method'] = 'talbot'
|
||
|
return ctx.invertlaplace(*args, **kwargs)
|
||
|
|
||
|
def invlapstehfest(ctx, *args, **kwargs):
|
||
|
kwargs['method'] = 'stehfest'
|
||
|
return ctx.invertlaplace(*args, **kwargs)
|
||
|
|
||
|
def invlapdehoog(ctx, *args, **kwargs):
|
||
|
kwargs['method'] = 'dehoog'
|
||
|
return ctx.invertlaplace(*args, **kwargs)
|
||
|
|
||
|
def invlapcohen(ctx, *args, **kwargs):
|
||
|
kwargs['method'] = 'cohen'
|
||
|
return ctx.invertlaplace(*args, **kwargs)
|
||
|
|
||
|
|
||
|
# ****************************************
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
import doctest
|
||
|
doctest.testmod()
|