#![allow(clippy::excessive_precision, clippy::approx_constant)]
use crate::coordinates::RaDec;
use crate::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Planet {
Mercury,
Venus,
Earth,
Mars,
Jupiter,
Saturn,
Uranus,
Neptune,
}
impl Planet {
pub fn name(&self) -> &'static str {
match self {
Planet::Mercury => "Mercury",
Planet::Venus => "Venus",
Planet::Earth => "Earth",
Planet::Mars => "Mars",
Planet::Jupiter => "Jupiter",
Planet::Saturn => "Saturn",
Planet::Uranus => "Uranus",
Planet::Neptune => "Neptune",
}
}
pub fn from_name(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"mercury" => Some(Planet::Mercury),
"venus" => Some(Planet::Venus),
"earth" => Some(Planet::Earth),
"mars" => Some(Planet::Mars),
"jupiter" => Some(Planet::Jupiter),
"saturn" => Some(Planet::Saturn),
"uranus" => Some(Planet::Uranus),
"neptune" => Some(Planet::Neptune),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Vsop87Term {
pub amplitude: f64,
pub phase: f64,
pub frequency: f64,
}
#[derive(Debug, Clone)]
pub struct Vsop87Series {
pub series_0: Vec<Vsop87Term>,
pub series_1: Vec<Vsop87Term>,
pub series_2: Vec<Vsop87Term>,
pub series_3: Vec<Vsop87Term>,
pub series_4: Option<Vec<Vsop87Term>>,
pub series_5: Option<Vec<Vsop87Term>>,
}
#[derive(Debug, Clone)]
pub struct PlanetVsop87Data {
pub longitude: Vsop87Series,
pub latitude: Vsop87Series,
pub radius: Vsop87Series,
}
#[derive(Debug, Clone, Copy)]
pub struct HeliocentricEcliptic {
pub longitude: f64,
pub latitude: f64,
pub radius: f64,
}
pub fn calculate_planet_position(planet: Planet, julian_date: f64) -> Result<RaDec> {
use log::{error, info, warn};
if julian_date.is_nan() {
error!("Invalid Julian Date: NaN (Not a Number)");
return Err(crate::error::AstroError::InvalidTime(
"Julian Date cannot be NaN (Not a Number). Please provide a valid date.".to_string(),
));
}
if julian_date.is_infinite() {
error!("Invalid Julian Date: {} (infinity)", julian_date);
return Err(crate::error::AstroError::InvalidTime(format!(
"Julian Date cannot be infinite (got {}). Please provide a valid date.",
julian_date
)));
}
const MIN_JD: f64 = 1000000.0; const MAX_JD: f64 = 3000000.0; if !(MIN_JD..=MAX_JD).contains(&julian_date) {
warn!("Julian Date {} is outside recommended range [{:.0}, {:.0}]. Results may be inaccurate.",
julian_date, MIN_JD, MAX_JD);
}
const J2000: f64 = 2451545.0;
const REASONABLE_RANGE_CENTURIES: f64 = 20.0; let centuries_from_j2000 = (julian_date - J2000) / 36525.0;
if centuries_from_j2000.abs() > REASONABLE_RANGE_CENTURIES {
warn!("Julian Date {} is {:.2} centuries from J2000.0. VSOP87 accuracy may degrade for extreme dates (>±20 centuries).",
julian_date, centuries_from_j2000);
}
let vsop87_data = get_planet_vsop87_data(planet).ok_or_else(|| {
error!("VSOP87 data not available for {}", planet.name());
crate::error::AstroError::InvalidCoordinate(format!(
"VSOP87 data not available for {}. This planet may not be fully implemented yet.",
planet.name()
))
})?;
info!(
"Calculating {} position at JD {:.6}",
planet.name(),
julian_date
);
let t = (julian_date - J2000) / 365250.0;
info!("Time in Julian millennia from J2000.0: t = {:.10}", t);
let heliocentric = calculate_heliocentric_ecliptic(&vsop87_data, t)?;
info!(
"Heliocentric ecliptic: L={:.10} rad ({:.6}°), B={:.10} rad ({:.6}°), R={:.10} AU",
heliocentric.longitude,
heliocentric.longitude.to_degrees(),
heliocentric.latitude,
heliocentric.latitude.to_degrees(),
heliocentric.radius
);
let earth_vsop87_data = get_planet_vsop87_data(Planet::Earth)
.ok_or_else(|| {
use log::error;
error!("Earth VSOP87 data not available - required for geocentric conversion");
crate::error::AstroError::CalculationError(
"Earth VSOP87 data not available for geocentric conversion. Earth's position is required to convert from heliocentric to geocentric coordinates. Please ensure Earth's VSOP87 coefficients are implemented.".to_string()
)
})?;
let earth_data_is_placeholder = earth_vsop87_data.longitude.series_0.is_empty()
&& earth_vsop87_data.latitude.series_0.is_empty()
&& earth_vsop87_data.radius.series_0.is_empty();
if earth_data_is_placeholder {
warn!("Earth VSOP87 data appears to be placeholder (empty series). Geocentric conversion may produce incorrect results.");
}
let earth_heliocentric = calculate_heliocentric_ecliptic(&earth_vsop87_data, t)
.map_err(|e| {
use log::error;
error!("Failed to calculate Earth's heliocentric position: {}", e);
crate::error::AstroError::CalculationError(
format!("Failed to calculate Earth's position for geocentric conversion: {}. This may indicate invalid VSOP87 data.", e)
)
})?;
info!(
"Earth heliocentric ecliptic: L={:.10} rad ({:.6}°), B={:.10} rad ({:.6}°), R={:.10} AU",
earth_heliocentric.longitude,
earth_heliocentric.longitude.to_degrees(),
earth_heliocentric.latitude,
earth_heliocentric.latitude.to_degrees(),
earth_heliocentric.radius
);
let ra_dec = heliocentric_to_geocentric(heliocentric, earth_heliocentric, julian_date)
.map_err(|e| {
use log::error;
error!("Coordinate conversion failed for {}: {}", planet.name(), e);
crate::error::AstroError::CalculationError(format!(
"Failed to convert {} coordinates from heliocentric to geocentric: {}",
planet.name(),
e
))
})?;
info!(
"{} position calculated: RA={:.6}h, Dec={:.6}°",
planet.name(),
ra_dec.ra,
ra_dec.dec
);
if ra_dec.ra.is_nan() || ra_dec.dec.is_nan() {
warn!("Calculated RA/Dec contains NaN values. This may indicate invalid VSOP87 data or calculation error.");
}
if ra_dec.dec.abs() > 90.0 {
warn!("Calculated declination ({:.6}°) is outside valid range [-90°, +90°]. This may indicate a calculation error.",
ra_dec.dec);
}
Ok(ra_dec)
}
fn calculate_heliocentric_ecliptic(
vsop87_data: &PlanetVsop87Data,
t: f64,
) -> Result<HeliocentricEcliptic> {
use log::{info, warn};
let l = calculate_longitude(&vsop87_data.longitude, t);
info!(
"VSOP87 longitude (L): {:.10} radians ({:.6} degrees)",
l,
l.to_degrees()
);
let b = calculate_latitude(&vsop87_data.latitude, t);
info!(
"VSOP87 latitude (B): {:.10} radians ({:.6} degrees)",
b,
b.to_degrees()
);
let r = calculate_radius(&vsop87_data.radius, t);
info!("VSOP87 radius (R): {:.10} AU", r);
if l.is_nan() || b.is_nan() || r.is_nan() {
warn!("VSOP87 calculation produced NaN values. This may indicate invalid coefficients or time argument.");
return Err(crate::error::AstroError::CalculationError(
"VSOP87 calculation produced NaN (Not a Number) values. Check VSOP87 coefficients and time argument.".to_string()
));
}
if r <= 0.0 {
warn!(
"VSOP87 radius is non-positive ({} AU). This is physically impossible.",
r
);
return Err(crate::error::AstroError::CalculationError(format!(
"VSOP87 radius must be positive, got {} AU. This may indicate invalid coefficients.",
r
)));
}
if b.abs() > std::f64::consts::PI / 2.0 {
warn!(
"VSOP87 latitude ({:.6}°) is outside valid range [-90°, +90°].",
b.to_degrees()
);
}
let l_normalized = l.rem_euclid(2.0 * std::f64::consts::PI);
Ok(HeliocentricEcliptic {
longitude: l_normalized,
latitude: b,
radius: r,
})
}
fn calculate_longitude(longitude_series: &Vsop87Series, t: f64) -> f64 {
evaluate_vsop87_series(longitude_series, t)
}
fn calculate_latitude(latitude_series: &Vsop87Series, t: f64) -> f64 {
evaluate_vsop87_series(latitude_series, t)
}
fn calculate_radius(radius_series: &Vsop87Series, t: f64) -> f64 {
evaluate_vsop87_series(radius_series, t)
}
fn evaluate_vsop87_series(series: &Vsop87Series, t: f64) -> f64 {
use log::debug;
let evaluate_series_terms = |terms: &[Vsop87Term]| -> f64 {
terms.iter().map(|term| evaluate_vsop87_term(term, t)).sum()
};
let s0 = evaluate_series_terms(&series.series_0);
debug!(
"VSOP87 series_0 evaluation: {} terms, result = {:.10}",
series.series_0.len(),
s0
);
let s1 = evaluate_series_terms(&series.series_1);
debug!(
"VSOP87 series_1 evaluation: {} terms, result = {:.10}",
series.series_1.len(),
s1
);
let s2 = evaluate_series_terms(&series.series_2);
debug!(
"VSOP87 series_2 evaluation: {} terms, result = {:.10}",
series.series_2.len(),
s2
);
let s3 = evaluate_series_terms(&series.series_3);
debug!(
"VSOP87 series_3 evaluation: {} terms, result = {:.10}",
series.series_3.len(),
s3
);
let s4 = series
.series_4
.as_ref()
.map(|terms| evaluate_series_terms(terms))
.unwrap_or(0.0);
if series.series_4.is_some() {
debug!(
"VSOP87 series_4 evaluation: {} terms, result = {:.10}",
series.series_4.as_ref().unwrap().len(),
s4
);
}
let s5 = series
.series_5
.as_ref()
.map(|terms| evaluate_series_terms(terms))
.unwrap_or(0.0);
if series.series_5.is_some() {
debug!(
"VSOP87 series_5 evaluation: {} terms, result = {:.10}",
series.series_5.as_ref().unwrap().len(),
s5
);
}
let t2 = t * t;
let t3 = t2 * t;
let t4 = t3 * t;
let t5 = t4 * t;
let result = s0 + s1 * t + s2 * t2 + s3 * t3 + s4 * t4 + s5 * t5;
debug!(
"VSOP87 series combination: t={:.10}, result={:.10}",
t, result
);
result
}
fn evaluate_vsop87_term(term: &Vsop87Term, t: f64) -> f64 {
term.amplitude * (term.phase + term.frequency * t).cos()
}
pub(crate) fn calculate_obliquity(julian_date: f64) -> f64 {
const J2000: f64 = 2451545.0;
let d = julian_date - J2000;
let obliquity_deg = 23.4393 - 0.0000004 * d;
obliquity_deg.to_radians()
}
pub(crate) fn ecliptic_to_equatorial(
heliocentric: HeliocentricEcliptic,
obliquity: f64,
) -> (f64, f64, f64) {
let l = heliocentric.longitude;
let b = heliocentric.latitude;
let r = heliocentric.radius;
let cos_b = b.cos();
let sin_b = b.sin();
let cos_l = l.cos();
let sin_l = l.sin();
let x_ecl = r * cos_b * cos_l;
let y_ecl = r * cos_b * sin_l;
let z_ecl = r * sin_b;
let cos_eps = obliquity.cos();
let sin_eps = obliquity.sin();
let x_eq = x_ecl;
let y_eq = y_ecl * cos_eps - z_ecl * sin_eps;
let z_eq = y_ecl * sin_eps + z_ecl * cos_eps;
(x_eq, y_eq, z_eq)
}
pub(crate) fn heliocentric_to_geocentric_rectangular(
planet_heliocentric_eq: (f64, f64, f64),
earth_heliocentric_eq: (f64, f64, f64),
) -> (f64, f64, f64) {
(
planet_heliocentric_eq.0 - earth_heliocentric_eq.0,
planet_heliocentric_eq.1 - earth_heliocentric_eq.1,
planet_heliocentric_eq.2 - earth_heliocentric_eq.2,
)
}
pub(crate) fn rectangular_to_ra_dec(x: f64, y: f64, z: f64) -> RaDec {
let r = (x * x + y * y + z * z).sqrt();
let ra_rad = y.atan2(x);
let ra_hours =
(ra_rad.rem_euclid(2.0 * std::f64::consts::PI).to_degrees() / 15.0).rem_euclid(24.0);
let dec_rad = if r > 0.0 {
(z / r).asin()
} else {
0.0 };
let dec_degrees = dec_rad.to_degrees();
RaDec {
ra: ra_hours,
dec: dec_degrees,
}
}
fn heliocentric_to_geocentric(
heliocentric: HeliocentricEcliptic,
earth_position: HeliocentricEcliptic,
julian_date: f64,
) -> Result<RaDec> {
use log::debug;
let obliquity = calculate_obliquity(julian_date);
debug!(
"Obliquity of the ecliptic: {:.10} rad ({:.6}°)",
obliquity,
obliquity.to_degrees()
);
let planet_heliocentric_eq = ecliptic_to_equatorial(heliocentric, obliquity);
debug!(
"Planet heliocentric equatorial: x={:.10}, y={:.10}, z={:.10} AU",
planet_heliocentric_eq.0, planet_heliocentric_eq.1, planet_heliocentric_eq.2
);
let earth_heliocentric_eq = ecliptic_to_equatorial(earth_position, obliquity);
debug!(
"Earth heliocentric equatorial: x={:.10}, y={:.10}, z={:.10} AU",
earth_heliocentric_eq.0, earth_heliocentric_eq.1, earth_heliocentric_eq.2
);
let geocentric_eq =
heliocentric_to_geocentric_rectangular(planet_heliocentric_eq, earth_heliocentric_eq);
debug!(
"Geocentric equatorial: x={:.10}, y={:.10}, z={:.10} AU",
geocentric_eq.0, geocentric_eq.1, geocentric_eq.2
);
let ra_dec = rectangular_to_ra_dec(geocentric_eq.0, geocentric_eq.1, geocentric_eq.2);
debug!("Final RA/Dec: RA={:.6}h, Dec={:.6}°", ra_dec.ra, ra_dec.dec);
Ok(ra_dec)
}
pub(crate) fn get_planet_vsop87_data(planet: Planet) -> Option<PlanetVsop87Data> {
match planet {
Planet::Mercury => Some(get_mercury_vsop87_data()),
Planet::Venus => Some(get_venus_vsop87_data()),
Planet::Earth => Some(get_earth_vsop87_data()),
Planet::Mars => Some(get_mars_vsop87_data()),
Planet::Jupiter => Some(get_jupiter_vsop87_data()),
Planet::Saturn => Some(get_saturn_vsop87_data()),
Planet::Uranus => Some(get_uranus_vsop87_data()),
Planet::Neptune => Some(get_neptune_vsop87_data()),
}
}
fn vt(amplitude: f64, phase: f64, frequency: f64) -> Vsop87Term {
Vsop87Term {
amplitude,
phase,
frequency,
}
}
fn series(s0: Vec<Vsop87Term>, s1: Vec<Vsop87Term>, s2: Vec<Vsop87Term>) -> Vsop87Series {
Vsop87Series {
series_0: s0,
series_1: s1,
series_2: s2,
series_3: vec![],
series_4: None,
series_5: None,
}
}
fn get_mercury_vsop87_data() -> PlanetVsop87Data {
let longitude = Vsop87Series {
series_0: vec![
Vsop87Term {
amplitude: 4.40250710144,
phase: 0.0,
frequency: 0.0,
},
Vsop87Term {
amplitude: 0.40989414976,
phase: 1.48302034194,
frequency: 26087.90314157420,
},
Vsop87Term {
amplitude: 0.05046294199,
phase: 4.47785489540,
frequency: 52175.80628314840,
},
Vsop87Term {
amplitude: 0.00855346843,
phase: 1.16520322359,
frequency: 78263.70942462180,
},
Vsop87Term {
amplitude: 0.00165506162,
phase: 4.11969133181,
frequency: 104351.61256629680,
},
],
series_1: vec![
Vsop87Term {
amplitude: 26087.90314157420,
phase: 0.0,
frequency: 0.0,
},
Vsop87Term {
amplitude: 0.01126007832,
phase: 6.21703970996,
frequency: 26087.90314157420,
},
Vsop87Term {
amplitude: 0.00303471395,
phase: 3.05524609620,
frequency: 52175.80628314840,
},
],
series_2: vec![
Vsop87Term {
amplitude: 0.00053049845,
phase: 0.0,
frequency: 0.0,
},
Vsop87Term {
amplitude: 0.00016903658,
phase: 4.69072300649,
frequency: 26087.90314157420,
},
],
series_3: vec![Vsop87Term {
amplitude: 0.00000169496,
phase: 3.20221586859,
frequency: 26087.90314157420,
}],
series_4: Some(vec![
Vsop87Term {
amplitude: 0.00000000000,
phase: 0.0,
frequency: 0.0,
}, ]),
series_5: Some(vec![
Vsop87Term {
amplitude: 0.00000000000,
phase: 0.0,
frequency: 0.0,
}, ]),
};
let latitude = Vsop87Series {
series_0: vec![
Vsop87Term {
amplitude: 0.11737528962,
phase: 1.98357498767,
frequency: 26087.90314157420,
},
Vsop87Term {
amplitude: 0.02388076996,
phase: 5.03738959686,
frequency: 52175.80628314840,
},
Vsop87Term {
amplitude: 0.01222839532,
phase: 3.14159265359,
frequency: 0.0,
},
],
series_1: vec![Vsop87Term {
amplitude: 0.00397535498,
phase: 4.93750888835,
frequency: 26087.90314157420,
}],
series_2: vec![Vsop87Term {
amplitude: 0.00000000000,
phase: 0.0,
frequency: 0.0,
}],
series_3: vec![Vsop87Term {
amplitude: 0.00000000000,
phase: 0.0,
frequency: 0.0,
}],
series_4: None, series_5: None, };
let radius = Vsop87Series {
series_0: vec![
Vsop87Term {
amplitude: 0.39528271652,
phase: 0.0,
frequency: 0.0,
},
Vsop87Term {
amplitude: 0.07834131717,
phase: 6.19233722599,
frequency: 26087.90314157420,
},
Vsop87Term {
amplitude: 0.00795532757,
phase: 2.95989680096,
frequency: 52175.80628314840,
},
],
series_1: vec![Vsop87Term {
amplitude: 0.00217347739,
phase: 4.65617158663,
frequency: 26087.90314157420,
}],
series_2: vec![Vsop87Term {
amplitude: 0.00000000000,
phase: 0.0,
frequency: 0.0,
}],
series_3: vec![Vsop87Term {
amplitude: 0.00000000000,
phase: 0.0,
frequency: 0.0,
}],
series_4: None, series_5: None, };
PlanetVsop87Data {
longitude,
latitude,
radius,
}
}
fn get_venus_vsop87_data() -> PlanetVsop87Data {
PlanetVsop87Data {
longitude: series(
vec![
vt(3.17614666774, 0.0, 0.0),
vt(0.01353968419, 5.59313319619, 10213.28554621100),
vt(0.00089891645, 5.30650047764, 20426.57109242200),
vt(0.00005477201, 4.41630661466, 7860.41939243920),
vt(0.00003455732, 2.69963892930, 11790.62908865880),
vt(0.00002372061, 2.99377538641, 3930.20969621960),
],
vec![
vt(10213.52943052898, 0.0, 0.0),
vt(0.00095707712, 2.46424448979, 10213.28554621100),
vt(0.00002104569, 5.54791255794, 20426.57109242200),
],
vec![
vt(0.00054127076, 0.0, 0.0),
vt(0.00003891460, 0.34514360047, 10213.28554621100),
],
),
latitude: series(
vec![
vt(0.05923638472, 0.26702775812, 10213.28554621100),
vt(0.00040107978, 1.14737178112, 20426.57109242200),
vt(0.00032814918, 3.14159265359, 0.0),
],
vec![vt(0.00287821243, 1.88964962838, 10213.28554621100)],
vec![],
),
radius: series(
vec![
vt(0.72334820891, 0.0, 0.0),
vt(0.00489824182, 4.02151831717, 10213.28554621100),
vt(0.00001658058, 4.90206728031, 20426.57109242200),
],
vec![vt(0.00034551041, 0.89198706276, 10213.28554621100)],
vec![],
),
}
}
fn get_earth_vsop87_data() -> PlanetVsop87Data {
PlanetVsop87Data {
longitude: series(
vec![
vt(1.75347045673, 0.0, 0.0),
vt(0.03341656456, 4.66925680417, 6283.07584999140),
vt(0.00034894275, 4.62610241759, 12566.15169998280),
vt(0.00003497056, 2.74411800971, 5753.38488489680),
vt(0.00003417571, 2.82886579606, 3.52311834900),
vt(0.00003135896, 3.62767041758, 77713.77146812050),
vt(0.00002676218, 4.41808351397, 7860.41939243920),
vt(0.00002342687, 6.13516237631, 3930.20969621960),
vt(0.00001324292, 0.74246356352, 11506.76976979360),
vt(0.00001273166, 2.03709655772, 529.69096509460),
vt(0.00001199167, 1.10962944315, 1577.34354244780),
vt(0.00000990250, 5.23268129594, 5884.92684658320),
vt(0.00000901855, 2.04505446933, 26.29831979980),
vt(0.00000857223, 3.50849152283, 398.14900340820),
],
vec![
vt(6283.31966747491, 0.0, 0.0),
vt(0.00206058863, 2.67823455584, 6283.07584999140),
vt(0.00004303430, 2.63512650414, 12566.15169998280),
],
vec![
vt(0.00052918870, 0.0, 0.0),
vt(0.00008719837, 1.07209665242, 6283.07584999140),
vt(0.00000309125, 0.86728818832, 12566.15169998280),
],
),
latitude: series(
vec![
vt(0.00000279620, 3.19870156017, 84334.66158130829),
vt(0.00000101643, 5.42248619256, 5507.55323866740),
vt(0.00000080445, 3.88013204458, 5223.69391980220),
],
vec![],
vec![],
),
radius: series(
vec![
vt(1.00013988784, 0.0, 0.0),
vt(0.01670699632, 3.09846350258, 6283.07584999140),
vt(0.00013956024, 3.05524609456, 12566.15169998280),
vt(0.00003083720, 5.19846674381, 77713.77146812050),
vt(0.00001628463, 1.17387558054, 5753.38488489680),
vt(0.00001575572, 2.84685214877, 7860.41939243920),
],
vec![
vt(0.00103018608, 1.10748969588, 6283.07584999140),
vt(0.00001721238, 1.06442301418, 12566.15169998280),
],
vec![vt(0.00004359385, 5.78455133738, 6283.07584999140)],
),
}
}
fn get_mars_vsop87_data() -> PlanetVsop87Data {
PlanetVsop87Data {
longitude: series(
vec![
vt(6.20347711581, 0.0, 0.0),
vt(0.18656368093, 5.05037100303, 3340.61242669980),
vt(0.01108216816, 5.40099836958, 6681.22485339960),
vt(0.00091798406, 5.75478744674, 10021.83728009940),
vt(0.00027744987, 5.97049513147, 3.52311834900),
vt(0.00012315897, 0.84956094002, 2810.92146160520),
vt(0.00010610235, 2.93958560338, 2281.23049651060),
vt(0.00008926784, 4.15697846427, 0.01725365220),
],
vec![
vt(3340.61242700512, 0.0, 0.0),
vt(0.01457554523, 3.60433733236, 3340.61242669980),
vt(0.00168414711, 3.92318567804, 6681.22485339960),
vt(0.00020622975, 4.26108844583, 10021.83728009940),
],
vec![
vt(0.00058152577, 2.04961712429, 3340.61242669980),
vt(0.00013459579, 2.45738706163, 6681.22485339960),
],
),
latitude: series(
vec![
vt(0.03197134986, 3.76832042431, 3340.61242669980),
vt(0.00298033234, 4.10616996305, 6681.22485339960),
vt(0.00289104742, 0.0, 0.0),
vt(0.00031365539, 4.44651053090, 10021.83728009940),
],
vec![vt(0.00217310991, 6.04472194776, 3340.61242669980)],
vec![],
),
radius: series(
vec![
vt(1.53033488271, 0.0, 0.0),
vt(0.14184953160, 3.47971283528, 3340.61242669980),
vt(0.00660776362, 3.81783443019, 6681.22485339960),
vt(0.00046179117, 4.15595316782, 10021.83728009940),
],
vec![
vt(0.01107433345, 2.03250524857, 3340.61242669980),
vt(0.00103175887, 3.05705419660, 6681.22485339960),
],
vec![vt(0.00044242249, 0.47930604954, 3340.61242669980)],
),
}
}
fn get_jupiter_vsop87_data() -> PlanetVsop87Data {
PlanetVsop87Data {
longitude: series(
vec![
vt(0.59954691494, 0.0, 0.0),
vt(0.09695898719, 5.06191793158, 529.69096509460),
vt(0.00573610142, 1.44406205629, 7.11354700080),
vt(0.00306389205, 5.41734730184, 1059.38193018920),
vt(0.00097178296, 4.14264726552, 632.78373931320),
vt(0.00072903078, 3.64042916389, 522.57741809380),
vt(0.00064263975, 3.41145165351, 103.09277421860),
vt(0.00039806064, 2.29376740788, 419.48464387520),
],
vec![
vt(529.69096508814, 0.0, 0.0),
vt(0.00489503243, 4.22082939470, 529.69096509460),
vt(0.00228917222, 6.02646855621, 7.11354700080),
vt(0.00030099479, 4.54540782858, 1059.38193018920),
],
vec![
vt(0.00047233601, 4.32148536482, 7.11354700080),
vt(0.00030649436, 2.92977788700, 529.69096509460),
],
),
latitude: series(
vec![
vt(0.02268615702, 3.55852606721, 529.69096509460),
vt(0.00109971634, 3.90809347197, 1059.38193018920),
vt(0.00110090358, 0.0, 0.0),
vt(0.00008101428, 3.60509572885, 522.57741809380),
],
vec![vt(0.00078203446, 1.52377859742, 529.69096509460)],
vec![],
),
radius: series(
vec![
vt(5.20887429326, 0.0, 0.0),
vt(0.25209327119, 3.49108639871, 529.69096509460),
vt(0.00610599976, 3.84115365948, 1059.38193018920),
vt(0.00282029458, 2.57419881293, 632.78373931320),
vt(0.00187647346, 2.07590383214, 522.57741809380),
],
vec![
vt(0.01271801520, 2.64937512894, 529.69096509460),
vt(0.00061661816, 3.00076460387, 1059.38193018920),
],
vec![vt(0.00078203446, 1.52377859742, 529.69096509460)],
),
}
}
fn get_saturn_vsop87_data() -> PlanetVsop87Data {
PlanetVsop87Data {
longitude: series(
vec![
vt(0.87401354025, 0.0, 0.0),
vt(0.11107659762, 3.96205090159, 213.29909543800),
vt(0.01414150957, 4.58581516874, 7.11354700080),
vt(0.00398379389, 0.52112032699, 206.18554843720),
vt(0.00350769243, 3.30329907896, 426.59819087600),
vt(0.00206816305, 0.24658372002, 103.09277421860),
vt(0.00079271300, 3.84007056878, 220.41264243880),
vt(0.00023990355, 4.66976924553, 110.20632121940),
],
vec![
vt(213.29909521690, 0.0, 0.0),
vt(0.01297370862, 1.82834923978, 213.29909543800),
vt(0.00564345393, 2.88499717272, 7.11354700080),
vt(0.00093734369, 1.06311793502, 426.59819087600),
],
vec![
vt(0.00116441330, 1.17988132879, 7.11354700080),
vt(0.00091841837, 0.07325195840, 213.29909543800),
],
),
latitude: series(
vec![
vt(0.04330678039, 3.60284428399, 213.29909543800),
vt(0.00240348302, 2.85238489373, 426.59819087600),
vt(0.00084745939, 0.0, 0.0),
vt(0.00030863357, 3.48441504555, 220.41264243880),
],
vec![vt(0.00397554613, 5.33289992556, 213.29909543800)],
vec![],
),
radius: series(
vec![
vt(9.55758135486, 0.0, 0.0),
vt(0.52921382865, 2.39226219573, 213.29909543800),
vt(0.01873679867, 5.23549604660, 206.18554843720),
vt(0.01464663929, 1.64763042902, 426.59819087600),
vt(0.00821891141, 5.93520042303, 316.39186965660),
],
vec![
vt(0.06182981340, 0.25843511480, 213.29909543800),
vt(0.00506577242, 0.71114625261, 206.18554843720),
],
vec![vt(0.00227714534, 3.41648670629, 213.29909543800)],
),
}
}
fn get_uranus_vsop87_data() -> PlanetVsop87Data {
PlanetVsop87Data {
longitude: series(
vec![
vt(5.48129294297, 0.0, 0.0),
vt(0.09260408234, 0.89106421507, 74.78159856730),
vt(0.01504247898, 3.62719260920, 1.48447270830),
vt(0.00365981674, 1.89962179044, 73.29712585900),
vt(0.00272328168, 3.35823706307, 149.56319713460),
vt(0.00070328461, 5.39254450063, 63.73589830340),
vt(0.00068892678, 6.09292483287, 76.26607127560),
],
vec![
vt(74.78159860910, 0.0, 0.0),
vt(0.00154332863, 5.24158770553, 74.78159856730),
vt(0.00024456474, 1.71260334156, 1.48447270830),
],
vec![
vt(0.00053033510, 0.0, 0.0),
vt(0.00002357844, 2.26014661705, 74.78159856730),
],
),
latitude: series(
vec![
vt(0.01346277648, 2.61877810547, 74.78159856730),
vt(0.00062341400, 5.08111189648, 149.56319713460),
vt(0.00061601196, 3.14159265359, 0.0),
vt(0.00009963722, 1.61603805646, 76.26607127560),
],
vec![vt(0.00034101978, 0.01321929936, 74.78159856730)],
vec![],
),
radius: series(
vec![
vt(19.21264847206, 0.0, 0.0),
vt(0.88784984413, 5.60377527014, 74.78159856730),
vt(0.03440836562, 0.32836099706, 73.29712585900),
vt(0.02055653860, 1.78295159330, 149.56319713460),
vt(0.00649322410, 4.52247285911, 76.26607127560),
],
vec![vt(0.01479896629, 3.67205697578, 74.78159856730)],
vec![],
),
}
}
fn get_neptune_vsop87_data() -> PlanetVsop87Data {
PlanetVsop87Data {
longitude: series(
vec![
vt(5.31188633046, 0.0, 0.0),
vt(0.01798475530, 2.90101273050, 38.13303563780),
vt(0.01019727652, 0.48580922867, 1.48447270830),
vt(0.00124531845, 4.83008090682, 36.64856292950),
vt(0.00042064466, 5.41054993053, 2.96894541660),
vt(0.00037714584, 6.09221808686, 35.16409022120),
],
vec![
vt(38.13303563957, 0.0, 0.0),
vt(0.00016604172, 4.86323329249, 1.48447270830),
vt(0.00015744045, 2.27887427527, 38.13303563780),
],
vec![
vt(0.00053892649, 0.0, 0.0),
vt(0.00000281251, 1.19084538887, 38.13303563780),
],
),
latitude: series(
vec![
vt(0.03088622933, 1.44104372644, 38.13303563780),
vt(0.00027780087, 5.91271884599, 76.26607127560),
vt(0.00027623609, 0.0, 0.0),
vt(0.00015355489, 2.52123799551, 36.64856292950),
],
vec![vt(0.00227279214, 3.80793089870, 38.13303563780)],
vec![],
),
radius: series(
vec![
vt(30.07013206102, 0.0, 0.0),
vt(0.27062259632, 1.32999459377, 38.13303563780),
vt(0.01691764014, 3.25186135653, 36.64856292950),
vt(0.00807830553, 5.18592878704, 1.48447270830),
vt(0.00537760510, 4.52113935896, 35.16409022120),
],
vec![vt(0.00236338502, 0.70497956691, 38.13303563780)],
vec![],
),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_planet_from_str() {
assert_eq!(Planet::from_name("mercury"), Some(Planet::Mercury));
assert_eq!(Planet::from_name("JUPITER"), Some(Planet::Jupiter));
assert_eq!(Planet::from_name("invalid"), None);
}
#[test]
fn test_planet_name() {
assert_eq!(Planet::Mars.name(), "Mars");
assert_eq!(Planet::Saturn.name(), "Saturn");
}
#[test]
fn test_evaluate_vsop87_term() {
let term = Vsop87Term {
amplitude: 1.0,
phase: 0.0,
frequency: 1.0,
};
assert!((evaluate_vsop87_term(&term, 0.0) - 1.0).abs() < 1e-10);
let t = std::f64::consts::PI;
assert!((evaluate_vsop87_term(&term, t) + 1.0).abs() < 1e-10);
}
#[test]
fn test_get_planet_vsop87_data() {
for planet in [
Planet::Mercury,
Planet::Venus,
Planet::Earth,
Planet::Mars,
Planet::Jupiter,
Planet::Saturn,
Planet::Uranus,
Planet::Neptune,
] {
let data = get_planet_vsop87_data(planet);
assert!(
data.is_some(),
"VSOP87 data should be available for {}",
planet.name()
);
}
}
#[test]
fn test_mercury_vsop87_data_structure() {
let data = get_planet_vsop87_data(Planet::Mercury).unwrap();
assert!(
!data.longitude.series_0.is_empty(),
"Mercury L0 series should have terms"
);
assert!(
!data.longitude.series_1.is_empty(),
"Mercury L1 series should have terms"
);
assert!(
data.longitude.series_4.is_some(),
"Mercury L4 series should exist"
);
assert!(
data.longitude.series_5.is_some(),
"Mercury L5 series should exist"
);
assert!(
!data.latitude.series_0.is_empty(),
"Mercury B0 series should have terms"
);
assert!(
data.latitude.series_4.is_none(),
"Mercury B4 series should not exist"
);
assert!(
data.latitude.series_5.is_none(),
"Mercury B5 series should not exist"
);
assert!(
!data.radius.series_0.is_empty(),
"Mercury R0 series should have terms"
);
assert!(
data.radius.series_4.is_none(),
"Mercury R4 series should not exist"
);
assert!(
data.radius.series_5.is_none(),
"Mercury R5 series should not exist"
);
}
#[test]
fn test_vsop87_term_structure() {
let term = Vsop87Term {
amplitude: 1.0,
phase: 0.0,
frequency: 26087.90314157420, };
let result = evaluate_vsop87_term(&term, 0.0);
assert!((result - 1.0).abs() < 1e-10, "Term evaluation should work");
assert!(
term.amplitude > 0.0,
"Amplitude should typically be positive"
);
}
#[test]
fn test_evaluate_vsop87_series() {
let series = Vsop87Series {
series_0: vec![
Vsop87Term {
amplitude: 1.0,
phase: 0.0,
frequency: 0.0,
}, ],
series_1: vec![
Vsop87Term {
amplitude: 0.5,
phase: 0.0,
frequency: 0.0,
}, ],
series_2: vec![], series_3: vec![], series_4: None,
series_5: None,
};
let result_t0 = evaluate_vsop87_series(&series, 0.0);
let expected_t0 = 1.0;
assert!(
(result_t0 - expected_t0).abs() < 1e-10,
"Series at t=0: expected {}, got {}",
expected_t0,
result_t0
);
let result_t1 = evaluate_vsop87_series(&series, 1.0);
let expected_t1 = 1.5;
assert!(
(result_t1 - expected_t1).abs() < 1e-10,
"Series at t=1: expected {}, got {}",
expected_t1,
result_t1
);
}
#[test]
fn test_calculate_heliocentric_ecliptic() {
let data = get_planet_vsop87_data(Planet::Mercury).unwrap();
let t = 0.0;
let result = calculate_heliocentric_ecliptic(&data, t).unwrap();
assert!(
result.longitude >= 0.0 && result.longitude < 2.0 * std::f64::consts::PI,
"Longitude should be in [0, 2π) range"
);
assert!(
result.latitude.abs() < std::f64::consts::PI / 2.0,
"Latitude should be in [-π/2, π/2] range"
);
assert!(
result.radius > 0.0 && result.radius < 2.0,
"Radius should be positive and reasonable for Mercury (< 2 AU)"
);
}
#[test]
fn test_calculate_planet_position_validation() {
let jd = 2451545.0;
let result_nan = calculate_planet_position(Planet::Mercury, f64::NAN);
assert!(
result_nan.is_err(),
"Should return error for NaN Julian Date"
);
let result_inf = calculate_planet_position(Planet::Mercury, f64::INFINITY);
assert!(
result_inf.is_err(),
"Should return error for infinite Julian Date"
);
let result_valid = calculate_planet_position(Planet::Mercury, jd);
match result_valid {
Ok(ra_dec) => {
assert!(
(0.0..24.0).contains(&ra_dec.ra),
"RA should be in [0, 24) hours"
);
assert!(
(-90.0..=90.0).contains(&ra_dec.dec),
"Dec should be in [-90, 90] degrees"
);
}
Err(err) => {
let error_msg = err.to_string();
assert!(
error_msg.contains("Earth") || error_msg.contains("VSOP87"),
"Error should mention Earth or VSOP87 data: {}",
error_msg
);
}
}
}
#[test]
fn test_vsop87_series_evaluation_edge_cases() {
let empty_series = Vsop87Series {
series_0: vec![],
series_1: vec![],
series_2: vec![],
series_3: vec![],
series_4: None,
series_5: None,
};
let result = evaluate_vsop87_series(&empty_series, 0.0);
assert_eq!(result, 0.0, "Empty series should evaluate to 0");
let simple_series = Vsop87Series {
series_0: vec![Vsop87Term {
amplitude: 1.0,
phase: 0.0,
frequency: 0.0,
}],
series_1: vec![],
series_2: vec![],
series_3: vec![],
series_4: None,
series_5: None,
};
let result_t0 = evaluate_vsop87_series(&simple_series, 0.0);
assert!(
(result_t0 - 1.0).abs() < 1e-15,
"Series at t=0 should equal constant term"
);
let result_t20 = evaluate_vsop87_series(&simple_series, 20.0);
assert!(
(result_t20 - 1.0).abs() < 1e-15,
"Series with only constant term should be independent of t"
);
let result_t_neg = evaluate_vsop87_series(&simple_series, -10.0);
assert!(
(result_t_neg - 1.0).abs() < 1e-15,
"Series should handle negative time values"
);
}
#[test]
fn test_vsop87_series_evaluation_trigonometric() {
let trig_series = Vsop87Series {
series_0: vec![Vsop87Term {
amplitude: 1.0,
phase: 0.0,
frequency: 1.0,
}],
series_1: vec![Vsop87Term {
amplitude: 0.5,
phase: std::f64::consts::PI / 2.0,
frequency: 1.0,
}],
series_2: vec![],
series_3: vec![],
series_4: None,
series_5: None,
};
let result_t0 = evaluate_vsop87_series(&trig_series, 0.0);
assert!(
(result_t0 - 1.0).abs() < 1e-10,
"Trigonometric series at t=0 should be correct"
);
let t_pi2 = std::f64::consts::PI / 2.0;
let result_t_pi2 = evaluate_vsop87_series(&trig_series, t_pi2);
let expected = -0.5 * t_pi2;
assert!(
(result_t_pi2 - expected).abs() < 1e-10,
"Trigonometric series at t=π/2 should be correct"
);
}
#[test]
fn test_mercury_heliocentric_j2000() {
let data = get_planet_vsop87_data(Planet::Mercury).unwrap();
let t = 0.0;
let heliocentric = calculate_heliocentric_ecliptic(&data, t).unwrap();
assert!(
heliocentric.longitude >= 0.0 && heliocentric.longitude < 2.0 * std::f64::consts::PI,
"Mercury longitude should be in [0, 2π) range, got {}",
heliocentric.longitude
);
assert!(
heliocentric.latitude.abs() < 0.2, "Mercury latitude should be small (orbit near ecliptic), got {} rad ({:.2}°)",
heliocentric.latitude,
heliocentric.latitude.to_degrees()
);
assert!(
heliocentric.radius.is_finite() && heliocentric.radius > 0.0,
"Mercury radius should be positive and finite, got {} AU",
heliocentric.radius
);
println!(
"Mercury at J2000.0: L={:.6}°, B={:.6}°, R={:.10} AU",
heliocentric.longitude.to_degrees(),
heliocentric.latitude.to_degrees(),
heliocentric.radius
);
}
#[test]
fn test_mercury_heliocentric_multiple_epochs() {
let data = get_planet_vsop87_data(Planet::Mercury).unwrap();
let t_j2000 = 0.0;
let pos_j2000 = calculate_heliocentric_ecliptic(&data, t_j2000).unwrap();
let t_plus1 = 1.0;
let pos_plus1 = calculate_heliocentric_ecliptic(&data, t_plus1).unwrap();
let t_minus1 = -1.0;
let pos_minus1 = calculate_heliocentric_ecliptic(&data, t_minus1).unwrap();
for (epoch, pos) in [
("J2000", pos_j2000),
("+1c", pos_plus1),
("-1c", pos_minus1),
] {
assert!(
pos.longitude >= 0.0 && pos.longitude < 2.0 * std::f64::consts::PI,
"Mercury longitude at {} should be in [0, 2π) range",
epoch
);
assert!(
pos.latitude.abs() < 0.2,
"Mercury latitude at {} should be small",
epoch
);
assert!(
pos.radius.is_finite() && pos.radius > 0.0,
"Mercury radius at {} should be positive and finite, got {}",
epoch,
pos.radius
);
}
let lon_diff = (pos_plus1.longitude - pos_j2000.longitude).abs();
assert!(
lon_diff.is_finite(),
"Mercury longitude difference over 1 century should be finite, got {lon_diff}"
);
}
#[test]
fn test_integration_multiple_planets_same_epoch() {
let _jd = 2451545.0; let t = 0.0;
let planets = [Planet::Mercury];
for planet in planets.iter() {
let data = get_planet_vsop87_data(*planet).unwrap();
let heliocentric = calculate_heliocentric_ecliptic(&data, t).unwrap();
assert!(
heliocentric.longitude >= 0.0
&& heliocentric.longitude < 2.0 * std::f64::consts::PI,
"{} longitude should be in [0, 2π) range",
planet.name()
);
assert!(
heliocentric.radius > 0.0,
"{} radius should be positive",
planet.name()
);
}
}
#[test]
fn test_integration_planets_different_epochs() {
let epochs = [
("J2000.0", 2451545.0),
("2024-01-01", 2460311.0), ("1900-01-01", 2415020.5), ];
for (epoch_name, jd) in epochs.iter() {
let t = (jd - 2451545.0) / 36525.0;
let data = get_planet_vsop87_data(Planet::Mercury).unwrap();
let heliocentric = calculate_heliocentric_ecliptic(&data, t).unwrap();
assert!(
heliocentric.longitude >= 0.0
&& heliocentric.longitude < 2.0 * std::f64::consts::PI,
"Mercury longitude at {} should be in [0, 2π) range",
epoch_name
);
assert!(
heliocentric.radius > 0.0 && heliocentric.radius < 1.0,
"Mercury radius at {} should be reasonable",
epoch_name
);
}
}
#[test]
fn test_vsop87_series_consistency() {
let data = get_planet_vsop87_data(Planet::Mercury).unwrap();
let t = 0.0;
let result1 = evaluate_vsop87_series(&data.longitude, t);
let result2 = evaluate_vsop87_series(&data.longitude, t);
let result3 = evaluate_vsop87_series(&data.longitude, t);
assert!(
(result1 - result2).abs() < 1e-15,
"VSOP87 series evaluation should be deterministic"
);
assert!(
(result2 - result3).abs() < 1e-15,
"VSOP87 series evaluation should be deterministic"
);
}
#[test]
fn test_time_calculation_accuracy() {
const J2000: f64 = 2451545.0;
let jd_j2000 = 2451545.0;
let t_j2000 = (jd_j2000 - J2000) / 36525.0;
assert!(
(t_j2000 - 0.0).abs() < 1e-10,
"Time at J2000.0 should be 0.0 centuries"
);
let jd_plus1c = 2451545.0 + 36525.0;
let t_plus1c = (jd_plus1c - J2000) / 36525.0;
assert!(
(t_plus1c - 1.0).abs() < 1e-10,
"Time 1 century after J2000.0 should be 1.0 centuries"
);
let jd_minus1c = 2451545.0 - 36525.0;
let t_minus1c = (jd_minus1c - J2000) / 36525.0;
assert!(
(t_minus1c + 1.0).abs() < 1e-10,
"Time 1 century before J2000.0 should be -1.0 centuries"
);
}
#[test]
fn test_heliocentric_coordinate_ranges() {
let data = get_planet_vsop87_data(Planet::Mercury).unwrap();
for t in [-10.0, -5.0, -1.0, 0.0, 1.0, 5.0, 10.0] {
let heliocentric = calculate_heliocentric_ecliptic(&data, t).unwrap();
assert!(
heliocentric.longitude >= 0.0
&& heliocentric.longitude < 2.0 * std::f64::consts::PI,
"Longitude at t={} should be in [0, 2π) range, got {}",
t,
heliocentric.longitude
);
assert!(
heliocentric.latitude >= -std::f64::consts::PI / 2.0
&& heliocentric.latitude <= std::f64::consts::PI / 2.0,
"Latitude at t={} should be in [-π/2, π/2] range, got {}",
t,
heliocentric.latitude
);
assert!(
heliocentric.radius > 0.0,
"Radius at t={} should be positive, got {}",
t,
heliocentric.radius
);
}
}
#[test]
fn test_performance_planet_calculation() {
let data = get_planet_vsop87_data(Planet::Mercury).unwrap();
let t = 0.0;
let start = std::time::Instant::now();
let iterations = 1000;
for _ in 0..iterations {
let _ = calculate_heliocentric_ecliptic(&data, t).unwrap();
}
let elapsed = start.elapsed();
let avg_time_ms = elapsed.as_secs_f64() * 1000.0 / iterations as f64;
assert!(
avg_time_ms < 10.0,
"Average calculation time should be < 10ms, got {:.3}ms",
avg_time_ms
);
println!(
"Performance: {} calculations in {:.2}ms, avg {:.3}ms per calculation",
iterations,
elapsed.as_secs_f64() * 1000.0,
avg_time_ms
);
}
#[test]
fn test_performance_vsop87_series_evaluation() {
let data = get_planet_vsop87_data(Planet::Mercury).unwrap();
let t = 0.0;
let start = std::time::Instant::now();
let iterations = 10000;
for _ in 0..iterations {
let _ = evaluate_vsop87_series(&data.longitude, t);
let _ = evaluate_vsop87_series(&data.latitude, t);
let _ = evaluate_vsop87_series(&data.radius, t);
}
let elapsed = start.elapsed();
let avg_time_us = elapsed.as_secs_f64() * 1_000_000.0 / iterations as f64;
assert!(
avg_time_us < 1000.0, "Average series evaluation time should be < 1ms, got {:.3}μs",
avg_time_us
);
println!(
"Performance: {} series evaluations in {:.2}ms, avg {:.3}μs per series",
iterations * 3,
elapsed.as_secs_f64() * 1000.0,
avg_time_us
);
}
#[test]
fn test_calculate_obliquity() {
let jd_j2000 = 2451545.0;
let obliquity = calculate_obliquity(jd_j2000);
let expected_deg = 23.4393;
assert!(
(obliquity.to_degrees() - expected_deg).abs() < 0.01,
"Obliquity at J2000.0 should be ~{:.4}°, got {:.4}°",
expected_deg,
obliquity.to_degrees()
);
let jd_2024 = 2460311.0;
let obliquity_2024 = calculate_obliquity(jd_2024);
assert!(
obliquity_2024 < obliquity,
"Obliquity should decrease over time"
);
}
#[test]
fn test_ecliptic_to_equatorial() {
let jd = 2451545.0;
let obliquity = calculate_obliquity(jd);
let heliocentric = HeliocentricEcliptic {
longitude: 0.0, latitude: 0.0, radius: 1.0, };
let (x, y, z) = ecliptic_to_equatorial(heliocentric, obliquity);
assert!((x - 1.0).abs() < 1e-10, "X coordinate should be 1.0");
assert!(y.abs() < 1e-10, "Y coordinate should be ~0");
assert!(
z.abs() < 1e-10,
"Z coordinate should be ~0 (on equatorial plane)"
);
}
#[test]
fn test_heliocentric_to_geocentric_rectangular() {
let planet_pos = (2.0, 0.0, 0.0); let earth_pos = (1.0, 0.0, 0.0);
let geocentric = heliocentric_to_geocentric_rectangular(planet_pos, earth_pos);
assert!((geocentric.0 - 1.0).abs() < 1e-10, "X should be 1.0");
assert!(geocentric.1.abs() < 1e-10, "Y should be 0");
assert!(geocentric.2.abs() < 1e-10, "Z should be 0");
}
#[test]
fn test_rectangular_to_ra_dec() {
let (x, y, z) = (1.0, 0.0, 0.0);
let ra_dec = rectangular_to_ra_dec(x, y, z);
assert!(
(ra_dec.ra - 0.0).abs() < 1e-10 || (ra_dec.ra - 24.0).abs() < 1e-10,
"RA should be 0h or 24h for point on +X axis"
);
assert!(
ra_dec.dec.abs() < 1e-10,
"Dec should be ~0° for point on equator"
);
let (x, y, z) = (0.0, 1.0, 0.0);
let ra_dec = rectangular_to_ra_dec(x, y, z);
assert!(
(ra_dec.ra - 6.0).abs() < 1e-10,
"RA should be 6h for point on +Y axis, got {}",
ra_dec.ra
);
assert!(ra_dec.dec.abs() < 1e-10, "Dec should be ~0°");
let (x, y, z) = (0.0, 0.0, 1.0);
let ra_dec = rectangular_to_ra_dec(x, y, z);
assert!(
(ra_dec.dec - 90.0).abs() < 1e-10,
"Dec should be 90° for point on +Z axis, got {}",
ra_dec.dec
);
}
#[test]
fn test_heliocentric_to_geocentric_full_pipeline() {
let jd = 2451545.0;
let planet_heliocentric = HeliocentricEcliptic {
longitude: 0.0,
latitude: 0.0,
radius: 2.0, };
let earth_heliocentric = HeliocentricEcliptic {
longitude: 0.0,
latitude: 0.0,
radius: 1.0, };
let result = heliocentric_to_geocentric(planet_heliocentric, earth_heliocentric, jd);
assert!(result.is_ok(), "Conversion should succeed");
let ra_dec = result.unwrap();
assert!(
ra_dec.ra >= 0.0 && ra_dec.ra < 24.0,
"RA should be in [0, 24) hours, got {}",
ra_dec.ra
);
assert!(
ra_dec.dec >= -90.0 && ra_dec.dec <= 90.0,
"Dec should be in [-90, 90] degrees, got {}",
ra_dec.dec
);
}
#[test]
fn test_calculate_planet_position_complete() {
let jd = 2451545.0;
let result = calculate_planet_position(Planet::Mercury, jd);
match result {
Ok(ra_dec) => {
assert!(
(0.0..24.0).contains(&ra_dec.ra),
"RA should be in [0, 24) hours, got {}",
ra_dec.ra
);
assert!(
(-90.0..=90.0).contains(&ra_dec.dec),
"Dec should be in [-90, 90] degrees, got {}",
ra_dec.dec
);
}
Err(err) => {
let error_msg = err.to_string();
assert!(
error_msg.contains("Earth") || error_msg.contains("VSOP87"),
"Error should mention Earth or VSOP87 data"
);
}
}
}
#[test]
fn test_earth_radius_near_one_au() {
let data = get_planet_vsop87_data(Planet::Earth).unwrap();
for t in [-0.05, 0.0, 0.024, 0.05] {
let h = calculate_heliocentric_ecliptic(&data, t).unwrap();
assert!(
h.radius > 0.98 && h.radius < 1.02,
"Earth radius should be ~1 AU, got {} AU at t={}",
h.radius,
t
);
}
}
#[test]
fn test_geocentric_positions_match_reference_j2000() {
let jd = 2451545.0;
let cases = [
(Planet::Mercury, 18.14, -24.4),
(Planet::Venus, 15.99, -18.5),
(Planet::Mars, 22.03, -13.2),
(Planet::Jupiter, 1.59, 8.6),
(Planet::Saturn, 2.58, 12.6),
(Planet::Uranus, 21.17, -17.0),
(Planet::Neptune, 20.36, -19.2),
];
for (planet, ra_h, dec_d) in cases {
let p = calculate_planet_position(planet, jd).unwrap();
let mut dra = (p.ra - ra_h).abs();
if dra > 12.0 {
dra = 24.0 - dra; }
assert!(
dra < 0.5,
"{} RA should be ~{}h, got {}h",
planet.name(),
ra_h,
p.ra
);
assert!(
(p.dec - dec_d).abs() < 1.5,
"{} Dec should be ~{}°, got {}°",
planet.name(),
dec_d,
p.dec
);
}
}
}