#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Ayanamsha {
Lahiri,
Raman,
Krishnamurti,
FaganBradley,
Yukteshwar,
JnBhasin,
DjwhalKhul,
Aldebaran15Tau,
Hipparchos,
GalacticCenter0Sag,
TrueChitrapaksha,
Tropical,
DeLuce,
BvRamanMean,
UshaShashi,
Krishnamurti2,
SuryaSiddhanta,
SuryaSiddhantaMean,
Aryabhata,
Aryabhata528,
SsDrevJul,
SsCitra,
TruePushya,
TrueRevati,
TrueMula,
SundaraRajan,
BabylonianHuber,
BabylonianEtpsc,
BabylonianKuglerStar1,
BabylonianKuglerStar2,
BabylonianKuglerStar3,
Sassanian,
GalacticCenterBrand,
GalacticCenterGalAlign,
GalacticEquatorIau1958,
GalacticEquatorTrue,
GalacticEquatorMidMula,
Skydram,
TrueMoonsNode,
Lahiri1940,
LahiriVp285,
ValensMoon,
AyanamshaOfDate,
DjwhalKhulTibetan2,
}
impl Ayanamsha {
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::Lahiri => "Lahiri (Chitrapaksha)",
Self::Raman => "Raman",
Self::Krishnamurti => "Krishnamurti (KP)",
Self::FaganBradley => "Fagan-Bradley",
Self::Yukteshwar => "Yukteshwar",
Self::JnBhasin => "JN Bhasin",
Self::DjwhalKhul => "Djwhal Khul (Tibetan)",
Self::Aldebaran15Tau => "Aldebaran at 15° Taurus (Sassanian)",
Self::Hipparchos => "Hipparchos",
Self::GalacticCenter0Sag => "Galactic Center at 0° Sagittarius",
Self::TrueChitrapaksha => "True Chitrapaksha",
Self::Tropical => "Tropical (0°)",
Self::DeLuce => "De Luce",
Self::BvRamanMean => "B. V. Raman Mean",
Self::UshaShashi => "Usha-Shashi",
Self::Krishnamurti2 => "Krishnamurti 2",
Self::SuryaSiddhanta => "Surya Siddhanta",
Self::SuryaSiddhantaMean => "Surya Siddhanta (Mean)",
Self::Aryabhata => "Aryabhata",
Self::Aryabhata528 => "Aryabhata (528 CE)",
Self::SsDrevJul => "SS Drev-Jul",
Self::SsCitra => "SS Citra",
Self::TruePushya => "True Pushya",
Self::TrueRevati => "True Revati",
Self::TrueMula => "True Mula",
Self::SundaraRajan => "Sundara Rajan",
Self::BabylonianHuber => "Babylonian (Huber)",
Self::BabylonianEtpsc => "Babylonian (ETPSC)",
Self::BabylonianKuglerStar1 => "Babylonian (Kugler Star 1)",
Self::BabylonianKuglerStar2 => "Babylonian (Kugler Star 2)",
Self::BabylonianKuglerStar3 => "Babylonian (Kugler Star 3)",
Self::Sassanian => "Sassanian",
Self::GalacticCenterBrand => "Galactic Center (Brand)",
Self::GalacticCenterGalAlign => "Galactic Center (Galactic Alignment)",
Self::GalacticEquatorIau1958 => "Galactic Equator IAU 1958",
Self::GalacticEquatorTrue => "Galactic Equator (True)",
Self::GalacticEquatorMidMula => "Galactic Equator Mid-Mula",
Self::Skydram => "Skydram",
Self::TrueMoonsNode => "True Moon's Node",
Self::Lahiri1940 => "Lahiri 1940",
Self::LahiriVp285 => "Lahiri VP285",
Self::ValensMoon => "Valensmoon",
Self::AyanamshaOfDate => "Ayanamsha Of Date",
Self::DjwhalKhulTibetan2 => "Djwhal Khul Tibetan 2",
}
}
}
#[must_use]
pub fn ayanamsha_value(system: Ayanamsha, jd: f64) -> f64 {
let ref_value = match system {
Ayanamsha::Tropical => return 0.0,
Ayanamsha::Lahiri | Ayanamsha::LahiriVp285 | Ayanamsha::AyanamshaOfDate => 23.856,
Ayanamsha::Raman => 22.411,
Ayanamsha::Krishnamurti => 23.763,
Ayanamsha::FaganBradley => 24.742,
Ayanamsha::Yukteshwar => 22.461,
Ayanamsha::BvRamanMean => 22.410,
Ayanamsha::TrueChitrapaksha => 23.841,
Ayanamsha::Hipparchos => 20.248,
Ayanamsha::SsCitra => 23.006,
Ayanamsha::Aldebaran15Tau => 24.763,
Ayanamsha::TruePushya | Ayanamsha::BabylonianKuglerStar3 => 22.721,
Ayanamsha::TrueRevati => 20.103,
Ayanamsha::TrueMula => 24.586,
Ayanamsha::GalacticCenter0Sag => 26.852,
Ayanamsha::GalacticEquatorIau1958 => 24.800,
Ayanamsha::GalacticEquatorMidMula => 25.250,
Ayanamsha::GalacticCenterBrand => 25.133,
Ayanamsha::GalacticCenterGalAlign => 25.033,
Ayanamsha::SuryaSiddhanta | Ayanamsha::DjwhalKhulTibetan2 => 22.460,
Ayanamsha::SuryaSiddhantaMean => 21.617,
Ayanamsha::Aryabhata => 20.895,
Ayanamsha::Aryabhata528 => 20.657,
Ayanamsha::SsDrevJul => 21.966,
Ayanamsha::JnBhasin => 22.762,
Ayanamsha::DjwhalKhul => 22.177,
Ayanamsha::DeLuce => 27.816,
Ayanamsha::UshaShashi => 23.399,
Ayanamsha::Krishnamurti2 => 23.793,
Ayanamsha::SundaraRajan => 23.630,
Ayanamsha::Lahiri1940 => 23.030,
Ayanamsha::BabylonianHuber => 24.734,
Ayanamsha::BabylonianEtpsc => 24.522,
Ayanamsha::BabylonianKuglerStar1 | Ayanamsha::GalacticEquatorTrue => 25.017,
Ayanamsha::BabylonianKuglerStar2 => 24.950,
Ayanamsha::Sassanian => 19.993,
Ayanamsha::Skydram => 24.767,
Ayanamsha::TrueMoonsNode => 24.730,
Ayanamsha::ValensMoon => 24.433,
};
let precession_deg =
vedaksha_ephem_core::precession::general_precession_in_longitude(jd) / 3600.0;
ref_value + precession_deg
}
#[must_use]
pub fn tropical_to_sidereal(tropical_longitude_deg: f64, system: Ayanamsha, jd: f64) -> f64 {
let ayan = ayanamsha_value(system, jd);
vedaksha_math::angle::normalize_degrees(tropical_longitude_deg - ayan)
}
#[must_use]
pub fn sidereal_to_tropical(sidereal_longitude_deg: f64, system: Ayanamsha, jd: f64) -> f64 {
let ayan = ayanamsha_value(system, jd);
vedaksha_math::angle::normalize_degrees(sidereal_longitude_deg + ayan)
}
#[cfg(test)]
mod tests {
use super::*;
use vedaksha_ephem_core::julian::J2000;
const J1950: f64 = 2_433_282.5;
#[test]
fn lahiri_at_j2000_approx_23_856() {
let v = ayanamsha_value(Ayanamsha::Lahiri, J2000);
assert!(
(v - 23.856).abs() < 0.001,
"Lahiri at J2000 should be ~23.856°, got {v}"
);
}
#[test]
fn lahiri_at_j1950_approx_23_16() {
let v = ayanamsha_value(Ayanamsha::Lahiri, J1950);
assert!(
(v - 23.16).abs() < 0.20,
"Lahiri at J1950 should be ~23.16°, got {v}"
);
}
#[test]
fn tropical_always_zero() {
for jd in [J1950, J2000, J2000 + 36525.0] {
let v = ayanamsha_value(Ayanamsha::Tropical, jd);
assert!(
v.abs() < f64::EPSILON,
"Tropical ayanamsha must be 0 at jd={jd}"
);
}
}
#[test]
fn fagan_bradley_at_j2000_approx_24_742() {
let v = ayanamsha_value(Ayanamsha::FaganBradley, J2000);
assert!(
(v - 24.742).abs() < 0.001,
"Fagan-Bradley at J2000 should be ~24.742°, got {v}"
);
}
#[test]
fn ayanamsha_increases_over_time() {
let past = ayanamsha_value(Ayanamsha::Lahiri, J1950);
let future = ayanamsha_value(Ayanamsha::Lahiri, J2000 + 36525.0);
assert!(
future > past,
"Ayanamsha must increase over time: past={past}, future={future}"
);
}
#[test]
fn all_systems_in_reasonable_range_at_j2000() {
let systems = [
Ayanamsha::Lahiri,
Ayanamsha::Raman,
Ayanamsha::Krishnamurti,
Ayanamsha::FaganBradley,
Ayanamsha::Yukteshwar,
Ayanamsha::JnBhasin,
Ayanamsha::DjwhalKhul,
Ayanamsha::Aldebaran15Tau,
Ayanamsha::Hipparchos,
Ayanamsha::GalacticCenter0Sag,
Ayanamsha::TrueChitrapaksha,
Ayanamsha::DeLuce,
Ayanamsha::BvRamanMean,
Ayanamsha::UshaShashi,
Ayanamsha::Krishnamurti2,
Ayanamsha::SuryaSiddhanta,
Ayanamsha::SuryaSiddhantaMean,
Ayanamsha::Aryabhata,
Ayanamsha::Aryabhata528,
Ayanamsha::SsDrevJul,
Ayanamsha::SsCitra,
Ayanamsha::TruePushya,
Ayanamsha::TrueRevati,
Ayanamsha::TrueMula,
Ayanamsha::SundaraRajan,
Ayanamsha::BabylonianHuber,
Ayanamsha::BabylonianEtpsc,
Ayanamsha::BabylonianKuglerStar1,
Ayanamsha::BabylonianKuglerStar2,
Ayanamsha::BabylonianKuglerStar3,
Ayanamsha::Sassanian,
Ayanamsha::GalacticCenterBrand,
Ayanamsha::GalacticCenterGalAlign,
Ayanamsha::GalacticEquatorIau1958,
Ayanamsha::GalacticEquatorTrue,
Ayanamsha::GalacticEquatorMidMula,
Ayanamsha::Skydram,
Ayanamsha::TrueMoonsNode,
Ayanamsha::Lahiri1940,
Ayanamsha::LahiriVp285,
Ayanamsha::ValensMoon,
Ayanamsha::AyanamshaOfDate,
Ayanamsha::DjwhalKhulTibetan2,
];
assert_eq!(systems.len(), 43, "expected 43 non-Tropical systems");
for sys in systems {
let v = ayanamsha_value(sys, J2000);
assert!(
(0.0..30.0).contains(&v),
"{} at J2000 should be in [0, 30)°, got {v}",
sys.name()
);
}
}
#[test]
fn total_ayanamsha_count_is_44() {
let non_tropical_count = 43_usize;
assert_eq!(non_tropical_count + 1, 44);
}
#[test]
fn roundtrip_tropical_sidereal() {
let tropical = 120.0_f64; let sid = tropical_to_sidereal(tropical, Ayanamsha::Lahiri, J2000);
let back = sidereal_to_tropical(sid, Ayanamsha::Lahiri, J2000);
assert!(
(back - tropical).abs() < 1e-10,
"Roundtrip mismatch: tropical={tropical}, back={back}"
);
}
#[test]
fn tropical_to_sidereal_normalizes_to_0_360() {
let result = tropical_to_sidereal(5.0, Ayanamsha::Lahiri, J2000);
assert!(
(0.0..360.0).contains(&result),
"Result {result} is outside [0, 360)"
);
assert!(
(result - 341.144).abs() < 0.01,
"Expected ~341.14°, got {result}"
);
}
#[test]
fn sidereal_to_tropical_normalizes_to_0_360() {
let result = sidereal_to_tropical(350.0, Ayanamsha::Lahiri, J2000);
assert!(
(0.0..360.0).contains(&result),
"Result {result} is outside [0, 360)"
);
}
#[test]
fn precession_upgrade_vs_quadratic_modern() {
let old_rate: f64 = 50.2875 / 3600.0;
let old_accel: f64 = 0.000222 / 3600.0;
let j2000: f64 = 2_451_545.0;
let days_per_year: f64 = 365.25;
let test_dates_jd: [f64; 10] = [
2_415_020.5,
2_421_639.5,
2_428_258.5,
2_434_877.5,
2_441_496.5,
2_448_115.5,
2_451_545.0,
2_455_197.5,
2_462_867.5,
2_488_069.5,
];
for &jd in &test_dates_jd {
let t_yr = (jd - j2000) / days_per_year;
let old_offset_deg = old_rate * t_yr + 0.5 * old_accel * t_yr * t_yr;
let new_offset_deg =
vedaksha_ephem_core::precession::general_precession_in_longitude(jd) / 3600.0;
let diff = (new_offset_deg - old_offset_deg).abs();
assert!(
diff < 0.02,
"Modern-date divergence too large at JD {jd}: old={old_offset_deg:.6}°, \
new={new_offset_deg:.6}°, diff={diff:.6}°"
);
}
}
#[test]
fn precession_upgrade_diverges_at_historical_dates() {
let old_rate: f64 = 50.2875 / 3600.0;
let old_accel: f64 = 0.000222 / 3600.0;
let j2000: f64 = 2_451_545.0;
let days_per_year: f64 = 365.25;
let historical_dates_jd: [f64; 4] = [625_295.0, 990_545.0, 1_063_295.0, 1_100_345.0];
let mut any_diverged = false;
for &jd in &historical_dates_jd {
let t_yr = (jd - j2000) / days_per_year;
let old_offset_deg = old_rate * t_yr + 0.5 * old_accel * t_yr * t_yr;
let new_offset_deg =
vedaksha_ephem_core::precession::general_precession_in_longitude(jd) / 3600.0;
let diff = (new_offset_deg - old_offset_deg).abs();
if diff > 0.05 {
any_diverged = true;
}
}
assert!(
any_diverged,
"Expected measurable divergence at historical dates, but models agreed too closely"
);
}
}