#[derive(Debug, Clone, PartialEq)]
pub struct RosslerConfig {
pub a: f64,
pub b: f64,
pub c: f64,
}
impl Default for RosslerConfig {
fn default() -> Self {
Self { a: 0.2, b: 0.2, c: 5.7 }
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct RosslerState {
pub x: f64,
pub y: f64,
pub z: f64,
}
impl Default for RosslerState {
fn default() -> Self {
Self { x: 1.0, y: 0.0, z: 0.0 }
}
}
impl RosslerState {
pub fn as_array(&self) -> [f64; 3] {
[self.x, self.y, self.z]
}
pub fn from_slice(s: &[f64]) -> Self {
Self {
x: s.get(0).copied().unwrap_or(1.0),
y: s.get(1).copied().unwrap_or(0.0),
z: s.get(2).copied().unwrap_or(0.0),
}
}
}
fn rk4_step(state: [f64; 3], dt: f64, deriv: impl Fn([f64; 3]) -> [f64; 3]) -> [f64; 3] {
let k1 = deriv(state);
let s2 = [state[0] + dt/2.0 * k1[0], state[1] + dt/2.0 * k1[1], state[2] + dt/2.0 * k1[2]];
let k2 = deriv(s2);
let s3 = [state[0] + dt/2.0 * k2[0], state[1] + dt/2.0 * k2[1], state[2] + dt/2.0 * k2[2]];
let k3 = deriv(s3);
let s4 = [state[0] + dt * k3[0], state[1] + dt * k3[1], state[2] + dt * k3[2]];
let k4 = deriv(s4);
[
state[0] + dt/6.0 * (k1[0] + 2.0*k2[0] + 2.0*k3[0] + k4[0]),
state[1] + dt/6.0 * (k1[1] + 2.0*k2[1] + 2.0*k3[1] + k4[1]),
state[2] + dt/6.0 * (k1[2] + 2.0*k2[2] + 2.0*k3[2] + k4[2]),
]
}
pub struct RosslerAttractor {
config: RosslerConfig,
state: RosslerState,
speed: f64,
}
impl RosslerAttractor {
pub fn new(config: RosslerConfig) -> Self {
Self {
config,
state: RosslerState::default(),
speed: 0.0,
}
}
pub fn state(&self) -> &RosslerState {
&self.state
}
pub fn config(&self) -> &RosslerConfig {
&self.config
}
pub fn speed(&self) -> f64 {
self.speed
}
pub fn set_state(&mut self, s: RosslerState) {
if s.x.is_finite() { self.state.x = s.x; }
if s.y.is_finite() { self.state.y = s.y; }
if s.z.is_finite() { self.state.z = s.z; }
}
pub fn derivatives(&self, s: &RosslerState) -> RosslerState {
let (a, b, c) = (self.config.a, self.config.b, self.config.c);
RosslerState {
x: -s.y - s.z,
y: s.x + a * s.y,
z: b + s.z * (s.x - c),
}
}
pub fn step(&mut self, dt: f64) {
let (a, b, c) = (self.config.a, self.config.b, self.config.c);
let prev = self.state.as_array();
let next = rk4_step(prev, dt, |s| {
[-s[1] - s[2], s[0] + a * s[1], b + s[2] * (s[0] - c)]
});
let ds = (0..3)
.map(|i| (next[i] - prev[i]).powi(2))
.sum::<f64>()
.sqrt();
self.speed = if dt != 0.0 { ds / dt.abs() } else { 0.0 };
self.state = RosslerState::from_slice(&next);
}
pub fn state_vec(&self) -> Vec<f64> {
vec![self.state.x, self.state.y, self.state.z]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_default_classic_params() {
let cfg = RosslerConfig::default();
assert!((cfg.a - 0.2).abs() < 1e-12);
assert!((cfg.b - 0.2).abs() < 1e-12);
assert!((cfg.c - 5.7).abs() < 1e-12);
}
#[test]
fn test_state_default() {
let s = RosslerState::default();
assert!((s.x - 1.0).abs() < 1e-12);
assert!((s.y - 0.0).abs() < 1e-12);
assert!((s.z - 0.0).abs() < 1e-12);
}
#[test]
fn test_step_changes_state() {
let mut a = RosslerAttractor::new(RosslerConfig::default());
let before = a.state().clone();
a.step(0.01);
let after = a.state();
assert!(
(after.x - before.x).abs() > 1e-15
|| (after.y - before.y).abs() > 1e-15
|| (after.z - before.z).abs() > 1e-15,
"State must change after a step"
);
}
#[test]
fn test_finite_output_after_many_steps() {
let mut a = RosslerAttractor::new(RosslerConfig::default());
for _ in 0..1000 {
a.step(0.01);
}
let s = a.state();
assert!(s.x.is_finite(), "x not finite: {}", s.x);
assert!(s.y.is_finite(), "y not finite: {}", s.y);
assert!(s.z.is_finite(), "z not finite: {}", s.z);
}
#[test]
fn test_bounded_orbit_classic_params() {
let mut a = RosslerAttractor::new(RosslerConfig::default());
for _ in 0..2000 {
a.step(0.01);
}
let s = a.state();
assert!(s.x.abs() < 30.0, "x out of bounds: {}", s.x);
assert!(s.y.abs() < 30.0, "y out of bounds: {}", s.y);
assert!(s.z >= 0.0 && s.z < 40.0, "z out of bounds: {}", s.z);
}
#[test]
fn test_derivatives_at_initial_state() {
let a = RosslerAttractor::new(RosslerConfig::default());
let d = a.derivatives(&RosslerState::default());
assert!((d.x - 0.0).abs() < 1e-12, "dx: {}", d.x);
assert!((d.y - 1.0).abs() < 1e-12, "dy: {}", d.y);
assert!((d.z - 0.2).abs() < 1e-12, "dz: {}", d.z);
}
#[test]
fn test_speed_positive_after_step() {
let mut a = RosslerAttractor::new(RosslerConfig::default());
a.step(0.01);
assert!(a.speed() > 0.0, "speed should be positive: {}", a.speed());
}
#[test]
fn test_set_state_ignores_nan() {
let mut a = RosslerAttractor::new(RosslerConfig::default());
a.set_state(RosslerState { x: f64::NAN, y: 5.0, z: f64::NAN });
assert!((a.state().x - 1.0).abs() < 1e-12);
assert!((a.state().y - 5.0).abs() < 1e-12);
assert!((a.state().z - 0.0).abs() < 1e-12);
}
#[test]
fn test_deterministic() {
let mut a1 = RosslerAttractor::new(RosslerConfig::default());
let mut a2 = RosslerAttractor::new(RosslerConfig::default());
for _ in 0..500 {
a1.step(0.01);
a2.step(0.01);
}
let s1 = a1.state();
let s2 = a2.state();
assert!((s1.x - s2.x).abs() < 1e-15);
assert!((s1.y - s2.y).abs() < 1e-15);
assert!((s1.z - s2.z).abs() < 1e-15);
}
#[test]
fn test_state_vec_length() {
let a = RosslerAttractor::new(RosslerConfig::default());
assert_eq!(a.state_vec().len(), 3);
}
#[test]
fn test_state_from_slice() {
let s = RosslerState::from_slice(&[2.0, 3.0, 4.0]);
assert!((s.x - 2.0).abs() < 1e-12);
assert!((s.y - 3.0).abs() < 1e-12);
assert!((s.z - 4.0).abs() < 1e-12);
}
#[test]
fn test_as_array_roundtrip() {
let s = RosslerState { x: 1.5, y: 2.5, z: 3.5 };
let arr = s.as_array();
assert!((arr[0] - 1.5).abs() < 1e-12);
assert!((arr[1] - 2.5).abs() < 1e-12);
assert!((arr[2] - 3.5).abs() < 1e-12);
}
}