use placidus house system, and system theme

This commit is contained in:
Bendik Aagaard Lynghaug
2026-03-23 12:57:25 +01:00
parent a6cb1bcf97
commit cee860050b
3 changed files with 96 additions and 16 deletions
-4
View File
@@ -63,10 +63,6 @@ fn main() {
// ── GTK / ADW application ─────────────────────────────────────────────────
let app = RelmApp::new("net.zodia.app");
// Request dark colour scheme — adwaita will use the system preference as
// fallback if the compositor ignores prefer-dark-scheme.
adw::StyleManager::default().set_color_scheme(adw::ColorScheme::PreferDark);
// Add the bundled icon resource path to the default icon theme so GTK
// resolves our icons on every platform including macOS.
if let Some(display) = gtk4::gdk::Display::default() {
+2 -2
View File
@@ -16,10 +16,10 @@ pub struct Chart {
}
impl Chart {
/// Compute a chart using Whole Sign houses (default).
/// Compute a chart using Placidus houses (default).
/// Falls back to an all-zero house stub if the geohash is too coarse.
pub fn compute(birth: BirthData) -> Result<Self, EphemerisError> {
Self::compute_with(birth, HouseKind::WholeSign)
Self::compute_with(birth, HouseKind::Placidus)
}
/// Compute a chart with an explicit house system.
+94 -10
View File
@@ -5,12 +5,12 @@
//! length — a 5-char hash gives ~±2.5 km, which is ±0.1° in ASC at mid-latitudes.
//!
//! Implemented systems:
//! - Placidus — semi-arc method; most widely used in Western astrology (default)
//! - Whole Sign — ASC determines the 1st house sign; each sign = one house
//! - Equal House — each cusp is exactly 30° from the ASC
//!
//! Stubs (fall back to Equal):
//! - Placidus and Koch require iterative latitude-dependent solving;
//! they will be added in a dedicated follow-up.
//! - Koch — similar iterative approach, not yet implemented
use crate::birth::BirthData;
use crate::planet::{Planet, PlanetPositions};
@@ -44,21 +44,22 @@ pub enum HouseError {
impl HouseSystem {
/// Compute house cusps for `birth` using the requested system.
///
/// Falls back to Equal House for Placidus/Koch until those are implemented.
pub fn compute(birth: &BirthData, kind: HouseKind) -> Result<Self, HouseError> {
if birth.geohash.len() < 3 {
return Err(HouseError::GeohashTooShort(birth.geohash.len()));
}
let (lat, lon) = decode_geohash(&birth.geohash)?;
let asc = ascendant(birth.jdn, lat, lon);
let mc = midheaven(birth.jdn, lon);
let asc = ascendant(birth.jdn, lat, lon);
let mc = midheaven(birth.jdn, lon);
let ramc = (gmst(birth.jdn) + lon).rem_euclid(360.0);
let eps = obliquity(birth.jdn);
let cusps = match kind {
HouseKind::WholeSign => whole_sign_cusps(asc),
HouseKind::Equal => equal_house_cusps(asc),
// Placidus and Koch: fall back to Equal for now
HouseKind::Placidus | HouseKind::Koch => equal_house_cusps(asc),
HouseKind::WholeSign => whole_sign_cusps(asc),
HouseKind::Equal => equal_house_cusps(asc),
HouseKind::Placidus => placidus_cusps(ramc, lat, eps, mc, asc),
// Koch: fall back to Placidus (close enough; full Koch to be added later)
HouseKind::Koch => placidus_cusps(ramc, lat, eps, mc, asc),
};
Ok(Self { kind, cusps, ascendant: asc, midheaven: mc })
@@ -169,3 +170,86 @@ fn equal_house_cusps(asc: f64) -> [f64; 12] {
}
cusps
}
/// Placidus house cusps — semi-arc method.
///
/// Houses 1, 4, 7, 10 are fixed at ASC, IC, DSC, MC.
/// Intermediate cusps (11, 12, 2, 3) are found by dividing each quadrant's
/// semi-arc into thirds, solved iteratively (~510 iterations converge to <1e-6°).
///
/// `ramc`: Right Ascension of MC (= Local Sidereal Time, degrees)
/// `lat`: geographic latitude (degrees)
/// `eps`: obliquity of the ecliptic (degrees)
/// `mc`: MC ecliptic longitude (degrees)
/// `asc`: Ascendant ecliptic longitude (degrees)
fn placidus_cusps(ramc: f64, lat: f64, eps: f64, mc: f64, asc: f64) -> [f64; 12] {
let phi = lat.to_radians();
let eps_r = eps.to_radians();
let mut cusps = [0.0f64; 12];
cusps[0] = asc; // H1 = ASC
cusps[3] = (mc + 180.0).rem_euclid(360.0); // H4 = IC
cusps[6] = (asc + 180.0).rem_euclid(360.0); // H7 = DSC
cusps[9] = mc; // H10 = MC
// H11, H12: 1/3 and 2/3 of the diurnal semi-arc from MC toward ASC.
for (i, frac) in [(10usize, 1.0_f64 / 3.0), (11, 2.0 / 3.0)] {
let ra = placidus_iter(ramc, frac, phi, eps_r, true);
cusps[i] = ra_to_ecl_lon(ra, eps_r);
}
// H2, H3: 1/3 and 2/3 of the nocturnal semi-arc from IC toward DSC.
let ic_ra = (ramc + 180.0).rem_euclid(360.0);
for (i, frac) in [(1usize, 1.0_f64 / 3.0), (2, 2.0 / 3.0)] {
let ra = placidus_iter(ic_ra, frac, phi, eps_r, false);
cusps[i] = ra_to_ecl_lon(ra, eps_r);
}
// Opposite cusps.
cusps[4] = (cusps[10] + 180.0).rem_euclid(360.0); // H5 = opposite H11
cusps[5] = (cusps[11] + 180.0).rem_euclid(360.0); // H6 = opposite H12
cusps[7] = (cusps[1] + 180.0).rem_euclid(360.0); // H8 = opposite H2
cusps[8] = (cusps[2] + 180.0).rem_euclid(360.0); // H9 = opposite H3
cusps
}
/// Iterate to find the RA of one Placidus intermediate cusp.
///
/// `base_ra`: starting RA of the quadrant (RAMC for H11/12, IC_RA for H2/3).
/// `frac`: 1/3 or 2/3.
/// `diurnal`: true → use diurnal semi-arc (DSA); false → nocturnal (NSA = π DSA).
fn placidus_iter(base_ra: f64, frac: f64, phi: f64, eps: f64, diurnal: bool) -> f64 {
// Seed the iteration with a 60° (1/3 of 180°) or 120° (2/3) offset.
let mut ra = (base_ra + frac * 180.0).rem_euclid(360.0);
for _ in 0..50 {
// Ecliptic longitude corresponding to this RA (assuming β = 0).
let ecl = ra_to_ecl_lon(ra, eps).to_radians();
// Declination of that ecliptic point.
let dec = (eps.sin() * ecl.sin()).asin();
// Diurnal semi-arc: the angular distance from the eastern horizon to the MC.
let cos_dsa = (-phi.tan() * dec.tan()).clamp(-1.0, 1.0);
let dsa = cos_dsa.acos(); // radians, 0–π
let semi_arc = if diurnal { dsa } else { std::f64::consts::PI - dsa };
let new_ra = (base_ra.to_radians() + frac * semi_arc)
.to_degrees()
.rem_euclid(360.0);
if (new_ra - ra).abs() < 1e-6 {
return new_ra;
}
ra = new_ra;
}
ra
}
/// Convert equatorial RA (degrees) to ecliptic longitude (degrees), assuming β = 0.
///
/// Inverse of: tan(RA) = sin(λ)·cos(ε) / cos(λ) → λ = atan2(sin(RA), cos(RA)·cos(ε))
fn ra_to_ecl_lon(ra: f64, eps: f64) -> f64 {
let ra_r = ra.to_radians();
ra_r.sin()
.atan2(ra_r.cos() * eps.cos())
.to_degrees()
.rem_euclid(360.0)
}