Files
Jyotisha/tests/test_tajika_five_fold_panchavargiya.py
T
Jesse_ChenandClaude Opus 5.5 1c39988b3c feat(tajika): year lord compared by the Tajika five-fold Panchavargiya (BUG-1217)
Ports _calc_native_panchavargiya_bala and its helpers verbatim from upstream
yinduzhanxing c7117f92 (astrologer round-5 Q5). calc_panchadhikari_year_lord
now scores candidates with it (engine tajika_native_five_fold); the
D1/D2/D3/D9/D12 dignity proxy no longer selects a year lord, and a missing
annual planet blocks instead of falling back to it. Basis note reads
「按 Tajika 五分力量」 / "by Tajika five-fold strength".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
2026-10-03 21:09:31 +08:00

140 lines
6.6 KiB
Python

"""Tajika five-fold Panchavargiya chooses the year lord (BUG-1217).
Astrologer round-5 ruling 2026-10-03, Q5: compare the Panchadhikari candidates
by the Tajika five-fold Panchavargiya (Kshetra/graha, Uchcha, Hadda, Drekkana,
Navamsa; raw out of 80, score = raw / 4 out of 20), not the D1/D2/D3/D9/D12
dignity proxy. The five-fold function is ported verbatim from upstream
yinduzhanxing c7117f92 (`_calc_native_panchavargiya_bala`).
"""
from __future__ import annotations
import ast
import hashlib
import inspect
import random
import subprocess
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
for entry in (str(ROOT), str(ROOT / "scripts")):
if entry not in sys.path:
sys.path.insert(0, entry)
from scripts import tajika # noqa: E402
UPSTREAM = Path("/workspace/yinduzhanxing")
UPSTREAM_SHA = "c7117f92"
CAPS = {"graha_bala": 30.0, "uchcha_bala": 20.0, "hudda_bala": 15.0, "drekkana_bala": 10.0, "navamsha_bala": 5.0}
# Annual signs: Sun Aries 5deg, Moon Taurus, Mars Leo, Mercury Gemini,
# Jupiter Sagittarius, Venus Pisces, Saturn Libra.
CHART = {"Sun": 5.0, "Moon": 40.0, "Mars": 130.0, "Mercury": 70.0, "Jupiter": 250.0, "Venus": 340.0, "Saturn": 190.0}
@pytest.fixture(autouse=True)
def _no_reference_engine(monkeypatch):
monkeypatch.delenv("JYOTISH_YEAR_LORD_PYJHORA_REFERENCE", raising=False)
def test_hand_worked_sun() -> None:
"""Sun 5deg Aries, worked by hand from references/tajika_panchavargiya_rules.json.
graha: sign lord Mars (Leo) -> Sun is 9th from Mars -> friendly 22.5
uchcha: arc from deep debilitation 190deg = 175 -> 175 / 9 = 19.44
hudda: Aries 0-6 Jupiter (Sagittarius) -> Sun 5th from it -> friendly 11.25
drekkana: Aries 0-10 Mars -> friendly 7.5
navamsa: Aries 5deg -> Taurus, lord Venus (Pisces) -> Sun 2nd -> neutral 2.5
raw 63.19, score 63.19 / 4 = 15.7975 -> 15.80
"""
sun = tajika._calc_native_panchavargiya_bala(CHART)["Sun"]
assert sun["components"] == {"graha_bala": 22.5, "uchcha_bala": 19.44, "hudda_bala": 11.25,
"drekkana_bala": 7.5, "navamsha_bala": 2.5}
assert sun["raw_total"] == 63.19
assert sun["score"] == 15.8
assert sun["component_trace"]["hudda_bala"]["lord"] == "Jupiter"
assert sun["component_trace"]["navamsha_bala"]["navamsha_sign"] == "Taurus"
def test_components_caps_and_score_on_random_charts() -> None:
rng = random.Random(20261003)
for _ in range(300):
chart = {planet: rng.uniform(0, 360) for planet in tajika.CLASSICAL_PLANETS}
for planet, row in tajika._calc_native_panchavargiya_bala(chart).items():
assert set(row["components"]) == set(CAPS)
for key, cap in CAPS.items():
assert 0 <= row["components"][key] <= cap, (planet, key)
assert row["raw_total"] <= 80.0
assert row["raw_total"] == tajika._round_half_up(sum(row["components"].values()))
assert row["score"] == tajika._round_half_up(row["raw_total"] / 4.0)
assert row["score"] <= 20.0
def test_own_sign_lord_scores_full_kshetra() -> None:
chart = dict(CHART, Mars=10.0) # Mars in Aries, its own sign
assert tajika._calc_native_panchavargiya_bala(chart)["Mars"]["components"]["graha_bala"] == 30.0
def test_year_lord_candidates_use_the_five_fold_scores() -> None:
result = tajika.calc_panchadhikari_year_lord(CHART, annual_asc_sign_idx=0, natal_lagna_sign_idx=3,
years_from_dob=10, muntha_sign_idx=6)
assert result["status"] == "partial_verified"
native = tajika._calc_native_panchavargiya_bala(CHART)
for row in result["candidates"]:
assert row["panchavargiya_engine"] == "tajika_native_five_fold"
assert row["panchavargiya_score"] == native[row["planet"]]["score"]
assert row["panchavargiya_raw_total"] == native[row["planet"]]["raw_total"]
assert row["panchavargiya_components"] == native[row["planet"]]["components"]
def test_dignity_proxy_never_selects(monkeypatch) -> None:
def boom(*_args, **_kwargs):
raise AssertionError("dignity proxy must not be used for the year lord")
monkeypatch.setattr(tajika, "_calc_panchavargiya_bala_for_planet", boom)
result = tajika.calc_panchadhikari_year_lord(CHART, annual_asc_sign_idx=0, natal_lagna_sign_idx=3,
years_from_dob=10, muntha_sign_idx=6)
assert result["status"] == "partial_verified"
def test_missing_annual_planet_blocks_instead_of_using_the_proxy() -> None:
partial = {key: value for key, value in CHART.items() if key != "Saturn"}
result = tajika.calc_panchadhikari_year_lord(partial, annual_asc_sign_idx=0, natal_lagna_sign_idx=3,
years_from_dob=10, muntha_sign_idx=6)
assert result["status"] == "blocked"
assert result["reason"] == "tajika_five_fold_panchavargiya_unavailable"
def _function_source(source: str, name: str) -> str:
tree = ast.parse(source)
node = next(n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name == name)
return "\n".join(source.splitlines()[node.lineno - 1:node.end_lineno])
# sha256 of each function text as ported from upstream c7117f92.
PORTED_SHA256 = {
"_tajika_relation": "c218f423ebb22ac24f841450db772a4c0d52865a6bbcba3c5d4fe0ca06576449",
"_panchavargiya_component": "f8725f04282b46e43df28158ae345ab02de8beb815fbdab000b227277a56d543",
"_hudda_lord": "1278496e99fd90c099021d20590a36f8666cab5a612bbe6bd75fb22a5fac1bd5",
"_tajika_drekkana_lord": "72128eea38e1e6ca343b45f5ae978b433c857739b425548e846704637a8bc50e",
"_navamsha_sign_idx": "79113057dbf0e5f026557c1d9af39bcd414e1b7fa0996ebab9f3e287de8c78fd",
"_tajika_uchcha_bala": "22768c89efcc794a8523252db1824f8521273fcfd0497bb27c426596101afa85",
"_round_half_up": "5defceacea3f799dd83e9a1b57e3bcc6251ede13fb5aa4d4dfb6419767f5722e",
"_calc_native_panchavargiya_bala": "9e7f3ae88c2f8dd04ea5479c732970bf912153f96a3a740a9999de13d6f7c052",
}
@pytest.mark.parametrize("name", sorted(PORTED_SHA256))
def test_ported_functions_are_verbatim(name: str) -> None:
ours = _function_source(inspect.getsource(tajika), name)
assert hashlib.sha256(ours.encode("utf-8")).hexdigest() == PORTED_SHA256[name]
if not (UPSTREAM / ".git").exists():
pytest.skip("upstream checkout not on this machine")
upstream = subprocess.run(["git", "show", f"{UPSTREAM_SHA}:scripts/tajika.py"], cwd=UPSTREAM,
capture_output=True, text=True, check=False)
if upstream.returncode != 0:
pytest.skip("upstream commit not available")
assert ours == _function_source(upstream.stdout, name)