feat(narayana): Table 18 as a flagged book example with the explicit seed; book software-verdict and Table 25 tests (BUG-1218)
Red line 3 revised by the product owner (2026-10-03 evening, audit da95d92d):
Indira Table 18 is a book-internal contradiction. calc_narayana_rath(...,
seed_sign_idx, book_example="rath_p66_indira_ak") reproduces it 12/12 flagged
book_example_exception and carries the general rule's Capricorn unchanged.
New tests: five D1 software-verdict tables 28/30 (two Wadiyar level-5 misses
pinned), co-lords 8/10, Table 25 sub-periods 24/36 (Gemini MD 0/12 pinned).
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
f5d24f42d2
commit
735d66fd65
@@ -52,8 +52,10 @@ upstream function). Tables 10 / 11 / 13 / 14, `_rasi_aspects`, the year and
|
||||
dual-lord functions are the same in both repositories and are reused from the
|
||||
frozen local `narayana_dasha.py`, which this module does not modify.
|
||||
|
||||
Not wired into scoring, consultation or reports (task red line 3: Table 18 is
|
||||
not reproduced by the general rule, see tests/test_narayana_rath_book_tables.py).
|
||||
Table 18 (Indira) is a book-internal contradiction (BOOK_EXAMPLES): the
|
||||
general rule's Capricorn is output as is; the book's Cancer only with the
|
||||
start sign given explicitly and flagged (product owner, 2026-10-03 evening).
|
||||
Not wired into scoring.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -72,6 +74,22 @@ DURATION_PROFILE = "jaimini_odd_footed_dignity_v2"
|
||||
DUAL_LORD_PROFILE = "jaimini_dual_lord_source_v1"
|
||||
ROLE_PROFILE = "rath_p36_role_conjunction_v1"
|
||||
INDIRA_EXCEPTION = "rath_p66_indira_ak"
|
||||
# Book examples whose printed start sign the general rule does not give.
|
||||
# Product owner ruling 2026-10-03 (evening, red line 3 revised): reproduce
|
||||
# them only with the start sign given explicitly, flagged; never by code that
|
||||
# bends the general rule.
|
||||
BOOK_EXAMPLES = {
|
||||
INDIRA_EXCEPTION: {
|
||||
"book_table": "Table 18",
|
||||
"book_start_sign": "Cancer",
|
||||
"book_internal_contradiction": True,
|
||||
"evidence": ("Hand example (PDF Page 62; astrologer cites p.65-66) concludes Cancer because "
|
||||
"'Saturn is the Atmakaraka and has gained the highest longitude', but Saturn sits in "
|
||||
"Cancer and rules Capricorn, so rules 5 and 6 read literally point to Capricorn; the "
|
||||
"book's own software verdict for the same native (Table 27) also has Capricorn "
|
||||
"stronger than Cancer. Audit docs/research/shared_engine_reuse_audit_2026_10_03.md N1."),
|
||||
},
|
||||
}
|
||||
KETU_FIRST_NOTE = "PyJHora 兼容:计都优先"
|
||||
KETU_FIRST_NOTE_EN = "PyJHora-compatible: Ketu takes precedence"
|
||||
SOURCE = {
|
||||
@@ -339,13 +357,19 @@ def antardasha(md_row: Dict[str, Any], planet_lons: Dict[str, float]) -> Dict[st
|
||||
|
||||
def calc_narayana_rath(lagna_sign_idx: int, planet_lons: Dict[str, float],
|
||||
*, seed_sign_idx: Optional[int] = None, cycle_count: int = 2,
|
||||
allow_book_example: bool = False) -> Dict[str, Any]:
|
||||
allow_book_example: bool = False,
|
||||
book_example: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Rath Narayana Dasha for one chart.
|
||||
|
||||
`seed_sign_idx` overrides the start sign (book-table replay only).
|
||||
`seed_sign_idx` overrides the start sign (book-table replay only); with
|
||||
`book_example` (a key of BOOK_EXAMPLES) the output is flagged
|
||||
`book_example_exception` and still carries the general rule's own
|
||||
verdict, unchanged, in `general_rule_selection`.
|
||||
`allow_book_example` lets the Indira AK example decide a full seven-level
|
||||
tie (flagged in the output).
|
||||
"""
|
||||
if book_example is not None and (seed_sign_idx is None or book_example not in BOOK_EXAMPLES):
|
||||
raise ValueError("book_example needs an explicit seed_sign_idx and a known BOOK_EXAMPLES key")
|
||||
base = {
|
||||
"profile": PROFILE,
|
||||
"duration_profile": DURATION_PROFILE,
|
||||
@@ -356,7 +380,14 @@ def calc_narayana_rath(lagna_sign_idx: int, planet_lons: Dict[str, float],
|
||||
}
|
||||
if seed_sign_idx is not None:
|
||||
selection = {"status": "explicit", "selected_sign_idx": seed_sign_idx,
|
||||
"strength_level_decided": None, "book_example_exception": None}
|
||||
"strength_level_decided": None, "book_example_exception": book_example}
|
||||
if book_example is not None:
|
||||
general = compare_signs(lagna_sign_idx, (lagna_sign_idx + 6) % 12, planet_lons)
|
||||
base["general_rule_selection"] = {
|
||||
key: general.get(key) for key in
|
||||
("status", "selected_sign", "strength_level_decided", "strength_level_name", "reason")
|
||||
}
|
||||
base["book_example"] = BOOK_EXAMPLES[book_example]
|
||||
else:
|
||||
selection = compare_signs(lagna_sign_idx, (lagna_sign_idx + 6) % 12, planet_lons,
|
||||
allow_book_example=allow_book_example)
|
||||
@@ -365,7 +396,9 @@ def calc_narayana_rath(lagna_sign_idx: int, planet_lons: Dict[str, float],
|
||||
base["book_example_exception"] = selection.get("book_example_exception")
|
||||
if selection["status"] in ("selected", "explicit"):
|
||||
seq = mahadasha_sequence(selection["selected_sign_idx"], planet_lons, cycle_count=cycle_count)
|
||||
if selection["status"] == "explicit":
|
||||
if selection["status"] == "explicit" and book_example is not None:
|
||||
status, basis = "book_example_exception", f"explicit_seed:book_example:{book_example}"
|
||||
elif selection["status"] == "explicit":
|
||||
status, basis = "explicit_seed", "explicit_seed"
|
||||
elif selection.get("book_example_exception"):
|
||||
status, basis = "book_example_exception", f"book_example:{selection['book_example_exception']}"
|
||||
|
||||
@@ -13,9 +13,14 @@ Lahiri ayanamsa. With the book's start sign, all 36 MD signs and years match.
|
||||
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.
|
||||
Capricorn lord Saturn 21.8 deg). Red line 3 as revised by the product owner
|
||||
(2026-10-03 evening, audit docs/research/shared_engine_reuse_audit_2026_10_03.md):
|
||||
Table 18 is a book-internal contradiction (the hand example's reasoning reads
|
||||
literally as Capricorn and the book's Table 27 software verdict is Capricorn),
|
||||
reproduced only with the Cancer seed given explicitly and flagged
|
||||
`book_example_exception`; the general rule's Capricorn is output unchanged.
|
||||
Further acceptance evidence below: the book's five D1 software-verdict tables
|
||||
(30 sign pairs, 28 match) and Table 25's 36 sub-period rows (24 match).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -124,6 +129,99 @@ def test_antardasha_starts_from_the_lord_of_the_stronger_of_md_and_seventh() ->
|
||||
assert [row["sign"] for row in ad["rows"]][:3] == ["Cancer", "Leo", "Virgo"]
|
||||
|
||||
|
||||
def test_table18_only_with_the_explicit_cancer_seed_and_flagged() -> None:
|
||||
asc, lons = _chart("Table 18")
|
||||
book = _book("Table 18")
|
||||
result = calc_narayana_rath(asc, lons, seed_sign_idx=SIGNS.index("Cancer"), cycle_count=1,
|
||||
book_example="rath_p66_indira_ak")
|
||||
assert result["status"] == "book_example_exception"
|
||||
assert result["book_example_exception"] == "rath_p66_indira_ak"
|
||||
assert result["book_example"]["book_internal_contradiction"] is True
|
||||
assert [row["sign"] for row in result["mahadasha"]] == [row[0] for row in book["rows"]]
|
||||
assert [row["years"] for row in result["mahadasha"]] == [row[1] for row in book["rows"]]
|
||||
# The general rule's own verdict travels with it, unchanged.
|
||||
assert (result["general_rule_selection"]["selected_sign"], result["general_rule_selection"]["strength_level_decided"]) == ("Capricorn", 5)
|
||||
with pytest.raises(ValueError):
|
||||
calc_narayana_rath(asc, lons, book_example="rath_p66_indira_ak")
|
||||
|
||||
|
||||
# Book software verdicts ("Stronger planets and signs") for five D1 charts:
|
||||
# Rath, Narayana Dasa, Tables 22 / 23 / 24 / 25 / 27 (book example charts
|
||||
# 14, 15 = the standard nativity, 16, 17 = Krishnaraja Wadiyar IV, 22 = Indira
|
||||
# Gandhi). Inputs and verdicts as transcribed in the audit (da95d92d).
|
||||
def _dms(d: float, m: float) -> float:
|
||||
return d + m / 60
|
||||
|
||||
|
||||
AB = dict(Ar=0, Ta=1, Ge=2, Cn=3, Le=4, Vi=5, Li=6, Sc=7, Sg=8, Cp=9, Aq=10, Pi=11)
|
||||
VERDICT_TABLES = {
|
||||
"Table 22": ((1962, 3, 28, 6, 29, _dms(22, 6), _dms(85, 17), 5.5), ("Saturn", "Mars"), "Ar Sc Ge Cp Aq Pi"),
|
||||
"Table 23": ((1963, 8, 7, 21, 15, _dms(21, 28), _dms(84, 1), 5.5), ("Saturn", "Mars"), "Li Ta Sg Cn Le Pi"),
|
||||
"Table 24": ((1954, 12, 19, 0, 20, _dms(28, 36), _dms(77, 12), 5.5), ("Saturn", "Mars"), "Li Sc Sg Cn Aq Vi"),
|
||||
"Table 25": ((1884, 6, 4, 10, 18, 12.0, _dms(76, 38), _dms(76, 38) / 15), ("Saturn", "Ketu"), "Li Ta Ge Cn Le Vi"),
|
||||
"Table 27": ((1917, 11, 19, 23, 8, _dms(25, 28), _dms(81, 52), 5.5), ("Rahu", "Mars"), "Ar Sc Sg Cp Le Pi"),
|
||||
}
|
||||
# Expected misses, documented: Wadiyar Gemini/Sagittarius and Virgo/Pisces —
|
||||
# the book takes Gemini and Virgo; the general rule decides at level 5
|
||||
# (Mercury 1.3 deg vs Jupiter 10.0 deg) for Sagittarius and Pisces. Not
|
||||
# sensitive to node mode or ayanamsa (audit). Open question to the astrologer.
|
||||
EXPECTED_SIGN_MISSES = {("Table 25", "Gemini", "Sagittarius"), ("Table 25", "Virgo", "Pisces")}
|
||||
# Dual lords (Aquarius, Scorpio): book Table 23 Aquarius = Saturn (ours Rahu),
|
||||
# Table 25 Scorpio = Ketu (ours Mars).
|
||||
EXPECTED_CO_LORD_MISSES = {("Table 23", "Aquarius"), ("Table 25", "Scorpio")}
|
||||
|
||||
|
||||
def test_book_software_verdicts_28_of_30_and_co_lords_8_of_10() -> None:
|
||||
from scripts.narayana_rath import _lord, compare_signs
|
||||
|
||||
sign_misses, co_lord_misses, pairs = set(), set(), 0
|
||||
for table, (birth, co_lords, stronger) in VERDICT_TABLES.items():
|
||||
chart, _asc, *_ = compute_chart_data(*birth, ayanamsa_name="lahiri")
|
||||
lons = {name: row["degree"] for name, row in chart["planets"].items()}
|
||||
book = {AB[code] for code in stronger.split()}
|
||||
for first in range(6):
|
||||
second = first + 6
|
||||
winner = first if first in book else second
|
||||
result = compare_signs(first, second, lons)
|
||||
pairs += 1
|
||||
if result.get("selected_sign_idx") != winner:
|
||||
assert result["strength_level_decided"] == 5
|
||||
sign_misses.add((table, SIGNS[winner], SIGNS[second if winner == first else first]))
|
||||
for sign_idx, want in zip((10, 7), co_lords):
|
||||
if _lord(sign_idx, lons) != want:
|
||||
co_lord_misses.add((table, SIGNS[sign_idx]))
|
||||
assert pairs == 30
|
||||
assert sign_misses == EXPECTED_SIGN_MISSES # 28/30
|
||||
assert co_lord_misses == EXPECTED_CO_LORD_MISSES # 8/10
|
||||
|
||||
|
||||
# Table 25 (Wadiyar) sub-periods for the first three MDs, from upstream
|
||||
# c7117f92 references/oracle/chara_narayana_rath_wadiyar_table25_replay_2026_09_07.json.
|
||||
TABLE25_AD = {
|
||||
"Cancer": ["Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces", "Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo"],
|
||||
"Gemini": ["Taurus", "Aries", "Pisces", "Aquarius", "Capricorn", "Sagittarius", "Scorpio", "Libra", "Virgo", "Leo", "Cancer", "Gemini"],
|
||||
"Taurus": ["Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces", "Aries", "Taurus", "Gemini"],
|
||||
}
|
||||
|
||||
|
||||
def test_table25_sub_periods_24_of_36() -> None:
|
||||
"""Cancer and Taurus (Saturn inside, Table 11) MDs: 12/12. Gemini MD: 0/12 —
|
||||
the Gemini/Sagittarius comparison picks Sagittarius (level 5, the same
|
||||
Wadiyar miss as above), so the ADs start from Jupiter's sign Cancer
|
||||
instead of Mercury's sign Taurus. Current state, pinned."""
|
||||
chart, asc, *_ = compute_chart_data(*VERDICT_TABLES["Table 25"][0], ayanamsa_name="lahiri")
|
||||
lons = {name: row["degree"] for name, row in chart["planets"].items()}
|
||||
mds = calc_narayana_rath(asc, lons, seed_sign_idx=SIGNS.index("Cancer"), cycle_count=1)["mahadasha"][:3]
|
||||
assert [row["sign"] for row in mds] == ["Cancer", "Gemini", "Taurus"]
|
||||
matched = {}
|
||||
for md in mds:
|
||||
rows = [row["sign"] for row in antardasha(md, lons)["rows"]]
|
||||
matched[md["sign"]] = sum(a == b for a, b in zip(rows, TABLE25_AD[md["sign"]]))
|
||||
assert matched == {"Cancer": 12, "Gemini": 0, "Taurus": 12}
|
||||
gemini = antardasha(mds[1], lons)["antardasha_start_rule"]
|
||||
assert (gemini["stronger_sign"], gemini["strength_level_decided"], gemini["start_sign"]) == ("Sagittarius", 5, "Cancer")
|
||||
|
||||
|
||||
def test_rath_profile_is_not_wired_into_any_product_path() -> None:
|
||||
pattern = re.compile(r"\bnarayana_rath\b")
|
||||
users = sorted(path.name for path in (ROOT / "scripts").rglob("*.py")
|
||||
|
||||
Reference in New Issue
Block a user