Upstream 9f9f16b3 and 872eaecf. Saham, Gulika, and Shayanadi opt into Hindu rising. The default window used by rectification and Panchanga stays the apparent-disk one. BUG-1388. Co-Authored-By: Grok (agent loop) Co-authored-by: Cursor <cursoragent@cursor.com>
91 lines
3.7 KiB
Python
91 lines
3.7 KiB
Python
"""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.",
|
|
}
|