1use crate::astro::math::vec3;
34
35const SMALL: f64 = 1.0e-8;
38
39const TWO_PI: f64 = 2.0 * std::f64::consts::PI;
40const PI: f64 = std::f64::consts::PI;
41const HALF_PI: f64 = std::f64::consts::FRAC_PI_2;
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum OrbitType {
48 EllipticalInclined,
50 EllipticalEquatorial,
53 CircularInclined,
56 CircularEquatorial,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq)]
68pub struct ClassicalElements {
69 pub p: f64,
71 pub a: f64,
74 pub ecc: f64,
76 pub incl: f64,
78 pub raan: f64,
81 pub argp: f64,
84 pub nu: f64,
87 pub arglat: f64,
90 pub truelon: f64,
93 pub lonper: f64,
96 pub orbit_type: OrbitType,
98}
99
100impl ClassicalElements {
101 pub fn new(p: f64, ecc: f64, incl: f64, raan: f64, argp: f64, nu: f64) -> Self {
112 let a = if (ecc - 1.0).abs() < SMALL {
113 f64::INFINITY
114 } else {
115 p / (1.0 - ecc * ecc)
116 };
117 Self {
118 p,
119 a,
120 ecc,
121 incl,
122 raan,
123 argp,
124 nu,
125 arglat: f64::NAN,
126 truelon: f64::NAN,
127 lonper: f64::NAN,
128 orbit_type: OrbitType::EllipticalInclined,
129 }
130 }
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
135pub enum ElementsError {
136 #[error("non-finite input {field}")]
138 NonFinite {
139 field: &'static str,
141 },
142 #[error("mu must be positive")]
144 NonPositiveMu,
145 #[error("position vector has near-zero magnitude")]
148 ZeroPosition,
149 #[error("angular momentum is near zero (degenerate orbit)")]
152 DegenerateOrbit,
153 #[error("semi-latus rectum must be positive")]
155 NonPositiveSemiLatus,
156}
157
158pub fn rv2coe(r: [f64; 3], v: [f64; 3], mu: f64) -> Result<ClassicalElements, ElementsError> {
165 validate_finite(&r, "r")?;
166 validate_finite(&v, "v")?;
167 if !mu.is_finite() {
168 return Err(ElementsError::NonFinite { field: "mu" });
169 }
170 if mu <= 0.0 {
171 return Err(ElementsError::NonPositiveMu);
172 }
173
174 let magr = vec3::norm3(r);
175 let magv = vec3::norm3(v);
176 if magr < SMALL {
177 return Err(ElementsError::ZeroPosition);
178 }
179
180 let hbar = vec3::cross3(r, v);
182 let magh = vec3::norm3(hbar);
183 if magh < SMALL {
184 return Err(ElementsError::DegenerateOrbit);
185 }
186
187 let nbar = [-hbar[1], hbar[0], 0.0];
189 let magn = vec3::norm3(nbar);
190
191 let rdotv = vec3::dot3(r, v);
193 let c1 = magv * magv - mu / magr;
194 let ebar = [
195 (c1 * r[0] - rdotv * v[0]) / mu,
196 (c1 * r[1] - rdotv * v[1]) / mu,
197 (c1 * r[2] - rdotv * v[2]) / mu,
198 ];
199 let ecc = vec3::norm3(ebar);
200
201 let sme = magv * magv * 0.5 - mu / magr;
203 let a = if sme.abs() > SMALL {
204 -mu / (2.0 * sme)
205 } else {
206 f64::INFINITY
207 };
208 let p = magh * magh / mu;
209
210 let incl = clamp_acos(hbar[2] / magh);
211
212 let orbit_type = classify(ecc, incl);
213
214 let raan = if magn > SMALL {
216 let mut omega = clamp_acos(nbar[0] / magn);
217 if nbar[1] < 0.0 {
218 omega = TWO_PI - omega;
219 }
220 omega
221 } else {
222 f64::NAN
223 };
224
225 let argp = if orbit_type == OrbitType::EllipticalInclined {
227 let mut ap = angle_between(nbar, ebar);
228 if ebar[2] < 0.0 {
229 ap = TWO_PI - ap;
230 }
231 ap
232 } else {
233 f64::NAN
234 };
235
236 let nu = if ecc > SMALL {
238 let mut ta = angle_between(ebar, r);
239 if rdotv < 0.0 {
240 ta = TWO_PI - ta;
241 }
242 ta
243 } else {
244 f64::NAN
245 };
246
247 let arglat = if orbit_type == OrbitType::CircularInclined {
249 let mut u = angle_between(nbar, r);
250 if r[2] < 0.0 {
251 u = TWO_PI - u;
252 }
253 u
254 } else {
255 f64::NAN
256 };
257
258 let lonper = if orbit_type == OrbitType::EllipticalEquatorial {
260 let mut lp = clamp_acos(ebar[0] / ecc);
261 if ebar[1] < 0.0 {
262 lp = TWO_PI - lp;
263 }
264 if incl > HALF_PI {
265 lp = TWO_PI - lp;
266 }
267 normalize_angle(lp)
268 } else {
269 f64::NAN
270 };
271
272 let truelon = if orbit_type == OrbitType::CircularEquatorial {
274 let mut tl = clamp_acos(r[0] / magr);
275 if r[1] < 0.0 {
276 tl = TWO_PI - tl;
277 }
278 if incl > HALF_PI {
279 tl = TWO_PI - tl;
280 }
281 normalize_angle(tl)
282 } else {
283 f64::NAN
284 };
285
286 let circular = matches!(
293 orbit_type,
294 OrbitType::CircularInclined | OrbitType::CircularEquatorial
295 );
296 let equatorial = matches!(
297 orbit_type,
298 OrbitType::EllipticalEquatorial | OrbitType::CircularEquatorial
299 );
300 let ecc = if circular { 0.0 } else { ecc };
301 let incl = if equatorial {
302 if incl > HALF_PI {
303 PI
304 } else {
305 0.0
306 }
307 } else {
308 incl
309 };
310
311 Ok(ClassicalElements {
312 p,
313 a,
314 ecc,
315 incl,
316 raan,
317 argp,
318 nu,
319 arglat,
320 truelon,
321 lonper,
322 orbit_type,
323 })
324}
325
326pub fn coe2rv(coe: &ClassicalElements, mu: f64) -> Result<([f64; 3], [f64; 3]), ElementsError> {
333 if !mu.is_finite() {
334 return Err(ElementsError::NonFinite { field: "mu" });
335 }
336 if mu <= 0.0 {
337 return Err(ElementsError::NonPositiveMu);
338 }
339 if !coe.p.is_finite() {
340 return Err(ElementsError::NonFinite { field: "p" });
341 }
342 if coe.p <= 0.0 {
343 return Err(ElementsError::NonPositiveSemiLatus);
344 }
345 if !coe.ecc.is_finite() {
346 return Err(ElementsError::NonFinite { field: "ecc" });
347 }
348 if !coe.incl.is_finite() {
349 return Err(ElementsError::NonFinite { field: "incl" });
350 }
351
352 let (raan, argp, nu) = match coe.orbit_type {
355 OrbitType::EllipticalInclined => {
356 check_angle(coe.raan, "raan")?;
357 check_angle(coe.argp, "argp")?;
358 check_angle(coe.nu, "nu")?;
359 (coe.raan, coe.argp, coe.nu)
360 }
361 OrbitType::CircularInclined => {
362 check_angle(coe.raan, "raan")?;
363 check_angle(coe.arglat, "arglat")?;
364 (coe.raan, 0.0, coe.arglat)
365 }
366 OrbitType::EllipticalEquatorial => {
367 check_angle(coe.lonper, "lonper")?;
368 check_angle(coe.nu, "nu")?;
369 (0.0, coe.lonper, coe.nu)
370 }
371 OrbitType::CircularEquatorial => {
372 check_angle(coe.truelon, "truelon")?;
373 (0.0, 0.0, coe.truelon)
374 }
375 };
376
377 let ecc = coe.ecc;
378 let p = coe.p;
379 let incl = coe.incl;
380
381 let (sin_nu, cos_nu) = nu.sin_cos();
382
383 let temp = p / (1.0 + ecc * cos_nu);
385 let r_pqw = [temp * cos_nu, temp * sin_nu, 0.0];
386 let sqrt_mu_p = (mu / p).sqrt();
387 let v_pqw = [-sin_nu * sqrt_mu_p, (ecc + cos_nu) * sqrt_mu_p, 0.0];
388
389 let (sin_raan, cos_raan) = raan.sin_cos();
391 let (sin_argp, cos_argp) = argp.sin_cos();
392 let (sin_incl, cos_incl) = incl.sin_cos();
393
394 let m11 = cos_raan * cos_argp - sin_raan * sin_argp * cos_incl;
395 let m12 = -cos_raan * sin_argp - sin_raan * cos_argp * cos_incl;
396 let m21 = sin_raan * cos_argp + cos_raan * sin_argp * cos_incl;
397 let m22 = -sin_raan * sin_argp + cos_raan * cos_argp * cos_incl;
398 let m31 = sin_argp * sin_incl;
399 let m32 = cos_argp * sin_incl;
400
401 let r = [
402 m11 * r_pqw[0] + m12 * r_pqw[1],
403 m21 * r_pqw[0] + m22 * r_pqw[1],
404 m31 * r_pqw[0] + m32 * r_pqw[1],
405 ];
406 let v = [
407 m11 * v_pqw[0] + m12 * v_pqw[1],
408 m21 * v_pqw[0] + m22 * v_pqw[1],
409 m31 * v_pqw[0] + m32 * v_pqw[1],
410 ];
411
412 Ok((r, v))
413}
414
415fn classify(ecc: f64, incl: f64) -> OrbitType {
418 let equatorial = incl < SMALL || (incl - PI).abs() < SMALL;
419 let circular = ecc < SMALL;
420 match (circular, equatorial) {
421 (true, true) => OrbitType::CircularEquatorial,
422 (true, false) => OrbitType::CircularInclined,
423 (false, true) => OrbitType::EllipticalEquatorial,
424 (false, false) => OrbitType::EllipticalInclined,
425 }
426}
427
428#[inline]
432fn normalize_angle(x: f64) -> f64 {
433 x.rem_euclid(TWO_PI)
434}
435
436#[inline]
439fn clamp_acos(x: f64) -> f64 {
440 x.clamp(-1.0, 1.0).acos()
441}
442
443#[inline]
445fn angle_between(a: [f64; 3], b: [f64; 3]) -> f64 {
446 let denom = vec3::norm3(a) * vec3::norm3(b);
447 clamp_acos(vec3::dot3(a, b) / denom)
448}
449
450#[inline]
451fn validate_finite(v: &[f64; 3], field: &'static str) -> Result<(), ElementsError> {
452 if v.iter().all(|x| x.is_finite()) {
453 Ok(())
454 } else {
455 Err(ElementsError::NonFinite { field })
456 }
457}
458
459#[inline]
460fn check_angle(x: f64, field: &'static str) -> Result<(), ElementsError> {
461 if x.is_finite() {
462 Ok(())
463 } else {
464 Err(ElementsError::NonFinite { field })
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471
472 const MU: f64 = 398600.4418;
474 const DEG: f64 = std::f64::consts::PI / 180.0;
475
476 fn assert_close(got: f64, want: f64, tol: f64, what: &str) {
477 assert!(
478 (got - want).abs() < tol,
479 "{what}: got {got}, want {want}, diff {}",
480 (got - want).abs()
481 );
482 }
483
484 fn assert_vec_close(got: [f64; 3], want: [f64; 3], tol: f64, what: &str) {
485 for i in 0..3 {
486 assert!(
487 (got[i] - want[i]).abs() < tol,
488 "{what}[{i}]: got {}, want {}, diff {}",
489 got[i],
490 want[i],
491 (got[i] - want[i]).abs()
492 );
493 }
494 }
495
496 #[test]
498 fn rv2coe_vallado_example_2_5() {
499 let r = [6524.834, 6862.875, 6448.296];
500 let v = [4.901327, 5.533756, -1.976341];
501
502 let coe = rv2coe(r, v, MU).unwrap();
503
504 assert_close(coe.p, 11067.790, 1.0e-2, "p");
506 assert_close(coe.a, 36127.343, 1.0e-2, "a");
507 assert_close(coe.ecc, 0.832853, 1.0e-5, "ecc");
508 assert_close(coe.incl, 87.870 * DEG, 1.0e-4, "incl");
509 assert_close(coe.raan, 227.898 * DEG, 1.0e-4, "raan");
510 assert_close(coe.argp, 53.38 * DEG, 1.0e-3, "argp");
511 assert_close(coe.nu, 92.335 * DEG, 1.0e-4, "nu");
512 assert_eq!(coe.orbit_type, OrbitType::EllipticalInclined);
513 }
514
515 #[test]
518 fn coe2rv_vallado_example_2_6() {
519 let coe = ClassicalElements::new(
525 11067.790,
526 0.83285,
527 87.87 * DEG,
528 227.89 * DEG,
529 53.38 * DEG,
530 92.335 * DEG,
531 );
532
533 let (r, v) = coe2rv(&coe, MU).unwrap();
534
535 assert_vec_close(r, [6525.344, 6861.535, 6449.125], 5.0e-2, "r");
539 assert_vec_close(v, [4.902276, 5.533124, -1.975709], 1.0e-3, "v");
540 }
541
542 #[test]
543 fn round_trip_elliptical_inclined() {
544 let r = [6524.834, 6862.875, 6448.296];
545 let v = [4.901327, 5.533756, -1.976341];
546
547 let coe = rv2coe(r, v, MU).unwrap();
548 let (r2, v2) = coe2rv(&coe, MU).unwrap();
549
550 assert_vec_close(r2, r, 1.0e-7, "r");
551 assert_vec_close(v2, v, 1.0e-10, "v");
552 }
553
554 #[test]
555 fn round_trip_circular_inclined() {
556 let raan0 = 80.0 * DEG;
560 let incl0 = 51.6 * DEG;
561 let arglat0 = 135.0 * DEG;
562
563 let mut coe = ClassicalElements::new(7000.0, 0.0, incl0, raan0, 0.0, 0.0);
564 coe.orbit_type = OrbitType::CircularInclined;
565 coe.argp = f64::NAN;
566 coe.nu = f64::NAN;
567 coe.arglat = arglat0;
568
569 let (r, v) = coe2rv(&coe, MU).unwrap();
570 let back = rv2coe(r, v, MU).unwrap();
571
572 assert_eq!(back.orbit_type, OrbitType::CircularInclined);
573 assert!(back.argp.is_nan());
574 assert!(back.arglat.is_finite());
575 assert_close(back.incl, incl0, 1.0e-10, "incl");
576 assert_close(back.raan, raan0, 1.0e-10, "raan");
577 assert_close(back.arglat, arglat0, 1.0e-10, "arglat");
578
579 let (r2, v2) = coe2rv(&back, MU).unwrap();
580 assert_vec_close(r2, r, 1.0e-8, "r");
581 assert_vec_close(v2, v, 1.0e-11, "v");
582 }
583
584 #[test]
585 fn round_trip_circular_equatorial() {
586 let radius = 7000.0_f64;
588 let speed = (MU / radius).sqrt();
589 let truelon0 = 35.0 * DEG;
590 let r = [radius * truelon0.cos(), radius * truelon0.sin(), 0.0];
591 let v = [-speed * truelon0.sin(), speed * truelon0.cos(), 0.0];
592
593 let coe = rv2coe(r, v, MU).unwrap();
594 assert_eq!(coe.orbit_type, OrbitType::CircularEquatorial);
595 assert!(coe.raan.is_nan());
596 assert!(coe.argp.is_nan());
597 assert!(coe.nu.is_nan());
598 assert_close(coe.truelon, truelon0, 1.0e-9, "truelon");
599
600 let (r2, v2) = coe2rv(&coe, MU).unwrap();
601 assert_vec_close(r2, r, 1.0e-8, "r");
602 assert_vec_close(v2, v, 1.0e-11, "v");
603 }
604
605 #[test]
606 fn round_trip_elliptical_equatorial() {
607 let p = 11067.790_f64;
610 let ecc = 0.4_f64;
611 let lonper0 = 110.0 * DEG;
612 let nu0 = 40.0 * DEG;
613
614 let mut coe = ClassicalElements::new(p, ecc, 0.0, 0.0, 0.0, nu0);
615 coe.orbit_type = OrbitType::EllipticalEquatorial;
616 coe.raan = f64::NAN;
617 coe.argp = f64::NAN;
618 coe.lonper = lonper0;
619
620 let (r, v) = coe2rv(&coe, MU).unwrap();
621 let back = rv2coe(r, v, MU).unwrap();
622
623 assert_eq!(back.orbit_type, OrbitType::EllipticalEquatorial);
624 assert_close(back.ecc, ecc, 1.0e-12, "ecc");
625 assert_close(back.incl, 0.0, 1.0e-12, "incl");
626 assert_close(back.lonper, lonper0, 1.0e-10, "lonper");
627 assert_close(back.nu, nu0, 1.0e-10, "nu");
628
629 let (r2, v2) = coe2rv(&back, MU).unwrap();
630 assert_vec_close(r2, r, 1.0e-8, "r");
631 assert_vec_close(v2, v, 1.0e-11, "v");
632 }
633
634 #[test]
635 fn rejects_bad_inputs() {
636 let r = [7000.0, 0.0, 0.0];
637 let v = [0.0, 7.5, 0.0];
638
639 assert_eq!(rv2coe(r, v, -1.0), Err(ElementsError::NonPositiveMu));
640 assert_eq!(
641 rv2coe([0.0, 0.0, 0.0], v, MU),
642 Err(ElementsError::ZeroPosition)
643 );
644 assert_eq!(
646 rv2coe(r, [7.5, 0.0, 0.0], MU),
647 Err(ElementsError::DegenerateOrbit)
648 );
649 assert!(matches!(
650 rv2coe([f64::NAN, 0.0, 0.0], v, MU),
651 Err(ElementsError::NonFinite { field: "r" })
652 ));
653
654 let coe = ClassicalElements::new(11067.79, 0.1, 0.5, 0.1, 0.2, 0.3);
655 assert_eq!(coe2rv(&coe, 0.0), Err(ElementsError::NonPositiveMu));
656 let mut bad = coe;
657 bad.p = -1.0;
658 assert_eq!(coe2rv(&bad, MU), Err(ElementsError::NonPositiveSemiLatus));
659 }
660}