fix(ephemeris): include nutation in the sidereal frame

恒星黄道改为与 Swiss Ephemeris FLG_SIDEREAL 同一套含章动岁差。
产品默认路径不再用平岁差去减含章动的视位置。上升和宫头用同一个岁差。
校正身份升到 rectification-v5-matrix-scoring-12。
BUG-1275。未推送,未部署。v5 先验头名三格下降 1.3 个百分点,不调参。
This commit is contained in:
jesse-ux
2026-10-08 18:42:45 +08:00
parent 1a8c97f64a
commit e5f2a75183
37 changed files with 52297 additions and 113 deletions
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""Freeze PyJHora 4.8.7 sidereal longitudes. Offline only.
Run from the repo root with a Python that has PyJHora 4.8.7:
python scripts/research/freeze_sidereal_nutation_pyjhora_fixture.py
Tests read the JSON and do not import this module.
"""
from __future__ import annotations
import importlib.metadata
import io
import json
import sys
from contextlib import redirect_stdout
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path[:0] = [str(ROOT), str(ROOT / "scripts")]
import swisseph as swe # noqa: E402
from jhora import const # noqa: E402
from jhora.panchanga import drik # noqa: E402
from jyotish_engine import compute_chart_data # noqa: E402
OUT = ROOT / "tests" / "fixtures" / "sidereal_nutation_pyjhora_487.json"
MODES = {"lahiri": "LAHIRI", "raman": "RAMAN", "kp": "KP"}
BODIES = (
("Sun", const._SUN), ("Moon", const._MOON), ("Mars", const._MARS),
("Mercury", const._MERCURY), ("Jupiter", const._JUPITER), ("Venus", const._VENUS),
("Saturn", const._SATURN), ("Rahu", const._RAHU),
)
def _births() -> dict[str, dict]:
public = json.loads((ROOT / "references" / "public_oracle_cases.json").read_text(encoding="utf-8"))
births = {
case["id"]: case["birth"]
for case in public["cases"]
if case["id"] in {"steve_jobs_1955_aa", "barack_obama_1961_aa", "einstein_1879_aa"}
}
special = json.loads((ROOT / "tests" / "fixtures" / "special_lagnas_pyjhora_487.json").read_text(encoding="utf-8"))
births["fictional_reader_main"] = special["cases"]["fictional_reader_main"]["birth"]
return births
def _longitudes(birth: dict, mode: str) -> dict[str, float]:
"""Same UTC conversion as drik.planetary_positions, via sidereal_longitude.
PyJHora 4.8.7's planetary_positions calls dict.index on planet_list and
raises AttributeError, so the wrapper cannot be the frozen source.
"""
local_hour = birth["hour"] + birth["minute"] / 60.0 + birth.get("second", 0) / 3600.0
jd_local = swe.julday(birth["year"], birth["month"], birth["day"], local_hour)
jd_ut = jd_local - float(birth["tz"]) / 24.0
with redirect_stdout(io.StringIO()):
drik.set_ayanamsa_mode(mode)
longitudes = {
name: float(drik.sidereal_longitude(jd_ut, planet)) % 360.0
for name, planet in BODIES
}
longitudes["Ketu"] = float(drik.ketu(longitudes["Rahu"])) % 360.0
return longitudes
def _gap(left: float, right: float) -> float:
return abs(((left - right + 180.0) % 360.0) - 180.0)
def main() -> None:
cases = {}
worst = 0.0
for case_id, birth in _births().items():
by_mode = {}
for name, mode in MODES.items():
frozen = _longitudes(birth, mode)
chart, *_rest = compute_chart_data(
birth["year"], birth["month"], birth["day"], birth["hour"], birth["minute"],
birth["lat"], birth["lon"], birth["tz"], node_mode="true",
second=birth.get("second", 0), ayanamsa_name=name,
)
for planet, longitude in frozen.items():
worst = max(worst, _gap(chart["planets"][planet]["degree_raw"], longitude))
by_mode[name] = frozen
cases[case_id] = {"ayanamsa": by_mode}
payload = {
"pyjhora_version": importlib.metadata.version("PyJHora"),
"generator": "python scripts/research/freeze_sidereal_nutation_pyjhora_fixture.py",
"generated_on": "2026-10-08",
"node_mode": "true",
"max_abs_difference_degrees": worst,
"note": "Frozen from drik.sidereal_longitude. PyJHora 4.8.7 planetary_positions raises AttributeError because planet_list is a dict. Rahu body is const._RAHU (Swiss 11). Differences are recorded, not a second runtime.",
"cases": cases,
}
OUT.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", newline="\n")
print(OUT.relative_to(ROOT).as_posix())
if __name__ == "__main__":
main()
+2 -2
View File
@@ -30,8 +30,8 @@ from scripts.research.sealed_holdout_rerun import (
OFFSETS = (-30, -20, -15, -10, -8, -5, -3, 0, 3, 5, 8, 10, 15, 20, 30)
RADII = (15, 30, 60)
MINUTE_STEP = 1
FREEZE = ROOT / "docs/research/reported_offset_upstream_sync6_2026_10_07.freeze.json"
REPORT = ROOT / "docs/research/reported_offset_upstream_sync6_2026_10_07.json"
FREEZE = ROOT / "docs/research/reported_offset_ephemeris_nutation_2026_10_08.freeze.json"
REPORT = ROOT / "docs/research/reported_offset_ephemeris_nutation_2026_10_08.json"
LEGACY_REPORT = ROOT / "docs/research/reported_offset_2026_09_20.json"
+2 -2
View File
@@ -27,8 +27,8 @@ from scripts.minute_rectification_feature_facts_v4 import build_feature_fact_row
from scripts.minute_rectification_holdout_validator import validate
DATASET = ROOT / "references/real_case_calibration/minute_rectification_holdout_v3.json"
FREEZE = ROOT / "docs/research/sealed_holdout_rerun_upstream_sync6_2026_10_07.freeze.json"
REPORT = ROOT / "docs/research/sealed_holdout_rerun_upstream_sync6_2026_10_07.json"
FREEZE = ROOT / "docs/research/sealed_holdout_rerun_ephemeris_nutation_2026_10_08.freeze.json"
REPORT = ROOT / "docs/research/sealed_holdout_rerun_ephemeris_nutation_2026_10_08.json"
LEGACY_REPORT = ROOT / "docs/research/sealed_holdout_rerun_2026_09_20.json"
ARCHIVE = ROOT / "docs/research/history/rectification_pre_cross_midnight_2026_09_20"
PRODUCTION_FILES = [