sympy Laplace Transforms for solving linear ODEs
We've found that the Laplace transform utility in sympy version up until now (v1.9) doesn't do what we need for solving linear ODEs. So we made some needed improvements and included the code in the MATH280 Package.
- Current sympy version 1.9 and hoping for a fix soon
- laplace_transform() in sympy 1.9
- laplace() in MATH280
- Discontinuous Forcing Functions
from sympy import *
import MATH280
x = Function("x")
t,s = symbols("t s",real=True, nonnegative=true )
A famously intense pen-and-paper technique from elementary ODEs class: Laplace Transforms. Sympy v1.9 has Laplace capabilities built-in, but with maddening shortcomings. This may be addressed in an upcomming new version. I see in the pre-release notes for sympy 1.10 "The Laplace transform is now rule-based, can transform a far wider range of expressions, and works better with undefined functions, including the ability to transform differential equations. "
Let's work with a 2nd order, linear, constant-coefficient, nonhomogeous IVP: $$ \ddot{x}+3\dot{x}+4x = \sin(2t), \;\;\;\;\;\; x(0)=1, \dot{x}(0)=0$$
ode = Eq(x(t).diff(t,2)+3*x(t).diff(t)+4*x(t) , sin(2*t) )
ode
First let's see how the built-in sympy laplace_transform()
handles that equation:
laplace_transform(ode,t,s)
Look at that! 'Equality' object has no attribute. laplace_transform()
would prefer that we assume the expression we enter is equal to 0:
Lx=laplace_transform(x(t).diff(t,2)+3*x(t).diff(t)+4*x(t) - sin(2*t),t,s)
Lx
We see the output above is a tuple that includes some conditional statements at the end. To hide that and see just the transform itself, use the option noconds=True
Lx=laplace_transform(x(t).diff(t,2)+3*x(t).diff(t)+4*x(t) - sin(2*t),t,s,noconds=True)
Lx
Notice in that output another hassle: laplace_transform()
ignores the single most useful property of the Laplace Transform:
$$ {\cal L}\{x'(t)\} = s{\cal L}\{x(t)\} - x(0), \mbox{ and } {\cal L}\{x''(t)\} = s^2{\cal L}\{x(t)\} - sx(0)- x'(0).$$
We've reworked that and included an alternative laplace()
in the MATH280 Module.
L = MATH280.laplace(ode,t,s)
L
The second step in solving the equation is to plug in the initial conditions:
L0=L.subs(x(0),1).subs(Subs(Derivative(x(t), t), t, 0),0)
L0
The third step is to solve the resulting equation for the symbol ${\cal L}\{x(t)\}$
Lx=solve(L0,LaplaceTransform(x(t),t,s))
Lx
The fourth and final step is to "look up" that complicated expression in the variable $s$ to determine the function of $t$ with that Laplace transform. The built-in sympy funtion inverse_laplace_transform()
works fine for that, with the slight annoyance that it doesn't align perfectly with the list format of output from solve()
. So I've included in MATH280 a little wrapper called laplaceInv()
that extracts the zeroth entry from that list. I've noticed that this step can run really slowly.
sol = MATH280.laplaceInv(Lx,s,t)
sol
Something to notice is that every term as a $\theta(t)$. That's the unit step function: $\theta(t)=1$ if $t\gt 0$ and otherwise zero. Seems to be enforcing the assumption that our solution only makes sense for nonnegative time $t$.
We can plot the solution with sympy plot:
plot(sol,(t,0,20))
And just in case there was any doubt, we notice that dsolve()
produces the same solution.
dsolve(ode,x(t),ics={x(0):1, x(t).diff(t).subs(t,0):0})
We haven't yet explored what I think is the reason to consider Laplace Transforms in the first place: discontinuous forcing functions.
Suppose we model a capacitor with an external voltage source that switches on at $t=2$ and off at $t=5$.
The equation, in some idealized units, could be
$$\dot{x} - x = \theta(t-2) - \theta(t-5), \;\;\;\;\; x(0)=0 $$
where $\theta(t-a)$ is the Heaviside unit step function that turns on at $t=a$.
capmodel = Eq(x(t).diff(t)+x(t), Heaviside(t-2)-Heaviside(t-5))
capmodel
We'll complete the four steps as in the previous example:
L=MATH280.laplace(capmodel,t,s)
L
L0=L.subs(x(0),0)
L0
Ls=solve(L0,LaplaceTransform(x(t),t,s))
Ls
capsol=MATH280.laplaceInv(Ls,s,t)
capsol
The functional form of the solution looks a little opaque, so lets make a plot of the solution together with the right hand side function:
plot(capsol,
Heaviside(t-2)-Heaviside(t-5),
(t,0,15),)
Here's an example from the undergraduate ODE textbook by Blanchard, Devaney and Hall: $$\ddot{x} +2\dot{x}+3x= \delta(t-1)+3\delta(t-4), \;\;\;\;\; x(0)=0, \dot{x}(0)=0$$
ode2 = Eq(x(t).diff(t,2)+2*x(t).diff(t)+3*x(t),DiracDelta(t-1)+3*DiracDelta(t-4))
ode2
L=MATH280.laplace(ode2,t,s)
L
Ls=L.subs(x(0),0).subs(Subs(Derivative(x(t), t), t, 0),0)
Ls
Lx=solve(Ls,LaplaceTransform(x(t),t,s))
Lx
sol2 = MATH280.laplaceInv(Lx,s,t)
sol2
plot(sol2,(t,0,20))
We can see in the plot above how the impulses at $t=1$ and $t=4$ energize the system.