use std::f64::consts::{PI, TAU};
use crate::errors::{MathError, PhysicsError};
use super::PhysicsResult;
pub const MA_EPSILON: f64 = 1e-12;
#[macro_export]
macro_rules! f64_eq {
($x:expr, $val:expr, $msg:expr) => {
f64_eq_tol!($x, $val, 1e-10, $msg)
};
}
#[macro_export]
macro_rules! f64_eq_tol {
($x:expr, $val:expr, $tol:expr, $msg:expr) => {
assert!(
($x - $val).abs() < $tol,
"{}: {:.2e}\tgot: {}\twant: {}",
$msg,
($x - $val).abs(),
$x,
$val
)
};
}
#[deprecated(since = "0.7.1", note = "use mean_anomaly_to_true_anomaly instead")]
pub fn compute_mean_to_true_anomaly_rad(ma_radians: f64, ecc: f64) -> PhysicsResult<f64> {
mean_anomaly_to_true_anomaly_rad(ma_radians, ecc)
}
pub fn mean_anomaly_to_true_anomaly_rad(ma_radians: f64, ecc: f64) -> PhysicsResult<f64> {
let rm = ma_radians;
if ecc <= 1.0 {
let mut e2 = rm + ecc * rm.sin();
let mut iter = 0;
loop {
iter += 1;
if iter > 1000 {
return Err(PhysicsError::AppliedMath {
source: MathError::MaxIterationsReached {
iter,
action: "computing the true anomaly from the mean anomaly",
},
});
}
let normalized_anomaly = 1.0 - ecc * e2.cos();
if normalized_anomaly.abs() < MA_EPSILON {
return Err(PhysicsError::AppliedMath {
source: MathError::DomainError {
value: normalized_anomaly,
msg: "normalized anomaly too small",
},
});
}
let e1 = e2 - (e2 - ecc * e2.sin() - rm) / normalized_anomaly;
if (e2 - e1).abs() < MA_EPSILON {
break;
}
e2 = e1;
}
let mut e = e2;
if e < 0.0 {
e += TAU;
}
let c = (e - PI).abs();
let mut ta = if c >= 1.0e-08 {
let normalized_anomaly = 1.0 - ecc;
if (normalized_anomaly).abs() < MA_EPSILON {
return Err(PhysicsError::AppliedMath {
source: MathError::DomainError {
value: normalized_anomaly,
msg: "normalized anomaly too small",
},
});
}
let eccentricity_ratio = (1.0 + ecc) / normalized_anomaly;
if eccentricity_ratio < 0.0 {
return Err(PhysicsError::AppliedMath {
source: MathError::DomainError {
value: eccentricity_ratio,
msg: "eccentricity ratio too small",
},
});
}
let f = eccentricity_ratio.sqrt();
let g = (e / 2.0).tan();
2.0 * (f * g).atan()
} else {
e
};
if ta < 0.0 {
ta += TAU;
}
Ok(ta)
} else {
let mut f2: f64 = 0.0; let mut iter = 0;
loop {
iter += 1;
if iter > 1000 {
return Err(PhysicsError::AppliedMath {
source: MathError::MaxIterationsReached {
iter,
action: "computing the true anomaly from the mean anomaly",
},
});
}
let normalizer = ecc * f2.cosh() - 1.0;
if normalizer.abs() < MA_EPSILON {
return Err(PhysicsError::AppliedMath {
source: MathError::DomainError {
value: normalizer,
msg: "normalized anomaly too small (hyperbolic case)",
},
});
}
let f1 = f2 - (ecc * f2.sinh() - f2 - rm) / normalizer; if (f2 - f1).abs() < MA_EPSILON {
break;
}
f2 = f1;
}
let f = f2;
let normalized_anomaly = ecc - 1.0;
if normalized_anomaly.abs() < MA_EPSILON {
return Err(PhysicsError::AppliedMath {
source: MathError::DomainError {
value: normalized_anomaly,
msg: "normalized anomaly too small (hyperbolic case)",
},
});
}
let eccentricity_ratio = (ecc + 1.0) / normalized_anomaly;
if eccentricity_ratio < 0.0 {
return Err(PhysicsError::AppliedMath {
source: MathError::DomainError {
value: eccentricity_ratio,
msg: "eccentricity ratio too small (hyperbolic case)",
},
});
}
let e = eccentricity_ratio.sqrt();
let g = (f / 2.0).tanh();
let mut ta = 2.0 * (e * g).atan();
if ta < 0.0 {
ta += TAU;
}
Ok(ta)
}
}
pub fn true_anomaly_to_mean_anomaly_rad(nu_rad: f64, ecc: f64) -> Result<f64, MathError> {
let e_rad = true_anomaly_to_eccentric_anomaly_rad(nu_rad, ecc)?;
let m_rad = if ecc < 1.0 {
e_rad - ecc * e_rad.sin()
} else {
ecc * e_rad.sinh() - e_rad
};
let mut m_normalized_rad = m_rad % TAU;
if m_normalized_rad < 0.0 {
m_normalized_rad += TAU;
}
Ok(m_normalized_rad)
}
pub fn true_anomaly_to_eccentric_anomaly_rad(nu_rad: f64, ecc: f64) -> Result<f64, MathError> {
if ecc < 0.0 {
return Err(MathError::DomainError {
value: ecc,
msg: "eccentricity cannot be negative",
});
}
if ecc < 1.0 {
let e_num = (1.0 - ecc * ecc).sqrt() * nu_rad.sin();
let e_den = ecc + nu_rad.cos();
Ok(e_num.atan2(e_den))
} else {
if (ecc + 1.0).abs() < f64::EPSILON {
return Err(MathError::DivisionByZero {
action: "computing hyperbolic eccentric anomaly, (e + 1.0) is zero",
});
}
let factor_sqrt = (ecc - 1.0) / (ecc + 1.0);
if factor_sqrt < 0.0 {
return Err(MathError::DomainError {
value: factor_sqrt,
msg: "argument for sqrt in hyperbolic case is negative",
});
}
let tan_nu_half = (nu_rad / 2.0).tan();
let atanh_arg = factor_sqrt.sqrt() * tan_nu_half;
if atanh_arg >= 1.0 || atanh_arg <= -1.0 {
if atanh_arg.is_nan() {
return Err(MathError::DomainError {
value: atanh_arg,
msg: "atanh argument is NaN in hyperbolic eccentric anomaly calculation",
});
}
return Err(MathError::DomainError {
value: atanh_arg,
msg: "atanh argument out of domain (-1, 1) in hyperbolic eccentric anomaly calculation",
});
}
Ok(2.0 * atanh_arg.atanh())
}
}
pub fn mean_anomaly_to_eccentric_anomaly_rad(
ma_rad: f64,
ecc: f64,
tol: f64,
) -> PhysicsResult<f64> {
if !(0.0..1.0).contains(&ecc) {
return Err(PhysicsError::Hyperbolic { ecc });
}
let mut ea = ma_rad;
for _ in 0..1000 {
let f = ea - ecc * ea.sin() - ma_rad;
let f_prime = 1.0 - ecc * ea.cos();
if f_prime.abs() < 1.0e-12 {
return Err(PhysicsError::AppliedMath {
source: MathError::DomainError {
value: f_prime,
msg: "eccentricity anomaly iteration too small",
},
});
}
let delta = f / f_prime;
ea -= delta;
if delta.abs() < tol {
return Ok(ea);
}
}
Err(PhysicsError::AppliedMath {
source: MathError::MaxIterationsReached {
iter: 1000,
action: "computing the eccentricity anomaly from the mean anomaly",
},
})
}
#[cfg(test)]
mod ut_utils {
use super::*;
use crate::errors::MathError;
use std::f64::consts::PI;
const TEST_EPS: f64 = 1e-9;
#[test]
fn test_ta_to_ea_elliptical() {
let ecc = 0.5;
let res1 = true_anomaly_to_eccentric_anomaly_rad(0.0, ecc);
f64_eq_tol!(res1.unwrap(), 0.0, TEST_EPS, "");
let res2 = true_anomaly_to_eccentric_anomaly_rad(PI, ecc);
f64_eq_tol!(res2.unwrap(), PI, TEST_EPS, "");
let res3 = true_anomaly_to_eccentric_anomaly_rad(PI / 2.0, ecc);
f64_eq_tol!(res3.unwrap(), PI / 3.0, TEST_EPS, "");
}
#[test]
fn test_ta_to_ea_circular() {
let ecc = 0.0;
let res1 = true_anomaly_to_eccentric_anomaly_rad(0.0, ecc);
f64_eq_tol!(res1.unwrap(), 0.0, TEST_EPS, "");
let res2 = true_anomaly_to_eccentric_anomaly_rad(PI / 2.0, ecc);
f64_eq_tol!(res2.unwrap(), PI / 2.0, TEST_EPS, "");
let res3 = true_anomaly_to_eccentric_anomaly_rad(PI, ecc);
f64_eq_tol!(res3.unwrap(), PI, TEST_EPS, "");
}
#[test]
fn test_ta_to_ea_hyperbolic() {
let ecc = 2.0;
let res1 = true_anomaly_to_eccentric_anomaly_rad(0.0, ecc);
f64_eq_tol!(res1.unwrap(), 0.0, TEST_EPS, "");
let res2 = true_anomaly_to_eccentric_anomaly_rad(PI / 3.0, ecc);
f64_eq_tol!(res2.unwrap(), 2.0 * (1.0 / 3.0_f64).atanh(), TEST_EPS, "");
}
#[test]
fn test_ta_to_ea_parabolic() {
let ecc = 1.0;
let res1 = true_anomaly_to_eccentric_anomaly_rad(0.0, ecc);
f64_eq_tol!(res1.unwrap(), 0.0, TEST_EPS, "");
let res2 = true_anomaly_to_eccentric_anomaly_rad(PI / 2.0, ecc);
f64_eq_tol!(res2.unwrap(), 0.0, TEST_EPS, "");
let res3 = true_anomaly_to_eccentric_anomaly_rad(PI - 0.00001, ecc);
f64_eq_tol!(res3.unwrap(), 0.0, TEST_EPS, "");
}
#[test]
fn test_ta_to_ea_hyperbolic_domain_errors() {
let ecc1 = 1.000000001;
let nu1 = PI;
let res1 = true_anomaly_to_eccentric_anomaly_rad(nu1, ecc1);
assert!(res1.is_err());
matches!(res1.err().unwrap(), MathError::DomainError { .. });
let ecc2 = 2.0;
let nu2 = 0.7 * PI;
let res2 = true_anomaly_to_eccentric_anomaly_rad(nu2, ecc2);
assert!(res2.is_err());
matches!(res2.err().unwrap(), MathError::DomainError { .. });
}
#[test]
fn test_ta_to_ea_negative_ecc_error() {
let ecc = -0.1;
let nu = PI / 2.0;
let res = true_anomaly_to_eccentric_anomaly_rad(nu, ecc);
assert!(res.is_err());
assert_eq!(
res.err().unwrap(),
MathError::DomainError {
value: -0.1,
msg: "eccentricity cannot be negative"
}
);
}
#[test]
fn test_ta_to_ma_elliptical() {
let ecc = 0.5;
let res1 = true_anomaly_to_mean_anomaly_rad(0.0, ecc);
f64_eq_tol!(res1.unwrap(), 0.0, TEST_EPS, "");
let res2 = true_anomaly_to_mean_anomaly_rad(PI, ecc);
f64_eq_tol!(res2.unwrap(), PI, TEST_EPS, "");
let ea_for_pi_half = PI / 3.0;
let expected_ma = ea_for_pi_half - ecc * ea_for_pi_half.sin(); let res3 = true_anomaly_to_mean_anomaly_rad(PI / 2.0, ecc);
f64_eq_tol!(res3.unwrap(), expected_ma, TEST_EPS, "");
}
#[test]
fn test_ta_to_ma_circular() {
let ecc = 0.0;
let res1 = true_anomaly_to_mean_anomaly_rad(0.0, ecc);
f64_eq_tol!(res1.unwrap(), 0.0, TEST_EPS, "");
let res2 = true_anomaly_to_mean_anomaly_rad(PI / 2.0, ecc);
f64_eq_tol!(res2.unwrap(), PI / 2.0, TEST_EPS, "");
}
#[test]
fn test_ta_to_ma_hyperbolic() {
let ecc = 2.0;
let res1 = true_anomaly_to_mean_anomaly_rad(0.0, ecc);
f64_eq_tol!(res1.unwrap(), 0.0, TEST_EPS, "");
let ea_for_pi_third = 2.0 * (1.0 / 3.0_f64).atanh();
let expected_ma = ecc * ea_for_pi_third.sinh() - ea_for_pi_third; let res2 = true_anomaly_to_mean_anomaly_rad(PI / 3.0, ecc);
f64_eq_tol!(res2.unwrap(), expected_ma, TEST_EPS, "");
}
#[test]
fn test_ta_to_ma_parabolic() {
let ecc = 1.0;
let res1 = true_anomaly_to_mean_anomaly_rad(0.0, ecc);
f64_eq_tol!(res1.unwrap(), 0.0, TEST_EPS, "");
let res2 = true_anomaly_to_mean_anomaly_rad(PI / 2.0, ecc);
f64_eq_tol!(res2.unwrap(), 0.0, TEST_EPS, "");
}
#[test]
fn test_ta_to_ma_normalization() {
let ecc = 0.1;
let nu = 1.8 * PI;
let ea_expected = true_anomaly_to_eccentric_anomaly_rad(nu, ecc).unwrap();
let m_expected_non_normalized = ea_expected - ecc * ea_expected.sin();
let res = true_anomaly_to_mean_anomaly_rad(nu, ecc);
f64_eq_tol!(
res.unwrap(),
m_expected_non_normalized.rem_euclid(TAU),
TEST_EPS,
""
);
let nu2 = -0.2 * PI; let ea2_expected = true_anomaly_to_eccentric_anomaly_rad(nu2, ecc).unwrap();
let m2_expected_non_normalized = ea2_expected - ecc * ea2_expected.sin();
let res2 = true_anomaly_to_mean_anomaly_rad(nu2, ecc);
f64_eq_tol!(
res2.unwrap(),
m2_expected_non_normalized.rem_euclid(TAU),
TEST_EPS,
""
);
f64_eq_tol!(res.unwrap(), res2.unwrap(), TEST_EPS, ""); }
#[test]
fn test_ta_to_ma_error_propagation() {
let ecc = -0.1;
let nu = PI / 2.0;
let res = true_anomaly_to_mean_anomaly_rad(nu, ecc);
assert!(res.is_err());
assert_eq!(
res.err().unwrap(),
MathError::DomainError {
value: -0.1,
msg: "eccentricity cannot be negative"
}
);
}
}