mod main_problem;
mod perturbation;
use crate::calendar::julian_centuries;
use crate::coords::{deg_to_rad, normalize_degrees, normalize_radians};
use crate::num::KahanSum;
fn mean_elongation(t: f64) -> f64 {
let t2 = t * t;
let t3 = t2 * t;
let t4 = t3 * t;
normalize_degrees(
297.850_195_47 + 445_267.111_467_86 * t - 0.001_914_2 * t2 + t3 / 189_474.0
- t4 / 121_164_000.0,
)
}
fn sun_mean_anomaly(t: f64) -> f64 {
let t2 = t * t;
let t3 = t2 * t;
let t4 = t3 * t;
normalize_degrees(
357.529_109_18 + 35_999.050_290_94 * t - 0.000_153_6 * t2 + t3 / 24_490_000.0
- t4 / 992_340_000.0,
)
}
fn moon_mean_anomaly(t: f64) -> f64 {
let t2 = t * t;
let t3 = t2 * t;
let t4 = t3 * t;
normalize_degrees(
134.963_398_39 + 477_198.867_505_06 * t + 0.008_941_4 * t2 + t3 / 69_699.0
- t4 / 14_712_000.0,
)
}
fn argument_of_latitude(t: f64) -> f64 {
let t2 = t * t;
let t3 = t2 * t;
let t4 = t3 * t;
normalize_degrees(
93.272_099_31 + 483_202.017_523_06 * t - 0.003_653_9 * t2 - t3 / 3_526_000.0
+ t4 / 863_310_000.0,
)
}
fn eccentricity(t: f64) -> f64 {
1.0 - 0.002_516 * t - 0.000_007_4 * t * t
}
fn e_power(e: f64, m_mult: i16) -> f64 {
match m_mult.unsigned_abs() {
0 => 1.0,
1 => e,
2 => e * e,
_ => e.powi(m_mult.unsigned_abs() as i32),
}
}
pub(crate) struct LonLatTerm {
pub d: i16,
pub m: i16,
pub mp: i16,
pub f: i16,
pub coeff: i64,
}
pub(crate) struct DistTerm {
pub d: i16,
pub m: i16,
pub mp: i16,
pub f: i16,
pub coeff: i64,
}
pub fn lunar_longitude(jd: f64) -> f64 {
let t = julian_centuries(jd);
let d = normalize_radians(deg_to_rad(mean_elongation(t)));
let m = normalize_radians(deg_to_rad(sun_mean_anomaly(t)));
let mp = normalize_radians(deg_to_rad(moon_mean_anomaly(t)));
let f = normalize_radians(deg_to_rad(argument_of_latitude(t)));
let e = eccentricity(t);
let lp = normalize_degrees(
218.316_447_9 + 481_267.881_234_21 * t - 0.001_579_86 * t * t + t * t * t / 538_841.0
- t * t * t * t / 65_194_000.0,
);
let mut sigma_l = KahanSum::new();
for term in main_problem::LONGITUDE_TERMS {
let arg = term.d as f64 * d + term.m as f64 * m + term.mp as f64 * mp + term.f as f64 * f;
let ep = e_power(e, term.m);
sigma_l.add(term.coeff as f64 * ep * arg.sin());
}
for term in perturbation::LONGITUDE_PERTURBATION {
let arg = term.d as f64 * d + term.m as f64 * m + term.mp as f64 * mp + term.f as f64 * f;
let ep = e_power(e, term.m);
sigma_l.add(term.coeff as f64 * ep * arg.sin());
}
let a1 = normalize_radians(deg_to_rad(119.75 + 131.849 * t));
let a2 = normalize_radians(deg_to_rad(53.09 + 479_264.290 * t));
sigma_l.add(3958.0 * a1.sin() + 1962.0 * a2.sin() + 318.0 * deg_to_rad(lp).sin());
normalize_degrees(lp + sigma_l.sum() * 1e-6)
}
pub fn lunar_latitude(jd: f64) -> f64 {
let t = julian_centuries(jd);
let d = normalize_radians(deg_to_rad(mean_elongation(t)));
let m = normalize_radians(deg_to_rad(sun_mean_anomaly(t)));
let mp = normalize_radians(deg_to_rad(moon_mean_anomaly(t)));
let f = normalize_radians(deg_to_rad(argument_of_latitude(t)));
let e = eccentricity(t);
let lp = normalize_degrees(
218.316_447_9 + 481_267.881_234_21 * t - 0.001_579_86 * t * t + t * t * t / 538_841.0
- t * t * t * t / 65_194_000.0,
);
let mut sigma_b = KahanSum::new();
for term in main_problem::LATITUDE_TERMS {
let arg = term.d as f64 * d + term.m as f64 * m + term.mp as f64 * mp + term.f as f64 * f;
let ep = e_power(e, term.m);
sigma_b.add(term.coeff as f64 * ep * arg.sin());
}
for term in perturbation::LATITUDE_PERTURBATION {
let arg = term.d as f64 * d + term.m as f64 * m + term.mp as f64 * mp + term.f as f64 * f;
let ep = e_power(e, term.m);
sigma_b.add(term.coeff as f64 * ep * arg.sin());
}
let a1 = normalize_radians(deg_to_rad(119.75 + 131.849 * t));
let a3 = normalize_radians(deg_to_rad(313.45 + 481_266.484 * t));
let lp_rad = deg_to_rad(lp);
sigma_b.add(
-2235.0 * lp_rad.sin()
+ 382.0 * a3.sin()
+ 175.0 * (a1 - f).sin()
+ 175.0 * (a1 + f).sin()
+ 127.0 * (lp_rad - mp).sin()
- 115.0 * (lp_rad + mp).sin(),
);
sigma_b.sum() * 1e-6
}
pub fn lunar_distance_km(jd: f64) -> f64 {
let t = julian_centuries(jd);
let d = normalize_radians(deg_to_rad(mean_elongation(t)));
let m = normalize_radians(deg_to_rad(sun_mean_anomaly(t)));
let mp = normalize_radians(deg_to_rad(moon_mean_anomaly(t)));
let f = normalize_radians(deg_to_rad(argument_of_latitude(t)));
let e = eccentricity(t);
let mut sigma_r = KahanSum::new();
for term in main_problem::DISTANCE_TERMS {
let arg = term.d as f64 * d + term.m as f64 * m + term.mp as f64 * mp + term.f as f64 * f;
let ep = e_power(e, term.m);
sigma_r.add(term.coeff as f64 * ep * arg.cos());
}
385_000.56 + sigma_r.sum() * 0.001
}
pub fn lunar_distance_au(jd: f64) -> f64 {
lunar_distance_km(jd) / 149_597_870.7
}
pub fn lunar_coordinates(jd: f64) -> (f64, f64, f64) {
(
lunar_longitude(jd),
lunar_latitude(jd),
lunar_distance_km(jd),
)
}
#[cfg(test)]
mod tests {
use super::*;
const JD_J2000: f64 = 2_451_545.0;
const JD_MEEUS_47A: f64 = 2_448_724.5;
#[test]
fn meeus_47a_longitude() {
let lon = lunar_longitude(JD_MEEUS_47A);
assert!(
(lon - 133.162).abs() < 0.02,
"lon = {lon}, expected ~133.162"
);
}
#[test]
fn meeus_47a_latitude() {
let lat = lunar_latitude(JD_MEEUS_47A);
assert!(
(lat - (-3.229)).abs() < 0.02,
"lat = {lat}, expected ~-3.229"
);
}
#[test]
fn meeus_47a_distance() {
let dist = lunar_distance_km(JD_MEEUS_47A);
assert!(
(dist - 368_409.7).abs() < 10.0,
"dist = {dist}, expected ~368409.7"
);
}
#[test]
fn coordinates_combined() {
let (lon, lat, dist) = lunar_coordinates(JD_MEEUS_47A);
assert!((lon - 133.162).abs() < 0.02);
assert!((lat - (-3.229)).abs() < 0.02);
assert!((dist - 368_409.7).abs() < 10.0);
}
#[test]
fn longitude_range_year() {
for day in 0..365 {
let jd = JD_J2000 + day as f64;
let lon = lunar_longitude(jd);
assert!((0.0..360.0).contains(&lon), "lon {lon} at day {day}");
}
}
#[test]
fn latitude_physical_range() {
for day in 0..365 {
let jd = JD_J2000 + day as f64;
let lat = lunar_latitude(jd);
assert!(lat.abs() < 6.0, "lat {lat} at day {day}");
}
}
#[test]
fn distance_physical_range() {
for day in 0..365 {
let jd = JD_J2000 + day as f64;
let dist = lunar_distance_km(jd);
assert!(
dist > 350_000.0 && dist < 410_000.0,
"dist {dist} at day {day}"
);
}
}
#[test]
fn distance_au_conversion() {
let km = lunar_distance_km(JD_J2000);
let au = lunar_distance_au(JD_J2000);
assert!((au * 149_597_870.7 - km).abs() < 0.1);
}
#[test]
fn delaunay_arguments_j2000() {
let d = mean_elongation(0.0);
assert!((d - 297.85).abs() < 0.01, "D = {d}");
let mp = moon_mean_anomaly(0.0);
assert!((mp - 134.96).abs() < 0.01, "l = {mp}");
}
#[test]
fn eccentricity_j2000() {
let e = eccentricity(0.0);
assert!((e - 1.0).abs() < 1e-10);
}
#[test]
fn elp_vs_meeus_consistency() {
for day in (0..365).step_by(30) {
let jd = JD_J2000 + day as f64;
let elp_lon = lunar_longitude(jd);
let meeus_lon = crate::moon::lunar_longitude(jd);
let diff = (elp_lon - meeus_lon).abs();
let diff = if diff > 180.0 { 360.0 - diff } else { diff };
assert!(
diff < 0.01,
"day {day}: ELP={elp_lon:.4} vs Meeus={meeus_lon:.4}, diff={diff:.6}"
);
}
}
}