Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17105964a2 | ||
|
|
0e4a0b1798 |
No files matched your search
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ordinary-diffeq"
|
name = "ordinary-diffeq"
|
||||||
version = "0.6.1"
|
version = "0.7.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Connor Johnstone"]
|
authors = ["Connor Johnstone"]
|
||||||
description = "A library for solving differential equations based on the DifferentialEquations.jl julia library."
|
description = "A library for solving differential equations based on the DifferentialEquations.jl julia library."
|
||||||
|
|||||||
@@ -134,6 +134,58 @@ 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
|
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.
|
found again on the step after it.
|
||||||
|
|
||||||
|
### Borrowed or owned
|
||||||
|
|
||||||
|
`with_callback` borrows, which is what lets you read a stateful callback's own fields once the solve
|
||||||
|
is done:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
# use nalgebra::Vector1;
|
||||||
|
# use ordinary_diffeq::prelude::*;
|
||||||
|
struct Count {
|
||||||
|
fired: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Callback<f64, 1, ()> for Count {
|
||||||
|
fn event(&self, t: f64, _y: &Vector1<f64>, _p: &()) -> f64 {
|
||||||
|
1.0 - t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn effect(&mut self, _ode: &mut ODE<'_, f64, 1, ()>) {
|
||||||
|
self.fired += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn derivative(_t: f64, _y: Vector1<f64>, _p: &()) -> Vector1<f64> {
|
||||||
|
Vector1::new(1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
let ode = ODE::new(&derivative, 0.0, 2.0, Vector1::new(0.0), ());
|
||||||
|
let mut counter = Count { fired: 0 };
|
||||||
|
let mut problem = Problem::new(ode, DormandPrince45::new(), PIController::default())
|
||||||
|
.with_callback(&mut counter);
|
||||||
|
problem.solve();
|
||||||
|
|
||||||
|
assert_eq!(counter.fired, 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
`with_owned_callback` takes a `Box` instead, for a caller that has nothing to lend. A type that wraps
|
||||||
|
this one has to keep the `Problem` in a field, since `solve` hands back a solution borrowing from it,
|
||||||
|
and it then cannot also hold a callback in a field and lend it to the problem — that is a
|
||||||
|
self-referential struct, and moving the wrapper would dangle the reference. Moving the callback into
|
||||||
|
the problem sidesteps it:
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
let problem = Problem::new(ode, integrator, controller)
|
||||||
|
.with_owned_callback(Box::new(my_callback));
|
||||||
|
self.problem = Some(problem);
|
||||||
|
self.problem.as_mut().unwrap().solve()
|
||||||
|
```
|
||||||
|
|
||||||
|
An owned callback may hold borrows of its own, so a boxed `FnCallback` works. Nothing can read its
|
||||||
|
state afterwards; a callback with something to report either stays borrowed, or shares an
|
||||||
|
`Rc<Cell<_>>` with its owner, which can be read while the solution is still alive.
|
||||||
|
|
||||||
## Choosing the integrator at runtime
|
## Choosing the integrator at runtime
|
||||||
|
|
||||||
The integrator is normally a type parameter, fixed when the code is compiled. `Method` is an enum
|
The integrator is normally a type parameter, fixed when the code is compiled. `Method` is an enum
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ Each feature below links to a detailed implementation plan in the `features/` di
|
|||||||
- `effect(&mut self)`, termination at the crossing, both sides of a jump
|
- `effect(&mut self)`, termination at the crossing, both sides of a jump
|
||||||
recorded, the controller reset across one, and a defined order for two events
|
recorded, the controller reset across one, and a defined order for two events
|
||||||
in one step
|
in one step
|
||||||
|
- extended in 0.7.0 so a `Problem` can own a callback as well as borrow one,
|
||||||
|
which is what a type that stores the problem needs
|
||||||
- Raised by `astrokit`, which needs a Modified Rodrigues Parameter shadow set
|
- Raised by `astrokit`, which needs a Modified Rodrigues Parameter shadow set
|
||||||
swap mid-propagation
|
swap mid-propagation
|
||||||
- **Dependencies**: None
|
- **Dependencies**: None
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Feature: Callback Effects That Can Change the State
|
# Feature: Callback Effects That Can Change the State
|
||||||
|
|
||||||
**Status**: completed (2026-10-01)
|
**Status**: completed (2026-10-01), extended with owned callbacks (2026-10-01)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -166,6 +166,108 @@ Brent bracketing a root at `h = 0`; and refusing a located crossing below some
|
|||||||
`h`, which is the same arbitrary fraction one step further down. If something
|
`h`, which is the same arbitrary fraction one step further down. If something
|
||||||
cleaner turns up, this is the place to change it.
|
cleaner turns up, this is the place to change it.
|
||||||
|
|
||||||
|
## Follow-up: a problem can own a callback (0.7.0)
|
||||||
|
|
||||||
|
`Problem` held `Vec<&'a mut dyn Callback>`, so something else had to own each
|
||||||
|
callback and outlive the problem. That is fine from a function body and impossible
|
||||||
|
from a type that stores the problem — and a wrapper must store it, because
|
||||||
|
`solve(&mut self) -> Solution<'_, ..>` hands back a solution that borrows from it,
|
||||||
|
so the problem has to live somewhere that outlives the solution, and a field is
|
||||||
|
the only such place. Such a wrapper could not then register a callback of its own:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
struct Wrapper<'a, ..> {
|
||||||
|
my_callback: MyCallback, // owned here
|
||||||
|
problem: Option<Problem<'a, ..>>, // and borrowed from, here
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
That is self-referential, and moving the wrapper would dangle the reference, so
|
||||||
|
the compiler is right to reject it. The workaround in the dependent crate was a
|
||||||
|
zero-sized callback, leaked to get a `&'static mut`.
|
||||||
|
|
||||||
|
`with_owned_callback(Box<dyn Callback<T, D, P> + 'o>)` is the addition. Storage is
|
||||||
|
one list of either kind:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
enum Registered<'a, 'o, T: Scalar, const D: usize, P> {
|
||||||
|
Borrowed(&'a mut dyn Callback<T, D, P>),
|
||||||
|
Owned(Box<dyn Callback<T, D, P> + 'o>),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`Registered` implements `Callback` itself, forwarding both methods, which is why
|
||||||
|
none of the event handling in `solve` changed: the immutable scan for the earliest
|
||||||
|
crossing goes through `event` and the single effect that fires goes through
|
||||||
|
`effect`, whichever kind is holding it. Root finding, jump recording, the
|
||||||
|
controller reset and `EVENT_PROBE_FRACTION` are untouched.
|
||||||
|
|
||||||
|
### The second lifetime, which was not in the plan
|
||||||
|
|
||||||
|
`Problem` gained a lifetime parameter: `Problem<'a, 'o, T, D, S, P, C>`. This was
|
||||||
|
meant to be purely additive and it cannot be, for a reason worth writing down.
|
||||||
|
|
||||||
|
Dropping a `Box<dyn Callback>` runs a destructor the compiler cannot see into, so
|
||||||
|
dropck requires everything that box's lifetime covers to be alive when the problem
|
||||||
|
is dropped. With one lifetime for both kinds, that requirement lands on the
|
||||||
|
borrowed callbacks too — and it is type-based, so it applies even to a problem that
|
||||||
|
owns no callback at all. The first attempt, a single `'a`, failed exactly here:
|
||||||
|
|
||||||
|
```
|
||||||
|
error[E0502]: cannot borrow `once.fired` as immutable because it is also borrowed as mutable
|
||||||
|
|
|
||||||
|
| mutable borrow might be used here, when `problem` is dropped
|
||||||
|
```
|
||||||
|
|
||||||
|
That is `test_callback_can_remember_firing`, reading a borrowed callback's own
|
||||||
|
state after the solve, which is the only way a caller learns what a stateful
|
||||||
|
callback did and the whole reason the borrowed form exists. Keeping it means the
|
||||||
|
owned box needs a lifetime of its own, so that only `'o` has to outlive the drop
|
||||||
|
and NLL can still end a borrowed callback's borrow at the problem's last use.
|
||||||
|
|
||||||
|
The cost is arity: code that names the type explicitly needs
|
||||||
|
`Problem<'a, '_, ..>`. Both lifetimes are inferred, so nothing that only calls
|
||||||
|
`Problem::new(..).with_callback(..).solve()` is affected.
|
||||||
|
|
||||||
|
### Not done: getting owned callbacks back
|
||||||
|
|
||||||
|
Deliberately skipped. Downcasting a `Box<dyn Callback>` to its concrete type needs
|
||||||
|
`Any`, which means a `'static` bound and an `as_any` method on every implementor —
|
||||||
|
a cost paid by all callbacks for a need that has two cheaper answers already: stay
|
||||||
|
borrowed, or share an `Rc<Cell<_>>`, which can be read while the solution is still
|
||||||
|
alive.
|
||||||
|
|
||||||
|
### Measurement
|
||||||
|
|
||||||
|
The benches register no callbacks, so what they exercise is the list's element type
|
||||||
|
changing from a reference to a two-variant enum, on a list that is empty. Measured
|
||||||
|
by alternating old and new rather than running one after the other, because the
|
||||||
|
first attempt at it reported a 4% improvement, which nothing in this change could
|
||||||
|
produce: between-sitting drift on this machine is larger than the effect being
|
||||||
|
looked for.
|
||||||
|
|
||||||
|
| bench | old | new |
|
||||||
|
|---|---|---|
|
||||||
|
| `bench_orbit` | 356.06, 358.91 us | 357.98, 355.18 us |
|
||||||
|
| `bench_simple_1d` | 877.7, 881.1 ns | 877.4, 895.7 ns |
|
||||||
|
| `bench_interpolation_1d` | 2.2683, 2.2104 us | 2.2444, 2.2496 us |
|
||||||
|
|
||||||
|
Two runs of the same binary differ by up to 2.6% here, and the old and new ranges
|
||||||
|
overlap on all three. No measurable change.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- an owned callback producing the same times and states as the identical borrowed
|
||||||
|
one, element for element;
|
||||||
|
- an owned stateful callback escalating on its second firing, read off the
|
||||||
|
trajectory since its state cannot be read directly;
|
||||||
|
- owned and borrowed callbacks on one problem with their events at the same time,
|
||||||
|
where the final state distinguishes registration order, and the borrowed one's
|
||||||
|
count is read after the solve;
|
||||||
|
- a boxed `FnCallback`, which holds two closure references, confirming the `+ 'o`
|
||||||
|
rather than `'static`;
|
||||||
|
- the wrapper from the top of this section, compiling and solving.
|
||||||
|
|
||||||
## Known Limitations
|
## Known Limitations
|
||||||
|
|
||||||
- An even number of crossings inside one step is still missed: the two ends of
|
- An even number of crossings inside one step is still missed: the two ends of
|
||||||
|
|||||||
+312
-6
@@ -33,7 +33,56 @@ use super::scalar::Scalar;
|
|||||||
/// the event.
|
/// the event.
|
||||||
const EVENT_PROBE_FRACTION: f64 = 1e-6;
|
const EVENT_PROBE_FRACTION: f64 = 1e-6;
|
||||||
|
|
||||||
pub struct Problem<'a, T: Scalar, const D: usize, S, P, C>
|
/// A callback a [`Problem`] watches, which the problem either borrows or owns.
|
||||||
|
///
|
||||||
|
/// Borrowing is what lets a caller read a stateful callback's own fields after
|
||||||
|
/// the solve; owning is what lets a struct that stores the problem register a
|
||||||
|
/// callback of its own, which it cannot do by reference without becoming
|
||||||
|
/// self-referential.
|
||||||
|
///
|
||||||
|
/// Implementing [`Callback`] on this, rather than reaching inside it, is what
|
||||||
|
/// keeps the event handling in [`Problem::solve`] written once: the immutable
|
||||||
|
/// scan for a crossing goes through `event` and the one effect that fires goes
|
||||||
|
/// through `effect`, whichever kind is holding it.
|
||||||
|
///
|
||||||
|
/// The two lifetimes are separate on purpose. Dropping a `Box<dyn Callback>` runs
|
||||||
|
/// a destructor the compiler cannot see into, so everything that box's lifetime
|
||||||
|
/// covers has to still be alive when the problem is dropped. Were that the same
|
||||||
|
/// lifetime as the borrowed callbacks', a borrowed callback could not be read
|
||||||
|
/// back until the problem went out of scope, and reading one back is the whole
|
||||||
|
/// reason the borrowed form exists.
|
||||||
|
enum Registered<'a, 'o, T: Scalar, const D: usize, P> {
|
||||||
|
Borrowed(&'a mut dyn Callback<T, D, P>),
|
||||||
|
Owned(Box<dyn Callback<T, D, P> + 'o>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Scalar, const D: usize, P> Callback<T, D, P> for Registered<'_, '_, T, D, P> {
|
||||||
|
#[inline]
|
||||||
|
fn event(&self, t: T, y: &SVector<T, D>, params: &P) -> T {
|
||||||
|
match self {
|
||||||
|
Registered::Borrowed(callback) => callback.event(t, y, params),
|
||||||
|
Registered::Owned(callback) => callback.event(t, y, params),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn effect(&mut self, ode: &mut ODE<'_, T, D, P>) {
|
||||||
|
match self {
|
||||||
|
Registered::Borrowed(callback) => callback.effect(ode),
|
||||||
|
Registered::Owned(callback) => callback.effect(ode),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An ODE, an integrator, a controller and any callbacks, ready to solve.
|
||||||
|
///
|
||||||
|
/// The first lifetime is the problem's own borrows: the right-hand side, and any
|
||||||
|
/// callback registered with [`with_callback`](Problem::with_callback). The second
|
||||||
|
/// is what an owned callback from
|
||||||
|
/// [`with_owned_callback`](Problem::with_owned_callback) is allowed to borrow.
|
||||||
|
/// Both are inferred, so `Problem<'_, '_, f64, 3, _, _, _>` is the usual way to
|
||||||
|
/// name the type when it has to be named at all.
|
||||||
|
pub struct Problem<'a, 'o, T: Scalar, const D: usize, S, P, C>
|
||||||
where
|
where
|
||||||
S: Integrator<D>,
|
S: Integrator<D>,
|
||||||
C: Controller<T>,
|
C: Controller<T>,
|
||||||
@@ -41,10 +90,10 @@ where
|
|||||||
ode: ODE<'a, T, D, P>,
|
ode: ODE<'a, T, D, P>,
|
||||||
integrator: S,
|
integrator: S,
|
||||||
controller: C,
|
controller: C,
|
||||||
callbacks: Vec<&'a mut dyn Callback<T, D, P>>,
|
callbacks: Vec<Registered<'a, 'o, T, D, P>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, T: Scalar, const D: usize, S, P, C> Problem<'a, T, D, S, P, C>
|
impl<'a, 'o, T: Scalar, const D: usize, S, P, C> Problem<'a, 'o, T, D, S, P, C>
|
||||||
where
|
where
|
||||||
S: Integrator<D> + Copy,
|
S: Integrator<D> + Copy,
|
||||||
C: Controller<T>,
|
C: Controller<T>,
|
||||||
@@ -238,12 +287,88 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Registers a callback.
|
/// Registers a borrowed callback.
|
||||||
///
|
///
|
||||||
/// Callbacks are watched in the order they are registered, which decides
|
/// Callbacks are watched in the order they are registered, which decides
|
||||||
/// which one fires when two events land at the same time.
|
/// which one fires when two events land at the same time, whether they are
|
||||||
|
/// borrowed or owned.
|
||||||
|
///
|
||||||
|
/// Borrowing means something else owns the callback and outlives the problem,
|
||||||
|
/// and it is what lets a caller read the callback's own state once the solve
|
||||||
|
/// is done:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use nalgebra::Vector1;
|
||||||
|
/// # use ordinary_diffeq::prelude::*;
|
||||||
|
/// struct Count {
|
||||||
|
/// fired: usize,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl Callback<f64, 1, ()> for Count {
|
||||||
|
/// fn event(&self, t: f64, _y: &Vector1<f64>, _p: &()) -> f64 {
|
||||||
|
/// 1.0 - t
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn effect(&mut self, _ode: &mut ODE<'_, f64, 1, ()>) {
|
||||||
|
/// self.fired += 1;
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn derivative(_t: f64, _y: Vector1<f64>, _p: &()) -> Vector1<f64> {
|
||||||
|
/// Vector1::new(1.0)
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// let ode = ODE::new(&derivative, 0.0, 2.0, Vector1::new(0.0), ());
|
||||||
|
/// let mut counter = Count { fired: 0 };
|
||||||
|
/// let mut problem = Problem::new(ode, DormandPrince45::new(), PIController::default())
|
||||||
|
/// .with_callback(&mut counter);
|
||||||
|
/// problem.solve();
|
||||||
|
///
|
||||||
|
/// // The borrow ended with the problem's last use, so the state is readable.
|
||||||
|
/// assert_eq!(counter.fired, 1);
|
||||||
|
/// ```
|
||||||
pub fn with_callback(mut self, callback: &'a mut dyn Callback<T, D, P>) -> Self {
|
pub fn with_callback(mut self, callback: &'a mut dyn Callback<T, D, P>) -> Self {
|
||||||
self.callbacks.push(callback);
|
self.callbacks.push(Registered::Borrowed(callback));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registers a callback the problem owns.
|
||||||
|
///
|
||||||
|
/// For a caller that cannot lend one: a struct that stores the problem in a
|
||||||
|
/// field — which it must, since [`solve`](Problem::solve) returns a solution
|
||||||
|
/// borrowing from it — cannot also hold the callback in a field and lend it
|
||||||
|
/// to the problem, because that makes the struct self-referential and moving
|
||||||
|
/// it would dangle the reference. Moving the callback into the problem avoids
|
||||||
|
/// the question.
|
||||||
|
///
|
||||||
|
/// The `+ 'a` is deliberate: an owned callback may hold borrows of its own,
|
||||||
|
/// which is exactly what [`FnCallback`](crate::callback::FnCallback) is.
|
||||||
|
///
|
||||||
|
/// Nothing can read an owned callback's state afterwards. A callback that has
|
||||||
|
/// something to report either stays borrowed, or shares a handle — an
|
||||||
|
/// `Rc<Cell<_>>` — with its owner, which works while the solution is still
|
||||||
|
/// alive.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use nalgebra::Vector1;
|
||||||
|
/// # use ordinary_diffeq::prelude::*;
|
||||||
|
/// # fn derivative(_t: f64, _y: Vector1<f64>, _p: &()) -> Vector1<f64> {
|
||||||
|
/// # Vector1::new(1.0)
|
||||||
|
/// # }
|
||||||
|
/// let ode = ODE::new(&derivative, 0.0, 10.0, Vector1::new(0.0), ());
|
||||||
|
/// let jump = FnCallback {
|
||||||
|
/// event: &|_t: f64, y: &Vector1<f64>, _p: &()| 5.0 - y[0],
|
||||||
|
/// effect: &|ode: &mut ODE<'_, f64, 1, ()>| ode.y = Vector1::new(100.0),
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// let mut problem = Problem::new(ode, DormandPrince45::new(), PIController::default())
|
||||||
|
/// .with_owned_callback(Box::new(jump));
|
||||||
|
/// let solution = problem.solve();
|
||||||
|
///
|
||||||
|
/// assert!((solution.states.last().unwrap()[0] - 105.0).abs() < 1e-9);
|
||||||
|
/// ```
|
||||||
|
pub fn with_owned_callback(mut self, callback: Box<dyn Callback<T, D, P> + 'o>) -> Self {
|
||||||
|
self.callbacks.push(Registered::Owned(callback));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -727,6 +852,187 @@ mod tests {
|
|||||||
assert_eq!(resets_during(&|_ode: &mut ODE<'_, f64, 1, ()>| {}), 0);
|
assert_eq!(resets_during(&|_ode: &mut ODE<'_, f64, 1, ()>| {}), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An owned callback does what the same callback does when borrowed: same
|
||||||
|
/// crossings, same jumps, same trajectory.
|
||||||
|
#[test]
|
||||||
|
fn test_owned_callback_matches_borrowed() {
|
||||||
|
fn jump_at_five<'c>() -> FnCallback<'c, f64, 1, ()> {
|
||||||
|
FnCallback {
|
||||||
|
event: &|_t: f64, y: &Vector1<f64>, _p: &()| 5.0 - y[0],
|
||||||
|
effect: &|ode: &mut ODE<'_, f64, 1, ()>| ode.y = Vector1::new(100.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let borrowed = {
|
||||||
|
let ode = ODE::new(&unit_rate, 0.0, 10.0, Vector1::new(0.0), ());
|
||||||
|
let mut callback = jump_at_five();
|
||||||
|
let mut problem = Problem::new(ode, DormandPrince45::new(), PIController::default())
|
||||||
|
.with_callback(&mut callback);
|
||||||
|
let solution = problem.solve();
|
||||||
|
(solution.times.clone(), solution.states.clone())
|
||||||
|
};
|
||||||
|
|
||||||
|
let owned = {
|
||||||
|
let ode = ODE::new(&unit_rate, 0.0, 10.0, Vector1::new(0.0), ());
|
||||||
|
let mut problem = Problem::new(ode, DormandPrince45::new(), PIController::default())
|
||||||
|
.with_owned_callback(Box::new(jump_at_five()));
|
||||||
|
let solution = problem.solve();
|
||||||
|
(solution.times.clone(), solution.states.clone())
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(owned.0, borrowed.0);
|
||||||
|
assert_eq!(owned.1, borrowed.1);
|
||||||
|
assert_relative_eq!(owned.1.last().unwrap()[0], 105.0, max_relative = 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An owned callback keeps state across its firings, even though nothing can
|
||||||
|
/// read that state afterwards: what it did shows up in the trajectory.
|
||||||
|
#[test]
|
||||||
|
fn test_owned_stateful_callback() {
|
||||||
|
struct Escalate {
|
||||||
|
fired: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Callback<f64, 1, ()> for Escalate {
|
||||||
|
fn event(&self, t: f64, _y: &Vector1<f64>, _p: &()) -> f64 {
|
||||||
|
// Crosses at t = 1 and t = 3.
|
||||||
|
(t * PI / 2.0).cos()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn effect(&mut self, ode: &mut ODE<'_, f64, 1, ()>) {
|
||||||
|
self.fired += 1;
|
||||||
|
ode.y += Vector1::new(if self.fired == 1 { 10.0 } else { 100.0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ode = ODE::new(&unit_rate, 0.0, 4.0, Vector1::new(0.0), ());
|
||||||
|
// Capped so one step cannot swallow two crossings; see the borrowed
|
||||||
|
// equivalent in `test_callback_can_remember_firing`.
|
||||||
|
let controller = PIController::new(0.17, 0.04, 10.0, 0.2, 1.0, 0.9, 1e-4);
|
||||||
|
let mut problem = Problem::new(ode, DormandPrince45::new(), controller)
|
||||||
|
.with_owned_callback(Box::new(Escalate { fired: 0 }));
|
||||||
|
let solution = problem.solve();
|
||||||
|
|
||||||
|
// 1 + 10, carried to 13, then + 100, carried to 114. Had the second
|
||||||
|
// firing added 10 as well it would end at 24.
|
||||||
|
assert_relative_eq!(
|
||||||
|
solution.states.last().unwrap()[0],
|
||||||
|
114.0,
|
||||||
|
max_relative = 1e-9
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Owned and borrowed callbacks on one problem. Both events land at the same
|
||||||
|
/// time, so registration order decides which effect applies first, and the
|
||||||
|
/// borrowed one's state is still readable once the solve is done.
|
||||||
|
#[test]
|
||||||
|
fn test_owned_and_borrowed_on_one_problem() {
|
||||||
|
struct AddOne {
|
||||||
|
fired: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Callback<f64, 1, ()> for AddOne {
|
||||||
|
fn event(&self, t: f64, _y: &Vector1<f64>, _p: &()) -> f64 {
|
||||||
|
2.0 - t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn effect(&mut self, ode: &mut ODE<'_, f64, 1, ()>) {
|
||||||
|
self.fired += 1;
|
||||||
|
ode.y += Vector1::new(1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let double = FnCallback {
|
||||||
|
event: &|t: f64, _y: &Vector1<f64>, _p: &()| 2.0 - t,
|
||||||
|
effect: &|ode: &mut ODE<'_, f64, 1, ()>| ode.y *= 2.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let ode = ODE::new(&unit_rate, 0.0, 4.0, Vector1::new(0.0), ());
|
||||||
|
let mut add_one = AddOne { fired: 0 };
|
||||||
|
let mut problem = Problem::new(ode, DormandPrince45::new(), PIController::default())
|
||||||
|
.with_owned_callback(Box::new(double))
|
||||||
|
.with_callback(&mut add_one);
|
||||||
|
let solution = problem.solve();
|
||||||
|
let last = solution.states.last().unwrap()[0];
|
||||||
|
|
||||||
|
// y = 2 at the crossing, doubled to 4 by the owned callback, then 5 from
|
||||||
|
// the borrowed one, then two more units of integration. The other order
|
||||||
|
// would give 8.
|
||||||
|
assert_relative_eq!(last, 7.0, max_relative = 1e-9);
|
||||||
|
assert_eq!(add_one.fired, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An owned callback may hold borrows of its own, which is what `FnCallback`
|
||||||
|
/// is: a pair of closure references. The box is not `'static`.
|
||||||
|
#[test]
|
||||||
|
fn test_owned_callback_holding_a_borrow() {
|
||||||
|
let threshold = 5.0;
|
||||||
|
let target = Vector1::new(100.0);
|
||||||
|
let event = move |_t: f64, y: &Vector1<f64>, _p: &()| threshold - y[0];
|
||||||
|
let effect = move |ode: &mut ODE<'_, f64, 1, ()>| ode.y = target;
|
||||||
|
|
||||||
|
let ode = ODE::new(&unit_rate, 0.0, 10.0, Vector1::new(0.0), ());
|
||||||
|
let mut problem = Problem::new(ode, DormandPrince45::new(), PIController::default())
|
||||||
|
.with_owned_callback(Box::new(FnCallback {
|
||||||
|
event: &event,
|
||||||
|
effect: &effect,
|
||||||
|
}));
|
||||||
|
let solution = problem.solve();
|
||||||
|
|
||||||
|
assert_relative_eq!(
|
||||||
|
solution.states.last().unwrap()[0],
|
||||||
|
105.0,
|
||||||
|
max_relative = 1e-9
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The case owned callbacks exist for: a type that stores the problem,
|
||||||
|
/// because the solution borrows from it, and registers a callback of its own.
|
||||||
|
/// By reference that would make the wrapper self-referential and would not
|
||||||
|
/// compile.
|
||||||
|
#[test]
|
||||||
|
fn test_wrapper_owning_both_problem_and_callback() {
|
||||||
|
struct Shift;
|
||||||
|
|
||||||
|
impl Callback<f64, 1, ()> for Shift {
|
||||||
|
fn event(&self, _t: f64, y: &Vector1<f64>, _p: &()) -> f64 {
|
||||||
|
5.0 - y[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn effect(&mut self, ode: &mut ODE<'_, f64, 1, ()>) {
|
||||||
|
ode.y = Vector1::new(100.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Run<'a> = Problem<'a, 'a, f64, 1, DormandPrince45<1>, (), PIController<f64>>;
|
||||||
|
|
||||||
|
struct Wrapper<'a> {
|
||||||
|
problem: Option<Run<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Wrapper<'a> {
|
||||||
|
fn solve(
|
||||||
|
&mut self,
|
||||||
|
ode: ODE<'a, f64, 1, ()>,
|
||||||
|
) -> Solution<'_, f64, DormandPrince45<1>, 1, ()> {
|
||||||
|
let problem = Problem::new(ode, DormandPrince45::new(), PIController::default())
|
||||||
|
.with_owned_callback(Box::new(Shift));
|
||||||
|
self.problem = Some(problem);
|
||||||
|
self.problem.as_mut().unwrap().solve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut wrapper = Wrapper { problem: None };
|
||||||
|
let ode = ODE::new(&unit_rate, 0.0, 10.0, Vector1::new(0.0), ());
|
||||||
|
let solution = wrapper.solve(ode);
|
||||||
|
|
||||||
|
assert_relative_eq!(
|
||||||
|
solution.states.last().unwrap()[0],
|
||||||
|
105.0,
|
||||||
|
max_relative = 1e-9
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// A bouncing ball, which is the standard exercise of repeated jumps: an
|
/// A bouncing ball, which is the standard exercise of repeated jumps: an
|
||||||
/// event every time the ball reaches the floor, and an effect that reverses
|
/// event every time the ball reaches the floor, and an effect that reverses
|
||||||
/// and shrinks its velocity.
|
/// and shrinks its velocity.
|
||||||
|
|||||||
Reference in new issue
Block a user