1use serde::{Deserialize, Serialize};
17use std::ops::{Add, Div, Mul, Neg, Sub};
18use uom::si::acceleration::meter_per_second_squared;
19use uom::si::f64::{Acceleration, Length, Mass, Time, Velocity};
20use uom::si::length::meter;
21use uom::si::mass::kilogram;
22use uom::si::time::second;
23use uom::si::velocity::meter_per_second;
24
25pub const G: f64 = 6.674_30e-11;
27
28pub const AU: f64 = 1.495_978_707e11;
30
31pub const SOLAR_MASS: f64 = 1.988_92e30;
33
34pub const EARTH_MASS: f64 = 5.972_2e24;
36
37#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
39pub struct Position3D {
40 pub x: Length,
41 pub y: Length,
42 pub z: Length,
43}
44
45impl Position3D {
46 #[must_use]
48 pub fn from_meters(x: f64, y: f64, z: f64) -> Self {
49 Self {
50 x: Length::new::<meter>(x),
51 y: Length::new::<meter>(y),
52 z: Length::new::<meter>(z),
53 }
54 }
55
56 #[must_use]
58 pub fn from_au(x: f64, y: f64, z: f64) -> Self {
59 Self::from_meters(x * AU, y * AU, z * AU)
60 }
61
62 #[must_use]
64 pub fn zero() -> Self {
65 Self::from_meters(0.0, 0.0, 0.0)
66 }
67
68 #[must_use]
70 pub fn magnitude(&self) -> Length {
71 let x = self.x.get::<meter>();
72 let y = self.y.get::<meter>();
73 let z = self.z.get::<meter>();
74 Length::new::<meter>((x * x + y * y + z * z).sqrt())
75 }
76
77 #[must_use]
79 pub fn magnitude_squared(&self) -> f64 {
80 let x = self.x.get::<meter>();
81 let y = self.y.get::<meter>();
82 let z = self.z.get::<meter>();
83 x * x + y * y + z * z
84 }
85
86 #[must_use]
88 pub fn normalize(&self) -> (f64, f64, f64) {
89 let mag = self.magnitude().get::<meter>();
90 if mag < f64::EPSILON {
91 return (0.0, 0.0, 0.0);
92 }
93 (
94 self.x.get::<meter>() / mag,
95 self.y.get::<meter>() / mag,
96 self.z.get::<meter>() / mag,
97 )
98 }
99
100 #[must_use]
102 pub fn dot(&self, other: &Self) -> f64 {
103 self.x.get::<meter>() * other.x.get::<meter>()
104 + self.y.get::<meter>() * other.y.get::<meter>()
105 + self.z.get::<meter>() * other.z.get::<meter>()
106 }
107
108 #[must_use]
110 pub fn cross(&self, other: &Self) -> Self {
111 let ax = self.x.get::<meter>();
112 let ay = self.y.get::<meter>();
113 let az = self.z.get::<meter>();
114 let bx = other.x.get::<meter>();
115 let by = other.y.get::<meter>();
116 let bz = other.z.get::<meter>();
117
118 Self::from_meters(ay * bz - az * by, az * bx - ax * bz, ax * by - ay * bx)
119 }
120
121 #[must_use]
123 pub fn scale(&self, factor: f64) -> Self {
124 Self {
125 x: self.x * factor,
126 y: self.y * factor,
127 z: self.z * factor,
128 }
129 }
130
131 #[must_use]
133 pub fn is_finite(&self) -> bool {
134 self.x.get::<meter>().is_finite()
135 && self.y.get::<meter>().is_finite()
136 && self.z.get::<meter>().is_finite()
137 }
138
139 #[must_use]
141 pub fn as_meters(&self) -> (f64, f64, f64) {
142 (
143 self.x.get::<meter>(),
144 self.y.get::<meter>(),
145 self.z.get::<meter>(),
146 )
147 }
148}
149
150impl Add for Position3D {
151 type Output = Self;
152
153 fn add(self, other: Self) -> Self {
154 Self {
155 x: self.x + other.x,
156 y: self.y + other.y,
157 z: self.z + other.z,
158 }
159 }
160}
161
162impl Sub for Position3D {
163 type Output = Self;
164
165 fn sub(self, other: Self) -> Self {
166 Self {
167 x: self.x - other.x,
168 y: self.y - other.y,
169 z: self.z - other.z,
170 }
171 }
172}
173
174impl Neg for Position3D {
175 type Output = Self;
176
177 fn neg(self) -> Self {
178 Self {
179 x: -self.x,
180 y: -self.y,
181 z: -self.z,
182 }
183 }
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
188pub struct Velocity3D {
189 pub x: Velocity,
190 pub y: Velocity,
191 pub z: Velocity,
192}
193
194impl Velocity3D {
195 #[must_use]
197 pub fn from_mps(x: f64, y: f64, z: f64) -> Self {
198 Self {
199 x: Velocity::new::<meter_per_second>(x),
200 y: Velocity::new::<meter_per_second>(y),
201 z: Velocity::new::<meter_per_second>(z),
202 }
203 }
204
205 #[must_use]
207 pub fn zero() -> Self {
208 Self::from_mps(0.0, 0.0, 0.0)
209 }
210
211 #[must_use]
213 pub fn magnitude(&self) -> Velocity {
214 let x = self.x.get::<meter_per_second>();
215 let y = self.y.get::<meter_per_second>();
216 let z = self.z.get::<meter_per_second>();
217 Velocity::new::<meter_per_second>((x * x + y * y + z * z).sqrt())
218 }
219
220 #[must_use]
222 pub fn magnitude_squared(&self) -> f64 {
223 let x = self.x.get::<meter_per_second>();
224 let y = self.y.get::<meter_per_second>();
225 let z = self.z.get::<meter_per_second>();
226 x * x + y * y + z * z
227 }
228
229 #[must_use]
231 pub fn scale(&self, factor: f64) -> Self {
232 Self {
233 x: self.x * factor,
234 y: self.y * factor,
235 z: self.z * factor,
236 }
237 }
238
239 #[must_use]
241 pub fn is_finite(&self) -> bool {
242 self.x.get::<meter_per_second>().is_finite()
243 && self.y.get::<meter_per_second>().is_finite()
244 && self.z.get::<meter_per_second>().is_finite()
245 }
246
247 #[must_use]
249 pub fn as_mps(&self) -> (f64, f64, f64) {
250 (
251 self.x.get::<meter_per_second>(),
252 self.y.get::<meter_per_second>(),
253 self.z.get::<meter_per_second>(),
254 )
255 }
256}
257
258impl Add for Velocity3D {
259 type Output = Self;
260
261 fn add(self, other: Self) -> Self {
262 Self {
263 x: self.x + other.x,
264 y: self.y + other.y,
265 z: self.z + other.z,
266 }
267 }
268}
269
270impl Sub for Velocity3D {
271 type Output = Self;
272
273 fn sub(self, other: Self) -> Self {
274 Self {
275 x: self.x - other.x,
276 y: self.y - other.y,
277 z: self.z - other.z,
278 }
279 }
280}
281
282impl Neg for Velocity3D {
283 type Output = Self;
284
285 fn neg(self) -> Self {
286 Self {
287 x: -self.x,
288 y: -self.y,
289 z: -self.z,
290 }
291 }
292}
293
294#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
296pub struct Acceleration3D {
297 pub x: Acceleration,
298 pub y: Acceleration,
299 pub z: Acceleration,
300}
301
302impl Acceleration3D {
303 #[must_use]
305 pub fn from_mps2(x: f64, y: f64, z: f64) -> Self {
306 Self {
307 x: Acceleration::new::<meter_per_second_squared>(x),
308 y: Acceleration::new::<meter_per_second_squared>(y),
309 z: Acceleration::new::<meter_per_second_squared>(z),
310 }
311 }
312
313 #[must_use]
315 pub fn zero() -> Self {
316 Self::from_mps2(0.0, 0.0, 0.0)
317 }
318
319 #[must_use]
321 pub fn magnitude(&self) -> Acceleration {
322 let x = self.x.get::<meter_per_second_squared>();
323 let y = self.y.get::<meter_per_second_squared>();
324 let z = self.z.get::<meter_per_second_squared>();
325 Acceleration::new::<meter_per_second_squared>((x * x + y * y + z * z).sqrt())
326 }
327
328 #[must_use]
330 pub fn scale(&self, factor: f64) -> Self {
331 Self {
332 x: self.x * factor,
333 y: self.y * factor,
334 z: self.z * factor,
335 }
336 }
337
338 #[must_use]
340 pub fn as_mps2(&self) -> (f64, f64, f64) {
341 (
342 self.x.get::<meter_per_second_squared>(),
343 self.y.get::<meter_per_second_squared>(),
344 self.z.get::<meter_per_second_squared>(),
345 )
346 }
347}
348
349impl Add for Acceleration3D {
350 type Output = Self;
351
352 fn add(self, other: Self) -> Self {
353 Self {
354 x: self.x + other.x,
355 y: self.y + other.y,
356 z: self.z + other.z,
357 }
358 }
359}
360
361impl Neg for Acceleration3D {
362 type Output = Self;
363
364 fn neg(self) -> Self {
365 Self {
366 x: -self.x,
367 y: -self.y,
368 z: -self.z,
369 }
370 }
371}
372
373#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
375pub struct OrbitMass(pub Mass);
376
377impl OrbitMass {
378 #[must_use]
380 pub fn from_kg(kg: f64) -> Self {
381 Self(Mass::new::<kilogram>(kg))
382 }
383
384 #[must_use]
386 pub fn from_solar_masses(m: f64) -> Self {
387 Self::from_kg(m * SOLAR_MASS)
388 }
389
390 #[must_use]
392 pub fn from_earth_masses(m: f64) -> Self {
393 Self::from_kg(m * EARTH_MASS)
394 }
395
396 #[must_use]
398 pub fn as_kg(&self) -> f64 {
399 self.0.get::<kilogram>()
400 }
401}
402
403#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
405pub struct OrbitTime(pub Time);
406
407impl OrbitTime {
408 #[must_use]
410 pub fn from_seconds(s: f64) -> Self {
411 Self(Time::new::<second>(s))
412 }
413
414 #[must_use]
416 pub fn from_days(d: f64) -> Self {
417 Self::from_seconds(d * 86400.0)
418 }
419
420 #[must_use]
422 pub fn from_years(y: f64) -> Self {
423 Self::from_days(y * 365.25)
424 }
425
426 #[must_use]
428 pub fn as_seconds(&self) -> f64 {
429 self.0.get::<second>()
430 }
431
432 #[must_use]
434 pub fn as_days(&self) -> f64 {
435 self.as_seconds() / 86400.0
436 }
437}
438
439impl Div<OrbitTime> for Position3D {
441 type Output = Velocity3D;
442
443 fn div(self, dt: OrbitTime) -> Velocity3D {
444 let t = dt.as_seconds();
445 Velocity3D::from_mps(
446 self.x.get::<meter>() / t,
447 self.y.get::<meter>() / t,
448 self.z.get::<meter>() / t,
449 )
450 }
451}
452
453impl Mul<OrbitTime> for Velocity3D {
455 type Output = Position3D;
456
457 fn mul(self, dt: OrbitTime) -> Position3D {
458 let t = dt.as_seconds();
459 Position3D::from_meters(
460 self.x.get::<meter_per_second>() * t,
461 self.y.get::<meter_per_second>() * t,
462 self.z.get::<meter_per_second>() * t,
463 )
464 }
465}
466
467impl Mul<OrbitTime> for Acceleration3D {
469 type Output = Velocity3D;
470
471 fn mul(self, dt: OrbitTime) -> Velocity3D {
472 let t = dt.as_seconds();
473 Velocity3D::from_mps(
474 self.x.get::<meter_per_second_squared>() * t,
475 self.y.get::<meter_per_second_squared>() * t,
476 self.z.get::<meter_per_second_squared>() * t,
477 )
478 }
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484
485 const EPSILON: f64 = 1e-10;
486
487 #[test]
488 fn test_position_from_meters() {
489 let pos = Position3D::from_meters(1.0, 2.0, 3.0);
490 let (x, y, z) = pos.as_meters();
491 assert!((x - 1.0).abs() < EPSILON);
492 assert!((y - 2.0).abs() < EPSILON);
493 assert!((z - 3.0).abs() < EPSILON);
494 }
495
496 #[test]
497 fn test_position_from_au() {
498 let pos = Position3D::from_au(1.0, 0.0, 0.0);
499 let (x, _, _) = pos.as_meters();
500 assert!((x - AU).abs() < 1.0); }
502
503 #[test]
504 fn test_position_magnitude() {
505 let pos = Position3D::from_meters(3.0, 4.0, 0.0);
506 let mag = pos.magnitude().get::<meter>();
507 assert!((mag - 5.0).abs() < EPSILON);
508 }
509
510 #[test]
511 fn test_position_normalize() {
512 let pos = Position3D::from_meters(3.0, 4.0, 0.0);
513 let (nx, ny, nz) = pos.normalize();
514 assert!((nx - 0.6).abs() < EPSILON);
515 assert!((ny - 0.8).abs() < EPSILON);
516 assert!(nz.abs() < EPSILON);
517 }
518
519 #[test]
520 fn test_position_dot_product() {
521 let a = Position3D::from_meters(1.0, 2.0, 3.0);
522 let b = Position3D::from_meters(4.0, 5.0, 6.0);
523 let dot = a.dot(&b);
524 assert!((dot - 32.0).abs() < EPSILON); }
526
527 #[test]
528 fn test_position_cross_product() {
529 let a = Position3D::from_meters(1.0, 0.0, 0.0);
530 let b = Position3D::from_meters(0.0, 1.0, 0.0);
531 let c = a.cross(&b);
532 let (x, y, z) = c.as_meters();
533 assert!(x.abs() < EPSILON);
534 assert!(y.abs() < EPSILON);
535 assert!((z - 1.0).abs() < EPSILON); }
537
538 #[test]
539 fn test_position_add_sub() {
540 let a = Position3D::from_meters(1.0, 2.0, 3.0);
541 let b = Position3D::from_meters(4.0, 5.0, 6.0);
542 let sum = a + b;
543 let (x, y, z) = sum.as_meters();
544 assert!((x - 5.0).abs() < EPSILON);
545 assert!((y - 7.0).abs() < EPSILON);
546 assert!((z - 9.0).abs() < EPSILON);
547 }
548
549 #[test]
550 fn test_velocity_from_mps() {
551 let vel = Velocity3D::from_mps(10.0, 20.0, 30.0);
552 let (x, y, z) = vel.as_mps();
553 assert!((x - 10.0).abs() < EPSILON);
554 assert!((y - 20.0).abs() < EPSILON);
555 assert!((z - 30.0).abs() < EPSILON);
556 }
557
558 #[test]
559 fn test_velocity_magnitude() {
560 let vel = Velocity3D::from_mps(3.0, 4.0, 0.0);
561 let mag = vel.magnitude().get::<meter_per_second>();
562 assert!((mag - 5.0).abs() < EPSILON);
563 }
564
565 #[test]
566 fn test_acceleration_from_mps2() {
567 let acc = Acceleration3D::from_mps2(1.0, 2.0, 3.0);
568 let (x, y, z) = acc.as_mps2();
569 assert!((x - 1.0).abs() < EPSILON);
570 assert!((y - 2.0).abs() < EPSILON);
571 assert!((z - 3.0).abs() < EPSILON);
572 }
573
574 #[test]
575 fn test_mass_from_kg() {
576 let m = OrbitMass::from_kg(1000.0);
577 assert!((m.as_kg() - 1000.0).abs() < EPSILON);
578 }
579
580 #[test]
581 fn test_mass_from_solar_masses() {
582 let m = OrbitMass::from_solar_masses(1.0);
583 assert!((m.as_kg() - SOLAR_MASS).abs() < 1e20);
584 }
585
586 #[test]
587 fn test_time_from_seconds() {
588 let t = OrbitTime::from_seconds(3600.0);
589 assert!((t.as_seconds() - 3600.0).abs() < EPSILON);
590 }
591
592 #[test]
593 fn test_time_from_days() {
594 let t = OrbitTime::from_days(1.0);
595 assert!((t.as_seconds() - 86400.0).abs() < EPSILON);
596 }
597
598 #[test]
599 fn test_time_from_years() {
600 let t = OrbitTime::from_years(1.0);
601 let expected = 365.25 * 86400.0;
602 assert!((t.as_seconds() - expected).abs() < 1.0);
603 }
604
605 #[test]
606 fn test_position_div_time_gives_velocity() {
607 let pos = Position3D::from_meters(1000.0, 2000.0, 3000.0);
608 let dt = OrbitTime::from_seconds(10.0);
609 let vel = pos / dt;
610 let (vx, vy, vz) = vel.as_mps();
611 assert!((vx - 100.0).abs() < EPSILON);
612 assert!((vy - 200.0).abs() < EPSILON);
613 assert!((vz - 300.0).abs() < EPSILON);
614 }
615
616 #[test]
617 fn test_velocity_mul_time_gives_position() {
618 let vel = Velocity3D::from_mps(100.0, 200.0, 300.0);
619 let dt = OrbitTime::from_seconds(10.0);
620 let pos = vel * dt;
621 let (x, y, z) = pos.as_meters();
622 assert!((x - 1000.0).abs() < EPSILON);
623 assert!((y - 2000.0).abs() < EPSILON);
624 assert!((z - 3000.0).abs() < EPSILON);
625 }
626
627 #[test]
628 fn test_acceleration_mul_time_gives_velocity() {
629 let acc = Acceleration3D::from_mps2(10.0, 20.0, 30.0);
630 let dt = OrbitTime::from_seconds(5.0);
631 let vel = acc * dt;
632 let (vx, vy, vz) = vel.as_mps();
633 assert!((vx - 50.0).abs() < EPSILON);
634 assert!((vy - 100.0).abs() < EPSILON);
635 assert!((vz - 150.0).abs() < EPSILON);
636 }
637
638 #[test]
639 fn test_is_finite() {
640 let pos = Position3D::from_meters(1.0, 2.0, 3.0);
641 assert!(pos.is_finite());
642
643 let vel = Velocity3D::from_mps(1.0, 2.0, 3.0);
644 assert!(vel.is_finite());
645 }
646
647 #[test]
648 fn test_gravitational_constant() {
649 assert!((G - 6.674_30e-11).abs() < 1e-15);
650 }
651
652 #[test]
653 fn test_au_constant() {
654 assert!((AU - 1.495_978_707e11).abs() < 1.0);
655 }
656
657 #[test]
658 fn test_position_scale() {
659 let pos = Position3D::from_meters(1.0, 2.0, 3.0);
660 let scaled = pos.scale(2.0);
661 let (x, y, z) = scaled.as_meters();
662 assert!((x - 2.0).abs() < EPSILON);
663 assert!((y - 4.0).abs() < EPSILON);
664 assert!((z - 6.0).abs() < EPSILON);
665 }
666
667 #[test]
668 fn test_position_magnitude_squared() {
669 let pos = Position3D::from_meters(3.0, 4.0, 0.0);
670 let mag_sq = pos.magnitude_squared();
671 assert!((mag_sq - 25.0).abs() < EPSILON);
672 }
673
674 #[test]
675 fn test_position_zero() {
676 let pos = Position3D::zero();
677 let (x, y, z) = pos.as_meters();
678 assert!(x.abs() < EPSILON);
679 assert!(y.abs() < EPSILON);
680 assert!(z.abs() < EPSILON);
681 }
682
683 #[test]
684 fn test_position_normalize_zero() {
685 let pos = Position3D::zero();
686 let (nx, ny, nz) = pos.normalize();
687 assert!(nx.abs() < EPSILON);
688 assert!(ny.abs() < EPSILON);
689 assert!(nz.abs() < EPSILON);
690 }
691
692 #[test]
693 fn test_position_sub() {
694 let a = Position3D::from_meters(5.0, 7.0, 9.0);
695 let b = Position3D::from_meters(1.0, 2.0, 3.0);
696 let diff = a - b;
697 let (x, y, z) = diff.as_meters();
698 assert!((x - 4.0).abs() < EPSILON);
699 assert!((y - 5.0).abs() < EPSILON);
700 assert!((z - 6.0).abs() < EPSILON);
701 }
702
703 #[test]
704 fn test_position_neg() {
705 let pos = Position3D::from_meters(1.0, 2.0, 3.0);
706 let neg = -pos;
707 let (x, y, z) = neg.as_meters();
708 assert!((x + 1.0).abs() < EPSILON);
709 assert!((y + 2.0).abs() < EPSILON);
710 assert!((z + 3.0).abs() < EPSILON);
711 }
712
713 #[test]
714 fn test_position_is_finite_with_nan() {
715 let mut pos = Position3D::from_meters(1.0, 2.0, 3.0);
716 assert!(pos.is_finite());
717 pos.x = Length::new::<meter>(f64::NAN);
718 assert!(!pos.is_finite());
719 }
720
721 #[test]
722 fn test_velocity_zero() {
723 let vel = Velocity3D::zero();
724 let (x, y, z) = vel.as_mps();
725 assert!(x.abs() < EPSILON);
726 assert!(y.abs() < EPSILON);
727 assert!(z.abs() < EPSILON);
728 }
729
730 #[test]
731 fn test_velocity_scale() {
732 let vel = Velocity3D::from_mps(1.0, 2.0, 3.0);
733 let scaled = vel.scale(3.0);
734 let (x, y, z) = scaled.as_mps();
735 assert!((x - 3.0).abs() < EPSILON);
736 assert!((y - 6.0).abs() < EPSILON);
737 assert!((z - 9.0).abs() < EPSILON);
738 }
739
740 #[test]
741 fn test_velocity_magnitude_squared() {
742 let vel = Velocity3D::from_mps(3.0, 4.0, 0.0);
743 let mag_sq = vel.magnitude_squared();
744 assert!((mag_sq - 25.0).abs() < EPSILON);
745 }
746
747 #[test]
748 fn test_velocity_add() {
749 let a = Velocity3D::from_mps(1.0, 2.0, 3.0);
750 let b = Velocity3D::from_mps(4.0, 5.0, 6.0);
751 let sum = a + b;
752 let (x, y, z) = sum.as_mps();
753 assert!((x - 5.0).abs() < EPSILON);
754 assert!((y - 7.0).abs() < EPSILON);
755 assert!((z - 9.0).abs() < EPSILON);
756 }
757
758 #[test]
759 fn test_velocity_sub() {
760 let a = Velocity3D::from_mps(5.0, 7.0, 9.0);
761 let b = Velocity3D::from_mps(1.0, 2.0, 3.0);
762 let diff = a - b;
763 let (x, y, z) = diff.as_mps();
764 assert!((x - 4.0).abs() < EPSILON);
765 assert!((y - 5.0).abs() < EPSILON);
766 assert!((z - 6.0).abs() < EPSILON);
767 }
768
769 #[test]
770 fn test_velocity_neg() {
771 let vel = Velocity3D::from_mps(1.0, 2.0, 3.0);
772 let neg = -vel;
773 let (x, y, z) = neg.as_mps();
774 assert!((x + 1.0).abs() < EPSILON);
775 assert!((y + 2.0).abs() < EPSILON);
776 assert!((z + 3.0).abs() < EPSILON);
777 }
778
779 #[test]
780 fn test_velocity_is_finite_with_inf() {
781 let mut vel = Velocity3D::from_mps(1.0, 2.0, 3.0);
782 assert!(vel.is_finite());
783 vel.y = Velocity::new::<meter_per_second>(f64::INFINITY);
784 assert!(!vel.is_finite());
785 }
786
787 #[test]
788 fn test_acceleration_zero() {
789 let acc = Acceleration3D::zero();
790 let (x, y, z) = acc.as_mps2();
791 assert!(x.abs() < EPSILON);
792 assert!(y.abs() < EPSILON);
793 assert!(z.abs() < EPSILON);
794 }
795
796 #[test]
797 fn test_acceleration_scale() {
798 let acc = Acceleration3D::from_mps2(1.0, 2.0, 3.0);
799 let scaled = acc.scale(2.0);
800 let (x, y, z) = scaled.as_mps2();
801 assert!((x - 2.0).abs() < EPSILON);
802 assert!((y - 4.0).abs() < EPSILON);
803 assert!((z - 6.0).abs() < EPSILON);
804 }
805
806 #[test]
807 fn test_acceleration_magnitude() {
808 let acc = Acceleration3D::from_mps2(3.0, 4.0, 0.0);
809 let mag = acc.magnitude().get::<meter_per_second_squared>();
810 assert!((mag - 5.0).abs() < EPSILON);
811 }
812
813 #[test]
814 fn test_acceleration_add() {
815 let a = Acceleration3D::from_mps2(1.0, 2.0, 3.0);
816 let b = Acceleration3D::from_mps2(4.0, 5.0, 6.0);
817 let sum = a + b;
818 let (x, y, z) = sum.as_mps2();
819 assert!((x - 5.0).abs() < EPSILON);
820 assert!((y - 7.0).abs() < EPSILON);
821 assert!((z - 9.0).abs() < EPSILON);
822 }
823
824 #[test]
825 fn test_acceleration_neg() {
826 let acc = Acceleration3D::from_mps2(1.0, 2.0, 3.0);
827 let neg = -acc;
828 let (x, y, z) = neg.as_mps2();
829 assert!((x + 1.0).abs() < EPSILON);
830 assert!((y + 2.0).abs() < EPSILON);
831 assert!((z + 3.0).abs() < EPSILON);
832 }
833
834 #[test]
835 fn test_mass_from_earth_masses() {
836 let m = OrbitMass::from_earth_masses(1.0);
837 assert!((m.as_kg() - EARTH_MASS).abs() < 1e18);
838 }
839
840 #[test]
841 fn test_time_as_days() {
842 let t = OrbitTime::from_seconds(172800.0); assert!((t.as_days() - 2.0).abs() < EPSILON);
844 }
845
846 #[test]
847 fn test_constants() {
848 const _: () = assert!(SOLAR_MASS > 1.9e30);
850 const _: () = assert!(EARTH_MASS > 5.9e24);
851 }
852}
853
854#[cfg(test)]
855mod proptests {
856 use super::*;
857 use proptest::prelude::*;
858
859 proptest! {
860 #[test]
862 fn prop_position_roundtrip(
863 meters in 1e3f64..1e15,
864 ) {
865 let p = Position3D::from_meters(meters, 0.0, 0.0);
866 let (x, _, _) = p.as_meters();
867 prop_assert!((x - meters).abs() / meters < 1e-10);
868 }
869
870 #[test]
872 fn prop_au_conversion(
873 au in 0.1f64..100.0,
874 ) {
875 let p = Position3D::from_au(au, 0.0, 0.0);
876 let (x, _, _) = p.as_meters();
877 let expected = au * AU;
878 prop_assert!((x - expected).abs() / expected < 1e-10);
879 }
880
881 #[test]
883 fn prop_velocity_magnitude_nonneg(
884 vx in -1e5f64..1e5,
885 vy in -1e5f64..1e5,
886 vz in -1e5f64..1e5,
887 ) {
888 let v = Velocity3D::from_mps(vx, vy, vz);
889 let mag = v.magnitude().get::<meter_per_second>();
890 prop_assert!(mag >= 0.0);
891 }
892
893 #[test]
895 fn prop_position_magnitude_nonneg(
896 x in -1e12f64..1e12,
897 y in -1e12f64..1e12,
898 z in -1e12f64..1e12,
899 ) {
900 let p = Position3D::from_meters(x, y, z);
901 let mag = p.magnitude().get::<meter>();
902 prop_assert!(mag >= 0.0);
903 }
904
905 #[test]
907 fn prop_mass_positive(
908 kg in 1.0f64..1e35,
909 ) {
910 let m = OrbitMass::from_kg(kg);
911 prop_assert!(m.as_kg() > 0.0);
912 }
913
914 #[test]
916 fn prop_time_roundtrip(
917 seconds in 1.0f64..1e10,
918 ) {
919 let t = OrbitTime::from_seconds(seconds);
920 let recovered = t.as_seconds();
921 prop_assert!((recovered - seconds).abs() / seconds < 1e-10);
922 }
923
924 #[test]
926 fn prop_velocity_neg_magnitude(
927 vx in -1e5f64..1e5,
928 vy in -1e5f64..1e5,
929 vz in -1e5f64..1e5,
930 ) {
931 let v = Velocity3D::from_mps(vx, vy, vz);
932 let neg_v = -v;
933 let mag1 = v.magnitude().get::<meter_per_second>();
934 let mag2 = neg_v.magnitude().get::<meter_per_second>();
935 prop_assert!((mag1 - mag2).abs() < 1e-10);
936 }
937 }
938}