SigmaField
Mathematical Foundations for Quant Finance
0 of 256 lessons
      1. 6.5.1 The explicit scheme and stabilityConcept · 35 min
      2. 6.5.2 Implicit schemes and Crank–NicolsonConcept · 35 min
      3. 6.5.3 Practice set: finite differencesPractice set · 20 min
      4. 6.5.4 Case study: pricing a European option on a gridCase study · 35 min

Level 6, Differential equations 6.5 Finite differences for PDEs Lesson 3 of 4

Practice setLesson 6.5.3 in Level 6, Differential equations. About 20 minutes.

Practice set: finite differences

The explicit scheme

The explicit scheme and stability (lesson 6.5.1)

0 of 8 correct

  1. 1.
  2. 2.
  3. 3.
  4. 4.
  5. 5.
  6. 6.
  7. 7.
  8. 8.
How to type answers
  • Powers with ^: x^7, 2^(1/3), a^-2.
  • Multiply with * or by writing side by side: 3x^2, 2(x+1).
  • ln(x) is the natural log, log(x, 2) is log base 2, e is Euler's number, sqrt(x) is a square root.
  • sin(x), cos(x) and tan(x) take radians; write pi for π and sin(x)^2 for sin²x.
  • Your first answer to each question is saved: it counts towards your score and brings the question back for review.
  • Exact answers like ln(20)/ln(3) are best. Decimals need about 4 significant figures.

Implicit schemes and Crank–Nicolson

Implicit schemes and Crank–Nicolson (lesson 6.5.2)

0 of 8 correct

  1. 1.
  2. 2.
  3. 3.
  4. 4.
  5. 5.
  6. 6.
  7. 7.
  8. 8.
How to type answers
  • Powers with ^: x^7, 2^(1/3), a^-2.
  • Multiply with * or by writing side by side: 3x^2, 2(x+1).
  • ln(x) is the natural log, log(x, 2) is log base 2, e is Euler's number, sqrt(x) is a square root.
  • sin(x), cos(x) and tan(x) take radians; write pi for π and sin(x)^2 for sin²x.
  • Your first answer to each question is saved: it counts towards your score and brings the question back for review.
  • Exact answers like ln(20)/ln(3) are best. Decimals need about 4 significant figures.

Coding exercise

Exercise

Three schemes in one function

Write heat_scheme(u0, r, steps, scheme="cn") for ut=uxxu_t = u_{xx} on a grid, as in lessons 6.5.1 and 6.5.2. u0 holds the grid values at time 0, ends included; r is ΔtΔx2\frac{\Delta t}{\Delta x^2}. Return the grid values after steps steps, as a NumPy array.

  • scheme is "explicit", "implicit" or "cn" (Crank–Nicolson). Hold the two end values fixed at every step, and include them in the boundary terms of the implicit schemes.
  • Solve the implicit systems with scipy.linalg.solve_banded. Never change u0 itself; steps = 0 returns a copy of it.
  • Raise ValueError if u0 has fewer than 3 values or if r is not positive. Raise it if steps is not a whole number of at least 0, or scheme is not one of the three. Raise it too for the explicit scheme with r>12r > \frac{1}{2}.

The tests use the spike and the sine of the lessons and a rod whose ends are held at 0 and 1. They add a straight line that must not move, and a tent with r=50r = 50.

See the tests
import math

import numpy as np


def expect_value_error(call, why):
    try:
        call()
    except ValueError:
        return
    raise AssertionError(f"expected ValueError: {why}")


def test_implicit_spike():
    """One implicit step with r = 1 spreads the spike (0, 0, 1, 0, 0) to 1/7, 3/7, 1/7 inside"""
    u = np.asarray(heat_scheme([0, 0, 1, 0, 0], 1, 1, "implicit"))
    expect_close(u, np.array([0, 1, 3, 1, 0]) / 7, what="heat_scheme(spike, 1, 1, 'implicit')", rel=1e-9, abs_tol=1e-12)


def test_cn_spike():
    """One Crank–Nicolson step with r = 1 gives 2/7, 1/7, 2/7 inside: the middle dips"""
    u = np.asarray(heat_scheme([0, 0, 1, 0, 0], 1, 1, "cn"))
    expect_close(u, np.array([0, 2, 1, 2, 0]) / 7, what="heat_scheme(spike, 1, 1, 'cn')", rel=1e-9, abs_tol=1e-12)


