Gradient checks¶
treams_rs.testing compares first-order derivatives with central finite
differences, using NumPy only. check_pullback checks a record's reverse mode,
check_pushforward checks its forward mode and adjoint pairing, and
check_gradient checks a scalar objective. All return None on success and raise an
error that names the input on disagreement. Finite differences serve only as
this check; every record computes its gradients analytically in Rust.
Records¶
Pass a record: a function returning (output, context), whose
context.pullback(...) returns one gradient per input, in the order of the
inputs. The pullback maps the gradient of a loss with respect to the output,
called the cotangent, back to these input gradients; the
glossary defines both
terms. A pullback function can replace the context. Keep fixed labels and
options in a closure.
import numpy as np
from treams_rs import diff
from treams_rs.testing import check_pullback
matrix = np.array([[2.0 + 0.1j, 0.2], [-0.1j, 1.7]])
incident = np.array([[0.3 + 0.2j], [0.5]])
check_pullback(diff.solve, matrix, incident)
# The Bessel order is fixed; only z is differentiated.
check_pullback(lambda z: diff.bessel([0, 1, 2], z), np.asarray(0.8 + 0.3j))
check_pullback draws one random direction per input and random cotangents
from seed=0. Each input is checked separately, so errors in different
inputs cannot cancel. Repeat with other seeds to probe other directions. The
record runs once at the given inputs and twice per input for the central
differences. The first context's pullback is called once with the selected
output cotangent.
Forward mode¶
check_pushforward moves all dynamic inputs together along one direction and
compares every output tangent with a central difference. A fresh context then
checks the real adjoint identity between the JVP and VJP:
Re(vdot(cotangent, JVP)) = Re(vdot(VJP, direction)), summed across inputs and
outputs. This checks the complex pairing as well as the derivative values.
import numpy as np
from treams_rs import diff
from treams_rs.testing import check_pushforward
matrix = np.array([[2.0, 0.2], [0.1, 1.5]], dtype=complex)
incident = np.array([[0.3], [0.7]], dtype=complex)
d_matrix = np.array([[0.1j, 0.2], [-0.1, 0.05j]])
d_incident = np.array([[0.2], [-0.1j]])
check_pushforward(diff.solve, matrix, incident, directions=(d_matrix, d_incident))
The record must provide both pushforward and pullback. The check makes
four record calls: the JVP, two perturbed values and a fresh VJP. Each derivative
context is called once. Omit directions and cotangents for probes drawn
from seed=0; at least one direction must be nonzero. Repeat across directions
and points that stay inside the differentiable physical domain. No full
Jacobian is built.
Scalar objectives¶
For a real scalar objective, pass a function that returns its gradient. It may come from a framework adapter or from an independent analytic formula:
import numpy as np
from treams_rs.testing import check_gradient
def energy(coefficients):
return np.vdot(coefficients, coefficients).real
def gradient(coefficients):
return 2 * coefficients
check_gradient(energy, gradient, np.array([0.3 + 0.2j, -0.1j]))
Complex gradients follow the pairing of the records, also for real objectives of complex inputs such as squared norms. JAX and HIPS Autograd pair complex numbers without the conjugate: pass the conjugate of their raw gradient. The gradient function runs once per check. For several inputs, return a tuple with one gradient per input; a list counts as one array gradient.
Explicit probes and errors¶
import numpy as np
from treams_rs.testing import check_pullback
def square_record(z):
return z * z, lambda cotangent: 2 * z.conj() * cotangent
check_pullback(
square_record,
np.asarray(0.4 + 0.3j),
directions=(np.asarray(1j),),
cotangents=np.asarray(1.0 + 0j),
step=1e-6,
rtol=1e-5,
atol=1e-7,
)
directions is always a tuple with one item per input, each of that input's
shape. Inputs must be float64 or complex128 arrays or scalars. Real inputs
require real directions; a complex gradient of a real input counts with its
real part.
For tuple outputs, pass a tuple of cotangents; the pullback then takes one cotangent per output. For a single output, pass one cotangent of its shape. Outputs are one array or a flat tuple, and the check covers first derivatives only.
A mismatch reports the input index and shape, the analytic directional
derivative, the finite difference, the absolute error, the tolerance, the step
and the seed. Shape errors name the gradient or output. The check passes when
the error is at most atol + rtol * abs(finite_difference); this bounds the
agreement, not the accuracy of either value. Choose a step that suits the scale
of the input, keep both perturbed inputs inside the physical domain, and expect
poor conditioning near resonances and thresholds. A pass holds for the checked
directions at one point only. Functions must be deterministic and must leave
their inputs unchanged.
Testing describes how the test suite uses these checks.