Skip to content
The Lumenary

Math laboratory

Truncation versus rounding, and smoothness in quadrature

Measures the error of forward and central difference quotients for the derivative of sin at 1 as h shrinks from 1e-1 to 1e-13, showing the V shape where rounding error takes over; then measures trapezoid and Simpson errors for e^cos(x) on [0, 2 pi] (fast, because periodic) and for sqrt(x) on [0, 1] (slow, because not smooth at 0).

Back to the labNumerical approximation
Truncation versus rounding, and smoothness in quadraturePythonNumerical approximationComputationally tested
"""Truncation error against rounding error, and when error bounds fail.

Part 1: the forward difference (f(x + h) - f(x))/h approximates f'(x) with a
truncation error about h|f''|/2, but rounding errors in f contribute about
2u|f|/h. As h shrinks the total error first falls and then rises again, with
its minimum near h = sqrt(u) = 1e-8; the central difference does better,
with its minimum near u^(1/3) = 1e-5.

Part 2: the composite trapezoid and Simpson rules on two integrands. For the
periodic function e^(cos x) on [0, 2 pi] the trapezoid rule converges far
faster than its O(h^2) bound suggests. For sqrt(x) on [0, 1], whose
derivatives blow up at 0, both rules converge only like h^1.5: the smoothness
hypotheses of the error theorems are not met.
"""

import math

def forward_difference(f, x, h):
    """(f(x + h) - f(x)) / h."""
    return (f(x + h) - f(x)) / h

def central_difference(f, x, h):
    """(f(x + h) - f(x - h)) / (2h)."""
    return (f(x + h) - f(x - h)) / (2 * h)

def trapezoid(f, a, b, n):
    """Composite trapezoid rule with n strips."""
    h = (b - a) / n
    return h * (0.5 * f(a) + sum(f(a + i * h) for i in range(1, n)) + 0.5 * f(b))

def simpson(f, a, b, n):
    """Composite Simpson rule with n strips (n even)."""
    if n <= 0 or n % 2:
        raise ValueError("Simpson's rule needs a positive even number of strips")
    h = (b - a) / n
    odd = sum(f(a + i * h) for i in range(1, n, 2))
    even = sum(f(a + i * h) for i in range(2, n, 2))
    return h / 3 * (f(a) + 4 * odd + 2 * even + f(b))

def bessel_i0(x, terms=30):
    """Modified Bessel function I0(x) = sum (x/2)^(2k) / (k!)^2."""
    return sum((x / 2) ** (2 * k) / math.factorial(k) ** 2 for k in range(terms))

def main():
    exact = math.cos(1.0)
    print("Derivative of sin at 1: errors of difference quotients")
    print("      h     forward    central")
    for k in range(1, 14, 2):
        h = 10.0 ** (-k)
        fe = abs(forward_difference(math.sin, 1.0, h) - exact)
        ce = abs(central_difference(math.sin, 1.0, h) - exact)
        print(f"  1e-{k:02d}  {fe:9.1e}  {ce:9.1e}")
    print()
    per_exact = 2 * math.pi * bessel_i0(1.0)
    print("Trapezoid errors for e^cos(x) on [0, 2 pi] (periodic):")
    for n in (2, 4, 8, 16):
        err = abs(trapezoid(lambda t: math.exp(math.cos(t)), 0, 2 * math.pi, n)
                  - per_exact)
        print(f"  n = {n:2d}: {err:.1e}")
    print("sqrt(x) on [0, 1] (exact 2/3): errors and observed orders")
    print("    n   trapezoid   Simpson")
    prev = None
    for n in (16, 64, 256, 1024):
        te = abs(trapezoid(math.sqrt, 0, 1, n) - 2 / 3)
        se = abs(simpson(math.sqrt, 0, 1, n) - 2 / 3)
        rate = "" if prev is None else (
            f"   orders {math.log(prev[0] / te, 4):.2f},"
            f" {math.log(prev[1] / se, 4):.2f}")
        print(f"  {n:4d}  {te:9.2e}  {se:9.2e}{rate}")
        prev = (te, se)
    print()
    print("Epistemic status: floating-point measurements for a few functions.")
    print("They agree with the error analysis (truncation ~ h or h^2, rounding")
    print("~ u/h) and show that the O(h^2) and O(h^4) quadrature bounds need")
    print("bounded derivatives; they do not prove any rate.")

if __name__ == "__main__":
    main()
Runs entirely in your browser, in an isolated worker. Nothing is sent to a server.

Used in