Skip to main content

lox_core/
anomalies.rs

1// SPDX-FileCopyrightText: 2025 Helge Eichhorn <git@helgeeichhorn.de>
2//
3// SPDX-License-Identifier: MPL-2.0
4
5//! Orbital Anomaly Conversions
6//!
7//! This module provides conversions between true anomaly, eccentric anomaly, and mean anomaly
8//! for all conic section orbit types: circular, elliptic, parabolic, and hyperbolic.
9//!
10//! # Conventions
11//! - All angles use the [`Angle`] type, normalized to (-π, π] unless otherwise specified
12//! - Eccentricity uses the [`Eccentricity`] type which validates e >= 0
13//! - Orbit types: circular (e≈0), elliptic (0<e<1), parabolic (e≈1), hyperbolic (e>1)
14//! - True anomaly (ν): angle from periapsis to the position vector
15//! - Eccentric anomaly (E): auxiliary angle for elliptic orbits, range (-π, π]
16//! - Hyperbolic anomaly (F): hyperbolic angle for hyperbolic orbits (unbounded)
17//! - Parabolic anomaly (D): tan(ν/2) for parabolic orbits (unbounded)
18//! - Mean anomaly (M): time-like parameter proportional to time since periapsis, range (-π, π]
19//!
20//! # Anomaly Enum Convention
21//! The [`Anomaly`] enum uses an overloaded convention for `Anomaly::Eccentric`:
22//! - For elliptic/circular orbits: represents eccentric anomaly E
23//! - For hyperbolic orbits: represents hyperbolic anomaly F
24//! - For parabolic orbits: represents parabolic anomaly D (stored as radians)
25
26use lox_test_utils::approx_eq::{ApproxEq, ApproxEqResults};
27use thiserror::Error;
28
29use crate::{
30    elements::{Eccentricity, OrbitType},
31    units::{Angle, AngleUnits},
32};
33
34use core::marker::PhantomData;
35use std::{
36    fmt::Display,
37    ops::{Add, Neg, Sub},
38};
39
40/// Sealed marker trait for anomaly kinds
41pub trait AnomalyKind: sealed::Sealed + std::fmt::Debug {}
42
43mod sealed {
44    /// Sealed trait to prevent external implementation
45    pub trait Sealed {}
46    impl Sealed for super::TrueKind {}
47    impl Sealed for super::EccentricKind {}
48    impl Sealed for super::MeanKind {}
49}
50
51/// Marker type for true anomaly
52#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
53#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
54pub struct TrueKind;
55
56/// Marker type for eccentric anomaly (E for elliptic, F for hyperbolic, D for parabolic)
57#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
58#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
59pub struct EccentricKind;
60
61/// Marker type for mean anomaly
62#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
63#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
64pub struct MeanKind;
65
66impl AnomalyKind for TrueKind {}
67impl AnomalyKind for EccentricKind {}
68impl AnomalyKind for MeanKind {}
69
70/// Generic anomaly type parameterized by kind marker
71#[derive(Debug, Clone, Copy, Default, PartialEq, PartialOrd)]
72#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
73#[cfg_attr(feature = "serde", serde(bound = ""))]
74pub struct Anomaly<K: AnomalyKind> {
75    anomaly: Angle,
76    _kind: PhantomData<K>,
77}
78
79impl<K> Display for Anomaly<K>
80where
81    K: AnomalyKind,
82{
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        self.anomaly.fmt(f)
85    }
86}
87
88impl<K> Anomaly<K>
89where
90    K: AnomalyKind,
91{
92    /// Returns the angle value
93    pub fn as_angle(self) -> Angle {
94        self.anomaly
95    }
96
97    /// Returns the raw f64 value
98    pub fn as_f64(self) -> f64 {
99        self.anomaly.as_f64()
100    }
101}
102
103impl<K> Add for Anomaly<K>
104where
105    K: AnomalyKind,
106{
107    type Output = Self;
108
109    fn add(self, rhs: Self) -> Self::Output {
110        Anomaly {
111            anomaly: self.anomaly + rhs.anomaly,
112            _kind: PhantomData,
113        }
114    }
115}
116
117impl<K> Sub for Anomaly<K>
118where
119    K: AnomalyKind,
120{
121    type Output = Self;
122
123    fn sub(self, rhs: Self) -> Self::Output {
124        Anomaly {
125            anomaly: self.anomaly - rhs.anomaly,
126            _kind: PhantomData,
127        }
128    }
129}
130
131impl<K> Neg for Anomaly<K>
132where
133    K: AnomalyKind,
134{
135    type Output = Self;
136
137    fn neg(self) -> Self::Output {
138        Anomaly {
139            anomaly: -self.anomaly,
140            _kind: PhantomData,
141        }
142    }
143}
144
145/// True anomaly (ν): angle from periapsis to the position vector
146pub type TrueAnomaly = Anomaly<TrueKind>;
147
148/// Eccentric anomaly: E for elliptic, F for hyperbolic, D for parabolic
149pub type EccentricAnomaly = Anomaly<EccentricKind>;
150
151/// Mean anomaly: M for elliptic/circular, M_h for hyperbolic, M_p for parabolic
152pub type MeanAnomaly = Anomaly<MeanKind>;
153
154// ============================================================================
155// CONVERSION IMPLEMENTATIONS
156// ============================================================================
157
158impl TrueAnomaly {
159    /// Creates a new anomaly
160    pub fn new(anomaly: Angle) -> Self {
161        Self {
162            anomaly,
163            _kind: PhantomData,
164        }
165    }
166
167    /// Converts true anomaly to eccentric anomaly
168    ///
169    /// Can fail for hyperbolic orbits if true anomaly is outside asymptote limits
170    pub fn to_eccentric(self, ecc: Eccentricity) -> Result<EccentricAnomaly> {
171        let orbit = ecc.orbit_type();
172        match orbit {
173            OrbitType::Circular | OrbitType::Elliptic => {
174                Ok(EccentricAnomaly::new(true_to_eccentric(self.anomaly, ecc)))
175            }
176            OrbitType::Hyperbolic => Ok(EccentricAnomaly::new(true_to_hyperbolic(
177                self.anomaly,
178                ecc,
179            )?)),
180            OrbitType::Parabolic => {
181                // For parabolic orbits, Eccentric represents parabolic anomaly D
182                Ok(EccentricAnomaly::new(true_to_parabolic(self.anomaly).rad()))
183            }
184        }
185    }
186
187    /// Converts true anomaly to mean anomaly
188    ///
189    /// Can fail for hyperbolic orbits if true anomaly is outside asymptote limits
190    pub fn to_mean(self, ecc: Eccentricity) -> Result<MeanAnomaly> {
191        let orbit = ecc.orbit_type();
192        match orbit {
193            OrbitType::Circular => {
194                // For circular orbits, mean anomaly equals true anomaly
195                Ok(MeanAnomaly::new(self.anomaly))
196            }
197            OrbitType::Elliptic => Ok(MeanAnomaly::new(true_to_mean(self.anomaly, ecc))),
198            OrbitType::Parabolic => {
199                // For parabolic orbits, store parabolic mean anomaly M_p as angle
200                Ok(MeanAnomaly::new(true_to_mean_parabolic(self.anomaly).rad()))
201            }
202            OrbitType::Hyperbolic => Ok(MeanAnomaly::new(true_to_mean_hyperbolic(
203                self.anomaly,
204                ecc,
205            )?)),
206        }
207    }
208}
209
210impl EccentricAnomaly {
211    /// Creates a new anomaly
212    pub fn new(anomaly: Angle) -> Self {
213        Self {
214            anomaly,
215            _kind: PhantomData,
216        }
217    }
218
219    /// Converts eccentric anomaly to true anomaly.
220    pub fn to_true(self, ecc: Eccentricity) -> TrueAnomaly {
221        let orbit = ecc.orbit_type();
222        match orbit {
223            OrbitType::Circular | OrbitType::Elliptic => {
224                TrueAnomaly::new(eccentric_to_true(self.anomaly, ecc))
225            }
226            OrbitType::Hyperbolic => TrueAnomaly::new(hyperbolic_to_true(self.anomaly, ecc)),
227            OrbitType::Parabolic => {
228                // For parabolic orbits, Eccentric represents parabolic anomaly D
229                TrueAnomaly::new(parabolic_to_true(self.anomaly.as_f64()))
230            }
231        }
232    }
233
234    /// Converts eccentric anomaly to mean anomaly.
235    pub fn to_mean(self, ecc: Eccentricity) -> MeanAnomaly {
236        let orbit = ecc.orbit_type();
237        match orbit {
238            OrbitType::Circular | OrbitType::Elliptic => {
239                MeanAnomaly::new(eccentric_to_mean(self.anomaly, ecc))
240            }
241            OrbitType::Hyperbolic => MeanAnomaly::new(hyperbolic_to_mean(self.anomaly, ecc)),
242            OrbitType::Parabolic => {
243                // For parabolic orbits, Eccentric represents parabolic anomaly D
244                MeanAnomaly::new(parabolic_to_mean(self.anomaly.as_f64()).rad())
245            }
246        }
247    }
248}
249
250impl MeanAnomaly {
251    /// Creates a new anomaly
252    pub fn new(anomaly: Angle) -> Self {
253        Self {
254            anomaly,
255            _kind: PhantomData,
256        }
257    }
258
259    /// Converts mean anomaly to true anomaly
260    ///
261    /// Can fail due to convergence issues in iterative solvers
262    pub fn to_true(self, ecc: Eccentricity) -> Result<TrueAnomaly> {
263        let orbit = ecc.orbit_type();
264        match orbit {
265            OrbitType::Circular => {
266                // For circular orbits, mean anomaly equals true anomaly
267                Ok(TrueAnomaly::new(self.anomaly))
268            }
269            OrbitType::Elliptic => Ok(TrueAnomaly::new(mean_to_true(self.anomaly, ecc)?)),
270            OrbitType::Parabolic => {
271                // For parabolic orbits, value stores M_p
272                Ok(TrueAnomaly::new(mean_parabolic_to_true(
273                    self.anomaly.as_f64(),
274                )))
275            }
276            OrbitType::Hyperbolic => Ok(TrueAnomaly::new(mean_hyperbolic_to_true(
277                self.anomaly,
278                ecc,
279            )?)),
280        }
281    }
282
283    /// Converts mean anomaly to eccentric anomaly
284    ///
285    /// Can fail due to convergence issues in iterative solvers
286    pub fn to_eccentric(self, ecc: Eccentricity) -> Result<EccentricAnomaly> {
287        let orbit = ecc.orbit_type();
288        match orbit {
289            OrbitType::Circular | OrbitType::Elliptic => Ok(EccentricAnomaly::new(
290                mean_to_eccentric(self.anomaly, ecc, None, None)?,
291            )),
292            OrbitType::Hyperbolic => Ok(EccentricAnomaly::new(mean_to_hyperbolic(
293                self.anomaly,
294                ecc,
295                None,
296                None,
297            )?)),
298            OrbitType::Parabolic => {
299                // For parabolic orbits, Eccentric represents parabolic anomaly D
300                Ok(EccentricAnomaly::new(
301                    mean_to_parabolic(self.anomaly.as_f64()).rad(),
302                ))
303            }
304        }
305    }
306}
307
308impl<K: AnomalyKind> ApproxEq for Anomaly<K> {
309    fn approx_eq(&self, rhs: &Self, atol: f64, rtol: f64) -> ApproxEqResults {
310        self.anomaly.approx_eq(&rhs.anomaly, atol, rtol)
311    }
312}
313
314/// Error types for anomaly conversions
315#[derive(Error, Debug, Clone, PartialEq)]
316pub enum AnomalyError {
317    /// The iterative solver did not converge within the allowed iterations.
318    #[error("failed to converge after {iterations} iterations (residual: {residual})")]
319    ConvergenceFailure {
320        /// Number of iterations attempted.
321        iterations: usize,
322        /// Final residual value.
323        residual: f64,
324    },
325    /// The true anomaly is outside the valid range for the given eccentricity.
326    #[error("True anomaly {nu} rad outside valid range [-{max_nu} rad, {max_nu} rad]")]
327    InvalidTrueAnomaly {
328        /// The invalid true anomaly.
329        nu: Angle,
330        /// The maximum valid true anomaly magnitude.
331        max_nu: Angle,
332    },
333}
334
335/// A type alias for `Result<T, AnomalyError>`.
336pub type Result<T> = std::result::Result<T, AnomalyError>;
337
338// ============================================================================
339// ELLIPTIC ORBIT CONVERSIONS (0 < e < 1)
340// ============================================================================
341
342/// Convert true anomaly to eccentric anomaly for elliptic orbits.
343///
344/// Uses the half-angle formula: tan(E/2) = sqrt((1-e)/(1+e)) * tan(ν/2)
345///
346/// # Arguments
347/// * `nu` - True anomaly
348/// * `e` - Eccentricity (should be elliptic: 0 < e < 1)
349///
350/// # Returns
351/// Eccentric anomaly, normalized to (-π, π]
352pub fn true_to_eccentric(nu: Angle, e: Eccentricity) -> Angle {
353    let ecc = e.as_f64();
354    // Half-angle formula (most numerically stable)
355    let factor = ((1.0_f64 - ecc) / (1.0_f64 + ecc)).sqrt();
356    let ecc_half = Angle::from_atan(factor * (nu.as_f64() / 2.0).tan());
357    let ecc_angle = Angle::new(2.0 * ecc_half.as_f64());
358
359    // Normalize to (-π, π]
360    ecc_angle.normalize_two_pi(Angle::ZERO)
361}
362
363/// Convert eccentric anomaly to true anomaly for elliptic orbits.
364///
365/// Uses the half-angle formula: tan(ν/2) = sqrt((1+e)/(1-e)) * tan(E/2)
366///
367/// # Arguments
368/// * `ecc` - Eccentric anomaly
369/// * `e` - Eccentricity (should be elliptic: 0 < e < 1)
370///
371/// # Returns
372/// True anomaly, normalized to (-π, π]
373pub fn eccentric_to_true(ecc: Angle, e: Eccentricity) -> Angle {
374    let ecc_val = e.as_f64();
375    // Half-angle formula
376    let factor = ((1.0_f64 + ecc_val) / (1.0_f64 - ecc_val)).sqrt();
377    let nu_half = Angle::from_atan(factor * (ecc.as_f64() / 2.0).tan());
378    let nu = Angle::new(2.0 * nu_half.as_f64());
379
380    // Normalize to (-π, π]
381    nu.normalize_two_pi(Angle::ZERO)
382}
383
384/// Convert eccentric anomaly to mean anomaly for elliptic orbits.
385///
386/// Uses Kepler's equation: M = E - e*sin(E)
387///
388/// # Arguments
389/// * `ecc` - Eccentric anomaly
390/// * `e` - Eccentricity (should be elliptic: 0 < e < 1)
391///
392/// # Returns
393/// Mean anomaly, normalized to (-π, π]
394pub fn eccentric_to_mean(ecc: Angle, e: Eccentricity) -> Angle {
395    let mean = Angle::new(ecc.as_f64() - e.as_f64() * ecc.sin());
396    mean.normalize_two_pi(Angle::ZERO)
397}
398
399/// Convert mean anomaly to eccentric anomaly for elliptic orbits.
400///
401/// Solves Kepler's equation: M = E - e*sin(E) iteratively using Newton-Raphson method.
402///
403/// # Arguments
404/// * `mean` - Mean anomaly
405/// * `e` - Eccentricity (should be elliptic: 0 < e < 1)
406/// * `tolerance` - Convergence tolerance (default: 1e-10)
407/// * `max_iter` - Maximum iterations (default: 50)
408///
409/// # Returns
410/// Eccentric anomaly
411pub fn mean_to_eccentric(
412    mean: Angle,
413    e: Eccentricity,
414    tolerance: Option<f64>,
415    max_iter: Option<usize>,
416) -> Result<Angle> {
417    let tol = tolerance.unwrap_or(1e-10);
418    let max_iterations = max_iter.unwrap_or(50);
419    let ecc = e.as_f64();
420
421    // Normalize mean anomaly to (-π, π]
422    let m = mean.normalize_two_pi(Angle::ZERO).as_f64();
423
424    // Initial guess (NASA approach)
425    let mut ecc_anomaly = if ecc < 0.8 {
426        m
427    } else if m < Angle::PI.as_f64() {
428        m + ecc / 2.0
429    } else {
430        m - ecc / 2.0
431    };
432
433    // Newton-Raphson iteration with second-order correction
434    for _iteration in 0..max_iterations {
435        let sin_e = ecc_anomaly.sin();
436        let cos_e = ecc_anomaly.cos();
437
438        // Function: f(E) = E - e*sin(E) - M
439        let f = ecc_anomaly - ecc * sin_e - m;
440
441        // First derivative: f'(E) = 1 - e*cos(E)
442        let df = 1.0 - ecc * cos_e;
443
444        // Second-order correction (NASA algorithm)
445        let d_prime = df + 0.5 * f * ecc * sin_e / df;
446
447        let delta = f / d_prime;
448        ecc_anomaly -= delta;
449
450        if delta.abs() < tol {
451            return Ok(Angle::new(ecc_anomaly).normalize_two_pi(Angle::ZERO));
452        }
453    }
454
455    Err(AnomalyError::ConvergenceFailure {
456        iterations: max_iterations,
457        residual: (ecc_anomaly - ecc * ecc_anomaly.sin() - m).abs(),
458    })
459}
460
461/// Convert true anomaly to mean anomaly for elliptic orbits.
462///
463/// This is a convenience function that chains true_to_eccentric and eccentric_to_mean.
464pub fn true_to_mean(nu: Angle, e: Eccentricity) -> Angle {
465    let ecc = true_to_eccentric(nu, e);
466    eccentric_to_mean(ecc, e)
467}
468
469/// Convert mean anomaly to true anomaly for elliptic orbits.
470///
471/// This is a convenience function that chains mean_to_eccentric and eccentric_to_true.
472pub fn mean_to_true(mean: Angle, e: Eccentricity) -> Result<Angle> {
473    let ecc = mean_to_eccentric(mean, e, None, None)?;
474    Ok(eccentric_to_true(ecc, e))
475}
476
477// ============================================================================
478// PARABOLIC ORBIT CONVERSIONS (e = 1)
479// ============================================================================
480
481/// Convert true anomaly to parabolic anomaly.
482///
483/// Parabolic anomaly D = tan(ν/2)
484///
485/// # Arguments
486/// * `nu` - True anomaly
487///
488/// # Returns
489/// Parabolic anomaly (dimensionless, unbounded)
490pub fn true_to_parabolic(nu: Angle) -> f64 {
491    (nu.as_f64() / 2.0).tan()
492}
493
494/// Convert parabolic anomaly to true anomaly.
495///
496/// True anomaly ν = 2*arctan(D)
497///
498/// # Arguments
499/// * `parabolic` - Parabolic anomaly (dimensionless)
500///
501/// # Returns
502/// True anomaly, in range (-π, π)
503pub fn parabolic_to_true(parabolic: f64) -> Angle {
504    Angle::new(2.0 * parabolic.atan())
505}
506
507/// Convert parabolic anomaly to parabolic mean anomaly.
508///
509/// Barker's equation: M_p = D + D³/3
510///
511/// # Arguments
512/// * `parabolic` - Parabolic anomaly (dimensionless)
513///
514/// # Returns
515/// Parabolic mean anomaly
516pub fn parabolic_to_mean(parabolic: f64) -> f64 {
517    parabolic + parabolic.powi(3) / 3.0
518}
519
520/// Convert parabolic mean anomaly to parabolic anomaly.
521///
522/// Solves Barker's equation: M_p = D + D³/3 using the analytical cubic solution.
523///
524/// # Arguments
525/// * `mean_parabolic` - Parabolic mean anomaly
526///
527/// # Returns
528/// Parabolic anomaly
529pub fn mean_to_parabolic(mean_parabolic: f64) -> f64 {
530    // Analytical solution using Cardano's method
531    // Solve: D³ + 3D - 3M_p = 0
532    // Let A = 3M_p/2
533    let a = 1.5 * mean_parabolic;
534
535    // z = cbrt(A + sqrt(A² + 1))
536    let discriminant = a * a + 1.0;
537    let z = (a + discriminant.sqrt()).cbrt();
538
539    // D = z - 1/z
540    z - 1.0 / z
541}
542
543/// Convert true anomaly to parabolic mean anomaly.
544pub fn true_to_mean_parabolic(nu: Angle) -> f64 {
545    let d = true_to_parabolic(nu);
546    parabolic_to_mean(d)
547}
548
549/// Convert parabolic mean anomaly to true anomaly.
550pub fn mean_parabolic_to_true(mean_parabolic: f64) -> Angle {
551    let d = mean_to_parabolic(mean_parabolic);
552    parabolic_to_true(d)
553}
554
555// ============================================================================
556// HYPERBOLIC ORBIT CONVERSIONS (e > 1)
557// ============================================================================
558
559/// Convert true anomaly to hyperbolic eccentric anomaly.
560///
561/// Uses the half-angle formula: tanh(F/2) = sqrt((e-1)/(e+1)) * tan(ν/2)
562///
563/// # Arguments
564/// * `nu` - True anomaly (should be within asymptote limits)
565/// * `e` - Eccentricity (should be hyperbolic: e > 1)
566///
567/// # Returns
568/// Hyperbolic eccentric anomaly (unbounded)
569///
570/// # Errors
571/// Returns `InvalidTrueAnomaly` if true anomaly is outside asymptote limits
572pub fn true_to_hyperbolic(nu: Angle, e: Eccentricity) -> Result<Angle> {
573    let ecc = e.as_f64();
574    // Check if true anomaly is within asymptote limits
575    let nu_max = Angle::from_acos(-1.0 / ecc);
576    if nu.as_f64().abs() >= nu_max.as_f64() {
577        return Err(AnomalyError::InvalidTrueAnomaly { nu, max_nu: nu_max });
578    }
579
580    // Half-angle formula
581    let factor = ((ecc - 1.0_f64) / (ecc + 1.0_f64)).sqrt();
582    let tanh_f_half = factor * (nu.as_f64() / 2.0).tan();
583
584    // F = 2 * atanh(tanh(F/2))
585    let f_half = tanh_f_half.atanh();
586    Ok(Angle::new(2.0 * f_half))
587}
588
589/// Convert hyperbolic eccentric anomaly to true anomaly.
590///
591/// Uses the half-angle formula: tan(ν/2) = sqrt((e+1)/(e-1)) * tanh(F/2)
592///
593/// # Arguments
594/// * `hyperbolic` - Hyperbolic eccentric anomaly
595/// * `e` - Eccentricity (should be hyperbolic: e > 1)
596///
597/// # Returns
598/// True anomaly
599pub fn hyperbolic_to_true(hyperbolic: Angle, e: Eccentricity) -> Angle {
600    let hyperbolic = hyperbolic.as_f64();
601    let ecc = e.as_f64();
602    // Half-angle formula
603    let factor = ((ecc + 1.0) / (ecc - 1.0)).sqrt();
604    let tan_nu_half = factor * (hyperbolic / 2.0).tanh();
605
606    Angle::new(2.0 * tan_nu_half.atan())
607}
608
609/// Convert hyperbolic eccentric anomaly to hyperbolic mean anomaly.
610///
611/// M_h = e*sinh(F) - F
612///
613/// # Arguments
614/// * `hyperbolic` - Hyperbolic eccentric anomaly
615/// * `e` - Eccentricity (should be hyperbolic: e > 1)
616///
617/// # Returns
618/// Hyperbolic mean anomaly
619pub fn hyperbolic_to_mean(hyperbolic: Angle, e: Eccentricity) -> Angle {
620    Angle::new(e.as_f64() * hyperbolic.sinh() - hyperbolic.as_f64())
621}
622
623/// Convert hyperbolic mean anomaly to hyperbolic eccentric anomaly.
624///
625/// Solves: M_h = e*sinh(F) - F iteratively using Newton-Raphson method.
626///
627/// # Arguments
628/// * `mean_hyperbolic` - Hyperbolic mean anomaly
629/// * `e` - Eccentricity (should be hyperbolic: e > 1)
630/// * `tolerance` - Convergence tolerance (default: 1e-10)
631/// * `max_iter` - Maximum iterations (default: 50)
632///
633/// # Returns
634/// Hyperbolic eccentric anomaly
635pub fn mean_to_hyperbolic(
636    mean_hyperbolic: Angle,
637    e: Eccentricity,
638    tolerance: Option<f64>,
639    max_iter: Option<usize>,
640) -> Result<Angle> {
641    let tol = tolerance.unwrap_or(1e-10);
642    let max_iterations = max_iter.unwrap_or(50);
643    let ecc = e.as_f64();
644    let mean_hyperbolic = mean_hyperbolic.as_f64();
645
646    // Initial guess using domain-informed approximation
647    // arcsinh(M/e) provides excellent starting point for hyperbolic case
648    let mut f = (mean_hyperbolic / ecc).asinh();
649
650    // Newton-Raphson iteration
651    for _iteration in 0..max_iterations {
652        let sinh_f = f.sinh();
653        let cosh_f = f.cosh();
654
655        // Function: g(F) = e*sinh(F) - F - M_h
656        let g = ecc * sinh_f - f - mean_hyperbolic;
657
658        // Derivative: g'(F) = e*cosh(F) - 1
659        let dg = ecc * cosh_f - 1.0;
660
661        let delta = g / dg;
662        f -= delta;
663
664        if delta.abs() < tol {
665            return Ok(f.rad());
666        }
667    }
668
669    Err(AnomalyError::ConvergenceFailure {
670        iterations: max_iterations,
671        residual: (ecc * f.sinh() - f - mean_hyperbolic).abs(),
672    })
673}
674
675/// Convert true anomaly to hyperbolic mean anomaly.
676pub fn true_to_mean_hyperbolic(nu: Angle, e: Eccentricity) -> Result<Angle> {
677    let f = true_to_hyperbolic(nu, e)?;
678    Ok(hyperbolic_to_mean(f, e))
679}
680
681/// Convert hyperbolic mean anomaly to true anomaly.
682pub fn mean_hyperbolic_to_true(mean_hyperbolic: Angle, e: Eccentricity) -> Result<Angle> {
683    let f = mean_to_hyperbolic(mean_hyperbolic, e, None, None)?;
684    Ok(hyperbolic_to_true(f, e))
685}
686
687// ============================================================================
688// UTILITY FUNCTIONS
689// ============================================================================
690
691/// Get the maximum true anomaly for a hyperbolic orbit (asymptote angle)
692///
693/// # Arguments
694/// * `e` - Eccentricity (should be hyperbolic: e > 1)
695///
696/// # Returns
697/// Asymptote angle
698pub fn hyperbolic_asymptote_angle(e: Eccentricity) -> Angle {
699    Angle::from_acos(-1.0 / e.as_f64())
700}
701
702#[cfg(test)]
703mod tests {
704    use super::*;
705
706    const EPSILON: f64 = 1e-9;
707
708    #[test]
709    fn test_elliptic_round_trip() {
710        let e = Eccentricity::try_new(0.5).unwrap();
711        let nu = Angle::new(1.0);
712
713        let ecc = true_to_eccentric(nu, e);
714        let nu_back = eccentric_to_true(ecc, e);
715
716        assert!((nu.as_f64() - nu_back.as_f64()).abs() < EPSILON);
717    }
718
719    #[test]
720    fn test_keplers_equation() {
721        let e = Eccentricity::try_new(0.3).unwrap();
722        let ecc = Angle::new(1.5);
723
724        let mean = eccentric_to_mean(ecc, e);
725        let ecc_back = mean_to_eccentric(mean, e, None, None).unwrap();
726
727        assert!((ecc.as_f64() - ecc_back.as_f64()).abs() < EPSILON);
728    }
729
730    #[test]
731    fn test_parabolic_round_trip() {
732        let nu = Angle::new(1.0);
733
734        let d = true_to_parabolic(nu);
735        let nu_back = parabolic_to_true(d);
736
737        assert!((nu.as_f64() - nu_back.as_f64()).abs() < EPSILON);
738    }
739
740    #[test]
741    fn test_barkers_equation() {
742        let d = 0.5;
743
744        let mp = parabolic_to_mean(d);
745        let d_back = mean_to_parabolic(mp);
746
747        assert!((d - d_back).abs() < EPSILON);
748    }
749
750    #[test]
751    fn test_hyperbolic_round_trip() {
752        let e = Eccentricity::try_new(1.5).unwrap();
753        let nu = Angle::new(0.5);
754
755        let f = true_to_hyperbolic(nu, e).unwrap();
756        let nu_back = hyperbolic_to_true(f, e);
757
758        assert!((nu.as_f64() - nu_back.as_f64()).abs() < EPSILON);
759    }
760
761    #[test]
762    fn test_hyperbolic_keplers_equation() {
763        let e = Eccentricity::try_new(2.0).unwrap();
764        let f = 0.8.rad();
765
766        let mh = hyperbolic_to_mean(f, e);
767        let f_back = mean_to_hyperbolic(mh, e, None, None).unwrap();
768
769        assert!((f - f_back).abs().as_f64() < EPSILON);
770    }
771
772    #[test]
773    fn test_periapsis() {
774        let e = Eccentricity::try_new(0.7).unwrap();
775        let nu = Angle::ZERO;
776
777        let ecc = true_to_eccentric(nu, e);
778        assert!(ecc.as_f64().abs() < EPSILON);
779
780        let mean = eccentric_to_mean(ecc, e);
781        assert!(mean.as_f64().abs() < EPSILON);
782    }
783
784    #[test]
785    fn test_apoapsis() {
786        let e = Eccentricity::try_new(0.6).unwrap();
787        let nu = Angle::PI;
788
789        let ecc = true_to_eccentric(nu, e);
790        // At apoapsis, eccentric anomaly should also be π (or -π, which is equivalent)
791        // The normalize_two_pi function normalizes to (-π, π], so π stays as π, but -π is also valid
792        let diff = (ecc.as_f64().abs() - Angle::PI.as_f64()).abs();
793        assert!(diff < EPSILON, "Expected π or -π, got {}", ecc.as_f64());
794    }
795}
796
797#[cfg(test)]
798mod anomaly_tests {
799    use super::*;
800
801    const EPSILON: f64 = 1e-9;
802
803    // ========================================================================
804    // ELLIPTIC ORBIT TESTS
805    // ========================================================================
806
807    #[test]
808    fn test_anomaly_elliptic_true_to_eccentric() {
809        let e = Eccentricity::try_new(0.5).unwrap();
810        let nu = Angle::new(1.0);
811
812        let anomaly = TrueAnomaly::new(nu);
813        let eccentric = anomaly.to_eccentric(e).unwrap();
814
815        let true_back = eccentric.to_true(e);
816        assert!((nu.as_f64() - true_back.as_angle().as_f64()).abs() < EPSILON);
817    }
818
819    #[test]
820    fn test_anomaly_elliptic_true_to_mean() {
821        let e = Eccentricity::try_new(0.3).unwrap();
822        let nu = Angle::new(1.5);
823
824        let anomaly = TrueAnomaly::new(nu);
825        let mean = anomaly.to_mean(e).unwrap();
826
827        // Convert back to verify
828        let true_back = mean.to_true(e).unwrap();
829        assert!((nu.as_f64() - true_back.as_angle().as_f64()).abs() < EPSILON);
830    }
831
832    #[test]
833    fn test_anomaly_elliptic_eccentric_to_mean() {
834        let e = Eccentricity::try_new(0.4).unwrap();
835        let ecc = Angle::new(0.8);
836
837        let anomaly = EccentricAnomaly::new(ecc);
838        let mean = anomaly.to_mean(e);
839
840        // Convert back to verify
841        let ecc_back = mean.to_eccentric(e).unwrap();
842        assert!((ecc.as_f64() - ecc_back.as_angle().as_f64()).abs() < EPSILON);
843    }
844
845    #[test]
846    fn test_anomaly_elliptic_mean_to_true() {
847        let e = Eccentricity::try_new(0.6).unwrap();
848        let mean = Angle::new(2.0);
849
850        let anomaly = MeanAnomaly::new(mean);
851        let true_anom = anomaly.to_true(e).unwrap();
852
853        // Convert back to verify
854        let mean_back = true_anom.to_mean(e).unwrap();
855        assert!((mean.as_f64() - mean_back.as_angle().as_f64()).abs() < EPSILON);
856    }
857
858    #[test]
859    fn test_anomaly_elliptic_mean_to_eccentric() {
860        let e = Eccentricity::try_new(0.5).unwrap();
861        let mean = Angle::new(1.2);
862
863        let anomaly = MeanAnomaly::new(mean);
864        let ecc = anomaly.to_eccentric(e).unwrap();
865
866        // Convert back to verify
867        let mean_back = ecc.to_mean(e);
868        assert!((mean.as_f64() - mean_back.as_angle().as_f64()).abs() < EPSILON);
869    }
870
871    // ========================================================================
872    // CIRCULAR ORBIT TESTS
873    // ========================================================================
874
875    #[test]
876    fn test_anomaly_circular_mean_equals_true() {
877        let e = Eccentricity::try_new(0.0).unwrap();
878        let angle = Angle::new(1.5);
879
880        let true_anom = TrueAnomaly::new(angle);
881        let mean = true_anom.to_mean(e).unwrap();
882
883        // For circular orbits, mean anomaly should equal true anomaly
884        assert!((angle.as_f64() - mean.as_angle().as_f64()).abs() < EPSILON);
885
886        // And converting back should give the same value
887        let true_back = mean.to_true(e).unwrap();
888        assert!((angle.as_f64() - true_back.as_angle().as_f64()).abs() < EPSILON);
889    }
890
891    #[test]
892    fn test_anomaly_circular_eccentric_conversion() {
893        let e = Eccentricity::try_new(1e-10).unwrap(); // Nearly circular
894        let nu = Angle::new(0.5);
895
896        let anomaly = TrueAnomaly::new(nu);
897        let ecc = anomaly.to_eccentric(e).unwrap();
898        let true_back = ecc.to_true(e);
899
900        assert!((nu.as_f64() - true_back.as_angle().as_f64()).abs() < EPSILON);
901    }
902
903    // ========================================================================
904    // HYPERBOLIC ORBIT TESTS
905    // ========================================================================
906
907    #[test]
908    fn test_anomaly_hyperbolic_true_to_eccentric() {
909        let e = Eccentricity::try_new(1.5).unwrap();
910        let nu = Angle::new(0.5); // Within asymptote limits
911
912        let anomaly = TrueAnomaly::new(nu);
913        let eccentric = anomaly.to_eccentric(e).unwrap();
914
915        // Convert back to verify
916        let true_back = eccentric.to_true(e);
917        assert!((nu.as_f64() - true_back.as_angle().as_f64()).abs() < EPSILON);
918    }
919
920    #[test]
921    fn test_anomaly_hyperbolic_true_to_mean() {
922        let e = Eccentricity::try_new(2.0).unwrap();
923        let nu = Angle::new(0.8);
924
925        let anomaly = TrueAnomaly::new(nu);
926        let mean = anomaly.to_mean(e).unwrap();
927
928        // Convert back to verify
929        let true_back = mean.to_true(e).unwrap();
930        assert!((nu.as_f64() - true_back.as_angle().as_f64()).abs() < EPSILON);
931    }
932
933    #[test]
934    fn test_anomaly_hyperbolic_eccentric_to_mean() {
935        let e = Eccentricity::try_new(1.8).unwrap();
936        let f = Angle::new(0.5);
937
938        let anomaly = EccentricAnomaly::new(f);
939        let mean = anomaly.to_mean(e);
940
941        // Convert back to verify
942        let ecc_back = mean.to_eccentric(e).unwrap();
943        assert!((f.as_f64() - ecc_back.as_angle().as_f64()).abs() < EPSILON);
944    }
945
946    #[test]
947    fn test_anomaly_hyperbolic_mean_to_true() {
948        let e = Eccentricity::try_new(1.5).unwrap();
949        let mean = Angle::new(1.0);
950
951        let anomaly = MeanAnomaly::new(mean);
952        let true_anom = anomaly.to_true(e).unwrap();
953
954        // Convert back to verify
955        let mean_back = true_anom.to_mean(e).unwrap();
956        assert!((mean.as_f64() - mean_back.as_angle().as_f64()).abs() < EPSILON);
957    }
958
959    #[test]
960    fn test_anomaly_hyperbolic_asymptote_error() {
961        let e = Eccentricity::try_new(1.5).unwrap();
962        let nu_max = hyperbolic_asymptote_angle(e);
963
964        // Try to convert true anomaly beyond asymptote limit
965        let nu_invalid = Angle::new(nu_max.as_f64() + 0.1);
966        let anomaly = TrueAnomaly::new(nu_invalid);
967
968        let result = anomaly.to_eccentric(e);
969        assert!(result.is_err());
970        match result {
971            Err(AnomalyError::InvalidTrueAnomaly { .. }) => (),
972            _ => panic!("Expected InvalidTrueAnomaly error"),
973        }
974    }
975
976    // ========================================================================
977    // PARABOLIC ORBIT TESTS
978    // ========================================================================
979
980    #[test]
981    fn test_anomaly_parabolic_true_to_mean() {
982        let e = Eccentricity::try_new(1.0).unwrap();
983        let nu = Angle::new(1.0);
984
985        let anomaly = TrueAnomaly::new(nu);
986        let mean = anomaly.to_mean(e).unwrap();
987
988        // Convert back to verify
989        let true_back = mean.to_true(e).unwrap();
990        assert!((nu.as_f64() - true_back.as_angle().as_f64()).abs() < EPSILON);
991    }
992
993    #[test]
994    fn test_anomaly_parabolic_mean_to_true() {
995        let e = Eccentricity::try_new(1.0).unwrap();
996        let mean = Angle::new(0.5);
997
998        let anomaly = MeanAnomaly::new(mean);
999        let true_anom = anomaly.to_true(e).unwrap();
1000
1001        // Convert back to verify
1002        let mean_back = true_anom.to_mean(e).unwrap();
1003        assert!((mean.as_f64() - mean_back.as_angle().as_f64()).abs() < EPSILON);
1004    }
1005
1006    #[test]
1007    fn test_anomaly_parabolic_eccentric_round_trip() {
1008        let e = Eccentricity::try_new(1.0).unwrap();
1009        let d = 0.5; // Parabolic anomaly D
1010
1011        // For parabolic orbits, Eccentric stores parabolic anomaly D
1012        let anomaly = EccentricAnomaly::new(d.rad());
1013
1014        // Convert to true anomaly
1015        let true_anom = anomaly.to_true(e);
1016
1017        // Convert back to eccentric (parabolic anomaly D)
1018        let ecc_back = true_anom.to_eccentric(e).unwrap();
1019
1020        assert!((d - ecc_back.as_angle().as_f64()).abs() < EPSILON);
1021    }
1022
1023    #[test]
1024    fn test_anomaly_parabolic_eccentric_to_mean() {
1025        let e = Eccentricity::try_new(1.0).unwrap();
1026        let d = 0.5; // Parabolic anomaly D
1027
1028        // For parabolic orbits, Eccentric stores parabolic anomaly D
1029        let anomaly = EccentricAnomaly::new(d.rad());
1030
1031        // Convert to mean anomaly
1032        let mean = anomaly.to_mean(e);
1033
1034        // Convert back to eccentric (parabolic anomaly D)
1035        let ecc_back = mean.to_eccentric(e).unwrap();
1036
1037        assert!((d - ecc_back.as_angle().as_f64()).abs() < EPSILON);
1038    }
1039
1040    // ========================================================================
1041    // ROUND-TRIP TESTS
1042    // ========================================================================
1043
1044    #[test]
1045    fn test_anomaly_elliptic_full_round_trip() {
1046        let e = Eccentricity::try_new(0.5).unwrap();
1047        let nu = Angle::new(1.2);
1048
1049        // True -> Eccentric -> Mean -> Eccentric -> True
1050        let true_anom = TrueAnomaly::new(nu);
1051        let ecc = true_anom.to_eccentric(e).unwrap();
1052        let mean = ecc.to_mean(e);
1053        let ecc2 = mean.to_eccentric(e).unwrap();
1054        let true_back = ecc2.to_true(e);
1055
1056        assert!((nu.as_f64() - true_back.as_angle().as_f64()).abs() < EPSILON);
1057    }
1058
1059    #[test]
1060    fn test_anomaly_hyperbolic_full_round_trip() {
1061        let e = Eccentricity::try_new(2.0).unwrap();
1062        let nu = Angle::new(0.5);
1063
1064        // True -> Eccentric -> Mean -> Eccentric -> True
1065        let true_anom = TrueAnomaly::new(nu);
1066        let ecc = true_anom.to_eccentric(e).unwrap();
1067        let mean = ecc.to_mean(e);
1068        let ecc2 = mean.to_eccentric(e).unwrap();
1069        let true_back = ecc2.to_true(e);
1070
1071        assert!((nu.as_f64() - true_back.as_angle().as_f64()).abs() < EPSILON);
1072    }
1073}