Skip to main content

simular/domains/
physics.rs

1//! Physics domain engine.
2//!
3//! Implements numerical integration for physical simulations:
4//! - Verlet (symplectic, 2nd order)
5//! - Euler (1st order)
6//! - RK4 (4th order)
7//!
8//! # Energy Conservation
9//!
10//! Symplectic integrators (Verlet) preserve phase space volume,
11//! leading to bounded energy error over long simulations.
12
13use crate::engine::state::{SimState, Vec3};
14use crate::error::SimResult;
15
16/// Force field trait for computing accelerations.
17pub trait ForceField {
18    /// Compute acceleration for a body at given position.
19    fn acceleration(&self, position: &Vec3, mass: f64) -> Vec3;
20
21    /// Compute potential energy at given position.
22    fn potential(&self, position: &Vec3, mass: f64) -> f64;
23}
24
25/// Simple gravity force field.
26#[derive(Debug, Clone)]
27pub struct GravityField {
28    /// Gravitational acceleration (default: -9.81 m/s² in z).
29    pub g: Vec3,
30}
31
32impl Default for GravityField {
33    fn default() -> Self {
34        Self {
35            g: Vec3::new(0.0, 0.0, -9.81),
36        }
37    }
38}
39
40impl ForceField for GravityField {
41    fn acceleration(&self, _position: &Vec3, _mass: f64) -> Vec3 {
42        self.g
43    }
44
45    fn potential(&self, position: &Vec3, mass: f64) -> f64 {
46        // PE = m * g * h (assuming g points in -z direction)
47        -mass * self.g.dot(position)
48    }
49}
50
51/// Central force field (e.g., gravity toward origin).
52#[derive(Debug, Clone)]
53pub struct CentralForceField {
54    /// Gravitational parameter (G * M).
55    pub mu: f64,
56    /// Center position.
57    pub center: Vec3,
58}
59
60impl CentralForceField {
61    /// Create a new central force field.
62    #[must_use]
63    pub const fn new(mu: f64, center: Vec3) -> Self {
64        Self { mu, center }
65    }
66}
67
68impl ForceField for CentralForceField {
69    fn acceleration(&self, position: &Vec3, _mass: f64) -> Vec3 {
70        let r = *position - self.center;
71        let r_mag = r.magnitude();
72
73        if r_mag < f64::EPSILON {
74            return Vec3::zero();
75        }
76
77        // a = -μ/r² * r̂
78        let r_hat = r.normalize();
79        r_hat.scale(-self.mu / (r_mag * r_mag))
80    }
81
82    fn potential(&self, position: &Vec3, mass: f64) -> f64 {
83        let r = (*position - self.center).magnitude();
84
85        if r < f64::EPSILON {
86            return 0.0;
87        }
88
89        // PE = -G*M*m/r = -μ*m/r
90        -self.mu * mass / r
91    }
92}
93
94/// Numerical integrator trait.
95pub trait Integrator {
96    /// Step the state forward by one timestep.
97    ///
98    /// # Errors
99    ///
100    /// Returns error if integration fails.
101    fn step(&self, state: &mut SimState, force_field: &dyn ForceField, dt: f64) -> SimResult<()>;
102
103    /// Get the error order of this integrator.
104    fn error_order(&self) -> u32;
105
106    /// Check if integrator is symplectic (preserves phase space volume).
107    fn is_symplectic(&self) -> bool;
108}
109
110/// Störmer-Verlet symplectic integrator.
111///
112/// Second-order, symplectic method with excellent energy conservation.
113/// Error is O(h²) but energy oscillates around true value without drift.
114///
115/// Algorithm:
116/// ```text
117/// q_{n+1/2} = q_n + (h/2) * v_n
118/// v_{n+1}   = v_n + h * a(q_{n+1/2})
119/// q_{n+1}   = q_{n+1/2} + (h/2) * v_{n+1}
120/// ```
121#[derive(Debug, Clone, Default)]
122pub struct VerletIntegrator;
123
124impl VerletIntegrator {
125    /// Create a new Verlet integrator.
126    #[must_use]
127    pub const fn new() -> Self {
128        Self
129    }
130}
131
132impl Integrator for VerletIntegrator {
133    fn step(&self, state: &mut SimState, force_field: &dyn ForceField, dt: f64) -> SimResult<()> {
134        let n = state.num_bodies();
135        let half_dt = dt / 2.0;
136
137        // Half-step position update: q_{n+1/2} = q_n + (h/2) * v_n
138        for i in 0..n {
139            let pos = state.positions()[i];
140            let vel = state.velocities()[i];
141            state.set_position(i, pos + vel * half_dt);
142        }
143
144        // Compute accelerations at half-step positions
145        let mut accelerations = Vec::with_capacity(n);
146        let mut potential_energy = 0.0;
147
148        for i in 0..n {
149            let pos = state.positions()[i];
150            let mass = state.masses()[i];
151            accelerations.push(force_field.acceleration(&pos, mass));
152            potential_energy += force_field.potential(&pos, mass);
153        }
154
155        // Full-step velocity update: v_{n+1} = v_n + h * a(q_{n+1/2})
156        for i in 0..n {
157            let vel = state.velocities()[i];
158            state.set_velocity(i, vel + accelerations[i] * dt);
159        }
160
161        // Half-step position update: q_{n+1} = q_{n+1/2} + (h/2) * v_{n+1}
162        for i in 0..n {
163            let pos = state.positions()[i];
164            let vel = state.velocities()[i];
165            state.set_position(i, pos + vel * half_dt);
166        }
167
168        // Update potential energy
169        state.set_potential_energy(potential_energy);
170
171        Ok(())
172    }
173
174    fn error_order(&self) -> u32 {
175        2
176    }
177
178    fn is_symplectic(&self) -> bool {
179        true
180    }
181}
182
183/// Runge-Kutta 4th order integrator.
184///
185/// Fourth-order accurate, non-symplectic. Excellent for smooth problems
186/// but energy may drift in long-term simulations.
187///
188/// Algorithm (classical RK4):
189/// ```text
190/// k1_v = a(q_n)
191/// k1_q = v_n
192///
193/// k2_v = a(q_n + h/2 * k1_q)
194/// k2_q = v_n + h/2 * k1_v
195///
196/// k3_v = a(q_n + h/2 * k2_q)
197/// k3_q = v_n + h/2 * k2_v
198///
199/// k4_v = a(q_n + h * k3_q)
200/// k4_q = v_n + h * k3_v
201///
202/// v_{n+1} = v_n + h/6 * (k1_v + 2*k2_v + 2*k3_v + k4_v)
203/// q_{n+1} = q_n + h/6 * (k1_q + 2*k2_q + 2*k3_q + k4_q)
204/// ```
205#[derive(Debug, Clone, Default)]
206pub struct RK4Integrator;
207
208impl RK4Integrator {
209    /// Create a new RK4 integrator.
210    #[must_use]
211    pub const fn new() -> Self {
212        Self
213    }
214}
215
216impl Integrator for RK4Integrator {
217    fn step(&self, state: &mut SimState, force_field: &dyn ForceField, dt: f64) -> SimResult<()> {
218        let n = state.num_bodies();
219        let half_dt = dt / 2.0;
220        let sixth_dt = dt / 6.0;
221
222        // Save initial state
223        let initial_positions: Vec<Vec3> = state.positions().to_vec();
224        let initial_velocities: Vec<Vec3> = state.velocities().to_vec();
225
226        // Stage 1: k1 at t_n
227        let mut k1_v = Vec::with_capacity(n);
228        let k1_q: Vec<Vec3> = initial_velocities.clone();
229
230        for i in 0..n {
231            let mass = state.masses()[i];
232            k1_v.push(force_field.acceleration(&initial_positions[i], mass));
233        }
234
235        // Stage 2: k2 at t_n + h/2
236        let mut k2_v = Vec::with_capacity(n);
237        let mut k2_q = Vec::with_capacity(n);
238
239        for i in 0..n {
240            let pos = initial_positions[i] + k1_q[i] * half_dt;
241            let vel = initial_velocities[i] + k1_v[i] * half_dt;
242            let mass = state.masses()[i];
243
244            k2_v.push(force_field.acceleration(&pos, mass));
245            k2_q.push(vel);
246        }
247
248        // Stage 3: k3 at t_n + h/2
249        let mut k3_v = Vec::with_capacity(n);
250        let mut k3_q = Vec::with_capacity(n);
251
252        for i in 0..n {
253            let pos = initial_positions[i] + k2_q[i] * half_dt;
254            let vel = initial_velocities[i] + k2_v[i] * half_dt;
255            let mass = state.masses()[i];
256
257            k3_v.push(force_field.acceleration(&pos, mass));
258            k3_q.push(vel);
259        }
260
261        // Stage 4: k4 at t_n + h
262        let mut k4_v = Vec::with_capacity(n);
263        let mut k4_q = Vec::with_capacity(n);
264
265        for i in 0..n {
266            let pos = initial_positions[i] + k3_q[i] * dt;
267            let vel = initial_velocities[i] + k3_v[i] * dt;
268            let mass = state.masses()[i];
269
270            k4_v.push(force_field.acceleration(&pos, mass));
271            k4_q.push(vel);
272        }
273
274        // Final update
275        let mut potential_energy = 0.0;
276
277        for i in 0..n {
278            // v_{n+1} = v_n + h/6 * (k1_v + 2*k2_v + 2*k3_v + k4_v)
279            let new_vel = initial_velocities[i]
280                + (k1_v[i] + k2_v[i] * 2.0 + k3_v[i] * 2.0 + k4_v[i]) * sixth_dt;
281
282            // q_{n+1} = q_n + h/6 * (k1_q + 2*k2_q + 2*k3_q + k4_q)
283            let new_pos = initial_positions[i]
284                + (k1_q[i] + k2_q[i] * 2.0 + k3_q[i] * 2.0 + k4_q[i]) * sixth_dt;
285
286            state.set_velocity(i, new_vel);
287            state.set_position(i, new_pos);
288
289            let mass = state.masses()[i];
290            potential_energy += force_field.potential(&new_pos, mass);
291        }
292
293        state.set_potential_energy(potential_energy);
294        Ok(())
295    }
296
297    fn error_order(&self) -> u32 {
298        4
299    }
300
301    fn is_symplectic(&self) -> bool {
302        false
303    }
304}
305
306/// Euler integrator (1st order, non-symplectic).
307///
308/// Simple but inaccurate. Energy drifts over time.
309/// Useful for comparison and debugging.
310#[derive(Debug, Clone, Default)]
311pub struct EulerIntegrator;
312
313impl EulerIntegrator {
314    /// Create a new Euler integrator.
315    #[must_use]
316    pub const fn new() -> Self {
317        Self
318    }
319}
320
321impl Integrator for EulerIntegrator {
322    fn step(&self, state: &mut SimState, force_field: &dyn ForceField, dt: f64) -> SimResult<()> {
323        let n = state.num_bodies();
324        let mut potential_energy = 0.0;
325
326        for i in 0..n {
327            let pos = state.positions()[i];
328            let vel = state.velocities()[i];
329            let mass = state.masses()[i];
330
331            let acc = force_field.acceleration(&pos, mass);
332            potential_energy += force_field.potential(&pos, mass);
333
334            // x_{n+1} = x_n + v_n * dt
335            // v_{n+1} = v_n + a_n * dt
336            state.set_position(i, pos + vel * dt);
337            state.set_velocity(i, vel + acc * dt);
338        }
339
340        state.set_potential_energy(potential_energy);
341        Ok(())
342    }
343
344    fn error_order(&self) -> u32 {
345        1
346    }
347
348    fn is_symplectic(&self) -> bool {
349        false
350    }
351}
352
353/// Physics engine wrapper.
354pub struct PhysicsEngine {
355    /// Force field.
356    force_field: Box<dyn ForceField + Send + Sync>,
357    /// Integrator.
358    integrator: Box<dyn Integrator + Send + Sync>,
359}
360
361impl std::fmt::Debug for PhysicsEngine {
362    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
363        f.debug_struct("PhysicsEngine")
364            .field("force_field", &"<dyn ForceField>")
365            .field("integrator", &"<dyn Integrator>")
366            .finish()
367    }
368}
369
370impl PhysicsEngine {
371    /// Create a new physics engine.
372    pub fn new<F, I>(force_field: F, integrator: I) -> Self
373    where
374        F: ForceField + Send + Sync + 'static,
375        I: Integrator + Send + Sync + 'static,
376    {
377        Self {
378            force_field: Box::new(force_field),
379            integrator: Box::new(integrator),
380        }
381    }
382
383    /// Create a new physics engine from boxed components.
384    #[must_use]
385    pub fn new_boxed(
386        force_field: Box<dyn ForceField + Send + Sync>,
387        integrator: Box<dyn Integrator + Send + Sync>,
388    ) -> Self {
389        Self {
390            force_field,
391            integrator,
392        }
393    }
394
395    /// Step the physics simulation.
396    ///
397    /// # Errors
398    ///
399    /// Returns error if integration fails.
400    pub fn step(&self, state: &mut SimState, dt: f64) -> SimResult<()> {
401        self.integrator.step(state, self.force_field.as_ref(), dt)
402    }
403}
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408
409    #[test]
410    fn test_gravity_field() {
411        let field = GravityField::default();
412        let pos = Vec3::new(0.0, 0.0, 10.0);
413        let mass = 1.0;
414
415        let acc = field.acceleration(&pos, mass);
416        assert!((acc.z - (-9.81)).abs() < f64::EPSILON);
417
418        let pe = field.potential(&pos, mass);
419        assert!((pe - 98.1).abs() < 0.01); // m * g * h = 1 * 9.81 * 10
420    }
421
422    #[test]
423    fn test_central_force_field() {
424        let field = CentralForceField::new(1.0, Vec3::zero());
425        let pos = Vec3::new(1.0, 0.0, 0.0);
426        let mass = 1.0;
427
428        let acc = field.acceleration(&pos, mass);
429        // Should point toward origin
430        assert!(acc.x < 0.0);
431        assert!(acc.y.abs() < f64::EPSILON);
432        assert!(acc.z.abs() < f64::EPSILON);
433    }
434
435    #[test]
436    fn test_verlet_energy_conservation() {
437        let mut state = SimState::new();
438
439        // Simple harmonic oscillator: mass on spring
440        // F = -kx, k = 1, m = 1
441        state.add_body(1.0, Vec3::new(1.0, 0.0, 0.0), Vec3::zero());
442
443        // Custom spring force
444        struct SpringField;
445        impl ForceField for SpringField {
446            fn acceleration(&self, position: &Vec3, _mass: f64) -> Vec3 {
447                // a = -x (k/m = 1)
448                Vec3::new(-position.x, -position.y, -position.z)
449            }
450            fn potential(&self, position: &Vec3, _mass: f64) -> f64 {
451                // PE = 0.5 * k * x^2
452                0.5 * position.magnitude_squared()
453            }
454        }
455
456        let integrator = VerletIntegrator::new();
457        let dt = 0.001;
458
459        // Initial energy
460        integrator.step(&mut state, &SpringField, dt).ok();
461        let initial_energy = state.total_energy();
462
463        // Simulate many steps
464        for _ in 0..10000 {
465            integrator.step(&mut state, &SpringField, dt).ok();
466        }
467
468        let final_energy = state.total_energy();
469        let drift = (final_energy - initial_energy).abs() / initial_energy;
470
471        // Verlet should have very small energy drift
472        assert!(drift < 0.01, "Energy drift {} too large", drift);
473    }
474
475    #[test]
476    fn test_euler_vs_verlet_energy() {
477        // Euler should have worse energy conservation than Verlet
478
479        fn run_simulation<I: Integrator>(integrator: &I, steps: usize, dt: f64) -> f64 {
480            let mut state = SimState::new();
481            state.add_body(1.0, Vec3::new(1.0, 0.0, 0.0), Vec3::zero());
482
483            struct SpringField;
484            impl ForceField for SpringField {
485                fn acceleration(&self, position: &Vec3, _mass: f64) -> Vec3 {
486                    Vec3::new(-position.x, -position.y, -position.z)
487                }
488                fn potential(&self, position: &Vec3, _mass: f64) -> f64 {
489                    0.5 * position.magnitude_squared()
490                }
491            }
492
493            integrator.step(&mut state, &SpringField, dt).ok();
494            let initial_energy = state.total_energy();
495
496            for _ in 0..steps {
497                integrator.step(&mut state, &SpringField, dt).ok();
498            }
499
500            (state.total_energy() - initial_energy).abs() / initial_energy
501        }
502
503        let verlet_drift = run_simulation(&VerletIntegrator::new(), 1000, 0.01);
504        let euler_drift = run_simulation(&EulerIntegrator::new(), 1000, 0.01);
505
506        // Verlet should be much better
507        assert!(
508            verlet_drift < euler_drift,
509            "Verlet drift {} should be less than Euler drift {}",
510            verlet_drift,
511            euler_drift
512        );
513    }
514
515    #[test]
516    fn test_integrator_properties() {
517        let verlet = VerletIntegrator::new();
518        assert_eq!(verlet.error_order(), 2);
519        assert!(verlet.is_symplectic());
520
521        let euler = EulerIntegrator::new();
522        assert_eq!(euler.error_order(), 1);
523        assert!(!euler.is_symplectic());
524
525        let rk4 = RK4Integrator::new();
526        assert_eq!(rk4.error_order(), 4);
527        assert!(!rk4.is_symplectic());
528    }
529
530    #[test]
531    fn test_rk4_accuracy() {
532        // RK4 should be more accurate than Euler for short-term simulation
533        let mut state = SimState::new();
534        state.add_body(1.0, Vec3::new(1.0, 0.0, 0.0), Vec3::zero());
535
536        struct SpringField;
537        impl ForceField for SpringField {
538            fn acceleration(&self, position: &Vec3, _mass: f64) -> Vec3 {
539                Vec3::new(-position.x, -position.y, -position.z)
540            }
541            fn potential(&self, position: &Vec3, _mass: f64) -> f64 {
542                0.5 * position.magnitude_squared()
543            }
544        }
545
546        let rk4 = RK4Integrator::new();
547        let dt = 0.01;
548
549        // Initial energy
550        rk4.step(&mut state, &SpringField, dt).ok();
551        let initial_energy = state.total_energy();
552
553        // Simulate
554        for _ in 0..100 {
555            rk4.step(&mut state, &SpringField, dt).ok();
556        }
557
558        let final_energy = state.total_energy();
559        let drift = (final_energy - initial_energy).abs() / initial_energy;
560
561        // RK4 should have good short-term accuracy
562        assert!(drift < 0.001, "RK4 energy drift {} too large", drift);
563    }
564
565    #[test]
566    fn test_rk4_vs_euler() {
567        // RK4 should be more accurate than Euler
568        fn run_simulation<I: Integrator>(integrator: &I, steps: usize, dt: f64) -> f64 {
569            let mut state = SimState::new();
570            state.add_body(1.0, Vec3::new(1.0, 0.0, 0.0), Vec3::zero());
571
572            struct SpringField;
573            impl ForceField for SpringField {
574                fn acceleration(&self, position: &Vec3, _mass: f64) -> Vec3 {
575                    Vec3::new(-position.x, -position.y, -position.z)
576                }
577                fn potential(&self, position: &Vec3, _mass: f64) -> f64 {
578                    0.5 * position.magnitude_squared()
579                }
580            }
581
582            integrator.step(&mut state, &SpringField, dt).ok();
583            let initial_energy = state.total_energy();
584
585            for _ in 0..steps {
586                integrator.step(&mut state, &SpringField, dt).ok();
587            }
588
589            (state.total_energy() - initial_energy).abs() / initial_energy
590        }
591
592        let rk4_drift = run_simulation(&RK4Integrator::new(), 100, 0.01);
593        let euler_drift = run_simulation(&EulerIntegrator::new(), 100, 0.01);
594
595        assert!(
596            rk4_drift < euler_drift,
597            "RK4 drift {} should be less than Euler drift {}",
598            rk4_drift,
599            euler_drift
600        );
601    }
602
603    #[test]
604    fn test_rk4_free_particle() {
605        // Free particle should move at constant velocity
606        let mut state = SimState::new();
607        state.add_body(1.0, Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 2.0, 3.0));
608
609        struct ZeroField;
610        impl ForceField for ZeroField {
611            fn acceleration(&self, _: &Vec3, _: f64) -> Vec3 {
612                Vec3::zero()
613            }
614            fn potential(&self, _: &Vec3, _: f64) -> f64 {
615                0.0
616            }
617        }
618
619        let rk4 = RK4Integrator::new();
620        let dt = 0.1;
621
622        for _ in 0..100 {
623            rk4.step(&mut state, &ZeroField, dt).ok();
624        }
625
626        // After 10 seconds, position should be (10, 20, 30)
627        let pos = state.positions()[0];
628        assert!((pos.x - 10.0).abs() < 1e-10, "x={}", pos.x);
629        assert!((pos.y - 20.0).abs() < 1e-10, "y={}", pos.y);
630        assert!((pos.z - 30.0).abs() < 1e-10, "z={}", pos.z);
631
632        // Velocity should be unchanged
633        let vel = state.velocities()[0];
634        assert!((vel.x - 1.0).abs() < 1e-10);
635        assert!((vel.y - 2.0).abs() < 1e-10);
636        assert!((vel.z - 3.0).abs() < 1e-10);
637    }
638}
639
640#[cfg(test)]
641mod proptests {
642    use super::*;
643    use proptest::prelude::*;
644
645    proptest! {
646        /// Falsification: Verlet preserves energy for harmonic oscillator.
647        #[test]
648        fn prop_verlet_energy_conservation(
649            x0 in 0.1f64..10.0,  // Ensure non-zero initial displacement
650            v0 in -10.0f64..10.0,
651            mass in 0.1f64..10.0,
652        ) {
653            let mut state = SimState::new();
654            state.add_body(mass, Vec3::new(x0, 0.0, 0.0), Vec3::new(v0, 0.0, 0.0));
655
656            struct SpringField;
657            impl ForceField for SpringField {
658                fn acceleration(&self, position: &Vec3, _mass: f64) -> Vec3 {
659                    Vec3::new(-position.x, 0.0, 0.0)
660                }
661                fn potential(&self, position: &Vec3, mass: f64) -> f64 {
662                    // PE for spring: 0.5 * k * x^2, with k = mass (so omega = 1)
663                    0.5 * mass * position.x * position.x
664                }
665            }
666
667            let integrator = VerletIntegrator::new();
668            let dt = 0.001;
669
670            // Get initial energy after first step
671            integrator.step(&mut state, &SpringField, dt).ok();
672            let initial_energy = state.total_energy();
673
674            // Simulate
675            for _ in 0..1000 {
676                integrator.step(&mut state, &SpringField, dt).ok();
677            }
678
679            let final_energy = state.total_energy();
680            let drift = (final_energy - initial_energy).abs() / initial_energy.abs().max(0.001);
681
682            prop_assert!(drift < 0.1, "Energy drift {} too large", drift);
683        }
684
685        /// Falsification: free particle moves at constant velocity.
686        #[test]
687        fn prop_free_particle_constant_velocity(
688            x0 in -100.0f64..100.0,
689            v0 in -10.0f64..10.0,
690            mass in 0.1f64..10.0,
691            steps in 10usize..100,
692        ) {
693            let mut state = SimState::new();
694            state.add_body(mass, Vec3::new(x0, 0.0, 0.0), Vec3::new(v0, 0.0, 0.0));
695
696            // Zero force field
697            struct ZeroField;
698            impl ForceField for ZeroField {
699                fn acceleration(&self, _: &Vec3, _: f64) -> Vec3 { Vec3::zero() }
700                fn potential(&self, _: &Vec3, _: f64) -> f64 { 0.0 }
701            }
702
703            let integrator = VerletIntegrator::new();
704            let dt = 0.01;
705
706            for _ in 0..steps {
707                integrator.step(&mut state, &ZeroField, dt).ok();
708            }
709
710            // Velocity should be unchanged
711            let final_vel = state.velocities()[0].x;
712            prop_assert!((final_vel - v0).abs() < 1e-9,
713                "Velocity changed from {} to {}", v0, final_vel);
714        }
715    }
716}