"""Swiss Ephemeris sunrise/sunset evidence for Saham day/night formula selection.""" from __future__ import annotations from datetime import datetime from typing import Any import swisseph as swe class SahamDayNightError(ValueError): pass def determine_daytime( moment: datetime, *, lat: float, lon: float, tz: float, rising: str = "apparent", ) -> dict[str, Any]: """Day/night window. ``rising="apparent"`` is the historical disk-edge window. Rectification and Panchanga rely on that default and must not pick up Hindu rising. ``rising="hindu"`` is disk-center, no refraction, with the real next sunrise. Saham day/night, Gulika, and Shayanadi pass it explicitly. """ if rising not in ("apparent", "hindu"): raise SahamDayNightError("unsupported sunrise convention") if not -90 <= float(lat) <= 90 or not -180 <= float(lon) <= 180: raise SahamDayNightError("invalid WGS84 latitude/longitude") local = moment.replace(tzinfo=None) jd = swe.julday(local.year, local.month, local.day, local.hour + local.minute / 60 + local.second / 3600 - float(tz)) geopos = (float(lon), float(lat), 0.0) if rising == "apparent": rise_status, rise = swe.rise_trans(jd - 1.0, swe.SUN, swe.CALC_RISE, geopos) set_status, sunset = swe.rise_trans(jd - 1.0, swe.SUN, swe.CALC_SET, geopos) if rise_status != 0 or set_status != 0: raise SahamDayNightError("sunrise_or_sunset_unavailable_for_location_date") sunrise_jd, sunset_jd = rise[0], sunset[0] # Normalize the next daily events around the queried instant. while sunrise_jd > jd: sunrise_jd -= 1.0 while sunset_jd > jd: sunset_jd -= 1.0 is_day = sunrise_jd <= jd < sunset_jd if sunrise_jd < sunset_jd else not (sunset_jd <= jd < sunrise_jd) return { "scope": "saham_daynight_swiss", "status": "computed", "is_daytime": is_day, "julian_day_ut": jd, "sunrise_jd_ut": sunrise_jd, "sunset_jd_ut": sunset_jd, "method": "swisseph.rise_trans", "boundary": "Formula-specific +30 degree exceptions must be applied by the Saham rule layer, not inferred from house placement.", } flags = swe.FLG_SWIEPH | swe.FLG_TRUEPOS def event(after, kind): status, times = swe.rise_trans(after, swe.SUN, kind | swe.BIT_HINDU_RISING, geopos, flags=flags) if status != 0: raise SahamDayNightError("sunrise_or_sunset_unavailable_for_location_date") return times[0] # Consecutive rise/set events. Adding one Julian day to a sunrise is not # the next sunrise, and it moves the night-segment endpoint. midnight_jd = swe.julday(local.year, local.month, local.day, -float(tz)) sunrise_jd = event(midnight_jd, swe.CALC_RISE) if sunrise_jd > jd: midnight_jd -= 1.0 sunrise_jd = event(midnight_jd, swe.CALC_RISE) next_sunrise_jd = event(midnight_jd + 1.0, swe.CALC_RISE) sunset_jd = event(midnight_jd, swe.CALC_SET) if not sunrise_jd <= jd < next_sunrise_jd or not sunrise_jd < sunset_jd < next_sunrise_jd: raise SahamDayNightError("solar_day_window_unavailable") is_day = sunrise_jd <= jd < sunset_jd return { "scope": "saham_daynight_swiss", "status": "computed", "is_daytime": is_day, "julian_day_ut": jd, "sunrise_jd_ut": sunrise_jd, "sunset_jd_ut": sunset_jd, "next_sunrise_jd_ut": next_sunrise_jd, "method": "swisseph.rise_trans_hindu_rising", "boundary": "Formula-specific +30 degree exceptions must be applied by the Saham rule layer, not inferred from house placement.", }