viva_math/autodiff
Forward-mode automatic differentiation via dual numbers.
A dual number a + b·ε (where ε² = 0) carries both a value and an
infinitesimal derivative. Applying f to a Dual(x, 1) yields
Dual(f(x), f'(x)) — exact gradients without symbolic
manipulation or finite-difference truncation error.
When to use
- You need an exact gradient at a point.
- The function is composed from arithmetic ops +
exp/log/sin/cos/.... - Cost of evaluating
fonce with duals ≈ 2-3× the regular cost.
For Vec3 gradients (gradient of a scalar field over PAD space), use
Dual3 which carries three partials in parallel.
Example
import viva_math/autodiff as ad
// f(x) = sin(x²)
let x = ad.var(2.0)
let result = ad.sin(ad.mul(x, x))
// result.value ≈ sin(4) ≈ -0.756
// result.tangent = 2x · cos(x²) ≈ -2.614
References
- Pearlmutter & Siskind (2008) “Reverse-Mode AD in a Functional Framework”
- Wengert (1964) “A simple automatic derivative evaluation program”
- JAX
jax.jvp, Zygote.jl, Stan’s Math library
Types
A dual number value + tangent·ε.
value carries the function evaluation; tangent carries the directional
derivative w.r.t. the independent variable.
pub type Dual {
Dual(value: Float, tangent: Float)
}
Constructors
-
Dual(value: Float, tangent: Float)
A dual number carrying three parallel partials. Use this to compute the
gradient ∇f of a scalar field f: ℝ³ → ℝ at a point in one evaluation.
3-dimensional dual carrying a value and three partial derivatives
(∂/∂x, ∂/∂y, ∂/∂z) — used by gradient3 to evaluate ∇f in one pass.
pub type Dual3 {
Dual3(
value: Float,
partial_x: Float,
partial_y: Float,
partial_z: Float,
)
}
Constructors
-
Dual3( value: Float, partial_x: Float, partial_y: Float, partial_z: Float, )
Values
pub fn add_scalar(a: Dual, s: Float) -> Dual
Add a constant — the tangent is unchanged (d/dx (x + c) = 1).
pub fn div(a: Dual, b: Dual) -> Dual
(a + a'ε) / (b + b'ε) = a/b + ((a'b − ab')/b²)ε — quotient rule.
pub fn gelu(a: Dual) -> Dual
GELU activation. GELU(x) = x·Φ(x) where Φ is the standard normal CDF.
d/dx GELU(x) = Φ(x) + x·φ(x), with φ the standard normal PDF.
pub fn grad(f: fn(Dual) -> Dual, x: Float) -> Float
Compute f’(x) at a point by lifting x to a dual.
pub fn gradient3(
f: fn(Dual3, Dual3, Dual3) -> Dual3,
x: Float,
y: Float,
z: Float,
) -> #(Float, Float, Float)
Gradient ∇f at point (x, y, z).
pub fn jacobian(
f: fn(List(Dual)) -> List(Dual),
point: List(Float),
) -> List(List(Float))
Generic n-d Jacobian via column-by-column forward AD. Given a function
f: ℝⁿ → ℝᵐ represented as fn(Dual, ..., Dual) -> List(Dual), returns
each row of the Jacobian by sweeping the unit tangent through each input.
This is the canonical forward-mode strategy: O(n) evaluations of f for
a full Jacobian, optimal when n ≤ m.
pub fn lift1(
d: Dual,
f: fn(Float) -> Float,
df: fn(Float) -> Float,
) -> Dual
Lift any unary function f together with its derivative f' to a dual.
pub fn mul(a: Dual, b: Dual) -> Dual
(a + a'ε)(b + b'ε) = ab + (a'b + ab')ε — Leibniz product rule on duals.
pub fn mul3(a: Dual3, b: Dual3) -> Dual3
Product on Dual3 — Leibniz rule applied component-wise to each partial.
pub fn relu(a: Dual) -> Dual
ReLU activation. Derivative is 1 for x > 0 and 0 for x ≤ 0
(subgradient choice at the kink).
pub fn scale(a: Dual, s: Float) -> Dual
Multiplication by a scalar constant (no tangent on the scalar).
pub fn value_and_grad(
f: fn(Dual) -> Dual,
x: Float,
) -> #(Float, Float)
Compute both f(x) and f’(x) in a single pass.
pub fn var(value: Float) -> Dual
A dual number representing an independent variable.
var(x) ↔ x + 1·ε so that ∂x/∂x = 1.