Numerical differentiation estimates from evaluations of . It is useful when an analytic derivative is unavailable, but it is more sensitive to floating-point rounding than evaluating itself.
Choosing a method
Use an analytic or automatic derivative when available; use complex-step for complex-safe analytic functions; use a central difference for a simple robust estimate; and use Richardson extrapolation when higher accuracy justifies more evaluations.
Forward difference
The derivative is defined by
For a small nonzero step , use the forward-difference approximation
Taylor expansion gives
therefore
Its truncation error is : halving approximately halves this error.
Smaller is not always better
When becomes very small, and are nearly equal. Their subtraction loses significant digits, and division by amplifies rounding error.
import numpy as np
def forward_difference(f, x, h):
return (f(x + h) - f(x)) / hComplex-step differentiation
If has an analytic extension to complex arguments, evaluate it at a purely imaginary displacement:
Taking the imaginary part yields
so . There is no subtraction of nearly equal numbers, so can often be extremely small (for example in double precision), giving near machine-precision derivatives.
Applicability
Complex-step differentiation requires a complex-safe, analytic implementation of . It can fail for
abs, comparisons or branching based on the input,max,min, and code that discards imaginary parts.
def complex_step(f, x, h=1e-20):
return np.imag(f(x + 1j * h)) / h
Richardson extrapolation
Central differences have an error expansion containing only even powers of . Write the unknown derivative as and the base approximation as :
Using half the step changes each error term predictably:
The leading error has improved from to . This is Richardson extrapolation: combine two estimates with known error scaling to eliminate the leading error term.
For the next step, start with and . Their leading errors scale as and , so the same cancellation uses :
where is the first-order extrapolation built from and . The result has error .
In general, define the first column by
and use the recurrence
Here is the extrapolation level. Since has leading error proportional to , its error differs by a factor from the corresponding entry in the preceding row. The recurrence removes that term, so has error .
The first entries form a triangular table:
Rounding-error limit
Extrapolation removes truncation error, but increasingly small steps eventually amplify floating-point error.
def richardson_derivative(f, x, h0=1.0, rtol=1e-12, atol=1e-12, max_levels=30):
"""Central differences accelerated by Richardson extrapolation."""
estimates = []
for level in range(max_levels):
h = h0 / (2 ** level)
row = [central_difference(f, x, h)]
for k in range(1, level + 1):
factor = 4 ** k
row.append((factor * row[k - 1] - estimates[level - 1][k - 1]) / (factor - 1))
estimates.append(row)
if level and abs(row[-1] - estimates[level - 1][-1]) < max(rtol * abs(row[-1]), atol):
return row[-1], h
return estimates[-1][-1], h0 / (2 ** (max_levels - 1))