use crate::duffing::{generate_trajectory, DuffingConfig, DuffingState};
#[derive(Debug, thiserror::Error)]
pub enum ZooError {
#[error("Unknown attractor: {0}")]
UnknownAttractor(String),
}
#[derive(Debug, Clone)]
pub struct AttractorInfo {
pub name: String,
pub dim: usize,
pub description: String,
pub default_params: String,
pub typical_dt: f64,
}
pub struct AttractorZoo;
impl AttractorZoo {
pub fn list() -> Vec<AttractorInfo> {
vec![
AttractorInfo {
name: "lorenz".to_string(),
dim: 3,
description: "Classic Lorenz butterfly attractor (1963)".to_string(),
default_params: "sigma=10 rho=28 beta=2.6667".to_string(),
typical_dt: 0.01,
},
AttractorInfo {
name: "rossler".to_string(),
dim: 3,
description: "Rossler spiral attractor with single-scroll topology".to_string(),
default_params: "a=0.2 b=0.2 c=5.7".to_string(),
typical_dt: 0.01,
},
AttractorInfo {
name: "double_pendulum".to_string(),
dim: 4,
description: "Gravitational double-pendulum — sensitive to initial conditions".to_string(),
default_params: "m1=1 m2=1 l1=1 l2=1 g=9.81".to_string(),
typical_dt: 0.005,
},
AttractorInfo {
name: "duffing".to_string(),
dim: 2,
description: "Driven nonlinear Duffing oscillator (double-well)".to_string(),
default_params: "alpha=-1 beta=1 delta=0.3 gamma=0.5 omega=1.2".to_string(),
typical_dt: 0.01,
},
AttractorInfo {
name: "van_der_pol".to_string(),
dim: 2,
description: "Self-sustaining van der Pol limit cycle".to_string(),
default_params: "mu=1.0".to_string(),
typical_dt: 0.01,
},
AttractorInfo {
name: "halvorsen".to_string(),
dim: 3,
description: "Halvorsen cyclic-symmetry attractor".to_string(),
default_params: "a=1.4".to_string(),
typical_dt: 0.01,
},
AttractorInfo {
name: "aizawa".to_string(),
dim: 3,
description: "Aizawa six-parameter torus-like attractor".to_string(),
default_params: "a=0.95 b=0.7 c=0.6 d=3.5 e=0.25 f=0.1".to_string(),
typical_dt: 0.01,
},
AttractorInfo {
name: "thomas".to_string(),
dim: 3,
description: "Thomas cyclically-symmetric dissipative chaotic attractor".to_string(),
default_params: "b=0.208186".to_string(),
typical_dt: 0.05,
},
AttractorInfo {
name: "chen".to_string(),
dim: 3,
description: "Chen double-scroll attractor derived from Lorenz by anticontrol".to_string(),
default_params: "a=35 b=3 c=28".to_string(),
typical_dt: 0.005,
},
AttractorInfo {
name: "burke_shaw".to_string(),
dim: 3,
description: "Burke-Shaw double-lobe chaotic attractor".to_string(),
default_params: "s=10 v=4.272".to_string(),
typical_dt: 0.01,
},
]
}
pub fn random() -> AttractorInfo {
let list = Self::list();
let idx = (std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.subsec_nanos() as usize)
% list.len();
list[idx].clone()
}
pub fn describe(name: &str) -> Option<AttractorInfo> {
let lower = name.to_lowercase();
Self::list().into_iter().find(|a| a.name == lower)
}
pub fn generate_trajectory(
name: &str,
steps: usize,
dt: f64,
) -> Result<Vec<(f64, f64, f64)>, ZooError> {
let lower = name.to_lowercase();
match lower.as_str() {
"duffing" => {
let cfg = DuffingConfig::default();
let init = DuffingState::default();
let traj = generate_trajectory(&cfg, init, steps, dt);
Ok(traj.into_iter().map(|s| (s.x, s.y, s.t)).collect())
}
"lorenz" => Ok(lorenz_trajectory(steps, dt)),
"rossler" => Ok(rossler_trajectory(steps, dt)),
other => {
if Self::describe(other).is_some() {
Ok((0..steps)
.map(|i| {
let t = i as f64 * dt;
(t.sin(), t.cos(), t)
})
.collect())
} else {
Err(ZooError::UnknownAttractor(name.to_string()))
}
}
}
}
}
fn lorenz_trajectory(steps: usize, dt: f64) -> Vec<(f64, f64, f64)> {
let (sigma, rho, beta) = (10.0_f64, 28.0_f64, 8.0 / 3.0);
let mut x = 1.0_f64;
let mut y = 0.0_f64;
let mut z = 0.0_f64;
let mut out = Vec::with_capacity(steps);
for _ in 0..steps {
out.push((x, y, z));
let dx = sigma * (y - x);
let dy = x * (rho - z) - y;
let dz = x * y - beta * z;
x += dx * dt;
y += dy * dt;
z += dz * dt;
}
out
}
fn rossler_trajectory(steps: usize, dt: f64) -> Vec<(f64, f64, f64)> {
let (a, b, c) = (0.2_f64, 0.2_f64, 5.7_f64);
let mut x = 1.0_f64;
let mut y = 0.0_f64;
let mut z = 0.0_f64;
let mut out = Vec::with_capacity(steps);
for _ in 0..steps {
out.push((x, y, z));
let dx = -(y + z);
let dy = x + a * y;
let dz = b + z * (x - c);
x += dx * dt;
y += dy * dt;
z += dz * dt;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_list_not_empty() {
assert!(!AttractorZoo::list().is_empty());
}
#[test]
fn test_list_contains_duffing() {
let list = AttractorZoo::list();
assert!(list.iter().any(|a| a.name == "duffing"));
}
#[test]
fn test_list_contains_lorenz() {
let list = AttractorZoo::list();
assert!(list.iter().any(|a| a.name == "lorenz"));
}
#[test]
fn test_describe_found() {
let info = AttractorZoo::describe("lorenz");
assert!(info.is_some());
assert_eq!(info.unwrap().name, "lorenz");
}
#[test]
fn test_describe_case_insensitive() {
let info = AttractorZoo::describe("DUFFING");
assert!(info.is_some());
}
#[test]
fn test_describe_not_found() {
assert!(AttractorZoo::describe("nonexistent_xyz").is_none());
}
#[test]
fn test_random_returns_valid_attractor() {
let info = AttractorZoo::random();
assert!(!info.name.is_empty());
assert!(info.dim >= 1);
}
#[test]
fn test_generate_duffing_trajectory() {
let traj = AttractorZoo::generate_trajectory("duffing", 100, 0.01).unwrap();
assert_eq!(traj.len(), 100);
}
#[test]
fn test_generate_lorenz_trajectory() {
let traj = AttractorZoo::generate_trajectory("lorenz", 50, 0.01).unwrap();
assert_eq!(traj.len(), 50);
}
#[test]
fn test_generate_rossler_trajectory() {
let traj = AttractorZoo::generate_trajectory("rossler", 50, 0.01).unwrap();
assert_eq!(traj.len(), 50);
}
#[test]
fn test_generate_unknown_attractor_error() {
let result = AttractorZoo::generate_trajectory("does_not_exist_abc", 10, 0.01);
assert!(result.is_err());
if let Err(ZooError::UnknownAttractor(name)) = result {
assert_eq!(name, "does_not_exist_abc");
}
}
#[test]
fn test_generate_empty_trajectory() {
let traj = AttractorZoo::generate_trajectory("duffing", 0, 0.01).unwrap();
assert!(traj.is_empty());
}
#[test]
fn test_attractor_info_fields() {
let info = AttractorZoo::describe("duffing").unwrap();
assert_eq!(info.dim, 2);
assert!(info.typical_dt > 0.0);
assert!(!info.description.is_empty());
assert!(!info.default_params.is_empty());
}
#[test]
fn test_all_registered_attractors_have_valid_dim() {
for a in AttractorZoo::list() {
assert!(a.dim >= 1, "attractor '{}' has dim={}", a.name, a.dim);
}
}
}