feat(narayana): Sanjay Rath profile replayed against the book tables; not wired (BUG-1215)

Round-4 ruling Q4. scripts/narayana_rath.py builds the Rath sequence from the
rule ledger and book rows ported verbatim from upstream yinduzhanxing c7117f92
(sanjay_rath_narayana_book_ledger JSON + md, chara_narayana_book_golden_cases):
Table 10 order (Saturn -> Table 11, Ketu -> reversed), years by
jaimini_odd_footed_dignity_v2 with jaimini_dual_lord_source_v1, ADs twelve
equal parts from the MD sign (Table 13 / 11 / 14), start sign by the existing
select_narayana_seed_sign, ties reported as parameter_sensitive with both
sequences.

Given the book's start sign, Tables 15 / 17 / 18 match 36/36 MD signs and
years. The start sign does not: Table 17 picks Virgo (book Pisces), Table 18
ties (book Cancer). Task red line 2: the profile stays out of every
user-visible surface and out of scoring; T5 trial not run. A test pins that no
script imports it.

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-03 16:36:31 +08:00
co-authored by Claude Opus 5.5
parent 0afcc2d9a3
commit 7cb332bce6
5 changed files with 4037 additions and 0 deletions
+152
View File
@@ -0,0 +1,152 @@
#!/usr/bin/env python3
"""Narayana Dasha, Sanjay Rath profile (`sanjay_rath`) — research only (BUG-1215).
Astrologer round-4 ruling 2026-10-03, Q4: the second time axis should be Sanjay
Rath's Narayana Dasha (start from the stronger of Lagna and 7th, order by the
start sign, dual lords by the traditional profile). The current product axis
(`narayana_dasha.calc_narayana_mahadasha`) always starts from the Lagna and runs
zodiacally; it is labelled `legacy_reference_only` on user-visible surfaces.
This module builds the Rath sequence from the rule ledger ported from upstream
yinduzhanxing c7117f92 (`references/oracle/sanjay_rath_narayana_book_ledger_2026_08_28.json`,
`docs/research/sanjay_rath_narayana_book_ledger_2026_08_28.md`):
- start sign: stronger of Lagna / 7th (`rath_md_start_stronger_lagna_seventh`),
decided by the existing `narayana_dasha.select_narayana_seed_sign`; a tie
(`later_narayana_sign_strength_rules_required`) gives
`status: parameter_sensitive` and both sequences;
- MD order: Table 10 by the start sign (`rath_md_general_order_table10`); Saturn
in the start sign -> Table 11, zodiacal (`rath_saturn_start_exception`); Ketu
in the start sign (no Saturn) -> Table 10 direction reversed (the reversal
rule, `rath_ketu_reversal_exception`; the ledger tables only the AD form);
- years: `jaimini_odd_footed_dignity_v2` (Upadesa 1.1.25-27 odd/even-footed
count with the fixed-sign exception, exalted +1 / debilitated -1, ledger
`upadesa_1125_1126_odd_even_count`, `upadesa_1127_fixed_sign_exception`,
`upadesa_bphs_exalt_debil_adjustment`); Scorpio / Aquarius lords by
`jaimini_dual_lord_source_v1` (`upadesa_dual_lord_strength_chain`, partial);
- AD: twelve equal parts from the MD sign, Table 13 order; Saturn in the MD sign
-> Table 11, Ketu -> Table 14 (`rath_ad_equal_twelfths`, `rath_ad_order_table13`).
Not wired into scoring, consultation or reports: the three book tables
(Table 15 / 17 / 18) are not all reproduced (see
`docs/tasks/PROGRESS-astrologer-rulings-batch2-20261003.md`, task red line 2).
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional
try: # scripts/ on sys.path
import narayana_dasha as nd
except ImportError: # pragma: no cover - package import
from scripts import narayana_dasha as nd
PROFILE = "sanjay_rath"
DURATION_PROFILE = "jaimini_odd_footed_dignity_v2"
DUAL_LORD_PROFILE = "jaimini_dual_lord_source_v1"
SOURCE = ("Sanjay Rath, Narayana Dasa — rule ledger and Tables 10/11/13/14 "
"(ported from upstream yinduzhanxing c7117f92)")
SIGNS = nd.SIGNS
def md_order(seed_sign_idx: int, planet_lons: Dict[str, float]) -> Dict[str, Any]:
"""MD sign order for one start sign, with the rule that decided it."""
saturn = nd._get_planet_sign("Saturn", planet_lons) == seed_sign_idx
ketu = nd._get_planet_sign("Ketu", planet_lons) == seed_sign_idx
if saturn:
order = nd.build_narayana_sign_sequence(seed_sign_idx, profile="rath_saturn_table11_v1")
direction = "table11_saturn_zodiacal"
elif ketu:
table10 = nd.build_narayana_sign_sequence(seed_sign_idx, profile="rath_general_table10_v1")
order = [(2 * seed_sign_idx - sign) % 12 for sign in table10]
direction = "table10_reversed_ketu"
else:
order = nd.build_narayana_sign_sequence(seed_sign_idx, profile="rath_general_table10_v1")
direction = "table10_general"
return {"order": order, "direction": direction, "saturn_in_seed": saturn, "ketu_in_seed": ketu}
def _ad_order_profile(md_sign_idx: int, planet_lons: Dict[str, float]) -> str:
if nd._get_planet_sign("Saturn", planet_lons) == md_sign_idx:
return "rath_saturn_table11_v1"
if nd._get_planet_sign("Ketu", planet_lons) == md_sign_idx:
return "rath_ketu_table14_v1"
return "rath_table13_v1"
def mahadasha_sequence(seed_sign_idx: int, planet_lons: Dict[str, float],
*, start_age: float = 0.0, cycle_count: int = 2) -> Dict[str, Any]:
"""Rath MD rows from one start sign (cycle 2 = twelve minus cycle 1)."""
order = md_order(seed_sign_idx, planet_lons)
rows: List[Dict[str, Any]] = []
cursor = float(start_age)
for position, sign_idx in enumerate(order["order"], start=1):
lord = nd._resolve_narayana_lord(sign_idx, planet_lons, dual_lord_profile=DUAL_LORD_PROFILE)
lord_sign_idx = nd._get_planet_sign(lord, planet_lons)
if lord_sign_idx is None:
lord_sign_idx = sign_idx
years, adjustment = nd.calculate_narayana_period_years(sign_idx, lord, lord_sign_idx, profile=DURATION_PROFILE)
rows.append({
"sign": SIGNS[sign_idx], "sign_idx": sign_idx, "lord": lord,
"lord_in_sign": SIGNS[lord_sign_idx], "lord_sign_idx": lord_sign_idx,
"years": years, "dignity_adjustment": adjustment, "cycle_index": 1,
"period_key": f"narayana_rath:1:{position:02d}:{SIGNS[sign_idx]}",
"start_age": round(cursor, 4), "end_age": round(cursor + years, 4),
})
cursor += years
if cycle_count == 2:
for position, first in enumerate(list(rows), start=1):
years = 12 - first["years"]
rows.append({**first, "years": years, "dignity_adjustment": None, "cycle_index": 2,
"period_key": f"narayana_rath:2:{position:02d}:{first['sign']}",
"start_age": round(cursor, 4), "end_age": round(cursor + years, 4)})
cursor += years
return {**order, "seed_sign": SIGNS[seed_sign_idx], "seed_sign_idx": seed_sign_idx, "mahadasha": rows}
def antardasha(md_row: Dict[str, Any], planet_lons: Dict[str, float]) -> List[Dict[str, Any]]:
"""Twelve equal ADs from the MD sign (Table 13 / 11 / 14)."""
profile = _ad_order_profile(md_row["sign_idx"], planet_lons)
rows = nd.subdivide_narayana_equal_order(
md_row, start_sign_idx=md_row["sign_idx"], order_profile=profile, level="AD",
parent_key="parent_md", parent_name=md_row["sign"],
)
return rows
def calc_narayana_rath(lagna_sign_idx: int, planet_lons: Dict[str, float],
*, seed_sign_idx: Optional[int] = None, cycle_count: int = 2) -> Dict[str, Any]:
"""Rath Narayana Dasha for one chart.
`seed_sign_idx` overrides the start sign (book-table replay only).
"""
if seed_sign_idx is not None:
selection = {"status": "explicit", "selected_sign_idx": seed_sign_idx}
else:
selection = nd.select_narayana_seed_sign(lagna_sign_idx, planet_lons)
base = {
"profile": PROFILE,
"duration_profile": DURATION_PROFILE,
"dual_lord_rule": DUAL_LORD_PROFILE,
"source": SOURCE,
"lagna_sign": SIGNS[lagna_sign_idx],
"seed_selection": selection,
}
if selection["status"] in ("selected", "explicit"):
seq = mahadasha_sequence(selection["selected_sign_idx"], planet_lons, cycle_count=cycle_count)
return {**base, "status": "partial_verified" if selection["status"] == "selected" else "explicit_seed",
"seed_sign": seq["seed_sign"],
"seed_basis": ("explicit_seed" if selection["status"] == "explicit"
else "stronger_of_lagna_seventh:" + "+".join(selection.get("selection_levels", []))),
"direction": seq["direction"], "mahadasha": seq["mahadasha"]}
alternatives = {
SIGNS[idx]: mahadasha_sequence(idx, planet_lons, cycle_count=cycle_count)
for idx in (lagna_sign_idx, (lagna_sign_idx + 6) % 12)
}
lagna_seq = alternatives[SIGNS[lagna_sign_idx]]
return {**base, "status": "parameter_sensitive", "reason": selection.get("reason"),
"seed_sign": None, "seed_basis": "tie_lagna_seventh_both_listed",
"direction": None, "mahadasha": None,
"alternatives": {sign: {"seed_sign": seq["seed_sign"], "direction": seq["direction"],
"mahadasha": seq["mahadasha"]} for sign, seq in alternatives.items()},
"lagna_start_mahadasha": lagna_seq["mahadasha"]}