1use crate::bodies::Earth;
13use crate::constants::Constants;
14use crate::math::{Matrix3, Vector3};
15use chrono::{DateTime, Utc};
16use std::f64::consts::PI;
17
18#[derive(Debug, Clone)]
32pub struct ECIState {
33 pub epoch: DateTime<Utc>,
35 pub position: Vector3,
37 pub velocity: Vector3,
39}
40
41impl ECIState {
42 pub fn new(epoch: DateTime<Utc>, position: Vector3, velocity: Vector3) -> Self {
44 Self {
45 epoch,
46 position,
47 velocity,
48 }
49 }
50
51 pub fn from_tuples(
53 epoch: DateTime<Utc>,
54 position: (f64, f64, f64),
55 velocity: (f64, f64, f64),
56 ) -> Self {
57 Self::new(
58 epoch,
59 Vector3::from_tuple(position),
60 Vector3::from_tuple(velocity),
61 )
62 }
63
64 pub fn radius(&self) -> f64 {
66 self.position.magnitude()
67 }
68
69 pub fn speed(&self) -> f64 {
71 self.velocity.magnitude()
72 }
73
74 pub fn angular_momentum(&self) -> Vector3 {
76 self.position.cross(&self.velocity)
77 }
78
79 pub fn specific_energy(&self) -> f64 {
81 let v_sq = self.velocity.magnitude_squared();
82 let r = self.radius();
83 v_sq / 2.0 - Constants::EARTH_MU_KM / r
84 }
85
86 pub fn to_teme(&self) -> TEMEState {
88 StateTransforms::eci_to_teme(self)
89 }
90
91 pub fn to_ecef(&self) -> ECEFState {
93 StateTransforms::eci_to_ecef(self)
94 }
95
96 pub fn to_keplerian(&self) -> KeplerianElements {
98 StateTransforms::cartesian_to_keplerian(self)
99 }
100}
101
102#[derive(Debug, Clone)]
111pub struct TEMEState {
112 pub epoch: DateTime<Utc>,
114 pub position: Vector3,
116 pub velocity: Vector3,
118}
119
120impl TEMEState {
121 pub fn new(epoch: DateTime<Utc>, position: Vector3, velocity: Vector3) -> Self {
123 Self {
124 epoch,
125 position,
126 velocity,
127 }
128 }
129
130 pub fn from_tuples(
132 epoch: DateTime<Utc>,
133 position: (f64, f64, f64),
134 velocity: (f64, f64, f64),
135 ) -> Self {
136 Self::new(
137 epoch,
138 Vector3::from_tuple(position),
139 Vector3::from_tuple(velocity),
140 )
141 }
142
143 pub fn radius(&self) -> f64 {
145 self.position.magnitude()
146 }
147
148 pub fn speed(&self) -> f64 {
150 self.velocity.magnitude()
151 }
152
153 pub fn to_eci(&self) -> ECIState {
155 StateTransforms::teme_to_eci(self)
156 }
157
158 pub fn to_ecef(&self) -> ECEFState {
160 StateTransforms::teme_to_ecef(self)
161 }
162}
163
164#[derive(Debug, Clone)]
179pub struct ECEFState {
180 pub epoch: DateTime<Utc>,
182 pub position: Vector3,
184 pub velocity: Vector3,
186}
187
188impl ECEFState {
189 pub fn new(epoch: DateTime<Utc>, position: Vector3, velocity: Vector3) -> Self {
191 Self {
192 epoch,
193 position,
194 velocity,
195 }
196 }
197
198 pub fn from_tuples(
200 epoch: DateTime<Utc>,
201 position: (f64, f64, f64),
202 velocity: (f64, f64, f64),
203 ) -> Self {
204 Self::new(
205 epoch,
206 Vector3::from_tuple(position),
207 Vector3::from_tuple(velocity),
208 )
209 }
210
211 pub fn radius(&self) -> f64 {
213 self.position.magnitude()
214 }
215
216 pub fn to_eci(&self) -> ECIState {
218 StateTransforms::ecef_to_eci(self)
219 }
220
221 pub fn to_geodetic(&self) -> GeodeticState {
223 GeodeticState::from_ecef(self)
224 }
225}
226
227#[derive(Debug, Clone, Copy)]
240pub struct GeodeticState {
241 pub latitude: f64,
243 pub longitude: f64,
245 pub altitude: f64,
247}
248
249impl GeodeticState {
250 pub fn new(latitude: f64, longitude: f64, altitude: f64) -> Self {
257 Self {
258 latitude,
259 longitude,
260 altitude,
261 }
262 }
263
264 pub fn latitude_rad(&self) -> f64 {
266 self.latitude * Constants::DEG_TO_RAD
267 }
268
269 pub fn longitude_rad(&self) -> f64 {
271 self.longitude * Constants::DEG_TO_RAD
272 }
273
274 pub fn to_ecef(&self, epoch: DateTime<Utc>) -> ECEFState {
278 let lat_rad = self.latitude_rad();
279 let lon_rad = self.longitude_rad();
280
281 let sin_lat = lat_rad.sin();
282 let cos_lat = lat_rad.cos();
283 let sin_lon = lon_rad.sin();
284 let cos_lon = lon_rad.cos();
285
286 let n =
288 Constants::EARTH_RADIUS_EQ_KM / (1.0 - Constants::EARTH_E2 * sin_lat * sin_lat).sqrt();
289
290 let x = (n + self.altitude) * cos_lat * cos_lon;
292 let y = (n + self.altitude) * cos_lat * sin_lon;
293 let z = (n * (1.0 - Constants::EARTH_E2) + self.altitude) * sin_lat;
294
295 ECEFState::new(epoch, Vector3::new(x, y, z), Vector3::zero())
297 }
298
299 pub fn to_eci(&self, epoch: DateTime<Utc>) -> ECIState {
301 let ecef = self.to_ecef(epoch);
302 ecef.to_eci()
303 }
304
305 pub fn from_ecef(ecef: &ECEFState) -> Self {
309 let x = ecef.position.x;
310 let y = ecef.position.y;
311 let z = ecef.position.z;
312
313 let lon = y.atan2(x) * Constants::RAD_TO_DEG;
315
316 let p = (x * x + y * y).sqrt();
318 let mut lat = (z / (p * (1.0 - Constants::EARTH_E2))).atan();
319
320 for _ in 0..10 {
322 let sin_lat = lat.sin();
323 let n = Constants::EARTH_RADIUS_EQ_KM
324 / (1.0 - Constants::EARTH_E2 * sin_lat * sin_lat).sqrt();
325 let new_lat = (z + Constants::EARTH_E2 * n * sin_lat).atan2(p);
326
327 if (new_lat - lat).abs() < 1e-12 {
328 break;
329 }
330 lat = new_lat;
331 }
332
333 let sin_lat = lat.sin();
335 let cos_lat = lat.cos();
336 let n =
337 Constants::EARTH_RADIUS_EQ_KM / (1.0 - Constants::EARTH_E2 * sin_lat * sin_lat).sqrt();
338
339 let alt = if cos_lat.abs() > 1e-10 {
340 p / cos_lat - n
341 } else {
342 z.abs() / sin_lat.abs() - n * (1.0 - Constants::EARTH_E2)
343 };
344
345 Self::new(lat * Constants::RAD_TO_DEG, lon, alt)
346 }
347
348 pub fn local_sidereal_time(&self, epoch: &DateTime<Utc>) -> f64 {
352 let nutation = Earth::nutation_angles(epoch);
353 let gast = nutation.gast;
354 let lon_rad = self.longitude_rad();
355
356 let lst = gast + lon_rad;
358
359 let mut normalized = lst % Constants::TWO_PI;
361 if normalized < 0.0 {
362 normalized += Constants::TWO_PI;
363 }
364 normalized
365 }
366}
367
368impl Default for GeodeticState {
369 fn default() -> Self {
370 Self::new(0.0, 0.0, 0.0)
371 }
372}
373
374#[derive(Debug, Clone, Copy)]
388pub struct KeplerianElements {
389 pub epoch: Option<DateTime<Utc>>,
391 pub semi_major_axis: f64,
393 pub eccentricity: f64,
395 pub inclination: f64,
397 pub raan: f64,
399 pub arg_perigee: f64,
401 pub true_anomaly: f64,
403 pub mu: f64,
405}
406
407impl KeplerianElements {
408 #[allow(clippy::too_many_arguments)]
418 pub fn new(
419 semi_major_axis: f64,
420 eccentricity: f64,
421 inclination: f64,
422 raan: f64,
423 arg_perigee: f64,
424 true_anomaly: f64,
425 ) -> Self {
426 Self {
427 epoch: None,
428 semi_major_axis,
429 eccentricity,
430 inclination,
431 raan,
432 arg_perigee,
433 true_anomaly,
434 mu: Constants::EARTH_MU_KM,
435 }
436 }
437
438 pub fn with_epoch(mut self, epoch: DateTime<Utc>) -> Self {
440 self.epoch = Some(epoch);
441 self
442 }
443
444 pub fn with_mu(mut self, mu: f64) -> Self {
446 self.mu = mu;
447 self
448 }
449
450 pub fn period(&self) -> f64 {
452 2.0 * PI * (self.semi_major_axis.powi(3) / self.mu).sqrt()
453 }
454
455 pub fn mean_motion(&self) -> f64 {
457 (self.mu / self.semi_major_axis.powi(3)).sqrt()
458 }
459
460 pub fn apoapsis(&self) -> f64 {
462 self.semi_major_axis * (1.0 + self.eccentricity)
463 }
464
465 pub fn periapsis(&self) -> f64 {
467 self.semi_major_axis * (1.0 - self.eccentricity)
468 }
469
470 pub fn apoapsis_altitude(&self) -> f64 {
472 self.apoapsis() - Constants::EARTH_RADIUS_EQ_KM
473 }
474
475 pub fn periapsis_altitude(&self) -> f64 {
477 self.periapsis() - Constants::EARTH_RADIUS_EQ_KM
478 }
479
480 pub fn specific_energy(&self) -> f64 {
482 -self.mu / (2.0 * self.semi_major_axis)
483 }
484
485 pub fn angular_momentum(&self) -> f64 {
487 (self.mu * self.semi_major_axis * (1.0 - self.eccentricity * self.eccentricity)).sqrt()
488 }
489
490 pub fn semi_latus_rectum(&self) -> f64 {
492 self.semi_major_axis * (1.0 - self.eccentricity * self.eccentricity)
493 }
494
495 pub fn eccentric_anomaly(&self) -> f64 {
497 let e = self.eccentricity;
498 let nu = self.true_anomaly;
499 ((1.0 - e * e).sqrt() * nu.sin()).atan2(e + nu.cos())
500 }
501
502 pub fn mean_anomaly(&self) -> f64 {
504 let ea = self.eccentric_anomaly();
505 ea - self.eccentricity * ea.sin()
506 }
507
508 pub fn inclination_deg(&self) -> f64 {
510 self.inclination * Constants::RAD_TO_DEG
511 }
512
513 pub fn raan_deg(&self) -> f64 {
515 self.raan * Constants::RAD_TO_DEG
516 }
517
518 pub fn arg_perigee_deg(&self) -> f64 {
520 self.arg_perigee * Constants::RAD_TO_DEG
521 }
522
523 pub fn true_anomaly_deg(&self) -> f64 {
525 self.true_anomaly * Constants::RAD_TO_DEG
526 }
527
528 pub fn to_eci(&self) -> ECIState {
530 StateTransforms::keplerian_to_cartesian(self)
531 }
532}
533
534pub struct StateTransforms;
543
544impl StateTransforms {
545 pub fn teme_to_eci(teme: &TEMEState) -> ECIState {
554 let precession = Earth::precession_angles(&teme.epoch);
555 let nutation = Earth::nutation_angles(&teme.epoch);
556
557 let eq_mat = Self::equinox_matrix(nutation.d_psi, nutation.eps);
559 let nut_mat = Self::nutation_matrix(nutation.m_eps, nutation.d_psi, nutation.d_eps);
560 let prec_mat = Self::precession_matrix(precession.zeta, precession.theta, precession.z);
561
562 let combined = prec_mat.mul_mat(&nut_mat.mul_mat(&eq_mat));
564
565 let pos_eci = combined.mul_vec(&teme.position);
567 let vel_eci = combined.mul_vec(&teme.velocity);
568
569 ECIState::new(teme.epoch, pos_eci, vel_eci)
570 }
571
572 pub fn eci_to_teme(eci: &ECIState) -> TEMEState {
574 let precession = Earth::precession_angles(&eci.epoch);
575 let nutation = Earth::nutation_angles(&eci.epoch);
576
577 let eq_mat = Self::equinox_matrix(nutation.d_psi, nutation.eps).transpose();
579 let nut_mat =
580 Self::nutation_matrix(nutation.m_eps, nutation.d_psi, nutation.d_eps).transpose();
581 let prec_mat =
582 Self::precession_matrix(precession.zeta, precession.theta, precession.z).transpose();
583
584 let combined = eq_mat.mul_mat(&nut_mat.mul_mat(&prec_mat));
586
587 let pos_teme = combined.mul_vec(&eci.position);
589 let vel_teme = combined.mul_vec(&eci.velocity);
590
591 TEMEState::new(eci.epoch, pos_teme, vel_teme)
592 }
593
594 pub fn eci_to_ecef(eci: &ECIState) -> ECEFState {
600 let nutation = Earth::nutation_angles(&eci.epoch);
601 let gast = nutation.gast;
602
603 let rot = Matrix3::rotation_z(gast);
605
606 let pos_ecef = rot.mul_vec(&eci.position);
608
609 let vel_rotated = rot.mul_vec(&eci.velocity);
612
613 let omega = Constants::EARTH_ROTATION_RATE;
614 let omega_cross_r = Vector3::new(-omega * pos_ecef.y, omega * pos_ecef.x, 0.0);
615
616 let vel_ecef = vel_rotated - omega_cross_r;
617
618 ECEFState::new(eci.epoch, pos_ecef, vel_ecef)
619 }
620
621 pub fn ecef_to_eci(ecef: &ECEFState) -> ECIState {
623 let nutation = Earth::nutation_angles(&ecef.epoch);
624 let gast = nutation.gast;
625
626 let rot_inv = Matrix3::rotation_z(-gast);
628
629 let omega = Constants::EARTH_ROTATION_RATE;
630
631 let omega_cross_r = Vector3::new(-omega * ecef.position.y, omega * ecef.position.x, 0.0);
634 let vel_corrected = ecef.velocity + omega_cross_r;
635
636 let pos_eci = rot_inv.mul_vec(&ecef.position);
638 let vel_eci = rot_inv.mul_vec(&vel_corrected);
639
640 ECIState::new(ecef.epoch, pos_eci, vel_eci)
641 }
642
643 pub fn teme_to_ecef(teme: &TEMEState) -> ECEFState {
649 let eci = Self::teme_to_eci(teme);
650 Self::eci_to_ecef(&eci)
651 }
652
653 pub fn ecef_to_teme(ecef: &ECEFState) -> TEMEState {
655 let eci = Self::ecef_to_eci(ecef);
656 Self::eci_to_teme(&eci)
657 }
658
659 pub fn cartesian_to_keplerian(state: &ECIState) -> KeplerianElements {
665 let mu = Constants::EARTH_MU_KM;
666 let pos = &state.position;
667 let vel = &state.velocity;
668
669 let r = pos.magnitude();
670 let v = vel.magnitude();
671
672 let h = pos.cross(vel);
674 let h_mag = h.magnitude();
675
676 let n = Vector3::new(-h.y, h.x, 0.0);
678 let n_mag = n.magnitude();
679
680 let r_dot_v = pos.dot(vel);
682 let e_vec = (*pos * (v * v - mu / r) - *vel * r_dot_v) / mu;
683 let e = e_vec.magnitude();
684
685 let energy = v * v / 2.0 - mu / r;
687 let a = -mu / (2.0 * energy);
688
689 let i = (h.z / h_mag).clamp(-1.0, 1.0).acos();
691
692 let raan = if n_mag > 1e-10 {
694 let raan_cos = (n.x / n_mag).clamp(-1.0, 1.0);
695 if n.y >= 0.0 {
696 raan_cos.acos()
697 } else {
698 Constants::TWO_PI - raan_cos.acos()
699 }
700 } else {
701 0.0
702 };
703
704 let w = if n_mag > 1e-10 && e > 1e-10 {
706 let n_dot_e = n.dot(&e_vec);
707 let w_cos = (n_dot_e / (n_mag * e)).clamp(-1.0, 1.0);
708 if e_vec.z >= 0.0 {
709 w_cos.acos()
710 } else {
711 Constants::TWO_PI - w_cos.acos()
712 }
713 } else {
714 0.0
715 };
716
717 let nu = if e > 1e-10 {
719 let e_dot_r = e_vec.dot(pos);
720 let nu_cos = (e_dot_r / (e * r)).clamp(-1.0, 1.0);
721 if r_dot_v >= 0.0 {
722 nu_cos.acos()
723 } else {
724 Constants::TWO_PI - nu_cos.acos()
725 }
726 } else {
727 0.0
728 };
729
730 KeplerianElements::new(a, e, i, raan, w, nu)
731 .with_epoch(state.epoch)
732 .with_mu(mu)
733 }
734
735 pub fn keplerian_to_cartesian(elements: &KeplerianElements) -> ECIState {
737 let a = elements.semi_major_axis;
738 let e = elements.eccentricity;
739 let i = elements.inclination;
740 let raan = elements.raan;
741 let w = elements.arg_perigee;
742 let nu = elements.true_anomaly;
743 let mu = elements.mu;
744
745 let p = a * (1.0 - e * e);
747
748 let cos_nu = nu.cos();
750 let sin_nu = nu.sin();
751
752 let r_mag = p / (1.0 + e * cos_nu);
753
754 let r_pf = Vector3::new(r_mag * cos_nu, r_mag * sin_nu, 0.0);
756
757 let sqrt_mu_p = (mu / p).sqrt();
759 let v_pf = Vector3::new(-sqrt_mu_p * sin_nu, sqrt_mu_p * (e + cos_nu), 0.0);
760
761 let cos_raan = raan.cos();
763 let sin_raan = raan.sin();
764 let cos_w = w.cos();
765 let sin_w = w.sin();
766 let cos_i = i.cos();
767 let sin_i = i.sin();
768
769 let r11 = cos_raan * cos_w - sin_raan * sin_w * cos_i;
771 let r12 = -cos_raan * sin_w - sin_raan * cos_w * cos_i;
772 let r13 = sin_raan * sin_i;
773 let r21 = sin_raan * cos_w + cos_raan * sin_w * cos_i;
774 let r22 = -sin_raan * sin_w + cos_raan * cos_w * cos_i;
775 let r23 = -cos_raan * sin_i;
776 let r31 = sin_w * sin_i;
777 let r32 = cos_w * sin_i;
778 let r33 = cos_i;
779
780 let rot = Matrix3::new(r11, r12, r13, r21, r22, r23, r31, r32, r33);
781
782 let pos = rot.mul_vec(&r_pf);
783 let vel = rot.mul_vec(&v_pf);
784
785 let epoch = elements.epoch.unwrap_or_else(Utc::now);
786 ECIState::new(epoch, pos, vel)
787 }
788
789 fn precession_matrix(zeta: f64, theta: f64, z: f64) -> Matrix3 {
795 let rz_neg_zeta = Matrix3::rotation_z(-zeta);
797 let ry_theta = Matrix3::rotation_y(theta);
798 let rz_neg_z = Matrix3::rotation_z(-z);
799
800 rz_neg_z.mul_mat(&ry_theta.mul_mat(&rz_neg_zeta))
801 }
802
803 fn nutation_matrix(mean_eps: f64, delta_psi: f64, delta_eps: f64) -> Matrix3 {
805 let eps = mean_eps + delta_eps;
806
807 let rx_mean_eps = Matrix3::rotation_x(mean_eps);
809 let rz_delta_psi = Matrix3::rotation_z(delta_psi);
810 let rx_neg_eps = Matrix3::rotation_x(-eps);
811
812 rx_mean_eps.mul_mat(&rz_delta_psi.mul_mat(&rx_neg_eps))
813 }
814
815 fn equinox_matrix(delta_psi: f64, eps: f64) -> Matrix3 {
817 let eq_eq = delta_psi * eps.cos();
819 Matrix3::rotation_z(-eq_eq)
820 }
821}
822
823#[cfg(test)]
824mod tests {
825 use super::*;
826 use chrono::TimeZone;
827
828 const EPSILON: f64 = 1e-6;
829
830 fn approx_eq(a: f64, b: f64, eps: f64) -> bool {
831 (a - b).abs() < eps
832 }
833
834 #[test]
835 fn test_eci_state_creation() {
836 let epoch = Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap();
837 let pos = Vector3::new(7000.0, 0.0, 0.0);
838 let vel = Vector3::new(0.0, 7.5, 0.0);
839
840 let state = ECIState::new(epoch, pos, vel);
841
842 assert!(approx_eq(state.radius(), 7000.0, EPSILON));
843 assert!(approx_eq(state.speed(), 7.5, EPSILON));
844 }
845
846 #[test]
847 fn test_geodetic_to_ecef() {
848 let epoch = Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap();
849
850 let geo = GeodeticState::new(0.0, 0.0, 0.0);
852 let ecef = geo.to_ecef(epoch);
853
854 assert!(approx_eq(
856 ecef.position.x,
857 Constants::EARTH_RADIUS_EQ_KM,
858 1e-3
859 ));
860 assert!(approx_eq(ecef.position.y, 0.0, 1e-3));
861 assert!(approx_eq(ecef.position.z, 0.0, 1e-3));
862 }
863
864 #[test]
865 fn test_geodetic_roundtrip() {
866 let epoch = Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap();
867
868 let geo_original = GeodeticState::new(40.0, -105.0, 1.6);
869 let ecef = geo_original.to_ecef(epoch);
870 let geo_back = GeodeticState::from_ecef(&ecef);
871
872 assert!(approx_eq(geo_original.latitude, geo_back.latitude, 1e-6));
873 assert!(approx_eq(geo_original.longitude, geo_back.longitude, 1e-6));
874 assert!(approx_eq(geo_original.altitude, geo_back.altitude, 1e-6));
875 }
876
877 #[test]
878 fn test_keplerian_period() {
879 let a = Constants::EARTH_RADIUS_EQ_KM + 400.0;
881 let elements = KeplerianElements::new(a, 0.0, 0.9, 0.0, 0.0, 0.0);
882
883 let period = elements.period();
885 assert!(period > 5400.0 && period < 5700.0);
886 }
887
888 #[test]
889 fn test_keplerian_apoapsis_periapsis() {
890 let a = 10000.0;
891 let e = 0.1;
892 let elements = KeplerianElements::new(a, e, 0.0, 0.0, 0.0, 0.0);
893
894 assert!(approx_eq(elements.apoapsis(), a * (1.0 + e), EPSILON));
895 assert!(approx_eq(elements.periapsis(), a * (1.0 - e), EPSILON));
896 }
897
898 #[test]
899 fn test_cartesian_keplerian_roundtrip() {
900 let epoch = Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap();
901
902 let pos = Vector3::new(6500.0, 1500.0, 1000.0);
904 let vel = Vector3::new(-1.0, 7.0, 0.5);
905 let eci = ECIState::new(epoch, pos, vel);
906
907 let kep = StateTransforms::cartesian_to_keplerian(&eci);
909 let eci_back = StateTransforms::keplerian_to_cartesian(&kep);
910
911 assert!(approx_eq(eci.position.x, eci_back.position.x, 1.0));
913 assert!(approx_eq(eci.position.y, eci_back.position.y, 1.0));
914 assert!(approx_eq(eci.position.z, eci_back.position.z, 1.0));
915 assert!(approx_eq(eci.velocity.x, eci_back.velocity.x, 1e-3));
916 assert!(approx_eq(eci.velocity.y, eci_back.velocity.y, 1e-3));
917 assert!(approx_eq(eci.velocity.z, eci_back.velocity.z, 1e-3));
918 }
919
920 #[test]
921 fn test_ecef_eci_roundtrip() {
922 let epoch = Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap();
923
924 let pos = Vector3::new(7000.0, 1000.0, 500.0);
925 let vel = Vector3::new(-1.0, 7.0, 0.5);
926 let eci = ECIState::new(epoch, pos, vel);
927
928 let ecef = StateTransforms::eci_to_ecef(&eci);
930 let eci_back = StateTransforms::ecef_to_eci(&ecef);
931
932 assert!(approx_eq(eci.position.x, eci_back.position.x, 1e-6));
934 assert!(approx_eq(eci.position.y, eci_back.position.y, 1e-6));
935 assert!(approx_eq(eci.position.z, eci_back.position.z, 1e-6));
936 assert!(approx_eq(eci.velocity.x, eci_back.velocity.x, 1e-6));
937 assert!(approx_eq(eci.velocity.y, eci_back.velocity.y, 1e-6));
938 assert!(approx_eq(eci.velocity.z, eci_back.velocity.z, 1e-6));
939 }
940
941 #[test]
942 fn test_keplerian_mean_anomaly() {
943 let kep = KeplerianElements::new(7000.0, 0.0, 0.5, 0.0, 0.0, 1.0);
945 assert!(approx_eq(kep.mean_anomaly(), 1.0, EPSILON));
946 }
947
948 #[test]
949 fn test_keplerian_angular_momentum() {
950 let a = 10000.0;
951 let e = 0.1;
952 let kep = KeplerianElements::new(a, e, 0.0, 0.0, 0.0, 0.0);
953
954 let expected = (Constants::EARTH_MU_KM * a * (1.0 - e * e)).sqrt();
956 assert!(approx_eq(kep.angular_momentum(), expected, EPSILON));
957 }
958}