import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
try:
import executable_engineering as exe
except ImportError:
%pip install -q executable_engineering
import executable_engineering as exeForward Difference¶
The forward difference is so named because it starts at and looks forward to obtain derivative information.
Consider the Taylor expansion of from a point , with the step size :
Truncating the series after the first two terms:
and solving for the first derivative yields the forward difference stencil:
which is to say that:
with a linear order of convergence since it depends on .
Example: Rocket Velocity¶
A rocket has velocity:
What is the acceleration as a function of ? Analytically we can find,
def v(t):
return 2000 * np.log((14 * 10**4) / (14 * 10**4 - 2100 * t)) - 9.8 * t
def a(t):
return (42 * 10**5) / (14 * 10**4 - 2100 * t) - 9.8
t = np.linspace(0, 30, 100)
vt, at = v(t), a(t)
fig = make_subplots(rows=1, cols=2, subplot_titles=["Velocity v(t)", "Acceleration a(t)"])
fig.add_trace(go.Scatter(x=t, y=vt, name="v(t)"), row=1, col=1)
fig.add_trace(go.Scatter(x=t, y=at, name="a(t)"), row=1, col=2)
fig.update_xaxes(title_text="Time (s)")
fig.update_yaxes(title_text="Velocity (m/s)", row=1, col=1)
fig.update_yaxes(title_text="Acceleration (m/s²)", row=1, col=2)
fig.update_layout(showlegend=False, height=400, margin=dict(l=40, r=40, t=40, b=40))
fig.show()Numerical Acceleration with Forward Difference¶
We implement the forward difference stencil and approximate the rocket acceleration across step sizes :
def forward_difference(f, x, h):
return (f(x + h) - f(x)) / h
a_fd_h2 = forward_difference(v, t, h=2)
a_fd_h1 = forward_difference(v, t, h=1)
a_fd_h0p1 = forward_difference(v, t, h=0.1)
fig = go.Figure()
fig.add_trace(go.Scatter(x=t, y=a_fd_h2, mode="lines", name="FD h=2"))
fig.add_trace(go.Scatter(x=t, y=a_fd_h1, mode="lines", name="FD h=1"))
fig.add_trace(go.Scatter(x=t, y=a_fd_h0p1, mode="lines", name="FD h=0.1"))
fig.add_trace(go.Scatter(x=t, y=at, mode="lines", name="Analytic", line=dict(dash="dash", color="black")))
fig.update_layout(title="Forward Difference vs Analytic Acceleration", xaxis_title="Time (s)", yaxis_title="Acceleration (m/s²)", height=420)
fig.show()Backward Difference¶
By contrast, the backward difference steps backwards. Consider replacing with :
and solving for the derivative:
def backward_difference(f, x, h):
return (f(x) - f(x - h)) / h
a_bd_h2 = backward_difference(v, t, h=2)
a_bd_h1 = backward_difference(v, t, h=1)
a_bd_h0p1 = backward_difference(v, t, h=0.1)
fig = go.Figure()
fig.add_trace(go.Scatter(x=t, y=a_bd_h2, mode="lines", name="BD h=2"))
fig.add_trace(go.Scatter(x=t, y=a_bd_h1, mode="lines", name="BD h=1"))
fig.add_trace(go.Scatter(x=t, y=a_bd_h0p1, mode="lines", name="BD h=0.1"))
fig.add_trace(go.Scatter(x=t, y=at, mode="lines", name="Analytic", line=dict(dash="dash", color="black")))
fig.update_layout(title="Backward Difference vs Analytic Acceleration", xaxis_title="Time (s)", yaxis_title="Acceleration (m/s²)", height=420)
fig.show()Central Difference¶
Note that the approximations are approaching the analytic solution from opposite directions. Can we combine forward and backward differences to get a better result?
Let’s examine the forward and backward expansions:
Subtracting the two equations cancels the quadratic term:
Rearranging leads to the central difference formula:
which is second order accurate in since the next correction is !
def central_difference(f, x, h):
return (f(x + h) - f(x - h)) / (2 * h)
a_cd_h1 = central_difference(v, t, h=1)
a_cd_h2 = central_difference(v, t, h=2)
fig = go.Figure()
fig.add_trace(go.Scatter(x=t, y=a_bd_h2, mode="lines", name="BD h=2"))
fig.add_trace(go.Scatter(x=t, y=a_fd_h2, mode="lines", name="FD h=2"))
fig.add_trace(go.Scatter(x=t, y=a_cd_h2, mode="lines", name="CD h=2"))
fig.add_trace(go.Scatter(x=t, y=at, mode="lines", name="Analytic", line=dict(dash="dash", color="black")))
fig.update_layout(title="Central Difference vs Forward and Backward", xaxis_title="Time (s)", yaxis_title="Acceleration (m/s²)", height=420)
fig.show()Error Comparison ()¶
Let’'s compare the error in forward, backward, and central difference for across the first few time steps:
np.stack([a_fd_h2 - a(t), a_bd_h2 - a(t), a_cd_h2 - a(t)]).T[1:5, :]Error Scaling: First-Order Schemes¶
Let’'s see how error scales when halving the step size from to . For forward and backward difference, the error roughly cuts in half: , while the central difference cuts by
print("Forward difference error ratio (h=2 vs h=1):")
print(np.stack([a_fd_h2 - a(t), a_fd_h1 - a(t)]).T[1:5, :])
print("\nBackward difference error ratio (h=2 vs h=1):")
print(np.stack([a_bd_h2 - a(t), a_bd_h1 - a(t)]).T[1:5, :])
print("\nCentral difference error ratio (h=2 vs h=1):")
np.stack([a_cd_h2 - a(t), a_cd_h1 - a(t)]).T[1:5, :]Summary of First-Order Derivatives¶
The central difference algorithm:
Finds a more accurate solution in the same number of function calls.
Becomes more accurate with step size quadratically ().
Requires information both before and after the evaluation point, meaning it cannot be used at boundary endpoints without one-sided stencils.
Requires equal step size!