feat(narayana): Rath start-sign seven-level rule, Ketu/Saturn exceptions, AD start rule; Table 18 not reproduced, not wired (BUG-1218)
Astrologer round-5 Q1-Q4 in scripts/narayana_rath.py (frozen narayana_dasha.py untouched). Levels 1-2 are upstream c7117f92's rath_p36_role_conjunction_v1 counting (test compares with the upstream function); Ketu reversal is the upstream rath_ketu_table12_v1 branch; Ketu first when Saturn shares the seed. Book tables: 15 Cancer (level 1) and 17 Pisces (level 2) reproduce; 18 gives Capricorn at level 5, so the Indira AK example (full tie only) is never reached -> red line 3: no T4 wiring, no T5 trial. 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:
co-authored by
Claude Opus 5.5
parent
1c39988b3c
commit
c41555f69e
@@ -1,4 +1,4 @@
|
||||
"""Sanjay Rath Narayana Dasha profile vs the book tables (BUG-1215, round-4 Q4).
|
||||
"""Sanjay Rath Narayana Dasha profile vs the book tables (BUG-1215 / BUG-1218).
|
||||
|
||||
The rule ledger and the book rows are ported verbatim from upstream
|
||||
yinduzhanxing c7117f92 (`references/oracle/sanjay_rath_narayana_book_ledger_2026_08_28.json`,
|
||||
@@ -10,9 +10,12 @@ computed for (public figures / the book's own example chart):
|
||||
- Table 18: Indira Gandhi, 1917-11-19 23:11 IST, Allahabad 25N27 81E51.
|
||||
|
||||
Lahiri ayanamsa. With the book's start sign, all 36 MD signs and years match.
|
||||
The start sign itself is not reproduced by the existing strength rule for
|
||||
Tables 17 and 18 — pinned below as the current state (task red line 2: the
|
||||
profile stays out of every user-visible surface and out of scoring).
|
||||
Start sign by the round-5 seven-level rule (astrologer 2026-10-03): Table 15
|
||||
Cancer (level 1) and Table 17 Pisces (level 2) reproduce; Table 18 does not —
|
||||
the general rule decides Capricorn at level 5 (Cancer lord Moon 5.6 deg vs
|
||||
Capricorn lord Saturn 21.8 deg), so the Indira AK book example (allowed only
|
||||
after all seven levels tie) is never reached. Task red line 3: the profile
|
||||
stays out of every user-visible surface and out of scoring.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -72,32 +75,53 @@ def test_saturn_in_the_start_sign_gives_the_zodiacal_order() -> None:
|
||||
assert md_order(SIGNS.index("Cancer"), lons)["direction"] == "table10_general"
|
||||
|
||||
|
||||
def test_start_sign_selection_current_state() -> None:
|
||||
"""Existing strength rule (occupants, then aspect by Jupiter / Mercury /
|
||||
lord): Table 15 right; Table 17 picks Virgo (book Pisces: Pisces holds its
|
||||
own lord Jupiter, which the aspect-only rule does not count); Table 18 ties
|
||||
(book Cancer). Update this test together with the rule once the astrologer
|
||||
answers; until then the profile is not wired anywhere."""
|
||||
def test_start_sign_by_the_seven_level_rule() -> None:
|
||||
"""原值:现状钉住——Table 17 选出处女(原书双鱼),Table 18 平局(巨蟹 / 摩羯),只用前两级
|
||||
新值:七级通则——Table 15 巨蟹(第 1 级)、Table 17 双鱼(第 2 级,三角色木星兼宫主记两次)、
|
||||
Table 18 摩羯(第 5 级,宫主度数),与原书巨蟹不符;书内特例(AK)只在七级全平时可用,此盘够不着
|
||||
原因:BUG-1218,占星师第五轮问 1、问 2"""
|
||||
asc, lons = _chart("Table 15")
|
||||
assert (calc_narayana_rath(asc, lons)["status"], calc_narayana_rath(asc, lons)["seed_sign"]) == ("partial_verified", "Cancer")
|
||||
result = calc_narayana_rath(asc, lons, cycle_count=1)
|
||||
assert (result["status"], result["seed_sign"], result["strength_level_decided"]) == ("partial_verified", "Cancer", 1)
|
||||
assert [row["sign"] for row in result["mahadasha"]] == [row[0] for row in _book("Table 15")["rows"]]
|
||||
asc, lons = _chart("Table 17")
|
||||
assert calc_narayana_rath(asc, lons)["seed_sign"] == "Virgo"
|
||||
result = calc_narayana_rath(asc, lons, cycle_count=1)
|
||||
assert (result["status"], result["seed_sign"], result["strength_level_decided"]) == ("partial_verified", "Pisces", 2)
|
||||
pisces = result["seed_selection"]["candidates"]["Pisces"]
|
||||
assert [row["role"] for row in pisces["role_factors"]] == ["jupiter", "sign_lord"]
|
||||
assert result["seed_selection"]["candidates"]["Virgo"]["aspect_factor_count"] == 1
|
||||
assert [row["sign"] for row in result["mahadasha"]] == [row[0] for row in _book("Table 17")["rows"]]
|
||||
assert [row["years"] for row in result["mahadasha"]] == [row[1] for row in _book("Table 17")["rows"]]
|
||||
asc, lons = _chart("Table 18")
|
||||
result = calc_narayana_rath(asc, lons)
|
||||
assert result["status"] == "parameter_sensitive" and result["reason"] == "later_narayana_sign_strength_rules_required"
|
||||
assert set(result["alternatives"]) == {"Cancer", "Capricorn"}
|
||||
assert [row["sign"] for row in result["alternatives"]["Cancer"]["mahadasha"][:12]] == [row[0] for row in _book("Table 18")["rows"]]
|
||||
for allow in (False, True):
|
||||
result = calc_narayana_rath(asc, lons, cycle_count=1, allow_book_example=allow)
|
||||
assert (result["seed_sign"], result["strength_level_decided"]) == ("Capricorn", 5)
|
||||
assert result["book_example_exception"] is None
|
||||
values = {sign: row["level_values"] for sign, row in result["seed_selection"]["candidates"].items()}
|
||||
assert values["Cancer"][:4] == values["Capricorn"][:4] # occupants 1:1, roles 2:2, no dignity, both movable
|
||||
assert values["Cancer"][4] < values["Capricorn"][4] # Moon 5.6 deg < Saturn 21.8 deg
|
||||
assert _book("Table 18")["start_sign"] == "Cancer"
|
||||
|
||||
|
||||
def test_antardasha_is_twelve_equal_parts_from_the_md_sign() -> None:
|
||||
def test_antardasha_starts_from_the_lord_of_the_stronger_of_md_and_seventh() -> None:
|
||||
"""原值:子运从大运星座本身起(Table 13),巨蟹大运 → 巨蟹、双子、金牛…
|
||||
新值:比较大运星座与其第 7 宫,取较强者,从该宫宫主所在星座起,12 等分;Table 13 / 11 / 14 由大运星座里的土星 / 计都决定
|
||||
原因:BUG-1218,占星师第五轮问 4(p.48 Arambha Rasi,p.50 说明 2)"""
|
||||
asc, lons = _chart("Table 15")
|
||||
md = calc_narayana_rath(asc, lons, cycle_count=1)["mahadasha"][0] # Cancer, 7 years; Mars and Jupiter there
|
||||
rows = antardasha(md, lons)
|
||||
assert [row["sign"] for row in rows][:3] == ["Cancer", "Gemini", "Taurus"] # Table 13, even sign reverse
|
||||
assert all(abs(row["years"] - 7 / 12) < 1e-6 for row in rows)
|
||||
ad = antardasha(md, lons)
|
||||
rule = ad["antardasha_start_rule"]
|
||||
assert (rule["stronger_sign"], rule["strength_level_decided"], rule["lord"], rule["start_sign"]) == ("Cancer", 1, "Moon", "Sagittarius")
|
||||
assert rule["order_profile"] == "rath_table13_v1"
|
||||
assert [row["sign"] for row in ad["rows"]][:3] == ["Sagittarius", "Capricorn", "Aquarius"] # Table 13, odd sign forward
|
||||
assert all(abs(row["years"] - 7 / 12) < 1e-6 for row in ad["rows"])
|
||||
asc, lons = _chart("Table 18")
|
||||
md = calc_narayana_rath(asc, lons, seed_sign_idx=SIGNS.index("Cancer"), cycle_count=1)["mahadasha"][0]
|
||||
assert [row["sign"] for row in antardasha(md, lons)][:3] == ["Cancer", "Leo", "Virgo"] # Saturn in Cancer: Table 11
|
||||
ad = antardasha(md, lons)
|
||||
rule = ad["antardasha_start_rule"]
|
||||
# Cancer vs Capricorn: Capricorn at level 5; its lord Saturn sits in Cancer; Saturn in the MD sign -> Table 11.
|
||||
assert (rule["stronger_sign"], rule["lord"], rule["start_sign"], rule["order_profile"]) == ("Capricorn", "Saturn", "Cancer", "rath_saturn_table11_v1")
|
||||
assert [row["sign"] for row in ad["rows"]][:3] == ["Cancer", "Leo", "Virgo"]
|
||||
|
||||
|
||||
def test_rath_profile_is_not_wired_into_any_product_path() -> None:
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user