Makes the solver generic over its scalar so a problem can be solved in f64 or
differentiated by solving it in dual numbers. Also fixes a step-controller bug
found on the way.
Autodiff
New scalar::Scalar trait (RealField + Copy + NumOps<f64, Self>, plus re()
and lift()). Implemented for f64, and for num-dual's forward-mode dual
numbers under the autodiff feature (on by default). State, time and step size
are all generic; all four integrators and both controllers are ported.
Solving at a dual scalar gives exact derivatives through the whole solve — the
initial state, t0, t_end, and any parameter the right-hand side reads. One
propagation in DualSVec64<6> yields a full 6x6 state transition matrix.
The only change a user needs is <T: Scalar> on the right-hand side signature;
the body is usually untouched, and Params can stay f64 unless you want to
differentiate with respect to it. A concrete f64 right-hand side still works
unchanged — the benches are the proof, they compile as they were.
Two things are deliberately not differentiated, because differentiating them
would be wrong rather than merely expensive: the adaptive step sequence (a
discrete accept/reject decision, as in DifferentialEquations.jl) and event
location (the event time is defined implicitly, so its derivative comes from the
implicit function theorem, not from differentiating Brent's iterations).
Step size controller fix
The rejection loop in Problem::solve judged a retried step by the previous
attempt's error, so a step could be accepted on the strength of a different
step's error. Its size depended on how often a method was rejected, so it hit
methods unequally: on the 6D orbit problem at 1e-10, DP45 took 2,023 steps
instead of 440, while Vern7 was mildly flattered (152 against 184).
Error now tracks tolerance cleanly: the harmonic oscillator gives 1.79e-6 at
1e-6, 1.85e-8 at 1e-8, 1.86e-10 at 1e-10. Both benchmark reports are
regenerated, which changes some headline ratios — Vern7 vs DP5 on the 6D orbit
is 1.1x, not 8.8x.
Breaking changes
Integrator::step returns StepResult { y, err, dense } instead of a (y, err, dense) tuple
Callback is now a trait; FnCallback holds the closure pair
Problem is generic over C: Controller<T>, which is what makes PIDController usable — it previously could not be passed to a Problem
PIDController's beta1/beta2/beta3 are now p_term/i_term/d_term
a_tol, a_tol_full, r_tol and with_callback are consuming builders
readme is included as crate docs, so every example in it is compiled and run
DormandPrince45 gained a docstring; it notes that FSAL is a property of the
method that neither it nor BS3 currently exploits
roadmap: new feature 39 (per-step allocation), note added to feature 19
(Jacobian computation) on how autodiff arrived by a different route
Verification
43 unit tests + 7 doctests; 38 + 5 with --no-default-features. AD checked
three ways: machine precision against an exactly-integrable quadrature
(y' = p·t^4, matching to 1e-14), against the analytic sensitivity of a linear
problem, and on the orbit problem via det(STM) = 1 (Liouville) plus a
central-difference cross-check. Vern7's lazy extra stages are exercised in dual
arithmetic.
f64 numerics are bit-identical to v0.3.1 for the genericisation itself —
verified by step count and final state on three integrators before the
controller fix was applied.
## Generic scalar type and forward-mode autodiff
Makes the solver generic over its scalar so a problem can be solved in `f64` or
differentiated by solving it in dual numbers. Also fixes a step-controller bug
found on the way.
### Autodiff
New `scalar::Scalar` trait (`RealField + Copy + NumOps<f64, Self>`, plus `re()`
and `lift()`). Implemented for `f64`, and for `num-dual`'s forward-mode dual
numbers under the `autodiff` feature (on by default). State, time and step size
are all generic; all four integrators and both controllers are ported.
Solving at a dual scalar gives exact derivatives through the whole solve — the
initial state, `t0`, `t_end`, and any parameter the right-hand side reads. One
propagation in `DualSVec64<6>` yields a full 6x6 state transition matrix.
The only change a user needs is `<T: Scalar>` on the right-hand side signature;
the body is usually untouched, and `Params` can stay `f64` unless you want to
differentiate with respect to it. A concrete `f64` right-hand side still works
unchanged — the benches are the proof, they compile as they were.
Two things are deliberately not differentiated, because differentiating them
would be wrong rather than merely expensive: the adaptive step sequence (a
discrete accept/reject decision, as in DifferentialEquations.jl) and event
location (the event time is defined implicitly, so its derivative comes from the
implicit function theorem, not from differentiating Brent's iterations).
### Step size controller fix
The rejection loop in `Problem::solve` judged a retried step by the *previous*
attempt's error, so a step could be accepted on the strength of a different
step's error. Its size depended on how often a method was rejected, so it hit
methods unequally: on the 6D orbit problem at 1e-10, DP45 took 2,023 steps
instead of 440, while Vern7 was mildly flattered (152 against 184).
Error now tracks tolerance cleanly: the harmonic oscillator gives 1.79e-6 at
1e-6, 1.85e-8 at 1e-8, 1.86e-10 at 1e-10. Both benchmark reports are
regenerated, which changes some headline ratios — Vern7 vs DP5 on the 6D orbit
is 1.1x, not 8.8x.
### Breaking changes
- `Integrator::step` returns `StepResult { y, err, dense }` instead of a
`(y, err, dense)` tuple
- `Callback` is now a trait; `FnCallback` holds the closure pair
- `Problem` is generic over `C: Controller<T>`, which is what makes
`PIDController` usable — it previously could not be passed to a `Problem`
- `PIDController`'s `beta1`/`beta2`/`beta3` are now `p_term`/`i_term`/`d_term`
- `a_tol`, `a_tol_full`, `r_tol` and `with_callback` are consuming builders
- nalgebra 0.34 → 0.35 (required by `num-dual`)
### Other
- `cargo clippy --all-targets -- -D warnings` is clean (was 25 warnings)
- readme is included as crate docs, so every example in it is compiled and run
- `DormandPrince45` gained a docstring; it notes that FSAL is a property of the
method that neither it nor BS3 currently exploits
- roadmap: new feature 39 (per-step allocation), note added to feature 19
(Jacobian computation) on how autodiff arrived by a different route
### Verification
43 unit tests + 7 doctests; 38 + 5 with `--no-default-features`. AD checked
three ways: machine precision against an exactly-integrable quadrature
(`y' = p·t^4`, matching to 1e-14), against the analytic sensitivity of a linear
problem, and on the orbit problem via `det(STM) = 1` (Liouville) plus a
central-difference cross-check. Vern7's lazy extra stages are exercised in dual
arithmetic.
`f64` numerics are bit-identical to v0.3.1 for the genericisation itself —
verified by step count and final state on three integrators before the
controller fix was applied.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Generic scalar type and forward-mode autodiff
Makes the solver generic over its scalar so a problem can be solved in
f64ordifferentiated by solving it in dual numbers. Also fixes a step-controller bug
found on the way.
Autodiff
New
scalar::Scalartrait (RealField + Copy + NumOps<f64, Self>, plusre()and
lift()). Implemented forf64, and fornum-dual's forward-mode dualnumbers under the
autodifffeature (on by default). State, time and step sizeare all generic; all four integrators and both controllers are ported.
Solving at a dual scalar gives exact derivatives through the whole solve — the
initial state,
t0,t_end, and any parameter the right-hand side reads. Onepropagation in
DualSVec64<6>yields a full 6x6 state transition matrix.The only change a user needs is
<T: Scalar>on the right-hand side signature;the body is usually untouched, and
Paramscan stayf64unless you want todifferentiate with respect to it. A concrete
f64right-hand side still worksunchanged — the benches are the proof, they compile as they were.
Two things are deliberately not differentiated, because differentiating them
would be wrong rather than merely expensive: the adaptive step sequence (a
discrete accept/reject decision, as in DifferentialEquations.jl) and event
location (the event time is defined implicitly, so its derivative comes from the
implicit function theorem, not from differentiating Brent's iterations).
Step size controller fix
The rejection loop in
Problem::solvejudged a retried step by the previousattempt's error, so a step could be accepted on the strength of a different
step's error. Its size depended on how often a method was rejected, so it hit
methods unequally: on the 6D orbit problem at 1e-10, DP45 took 2,023 steps
instead of 440, while Vern7 was mildly flattered (152 against 184).
Error now tracks tolerance cleanly: the harmonic oscillator gives 1.79e-6 at
1e-6, 1.85e-8 at 1e-8, 1.86e-10 at 1e-10. Both benchmark reports are
regenerated, which changes some headline ratios — Vern7 vs DP5 on the 6D orbit
is 1.1x, not 8.8x.
Breaking changes
Integrator::stepreturnsStepResult { y, err, dense }instead of a(y, err, dense)tupleCallbackis now a trait;FnCallbackholds the closure pairProblemis generic overC: Controller<T>, which is what makesPIDControllerusable — it previously could not be passed to aProblemPIDController'sbeta1/beta2/beta3are nowp_term/i_term/d_terma_tol,a_tol_full,r_tolandwith_callbackare consuming buildersnum-dual)Other
cargo clippy --all-targets -- -D warningsis clean (was 25 warnings)DormandPrince45gained a docstring; it notes that FSAL is a property of themethod that neither it nor BS3 currently exploits
(Jacobian computation) on how autodiff arrived by a different route
Verification
43 unit tests + 7 doctests; 38 + 5 with
--no-default-features. AD checkedthree ways: machine precision against an exactly-integrable quadrature
(
y' = p·t^4, matching to 1e-14), against the analytic sensitivity of a linearproblem, and on the orbit problem via
det(STM) = 1(Liouville) plus acentral-difference cross-check. Vern7's lazy extra stages are exercised in dual
arithmetic.
f64numerics are bit-identical to v0.3.1 for the genericisation itself —verified by step count and final state on three integrators before the
controller fix was applied.