use crate::engine::state::{SimState, Vec3};
use crate::error::SimResult;
pub trait ForceField {
fn acceleration(&self, position: &Vec3, mass: f64) -> Vec3;
fn potential(&self, position: &Vec3, mass: f64) -> f64;
}
#[derive(Debug, Clone)]
pub struct GravityField {
pub g: Vec3,
}
impl Default for GravityField {
fn default() -> Self {
Self {
g: Vec3::new(0.0, 0.0, -9.81),
}
}
}
impl ForceField for GravityField {
fn acceleration(&self, _position: &Vec3, _mass: f64) -> Vec3 {
self.g
}
fn potential(&self, position: &Vec3, mass: f64) -> f64 {
-mass * self.g.dot(position)
}
}
#[derive(Debug, Clone)]
pub struct CentralForceField {
pub mu: f64,
pub center: Vec3,
}
impl CentralForceField {
#[must_use]
pub const fn new(mu: f64, center: Vec3) -> Self {
Self { mu, center }
}
}
impl ForceField for CentralForceField {
fn acceleration(&self, position: &Vec3, _mass: f64) -> Vec3 {
let r = *position - self.center;
let r_mag = r.magnitude();
if r_mag < f64::EPSILON {
return Vec3::zero();
}
let r_hat = r.normalize();
r_hat.scale(-self.mu / (r_mag * r_mag))
}
fn potential(&self, position: &Vec3, mass: f64) -> f64 {
let r = (*position - self.center).magnitude();
if r < f64::EPSILON {
return 0.0;
}
-self.mu * mass / r
}
}
pub trait Integrator {
fn step(&self, state: &mut SimState, force_field: &dyn ForceField, dt: f64) -> SimResult<()>;
fn error_order(&self) -> u32;
fn is_symplectic(&self) -> bool;
}
#[derive(Debug, Clone, Default)]
pub struct VerletIntegrator;
impl VerletIntegrator {
#[must_use]
pub const fn new() -> Self {
Self
}
}
impl Integrator for VerletIntegrator {
fn step(&self, state: &mut SimState, force_field: &dyn ForceField, dt: f64) -> SimResult<()> {
let n = state.num_bodies();
let half_dt = dt / 2.0;
for i in 0..n {
let pos = state.positions()[i];
let vel = state.velocities()[i];
state.set_position(i, pos + vel * half_dt);
}
let mut accelerations = Vec::with_capacity(n);
let mut potential_energy = 0.0;
for i in 0..n {
let pos = state.positions()[i];
let mass = state.masses()[i];
accelerations.push(force_field.acceleration(&pos, mass));
potential_energy += force_field.potential(&pos, mass);
}
for i in 0..n {
let vel = state.velocities()[i];
state.set_velocity(i, vel + accelerations[i] * dt);
}
for i in 0..n {
let pos = state.positions()[i];
let vel = state.velocities()[i];
state.set_position(i, pos + vel * half_dt);
}
state.set_potential_energy(potential_energy);
Ok(())
}
fn error_order(&self) -> u32 {
2
}
fn is_symplectic(&self) -> bool {
true
}
}
#[derive(Debug, Clone, Default)]
pub struct RK4Integrator;
impl RK4Integrator {
#[must_use]
pub const fn new() -> Self {
Self
}
}
impl Integrator for RK4Integrator {
fn step(&self, state: &mut SimState, force_field: &dyn ForceField, dt: f64) -> SimResult<()> {
let n = state.num_bodies();
let half_dt = dt / 2.0;
let sixth_dt = dt / 6.0;
let initial_positions: Vec<Vec3> = state.positions().to_vec();
let initial_velocities: Vec<Vec3> = state.velocities().to_vec();
let mut k1_v = Vec::with_capacity(n);
let k1_q: Vec<Vec3> = initial_velocities.clone();
for i in 0..n {
let mass = state.masses()[i];
k1_v.push(force_field.acceleration(&initial_positions[i], mass));
}
let mut k2_v = Vec::with_capacity(n);
let mut k2_q = Vec::with_capacity(n);
for i in 0..n {
let pos = initial_positions[i] + k1_q[i] * half_dt;
let vel = initial_velocities[i] + k1_v[i] * half_dt;
let mass = state.masses()[i];
k2_v.push(force_field.acceleration(&pos, mass));
k2_q.push(vel);
}
let mut k3_v = Vec::with_capacity(n);
let mut k3_q = Vec::with_capacity(n);
for i in 0..n {
let pos = initial_positions[i] + k2_q[i] * half_dt;
let vel = initial_velocities[i] + k2_v[i] * half_dt;
let mass = state.masses()[i];
k3_v.push(force_field.acceleration(&pos, mass));
k3_q.push(vel);
}
let mut k4_v = Vec::with_capacity(n);
let mut k4_q = Vec::with_capacity(n);
for i in 0..n {
let pos = initial_positions[i] + k3_q[i] * dt;
let vel = initial_velocities[i] + k3_v[i] * dt;
let mass = state.masses()[i];
k4_v.push(force_field.acceleration(&pos, mass));
k4_q.push(vel);
}
let mut potential_energy = 0.0;
for i in 0..n {
let new_vel = initial_velocities[i]
+ (k1_v[i] + k2_v[i] * 2.0 + k3_v[i] * 2.0 + k4_v[i]) * sixth_dt;
let new_pos = initial_positions[i]
+ (k1_q[i] + k2_q[i] * 2.0 + k3_q[i] * 2.0 + k4_q[i]) * sixth_dt;
state.set_velocity(i, new_vel);
state.set_position(i, new_pos);
let mass = state.masses()[i];
potential_energy += force_field.potential(&new_pos, mass);
}
state.set_potential_energy(potential_energy);
Ok(())
}
fn error_order(&self) -> u32 {
4
}
fn is_symplectic(&self) -> bool {
false
}
}
#[derive(Debug, Clone, Default)]
pub struct EulerIntegrator;
impl EulerIntegrator {
#[must_use]
pub const fn new() -> Self {
Self
}
}
impl Integrator for EulerIntegrator {
fn step(&self, state: &mut SimState, force_field: &dyn ForceField, dt: f64) -> SimResult<()> {
let n = state.num_bodies();
let mut potential_energy = 0.0;
for i in 0..n {
let pos = state.positions()[i];
let vel = state.velocities()[i];
let mass = state.masses()[i];
let acc = force_field.acceleration(&pos, mass);
potential_energy += force_field.potential(&pos, mass);
state.set_position(i, pos + vel * dt);
state.set_velocity(i, vel + acc * dt);
}
state.set_potential_energy(potential_energy);
Ok(())
}
fn error_order(&self) -> u32 {
1
}
fn is_symplectic(&self) -> bool {
false
}
}
pub struct PhysicsEngine {
force_field: Box<dyn ForceField + Send + Sync>,
integrator: Box<dyn Integrator + Send + Sync>,
}
impl std::fmt::Debug for PhysicsEngine {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PhysicsEngine")
.field("force_field", &"<dyn ForceField>")
.field("integrator", &"<dyn Integrator>")
.finish()
}
}
impl PhysicsEngine {
pub fn new<F, I>(force_field: F, integrator: I) -> Self
where
F: ForceField + Send + Sync + 'static,
I: Integrator + Send + Sync + 'static,
{
Self {
force_field: Box::new(force_field),
integrator: Box::new(integrator),
}
}
#[must_use]
pub fn new_boxed(
force_field: Box<dyn ForceField + Send + Sync>,
integrator: Box<dyn Integrator + Send + Sync>,
) -> Self {
Self {
force_field,
integrator,
}
}
pub fn step(&self, state: &mut SimState, dt: f64) -> SimResult<()> {
self.integrator.step(state, self.force_field.as_ref(), dt)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gravity_field() {
let field = GravityField::default();
let pos = Vec3::new(0.0, 0.0, 10.0);
let mass = 1.0;
let acc = field.acceleration(&pos, mass);
assert!((acc.z - (-9.81)).abs() < f64::EPSILON);
let pe = field.potential(&pos, mass);
assert!((pe - 98.1).abs() < 0.01); }
#[test]
fn test_central_force_field() {
let field = CentralForceField::new(1.0, Vec3::zero());
let pos = Vec3::new(1.0, 0.0, 0.0);
let mass = 1.0;
let acc = field.acceleration(&pos, mass);
assert!(acc.x < 0.0);
assert!(acc.y.abs() < f64::EPSILON);
assert!(acc.z.abs() < f64::EPSILON);
}
#[test]
fn test_verlet_energy_conservation() {
let mut state = SimState::new();
state.add_body(1.0, Vec3::new(1.0, 0.0, 0.0), Vec3::zero());
struct SpringField;
impl ForceField for SpringField {
fn acceleration(&self, position: &Vec3, _mass: f64) -> Vec3 {
Vec3::new(-position.x, -position.y, -position.z)
}
fn potential(&self, position: &Vec3, _mass: f64) -> f64 {
0.5 * position.magnitude_squared()
}
}
let integrator = VerletIntegrator::new();
let dt = 0.001;
integrator.step(&mut state, &SpringField, dt).ok();
let initial_energy = state.total_energy();
for _ in 0..10000 {
integrator.step(&mut state, &SpringField, dt).ok();
}
let final_energy = state.total_energy();
let drift = (final_energy - initial_energy).abs() / initial_energy;
assert!(drift < 0.01, "Energy drift {} too large", drift);
}
#[test]
fn test_euler_vs_verlet_energy() {
fn run_simulation<I: Integrator>(integrator: &I, steps: usize, dt: f64) -> f64 {
let mut state = SimState::new();
state.add_body(1.0, Vec3::new(1.0, 0.0, 0.0), Vec3::zero());
struct SpringField;
impl ForceField for SpringField {
fn acceleration(&self, position: &Vec3, _mass: f64) -> Vec3 {
Vec3::new(-position.x, -position.y, -position.z)
}
fn potential(&self, position: &Vec3, _mass: f64) -> f64 {
0.5 * position.magnitude_squared()
}
}
integrator.step(&mut state, &SpringField, dt).ok();
let initial_energy = state.total_energy();
for _ in 0..steps {
integrator.step(&mut state, &SpringField, dt).ok();
}
(state.total_energy() - initial_energy).abs() / initial_energy
}
let verlet_drift = run_simulation(&VerletIntegrator::new(), 1000, 0.01);
let euler_drift = run_simulation(&EulerIntegrator::new(), 1000, 0.01);
assert!(
verlet_drift < euler_drift,
"Verlet drift {} should be less than Euler drift {}",
verlet_drift,
euler_drift
);
}
#[test]
fn test_integrator_properties() {
let verlet = VerletIntegrator::new();
assert_eq!(verlet.error_order(), 2);
assert!(verlet.is_symplectic());
let euler = EulerIntegrator::new();
assert_eq!(euler.error_order(), 1);
assert!(!euler.is_symplectic());
let rk4 = RK4Integrator::new();
assert_eq!(rk4.error_order(), 4);
assert!(!rk4.is_symplectic());
}
#[test]
fn test_rk4_accuracy() {
let mut state = SimState::new();
state.add_body(1.0, Vec3::new(1.0, 0.0, 0.0), Vec3::zero());
struct SpringField;
impl ForceField for SpringField {
fn acceleration(&self, position: &Vec3, _mass: f64) -> Vec3 {
Vec3::new(-position.x, -position.y, -position.z)
}
fn potential(&self, position: &Vec3, _mass: f64) -> f64 {
0.5 * position.magnitude_squared()
}
}
let rk4 = RK4Integrator::new();
let dt = 0.01;
rk4.step(&mut state, &SpringField, dt).ok();
let initial_energy = state.total_energy();
for _ in 0..100 {
rk4.step(&mut state, &SpringField, dt).ok();
}
let final_energy = state.total_energy();
let drift = (final_energy - initial_energy).abs() / initial_energy;
assert!(drift < 0.001, "RK4 energy drift {} too large", drift);
}
#[test]
fn test_rk4_vs_euler() {
fn run_simulation<I: Integrator>(integrator: &I, steps: usize, dt: f64) -> f64 {
let mut state = SimState::new();
state.add_body(1.0, Vec3::new(1.0, 0.0, 0.0), Vec3::zero());
struct SpringField;
impl ForceField for SpringField {
fn acceleration(&self, position: &Vec3, _mass: f64) -> Vec3 {
Vec3::new(-position.x, -position.y, -position.z)
}
fn potential(&self, position: &Vec3, _mass: f64) -> f64 {
0.5 * position.magnitude_squared()
}
}
integrator.step(&mut state, &SpringField, dt).ok();
let initial_energy = state.total_energy();
for _ in 0..steps {
integrator.step(&mut state, &SpringField, dt).ok();
}
(state.total_energy() - initial_energy).abs() / initial_energy
}
let rk4_drift = run_simulation(&RK4Integrator::new(), 100, 0.01);
let euler_drift = run_simulation(&EulerIntegrator::new(), 100, 0.01);
assert!(
rk4_drift < euler_drift,
"RK4 drift {} should be less than Euler drift {}",
rk4_drift,
euler_drift
);
}
#[test]
fn test_rk4_free_particle() {
let mut state = SimState::new();
state.add_body(1.0, Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 2.0, 3.0));
struct ZeroField;
impl ForceField for ZeroField {
fn acceleration(&self, _: &Vec3, _: f64) -> Vec3 {
Vec3::zero()
}
fn potential(&self, _: &Vec3, _: f64) -> f64 {
0.0
}
}
let rk4 = RK4Integrator::new();
let dt = 0.1;
for _ in 0..100 {
rk4.step(&mut state, &ZeroField, dt).ok();
}
let pos = state.positions()[0];
assert!((pos.x - 10.0).abs() < 1e-10, "x={}", pos.x);
assert!((pos.y - 20.0).abs() < 1e-10, "y={}", pos.y);
assert!((pos.z - 30.0).abs() < 1e-10, "z={}", pos.z);
let vel = state.velocities()[0];
assert!((vel.x - 1.0).abs() < 1e-10);
assert!((vel.y - 2.0).abs() < 1e-10);
assert!((vel.z - 3.0).abs() < 1e-10);
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn prop_verlet_energy_conservation(
x0 in 0.1f64..10.0, v0 in -10.0f64..10.0,
mass in 0.1f64..10.0,
) {
let mut state = SimState::new();
state.add_body(mass, Vec3::new(x0, 0.0, 0.0), Vec3::new(v0, 0.0, 0.0));
struct SpringField;
impl ForceField for SpringField {
fn acceleration(&self, position: &Vec3, _mass: f64) -> Vec3 {
Vec3::new(-position.x, 0.0, 0.0)
}
fn potential(&self, position: &Vec3, mass: f64) -> f64 {
0.5 * mass * position.x * position.x
}
}
let integrator = VerletIntegrator::new();
let dt = 0.001;
integrator.step(&mut state, &SpringField, dt).ok();
let initial_energy = state.total_energy();
for _ in 0..1000 {
integrator.step(&mut state, &SpringField, dt).ok();
}
let final_energy = state.total_energy();
let drift = (final_energy - initial_energy).abs() / initial_energy.abs().max(0.001);
prop_assert!(drift < 0.1, "Energy drift {} too large", drift);
}
#[test]
fn prop_free_particle_constant_velocity(
x0 in -100.0f64..100.0,
v0 in -10.0f64..10.0,
mass in 0.1f64..10.0,
steps in 10usize..100,
) {
let mut state = SimState::new();
state.add_body(mass, Vec3::new(x0, 0.0, 0.0), Vec3::new(v0, 0.0, 0.0));
struct ZeroField;
impl ForceField for ZeroField {
fn acceleration(&self, _: &Vec3, _: f64) -> Vec3 { Vec3::zero() }
fn potential(&self, _: &Vec3, _: f64) -> f64 { 0.0 }
}
let integrator = VerletIntegrator::new();
let dt = 0.01;
for _ in 0..steps {
integrator.step(&mut state, &ZeroField, dt).ok();
}
let final_vel = state.velocities()[0].x;
prop_assert!((final_vel - v0).abs() < 1e-9,
"Velocity changed from {} to {}", v0, final_vel);
}
}
}