Fixed event handling to properly keep the state changes, plus a few other fixes

This commit is contained in:
connor committed 2026-10-01 12:41:39 -04:00
1 parent 55884190fa
commit c5768ad9c0
8 files changed
+1064 -83

No files matched your search

+53 -3
View File
@@ -23,7 +23,8 @@ See [benchmark report](VERN7_BENCHMARK_REPORT.md) for detailed performance analy
### Other Features
- **Adaptive time stepping** with PI controller
- **Callback events** with zero-crossing detection
- **Callback events** with zero-crossing detection, whose effects can change the state, change the
parameters, or end the solution at the event
- **Dense output interpolation** at any time point
- **Parameters** in derivative and callback functions
- **Lazy computation** of extra interpolation stages (Vern7)
@@ -71,19 +72,68 @@ let ode = ODE::new(&derivative, 0.0, 6.3, y0, params);
let dp45 = DormandPrince45::new().a_tol(1e-12).r_tol(1e-6);
let controller = PIController::default();
let value_too_high = FnCallback {
let mut value_too_high = FnCallback {
event: &|t: f64, _y: &Vector2<f64>, _p: &Params| { 5.0 - t },
effect: &stop,
};
// Solve the problem
let mut problem = Problem::new(ode, dp45, controller).with_callback(&value_too_high);
let mut problem = Problem::new(ode, dp45, controller).with_callback(&mut value_too_high);
let solution = problem.solve();
// Can interpolate solutions to whatever you want
let _interpolated_answer = solution.interpolate(4.4);
```
## Events that change the state
A callback watches a function of the state for a zero crossing. When one is found the step is
shortened to land on the crossing, the state there is recorded, and the effect runs with the ODE at
that point — so the effect can move the state, and the integration continues from where it leaves it.
That is what it takes to integrate through a discontinuity. A rotating body parameterized by
Modified Rodrigues Parameters, for instance, has a singularity at a full turn, avoided by swapping
the parameter vector for `-sigma / |sigma|^2` — the same attitude, different parameters — whenever
`|sigma|` passes 1:
```rust
use nalgebra::{Matrix3, Vector3};
use ordinary_diffeq::prelude::*;
/// MRP kinematics for a constant angular velocity.
fn attitude_rate(_t: f64, s: Vector3<f64>, omega: &Vector3<f64>) -> Vector3<f64> {
let skew = Matrix3::new(0.0, -s[2], s[1], s[2], 0.0, -s[0], -s[1], s[0], 0.0);
let b = Matrix3::identity() * (1.0 - s.dot(&s)) + skew * 2.0 + s * s.transpose() * 2.0;
b * omega * 0.25
}
let mut shadow_set = FnCallback {
event: &|_t: f64, s: &Vector3<f64>, _p: &Vector3<f64>| 1.0 - s.norm(),
effect: &|ode: &mut ODE<'_, f64, 3, Vector3<f64>>| {
ode.y = -ode.y / ode.y.norm_squared();
},
};
let ode = ODE::new(&attitude_rate, 0.0, 5.0, Vector3::zeros(), Vector3::new(0.0, 0.0, 1.0));
let vern7 = Vern7::new().a_tol(1e-12).r_tol(1e-12);
let mut problem = Problem::new(ode, vern7, PIController::default())
.with_callback(&mut shadow_set);
let solution = problem.solve();
// The parameters never approach the singularity, having swapped at half a turn.
assert!(solution.states.iter().all(|s| s.norm() <= 1.0 + 1e-9));
```
A jump like that one puts the same time in the solution twice, holding the state on each side of it,
and makes the step size controller forget a history that described the solution before the jump.
Interpolating at the jump reports the state the effect left, the one the trajectory goes on with.
`effect` takes `&mut self`, so a callback that implements the trait itself rather than going through
`FnCallback` can record that it has fired. `stop` ends the solution at the crossing, with the
crossing as its last point and the problem's own `t_end` left alone.
If two events fall inside one step the earlier one fires and the step ends there; the later one is
found again on the step after it.
## Choosing the integrator at runtime
The integrator is normally a type parameter, fixed when the code is compiled. `Method` is an enum