1use crate::astro::anomaly::{mean_to_true, true_to_mean, AnomalyError};
9use crate::astro::elements::{
10 clamp_acos, classify, coe2rv, rv2coe, ClassicalElements, ElementsError, OrbitType,
11};
12use crate::astro::math::{normalize_angle, wrap_to_pi, SMALL, TWO_PI};
13
14const PI: f64 = std::f64::consts::PI;
15const HALF_PI: f64 = std::f64::consts::FRAC_PI_2;
16const STABLE_HALF_ANGLE_MIN: f64 = 1.0e-4;
17const STABLE_HALF_ANGLE_MAX: f64 = 1.0e4;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum RetrogradeFactor {
22 Prograde,
24 Retrograde,
26}
27
28impl RetrogradeFactor {
29 #[inline]
30 fn sign(self) -> f64 {
31 match self {
32 Self::Prograde => 1.0,
33 Self::Retrograde => -1.0,
34 }
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq)]
58pub struct EquinoctialElements {
59 pub a: f64,
61 pub h: f64,
63 pub k: f64,
65 pub p: f64,
67 pub q: f64,
69 pub lambda: f64,
71 pub retrograde: RetrogradeFactor,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq)]
97pub struct ModifiedEquinoctialElements {
98 pub p: f64,
101 pub f: f64,
103 pub g: f64,
105 pub h: f64,
107 pub k: f64,
109 pub l: f64,
111 pub retrograde: RetrogradeFactor,
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, thiserror::Error)]
117pub enum EquinoctialError {
118 #[error(transparent)]
120 Elements(#[from] ElementsError),
121 #[error(transparent)]
123 Anomaly(#[from] AnomalyError),
124 #[error("inclination at the singular pole for the selected retrograde factor")]
126 RetrogradePole,
127 #[error("parabolic orbit is not representable in the equinoctial set; use MEE")]
129 ParabolicEquinoctial,
130}
131
132pub fn coe2eq(
134 coe: &ClassicalElements,
135 factor: RetrogradeFactor,
136) -> Result<EquinoctialElements, EquinoctialError> {
137 validate_classical_common(coe)?;
138 if is_parabolic(coe.ecc) {
139 return Err(EquinoctialError::ParabolicEquinoctial);
140 }
141 check_finite(coe.a, "a")?;
142 check_retrograde_pole(coe.incl, factor)?;
143
144 let resolved = resolve_classical_angles(coe)?;
145 let varpi = wrap_to_pi(resolved.argp + factor.sign() * resolved.raan);
146 let mean = true_to_mean(resolved.nu, coe.ecc)?;
147 let lambda = mean_longitude(mean, varpi, coe.ecc);
148 let scale = inclination_scale(coe.incl, factor)?;
149
150 Ok(EquinoctialElements {
151 a: coe.a,
152 h: coe.ecc * varpi.sin(),
153 k: coe.ecc * varpi.cos(),
154 p: scale * resolved.raan.sin(),
155 q: scale * resolved.raan.cos(),
156 lambda,
157 retrograde: factor,
158 })
159}
160
161pub fn eq2coe(eq: &EquinoctialElements) -> Result<ClassicalElements, EquinoctialError> {
163 let recovered = recover_equinoctial(eq)?;
164 Ok(recovered_to_classical(&recovered))
165}
166
167pub fn coe2mee(
169 coe: &ClassicalElements,
170 factor: RetrogradeFactor,
171) -> Result<ModifiedEquinoctialElements, EquinoctialError> {
172 validate_classical_common(coe)?;
173 validate_classical_a_for_mee(coe)?;
174 check_retrograde_pole(coe.incl, factor)?;
175
176 let resolved = resolve_classical_angles(coe)?;
177 let varpi = wrap_to_pi(resolved.argp + factor.sign() * resolved.raan);
178 let l = normalize_angle(varpi + resolved.nu);
179 let scale = inclination_scale(coe.incl, factor)?;
180
181 Ok(ModifiedEquinoctialElements {
182 p: coe.p,
183 f: coe.ecc * varpi.cos(),
184 g: coe.ecc * varpi.sin(),
185 h: scale * resolved.raan.cos(),
186 k: scale * resolved.raan.sin(),
187 l,
188 retrograde: factor,
189 })
190}
191
192pub fn mee2coe(mee: &ModifiedEquinoctialElements) -> Result<ClassicalElements, EquinoctialError> {
194 let recovered = recover_modified_equinoctial(mee)?;
195 Ok(recovered_to_classical(&recovered))
196}
197
198pub fn rv2eq(
200 r: [f64; 3],
201 v: [f64; 3],
202 mu: f64,
203 factor: RetrogradeFactor,
204) -> Result<EquinoctialElements, EquinoctialError> {
205 let coe = rv2coe(r, v, mu)?;
206 coe2eq(&coe, factor)
207}
208
209pub fn eq2rv(eq: &EquinoctialElements, mu: f64) -> Result<([f64; 3], [f64; 3]), EquinoctialError> {
211 let coe = eq2coe(eq)?;
212 Ok(coe2rv(&coe, mu)?)
213}
214
215pub fn rv2mee(
217 r: [f64; 3],
218 v: [f64; 3],
219 mu: f64,
220 factor: RetrogradeFactor,
221) -> Result<ModifiedEquinoctialElements, EquinoctialError> {
222 let coe = rv2coe(r, v, mu)?;
223 coe2mee(&coe, factor)
224}
225
226pub fn mee2rv(
228 mee: &ModifiedEquinoctialElements,
229 mu: f64,
230) -> Result<([f64; 3], [f64; 3]), EquinoctialError> {
231 let coe = mee2coe(mee)?;
232 Ok(coe2rv(&coe, mu)?)
233}
234
235pub fn eq2mee(eq: &EquinoctialElements) -> Result<ModifiedEquinoctialElements, EquinoctialError> {
237 let recovered = recover_equinoctial(eq)?;
238 let scale = inclination_scale(recovered.incl, recovered.retrograde)?;
239
240 Ok(ModifiedEquinoctialElements {
241 p: recovered.p,
242 f: recovered.ecc * recovered.varpi.cos(),
243 g: recovered.ecc * recovered.varpi.sin(),
244 h: scale * recovered.raan.cos(),
245 k: scale * recovered.raan.sin(),
246 l: recovered.l,
247 retrograde: recovered.retrograde,
248 })
249}
250
251pub fn mee2eq(mee: &ModifiedEquinoctialElements) -> Result<EquinoctialElements, EquinoctialError> {
253 let recovered = recover_modified_equinoctial(mee)?;
254 if is_parabolic(recovered.ecc) {
255 return Err(EquinoctialError::ParabolicEquinoctial);
256 }
257 let mean = true_to_mean(recovered.nu, recovered.ecc)?;
258 let lambda = mean_longitude(mean, recovered.varpi, recovered.ecc);
259 let scale = inclination_scale(recovered.incl, recovered.retrograde)?;
260
261 Ok(EquinoctialElements {
262 a: recovered.a,
263 h: recovered.ecc * recovered.varpi.sin(),
264 k: recovered.ecc * recovered.varpi.cos(),
265 p: scale * recovered.raan.sin(),
266 q: scale * recovered.raan.cos(),
267 lambda,
268 retrograde: recovered.retrograde,
269 })
270}
271
272#[derive(Debug, Clone, Copy)]
273struct ResolvedClassicalAngles {
274 raan: f64,
275 argp: f64,
276 nu: f64,
277}
278
279#[derive(Debug, Clone, Copy)]
280struct RecoveredConic {
281 p: f64,
282 a: f64,
283 ecc: f64,
284 incl: f64,
285 raan: f64,
286 varpi: f64,
287 nu: f64,
288 l: f64,
289 retrograde: RetrogradeFactor,
290}
291
292fn recover_equinoctial(eq: &EquinoctialElements) -> Result<RecoveredConic, EquinoctialError> {
293 validate_equinoctial(eq)?;
294
295 let ecc = eq.h.hypot(eq.k);
296 if is_parabolic(ecc) {
297 return Err(EquinoctialError::ParabolicEquinoctial);
298 }
299 let varpi = eq.h.atan2(eq.k);
300 let incl = recover_inclination(eq.p, eq.q, eq.retrograde)?;
301 let raan = eq.p.atan2(eq.q);
302 let mean = eq.lambda - varpi;
303 let nu = mean_to_true(mean, ecc)?;
304 let l = normalize_angle(varpi + nu);
305 let p = eq.a * (1.0 - ecc * ecc);
306 if !p.is_finite() {
307 return Err(ElementsError::NonFinite { field: "p" }.into());
308 }
309 if p <= 0.0 {
310 return Err(ElementsError::NonPositiveSemiLatus.into());
311 }
312
313 Ok(RecoveredConic {
314 p,
315 a: eq.a,
316 ecc,
317 incl,
318 raan,
319 varpi,
320 nu,
321 l,
322 retrograde: eq.retrograde,
323 })
324}
325
326fn recover_modified_equinoctial(
327 mee: &ModifiedEquinoctialElements,
328) -> Result<RecoveredConic, EquinoctialError> {
329 validate_modified_equinoctial(mee)?;
330
331 let ecc = mee.f.hypot(mee.g);
332 let varpi = mee.g.atan2(mee.f);
333 let incl = recover_inclination(mee.h, mee.k, mee.retrograde)?;
334 let raan = mee.k.atan2(mee.h);
335 let nu = mee.l - varpi;
336 let a = if is_parabolic(ecc) {
337 f64::INFINITY
338 } else {
339 mee.p / (1.0 - ecc * ecc)
340 };
341 if !is_parabolic(ecc) && !a.is_finite() {
342 return Err(ElementsError::NonFinite { field: "a" }.into());
343 }
344
345 Ok(RecoveredConic {
346 p: mee.p,
347 a,
348 ecc,
349 incl,
350 raan,
351 varpi,
352 nu,
353 l: normalize_angle(mee.l),
354 retrograde: mee.retrograde,
355 })
356}
357
358fn recovered_to_classical(recovered: &RecoveredConic) -> ClassicalElements {
359 let orbit_type = classify(recovered.ecc, recovered.incl);
360 let circular = matches!(
361 orbit_type,
362 OrbitType::CircularInclined | OrbitType::CircularEquatorial
363 );
364 let equatorial = matches!(
365 orbit_type,
366 OrbitType::EllipticalEquatorial | OrbitType::CircularEquatorial
367 );
368 let ecc = if circular { 0.0 } else { recovered.ecc };
369 let incl = if equatorial {
370 if recovered.incl > HALF_PI {
371 PI
372 } else {
373 0.0
374 }
375 } else {
376 recovered.incl
377 };
378 let (p, a) = if circular && recovered.a.is_finite() {
379 (recovered.a, recovered.a)
380 } else {
381 (recovered.p, recovered.a)
382 };
383
384 let raan = normalize_angle(recovered.raan);
385 let argp = normalize_angle(recovered.varpi - recovered.retrograde.sign() * raan);
386 let nu = normalize_angle(recovered.nu);
387 let l = normalize_angle(recovered.l);
388
389 match orbit_type {
390 OrbitType::EllipticalInclined => ClassicalElements {
391 p,
392 a,
393 ecc,
394 incl,
395 raan,
396 argp,
397 nu,
398 arglat: f64::NAN,
399 truelon: f64::NAN,
400 lonper: f64::NAN,
401 orbit_type,
402 },
403 OrbitType::CircularInclined => ClassicalElements {
404 p,
405 a,
406 ecc,
407 incl,
408 raan,
409 argp: f64::NAN,
410 nu: f64::NAN,
411 arglat: normalize_angle(l - recovered.retrograde.sign() * raan),
412 truelon: f64::NAN,
413 lonper: f64::NAN,
414 orbit_type,
415 },
416 OrbitType::EllipticalEquatorial => ClassicalElements {
417 p,
418 a,
419 ecc,
420 incl,
421 raan: f64::NAN,
422 argp: f64::NAN,
423 nu,
424 arglat: f64::NAN,
425 truelon: f64::NAN,
426 lonper: folded_equatorial_angle(recovered.varpi, incl),
427 orbit_type,
428 },
429 OrbitType::CircularEquatorial => ClassicalElements {
430 p,
431 a,
432 ecc,
433 incl,
434 raan: f64::NAN,
435 argp: f64::NAN,
436 nu: f64::NAN,
437 arglat: f64::NAN,
438 truelon: folded_equatorial_angle(l, incl),
439 lonper: f64::NAN,
440 orbit_type,
441 },
442 }
443}
444
445fn resolve_classical_angles(
446 coe: &ClassicalElements,
447) -> Result<ResolvedClassicalAngles, EquinoctialError> {
448 match coe.orbit_type {
449 OrbitType::EllipticalInclined => {
450 check_finite(coe.raan, "raan")?;
451 check_finite(coe.argp, "argp")?;
452 check_finite(coe.nu, "nu")?;
453 Ok(ResolvedClassicalAngles {
454 raan: coe.raan,
455 argp: coe.argp,
456 nu: coe.nu,
457 })
458 }
459 OrbitType::CircularInclined => {
460 check_finite(coe.raan, "raan")?;
461 check_finite(coe.arglat, "arglat")?;
462 Ok(ResolvedClassicalAngles {
463 raan: coe.raan,
464 argp: 0.0,
465 nu: coe.arglat,
466 })
467 }
468 OrbitType::EllipticalEquatorial => {
469 check_finite(coe.lonper, "lonper")?;
470 check_finite(coe.nu, "nu")?;
471 Ok(ResolvedClassicalAngles {
472 raan: 0.0,
473 argp: unfolded_equatorial_angle(coe.lonper, coe.incl),
474 nu: coe.nu,
475 })
476 }
477 OrbitType::CircularEquatorial => {
478 check_finite(coe.truelon, "truelon")?;
479 Ok(ResolvedClassicalAngles {
480 raan: 0.0,
481 argp: 0.0,
482 nu: unfolded_equatorial_angle(coe.truelon, coe.incl),
483 })
484 }
485 }
486}
487
488fn validate_classical_common(coe: &ClassicalElements) -> Result<(), EquinoctialError> {
489 check_finite(coe.p, "p")?;
490 if coe.p <= 0.0 {
491 return Err(ElementsError::NonPositiveSemiLatus.into());
492 }
493 check_finite(coe.ecc, "ecc")?;
494 check_finite(coe.incl, "incl")?;
495 Ok(())
496}
497
498fn validate_classical_a_for_mee(coe: &ClassicalElements) -> Result<(), EquinoctialError> {
499 if is_parabolic(coe.ecc) {
500 Ok(())
501 } else {
502 check_finite(coe.a, "a")
503 }
504}
505
506fn validate_equinoctial(eq: &EquinoctialElements) -> Result<(), EquinoctialError> {
507 check_finite(eq.a, "a")?;
508 check_finite(eq.h, "h")?;
509 check_finite(eq.k, "k")?;
510 check_finite(eq.p, "p")?;
511 check_finite(eq.q, "q")?;
512 check_finite(eq.lambda, "lambda")
513}
514
515fn validate_modified_equinoctial(
516 mee: &ModifiedEquinoctialElements,
517) -> Result<(), EquinoctialError> {
518 check_finite(mee.p, "p")?;
519 if mee.p <= 0.0 {
520 return Err(ElementsError::NonPositiveSemiLatus.into());
521 }
522 check_finite(mee.f, "f")?;
523 check_finite(mee.g, "g")?;
524 check_finite(mee.h, "h")?;
525 check_finite(mee.k, "k")?;
526 check_finite(mee.l, "l")
527}
528
529fn check_finite(value: f64, field: &'static str) -> Result<(), EquinoctialError> {
530 if value.is_finite() {
531 Ok(())
532 } else {
533 Err(ElementsError::NonFinite { field }.into())
534 }
535}
536
537fn inclination_scale(incl: f64, factor: RetrogradeFactor) -> Result<f64, EquinoctialError> {
538 check_retrograde_pole(incl, factor)?;
539 let tan_half = (0.5 * incl).tan();
540 let scale = match factor {
541 RetrogradeFactor::Prograde => tan_half,
542 RetrogradeFactor::Retrograde => 1.0 / tan_half,
543 };
544 if scale.is_finite() {
545 Ok(scale)
546 } else {
547 Err(EquinoctialError::RetrogradePole)
548 }
549}
550
551fn recover_inclination(x: f64, y: f64, factor: RetrogradeFactor) -> Result<f64, EquinoctialError> {
552 let scale = x.hypot(y);
553 if !scale.is_finite() {
554 return Err(EquinoctialError::RetrogradePole);
555 }
556
557 let atan_incl = match factor {
558 RetrogradeFactor::Prograde => 2.0 * scale.atan(),
559 RetrogradeFactor::Retrograde => PI - 2.0 * scale.atan(),
560 };
561 let incl = if (STABLE_HALF_ANGLE_MIN..=STABLE_HALF_ANGLE_MAX).contains(&scale) {
562 let scale_sq = scale * scale;
563 let cos_incl = match factor {
564 RetrogradeFactor::Prograde => (1.0 - scale_sq) / (1.0 + scale_sq),
565 RetrogradeFactor::Retrograde => (scale_sq - 1.0) / (1.0 + scale_sq),
566 };
567 clamp_acos(cos_incl)
568 } else {
569 atan_incl
570 };
571 check_retrograde_pole(incl, factor)?;
572 Ok(incl)
573}
574
575fn check_retrograde_pole(incl: f64, factor: RetrogradeFactor) -> Result<(), EquinoctialError> {
576 match factor {
577 RetrogradeFactor::Prograde if (incl - PI).abs() < SMALL => {
578 Err(EquinoctialError::RetrogradePole)
579 }
580 RetrogradeFactor::Retrograde if incl.abs() < SMALL => Err(EquinoctialError::RetrogradePole),
581 _ => Ok(()),
582 }
583}
584
585fn folded_equatorial_angle(angle: f64, incl: f64) -> f64 {
586 let angle = normalize_angle(angle);
587 if incl > HALF_PI {
588 normalize_angle(TWO_PI - angle)
589 } else {
590 angle
591 }
592}
593
594fn unfolded_equatorial_angle(angle: f64, incl: f64) -> f64 {
595 folded_equatorial_angle(angle, incl)
596}
597
598fn mean_longitude(mean: f64, varpi: f64, ecc: f64) -> f64 {
599 if ecc < 1.0 {
600 normalize_angle(mean + varpi)
601 } else {
602 mean + varpi
603 }
604}
605
606fn is_parabolic(ecc: f64) -> bool {
607 (ecc - 1.0).abs() < SMALL
608}
609
610#[cfg(test)]
611mod tests {
612 use super::*;
613
614 const MU: f64 = 398600.4418;
615 const DEG: f64 = std::f64::consts::PI / 180.0;
616
617 #[derive(Debug, Clone, Copy)]
618 struct Case {
619 name: &'static str,
620 coe: ClassicalElements,
621 factor: RetrogradeFactor,
622 }
623
624 fn assert_close(got: f64, want: f64, tol: f64, label: &str) {
625 assert!(
626 (got - want).abs() <= tol,
627 "{label}: got {got}, want {want}, diff {}",
628 (got - want).abs()
629 );
630 }
631
632 fn assert_scaled_close(got: f64, want: f64, rel: f64, label: &str) {
633 let scale = 1.0_f64.max(want.abs());
634 assert!(
635 (got - want).abs() <= rel * scale,
636 "{label}: got {got}, want {want}, diff {}",
637 (got - want).abs()
638 );
639 }
640
641 fn angle_diff(a: f64, b: f64) -> f64 {
642 let diff = normalize_angle(a - b);
643 diff.min(TWO_PI - diff)
644 }
645
646 fn assert_angle_close(got: f64, want: f64, tol: f64, label: &str) {
647 let diff = angle_diff(got, want);
648 assert!(diff <= tol, "{label}: got {got}, want {want}, diff {diff}");
649 }
650
651 fn assert_vec_close(got: [f64; 3], want: [f64; 3], tol: f64, label: &str) {
652 for i in 0..3 {
653 assert!(
654 (got[i] - want[i]).abs() <= tol,
655 "{label}[{i}]: got {}, want {}, diff {}",
656 got[i],
657 want[i],
658 (got[i] - want[i]).abs()
659 );
660 }
661 }
662
663 fn assert_state_close(got: ([f64; 3], [f64; 3]), want: ([f64; 3], [f64; 3]), label: &str) {
664 assert_vec_close(got.0, want.0, 1.0e-7, &format!("{label} r"));
665 assert_vec_close(got.1, want.1, 1.0e-10, &format!("{label} v"));
666 }
667
668 fn classical(p: f64, ecc: f64, incl: f64, raan: f64, argp: f64, nu: f64) -> ClassicalElements {
669 ClassicalElements::new(p, ecc, incl, raan, argp, nu)
670 }
671
672 fn circular_inclined(p: f64, incl: f64, raan: f64, arglat: f64) -> ClassicalElements {
673 let mut coe = ClassicalElements::new(p, 0.0, incl, raan, 0.0, 0.0);
674 coe.orbit_type = OrbitType::CircularInclined;
675 coe.argp = f64::NAN;
676 coe.nu = f64::NAN;
677 coe.arglat = arglat;
678 coe
679 }
680
681 fn elliptical_equatorial(
682 p: f64,
683 ecc: f64,
684 incl: f64,
685 lonper: f64,
686 nu: f64,
687 ) -> ClassicalElements {
688 let mut coe = ClassicalElements::new(p, ecc, incl, 0.0, 0.0, nu);
689 coe.orbit_type = OrbitType::EllipticalEquatorial;
690 coe.raan = f64::NAN;
691 coe.argp = f64::NAN;
692 coe.lonper = lonper;
693 coe
694 }
695
696 fn regimes() -> Vec<Case> {
697 vec![
698 Case {
699 name: "leo circular inclined",
700 coe: circular_inclined(7000.0, 51.6 * DEG, 80.0 * DEG, 135.0 * DEG),
701 factor: RetrogradeFactor::Prograde,
702 },
703 Case {
704 name: "geo near equatorial",
705 coe: classical(
706 42164.0 * (1.0 - 0.001_f64.powi(2)),
707 0.001,
708 0.01 * DEG,
709 40.0 * DEG,
710 20.0 * DEG,
711 10.0 * DEG,
712 ),
713 factor: RetrogradeFactor::Prograde,
714 },
715 Case {
716 name: "molniya",
717 coe: classical(
718 26600.0 * (1.0 - 0.74_f64.powi(2)),
719 0.74,
720 63.4 * DEG,
721 30.0 * DEG,
722 270.0 * DEG,
723 15.0 * DEG,
724 ),
725 factor: RetrogradeFactor::Prograde,
726 },
727 Case {
728 name: "near circular",
729 coe: classical(
730 7200.0 * (1.0 - 1.0e-6_f64.powi(2)),
731 1.0e-6,
732 40.0 * DEG,
733 12.0 * DEG,
734 25.0 * DEG,
735 80.0 * DEG,
736 ),
737 factor: RetrogradeFactor::Prograde,
738 },
739 Case {
740 name: "near equatorial",
741 coe: classical(
742 9000.0 * (1.0 - 0.2_f64.powi(2)),
743 0.2,
744 1.0e-4 * DEG,
745 90.0 * DEG,
746 80.0 * DEG,
747 70.0 * DEG,
748 ),
749 factor: RetrogradeFactor::Prograde,
750 },
751 Case {
752 name: "combined near circular near equatorial",
753 coe: classical(
754 7100.0 * (1.0 - 1.0e-6_f64.powi(2)),
755 1.0e-6,
756 1.0e-4 * DEG,
757 5.0 * DEG,
758 5.0 * DEG,
759 45.0 * DEG,
760 ),
761 factor: RetrogradeFactor::Prograde,
762 },
763 Case {
764 name: "retrograde prograde factor",
765 coe: classical(
766 12000.0 * (1.0 - 0.1_f64.powi(2)),
767 0.1,
768 175.0 * DEG,
769 45.0 * DEG,
770 10.0 * DEG,
771 250.0 * DEG,
772 ),
773 factor: RetrogradeFactor::Prograde,
774 },
775 Case {
776 name: "true retrograde",
777 coe: elliptical_equatorial(
778 11000.0 * (1.0 - 0.2_f64.powi(2)),
779 0.2,
780 PI,
781 40.0 * DEG,
782 80.0 * DEG,
783 ),
784 factor: RetrogradeFactor::Retrograde,
785 },
786 ]
787 }
788
789 fn assert_classical_recombined_close(
790 got: &ClassicalElements,
791 want: &ClassicalElements,
792 factor: RetrogradeFactor,
793 label: &str,
794 ) {
795 assert_eq!(got.orbit_type, want.orbit_type, "{label} orbit_type");
796 assert_scaled_close(got.p, want.p, 1.0e-10, &format!("{label} p"));
797 if got.a.is_finite() && want.a.is_finite() {
798 assert_scaled_close(got.a, want.a, 1.0e-10, &format!("{label} a"));
799 } else {
800 assert_eq!(got.a.is_infinite(), want.a.is_infinite(), "{label} a inf");
801 }
802 assert_close(got.ecc, want.ecc, 1.0e-10, &format!("{label} ecc"));
803 assert_angle_close(got.incl, want.incl, 1.0e-10, &format!("{label} incl"));
804
805 let got_resolved = resolve_classical_angles(got).unwrap();
806 let want_resolved = resolve_classical_angles(want).unwrap();
807 let got_varpi = wrap_to_pi(got_resolved.argp + factor.sign() * got_resolved.raan);
808 let want_varpi = wrap_to_pi(want_resolved.argp + factor.sign() * want_resolved.raan);
809 assert_angle_close(got_varpi, want_varpi, 1.0e-10, &format!("{label} varpi"));
810 assert_angle_close(
811 normalize_angle(got_varpi + got_resolved.nu),
812 normalize_angle(want_varpi + want_resolved.nu),
813 1.0e-10,
814 &format!("{label} L"),
815 );
816 }
817
818 fn assert_eq_close(
819 got: &EquinoctialElements,
820 want: &EquinoctialElements,
821 ecc: f64,
822 label: &str,
823 ) {
824 assert_scaled_close(got.a, want.a, 1.0e-10, &format!("{label} a"));
825 assert_close(got.h, want.h, 1.0e-10, &format!("{label} h"));
826 assert_close(got.k, want.k, 1.0e-10, &format!("{label} k"));
827 assert_close(got.p, want.p, 1.0e-10, &format!("{label} p"));
828 assert_close(got.q, want.q, 1.0e-10, &format!("{label} q"));
829 if ecc > 1.0 {
830 assert_scaled_close(got.lambda, want.lambda, 1.0e-9, &format!("{label} lambda"));
831 } else {
832 assert_angle_close(got.lambda, want.lambda, 1.0e-10, &format!("{label} lambda"));
833 }
834 assert_eq!(got.retrograde, want.retrograde, "{label} factor");
835 }
836
837 fn assert_mee_close(
838 got: &ModifiedEquinoctialElements,
839 want: &ModifiedEquinoctialElements,
840 label: &str,
841 ) {
842 assert_scaled_close(got.p, want.p, 1.0e-10, &format!("{label} p"));
843 assert_close(got.f, want.f, 1.0e-10, &format!("{label} f"));
844 assert_close(got.g, want.g, 1.0e-10, &format!("{label} g"));
845 assert_close(got.h, want.h, 1.0e-10, &format!("{label} h"));
846 assert_close(got.k, want.k, 1.0e-10, &format!("{label} k"));
847 assert_angle_close(got.l, want.l, 1.0e-10, &format!("{label} l"));
848 assert_eq!(got.retrograde, want.retrograde, "{label} factor");
849 }
850
851 #[test]
852 fn round_trip_state_across_regimes() {
853 for case in regimes() {
854 let state = coe2rv(&case.coe, MU).unwrap();
855
856 let eq = rv2eq(state.0, state.1, MU, case.factor).unwrap();
857 let eq_state = eq2rv(&eq, MU).unwrap();
858 assert_state_close(eq_state, state, case.name);
859
860 let mee = rv2mee(state.0, state.1, MU, case.factor).unwrap();
861 let mee_state = mee2rv(&mee, MU).unwrap();
862 assert_state_close(mee_state, state, case.name);
863 }
864 }
865
866 #[test]
867 fn round_trip_element_algebra_across_regimes() {
868 for case in regimes() {
869 let eq = coe2eq(&case.coe, case.factor).unwrap();
870 let eq_back = eq2coe(&eq).unwrap();
871 assert_classical_recombined_close(&eq_back, &case.coe, case.factor, case.name);
872
873 let mee = coe2mee(&case.coe, case.factor).unwrap();
874 let mee_back = mee2coe(&mee).unwrap();
875 assert_classical_recombined_close(&mee_back, &case.coe, case.factor, case.name);
876
877 let converted_mee = eq2mee(&eq).unwrap();
878 assert_mee_close(&converted_mee, &mee, case.name);
879
880 let converted_eq = mee2eq(&mee).unwrap();
881 assert_eq_close(&converted_eq, &eq, case.coe.ecc, case.name);
882 }
883 }
884
885 #[test]
886 fn vallado_reference_components_match_full_precision_and_rounded_values() {
887 let r = [6524.834, 6862.875, 6448.296];
888 let v = [4.901327, 5.533756, -1.976341];
889 let coe = rv2coe(r, v, MU).unwrap();
890
891 let eq = coe2eq(&coe, RetrogradeFactor::Prograde).unwrap();
892 let mee = coe2mee(&coe, RetrogradeFactor::Prograde).unwrap();
893
894 let varpi = wrap_to_pi(coe.argp + coe.raan);
899 let mean = true_to_mean(coe.nu, coe.ecc).unwrap();
900 let scale = (0.5 * coe.incl).tan();
901 assert_close(eq.a, coe.a, 1.0e-12, "eq a full");
902 assert_close(eq.h, coe.ecc * varpi.sin(), 1.0e-12, "eq h full");
903 assert_close(eq.k, coe.ecc * varpi.cos(), 1.0e-12, "eq k full");
904 assert_close(eq.p, scale * coe.raan.sin(), 1.0e-12, "eq p full");
905 assert_close(eq.q, scale * coe.raan.cos(), 1.0e-12, "eq q full");
906 assert_angle_close(eq.lambda, mean + varpi, 1.0e-12, "eq lambda full");
907
908 assert_close(mee.p, coe.p, 1.0e-12, "mee p full");
909 assert_close(mee.f, coe.ecc * varpi.cos(), 1.0e-12, "mee f full");
910 assert_close(mee.g, coe.ecc * varpi.sin(), 1.0e-12, "mee g full");
911 assert_close(mee.h, scale * coe.raan.cos(), 1.0e-12, "mee h full");
912 assert_close(mee.k, scale * coe.raan.sin(), 1.0e-12, "mee k full");
913 assert_angle_close(mee.l, varpi + coe.nu, 1.0e-12, "mee l full");
914
915 let e = 0.832853_f64;
916 let incl = 87.870 * DEG;
917 let raan = 227.898 * DEG;
918 let argp = 53.38 * DEG;
919 let nu = 92.335 * DEG;
920 let varpi = wrap_to_pi(argp + raan);
921 let mean = true_to_mean(nu, e).unwrap();
922 let scale = (0.5 * incl).tan();
923
924 assert_close(eq.a, 36127.343, 1.0e-2, "eq a rounded");
925 assert_close(eq.h, e * varpi.sin(), 1.0e-4, "eq h rounded");
926 assert_close(eq.k, e * varpi.cos(), 1.0e-4, "eq k rounded");
927 assert_close(eq.p, scale * raan.sin(), 1.0e-4, "eq p rounded");
928 assert_close(eq.q, scale * raan.cos(), 1.0e-4, "eq q rounded");
929 assert_angle_close(eq.lambda, mean + varpi, 1.0e-4, "eq lambda rounded");
930
931 assert_close(mee.p, 11067.790, 1.0e-2, "mee p rounded");
932 assert_close(mee.f, e * varpi.cos(), 1.0e-4, "mee f rounded");
933 assert_close(mee.g, e * varpi.sin(), 1.0e-4, "mee g rounded");
934 assert_close(mee.h, scale * raan.cos(), 1.0e-4, "mee h rounded");
935 assert_close(mee.k, scale * raan.sin(), 1.0e-4, "mee k rounded");
936 assert_angle_close(mee.l, varpi + nu, 1.0e-4, "mee l rounded");
937 }
938
939 #[test]
940 fn mee2rv_near_equatorial_low_eccentricity_regression() {
941 let coe = classical(
942 7000.0 * (1.0 - 1.0e-4_f64.powi(2)),
943 1.0e-4,
944 1.0e-3 * DEG,
945 15.0 * DEG,
946 25.0 * DEG,
947 35.0 * DEG,
948 );
949 let state = coe2rv(&coe, MU).unwrap();
950 let mee = coe2mee(&coe, RetrogradeFactor::Prograde).unwrap();
951 assert_state_close(mee2rv(&mee, MU).unwrap(), state, "near equatorial mee2rv");
952 }
953
954 #[test]
955 fn retrograde_pole_errors_and_opposite_factor_round_trips() {
956 let coe = elliptical_equatorial(
957 11000.0 * (1.0 - 0.2_f64.powi(2)),
958 0.2,
959 PI,
960 40.0 * DEG,
961 80.0 * DEG,
962 );
963 assert_eq!(
964 coe2eq(&coe, RetrogradeFactor::Prograde),
965 Err(EquinoctialError::RetrogradePole)
966 );
967 assert_eq!(
968 coe2mee(&coe, RetrogradeFactor::Prograde),
969 Err(EquinoctialError::RetrogradePole)
970 );
971
972 let state = coe2rv(&coe, MU).unwrap();
973 let eq = coe2eq(&coe, RetrogradeFactor::Retrograde).unwrap();
974 assert_state_close(eq2rv(&eq, MU).unwrap(), state, "retrograde eq");
975 let mee = coe2mee(&coe, RetrogradeFactor::Retrograde).unwrap();
976 assert_state_close(mee2rv(&mee, MU).unwrap(), state, "retrograde mee");
977 }
978
979 #[test]
980 fn parabolic_equinoctial_rejected_and_mee_round_trips() {
981 let coe = classical(12000.0, 1.0, 30.0 * DEG, 40.0 * DEG, 50.0 * DEG, 60.0 * DEG);
982 assert_eq!(
983 coe2eq(&coe, RetrogradeFactor::Prograde),
984 Err(EquinoctialError::ParabolicEquinoctial)
985 );
986
987 let state = coe2rv(&coe, MU).unwrap();
988 assert_eq!(
989 rv2eq(state.0, state.1, MU, RetrogradeFactor::Prograde),
990 Err(EquinoctialError::ParabolicEquinoctial)
991 );
992
993 let mee = coe2mee(&coe, RetrogradeFactor::Prograde).unwrap();
994 assert_close(mee.p, coe.p, 1.0e-12, "mee parabolic p");
995 let back = mee2coe(&mee).unwrap();
996 assert!(back.a.is_infinite());
997 assert_close(back.p, coe.p, 1.0e-12, "coe parabolic p");
998 assert_state_close(mee2rv(&mee, MU).unwrap(), state, "parabolic mee");
999 }
1000
1001 #[test]
1002 fn input_rejections_are_typed() {
1003 let coe = classical(
1004 7200.0 * (1.0 - 0.01_f64.powi(2)),
1005 0.01,
1006 40.0 * DEG,
1007 12.0 * DEG,
1008 25.0 * DEG,
1009 80.0 * DEG,
1010 );
1011 let state = coe2rv(&coe, MU).unwrap();
1012 let mut eq = coe2eq(&coe, RetrogradeFactor::Prograde).unwrap();
1013 eq.h = f64::NAN;
1014 assert!(matches!(
1015 eq2coe(&eq),
1016 Err(EquinoctialError::Elements(ElementsError::NonFinite {
1017 field: "h"
1018 }))
1019 ));
1020
1021 let mut mee = coe2mee(&coe, RetrogradeFactor::Prograde).unwrap();
1022 mee.l = f64::INFINITY;
1023 assert!(matches!(
1024 mee2coe(&mee),
1025 Err(EquinoctialError::Elements(ElementsError::NonFinite {
1026 field: "l"
1027 }))
1028 ));
1029
1030 let eq = coe2eq(&coe, RetrogradeFactor::Prograde).unwrap();
1031 assert_eq!(
1032 eq2rv(&eq, 0.0),
1033 Err(EquinoctialError::Elements(ElementsError::NonPositiveMu))
1034 );
1035 assert_eq!(
1036 rv2eq(state.0, state.1, 0.0, RetrogradeFactor::Prograde),
1037 Err(EquinoctialError::Elements(ElementsError::NonPositiveMu))
1038 );
1039 assert_eq!(
1040 rv2eq([0.0, 0.0, 0.0], state.1, MU, RetrogradeFactor::Prograde),
1041 Err(EquinoctialError::Elements(ElementsError::ZeroPosition))
1042 );
1043 assert_eq!(
1044 rv2mee(
1045 [7000.0, 0.0, 0.0],
1046 [7.5, 0.0, 0.0],
1047 MU,
1048 RetrogradeFactor::Prograde
1049 ),
1050 Err(EquinoctialError::Elements(ElementsError::DegenerateOrbit))
1051 );
1052
1053 let hyperbolic = classical(10000.0, 1.5, 40.0 * DEG, 20.0 * DEG, 30.0 * DEG, PI);
1054 assert!(matches!(
1055 coe2eq(&hyperbolic, RetrogradeFactor::Prograde),
1056 Err(EquinoctialError::Anomaly(
1057 AnomalyError::BeyondAsymptote { .. }
1058 ))
1059 ));
1060 }
1061}