#!/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"]}