r"""Finite-difference derivatives (forward/backward/central) and Richardson
extrapolation for higher accuracy.
See Burden & Faires, *Numerical Analysis*, 10th ed., Ch. 4.1
("Numerical Differentiation") for the difference formulas and their
truncation-error orders, and Ch. 4.2 ("Richardson's Extrapolation") for
combining two lower-order estimates into a higher-order one.
"""
from __future__ import annotations
from typing import Callable
import numpy as np
from mathematicskit.calculus.core.base import DerivativeResult
__all__ = ["forward_difference", "backward_difference", "central_difference", "complex_step_derivative", "richardson_extrapolation"]
[docs]
def forward_difference(f: Callable[[float], float], x: float, h: float = 1e-5) -> float:
r"""Forward-difference derivative estimate, :math:`O(h)`.
:math:`f'(x) \approx \dfrac{f(x+h) - f(x)}{h}`. See Burden & Faires,
*Numerical Analysis*, 10th ed., Ch. 4.1.
Parameters
----------
f : callable
x : float
h : float
Step size.
Returns
-------
float
Examples
--------
>>> round(forward_difference(lambda t: t**2, 3.0, h=1e-4), 3)
6.0
"""
return (f(x + h) - f(x)) / h
[docs]
def backward_difference(f: Callable[[float], float], x: float, h: float = 1e-5) -> float:
r"""Backward-difference derivative estimate, :math:`O(h)`.
:math:`f'(x) \approx \dfrac{f(x) - f(x-h)}{h}`.
Parameters
----------
f : callable
x : float
h : float
Returns
-------
float
Examples
--------
>>> round(backward_difference(lambda t: t**2, 3.0, h=1e-4), 3)
6.0
"""
return (f(x) - f(x - h)) / h
[docs]
def central_difference(f: Callable[[float], float], x: float, h: float = 1e-5) -> float:
r"""Central-difference derivative estimate, :math:`O(h^2)`.
:math:`f'(x) \approx \dfrac{f(x+h) - f(x-h)}{2h}` -- one order of
accuracy better than forward/backward differences at the same cost
(2 evaluations), since the :math:`O(h)` error terms in the underlying
Taylor expansions cancel. See Burden & Faires, *Numerical Analysis*,
10th ed., Ch. 4.1, eq. (4.4).
Parameters
----------
f : callable
x : float
h : float
Returns
-------
float
Examples
--------
>>> import numpy as np
>>> round(float(central_difference(np.sin, 0.0, h=1e-4)), 8)
1.0
"""
return (f(x + h) - f(x - h)) / (2.0 * h)
[docs]
def complex_step_derivative(f: Callable, x: float, h: float = 1e-20) -> float:
r"""Complex-step derivative estimate, :math:`O(h^2)` with no subtractive cancellation.
:math:`f'(x) \approx \operatorname{Im} f(x + ih) / h`. Because no
two nearly equal numbers are subtracted, ``h`` can be taken absurdly
small and the result is accurate to machine precision. Requires ``f``
to be real-analytic and implemented with complex-capable operations
(e.g. :mod:`numpy` ufuncs). See J. N. Lyness and C. B. Moler,
"Numerical Differentiation of Analytic Functions," SIAM Journal on
Numerical Analysis 4(2) (1967), 202-210, and W. Squire and G. Trapp,
"Using Complex Variables to Estimate Derivatives of Real Functions,"
SIAM Review 40(1) (1998), 110-112.
Parameters
----------
f : callable
Accepts a complex argument.
x : float
h : float
Imaginary step size.
Returns
-------
float
Examples
--------
>>> import numpy as np
>>> abs(complex_step_derivative(np.exp, 1.0) - np.e) < 1e-15
True
"""
return float(np.imag(f(x + 1j * h)) / h)