Added an ability for an owned callback
This commit is contained in:
1 parent
78ac6a60a0
commit
0e4a0b1798
4 files changed
+469
-7
No files matched your search
@@ -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
|
||||
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
|
||||
|
||||
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
|
||||
recorded, the controller reset across one, and a defined order for two events
|
||||
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
|
||||
swap mid-propagation
|
||||
- **Dependencies**: None
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
- 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.
|
||||
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
|
||||
S: Integrator<D>,
|
||||
C: Controller<T>,
|
||||
@@ -41,10 +90,10 @@ where
|
||||
ode: ODE<'a, T, D, P>,
|
||||
integrator: S,
|
||||
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
|
||||
S: Integrator<D> + Copy,
|
||||
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
|
||||
/// 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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -727,6 +852,187 @@ mod tests {
|
||||
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
|
||||
/// event every time the ball reaches the floor, and an effect that reverses
|
||||
/// and shrinks its velocity.
|
||||
|
||||
Reference in new issue
Block a user