use crate::domains::physics::ForceField;
use crate::engine::state::{SimState, Vec3};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PendulumConfig {
pub length: f64,
pub mass: f64,
pub g: f64,
pub damping: f64,
pub initial_angle: f64,
pub initial_angular_velocity: f64,
}
impl Default for PendulumConfig {
fn default() -> Self {
Self {
length: 1.0,
mass: 1.0,
g: 9.81,
damping: 0.0,
initial_angle: std::f64::consts::FRAC_PI_4, initial_angular_velocity: 0.0,
}
}
}
impl PendulumConfig {
#[must_use]
pub fn small_angle() -> Self {
Self {
initial_angle: 0.1, ..Default::default()
}
}
#[must_use]
pub fn large_angle() -> Self {
Self {
initial_angle: std::f64::consts::FRAC_PI_2, ..Default::default()
}
}
#[must_use]
pub fn damped(damping: f64) -> Self {
Self {
damping,
..Default::default()
}
}
#[must_use]
pub fn small_angle_period(&self) -> f64 {
2.0 * std::f64::consts::PI * (self.length / self.g).sqrt()
}
}
#[derive(Debug, Clone)]
pub struct PendulumScenario {
config: PendulumConfig,
}
impl PendulumScenario {
#[must_use]
pub fn new(config: PendulumConfig) -> Self {
Self { config }
}
#[must_use]
pub fn init_state(&self) -> SimState {
let mut state = SimState::new();
let x = self.config.length * self.config.initial_angle.sin();
let y = -self.config.length * self.config.initial_angle.cos();
let vx = self.config.length
* self.config.initial_angular_velocity
* self.config.initial_angle.cos();
let vy = self.config.length
* self.config.initial_angular_velocity
* self.config.initial_angle.sin();
state.add_body(
self.config.mass,
Vec3::new(x, y, 0.0),
Vec3::new(vx, vy, 0.0),
);
state
}
#[must_use]
pub fn create_force_field(&self) -> PendulumForceField {
PendulumForceField {
g: self.config.g,
length: self.config.length,
damping: self.config.damping,
}
}
#[must_use]
pub fn theoretical_period(&self) -> f64 {
self.config.small_angle_period()
}
#[must_use]
pub const fn config(&self) -> &PendulumConfig {
&self.config
}
#[must_use]
pub fn current_angle(state: &SimState) -> f64 {
let pos = state.positions()[0];
pos.x.atan2(-pos.y)
}
#[must_use]
pub fn current_angular_velocity(state: &SimState, length: f64) -> f64 {
let pos = state.positions()[0];
let vel = state.velocities()[0];
let r = pos.magnitude();
if r < f64::EPSILON {
return 0.0;
}
(pos.x * vel.y - pos.y * vel.x) / (length * length)
}
}
#[derive(Debug, Clone)]
pub struct PendulumForceField {
pub g: f64,
pub length: f64,
pub damping: f64,
}
impl ForceField for PendulumForceField {
fn acceleration(&self, position: &Vec3, _mass: f64) -> Vec3 {
let r = (position.x * position.x + position.y * position.y).sqrt();
if r < f64::EPSILON {
return Vec3::zero();
}
let theta = position.x.atan2(-position.y);
let a_tangent = -self.g * theta.sin();
let tan_x = -position.y / r;
let tan_y = position.x / r;
Vec3::new(a_tangent * tan_x, a_tangent * tan_y, 0.0)
}
fn potential(&self, position: &Vec3, mass: f64) -> f64 {
let theta = position.x.atan2(-position.y);
mass * self.g * self.length * (1.0 - theta.cos())
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_pendulum_config_default() {
let config = PendulumConfig::default();
assert!((config.length - 1.0).abs() < f64::EPSILON);
assert!((config.mass - 1.0).abs() < f64::EPSILON);
assert!((config.g - 9.81).abs() < 0.01);
}
#[test]
fn test_pendulum_config_small_angle() {
let config = PendulumConfig::small_angle();
assert!((config.initial_angle - 0.1).abs() < f64::EPSILON);
}
#[test]
fn test_pendulum_config_large_angle() {
let config = PendulumConfig::large_angle();
assert!((config.initial_angle - std::f64::consts::FRAC_PI_2).abs() < f64::EPSILON);
}
#[test]
fn test_pendulum_config_damped() {
let config = PendulumConfig::damped(0.5);
assert!((config.damping - 0.5).abs() < f64::EPSILON);
}
#[test]
fn test_pendulum_config_clone() {
let config = PendulumConfig::default();
let cloned = config.clone();
assert!((cloned.length - config.length).abs() < f64::EPSILON);
}
#[test]
fn test_pendulum_small_angle_period() {
let config = PendulumConfig {
length: 1.0,
g: 9.81,
..Default::default()
};
let period = config.small_angle_period();
assert!((period - 2.006).abs() < 0.01, "Period={}", period);
}
#[test]
fn test_pendulum_scenario_init() {
let config = PendulumConfig {
initial_angle: std::f64::consts::FRAC_PI_4,
length: 1.0,
..Default::default()
};
let scenario = PendulumScenario::new(config);
let state = scenario.init_state();
assert_eq!(state.num_bodies(), 1);
let angle = PendulumScenario::current_angle(&state);
assert!((angle - std::f64::consts::FRAC_PI_4).abs() < 0.01);
}
#[test]
fn test_pendulum_scenario_clone() {
let config = PendulumConfig::default();
let scenario = PendulumScenario::new(config);
let cloned = scenario.clone();
assert!((cloned.theoretical_period() - scenario.theoretical_period()).abs() < f64::EPSILON);
}
#[test]
fn test_pendulum_scenario_config() {
let config = PendulumConfig {
length: 2.0,
..Default::default()
};
let scenario = PendulumScenario::new(config);
assert!((scenario.config().length - 2.0).abs() < f64::EPSILON);
}
#[test]
fn test_pendulum_scenario_theoretical_period() {
let config = PendulumConfig {
length: 1.0,
g: 9.81,
..Default::default()
};
let scenario = PendulumScenario::new(config);
let period = scenario.theoretical_period();
assert!((period - 2.006).abs() < 0.01);
}
#[test]
fn test_pendulum_current_angular_velocity() {
let config = PendulumConfig {
length: 1.0,
initial_angular_velocity: 1.0,
..Default::default()
};
let scenario = PendulumScenario::new(config);
let state = scenario.init_state();
let omega = PendulumScenario::current_angular_velocity(&state, 1.0);
assert!(omega.abs() < 2.0);
}
#[test]
fn test_pendulum_angular_velocity_at_origin() {
let mut state = SimState::new();
state.add_body(1.0, Vec3::zero(), Vec3::zero());
let omega = PendulumScenario::current_angular_velocity(&state, 1.0);
assert!((omega - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_pendulum_force_field() {
let field = PendulumForceField {
g: 9.81,
length: 1.0,
damping: 0.0,
};
let pos_vertical = Vec3::new(0.0, -1.0, 0.0);
let acc = field.acceleration(&pos_vertical, 1.0);
assert!(acc.magnitude() < 0.01, "acc={:?}", acc);
let pos_horizontal = Vec3::new(1.0, 0.0, 0.0);
let acc = field.acceleration(&pos_horizontal, 1.0);
assert!(acc.magnitude() > 9.0, "acc magnitude={}", acc.magnitude());
}
#[test]
fn test_pendulum_force_field_at_origin() {
let field = PendulumForceField {
g: 9.81,
length: 1.0,
damping: 0.0,
};
let pos = Vec3::zero();
let acc = field.acceleration(&pos, 1.0);
assert!((acc.magnitude() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_pendulum_force_field_clone() {
let field = PendulumForceField {
g: 9.81,
length: 1.0,
damping: 0.5,
};
let cloned = field.clone();
assert!((cloned.damping - 0.5).abs() < f64::EPSILON);
}
#[test]
fn test_pendulum_create_force_field() {
let config = PendulumConfig {
g: 10.0,
length: 2.0,
damping: 0.1,
..Default::default()
};
let scenario = PendulumScenario::new(config);
let field = scenario.create_force_field();
assert!((field.g - 10.0).abs() < f64::EPSILON);
assert!((field.length - 2.0).abs() < f64::EPSILON);
assert!((field.damping - 0.1).abs() < f64::EPSILON);
}
#[test]
fn test_pendulum_potential_energy() {
let field = PendulumForceField {
g: 9.81,
length: 1.0,
damping: 0.0,
};
let pos_low = Vec3::new(0.0, -1.0, 0.0);
let pe_low = field.potential(&pos_low, 1.0);
assert!(pe_low.abs() < 0.01, "PE at bottom={}", pe_low);
let pos_horiz = Vec3::new(1.0, 0.0, 0.0);
let pe_horiz = field.potential(&pos_horiz, 1.0);
assert!(
(pe_horiz - 9.81).abs() < 0.01,
"PE at horizontal={}",
pe_horiz
);
let pos_top = Vec3::new(0.0, 1.0, 0.0);
let pe_top = field.potential(&pos_top, 1.0);
assert!((pe_top - 2.0 * 9.81).abs() < 0.01, "PE at top={}", pe_top);
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn prop_period_increases_with_length(
length1 in 0.1f64..10.0,
length2 in 0.1f64..10.0,
) {
let config1 = PendulumConfig { length: length1, ..Default::default() };
let config2 = PendulumConfig { length: length2, ..Default::default() };
let period1 = config1.small_angle_period();
let period2 = config2.small_angle_period();
if length1 > length2 {
prop_assert!(period1 > period2);
} else if length1 < length2 {
prop_assert!(period1 < period2);
}
}
#[test]
fn prop_period_decreases_with_gravity(
g1 in 1.0f64..20.0,
g2 in 1.0f64..20.0,
) {
let config1 = PendulumConfig { g: g1, ..Default::default() };
let config2 = PendulumConfig { g: g2, ..Default::default() };
let period1 = config1.small_angle_period();
let period2 = config2.small_angle_period();
if g1 > g2 {
prop_assert!(period1 < period2);
} else if g1 < g2 {
prop_assert!(period1 > period2);
}
}
#[test]
fn prop_restoring_force(
angle in -std::f64::consts::PI..std::f64::consts::PI,
length in 0.5f64..5.0,
) {
let field = PendulumForceField {
g: 9.81,
length,
damping: 0.0,
};
let x = length * angle.sin();
let y = -length * angle.cos();
let pos = Vec3::new(x, y, 0.0);
let accel = field.acceleration(&pos, 1.0);
if angle.abs() > 0.01 {
let tangent_x = angle.cos();
let tangent_y = angle.sin();
let tangent_accel = accel.x * tangent_x + accel.y * tangent_y;
if angle > 0.0 {
prop_assert!(tangent_accel < 0.1, "tangent_accel={}", tangent_accel);
} else {
prop_assert!(tangent_accel > -0.1, "tangent_accel={}", tangent_accel);
}
}
}
#[test]
fn prop_potential_energy_nonnegative(
angle in -std::f64::consts::PI..std::f64::consts::PI,
length in 0.5f64..5.0,
mass in 0.1f64..10.0,
) {
let field = PendulumForceField {
g: 9.81,
length,
damping: 0.0,
};
let x = length * angle.sin();
let y = -length * angle.cos();
let pos = Vec3::new(x, y, 0.0);
let pe = field.potential(&pos, mass);
prop_assert!(pe >= -0.001, "PE={} should be non-negative", pe);
}
}
}