Expand description
Enum-dispatched ODE integration — solver choice and system ownership.
§What this adds over the bare steppers
Euler, Rkf45 and Rosenbrock23 each integrate a system you hand
them by reference on every call. That is fine inside one function, but a
constitutive law or a solver loop usually wants to store “the integrator
for this material point” — solver plus system together — and step it later.
Storing a borrow would require a lifetime parameter on the storing struct,
which the workspace design rules forbid outright.
This module removes the need for one. Two enums, no lifetimes anywhere:
OdeSolver— which stepper. A closed set (Euler,Rkf45,Rosenbrock23), so the scheme can be chosen at run time without a trait object and without heap allocation. Adding a stepper forces everymatchsite to be updated.OdeIntegrator— how the system is supplied. Two variants, and they are the whole point of this module:OdeIntegrator::TypedStateowns a concrete, statically-known system by value. Derivative calls are statically dispatched and inlinable. This is the preferred variant.OdeIntegrator::DynSystemholds the system behindSharedOdeSystem(Arc<dyn OdeSystem + Send + Sync>). It exists by maintainer decision, for flexibility, so a caller that genuinely does not know the system type at the call site — a registry, a case-file reader, a test harness sweeping several systems — has a path that does not require inventing an enum of its own.
§Why there is no lifetime parameter
Both variants own their system: S by value, or shared ownership
through Arc. Nothing here borrows the system, so nothing here needs to
name the region the borrow is valid for. An OdeIntegrator can therefore
be stored in a struct, moved between threads, or held in a Vec for one
integrator per material point, with no lifetime plumbing at any call site.
§On the dyn in OdeIntegrator::DynSystem
The workspace rule is “no trait objects for dispatch — use enums”, and the
dispatch here is the enum: OdeIntegrator chooses between two owning
strategies by match, exhaustively. The Arc<dyn OdeSystem + Send + Sync>
inside one of its variants is a boundary coercion for callers who ask for
it, kept deliberately, not an accident to be cleaned up later. Prefer
OdeIntegrator::TypedState; reach for OdeIntegrator::DynSystem when
the type genuinely is not known statically.
§Example — the preferred typed path
use outram_foam_basic_lib::ode::{OdeIntegrator, OdeSolver, OdeSystem};
struct Decay;
impl OdeSystem for Decay {
fn n_eqns(&self) -> usize { 1 }
fn derivatives(&self, _x: f64, y: &[f64], dydx: &mut Vec<f64>) {
dydx[0] = -y[0];
}
}
// Owned by value — no borrow, no lifetime, storable in any struct.
let mut integrator = OdeIntegrator::typed(Decay, OdeSolver::rkf45(1, 1e-10, 1e-8));
let mut y = vec![1.0_f64];
let mut dx = 0.1;
integrator.integrate(0.0, 1.0, &mut y, &mut dx).unwrap();
assert!((y[0] - (-1.0_f64).exp()).abs() < 1e-8);§Example — the shared dyn path
use std::sync::Arc;
use outram_foam_basic_lib::ode::{OdeIntegrator, OdeSolver, OdeSystem, SharedOdeSystem};
struct Decay;
impl OdeSystem for Decay {
fn n_eqns(&self) -> usize { 1 }
fn derivatives(&self, _x: f64, y: &[f64], dydx: &mut Vec<f64>) {
dydx[0] = -y[0];
}
}
let shared: SharedOdeSystem = Arc::new(Decay);
let mut integrator = OdeIntegrator::shared(shared, OdeSolver::rkf45(1, 1e-10, 1e-8));
let mut y = vec![1.0_f64];
let mut dx = 0.1;
integrator.integrate(0.0, 1.0, &mut y, &mut dx).unwrap();
assert!((y[0] - (-1.0_f64).exp()).abs() < 1e-8);Structs§
- DynSystem
Integrator - Solver plus a shared, runtime-typed system.
- NoTyped
System - The zero-equation system, and the default type argument of
OdeIntegrator. - Typed
State Integrator - Solver plus a statically-typed, owned system — the preferred variant of
OdeIntegrator.
Enums§
- OdeIntegrator
- Enum-dispatch wrapper over the two ways of owning an ODE system.
- OdeSolver
- Which stepper integrates the system — enum dispatch over the closed set of solvers this crate ports.
Type Aliases§
- Shared
OdeSystem - A shared, runtime-typed ODE system.