#[cfg(feature = "python-tests")]
mod python_tests;
use std::cell::RefCell;
use crate::constants::EARTH_RADIUS;
use crate::jplephem::kernel::SpiceKernel;
use crate::jplephem_ext::SpiceKernelExt;
use crate::searchlib::find_maxima;
use crate::time::Timescale;
const SOLAR_RADIUS_KM: f64 = 696_340.0;
const MOON_RADIUS_KM: f64 = 1_737.1;
const EARTH_RADIUS_KM: f64 = EARTH_RADIUS / 1000.0;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LunarEclipseType {
Penumbral = 0,
Partial = 1,
Total = 2,
}
impl std::fmt::Display for LunarEclipseType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LunarEclipseType::Penumbral => write!(f, "Penumbral"),
LunarEclipseType::Partial => write!(f, "Partial"),
LunarEclipseType::Total => write!(f, "Total"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SolarEclipseType {
Partial = 0,
Annular = 1,
Total = 2,
}
impl std::fmt::Display for SolarEclipseType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SolarEclipseType::Partial => write!(f, "Partial"),
SolarEclipseType::Annular => write!(f, "Annular"),
SolarEclipseType::Total => write!(f, "Total"),
}
}
}
#[derive(Debug, Clone)]
pub struct LunarEclipseInfo {
pub jd_tdb: f64,
pub eclipse_type: LunarEclipseType,
pub closest_approach: f64,
pub umbral_magnitude: f64,
pub penumbral_magnitude: f64,
}
#[derive(Debug, Clone)]
pub struct SolarEclipseInfo {
pub jd_tdb: f64,
pub eclipse_type: SolarEclipseType,
pub closest_approach: f64,
pub magnitude: f64,
}
pub fn lunar_eclipses(
kernel: &mut SpiceKernel,
jd_start: f64,
jd_end: f64,
) -> Vec<LunarEclipseInfo> {
let ts = Timescale::default();
let kernel_cell = RefCell::new(kernel);
let f = |jds: &[f64]| -> Vec<f64> {
let mut k = kernel_cell.borrow_mut();
jds.iter()
.map(|&jd| {
let t = ts.tdb_jd(jd);
let (earth_to_sun_km, earth_to_moon_km) = earth_sun_moon_km(*k, &t);
angle_between_vec(&earth_to_sun_km, &earth_to_moon_km)
})
.collect()
};
let maxima = find_maxima(jd_start, jd_end, &f, 5.0, 1.0 / 86400.0, 12);
let kernel = kernel_cell.into_inner();
let mut eclipses = Vec::new();
for (jd, _) in &maxima {
let t = ts.tdb_jd(*jd);
let (earth_to_sun_km, earth_to_moon_km) = earth_sun_moon_km(kernel, &t);
let moon_to_earth_km = [
-earth_to_moon_km[0],
-earth_to_moon_km[1],
-earth_to_moon_km[2],
];
let moon_dist = vec_len(&moon_to_earth_km);
let sun_dist = vec_len(&earth_to_sun_km);
let pi_m = EARTH_RADIUS_KM / moon_dist; let pi_s = EARTH_RADIUS_KM / sun_dist; let s_s = SOLAR_RADIUS_KM / sun_dist;
let closest_approach = angle_between_vec(&earth_to_sun_km, &moon_to_earth_km);
let moon_radius = (MOON_RADIUS_KM / moon_dist).asin();
let pi_1 = 1.01 * pi_m;
let penumbra_radius = pi_1 + pi_s + s_s;
let umbra_radius = pi_1 + pi_s - s_s;
let penumbral = closest_approach < penumbra_radius + moon_radius;
if !penumbral {
continue;
}
let twice_radius = 2.0 * moon_radius;
let umbral_magnitude = (umbra_radius + moon_radius - closest_approach) / twice_radius;
let penumbral_magnitude = (penumbra_radius + moon_radius - closest_approach) / twice_radius;
let partial = closest_approach < umbra_radius + moon_radius;
let total = closest_approach < umbra_radius - moon_radius;
let eclipse_type = if total {
LunarEclipseType::Total
} else if partial {
LunarEclipseType::Partial
} else {
LunarEclipseType::Penumbral
};
eclipses.push(LunarEclipseInfo {
jd_tdb: *jd,
eclipse_type,
closest_approach,
umbral_magnitude,
penumbral_magnitude,
});
}
eclipses
}
pub fn solar_eclipses(
kernel: &mut SpiceKernel,
jd_start: f64,
jd_end: f64,
) -> Vec<SolarEclipseInfo> {
let ts = Timescale::default();
let kernel_cell = RefCell::new(kernel);
let f = |jds: &[f64]| -> Vec<f64> {
let mut k = kernel_cell.borrow_mut();
jds.iter()
.map(|&jd| {
let t = ts.tdb_jd(jd);
let (earth_to_sun_km, earth_to_moon_km) = earth_sun_moon_km(*k, &t);
-angle_between_vec(&earth_to_sun_km, &earth_to_moon_km)
})
.collect()
};
let maxima = find_maxima(jd_start, jd_end, &f, 5.0, 1.0 / 86400.0, 12);
let kernel = kernel_cell.into_inner();
let mut eclipses = Vec::new();
for (jd, _) in &maxima {
let t = ts.tdb_jd(*jd);
let (earth_to_sun_km, earth_to_moon_km) = earth_sun_moon_km(kernel, &t);
let separation = angle_between_vec(&earth_to_sun_km, &earth_to_moon_km);
let sun_dist = vec_len(&earth_to_sun_km);
let moon_dist = vec_len(&earth_to_moon_km);
let sun_angular_radius = (SOLAR_RADIUS_KM / sun_dist).asin();
let moon_angular_radius = (MOON_RADIUS_KM / moon_dist).asin();
let earth_angular_radius = (EARTH_RADIUS_KM / moon_dist).asin();
let penumbral_half_angle = sun_angular_radius + moon_angular_radius;
let is_eclipse = separation < penumbral_half_angle + earth_angular_radius;
if !is_eclipse {
continue;
}
let magnitude = moon_angular_radius / sun_angular_radius;
let central = separation < earth_angular_radius;
let eclipse_type = if !central {
SolarEclipseType::Partial
} else if magnitude >= 1.0 {
SolarEclipseType::Total
} else {
SolarEclipseType::Annular
};
eclipses.push(SolarEclipseInfo {
jd_tdb: *jd,
eclipse_type,
closest_approach: separation,
magnitude,
});
}
eclipses
}
fn earth_sun_moon_km(kernel: &mut SpiceKernel, t: &crate::time::Time) -> ([f64; 3], [f64; 3]) {
let (sun_pos, _) = kernel.compute_km("sun", t).unwrap();
let (earth_pos, _) = kernel.compute_km("earth", t).unwrap();
let (moon_pos, _) = kernel.compute_km("moon", t).unwrap();
let earth_to_sun = [
sun_pos.x - earth_pos.x,
sun_pos.y - earth_pos.y,
sun_pos.z - earth_pos.z,
];
let earth_to_moon = [
moon_pos.x - earth_pos.x,
moon_pos.y - earth_pos.y,
moon_pos.z - earth_pos.z,
];
(earth_to_sun, earth_to_moon)
}
fn vec_len(v: &[f64; 3]) -> f64 {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
fn angle_between_vec(a: &[f64; 3], b: &[f64; 3]) -> f64 {
let dot = a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
let mag_a = vec_len(a);
let mag_b = vec_len(b);
(dot / (mag_a * mag_b)).clamp(-1.0, 1.0).acos()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::jplephem_ext::SpiceKernelExt;
fn test_kernel() -> SpiceKernel {
SpiceKernel::open("test_data/de421.bsp").expect("de421.bsp required for eclipse tests")
}
#[test]
fn test_lunar_eclipses_2020() {
let mut kernel = test_kernel();
let ts = Timescale::default();
let t0 = ts.tdb((2020, 1, 1, 0, 0, 0.0)).tdb();
let t1 = ts.tdb((2021, 1, 1, 0, 0, 0.0)).tdb();
let eclipses = lunar_eclipses(&mut kernel, t0, t1);
assert!(
eclipses.len() >= 3,
"Expected at least 3 lunar eclipses in 2020, got {}",
eclipses.len()
);
for e in &eclipses {
assert!(
e.penumbral_magnitude > 0.0,
"Eclipse should have positive penumbral magnitude"
);
}
}
#[test]
fn test_lunar_eclipse_types() {
let mut kernel = test_kernel();
let ts = Timescale::default();
let t0 = ts.tdb((2015, 1, 1, 0, 0, 0.0)).tdb();
let t1 = ts.tdb((2016, 1, 1, 0, 0, 0.0)).tdb();
let eclipses = lunar_eclipses(&mut kernel, t0, t1);
assert!(
eclipses.len() >= 2,
"Expected at least 2 lunar eclipses in 2015, got {}",
eclipses.len()
);
let has_total = eclipses
.iter()
.any(|e| e.eclipse_type == LunarEclipseType::Total);
assert!(has_total, "Expected a total lunar eclipse in 2015");
}
#[test]
fn test_solar_eclipses_2017() {
let mut kernel = test_kernel();
let ts = Timescale::default();
let t0 = ts.tdb((2017, 1, 1, 0, 0, 0.0)).tdb();
let t1 = ts.tdb((2018, 1, 1, 0, 0, 0.0)).tdb();
let eclipses = solar_eclipses(&mut kernel, t0, t1);
assert!(
eclipses.len() >= 2,
"Expected at least 2 solar eclipses in 2017, got {}",
eclipses.len()
);
let has_total = eclipses
.iter()
.any(|e| e.eclipse_type == SolarEclipseType::Total);
assert!(has_total, "Expected a total solar eclipse in 2017");
}
#[test]
fn test_solar_eclipse_classification() {
let mut kernel = test_kernel();
let ts = Timescale::default();
let t0 = ts.tdb((2012, 1, 1, 0, 0, 0.0)).tdb();
let t1 = ts.tdb((2013, 1, 1, 0, 0, 0.0)).tdb();
let eclipses = solar_eclipses(&mut kernel, t0, t1);
assert!(
eclipses.len() >= 2,
"Expected at least 2 solar eclipses in 2012, got {}",
eclipses.len()
);
let types: Vec<_> = eclipses.iter().map(|e| e.eclipse_type).collect();
let has_annular = types.contains(&SolarEclipseType::Annular);
let has_total = types.contains(&SolarEclipseType::Total);
assert!(
has_annular || has_total,
"Expected annular or total in 2012, got {:?}",
types
);
}
#[test]
fn test_no_eclipses_in_short_period() {
let mut kernel = test_kernel();
let t0 = 2451545.0; let t1 = t0 + 10.0;
let lunar = lunar_eclipses(&mut kernel, t0, t1);
let solar = solar_eclipses(&mut kernel, t0, t1);
assert!(
lunar.len() + solar.len() <= 1,
"Unexpected eclipses in 10-day window"
);
}
#[test]
fn test_eclipse_display() {
assert_eq!(LunarEclipseType::Total.to_string(), "Total");
assert_eq!(LunarEclipseType::Partial.to_string(), "Partial");
assert_eq!(LunarEclipseType::Penumbral.to_string(), "Penumbral");
assert_eq!(SolarEclipseType::Total.to_string(), "Total");
assert_eq!(SolarEclipseType::Annular.to_string(), "Annular");
assert_eq!(SolarEclipseType::Partial.to_string(), "Partial");
}
}