1use crate::constants::{mu, orbit_radius, AU_KM};
12use crate::kepler;
13use crate::units::{Eccentricity, Km, Mu, Radians, Seconds};
14use std::f64::consts::PI;
15
16pub const J2000_JD: f64 = 2_451_545.0;
18
19const JULIAN_CENTURY_DAYS: f64 = 36525.0;
21
22const SECONDS_PER_DAY: f64 = 86400.0;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Planet {
28 Mercury,
29 Venus,
30 Earth,
31 Mars,
32 Jupiter,
33 Saturn,
34 Uranus,
35 Neptune,
36}
37
38impl Planet {
39 pub const ALL: [Planet; 8] = [
41 Planet::Mercury,
42 Planet::Venus,
43 Planet::Earth,
44 Planet::Mars,
45 Planet::Jupiter,
46 Planet::Saturn,
47 Planet::Uranus,
48 Planet::Neptune,
49 ];
50
51 pub fn semi_major_axis(self) -> Km {
53 match self {
54 Planet::Mercury => orbit_radius::MERCURY,
55 Planet::Venus => orbit_radius::VENUS,
56 Planet::Earth => orbit_radius::EARTH,
57 Planet::Mars => orbit_radius::MARS,
58 Planet::Jupiter => orbit_radius::JUPITER,
59 Planet::Saturn => orbit_radius::SATURN,
60 Planet::Uranus => orbit_radius::URANUS,
61 Planet::Neptune => Km(4_495_060_000.0),
62 }
63 }
64
65 pub fn mu(self) -> Mu {
67 match self {
68 Planet::Mercury => mu::MERCURY,
69 Planet::Venus => mu::VENUS,
70 Planet::Earth => mu::EARTH,
71 Planet::Mars => mu::MARS,
72 Planet::Jupiter => mu::JUPITER,
73 Planet::Saturn => mu::SATURN,
74 Planet::Uranus => mu::URANUS,
75 Planet::Neptune => mu::NEPTUNE,
76 }
77 }
78}
79
80#[derive(Debug, Clone, Copy)]
85pub struct MeanElements {
86 pub a0: f64,
88 pub a_dot: f64,
90 pub e0: f64,
92 pub e_dot: f64,
94 pub i0: f64,
96 pub i_dot: f64,
98 pub l0: f64,
100 pub l_dot: f64,
102 pub w_bar0: f64,
104 pub w_bar_dot: f64,
106 pub omega0: f64,
108 pub omega_dot: f64,
110}
111
112pub fn mean_elements(planet: Planet) -> MeanElements {
118 match planet {
119 Planet::Mercury => MeanElements {
120 a0: 0.387_098_31,
121 a_dot: 0.0,
122 e0: 0.205_630_69,
123 e_dot: 0.000_020_04,
124 i0: 7.004_86,
125 i_dot: -0.005_93,
126 l0: 252.250_84,
127 l_dot: 149_472.674_11,
128 w_bar0: 77.456_45,
129 w_bar_dot: 0.159_29,
130 omega0: 48.330_67,
131 omega_dot: -0.125_34,
132 },
133 Planet::Venus => MeanElements {
134 a0: 0.723_329_56,
135 a_dot: 0.0,
136 e0: 0.006_773_23,
137 e_dot: -0.000_047_64,
138 i0: 3.394_71,
139 i_dot: -0.008_67,
140 l0: 181.979_73,
141 l_dot: 58_517.815_39,
142 w_bar0: 131.563_70,
143 w_bar_dot: 0.002_68,
144 omega0: 76.679_92,
145 omega_dot: -0.278_01,
146 },
147 Planet::Earth => MeanElements {
148 a0: 1.000_002_61,
149 a_dot: 0.000_005_62,
150 e0: 0.016_708_57,
151 e_dot: -0.000_042_04,
152 i0: -0.000_15,
153 i_dot: -0.013_37,
154 l0: 100.464_57,
155 l_dot: 35_999.372_44,
156 w_bar0: 102.937_35,
157 w_bar_dot: 0.323_29,
158 omega0: 0.0,
159 omega_dot: 0.0,
160 },
161 Planet::Mars => MeanElements {
162 a0: 1.523_662_31,
163 a_dot: -0.000_073_28,
164 e0: 0.093_412_33,
165 e_dot: 0.000_090_48,
166 i0: 1.850_26,
167 i_dot: -0.006_75,
168 l0: -4.553_43,
169 l_dot: 19_140.299_34,
170 w_bar0: -23.943_62,
171 w_bar_dot: 0.445_41,
172 omega0: 49.558_09,
173 omega_dot: -0.291_08,
174 },
175 Planet::Jupiter => MeanElements {
176 a0: 5.202_603_91,
177 a_dot: 0.000_016_63,
178 e0: 0.048_497_64,
179 e_dot: 0.000_163_41,
180 i0: 1.303_30,
181 i_dot: -0.001_98,
182 l0: 34.396_44,
183 l_dot: 3_034.905_67,
184 w_bar0: 14.728_47,
185 w_bar_dot: 0.215_36,
186 omega0: 100.464_44,
187 omega_dot: 0.176_56,
188 },
189 Planet::Saturn => MeanElements {
190 a0: 9.554_909_16,
191 a_dot: -0.000_213_89,
192 e0: 0.055_508_62,
193 e_dot: -0.000_346_61,
194 i0: 2.488_68,
195 i_dot: 0.007_74,
196 l0: 49.954_24,
197 l_dot: 1_222.113_71,
198 w_bar0: 92.598_87,
199 w_bar_dot: -0.418_97,
200 omega0: 113.665_24,
201 omega_dot: -0.250_60,
202 },
203 Planet::Uranus => MeanElements {
204 a0: 19.218_446_10,
205 a_dot: -0.000_202_57,
206 e0: 0.046_295_11,
207 e_dot: -0.000_030_26,
208 i0: 0.773_20,
209 i_dot: 0.000_74,
210 l0: 313.238_18,
211 l_dot: 428.481_03,
212 w_bar0: 170.954_27,
213 w_bar_dot: 0.403_17,
214 omega0: 74.016_92,
215 omega_dot: 0.042_40,
216 },
217 Planet::Neptune => MeanElements {
218 a0: 30.110_386_88,
219 a_dot: 0.000_069_47,
220 e0: 0.008_989_22,
221 e_dot: 0.000_006_06,
222 i0: 1.769_17,
223 i_dot: -0.005_42,
224 l0: -55.120_02,
225 l_dot: 218.456_52,
226 w_bar0: 44.964_76,
227 w_bar_dot: -0.326_36,
228 omega0: 131.784_06,
229 omega_dot: -0.006_51,
230 },
231 }
232}
233
234pub fn calendar_to_jd(year: i32, month: u32, day: f64) -> f64 {
239 let (y, m) = if month <= 2 {
240 (year as f64 - 1.0, month as f64 + 12.0)
241 } else {
242 (year as f64, month as f64)
243 };
244
245 let a = (y / 100.0).floor();
246 let b = 2.0 - a + (a / 4.0).floor();
247
248 (365.25 * (y + 4716.0)).floor() + (30.6001 * (m + 1.0)).floor() + day + b - 1524.5
249}
250
251pub fn jd_to_calendar(jd: f64) -> (i32, u32, f64) {
253 let z = (jd + 0.5).floor();
254 let f = jd + 0.5 - z;
255
256 let a = if z < 2_299_161.0 {
257 z
258 } else {
259 let alpha = ((z - 1_867_216.25) / 36_524.25).floor();
260 z + 1.0 + alpha - (alpha / 4.0).floor()
261 };
262
263 let b = a + 1524.0;
264 let c = ((b - 122.1) / 365.25).floor();
265 let d = (365.25 * c).floor();
266 let e = ((b - d) / 30.6001).floor();
267
268 let day = b - d - (30.6001 * e).floor() + f;
269 let month = if e < 14.0 { e - 1.0 } else { e - 13.0 };
270 let year = if month > 2.0 { c - 4716.0 } else { c - 4715.0 };
271
272 (year as i32, month as u32, day)
273}
274
275#[derive(Debug, Clone, Copy)]
282pub struct PlanetPosition {
283 pub longitude: Radians,
285 pub latitude: Radians,
287 pub distance: Km,
289 pub x: f64,
291 pub y: f64,
293 pub z: f64,
295 pub inclination: Radians,
297}
298
299pub fn planet_position(planet: Planet, jd: f64) -> PlanetPosition {
305 let elem = mean_elements(planet);
306 let t = (jd - J2000_JD) / JULIAN_CENTURY_DAYS; let a_au = elem.a0 + elem.a_dot * t;
310 let e = elem.e0 + elem.e_dot * t;
311 let i_deg = elem.i0 + elem.i_dot * t;
312 let l_deg = elem.l0 + elem.l_dot * t;
313 let w_bar_deg = elem.w_bar0 + elem.w_bar_dot * t;
314 let omega_deg = elem.omega0 + elem.omega_dot * t;
315
316 let i_rad = i_deg.to_radians();
318 let omega_rad = omega_deg.to_radians();
319
320 let m_deg = l_deg - w_bar_deg;
322 let m_rad = Radians(m_deg.to_radians()).normalize();
323
324 let w_deg = w_bar_deg - omega_deg;
326 let w_rad = w_deg.to_radians();
327
328 let ecc = Eccentricity::elliptical(e.clamp(0.0, 0.999)).unwrap();
330 let true_anomaly = kepler::mean_to_true_anomaly(m_rad, ecc)
331 .expect("Kepler solver should converge for planetary eccentricities");
332
333 let a_km = a_au * AU_KM;
335 let r_km = a_km * (1.0 - e * e) / (1.0 + e * true_anomaly.cos());
336
337 let u = w_rad + true_anomaly.value();
339
340 let x_orb = r_km * u.cos();
342 let y_orb = r_km * u.sin();
343
344 let cos_omega = omega_rad.cos();
349 let sin_omega = omega_rad.sin();
350 let cos_i = i_rad.cos();
351 let sin_i = i_rad.sin();
352
353 let x = cos_omega * x_orb - sin_omega * cos_i * y_orb;
354 let y = sin_omega * x_orb + cos_omega * cos_i * y_orb;
355 let z = sin_i * y_orb;
356
357 let longitude = Radians(y.atan2(x)).normalize();
359 let latitude = Radians((z / r_km).clamp(-1.0, 1.0).asin());
360
361 PlanetPosition {
362 longitude,
363 latitude,
364 distance: Km(r_km),
365 x,
366 y,
367 z,
368 inclination: Radians(i_rad),
369 }
370}
371
372pub fn planet_longitude(planet: Planet, jd: f64) -> Radians {
376 planet_position(planet, jd).longitude
377}
378
379pub fn phase_angle(planet1: Planet, planet2: Planet, jd: f64) -> Radians {
384 let lon1 = planet_longitude(planet1, jd);
385 let lon2 = planet_longitude(planet2, jd);
386 (lon2 - lon1).normalize_signed()
387}
388
389pub fn synodic_period(planet1: Planet, planet2: Planet) -> Seconds {
395 let t1 = crate::orbits::orbital_period(mu::SUN, planet1.semi_major_axis());
396 let t2 = crate::orbits::orbital_period(mu::SUN, planet2.semi_major_axis());
397 let inv_diff = (1.0 / t1.value() - 1.0 / t2.value()).abs();
398 Seconds(1.0 / inv_diff)
399}
400
401pub fn hohmann_phase_angle(departure: Planet, arrival: Planet) -> Radians {
410 let r1 = departure.semi_major_axis().value();
411 let r2 = arrival.semi_major_axis().value();
412
413 let a_transfer = (r1 + r2) / 2.0;
415
416 let t_transfer = PI * (a_transfer.powi(3) / mu::SUN.value()).sqrt();
418
419 let n2 = kepler::mean_motion(mu::SUN, Km(r2));
421
422 let theta_travel = n2 * t_transfer;
424
425 Radians(PI - theta_travel).normalize()
427}
428
429pub fn hohmann_transfer_time(departure: Planet, arrival: Planet) -> Seconds {
433 let r1 = departure.semi_major_axis().value();
434 let r2 = arrival.semi_major_axis().value();
435 let a_transfer = (r1 + r2) / 2.0;
436 Seconds(PI * (a_transfer.powi(3) / mu::SUN.value()).sqrt())
437}
438
439pub fn next_hohmann_window(departure: Planet, arrival: Planet, after_jd: f64) -> Option<f64> {
447 let required = hohmann_phase_angle(departure, arrival);
448 let synodic = synodic_period(departure, arrival);
449
450 let search_days = synodic.value() / SECONDS_PER_DAY * 1.2;
452 let step = 0.1; let mut best_jd = after_jd;
455 let mut best_diff = f64::MAX;
456
457 let mut jd = after_jd;
458 while jd < after_jd + search_days {
459 let current_phase = phase_angle(departure, arrival, jd);
460 let diff = (current_phase - required).normalize_signed().value().abs();
461 if diff < best_diff {
462 best_diff = diff;
463 best_jd = jd;
464 }
465 jd += step;
466 }
467
468 let mut lo = best_jd - step;
470 let mut hi = best_jd + step;
471
472 for _ in 0..60 {
473 let mid = (lo + hi) / 2.0;
474 let phase_lo = phase_angle(departure, arrival, lo);
475 let phase_mid = phase_angle(departure, arrival, mid);
476
477 let diff_lo = (phase_lo - required).normalize_signed().value();
478 let diff_mid = (phase_mid - required).normalize_signed().value();
479
480 if diff_lo * diff_mid <= 0.0 {
481 hi = mid;
482 } else {
483 lo = mid;
484 }
485 }
486
487 let result_jd = (lo + hi) / 2.0;
488 let final_diff = (phase_angle(departure, arrival, result_jd) - required)
489 .normalize_signed()
490 .value()
491 .abs();
492
493 if final_diff < 0.1_f64.to_radians() {
495 Some(result_jd)
496 } else {
497 None
498 }
499}
500
501pub fn arrival_position(
507 arrival_planet: Planet,
508 departure_jd: f64,
509 transfer_time: Seconds,
510) -> PlanetPosition {
511 let arrival_jd = departure_jd + transfer_time.value() / SECONDS_PER_DAY;
512 planet_position(arrival_planet, arrival_jd)
513}
514
515pub fn jd_to_date_string(jd: f64) -> String {
517 let (year, month, day) = jd_to_calendar(jd);
518 format!("{:04}-{:02}-{:02}", year, month, day.floor() as u32)
519}
520
521pub fn elapsed_days(jd1: f64, jd2: f64) -> f64 {
523 jd2 - jd1
524}
525
526pub fn elapsed_hours(jd1: f64, jd2: f64) -> f64 {
528 (jd2 - jd1) * 24.0
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534 #[test]
535 fn test_calendar_to_jd_j2000() {
536 let jd = calendar_to_jd(2000, 1, 1.5);
538 assert!(
539 (jd - J2000_JD).abs() < 1e-6,
540 "J2000 JD = {}, expected {}",
541 jd,
542 J2000_JD
543 );
544 }
545
546 #[test]
547 fn test_calendar_to_jd_known_dates() {
548 let jd = calendar_to_jd(1999, 12, 31.0);
550 assert!(
551 (jd - 2_451_543.5).abs() < 1e-6,
552 "1999-12-31 JD = {}, expected 2451543.5",
553 jd
554 );
555
556 let jd = calendar_to_jd(1957, 10, 4.0);
558 assert!(
559 (jd - 2_436_115.5).abs() < 1e-4,
560 "Sputnik JD = {}, expected ~2436115.5",
561 jd
562 );
563 }
564
565 #[test]
566 fn test_jd_calendar_round_trip() {
567 let test_dates = [
568 (2000, 1, 1.5),
569 (2024, 6, 15.0),
570 (1990, 3, 21.75),
571 (2100, 12, 31.25),
572 ];
573 for (y, m, d) in test_dates {
574 let jd = calendar_to_jd(y, m, d);
575 let (y2, m2, d2) = jd_to_calendar(jd);
576 assert_eq!(y, y2, "year mismatch for {:04}-{:02}-{}", y, m, d);
577 assert_eq!(m, m2, "month mismatch for {:04}-{:02}-{}", y, m, d);
578 assert!(
579 (d - d2).abs() < 1e-10,
580 "day mismatch for {:04}-{:02}-{}: got {}",
581 y,
582 m,
583 d,
584 d2
585 );
586 }
587 }
588
589 #[test]
590 fn test_planet_position_earth_j2000() {
591 let pos = planet_position(Planet::Earth, J2000_JD);
593
594 let dist_au = pos.distance.value() / AU_KM;
596 assert!(
597 (dist_au - 1.0).abs() < 0.02,
598 "Earth distance at J2000 = {} AU, expected ~1.0",
599 dist_au
600 );
601 }
602
603 #[test]
604 fn test_planet_position_mars_distance() {
605 let pos = planet_position(Planet::Mars, J2000_JD);
607 let dist_au = pos.distance.value() / AU_KM;
608 assert!(
609 (dist_au - 1.524).abs() < 0.15,
610 "Mars distance at J2000 = {} AU, expected ~1.524",
611 dist_au
612 );
613 }
614
615 #[test]
616 fn test_planet_position_jupiter_distance() {
617 let pos = planet_position(Planet::Jupiter, J2000_JD);
619 let dist_au = pos.distance.value() / AU_KM;
620 assert!(
621 (dist_au - 5.2).abs() < 0.3,
622 "Jupiter distance at J2000 = {} AU, expected ~5.2",
623 dist_au
624 );
625 }
626
627 #[test]
628 fn test_planet_position_saturn_distance() {
629 let pos = planet_position(Planet::Saturn, J2000_JD);
630 let dist_au = pos.distance.value() / AU_KM;
631 assert!(
632 (dist_au - 9.54).abs() < 0.5,
633 "Saturn distance at J2000 = {} AU, expected ~9.54",
634 dist_au
635 );
636 }
637
638 #[test]
639 fn test_planet_position_uranus_distance() {
640 let pos = planet_position(Planet::Uranus, J2000_JD);
641 let dist_au = pos.distance.value() / AU_KM;
642 assert!(
643 (dist_au - 19.2).abs() < 1.0,
644 "Uranus distance at J2000 = {} AU, expected ~19.2",
645 dist_au
646 );
647 }
648
649 #[test]
650 fn test_planet_orbital_period_consistency() {
651 let jd0 = J2000_JD;
654 let jd1 = jd0 + 365.25;
655 let lon0 = planet_longitude(Planet::Earth, jd0);
656 let lon1 = planet_longitude(Planet::Earth, jd1);
657
658 let diff = (lon1 - lon0).normalize_signed().value().abs();
660 assert!(
661 diff < 2.0_f64.to_radians(),
662 "Earth longitude after 1 year differs by {:.1}°, expected ~0°",
663 diff.to_degrees()
664 );
665 }
666
667 #[test]
668 fn test_mars_orbital_period() {
669 let jd0 = J2000_JD;
671 let jd1 = jd0 + 686.97;
672 let lon0 = planet_longitude(Planet::Mars, jd0);
673 let lon1 = planet_longitude(Planet::Mars, jd1);
674
675 let diff = (lon1 - lon0).normalize_signed().value().abs();
676 assert!(
677 diff < 3.0_f64.to_radians(),
678 "Mars longitude after 687 days differs by {:.1}°, expected ~0°",
679 diff.to_degrees()
680 );
681 }
682
683 #[test]
684 fn test_jupiter_orbital_period() {
685 let jd0 = J2000_JD;
687 let jd1 = jd0 + 4332.59;
688 let lon0 = planet_longitude(Planet::Jupiter, jd0);
689 let lon1 = planet_longitude(Planet::Jupiter, jd1);
690
691 let diff = (lon1 - lon0).normalize_signed().value().abs();
692 assert!(
693 diff < 5.0_f64.to_radians(),
694 "Jupiter longitude after 4333 days differs by {:.1}°, expected ~0°",
695 diff.to_degrees()
696 );
697 }
698
699 #[test]
700 fn test_phase_angle_symmetry() {
701 let jd = J2000_JD;
703 let ab = phase_angle(Planet::Earth, Planet::Mars, jd);
704 let ba = phase_angle(Planet::Mars, Planet::Earth, jd);
705
706 assert!(
707 (ab.value() + ba.value()).abs() < 1e-10,
708 "phase_angle symmetry: {} + {} ≠ 0",
709 ab.value(),
710 ba.value()
711 );
712 }
713
714 #[test]
715 fn test_synodic_period_earth_mars() {
716 let synodic = synodic_period(Planet::Earth, Planet::Mars);
718 let days = synodic.value() / SECONDS_PER_DAY;
719 assert!(
720 (days - 780.0).abs() < 10.0,
721 "Earth-Mars synodic period = {:.1} days, expected ~780",
722 days
723 );
724 }
725
726 #[test]
727 fn test_synodic_period_earth_jupiter() {
728 let synodic = synodic_period(Planet::Earth, Planet::Jupiter);
730 let days = synodic.value() / SECONDS_PER_DAY;
731 assert!(
732 (days - 398.88).abs() < 5.0,
733 "Earth-Jupiter synodic period = {:.1} days, expected ~398.88",
734 days
735 );
736 }
737
738 #[test]
739 fn test_hohmann_phase_angle_earth_mars() {
740 let phase = hohmann_phase_angle(Planet::Earth, Planet::Mars);
742 let deg = phase.value().to_degrees();
743 assert!(
744 (deg - 44.4).abs() < 2.0,
745 "Earth→Mars Hohmann phase angle = {:.1}°, expected ~44.4°",
746 deg
747 );
748 }
749
750 #[test]
751 fn test_hohmann_transfer_time_earth_mars() {
752 let time = hohmann_transfer_time(Planet::Earth, Planet::Mars);
754 let days = time.value() / SECONDS_PER_DAY;
755 assert!(
756 (days - 259.0).abs() < 5.0,
757 "Earth→Mars Hohmann time = {:.0} days, expected ~259",
758 days
759 );
760 }
761
762 #[test]
763 fn test_hohmann_transfer_time_earth_jupiter() {
764 let time = hohmann_transfer_time(Planet::Earth, Planet::Jupiter);
766 let days = time.value() / SECONDS_PER_DAY;
767 assert!(
768 (days - 997.0).abs() < 20.0,
769 "Earth→Jupiter Hohmann time = {:.0} days, expected ~997",
770 days
771 );
772 }
773
774 #[test]
775 fn test_next_hohmann_window_finds_window() {
776 let window = next_hohmann_window(Planet::Earth, Planet::Mars, J2000_JD);
778 assert!(
779 window.is_some(),
780 "Should find Earth→Mars Hohmann window near J2000"
781 );
782
783 let jd = window.unwrap();
784 let synodic_days = synodic_period(Planet::Earth, Planet::Mars).value() / SECONDS_PER_DAY;
786 assert!(
787 jd - J2000_JD < synodic_days * 1.2,
788 "Window at JD {} is too far from J2000 ({:.0} days, synodic = {:.0})",
789 jd,
790 jd - J2000_JD,
791 synodic_days
792 );
793 }
794
795 #[test]
796 fn test_jd_to_date_string() {
797 let s = jd_to_date_string(J2000_JD);
799 assert_eq!(s, "2000-01-01", "J2000 date string");
800
801 let jd = calendar_to_jd(2024, 1, 1.0);
803 let s = jd_to_date_string(jd);
804 assert_eq!(s, "2024-01-01");
805 }
806
807 #[test]
808 fn test_elapsed_time() {
809 let jd1 = J2000_JD;
810 let jd2 = J2000_JD + 30.0;
811 assert!((elapsed_days(jd1, jd2) - 30.0).abs() < 1e-10);
812 assert!((elapsed_hours(jd1, jd2) - 720.0).abs() < 1e-8);
813 }
814
815 #[test]
816 fn test_planet_all() {
817 for planet in &Planet::ALL {
819 let pos = planet_position(*planet, J2000_JD);
820 assert!(pos.distance.value() > 0.0, "{:?} has zero distance", planet);
821 assert!(
822 pos.distance.value().is_finite(),
823 "{:?} distance is not finite",
824 planet
825 );
826 }
827 }
828
829 #[test]
830 fn test_planet_distance_ordering_at_j2000() {
831 let planets = [
834 Planet::Mercury,
835 Planet::Venus,
836 Planet::Earth,
837 Planet::Mars,
838 Planet::Jupiter,
839 Planet::Saturn,
840 Planet::Uranus,
841 Planet::Neptune,
842 ];
843
844 for i in 0..planets.len() - 1 {
845 let a1 = planets[i].semi_major_axis().value();
846 let a2 = planets[i + 1].semi_major_axis().value();
847 assert!(
848 a1 < a2,
849 "{:?} semi-major axis ({}) >= {:?} semi-major axis ({})",
850 planets[i],
851 a1,
852 planets[i + 1],
853 a2,
854 );
855 }
856 }
857
858 #[test]
859 fn test_hohmann_phase_angle_mars_jupiter() {
860 let phase = hohmann_phase_angle(Planet::Mars, Planet::Jupiter);
862 let deg = phase.value().to_degrees();
863 assert!(
865 deg > 0.0 && deg < 360.0,
866 "Mars→Jupiter phase angle = {:.1}°, should be in (0, 360)",
867 deg
868 );
869 }
870
871 #[test]
872 fn test_synodic_period_symmetry() {
873 let ab = synodic_period(Planet::Earth, Planet::Mars);
875 let ba = synodic_period(Planet::Mars, Planet::Earth);
876 assert!(
877 (ab.value() - ba.value()).abs() < 1e-6,
878 "synodic period asymmetry: {} vs {}",
879 ab.value(),
880 ba.value()
881 );
882 }
883
884 #[test]
885 fn test_mean_elements_earth() {
886 let e = mean_elements(Planet::Earth);
887 assert!(
889 (e.a0 - 1.0).abs() < 0.01,
890 "Earth a0 = {}, expected ~1.0 AU",
891 e.a0
892 );
893 assert!(
895 (e.e0 - 0.0167).abs() < 0.01,
896 "Earth e0 = {}, expected ~0.0167",
897 e.e0
898 );
899 assert!(e.i0.abs() < 0.01, "Earth i0 = {}, expected ~0°", e.i0);
901 }
902
903 #[test]
904 fn test_mean_elements_all_planets_sane() {
905 for planet in Planet::ALL {
906 let e = mean_elements(planet);
907 assert!(
909 e.a0 > 0.0,
910 "{:?}: a0 must be positive, got {}",
911 planet,
912 e.a0
913 );
914 assert!(
916 e.e0 >= 0.0 && e.e0 < 1.0,
917 "{:?}: eccentricity must be in [0,1), got {}",
918 planet,
919 e.e0
920 );
921 assert!(
923 e.i0.abs() < 180.0,
924 "{:?}: inclination out of range: {}",
925 planet,
926 e.i0
927 );
928 assert!(
930 e.l_dot > 0.0,
931 "{:?}: mean longitude rate should be positive, got {}",
932 planet,
933 e.l_dot
934 );
935 }
936 }
937
938 #[test]
939 fn test_mean_elements_ordering() {
940 let mercury = mean_elements(Planet::Mercury);
942 let venus = mean_elements(Planet::Venus);
943 let earth = mean_elements(Planet::Earth);
944 let mars = mean_elements(Planet::Mars);
945 let jupiter = mean_elements(Planet::Jupiter);
946 let saturn = mean_elements(Planet::Saturn);
947 let uranus = mean_elements(Planet::Uranus);
948 let neptune = mean_elements(Planet::Neptune);
949
950 assert!(mercury.a0 < venus.a0);
951 assert!(venus.a0 < earth.a0);
952 assert!(earth.a0 < mars.a0);
953 assert!(mars.a0 < jupiter.a0);
954 assert!(jupiter.a0 < saturn.a0);
955 assert!(saturn.a0 < uranus.a0);
956 assert!(uranus.a0 < neptune.a0);
957 }
958
959 #[test]
960 fn test_mean_elements_known_values() {
961 let mars = mean_elements(Planet::Mars);
963 assert!(
964 (mars.a0 - 1.524).abs() < 0.01,
965 "Mars a0 = {}, expected ~1.524",
966 mars.a0
967 );
968 let jup = mean_elements(Planet::Jupiter);
970 assert!(
971 (jup.a0 - 5.203).abs() < 0.01,
972 "Jupiter a0 = {}, expected ~5.203",
973 jup.a0
974 );
975 let nep = mean_elements(Planet::Neptune);
977 assert!(
978 (nep.a0 - 30.07).abs() < 0.1,
979 "Neptune a0 = {}, expected ~30.07",
980 nep.a0
981 );
982 }
983
984 #[test]
985 fn test_arrival_position_mars_after_hohmann() {
986 let jd_j2000 = 2_451_545.0;
988 let transfer_time = Seconds(259.0 * 86400.0);
989 let pos = arrival_position(Planet::Mars, jd_j2000, transfer_time);
990
991 let dist_km = (pos.x * pos.x + pos.y * pos.y).sqrt();
993 let dist_au = dist_km / AU_KM;
994 assert!(
995 dist_au > 1.3 && dist_au < 1.7,
996 "Mars distance should be 1.3-1.7 AU, got {} AU",
997 dist_au
998 );
999 }
1000
1001 #[test]
1002 fn test_arrival_position_equals_planet_position_at_arrival_jd() {
1003 let jd = 2_460_000.0;
1005 let transfer = Seconds(100.0 * 86400.0);
1006 let arrival_jd = jd + 100.0;
1007
1008 let pos_via_arrival = arrival_position(Planet::Jupiter, jd, transfer);
1009 let pos_via_direct = planet_position(Planet::Jupiter, arrival_jd);
1010
1011 assert!(
1012 (pos_via_arrival.x - pos_via_direct.x).abs() < 1e-10,
1013 "x mismatch: {} vs {}",
1014 pos_via_arrival.x,
1015 pos_via_direct.x
1016 );
1017 assert!(
1018 (pos_via_arrival.y - pos_via_direct.y).abs() < 1e-10,
1019 "y mismatch: {} vs {}",
1020 pos_via_arrival.y,
1021 pos_via_direct.y
1022 );
1023 }
1024
1025 #[test]
1026 fn test_jd_calendar_round_trip_solar_line_epoch() {
1027 let (year, month, day) = (2215, 1, 15.5);
1030 let jd = calendar_to_jd(year, month, day);
1031 let (y2, m2, d2) = jd_to_calendar(jd);
1032 assert_eq!(y2, year, "year mismatch");
1033 assert_eq!(m2, month, "month mismatch");
1034 assert!((d2 - day).abs() < 1e-6, "day mismatch: {d2} vs {day}");
1035
1036 let j2000 = calendar_to_jd(2000, 1, 1.5);
1038 assert!(
1039 (j2000 - J2000_JD).abs() < 1e-6,
1040 "J2000 mismatch: {j2000} vs {J2000_JD}"
1041 );
1042 }
1043
1044 #[test]
1045 fn test_synodic_periods_known_values() {
1046 let synodic = synodic_period(Planet::Earth, Planet::Mars);
1048 let days = synodic.value() / 86400.0;
1049 assert!(
1050 (days - 780.0).abs() < 10.0,
1051 "Earth-Mars synodic period: {days:.1} days (expected ~780)"
1052 );
1053
1054 let synodic_j = synodic_period(Planet::Earth, Planet::Jupiter);
1056 let days_j = synodic_j.value() / 86400.0;
1057 assert!(
1058 (days_j - 399.0).abs() < 5.0,
1059 "Earth-Jupiter synodic period: {days_j:.1} days (expected ~399)"
1060 );
1061 }
1062}