Source code for stellium.engines.fixed_stars

"""
Fixed Stars calculation engine using Swiss Ephemeris.

This module provides the engine for calculating fixed star positions at any given time.
Swiss Ephemeris handles precession automatically - just pass the Julian Day and it returns
the correct ecliptic longitude for that epoch.

Usage:
    >>> from stellium.engines.fixed_stars import SwissEphemerisFixedStarsEngine
    >>> engine = SwissEphemerisFixedStarsEngine()
    >>> stars = engine.calculate_stars(julian_day=2451545.0)  # All registered stars
    >>> royal_stars = engine.calculate_stars(julian_day, stars=["Regulus", "Aldebaran"])
"""

from typing import Protocol

import swisseph as swe

from stellium.core.models import FixedStarPosition, ObjectType
from stellium.core.registry import FIXED_STARS_REGISTRY, FixedStarInfo
from stellium.data.paths import initialize_ephemeris


def _set_ephemeris_path() -> None:
    """Set the path to Swiss Ephemeris data files (including sefstars.txt)."""
    initialize_ephemeris()


[docs] class FixedStarsEngine(Protocol): """ Protocol for fixed star calculation engines. Implementations must provide a method to calculate positions for fixed stars at a given Julian Day. """
[docs] def calculate_stars( self, julian_day: float, stars: list[str] | None = None, ) -> list[FixedStarPosition]: """ Calculate positions for specified fixed stars. Args: julian_day: The Julian Day for calculation stars: List of star names to calculate. If None, calculates all stars in FIXED_STARS_REGISTRY. Returns: List of FixedStarPosition objects with calculated positions """ ...
[docs] class SwissEphemerisFixedStarsEngine: """ Swiss Ephemeris implementation of fixed star calculations. Uses swe.fixstar_ut() to calculate precise ecliptic positions for fixed stars, with automatic precession handling. The engine pulls metadata from FIXED_STARS_REGISTRY to enrich the position objects with traditional astrological meanings. Attributes: registry: The fixed star registry to use (defaults to FIXED_STARS_REGISTRY) Example: >>> engine = SwissEphemerisFixedStarsEngine() >>> # Calculate all stars >>> all_stars = engine.calculate_stars(julian_day=2451545.0) >>> # Calculate specific stars >>> royal = engine.calculate_stars(2451545.0, stars=["Regulus", "Aldebaran"]) >>> print(f"{royal[0].name}: {royal[0].sign_position}") Regulus: 29°50' Leo """ def __init__(self, registry: dict[str, FixedStarInfo] | None = None): """ Initialize the fixed stars engine. Args: registry: Optional custom registry. Defaults to FIXED_STARS_REGISTRY. """ # Ensure ephemeris path is set for sefstars.txt _set_ephemeris_path() self.registry = registry if registry is not None else FIXED_STARS_REGISTRY
[docs] def calculate_stars( self, julian_day: float, stars: list[str] | None = None, ) -> list[FixedStarPosition]: """ Calculate positions for specified fixed stars. Args: julian_day: The Julian Day for calculation. Swiss Ephemeris handles precession automatically based on this value. stars: List of star names to calculate. If None, calculates all stars in the registry. Returns: List of FixedStarPosition objects with calculated ecliptic positions and registry metadata. Raises: ValueError: If a requested star is not in the registry """ if stars is None: stars = list(self.registry.keys()) results: list[FixedStarPosition] = [] for star_name in stars: star_info = self.registry.get(star_name) if star_info is None: raise ValueError( f"Star '{star_name}' not found in registry. " f"Available stars: {list(self.registry.keys())}" ) position = self._calculate_single_star(julian_day, star_info) if position is not None: results.append(position) return results
[docs] def calculate_royal_stars( self, julian_day: float, ) -> list[FixedStarPosition]: """ Calculate positions for the four Royal Stars of Persia. A convenience method for getting just the most important stars: Aldebaran, Regulus, Antares, and Fomalhaut. Args: julian_day: The Julian Day for calculation Returns: List of FixedStarPosition objects for the four royal stars """ royal_names = [name for name, info in self.registry.items() if info.is_royal] return self.calculate_stars(julian_day, stars=royal_names)
[docs] def calculate_stars_by_tier( self, julian_day: float, tier: int, ) -> list[FixedStarPosition]: """ Calculate positions for all stars of a specific tier. Args: julian_day: The Julian Day for calculation tier: The tier level (1=Royal, 2=Major, 3=Extended) Returns: List of FixedStarPosition objects for stars of the specified tier """ tier_names = [name for name, info in self.registry.items() if info.tier == tier] return self.calculate_stars(julian_day, stars=tier_names)
def _calculate_single_star( self, julian_day: float, star_info: FixedStarInfo, ) -> FixedStarPosition | None: """ Calculate position for a single fixed star. Args: julian_day: The Julian Day for calculation star_info: The star's registry metadata Returns: FixedStarPosition with calculated coordinates, or None if calculation fails """ try: # swe.fixstar_ut returns: # ((lon, lat, dist, speed_lon, speed_lat, speed_dist), star_name, retflag) result = swe.fixstar_ut(star_info.swe_name, julian_day) # Unpack the position tuple position_data = result[0] longitude = position_data[0] latitude = position_data[1] distance = position_data[2] speed_lon = position_data[3] speed_lat = position_data[4] speed_dist = position_data[5] return FixedStarPosition( # CelestialPosition fields name=star_info.name, object_type=ObjectType.FIXED_STAR, longitude=longitude, latitude=latitude, distance=distance, speed_longitude=speed_lon, speed_latitude=speed_lat, speed_distance=speed_dist, # FixedStarPosition-specific fields swe_name=star_info.swe_name, constellation=star_info.constellation, bayer=star_info.bayer, tier=star_info.tier, is_royal=star_info.is_royal, magnitude=star_info.magnitude, nature=star_info.nature, keywords=star_info.keywords, ) except Exception as e: # Swiss Ephemeris raises various exceptions for unknown stars # Log and return None rather than crashing import warnings warnings.warn( f"Failed to calculate position for star '{star_info.name}': {e}", stacklevel=2, ) return None
[docs] def get_magnitude(self, star_name: str) -> float | None: """ Get the apparent magnitude of a star. This uses the registry value rather than calling swe.fixstar_mag() since we've already populated magnitude in FixedStarInfo. Args: star_name: Name of the star Returns: Apparent magnitude (lower = brighter), or None if not found """ star_info = self.registry.get(star_name) if star_info is None: return None return star_info.magnitude