Tools for numerical analysis of Ordinary Differential Equations(ODE) in Python using sympy.
The goal of equaPy is to numerically solve your ODEs in an easy, compact and visually good looking way.
For this project I want to do as many thing as possible from scratch. I'm not using any python library to solve ODEs. However I'm using sympy to read ODEs from string inputs and compute gradients. This might be replaced by my own tools if I find the motivation to build them. I also might not use sympy to compute gradients in the future since I plan on using GPU acceleration.
Most of the science behind the project is from a course I took at Sorbonne Université, Paris in my 3rd year. "Analyse Numérique". You can email me if you are interested in the course's pdf. But please note that it is written in french.
For now equaPy includes :
- Definition of ODEs
- Numerical methods:
- Explicit Euler
- Modified Euler (middle point)
- Implicit Euler
- Crank-Nicolson
- Taylor
- Runge Kutta
And ODE with initial values is usualy defined this way :
where
To solve this ODE for values of
where
Then once
But this is not exactly how equaPy stores an ODE. Since there can be systems of ODEs, the ODE
class is a bit more general.
An ODE system can have many equations, not just one.
For example :
where
How to fit this system to our model ? The idea : split every high-dimension equations into many 1-dimensions equations.
In our case, it would first become like this :
Which can be written :
The process is the same for higher orders, the equation would look like something like this :
The order can also be reduced, the same way we explained earlier.
Once your system of ODEs is split into 1-dimension equations, you can feed it to the ODE
class and solve it.
When setting a 1-dimension equation of order "d<var><index>"
.
For example if i want to write "-dy1 + 2*t"
(assuming
For higher order derivatives, juste add as many d
before the variable's name.
More generally, an ODE system of dimension
Then its reduced form is :
While systems of ODEs can model a lot of objects, there are some limitations.
For example, with an ODE you can easily predict a ball's trajectory while it is in the air. But how about when it reaches the ground ? The ball could bounce. To take this phenomenon into account we can apply an update layer on the parameters at each step of the approximation.
The ODE
class stores a list of functions, called an update layer. At each step of a Scheme
's approximation algorithm, the functions of the update layer are applied to the computed value. (see example : bouncing ball 2D)
Once your ODE is ready, you can feed it to a scheme object for it to solve. The list of available methods is here.
Let's try to solve the ODE system for the brine tank cascade problem.
(source : University of Utah, Math Department)
And the equations are
To solve it with equaPy :
from lib.ode import ODE
from lib.ode_methods import ExplicitEuler
ode = ODE(3, 1, 0) # dim=3 1-dimension equations | order=1 | t0=0
ode.setinit([20, 40, 60])
ode.setsymbols("x") # arbitrary symbol
ode.setfunction(1, "-x1/2")
ode.setfunction(2, "x1/2 - x2/4")
ode.setfunction(3, "x2/4 - x3/6")
scheme = ExplicitEuler(ode) # choosing method
T, N = 50, 1000
time, values = scheme.solve(T, N)
print(values[:10])
>>> [[20. 40. 60. ]
[19.5 40. 60. ]
[19.0125 39.9875 60. ]
[18.5371875 39.96296875 59.99984375]
[18.07375781 39.92686133 59.99938216]
[17.62191387 39.87961951 59.99847308]
[17.18136602 39.82167211 59.99698104]
[16.75183187 39.75343536 59.9947771 ]
[16.33303607 39.67531321 59.99173857]
[15.92471017 39.5876977 59.98774883]]
Let's try to model the trajectory of a 2D bouncing ball inside a box. We will only consider gravity and air friction(as constants) here.
The equations : $$\left\lbrace \begin{aligned} x''(t) &= -cx'(t) \ y''(t) &= (-g - cy'(t))\frac{1}{m} &\text{where } c=0.1, m=1 \text{ and } g=9.81 \end{aligned} \right.$$
In addition we want the ball to bounce if it reaches a boundary. Our update layer will be:
def bounce2D(value):
loss = .9
if value[0] < 0.02:
value[2] *= -loss
value[0] = 0.02
elif value[0] > 0.98:
value[2] *= -loss
value[0] = 0.98
if value[1] < 0.05:
value[3] *= -loss
value[1] = 0.049
What it does is change the direction of the velocity vector on the corresponding axis when a boundary is reached. It also reduces the speed on impact. If the ball goes beyond a boundary, it's position is adjusted.
Then to solve the ODE:
frot, m, grav = 0.1, 1, 9.81
ode = ODE(2, 2, 0)
ode.setinit([0.3, 1, 2, 0])
ode.setsymbols("x")
ode.setfunction(1, f"-dx1 * {frot}")
ode.setfunction(2, f"(-{grav}-dx2 * {frot}) / {m}")
ode.add_update_layer(bounce2D)
scheme = ExplicitEuler(ode)
T = 50
N = 5000
time, values = scheme.solve(T, N)
Then with some custom animation function we obtain :
- Save/load ODEs
- Save/load solutions data
- graphical tools
- For M1 and later macs : gpu acceleration using pyTorch