def test_explicit_sine():
    """One explicit step with r = 0.25 on sin(πx) with 4 cells multiplies it by 1 − sin²(π/8)"""
    x = np.linspace(0, 1, 5)
    u0 = np.sin(math.pi * x)
    u = np.asarray(heat_scheme(u0, 0.25, 1, "explicit"))
    expect_close(u, (1 - math.sin(math.pi / 8) ** 2) * u0, what="one explicit step", rel=1e-9, abs_tol=1e-12)


def test_accuracy():
    """On 40 cells to t = 0.1 with 40 steps (r = 4), Crank–Nicolson is within 2e-4 of e^(−π²t) sin(πx) and the implicit scheme within 5e-3"""
    x = np.linspace(0, 1, 41)
    exact = math.exp(-math.pi**2 * 0.1) * np.sin(math.pi * x)
    cn = np.max(np.abs(np.asarray(heat_scheme(np.sin(math.pi * x), 4, 40, "cn")) - exact))
    im = np.max(np.abs(np.asarray(heat_scheme(np.sin(math.pi * x), 4, 40, "implicit")) - exact))
    assert cn < 2e-4, f"Crank–Nicolson error should be under 2e-4, got {cn:.2e}"
    assert 1e-3 < im < 5e-3, f"implicit error should be between 1e-3 and 5e-3, got {im:.2e}"


def test_ends_held():
    """The ends keep their values: from 0 inside with ends 0 and 1, the profile settles on the line u = x"""
    u0 = np.zeros(11)
    u0[-1] = 1
    for scheme, r, steps in [("explicit", 0.4, 250), ("implicit", 5, 20), ("cn", 2, 50)]:
        u = np.asarray(heat_scheme(u0, r, steps, scheme))
        expect_close(u[[0, -1]], np.array([0.0, 1.0]), what=f"{scheme}: the end values", abs_tol=1e-12)
        expect_close(u, np.linspace(0, 1, 11), what=f"{scheme}: the profile at t = 1", abs_tol=1e-3)


def test_line_is_steady():
    """A straight line between the end values does not change, for every scheme"""
    line = np.linspace(2, 6, 9)
    for scheme in ["explicit", "implicit", "cn"]:
        expect_close(np.asarray(heat_scheme(line, 0.5, 10, scheme)), line, what=f"{scheme} on a line", abs_tol=1e-12)


def test_large_r_stays_bounded():
    """With r = 50 the implicit scheme keeps a tent between 0 and 0.5"""
    x = np.linspace(0, 1, 21)
    u = np.asarray(heat_scheme(np.minimum(x, 1 - x), 50, 5, "implicit"))
    assert u.min() >= 0 and u.max() <= 0.5, f"values should stay between 0 and 0.5, got {u.min():.3g} to {u.max():.3g}"


def test_input_untouched():
    """Zero steps return the start unchanged, and u0 itself is never modified"""
    u0 = np.array([0.0, 0.5, 1.0, 0.5, 0.0])
    keep = u0.copy()
    expect_close(np.asarray(heat_scheme(u0, 1, 0, "cn")), keep, what="heat_scheme(u0, 1, 0)", abs_tol=0)
    heat_scheme(u0, 1, 3, "implicit")
    expect_close(u0, keep, what="u0 after the call", abs_tol=0)


def test_bad_inputs():
    """An unstable explicit r, a non-positive r, a fractional step count, too few points or an unknown scheme raise ValueError"""
    u0 = [0, 0.5, 1, 0.5, 0]
    expect_value_error(lambda: heat_scheme(u0, 0.6, 5, "explicit"), "the explicit scheme is unstable for r = 0.6")
    expect_value_error(lambda: heat_scheme(u0, 0, 5, "cn"), "r = 0 is not a time step")
    expect_value_error(lambda: heat_scheme(u0, -1, 5, "implicit"), "r must be positive")
    expect_value_error(lambda: heat_scheme(u0, 1, 2.5, "cn"), "2.5 steps is not a whole number")
    expect_value_error(lambda: heat_scheme([0, 1], 1, 1, "cn"), "two values leave no inside point")
    expect_value_error(lambda: heat_scheme(u0, 1, 1, "euler"), "'euler' is not one of the three schemes")

Free further reading

Optional. This lesson covers what you need; these add more examples and practice. All are free to read.