MTH3007b Weekly Problems 3

Original Documents: Problem Sheet / Provided Solutions

Vibes: …

Used Techniques:


Note

The original exercises for session 3 are posted on Blackboard (assessment section). The problems below cover the core session 3 content: deriving and implementing RK4, the implicit trapezoid method, and comparing their accuracy.


3.1. Implementing RK4

Question

Implement the fourth-order Runge-Kutta method (RK4) for with , , , .

RK4 is the standard four-stage explicit Runge-Kutta method. It evaluates the slope function at four points per step and combines them with weights :

This is a fourth-order method: GTE . Each halving of reduces the error by .

import numpy as np
 
forcing_coefficient = 1.0
decay_rate = 22.0
time_start = 0.0
time_end = 1.0
initial_value = 1.0
 
def g(t: float, y: float) -> float:
    return forcing_coefficient * t - decay_rate * y
 
def y_exact(t: float) -> float:
    return (
        np.exp(-decay_rate * t) * (initial_value + forcing_coefficient / decay_rate ** 2)
        + forcing_coefficient * t / decay_rate
        - forcing_coefficient / decay_rate ** 2
    )
 
def rk4_step(g, t: float, y: float, time_step: float) -> float:
    k1 = g(t, y)
    k2 = g(t + time_step / 2, y + time_step * k1 / 2)
    k3 = g(t + time_step / 2, y + time_step * k2 / 2)
    k4 = g(t + time_step, y + time_step * k3)
    return y + time_step / 6 * (k1 + 2 * k2 + 2 * k3 + k4)
 
def run_rk4(time_step: float) -> float:
    number_of_steps = int(round((time_end - time_start) / time_step))
    current_t = time_start
    current_y = initial_value
    for _ in range(number_of_steps):
        current_y = rk4_step(g, current_t, current_y, time_step)
        current_t += time_step
    return current_y
 
y_true = y_exact(time_end)
print(f"Exact y(1) = {y_true:.10f}")
print(f"{'dt':>10} {'error':>14} {'ratio':>8}")
previous_error = None
for time_step in [0.1, 0.05, 0.025, 0.0125]:
    y_numerical = run_rk4(time_step)
    error = abs(y_numerical - y_true)
    ratio = previous_error / error if previous_error is not None else float('nan')
    print(f"{time_step:>10.4f} {error:>14.6e} {ratio:>8.2f}")
    previous_error = error

The error ratio should converge to , confirming fourth-order convergence.


3.2. Implicit Trapezoid Method

Question

Derive and implement the implicit trapezoid method for with , , , .

The Implicit Trapezoid Method averages the slopes at both ends of the step:

For this becomes:

Collecting terms on the left:

Since , we have , so:

import numpy as np
 
forcing_coefficient = 1.0
decay_rate = 22.0
time_start = 0.0
time_end = 1.0
initial_value = 1.0
 
def trap_step(t: float, y: float, time_step: float) -> float:
    return (
        y * (1 - decay_rate * time_step / 2.0)
        + time_step * forcing_coefficient * (t + time_step / 2.0)
    ) / (1.0 + decay_rate * time_step / 2.0)
 
def y_exact(t: float) -> float:
    return (
        np.exp(-decay_rate * t) * (initial_value + forcing_coefficient / decay_rate ** 2)
        + forcing_coefficient * t / decay_rate
        - forcing_coefficient / decay_rate ** 2
    )
 
def run_trap(time_step: float) -> float:
    number_of_steps = int(round((time_end - time_start) / time_step))
    current_t = time_start
    current_y = initial_value
    for _ in range(number_of_steps):
        current_y = trap_step(current_t, current_y, time_step)
        current_t += time_step
    return current_y
 
y_true = y_exact(time_end)
print(f"{'dt':>10} {'error':>14} {'ratio':>8}")
previous_error = None
for time_step in [0.1, 0.05, 0.025, 0.0125]:
    y_numerical = run_trap(time_step)
    error = abs(y_numerical - y_true)
    ratio = previous_error / error if previous_error is not None else float('nan')
    print(f"{time_step:>10.4f} {error:>14.6e} {ratio:>8.2f}")
    previous_error = error

The ratio should converge to , confirming second-order convergence (the implicit trapezoid is a second-order method with unconditional stability).


3.3. Comparing Euler, RK4, and Implicit Trapezoid

Question

Compare the errors of explicit Euler, RK4, and the implicit trapezoid method for at several step sizes. What do you observe?

import numpy as np
 
forcing_coefficient = 1.0
decay_rate = 22.0
time_start = 0.0
time_end = 1.0
initial_value = 1.0
 
def g(t: float, y: float) -> float:
    return forcing_coefficient * t - decay_rate * y
 
def y_exact(t: float) -> float:
    return (
        np.exp(-decay_rate * t) * (initial_value + forcing_coefficient / decay_rate ** 2)
        + forcing_coefficient * t / decay_rate
        - forcing_coefficient / decay_rate ** 2
    )
 
def run_euler(time_step: float) -> float:
    number_of_steps = int(round((time_end - time_start) / time_step))
    current_t = time_start
    current_y = initial_value
    for _ in range(number_of_steps):
        current_y = current_y + time_step * g(current_t, current_y)
        current_t += time_step
    return current_y
 
def rk4_step(g, t: float, y: float, time_step: float) -> float:
    k1 = g(t, y)
    k2 = g(t + time_step / 2, y + time_step * k1 / 2)
    k3 = g(t + time_step / 2, y + time_step * k2 / 2)
    k4 = g(t + time_step, y + time_step * k3)
    return y + time_step / 6 * (k1 + 2 * k2 + 2 * k3 + k4)
 
def run_rk4(time_step: float) -> float:
    number_of_steps = int(round((time_end - time_start) / time_step))
    current_t = time_start
    current_y = initial_value
    for _ in range(number_of_steps):
        current_y = rk4_step(g, current_t, current_y, time_step)
        current_t += time_step
    return current_y
 
def trap_step(t: float, y: float, time_step: float) -> float:
    return (
        y * (1 - decay_rate * time_step / 2.0)
        + time_step * forcing_coefficient * (t + time_step / 2.0)
    ) / (1.0 + decay_rate * time_step / 2.0)
 
def run_trap(time_step: float) -> float:
    number_of_steps = int(round((time_end - time_start) / time_step))
    current_t = time_start
    current_y = initial_value
    for _ in range(number_of_steps):
        current_y = trap_step(current_t, current_y, time_step)
        current_t += time_step
    return current_y
 
y_true = y_exact(time_end)
step_sizes = [0.04, 0.02, 0.01, 0.005]
print(f"{'dt':>8}  {'Euler err':>14}  {'RK4 err':>14}  {'Trap err':>14}")
for time_step in step_sizes:
    euler_error = abs(run_euler(time_step) - y_true)
    rk4_error   = abs(run_rk4(time_step)   - y_true)
    trap_error  = abs(run_trap(time_step)   - y_true)
    print(f"{time_step:>8.4f}  {euler_error:>14.4e}  {rk4_error:>14.4e}  {trap_error:>14.4e}")

Expected observations:

  • Explicit Euler errors halve as halves (first order).
  • RK4 errors decrease by a factor of per halving of (fourth order) - far more accurate for the same step size.
  • Implicit trapezoid errors decrease by a factor of per halving (second order), and remain stable even for large since it is unconditionally stable.
  • At small all methods converge, but RK4 reaches machine precision fastest.