Laplace transform using numerical integration in Python has very poor precision
Asked Answered
D

1

8

I have written a function to compute the Laplace transform of a function using scipy.integrate.quad. It is not a very sophisticated function and currently performs poorly on the probability density function of an Erlang distribution.

I have included all my work below. I first compute the Laplace transform and then the inverse in order to compare it to the original p.d.f. of the Erlang. I use mpmath for this. The mpmath.invertlaplace is not the problem as it manages to convert the closed-form Laplace transform back to the original p.d.f. quite perfectly.

Please help me understand what the problem is with my numerical laplace transform. I get the following error but have not been able to resolve it.

IntegrationWarning: The occurrence of roundoff error is detected, which prevents the requested tolerance from being achieved. The error may be underestimated. a=0,b=np.inf,limit=limit)[0]

PLOT

enter image description here

CODE

import numpy as np
import matplotlib.pyplot as plt
import math as m
import mpmath as mp
import scipy.stats as st
from scipy.integrate import quad


def get_laplace(func,limit=10000):
    
    '''
    Returns laplace transfrom function
    '''
    
    def laplace(s):
        '''Numerical laplace transform'''
        # Seperate into real and imaginary parts
        x = np.real(s)
        y = np.imag(s)
        
        def real_func(t):
            return m.exp(-x*t)*m.cos(y*t)*func(t)
        
        def imag_func(t):
            return m.exp(-x*t)*m.sin(y*t)*func(t)
        
        real_integral = quad(real_func,
                             a=0,b=np.inf,limit=limit)[0]
        
        imag_intergal = quad(real_func,
                             a=0,b=np.inf,limit=limit)[0]
        
        return complex(real_integral,-imag_intergal)
    
    return laplace
        

def L_erlang(s,lam,k):
    '''
    Closed form laplace transform of Erlang or Gamma distribution.
    '''
    return (lam/(lam+s))**k


if __name__ == '__main__':
    
    # Setup Erlang probability density function
    k = 5
    lam = 1
    pdf = st.erlang(a=k,scale=1/lam).pdf
    
    # Laplace transforms
    Lnum = get_laplace(pdf)          # numerical approximation
    L = lambda s: L_erlang(s,lam,k)  # closed form
    
    
    # Use mpmath library to perform inverse laplace
    
    # Invserse transfrom on numerical laplace function
    invLnum = lambda t: mp.invertlaplace(Lnum,t,
                                         method='dehoog',
                                         dps=5, 
                                         degree=3)
    
    # Invserse transfrom on closed-form laplace function
    invL = lambda t: mp.invertlaplace(L,t,
                                      method='dehoog',
                                      dps=5, 
                                      degree=3)
    
    # Grid to visualise
    T = np.linspace(0.1,10,10)
    # Get values of inverse transforms
    lnum = np.array([invLnum(t) for t in T])
    l = np.array([invL(t) for t in T])
    
    # Plot
    plt.plot(T,lnum,label='from numerical laplace')
    plt.plot(T,l,label='from closed-form laplace')
    plt.plot(T,pdf(T),label='original pdf',linestyle='--')
    plt.legend(loc='best')
    plt.show()

UPDATE

After two cups of VERY strong coffee, I managed to see the obvious mistake and make the code work. It quite embarrassing actually. Have a look at this line of code:

imag_intergal = quad(real_func,
                     a=0,b=np.inf,limit=limit)[0]

Hmmm, real_func hey? So it should read:

imag_intergal = quad(imag_func_func,
                     a=0,b=np.inf,limit=limit)[0]

The one gets this lovely plot:

enter image description here

Conclusion So why go through all the trouble to numerically perform the Laplace transform of something that we have a closed form solution for. That is because the interest lies somewhere else. We do not have a closed expression for the conditional future lifetime distribution which is similar to the hazard function. Lets call it h. Then for the Erlang distribution erl = st.erlang(a=k,scale=1/lam) that has been active for tau units of time we have h = lambda t: erl.pdf(t+tau)/erl.sf(tau). This distribution can be used as a holding time in a Semi-Markov Model (SMP). To analyse the transient behaviour of the SMP one use Laplace transforms. Usually only pdfs are used but now I can use hazard functions. Its pretty cool because it means one can model transient behaviour without assuming everything is new.

Distinctive answered 21/10, 2020 at 11:28 Comment(6)
Best answer your own question and accept the answer, this way people see that this is "resolved". (Right now, the overview just shows a question with zero replies.)Puffery
@Nico Schlömer--Isn't this the the responsibility of SO? Or are we pretending SO is a public utility? "Stack Exchange, Inc. has 35 total employees across all of its locations and **generates $20.41 million in sales** (USD)." -- dnb.com/business-directory/… So manipulating folks to work for free, instead of using some of that 20M$ to hire people, seems unethical to me. Did I miss something? I'm trying to understand the moral justification. It almost seems like theft.Truism
@Truism OP asked a question, came up with an answer before anyone else did, and simply added the answer to the question. It should rather be a reply.Puffery
I agree; my point is that the OP, AFAIK, is not employed by SO. So "should" under what moral code? SO, IMHO, "should" do a lot of things. It seems if you have , inc; .com; etc. after your name then you can dictate theses "shoulds". When's my tern to re-define morality? I'd suggest if you feel strongly then post it yourself, iff that makes sense to you. :-)Truism
Hello everyone. I feel immoral towards answering my own questions and marking the as correct. I always hope that someone might come up with an alternative solution. However, I can see the purpose of answering my own question as to get it marked as resolved. I appreciate this perspective. As such, the questions is now resolved.Distinctive
Nico is right. All this talk about re-defining morality is useless nonsense. Answering and accepting your own answer is the norm here.Glomerulus
D
1

Good day. I am repeating the Updates section from the original questions as this is the solution to the questions. This way, the questions can be marked as resolved.

UPDATE

After two cups of VERY strong coffee, I managed to see the obvious mistake and make the code work. It quite embarrassing actually. Have a look at this line of code:

imag_intergal = quad(real_func,
                     a=0,b=np.inf,limit=limit)[0]

Hmmm, real_func hey? So it should read:

imag_intergal = quad(imag_func_func,
                     a=0,b=np.inf,limit=limit)[0]

The one gets this lovely plot:

enter image description here

Conclusion So why go through all the trouble to numerically perform the Laplace transform of something that we have a closed form solution for. That is because the interest lies somewhere else. We do not have a closed expression for the conditional future lifetime distribution which is similar to the hazard function. Lets call it h. Then for the Erlang distribution erl = st.erlang(a=k,scale=1/lam) that has been active for tau units of time we have h = lambda t: erl.pdf(t+tau)/erl.sf(tau). This distribution can be used as a holding time in a Semi-Markov Model (SMP). To analyse the transient behaviour of the SMP one use Laplace transforms. Usually only pdfs are used but now I can use hazard functions. Its pretty cool because it means one can model transient behaviour without assuming everything is new.

Distinctive answered 3/4, 2023 at 11:56 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.