#[derive(Debug, Clone, PartialEq)]
pub struct DuffingConfig {
pub alpha: f64,
pub beta: f64,
pub delta: f64,
pub gamma: f64,
pub omega: f64,
}
impl Default for DuffingConfig {
fn default() -> Self {
Self {
alpha: -1.0,
beta: 1.0,
delta: 0.3,
gamma: 0.5,
omega: 1.2,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct DuffingState {
pub x: f64,
pub y: f64,
pub t: f64,
}
impl Default for DuffingState {
fn default() -> Self {
Self { x: 1.0, y: 0.0, t: 0.0 }
}
}
impl DuffingState {
pub fn new(x: f64, y: f64, t: f64) -> Self {
Self { x, y, t }
}
}
fn deriv(state: &DuffingState, cfg: &DuffingConfig) -> (f64, f64) {
let dx = state.y;
let dy = -cfg.delta * state.y
- cfg.alpha * state.x
- cfg.beta * state.x * state.x * state.x
+ cfg.gamma * (cfg.omega * state.t).cos();
(dx, dy)
}
pub fn step_rk4(state: &mut DuffingState, cfg: &DuffingConfig, dt: f64) {
let (dx1, dy1) = deriv(state, cfg);
let s2 = DuffingState {
x: state.x + 0.5 * dt * dx1,
y: state.y + 0.5 * dt * dy1,
t: state.t + 0.5 * dt,
};
let (dx2, dy2) = deriv(&s2, cfg);
let s3 = DuffingState {
x: state.x + 0.5 * dt * dx2,
y: state.y + 0.5 * dt * dy2,
t: state.t + 0.5 * dt,
};
let (dx3, dy3) = deriv(&s3, cfg);
let s4 = DuffingState {
x: state.x + dt * dx3,
y: state.y + dt * dy3,
t: state.t + dt,
};
let (dx4, dy4) = deriv(&s4, cfg);
state.x += dt / 6.0 * (dx1 + 2.0 * dx2 + 2.0 * dx3 + dx4);
state.y += dt / 6.0 * (dy1 + 2.0 * dy2 + 2.0 * dy3 + dy4);
state.t += dt;
}
pub fn generate_trajectory(
cfg: &DuffingConfig,
initial: DuffingState,
steps: usize,
dt: f64,
) -> Vec<DuffingState> {
let mut trajectory = Vec::with_capacity(steps);
let mut state = initial;
for _ in 0..steps {
trajectory.push(state.clone());
step_rk4(&mut state, cfg, dt);
}
trajectory
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
#[test]
fn test_default_config() {
let cfg = DuffingConfig::default();
assert_eq!(cfg.alpha, -1.0);
assert_eq!(cfg.beta, 1.0);
assert_eq!(cfg.delta, 0.3);
assert_eq!(cfg.gamma, 0.5);
assert_eq!(cfg.omega, 1.2);
}
#[test]
fn test_default_state() {
let s = DuffingState::default();
assert_eq!(s.x, 1.0);
assert_eq!(s.y, 0.0);
assert_eq!(s.t, 0.0);
}
#[test]
fn test_step_advances_time() {
let cfg = DuffingConfig::default();
let mut state = DuffingState::default();
step_rk4(&mut state, &cfg, 0.01);
assert!((state.t - 0.01).abs() < 1e-12);
}
#[test]
fn test_step_changes_position() {
let cfg = DuffingConfig::default();
let mut state = DuffingState::default();
let x0 = state.x;
step_rk4(&mut state, &cfg, 0.01);
for _ in 0..10 {
step_rk4(&mut state, &cfg, 0.01);
}
assert!((state.x - x0).abs() > 1e-6);
}
#[test]
fn test_trajectory_length() {
let cfg = DuffingConfig::default();
let init = DuffingState::default();
let traj = generate_trajectory(&cfg, init, 100, 0.01);
assert_eq!(traj.len(), 100);
}
#[test]
fn test_trajectory_first_state_is_initial() {
let cfg = DuffingConfig::default();
let init = DuffingState::new(2.0, 1.0, 0.5);
let traj = generate_trajectory(&cfg, init.clone(), 10, 0.01);
assert_eq!(traj[0], init);
}
#[test]
fn test_trajectory_time_monotone() {
let cfg = DuffingConfig::default();
let init = DuffingState::default();
let traj = generate_trajectory(&cfg, init, 50, 0.01);
for i in 1..traj.len() {
assert!(traj[i].t > traj[i - 1].t);
}
}
#[test]
fn test_zero_gamma_no_forcing() {
let mut cfg = DuffingConfig::default();
cfg.gamma = 0.0;
let mut state = DuffingState::default();
for _ in 0..1000 {
step_rk4(&mut state, &cfg, 0.01);
}
assert!(state.x.abs() < 2.0);
}
#[test]
fn test_zero_damping_bounded() {
let cfg = DuffingConfig {
alpha: -1.0,
beta: 1.0,
delta: 0.0,
gamma: 0.0,
omega: 1.2,
};
let mut state = DuffingState::default();
for _ in 0..500 {
step_rk4(&mut state, &cfg, 0.01);
}
assert!(state.x.abs() < 10.0);
assert!(state.y.abs() < 10.0);
}
#[test]
fn test_forcing_frequency() {
let cfg1 = DuffingConfig { omega: 1.0, gamma: 0.0, delta: 0.0, alpha: 0.0, beta: 0.0 };
let cfg2 = DuffingConfig { omega: 2.0, gamma: 0.0, delta: 0.0, alpha: 0.0, beta: 0.0 };
let s1 = DuffingState::new(0.0, 0.0, 0.0);
let s2 = s1.clone();
let t1 = generate_trajectory(&cfg1, s1, 10, 0.1);
let t2 = generate_trajectory(&cfg2, s2, 10, 0.1);
for (a, b) in t1.iter().zip(t2.iter()) {
assert!((a.x - b.x).abs() < 1e-12);
}
}
#[test]
fn test_deriv_at_origin_with_default() {
let cfg = DuffingConfig::default();
let s = DuffingState::new(0.0, 0.0, 0.0);
let (dx, dy) = deriv(&s, &cfg);
assert!((dx - 0.0).abs() < 1e-12);
assert!((dy - 0.5).abs() < 1e-12);
}
#[test]
fn test_pi_forcing() {
let cfg = DuffingConfig::default();
let t = PI / cfg.omega;
let s = DuffingState::new(0.0, 0.0, t);
let (_dx, dy) = deriv(&s, &cfg);
assert!((dy - (-0.5)).abs() < 1e-12);
}
#[test]
fn test_empty_trajectory() {
let cfg = DuffingConfig::default();
let init = DuffingState::default();
let traj = generate_trajectory(&cfg, init, 0, 0.01);
assert!(traj.is_empty());
}
}