123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828 |
- # contributed to mpmath by Kristopher L. Kuhlman, February 2017
- 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 LaplaceTransformInversionMethods(object):
- def __init__(ctx, *args, **kwargs):
- ctx._fixed_talbot = FixedTalbot(ctx)
- ctx._stehfest = Stehfest(ctx)
- ctx._de_hoog = deHoog(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])
- (0.999999750000016, -6.09717765032273e-18)
- >>> ft(tt[1]),ft(tt[1])-invertlaplace(fp,tt[1])
- (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 three 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'*, or *method='dehoog'*
- or by passing the classes *method=FixedTalbot*,
- *method=Stehfest*, or *method=deHoog*. The functions
- :func:`~mpmath.invlaptalbot`, :func:`~mpmath.invlapstehfest`,
- and :func:`~mpmath.invlapdehoog` are also available as
- shortcuts.
- All three 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 the most robust and is therefore the default
- method. This method also involves the greatest amount of
- overhead, so it is typically the slowest of the three methods
- at high precision.
- **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','dehoog')
- 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
- 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)
- # ****************************************
- if __name__ == '__main__':
- import doctest
- doctest.testmod()
|