diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py index 75b438df..54b2ffd0 100644 --- a/scripts/jyotish_engine.py +++ b/scripts/jyotish_engine.py @@ -660,7 +660,10 @@ def _get_dignity_level(planet, sign, deg_in_sign=None, planets_data=None): if deg_in_sign is not None and mt[1] <= deg_in_sign < mt[2]: return 'MOOLATRIKONA' - if SIGN_LORDS.get(sign) == planet or (planet == 'Rahu' and sign == 'Virgo') or (planet == 'Ketu' and sign == 'Pisces'): + # BUG-1211 (ruling 甲12): Rahu and Ketu have no traditional own sign. The old + # Rahu-Virgo / Ketu-Pisces "own sign" fed the D1 status column, Vimsopaka and + # varga_dignity; neither Virgo nor Aquarius is shown as Rahu's own sign now. + if SIGN_LORDS.get(sign) == planet: return 'OWN_SIGN' if DEBILITATION.get(planet) == sign: diff --git a/scripts/yoga_engine.py b/scripts/yoga_engine.py index eb6dfd23..66cf5740 100644 --- a/scripts/yoga_engine.py +++ b/scripts/yoga_engine.py @@ -67,6 +67,7 @@ FIXED_SIGNS = ['Taurus', 'Leo', 'Scorpio', 'Aquarius'] DUAL_SIGNS = ['Gemini', 'Virgo', 'Sagittarius', 'Pisces'] ALL_PLANETS = ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'] PLANET_INDEX = {planet: idx for idx, planet in enumerate(ALL_PLANETS)} +# Jaimini co-lord comparison only (_stronger_co_lord); not a traditional own sign (BUG-1211). RAHU_KETU_OWNED_SIGN_INDEX = {'Rahu': 10, 'Ketu': 7} # Planets that form the solar / lunar neighbour yogas (BUG-1187, BUG-1206). FIVE_TRUE_PLANETS = ('Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn') @@ -200,9 +201,11 @@ class YogaContext: self.panchanga = self.context.get("panchanga", {}) if isinstance(self.context, dict) else {} self.upagraha = self.context.get("upagraha", {}) if isinstance(self.context, dict) else {} - # 预计算 D1 宫主星。v6.0.39: Scorpio/Aquarius follow PyJHora's dynamic co-lord resolver - # (Scorpio: stronger of Mars/Ketu; Aquarius: stronger of Saturn/Rahu), while SIGN_LORDS stays - # as the classical fixed fallback for dignity tables and non-Jaimini contexts. + # 预计算 D1 宫主星。BUG-1211 (ruling 甲12): the main detection path uses the + # fixed traditional lords (Scorpio -> Mars, Aquarius -> Saturn), as the lord + # chain, functional roles and the frontend do. The v6.0.39 PyJHora "stronger + # co-lord" resolver (_stronger_co_lord) is kept for Jaimini variants only and + # runs only when a caller asks for dynamic_co_lords=True. self._house_lords: Dict[int, str] = {} for h in range(1, 13): sign = SIGNS[(self.asc_idx + h - 1) % 12] @@ -239,7 +242,13 @@ class YogaContext: return [SIGNS.index(s) for s in DUAL_SIGNS if SIGNS.index(s) != sign_idx] def _stronger_co_lord(self, planet1: str, planet2: str) -> str: - """Approximate PyJHora stronger_planet_from_planet_positions() for Sc/Aq co-lords.""" + """Approximate PyJHora stronger_planet_from_planet_positions() for Sc/Aq co-lords. + + Jaimini-variant only (BUG-1211): reached only through + `_resolve_sign_lord(..., dynamic_co_lords=True)`; no main-path caller. + RAHU_KETU_OWNED_SIGN_INDEX is this comparison's internal convention, not a + traditional own sign. + """ h1 = self._sign_index_of_planet(planet1) h2 = self._sign_index_of_planet(planet2) if h1 is None: @@ -314,7 +323,7 @@ class YogaContext: return planet1 if degree1 > degree2 else planet2 return planet1 - def _resolve_sign_lord(self, sign: str, dynamic_co_lords: bool = True) -> Optional[str]: + def _resolve_sign_lord(self, sign: str, dynamic_co_lords: bool = False) -> Optional[str]: if not dynamic_co_lords: return SIGN_LORDS.get(sign) if sign == 'Scorpio': diff --git a/tests/test_node_own_sign_and_fixed_lords.py b/tests/test_node_own_sign_and_fixed_lords.py new file mode 100644 index 00000000..2da9d59a --- /dev/null +++ b/tests/test_node_own_sign_and_fixed_lords.py @@ -0,0 +1,65 @@ +"""Rahu / Ketu have no own sign; the main path uses fixed lords (BUG-1211, ruling 甲12). + +`jyotish_engine._get_dignity_level` no longer calls Rahu in Virgo or Ketu in +Pisces "own sign" (nor Aquarius / Scorpio); `yoga_engine.YogaContext` resolves +Scorpio to Mars and Aquarius to Saturn on the main path, keeping the stronger +co-lord comparison for Jaimini variants (dynamic_co_lords=True) only. +""" + +from __future__ import annotations + +import pytest + +from scripts.jyotish_engine import _get_dignity_level +from scripts.yoga_engine import YogaContext + + +@pytest.mark.parametrize("planet,sign", [("Rahu", "Virgo"), ("Rahu", "Aquarius"), ("Ketu", "Pisces"), ("Ketu", "Scorpio")]) +def test_nodes_are_never_own_sign(planet: str, sign: str) -> None: + assert _get_dignity_level(planet, sign) != "OWN_SIGN" + planets = {"Sun": {"sign": "Aries", "house": 1}, planet: {"sign": sign, "house": 2}} + assert _get_dignity_level(planet, sign, 15.0, planets) != "OWN_SIGN" + + +def test_classical_own_signs_unchanged() -> None: + assert _get_dignity_level("Saturn", "Aquarius", 25.0) == "OWN_SIGN" + assert _get_dignity_level("Mars", "Scorpio") == "OWN_SIGN" + + +def _context(asc: str) -> YogaContext: + # Ketu crowded in Taurus and Rahu with company: the old resolver could pick + # Ketu / Rahu as the stronger co-lord of Scorpio / Aquarius. + planets = { + "Mars": {"sign": "Leo", "house": 1, "degree": 5.0}, + "Saturn": {"sign": "Pisces", "house": 2, "degree": 5.0}, + "Ketu": {"sign": "Taurus", "house": 3, "degree": 5.0}, + "Rahu": {"sign": "Scorpio", "house": 4, "degree": 5.0}, + "Sun": {"sign": "Taurus", "house": 5, "degree": 6.0}, + "Moon": {"sign": "Taurus", "house": 6, "degree": 7.0}, + "Mercury": {"sign": "Scorpio", "house": 7, "degree": 8.0}, + "Jupiter": {"sign": "Scorpio", "house": 8, "degree": 9.0}, + "Venus": {"sign": "Taurus", "house": 9, "degree": 10.0}, + } + return YogaContext(planets, asc) + + +@pytest.mark.parametrize("asc", ["Scorpio", "Aquarius", "Leo"]) +def test_main_path_lords_are_fixed(asc: str) -> None: + ctx = _context(asc) + signs = ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", + "Sagittarius", "Capricorn", "Aquarius", "Pisces"] + for house in range(1, 13): + sign = signs[(signs.index(asc) + house - 1) % 12] + lord = ctx.lord_of_house(house) + if sign == "Scorpio": + assert lord == "Mars" + if sign == "Aquarius": + assert lord == "Saturn" + assert lord not in ("Rahu", "Ketu") + assert not ctx.is_own_sign("Rahu") + + +def test_jaimini_variant_still_available() -> None: + ctx = _context("Leo") + assert ctx._resolve_sign_lord("Scorpio", dynamic_co_lords=True) in ("Mars", "Ketu") + assert ctx._resolve_sign_lord("Scorpio") == "Mars"