fix(solar-return): return instant in the solver's frame, annual chart keeps seconds (BUG-1220)
Independent Staging Quality Gate / validate (push) Successful in 20m3s
Independent Staging Quality Gate / publish (push) Successful in 4m2s

The natal Sun target came from compute_chart_data while the solver iterates
on _get_sun_lon_jd (swe sidereal flags); the frames differ by up to ~17",
moving the return by up to ~7 minutes. The annual chart was also cast at the
truncated minute. Ports upstream yinduzhanxing 7cc6425d / 46ed9399 (these two
hunks only). PyJHora reference: 3 cases within 0.03 min. 243 public-figure
annual charts: annual Lagna changes 13, year lord 11, Mudda first lord 0.
v5 77 cases identical to 3c27c38e; consult goldens re-captured byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
Jesse_Chen
2026-10-03 22:10:20 +08:00
co-authored by Claude Opus 5.5
parent d0b783bd05
commit 01929baea6
7 changed files with 200 additions and 9 deletions
+81
View File
@@ -0,0 +1,81 @@
"""Solar-return instant in the solver's frame, annual chart with seconds (BUG-1220).
The return is the instant the Sun regains its natal sidereal longitude. The
website took the natal Sun from ``compute_chart_data`` while
``_find_solar_return_swe`` iterates on ``_get_sun_lon_jd`` (swe sidereal
flags); the two differ by up to ~17", which moved the return by up to ~7
minutes and changed the annual Ascendant sign in 13 of 243 public-figure
years. The annual chart was also cast at the truncated minute. Upstream
yinduzhanxing 7cc6425d / 46ed9399.
Reference instants were computed once with PyJHora ``drik.next_solar_date``
(Lahiri) as an external reference engine; only the resulting numbers are kept
here, PyJHora is not imported. Charts: Sanjay Rath's "standard nativity" and
two public figures from references/real_case_calibration.
"""
from __future__ import annotations
import contextlib
import io
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
SCRIPTS = ROOT / "scripts"
if str(SCRIPTS) not in sys.path:
sys.path.insert(0, str(SCRIPTS))
import solar_return # noqa: E402
try: # pragma: no cover - environment probe
import swisseph # noqa: F401
HAS_SWE = True
except Exception: # pragma: no cover
HAS_SWE = False
pytestmark = pytest.mark.skipif(not HAS_SWE, reason="swisseph required")
# (birth y, m, d, h, min, lat, lon, tz), target year, PyJHora return JD (UT)
PYJHORA_RETURNS = {
"rath_standard_nativity_2025": ((1963, 8, 7, 21, 15, 21.4667, 84.0167, 5.5), 2025, 2460895.0424092),
"steve_jobs_2012": ((1955, 2, 24, 19, 15, 37.7749, -122.4194, -8.0), 2012, 2455983.2569387),
"zinedine_zidane_2002": ((1972, 6, 23, 3, 0, 43.3, 5.4, 1.0), 2002, 2452449.2759534),
}
def _chart(birth, target_year):
with contextlib.redirect_stdout(io.StringIO()):
return solar_return.calc_solar_return_chart(*birth, target_year, "lahiri")
@pytest.mark.parametrize("case", sorted(PYJHORA_RETURNS))
def test_return_instant_matches_the_external_reference(case: str) -> None:
birth, target_year, reference_jd = PYJHORA_RETURNS[case]
result = _chart(birth, target_year)
assert result["solar_return"]["target_sun_frame"] == "swe_sidereal_flags"
# Before BUG-1220 these were 6-7 minutes apart.
assert abs(result["solar_return"]["jd_ut"] - reference_jd) * 1440 <= 0.5
def test_target_longitude_is_the_natal_sun_in_the_solver_frame() -> None:
birth, target_year, _ = PYJHORA_RETURNS["rath_standard_nativity_2025"]
with contextlib.redirect_stdout(io.StringIO()):
from jyotish_engine import compute_chart_data
natal, _asc, natal_jd, _ay = compute_chart_data(*birth, "mean", ayanamsa_name="lahiri")
solver_sun = solar_return._get_sun_lon_jd(natal_jd, ayanamsa_name="lahiri")
chart_sun = natal["planets"]["Sun"]["degree_raw"]
assert abs(chart_sun - solver_sun) * 3600 > 10 # the two frames really differ here
result = _chart(birth, target_year)
assert abs(result["solar_return"]["sun_lon"] - solver_sun) * 3600 < 1
def test_annual_chart_keeps_the_seconds_of_the_return() -> None:
birth, target_year, _ = PYJHORA_RETURNS["rath_standard_nativity_2025"]
result = _chart(birth, target_year)
info = result["chart"]["birth_info"]
return_dt = result["solar_return"]["dt_ut"]
assert (info["hour"], info["minute"], info["second"]) == (return_dt.hour, return_dt.minute, return_dt.second)
assert abs(info["julian_day"] - result["solar_return"]["jd_ut"]) * 86400 < 1