"""Sanjay Rath Narayana rules from the astrologer's round-5 ruling (BUG-1218). Constructed charts (synthetic longitudes, no birth data) exercise each of the seven start-sign levels, the Indira book-example path, the Saturn / Ketu exceptions and the AD start rule. Two cases are ported from upstream yinduzhanxing c7117f92: `tests/test_rath_role_strength.py::test_p36_two_roles_for_jupiter_and_one_opposite` and the Saturn-seed case of `tests/test_varsha_narayana_native.py` (`test_published_muntha_seed_saturn_exception_and_first_five_lengths`, MD part). """ from __future__ import annotations import importlib.util 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 narayana_dasha as nd # noqa: E402 from scripts.narayana_rath import ( # noqa: E402 GRAHAS, INDIRA_EXCEPTION, KETU_FIRST_NOTE, SIGN_LORDS, SIGNS, _role_candidate, antardasha, calc_narayana_rath, compare_signs, md_order, mahadasha_sequence, ) UPSTREAM = Path("/workspace/yinduzhanxing") UPSTREAM_SHA = "c7117f92" # (sign A, sign B, longitudes, expected winner, expected level). A/B are Lagna # and 7th except level 4: opposite signs always share modality, so level 4 is # shown on Aries vs Gemini (the comparator is generic). LEVEL_CASES = { 1: (0, 6, {"Sun": 116.58, "Moon": 54.31, "Mars": 234.34, "Mercury": 26.08, "Jupiter": 192.92, "Venus": 131.65, "Saturn": 20.88, "Rahu": 182.68, "Ketu": 2.68}, "Aries"), 2: (1, 7, {"Sun": 341.18, "Moon": 207.76, "Mars": 142.8, "Mercury": 351.45, "Jupiter": 16.77, "Venus": 309.05, "Saturn": 104.26, "Rahu": 51.93, "Ketu": 231.93}, "Scorpio"), 3: (0, 6, {"Sun": 222.88, "Moon": 43.32, "Mars": 22.23, "Mercury": 245.64, "Jupiter": 191.06, "Venus": 173.7, "Saturn": 279.54, "Rahu": 317.96, "Ketu": 137.96}, "Aries"), 4: (0, 2, {"Sun": 95.0, "Moon": 125.0, "Mars": 35.0, "Mercury": 45.0, "Jupiter": 55.0, "Venus": 155.0, "Saturn": 275.0, "Rahu": 305.0, "Ketu": 125.0}, "Gemini"), 5: (9, 3, {"Sun": 30.94, "Moon": 150.54, "Mars": 86.64, "Mercury": 198.38, "Jupiter": 21.28, "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, "Venus": 328.44, "Saturn": 266.23, "Rahu": 223.1, "Ketu": 43.1}, "Aries"), } # Aries vs Libra tie on all seven levels: two occupants each (Venus + Moon in # Aries, Mars + Sun in Libra), no role contact, no dignity, lords Mars / Venus # both at 10 deg in each other's sign, six years each. Moon 29.5 deg is the AK. TIE = {"Sun": 185.0, "Moon": 29.5, "Mars": 190.0, "Mercury": 335.0, "Jupiter": 65.0, "Venus": 10.0, "Saturn": 275.0, "Rahu": 85.0, "Ketu": 265.0} @pytest.mark.parametrize("level", sorted(LEVEL_CASES)) def test_each_level_decides_and_earlier_levels_tie(level: int) -> None: first, second, lons, winner = LEVEL_CASES[level] result = compare_signs(first, second, lons) assert result["status"] == "selected" assert (result["selected_sign"], result["strength_level_decided"]) == (winner, level) a, b = (result["candidates"][SIGNS[idx]]["level_values"] for idx in (first, second)) assert a[:level - 1] == b[:level - 1] assert a[level - 1] != b[level - 1] def test_level_two_counts_roles_not_planets() -> None: """Pisces with Jupiter inside: Jupiter as Jupiter and as lord = two roles.""" lons = {"Sun": 0.0, "Moon": 30.0, "Mars": 165.0, "Mercury": 120.0, "Jupiter": 345.0, "Venus": 60.0, "Saturn": 90.0, "Rahu": 180.0, "Ketu": 240.0} result = compare_signs(11, 5, lons) assert (result["selected_sign"], result["strength_level_decided"]) == ("Pisces", 2) assert [row["role"] for row in result["candidates"]["Pisces"]["role_factors"]] == ["jupiter", "sign_lord"] def test_ported_upstream_p36_two_roles_for_jupiter_and_one_opposite() -> None: # Ported from upstream c7117f92 tests/test_rath_role_strength.py. positions = dict(Jupiter=345., Mars=165., Mercury=120., Sun=0., Moon=30., Venus=60., Saturn=90., Rahu=180., Ketu=240.) r = calc_narayana_rath(11, positions) assert r["seed_sign"] == "Pisces" candidates = r["seed_selection"]["candidates"] assert candidates["Pisces"]["aspect_factor_count"] == 2 assert candidates["Virgo"]["aspect_factor_count"] == 1 assert candidates["Pisces"]["role_factors"] == [ dict(role="jupiter", planet="Jupiter", contact="conjunction"), dict(role="sign_lord", planet="Jupiter", contact="conjunction")] # The legacy aspect-only rule (frozen narayana_dasha) picks Virgo here. assert nd.select_narayana_seed_sign(11, positions)["selected_sign_idx"] == 5 def _upstream_narayana(): if not (UPSTREAM / ".git").exists(): pytest.skip("upstream checkout not on this machine") source = subprocess.run(["git", "show", f"{UPSTREAM_SHA}:scripts/narayana_dasha.py"], cwd=UPSTREAM, capture_output=True, text=True, check=False) if source.returncode != 0: pytest.skip("upstream commit not available") spec = importlib.util.spec_from_loader("upstream_narayana_c7117f92", loader=None) module = importlib.util.module_from_spec(spec) exec(compile(source.stdout, "upstream_narayana_dasha.py", "exec"), module.__dict__) return module def test_levels_one_two_equal_upstream_role_profile() -> None: """Levels 1-2 are upstream's rath_p36_role_conjunction_v1 counting.""" upstream = _upstream_narayana() rng = random.Random(1003) for _ in range(400): lons = {planet: rng.uniform(0, 360) for planet in GRAHAS} lagna = rng.randrange(12) theirs = upstream.select_narayana_seed_sign(lagna, lons, strength_profile="rath_p36_role_conjunction_v1") for idx in (lagna, (lagna + 6) % 12): ours = _role_candidate(idx, lons, SIGN_LORDS[SIGNS[idx]]) for key in ("occupants", "occupant_count", "aspect_factors", "aspect_factor_count", "role_factors"): assert ours[key] == theirs["candidates"][idx][key] def test_full_tie_uses_the_indira_example_only_when_allowed() -> None: plain = compare_signs(0, 6, TIE) assert plain["status"] == "tie" and plain["strength_level_decided"] is None flagged = compare_signs(0, 6, TIE, allow_book_example=True) assert (flagged["selected_sign"], flagged["book_example_exception"], flagged["atmakaraka"]) == ("Aries", INDIRA_EXCEPTION, "Moon") result = calc_narayana_rath(0, TIE, allow_book_example=True) assert result["status"] == "book_example_exception" assert result["book_example_exception"] == INDIRA_EXCEPTION tied = calc_narayana_rath(0, TIE) assert tied["status"] == "parameter_sensitive" and tied["mahadasha"] is None assert set(tied["alternatives"]) == {"Aries", "Libra"} def test_book_example_never_overrides_a_general_level() -> None: first, second, lons, winner = LEVEL_CASES[5] assert compare_signs(first, second, lons, allow_book_example=True)["book_example_exception"] is None def _with(lons: dict, **moves: float) -> dict: return {**lons, **moves} BASE = {"Sun": 95.0, "Moon": 125.0, "Mars": 35.0, "Mercury": 45.0, "Jupiter": 55.0, "Venus": 155.0, "Saturn": 275.0, "Rahu": 305.0, "Ketu": 125.0} def test_ketu_reverses_table10_keeping_the_steps() -> None: lons = _with(BASE, Rahu=245.0, Ketu=65.0) # Ketu in Gemini (dual: steps of 4/5/... differ from a read-back) table10 = nd.build_narayana_sign_sequence(2, profile="rath_general_table10_v1") order = md_order(2, lons) assert order["direction"] == "table12_ketu_reversed" and order["exception_applied"] == "ketu_in_start_sign" assert order["order"] == [2, 6, 10, 11, 3, 7, 8, 0, 4, 5, 9, 1] assert [(b - a) % 12 for a, b in zip(order["order"], order["order"][1:])] == \ [(a - b) % 12 for a, b in zip(table10, table10[1:])] assert order["order"] != [2] + table10[:0:-1] # not the table read backwards def test_saturn_runs_zodiacal_and_ketu_wins_when_both() -> None: saturn = md_order(1, _with(BASE, Saturn=40.0)) assert saturn["direction"] == "table11_saturn_zodiacal" assert saturn["order"] == [(1 + i) % 12 for i in range(12)] both = md_order(1, _with(BASE, Saturn=40.0, Rahu=215.0, Ketu=35.0)) assert both["direction"] == "table12_ketu_reversed" assert both["method_note"] == KETU_FIRST_NOTE == "PyJHora 兼容:计都优先" def test_ported_upstream_varsha_saturn_seed_order_and_lengths() -> None: # Ported from upstream c7117f92 tests/test_varsha_narayana_native.py: PVR # textbook Chart 67 D9 positions; seed Scorpio holds Saturn -> Table 11; # first five MD [7, 4, 2, 3, 11] with Ketu as Scorpio's lord. rasi = dict(Sun=47+4/60, Moon=184+48/60, Mars=113+53/60, Mercury=64+47/60, Jupiter=160+59/60, Venus=1+37/60, Saturn=306+29/60, Rahu=228+38/60, Ketu=48+38/60) d9 = {planet: lon * 9 % 360 for planet, lon in rasi.items()} seq = mahadasha_sequence(7, d9, cycle_count=1) first = seq["mahadasha"][:5] assert seq["direction"] == "table11_saturn_zodiacal" assert [row["sign_idx"] for row in first] == [7, 8, 9, 10, 11] assert [row["years"] for row in first] == [7, 4, 2, 3, 11] assert first[0]["lord"] == "Ketu" def test_output_fields() -> None: first, second, lons, _ = LEVEL_CASES[1] result = calc_narayana_rath(first, lons) for key in ("profile", "seed_sign", "seed_basis", "strength_level_decided", "direction", "exception_applied", "dual_lord_rule", "mahadasha", "antardasha_start_rule", "source", "status"): assert key in result, key assert result["profile"] == "sanjay_rath" assert result["seed_basis"] == "stronger_of_lagna_seventh:level_1_occupant_count" assert len(result["mahadasha"]) == 24 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)