use crate::coords::{deg_to_rad, normalize_degrees, rad_to_deg};
use crate::error::{JyotishError, Result};
use serde::{Deserialize, Serialize};
use std::f64::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum HouseSystem {
Placidus,
Equal,
WholeSign,
Porphyry,
}
#[derive(Debug, Clone)]
pub struct HouseCusps {
pub system: HouseSystem,
pub cusps: [f64; 12],
pub ascendant: f64,
pub midheaven: f64,
}
pub fn ascendant(lst_deg: f64, lat_deg: f64, obliquity_deg: f64) -> f64 {
let lst = deg_to_rad(lst_deg);
let lat = deg_to_rad(lat_deg);
let eps = deg_to_rad(obliquity_deg);
let asc = (-lst.cos()).atan2(lst.sin() * eps.cos() + lat.tan() * eps.sin());
normalize_degrees(rad_to_deg(asc))
}
pub fn midheaven(lst_deg: f64, obliquity_deg: f64) -> f64 {
let lst = deg_to_rad(lst_deg);
let eps = deg_to_rad(obliquity_deg);
let mc = lst.tan().atan2(eps.cos());
let mut mc_deg = normalize_degrees(rad_to_deg(mc));
let lst_norm = normalize_degrees(lst_deg);
if (mc_deg - lst_norm).abs() > 90.0 && (mc_deg - lst_norm).abs() < 270.0 {
mc_deg = normalize_degrees(mc_deg + 180.0);
}
mc_deg
}
pub fn compute_houses(
system: HouseSystem,
lst_deg: f64,
lat_deg: f64,
obliquity_deg: f64,
) -> Result<HouseCusps> {
let asc = ascendant(lst_deg, lat_deg, obliquity_deg);
let mc = midheaven(lst_deg, obliquity_deg);
let cusps = match system {
HouseSystem::Equal => equal_houses(asc),
HouseSystem::WholeSign => whole_sign_houses(asc),
HouseSystem::Porphyry => porphyry_houses(asc, mc),
HouseSystem::Placidus => placidus_houses(asc, mc, lst_deg, lat_deg, obliquity_deg)?,
};
Ok(HouseCusps {
system,
cusps,
ascendant: asc,
midheaven: mc,
})
}
fn equal_houses(asc: f64) -> [f64; 12] {
let mut cusps = [0.0; 12];
for (i, cusp) in cusps.iter_mut().enumerate() {
*cusp = normalize_degrees(asc + i as f64 * 30.0);
}
cusps
}
fn whole_sign_houses(asc: f64) -> [f64; 12] {
let first_sign_start = (asc / 30.0).floor() * 30.0;
let mut cusps = [0.0; 12];
for (i, cusp) in cusps.iter_mut().enumerate() {
*cusp = normalize_degrees(first_sign_start + i as f64 * 30.0);
}
cusps
}
fn porphyry_houses(asc: f64, mc: f64) -> [f64; 12] {
let dsc = normalize_degrees(asc + 180.0);
let ic = normalize_degrees(mc + 180.0);
let q1 = arc_between(asc, ic); let q2 = arc_between(ic, dsc); let q3 = arc_between(dsc, mc); let q4 = arc_between(mc, asc);
let mut cusps = [0.0; 12];
cusps[0] = asc;
cusps[1] = normalize_degrees(asc + q1 / 3.0);
cusps[2] = normalize_degrees(asc + 2.0 * q1 / 3.0);
cusps[3] = ic;
cusps[4] = normalize_degrees(ic + q2 / 3.0);
cusps[5] = normalize_degrees(ic + 2.0 * q2 / 3.0);
cusps[6] = dsc;
cusps[7] = normalize_degrees(dsc + q3 / 3.0);
cusps[8] = normalize_degrees(dsc + 2.0 * q3 / 3.0);
cusps[9] = mc;
cusps[10] = normalize_degrees(mc + q4 / 3.0);
cusps[11] = normalize_degrees(mc + 2.0 * q4 / 3.0);
cusps
}
fn placidus_houses(
asc: f64,
mc: f64,
_lst_deg: f64,
lat_deg: f64,
obliquity_deg: f64,
) -> Result<[f64; 12]> {
let lat = deg_to_rad(lat_deg);
let eps = deg_to_rad(obliquity_deg);
if lat_deg.abs() > 66.0 {
return Err(JyotishError::InvalidParameter(
"Placidus houses undefined for latitudes above ±66°".into(),
));
}
let dsc = normalize_degrees(asc + 180.0);
let ic = normalize_degrees(mc + 180.0);
let mut cusps = [0.0; 12];
cusps[0] = asc;
cusps[3] = ic;
cusps[6] = dsc;
cusps[9] = mc;
for (house_idx, fraction) in [(1, 1.0 / 3.0), (2, 2.0 / 3.0)] {
cusps[house_idx] = placidus_cusp(mc, fraction, lat, eps, false);
cusps[house_idx + 6] = normalize_degrees(cusps[house_idx] + 180.0);
}
for (house_idx, fraction) in [(4, 1.0 / 3.0), (5, 2.0 / 3.0)] {
cusps[house_idx] = placidus_cusp(mc, fraction, lat, eps, true);
cusps[house_idx + 6] = normalize_degrees(cusps[house_idx] + 180.0);
}
Ok(cusps)
}
fn placidus_cusp(mc: f64, fraction: f64, lat: f64, eps: f64, below_horizon: bool) -> f64 {
let mc_rad = deg_to_rad(mc);
let mut cusp = if below_horizon {
mc_rad + PI * fraction
} else {
mc_rad + PI + PI * fraction
};
for _ in 0..50 {
let dec = (eps.sin() * cusp.sin()).asin();
let ad_arg = lat.tan() * dec.tan();
let ad = ad_arg.clamp(-0.999_999, 0.999_999).asin();
let sa = PI / 2.0 + ad;
let dsa = if below_horizon { PI - sa } else { sa };
let new_cusp = mc_rad + (cusp - mc_rad).rem_euclid(2.0 * PI);
let ra_fraction = fraction * dsa;
let target_ra = if below_horizon {
mc_rad + PI + ra_fraction
} else {
mc_rad + PI + sa + ra_fraction
};
let diff = target_ra - new_cusp;
if diff.abs() < 1e-10 {
break;
}
cusp += diff * 0.5;
}
normalize_degrees(rad_to_deg(cusp))
}
fn arc_between(from: f64, to: f64) -> f64 {
let arc = to - from;
if arc < 0.0 { arc + 360.0 } else { arc }
}
#[cfg(test)]
mod tests {
use super::*;
const LST: f64 = 197.693; const LAT: f64 = 51.5; const EPS: f64 = 23.44;
#[test]
fn ascendant_in_range() {
let asc = ascendant(LST, LAT, EPS);
assert!((0.0..360.0).contains(&asc), "ASC = {asc}");
}
#[test]
fn midheaven_in_range() {
let mc = midheaven(LST, EPS);
assert!((0.0..360.0).contains(&mc), "MC = {mc}");
}
#[test]
fn equal_houses_30_degree_spacing() {
let houses = compute_houses(HouseSystem::Equal, LST, LAT, EPS).unwrap();
for i in 0..11 {
let diff = arc_between(houses.cusps[i], houses.cusps[i + 1]);
assert!(
(diff - 30.0).abs() < 1e-10,
"house {i} to {} gap = {diff}",
i + 1
);
}
}
#[test]
fn whole_sign_houses_aligned_to_signs() {
let houses = compute_houses(HouseSystem::WholeSign, LST, LAT, EPS).unwrap();
for cusp in &houses.cusps {
assert!(
(cusp % 30.0).abs() < 1e-10 || (cusp % 30.0 - 30.0).abs() < 1e-10,
"cusp {cusp} not on sign boundary"
);
}
}
#[test]
fn porphyry_houses_quadrant_trisection() {
let houses = compute_houses(HouseSystem::Porphyry, LST, LAT, EPS).unwrap();
assert!(
(houses.cusps[0] - houses.ascendant).abs() < 1e-10,
"cusp[0] = {}, ASC = {}",
houses.cusps[0],
houses.ascendant
);
let ic = normalize_degrees(houses.midheaven + 180.0);
assert!(
(houses.cusps[3] - ic).abs() < 1e-10,
"cusp[3] = {}, IC = {ic}",
houses.cusps[3]
);
}
#[test]
fn placidus_houses_basic() {
let houses = compute_houses(HouseSystem::Placidus, LST, LAT, EPS).unwrap();
assert!(
(houses.cusps[0] - houses.ascendant).abs() < 1e-10,
"cusp[0] should be ASC"
);
assert!(
(houses.cusps[9] - houses.midheaven).abs() < 1e-10,
"cusp[9] should be MC"
);
}
#[test]
fn placidus_fails_at_poles() {
assert!(compute_houses(HouseSystem::Placidus, LST, 80.0, EPS).is_err());
assert!(compute_houses(HouseSystem::Placidus, LST, 66.1, EPS).is_err());
assert!(compute_houses(HouseSystem::Placidus, LST, -66.1, EPS).is_err());
assert!(compute_houses(HouseSystem::Placidus, LST, 65.9, EPS).is_ok());
}
#[test]
fn all_systems_produce_12_cusps() {
for system in [
HouseSystem::Equal,
HouseSystem::WholeSign,
HouseSystem::Porphyry,
HouseSystem::Placidus,
] {
let houses = compute_houses(system, LST, LAT, EPS).unwrap();
assert_eq!(houses.cusps.len(), 12, "{system:?} produced wrong count");
for (i, &cusp) in houses.cusps.iter().enumerate() {
assert!(
(0.0..360.0).contains(&cusp),
"{system:?} cusp {i} = {cusp} out of range"
);
}
}
}
#[test]
fn house_system_serde() {
let sys = HouseSystem::Porphyry;
let json = serde_json::to_string(&sys).unwrap();
let restored: HouseSystem = serde_json::from_str(&json).unwrap();
assert_eq!(restored, sys);
}
#[test]
fn ascendant_varies_with_latitude() {
let asc_london = ascendant(LST, 51.5, EPS);
let asc_equator = ascendant(LST, 0.0, EPS);
assert!(
(asc_london - asc_equator).abs() > 1.0,
"ASC should vary with latitude"
);
}
}