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:
co-authored by
Grok (agent loop)
Cursor
parent
3490e89403
commit
85567071c7
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user