1use crate::anomalies::AnomalyError;
16use crate::coords::Cartesian;
17use crate::elements::keplerian::{GravitationalParameter, Keplerian, KeplerianError};
18use crate::math::float::{atan, atan2, cos, sin, sqrt, tan};
19use crate::units::{Angle, AngleUnits, Distance};
20use glam::DVec3;
21use thiserror::Error;
22
23#[derive(Debug, Clone, Copy, PartialEq)]
30pub struct ModifiedEquinoctial {
31 p: Distance,
33 f: f64,
35 g: f64,
37 h: f64,
39 k: f64,
41 l: Angle,
43}
44
45#[derive(Debug, Clone, Error)]
47pub enum ModifiedEquinoctialError {
48 #[error(transparent)]
51 Anomaly(#[from] AnomalyError),
52 #[error(transparent)]
55 Keplerian(#[from] KeplerianError),
56}
57
58impl ModifiedEquinoctial {
59 pub fn new(p: Distance, f: f64, g: f64, h: f64, k: f64, l: Angle) -> Self {
64 Self { p, f, g, h, k, l }
65 }
66
67 pub fn to_cartesian(
69 &self,
70 mu: GravitationalParameter,
71 ) -> Result<Cartesian, ModifiedEquinoctialError> {
72 let p = self.p.as_f64();
73 let mu_f64 = mu.as_f64();
74
75 let s2 = 1.0 + self.h * self.h + self.k * self.k;
76 let f_hat = DVec3::new(
77 1.0 + self.h * self.h - self.k * self.k,
78 2.0 * self.h * self.k,
79 -2.0 * self.k,
80 ) / s2;
81 let g_hat = DVec3::new(
82 2.0 * self.h * self.k,
83 1.0 - self.h * self.h + self.k * self.k,
84 2.0 * self.h,
85 ) / s2;
86
87 let (sin_l, cos_l) = self.l.sin_cos();
88 let w = 1.0 + self.f * cos_l + self.g * sin_l;
89 let r_mag = p / w;
90
91 let r_orb = DVec3::new(r_mag * cos_l, r_mag * sin_l, 0.0);
92 let v_orb = DVec3::new(
93 -sqrt(mu_f64 / p) * (sin_l + self.g),
94 sqrt(mu_f64 / p) * (cos_l + self.f),
95 0.0,
96 );
97
98 let r_vec = r_orb.x * f_hat + r_orb.y * g_hat;
99 let v_vec = v_orb.x * f_hat + v_orb.y * g_hat;
100
101 Ok(Cartesian::from_vecs(r_vec, v_vec))
102 }
103
104 pub fn to_keplerian(&self) -> Result<Keplerian, ModifiedEquinoctialError> {
106 let e = self.eccentricity();
107 let pomega = atan2(self.g, self.f); let half_i_tan = sqrt(self.h * self.h + self.k * self.k);
109 let i = 2.0 * atan(half_i_tan);
110 let big_omega = atan2(self.k, self.h);
111
112 let omega = pomega - big_omega;
113 let nu = self.l.as_f64() - pomega;
114 let semi_major_axis = Distance::new(self.p.as_f64() / (1.0 - e * e));
115
116 let kep = Keplerian::builder()
117 .with_semi_major_axis(semi_major_axis, e)
118 .with_inclination(i.rad())
119 .with_longitude_of_ascending_node(big_omega.rad().mod_two_pi())
120 .with_argument_of_periapsis(omega.rad().mod_two_pi())
121 .with_true_anomaly(nu.rad().mod_two_pi())
122 .build()?;
123
124 Ok(kep)
125 }
126
127 pub fn p(&self) -> Distance {
129 self.p
130 }
131
132 pub fn f(&self) -> f64 {
134 self.f
135 }
136
137 pub fn g(&self) -> f64 {
139 self.g
140 }
141
142 pub fn h(&self) -> f64 {
144 self.h
145 }
146
147 pub fn k(&self) -> f64 {
149 self.k
150 }
151
152 pub fn l(&self) -> Angle {
154 self.l
155 }
156
157 pub fn eccentricity(&self) -> f64 {
159 sqrt(self.f * self.f + self.g * self.g)
160 }
161
162 pub fn inclination(&self) -> f64 {
164 2.0 * atan(sqrt(self.h * self.h + self.k * self.k))
165 }
166}
167
168impl Cartesian {
169 pub fn to_modified_equinoctial(
171 &self,
172 mu: GravitationalParameter,
173 ) -> Result<ModifiedEquinoctial, ModifiedEquinoctialError> {
174 let r_vec = self.position();
175 let v_vec = self.velocity();
176 let r_mag = r_vec.length();
177 let mu_f64 = mu.as_f64();
178
179 let h_vec = r_vec.cross(v_vec);
181 let h_mag = h_vec.length();
182
183 let p = Distance::new((h_mag * h_mag) / mu_f64);
185
186 let e_vec = v_vec.cross(h_vec) / mu_f64 - r_vec / r_mag;
188
189 let h = -h_vec.y / (h_mag + h_vec.z);
191 let k = h_vec.x / (h_mag + h_vec.z);
192
193 let s2 = 1.0 + h * h + k * k;
195 let f_hat = DVec3::new(1.0 + h * h - k * k, 2.0 * h * k, -2.0 * k) / s2;
196 let g_hat = DVec3::new(2.0 * h * k, 1.0 - h * h + k * k, 2.0 * h) / s2;
197
198 let f = e_vec.dot(f_hat);
199 let g = e_vec.dot(g_hat);
200
201 let x = r_vec.dot(f_hat);
202 let y = r_vec.dot(g_hat);
203 let l = Angle::from_atan2(y, x);
204
205 Ok(ModifiedEquinoctial { p, f, g, h, k, l })
206 }
207}
208
209impl Keplerian {
210 pub fn to_modified_equinoctial(&self) -> ModifiedEquinoctial {
212 let e = self.eccentricity().as_f64();
213 let i = self.inclination().as_f64();
214 let omega = self.argument_of_periapsis().as_f64();
215 let big_omega = self.longitude_of_ascending_node().as_f64();
216 let nu = self.true_anomaly().as_f64();
217
218 let p_val = self.semi_parameter();
219 let pomega = omega + big_omega; let f = e * cos(pomega);
222 let g = e * sin(pomega);
223 let half_i_tan = tan(i / 2.0);
224 let h = half_i_tan * cos(big_omega);
225 let k = half_i_tan * sin(big_omega);
226 let l = Angle::new(pomega + nu);
227
228 ModifiedEquinoctial {
229 p: p_val,
230 f,
231 g,
232 h,
233 k,
234 l,
235 }
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use alloc::string::ToString;
242 use lox_approx::assert_approx_eq;
243
244 use crate::elements::keplerian::GravitationalParameter;
245 use crate::units::{AngleUnits, DistanceUnits, VelocityUnits};
246
247 use super::*;
248
249 fn general_orbit_keplerian() -> Keplerian {
250 Keplerian::builder()
251 .with_semi_major_axis(24464.560.km(), 0.7311)
252 .with_inclination(0.122138.rad())
253 .with_longitude_of_ascending_node(1.00681.rad())
254 .with_argument_of_periapsis(3.10686.rad())
255 .with_true_anomaly(0.44369564302687126.rad())
256 .build()
257 .unwrap()
258 }
259
260 fn general_orbit_mu() -> GravitationalParameter {
261 GravitationalParameter::km3_per_s2(398600.43550702266)
262 }
263
264 #[test]
265 fn test_keplerian_roundtrip() {
266 let kep = general_orbit_keplerian();
267 let eq = kep.to_modified_equinoctial();
268 let kep2 = eq.to_keplerian().unwrap();
269
270 assert_approx_eq!(
271 kep.semi_major_axis().as_f64(),
272 kep2.semi_major_axis().as_f64(),
273 rtol <= 1e-12
274 );
275 assert_approx_eq!(
276 kep.eccentricity().as_f64(),
277 kep2.eccentricity().as_f64(),
278 atol <= 1e-12
279 );
280 assert_approx_eq!(
281 kep.inclination().as_f64(),
282 kep2.inclination().as_f64(),
283 atol <= 1e-12
284 );
285
286 let mu = general_orbit_mu();
287 let c1 = kep.to_cartesian(mu);
288 let c2 = kep2.to_cartesian(mu);
289 assert_approx_eq!(c1.position(), c2.position(), rtol <= 1e-10);
290 assert_approx_eq!(c1.velocity(), c2.velocity(), rtol <= 1e-10);
291 }
292
293 #[test]
294 fn test_cartesian_roundtrip() {
295 let mu = general_orbit_mu();
296 let cart = Cartesian::builder()
297 .position(
298 -0.107622532467967e7.m(),
299 -0.676589636432773e7.m(),
300 -0.332308783350379e6.m(),
301 )
302 .velocity(
303 0.935685775154103e4.mps(),
304 -0.331234775037644e4.mps(),
305 -0.118801577532701e4.mps(),
306 )
307 .build();
308
309 let eq = cart.to_modified_equinoctial(mu).unwrap();
310 let cart2 = eq.to_cartesian(mu).unwrap();
311
312 assert_approx_eq!(cart.position(), cart2.position(), rtol <= 1e-10);
313 assert_approx_eq!(cart.velocity(), cart2.velocity(), rtol <= 1e-10);
314 }
315
316 #[test]
317 fn test_circular_orbit() {
318 let kep = Keplerian::builder()
320 .with_semi_major_axis(6878.137.km(), 0.0)
321 .with_inclination(97.42_f64.to_radians().rad())
322 .with_longitude_of_ascending_node(69.3_f64.to_radians().rad())
323 .with_argument_of_periapsis(0.0.rad())
324 .with_true_anomaly(45.0_f64.to_radians().rad())
325 .build()
326 .unwrap();
327
328 let eq = kep.to_modified_equinoctial();
329
330 assert_approx_eq!(eq.f(), 0.0, atol <= 1e-15);
331 assert_approx_eq!(eq.g(), 0.0, atol <= 1e-15);
332 assert_approx_eq!(eq.eccentricity(), 0.0, atol <= 1e-15);
333 assert_approx_eq!(eq.inclination(), 97.42_f64.to_radians(), atol <= 1e-12);
334 assert!(eq.h().abs() > 0.1);
335 assert!(eq.k().abs() > 0.1);
336
337 let mu = GravitationalParameter::km3_per_s2(398600.4418);
338
339 let c1 = kep.to_cartesian(mu);
340
341 let eq_from_c = c1.to_modified_equinoctial(mu).unwrap();
343 assert_approx_eq!(eq_from_c.f(), 0.0, atol <= 1e-12);
344 assert_approx_eq!(eq_from_c.g(), 0.0, atol <= 1e-12);
345
346 let c2 = eq.to_cartesian(mu).unwrap();
347 assert_approx_eq!(c1.position(), c2.position(), rtol <= 1e-10);
348 assert_approx_eq!(c1.velocity(), c2.velocity(), rtol <= 1e-10);
349 }
350
351 #[test]
352 fn test_equatorial_orbit() {
353 let kep = Keplerian::builder()
355 .with_semi_major_axis(42164.0.km(), 0.001)
356 .with_inclination(0.0.rad())
357 .with_longitude_of_ascending_node(0.0.rad())
358 .with_argument_of_periapsis(45.0_f64.to_radians().rad())
359 .with_true_anomaly(30.0_f64.to_radians().rad())
360 .build()
361 .unwrap();
362
363 let eq = kep.to_modified_equinoctial();
364
365 assert_approx_eq!(eq.h(), 0.0, atol <= 1e-15);
366 assert_approx_eq!(eq.k(), 0.0, atol <= 1e-15);
367 assert_approx_eq!(eq.inclination(), 0.0, atol <= 1e-15);
368 assert!(eq.eccentricity() > 0.0);
369
370 let mu = GravitationalParameter::km3_per_s2(398600.4418);
371 let c1 = kep.to_cartesian(mu);
372
373 let eq_from_c = c1.to_modified_equinoctial(mu).unwrap();
374 assert_approx_eq!(eq_from_c.h(), 0.0, atol <= 1e-15);
375 assert_approx_eq!(eq_from_c.k(), 0.0, atol <= 1e-15);
376
377 let c2 = eq.to_cartesian(mu).unwrap();
378 assert_approx_eq!(c1.position(), c2.position(), rtol <= 1e-10);
379 assert_approx_eq!(c1.velocity(), c2.velocity(), rtol <= 1e-10);
380 }
381
382 #[test]
383 fn test_circular_equatorial_orbit() {
384 let kep = Keplerian::builder()
386 .with_semi_major_axis(42164.0.km(), 0.0)
387 .with_inclination(0.0.rad())
388 .with_longitude_of_ascending_node(0.0.rad())
389 .with_argument_of_periapsis(0.0.rad())
390 .with_true_anomaly(90.0_f64.to_radians().rad())
391 .build()
392 .unwrap();
393
394 let eq = kep.to_modified_equinoctial();
395
396 assert_approx_eq!(eq.f(), 0.0, atol <= 1e-15);
397 assert_approx_eq!(eq.g(), 0.0, atol <= 1e-15);
398 assert_approx_eq!(eq.h(), 0.0, atol <= 1e-15);
399 assert_approx_eq!(eq.k(), 0.0, atol <= 1e-15);
400 assert_approx_eq!(eq.eccentricity(), 0.0, atol <= 1e-15);
401 assert_approx_eq!(eq.inclination(), 0.0, atol <= 1e-15);
402
403 assert_approx_eq!(eq.l().as_f64(), 90.0_f64.to_radians(), atol <= 1e-12);
405
406 let mu = GravitationalParameter::km3_per_s2(398600.4418);
407 let c1 = kep.to_cartesian(mu);
408 let c2 = eq.to_cartesian(mu).unwrap();
409 assert_approx_eq!(c1.position(), c2.position(), rtol <= 1e-10);
410 assert_approx_eq!(c1.velocity(), c2.velocity(), rtol <= 1e-10);
411 }
412
413 #[test]
414 fn test_parabolic_orbit_directly() {
415 let mu = general_orbit_mu();
416 let mu_f64 = mu.as_f64();
417 let p_vec = 10000.0 * 1000.0; let r_mag = p_vec / 2.0;
423 let v_mag = (2.0 * mu_f64 / r_mag).sqrt();
424
425 let cart = Cartesian::builder()
426 .position(r_mag.m(), 0.0.m(), 0.0.m())
427 .velocity(0.0.mps(), v_mag.mps(), 0.0.mps())
428 .build();
429
430 let eq = cart.to_modified_equinoctial(mu).unwrap();
431
432 assert_approx_eq!(eq.eccentricity(), 1.0, atol <= 1e-12);
434
435 let cart2 = eq.to_cartesian(mu).unwrap();
437 assert_approx_eq!(cart.position(), cart2.position(), rtol <= 1e-10);
438 assert_approx_eq!(cart.velocity(), cart2.velocity(), rtol <= 1e-10);
439 }
440
441 #[test]
442 fn test_error_and_new_constructor() {
443 let mee = ModifiedEquinoctial::new(100.0.m(), 0.1, 0.2, 0.3, 0.4, 0.5.rad());
444 assert_eq!(mee.p(), 100.0.m());
445 assert_eq!(mee.f(), 0.1);
446 assert_eq!(mee.g(), 0.2);
447 assert_eq!(mee.h(), 0.3);
448 assert_eq!(mee.k(), 0.4);
449 assert_eq!(mee.l(), 0.5.rad());
450
451 let err1: ModifiedEquinoctialError =
452 crate::elements::keplerian::KeplerianError::MissingShape.into();
453 assert_eq!(
454 err1.to_string(),
455 "no orbital shape parameters (semi-major axis and eccentricity, radii, or altitudes) were provided"
456 );
457
458 let err2: ModifiedEquinoctialError = crate::anomalies::AnomalyError::InvalidTrueAnomaly {
459 nu: 1.0.rad(),
460 max_nu: 0.5.rad(),
461 }
462 .into();
463 assert!(err2.to_string().contains("outside valid range"));
464 }
465}