Skip to main content

simular/engine/
state.rs

1//! Simulation state management.
2//!
3//! Implements the world state with:
4//! - Entity positions and velocities
5//! - Mass properties
6//! - Energy computation
7//! - Constraint tracking
8
9use crate::error::SimResult;
10use serde::{Deserialize, Serialize};
11
12/// 3D vector for positions and velocities.
13#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
14pub struct Vec3 {
15    /// X component.
16    pub x: f64,
17    /// Y component.
18    pub y: f64,
19    /// Z component.
20    pub z: f64,
21}
22
23impl Vec3 {
24    /// Create a new vector.
25    #[must_use]
26    pub const fn new(x: f64, y: f64, z: f64) -> Self {
27        Self { x, y, z }
28    }
29
30    /// Zero vector.
31    #[must_use]
32    pub const fn zero() -> Self {
33        Self {
34            x: 0.0,
35            y: 0.0,
36            z: 0.0,
37        }
38    }
39
40    /// Magnitude squared.
41    #[must_use]
42    pub fn magnitude_squared(&self) -> f64 {
43        self.x * self.x + self.y * self.y + self.z * self.z
44    }
45
46    /// Magnitude (length).
47    #[must_use]
48    pub fn magnitude(&self) -> f64 {
49        self.magnitude_squared().sqrt()
50    }
51
52    /// Dot product.
53    #[must_use]
54    pub fn dot(&self, other: &Self) -> f64 {
55        self.x * other.x + self.y * other.y + self.z * other.z
56    }
57
58    /// Cross product.
59    #[must_use]
60    pub fn cross(&self, other: &Self) -> Self {
61        Self {
62            x: self.y * other.z - self.z * other.y,
63            y: self.z * other.x - self.x * other.z,
64            z: self.x * other.y - self.y * other.x,
65        }
66    }
67
68    /// Normalize to unit vector.
69    #[must_use]
70    pub fn normalize(&self) -> Self {
71        let mag = self.magnitude();
72        if mag < f64::EPSILON {
73            Self::zero()
74        } else {
75            Self {
76                x: self.x / mag,
77                y: self.y / mag,
78                z: self.z / mag,
79            }
80        }
81    }
82
83    /// Scale by scalar.
84    #[must_use]
85    pub fn scale(&self, s: f64) -> Self {
86        Self {
87            x: self.x * s,
88            y: self.y * s,
89            z: self.z * s,
90        }
91    }
92
93    /// Check if all components are finite.
94    #[must_use]
95    #[allow(clippy::missing_const_for_fn)] // is_finite not const
96    pub fn is_finite(&self) -> bool {
97        self.x.is_finite() && self.y.is_finite() && self.z.is_finite()
98    }
99}
100
101impl std::ops::Add for Vec3 {
102    type Output = Self;
103
104    fn add(self, rhs: Self) -> Self::Output {
105        Self {
106            x: self.x + rhs.x,
107            y: self.y + rhs.y,
108            z: self.z + rhs.z,
109        }
110    }
111}
112
113impl std::ops::Sub for Vec3 {
114    type Output = Self;
115
116    fn sub(self, rhs: Self) -> Self::Output {
117        Self {
118            x: self.x - rhs.x,
119            y: self.y - rhs.y,
120            z: self.z - rhs.z,
121        }
122    }
123}
124
125impl std::ops::Mul<f64> for Vec3 {
126    type Output = Self;
127
128    fn mul(self, rhs: f64) -> Self::Output {
129        self.scale(rhs)
130    }
131}
132
133impl std::ops::Neg for Vec3 {
134    type Output = Self;
135
136    fn neg(self) -> Self::Output {
137        Self {
138            x: -self.x,
139            y: -self.y,
140            z: -self.z,
141        }
142    }
143}
144
145/// Simulation event for state updates.
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub enum SimEvent {
148    /// Add a body to the simulation.
149    AddBody {
150        /// Body mass.
151        mass: f64,
152        /// Initial position.
153        position: Vec3,
154        /// Initial velocity.
155        velocity: Vec3,
156    },
157    /// Apply force to a body.
158    ApplyForce {
159        /// Body index.
160        body_index: usize,
161        /// Force vector.
162        force: Vec3,
163    },
164    /// Set body position.
165    SetPosition {
166        /// Body index.
167        body_index: usize,
168        /// New position.
169        position: Vec3,
170    },
171    /// Set body velocity.
172    SetVelocity {
173        /// Body index.
174        body_index: usize,
175        /// New velocity.
176        velocity: Vec3,
177    },
178    /// Custom event.
179    Custom {
180        /// Event name.
181        name: String,
182        /// Serialized data.
183        data: Vec<u8>,
184    },
185}
186
187/// Simulation state.
188///
189/// Contains all state variables needed to reproduce the simulation.
190#[derive(Debug, Clone, Default, Serialize, Deserialize)]
191pub struct SimState {
192    /// Body masses.
193    masses: Vec<f64>,
194    /// Body positions.
195    positions: Vec<Vec3>,
196    /// Body velocities.
197    velocities: Vec<Vec3>,
198    /// Accumulated forces (cleared each step).
199    forces: Vec<Vec3>,
200    /// Active constraints and their violations.
201    constraints: Vec<(String, f64)>,
202    /// Potential energy (set by domain engine).
203    potential_energy: f64,
204}
205
206impl SimState {
207    /// Create a new empty state.
208    #[must_use]
209    pub fn new() -> Self {
210        Self::default()
211    }
212
213    /// Get number of bodies.
214    #[must_use]
215    #[allow(clippy::missing_const_for_fn)] // Vec::len not const in older Rust
216    pub fn num_bodies(&self) -> usize {
217        self.masses.len()
218    }
219
220    /// Add a body to the simulation.
221    pub fn add_body(&mut self, mass: f64, position: Vec3, velocity: Vec3) {
222        self.masses.push(mass);
223        self.positions.push(position);
224        self.velocities.push(velocity);
225        self.forces.push(Vec3::zero());
226    }
227
228    /// Get body masses.
229    #[must_use]
230    pub fn masses(&self) -> &[f64] {
231        &self.masses
232    }
233
234    /// Get body positions.
235    #[must_use]
236    pub fn positions(&self) -> &[Vec3] {
237        &self.positions
238    }
239
240    /// Get mutable body positions.
241    #[must_use]
242    pub fn positions_mut(&mut self) -> &mut [Vec3] {
243        &mut self.positions
244    }
245
246    /// Get body velocities.
247    #[must_use]
248    pub fn velocities(&self) -> &[Vec3] {
249        &self.velocities
250    }
251
252    /// Get mutable body velocities.
253    #[must_use]
254    pub fn velocities_mut(&mut self) -> &mut [Vec3] {
255        &mut self.velocities
256    }
257
258    /// Get body forces.
259    #[must_use]
260    pub fn forces(&self) -> &[Vec3] {
261        &self.forces
262    }
263
264    /// Get mutable body forces.
265    #[must_use]
266    pub fn forces_mut(&mut self) -> &mut [Vec3] {
267        &mut self.forces
268    }
269
270    /// Set position for a body.
271    ///
272    /// # Panics
273    ///
274    /// Panics if index is out of bounds.
275    pub fn set_position(&mut self, index: usize, position: Vec3) {
276        self.positions[index] = position;
277    }
278
279    /// Set velocity for a body.
280    ///
281    /// # Panics
282    ///
283    /// Panics if index is out of bounds.
284    pub fn set_velocity(&mut self, index: usize, velocity: Vec3) {
285        self.velocities[index] = velocity;
286    }
287
288    /// Apply force to a body.
289    ///
290    /// # Panics
291    ///
292    /// Panics if index is out of bounds.
293    pub fn apply_force(&mut self, index: usize, force: Vec3) {
294        self.forces[index] = self.forces[index] + force;
295    }
296
297    /// Clear all forces (called at start of each step).
298    pub fn clear_forces(&mut self) {
299        for f in &mut self.forces {
300            *f = Vec3::zero();
301        }
302    }
303
304    /// Set potential energy (called by domain engine).
305    #[allow(clippy::missing_const_for_fn)] // Mutable const not stable
306    pub fn set_potential_energy(&mut self, energy: f64) {
307        self.potential_energy = energy;
308    }
309
310    /// Get total kinetic energy.
311    #[must_use]
312    pub fn kinetic_energy(&self) -> f64 {
313        self.masses
314            .iter()
315            .zip(&self.velocities)
316            .map(|(m, v)| 0.5 * m * v.magnitude_squared())
317            .sum()
318    }
319
320    /// Get potential energy.
321    #[must_use]
322    pub const fn potential_energy(&self) -> f64 {
323        self.potential_energy
324    }
325
326    /// Get total energy (kinetic + potential).
327    #[must_use]
328    pub fn total_energy(&self) -> f64 {
329        self.kinetic_energy() + self.potential_energy
330    }
331
332    /// Add a constraint violation.
333    pub fn add_constraint(&mut self, name: impl Into<String>, violation: f64) {
334        self.constraints.push((name.into(), violation));
335    }
336
337    /// Clear all constraints.
338    pub fn clear_constraints(&mut self) {
339        self.constraints.clear();
340    }
341
342    /// Get constraint violations.
343    pub fn constraint_violations(&self) -> impl Iterator<Item = (String, f64)> + '_ {
344        self.constraints.iter().cloned()
345    }
346
347    /// Check if all state values are finite.
348    #[must_use]
349    pub fn all_finite(&self) -> bool {
350        self.positions.iter().all(Vec3::is_finite)
351            && self.velocities.iter().all(Vec3::is_finite)
352            && self.masses.iter().all(|m| m.is_finite())
353    }
354
355    /// Apply an event to the state.
356    ///
357    /// # Errors
358    ///
359    /// Returns error if event cannot be applied.
360    pub fn apply_event(&mut self, event: &SimEvent) -> SimResult<()> {
361        match event {
362            SimEvent::AddBody {
363                mass,
364                position,
365                velocity,
366            } => {
367                self.add_body(*mass, *position, *velocity);
368            }
369            SimEvent::ApplyForce { body_index, force } => {
370                if *body_index < self.num_bodies() {
371                    self.apply_force(*body_index, *force);
372                }
373            }
374            SimEvent::SetPosition {
375                body_index,
376                position,
377            } => {
378                if *body_index < self.num_bodies() {
379                    self.set_position(*body_index, *position);
380                }
381            }
382            SimEvent::SetVelocity {
383                body_index,
384                velocity,
385            } => {
386                if *body_index < self.num_bodies() {
387                    self.set_velocity(*body_index, *velocity);
388                }
389            }
390            SimEvent::Custom { .. } => {
391                // Custom events are handled by domain-specific code
392            }
393        }
394        Ok(())
395    }
396}
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401
402    #[test]
403    fn test_vec3_operations() {
404        let v1 = Vec3::new(1.0, 2.0, 3.0);
405        let v2 = Vec3::new(4.0, 5.0, 6.0);
406
407        // Addition
408        let sum = v1 + v2;
409        assert!((sum.x - 5.0).abs() < f64::EPSILON);
410        assert!((sum.y - 7.0).abs() < f64::EPSILON);
411        assert!((sum.z - 9.0).abs() < f64::EPSILON);
412
413        // Subtraction
414        let diff = v2 - v1;
415        assert!((diff.x - 3.0).abs() < f64::EPSILON);
416
417        // Dot product
418        let dot = v1.dot(&v2);
419        assert!((dot - 32.0).abs() < f64::EPSILON); // 1*4 + 2*5 + 3*6 = 32
420
421        // Cross product
422        let cross = v1.cross(&v2);
423        assert!((cross.x - (-3.0)).abs() < f64::EPSILON);
424        assert!((cross.y - 6.0).abs() < f64::EPSILON);
425        assert!((cross.z - (-3.0)).abs() < f64::EPSILON);
426
427        // Magnitude
428        let v = Vec3::new(3.0, 4.0, 0.0);
429        assert!((v.magnitude() - 5.0).abs() < f64::EPSILON);
430    }
431
432    #[test]
433    fn test_vec3_normalize() {
434        let v = Vec3::new(3.0, 4.0, 0.0);
435        let n = v.normalize();
436
437        assert!((n.magnitude() - 1.0).abs() < f64::EPSILON);
438        assert!((n.x - 0.6).abs() < f64::EPSILON);
439        assert!((n.y - 0.8).abs() < f64::EPSILON);
440    }
441
442    #[test]
443    fn test_state_add_body() {
444        let mut state = SimState::new();
445
446        state.add_body(1.0, Vec3::new(1.0, 0.0, 0.0), Vec3::zero());
447        state.add_body(2.0, Vec3::new(0.0, 1.0, 0.0), Vec3::zero());
448
449        assert_eq!(state.num_bodies(), 2);
450        assert!((state.masses()[0] - 1.0).abs() < f64::EPSILON);
451        assert!((state.masses()[1] - 2.0).abs() < f64::EPSILON);
452    }
453
454    #[test]
455    fn test_state_energy() {
456        let mut state = SimState::new();
457
458        // Body with mass 2, velocity (3, 0, 0) -> KE = 0.5 * 2 * 9 = 9
459        state.add_body(2.0, Vec3::zero(), Vec3::new(3.0, 0.0, 0.0));
460
461        assert!((state.kinetic_energy() - 9.0).abs() < f64::EPSILON);
462
463        state.set_potential_energy(-5.0);
464        assert!((state.total_energy() - 4.0).abs() < f64::EPSILON);
465    }
466
467    #[test]
468    fn test_state_apply_event() {
469        let mut state = SimState::new();
470
471        let event = SimEvent::AddBody {
472            mass: 1.0,
473            position: Vec3::new(1.0, 2.0, 3.0),
474            velocity: Vec3::zero(),
475        };
476
477        state.apply_event(&event).ok();
478        assert_eq!(state.num_bodies(), 1);
479        assert!((state.positions()[0].x - 1.0).abs() < f64::EPSILON);
480    }
481
482    #[test]
483    fn test_state_constraints() {
484        let mut state = SimState::new();
485
486        state.add_constraint("test1", 0.001);
487        state.add_constraint("test2", -0.002);
488
489        let violations: Vec<_> = state.constraint_violations().collect();
490        assert_eq!(violations.len(), 2);
491
492        state.clear_constraints();
493        let violations: Vec<_> = state.constraint_violations().collect();
494        assert!(violations.is_empty());
495    }
496
497    #[test]
498    fn test_state_all_finite() {
499        let mut state = SimState::new();
500        state.add_body(1.0, Vec3::new(1.0, 2.0, 3.0), Vec3::zero());
501
502        assert!(state.all_finite());
503
504        state.set_position(0, Vec3::new(f64::NAN, 0.0, 0.0));
505        assert!(!state.all_finite());
506    }
507
508    #[test]
509    fn test_vec3_scale() {
510        let v = Vec3::new(1.0, 2.0, 3.0);
511        let scaled = v.scale(2.0);
512        assert!((scaled.x - 2.0).abs() < f64::EPSILON);
513        assert!((scaled.y - 4.0).abs() < f64::EPSILON);
514        assert!((scaled.z - 6.0).abs() < f64::EPSILON);
515    }
516
517    #[test]
518    fn test_vec3_mul_scalar() {
519        let v = Vec3::new(1.0, 2.0, 3.0);
520        let scaled = v * 2.5;
521        assert!((scaled.x - 2.5).abs() < f64::EPSILON);
522        assert!((scaled.y - 5.0).abs() < f64::EPSILON);
523        assert!((scaled.z - 7.5).abs() < f64::EPSILON);
524    }
525
526    #[test]
527    fn test_vec3_neg() {
528        let v = Vec3::new(1.0, -2.0, 3.0);
529        let neg = -v;
530        assert!((neg.x - (-1.0)).abs() < f64::EPSILON);
531        assert!((neg.y - 2.0).abs() < f64::EPSILON);
532        assert!((neg.z - (-3.0)).abs() < f64::EPSILON);
533    }
534
535    #[test]
536    fn test_vec3_is_finite() {
537        let v1 = Vec3::new(1.0, 2.0, 3.0);
538        assert!(v1.is_finite());
539
540        let v2 = Vec3::new(f64::INFINITY, 0.0, 0.0);
541        assert!(!v2.is_finite());
542
543        let v3 = Vec3::new(0.0, f64::NEG_INFINITY, 0.0);
544        assert!(!v3.is_finite());
545
546        let v4 = Vec3::new(0.0, 0.0, f64::NAN);
547        assert!(!v4.is_finite());
548    }
549
550    #[test]
551    fn test_vec3_normalize_zero() {
552        let v = Vec3::zero();
553        let n = v.normalize();
554        assert!((n.x).abs() < f64::EPSILON);
555        assert!((n.y).abs() < f64::EPSILON);
556        assert!((n.z).abs() < f64::EPSILON);
557    }
558
559    #[test]
560    fn test_vec3_default() {
561        let v = Vec3::default();
562        assert!((v.x).abs() < f64::EPSILON);
563        assert!((v.y).abs() < f64::EPSILON);
564        assert!((v.z).abs() < f64::EPSILON);
565    }
566
567    #[test]
568    fn test_vec3_partial_eq() {
569        let v1 = Vec3::new(1.0, 2.0, 3.0);
570        let v2 = Vec3::new(1.0, 2.0, 3.0);
571        let v3 = Vec3::new(1.0, 2.0, 4.0);
572        assert_eq!(v1, v2);
573        assert_ne!(v1, v3);
574    }
575
576    #[test]
577    fn test_vec3_magnitude_squared() {
578        let v = Vec3::new(3.0, 4.0, 0.0);
579        assert!((v.magnitude_squared() - 25.0).abs() < f64::EPSILON);
580    }
581
582    #[test]
583    fn test_state_positions_mut() {
584        let mut state = SimState::new();
585        state.add_body(1.0, Vec3::new(1.0, 2.0, 3.0), Vec3::zero());
586
587        {
588            let positions = state.positions_mut();
589            positions[0] = Vec3::new(10.0, 20.0, 30.0);
590        }
591
592        assert!((state.positions()[0].x - 10.0).abs() < f64::EPSILON);
593        assert!((state.positions()[0].y - 20.0).abs() < f64::EPSILON);
594        assert!((state.positions()[0].z - 30.0).abs() < f64::EPSILON);
595    }
596
597    #[test]
598    fn test_state_velocities_mut() {
599        let mut state = SimState::new();
600        state.add_body(1.0, Vec3::zero(), Vec3::new(1.0, 2.0, 3.0));
601
602        {
603            let velocities = state.velocities_mut();
604            velocities[0] = Vec3::new(5.0, 6.0, 7.0);
605        }
606
607        assert!((state.velocities()[0].x - 5.0).abs() < f64::EPSILON);
608    }
609
610    #[test]
611    fn test_state_forces_mut() {
612        let mut state = SimState::new();
613        state.add_body(1.0, Vec3::zero(), Vec3::zero());
614
615        {
616            let forces = state.forces_mut();
617            forces[0] = Vec3::new(100.0, 200.0, 300.0);
618        }
619
620        assert!((state.forces()[0].x - 100.0).abs() < f64::EPSILON);
621    }
622
623    #[test]
624    fn test_state_clear_forces() {
625        let mut state = SimState::new();
626        state.add_body(1.0, Vec3::zero(), Vec3::zero());
627        state.apply_force(0, Vec3::new(100.0, 200.0, 300.0));
628
629        assert!((state.forces()[0].x - 100.0).abs() < f64::EPSILON);
630
631        state.clear_forces();
632
633        assert!((state.forces()[0].x).abs() < f64::EPSILON);
634        assert!((state.forces()[0].y).abs() < f64::EPSILON);
635        assert!((state.forces()[0].z).abs() < f64::EPSILON);
636    }
637
638    #[test]
639    fn test_state_apply_event_apply_force() {
640        let mut state = SimState::new();
641        state.add_body(1.0, Vec3::zero(), Vec3::zero());
642
643        let event = SimEvent::ApplyForce {
644            body_index: 0,
645            force: Vec3::new(10.0, 20.0, 30.0),
646        };
647
648        state.apply_event(&event).unwrap();
649        assert!((state.forces()[0].x - 10.0).abs() < f64::EPSILON);
650    }
651
652    #[test]
653    fn test_state_apply_event_apply_force_out_of_bounds() {
654        let mut state = SimState::new();
655        state.add_body(1.0, Vec3::zero(), Vec3::zero());
656
657        let event = SimEvent::ApplyForce {
658            body_index: 100, // out of bounds
659            force: Vec3::new(10.0, 20.0, 30.0),
660        };
661
662        // Should not panic, just ignore
663        state.apply_event(&event).unwrap();
664    }
665
666    #[test]
667    fn test_state_apply_event_set_position() {
668        let mut state = SimState::new();
669        state.add_body(1.0, Vec3::zero(), Vec3::zero());
670
671        let event = SimEvent::SetPosition {
672            body_index: 0,
673            position: Vec3::new(5.0, 6.0, 7.0),
674        };
675
676        state.apply_event(&event).unwrap();
677        assert!((state.positions()[0].x - 5.0).abs() < f64::EPSILON);
678    }
679
680    #[test]
681    fn test_state_apply_event_set_position_out_of_bounds() {
682        let mut state = SimState::new();
683        state.add_body(1.0, Vec3::zero(), Vec3::zero());
684
685        let event = SimEvent::SetPosition {
686            body_index: 100, // out of bounds
687            position: Vec3::new(5.0, 6.0, 7.0),
688        };
689
690        // Should not panic, just ignore
691        state.apply_event(&event).unwrap();
692    }
693
694    #[test]
695    fn test_state_apply_event_set_velocity() {
696        let mut state = SimState::new();
697        state.add_body(1.0, Vec3::zero(), Vec3::zero());
698
699        let event = SimEvent::SetVelocity {
700            body_index: 0,
701            velocity: Vec3::new(8.0, 9.0, 10.0),
702        };
703
704        state.apply_event(&event).unwrap();
705        assert!((state.velocities()[0].x - 8.0).abs() < f64::EPSILON);
706    }
707
708    #[test]
709    fn test_state_apply_event_set_velocity_out_of_bounds() {
710        let mut state = SimState::new();
711        state.add_body(1.0, Vec3::zero(), Vec3::zero());
712
713        let event = SimEvent::SetVelocity {
714            body_index: 100, // out of bounds
715            velocity: Vec3::new(8.0, 9.0, 10.0),
716        };
717
718        // Should not panic, just ignore
719        state.apply_event(&event).unwrap();
720    }
721
722    #[test]
723    fn test_state_apply_event_custom() {
724        let mut state = SimState::new();
725
726        let event = SimEvent::Custom {
727            name: "custom_event".to_string(),
728            data: vec![1, 2, 3, 4],
729        };
730
731        // Custom events are no-ops at this level
732        state.apply_event(&event).unwrap();
733    }
734
735    #[test]
736    fn test_sim_event_clone() {
737        let event = SimEvent::AddBody {
738            mass: 1.0,
739            position: Vec3::new(1.0, 2.0, 3.0),
740            velocity: Vec3::zero(),
741        };
742        let cloned = event.clone();
743        match cloned {
744            SimEvent::AddBody { mass, .. } => assert!((mass - 1.0).abs() < f64::EPSILON),
745            _ => panic!("unexpected event type"),
746        }
747    }
748
749    #[test]
750    fn test_sim_event_debug() {
751        let event = SimEvent::AddBody {
752            mass: 1.0,
753            position: Vec3::zero(),
754            velocity: Vec3::zero(),
755        };
756        let debug = format!("{:?}", event);
757        assert!(debug.contains("AddBody"));
758    }
759
760    #[test]
761    fn test_state_clone() {
762        let mut state = SimState::new();
763        state.add_body(1.0, Vec3::new(1.0, 2.0, 3.0), Vec3::zero());
764        state.set_potential_energy(-10.0);
765        state.add_constraint("test", 0.5);
766
767        let cloned = state.clone();
768        assert_eq!(cloned.num_bodies(), 1);
769        assert!((cloned.potential_energy() - (-10.0)).abs() < f64::EPSILON);
770    }
771
772    #[test]
773    fn test_state_debug() {
774        let state = SimState::new();
775        let debug = format!("{:?}", state);
776        assert!(debug.contains("SimState"));
777    }
778
779    #[test]
780    fn test_state_all_finite_with_infinity_in_velocity() {
781        let mut state = SimState::new();
782        state.add_body(1.0, Vec3::zero(), Vec3::new(f64::INFINITY, 0.0, 0.0));
783        assert!(!state.all_finite());
784    }
785
786    #[test]
787    fn test_state_all_finite_with_nan_in_mass() {
788        let mut state = SimState::new();
789        state.add_body(f64::NAN, Vec3::zero(), Vec3::zero());
790        assert!(!state.all_finite());
791    }
792}
793
794#[cfg(test)]
795mod proptests {
796    use super::*;
797    use proptest::prelude::*;
798
799    proptest! {
800        /// Falsification: dot product is commutative.
801        #[test]
802        fn prop_dot_commutative(
803            x1 in -1e6f64..1e6, y1 in -1e6f64..1e6, z1 in -1e6f64..1e6,
804            x2 in -1e6f64..1e6, y2 in -1e6f64..1e6, z2 in -1e6f64..1e6,
805        ) {
806            let v1 = Vec3::new(x1, y1, z1);
807            let v2 = Vec3::new(x2, y2, z2);
808
809            let d1 = v1.dot(&v2);
810            let d2 = v2.dot(&v1);
811
812            prop_assert!((d1 - d2).abs() < 1e-9 * d1.abs().max(1.0));
813        }
814
815        /// Falsification: normalized vectors have unit length.
816        #[test]
817        fn prop_normalize_unit_length(
818            x in -1e6f64..1e6, y in -1e6f64..1e6, z in -1e6f64..1e6,
819        ) {
820            let v = Vec3::new(x, y, z);
821
822            // Skip zero vectors
823            if v.magnitude() < f64::EPSILON {
824                return Ok(());
825            }
826
827            let n = v.normalize();
828            prop_assert!((n.magnitude() - 1.0).abs() < 1e-9);
829        }
830
831        /// Falsification: kinetic energy is non-negative.
832        #[test]
833        fn prop_kinetic_energy_nonnegative(
834            mass in 0.1f64..1e6,
835            vx in -1e3f64..1e3, vy in -1e3f64..1e3, vz in -1e3f64..1e3,
836        ) {
837            let mut state = SimState::new();
838            state.add_body(mass, Vec3::zero(), Vec3::new(vx, vy, vz));
839
840            prop_assert!(state.kinetic_energy() >= 0.0);
841        }
842    }
843}