use crate::error::SimResult;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
pub struct Vec3 {
pub x: f64,
pub y: f64,
pub z: f64,
}
impl Vec3 {
#[must_use]
pub const fn new(x: f64, y: f64, z: f64) -> Self {
Self { x, y, z }
}
#[must_use]
pub const fn zero() -> Self {
Self {
x: 0.0,
y: 0.0,
z: 0.0,
}
}
#[must_use]
pub fn magnitude_squared(&self) -> f64 {
self.x * self.x + self.y * self.y + self.z * self.z
}
#[must_use]
pub fn magnitude(&self) -> f64 {
self.magnitude_squared().sqrt()
}
#[must_use]
pub fn dot(&self, other: &Self) -> f64 {
self.x * other.x + self.y * other.y + self.z * other.z
}
#[must_use]
pub fn cross(&self, other: &Self) -> Self {
Self {
x: self.y * other.z - self.z * other.y,
y: self.z * other.x - self.x * other.z,
z: self.x * other.y - self.y * other.x,
}
}
#[must_use]
pub fn normalize(&self) -> Self {
let mag = self.magnitude();
if mag < f64::EPSILON {
Self::zero()
} else {
Self {
x: self.x / mag,
y: self.y / mag,
z: self.z / mag,
}
}
}
#[must_use]
pub fn scale(&self, s: f64) -> Self {
Self {
x: self.x * s,
y: self.y * s,
z: self.z * s,
}
}
#[must_use]
#[allow(clippy::missing_const_for_fn)] pub fn is_finite(&self) -> bool {
self.x.is_finite() && self.y.is_finite() && self.z.is_finite()
}
}
impl std::ops::Add for Vec3 {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self {
x: self.x + rhs.x,
y: self.y + rhs.y,
z: self.z + rhs.z,
}
}
}
impl std::ops::Sub for Vec3 {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Self {
x: self.x - rhs.x,
y: self.y - rhs.y,
z: self.z - rhs.z,
}
}
}
impl std::ops::Mul<f64> for Vec3 {
type Output = Self;
fn mul(self, rhs: f64) -> Self::Output {
self.scale(rhs)
}
}
impl std::ops::Neg for Vec3 {
type Output = Self;
fn neg(self) -> Self::Output {
Self {
x: -self.x,
y: -self.y,
z: -self.z,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SimEvent {
AddBody {
mass: f64,
position: Vec3,
velocity: Vec3,
},
ApplyForce {
body_index: usize,
force: Vec3,
},
SetPosition {
body_index: usize,
position: Vec3,
},
SetVelocity {
body_index: usize,
velocity: Vec3,
},
Custom {
name: String,
data: Vec<u8>,
},
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SimState {
masses: Vec<f64>,
positions: Vec<Vec3>,
velocities: Vec<Vec3>,
forces: Vec<Vec3>,
constraints: Vec<(String, f64)>,
potential_energy: f64,
}
impl SimState {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
#[allow(clippy::missing_const_for_fn)] pub fn num_bodies(&self) -> usize {
self.masses.len()
}
pub fn add_body(&mut self, mass: f64, position: Vec3, velocity: Vec3) {
self.masses.push(mass);
self.positions.push(position);
self.velocities.push(velocity);
self.forces.push(Vec3::zero());
}
#[must_use]
pub fn masses(&self) -> &[f64] {
&self.masses
}
#[must_use]
pub fn positions(&self) -> &[Vec3] {
&self.positions
}
#[must_use]
pub fn positions_mut(&mut self) -> &mut [Vec3] {
&mut self.positions
}
#[must_use]
pub fn velocities(&self) -> &[Vec3] {
&self.velocities
}
#[must_use]
pub fn velocities_mut(&mut self) -> &mut [Vec3] {
&mut self.velocities
}
#[must_use]
pub fn forces(&self) -> &[Vec3] {
&self.forces
}
#[must_use]
pub fn forces_mut(&mut self) -> &mut [Vec3] {
&mut self.forces
}
pub fn set_position(&mut self, index: usize, position: Vec3) {
self.positions[index] = position;
}
pub fn set_velocity(&mut self, index: usize, velocity: Vec3) {
self.velocities[index] = velocity;
}
pub fn apply_force(&mut self, index: usize, force: Vec3) {
self.forces[index] = self.forces[index] + force;
}
pub fn clear_forces(&mut self) {
for f in &mut self.forces {
*f = Vec3::zero();
}
}
#[allow(clippy::missing_const_for_fn)] pub fn set_potential_energy(&mut self, energy: f64) {
self.potential_energy = energy;
}
#[must_use]
pub fn kinetic_energy(&self) -> f64 {
self.masses
.iter()
.zip(&self.velocities)
.map(|(m, v)| 0.5 * m * v.magnitude_squared())
.sum()
}
#[must_use]
pub const fn potential_energy(&self) -> f64 {
self.potential_energy
}
#[must_use]
pub fn total_energy(&self) -> f64 {
self.kinetic_energy() + self.potential_energy
}
pub fn add_constraint(&mut self, name: impl Into<String>, violation: f64) {
self.constraints.push((name.into(), violation));
}
pub fn clear_constraints(&mut self) {
self.constraints.clear();
}
pub fn constraint_violations(&self) -> impl Iterator<Item = (String, f64)> + '_ {
self.constraints.iter().cloned()
}
#[must_use]
pub fn all_finite(&self) -> bool {
self.positions.iter().all(Vec3::is_finite)
&& self.velocities.iter().all(Vec3::is_finite)
&& self.masses.iter().all(|m| m.is_finite())
}
pub fn apply_event(&mut self, event: &SimEvent) -> SimResult<()> {
match event {
SimEvent::AddBody {
mass,
position,
velocity,
} => {
self.add_body(*mass, *position, *velocity);
}
SimEvent::ApplyForce { body_index, force } => {
if *body_index < self.num_bodies() {
self.apply_force(*body_index, *force);
}
}
SimEvent::SetPosition {
body_index,
position,
} => {
if *body_index < self.num_bodies() {
self.set_position(*body_index, *position);
}
}
SimEvent::SetVelocity {
body_index,
velocity,
} => {
if *body_index < self.num_bodies() {
self.set_velocity(*body_index, *velocity);
}
}
SimEvent::Custom { .. } => {
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vec3_operations() {
let v1 = Vec3::new(1.0, 2.0, 3.0);
let v2 = Vec3::new(4.0, 5.0, 6.0);
let sum = v1 + v2;
assert!((sum.x - 5.0).abs() < f64::EPSILON);
assert!((sum.y - 7.0).abs() < f64::EPSILON);
assert!((sum.z - 9.0).abs() < f64::EPSILON);
let diff = v2 - v1;
assert!((diff.x - 3.0).abs() < f64::EPSILON);
let dot = v1.dot(&v2);
assert!((dot - 32.0).abs() < f64::EPSILON);
let cross = v1.cross(&v2);
assert!((cross.x - (-3.0)).abs() < f64::EPSILON);
assert!((cross.y - 6.0).abs() < f64::EPSILON);
assert!((cross.z - (-3.0)).abs() < f64::EPSILON);
let v = Vec3::new(3.0, 4.0, 0.0);
assert!((v.magnitude() - 5.0).abs() < f64::EPSILON);
}
#[test]
fn test_vec3_normalize() {
let v = Vec3::new(3.0, 4.0, 0.0);
let n = v.normalize();
assert!((n.magnitude() - 1.0).abs() < f64::EPSILON);
assert!((n.x - 0.6).abs() < f64::EPSILON);
assert!((n.y - 0.8).abs() < f64::EPSILON);
}
#[test]
fn test_state_add_body() {
let mut state = SimState::new();
state.add_body(1.0, Vec3::new(1.0, 0.0, 0.0), Vec3::zero());
state.add_body(2.0, Vec3::new(0.0, 1.0, 0.0), Vec3::zero());
assert_eq!(state.num_bodies(), 2);
assert!((state.masses()[0] - 1.0).abs() < f64::EPSILON);
assert!((state.masses()[1] - 2.0).abs() < f64::EPSILON);
}
#[test]
fn test_state_energy() {
let mut state = SimState::new();
state.add_body(2.0, Vec3::zero(), Vec3::new(3.0, 0.0, 0.0));
assert!((state.kinetic_energy() - 9.0).abs() < f64::EPSILON);
state.set_potential_energy(-5.0);
assert!((state.total_energy() - 4.0).abs() < f64::EPSILON);
}
#[test]
fn test_state_apply_event() {
let mut state = SimState::new();
let event = SimEvent::AddBody {
mass: 1.0,
position: Vec3::new(1.0, 2.0, 3.0),
velocity: Vec3::zero(),
};
state.apply_event(&event).ok();
assert_eq!(state.num_bodies(), 1);
assert!((state.positions()[0].x - 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_state_constraints() {
let mut state = SimState::new();
state.add_constraint("test1", 0.001);
state.add_constraint("test2", -0.002);
let violations: Vec<_> = state.constraint_violations().collect();
assert_eq!(violations.len(), 2);
state.clear_constraints();
let violations: Vec<_> = state.constraint_violations().collect();
assert!(violations.is_empty());
}
#[test]
fn test_state_all_finite() {
let mut state = SimState::new();
state.add_body(1.0, Vec3::new(1.0, 2.0, 3.0), Vec3::zero());
assert!(state.all_finite());
state.set_position(0, Vec3::new(f64::NAN, 0.0, 0.0));
assert!(!state.all_finite());
}
#[test]
fn test_vec3_scale() {
let v = Vec3::new(1.0, 2.0, 3.0);
let scaled = v.scale(2.0);
assert!((scaled.x - 2.0).abs() < f64::EPSILON);
assert!((scaled.y - 4.0).abs() < f64::EPSILON);
assert!((scaled.z - 6.0).abs() < f64::EPSILON);
}
#[test]
fn test_vec3_mul_scalar() {
let v = Vec3::new(1.0, 2.0, 3.0);
let scaled = v * 2.5;
assert!((scaled.x - 2.5).abs() < f64::EPSILON);
assert!((scaled.y - 5.0).abs() < f64::EPSILON);
assert!((scaled.z - 7.5).abs() < f64::EPSILON);
}
#[test]
fn test_vec3_neg() {
let v = Vec3::new(1.0, -2.0, 3.0);
let neg = -v;
assert!((neg.x - (-1.0)).abs() < f64::EPSILON);
assert!((neg.y - 2.0).abs() < f64::EPSILON);
assert!((neg.z - (-3.0)).abs() < f64::EPSILON);
}
#[test]
fn test_vec3_is_finite() {
let v1 = Vec3::new(1.0, 2.0, 3.0);
assert!(v1.is_finite());
let v2 = Vec3::new(f64::INFINITY, 0.0, 0.0);
assert!(!v2.is_finite());
let v3 = Vec3::new(0.0, f64::NEG_INFINITY, 0.0);
assert!(!v3.is_finite());
let v4 = Vec3::new(0.0, 0.0, f64::NAN);
assert!(!v4.is_finite());
}
#[test]
fn test_vec3_normalize_zero() {
let v = Vec3::zero();
let n = v.normalize();
assert!((n.x).abs() < f64::EPSILON);
assert!((n.y).abs() < f64::EPSILON);
assert!((n.z).abs() < f64::EPSILON);
}
#[test]
fn test_vec3_default() {
let v = Vec3::default();
assert!((v.x).abs() < f64::EPSILON);
assert!((v.y).abs() < f64::EPSILON);
assert!((v.z).abs() < f64::EPSILON);
}
#[test]
fn test_vec3_partial_eq() {
let v1 = Vec3::new(1.0, 2.0, 3.0);
let v2 = Vec3::new(1.0, 2.0, 3.0);
let v3 = Vec3::new(1.0, 2.0, 4.0);
assert_eq!(v1, v2);
assert_ne!(v1, v3);
}
#[test]
fn test_vec3_magnitude_squared() {
let v = Vec3::new(3.0, 4.0, 0.0);
assert!((v.magnitude_squared() - 25.0).abs() < f64::EPSILON);
}
#[test]
fn test_state_positions_mut() {
let mut state = SimState::new();
state.add_body(1.0, Vec3::new(1.0, 2.0, 3.0), Vec3::zero());
{
let positions = state.positions_mut();
positions[0] = Vec3::new(10.0, 20.0, 30.0);
}
assert!((state.positions()[0].x - 10.0).abs() < f64::EPSILON);
assert!((state.positions()[0].y - 20.0).abs() < f64::EPSILON);
assert!((state.positions()[0].z - 30.0).abs() < f64::EPSILON);
}
#[test]
fn test_state_velocities_mut() {
let mut state = SimState::new();
state.add_body(1.0, Vec3::zero(), Vec3::new(1.0, 2.0, 3.0));
{
let velocities = state.velocities_mut();
velocities[0] = Vec3::new(5.0, 6.0, 7.0);
}
assert!((state.velocities()[0].x - 5.0).abs() < f64::EPSILON);
}
#[test]
fn test_state_forces_mut() {
let mut state = SimState::new();
state.add_body(1.0, Vec3::zero(), Vec3::zero());
{
let forces = state.forces_mut();
forces[0] = Vec3::new(100.0, 200.0, 300.0);
}
assert!((state.forces()[0].x - 100.0).abs() < f64::EPSILON);
}
#[test]
fn test_state_clear_forces() {
let mut state = SimState::new();
state.add_body(1.0, Vec3::zero(), Vec3::zero());
state.apply_force(0, Vec3::new(100.0, 200.0, 300.0));
assert!((state.forces()[0].x - 100.0).abs() < f64::EPSILON);
state.clear_forces();
assert!((state.forces()[0].x).abs() < f64::EPSILON);
assert!((state.forces()[0].y).abs() < f64::EPSILON);
assert!((state.forces()[0].z).abs() < f64::EPSILON);
}
#[test]
fn test_state_apply_event_apply_force() {
let mut state = SimState::new();
state.add_body(1.0, Vec3::zero(), Vec3::zero());
let event = SimEvent::ApplyForce {
body_index: 0,
force: Vec3::new(10.0, 20.0, 30.0),
};
state.apply_event(&event).unwrap();
assert!((state.forces()[0].x - 10.0).abs() < f64::EPSILON);
}
#[test]
fn test_state_apply_event_apply_force_out_of_bounds() {
let mut state = SimState::new();
state.add_body(1.0, Vec3::zero(), Vec3::zero());
let event = SimEvent::ApplyForce {
body_index: 100, force: Vec3::new(10.0, 20.0, 30.0),
};
state.apply_event(&event).unwrap();
}
#[test]
fn test_state_apply_event_set_position() {
let mut state = SimState::new();
state.add_body(1.0, Vec3::zero(), Vec3::zero());
let event = SimEvent::SetPosition {
body_index: 0,
position: Vec3::new(5.0, 6.0, 7.0),
};
state.apply_event(&event).unwrap();
assert!((state.positions()[0].x - 5.0).abs() < f64::EPSILON);
}
#[test]
fn test_state_apply_event_set_position_out_of_bounds() {
let mut state = SimState::new();
state.add_body(1.0, Vec3::zero(), Vec3::zero());
let event = SimEvent::SetPosition {
body_index: 100, position: Vec3::new(5.0, 6.0, 7.0),
};
state.apply_event(&event).unwrap();
}
#[test]
fn test_state_apply_event_set_velocity() {
let mut state = SimState::new();
state.add_body(1.0, Vec3::zero(), Vec3::zero());
let event = SimEvent::SetVelocity {
body_index: 0,
velocity: Vec3::new(8.0, 9.0, 10.0),
};
state.apply_event(&event).unwrap();
assert!((state.velocities()[0].x - 8.0).abs() < f64::EPSILON);
}
#[test]
fn test_state_apply_event_set_velocity_out_of_bounds() {
let mut state = SimState::new();
state.add_body(1.0, Vec3::zero(), Vec3::zero());
let event = SimEvent::SetVelocity {
body_index: 100, velocity: Vec3::new(8.0, 9.0, 10.0),
};
state.apply_event(&event).unwrap();
}
#[test]
fn test_state_apply_event_custom() {
let mut state = SimState::new();
let event = SimEvent::Custom {
name: "custom_event".to_string(),
data: vec![1, 2, 3, 4],
};
state.apply_event(&event).unwrap();
}
#[test]
fn test_sim_event_clone() {
let event = SimEvent::AddBody {
mass: 1.0,
position: Vec3::new(1.0, 2.0, 3.0),
velocity: Vec3::zero(),
};
let cloned = event.clone();
match cloned {
SimEvent::AddBody { mass, .. } => assert!((mass - 1.0).abs() < f64::EPSILON),
_ => panic!("unexpected event type"),
}
}
#[test]
fn test_sim_event_debug() {
let event = SimEvent::AddBody {
mass: 1.0,
position: Vec3::zero(),
velocity: Vec3::zero(),
};
let debug = format!("{:?}", event);
assert!(debug.contains("AddBody"));
}
#[test]
fn test_state_clone() {
let mut state = SimState::new();
state.add_body(1.0, Vec3::new(1.0, 2.0, 3.0), Vec3::zero());
state.set_potential_energy(-10.0);
state.add_constraint("test", 0.5);
let cloned = state.clone();
assert_eq!(cloned.num_bodies(), 1);
assert!((cloned.potential_energy() - (-10.0)).abs() < f64::EPSILON);
}
#[test]
fn test_state_debug() {
let state = SimState::new();
let debug = format!("{:?}", state);
assert!(debug.contains("SimState"));
}
#[test]
fn test_state_all_finite_with_infinity_in_velocity() {
let mut state = SimState::new();
state.add_body(1.0, Vec3::zero(), Vec3::new(f64::INFINITY, 0.0, 0.0));
assert!(!state.all_finite());
}
#[test]
fn test_state_all_finite_with_nan_in_mass() {
let mut state = SimState::new();
state.add_body(f64::NAN, Vec3::zero(), Vec3::zero());
assert!(!state.all_finite());
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn prop_dot_commutative(
x1 in -1e6f64..1e6, y1 in -1e6f64..1e6, z1 in -1e6f64..1e6,
x2 in -1e6f64..1e6, y2 in -1e6f64..1e6, z2 in -1e6f64..1e6,
) {
let v1 = Vec3::new(x1, y1, z1);
let v2 = Vec3::new(x2, y2, z2);
let d1 = v1.dot(&v2);
let d2 = v2.dot(&v1);
prop_assert!((d1 - d2).abs() < 1e-9 * d1.abs().max(1.0));
}
#[test]
fn prop_normalize_unit_length(
x in -1e6f64..1e6, y in -1e6f64..1e6, z in -1e6f64..1e6,
) {
let v = Vec3::new(x, y, z);
if v.magnitude() < f64::EPSILON {
return Ok(());
}
let n = v.normalize();
prop_assert!((n.magnitude() - 1.0).abs() < 1e-9);
}
#[test]
fn prop_kinetic_energy_nonnegative(
mass in 0.1f64..1e6,
vx in -1e3f64..1e3, vy in -1e3f64..1e3, vz in -1e3f64..1e3,
) {
let mut state = SimState::new();
state.add_body(mass, Vec3::zero(), Vec3::new(vx, vy, vz));
prop_assert!(state.kinetic_energy() >= 0.0);
}
}
}