130 lines
4.7 KiB
Markdown
130 lines
4.7 KiB
Markdown
# Feature: Tolerances Trait
|
|
|
|
**Status**: completed (2026-09-30)
|
|
|
|
## Overview
|
|
|
|
Error tolerances are set through inherent builder methods on each integrator
|
|
rather than through a trait:
|
|
|
|
```rust
|
|
impl<const D: usize> Vern7<D> where Vern7<D>: Integrator<D> {
|
|
pub fn a_tol(mut self, a_tol: f64) -> Self { .. }
|
|
pub fn a_tol_full(mut self, a_tol: SVector<f64, D>) -> Self { .. }
|
|
pub fn r_tol(mut self, r_tol: f64) -> Self { .. }
|
|
}
|
|
```
|
|
|
|
`DormandPrince45`, `BS3` and `Rosenbrock23` each have the same four methods,
|
|
written out separately. There is no trait bound that says "this integrator has
|
|
tolerances", so a downstream crate generic over `S: Integrator<D>` cannot set
|
|
them, and cannot read back what they were.
|
|
|
|
## Why This Feature Matters
|
|
|
|
A library wrapping this one wants to offer accuracy presets:
|
|
|
|
```rust
|
|
Propagation::new(..).accuracy(Accuracy::Medium) // sets a_tol and r_tol
|
|
Propagation::new(..).a_tol(1e-10)
|
|
```
|
|
|
|
That is impossible over a generic `S`. `astrokit` worked around it by declaring
|
|
its own trait and implementing it for three of the four integrators:
|
|
|
|
```rust
|
|
pub trait Tolerances<const D: usize>: Integrator<D> {
|
|
fn with_a_tol(self, a_tol: f64) -> Self;
|
|
fn with_a_tol_per_component(self, a_tol: SVector<f64, D>) -> Self;
|
|
fn with_r_tol(self, r_tol: f64) -> Self;
|
|
}
|
|
impl<const D: usize> Tolerances<D> for Vern7<D> { .. }
|
|
```
|
|
|
|
That works, but the trait belongs here. Downstream it has three problems: any
|
|
integrator added to this crate is invisible to it until the downstream crate is
|
|
updated, a second dependent would declare the same trait again, and the orphan
|
|
rule means only crates that own the trait can extend it.
|
|
|
|
Reading tolerances back matters separately. A run record that says which
|
|
integrator ran but not at what tolerance does not describe the run.
|
|
|
|
## Dependencies
|
|
|
|
None.
|
|
|
|
## Implementation Approach
|
|
|
|
A separate trait rather than more methods on `Integrator`. `Integrator` already
|
|
carries `const ADAPTIVE: bool` because not every method is adaptive, and a
|
|
fixed-step method has no tolerances to set. Splitting them keeps that honest and
|
|
leaves room for a fixed-step integrator that implements `Integrator` and not
|
|
this.
|
|
|
|
```rust
|
|
/// An adaptive integrator's error tolerances.
|
|
pub trait Tolerances<const D: usize>: Integrator<D> {
|
|
/// The absolute tolerance, per component.
|
|
fn absolute_tolerance(&self) -> SVector<f64, D>;
|
|
|
|
/// The relative tolerance.
|
|
fn relative_tolerance(&self) -> f64;
|
|
|
|
/// Sets the absolute tolerance for every component.
|
|
fn a_tol(self, a_tol: f64) -> Self;
|
|
|
|
/// Sets the absolute tolerance per component.
|
|
fn a_tol_full(self, a_tol: SVector<f64, D>) -> Self;
|
|
|
|
/// Sets the relative tolerance.
|
|
fn r_tol(self, r_tol: f64) -> Self;
|
|
}
|
|
```
|
|
|
|
The setters keep the names they already have, so **no existing call site
|
|
changes**. `Vern7::new().a_tol(1e-12).r_tol(1e-12)` continues to compile,
|
|
resolving through the trait instead of the inherent impl, because `Integrator` is
|
|
already in the prelude and `Tolerances` would be too.
|
|
|
|
The getters need different names because `a_tol` is taken by the setter. Naming
|
|
them `absolute_tolerance`/`relative_tolerance` avoids the collision and reads
|
|
better than `a_tol_value`.
|
|
|
|
### Steps
|
|
|
|
1. Declare `Tolerances<D>` in `src/integrator/mod.rs`.
|
|
2. For each of `Vern7`, `DormandPrince45`, `BS3`, `Rosenbrock23`: move the three
|
|
setter bodies from the inherent impl into an `impl Tolerances<D>`, and add the
|
|
two getters returning the stored fields. Keep `new()` inherent.
|
|
3. Export `Tolerances` from the prelude.
|
|
4. Delete `astrokit`'s local copy and import this one.
|
|
|
|
### What not to do
|
|
|
|
Do not add the tolerance methods to `Integrator` itself. That would oblige a
|
|
fixed-step method to implement setters that mean nothing, and `const ADAPTIVE`
|
|
exists precisely to admit such methods.
|
|
|
|
Do not add `Default` or `Copy` as supertraits of `Integrator` while doing this,
|
|
tempting as it is for downstream bounds. All four current integrators are both,
|
|
but a multistep method holding Nordsieck history would reasonably be neither, and
|
|
a downstream crate can bound on them explicitly where it needs them.
|
|
|
|
## Testing Requirements
|
|
|
|
- Every integrator round-trips: set a tolerance, read it back.
|
|
- `a_tol` sets all components; `a_tol_full` sets them independently.
|
|
- A generic function over `S: Tolerances<D>` compiles and changes step counts as
|
|
the tolerance changes, for each integrator.
|
|
- Existing tests continue to pass unchanged, which is the point: the setter names
|
|
do not move.
|
|
|
|
## Success Criteria
|
|
|
|
- A downstream crate generic over `S: Tolerances<D>` can set and read tolerances
|
|
for every adaptive integrator in this crate.
|
|
- No call site in this crate or its tests changes.
|
|
- `astrokit` deletes its local `Tolerances`.
|
|
|
|
**Effort**: Small
|