I am writing a solver for systems of (delay-)coupled differential equations i.e. "integrating the equations". Similar to the Lorenz-System (no delay feedback) or the Mackey-Glass-system (with delay-feedback), but with (potentially) multiple of such systems that feedback into each other either directly or after a certain delay. The feedback is through a complex network topology. I started using traits in order to restrict how a certain system can be defined so that it can work with the rest of my code (using Runge-Kutta methods for example). In general I have two kinds of systems - with and without delay (for which I have some special cases each that I'd like to optimize the integration process a little). Right now I feel I have to write everything two or more times and this is inefficient hard to understand.
Every "dynamical system" has a "state" (think of a vector of coordinates) and a function f(state) which gives the derivative for a state. Think about it like a vector-field that attaches an arrow to every point in space. Model
is just a set of parameters important for f
, that characterize a given system.
My two traits I thought of:
pub trait DynamicalSystem {
type StateT;
type ModelT; // the model parameters
fn f(state: &StateT, model: &ModelT) -> StateT;
}
pub trait DynamicalDelaySystem {
type StateT;
type ModelT;
type DelayT;
fn f(state: &StateT, model: &ModelT, delay: &DelayT) -> StateT;
fn keep_delay(state: &StateT) -> DelayT; // keep *something* for the feedback
}
If I am integrating this with the Euler method I need to define two integrations:
euler(state: &mut StateT, model: &ModelT, dt: &f64, f: fn(&StateT, &ModelT) -> StateT) {
*state += f(&state, &model) * dt;
}
euler_delay(state: &mut StateT, model: &ModelT, delay: &DelayT, dt: &f64, f: fn(&StateT, &ModelT) -> StateT, keep_delay: fn(&StateT) -> DelayT) {
*state += f(&state, &model, &delay) * dt;
keep_delay(state); // some function that puts the delay into some container e.g. a ringbuffer for later use
}
My idea was to write an "Integrator"-struct that keeps a vector of StateT
's, ModelT
's and so on. But I need to do it twice - once with and once without delay.
struct Integrator<SystemT> {
dt: f64,
states: Vec<SystemT::StateT>,
models: Vec<SystemT::ModelT>,
}
impl<SystemT> Integrator<SystemT> {
fn integration_step(&mut self) {
for (s, m) in &mut self.states.iter_mut().zip(self.models) {
euler_step(&mut s, &m, &dt);
}
struct DelayIntegrator<SystemT> {
// the same as `Integrator<SystemT>`
history: Vec<Vec<SystemT::DelayT>>,
}
impl<SystemT> DelayIntegrator<SystemT> {
// everything again, but retrieve delayed values and save new values after integration step.
}
This seems kind of redundant. Even worse: Besides the general case I have special cases that I want to address (i.e. optimize for):
integration_step
that I set in the new
-method of the Integrator. Depending on which case of the system is setup (number of systems, number of model parameters). But function pointers seem like a hack that would be slow.What general advice for refactorization / more "DRY" code would you recommend here? I am unsure if I understand the full potential of traits, yet. Especially combining multiple traits instead of writing everything into one is difficult for me to conceive. Could I split this out into multiple traits like DelayFeedback
, InstantFeedback
, NoFeedback
, SingleSystem
, MultipleIdenticalSystems
and MultipleDistinctSystems
? I seem unable to use traits to enforce the existence of a structs member-variable e.g. the delay-container.
It seems to me that a DynamicalSystem
is a special case of a DynamicalDelaySystem
where DelayT = ()
. That observation may let you eliminate the DynamicalSystem
trait.
pub trait DynamicalSystem {
type StateT;
type ModelT;
type DelayT;
fn next(state: &Self::StateT, model: &Self::ModelT, delay: &Self::DelayT) -> Self::StateT;
fn keep_delay(state: &Self::StateT) -> Self::DelayT;
}
impl DynamicalSystem for usize {
type StateT = usize;
type ModelT = usize;
type DelayT = (); // there is no delay
fn next(state: &Self::StateT, model: &Self::ModelT, delay: &Self::DelayT) -> Self::StateT {
*state
}
fn keep_delay(state: &Self::StateT) -> Self::DelayT {
()
}
}
Or maybe try the experimental trait alias feature:
#![feature(trait_alias)]
pub trait DynamicalSystem = DynamicalDelaySystem<DelayT = ()>;
pub trait DynamicalDelaySystem {
type StateT;
type ModelT;
type DelayT;
fn next(state: &Self::StateT, model: &Self::ModelT, delay: &Self::DelayT) -> Self::StateT;
fn keep_delay(state: &Self::StateT) -> Self::DelayT;
}
Another possible way to do it is to make DynamicalSystem
a super trait of DynamicalDelaySystem
:
pub trait DynamicalSystem {
type StateT;
type ModelT;
fn next(state: &Self::StateT, model: &Self::ModelT) -> Self::StateT;
}
pub trait DynamicalDelaySystem: DynamicalSystem {
type DelayT;
fn next_with_delay(
state: &Self::StateT,
model: &Self::ModelT,
delay: &Self::DelayT,
) -> Self::StateT;
fn keep_delay(state: &Self::StateT) -> Self::DelayT;
}
Finally, if nothing else is good enough, you could use macros to do the code duplication work for you.