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
140 lines
6.6 KiB
Python
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)
|