1use lox_test_utils::approx_eq::{ApproxEq, ApproxEqResults};
27use thiserror::Error;
28
29use crate::{
30 elements::{Eccentricity, OrbitType},
31 units::{Angle, AngleUnits},
32};
33
34use core::marker::PhantomData;
35use std::{
36 fmt::Display,
37 ops::{Add, Neg, Sub},
38};
39
40pub trait AnomalyKind: sealed::Sealed + std::fmt::Debug {}
42
43mod sealed {
44 pub trait Sealed {}
46 impl Sealed for super::TrueKind {}
47 impl Sealed for super::EccentricKind {}
48 impl Sealed for super::MeanKind {}
49}
50
51#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
53#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
54pub struct TrueKind;
55
56#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
58#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
59pub struct EccentricKind;
60
61#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
63#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
64pub struct MeanKind;
65
66impl AnomalyKind for TrueKind {}
67impl AnomalyKind for EccentricKind {}
68impl AnomalyKind for MeanKind {}
69
70#[derive(Debug, Clone, Copy, Default, PartialEq, PartialOrd)]
72#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
73#[cfg_attr(feature = "serde", serde(bound = ""))]
74pub struct Anomaly<K: AnomalyKind> {
75 anomaly: Angle,
76 _kind: PhantomData<K>,
77}
78
79impl<K> Display for Anomaly<K>
80where
81 K: AnomalyKind,
82{
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 self.anomaly.fmt(f)
85 }
86}
87
88impl<K> Anomaly<K>
89where
90 K: AnomalyKind,
91{
92 pub fn as_angle(self) -> Angle {
94 self.anomaly
95 }
96
97 pub fn as_f64(self) -> f64 {
99 self.anomaly.as_f64()
100 }
101}
102
103impl<K> Add for Anomaly<K>
104where
105 K: AnomalyKind,
106{
107 type Output = Self;
108
109 fn add(self, rhs: Self) -> Self::Output {
110 Anomaly {
111 anomaly: self.anomaly + rhs.anomaly,
112 _kind: PhantomData,
113 }
114 }
115}
116
117impl<K> Sub for Anomaly<K>
118where
119 K: AnomalyKind,
120{
121 type Output = Self;
122
123 fn sub(self, rhs: Self) -> Self::Output {
124 Anomaly {
125 anomaly: self.anomaly - rhs.anomaly,
126 _kind: PhantomData,
127 }
128 }
129}
130
131impl<K> Neg for Anomaly<K>
132where
133 K: AnomalyKind,
134{
135 type Output = Self;
136
137 fn neg(self) -> Self::Output {
138 Anomaly {
139 anomaly: -self.anomaly,
140 _kind: PhantomData,
141 }
142 }
143}
144
145pub type TrueAnomaly = Anomaly<TrueKind>;
147
148pub type EccentricAnomaly = Anomaly<EccentricKind>;
150
151pub type MeanAnomaly = Anomaly<MeanKind>;
153
154impl TrueAnomaly {
159 pub fn new(anomaly: Angle) -> Self {
161 Self {
162 anomaly,
163 _kind: PhantomData,
164 }
165 }
166
167 pub fn to_eccentric(self, ecc: Eccentricity) -> Result<EccentricAnomaly> {
171 let orbit = ecc.orbit_type();
172 match orbit {
173 OrbitType::Circular | OrbitType::Elliptic => {
174 Ok(EccentricAnomaly::new(true_to_eccentric(self.anomaly, ecc)))
175 }
176 OrbitType::Hyperbolic => Ok(EccentricAnomaly::new(true_to_hyperbolic(
177 self.anomaly,
178 ecc,
179 )?)),
180 OrbitType::Parabolic => {
181 Ok(EccentricAnomaly::new(true_to_parabolic(self.anomaly).rad()))
183 }
184 }
185 }
186
187 pub fn to_mean(self, ecc: Eccentricity) -> Result<MeanAnomaly> {
191 let orbit = ecc.orbit_type();
192 match orbit {
193 OrbitType::Circular => {
194 Ok(MeanAnomaly::new(self.anomaly))
196 }
197 OrbitType::Elliptic => Ok(MeanAnomaly::new(true_to_mean(self.anomaly, ecc))),
198 OrbitType::Parabolic => {
199 Ok(MeanAnomaly::new(true_to_mean_parabolic(self.anomaly).rad()))
201 }
202 OrbitType::Hyperbolic => Ok(MeanAnomaly::new(true_to_mean_hyperbolic(
203 self.anomaly,
204 ecc,
205 )?)),
206 }
207 }
208}
209
210impl EccentricAnomaly {
211 pub fn new(anomaly: Angle) -> Self {
213 Self {
214 anomaly,
215 _kind: PhantomData,
216 }
217 }
218
219 pub fn to_true(self, ecc: Eccentricity) -> TrueAnomaly {
221 let orbit = ecc.orbit_type();
222 match orbit {
223 OrbitType::Circular | OrbitType::Elliptic => {
224 TrueAnomaly::new(eccentric_to_true(self.anomaly, ecc))
225 }
226 OrbitType::Hyperbolic => TrueAnomaly::new(hyperbolic_to_true(self.anomaly, ecc)),
227 OrbitType::Parabolic => {
228 TrueAnomaly::new(parabolic_to_true(self.anomaly.as_f64()))
230 }
231 }
232 }
233
234 pub fn to_mean(self, ecc: Eccentricity) -> MeanAnomaly {
236 let orbit = ecc.orbit_type();
237 match orbit {
238 OrbitType::Circular | OrbitType::Elliptic => {
239 MeanAnomaly::new(eccentric_to_mean(self.anomaly, ecc))
240 }
241 OrbitType::Hyperbolic => MeanAnomaly::new(hyperbolic_to_mean(self.anomaly, ecc)),
242 OrbitType::Parabolic => {
243 MeanAnomaly::new(parabolic_to_mean(self.anomaly.as_f64()).rad())
245 }
246 }
247 }
248}
249
250impl MeanAnomaly {
251 pub fn new(anomaly: Angle) -> Self {
253 Self {
254 anomaly,
255 _kind: PhantomData,
256 }
257 }
258
259 pub fn to_true(self, ecc: Eccentricity) -> Result<TrueAnomaly> {
263 let orbit = ecc.orbit_type();
264 match orbit {
265 OrbitType::Circular => {
266 Ok(TrueAnomaly::new(self.anomaly))
268 }
269 OrbitType::Elliptic => Ok(TrueAnomaly::new(mean_to_true(self.anomaly, ecc)?)),
270 OrbitType::Parabolic => {
271 Ok(TrueAnomaly::new(mean_parabolic_to_true(
273 self.anomaly.as_f64(),
274 )))
275 }
276 OrbitType::Hyperbolic => Ok(TrueAnomaly::new(mean_hyperbolic_to_true(
277 self.anomaly,
278 ecc,
279 )?)),
280 }
281 }
282
283 pub fn to_eccentric(self, ecc: Eccentricity) -> Result<EccentricAnomaly> {
287 let orbit = ecc.orbit_type();
288 match orbit {
289 OrbitType::Circular | OrbitType::Elliptic => Ok(EccentricAnomaly::new(
290 mean_to_eccentric(self.anomaly, ecc, None, None)?,
291 )),
292 OrbitType::Hyperbolic => Ok(EccentricAnomaly::new(mean_to_hyperbolic(
293 self.anomaly,
294 ecc,
295 None,
296 None,
297 )?)),
298 OrbitType::Parabolic => {
299 Ok(EccentricAnomaly::new(
301 mean_to_parabolic(self.anomaly.as_f64()).rad(),
302 ))
303 }
304 }
305 }
306}
307
308impl<K: AnomalyKind> ApproxEq for Anomaly<K> {
309 fn approx_eq(&self, rhs: &Self, atol: f64, rtol: f64) -> ApproxEqResults {
310 self.anomaly.approx_eq(&rhs.anomaly, atol, rtol)
311 }
312}
313
314#[derive(Error, Debug, Clone, PartialEq)]
316pub enum AnomalyError {
317 #[error("failed to converge after {iterations} iterations (residual: {residual})")]
319 ConvergenceFailure {
320 iterations: usize,
322 residual: f64,
324 },
325 #[error("True anomaly {nu} rad outside valid range [-{max_nu} rad, {max_nu} rad]")]
327 InvalidTrueAnomaly {
328 nu: Angle,
330 max_nu: Angle,
332 },
333}
334
335pub type Result<T> = std::result::Result<T, AnomalyError>;
337
338pub fn true_to_eccentric(nu: Angle, e: Eccentricity) -> Angle {
353 let ecc = e.as_f64();
354 let factor = ((1.0_f64 - ecc) / (1.0_f64 + ecc)).sqrt();
356 let ecc_half = Angle::from_atan(factor * (nu.as_f64() / 2.0).tan());
357 let ecc_angle = Angle::new(2.0 * ecc_half.as_f64());
358
359 ecc_angle.normalize_two_pi(Angle::ZERO)
361}
362
363pub fn eccentric_to_true(ecc: Angle, e: Eccentricity) -> Angle {
374 let ecc_val = e.as_f64();
375 let factor = ((1.0_f64 + ecc_val) / (1.0_f64 - ecc_val)).sqrt();
377 let nu_half = Angle::from_atan(factor * (ecc.as_f64() / 2.0).tan());
378 let nu = Angle::new(2.0 * nu_half.as_f64());
379
380 nu.normalize_two_pi(Angle::ZERO)
382}
383
384pub fn eccentric_to_mean(ecc: Angle, e: Eccentricity) -> Angle {
395 let mean = Angle::new(ecc.as_f64() - e.as_f64() * ecc.sin());
396 mean.normalize_two_pi(Angle::ZERO)
397}
398
399pub fn mean_to_eccentric(
412 mean: Angle,
413 e: Eccentricity,
414 tolerance: Option<f64>,
415 max_iter: Option<usize>,
416) -> Result<Angle> {
417 let tol = tolerance.unwrap_or(1e-10);
418 let max_iterations = max_iter.unwrap_or(50);
419 let ecc = e.as_f64();
420
421 let m = mean.normalize_two_pi(Angle::ZERO).as_f64();
423
424 let mut ecc_anomaly = if ecc < 0.8 {
426 m
427 } else if m < Angle::PI.as_f64() {
428 m + ecc / 2.0
429 } else {
430 m - ecc / 2.0
431 };
432
433 for _iteration in 0..max_iterations {
435 let sin_e = ecc_anomaly.sin();
436 let cos_e = ecc_anomaly.cos();
437
438 let f = ecc_anomaly - ecc * sin_e - m;
440
441 let df = 1.0 - ecc * cos_e;
443
444 let d_prime = df + 0.5 * f * ecc * sin_e / df;
446
447 let delta = f / d_prime;
448 ecc_anomaly -= delta;
449
450 if delta.abs() < tol {
451 return Ok(Angle::new(ecc_anomaly).normalize_two_pi(Angle::ZERO));
452 }
453 }
454
455 Err(AnomalyError::ConvergenceFailure {
456 iterations: max_iterations,
457 residual: (ecc_anomaly - ecc * ecc_anomaly.sin() - m).abs(),
458 })
459}
460
461pub fn true_to_mean(nu: Angle, e: Eccentricity) -> Angle {
465 let ecc = true_to_eccentric(nu, e);
466 eccentric_to_mean(ecc, e)
467}
468
469pub fn mean_to_true(mean: Angle, e: Eccentricity) -> Result<Angle> {
473 let ecc = mean_to_eccentric(mean, e, None, None)?;
474 Ok(eccentric_to_true(ecc, e))
475}
476
477pub fn true_to_parabolic(nu: Angle) -> f64 {
491 (nu.as_f64() / 2.0).tan()
492}
493
494pub fn parabolic_to_true(parabolic: f64) -> Angle {
504 Angle::new(2.0 * parabolic.atan())
505}
506
507pub fn parabolic_to_mean(parabolic: f64) -> f64 {
517 parabolic + parabolic.powi(3) / 3.0
518}
519
520pub fn mean_to_parabolic(mean_parabolic: f64) -> f64 {
530 let a = 1.5 * mean_parabolic;
534
535 let discriminant = a * a + 1.0;
537 let z = (a + discriminant.sqrt()).cbrt();
538
539 z - 1.0 / z
541}
542
543pub fn true_to_mean_parabolic(nu: Angle) -> f64 {
545 let d = true_to_parabolic(nu);
546 parabolic_to_mean(d)
547}
548
549pub fn mean_parabolic_to_true(mean_parabolic: f64) -> Angle {
551 let d = mean_to_parabolic(mean_parabolic);
552 parabolic_to_true(d)
553}
554
555pub fn true_to_hyperbolic(nu: Angle, e: Eccentricity) -> Result<Angle> {
573 let ecc = e.as_f64();
574 let nu_max = Angle::from_acos(-1.0 / ecc);
576 if nu.as_f64().abs() >= nu_max.as_f64() {
577 return Err(AnomalyError::InvalidTrueAnomaly { nu, max_nu: nu_max });
578 }
579
580 let factor = ((ecc - 1.0_f64) / (ecc + 1.0_f64)).sqrt();
582 let tanh_f_half = factor * (nu.as_f64() / 2.0).tan();
583
584 let f_half = tanh_f_half.atanh();
586 Ok(Angle::new(2.0 * f_half))
587}
588
589pub fn hyperbolic_to_true(hyperbolic: Angle, e: Eccentricity) -> Angle {
600 let hyperbolic = hyperbolic.as_f64();
601 let ecc = e.as_f64();
602 let factor = ((ecc + 1.0) / (ecc - 1.0)).sqrt();
604 let tan_nu_half = factor * (hyperbolic / 2.0).tanh();
605
606 Angle::new(2.0 * tan_nu_half.atan())
607}
608
609pub fn hyperbolic_to_mean(hyperbolic: Angle, e: Eccentricity) -> Angle {
620 Angle::new(e.as_f64() * hyperbolic.sinh() - hyperbolic.as_f64())
621}
622
623pub fn mean_to_hyperbolic(
636 mean_hyperbolic: Angle,
637 e: Eccentricity,
638 tolerance: Option<f64>,
639 max_iter: Option<usize>,
640) -> Result<Angle> {
641 let tol = tolerance.unwrap_or(1e-10);
642 let max_iterations = max_iter.unwrap_or(50);
643 let ecc = e.as_f64();
644 let mean_hyperbolic = mean_hyperbolic.as_f64();
645
646 let mut f = (mean_hyperbolic / ecc).asinh();
649
650 for _iteration in 0..max_iterations {
652 let sinh_f = f.sinh();
653 let cosh_f = f.cosh();
654
655 let g = ecc * sinh_f - f - mean_hyperbolic;
657
658 let dg = ecc * cosh_f - 1.0;
660
661 let delta = g / dg;
662 f -= delta;
663
664 if delta.abs() < tol {
665 return Ok(f.rad());
666 }
667 }
668
669 Err(AnomalyError::ConvergenceFailure {
670 iterations: max_iterations,
671 residual: (ecc * f.sinh() - f - mean_hyperbolic).abs(),
672 })
673}
674
675pub fn true_to_mean_hyperbolic(nu: Angle, e: Eccentricity) -> Result<Angle> {
677 let f = true_to_hyperbolic(nu, e)?;
678 Ok(hyperbolic_to_mean(f, e))
679}
680
681pub fn mean_hyperbolic_to_true(mean_hyperbolic: Angle, e: Eccentricity) -> Result<Angle> {
683 let f = mean_to_hyperbolic(mean_hyperbolic, e, None, None)?;
684 Ok(hyperbolic_to_true(f, e))
685}
686
687pub fn hyperbolic_asymptote_angle(e: Eccentricity) -> Angle {
699 Angle::from_acos(-1.0 / e.as_f64())
700}
701
702#[cfg(test)]
703mod tests {
704 use super::*;
705
706 const EPSILON: f64 = 1e-9;
707
708 #[test]
709 fn test_elliptic_round_trip() {
710 let e = Eccentricity::try_new(0.5).unwrap();
711 let nu = Angle::new(1.0);
712
713 let ecc = true_to_eccentric(nu, e);
714 let nu_back = eccentric_to_true(ecc, e);
715
716 assert!((nu.as_f64() - nu_back.as_f64()).abs() < EPSILON);
717 }
718
719 #[test]
720 fn test_keplers_equation() {
721 let e = Eccentricity::try_new(0.3).unwrap();
722 let ecc = Angle::new(1.5);
723
724 let mean = eccentric_to_mean(ecc, e);
725 let ecc_back = mean_to_eccentric(mean, e, None, None).unwrap();
726
727 assert!((ecc.as_f64() - ecc_back.as_f64()).abs() < EPSILON);
728 }
729
730 #[test]
731 fn test_parabolic_round_trip() {
732 let nu = Angle::new(1.0);
733
734 let d = true_to_parabolic(nu);
735 let nu_back = parabolic_to_true(d);
736
737 assert!((nu.as_f64() - nu_back.as_f64()).abs() < EPSILON);
738 }
739
740 #[test]
741 fn test_barkers_equation() {
742 let d = 0.5;
743
744 let mp = parabolic_to_mean(d);
745 let d_back = mean_to_parabolic(mp);
746
747 assert!((d - d_back).abs() < EPSILON);
748 }
749
750 #[test]
751 fn test_hyperbolic_round_trip() {
752 let e = Eccentricity::try_new(1.5).unwrap();
753 let nu = Angle::new(0.5);
754
755 let f = true_to_hyperbolic(nu, e).unwrap();
756 let nu_back = hyperbolic_to_true(f, e);
757
758 assert!((nu.as_f64() - nu_back.as_f64()).abs() < EPSILON);
759 }
760
761 #[test]
762 fn test_hyperbolic_keplers_equation() {
763 let e = Eccentricity::try_new(2.0).unwrap();
764 let f = 0.8.rad();
765
766 let mh = hyperbolic_to_mean(f, e);
767 let f_back = mean_to_hyperbolic(mh, e, None, None).unwrap();
768
769 assert!((f - f_back).abs().as_f64() < EPSILON);
770 }
771
772 #[test]
773 fn test_periapsis() {
774 let e = Eccentricity::try_new(0.7).unwrap();
775 let nu = Angle::ZERO;
776
777 let ecc = true_to_eccentric(nu, e);
778 assert!(ecc.as_f64().abs() < EPSILON);
779
780 let mean = eccentric_to_mean(ecc, e);
781 assert!(mean.as_f64().abs() < EPSILON);
782 }
783
784 #[test]
785 fn test_apoapsis() {
786 let e = Eccentricity::try_new(0.6).unwrap();
787 let nu = Angle::PI;
788
789 let ecc = true_to_eccentric(nu, e);
790 let diff = (ecc.as_f64().abs() - Angle::PI.as_f64()).abs();
793 assert!(diff < EPSILON, "Expected π or -π, got {}", ecc.as_f64());
794 }
795}
796
797#[cfg(test)]
798mod anomaly_tests {
799 use super::*;
800
801 const EPSILON: f64 = 1e-9;
802
803 #[test]
808 fn test_anomaly_elliptic_true_to_eccentric() {
809 let e = Eccentricity::try_new(0.5).unwrap();
810 let nu = Angle::new(1.0);
811
812 let anomaly = TrueAnomaly::new(nu);
813 let eccentric = anomaly.to_eccentric(e).unwrap();
814
815 let true_back = eccentric.to_true(e);
816 assert!((nu.as_f64() - true_back.as_angle().as_f64()).abs() < EPSILON);
817 }
818
819 #[test]
820 fn test_anomaly_elliptic_true_to_mean() {
821 let e = Eccentricity::try_new(0.3).unwrap();
822 let nu = Angle::new(1.5);
823
824 let anomaly = TrueAnomaly::new(nu);
825 let mean = anomaly.to_mean(e).unwrap();
826
827 let true_back = mean.to_true(e).unwrap();
829 assert!((nu.as_f64() - true_back.as_angle().as_f64()).abs() < EPSILON);
830 }
831
832 #[test]
833 fn test_anomaly_elliptic_eccentric_to_mean() {
834 let e = Eccentricity::try_new(0.4).unwrap();
835 let ecc = Angle::new(0.8);
836
837 let anomaly = EccentricAnomaly::new(ecc);
838 let mean = anomaly.to_mean(e);
839
840 let ecc_back = mean.to_eccentric(e).unwrap();
842 assert!((ecc.as_f64() - ecc_back.as_angle().as_f64()).abs() < EPSILON);
843 }
844
845 #[test]
846 fn test_anomaly_elliptic_mean_to_true() {
847 let e = Eccentricity::try_new(0.6).unwrap();
848 let mean = Angle::new(2.0);
849
850 let anomaly = MeanAnomaly::new(mean);
851 let true_anom = anomaly.to_true(e).unwrap();
852
853 let mean_back = true_anom.to_mean(e).unwrap();
855 assert!((mean.as_f64() - mean_back.as_angle().as_f64()).abs() < EPSILON);
856 }
857
858 #[test]
859 fn test_anomaly_elliptic_mean_to_eccentric() {
860 let e = Eccentricity::try_new(0.5).unwrap();
861 let mean = Angle::new(1.2);
862
863 let anomaly = MeanAnomaly::new(mean);
864 let ecc = anomaly.to_eccentric(e).unwrap();
865
866 let mean_back = ecc.to_mean(e);
868 assert!((mean.as_f64() - mean_back.as_angle().as_f64()).abs() < EPSILON);
869 }
870
871 #[test]
876 fn test_anomaly_circular_mean_equals_true() {
877 let e = Eccentricity::try_new(0.0).unwrap();
878 let angle = Angle::new(1.5);
879
880 let true_anom = TrueAnomaly::new(angle);
881 let mean = true_anom.to_mean(e).unwrap();
882
883 assert!((angle.as_f64() - mean.as_angle().as_f64()).abs() < EPSILON);
885
886 let true_back = mean.to_true(e).unwrap();
888 assert!((angle.as_f64() - true_back.as_angle().as_f64()).abs() < EPSILON);
889 }
890
891 #[test]
892 fn test_anomaly_circular_eccentric_conversion() {
893 let e = Eccentricity::try_new(1e-10).unwrap(); let nu = Angle::new(0.5);
895
896 let anomaly = TrueAnomaly::new(nu);
897 let ecc = anomaly.to_eccentric(e).unwrap();
898 let true_back = ecc.to_true(e);
899
900 assert!((nu.as_f64() - true_back.as_angle().as_f64()).abs() < EPSILON);
901 }
902
903 #[test]
908 fn test_anomaly_hyperbolic_true_to_eccentric() {
909 let e = Eccentricity::try_new(1.5).unwrap();
910 let nu = Angle::new(0.5); let anomaly = TrueAnomaly::new(nu);
913 let eccentric = anomaly.to_eccentric(e).unwrap();
914
915 let true_back = eccentric.to_true(e);
917 assert!((nu.as_f64() - true_back.as_angle().as_f64()).abs() < EPSILON);
918 }
919
920 #[test]
921 fn test_anomaly_hyperbolic_true_to_mean() {
922 let e = Eccentricity::try_new(2.0).unwrap();
923 let nu = Angle::new(0.8);
924
925 let anomaly = TrueAnomaly::new(nu);
926 let mean = anomaly.to_mean(e).unwrap();
927
928 let true_back = mean.to_true(e).unwrap();
930 assert!((nu.as_f64() - true_back.as_angle().as_f64()).abs() < EPSILON);
931 }
932
933 #[test]
934 fn test_anomaly_hyperbolic_eccentric_to_mean() {
935 let e = Eccentricity::try_new(1.8).unwrap();
936 let f = Angle::new(0.5);
937
938 let anomaly = EccentricAnomaly::new(f);
939 let mean = anomaly.to_mean(e);
940
941 let ecc_back = mean.to_eccentric(e).unwrap();
943 assert!((f.as_f64() - ecc_back.as_angle().as_f64()).abs() < EPSILON);
944 }
945
946 #[test]
947 fn test_anomaly_hyperbolic_mean_to_true() {
948 let e = Eccentricity::try_new(1.5).unwrap();
949 let mean = Angle::new(1.0);
950
951 let anomaly = MeanAnomaly::new(mean);
952 let true_anom = anomaly.to_true(e).unwrap();
953
954 let mean_back = true_anom.to_mean(e).unwrap();
956 assert!((mean.as_f64() - mean_back.as_angle().as_f64()).abs() < EPSILON);
957 }
958
959 #[test]
960 fn test_anomaly_hyperbolic_asymptote_error() {
961 let e = Eccentricity::try_new(1.5).unwrap();
962 let nu_max = hyperbolic_asymptote_angle(e);
963
964 let nu_invalid = Angle::new(nu_max.as_f64() + 0.1);
966 let anomaly = TrueAnomaly::new(nu_invalid);
967
968 let result = anomaly.to_eccentric(e);
969 assert!(result.is_err());
970 match result {
971 Err(AnomalyError::InvalidTrueAnomaly { .. }) => (),
972 _ => panic!("Expected InvalidTrueAnomaly error"),
973 }
974 }
975
976 #[test]
981 fn test_anomaly_parabolic_true_to_mean() {
982 let e = Eccentricity::try_new(1.0).unwrap();
983 let nu = Angle::new(1.0);
984
985 let anomaly = TrueAnomaly::new(nu);
986 let mean = anomaly.to_mean(e).unwrap();
987
988 let true_back = mean.to_true(e).unwrap();
990 assert!((nu.as_f64() - true_back.as_angle().as_f64()).abs() < EPSILON);
991 }
992
993 #[test]
994 fn test_anomaly_parabolic_mean_to_true() {
995 let e = Eccentricity::try_new(1.0).unwrap();
996 let mean = Angle::new(0.5);
997
998 let anomaly = MeanAnomaly::new(mean);
999 let true_anom = anomaly.to_true(e).unwrap();
1000
1001 let mean_back = true_anom.to_mean(e).unwrap();
1003 assert!((mean.as_f64() - mean_back.as_angle().as_f64()).abs() < EPSILON);
1004 }
1005
1006 #[test]
1007 fn test_anomaly_parabolic_eccentric_round_trip() {
1008 let e = Eccentricity::try_new(1.0).unwrap();
1009 let d = 0.5; let anomaly = EccentricAnomaly::new(d.rad());
1013
1014 let true_anom = anomaly.to_true(e);
1016
1017 let ecc_back = true_anom.to_eccentric(e).unwrap();
1019
1020 assert!((d - ecc_back.as_angle().as_f64()).abs() < EPSILON);
1021 }
1022
1023 #[test]
1024 fn test_anomaly_parabolic_eccentric_to_mean() {
1025 let e = Eccentricity::try_new(1.0).unwrap();
1026 let d = 0.5; let anomaly = EccentricAnomaly::new(d.rad());
1030
1031 let mean = anomaly.to_mean(e);
1033
1034 let ecc_back = mean.to_eccentric(e).unwrap();
1036
1037 assert!((d - ecc_back.as_angle().as_f64()).abs() < EPSILON);
1038 }
1039
1040 #[test]
1045 fn test_anomaly_elliptic_full_round_trip() {
1046 let e = Eccentricity::try_new(0.5).unwrap();
1047 let nu = Angle::new(1.2);
1048
1049 let true_anom = TrueAnomaly::new(nu);
1051 let ecc = true_anom.to_eccentric(e).unwrap();
1052 let mean = ecc.to_mean(e);
1053 let ecc2 = mean.to_eccentric(e).unwrap();
1054 let true_back = ecc2.to_true(e);
1055
1056 assert!((nu.as_f64() - true_back.as_angle().as_f64()).abs() < EPSILON);
1057 }
1058
1059 #[test]
1060 fn test_anomaly_hyperbolic_full_round_trip() {
1061 let e = Eccentricity::try_new(2.0).unwrap();
1062 let nu = Angle::new(0.5);
1063
1064 let true_anom = TrueAnomaly::new(nu);
1066 let ecc = true_anom.to_eccentric(e).unwrap();
1067 let mean = ecc.to_mean(e);
1068 let ecc2 = mean.to_eccentric(e).unwrap();
1069 let true_back = ecc2.to_true(e);
1070
1071 assert!((nu.as_f64() - true_back.as_angle().as_f64()).abs() < EPSILON);
1072 }
1073}