test(dasha): Yogini 大运对照 PyJHora,小运做内部核对

Presley、Spears、Einstein 走接口。大运主星顺序一致,起止日误差在一天内。小运按比例自洽,不跟 PyJHora 等分比。

Co-Authored-By: Grok (agent loop)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jesse-ux
2026-10-22 03:37:17 +08:00
co-authored by Grok (agent loop) Cursor
parent 3490e89403
commit 85567071c7
+167 -1
View File
@@ -16,7 +16,7 @@ import os
import random
import re
import sys
from datetime import date, datetime
from datetime import date, datetime, timedelta
from functools import lru_cache
from pathlib import Path
@@ -446,6 +446,172 @@ def test_ashtottari_birth_balance_matches_pyjhora_within_one_day() -> None:
_public_case("spears", spears, "moon")
YOGINI_PUBLIC_CHARTS = (
("presley", {
"year": 1935,
"month": 1,
"day": 8,
"hour": 4,
"minute": 35,
"lat": 34.3,
"lon": -88.4,
"tz": -6.0,
}),
("spears", {
"year": 1981,
"month": 12,
"day": 2,
"hour": 19,
"minute": 55,
"lat": 31.17,
"lon": -89.18,
"tz": 0.0,
}),
("einstein", {
"year": 1879,
"month": 3,
"day": 14,
"hour": 11,
"minute": 30,
"lat": 48.4,
"lon": 9.98,
"tz": 0.89,
}),
)
def _api_yogini_periods(birth: dict) -> list:
from scripts.domain_calculation_service import compute_chart
chart = compute_chart({**birth, "second": 0, "node_mode": "mean", "ayanamsa": "lahiri"})
planets = {
name: {"lon": row["lon"]}
for name, row in chart["planets"].items()
if isinstance(row, dict) and isinstance(row.get("lon"), (int, float))
}
body = {
"dasha": "yogini",
"year": birth["year"],
"month": birth["month"],
"day": birth["day"],
"hour": birth["hour"],
"minute": birth["minute"],
"second": 0,
"lat": birth["lat"],
"lon": birth["lon"],
"tz": birth["tz"],
"today": "2026-10-22",
"ayanamsa": "lahiri",
"node_mode": "mean",
"planets": planets,
"ascendant": {"lon": chart["ascendant"]["lon"]},
}
result = _handler()._compute_dasha_system(body)
assert result["precision"] == "engine_module"
return result["periods"]
def _pyjhora_yogini_end(row, following) -> date:
if following is not None:
return date(*following[1][:3])
from jhora import const
parts = row[1]
hours = float(parts[3]) if len(parts) > 3 else 0.0
start = datetime(int(parts[0]), int(parts[1]), int(parts[2])) + timedelta(hours=hours)
return (start + timedelta(days=float(row[2]) * float(const.sidereal_year))).date()
def _assert_yogini_majors_against_pyjhora(name: str, periods: list, py_rows: list) -> None:
assert len(py_rows) >= 24, name
assert len(periods) >= 24, name
mismatches = []
for index, (engine, oracle) in enumerate(zip(periods[:24], py_rows[:24])):
engine_lord = engine.get("planet")
oracle_lord = PYJHORA_LORDS[oracle[0][0]]
engine_start = _as_date(engine["unclipped_start_date"])
oracle_start = date(*oracle[1][:3])
following = py_rows[index + 1] if index + 1 < 24 else None
engine_end = _as_date(engine["end_date"])
oracle_end = _pyjhora_yogini_end(oracle, following)
start_delta = abs((engine_start - oracle_start).days)
end_delta = abs((engine_end - oracle_end).days)
if engine_lord != oracle_lord or start_delta > 1 or end_delta > 1:
mismatches.append(
f"{name} major[{index}] engine={engine_lord} {engine_start}..{engine_end} "
f"pyjhora={oracle_lord} {oracle_start}..{oracle_end} "
f"start_delta_days={start_delta} end_delta_days={end_delta}"
)
assert not mismatches, "\n".join(mismatches)
def _assert_yogini_antardasha_consistency(name: str, periods: list, birth: datetime) -> None:
from scripts.yogini_dasha import TOTAL_CYCLE, YOGINI_SEQUENCE
year_days = 365.25
problems = []
for index, major in enumerate(periods):
subs = major.get("antardasha") or []
if not subs:
problems.append(f"{name} major[{index}] {major.get('yogini')} has no antardasha")
continue
start_idx = next(item_index for item_index, item in enumerate(YOGINI_SEQUENCE) if item["name"] == major["yogini"])
expected = [YOGINI_SEQUENCE[(start_idx + offset) % 8] for offset in range(8)]
got = [row["yogini"] for row in subs]
suffixes = [[item["name"] for item in expected[cut:]] for cut in range(8)]
if got not in suffixes:
problems.append(f"{name} major[{index}] order {got} is not a Goel suffix of {major['yogini']}")
continue
matched = expected[8 - len(got):]
for row, item in zip(subs, matched):
if row.get("level") != "antardasha" or row.get("planet") != item["planet"] or row.get("lord") != item["planet"]:
problems.append(f"{name} major[{index}] {row.get('yogini')} fields {row.get('level')} {row.get('planet')} {row.get('lord')}")
raw_start = datetime.fromisoformat(row.get("unclipped_start_date") or row["start_date"])
raw_end = datetime.fromisoformat(row["end_date"])
expected_seconds = float(major["full_years"]) * item["years"] / TOTAL_CYCLE * year_days * 86400
delta_seconds = abs((raw_end - raw_start).total_seconds() - expected_seconds)
if delta_seconds > 2:
problems.append(
f"{name} major[{index}] {item['name']} span_delta_seconds={delta_seconds:.3f}"
)
for left, right in zip(subs, subs[1:]):
if left["end_date"] != right["start_date"]:
problems.append(f"{name} major[{index}] gap {left['end_date']} -> {right['start_date']}")
if subs[0]["start_date"] != major["start_date"]:
problems.append(f"{name} major[{index}] first sub {subs[0]['start_date']} != major {major['start_date']}")
if index == 0 and datetime.fromisoformat(subs[0]["start_date"]) != birth:
problems.append(f"{name} first sub {subs[0]['start_date']} != birth {birth.isoformat(timespec='seconds')}")
if index > 0 and any("unclipped_start_date" in row for row in subs):
problems.append(f"{name} major[{index}] later mahadasha was clipped")
visible = sum(
(datetime.fromisoformat(row["end_date"]) - datetime.fromisoformat(row["start_date"])).total_seconds()
for row in subs
)
major_span = (datetime.fromisoformat(major["end_date"]) - datetime.fromisoformat(major["start_date"])).total_seconds()
if abs(visible - major_span) > 86400:
problems.append(f"{name} major[{index}] sub_sum_delta_seconds={visible - major_span:.3f}")
if index == 0 and major["unclipped_start_date"] == major["start_date"]:
if len(subs) != 8:
problems.append(f"{name} unclipped first major has {len(subs)} subs")
for row, item in zip(subs, expected):
formula = float(major["full_years"]) * item["years"] / TOTAL_CYCLE
if abs(float(row["years"]) - formula) > 1e-6:
problems.append(f"{name} unclipped {item['name']} years {row['years']} != {formula}")
assert not problems, "\n".join(problems)
def test_yogini_api_majors_match_pyjhora_and_antardashas_are_proportional() -> None:
import importlib.metadata as metadata
version = metadata.version("PyJHora")
assert version == "4.8.7", version
for name, birth in YOGINI_PUBLIC_CHARTS:
periods = _api_yogini_periods(birth)
born = datetime(birth["year"], birth["month"], birth["day"], birth["hour"], birth["minute"])
_assert_yogini_majors_against_pyjhora(name, periods, _pyjhora_rows("yogini", birth))
_assert_yogini_antardasha_consistency(name, periods, born)
def test_placeholder_modules_are_gone() -> None:
assert not (SCRIPTS / "extended_dashas.py").exists()
assert not (SCRIPTS / "conditional_dashas.py").exists()