1use crate::astro::math::{normalize_angle, vec3, SMALL, TWO_PI};
34
35const PI: f64 = std::f64::consts::PI;
36const HALF_PI: f64 = std::f64::consts::FRAC_PI_2;
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum OrbitType {
43 EllipticalInclined,
45 EllipticalEquatorial,
48 CircularInclined,
51 CircularEquatorial,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq)]
63pub struct ClassicalElements {
64 pub p: f64,
66 pub a: f64,
69 pub ecc: f64,
71 pub incl: f64,
73 pub raan: f64,
76 pub argp: f64,
79 pub nu: f64,
82 pub arglat: f64,
85 pub truelon: f64,
88 pub lonper: f64,
91 pub orbit_type: OrbitType,
93}
94
95impl ClassicalElements {
96 pub fn new(p: f64, ecc: f64, incl: f64, raan: f64, argp: f64, nu: f64) -> Self {
107 let a = if (ecc - 1.0).abs() < SMALL {
108 f64::INFINITY
109 } else {
110 p / (1.0 - ecc * ecc)
111 };
112 Self {
113 p,
114 a,
115 ecc,
116 incl,
117 raan,
118 argp,
119 nu,
120 arglat: f64::NAN,
121 truelon: f64::NAN,
122 lonper: f64::NAN,
123 orbit_type: OrbitType::EllipticalInclined,
124 }
125 }
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
130pub enum ElementsError {
131 #[error("non-finite input {field}")]
133 NonFinite {
134 field: &'static str,
136 },
137 #[error("mu must be positive")]
139 NonPositiveMu,
140 #[error("position vector has near-zero magnitude")]
143 ZeroPosition,
144 #[error("angular momentum is near zero (degenerate orbit)")]
147 DegenerateOrbit,
148 #[error("semi-latus rectum must be positive")]
150 NonPositiveSemiLatus,
151}
152
153pub fn rv2coe(r: [f64; 3], v: [f64; 3], mu: f64) -> Result<ClassicalElements, ElementsError> {
160 validate_finite(&r, "r")?;
161 validate_finite(&v, "v")?;
162 if !mu.is_finite() {
163 return Err(ElementsError::NonFinite { field: "mu" });
164 }
165 if mu <= 0.0 {
166 return Err(ElementsError::NonPositiveMu);
167 }
168
169 let magr = vec3::norm3(r);
170 let magv = vec3::norm3(v);
171 if magr < SMALL {
172 return Err(ElementsError::ZeroPosition);
173 }
174
175 let hbar = vec3::cross3(r, v);
177 let magh = vec3::norm3(hbar);
178 if magh < SMALL {
179 return Err(ElementsError::DegenerateOrbit);
180 }
181
182 let nbar = [-hbar[1], hbar[0], 0.0];
184 let magn = vec3::norm3(nbar);
185
186 let rdotv = vec3::dot3(r, v);
188 let c1 = magv * magv - mu / magr;
189 let ebar = [
190 (c1 * r[0] - rdotv * v[0]) / mu,
191 (c1 * r[1] - rdotv * v[1]) / mu,
192 (c1 * r[2] - rdotv * v[2]) / mu,
193 ];
194 let ecc = vec3::norm3(ebar);
195
196 let sme = magv * magv * 0.5 - mu / magr;
198 let a = if sme.abs() > SMALL {
199 -mu / (2.0 * sme)
200 } else {
201 f64::INFINITY
202 };
203 let p = magh * magh / mu;
204
205 let incl = clamp_acos(hbar[2] / magh);
206
207 let orbit_type = classify(ecc, incl);
208
209 let raan = if magn > SMALL {
211 let mut omega = clamp_acos(nbar[0] / magn);
212 if nbar[1] < 0.0 {
213 omega = TWO_PI - omega;
214 }
215 omega
216 } else {
217 f64::NAN
218 };
219
220 let argp = if orbit_type == OrbitType::EllipticalInclined {
222 let mut ap = angle_between(nbar, ebar);
223 if ebar[2] < 0.0 {
224 ap = TWO_PI - ap;
225 }
226 ap
227 } else {
228 f64::NAN
229 };
230
231 let nu = if ecc > SMALL {
233 let mut ta = angle_between(ebar, r);
234 if rdotv < 0.0 {
235 ta = TWO_PI - ta;
236 }
237 ta
238 } else {
239 f64::NAN
240 };
241
242 let arglat = if orbit_type == OrbitType::CircularInclined {
244 let mut u = angle_between(nbar, r);
245 if r[2] < 0.0 {
246 u = TWO_PI - u;
247 }
248 u
249 } else {
250 f64::NAN
251 };
252
253 let lonper = if orbit_type == OrbitType::EllipticalEquatorial {
255 let mut lp = clamp_acos(ebar[0] / ecc);
256 if ebar[1] < 0.0 {
257 lp = TWO_PI - lp;
258 }
259 if incl > HALF_PI {
260 lp = TWO_PI - lp;
261 }
262 normalize_angle(lp)
263 } else {
264 f64::NAN
265 };
266
267 let truelon = if orbit_type == OrbitType::CircularEquatorial {
269 let mut tl = clamp_acos(r[0] / magr);
270 if r[1] < 0.0 {
271 tl = TWO_PI - tl;
272 }
273 if incl > HALF_PI {
274 tl = TWO_PI - tl;
275 }
276 normalize_angle(tl)
277 } else {
278 f64::NAN
279 };
280
281 let circular = matches!(
288 orbit_type,
289 OrbitType::CircularInclined | OrbitType::CircularEquatorial
290 );
291 let equatorial = matches!(
292 orbit_type,
293 OrbitType::EllipticalEquatorial | OrbitType::CircularEquatorial
294 );
295 let ecc = if circular { 0.0 } else { ecc };
296 let incl = if equatorial {
297 if incl > HALF_PI {
298 PI
299 } else {
300 0.0
301 }
302 } else {
303 incl
304 };
305
306 Ok(ClassicalElements {
307 p,
308 a,
309 ecc,
310 incl,
311 raan,
312 argp,
313 nu,
314 arglat,
315 truelon,
316 lonper,
317 orbit_type,
318 })
319}
320
321pub fn coe2rv(coe: &ClassicalElements, mu: f64) -> Result<([f64; 3], [f64; 3]), ElementsError> {
328 if !mu.is_finite() {
329 return Err(ElementsError::NonFinite { field: "mu" });
330 }
331 if mu <= 0.0 {
332 return Err(ElementsError::NonPositiveMu);
333 }
334 if !coe.p.is_finite() {
335 return Err(ElementsError::NonFinite { field: "p" });
336 }
337 if coe.p <= 0.0 {
338 return Err(ElementsError::NonPositiveSemiLatus);
339 }
340 if !coe.ecc.is_finite() {
341 return Err(ElementsError::NonFinite { field: "ecc" });
342 }
343 if !coe.incl.is_finite() {
344 return Err(ElementsError::NonFinite { field: "incl" });
345 }
346
347 let (raan, argp, nu) = match coe.orbit_type {
350 OrbitType::EllipticalInclined => {
351 check_angle(coe.raan, "raan")?;
352 check_angle(coe.argp, "argp")?;
353 check_angle(coe.nu, "nu")?;
354 (coe.raan, coe.argp, coe.nu)
355 }
356 OrbitType::CircularInclined => {
357 check_angle(coe.raan, "raan")?;
358 check_angle(coe.arglat, "arglat")?;
359 (coe.raan, 0.0, coe.arglat)
360 }
361 OrbitType::EllipticalEquatorial => {
362 check_angle(coe.lonper, "lonper")?;
363 check_angle(coe.nu, "nu")?;
364 (0.0, coe.lonper, coe.nu)
365 }
366 OrbitType::CircularEquatorial => {
367 check_angle(coe.truelon, "truelon")?;
368 (0.0, 0.0, coe.truelon)
369 }
370 };
371
372 let ecc = coe.ecc;
373 let p = coe.p;
374 let incl = coe.incl;
375
376 let (sin_nu, cos_nu) = nu.sin_cos();
377
378 let temp = p / (1.0 + ecc * cos_nu);
380 let r_pqw = [temp * cos_nu, temp * sin_nu, 0.0];
381 let sqrt_mu_p = (mu / p).sqrt();
382 let v_pqw = [-sin_nu * sqrt_mu_p, (ecc + cos_nu) * sqrt_mu_p, 0.0];
383
384 let (sin_raan, cos_raan) = raan.sin_cos();
386 let (sin_argp, cos_argp) = argp.sin_cos();
387 let (sin_incl, cos_incl) = incl.sin_cos();
388
389 let m11 = cos_raan * cos_argp - sin_raan * sin_argp * cos_incl;
390 let m12 = -cos_raan * sin_argp - sin_raan * cos_argp * cos_incl;
391 let m21 = sin_raan * cos_argp + cos_raan * sin_argp * cos_incl;
392 let m22 = -sin_raan * sin_argp + cos_raan * cos_argp * cos_incl;
393 let m31 = sin_argp * sin_incl;
394 let m32 = cos_argp * sin_incl;
395
396 let r = [
397 m11 * r_pqw[0] + m12 * r_pqw[1],
398 m21 * r_pqw[0] + m22 * r_pqw[1],
399 m31 * r_pqw[0] + m32 * r_pqw[1],
400 ];
401 let v = [
402 m11 * v_pqw[0] + m12 * v_pqw[1],
403 m21 * v_pqw[0] + m22 * v_pqw[1],
404 m31 * v_pqw[0] + m32 * v_pqw[1],
405 ];
406
407 Ok((r, v))
408}
409
410pub(crate) fn classify(ecc: f64, incl: f64) -> OrbitType {
413 let equatorial = incl < SMALL || (incl - PI).abs() < SMALL;
414 let circular = ecc < SMALL;
415 match (circular, equatorial) {
416 (true, true) => OrbitType::CircularEquatorial,
417 (true, false) => OrbitType::CircularInclined,
418 (false, true) => OrbitType::EllipticalEquatorial,
419 (false, false) => OrbitType::EllipticalInclined,
420 }
421}
422
423#[inline]
426pub(crate) fn clamp_acos(x: f64) -> f64 {
427 x.clamp(-1.0, 1.0).acos()
428}
429
430#[inline]
432fn angle_between(a: [f64; 3], b: [f64; 3]) -> f64 {
433 let denom = vec3::norm3(a) * vec3::norm3(b);
434 clamp_acos(vec3::dot3(a, b) / denom)
435}
436
437#[inline]
438fn validate_finite(v: &[f64; 3], field: &'static str) -> Result<(), ElementsError> {
439 if v.iter().all(|x| x.is_finite()) {
440 Ok(())
441 } else {
442 Err(ElementsError::NonFinite { field })
443 }
444}
445
446#[inline]
447fn check_angle(x: f64, field: &'static str) -> Result<(), ElementsError> {
448 if x.is_finite() {
449 Ok(())
450 } else {
451 Err(ElementsError::NonFinite { field })
452 }
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458
459 const MU: f64 = 398600.4418;
461 const DEG: f64 = std::f64::consts::PI / 180.0;
462
463 fn assert_close(got: f64, want: f64, tol: f64, what: &str) {
464 assert!(
465 (got - want).abs() < tol,
466 "{what}: got {got}, want {want}, diff {}",
467 (got - want).abs()
468 );
469 }
470
471 fn assert_vec_close(got: [f64; 3], want: [f64; 3], tol: f64, what: &str) {
472 for i in 0..3 {
473 assert!(
474 (got[i] - want[i]).abs() < tol,
475 "{what}[{i}]: got {}, want {}, diff {}",
476 got[i],
477 want[i],
478 (got[i] - want[i]).abs()
479 );
480 }
481 }
482
483 #[test]
485 fn rv2coe_vallado_example_2_5() {
486 let r = [6524.834, 6862.875, 6448.296];
487 let v = [4.901327, 5.533756, -1.976341];
488
489 let coe = rv2coe(r, v, MU).unwrap();
490
491 assert_close(coe.p, 11067.790, 1.0e-2, "p");
493 assert_close(coe.a, 36127.343, 1.0e-2, "a");
494 assert_close(coe.ecc, 0.832853, 1.0e-5, "ecc");
495 assert_close(coe.incl, 87.870 * DEG, 1.0e-4, "incl");
496 assert_close(coe.raan, 227.898 * DEG, 1.0e-4, "raan");
497 assert_close(coe.argp, 53.38 * DEG, 1.0e-3, "argp");
498 assert_close(coe.nu, 92.335 * DEG, 1.0e-4, "nu");
499 assert_eq!(coe.orbit_type, OrbitType::EllipticalInclined);
500 }
501
502 #[test]
505 fn coe2rv_vallado_example_2_6() {
506 let coe = ClassicalElements::new(
512 11067.790,
513 0.83285,
514 87.87 * DEG,
515 227.89 * DEG,
516 53.38 * DEG,
517 92.335 * DEG,
518 );
519
520 let (r, v) = coe2rv(&coe, MU).unwrap();
521
522 assert_vec_close(r, [6525.344, 6861.535, 6449.125], 5.0e-2, "r");
526 assert_vec_close(v, [4.902276, 5.533124, -1.975709], 1.0e-3, "v");
527 }
528
529 #[test]
530 fn round_trip_elliptical_inclined() {
531 let r = [6524.834, 6862.875, 6448.296];
532 let v = [4.901327, 5.533756, -1.976341];
533
534 let coe = rv2coe(r, v, MU).unwrap();
535 let (r2, v2) = coe2rv(&coe, MU).unwrap();
536
537 assert_vec_close(r2, r, 1.0e-7, "r");
538 assert_vec_close(v2, v, 1.0e-10, "v");
539 }
540
541 #[test]
542 fn round_trip_circular_inclined() {
543 let raan0 = 80.0 * DEG;
547 let incl0 = 51.6 * DEG;
548 let arglat0 = 135.0 * DEG;
549
550 let mut coe = ClassicalElements::new(7000.0, 0.0, incl0, raan0, 0.0, 0.0);
551 coe.orbit_type = OrbitType::CircularInclined;
552 coe.argp = f64::NAN;
553 coe.nu = f64::NAN;
554 coe.arglat = arglat0;
555
556 let (r, v) = coe2rv(&coe, MU).unwrap();
557 let back = rv2coe(r, v, MU).unwrap();
558
559 assert_eq!(back.orbit_type, OrbitType::CircularInclined);
560 assert!(back.argp.is_nan());
561 assert!(back.arglat.is_finite());
562 assert_close(back.incl, incl0, 1.0e-10, "incl");
563 assert_close(back.raan, raan0, 1.0e-10, "raan");
564 assert_close(back.arglat, arglat0, 1.0e-10, "arglat");
565
566 let (r2, v2) = coe2rv(&back, MU).unwrap();
567 assert_vec_close(r2, r, 1.0e-8, "r");
568 assert_vec_close(v2, v, 1.0e-11, "v");
569 }
570
571 #[test]
572 fn round_trip_circular_equatorial() {
573 let radius = 7000.0_f64;
575 let speed = (MU / radius).sqrt();
576 let truelon0 = 35.0 * DEG;
577 let r = [radius * truelon0.cos(), radius * truelon0.sin(), 0.0];
578 let v = [-speed * truelon0.sin(), speed * truelon0.cos(), 0.0];
579
580 let coe = rv2coe(r, v, MU).unwrap();
581 assert_eq!(coe.orbit_type, OrbitType::CircularEquatorial);
582 assert!(coe.raan.is_nan());
583 assert!(coe.argp.is_nan());
584 assert!(coe.nu.is_nan());
585 assert_close(coe.truelon, truelon0, 1.0e-9, "truelon");
586
587 let (r2, v2) = coe2rv(&coe, MU).unwrap();
588 assert_vec_close(r2, r, 1.0e-8, "r");
589 assert_vec_close(v2, v, 1.0e-11, "v");
590 }
591
592 #[test]
593 fn round_trip_elliptical_equatorial() {
594 let p = 11067.790_f64;
597 let ecc = 0.4_f64;
598 let lonper0 = 110.0 * DEG;
599 let nu0 = 40.0 * DEG;
600
601 let mut coe = ClassicalElements::new(p, ecc, 0.0, 0.0, 0.0, nu0);
602 coe.orbit_type = OrbitType::EllipticalEquatorial;
603 coe.raan = f64::NAN;
604 coe.argp = f64::NAN;
605 coe.lonper = lonper0;
606
607 let (r, v) = coe2rv(&coe, MU).unwrap();
608 let back = rv2coe(r, v, MU).unwrap();
609
610 assert_eq!(back.orbit_type, OrbitType::EllipticalEquatorial);
611 assert_close(back.ecc, ecc, 1.0e-12, "ecc");
612 assert_close(back.incl, 0.0, 1.0e-12, "incl");
613 assert_close(back.lonper, lonper0, 1.0e-10, "lonper");
614 assert_close(back.nu, nu0, 1.0e-10, "nu");
615
616 let (r2, v2) = coe2rv(&back, MU).unwrap();
617 assert_vec_close(r2, r, 1.0e-8, "r");
618 assert_vec_close(v2, v, 1.0e-11, "v");
619 }
620
621 #[test]
622 fn rejects_bad_inputs() {
623 let r = [7000.0, 0.0, 0.0];
624 let v = [0.0, 7.5, 0.0];
625
626 assert_eq!(rv2coe(r, v, -1.0), Err(ElementsError::NonPositiveMu));
627 assert_eq!(
628 rv2coe([0.0, 0.0, 0.0], v, MU),
629 Err(ElementsError::ZeroPosition)
630 );
631 assert_eq!(
633 rv2coe(r, [7.5, 0.0, 0.0], MU),
634 Err(ElementsError::DegenerateOrbit)
635 );
636 assert!(matches!(
637 rv2coe([f64::NAN, 0.0, 0.0], v, MU),
638 Err(ElementsError::NonFinite { field: "r" })
639 ));
640
641 let coe = ClassicalElements::new(11067.79, 0.1, 0.5, 0.1, 0.2, 0.3);
642 assert_eq!(coe2rv(&coe, 0.0), Err(ElementsError::NonPositiveMu));
643 let mut bad = coe;
644 bad.p = -1.0;
645 assert_eq!(coe2rv(&bad, MU), Err(ElementsError::NonPositiveSemiLatus));
646 }
647}