Systems of Equations
A system of differential equations is a collection of equations involving unknown functions $u_0,\dots,u_{N-1}$ and their derivatives. The dimension of a system is the number $N$ of unknown functions. The order of the system is the highest order derivative appearing in the collection of equations. Every system of differential equations is equivalent to a first order system in a higher dimension.
import numpy as np
import scipy.integrate as spi
import matplotlib.pyplot as plt
First Order Systems
Every system of differential equations is equivalent to a first order system in a higher dimension. For example, consider a second order differential equation
$$ ay'' + by' + cy = F(t) $$
where $a,b,c$ are constants (with $a \ne 0$) and $F(t)$ is a known function. The unknown function $y(t)$ is second order in the equation therefore we introduce two new variables $u_0 = y$ and $u_1 = y'$ and write the 1-dimensional second order equation as a 2-dimensional first order system
\begin{align} u_0' &= u_1 \\ u_1' &= (F(t) - b u_1 - c u_0)/a \end{align}
The procedure for any system is similar:
- Identify the order of each unknown function in the system.
- If $y$ has order $n$ then introduce $n$ new variables $u_0 = y,u_1=y',\dots,u_{n-1}=y^{(n-1)}$.
- Rewrite the equations in terms of the new variables only.
For example, consider the system
\begin{align} x' &= x + y \\ y'' &= xy' + y \end{align}
Introduce new variables $u_0 = x, u_1 = y, u_2 = y'$ and rewrite the system
\begin{align} u_0' &= u_0 + u_1 \\ u_1' &= u_2 \\ u_2' &= u_0 u_2 + u_1 \end{align}
Vector Notation
An $N$-dimensional first order system of differential equations is of the form
\begin{align} u_0' &= f_0(t,u_0,\dots,u_{N-1}) \\ u_1' &= f_1(t,u_0,\dots,u_{N-1}) \\ & \ \ \vdots \\ u_{N-1}' &= f_{N-1}(t,u_0,\dots,u_{N-1}) \end{align}
Write the system in vector notation
$$ \frac{d \mathbf{u}}{dt} = \mathbf{f}(t,\mathbf{u}) $$
where
$$ \mathbf{u} = \begin{bmatrix} u_0 \\ \vdots \\ u_{N-1} \end{bmatrix} \hspace{10mm} \frac{d \mathbf{u}}{dt} = \begin{bmatrix} u_0' \\ \vdots \\ u_{N-1}' \end{bmatrix} \hspace{10mm} \mathbf{f}(t,\mathbf{u}) = \begin{bmatrix} f_0(t,u_0,\dots,u_{N-1}) \\ \vdots \\ f_{N-1}(t,u_0,\dots,u_{N-1}) \end{bmatrix} $$
Euler's Method
How do we apply Euler's method to a first order system of equations? Simply apply the method to each unknown function in the system.
Second Order Equations
Consider a second order differential equation with constant coefficients
$$ ay'' + by' + cy = F(t) \ , \ y(0)=y_0 \ , \ y'(0)=v_0 $$
Apply Euler's method to both $y$ and $y'$ simultaneously:
\begin{align} y_{n+1} &= y_n + y_n' h \\ y_{n+1}' &= y_n' + y_n'' h \end{align}
where
$$ y_n'' = (F(t_n) - cy_n - by_n')/a $$
For example, consider the equation $y'' + y = 0$, $y(0)=1$, $y'(0)=0$. We know the exact solution is $y(t) = \cos(t)$. Compute the approximation by Euler's method and compare with the exact solution.
y0 = 1; v0 = 0;
t = np.linspace(0,2*np.pi,100)
y = np.zeros(len(t))
y[0] = y0
dy = np.zeros(len(t))
dy[0] = v0
for n in range(0,len(t)-1):
h = t[n+1] - t[n]
y[n+1] = y[n] + dy[n]*h
dy[n+1] = dy[n] - y[n]*h
plt.plot(t,y,'b',t,np.cos(t),'r'), plt.grid(True)
plt.title("$y'' + y = 0 , y(0) = 1 , y'(0) = 1$")
plt.legend(["Euler","Exact"])
plt.show()
Implementation
Consider a first order system in vector notation
$$ \frac{d \mathbf{u}}{dt} = \mathbf{f}(t,\mathbf{u}) $$
The formula for Euler's method is almost exactly the same as for scalar equations
$$ \mathbf{u}_{n+1} = \mathbf{u}_n + \mathbf{f}(t_n,\mathbf{u}_n) h $$
where $\mathbf{u}_n$ is the vector of values at step $n$
$$ \begin{bmatrix} u_{0,n} \\ \vdots \\ u_{N-1,n} \end{bmatrix} $$
such that $u_{k,n} \approx u_k(t_n)$ at step $n$ for $k=0,\dots,N-1$.
Write a function called odeEuler
which takes input parameters f
, t
and u0
where:
f
is a function which represents the right side of the equation $\mathbf{u}' = \mathbf{f}(t,\mathbf{u})$t
is a 1D NumPy arrayu0
is an intial value $\mathbf{u}(t_0)=\mathbf{u}_0$ where $t_0$ is the valuet[0]
The function odeEuler
returns a 2D NumPy array U
of size (len(t),len(u0))
with values $u_k(t_n)$ in column $k$ and row $n$.
def odeEuler(f,t,u0):
U = np.zeros((len(t),len(u0)))
U[0,:] = u0
for n in range(0,len(t)-1):
h = t[n+1] - t[n]
k1 = f(t[n],U[n,:])
U[n+1,:] = U[n,:] + k1*h
return U
Test the function odeEuler
on the example $y'' + y = 0, y(0)=1, y'(0)=0$ and compare to the exact solution $y(t) = \cos(t)$.
def f(t,u):
dudt = np.zeros(2)
dudt[0] = u[1]
dudt[1] = -u[0]
return dudt
t = np.linspace(0,2*np.pi,100)
u0 = [1,0]
U = odeEuler(f,t,u0)
plt.plot(t,U[:,0],'b',t,np.cos(t),'r'), plt.grid(True)
plt.title("$y'' + y = 0 , y(0) = 1 , y'(0) = 1$")
plt.legend(["Euler","Exact"])
plt.show()
scipy.integrate.odeint
The function scipy.integrate.odeint
works the same way as our funciton odeEuler
above except the order of $t$ and $\mathbf{u}$ is reversed
$$ \frac{d \mathbf{u}}{dt} = \mathbf{f}(\mathbf{u},t) $$
For example, use odeint
to approximate the solution of the second order equation
$$ y'' + y = 0 \ , \ \ y(0)=1 \ , \ \ y'(0)=0 $$
and compare to the exact solution. The result is much more accurate than Euler's method!
def f(u,t):
dudt = np.zeros(2)
dudt[0] = u[1]
dudt[1] = -u[0]
return dudt
t = np.linspace(0,2*np.pi,50)
u0 = [1,0]
U = spi.odeint(f,u0,t)
plt.plot(t,U[:,0],'b.',t,np.cos(t),'r'), plt.grid(True)
plt.title("$y'' + y = 0 , y(0) = 1 , y'(0) = 1$")
plt.legend(["odeint","Exact"])
plt.show()
Applications
Mass-Spring-Damper System
A mass-spring-damper system is a second order equation with constant coefficients:
$$ my'' + cy' + ky = F(t) $$
where $m > 0, c \geq 0, k \geq 0$, and $F(t)$ is the forcing function. Write the equation as a 2-dimensional first order system by introducing new variables $u_0 = y$ and $u_1 = y'$:
\begin{align} u'_0 &= u_1 \\ u'_1 &= (F(t)-ku_0-cu_1)/m \end{align}
Write a function called mass_spring_damper
which takes input parameters m
, c
, k
, F
, u0
and t
where
m
,c
,k
are coefficients of the systemF
is the forcing function $F(t)$u0
is the vector of initial conditionsu0 = [y(0), y'(0)]
t
is a 1D NumPy array
The function returns an array y
of values approximating the solution $y(t)$ at the $t$ values given by t
.
def mass_spring_damper(m,c,k,F,u0,t):
def f(u,t):
dudt = np.zeros(2)
dudt[0] = u[1]
dudt[1] = (F(t) - k*u[0] - c*u[1])/m
return dudt
U = spi.odeint(f,u0,t)
y = U[:,0]
return y
If $m=1$, $c=0$, $k=1$, $f(t)=0$, $y(0)=0$, $y'(0)=1$ then the equation is
$$ y'' + y = 0 $$
for $y(0)=0$, $y'(0)=1$ and the solution is
$$ y=\sin(t) $$
m = 1; c = 0; k = 1;
F = lambda t: 0
t = np.linspace(0,4*np.pi,100)
u0 = [0,1]
y = mass_spring_damper(m,c,k,F,u0,t)
plt.plot(t,y), plt.grid(True)
plt.show()
Resonance occurs when there's no damping $c=0$ and the forcing frequency is equal to the natural frequency $\omega_n = \sqrt{k/m}$.
m = 1; c = 0; k = 10;
F = lambda t: np.sin(10**0.5*t)
t = np.linspace(0,50,1000)
u0 = [0,0]
y = mass_spring_damper(m,c,k,F,u0,t)
plt.plot(t,y), plt.grid(True)
plt.show()
Beats describes the behaviour of an undamped system $c=0$ when the forcing frequency is near but not equal to the natural frequency $\omega_n = \sqrt{k/m}$.
m = 1; c = 0; k = 10;
F = lambda t: np.sin(3*t)
t = np.linspace(0,100,1000)
u0 = [0,0]
y = mass_spring_damper(m,c,k,F,u0,t)
plt.plot(t,y), plt.grid(True)
plt.show()
In a damped system $c \not= 0$ there cannot be resonance however there is a forcing frequency which produces the largest steady state amplitude. This frequency is called the damped natural frequency (or practical resonance).
m = 1; c = 2; k = 10;
F = lambda t: np.sin(2.7*t)
t = np.linspace(0,15,200)
u0 = [0,0]
y = mass_spring_damper(m,c,k,F,u0,t)
plt.plot(t,y), plt.grid(True)
plt.show()
Van der Pol Oscillator
The Van der pol oscillator equation is given by
$$ x'' - \mu(1 - x^2)x' + x = 0 $$
Write a function called van_der_pol
which takes parameters mu
, u0
and t
and returns a 2D NumPy array of size (len(t),2)
with $x$ values in the first column and $x'$ values in the second column for the solution with initial values u0 = [x(0),x'(0)]
.
def van_der_pol(mu,u0,t):
def f(u,t):
dudt = np.zeros(2)
dudt[0] = u[1]
dudt[1] = mu*(1 - u[0]**2)*u[1] - u[0]
return dudt
U = spi.odeint(f,u0,t)
x = U[:,0]
dxdt = U[:,1]
return np.column_stack((x,dxdt))
Plot $x$ versus $x'$ for the solution with $\mu = 1$, $x(0) = 1$ and $x'(0)=4$.
U = van_der_pol(1,[1,4],np.linspace(0,20,500))
plt.plot(U[:,0],U[:,1]), plt.grid(True)
plt.show()
Plot $x$ versus $x'$ for $x(0) = 1$ and $x'(0)=4$ for each value $\mu = 1,2,3,4$.
for mu in [1,2,3,4]:
U = van_der_pol(mu,[1,4],np.linspace(0,10,500))
plt.plot(U[:,0],U[:,1])
plt.title("Van der pol equation")
plt.grid(True), plt.legend(['$\mu=1$','$\mu=2$','$\mu=3$','$\mu=4$'])
plt.show()
Planetary Orbits
Given an object with mass $M$, the magnitude of the force of gravity it exerts on another object with mass $m$ is
$$ F = \frac{GMm}{d^2} $$
where $d$ is the distance between the objects. Let's consider the trajectory of a planet of mass $m$ as it orbits a star of mass $M$ (fixed in space). Let's use the following units:
- solar mass (multiples of the mass of Earth's Sun)
- years
- astronomical units (approximately the distance from the Earth to the Sun)
With these units, the gravitational constant $G$ is $4 \pi^2$.
Let $\mathbf{p} = (x,y)$ be the position of the orbiting planet with the star fixed at the origin $(0,0)$, and let $m_P$ be the mass of the planet and let $m_S$ be the mass of the star. Starting with Newton's Second Law, we have
$$ m_P \frac{d^2 \mathbf{p}}{dt^2} = \mathbf{F} $$
where $\mathbf{F}$ is the vector of total force acting on the planet (which is only due to gravity in this case). Newton's law of gravity states
$$ \mathbf{F} = - \frac{G m_S m_P}{ || \mathbf{p} ||^2 } \frac{\mathbf{p}}{ || \mathbf{p} || } $$
where $|| \mathbf{p} || = \sqrt{x^2 + y^2}$ is distance between the star and the planet, and $-\mathbf{p} / || \mathbf{p} ||$ is the unit vector which points in the direction from the planet to the star.
Putting these equations together, we obtain a 2-dimensional system of second-order differential equations
\begin{align} \frac{d^2 x}{dt^2} &= - \frac{G m_S x }{ (x^2 + y^2)^{3/2} } \\ \frac{d^2 y}{dt^2} &= - \frac{G m_S y }{ (x^2 + y^2)^{3/2} } \end{align}
Write the system as a first order system by introducing new variables: $u_0 = x$, $u_1 = x'$, $u_2 = y$ and $u_3 = y'$. The system becomes
\begin{align} u_0' &= u_1 \\ u_1' &= - \frac{G m_S u_0 }{ (u_0^2 + u_2^2)^{3/2} } \\ u_2' &= u_3 \\ u_3' &= - \frac{G m_S u_2 }{ (u_0^2 + u_2^2)^{3/2} } \end{align}
Plot the orbit for $m_S = 1$ with initial conditions $x(0) = 1, x'(0) = 0, y(0) = 0, y'(0) = 7$.
G = 4*np.pi**2; mS = 1;
def f(u,t):
dudt = np.zeros(4)
dudt[0] = u[1]
dudt[1] = -G*mS*u[0]/(u[0]**2 + u[2]**2)**(3/2)
dudt[2] = u[3]
dudt[3] = -G*mS*u[2]/(u[0]**2 + u[2]**2)**(3/2)
return dudt
u0 = [1,0,0,7]
t = np.linspace(0,2,1000)
U = spi.odeint(f,u0,t)
plt.plot(U[:,0],U[:,2],0,0,'r*')
plt.axis('equal'), plt.grid(True)
plt.show()
Euler's 3-Body Problem
Euler's 3-body problem is a simplified (and admittedly physically impossible) version of the general 3-body problem. Euler's problem considers two stars fixed in space and a planet orbiting the stars in 2 dimensions. We will derive the equations of motion of the planet and then plot trajectories using SciPy's ODE solver odeint
.
Use the following units:
- solar mass (multiples of the mass of Earth's Sun)
- years
- astronomical units (AU) (approximately the distance from the Earth to the Sun)
With these units, the gravitational constant is $G = 4 \pi^2$. Introduce variables for the planet and the stars:
- $m_{S_1}$ - mass of star 1
- $m_{S_2}$ - mass of star 2
- $m_P$ - mass of the planet
- $x_{S_1}$ - (fixed) $x$-position of star 1
- $y_{S_1}$ - (fixed) $y$-position of star 1
- $x_{S_2}$ - (fixed) $x$-position of star 2
- $y_{S_2}$ - (fixed) $x$-position of star 2
- $x_P$ - $x$-position of the planet
- $y_P$ - $y$-position of the planet
- $\mathbf{p} = (x_P,y_P)$ - position vector of the planet
Let $\mathbf{F_1}$ be the force of gravity of star 1 acting on the planet, and let $\mathbf{F_2}$ be the force of gravity of star 2 acting on the planet. Newton's Law of Gravity states:
$$ \mathbf{F_1} = - \frac{ G m_P m_{S_1} }{ || \mathbf{d_1} ||^2} \frac{ \mathbf{d_1} }{ || \mathbf{d_1} || } $$
$$ \mathbf{F_2} = - \frac{ G m_P m_{S_2} }{ || \mathbf{d_2} ||^2} \frac{ \mathbf{d_2} }{ || \mathbf{d_2} || } $$
where $\mathbf{d_1} = (x_P-x_{S_1},y_P-y_{S_1})$ is the vector from star 1 to the planet, and $\mathbf{d_2} = (x_P-x_{S_2},y_P-y_{S_2})$ is the vector from star 2 to the planet.
Newton's Second Law of Motion states:
$$ \frac{ d^2 \mathbf{x} }{ dt^2 } = \mathbf{F}_1 + \mathbf{F}_2 $$
and this leads us to the system of second order ODEs which govern the motion of the planet:
$$ \frac{d^2 x_P}{dt^2} = - \frac{ G m_{S_1} (x_P - x_{S_1}) }{ || \mathbf{d_1} ||^3} - \frac{ G m_{S_2} (x_P - x_{S_2}) }{ || \mathbf{d_2} ||^3} $$ $$ \frac{d^2 y_P}{dt^2} = - \frac{ G m_{S_1} (y_P - y_{S_1}) }{ || \mathbf{d_1} ||^3} - \frac{ G m_{S_2} (y_P - y_{S_2}) }{ || \mathbf{d_2} ||^3} $$
To plot trajectories of the planet using odeint
, we first need to write the system as a first order system. Introduce new variables $u_1 = x_P$, $u_2 = x_P'$, $u_3 = y_P$ and $u_4 = y_P'$ and write
\begin{align} u_1' &= u_2 \\ u_2' &= - \frac{ G m_{S_1} (u_1 - x_{S_1}) }{ || \mathbf{d_1} ||^3} - \frac{ G m_{S_2} (u_1 - x_{S_2}) }{ || \mathbf{d_2} ||^3} \\ u_3' &= u_4 \\ u_4' &= - \frac{ G m_{S_1} (u_3 - y_{S_1}) }{ || \mathbf{d_1} ||^3} - \frac{ G m_{S_2} (u_3 - y_{S_2}) }{ || \mathbf{d_2} ||^3} \end{align}
where $\mathbf{d_1} = (u_1-x_{S_1},u_3-y_{S_1})$ and $\mathbf{d_2} = (u_1-x_{S_2},u_3-y_{S_2})$.
G = 4*np.pi**2 # Gravitational constant
S1 = [-2,0] # Coordinates of Star 1
S2 = [2,0] # Coordinates of Star 2
M1 = 2 # Mass of Star 1 (in solar mass)
M2 = 1 # Mass of Star 2 (in solar mass)
def f(u,t):
d1 = np.linalg.norm([u[0] - S1[0],u[2] - S1[1]])
d2 = np.linalg.norm([u[0] - S2[0],u[2] - S2[1]])
dudt = [0,0,0,0]
dudt[0] = u[1]
dudt[1] = -G*M1*(u[0] - S1[0])/d1**3 - G*M2*(u[0] - S2[0])/d2**3
dudt[2] = u[3]
dudt[3] = -G*M1*(u[2] - S1[1])/d1**3 - G*M2*(u[2] - S2[1])/d2**3
return dudt
u0 = [0,5,3,0] # Initial conditions of the planet: [xposition,xvelocity,yposition,yvelocity]
t = np.linspace(0,30,2000) # Array of time values (in years)
u = spi.odeint(f,u0,t) # Solve system: u = [xposition,xvelocity,yposition,yvelocity]
plt.plot(u[:,0],u[:,2]) # Plot trajectory of the planet
plt.plot(S1[0],S1[1],'ro',markersize=5*M1) # Plot Star 1 as a red star
plt.plot(S2[0],S2[1],'ro',markersize=5*M2) # Plot Star 2 as a red star
plt.axis('equal')
plt.show()
Let's put the code above into a function that we can call with different initial conditions to see what kinds of orbits we can create!
def euler3body(S1,S2,M1,M2,u0,tf,numpoints=1000):
# Define the vector function on the right side of the system of the equations
def f(u,t):
d1 = np.linalg.norm([u[0]-S1[0],u[2]-S1[1]])
d2 = np.linalg.norm([u[0]-S2[0],u[2]-S2[1]])
dudt = [0,0,0,0]
dudt[0] = u[1]
dudt[1] = -G*M1*(u[0]-S1[0])/d1**3 - G*M2*(u[0]-S2[0])/d2**3
dudt[2] = u[3]
dudt[3] = -G*M1*(u[2]-S1[1])/d1**3 - G*M2*(u[2]-S2[1])/d2**3
return dudt
t = np.linspace(0,tf,numpoints) # Array of time values (in years)
u = spi.odeint(f,u0,t) # Solve system: u = [xposition,xvelocity,yposition,yvelocity]
plt.plot(u[:,0],u[:,2]) # Plot trajectory of the planet
plt.plot(S1[0],S1[1],'ro',markersize=5*M1) # Plot Star 1 as a red star
plt.plot(S2[0],S2[1],'ro',markersize=5*M2) # Plot Star 2 as a red star
plt.axis('equal')
plt.show()
euler3body([-1,0],[1,0],2,1,[0,5,3,0],30)
euler3body([-1,1],[1,-1],2,1,[0,10,0,5],30)
euler3body([-2,0],[2,0],1,1,[0,0,0,5],5)
euler3body([-2,0],[2,0],1.5,2.5,[0,4.8,3,0],20)
euler3body([0,10],[0,-10],4.9,4.9,[0,np.pi,0,np.pi],100)
Exercises
Under construction