1use crate::error::SimResult;
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
14pub struct Vec3 {
15 pub x: f64,
17 pub y: f64,
19 pub z: f64,
21}
22
23impl Vec3 {
24 #[must_use]
26 pub const fn new(x: f64, y: f64, z: f64) -> Self {
27 Self { x, y, z }
28 }
29
30 #[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 #[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 #[must_use]
48 pub fn magnitude(&self) -> f64 {
49 self.magnitude_squared().sqrt()
50 }
51
52 #[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 #[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 #[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 #[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 #[must_use]
95 #[allow(clippy::missing_const_for_fn)] 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#[derive(Debug, Clone, Serialize, Deserialize)]
147pub enum SimEvent {
148 AddBody {
150 mass: f64,
152 position: Vec3,
154 velocity: Vec3,
156 },
157 ApplyForce {
159 body_index: usize,
161 force: Vec3,
163 },
164 SetPosition {
166 body_index: usize,
168 position: Vec3,
170 },
171 SetVelocity {
173 body_index: usize,
175 velocity: Vec3,
177 },
178 Custom {
180 name: String,
182 data: Vec<u8>,
184 },
185}
186
187#[derive(Debug, Clone, Default, Serialize, Deserialize)]
191pub struct SimState {
192 masses: Vec<f64>,
194 positions: Vec<Vec3>,
196 velocities: Vec<Vec3>,
198 forces: Vec<Vec3>,
200 constraints: Vec<(String, f64)>,
202 potential_energy: f64,
204}
205
206impl SimState {
207 #[must_use]
209 pub fn new() -> Self {
210 Self::default()
211 }
212
213 #[must_use]
215 #[allow(clippy::missing_const_for_fn)] pub fn num_bodies(&self) -> usize {
217 self.masses.len()
218 }
219
220 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 #[must_use]
230 pub fn masses(&self) -> &[f64] {
231 &self.masses
232 }
233
234 #[must_use]
236 pub fn positions(&self) -> &[Vec3] {
237 &self.positions
238 }
239
240 #[must_use]
242 pub fn positions_mut(&mut self) -> &mut [Vec3] {
243 &mut self.positions
244 }
245
246 #[must_use]
248 pub fn velocities(&self) -> &[Vec3] {
249 &self.velocities
250 }
251
252 #[must_use]
254 pub fn velocities_mut(&mut self) -> &mut [Vec3] {
255 &mut self.velocities
256 }
257
258 #[must_use]
260 pub fn forces(&self) -> &[Vec3] {
261 &self.forces
262 }
263
264 #[must_use]
266 pub fn forces_mut(&mut self) -> &mut [Vec3] {
267 &mut self.forces
268 }
269
270 pub fn set_position(&mut self, index: usize, position: Vec3) {
276 self.positions[index] = position;
277 }
278
279 pub fn set_velocity(&mut self, index: usize, velocity: Vec3) {
285 self.velocities[index] = velocity;
286 }
287
288 pub fn apply_force(&mut self, index: usize, force: Vec3) {
294 self.forces[index] = self.forces[index] + force;
295 }
296
297 pub fn clear_forces(&mut self) {
299 for f in &mut self.forces {
300 *f = Vec3::zero();
301 }
302 }
303
304 #[allow(clippy::missing_const_for_fn)] pub fn set_potential_energy(&mut self, energy: f64) {
307 self.potential_energy = energy;
308 }
309
310 #[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 #[must_use]
322 pub const fn potential_energy(&self) -> f64 {
323 self.potential_energy
324 }
325
326 #[must_use]
328 pub fn total_energy(&self) -> f64 {
329 self.kinetic_energy() + self.potential_energy
330 }
331
332 pub fn add_constraint(&mut self, name: impl Into<String>, violation: f64) {
334 self.constraints.push((name.into(), violation));
335 }
336
337 pub fn clear_constraints(&mut self) {
339 self.constraints.clear();
340 }
341
342 pub fn constraint_violations(&self) -> impl Iterator<Item = (String, f64)> + '_ {
344 self.constraints.iter().cloned()
345 }
346
347 #[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 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 }
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 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 let diff = v2 - v1;
415 assert!((diff.x - 3.0).abs() < f64::EPSILON);
416
417 let dot = v1.dot(&v2);
419 assert!((dot - 32.0).abs() < f64::EPSILON); 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 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 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, force: Vec3::new(10.0, 20.0, 30.0),
660 };
661
662 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, position: Vec3::new(5.0, 6.0, 7.0),
688 };
689
690 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, velocity: Vec3::new(8.0, 9.0, 10.0),
716 };
717
718 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 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 #[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 #[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 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 #[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}