Automatic differentiation#
The autodiff module provides basic
forward-mode automatic differentiation routines for python functions.
Note
For Numba compatibility, these are implemented as a python
namedtuple and not a class. Numba’s jitclass does
not associate reflected operators (i.e. scalar + a Dual object)
with a class. The namedtuple approach was recommended by
the Numba developers (see Numba issue #10921).
Note
For AmrexAstroCxxNetwork, we use the C++ autodiff library.
Single derivative#
Dual
provides forward-mode automatic differentiation with one
derivative per value.
Here’s an example. We seed the independent variable with Dual(value,
1.0) (the default derivative is one):
from numba import njit
from pynucastro.autodiff.dual import Dual
@njit
def polynomial(x):
return x * x + 3.0 * x - 1.0
result = polynomial(Dual(1.3))
This gives:
Dual(value=4.590000000000001, deriv=5.6)
Dual is an immutable namedtuple with fields value and deriv.
Numba operator overloads support dual-dual arithmetic and real scalars on
either side of addition, subtraction, multiplication, and division.
Powers support a dual base and a scalar or dual exponent; a dual exponent
requires a positive base.
Important
For pure python, the standard operator functions, e.g., __add__()
are invoked for arithmetic. When compiled with Numba, separate
functions decorated with @overload provide the implementations.
Both use the same derivative formulas. Numba represents the namedtuple
as a native structure rather than a heap-allocated jitclass.
Tip
Constants can simply be written as scalars or explicitly as a
Dual with a seed of zero, i.e., Dual(value, 0.0).
Math functions#
The module’s version of math functions, exp, log, log1p,
sqrt, cbrt, sin, cos, arctan, should be used when
an argument may be a dual. These fall-back to their Numpy equivalents
for scalar input. The derivative of abs is defined as zero at zero.
Comparisons (e.g. <, >) use only the value.
Compiled two-argument min and max return a dual with a
floating-point value, even when a scalar bound wins; that bound has
zero derivative components with the same seed kind as the dual
operand. Ties select the first operand. Python’s built-in min and
max return the selected operand itself, which may be a scalar. In
other compiled branches, keep the return types compatible explicitly.
Two independent derivatives#
DerivativePair differentiates
with respect to two variables in one evaluation. Both components are
scalar fields in a namedtuple, so this does not require a NumPy
derivative array
from pynucastro.autodiff.derivative_pair import DerivativePair
x = Dual(2.0, DerivativePair(1.0, 0.0))
y = Dual(3.0, DerivativePair(0.0, 1.0))
result = x * y
This gives:
Dual(value=6.0, deriv=DerivativePair(d1=3.0, d2=2.0))
Important
All dual operands in an expression must use the same kind of seed, i.e., you cannot mix the representations:
x = Dual(2.0, DerivativePair(1.0, 0.0))
y = Dual(3.0) # defaults to scalar derivative 1.0
result = x * y # unsupported mixture
Note
Constants may be ordinary real scalars. The helper function
constant_like(x, value) creates a constant with
the same scalar or dual representation as x, including zero
derivative components. This is useful in Numba-compiled
functions that clamp values to bounds, like:
@njit
def cap(x):
if x > 10.0:
return constant_like(x, 10.0)
return x