use crate::ephemeris::SunMoonSample;
use crate::syzygy::Syzygy;
use crate::types::{LunarEclipseType, SolarEclipseType};
use pleiades_types::OBLIQUITY_J2000_DEG;
pub(crate) mod constants {
pub const R_SUN_KM: f64 = 696_000.0;
pub const R_MOON_KM: f64 = 1_737.4;
pub const R_EARTH_KM: f64 = 6_378.137;
pub const AU_KM: f64 = 149_597_870.7;
pub const SHADOW_INFLATION: f64 = 1.01;
}
use constants::*;
const CENTRAL_GAMMA_LIMIT: f64 = 0.9972;
#[derive(Clone, Copy, Debug)]
pub(crate) struct SolarCircumstances {
pub eclipse_type: SolarEclipseType,
pub magnitude: f64,
pub gamma: f64,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct LunarCircumstances {
pub eclipse_type: LunarEclipseType,
pub magnitude: f64,
pub gamma: f64,
}
fn separation_rad(sample: &SunMoonSample) -> f64 {
let (l1, b1) = (
sample.sun_longitude_deg.to_radians(),
sample.sun_latitude_deg.to_radians(),
);
let (l2, b2) = (
sample.moon_longitude_deg.to_radians(),
sample.moon_latitude_deg.to_radians(),
);
let cos_sep = (b1.sin() * b2.sin() + b1.cos() * b2.cos() * (l1 - l2).cos()).clamp(-1.0, 1.0);
cos_sep.acos()
}
pub(crate) fn classify_solar(sample: &SunMoonSample) -> Option<SolarCircumstances> {
let sun_dist_km = sample.sun_distance_au * AU_KM;
let moon_dist_km = sample.moon_distance_au * AU_KM;
let s = (R_SUN_KM / sun_dist_km).asin();
let m_geo = (R_MOON_KM / moon_dist_km).asin();
let parallax = (R_EARTH_KM / moon_dist_km).asin();
let sigma = separation_rad(sample);
if sigma >= parallax + s + m_geo {
return None; }
let gamma = (sigma / parallax) * sample.moon_latitude_deg.signum();
let s_vec = rect(
sample.sun_longitude_deg,
sample.sun_latitude_deg,
sample.sun_distance_au,
);
let m_vec = rect(
sample.moon_longitude_deg,
sample.moon_latitude_deg,
sample.moon_distance_au,
);
let r_earth_au = R_EARTH_KM / AU_KM;
let gamma_linear = {
let ax = {
let d = sub(m_vec, s_vec);
scale(d, 1.0 / norm(d))
};
norm(sub(m_vec, scale(ax, dot(m_vec, ax)))) / r_earth_au
};
let sf = flatten_to_sphere(s_vec);
let mf = flatten_to_sphere(m_vec);
let axis = {
let d = sub(mf, sf);
scale(d, 1.0 / norm(d))
};
let foot = sub(mf, scale(axis, dot(mf, axis)));
let perp = norm(foot);
let observer_flat = if perp < r_earth_au {
let t = (r_earth_au * r_earth_au - perp * perp).sqrt();
let cand_a = add(foot, scale(axis, t));
let cand_b = sub(foot, scale(axis, t));
if norm(sub(sf, cand_a)) < norm(sub(sf, cand_b)) {
cand_a
} else {
cand_b
}
} else {
scale(foot, r_earth_au / perp)
};
let observer = unflatten_from_sphere(observer_flat);
let to_moon = sub(m_vec, observer);
let to_sun = sub(s_vec, observer);
let d_moon_topo = norm(to_moon);
let d_sun_topo = norm(to_sun);
let m_topo = ((R_MOON_KM / AU_KM) / d_moon_topo).asin();
let s_topo = ((R_SUN_KM / AU_KM) / d_sun_topo).asin();
let cos_sep = (dot(to_moon, to_sun) / (d_moon_topo * d_sun_topo)).clamp(-1.0, 1.0);
let sigma_topo = cos_sep.acos();
let (eclipse_type, magnitude) = if gamma_linear < CENTRAL_GAMMA_LIMIT {
let mag = m_topo / s_topo;
let etype = if m_topo >= s_topo {
if m_geo >= s {
SolarEclipseType::Total
} else {
SolarEclipseType::Hybrid
}
} else {
SolarEclipseType::Annular
};
(etype, mag)
} else {
let mag = ((s_topo + m_topo - sigma_topo) / (2.0 * s_topo)).max(0.0);
let etype = if sigma_topo + s_topo <= m_topo {
SolarEclipseType::Total
} else if sigma_topo + m_topo <= s_topo {
SolarEclipseType::Annular
} else {
SolarEclipseType::Partial
};
(etype, mag)
};
Some(SolarCircumstances {
eclipse_type,
magnitude,
gamma,
})
}
fn shadow_axis_separation_rad(sample: &SunMoonSample) -> f64 {
let anti_lon = (sample.sun_longitude_deg + 180.0).rem_euclid(360.0);
let anti = SunMoonSample {
sun_longitude_deg: anti_lon,
sun_latitude_deg: -sample.sun_latitude_deg,
..*sample
};
separation_rad(&anti)
}
pub(crate) struct LunarShadow {
pub sigma: f64,
pub u: f64,
pub p: f64,
pub m_moon: f64,
}
pub(crate) fn lunar_shadow(sample: &SunMoonSample) -> LunarShadow {
let s = (R_SUN_KM / (sample.sun_distance_au * AU_KM)).asin();
let m_moon = (R_MOON_KM / (sample.moon_distance_au * AU_KM)).asin();
let pi_moon = (R_EARTH_KM / (sample.moon_distance_au * AU_KM)).asin();
let pi_sun = (R_EARTH_KM / (sample.sun_distance_au * AU_KM)).asin();
let earth_shadow = SHADOW_INFLATION * (pi_moon + pi_sun);
LunarShadow {
sigma: shadow_axis_separation_rad(sample),
u: earth_shadow - s,
p: earth_shadow + s,
m_moon,
}
}
pub(crate) fn classify_lunar(sample: &SunMoonSample) -> Option<LunarCircumstances> {
let pi_moon = (R_EARTH_KM / (sample.moon_distance_au * AU_KM)).asin();
let LunarShadow {
sigma,
u,
p,
m_moon,
} = lunar_shadow(sample);
if sigma >= p + m_moon {
return None;
}
let gamma = (sigma / pi_moon) * sample.moon_latitude_deg.signum();
let (eclipse_type, magnitude) = if sigma + m_moon <= u {
(
LunarEclipseType::Total,
(u + m_moon - sigma) / (2.0 * m_moon),
)
} else if sigma - m_moon < u {
(
LunarEclipseType::Partial,
(u + m_moon - sigma) / (2.0 * m_moon),
)
} else {
(
LunarEclipseType::Penumbral,
(p + m_moon - sigma) / (2.0 * m_moon),
)
};
Some(LunarCircumstances {
eclipse_type,
magnitude,
gamma,
})
}
fn rect(lon_deg: f64, lat_deg: f64, dist_au: f64) -> [f64; 3] {
let l = lon_deg.to_radians();
let b = lat_deg.to_radians();
[
dist_au * b.cos() * l.cos(),
dist_au * b.cos() * l.sin(),
dist_au * b.sin(),
]
}
fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn norm(a: [f64; 3]) -> f64 {
(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]).sqrt()
}
fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
fn scale(a: [f64; 3], k: f64) -> [f64; 3] {
[a[0] * k, a[1] * k, a[2] * k]
}
const EARTH_FLATTENING: f64 = 1.0 / 298.257_223_563;
const OBLIQUITY_RAD: f64 = OBLIQUITY_J2000_DEG * core::f64::consts::PI / 180.0;
fn ecliptic_to_equatorial(v: [f64; 3]) -> [f64; 3] {
let (c, s) = (OBLIQUITY_RAD.cos(), OBLIQUITY_RAD.sin());
[v[0], v[1] * c - v[2] * s, v[1] * s + v[2] * c]
}
fn equatorial_to_ecliptic(v: [f64; 3]) -> [f64; 3] {
let (c, s) = (OBLIQUITY_RAD.cos(), OBLIQUITY_RAD.sin());
[v[0], v[1] * c + v[2] * s, -v[1] * s + v[2] * c]
}
fn flatten_to_sphere(v: [f64; 3]) -> [f64; 3] {
let e = ecliptic_to_equatorial(v);
[e[0], e[1], e[2] / (1.0 - EARTH_FLATTENING)]
}
fn unflatten_from_sphere(v: [f64; 3]) -> [f64; 3] {
equatorial_to_ecliptic([v[0], v[1], v[2] * (1.0 - EARTH_FLATTENING)])
}
pub(crate) fn separation_for(syzygy: Syzygy, sample: &SunMoonSample) -> f64 {
let s = rect(
sample.sun_longitude_deg,
sample.sun_latitude_deg,
sample.sun_distance_au,
);
let m = rect(
sample.moon_longitude_deg,
sample.moon_latitude_deg,
sample.moon_distance_au,
);
match syzygy {
Syzygy::NewMoon => {
let diff = [m[0] - s[0], m[1] - s[1], m[2] - s[2]];
norm(cross(s, m)) / norm(diff)
}
Syzygy::FullMoon => norm(cross(m, s)) / norm(s),
}
}
pub(crate) fn sub_shadow_point(
sample: &SunMoonSample,
greatest_jd: f64,
) -> crate::types::GeoLocation {
use pleiades_types::{Angle, EclipticCoordinates, Latitude, Longitude};
let coords = EclipticCoordinates::new(
Longitude::from_degrees(sample.moon_longitude_deg),
Latitude::from_degrees(sample.moon_latitude_deg),
Some(sample.moon_distance_au),
);
let t = (greatest_jd - 2_451_545.0) / 36_525.0;
let eps_deg = 23.439_291 - 0.013_004_2 * t;
let equatorial = coords.to_equatorial(Angle::from_degrees(eps_deg));
let declination = equatorial.declination.degrees();
let ra_deg = equatorial.right_ascension.degrees();
let gmst_deg =
(280.460_618_37 + 360.985_647_366_29 * (greatest_jd - 2_451_545.0)).rem_euclid(360.0);
let mut lon = (ra_deg - gmst_deg + 180.0).rem_euclid(360.0) - 180.0;
if lon <= -180.0 {
lon += 360.0;
}
crate::types::GeoLocation {
latitude_degrees: declination,
longitude_degrees: lon,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ephemeris::SunMoonSample;
fn sample(moon_lat_deg: f64, moon_dist_au: f64, sun_dist_au: f64) -> SunMoonSample {
SunMoonSample {
sun_longitude_deg: 100.0,
sun_latitude_deg: 0.0,
sun_distance_au: sun_dist_au,
moon_longitude_deg: 100.0,
moon_latitude_deg: moon_lat_deg,
moon_distance_au: moon_dist_au,
}
}
#[test]
fn central_close_moon_is_total() {
let c = classify_solar(&sample(0.0, 0.00238, 1.000)).unwrap();
assert_eq!(c.eclipse_type, SolarEclipseType::Total);
assert!(c.magnitude >= 1.0);
assert!(c.gamma.abs() < 0.1);
}
#[test]
fn central_far_moon_is_annular() {
let c = classify_solar(&sample(0.0, 0.00271, 1.000)).unwrap();
assert_eq!(c.eclipse_type, SolarEclipseType::Annular);
assert!(c.magnitude < 1.0);
}
#[test]
fn far_from_node_is_no_eclipse() {
assert!(classify_solar(&sample(1.5, 0.00257, 1.000)).is_none());
}
fn full_moon_sample(moon_lat_deg: f64, moon_dist_au: f64) -> SunMoonSample {
SunMoonSample {
sun_longitude_deg: 100.0,
sun_latitude_deg: 0.0,
sun_distance_au: 1.000,
moon_longitude_deg: 280.0,
moon_latitude_deg: moon_lat_deg,
moon_distance_au: moon_dist_au,
}
}
#[test]
fn central_full_moon_is_total_lunar() {
let c = classify_lunar(&full_moon_sample(0.0, 0.00257)).unwrap();
assert_eq!(c.eclipse_type, LunarEclipseType::Total);
assert!(c.magnitude >= 1.0);
}
#[test]
fn distant_latitude_full_moon_is_no_eclipse() {
assert!(classify_lunar(&full_moon_sample(1.6, 0.00257)).is_none());
}
}