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}