1use crate::engine::state::{SimState, Vec3};
14use crate::error::SimResult;
15
16pub trait ForceField {
18 fn acceleration(&self, position: &Vec3, mass: f64) -> Vec3;
20
21 fn potential(&self, position: &Vec3, mass: f64) -> f64;
23}
24
25#[derive(Debug, Clone)]
27pub struct GravityField {
28 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 -mass * self.g.dot(position)
48 }
49}
50
51#[derive(Debug, Clone)]
53pub struct CentralForceField {
54 pub mu: f64,
56 pub center: Vec3,
58}
59
60impl CentralForceField {
61 #[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 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 -self.mu * mass / r
91 }
92}
93
94pub trait Integrator {
96 fn step(&self, state: &mut SimState, force_field: &dyn ForceField, dt: f64) -> SimResult<()>;
102
103 fn error_order(&self) -> u32;
105
106 fn is_symplectic(&self) -> bool;
108}
109
110#[derive(Debug, Clone, Default)]
122pub struct VerletIntegrator;
123
124impl VerletIntegrator {
125 #[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 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 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 for i in 0..n {
157 let vel = state.velocities()[i];
158 state.set_velocity(i, vel + accelerations[i] * dt);
159 }
160
161 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 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#[derive(Debug, Clone, Default)]
206pub struct RK4Integrator;
207
208impl RK4Integrator {
209 #[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 let initial_positions: Vec<Vec3> = state.positions().to_vec();
224 let initial_velocities: Vec<Vec3> = state.velocities().to_vec();
225
226 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 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 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 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 let mut potential_energy = 0.0;
276
277 for i in 0..n {
278 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 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#[derive(Debug, Clone, Default)]
311pub struct EulerIntegrator;
312
313impl EulerIntegrator {
314 #[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 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
353pub struct PhysicsEngine {
355 force_field: Box<dyn ForceField + Send + Sync>,
357 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 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 #[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 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); }
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 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 state.add_body(1.0, Vec3::new(1.0, 0.0, 0.0), Vec3::zero());
442
443 struct SpringField;
445 impl ForceField for SpringField {
446 fn acceleration(&self, position: &Vec3, _mass: f64) -> Vec3 {
447 Vec3::new(-position.x, -position.y, -position.z)
449 }
450 fn potential(&self, position: &Vec3, _mass: f64) -> f64 {
451 0.5 * position.magnitude_squared()
453 }
454 }
455
456 let integrator = VerletIntegrator::new();
457 let dt = 0.001;
458
459 integrator.step(&mut state, &SpringField, dt).ok();
461 let initial_energy = state.total_energy();
462
463 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 assert!(drift < 0.01, "Energy drift {} too large", drift);
473 }
474
475 #[test]
476 fn test_euler_vs_verlet_energy() {
477 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 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 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 rk4.step(&mut state, &SpringField, dt).ok();
551 let initial_energy = state.total_energy();
552
553 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 assert!(drift < 0.001, "RK4 energy drift {} too large", drift);
563 }
564
565 #[test]
566 fn test_rk4_vs_euler() {
567 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 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 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 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 #[test]
648 fn prop_verlet_energy_conservation(
649 x0 in 0.1f64..10.0, 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 0.5 * mass * position.x * position.x
664 }
665 }
666
667 let integrator = VerletIntegrator::new();
668 let dt = 0.001;
669
670 integrator.step(&mut state, &SpringField, dt).ok();
672 let initial_energy = state.total_energy();
673
674 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 #[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 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 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}