All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.15.0 - 2026-07-11
BytecodeTape::validate(and therefore deserialization of serialized tapes) is stricter:InputandConstentries must carry the unused arg sentinel in both operand slots, and a payload whosenum_variablesfield disagrees withopcodes.len()is rejected. Tapes produced byrecord/record_multior thepush_*builders are unaffected.- The AD value-type constructors (
Dual::new/constant/variable,DualVec::new/constant/with_tangent,Reverse::constant/from_tape,BReverse::constant/from_tape) are now#[must_use], matching theTapeconstructors; discarding their results warns.
- The deprecated (since 0.5.0) CUDA-specific STDE wrappers
stde_gpu::laplacian_gpu_cudaandstde_gpu::hessian_diagonal_gpu_cuda— the genericstde_gpu::laplacian_gpu/stde_gpu::hessian_diagonal_gpuwork with both backends. - The deprecated (since 0.5.0) inherent
CudaContext::taylor_forward_2nd_batch— import theGpuBackendtrait and call the trait method of the same name. TaylorDynGuard::arenaandJetPlan::multi_indices, unused accessors.- The unused optional
bumpalodependency (the bytecode tape is Vec-backed; builds with thebytecodefeature no longer pull it in).
- The never-constructed
SolverDiagnostics::Othervariant (the enum is#[non_exhaustive], so matches already need a wildcard arm). - The unused accessors
TapeObjective::tape,SparseImplicitContext::nnz, andSparseImplicitContext::fx_nnz(fz_nnzremains). - The
linalgandconvergencemodules are no longer public; they were internal solver plumbing.ConvergenceParamsremains re-exported at the crate root.
- The generated GPU Taylor kernels route
Powf/Powithrough the guardedpowf_realhelper instead of raw WGSLpow(which naga lowers toexp2(y*log2(x))):0^0now returns 1 and negative bases with integer exponents evaluate with the correct sign, matching the CPU paths and the static shaders.
0.14.1 - 2026-07-08
Coordinated release: echidna 0.14.1 and echidna-optim 0.14.1.
- Corrects the 0.14.0 security note about RUSTSEC-2026-0097 (
rand0.8.5 unsoundness): the advisory is reachable not only through dev-dependencies but also in the published dependency graph when the optionalfaerfeature is enabled (faer→num-complex→rand ^0.8). No fixed 0.8.x release exists anywhere in that chain (num-complex0.4.6, the latest, requiresrand ^0.8), so no version bump can clear it; the advisory's trigger — a custom logger interacting with rand's global RNG — is not exercised by echidna, faer's numerics, or the benchmarks. The audit-configuration rationale is corrected accordingly and the ignore will be removed whennum-complexmoves torand0.9.
- The
ad_traitbenchmark dev-dependency is refreshed to 0.3.1.
0.14.0 - 2026-07-08
Coordinated release: echidna 0.14.0 and echidna-optim 0.14.0.
echidna-optim's dep on echidna updated from 0.13.0 to 0.14.0.
- RUSTSEC-2026-0097 (
rand0.8.5 unsoundness) is documented as ignored in the audit configuration: it reaches the workspace only through thead_traitdev-dependency (comparison benchmarks), is not part of either published crate's dependency graph, and has no fixed 0.8.x release. The ignore is removed whenad_traitmoves torand0.9.
BytecodeTape::forward_tangent_dual2: aDual<Dual<F>>forward sweep that routes custom operations throughCustomOp::eval_dualandCustomOp::partials_dual, carrying exact first- and second-order information through custom ops at the current evaluation point. The existingDual<F>counterpartforward_tangent_dualis now public as well.
line_search::backtracking_armijo_with_evals:backtracking_armijowith an evaluation accumulator that survives the failure paths, so callers accounting for total objective work can include the evaluations a failed search spent.backtracking_armijonow delegates to it and is unchanged.
implicit_hvpandimplicit_hessiannow compute exact second-order derivatives through residual tapes containing custom operations (for ops that implementeval_dual/partials_dual). Previously the nested pass linearized custom ops to first order around recording-time primals, silently dropping their curvature — and, away from the recording point, evaluating the linearization at stale primals. Ops relying on the trait's default dual implementations still contribute constant partials, but now at the current evaluation point.piggyback_tangent_step(and everything built on it, includingpiggyback_tangent_solve) now evaluates custom operations' primals and tangents at the current step point viaCustomOp::eval_dual. Previously custom ops were linearized around recording-time primals, so both the stepped state and its tangent acquired O(distance-from-recording-point) errors on tapes containing custom ops.lbfgsandnewtonresults terminating withLineSearchFailednow include the failed line search's objective evaluations infunc_evals; previously those evaluations were silently dropped from the reported count.
- STDE estimators no longer panic on a non-finite sample: samples whose
estimator value is NaN or Inf (singular jets, overflowed higher-order
coefficients) are skipped,
num_samplesnow reports the finite (contributing) sample count across every estimator entry point, and an estimator left with zero contributing samples reports a NaN estimate instead of a confident 0. Results for all-finite runs are unchanged.estimate_weightednow panics on negative or NaN weights (West's algorithm requires non-negative weights); zero-weight directions still count as samples. diagonal_kth_order_constnow enforces the samek ≤ 18factorial bound as the dynamic path (previously unguarded —ORDER ≥ 20silently lost exactness), recovers the primal for zero-input (constant) tapes likehessian_diagonal(previously returned 0), and its precision docs match the actual guards (f32 bound isORDER ≤ 13, not 14; the f64 factorial bound is 18, not 20).
-
The wgpu forward kernel's
signumnow tests NaN by bit inspection like its sibling shaders; the bare self-inequality test could be folded away under Metal fast-math, misclassifying NaN inputs. (CUDA is compiled IEEE-strict and is unaffected.) -
Reverse sweeps on every backend (bytecode, eager, tangent-carrying second-order, wgpu, CUDA) now follow the zero-multiplier convention: an exactly-zero partial absorbs any adjoint, so an Inf/NaN adjoint arriving from a chained singularity (e.g.
sqrt'(0) = Inf) no longer turns the gradients of non-participating inputs into NaN —hypot/atan2at the origin, kink losing branches, and multiplications by a zero operand all contribute exactly 0. NaN partials from out-of-domain points are not zero and still propagate. This completes the long-documented zero-adjoint skip; the full structural-zero convention (forward and reverse, all mechanisms) is now written down once in the kernels module documentation. -
Max/Minkink entries now record the first-wins branch label when the second operand is NaN, matching the value and partials paths: forced-sign Clarke sweeps attribute the gradient by this label, and the stale label sent it to the NaN operand. -
mixed_partialon a zero-input (constant) tape returns the constant in both slots (∂⁰f = f) instead of panicking; theDiffOpconvenience constructors (laplacian,biharmonic,diagonal) now panic with a clear message forn == 0(a zero-variable operator specification is malformed, unlike a zero-input tape) instead of building a silently invalid empty operator; and the factorial-extraction degradation beyond18!(log-domain,+Infsaturation) is documented and pinned. -
taylor_powfat a zero base now follows the branch-point convention oftaylor_sqrt/taylor_cbrton every backend (CPU, wgpu, CUDA): with a non-integer exponentb0, coefficients of orderk < b0are exactly 0 and ordersk > b0are+Inf; a negative exponent gives anInfprimal. Previously the jet was a finite primal beside NaN derivative coefficients. A live (non-constant) integer exponent at a zero base now yields a consistent all-NaN jet. -
taylor_div's primal is now the correctly roundeda₀ / b₀(a single division) instead ofa₀ · (1/b₀); higher coefficients keep the reciprocal-multiply recurrence. -
Laurent::normalizeno longer rebases the pole order when every surviving coefficient is subnormal: leading exact zeros in a globally collapsed (flushed-to-zero) series are plausibly underflow artifacts, and shifting would misattribute the pole order. Structural leading zeros ahead of normally scaled coefficients rebase as before. -
Laurent
Add/Subdocument their pole-order-gap panic (# Panics) and why the gap is a loud structural failure whereMul/Divreturn a NaN series. -
Reverse::hypotat the origin no longer records a zero-partial tape node: it returns a tape-free constant (mirroringatan2's origin short-circuit and forward mode's structural-zero convention), so a non-finite adjoint arriving from downstream — e.g. throughsqrt'(0) = Inf— no longer turns both gradients into NaN. -
The faer sparse wrappers (
sparsity_to_faer_symmetric,solve_sparse_cholesky_faer,solve_sparse_lu_faer) now returnNoneon a pattern/values length mismatch instead of panicking, matching theirOptioncontracts. -
Nested second-order tangents (
Dual<Dual<F>>,DualVec<Dual<F>, N>) are no longer dropped by the zero-tangent guards inrecipand thepowfconstant-integer fast path. The guards now inspect the whole tangent rather than only its primal component, so second derivatives such asd²/dx² 1/(x²+1)atx = 0(previously0) are now correct (-2), and.recip()agrees with the1/gspelling. -
powfno longer produces a NaN derivative when the primal overflows under a constant exponent (e.g.x^2.5atx = 1e200now yields the representable2.5e300), and reverse mode now matches the other AD modes at an infinite base (Infderivative instead of NaN). -
The generated GPU Taylor kernels (wgpu and CUDA) now match the CPU jets in four cases that previously diverged silently:
fractuses the truncation convention (fract(-2.3)=-0.3, previously+0.7);hypotwith leading zeros at any depth expands correctly (hypot(t², 0)=t², previously an[0, Inf, …]jet) and a NaN operand yields an all-NaN jet;powiwith a zero base uses binary exponentiation for every positive exponent (previously exponents above 8 produced NaN coefficients instead of zeros). The1/krecurrence weights in generated kernels now carry fullf64precision (previously 10 decimal digits, a ~1e-10 relative error on fourth-order CUDAf64coefficients), and CUDA jet-buffer indexing is 64-bit clean for very large tapes. -
The wgpu Hessian-vector-product kernel now computes the
hypotprimal with the overflow-safe rescaled form used by every other kernel (previouslysqrt(a² + b²), which overflowed toInffor operands above ~1.8e19 and zeroed the resulting gradient), routesmax/minadjoints past NaN operands with the bit-pattern NaN test used elsewhere (a barex != xcan be folded away under fast-math shader compilers), and shares the division-tangent factoring of the other kernels. The wgpu%operator's domain (exact only for quotient ratios below 2^24; WGSL has no exactfmod) is now documented on the backend. -
grad_checkpointed,grad_checkpointed_disk, andgrad_checkpointed_with_hintsnow place stored checkpoints evenly across the trajectory. Previously the schedule positions were capped by keeping the smallest step indices, which clustered every checkpoint at the start and let the backward pass hold up to the entire trajectory's states in memory at once whennum_checkpointswas much smaller thannum_steps. Peak memory is now bounded by roughlynum_steps / num_checkpointsstates per segment; gradient values are unchanged, andgrad_checkpointed_onlinewas already well spaced. -
Recording-time algebraic folds that froze a value the tape could not reproduce at other inputs have been removed:
x * 0,x - x, andx / xnow stay on the tape, so re-evaluating at a singular or non-finite point yields the true IEEE result (x / xreplayed atx = 0is now NaN in both the value and the gradient instead of a spurious finite1). Identity folds are now signed-zero-exact:x + (-0.0)andx - (+0.0)alias the operand, whilex + (+0.0)andx - (-0.0)stay on the tape (they are not IEEE identities for a-0.0operand).x * 1,x / 1, and thepowifast paths are unchanged. -
The GPU batched entry points (
forward_batch,gradient_batch,hvp_batch,taylor_forward_kth_batch, andtaylor_forward_2nd_batchon both wgpu and CUDA backends, including the CUDA_f64variants) and the STDE helpers (laplacian_gpu,laplacian_with_control_gpu,hessian_diagonal_gpu) now reject empty batches and zero-input (constant-function) tapes with a recoverableGpuErrorinstead of panicking on zero-sized GPU buffer creation deep inside the backend. -
Debug builds now tag each
BReversewith the identity of the tape that recorded it and panic when a value crosses into a different recording (capturing an outer variable inside a nestedrecord, stashing values across recordings, or moving them between recording threads) — misuse that previously either panicked out-of-range or silently recorded a dependency on an unrelated tape slot. Release builds are unchanged in layout, behaviour, and cost; the invariant is now documented onrecordandBReverse. -
Dropping
BtapeGuards out of LIFO order now panics in every build profile (previously a debug-only assertion): an out-of-order drop installs a stale tape pointer that can dangle, so the violation is a hard error rather than release-mode undefined behaviour.
- Division primals for
Dual,DualVec, andReverseare now the correctly rounded IEEE quotienta / b(previously the double-roundeda · (1/b), up to 1 ULP off), matching plainf64arithmetic and the bytecode tape bit-for-bit. Derivative terms still reuse the reciprocal; the eager reverse-mode divisor partial now uses the same-(a/b)/bform as the bytecode opcode. Division now performs two hardware divisions instead of one division plus multiplies. - Forward-mode elementals routed through the chain rule (
sqrt,cbrt,recip,powi,ln,log2,log10,ln_1p,asin,acos,acosh,atanh, and thepowfdirection terms) now short-circuit structurally-zero tangents to exactly0at points where the derivative is unbounded or NaN: a constant stays a constant (e.g. the tangent ofsqrt(x²+y²)at the origin is now0, matchinghypot) instead of becoming NaN via IEEE0 × Inf. Live tangents keep the non-finite derivative. The GPU forward-tangent and Hessian-vector-product kernels (wgpu and CUDA) apply the same convention to their tangent and second-order direction slots, so constant direction components no longer produce NaN at singular primals on the GPU either. The-0.0behaviour of the logarithm derivative family (-Inf, following the IEEE reciprocal sign) is unchanged, now documented and pinned by tests. hypotof two identically-zero Taylor / TaylorDyn jets now returns the all-zero jet (the composite function is the constant 0) instead of the singular[0, Inf, …]jet, matching the existingLaurent::hypotbehaviour so all three series types agree. Deeper zeros with a higher-order signal (e.g.hypot(t², 0)) are unchanged. The GPU Taylor kernels follow the same convention.
0.13.0 - 2026-07-06
Coordinated release: echidna 0.13.0 and echidna-optim 0.13.3.
echidna-optim's dep on echidna updated from 0.12.0 to 0.13.0.
Minor bump: the optional nalgebra and simba integrations move to new
major versions (nalgebra 0.35, simba 0.10), an ABI-breaking change for
consumers of those features. No echidna API or numerical behaviour changed.
- Bumped the optional
nalgebraintegration from 0.34 to 0.35 and the optionalsimbascalar-trait dependency from 0.9 to 0.10. Consumers using echidna'snalgebraorsimbafeatures must move to the matching major versions; no echidna API changed. (The dev-onlynum-dualbenchmark comparison dependency was bumped to 0.14 to keep the workspace on a singlenalgebraversion.)
echidnadep updated from0.12.0to0.13.0(follows the coordinated release;echidna-optim's public API is unchanged).
0.12.0 - 2026-07-05
Coordinated release: echidna 0.12.0 and echidna-optim 0.13.2.
echidna-optim's dep on echidna updated from 0.11.0 to 0.12.0.
Minor bump: adds new public API (BytecodeTape::validate /
TapeValidationError, GpuTapeData::validate, and the kernels::*_deriv
derivative helpers) and changes two numerical conventions — abs'(0) is now
the minimal-norm subgradient 0 (was ±1), and Laurent::is_zero is
value-based. The bulk of the release hardens a broad set of out-of-domain, GPU,
and series edge cases spanning every AD mode and both GPU backends.
- GPU
powf(a, b)now matches the CPU at a zero base on the wgpu backend across the reverse, forward-tangent, and Hessian-vector-product sweeps. The primal computed0^0asNaN(WGSLpow(0, 0)is undefined); the base-direction first derivative gave0·Inf = NaNat exponent0; and the HVP's second-order term gave0·Inf = NaNata = 0for any exponentb ≥ 2, silently corrupting the Hessian.0^0is now1, the derivative at exponent0is0, and the HVP uses the division-freeb·(b−1)·a^(b−2)form that stays finite ata = 0. The CUDA backend already matched. - GPU
asinh/acoshprimals stay finite at very large arguments. Both formedx²under the square root, overflowing f32 for|x| ≳ 1.8·10¹⁹(sqrt(Inf) = Inf,log(Inf) = Inf) where the true value is finite and the guarded derivative was already correct — so a sweep could return a correct gradient beside anInfprimal. The wgpu forward, tangent, and 2nd-order Taylor kernels now use overflow-safe asymptotic forms (CUDA already used the deviceasinh/acoshintrinsics). Taylor::powfwith a negative base and a live (differentiated) exponent now returns an all-NaN jet instead of a mixed jet (a finite primala₀^b₀beside NaN derivative coefficients). A varying exponent makesa(t)^b(t)complex, so the whole jet is undefined — matchingtaylor_ln/taylor_sqrt. Applied on the CPU and both GPU Taylor emitters.Dual::powf/DualVec::powfkeep the base-direction derivative finite at an infinite base. The fast-pathn·xⁿ/xform evaluatedInf/Inf = NaN(e.g. the derivative ofInf²); it now falls back to then·xⁿ⁻¹form, yielding the correctInfand matching reverse mode and the bytecodeOpCode.diffop::hessianof a constant (zero-input) tape returns the value with an empty gradient and Hessian instead of panicking on an empty multi-index list, matchingBytecodeTape::hessian.LaurentAdd/Subwiden their pole-order shift toi64, so extreme opposite-sign pole orders can no longer overflowi32and wrap past the gap check, which would silently truncate coefficients.- STDE robustness:
dense_stde_2ndvalidates each Cholesky row's length up front (a clear message instead of an opaque out-of-bounds panic),diagonal_kth_orderrejectsk > 18(k!is exact in f64 only through18!), and the weighted estimator skips zero-weight directions before its finiteness check. - Internal robustness guards: forward-mode
jacobianrejects an output-count mismatch instead of silently truncating viazip; the wgpu compute dispatches reject a workgroup count over the backend limit with a clear message rather than a silent no-op; the bytecode-tape optimizer and dead-code pass assert DAG order and output-index bounds; the thread-local tape guard asserts LIFO drop order; and the genericOpCodedispatch flags a lossy f32Powiexponent decode (the tape sweeps use the exact raw-u32path). - GPU power operations (
powi, andpowfwith an integer exponent) at a negative base silently produced NaN values and gradients on the wgpu backend — WGSLpow(x, y)is undefined forx < 0. All WGSL shader sites now route through a sign-correcting helper, the tangent kernel keeps the finite second-order term for negative bases, and the generated Taylor kernels route constant integer exponents through the integer-power jet on both backends (previously NaN for coefficients of order ≥ 2 even on CUDA). - Deserializing a
BytecodeTapenow fully validates the payload via the newBytecodeTape::validate(also exported, withTapeValidationError): extraInputopcodes, out-of-range or sentinel output indices, forward-referencing operand indices, and stalecustom_second_argsentries are rejected with a clean error instead of panicking — or silently producing wrong derivatives from uncomputed slots — at evaluation time. GpuTapeDatais validated at upload via the newGpuTapeData::validate: an out-of-range operand or output index in hand-built data would index raw device memory out of bounds on CUDA. Both backends'upload_tapenow panic with a clear message (documented on the trait), and the CUDA f64 upload path returns an error instead.grad_checkpointed_diskisolates each invocation's checkpoint files in a uniquely-named subdirectory. Concurrent or nested computations sharing one scratch directory previously overwrote — and deleted during cleanup — each other's checkpoints, producing silently wrong gradients or panics. Checkpoint files are also registered with the cleanup guard before being written, so a panic mid-write cannot leak them.- Derivatives of the domain-restricted logarithms and
atanh(ln,log2,log10,ln_1p,atanh) now return NaN when the input is strictly outside the real domain across every AD mode — the scalar forward and reverse types (Dual,DualVec,Reverse), the bytecode tape, and the wgpu and CUDA GPU kernels (reverse, forward-tangent, and Hessian-vector-product sweeps). Previously these returned a finite but meaningless partial — e.g.grad(|x| x[0].ln(), &[-2.0])gave[-0.5]instead of[NaN], and the GPU backends disagreed with the CPU. Boundary values (x = 0forln,x = -1forln_1p,|x| = 1foratanh) keep the IEEE1/0 = ±Infone-sided limit. - The sparse-Hessian family (
sparse_hessian,sparse_hessian_vec,sparse_hessian_with_pattern,sparse_hessian_par) andhessian_parnow require a scalar-output tape, matching the densehessian/hvp/hessian_vec. On a multi-output tape they previously returned, without warning, the Hessian of a single output. third_order_hvvpnow requires a scalar-output tape and rejects tapes containing custom ops (which its nested-dual sweep can only linearize to first order), matchinghessian_vec.hessian_diagonal_gpunow returns an error for multi-output tapes instead of silently interleaving outputs, matchinglaplacian_gpu.Taylor % scalarandscalar % Taylornow flag the whole series NaN when the divisor is zero, matchingTaylor % Taylor. Previously they left a NaN constant term beside finite (orInf) higher-order coefficients — an internally inconsistent series.jacobian_forwardnow computes the Jacobian through custom ops exactly (viaCustomOp::eval_dual) instead of panicking. Forward-mode dense Jacobians previously rejected custom ops and directed callers to reverse-modejacobian; they now match the sparse Jacobian paths, which already handled custom ops.Taylor::hypot/Laurent::hypotnow return the correct series when both leading coefficients are zero and the lowest non-zero term is at order ≥ 2 (e.g.hypot(t², 0) = t²). Previously only an order-1 leading zero was handled and higher-order cases returned[0, Inf, …].Dual::powf/DualVec::powfno longer produce a NaN tangent for a negative base with a live (differentiated) exponent. The exponent-direction termxⁿ·ln(x)is guarded to zero for a base ≤ 0 (matching reverse mode); the primal and base-direction tangent were already finite for integer exponents.- The
Taylor/TaylorDynseries kernels forln/log2/log10/ln_1p/atanh/acosh(andLaurent::atanh/ln_1p) now return an all-NaN jet when the leading coefficient is strictly outside the real domain, matching the scalar AD modes. Previously they emitted a NaN primal beside finite higher-order coefficients. Branch-point boundaries (e.g.lnat 0) keep their IEEE singularity. - The GPU 2nd-order Taylor kernels (wgpu + CUDA) now match the CPU series
accuracy for
asin/acos/atanhnear|x| = 1and forexp_m1/ln_1pat small arguments. The inverse-trig jet denominators use the cancellation-safe factored form(1 − x)(1 + x)(asacoshalready did), and theexp_m1/ln_1pprimals are computed accurately — via the nativeexpm1/log1pdevice intrinsics on CUDA and guard-free series polyfills on wgpu (the naiveexp(x) − 1/log(1 + x)lost most of their significance for small arguments). - The GPU 2nd-order Taylor kernels (wgpu + CUDA) now return an all-NaN jet for
ln/log2/log10/ln_1p/atanh/acoshwhen the leading coefficient is strictly outside the real domain, matching the CPU series kernels and the scalar AD modes. Previously they emitted a NaN primal beside finite higher-order coefficients. Branch-point boundaries (lnat 0,acoshat 1,atanhat ±1) keep their IEEE singular value. acosh's derivative now returns NaN when the input is strictly outside the real domain (a < 1) across every AD mode — the scalar forward and reverse types (Dual,DualVec,Reverse), the bytecode tape, and the wgpu and CUDA GPU kernels (reverse, forward-tangent, and both Hessian-vector-product sweeps) — matching the convention already applied toln/atanh. Previously it returned a finite but meaningless value fora ≤ -1(where thesqrtargument stays positive). The boundarya = 1keeps its+Infone-sided limit. Two GPU sites that still evaluateda² - 1directly under the square root (the CUDA reverse-gradient sweep and the wgpu Hessian-vector-product first phase) now use the cancellation-safe factored(a-1)(a+1), matching every other site and the CPU kernel.- The GPU 2nd-order Taylor kernels (wgpu + CUDA) now match the CPU on three edge
cases:
max(x, NaN)/min(x, NaN)return the finite operand (the NaN tie-break was previously dropped, so the GPU returnedNaN);sqrtat a zero primal produces a uniform+Infhigher-coefficient jet (the vertical tangent) instead of a sign-dependent±Inf/NaNmix; andx % ywith a zero-leading divisor returns an all-NaN jet instead of anInf/NaNmix. - The wgpu GPU kernels now match the CPU on two piecewise ops:
signum(-0.0)returns-1(it previously returned+1from ana >= 0.0test that ignores the sign bit), androunduses ties-away-from-zero — WGSL's built-inroundis ties-to-even, soround(2.5)gave2instead of3. Both now agree with Rustf32::round/f32::signumand the CUDA backend.
- Guarded per-op derivative helpers
kernels::ln_deriv,log2_deriv,log10_deriv,ln_1p_deriv, andatanh_deriv— the single source of truth for the out-of-domain (NaN) derivative convention shared by the AD types and the bytecodeOpCodedispatcher.
- The derivative of
absat the kinkx = 0is now0(the minimal-norm subgradient) across every AD mode (Dual,DualVec,Reverse, the bytecode tape) and both GPU backends, via the new single-sourcekernels::abs_deriv. Previously most modes returned±1(fromsignum, which leaks the sign bit of a zero —signum(-0.0) = -1), so algebraically identical points could report different subgradients depending on how the zero was produced.0is value-based (+0and-0agree) and matches PyTorch / JAX / TensorFlow.sign(x)off the kink andNaNatNaNare unchanged, and the wgpu tangent shaders now propagateNaNat a NaN input (they previously zeroed the derivative there). Sharp / limiting subgradients still force±1via the nonsmooth machinery. <Laurent as num_traits::Zero>::is_zerois now value-based — it returns true when the series evaluates to0at the expansion point — instead of structural (all coefficients zero). This alignsis_zerowith theis_zero() ⟺ == Self::zero()contract (Laurent's==/PartialOrdare value-based) and with the other AD types. A positive-pole series such astnow reportsis_zero() == true.Laurent's own arithmetic uses a structural check internally, so pole formation (e.g.1/t) is unaffected.
- The trust-region solver rejects a
TrustRegionConfigwithetaoutside[0, 1/4)at entry (NumericalError). Aneta >= 1/4could reject a step without shrinking the radius, silently stalling toMaxIterations;[0, 1/4)is the range required for the standard convergence guarantee. - The trust-region radius-expansion boundary test now uses a
sqrt(eps)relative tolerance instead of ~1 ULP, so the expansion branch actually fires on genuine boundary steps (faster convergence on large-step problems; results unchanged). - The L-BFGS curvature filter tightened from
cos θ > epstocos θ > sqrt(eps), rejecting near-orthogonal(s, y)pairs whoseρ = 1/(s·y)would otherwise blow up and corrupt the two-loop recursion into a garbage search direction. - The backtracking Armijo line search validates its parameters (
rhoin(0, 1),alpha_min > 0): a misconfiguredrho >= 1previously never shrank the step length and spun forever, andalpha_min <= 0returned a degenerate zero-step; both now fail cleanly asLineSearchFailed. piggyback_tangent_solvenow requires both the primal and the tangent iterates to converge before returning. The tangent starts at zero and converges on its own schedule, so a warm-started primal previously returned a truncated Neumann sum — a silently unconverged JVP (withz0 = z*it returned after one iteration withG_x·ẋinstead of(I − G_z)⁻¹·G_x·ẋ). Reachingmax_iterwith a converged primal but unconverged tangent now reportsIterationsExhaustedTangent(whosez_normcan therefore be at or belowtol; see its documentation).implicit_tangent_sparse,implicit_adjoint_sparse, andimplicit_jacobian_sparsenow returnNumericSingularwhen the solve produces a non-finite result (e.g. a NaN reaching the right-hand side viax_dot/z̄), matching the dense implicit functions. Previously they could returnOkwith NaN entries.
echidnadep updated from0.11.0to0.12.0(follows the coordinated release).
0.11.0 - 2026-05-20
Coordinated release: echidna 0.11.0 and echidna-optim 0.13.1.
echidna-optim's dep on echidna updated from 0.10.0 to 0.11.0.
Minor bump: adds new public API (simba trait implementations for
DualVec<F, N>). No existing types changed and no numerical behaviour
changed.
- simba traits for
DualVec<F, N>: implementedSimdValue,PrimitiveSimdValue,SubsetOf,AbsDiffEq,RelativeEq,UlpsEq,Field,ComplexField, andRealField, soDualVec<F, N>can be used as a scalar type insidenalgebramatrices and solvers. The implementations mirror the existingDual<F>simba impls.
echidnadep updated from0.10.0to0.11.0(follows the coordinated release;echidna-optim's public API is unchanged).
Coordinated release: echidna 0.10.0 and echidna-optim 0.13.0.
echidna-optim's dep on echidna updated from 0.9.0 to 0.10.0.
Minor bump (rather than patch) because wgpu 28 → 29 is a transitive
major for anyone enabling the gpu-wgpu feature. No echidna public-API
types changed; the bump reflects the wgpu API-break that downstream
gpu-wgpu consumers will see in their own Cargo.lock.
wgpudependency bumped to 29 (gpu-wgpufeature). Backend migration:PipelineLayoutDescriptor::bind_group_layoutsnow takes&[Option<&BindGroupLayout>]instead of&[&BindGroupLayout], so each entry is wrapped inSome(...)at the five pipeline creation sites insrc/gpu/wgpu_backend.rs. Additionally, wgpu 29 tightenedLimits::downlevel_defaults()—max_storage_buffers_per_shader_stagedropped to a level that no longer fits our tangent-reverse pipeline's 13 storage buffers (5 tape + 8 I/O). The backend now requests the limit explicitly inDeviceDescriptorand resolves it against the adapter's actual capability viausing_resolution(adapter.limits()). Older drivers at the floor keep working; modern drivers get the 13-buffer pipeline. No publicWgpuContextAPI changes.src/gpu/mod.rs::compute_chunk_size: replaced manual guarded division (if nv_k > 0 { chunk.min(u32::MAX as u64 / nv_k) }) withu64::checked_divfor identical behaviour and a cleaner expression.
- TLA+ spec-to-code sync enforcement (
.github/workflows/specs.yml,specs/verify_anchors.sh,tests/spec_invariants_*.rs). Every TLA+ invariant inspecs/README.mdnow carries a// SPEC: <InvariantName>anchor at the Rust line upholding it (21 anchors insrc/checkpoint.rs,src/bytecode_tape/optimize.rs,src/bytecode_tape/mod.rs). A newverify-anchorsCI job parses the cross-reference table and greps sources, failing on any missing anchor. Two new test files (tests/spec_invariants_checkpoint.rs,tests/spec_invariants_tape_optimize.rs) exercise the specs' properties against the real Rust implementation over deterministic bounded input spaces (gradient equality across checkpointing strategies;optimize ∘ optimize = optimize; post-optimise structural assertions gated oncfg(debug_assertions); explicit high-N/low-C online-thinning witness). README.mdandCONTRIBUTING.mddocument the three gates (source anchors, semantic property tests, TLC model checking) with local run commands for each.
- CI
Lintjob now runscargo clippy --all-targets -- -D warningson bothechidnaandechidna-optim. Previously--all-targetswas omitted, letting benches and integration-test crates accumulate lint debt (~147 warnings + several hard compile errors) invisibly. The backlog was cleared first (benchblack_boxdeprecation, redundant closures, anum-dual-gradient API break inbenches/comparison.rs, scientific-idiomneedless_range_loopallows, a stale3.14literal flagged as approximating PI), then CI was tightened. - Dependabot maintenance:
codecov/codecov-actionv5 → v6,actions/setup-javav4 → v5,softprops/action-gh-releasev2 → v3.
echidnadep updated from0.9.0to0.10.0(follows the coordinated release;echidna-optim's public API is unchanged).- Test hygiene:
assign_op_patternandfield_reassign_with_defaultclippy fixes intests/sparse_implicit.rsandtests/ws4_diagnostics.rs. No runtime behaviour change.
Coordinated release: echidna 0.9.0 and echidna-optim 0.12.0.
echidna-optim's dep on echidna updated from 0.8.2 to 0.9.0.
echidna::assert_send_sync!macro (#[macro_export]) for compile-timeSend + Sync + 'staticassertions on error types. Applied toClarkeError(src/nonsmooth.rs) andGpuError(src/gpu/mod.rs), both of which previously had no guard — future variants carrying non-Send/ non-Sync/ non-'staticpayloads now produce a build-time failure at the type definition rather than at the (often distant) call site where the error meets anasyncboundary or a threadpool. Also adopted byechidna-optim's three error types in place of copy-pasted inlineconst _: fn() = || { ... }blocks. The macro tightens the previously-inlineSend + Sync-only bound by also requiring'static; all five error types satisfy it today and no caller relied on a non-'staticpayload, but callers adding an error type with a borrowed field must now use a different pattern.
Laurent::hypotnow routes its rescale + sum-of-squares + sqrt recipe throughtaylor_ops::taylor_hypot, the shared CPU HYPOT kernel already used byTaylor::hypotandTaylorDyn::hypot. Eliminates the last CPU HYPOT implementation outside the shared kernel. Public signature unchanged; the common matched-pole-order case produces identical output. Mismatched pole orders are handled by an explicit rebase-to-min(pole_order_a, pole_order_b)prelude that shifts the higher-pole operand's coefficient array right by the delta (zero-filling leading slots, truncating past indexK-1) — matches the Laurent*/+truncation semantics. The cone-singularity casehypot(zero, zero)short-circuits ahead of the kernel call so the zero Laurent stays all-zero (delegating through the kernel would produce a pole-of-order-1 with Inf coefficients afternormalize()— nonsense for the cone point). Note: the kernel'sscale == 0recursive shift- and-square branch is unreachable from normalized Laurent inputs (Laurent::new's normalization absorbs leading zeros intopole_order), so it has no Laurent-level behavioural effect.- GPU Taylor jet
HYPOThigher-order coefficients now match CPUTaylor::hypotvia max-rescale across both WGSL and CUDA codegen (src/gpu/taylor_codegen.rs). Pre-WS2, the primal was patched (Phase 7) butv[1]..v[K-1]still passed through unscaledjet_mul(a, a) + jet_mul(b, b), overflowing to Inf/NaN ata.v[0] ~ 1e20in f32 — silently corrupting GPU Hessians and higher Taylor coefficients ofhypotat extreme magnitudes. The new emission rescales byh = max(|a.v[0]|, |b.v[0]|), computes the sum-of-squares on the bounded scaled jets, then scales the result back, mirroringtaylor_ops::taylor_hypot. Both branches explicitly zero higher-order coefficients before early-return (uninitialisedvar r: JetKhazard). - GPU Taylor jet
HYPOTfunction-domain-boundary cases now match CPUtaylor_ops::taylor_hypotacross both WGSL and CUDA codegen (src/gpu/taylor_codegen.rs). Three boundary conventions closed:hypot(Inf, finite)— primal stays Inf (IEEE); higher-order coefficients switched from zero to NaN, matching CPU'sInf * 0 = NaNrescale-path output. NaN synthesised at runtime viainf - inf(WGSL / CUDA) rather than a compile- time bitcast, to stay portable across driver fast-math settings.hypot(0, 0)with non-zero first-order seed — GPU previously returned the zero jet; now emits a one-level shift-and-square unroll that reproduces CPU's|t| · hypot(a/t, b/t)expansion. CPU recursion is at most one level deep (the entry checka[1] != 0 || b[1] != 0guarantees the recursive call lands in the generalscale > 0path), so the unroll is ~30 WGSL / CUDA lines per backend rather than aK-bounded loop.hypot(0, 0)with all higher-order seeds also zero — GPU previously returned the zero jet; now emits 0 primal + Inf higher-order, matching CPU'staylor_sqrtat a zero leading coefficient (taylor_ops.rs:146-152). Primal override bringsc[0]back to 0 from thetaylor_sqrt's implicit Inf. Pinned byws9_{wgpu,cuda}_hypot_*tests intests/gpu_stde.rs(zero-origin shift-and-square parity, Inf-finite NaN propagation, deeper-order-zero Inf-higher convention). Callers that were defensively treating GPU zero higher-order as a "singularity detected" sentinel may see the new Inf/NaN outputs; CPU callers at those inputs already observed the new values, so CPU+GPU now behave identically.
- Internal:
Dual,DualVec, andReversenow route per-component partial-derivative computation foratan,atan2,asinh,acosh, andhypotthrough the sharedsrc/kernels/module — eliminating the CPU-vs-CPU drift surface that produced three Phase 7 bugs (atan large-|a|, hypot Inf, atan2 overflow). No public API change. - Numerics:
acoshderivative (and the WGSLacosh_f32/ CUDAacosh_fprimal helpers) uniformly switched from the unfactoredsqrt(a²-1)form to the factoredsqrt((a-1)·(a+1))form, which retains theε²contribution ata = 1 + ε(the unfactored form rounds it away in both f32 and f64). Applied across CPU (kernels::acosh_deriv), the three WGSL shaders (forward.wgsl,tangent_forward.wgsl,tangent_reverse.wgsl, plus the WGSL reverse derivative inreverse.wgsl), the CUDA kernel (tape_eval.cuat all three derivative sites), and the Taylor jet codegen for both backends (taylor_codegen.rs, including the primalacosh_fhelper). All sweeps (forward, reverse, tangent_forward, tangent_reverse) updated. Pinned by a CPU unit test inkernels::teststhat compares factored vs unfactored output ata = 1 + 1e-12and asserts they differ — any swap back trips the test. f32 GPU precision neara = 1remains dominated by the input quantisation rather than formula choice, sotests/gpu_cpu_parity.rscontinues to probe at moderate inputs (a ∈ {1.5, 2.0, 10.0}); near-one regression coverage lives in the f64 kernel unit test.
OptimResult.diagnostics: SolverDiagnosticsexposes per-solver internal counts that were previously silent — curvature pair acceptance / rejection / memory eviction in L-BFGS, gamma clamp hits, line-search backtracks, Newton steepest-descent fallback steps, and trust-region CG iterations and radius-shrink branch counts (split intobad_modelvslow_rho). Use these to detect when a solver reportsGradientNormconvergence but actually spent most of its work in fallback or filter paths.- New public types:
SolverDiagnostics(enum),LbfgsDiagnostics,NewtonDiagnostics,TrustRegionDiagnostics. PiggybackErrorenum (piggybackmodule) with variantsPrimalDivergence { iteration, last_norm },TangentDivergence { iteration, last_norm },AdjointDivergence { iteration, last_norm },IterationsExhaustedTangent { iteration, z_norm },IterationsExhaustedAdjoint { iteration, lam_norm },IterationsExhaustedForwardAdjoint { iteration, z_norm, lam_norm }, andDimensionMismatch { field: &'static str, expected, actual }. ImplementsDisplay+std::error::Error,Send + Sync + 'static,#[non_exhaustive].last_normsurfaces the iteration-level relative norm at the detecting point (non-finite on norm-check paths; finite in ratio-converging componentwise cases). The three typestate-split exhaustion variants replace the earlierMaxIterations { Option<f64>, Option<f64> }shape; each carries exactly the norm(s) its solver tracks.SparseImplicitErrorenum (sparse_implicitmodule, gated on thesparse-implicitfeature) with variantsStructuralSingular { source },FactorSingular { source },NumericSingular,ResidualExceeded { relative_residual, tolerance, dimension }, andDimensionMismatch { field, expected, actual }. ImplementsDisplay+std::error::Error(withsource()returning the underlyingfaer::sparse::{CreationError, linalg::LuError}for the two sourced variants),Send + Sync + 'static,#[non_exhaustive]. Does not deriveClone(theBox<dyn Error + Send + Sync>sources aren'tClone).ImplicitErrorenum (implicitmodule) with aSingularvariant. ImplementsDisplay+std::error::Error,Send + Sync,#[non_exhaustive]. The single variant collapses three failure modes that the dense LU does not currently distinguish: exactly-zero pivot (structural), pivot belowε·n·‖F_z‖∞(numerical), or non-finite pivot / second-order RHS (NaN / ±Inf caught by the newly added finite-pivot guard inlu_factorand by post-solve guards inimplicit_hvp/implicit_hessian). Sparse's richer variant set doesn't carry over because dense runs only one pivot-level check; future subdivision remains additive.
linalg::lu_factornow rejects non-finite pivots (NaN / ±Inf) up front. IEEE comparisons against NaN returnfalse, so without this guard a NaN pivot silently passed both the exact-zero and tolerance checks and propagated through the stored LU factors as a successful solve — callers downstream (including the denseimplicit_*entry points and the Newton solver) then returned NaN-tainted results under anOk/Somelabel. A NaN / ±Inf pivot now short-circuits toNone, surfaced toimplicit_*callers asImplicitError::Singular. All five dense implicit entry points (implicit_tangent,implicit_adjoint,implicit_jacobian,implicit_hvp,implicit_hessian) additionally check their back-solve output for non-finite entries, catching NaN that reaches the solve RHS withoutF_zitself being non-finite — e.g. a NaNx_dot/z_bar, a tape where∂F/∂xgoes non-finite independently of∂F/∂z, or a nested-dual forward pass producing non-finite higher-order coefficients at a function-domain boundary.
OptimResultis now#[non_exhaustive]so future field additions don't keep breaking downstream pattern-destructures. Construct results via the solver entry points (lbfgs,newton,trust_region); never with a struct literal.piggyback_tangent_solve,piggyback_adjoint_solve, andpiggyback_forward_adjoint_solvenow returnResult<T, PiggybackError>instead ofOption<T>. Failure modes that previously collapsed toNone(primal divergence, tangent divergence, adjoint divergence, max-iter exhaustion) are now distinguishable so callers can decide whether to retry, re-formulate, or give up. Migration:.is_none()→.is_err(),.unwrap()/.expect(...)continue to work; callers that pattern- matched onSome(_) | Nonenow match onOk(_) | Err(_).implicit_tangent_sparse,implicit_adjoint_sparse, andimplicit_jacobian_sparsenow returnResult<T, SparseImplicitError>instead ofOption<T>. The error variant pinpoints whether failure was structural (matrix construction), factor-failure (faer LU), numeric singularity (probe produced non-finite output), or residual exceedance (probe finite but||F_z·x − rhs|| / ||rhs||oversqrt(eps)·sqrt(m)). Same migration path as the piggyback functions.implicit_tangent,implicit_adjoint,implicit_jacobian,implicit_hvp, andimplicit_hessiannow returnResult<T, ImplicitError>instead ofOption<T>, closing the API asymmetry with the sparse counterparts shipped alongside. Same migration mechanic as the piggyback / sparse_implicit conversions:.is_none()→.is_err();.unwrap()/.expect(...)continue to work; callers that pattern-matched onSome(_) | Nonenow match onOk(_) | Err(_).PiggybackError::{PrimalDivergence, TangentDivergence, AdjointDivergence}gainlast_norm: f64— the relative-convergence norm at the detecting iteration. Distinguishes Inf-overflow from NaN-cancellation when non-finite; surfaces a finite bounded value in the ratio-converging case (tangent /lambda_newcomponentwise overflow while the primal/adjoint norm stayed bounded). Captured inline at eachErrreturn so the reported scalar reflects the failing iteration, not the previous one.SparseImplicitError::ResidualExceeded(renamed fromResidual) gainstolerance: f64, dimension: usize— the probe threshold (sqrt(eps)·sqrt(m)) and the state-block dimensionm(=num_states). Callers can now see how far over tolerance the probe solve landed without re-deriving the threshold.SparseImplicitError::{StructuralSingular, FactorSingular}(renamed fromStructuralFailure/FactorFailed) gainsource: Box<dyn std::error::Error + Send + Sync + 'static>;SparseImplicitErrornow implementsstd::error::Error::source()returning the underlyingfaer::sparse::CreationError/LuError. Previouslymap_err(|_| ...)discarded the faer diagnostic; callers hittingFactorSingulartypically getSymbolicSingular { index }from the chained source, pinpointing the failing column.SparseImplicitErrorno longer derivesClone(the newBox<dyn Error>field on two variants is notClone). No workspace consumer cloned the type.PiggybackError::MaxIterationsreplaced by three typed variants (typestate split):IterationsExhaustedTangent { iteration, z_norm },IterationsExhaustedAdjoint { iteration, lam_norm },IterationsExhaustedForwardAdjoint { iteration, z_norm, lam_norm }. The impossible(None, None)state is now unrepresentable; each variant carries exactly the norm(s) its solver tracks, as plainf64rather thanOption<f64>. Each variant also addsiteration: usize(=max_iter) for parity with the three*Divergencevariants.SparseImplicitErrorvariant renames for naming-axis unity:StructuralFailure → StructuralSingular,FactorFailed → FactorSingular,Residual → ResidualExceeded. Payload fields unchanged; only the names move onto the<Mode><FailureClass>axis already used byNumericSingular.PiggybackError,SparseImplicitError, andImplicitErroreach gain aDimensionMismatch { field: &'static str, expected: usize, actual: usize }variant. 14 existing + 1 new operation-time runtime-argumentassert_eq!sites in public*_solve/implicit_*fns now surface as thisErrvariant (4 implicit + 8 sparse_implicit + 2 piggyback existing + 1 new inline check inpiggyback_tangent_solve). Construction-time (SparseImplicitContext::new), tape-shape (validate_*helpers), and step-fn (piggyback_tangent_step[_with_buf]) assertions remain panics per the programmer-contract convention.echidna-optimversion bumped to0.12.0to reflect the breaking variant renames,MaxIterationstypestate split, and newDimensionMismatchvariant on three error enums.
- Inline
const _: fn() = || { ... assert_send_sync ... }blocks inechidna-optim/src/piggyback.rs,echidna-optim/src/sparse_implicit.rs, andechidna-optim/src/implicit.rs— replaced by the hoistedechidna::assert_send_sync!macro invoked once per error type. PiggybackError::MaxIterations(see### Changed— replaced by three typestate variants).- The WS5-introduced 4-arm
MaxIterationsDisplay match inpiggyback.rs— superseded by per-variant Display impls in the typestate split; each new variant has a single-arm Display, so the defensive(None, None)case no longer exists.
- atan derivative overflow:
1/(1+x²)overflows to 0 for|x| > ~1.34e154(f64). Now uses(1/x)²/(1+(1/x)²)for large inputs across Dual, DualVec, Reverse, and bytecode reverse_partials. - powf derivative underflow: when
x^bunderflows to 0 butx ≠ 0, the derivativeb·x^b/xsilently returned 0. Now falls back to directb·x^(b-1)computation. Fixed in all 4 AD modes. - powi(i32::MIN) f32 precision: converting
i32::MIN - 1to f32 rounds the exponent. Now usesx^n/xto computex^(n-1)without float conversion. Fixed across 7 code sites (dual, dual_vec, Reverse, opcode, bytecode reverse, bytecode tangent, cross-country). - taylor_acosh cancellation:
a²-1neara=1suffered catastrophic cancellation. Replaced with factored form(a-1)(a+1), matching sister functions asin/atanh. - Taylor/TaylorDyn max/min NaN handling:
max(valid, NaN)returned NaN instead of the valid value. Added IEEE 754 fmax/fmin NaN guard matching Dual/Laurent implementations. - Taylor::derivative factorial overflow: standalone
k!computation overflows f64 at k=171. Now interleaves multiplication with the coefficient, extending the usable range.
- WGSL u32 index overflow:
forward_batch,gradient_batch, andhvp_batchnow assertbatch_size × num_variables ≤ u32::MAXbefore GPU dispatch. Previously, large workloads silently produced corrupted results. - POWI Taylor codegen at x=0:
0^nfor n=2,3,4 now uses repeatedjet_mulinstead ofjet_const(0), preserving higher-order Taylor coefficients (e.g., Hessian ofx²at zero). Both WGSL and CUDA codegen. - WGSL abs reverse NaN divergence:
absreverse derivative returned 0 for NaN inputs instead of propagating NaN. Now matches CUDA/CPU behavior. - WGSL signum(-0.0): now uses
bitcast<u32>sign-bit check to correctly return -1 for negative zero, matching Rust'sf32::signum.
- Revolve checkpoint memory budget:
revolve_scheduleproduced O(num_steps) checkpoint positions instead of O(num_checkpoints). Forward pass now truncates to the budget. Gradients were always correct; only memory usage was affected. Fixed in all 3 call sites (standard, hints, disk).
- diagonal_kth_order_const f32 guard: const-generic variant now rejects k ≥ 13 for f32, matching the dynamic version's precision guard.
- Weighted estimator finiteness:
estimate_weightednow asserts sample finiteness, matchingWelfordAccumulatorbehavior. - Factorial guard comment: corrected misleading "overflows f64 for k > 20" (actual overflow is k=171; precision loss begins at k=19).
- Trust region boundary_tau cancellation: quadratic formula now uses Vieta's formula to avoid catastrophic cancellation when
|b| ≈ √discriminant. - L-BFGS step vector cancellation:
s = (x + α·d) - xreplaced withs = α·dto avoid cancellation when‖x‖ ≫ α·‖d‖.
- 9 new regression tests covering all fixed issues.
- Clarifying comments at 8 code sites frequently flagged as false positives during review (zero-adjoint skip, primal patching, powi encoding, Rem derivative, atan2 at origin, sqrt/cbrt singularity, custom op linearization).
- cbrt HVP second derivative: tangent_reverse kernels computed
-2at/(9r³)instead of-2at/(9r⁵), producing wrong Hessian-vector products throughcbrt. Fixed in both CUDA and WGSL. - asin/acos/atanh cancellation in GPU shaders: GPU derivative formulas used
1-a*awhich loses ~15 digits near |a|→1. Replaced with(1-a)*(1+a)across all GPU kernels (reverse, tangent_forward, tangent_reverse) for CPU-GPU parity. - CUDA Taylor codegen u32 truncation: generated code assigned 64-bit
j_basetounsigned intintermediates, silently truncating for large tapes. All offset variables now useunsigned long long.
- asin/acos/atanh catastrophic cancellation:
1 - x*xreplaced with(1-x)*(1+x)in Dual, DualVec, Reverse, bytecode reverse_partials, and Taylor recurrences to preserve precision near domain boundaries. - atan2 bytecode overflow:
a*a + b*breplaced withhypot(a,b)²in bytecode reverse_partials, preventing zero derivatives for large inputs (|a| or |b| > ~1.34e154). - Division derivative overflow: quotient rule restructured from
(a'b - ab') * inv²to(a' - a*inv*b') * inv, avoiding intermediate overflow for small denominators. - Taylor hypot overflow/underflow: inputs rescaled by
max(|a₀|, |b₀|)before squaring, preventing silent zeroing of derivative coefficients for large inputs and infinity for small inputs.
- Custom ops in Hessian (release builds):
debug_assert!promoted toassert!for custom-ops guards inhessian_vec,sparse_hessian_vec, andsparse_jacobian_vec, preventing silently wrong second derivatives in release builds. - Serde Custom opcode rejection: deserialization now rejects tapes containing Custom opcodes (which have no serializable callback) instead of silently accepting them.
- Per-type thread-local borrow guards: borrow guards for
with_active_tapeandwith_active_btapeare now per-type instead of global, preventing false reentrance panics when nesting different float types on the same thread.
- Welford negative variance:
m2.max(0.0)clamp prevents NaN standard errors from floating-point cancellation in nearly-identical samples. - estimate_weighted zero-weight division: guarded
w_sum2 / w_sumagainst all-zero weights producing NaN. - Gram-Schmidt epsilon: replaced hardcoded
1e-12withF::epsilon().sqrt()in Hutch++, fixing f32 compatibility. - Higher-order f32 precision guard:
diagonal_kth_ordernow rejects k ≥ 13 for f32 (k! exceeds f32 mantissa precision).
- WGSL u32 index overflow: chunking logic now caps
chunk_sizesobid * num_variables * Kcannot exceedu32::MAX.
- L-BFGS rho overflow: curvature pair acceptance tightened from
sy > 0tosy > epsilon * yy, preventing infiniterho = 1/syfrom near-zero curvature. - LU singularity threshold: replaced
sqrt(epsilon)with relative thresholdepsilon * n * max_pivot, adapting to both f32 and matrix scale. Explicit zero-pivot check added.
- 5 additional
debug_assert!→assert!promotions for correctness-critical guards (Welford finite-sample, Laurent pole guard, GPU dispatch u32 bounds). - 35 new regression/structural tests:
- 8 boundary-value derivative tests (asin/acos/atanh near ±1, atan2 large inputs, division small denominator, Taylor hypot extremes)
- 2 Welford accumulator edge case tests (nearly-identical samples, all-zero weights)
- 5 GPU chunking safety tests (u32 overflow prevention)
- 4 f32 derivative correctness tests (cross-validation + diagonal_kth_order)
- 15 per-opcode GPU HVP parity tests (exp, log, sqrt, cbrt, recip, sin, cos, tan, asin, acos, atan, sinh, cosh, tanh, powf)
- 1 serde Custom opcode rejection test (existing assert test updated)
- POWI at x=0: GPU now returns
jet_const(0)for0^n(n>=2) instead of NaN fromln(0). Negative exponents at x=0 correctly produce Inf. - REM higher-order coefficients: GPU REM now loads the full b jet and computes
r[k] = a[k] - q * b[k], matching CPU Taylor behavior for non-constant divisors. - POWF a<=0 higher-order coefficients: GPU now propagates explicit NaN for c2+ when base is non-positive, matching CPU
powfIEEE semantics. Previously silently zeroed. - POWF WGSL K=1 out-of-bounds: guarded
r.v[1]write withif k > 1, matching CUDA. - ATAN2 b=0 higher-order coefficients: GPU now computes full Taylor composition via
jet_div(b,a)+jet_atan+ negate, matching CPU for K>=3. _signconsistency: CUDA codegen andtape_eval.cunow usecopysign(1, x)matching Rust'ssignumfor-0.0. WGSL usesselect(cannot distinguish-0.0).- u32 index overflow: all CUDA kernel index arithmetic widened to
unsigned long longto support batch_size * num_variables > 2^32.
- atan2 underflow:
Dual,DualVec, andReversenow usehypotfor the denominator inatan2, preventing underflow for very small inputs (~1e-200). - taylor_cbrt: uses
c.len()(output length) for iteration, consistent with all other Taylor functions. - Nonsmooth NaN consistency:
is_smoothandactive_kinksnow handle NaN switching values consistently (NaN = not smooth, appears in active kinks). - Fract kink tracking:
OpCode::Fractadded tois_nonsmoothandforward_nonsmoothfor kink proximity detection.
- Sub assertion parity:
Laurent::Subnow has the same pole-order gap assertion asAdd, preventing silent truncation. is_zerosemantics:Laurent::is_zero()now checks all coefficients viais_all_zero_pub(), correctly returningfalsefor series withpole_order > 0and nonzero coefficients.- max/min NaN handling:
Laurent::max/minnow return the non-NaN argument, matchingDual/Reverse/BReverse. - powi pole_order overflow: uses
checked_mulinstead of uncheckedi32multiplication.
- Piggyback forward-adjoint stale x_bar:
piggyback_forward_adjoint_solvenow performs a final reverse pass with the converged lambda, matchingpiggyback_adjoint_solveand eliminating O(tol) gradient bias. - NaN gradient detection: all three solvers (trust region, L-BFGS, Newton) now detect NaN/Inf in gradient or function value and terminate with
NumericalError. - Trust region negative predicted reduction: rejects step and shrinks radius when the quadratic model predicts worsening, preventing spurious expansion.
- Steihaug CG tolerance: uses relative tolerance (
sqrt(epsilon) * ||g||) instead of absoluteepsilon, improving CG convergence for both large and small gradients. - Trust region radius shrink: uses
0.25 * radius(standard algorithm) instead of0.25 * step_norm. - L-BFGS gamma overflow: guards against subnormal
y^T ycausing infinite scaling.
sparse_jacobian_parcustom ops: forward-mode path now usesforward_tangent_dualfor correct primal evaluation at fresh inputs.- Deserialization validation: validates structural consistency (lengths, bounds, opcode types) on deserialization, preventing panics from malformed tapes.
- Hessian custom ops:
debug_assert!at entry ofhessian_vec,sparse_hessian_vec, andsparse_jacobian_vecwarns about approximate second derivatives through custom ops.
- Reentrant borrow guards:
with_active_tapeandwith_active_btapenow detect reentrant calls via RAII guards, panicking instead of creating aliased&mutreferences. - u32 overflow guard:
debug_asserton tape variable count prevents silent index wrapping. - 17 regression tests covering all fixed bugs.
- CUDA kth-order Taylor evaluation:
CudaContext::taylor_forward_kth_batchandtaylor_forward_kth_batch_f64support K=1..5 Taylor jet evaluation, bringing CUDA to parity with wgpu. Kernels are lazy-compiled on first use viataylor_codegen::generate_taylor_cuda. taylor_forward_kth_batchonGpuBackendtrait: promoted from inherent method to trait method, enabling generic GPU STDE code to use arbitrary-order Taylor evaluation on either backend.taylor_forward_2nd_batchdefault trait impl: delegates totaylor_forward_kth_batch(order=3), eliminating duplicated logic across both backends.- CUDA Taylor opcode test parity:
gpu_stde.rsrefactored withopcode_tests_for_backend!macro to run all per-opcode Taylor tests on both wgpu and CUDA.
- Handwritten
taylor_eval.cu(527 lines): replaced by codegen K=3 output. The CUDA backend now usestaylor_codegen::generate_taylor_cudafor all Taylor kernels. - Handwritten
taylor_forward_2nd.wgsl(570 lines): replaced by codegen K=3 output. The wgpu backend now usestaylor_codegen::generate_taylor_wgslfor all Taylor shaders. cuda_taylor_fwd_2nd_body!macro: no longer needed sincetaylor_forward_2nd_batchdelegates to the kth-order path.
TrustRegionConfig(echidna-optim): newmin_radius: Ffield added in 0.6.0 (breaking for direct struct construction;Defaultimpls provide it).
- Max/Min NaN handling:
BReverse::max/minand bytecodeeval_forward/reverse_partialsnow return the non-NaN argument when one input is NaN, matchingFloat::max/Float::minsemantics and the behavior of Dual/Reverse modes. - atan2(0,0) derivative: returns zero gradient (matching JAX/PyTorch convention) instead of NaN from division by zero in
x²+y². - powf(x, 0) derivative: correctly returns
d/db = ln(x)forx > 0instead of silently dropping the exponent derivative.d/da = 0was already correct. - powf(x, y) at x=0: Reverse and bytecode modes now use the
y * x^(y-1)form (matching Dual) instead ofy * val / xwhich produces0/0 = NaN. - powi_exp_decode: deleted broken generic float round-trip decoder (failed for f32 negative exponents); replaced with
powi_exp_decode_rawat both call sites.
- Taylor abs(0):
Taylor::absandTaylorDyn::absnow use the first nonzero coefficient's sign instead ofsignum(+0.0) = 0, preventing the entire jet from being annihilated. - taylor_cbrt at zero: returns
[0, Inf, ...](signaling the vertical tangent) instead of NaN fromln(0). - taylor_sqrt at zero: runtime guard returns
[0, Inf, ...]instead of silentInf/NaNfrom division by2*sqrt(0). - Laurent Add/Sub truncation: promoted from
debug_asserttoassert!— silent coefficient loss when pole-order gap exceedsK-1is now caught at runtime.
- Checkpoint thinning: online checkpoint thinning now maintains uniform spacing after doubling (
skip(1).step_by(2)instead ofstep_by(2)), fixing O(N) recomputation degradation for small checkpoint budgets. - Abs sparse Hessian: reclassified from
UnaryNonlineartoZeroDerivative—d²|x|/dx² = 0a.e., reducing spurious Hessian sparsity pattern entries. - Added
debug_assertfor tape variable count overflow (u32::MAX) and contiguous input opcodes.
- POWI negative bases: GPU Taylor mode now handles negative bases via
sign(a)^n * exp(n * ln(|a|))instead ofexp(n * ln(a))which produced NaN forln(negative). - POWF at a≤0: added first-order chain-rule fallback when
a ≤ 0(whereln(a)is undefined). - REM Taylor coefficients:
c1/c2now pass througha's jet (d(a%b)/da = 1a.e.) instead of being zeroed. _signin taylor_eval.cu: now matchestape_eval.cu— returns1for+0.0andNaNforNaN(Rustsignumsemantics).- ATAN2 at b=0: uses direct derivative formula instead of
jet_div(a, b)which divided by zero. - CBRT at a=0: returns
[0, Inf, ...]instead of NaN fromln(0). - wgpu expm1/ln1p: polynomial approximation for
|x| < 1e-4to avoid catastrophic cancellation.
- Trust-region min radius: added
min_radiusfield toTrustRegionConfig(defaultF::epsilon()), preventing silent stall from geometric radius collapse. - boundary_tau zero direction: guards
||d||² < epsilonto prevent NaN from degenerate CG directions. - LU singularity threshold: changed from hardcoded
1e-12toF::epsilon().sqrt(), fixing false negatives for f32 near-singular matrices. - Piggyback adjoint: extra reverse pass with converged
lambda_neweliminates O(tol × ||G_x||) error in parameter gradients. - Weighted STDE:
estimate_weightedskips zero-weight samples to prevent division by zero in West's algorithm. - Welford accumulator:
debug_assert!(sample.is_finite())catches NaN/Inf inputs in debug builds.
- GPU STDE test parity: refactored
tests/gpu_stde.rsto use a genericopcode_tests_for_backend!macro, running all per-opcode Taylor tests on both wgpu and CUDA backends instead of wgpu-only. TrustRegionConfig: addedmin_radius: Ffield (breaking change for direct struct construction;Defaultimpls updated).
0.5.0 - 2026-03-14
- GPU cast safety audit: SAFETY comments on all
as u32casts in GPU paths (mod.rs,cuda_backend.rs,wgpu_backend.rs,stde_gpu.rs). Addeddebug_assert!guards on user-provided direction/batch counts instde_gpu.rs. #[must_use]annotations: 19 pure functions now carry#[must_use](support module helpers, GPU codegen, solver wrappers,Laurent::zero/one).#![warn(missing_docs)]: enabled crate-wide. All public items — 35OpCodevariants, ~190 elemental methods acrossDual,DualVec,Taylor,TaylorDyn,Laurent, struct fields, and trait methods — now have doc comments.
- Test decomposition: split
tests/stde.rs(1630 lines, 76 tests) into 5 focused files:stde_core,stde_stats,stde_pipeline,stde_higher_order,stde_dense. All 76 tests preserved. - Removed
ROADMAP.md— all phases (0–5) complete.
0.4.1 - 2026-03-14
- Powi f32 exponent encoding:
powi(n)on f32 bytecode tapes silently produced wrong values and gradients for negative exponents (n <= -2). Thei32exponent was stored asu32then round-tripped throughf32, which loses precision for values > 2^24 (all negative exponents). All 5 dispatch sites (forward, reverse, tangent forward, tangent reverse, cross-country) now decode the exponent directly from the rawu32viapowi_exp_decode_raw, bypassing the float conversion entirely. - taylor_powi negative base:
Taylor::powiand bytecode Taylor-mode produced NaN for negative base values (e.g.(-2)^3) because the implementation usedexp(n * ln(a))which fails forln(negative). Addedtaylor_powi_squaringusing binary exponentiation withtaylor_mul, dispatched whena[0] < 0or|n| <= 8. - Checkpoint position lookup:
grad_checkpointed,grad_checkpointed_disk, andgrad_checkpointed_with_hintsusedVec::contains()for checkpoint position lookups (O(n) per step). Converted toHashSetfor O(1) lookups. - Nonsmooth Round kink detection:
forward_nonsmoothnow correctly detects Round kinks at half-integers (0.5, 1.5, ...) instead of at integers, matching the actual discontinuity locations of theroundfunction. Updated test to match.
0.4.0 - 2026-02-26
- BytecodeTape decomposition: split 2,689-line monolithic
bytecode_tape.rsinto a directory module with 10 focused submodules (forward.rs,reverse.rs,tangent.rs,jacobian.rs,sparse.rs,optimize.rs,taylor.rs,parallel.rs,serde_support.rs,thread_local.rs). Zero public API changes; benchmarks confirm no performance impact. - Deduplicated reverse sweep in
gradient_with_buf()andsparse_jacobian_par()— both now call sharedreverse_sweep_core()instead of inlining the loop.gradient_with_bufgains the zero-adjoint skip optimization it was previously missing. - Bumped
nalgebradependency from 0.33 to 0.34
- Corrected opcode variant count in documentation (44 variants, not 38/43)
- Fixed CONTRIBUTING.md MSRV reference (1.93, not 1.80)
0.3.0 - 2026-02-25
diffop::mixed_partial(tape, x, orders)— compute any mixed partial derivative via jet coefficient extractiondiffop::hessian(tape, x)— full Hessian via jet extraction (cross-validated againsttape.hessian())MultiIndex— specify which mixed partial to compute (e.g.,[2, 0, 1]= ∂³u/∂x₀²∂x₂)JetPlan::plan(n, indices)— precompute slot assignments and extraction prefactors; reuse across evaluation pointsdiffop::eval_dyn(plan, tape, x)— evaluate a plan at a new point usingTaylorDyn- Pushforward grouping: multi-indices with different active variable sets get separate forward passes to avoid slot contamination
- Prime window sliding for collision-free slot assignment up to high derivative orders
0.2.0 - 2026-02-25
BytecodeTapeSoA graph-mode AD with opcode dispatch and tape optimization (CSE, DCE, constant folding)BReverse<F>tape-recording reverse-mode variablerecord()/record_multi()to build tapes from closures- Hessian computation via forward-over-reverse (
hessian,hvp) DualVec<F, N>batched forward-mode with N tangent lanes for vectorized Hessians (hessian_vec)
- Sparsity pattern detection via bitset propagation
- Graph coloring: greedy distance-2 for Jacobians, star bicoloring for Hessians
sparse_jacobian,sparse_hessian,sparse_hessian_vec- CSR storage (
CsrPattern,JacobianSparsityPattern,SparsityPattern)
Taylor<F, K>const-generic Taylor coefficients with Cauchy product propagationTaylorDyn<F>arena-based dynamic Taylor (runtime degree)taylor_grad/taylor_grad_with_buf— reverse-over-Taylor for gradient + HVP + higher-order adjointsode_taylor_step/ode_taylor_step_with_buf— ODE Taylor series integration via coefficient bootstrapping
laplacian— Hutchinson trace estimator for Laplacian approximationhessian_diagonal— exact Hessian diagonal via coordinate basisdirectional_derivatives— batched second-order directional derivativeslaplacian_with_stats— Welford's online variance trackinglaplacian_with_control— diagonal control variate variance reductionEstimatortrait generalizing per-direction sample computation (Laplacian,GradientSquaredNorm)estimate/estimate_weightedgeneric pipeline- Hutchinson divergence estimator for vector fields via
Dual<F>forward mode - Hutch++ (Meyer et al. 2021) O(1/S²) trace estimator via sketch + residual decomposition
- Importance-weighted estimation (West's 1979 algorithm)
jacobian_cross_country— Markowitz vertex elimination on linearized computational graph
eval_dual/partials_dualdefault methods onCustomOp<F>for correct second-order derivatives (HVP, Hessian) through custom ops
forward_nonsmooth— branch tracking and kink detection for abs/min/max/signum/floor/ceil/round/truncclarke_jacobian— Clarke generalized Jacobian via limiting Jacobian enumerationhas_nontrivial_subdifferential()— two-tier classification: all 8 nonsmooth ops tracked for proximity detection; only abs/min/max enumerated in Clarke JacobianKinkEntry,NonsmoothInfo,ClarkeErrortypes
Laurent<F, K>— singularity analysis with pole tracking, flows throughBytecodeTape::forward_tangent
grad_checkpointed— binomial Revolve checkpointinggrad_checkpointed_online— periodic thinning for unknown step countgrad_checkpointed_disk— disk-backed for large state vectorsgrad_checkpointed_with_hints— user-controlled checkpoint placement
- wgpu backend: batched forward, gradient, sparse Jacobian, HVP, sparse Hessian (f32, Metal/Vulkan/DX12)
- CUDA backend: same operations with f32 + f64 support (NVRTC runtime compilation)
GpuBackendtrait unifying wgpu and CUDA backends behind a common interface
- Type-level AD composition:
Dual<BReverse<f64>>,Taylor<BReverse<f64>, K>,DualVec<BReverse<f64>, N> composed_hvpconvenience function for forward-over-reverse HVPBReverse<Dual<f64>>reverse-wrapping-forward composition viaBtapeThreadLocalimpls forDual<f32>andDual<f64>
serdesupport forBytecodeTape,Laurent<F, K>,KinkEntry,NonsmoothInfo,ClarkeError- JSON and bincode roundtrip support
faer_support: HVP, sparse Hessian, dense/sparse solvers (LU, Cholesky)nalgebra_support: gradient, Hessian, Jacobian with nalgebra typesndarray_support: HVP, sparse Hessian, sparse Jacobian with ndarray types
- L-BFGS solver with two-loop recursion
- Newton solver with Cholesky factorization
- Trust-region solver with Steihaug-Toint CG
- Armijo line search
- Implicit differentiation:
implicit_tangent,implicit_adjoint,implicit_jacobian,implicit_hvp,implicit_hessian - Piggyback differentiation: tangent, adjoint, and interleaved forward-adjoint modes
- Sparse implicit differentiation via faer sparse LU (
sparse-implicitfeature)
- Criterion benchmarks for Taylor mode, STDE, cross-country, sparse derivatives, nonsmooth
- Comparison benchmarks against num-dual and ad-trait (forward + reverse gradient)
- Correctness cross-check tests verifying ad-trait gradient agreement with echidna
- CI regression detection via criterion-compare-action
- Tape optimization: algebraic simplification at recording time (identity, absorbing, powi patterns)
- Tape optimization: targeted multi-output DCE (
dead_code_elimination_for_outputs) - Thread-local Adept tape pooling —
grad()/vjp()reuse cleared tapes via thread-local pool instead of per-call allocation Signed::signum()forBReverse<F>now recordsOpCode::Signumto tape (was returning a constant)- MSRV raised from 1.80 to 1.93
WelfordAccumulatorstruct extracted, deduplicating Welford's algorithm across 4 STDE functionscuda_errhelper extracted, replacing 72 inline.map_errclosures in CUDA backendcreate_tape_bind_groupmethod extracted, replacing 4 duplicated bind group blocks in wgpu backend
0.1.0 - 2026-02-21
Dual<F>forward-mode dual number with all 30+ elemental operationsReverse<F>reverse-mode AD variable (12 bytes for f64,Copy)Floatmarker trait forf32/f64Scalartrait for writing AD-generic code- Type aliases:
Dual64,Dual32,Reverse64,Reverse32
- Adept-style two-stack tape with precomputed partial derivatives
- Thread-local active tape with RAII guard (
TapeGuard) - Constant sentinel (
u32::MAX) to avoid tape bloat from literals - Zero-adjoint skipping in the reverse sweep
grad(f, x)— gradient via reverse modejvp(f, x, v)— Jacobian-vector product via forward modevjp(f, x, w)— vector-Jacobian product via reverse modejacobian(f, x)— full Jacobian via forward mode
- Powers:
recip,sqrt,cbrt,powi,powf - Exp/Log:
exp,exp2,exp_m1,ln,log2,log10,ln_1p,log - Trig:
sin,cos,tan,sin_cos,asin,acos,atan,atan2 - Hyperbolic:
sinh,cosh,tanh,asinh,acosh,atanh - Misc:
abs,signum,floor,ceil,round,trunc,fract,mul_add,hypot
num-traits:Float,Zero,One,Num,Signed,FloatConst,FromPrimitive,ToPrimitive,NumCaststd::ops:Add,Sub,Mul,Div,Neg,Remwith assign variants- Mixed scalar ops (
Dual<f64> + f64,f64 * Reverse<f64>, etc.)
- 94 tests: forward mode, reverse mode, API, and cross-validation
- Every elemental validated against central finite differences
- Forward-vs-reverse cross-validation on Rosenbrock, Beale, Ackley, Booth, and more
- Criterion benchmarks for forward overhead and reverse gradient