nolan (hyperjet)
Const-generic hyperdual numbers for automatic differentiation in Rust
nolan is a forward-mode automatic differentiation library built around
const-generic, stack-allocated hyperdual numbers (jets). It provides
Jet1<N> for gradients, Jet2<N, H> for full Hessians, and Jet3<N, H, T>
for third-order tensors — all without Vec or Box, all generic over the
parameter dimension N. The same function body type-checks against f64
(no derivatives), Jet1 (gradients), Jet2 (Hessians), or Jet3 (third-
order tensors). Linear algebra, statistics, grids, and angle-wrapping
primitives layered on top compose naturally with any jet order, so an
orbit-determination solve and its covariance propagation can share the
same code path.
The library was developed for astrodynamics — orbit determination, covariance propagation, close-approach sensitivity analysis — but the core types are domain-agnostic and useful anywhere exact, stack-allocated forward-mode derivatives matter: optimization, robotics inverse kinematics, physics simulation, ML gradient checking, sensitivity in scientific computing.
Installation
The crate is published on crates.io as hyperjet.
The repo and internal codename stay nolan.
[]
= "1.8.0"
use Jet1;
Internal Empyrean callers can alias the dep back to nolan so existing
use nolan::... source keeps working unchanged:
[]
= { = "hyperjet", = "1.8.0" }
use Jet1;
Types
Jet1<N>
First-order jet: tracks a value and its gradient with respect to N parameters.
use Jet1;
// Create a variable seeded at parameter index 0
let x = variable;
let y = variable;
let f = x * x + y;
assert_eq!; // 2² + 3
assert_eq!; // ∂f/∂x = 2x = 4
assert_eq!; // ∂f/∂y = 1
Jet2<N, H>
Second-order jet: tracks value, gradient, and the full Hessian matrix (stored as a lower-triangular array of size H = N(N+1)/2).
use ;
let x = hess_size }>variable;
let y = hess_size }>variable;
let f = x * x * y;
// f = x²y, value = 12
// ∂f/∂x = 2xy = 12, ∂f/∂y = x² = 4
// ∂²f/∂x² = 2y = 6, ∂²f/∂x∂y = 2x = 4, ∂²f/∂y² = 0
Jet3<N, H, T>
Third-order jet: tracks value, gradient, Hessian, and the full third-order tensor (stored as a lower-triangular array of size T = N(N+1)(N+2)/6).
use ;
let x = hess_size }, >variable;
let f = x.powi;
// f = x⁴, value = 1
// df/dx = 4x³ = 4
// d²f/dx² = 12x² = 12
// d³f/dx³ = 24x = 24
assert_eq!;
| Type | Storage (N=6) | Storage (N=9) |
|---|---|---|
| Jet1 | 56 B | 80 B |
| Jet2 | 224 B | 440 B |
| Jet3 | 672 B | 1,760 B |
Type Aliases
# use ;
type Dual = ; // Single-variable first derivative
type HyperDual = ; // Two-variable second derivatives
type HyperHyperDual = ; // Two-variable third derivatives
Traits
Differentiable— Base trait:value(),constant(),variable(). Implemented forf64,Jet1,Jet2,Jet3.FirstOrder— First derivatives:grad(i). Implemented forJet1,Jet2,Jet3.SecondOrder— Second derivatives:hess(i, j). Implemented forJet2,Jet3.ThirdOrder— Third derivatives:tens(i, j, k). Implemented forJet3.DifferentiableMath— Transcendental functions:sin,cos,tan,asin,acos,atan,atan2,sinh,cosh,tanh,exp,ln,log,sqrt,powf,powi,abs.AutoDiff— Marker combining all of the above. Use this as a generic bound.
Generic Programming
Write functions once that work with f64 (no derivatives), Jet1 (gradients),
Jet2 (Hessians), or Jet3 (third-order tensors):
use AutoDiff;
Convenience API: differentiate
For one-shot derivative computations, the differentiate module hides the
Jet seeding + extraction boilerplate:
use ;
// First derivatives
let = differentiate1;
// value = 5.25, grad = [5.0, 1.5]
// Second derivatives (6-parameter specialization avoids spelling out H)
let =
differentiate2_6;
// Third derivatives
let = differentiate3_6;
Six variants: differentiate1/differentiate2/differentiate3 for scalar
\( f: \mathbb{R}^N \to \mathbb{R} \), plus _vec variants for vector-valued
\( f: \mathbb{R}^N \to \mathbb{R}^M \) that return the full Jacobian (and
higher-order tensors stacked per output). Specialized differentiate2_6,
differentiate2_9, differentiate3_6, differentiate3_9 helpers inline the
hessian/tensor sizes for the common 6- and 9-parameter state cases.
The wrapper is essentially overhead-free — the seeding + extraction is in the
same ballpark as the compute itself (~0.4 ns overhead on a 25 ns Jet1 gravity
evaluation, see benchmark_jets.rs).
Runtime-dispatched differentiate_dyn
When the derivative order must be chosen at runtime — e.g., propagate with Jet2 covariance by default, then escalate to Jet3 for skewness diagnostics when a nonlinearity metric trips at a close-approach event — use the dispatched form:
use ;
use AutoDiff;
// Write the function body once; it works for Jet1, Jet2, or Jet3.
;
let order = if nonlinearity > threshold else ;
let d = differentiate_dyn_6;
// Uniform consumption via accessors; hessians/tensors are Option.
let jac = d.jacobian;
if let Some = d.hessians
if let Some = d.tensors
The hessian and tensor fields are boxed so the enum stays small regardless
of dispatched order — First dispatch costs only ~7 ns more than the flat
differentiate1_vec.
Linear Algebra
Stack-allocated generic matrix operations for any N:
use *;
// Solve Ax = b (Gauss-Jordan with scaled partial pivoting, NR §2.5)
let x = mat_solve.unwrap;
// Cholesky decomposition (symmetric positive-definite)
let l = mat_cholesky.unwrap; // A = L L^T
// Inverses, eigen, and decompositions
let inv = mat_inv.unwrap;
let d2 = mahalanobis_distance_squared.unwrap;
let = mat_eigenvector_max;
let = mat_symmetric_eigen.unwrap; // Jacobi
let kopp = sym_eigenvalues_3; // closed-form 3×3
let det = mat_det;
let ld = mat_log_det;
let kappa = condition_number; // κ₂(A) = σ_max / σ_min
let sigma_max = mat_largest_singular_value;
let trace_cube = mat_trace_cube; // Tr((AB)³)
// Rectangular `f64`-only primitives over const-generic shapes
let c = ; // 2×3 · 3×4 → 2×4
let at = ;
let ata = ; // Aᵀ A, 3×3
let frob = ;
Scaled partial pivoting (NR §2.5) underpins all mat_solve / mat_inv /
matN_solve / matN_inv / mat_det paths via the shared
hyperjet::linalg::NOLAN_REL_TOL (1e-14) and NOLAN_MIN_SCALE (1e-150)
constants. mat3_inv / mat3_solve use a relative determinant guard
|det| < REL_TOL · max_entry³; for marginally-conditioned 3×3 inputs
prefer mat_inv::<3> (carries the scaled pivot).
Stack-allocated specialised fast paths for 3×3, 6×6, and 9×9 matrices
(mat3_*, mat6_*, mat9_*) — generic over T: DifferentiableMath
so they work with f64 and Jet types alike.
Covariance regularization (hyperjet::linalg::regularize)
use *;
// Project a possibly-indefinite covariance onto the PSD cone with a
// minimum eigenvalue floor (Higham 1988).
let report = .unwrap;
println!;
// Tikhonov damping: A + αI.
let damped = ;
// Tikhonov with before/after κ₂ — surfaces under-damping at the call site.
let report = ;
if report.condition_number_after > 1e10
The RegularizationReport<N> and TikhonovReport<N> structs are
#[must_use] — callers cannot silently drop the diagnostic fields.
Optimization
Generic nonlinear least-squares solver (Gauss-Newton / Levenberg-Marquardt):
use *;
let solution = solve_nlls.unwrap;
println!;
For stateful problems, implement the NLLSProblem<N> trait:
let solution = solve?;
Features: LM adaptive damping, Bayesian prior augmentation, second-order
Hessian correction (solve2), formal covariance extraction, optional
problem-driven step bounds.
Statistics
hyperjet::statistics::distributions ships the scalar-input distribution
primitives:
use ;
let p = chi2_sf; // χ² survival
let z = / sigma;
let prob = normal_cdf;
hyperjet::statistics::multivariate ships N-dimensional Gaussian primitives
generic over the state dimension:
use ;
// Canonical 2N+1 unscaled Julier-Uhlmann sigma points: sample_statistics
// over the returned set round-trips exactly to (mu, cov).
let points = .unwrap;
let = .unwrap;
// Merwe scaled unscented transform with a tunable spread (alpha, beta,
// kappa). Propagate `sp.points` through a map, then reconstruct the
// transformed moments with weighted_sample_statistics; for an affine map
// this recovers (mu, cov) exactly.
let sp = .unwrap;
let =
.unwrap;
// Equal-weight Gaussian mixture decomposition along a chosen direction
// (DeMars-style uniform spacing; preserves the mixture mean and
// covariance for any K ≥ 1).
let components = .unwrap;
Grids
NumPy-semantics endpoint-inclusive grid generators and a clamping linear interpolator:
use ;
let lin = linspace; // [0.0, 0.1, ..., 1.0]
let log = logspace; // log-spaced over 6 decades
let y = linear_clamped; // clamped at boundaries
linear_clamped<T> is generic over T: Copy + Add<Output = T> + Mul<f64, Output = T> —
so it works for scalar values and user-defined types that impl those traits.
Angles
use ;
let residual = wrap_pi; // half-open (-π, π]
let lon = wrap_2pi; // [0, 2π)
let dec_deg = wrap_180; // (-180°, 180°]
Version
println!;
// Tagged release: "1.0.0"
// Development: "1.0.1-dev+a3f7b2c"
// Dirty: "1.0.1-dev+a3f7b2c-dirty"