"""House system calculation engines."""
import swisseph as swe
from stellium.core.ayanamsa import ZodiacType, get_ayanamsa
from stellium.core.config import CalculationConfig
from stellium.core.models import (
CelestialPosition,
ChartDateTime,
ChartLocation,
HouseCusps,
ObjectType,
)
from stellium.utils.cache import cached
# Swiss Ephemeris house system codes
HOUSE_SYSTEM_CODES = {
"Alcabitius": b"B",
"APC": b"Y",
"Axial Rotation": b"X",
"Campanus": b"C",
"Equal": b"A",
"Equal (MC)": b"D",
"Equal (Vertex)": b"E",
"Gauquelin": b"G",
"Horizontal": b"H",
"Koch": b"K",
"Krusinski": b"U",
"Morinus": b"M",
"Placidus": b"P",
"Porphyry": b"O",
"Regiomontanus": b"R",
"Topocentric": b"T",
"Vehlow Equal": b"V",
"Whole Sign": b"W",
}
[docs]
class SwissHouseSystemBase:
"""
Provides a default implementation for calling swisseph and assigning houses.
This is NOT a protocol, just a helper class for code reuse.
"""
@property
def system_name(self) -> str:
return "BaseClass"
def _setup_sidereal_mode(self, config: CalculationConfig | None) -> None:
"""Set up sidereal mode if needed.
Args:
config: Calculation configuration (None = use tropical)
"""
if config and config.zodiac_type == ZodiacType.SIDEREAL:
if config.ayanamsa is None:
raise ValueError("Ayanamsa must be specified for sidereal calculations")
ayanamsa_info = get_ayanamsa(config.ayanamsa)
swe.set_sid_mode(ayanamsa_info.swe_constant)
def _get_calculation_flags(self, config: CalculationConfig | None) -> int:
"""Get Swiss Ephemeris flags based on configuration.
Args:
config: Calculation configuration (None = use tropical)
Returns:
Flags for swe.houses_ex()
"""
flags = 0 # Default for tropical
if config and config.zodiac_type == ZodiacType.SIDEREAL:
flags = swe.FLG_SIDEREAL
return flags
@cached(cache_type="ephemeris", max_age_seconds=86400)
def _calculate_swiss_houses(
self,
julian_day: float,
latitude: float,
longitude: float,
system_code: bytes,
config: CalculationConfig | None = None,
) -> tuple:
"""Cached Swiss Ephemeris house calculation.
Args:
julian_day: Julian day number
latitude: Geographic latitude
longitude: Geographic longitude
system_code: House system code (e.g., b"P" for Placidus)
config: Calculation configuration (for zodiac type)
Returns:
Tuple of (cusps, angles) from Swiss Ephemeris
"""
# Set up sidereal mode if needed
self._setup_sidereal_mode(config)
# Get appropriate flags
flags = self._get_calculation_flags(config)
# Use houses_ex for sidereal support
return swe.houses_ex(
julian_day, latitude, longitude, hsys=system_code, flags=flags
)
[docs]
def assign_houses(
self, positions: list[CelestialPosition], cusps: HouseCusps
) -> dict[str, int]:
"""Assign house numbers to positions. Returns a simple name: house dict."""
placements = {}
for pos in positions:
house_num = self._find_house(pos.longitude, cusps.cusps)
placements[pos.name] = house_num
return placements
def _find_house(self, longitude: float, cusps: tuple) -> int:
"""Find which house a longitude falls into."""
cusp_list = list(cusps)
for i in range(12):
cusp1 = cusp_list[i]
cusp2 = cusp_list[(i + 1) % 12]
# Handles wrapping about 360 degrees
if cusp2 < cusp1:
cusp2 += 360
test_long = longitude if longitude >= cusp1 else longitude + 360
else:
test_long = longitude
if cusp1 <= test_long < cusp2:
return i + 1
return 1 # fallback
[docs]
def calculate_house_data(
self,
datetime: ChartDateTime,
location: ChartLocation,
config: CalculationConfig | None = None,
) -> tuple[HouseCusps, list[CelestialPosition]]:
"""Calculate house system's house cusps and chart angles.
Args:
datetime: Chart datetime
location: Chart location
config: Calculation configuration (for zodiac type)
Returns:
Tuple of (house cusps, angle positions)
"""
# Cusps
cusps_list, angles_list = self._calculate_swiss_houses(
datetime.julian_day,
location.latitude,
location.longitude,
HOUSE_SYSTEM_CODES[self.system_name],
config,
)
cusps = HouseCusps(system=self.system_name, cusps=tuple(cusps_list))
# Chart angles
asc = angles_list[0]
mc = angles_list[1]
ramc = angles_list[2]
vertex = angles_list[3]
angles = [
CelestialPosition(name="ASC", object_type=ObjectType.ANGLE, longitude=asc),
CelestialPosition(name="MC", object_type=ObjectType.ANGLE, longitude=mc),
# Derive Dsc and IC
CelestialPosition(
name="DSC", object_type=ObjectType.ANGLE, longitude=(asc + 180) % 360
),
CelestialPosition(
name="IC", object_type=ObjectType.ANGLE, longitude=(mc + 180) % 360
),
# Include Vertex
CelestialPosition(
name="Vertex", object_type=ObjectType.POINT, longitude=vertex
),
CelestialPosition(
name="RAMC", object_type=ObjectType.TECHNICAL, longitude=ramc
),
]
return cusps, angles
[docs]
class PlacidusHouses(SwissHouseSystemBase):
"""Placidus house system engine."""
@property
def system_name(self) -> str:
return "Placidus"
[docs]
class WholeSignHouses(SwissHouseSystemBase):
"""Whole sign house system engine."""
@property
def system_name(self) -> str:
return "Whole Sign"
[docs]
class KochHouses(SwissHouseSystemBase):
"""Koch house system engine."""
@property
def system_name(self) -> str:
return "Koch"
[docs]
class EqualHouses(SwissHouseSystemBase):
"""Equal house system engine."""
@property
def system_name(self) -> str:
return "Equal"
[docs]
class PorphyryHouses(SwissHouseSystemBase):
"""Porphyry house system engine."""
@property
def system_name(self) -> str:
return "Porphyry"
[docs]
class RegiomontanusHouses(SwissHouseSystemBase):
"""Regiomontanus house system engine."""
@property
def system_name(self) -> str:
return "Regiomontanus"
[docs]
class CampanusHouses(SwissHouseSystemBase):
"""Campanus house system engine."""
@property
def system_name(self) -> str:
return "Campanus"
[docs]
class EqualMCHouses(SwissHouseSystemBase):
"""Equal (MC) house system engine."""
@property
def system_name(self) -> str:
return "Equal (MC)"
[docs]
class VehlowEqualHouses(SwissHouseSystemBase):
"""Vehlow Equal house system engine."""
@property
def system_name(self) -> str:
return "Vehlow Equal"
[docs]
class AlcabitiusHouses(SwissHouseSystemBase):
"""Alcabitius house system engine."""
@property
def system_name(self) -> str:
return "Alcabitius"
[docs]
class TopocentricHouses(SwissHouseSystemBase):
"""Topocentric house system engine."""
@property
def system_name(self) -> str:
return "Topocentric"
[docs]
class MorinusHouses(SwissHouseSystemBase):
"""Morinus house system engine."""
@property
def system_name(self) -> str:
return "Morinus"
[docs]
class EqualVertexHouses(SwissHouseSystemBase):
"""Equal (Vertex) house system engine."""
@property
def system_name(self) -> str:
return "Equal (Vertex)"
[docs]
class GauquelinHouses(SwissHouseSystemBase):
"""Gauquelin house system engine."""
@property
def system_name(self) -> str:
return "Gauquelin"
[docs]
class HorizontalHouses(SwissHouseSystemBase):
"""Horizontal house system engine."""
@property
def system_name(self) -> str:
return "Horizontal"
[docs]
class KrusinskiHouses(SwissHouseSystemBase):
"""Krusinski house system engine."""
@property
def system_name(self) -> str:
return "Krusinski"
[docs]
class AxialRotationHouses(SwissHouseSystemBase):
"""Axial Rotation house system engine."""
@property
def system_name(self) -> str:
return "Axial Rotation"
[docs]
class APCHouses(SwissHouseSystemBase):
"""APC house system engine."""
@property
def system_name(self) -> str:
return "APC"