use vedaksha_math::angle::{deg_to_rad, normalize_degrees, rad_to_deg};
use super::{CONVERGENCE_THRESHOLD, HouseCusps, HouseSystem, MAX_ITERATIONS, POLAR_LAT_THRESHOLD};
fn _ecliptic_ra(lon_deg: f64, eps_deg: f64) -> f64 {
let lon = deg_to_rad(lon_deg);
let eps = deg_to_rad(eps_deg);
normalize_degrees(rad_to_deg(libm::atan2(
libm::sin(lon) * libm::cos(eps),
libm::cos(lon),
)))
}
fn ecliptic_dec(lon_deg: f64, eps_deg: f64) -> f64 {
let lon = deg_to_rad(lon_deg);
let eps = deg_to_rad(eps_deg);
rad_to_deg(libm::asin(libm::sin(lon) * libm::sin(eps)))
}
fn semi_arc(lat_deg: f64, dec_deg: f64) -> Option<f64> {
let lat = deg_to_rad(lat_deg);
let dec = deg_to_rad(dec_deg);
let arg = -libm::tan(lat) * libm::tan(dec);
if !(-1.0..=1.0).contains(&arg) {
return None;
}
Some(rad_to_deg(libm::acos(arg)))
}
fn find_cusp(ramc_deg: f64, lat_deg: f64, eps_deg: f64, fraction: f64, above: bool) -> Option<f64> {
let eps = deg_to_rad(eps_deg);
let (_, mc) = super::compute_asc_mc(ramc_deg, lat_deg, eps_deg);
let mc_dec = ecliptic_dec(mc, eps_deg);
let initial_sa = semi_arc(lat_deg, mc_dec)?;
let mut ra = if above {
normalize_degrees(ramc_deg + fraction * initial_sa)
} else {
let raic = normalize_degrees(ramc_deg + 180.0);
normalize_degrees(raic - (1.0 - fraction) * (180.0 - initial_sa))
};
for _ in 0..MAX_ITERATIONS {
let ra_rad = deg_to_rad(ra);
let lon_rad = libm::atan2(libm::sin(ra_rad), libm::cos(ra_rad) * libm::cos(eps));
let lon = normalize_degrees(rad_to_deg(lon_rad));
let dec = ecliptic_dec(lon, eps_deg);
let sa = match semi_arc(lat_deg, dec) {
Some(sa) => sa,
None => return None, };
let new_ra = if above {
normalize_degrees(ramc_deg + fraction * sa)
} else {
let raic = normalize_degrees(ramc_deg + 180.0);
normalize_degrees(raic - (1.0 - fraction) * (180.0 - sa))
};
let mut diff = new_ra - ra;
if diff > 180.0 {
diff -= 360.0;
}
if diff < -180.0 {
diff += 360.0;
}
if libm::fabs(diff) < CONVERGENCE_THRESHOLD {
let final_ra_rad = deg_to_rad(new_ra);
let final_lon = libm::atan2(
libm::sin(final_ra_rad),
libm::cos(final_ra_rad) * libm::cos(eps),
);
return Some(normalize_degrees(rad_to_deg(final_lon)));
}
ra = new_ra;
}
let ra_rad = deg_to_rad(ra);
let lon = libm::atan2(libm::sin(ra_rad), libm::cos(ra_rad) * libm::cos(eps));
Some(normalize_degrees(rad_to_deg(lon)))
}
pub(super) fn compute(ramc: f64, latitude: f64, obliquity: f64) -> HouseCusps {
if libm::fabs(latitude) > POLAR_LAT_THRESHOLD {
let mut result = super::equal::compute(ramc, latitude, obliquity);
result.system = HouseSystem::Placidus;
result.polar_fallback = true;
return result;
}
let (asc, mc) = super::compute_asc_mc(ramc, latitude, obliquity);
let dsc = normalize_degrees(asc + 180.0);
let ic = normalize_degrees(mc + 180.0);
let mut cusps = [0.0_f64; 12];
cusps[0] = asc;
cusps[6] = dsc;
cusps[9] = mc;
cusps[3] = ic;
let c11 = find_cusp(ramc, latitude, obliquity, 1.0 / 3.0, true);
let c12 = find_cusp(ramc, latitude, obliquity, 2.0 / 3.0, true);
let c2 = find_cusp(ramc, latitude, obliquity, 1.0 / 3.0, false);
let c3 = find_cusp(ramc, latitude, obliquity, 2.0 / 3.0, false);
if c11.is_none() || c12.is_none() || c2.is_none() || c3.is_none() {
let mut result = super::equal::compute(ramc, latitude, obliquity);
result.system = HouseSystem::Placidus;
result.polar_fallback = true;
return result;
}
cusps[10] = c11.unwrap();
cusps[11] = c12.unwrap();
cusps[1] = c2.unwrap();
cusps[2] = c3.unwrap();
cusps[4] = normalize_degrees(cusps[10] + 180.0);
cusps[5] = normalize_degrees(cusps[11] + 180.0);
cusps[7] = normalize_degrees(cusps[1] + 180.0);
cusps[8] = normalize_degrees(cusps[2] + 180.0);
HouseCusps {
cusps,
asc,
mc,
system: HouseSystem::Placidus,
polar_fallback: false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn equator_produces_cusps() {
let result = compute(0.0, 0.0, 23.44);
assert_eq!(result.system, HouseSystem::Placidus);
assert!(!result.polar_fallback);
for (i, &c) in result.cusps.iter().enumerate() {
assert!((0.0..360.0).contains(&c), "cusp {i}: {c}");
}
}
#[test]
fn polar_fallback() {
let result = compute(0.0, 70.0, 23.44);
assert_eq!(result.system, HouseSystem::Placidus);
assert!(result.polar_fallback, "should fallback at lat=70");
}
#[test]
fn cusp1_equals_asc() {
let result = compute(30.0, 45.0, 23.44);
if !result.polar_fallback {
assert!(
(result.cusps[0] - result.asc).abs() < 1e-6,
"cusp1={} asc={}",
result.cusps[0],
result.asc
);
}
}
#[test]
fn cusp10_equals_mc() {
let result = compute(30.0, 45.0, 23.44);
if !result.polar_fallback {
assert!(
(result.cusps[9] - result.mc).abs() < 1e-6,
"cusp10={} mc={}",
result.cusps[9],
result.mc
);
}
}
#[test]
fn opposite_cusps() {
let result = compute(90.0, 40.0, 23.44);
if !result.polar_fallback {
for i in 0..6 {
let diff = normalize_degrees(result.cusps[i + 6] - result.cusps[i]);
assert!(
(diff - 180.0).abs() < 0.01,
"cusps {i} and {}: diff={diff}",
i + 6
);
}
}
}
#[test]
fn mid_latitude() {
let result = compute(120.0, 51.5, 23.44);
assert!(!result.polar_fallback);
for (i, &c) in result.cusps.iter().enumerate() {
assert!((0.0..360.0).contains(&c), "cusp {i}: {c}");
}
}
}