anise/astro/
utils.rs

1/*
2 * ANISE Toolkit
3 * Copyright (C) 2021-onward Christopher Rabotin <christopher.rabotin@gmail.com> et al. (cf. AUTHORS.md)
4 * This Source Code Form is subject to the terms of the Mozilla Public
5 * License, v. 2.0. If a copy of the MPL was not distributed with this
6 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
7 *
8 * Documentation: https://nyxspace.com/
9 */
10
11use std::f64::consts::{PI, TAU};
12
13use crate::errors::{MathError, PhysicsError};
14
15use super::PhysicsResult;
16
17/// Mean anomaly f64::EPSILON
18pub const MA_EPSILON: f64 = 1e-12;
19
20/// Computes the true anomaly from the given mean anomaly for an orbit.
21///
22/// The computation process varies depending on whether the orbit is elliptical (eccentricity less than or equal to 1)
23/// or hyperbolic (eccentricity greater than 1). In each case, the method uses an iterative algorithm to find a
24/// sufficiently accurate approximation of the true anomaly.
25///
26/// # Arguments
27///
28/// * `ma_radians` - The mean anomaly in radians.
29/// * `ecc` - The eccentricity of the orbit.
30///
31/// # Remarks
32///
33/// This function uses GTDS MathSpec Equations 3-180, 3-181, and 3-186 for the iterative computation process.
34///
35/// Source: GMAT source code (`compute_mean_to_true_anomaly`)
36pub fn compute_mean_to_true_anomaly_rad(ma_radians: f64, ecc: f64) -> PhysicsResult<f64> {
37    let rm = ma_radians;
38    if ecc <= 1.0 {
39        // Elliptical orbit
40        let mut e2 = rm + ecc * rm.sin(); // GTDS MathSpec Equation 3-182
41
42        let mut iter = 0;
43
44        loop {
45            iter += 1;
46            if iter > 1000 {
47                return Err(PhysicsError::AppliedMath {
48                    source: MathError::MaxIterationsReached {
49                        iter,
50                        action: "computing the true anomaly from the mean anomaly",
51                    },
52                });
53            }
54
55            // GTDS MathSpec Equation 3-180  Note: a little difference here is that it uses Cos(E) instead of Cos(E-0.5*f)
56            let normalized_anomaly = 1.0 - ecc * e2.cos();
57
58            if normalized_anomaly.abs() < MA_EPSILON {
59                return Err(PhysicsError::AppliedMath {
60                    source: MathError::DomainError {
61                        value: normalized_anomaly,
62                        msg: "normalized anomaly too small",
63                    },
64                });
65            }
66
67            // GTDS MathSpec Equation 3-181
68            let e1 = e2 - (e2 - ecc * e2.sin() - rm) / normalized_anomaly;
69
70            if (e2 - e1).abs() < MA_EPSILON {
71                break;
72            }
73
74            e2 = e1;
75        }
76
77        let mut e = e2;
78
79        if e < 0.0 {
80            e += TAU;
81        }
82
83        let c = (e - PI).abs();
84
85        let mut ta = if c >= 1.0e-08 {
86            let normalized_anomaly = 1.0 - ecc;
87
88            if (normalized_anomaly).abs() < MA_EPSILON {
89                return Err(PhysicsError::AppliedMath {
90                    source: MathError::DomainError {
91                        value: normalized_anomaly,
92                        msg: "normalized anomaly too small",
93                    },
94                });
95            }
96
97            let eccentricity_ratio = (1.0 + ecc) / normalized_anomaly; // temp2 = (1+ecc)/(1-ecc)
98
99            if eccentricity_ratio < 0.0 {
100                return Err(PhysicsError::AppliedMath {
101                    source: MathError::DomainError {
102                        value: eccentricity_ratio,
103                        msg: "eccentricity ratio too small",
104                    },
105                });
106            }
107
108            let f = eccentricity_ratio.sqrt();
109            let g = (e / 2.0).tan();
110            // tan(TA/2) = Sqrt[(1+ecc)/(1-ecc)] * tan(E/2)
111            2.0 * (f * g).atan()
112        } else {
113            e
114        };
115
116        if ta < 0.0 {
117            ta += TAU;
118        }
119        Ok(ta)
120    } else {
121        //---------------------------------------------------------
122        // hyperbolic orbit
123        //---------------------------------------------------------
124
125        // For hyperbolic orbit, anomaly is nolonger to be an angle so we cannot use mod of 2*PI to mean anomaly.
126        // We need to keep its original value for calculation.
127        //if (rm > PI)                       // incorrect
128        //   rm = rm - TWO_PI;               // incorrect
129
130        //f2 = ecc * Sinh(rm) - rm;          // incorrect
131        //f2 = rm / 2;                       // incorrect  // GTDS MathSpec Equation 3-186
132        let mut f2: f64 = 0.0; // This is the correct initial value for hyperbolic eccentric anomaly.
133        let mut iter = 0;
134
135        loop {
136            iter += 1;
137            if iter > 1000 {
138                return Err(PhysicsError::AppliedMath {
139                    source: MathError::MaxIterationsReached {
140                        iter,
141                        action: "computing the true anomaly from the mean anomaly",
142                    },
143                });
144            }
145
146            let normalizer = ecc * f2.cosh() - 1.0;
147
148            if normalizer.abs() < MA_EPSILON {
149                return Err(PhysicsError::AppliedMath {
150                    source: MathError::DomainError {
151                        value: normalizer,
152                        msg: "normalized anomaly too small (hyperbolic case)",
153                    },
154                });
155            }
156
157            let f1 = f2 - (ecc * f2.sinh() - f2 - rm) / normalizer; // GTDS MathSpec Equation 3-186
158            if (f2 - f1).abs() < MA_EPSILON {
159                break;
160            }
161            f2 = f1;
162        }
163
164        let f = f2;
165        let normalized_anomaly = ecc - 1.0;
166
167        if normalized_anomaly.abs() < MA_EPSILON {
168            return Err(PhysicsError::AppliedMath {
169                source: MathError::DomainError {
170                    value: normalized_anomaly,
171                    msg: "normalized anomaly too small (hyperbolic case)",
172                },
173            });
174        }
175
176        let eccentricity_ratio = (ecc + 1.0) / normalized_anomaly; // temp2 = (ecc+1)/(ecc-1)
177
178        if eccentricity_ratio < 0.0 {
179            return Err(PhysicsError::AppliedMath {
180                source: MathError::DomainError {
181                    value: eccentricity_ratio,
182                    msg: "eccentricity ratio too small (hyperbolic case)",
183                },
184            });
185        }
186
187        let e = eccentricity_ratio.sqrt();
188        let g = (f / 2.0).tanh();
189        let mut ta = 2.0 * (e * g).atan(); // tan(TA/2) = Sqrt[(ecc+1)/(ecc-1)] * Tanh(F/2)    where: F is hyperbolic centric anomaly
190
191        if ta < 0.0 {
192            ta += TAU;
193        }
194        Ok(ta)
195    }
196}