use std::f64::consts::PI;
use crate::frames::Orientation;
use crate::pyfloat;
use crate::time::Time;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum HouseSystem {
Placidus,
Koch,
Regiomontanus,
Campanus,
Topocentric,
Alcabitius,
Morinus,
Porphyry,
Equal,
Vehlow,
WholeSign,
}
impl HouseSystem {
pub fn from_code(code: &str) -> Self {
match code.chars().next().map(|c| c.to_ascii_uppercase()) {
Some('K') => HouseSystem::Koch,
Some('R') => HouseSystem::Regiomontanus,
Some('C') => HouseSystem::Campanus,
Some('T') => HouseSystem::Topocentric,
Some('B') => HouseSystem::Alcabitius,
Some('M') => HouseSystem::Morinus,
Some('O') => HouseSystem::Porphyry,
Some('E') => HouseSystem::Equal,
Some('V') => HouseSystem::Vehlow,
Some('W') => HouseSystem::WholeSign,
_ => HouseSystem::Placidus,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Houses {
pub cusps: [f64; 12],
pub ascendant: f64,
pub midheaven: f64,
}
pub fn calculate_houses(jd: f64, lat: f64, lon: f64, system: HouseSystem, shift: f64) -> Houses {
let t = Time::from_ut1(jd);
let gast_hours = Orientation::at(&t).gast_hours;
let ramc = pyfloat::rem(pyfloat::rem(gast_hours * 15.0 + lon, 360.0) + 360.0, 360.0);
let ramc_r = ramc.to_radians();
let lat_r = lat.to_radians();
let t_cent = (jd - 2_451_545.0) / 36525.0;
let eps_deg = 23.4392911 - 0.0130042 * t_cent - 0.00000016 * t_cent.powf(2.0)
+ 0.000000504 * t_cent.powf(3.0);
let eps_r = eps_deg.to_radians();
let mc_deg = pyfloat::rem(
ramc_r.sin().atan2(ramc_r.cos() * eps_r.cos()).to_degrees(),
360.0,
);
let y = ramc_r.cos();
let x = -ramc_r.sin() * eps_r.cos() - lat_r.tan() * eps_r.sin();
let asc_deg = pyfloat::rem(y.atan2(x).to_degrees(), 360.0);
let mut c = [0.0_f64; 13]; match system {
HouseSystem::WholeSign => {
let asc_sign = pyfloat::floordiv(asc_deg, 30.0);
for (i, cusp) in c.iter_mut().enumerate().skip(1) {
*cusp = pyfloat::rem((asc_sign + i as f64 - 1.0) * 30.0, 360.0);
}
}
HouseSystem::Equal => {
for (i, cusp) in c.iter_mut().enumerate().skip(1) {
*cusp = pyfloat::rem(asc_deg + (i as f64 - 1.0) * 30.0, 360.0);
}
}
HouseSystem::Vehlow => {
for (i, cusp) in c.iter_mut().enumerate().skip(1) {
*cusp = pyfloat::rem(asc_deg - 15.0 + (i as f64 - 1.0) * 30.0 + 360.0, 360.0);
}
}
HouseSystem::Morinus => {
for i in 1..=12 {
let ra = (ramc + 30.0 * i as f64).to_radians();
c[(i + 9) % 12 + 1] = norm((ra.sin() * eps_r.cos()).atan2(ra.cos()).to_degrees());
}
}
HouseSystem::Koch if lat.abs() >= 90.0 - eps_deg => {
porphyry(&mut c, asc_deg, mc_deg);
}
HouseSystem::Koch
| HouseSystem::Regiomontanus
| HouseSystem::Campanus
| HouseSystem::Topocentric
| HouseSystem::Alcabitius => {
set_angles(&mut c, asc_deg, mc_deg);
let circles: [(f64, f64); 4] = match system {
HouseSystem::Koch => {
let sin_a =
(mc_deg.to_radians().sin() * eps_r.sin() / lat_r.cos()).clamp(-1.0, 1.0);
let cos_a = (1.0 - sin_a * sin_a).sqrt();
let pole = (lat_r.tan() / cos_a).atan();
let ad3 = (pole.sin() * sin_a).asin().to_degrees() / 3.0;
[
(ramc + 30.0 - 2.0 * ad3, lat),
(ramc + 60.0 - ad3, lat),
(ramc + 120.0 + ad3, lat),
(ramc + 150.0 + 2.0 * ad3, lat),
]
}
HouseSystem::Regiomontanus | HouseSystem::Topocentric => {
let (k1, k2) = if system == HouseSystem::Regiomontanus {
(0.5, 30.0_f64.to_radians().cos())
} else {
(1.0 / 3.0, 2.0 / 3.0)
};
let p1 = (lat_r.tan() * k1).atan().to_degrees();
let p2 = (lat_r.tan() * k2).atan().to_degrees();
[
(ramc + 30.0, p1),
(ramc + 60.0, p2),
(ramc + 120.0, p2),
(ramc + 150.0, p1),
]
}
HouseSystem::Campanus => {
let p1 = (lat_r.sin() / 2.0).asin().to_degrees();
let p2 = (3.0_f64.sqrt() / 2.0 * lat_r.sin()).asin().to_degrees();
let x1 = (3.0_f64.sqrt() / lat_r.cos()).atan().to_degrees();
let x2 = (1.0 / 3.0_f64.sqrt() / lat_r.cos()).atan().to_degrees();
[
(ramc + 90.0 - x1, p1),
(ramc + 90.0 - x2, p2),
(ramc + 90.0 + x2, p2),
(ramc + 90.0 + x1, p1),
]
}
_ => {
let dec = (asc_deg.to_radians().sin() * eps_r.sin()).asin();
let sda = (-lat_r.tan() * dec.tan())
.clamp(-1.0, 1.0)
.acos()
.to_degrees();
let sna = 180.0 - sda;
[
(ramc + sda / 3.0, 0.0),
(ramc + 2.0 * sda / 3.0, 0.0),
(ramc + 180.0 - 2.0 * sna / 3.0, 0.0),
(ramc + 180.0 - sna / 3.0, 0.0),
]
}
};
for (house, (ra, pole)) in [11, 12, 2, 3].into_iter().zip(circles) {
c[house] = ecliptic_intersection(ra, pole, eps_r);
}
opposite_cusps(&mut c);
}
HouseSystem::Porphyry => porphyry(&mut c, asc_deg, mc_deg),
HouseSystem::Placidus => {
set_angles(&mut c, asc_deg, mc_deg);
let solve = |offset: f64, semi_factor: f64| {
let target_ra = pyfloat::rem(ramc + offset, 360.0).to_radians();
let mut lon = pyfloat::rem(ramc + offset, 360.0);
for _ in 0..25 {
let l_r = lon.to_radians();
let sin_d = eps_r.sin() * l_r.sin();
let decl = sin_d.clamp(-1.0, 1.0).asin();
let ra = pyfloat::rem((eps_r.cos() * l_r.sin()).atan2(l_r.cos()), 2.0 * PI);
let tan_d = decl.tan() * lat_r.tan();
if tan_d.abs() >= 1.0 {
break;
}
let ad = tan_d.asin();
let diff = (ra - target_ra) - semi_factor * ad;
let diff = pyfloat::rem(diff + PI, 2.0 * PI) - PI;
if diff.abs() < 1e-8 {
break;
}
lon = pyfloat::rem(lon - diff.to_degrees(), 360.0);
}
lon
};
c[11] = solve(30.0, 1.0 / 3.0);
c[12] = solve(60.0, 2.0 / 3.0);
c[9] = solve(-30.0, -1.0 / 3.0);
c[8] = solve(-60.0, -2.0 / 3.0);
c[5] = pyfloat::rem(c[11] + 180.0, 360.0);
c[6] = pyfloat::rem(c[12] + 180.0, 360.0);
c[2] = pyfloat::rem(c[8] + 180.0, 360.0);
c[3] = pyfloat::rem(c[9] + 180.0, 360.0);
}
}
let mut cusps = [0.0; 12];
for i in 0..12 {
cusps[i] = pyfloat::rem(c[i + 1] - shift, 360.0);
}
Houses {
cusps,
ascendant: pyfloat::rem(asc_deg - shift, 360.0),
midheaven: pyfloat::rem(mc_deg - shift, 360.0),
}
}
fn norm(deg: f64) -> f64 {
pyfloat::rem(pyfloat::rem(deg, 360.0) + 360.0, 360.0)
}
fn ecliptic_intersection(ra: f64, pole: f64, eps_r: f64) -> f64 {
let ra = ra.to_radians();
let x = ra.cos() * eps_r.cos() - pole.to_radians().tan() * eps_r.sin();
norm(ra.sin().atan2(x).to_degrees())
}
fn porphyry(c: &mut [f64; 13], asc_deg: f64, mc_deg: f64) {
set_angles(c, asc_deg, mc_deg);
let q1 = pyfloat::rem(asc_deg - mc_deg, 360.0);
c[11] = pyfloat::rem(mc_deg + q1 / 3.0, 360.0);
c[12] = pyfloat::rem(mc_deg + (2.0 * q1) / 3.0, 360.0);
let q2 = pyfloat::rem(c[4] - asc_deg, 360.0);
c[2] = pyfloat::rem(asc_deg + q2 / 3.0, 360.0);
c[3] = pyfloat::rem(asc_deg + (2.0 * q2) / 3.0, 360.0);
opposite_cusps(c);
}
fn opposite_cusps(c: &mut [f64; 13]) {
c[5] = pyfloat::rem(c[11] + 180.0, 360.0);
c[6] = pyfloat::rem(c[12] + 180.0, 360.0);
c[8] = pyfloat::rem(c[2] + 180.0, 360.0);
c[9] = pyfloat::rem(c[3] + 180.0, 360.0);
}
fn set_angles(c: &mut [f64; 13], asc: f64, mc: f64) {
c[1] = asc;
c[10] = mc;
c[7] = pyfloat::rem(asc + 180.0, 360.0);
c[4] = pyfloat::rem(mc + 180.0, 360.0);
}
#[cfg(test)]
mod tests {
use super::*;
const JD: f64 = 2_448_027.104_166_666_5;
fn gap(a: f64, b: f64) -> f64 {
let d = (a - b).rem_euclid(360.0);
d.min(360.0 - d)
}
#[test]
fn systems_match_swiss_ephemeris() {
let reference: [(&str, [f64; 12]); 7] = [
(
"K",
[
190.2408, 219.3528, 248.6584, 282.0137, 311.8995, 340.9197, 10.2408, 39.3528,
68.6584, 102.0137, 131.8995, 160.9197,
],
),
(
"R",
[
190.2408, 214.8788, 245.0128, 282.0137, 317.7672, 346.2050, 10.2408, 34.8788,
65.0128, 102.0137, 137.7672, 166.2050,
],
),
(
"C",
[
190.2408, 222.0070, 252.8709, 282.0137, 310.3369, 339.4310, 10.2408, 42.0070,
72.8709, 102.0137, 130.3369, 159.4310,
],
),
(
"T",
[
190.2408, 216.9311, 248.0089, 282.0137, 315.5716, 345.3618, 10.2408, 36.9311,
68.0089, 102.0137, 135.5716, 165.3618,
],
),
(
"B",
[
190.2408, 223.0794, 253.2543, 282.0137, 309.4047, 339.0304, 10.2408, 43.0794,
73.2543, 102.0137, 129.4047, 159.0304,
],
),
(
"M",
[
192.0137, 220.6069, 251.6333, 284.1881, 315.5252, 344.3861, 12.0137, 40.6069,
71.6333, 104.1881, 135.5252, 164.3861,
],
),
(
"V",
[
175.2408, 205.2408, 235.2408, 265.2408, 295.2408, 325.2408, 355.2408, 25.2408,
55.2408, 85.2408, 115.2408, 145.2408,
],
),
];
for (code, expected) in reference {
let houses = calculate_houses(JD, 41.9, 12.5, HouseSystem::from_code(code), 0.0);
for (i, (ours, theirs)) in houses.cusps.iter().zip(expected).enumerate() {
assert!(
gap(*ours, theirs) < 1.0 / 60.0,
"{code} cusp {}: {ours:.4} vs {theirs:.4}",
i + 1
);
}
}
}
#[test]
fn codes_select_their_systems() {
for (code, system) in [
("P", HouseSystem::Placidus),
("koch", HouseSystem::Koch),
("R", HouseSystem::Regiomontanus),
("C", HouseSystem::Campanus),
("T", HouseSystem::Topocentric),
("B", HouseSystem::Alcabitius),
("M", HouseSystem::Morinus),
("O", HouseSystem::Porphyry),
("E", HouseSystem::Equal),
("V", HouseSystem::Vehlow),
("W", HouseSystem::WholeSign),
("?", HouseSystem::Placidus),
("", HouseSystem::Placidus),
] {
assert_eq!(HouseSystem::from_code(code), system, "{code}");
}
}
#[test]
fn koch_falls_back_to_porphyry_inside_the_polar_circles() {
let koch = calculate_houses(JD, 70.0, 25.0, HouseSystem::Koch, 0.0);
let porphyry = calculate_houses(JD, 70.0, 25.0, HouseSystem::Porphyry, 0.0);
assert_eq!(koch, porphyry);
}
}