xad-rs
Exact automatic differentiation for Rust — forward-mode, reverse-mode, first- and second-order, with named variable support for ergonomic gradient readback.
Unofficial Rust port of the C++ XAD library. Not affiliated with the upstream project.
Choosing a mode
(1) Program your numerical code against the trait [Real].
(2) Instantiate it with the concrete mode that matches your problem shape.
use *;
// Same body, four call sites:
let v_passive = quadratic; // no AD
let v_jet1 = quadratic; // forward 1st order
let v_jet2 = quadratic; // forward 1st + 2nd
// Reverse mode: instantiate inside an active Tape.
The forward-mode types are named after k-jets — Taylor-coefficient
bundles from differential geometry. The number is the order, and the
Vec suffix means the bundle is propagated against a multi-variable
tangent vector (full gradient / full Hessian) rather than a single
seeded direction.
| Type | Mode | Order | Best for |
|---|---|---|---|
f64 |
none (passive) | 0 | no derivatives needed |
Jet1<T> |
Forward, single direction | 1st | 1 input direction, many outputs |
Jet1Vec |
Forward, multi-var | 1st | full gradient in one pass |
Jet2<T> |
Forward, single direction | 1st + 2nd | diagonal Hessian / gamma |
Jet2Vec |
Forward, multi-var (dense) | 1st + 2nd | full n × n Hessian, n ≲ 50 |
AReal<T> + Tape |
Reverse (adjoint) | 1st | many inputs, scalar output |
In v0.5.0, Real is implemented for f64, AReal<f64>, Jet1<f64>, and
Jet2<f64>. Jet1Vec / Jet2Vec impls and the f32 flavours are tracked
as follow-up changes (length-of-partial-vector and lossy-From<f64> issues
respectively).
Every mode also has a named variant (NamedJet1Vec, NamedTape, etc.)
that lets you read gradients by variable name instead of positional index.
Installation
[]
= "0.6"
MSRV: 1.85 (Rust edition 2024).
Migration from 0.4 / 0.5 / 0.6
From 0.4 → 0.5
- Renamed trait:
xad_rs::Scalar→xad_rs::Passive. - New trait:
xad_rs::Real— the unified active-scalar trait. - New prelude:
use xad_rs::prelude::*;. - Bug fix:
math::norm_pdf√2magnitude error fixed.
From 0.5 → 0.6 (breaking changes)
- Removed: the deprecated alias
xad_rs::Scalar. Usexad_rs::Passivedirectly. - Trait shape:
xad_rs::Realno longer requiresDefaultas a supertrait. Generic code that wrotewhere R: Defaultshould add the bound explicitly (the inherited bound is gone). - Added:
xad_rs::RealStats— extension trait providingerf,erfc,norm_cdf,inv_norm_cdfforf64,AReal<f64>, andJet1<f64>. UseR: RealStatsfor option-style pricers. - Added:
Tape::activate_guard()returning an RAIITapeGuard<T>whoseDropdeactivates the tape. The existingTape::activate()/Tape::deactivate_all()APIs remain available.
From 0.6 → 0.7 (breaking changes)
This release trims the public surface to items with at least one in-repo caller and unblocks the build. Every removal below has a recommended replacement.
- Removed C++ XAD-compat alias modules at the crate root:
xad_rs::adj,xad_rs::adjf,xad_rs::fwd,xad_rs::fwdf. Replace with the underlying types directly:adj::TapeType→Tape<f64>adj::ActiveType→AReal<f64>adj::PassiveType→f64adjf::*/fwdf::*— analogous f32 typesfwd::ActiveType→Jet1<f64>(note:xad_rs::math::fwd, the forward-mode AD math module, is unrelated and stays)
- Removed unused
Tapemethods:Tape::push_statement— useTape::push_unary/Tape::push_binary(the fixed-arity hot-path equivalents that the crate already uses everywhere internally).Tape::compute_adjoints_to(pos)— no equivalent. Use the standardTape::compute_adjointsto sweep the whole tape; the partial-sweep path had no callers and is dropped.Tape::clear_derivatives_after(pos)— no equivalent (no callers).Tape::increment_adjoint(slot, v)— no equivalent (no callers).- The pair
Tape::get_position+Tape::reset_to(pos)is kept — that rollback flow is still supported.
- Removed AD-wrapped admin functions that propagated no derivative
information and only shadowed
f64::*methods:xad_rs::math::ad::is_nan,is_infinite,is_finite,is_normal,signum,floor,ceil,round,trunc,fract. Replace withx.value().is_nan()(etc.). - Removed
Jet2Vec::symmetrize—Jet2Vecops produce structurally symmetric Hessians; if a downstream user accumulates a Hessian outside theJet2Vechot path, averageHandH.t()directly withndarray. - Removed
Jet2Vec::powd— for an activeJet2Vecexponent, route throughexp(y * ln(x))explicitly (y.clone() * x.clone().ln()).exp()). - Removed
impl From<i32> for AReal<f32>—Realis implemented only forAReal<f64>so the f32 conversion was unconstrained. UseAReal::new(value as f32). - Build hygiene:
Cargo.tomlno longer declares the 5 dead[[bench]]targets and thecriteriondev-dependency is gone. The README's benchmarking section is also removed. - Internal refactor (no public behavior change): the four
ref-permutation impls per binary
+/-/*//onAReal<T>are now stamped viaimpl_areal_binop!macros, mirroring the existing__named_areal_binop!andimpl_unary_math!patterns. All operator paths produce identical numerical results. - Examples:
examples/hessian.rsnow uses the exactcompute_full_hessianhelper (Jet2Vecbased, machine-precision) instead of the finite-differencecompute_hessian. The finite-difference helper is still in the public API for consumers who specifically want it.
From 0.7 → 0.8 (breaking changes)
This release consolidates the named-forward AD path: one lifecycle for
every named-forward type, type-generic vec types, and a uniform
declare_* naming. Every removal/rename below has a recommended
replacement.
-
Unified named-forward lifecycle. The eager-construction half of
NamedForwardTapeis gone; everything now goes through declare → into_scope:- Removed
NamedForwardTape::input_jet1<T>,NamedForwardTape::constant_jet1<T>,NamedForwardTape::freeze(),NamedForwardTape::is_frozen(), the tape-sideNamedForwardTape::registry()accessor. - Replaced by:
NamedForwardTape::<T>::declare_jet1(name, value)returning aJet1Handle<T>, thentape.into_scope()to obtain aNamedForwardScope<T>, thenscope.jet1(handle)for the value.scope.constant_jet1(value)produces a constant;scope.registry()is the new accessor.
Before:
let mut ft = new; let x: = ft.input_jet1; let _registry = ft.freeze; let f = &x * &x + &x;After:
let mut ft = new; let x_h = ft.declare_jet1; let scope = ft.into_scope; let x = scope.jet1; let f = x * x + x; - Removed
-
NamedForwardTapeandNamedForwardScopeare now generic overT: Passive(withT = f64default). A single tape/scope picks one underlying scalar type for the whole problem.Jet1Handle<T>,Jet1VecHandle<T>, andJet2Handle<T>are typed so the wrong scope cannot be indexed with a foreign-T handle. Existing call sites that used the f64-only path keep working without turbofish thanks to the default; explicit<f64>(or other T) only needed if a literal is ambiguous. -
Jet1VecandJet2Vecare now generic overT: Passive(withT = f64default).NamedJet1Vec<T>similarly. The genericity opens the f32-mode AD path for multi-variable forward mode that already existed for the single-directionJet1<T>/Jet2<T>siblings. Most call sites are unchanged; ambiguous-literal cases (e.g.Jet1Vec::variable(2.0, 0, 2)with no other constraint) need an explicit_f64literal suffix or: Jet1Vec<f64>annotation. -
declare_jet2_f64renamed todeclare_jet2(and now generic overT). Same forconstant_jet2_f64→constant_jet2. The_f64suffix existed because the previous tape stored onlyf64; with the type-generic tape it's a misnomer. -
Internal:
forward_tape.rswas rewritten as a mono-T builder. The TLS generation guard (check_genin debug builds) is unchanged; the cross-registry-op debug panic still catches mixing values from different tape scopes.
Quick start
Reverse mode
use ;
let mut tape = new;
tape.activate;
let mut x = new;
let mut y = new;
register_input;
register_input;
// f(x, y) = x^2 * y + sin(x)
let mut f = & * &y + sin;
register_output;
f.set_adjoint;
tape.compute_adjoints;
println!; // 2xy + cos(x)
println!; // x^2
Forward mode (full gradient)
use Jet1Vec;
let = ;
let f = & * &y; // x^2 * y
assert_eq!; // df/dx = 2xy
assert_eq!; // df/dy = x^2
Second-order derivatives
use Jet2;
let x = variable;
let y = x * x * x; // x^3
assert_eq!; // 3x^2
assert_eq!; // 6x
Named variables
Access derivatives by name — useful in financial models with many risk factors:
use ;
let mut ft = new;
let spot_h = ft.declare_jet1vec;
let strike_h = ft.declare_jet1vec;
let scope: = ft.into_scope;
let spot = scope.jet1vec;
let strike = scope.jet1vec;
let ratio = spot / strike;
assert!;
Named reverse mode returns gradients as IndexMap<String, f64>:
use NamedTape;
let mut tape = new;
let x = tape.input;
let y = tape.input;
let _registry = tape.freeze;
let f = & * &y + x.sin;
let grad = tape.gradient;
assert!;
assert!;
Jacobian and Hessian
use ;
// f: R^2 -> R^2, f(x, y) = [x*y, x + y]
let jac = compute_jacobian_rev;
// g: R^2 -> R, g(x, y) = x^2 * y + y^3
let hess = compute_hessian;
Dense full Hessian (Jet2Vec)
use Jet2Vec;
let x = variable;
let y = variable;
let f = & + &;
assert_eq!; // d2f/dx2 = 2y
assert_eq!; // d2f/dxdy = 2x
assert_eq!; // d2f/dy2 = 6y
Per-op cost is O(n^2). For n > ~50, prefer seeded Jet2<T> with n passes.
Crate structure
src/
real.rs The unified active-scalar trait Real
passive.rs The passive-scalar bound Passive (f32, f64) — was scalar.rs
prelude.rs Real, Passive, AReal, Jet1, Jet2, Tape, TapeStorage
forward/ Jet1, Jet1Vec, Jet2, Jet2Vec + Named wrappers
reverse/ AReal, NamedAReal, NamedTape
ops/ compute_jacobian_*, compute_hessian, compute_full_hessian
math.rs AD-aware transcendentals (sin, exp, erf, norm_cdf, ...)
tape.rs Reverse-mode tape and thread-local active-tape slot
registry.rs VarRegistry — ordered name-to-index map
forward_tape.rs NamedForwardTape / NamedForwardScope setup
Examples
| Example | What it demonstrates |
|---|---|
swap_pricer.rs |
30-input IRS DV01 and gamma via reverse, Jet1Vec, and Jet2 |
fx_option.rs |
Garman-Kohlhagen FX option Greeks |
fixed_rate_bond.rs |
YTM / duration / convexity |
jacobian.rs |
4x4 Jacobian (reverse mode) |
hessian.rs |
4x4 Hessian with analytic cross-check |
adjoint_first_order.rs |
First-order adjoint mode — full 4-input gradient in a single reverse sweep |
fwd_adj_second_order.rs |
Forward-over-adjoint 2nd order — gradient + Hessian row via Jet2Vec |
Design notes
- Tape storage is thread-local. One
Tape<T>per thread;NamedTapeis!Send. - Forward mode is allocation-light.
Jet1Veckeeps tangents in a singleVec<f64>with fused, autovectorizable loops. - Zero-alloc operator fast paths. Every
ARealbinary op uses fixed-arityTape::push_binary/push_unary— no intermediateVecper op.
Tests
165 tests covering operator correctness, transcendentals, second-order derivatives, Jacobian/Hessian helpers, named variable readback, and cross-mode consistency.
License
AGPL-3.0-or-later, matching the upstream XAD project. See LICENSE.md.
Acknowledgements
- The C++ XAD library — architectural inspiration and source of the financial examples.
- QuantLibAAD — XAD-instrumented QuantLib build; reference for the AAD-on-quant-finance patterns the financial examples in this crate are modelled after.
num-traitsfor generic scalar plumbing.