Improved Forward Euler - Leveraging Central Difference as an integration method.
Dr. Clément Coïc and Robin Leuering
0. Forward Euleur
If you don't remember Forward Euler, check out:
this write-up: https://shorturl.at/88eul
this animation: https://shorturl.at/7WZVw
These two are from previous posts on LinkedIn.
1. Quick review of the Central Difference method
The Central Difference Method approximates the derivative of a function by taking the average of the forward and backward differences. This provides a
more accurate estimate of the derivative compared to the Forward or Backward Difference methods. Central Difference approximates the derivative with an
error of
compared to error of
for Forward and Backward Differences.
For a function , the central difference approximation of the first derivative at a point is given by:
2
O(Δt )
O(Δt)
f (t)
x
f (t+Δt)−f (t−Δt)
′
f (t) ≈
2Δt
In practice, this means that the derivative is computed as the tangent between the previous and next points at the time of interest.
2. From difference to Integration method
For numerical integration, the future points are not known. Integration methods aim at computing the next point in time from the previous and current
points.
With that perspective, the Central Difference method can be leveraged to solve for the
point, leading to:
t + Δt
′
f (t + Δt) ≈ f (t − Δt) + 2Δt ∗ f (t)
Rewritten for the purpose of integration, it becomes:
yn+1 ≈ yn−1 + 2.h. ẏ n
Where:
is the value of a state drivative at the current time
is the value of the same state at the previous time step
is the value of the same state at the next time step
is the step size, between the two points in time
As a reminder, the Forward Euler method is expressed as:
ẏ
n
yn−1
yn+1
h
yn+1 ≈ yn + h. ẏ n
It relevant to note:
The Central Difference for Integration (CD4I) method looks similar to the Forward Euler (FE) one and yet takes the previous point as basis and the
derivative of the current point, and hence extends the extrapolation over two time steps.
The CD4I method has an error of
when Forward Euler has an error of . This means that much larger steps can be taken for the same
accuracy with the CD4I.
The CD4I requires the past point - which could be unkown at the initial point. However, taking a wrong value can lead to disturbance in the state values.
It is thus lated investigated to use the CD4I after the first time step - to have and as known values.
2
O(h )
O(h)
t0
t1
3. Application on a simple example
In [1]: import numpy as np
import matplotlib.pyplot as plt
In [2]: def system_model(y_n, time):
"""
Takes the state variable and time as input, and
returns the derivative on the same time step.
"""
y_n_dot = 2*np.pi*np.cos(2*np.pi*time)
# to simplify, there is no dependency on the state for this example
return y_n_dot
In [3]: def CD4I(y_n_1, y_n_dot, step_size):
"""
Takes values of the state y_n at the previous time step and
its derivative y_n_dot at the current time step and the step size, and
returns the value of the state at the next time step y_n1.
"""
y_n1 = y_n_1 + y_n_dot*2*step_size
# y_n_1 stands for y_{n-1}, which cannot be used as variable name
return y_n1
In [4]: def run_simulation(t_init, t_final, y_0, y__1, step_size, results):
# Storing results
time = np.arange(start=t_init, stop=t_final, step=step_size)
y = np.zeros_like(time)
# Initialize variables
y_n = y_0
y_n_1 = y__1
# Time solving
for index in range(0, len(time)):
y[index] = y_n
# Compute derivative
y_n_dot = system_model(y_n, time[index])
# Solver to compute next time step value of the state
y_n = CD4I(y_n_1, y_n_dot, step_size)
y_n_1 = y[index]
results[step_size] = (time, y)
In [5]: # Define duration of the simulation and step size
t_init = 0 # seconds
t_final = 1 # seconds
step_sizes = [0.1, 0.05, 0.01, 0.005, 0.001]
# List to iterate over and compare
results = {}
# Run simulations
for step_size in step_sizes:
# Define initial condition
y_0 = 0 #1
y__1 = np.sin(2*np.pi*(t_init - step_size))
# assigning previous value as y0 too.
run_simulation(t_init, t_final, y_0, y__1, step_size, results)
# Plot results
for step_size, (time, y) in results.items():
plt.plot(time, y, label=f'step_size={step_size}')
# Adding the analytical solution
time = np.arange(0, 1, 0.005)
plt.plot(time, np.sin(2*np.pi*time), label='Analytical solution')
# Show plot
plt.xlabel('Time')
plt.ylabel('y')
plt.legend()
plt.title('Solving the ODE using Central Difference with different step_size')
plt.show()
In [6]: # Calculate and plot errors
for step_size, (time, y) in results.items():
analytical_solution = np.sin(2*np.pi*np.array(time))
error = np.abs((analytical_solution - np.array(y)))
# we do not divide by the reference to avoid division by zero...
plt.plot(time, error, label=f'step_size={step_size}')
# Show error plot
plt.xlabel('Time')
plt.ylabel('Error (-)')
plt.legend()
plt.title('Numerical simulation error, using CD4I, for different step_size')
plt.show()
What if we don't know the previous time step at initialization?
It is here suggested to use another solver (here Forward Euler) to compute the first time step and from then on continue with CD4I.
In [7]: def forward_Euler(y_n, y_n_dot, step_size):
"""
Takes values of the state y_n and its derivative y_n_dot on a given time step and
the step size, and
returns the value of the state at the next time step y_n1.
"""
y_n1 = y_n + y_n_dot*step_size # y_n1 stands for y_{n+1}, which cannot be used as variable name
return y_n1
In [8]: def run_simulation(t_init, t_final, y_0, step_size, results):
# Storing results
time = np.arange(start=t_init, stop=t_final, step=step_size)
y = np.zeros_like(time)
# First step - variable initializations
y_n = forward_Euler(y_0, system_model(y_0, t_init), step_size)
# y_1 computed with Forward Euler
y[0] = y_0
y_n_1 = y[0]
# Time solving
for index in range(1, len(time)):
# Store variables at time step
y[index] = y_n
# Compute derivative
y_n_dot = system_model(y_n, time[index])
# Solver to compute next time step value of the state
y_n = CD4I(y_n_1, y_n_dot, step_size)
y_n_1 = y[index]
# Store results in dictionary
results[step_size] = (time, y)
In [9]: # Define initial condition
y_0 = 0 #1
# Define duration of the simulation and step size
t_init = 0 # seconds
t_final = 1 # seconds
step_sizes = [0.1, 0.05, 0.01, 0.005, 0.001] # List to iterate over and compare
results = {}
# Run simulations
for step_size in step_sizes:
run_simulation(t_init, t_final, y_0, step_size, results)
# Plot results
for step_size, (time, y) in results.items():
plt.plot(time, y, label=f'step_size={step_size}')
# Adding the analytical solution
time = np.arange(0, 1, 0.005)
#plt.plot(time, np.exp(2.*time), label='Analytical solution')
plt.plot(time, np.sin(2*np.pi*time), label='Analytical solution')
# Show plot
plt.xlabel('Time')
plt.ylabel('y')
plt.legend()
plt.title('Solving the ODE using Central Difference with different step_size')
plt.show()
Is CD4I really more accurate than Forward Euler for a given step size?
Yes - as long as the solver is in its stability region.
Let's illustrate it!
In [10]: def compare_simulations(t_init, t_final, y_0, step_size, results):
# Storing results
time = np.arange(start=t_init, stop=t_final, step=step_size)
y_FE = np.zeros_like(time)
y_CD4I = np.zeros_like(time)
y_FE[0] = y_0 # First step - variable initializations
y_CD4I[0] = y_0
y_FE_n = forward_Euler(y_0, system_model(y_0, t_init), step_size)
# y_1 computed with Forward Euler
y_CD4I_n = y_FE_n
# y_1 computed with Forward Euler for CD4I too
y_CD4I_1 = y_0
# Time solving
for index in range(1, len(time)):
# Store variables at time step
# FE
y_FE[index] = y_FE_n
y_FE_dot = system_model(y_FE_n, time[index]) # Compute derivative
y_FE_n = forward_Euler(y_FE_n, y_FE_dot, step_size) # Compute next step
# Solver to compute next time step value of the state
y_CD4I[index] = y_CD4I_n
y_CD4I_dot = system_model(y_CD4I_n, time[index]) # Compute derivative
y_CD4I_n = CD4I(y_CD4I_1, y_CD4I_dot, step_size)
y_CD4I_1 = y_CD4I[index]
# Store results in dictionary
results[step_size] = (time, y_FE, y_CD4I)
In [11]: # Define initial condition, duration of the simulation and step size
y_0 = 0
t_init = 0 # seconds
t_final = 1 # seconds
step_sizes = [0.1, 0.05, 0.01, 0.005, 0.001] # List to iterate over and compare
results = {}
# Run simulations
for step_size in step_sizes:
compare_simulations(t_init, t_final, y_0, step_size, results)
# Plot results
fig, axes = plt.subplots(len(step_sizes), 1, figsize=(8, len(step_sizes) * 3), sharex=True)
for ax, (step_size, (time, y_FE, y_CD4I)) in zip(axes, results.items()):
# Plot Forward Euler results
ax.plot(time, y_FE, label='Forward Euler', linestyle='--')
# Plot Central Difference results
ax.plot(time, y_CD4I, label='Central Difference', linestyle='-')
# Plot analytical solution
analytical_solution = np.sin(2 * np.pi * np.array(time))
ax.plot(time, analytical_solution, label='Analytical Solution', linestyle=':')
# Add labels and title
ax.set_title(f'Step size = {step_size}')
ax.set_ylabel('y')
ax.legend()
plt.xlabel('Time')
plt.tight_layout()
plt.show()
Very nice, no? :)