#![allow(clippy::approx_constant)]
mod earth;
mod jupiter;
mod mars;
mod mercury;
mod neptune;
mod saturn;
mod uranus;
mod venus;
use crate::calendar::julian_centuries;
use crate::coords::{normalize_degrees, rad_to_deg};
use crate::error::{JyotishError, Result};
use crate::num::KahanSum;
use crate::planet::Planet;
#[derive(Clone, Copy)]
pub(crate) struct VsopTerm {
pub a: f64,
pub b: f64,
pub c: f64,
}
fn evaluate_series(sub_series: &[&[VsopTerm]], tau: f64) -> f64 {
let mut result = KahanSum::new();
let mut tau_power = 1.0;
for &terms in sub_series {
let mut sub_sum = KahanSum::new();
for term in terms {
sub_sum.add(term.a * (term.b + term.c * tau).cos());
}
result.add(sub_sum.sum() * tau_power);
tau_power *= tau;
}
result.sum()
}
pub fn earth_heliocentric(jd: f64) -> (f64, f64, f64) {
let t = julian_centuries(jd);
let tau = t / 10.0;
let l = evaluate_series(&earth::L_SERIES, tau);
let b = evaluate_series(&earth::B_SERIES, tau);
let r = evaluate_series(&earth::R_SERIES, tau);
(normalize_degrees(rad_to_deg(l)), rad_to_deg(b), r)
}
pub fn planet_heliocentric(planet: Planet, jd: f64) -> Result<(f64, f64, f64)> {
let t = julian_centuries(jd);
let tau = t / 10.0;
let (l_series, b_series, r_series) = match planet {
Planet::Mercury => (
mercury::L_SERIES.as_slice(),
mercury::B_SERIES.as_slice(),
mercury::R_SERIES.as_slice(),
),
Planet::Venus => (
venus::L_SERIES.as_slice(),
venus::B_SERIES.as_slice(),
venus::R_SERIES.as_slice(),
),
Planet::Mars => (
mars::L_SERIES.as_slice(),
mars::B_SERIES.as_slice(),
mars::R_SERIES.as_slice(),
),
Planet::Jupiter => (
jupiter::L_SERIES.as_slice(),
jupiter::B_SERIES.as_slice(),
jupiter::R_SERIES.as_slice(),
),
Planet::Saturn => (
saturn::L_SERIES.as_slice(),
saturn::B_SERIES.as_slice(),
saturn::R_SERIES.as_slice(),
),
Planet::Uranus => (
uranus::L_SERIES.as_slice(),
uranus::B_SERIES.as_slice(),
uranus::R_SERIES.as_slice(),
),
Planet::Neptune => (
neptune::L_SERIES.as_slice(),
neptune::B_SERIES.as_slice(),
neptune::R_SERIES.as_slice(),
),
Planet::Sun | Planet::Moon | Planet::Pluto => {
return Err(JyotishError::InvalidParameter(format!(
"{planet} is not supported by VSOP87"
)));
}
};
let l = evaluate_series(l_series, tau);
let b = evaluate_series(b_series, tau);
let r = evaluate_series(r_series, tau);
Ok((normalize_degrees(rad_to_deg(l)), rad_to_deg(b), r))
}
#[cfg(test)]
mod tests {
use super::*;
const JD_J2000: f64 = 2_451_545.0;
#[test]
fn earth_heliocentric_j2000() {
let (lon, lat, r) = earth_heliocentric(JD_J2000);
assert!(
(lon - 100.47).abs() < 0.5,
"Earth lon = {lon}, expected ~100.47"
);
assert!(lat.abs() < 0.01, "Earth lat = {lat}");
assert!((r - 0.983).abs() < 0.02, "Earth r = {r}, expected ~0.983");
}
#[test]
fn sun_geocentric_from_earth() {
let (e_lon, e_lat, e_r) = earth_heliocentric(JD_J2000);
let sun_lon = normalize_degrees(e_lon + 180.0);
assert!(
(sun_lon - 280.47).abs() < 0.5,
"Sun lon = {sun_lon}, expected ~280.47"
);
let sun_lat = -e_lat;
assert!(sun_lat.abs() < 0.01, "Sun lat = {sun_lat}");
assert!(e_r > 0.98 && e_r < 1.02);
}
#[test]
fn mars_heliocentric_j2000() {
let (lon, lat, r) = planet_heliocentric(Planet::Mars, JD_J2000).unwrap();
assert!((0.0..360.0).contains(&lon), "Mars lon = {lon}");
assert!(lat.abs() < 10.0, "Mars lat = {lat}");
assert!(r > 1.3 && r < 1.7, "Mars r = {r}");
}
#[test]
fn all_vsop87_planets() {
for planet in [
Planet::Mercury,
Planet::Venus,
Planet::Mars,
Planet::Jupiter,
Planet::Saturn,
Planet::Uranus,
Planet::Neptune,
] {
let (lon, lat, r) = planet_heliocentric(planet, JD_J2000).unwrap();
assert!((0.0..360.0).contains(&lon), "{planet} lon = {lon}");
assert!(lat.abs() < 15.0, "{planet} lat = {lat}");
assert!(r > 0.3 && r < 35.0, "{planet} r = {r}");
}
}
#[test]
fn pluto_rejected() {
assert!(planet_heliocentric(Planet::Pluto, JD_J2000).is_err());
}
#[test]
fn sun_moon_rejected() {
assert!(planet_heliocentric(Planet::Sun, JD_J2000).is_err());
assert!(planet_heliocentric(Planet::Moon, JD_J2000).is_err());
}
}