feat(narayana-rath): Rahu/Ketu dignity and debilitation at level 3 (BUG-1224)

Round-6 ruling Q2: node dignity by Rath Table 9 p.41-42 (own Aq/Sc, MT Vi/Pi,
Phalita exaltation Ge/Sg), debilitated ranks below none (p.67: debilitated
Ketu weaker than Mars by Rule 3). Rath module only; natal table unchanged.
Book acceptance unchanged (15/17 general, 18 explicit 12/12, 28/30, 24/36).
Goldens recaptured (only rath.source text changes).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
Jesse_Chen
2026-10-04 07:33:55 +08:00
co-authored by Claude Opus 5.5
parent 7dda1b45c3
commit 61bd4425cd
4 changed files with 99 additions and 13 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+43 -10
View File
@@ -14,8 +14,10 @@ p.63]:
role counted once when the planet is in the sign or casts Rasi Drishti on
it [p.48; no graha drishti]; roles are not de-duplicated by planet
(Jupiter as Jupiter and as lord counts twice) [p.36];
3. occupant dignity: exalted > moolatrikona > own sign (whole sign) — best
rank, then the number of dignified occupants;
3. occupant dignity: exalted > moolatrikona > own sign (whole sign) > none >
debilitated — best rank, then the number of dignified occupants; Rahu /
Ketu by Rath Table 9 (p.41-42, Phalita set; round-6 ruling Q2,
BUG-1224), debilitation below none per p.67;
4. dual > fixed > movable;
5. the lord's degree inside its sign, Chara Karaka sense (higher is
stronger; Rahu / Ketu as 30 - degree);
@@ -98,7 +100,8 @@ SOURCE = {
"md_order": "p.51 Table 10; Saturn p.51-53 Table 11; Ketu p.51, p.53 Table 12",
"ad": "p.48 Arambha Rasi, p.50 note 2; Tables 13 / 11 / 14 (p.54)",
"indira_example": "p.65-66 (AK), Table 18 p.67",
"ruling": "astrologer round 5, 2026-10-03, Q1-Q4; product owner: Ketu first",
"ruling": "astrologer round 5, 2026-10-03, Q1-Q4; product owner: Ketu first; round 6, 2026-10-04, Q2 (node dignity)",
"node_dignity": "Table 9 p.41-42 (Phalita exaltation Rahu Gemini / Ketu Sagittarius); debilitated weaker p.67",
"port": "upstream yinduzhanxing c7117f92 scripts/narayana_dasha.py",
}
SIGNS = nd.SIGNS
@@ -163,16 +166,44 @@ def _role_candidate(sign_idx: int, planet_lons: Dict[str, float], lord: str) ->
}
# Rahu / Ketu dignity inside Rath Narayana only (round-6 ruling Q2, Rath
# Table 9 p.41-42): own sign Rahu Aquarius / Ketu Scorpio; moolatrikona Rahu
# Virgo / Ketu Pisces; exaltation for Phalita dasas such as Narayana Rahu
# Gemini / Ketu Sagittarius (the Taurus / Scorpio pair is for Ayur dasas
# only), debilitation in the opposite signs. Not the natal dignity table
# (BUG-1211 "nodes have no own sign" still holds on the D1 main path).
RATH_NODE_DIGNITY = {
"Rahu": {"exalted": {2}, "moolatrikona": 5, "own": {10}, "debilitated": {8}},
"Ketu": {"exalted": {8}, "moolatrikona": 11, "own": {7}, "debilitated": {2}},
}
DEBILITATED_RANK = -1
def _occupant_dignity(planet: str, sign_idx: int) -> int:
table = nd._NARAYANA_DIGNITY.get(planet)
if table is None: # Rahu / Ketu: no ruling on node dignity; not counted.
return 0
if sign_idx in table["exalted"]:
"""Level 3 rank: exalted 3 > moolatrikona 2 > own 1 > none 0 > debilitated -1.
Debilitated below none: p.67 (Indira, Table 18) "Ketu is debilitated and
as per Rule (3) of first source of strength is weaker than Mars" (Mars in
Leo, no dignity). Applied to all nine bodies (Rule 3 is general).
"""
node = RATH_NODE_DIGNITY.get(planet)
if node is not None:
exalted, moolatrikona, own, debilitated = node["exalted"], node["moolatrikona"], node["own"], node["debilitated"]
else:
table = nd._NARAYANA_DIGNITY.get(planet)
if table is None:
return 0
exalted, debilitated = table["exalted"], table["debilitated"]
moolatrikona = _RATH_CLASSICAL_MOOLATRIKONA_SIGNS[planet]
own = {idx for idx, sign in enumerate(SIGNS) if SIGN_LORDS[sign] == planet}
if sign_idx in exalted:
return 3
if sign_idx == _RATH_CLASSICAL_MOOLATRIKONA_SIGNS[planet]:
if sign_idx == moolatrikona:
return 2
if SIGN_LORDS[SIGNS[sign_idx]] == planet:
if sign_idx in own:
return 1
if sign_idx in debilitated:
return DEBILITATED_RANK
return 0
@@ -189,12 +220,14 @@ def sign_strength(sign_idx: int, planet_lons: Dict[str, float]) -> Dict[str, Any
ranks = {planet: _occupant_dignity(planet, sign_idx) for planet in row["occupants"]}
best = max(ranks.values(), default=0)
dignified = sum(1 for value in ranks.values() if value > 0)
debilitated = sum(1 for value in ranks.values() if value == DEBILITATED_RANK)
lord_sign_idx = nd._get_planet_sign(lord, lons)
if lord_sign_idx is None:
lord_sign_idx = sign_idx
years, _ = nd.calculate_narayana_period_years(sign_idx, lord, lord_sign_idx, profile=DURATION_PROFILE)
row.update({
"occupant_dignity": {"ranks": ranks, "best_rank": best, "dignified_count": dignified},
"occupant_dignity": {"ranks": ranks, "best_rank": best, "dignified_count": dignified,
"debilitated_count": debilitated},
"modality": "dual" if sign_idx in (2, 5, 8, 11) else "fixed" if sign_idx in nd._FIXED_SIGNS else "movable",
"lord_sign": SIGNS[lord_sign_idx],
"lord_degree_in_sign": round(_ck_degree(lord, lons[lord]), 6) if lord in lons else None,
+54 -1
View File
@@ -48,7 +48,11 @@ LEVEL_CASES = {
"Venus": 203.56, "Saturn": 341.08, "Rahu": 227.03, "Ketu": 47.03}, "Capricorn"),
6: (3, 9, {"Sun": 136.2, "Moon": 178.62, "Mars": 80.38, "Mercury": 202.16, "Jupiter": 260.66,
"Venus": 355.0, "Saturn": 268.62, "Rahu": 0.8, "Ketu": 180.8}, "Capricorn"),
7: (6, 0, {"Sun": 195.37, "Moon": 223.57, "Mars": 268.44, "Mercury": 0.02, "Jupiter": 349.15,
# 原值:Sun 195.37(天秤)、Moon 223.57(天蝎)
# 新值:Sun 223.57、Moon 195.37(日月互换,天秤里换成不落陷的月亮)
# 原因:BUG-1224(第六轮问 2,p.67)第 3 级把落陷算作弱于无尊贵;太阳落陷天秤会让本例在第 3 级分出,
# 不再演示第 7 级。互换后前六级仍全平,第 7 级照旧判白羊。
7: (6, 0, {"Sun": 223.57, "Moon": 195.37, "Mars": 268.44, "Mercury": 0.02, "Jupiter": 349.15,
"Venus": 328.44, "Saturn": 266.23, "Rahu": 223.1, "Ketu": 43.1}, "Aries"),
}
@@ -195,3 +199,52 @@ def test_output_fields() -> None:
ad = antardasha(result["mahadasha"][0], lons)
assert ad["status"] == "selected" and len(ad["rows"]) == 12
assert ad["antardasha_start_rule"]["strength_level_decided"] in range(1, 8)
# --- BUG-1224: Rahu / Ketu dignity inside Rath Narayana (round-6 ruling Q2,
# Rath Table 9 p.41-42; Phalita exaltation set) and debilitation at level 3 (p.67).
@pytest.mark.parametrize("planet,sign,rank", [
("Rahu", "Gemini", 3), ("Rahu", "Virgo", 2), ("Rahu", "Aquarius", 1), ("Rahu", "Sagittarius", -1),
("Rahu", "Taurus", 0), # Taurus / Scorpio exaltation is the Ayur-dasa set, not Phalita
("Ketu", "Sagittarius", 3), ("Ketu", "Pisces", 2), ("Ketu", "Scorpio", 1), ("Ketu", "Gemini", -1),
("Ketu", "Scorpio", 1), ("Ketu", "Taurus", 0),
("Mars", "Capricorn", 3), ("Mars", "Aries", 2), ("Mars", "Scorpio", 1), ("Mars", "Cancer", -1), ("Mars", "Leo", 0),
])
def test_node_and_debilitation_ranks(planet: str, sign: str, rank: int) -> None:
from scripts.narayana_rath import _occupant_dignity
assert _occupant_dignity(planet, SIGNS.index(sign)) == rank
def test_p67_debilitated_ketu_is_weaker_than_mars_by_rule_3() -> None:
"""Rath p.67 (Indira Gandhi, Table 18): "Scorpio: Ketu is debilitated and as
per Rule (3) of first source of strength is weaker than Mars" — Mars alone
in Leo, Ketu alone in Gemini. Before BUG-1224 Gemini won at level 4 (dual > fixed)."""
from tests.test_narayana_rath_book_tables import _chart
_asc, lons = _chart("Table 18")
result = compare_signs(SIGNS.index("Leo"), SIGNS.index("Gemini"), lons)
assert (result["selected_sign"], result["strength_level_decided"]) == ("Leo", 3)
assert result["candidates"]["Gemini"]["occupant_dignity"]["ranks"] == {"Ketu": -1}
assert result["candidates"]["Leo"]["occupant_dignity"]["ranks"] == {"Mars": 0}
def test_constructed_debilitated_ketu_loses_to_plain_mars_at_level_3() -> None:
lons = {"Sun": 95.0, "Moon": 185.0, "Mars": 135.0, "Mercury": 105.0, "Jupiter": 100.0,
"Venus": 155.0, "Saturn": 275.0, "Rahu": 245.0, "Ketu": 65.0} # Mars Leo, Ketu Gemini
result = compare_signs(SIGNS.index("Leo"), SIGNS.index("Gemini"), lons)
a, b = (result["candidates"][name]["level_values"] for name in ("Leo", "Gemini"))
assert a[:2] == b[:2]
assert (result["selected_sign"], result["strength_level_decided"]) == ("Leo", 3)
def test_node_dignity_stays_inside_rath() -> None:
"""The natal dignity table is untouched (BUG-1211: nodes have no own sign on the D1 path)."""
import inspect
import scripts.narayana_rath as rath
users = sorted(path.name for path in (ROOT / "scripts").glob("*.py")
if path.name != "narayana_rath.py" and "RATH_NODE_DIGNITY" in path.read_text(encoding="utf-8"))
assert users == []
assert "RATH_NODE_DIGNITY" in inspect.getsource(rath._occupant_dignity)