Source code for mathematicskit.calculus.systems.dual_numbers

r"""Forward-mode automatic differentiation via dual numbers.

A dual number :math:`a + b\varepsilon` with :math:`\varepsilon^2 = 0`
propagates a function's value *and* its exact derivative together
through ordinary arithmetic: substituting :math:`x + \varepsilon` (i.e.
:math:`dx/dx = 1`) into any function built from ``+``, ``-``, ``*``,
``/``, and elementary functions and expanding via
:math:`f(a+b\varepsilon) = f(a) + b f'(a)\varepsilon` (Taylor's theorem,
truncated after the linear term since :math:`\varepsilon^2=0`) computes
:math:`f(x)` and :math:`f'(x)` in one pass with no finite-difference
truncation error. See Griewank & Walther, *Evaluating Derivatives*, 2nd
ed., Ch. 3, and Clifford (1873) for the original dual-number algebra.
"""

from __future__ import annotations

import math
from typing import Callable

__all__ = ["Dual", "derivative"]


[docs] class Dual: r"""A dual number :math:`\text{real} + \text{dual}\,\varepsilon`. Parameters ---------- real : float The value part. dual : float The derivative (tangent) part. Examples -------- >>> x = Dual(3.0, 1.0) # seed dx/dx = 1 >>> y = x * x + 2.0 * x # f(x) = x^2 + 2x, f'(x) = 2x + 2 >>> y.real, y.dual (15.0, 8.0) """ __slots__ = ("real", "dual") def __init__(self, real: float, dual: float = 0.0): self.real = float(real) self.dual = float(dual) def __repr__(self): return f"Dual({self.real!r}, {self.dual!r})" @staticmethod def _coerce(other): return other if isinstance(other, Dual) else Dual(other, 0.0) def __add__(self, other): other = self._coerce(other) return Dual(self.real + other.real, self.dual + other.dual) __radd__ = __add__ def __sub__(self, other): other = self._coerce(other) return Dual(self.real - other.real, self.dual - other.dual) def __rsub__(self, other): return self._coerce(other) - self def __mul__(self, other): other = self._coerce(other) # Product rule: (u v)' = u'v + u v' return Dual(self.real * other.real, self.dual * other.real + self.real * other.dual) __rmul__ = __mul__ def __truediv__(self, other): other = self._coerce(other) # Quotient rule: (u/v)' = (u'v - u v') / v^2 return Dual(self.real / other.real, (self.dual * other.real - self.real * other.dual) / (other.real**2)) def __rtruediv__(self, other): return self._coerce(other) / self def __neg__(self): return Dual(-self.real, -self.dual) def __pow__(self, p: float): # Power rule: (u^p)' = p u^(p-1) u' return Dual(self.real**p, p * self.real ** (p - 1) * self.dual)
[docs] def sin(self): return Dual(math.sin(self.real), math.cos(self.real) * self.dual)
[docs] def cos(self): return Dual(math.cos(self.real), -math.sin(self.real) * self.dual)
[docs] def exp(self): e = math.exp(self.real) return Dual(e, e * self.dual)
[docs] def log(self): return Dual(math.log(self.real), self.dual / self.real)
[docs] def sqrt(self): s = math.sqrt(self.real) return Dual(s, self.dual / (2.0 * s))
[docs] def derivative(f: Callable[[Dual], Dual], x: float) -> float: r"""Exact derivative of ``f`` at ``x`` via forward-mode dual numbers. Parameters ---------- f : callable A function built from ``Dual`` arithmetic and methods (``+``, ``-``, ``*``, ``/``, ``**``, ``.sin()``, ``.cos()``, ``.exp()``, ``.log()``, ``.sqrt()``), taking and returning ``Dual``. x : float Point at which to differentiate. Returns ------- float :math:`f'(x)`, exact up to floating-point roundoff (no truncation error, unlike finite differences). Examples -------- >>> f = lambda x: (x * x).sin() >>> round(derivative(f, 1.0), 10) == round(2.0 * 1.0 * math.cos(1.0 * 1.0), 10) True """ return f(Dual(x, 1.0)).dual