恒星黄道改为与 Swiss Ephemeris FLG_SIDEREAL 同一套含章动岁差。 产品默认路径不再用平岁差去减含章动的视位置。上升和宫头用同一个岁差。 校正身份升到 rectification-v5-matrix-scoring-12。 BUG-1275。未推送,未部署。v5 先验头名三格下降 1.3 个百分点,不调参。
139 lines
5.8 KiB
Python
139 lines
5.8 KiB
Python
"""BUG-1275: sidereal longitudes use the same nutation as FLG_SIDEREAL.
|
|
|
|
PyJHora 4.8.7 answers are frozen in tests/fixtures/sidereal_nutation_pyjhora_487.json.
|
|
This file does not import jhora. The offline generator is
|
|
scripts/research/freeze_sidereal_nutation_pyjhora_fixture.py.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import swisseph as swe
|
|
|
|
from scripts.ayanamsa_utils import apply_ayanamsa
|
|
from scripts.jyotish_engine import (
|
|
POSITION_MODES,
|
|
_calc_sidereal_planets_for_jd,
|
|
_sidereal_calculation_profile,
|
|
compute_chart_data,
|
|
)
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
PUBLIC_CASES = ROOT / "references" / "public_oracle_cases.json"
|
|
SPECIAL_FIXTURE = ROOT / "tests" / "fixtures" / "special_lagnas_pyjhora_487.json"
|
|
PYJHORA_FIXTURE = ROOT / "tests" / "fixtures" / "sidereal_nutation_pyjhora_487.json"
|
|
CASE_IDS = (
|
|
"steve_jobs_1955_aa",
|
|
"barack_obama_1961_aa",
|
|
"einstein_1879_aa",
|
|
"fictional_reader_main",
|
|
)
|
|
AYANAMSAS = ("lahiri", "raman", "kp")
|
|
PLANETS = ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu")
|
|
LIMIT = 1e-6
|
|
|
|
|
|
def _births() -> dict[str, dict]:
|
|
public = json.loads(PUBLIC_CASES.read_text(encoding="utf-8"))
|
|
births = {case["id"]: case["birth"] for case in public["cases"]}
|
|
special = json.loads(SPECIAL_FIXTURE.read_text(encoding="utf-8"))
|
|
births["fictional_reader_main"] = special["cases"]["fictional_reader_main"]["birth"]
|
|
return births
|
|
|
|
|
|
def _jd(birth: dict) -> float:
|
|
hour = birth["hour"] + birth["minute"] / 60.0 + birth.get("second", 0) / 3600.0 - birth["tz"]
|
|
return swe.julday(birth["year"], birth["month"], birth["day"], hour)
|
|
|
|
|
|
def _delta(left: float, right: float) -> float:
|
|
return ((left - right + 180.0) % 360.0) - 180.0
|
|
|
|
|
|
def _reference_longitudes(jd: float, ayanamsa: str, node_mode: str) -> dict[str, float]:
|
|
apply_ayanamsa(ayanamsa)
|
|
node = swe.TRUE_NODE if node_mode == "true" else swe.MEAN_NODE
|
|
flags = swe.FLG_SWIEPH | swe.FLG_SPEED | swe.FLG_SIDEREAL
|
|
bodies = {
|
|
"Sun": swe.SUN, "Moon": swe.MOON, "Mars": swe.MARS, "Mercury": swe.MERCURY,
|
|
"Jupiter": swe.JUPITER, "Venus": swe.VENUS, "Saturn": swe.SATURN, "Rahu": node,
|
|
}
|
|
longitudes = {name: swe.calc_ut(jd, pid, flags)[0][0] % 360.0 for name, pid in bodies.items()}
|
|
longitudes["Ketu"] = (longitudes["Rahu"] + 180.0) % 360.0
|
|
return longitudes
|
|
|
|
|
|
@pytest.mark.parametrize("case_id", CASE_IDS)
|
|
@pytest.mark.parametrize("ayanamsa", AYANAMSAS)
|
|
@pytest.mark.parametrize("node_mode", ("mean", "true"))
|
|
def test_nine_planets_match_swiss_sidereal_flag(case_id, ayanamsa, node_mode):
|
|
birth = _births()[case_id]
|
|
jd = _jd(birth)
|
|
expected = _reference_longitudes(jd, ayanamsa, node_mode)
|
|
chart, _asc_idx, returned_jd, ayanamsa_value = compute_chart_data(
|
|
birth["year"], birth["month"], birth["day"], birth["hour"], birth["minute"],
|
|
birth["lat"], birth["lon"], birth["tz"], node_mode=node_mode, second=birth.get("second", 0),
|
|
ayanamsa_name=ayanamsa,
|
|
)
|
|
assert returned_jd == pytest.approx(jd, abs=1e-6)
|
|
for name in PLANETS:
|
|
actual = chart["planets"][name]["degree_raw"]
|
|
assert abs(_delta(actual, expected[name])) <= LIMIT, name
|
|
transit, _ = _calc_sidereal_planets_for_jd(jd, node_mode=node_mode, ayanamsa_name=ayanamsa)
|
|
for name in PLANETS:
|
|
assert abs(_delta(transit[name]["degree_raw"], expected[name])) <= LIMIT, name
|
|
|
|
cusps, _ascmc = swe.houses(jd, birth["lat"], birth["lon"], b"A")
|
|
sidereal_cusps, sidereal_ascmc = swe.houses_ex(
|
|
jd, birth["lat"], birth["lon"], b"A", swe.FLG_SWIEPH | swe.FLG_SIDEREAL,
|
|
)
|
|
for index in range(12):
|
|
assert abs(_delta((cusps[index] - ayanamsa_value) % 360.0, sidereal_cusps[index] % 360.0)) <= LIMIT
|
|
assert abs(_delta(chart["ascendant"]["degree_raw"], sidereal_cusps[0] % 360.0)) <= 5.1e-5
|
|
_mode, flags, _aya = _sidereal_calculation_profile(jd, "legacy")
|
|
_mc_cusps, mc_ascmc = swe.houses_ex(jd, birth["lat"], birth["lon"], b"R", flags | swe.FLG_SIDEREAL)
|
|
assert abs(_delta(sidereal_ascmc[1] % 360.0, mc_ascmc[1] % 360.0)) <= LIMIT
|
|
|
|
|
|
def test_legacy_matches_apparent_and_mean_still_drops_nutation():
|
|
assert POSITION_MODES == ("legacy", "mean", "apparent")
|
|
birth = _births()["steve_jobs_1955_aa"]
|
|
jd = _jd(birth)
|
|
apply_ayanamsa("lahiri")
|
|
legacy = _sidereal_calculation_profile(jd, "legacy")
|
|
apparent = _sidereal_calculation_profile(jd, "apparent")
|
|
mean = _sidereal_calculation_profile(jd, "mean")
|
|
assert legacy[0] == "legacy"
|
|
assert legacy[1] == apparent[1]
|
|
assert legacy[2] == pytest.approx(apparent[2], abs=0.0)
|
|
assert mean[1] & swe.FLG_NONUT
|
|
nutation = swe.calc_ut(jd, swe.ECL_NUT)[0][2]
|
|
assert abs((legacy[2] - mean[2]) - nutation) <= 1e-6
|
|
assert abs((legacy[2] - swe.get_ayanamsa(jd)) - nutation) <= 1e-4
|
|
|
|
|
|
def test_frozen_pyjhora_difference_stays_inside_the_recorded_bound():
|
|
payload = json.loads(PYJHORA_FIXTURE.read_text(encoding="utf-8"))
|
|
assert payload["pyjhora_version"] == "4.8.7"
|
|
assert payload["node_mode"] == "true"
|
|
births = _births()
|
|
worst = 0.0
|
|
for case_id in CASE_IDS:
|
|
birth = births[case_id]
|
|
for ayanamsa in AYANAMSAS:
|
|
chart, *_ = 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=ayanamsa,
|
|
)
|
|
expected = payload["cases"][case_id]["ayanamsa"][ayanamsa]
|
|
for name in PLANETS:
|
|
gap = abs(_delta(chart["planets"][name]["degree_raw"], expected[name]))
|
|
worst = max(worst, gap)
|
|
assert gap <= payload["max_abs_difference_degrees"] + 1e-9, (case_id, ayanamsa, name)
|
|
assert worst <= payload["max_abs_difference_degrees"] + 1e-9
|
|
assert worst > 0.0
|