Files
Jyotisha/scripts/narayana_rath.py
T
Jesse_ChenandClaude Opus 5.5 1dd8e11bff feat(narayana-rath): flag Wadiyar pairs as book_software_rule8_exception (BUG-1226)
Round-6 ruling Q6b: Table 25 Gemini/Sagittarius and Virgo/Pisces are the
book software's seventh-level Rule 8 special case; keep the general result
(Sagittarius, Pisces at level 5), every Rath output carries the note
(known_book_differences), test pins the two pairs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
2026-10-04 07:35:37 +08:00

528 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Narayana Dasha, Sanjay Rath profile (`sanjay_rath`) — research only (BUG-1215 / BUG-1218).
Astrologer round-4 ruling (Q4): the second time axis should be Sanjay Rath's
Narayana Dasha. Round-5 ruling 2026-10-03 fixed the open points (book pages
in brackets; Sanjay Rath, *Narayana Dasa*):
Start sign = the stronger of Lagna and 7th [p.48]. General rule, seven levels,
stop at the first level that separates the two signs, no weighting [p.35-36,
p.63]:
1. more occupants (actual bodies, Rahu/Ketu included);
2. more support from three roles — Jupiter, Mercury and the sign lord — each
role counted once when the planet is in the sign or casts Rasi Drishti on
it [p.48; no graha drishti]; roles are not de-duplicated by planet
(Jupiter as Jupiter and as lord counts twice) [p.36];
3. occupant dignity: exalted > moolatrikona > own sign (whole sign) > none >
debilitated — best rank, then the number of dignified occupants; Rahu /
Ketu by Rath Table 9 (p.41-42, Phalita set; round-6 ruling Q2,
BUG-1224), debilitation below none per p.67;
4. dual > fixed > movable;
5. the lord's degree inside its sign, Chara Karaka sense (higher is
stronger; Rahu / Ketu as 30 - degree);
6. the lord placed in a sign of the opposite parity to the sign it rules;
7. the sign giving the longer dasha.
"Contains the AK" / "lord is the AK" are not general rules [p.35]. The
Indira example (p.65-66) decides Cancer by the AK; it is kept as a flagged
book example (`book_example_exception: rath_p66_indira_ak`), used only when
all seven levels tie.
MD order: Table 10 by the start sign [p.51]; Saturn in the start sign -> zodiacal,
sign by sign (Table 11) [p.51-53]; Ketu in the start sign -> Table 10 direction
reversed, keeping the fixed / dual stepping (not the whole table read
backwards) [p.51, p.53 Table 12; p.54 Table 14 is the AD table]. Saturn and
Ketu both in the start sign: Ketu first (PyJHora's practice, product owner
ruling 2026-10-03, `method_note`).
Years: `jaimini_odd_footed_dignity_v2` (odd/even-footed count, fixed-sign
exception, exalted +1 / debilitated -1); Scorpio / Aquarius lords by
`jaimini_dual_lord_source_v1`. Cycle 2 = 12 minus cycle 1.
AD [p.48 Arambha Rasi, p.50 note 2]: compare the MD sign and its 7th with the
same seven levels, take the stronger, start from the sign holding that sign's
lord (dual lords by the same rule), twelve equal parts. Order: Table 13 from
the start sign (direction by the start sign's odd/even, as in Table 25;
round-6 ruling Q7); Saturn in the MD sign -> Table 11 (zodiacal); Ketu in the
MD sign -> Table 14 (Ketu first when both) [p.51]. The book's other two
statements (p.47 J.S. 1.1.34: by the MD sign; J.S. 2.4.31-32: by the MD sign
or its lord's sign) disagree with Table 25; each AD rule carries them as
`direction.book_divergence` (BUG-1225), not applied.
Ported verbatim from upstream yinduzhanxing c7117f92 `scripts/narayana_dasha.py`:
`_rath_ketu_table12_sequence` (the `rath_ketu_table12_v1` branch of
`build_narayana_sign_sequence`), `_RATH_CLASSICAL_MOOLATRIKONA_SIGNS`, and the
level 1-2 counting of `select_narayana_seed_sign(strength_profile=
"rath_p36_role_conjunction_v1")` (`_role_candidate`; tests compare it with the
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.
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
from typing import Any, Dict, List, Optional, Tuple
try: # scripts/ on sys.path
import narayana_dasha as nd
from jaimini import calc_chara_karaka_8
except ImportError: # pragma: no cover - package import
from scripts import narayana_dasha as nd
from scripts.jaimini import calc_chara_karaka_8
PROFILE = "sanjay_rath"
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."),
},
}
# Book software verdicts the general rule does not follow (round-6 ruling
# Q6b, BUG-1226): Table 25 (Krishnaraja Wadiyar IV) takes Gemini over
# Sagittarius and Virgo over Pisces; the seven general levels decide
# Sagittarius / Pisces at level 5 (Mercury 1.3 deg vs Jupiter 10.0 deg). Only
# the book software's Rule 8 at the seventh level separates them that way — a
# software special case. The general result is kept and every output carries
# this note.
BOOK_SOFTWARE_RULE8 = "book_software_rule8_exception"
BOOK_SOFTWARE_DIFFERENCES = {
BOOK_SOFTWARE_RULE8: {
"book_table": "Table 25",
"pairs": [{"book": "Gemini", "general_rule": "Sagittarius", "general_level": 5},
{"book": "Virgo", "general_rule": "Pisces", "general_level": 5}],
"kept": "general_rule",
"note": "原书软件判定差异:原书软件用第 7 级 Rule 8 取双子、处女;通则第 5 级取射手、双鱼,保持通则",
"note_en": ("Book software verdict differs: the book's software used Rule 8 at the seventh level to take "
"Gemini and Virgo; the general rule takes Sagittarius and Pisces at level 5 and is kept"),
"ruling": "astrologer round 6, 2026-10-04, Q6b (p.36, p.95)",
},
}
KETU_FIRST_NOTE = "PyJHora 兼容:计都优先"
KETU_FIRST_NOTE_EN = "PyJHora-compatible: Ketu takes precedence"
SOURCE = {
"book": "Sanjay Rath, Narayana Dasa",
"start_sign": "p.48 (stronger of Lagna / 7th); levels p.35-36, p.63; Rasi Drishti p.48",
"md_order": "p.51 Table 10; Saturn p.51-53 Table 11; Ketu p.51, p.53 Table 12",
"ad": "p.48 Arambha Rasi, p.50 note 2; Tables 13 / 11 / 14 (p.54)",
"indira_example": "p.65-66 (AK), Table 18 p.67",
"ruling": "astrologer round 5, 2026-10-03, Q1-Q4; product owner: Ketu first; round 6, 2026-10-04, Q2 (node dignity)",
"node_dignity": "Table 9 p.41-42 (Phalita exaltation Rahu Gemini / Ketu Sagittarius); debilitated weaker p.67",
"port": "upstream yinduzhanxing c7117f92 scripts/narayana_dasha.py",
}
SIGNS = nd.SIGNS
SIGN_LORDS = nd.SIGN_LORDS
GRAHAS = ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu")
LEVELS = (
"occupant_count",
"role_conjunction_or_rasi_aspect",
"occupant_dignity",
"modality",
"lord_degree_in_sign",
"lord_opposite_parity",
"longer_dasha",
)
# Ported from upstream c7117f92 (Rath p80, whole-sign scope).
_RATH_CLASSICAL_MOOLATRIKONA_SIGNS = dict(Sun=4, Moon=1, Mars=0,
Mercury=5, Jupiter=8, Venus=6, Saturn=10) # Rath p80, whole-sign scope.
def _rath_ketu_table12_sequence(seed_sign_idx: int) -> List[int]:
# Ported from upstream c7117f92 build_narayana_sign_sequence,
# profile "rath_ketu_table12_v1".
# Rath pp.53-54 Table12 note: reverse Table10 direction, retain motion.
return [(2 * seed_sign_idx - sign) % 12
for sign in nd._NARAYANA_RATH_GENERAL_SEQUENCE[seed_sign_idx]]
def _grahas(planet_lons: Dict[str, float]) -> Dict[str, float]:
return {planet: float(planet_lons[planet]) for planet in GRAHAS if planet_lons.get(planet) is not None}
def _lord(sign_idx: int, planet_lons: Dict[str, float]) -> str:
return nd._resolve_narayana_lord(sign_idx, planet_lons, dual_lord_profile=DUAL_LORD_PROFILE)
def _role_candidate(sign_idx: int, planet_lons: Dict[str, float], lord: str) -> Dict[str, Any]:
"""Levels 1-2 for one sign: upstream `select_narayana_seed_sign` with
strength_profile="rath_p36_role_conjunction_v1" (c7117f92), per sign."""
occupants = sorted(
planet
for planet in planet_lons
if nd._get_planet_sign(planet, planet_lons) == sign_idx
)
aspect_factors = []
role_factors = []
roles = (("jupiter", "Jupiter"), ("mercury", "Mercury"), ("sign_lord", lord))
for role, planet in roles:
planet_sign_idx = nd._get_planet_sign(planet, planet_lons)
conjunction = planet_sign_idx == sign_idx
if planet_sign_idx is not None and (conjunction or nd._rasi_aspects(planet_sign_idx, sign_idx)):
aspect_factors.append(planet)
role_factors.append(dict(role=role, planet=planet, contact="conjunction" if conjunction else "rasi_aspect"))
return {
"sign": SIGNS[sign_idx],
"lord": lord,
"occupants": occupants,
"occupant_count": len(occupants),
"aspect_factors": aspect_factors,
"aspect_factor_count": len(aspect_factors),
"role_factors": role_factors,
}
# Rahu / Ketu dignity inside Rath Narayana only (round-6 ruling Q2, Rath
# Table 9 p.41-42): own sign Rahu Aquarius / Ketu Scorpio; moolatrikona Rahu
# Virgo / Ketu Pisces; exaltation for Phalita dasas such as Narayana Rahu
# Gemini / Ketu Sagittarius (the Taurus / Scorpio pair is for Ayur dasas
# only), debilitation in the opposite signs. Not the natal dignity table
# (BUG-1211 "nodes have no own sign" still holds on the D1 main path).
RATH_NODE_DIGNITY = {
"Rahu": {"exalted": {2}, "moolatrikona": 5, "own": {10}, "debilitated": {8}},
"Ketu": {"exalted": {8}, "moolatrikona": 11, "own": {7}, "debilitated": {2}},
}
DEBILITATED_RANK = -1
def _occupant_dignity(planet: str, sign_idx: int) -> int:
"""Level 3 rank: exalted 3 > moolatrikona 2 > own 1 > none 0 > debilitated -1.
Debilitated below none: p.67 (Indira, Table 18) "Ketu is debilitated and
as per Rule (3) of first source of strength is weaker than Mars" (Mars in
Leo, no dignity). Applied to all nine bodies (Rule 3 is general).
"""
node = RATH_NODE_DIGNITY.get(planet)
if node is not None:
exalted, moolatrikona, own, debilitated = node["exalted"], node["moolatrikona"], node["own"], node["debilitated"]
else:
table = nd._NARAYANA_DIGNITY.get(planet)
if table is None:
return 0
exalted, debilitated = table["exalted"], table["debilitated"]
moolatrikona = _RATH_CLASSICAL_MOOLATRIKONA_SIGNS[planet]
own = {idx for idx, sign in enumerate(SIGNS) if SIGN_LORDS[sign] == planet}
if sign_idx in exalted:
return 3
if sign_idx == moolatrikona:
return 2
if sign_idx in own:
return 1
if sign_idx in debilitated:
return DEBILITATED_RANK
return 0
def _ck_degree(planet: str, lon: float) -> float:
degree = float(lon) % 30.0
return 30.0 - degree if planet in ("Rahu", "Ketu") else degree
def sign_strength(sign_idx: int, planet_lons: Dict[str, float]) -> Dict[str, Any]:
"""All seven level values for one sign (see module docstring)."""
lons = _grahas(planet_lons)
lord = _lord(sign_idx, lons)
row = _role_candidate(sign_idx, lons, lord)
ranks = {planet: _occupant_dignity(planet, sign_idx) for planet in row["occupants"]}
best = max(ranks.values(), default=0)
dignified = sum(1 for value in ranks.values() if value > 0)
debilitated = sum(1 for value in ranks.values() if value == DEBILITATED_RANK)
lord_sign_idx = nd._get_planet_sign(lord, lons)
if lord_sign_idx is None:
lord_sign_idx = sign_idx
years, _ = nd.calculate_narayana_period_years(sign_idx, lord, lord_sign_idx, profile=DURATION_PROFILE)
row.update({
"occupant_dignity": {"ranks": ranks, "best_rank": best, "dignified_count": dignified,
"debilitated_count": debilitated},
"modality": "dual" if sign_idx in (2, 5, 8, 11) else "fixed" if sign_idx in nd._FIXED_SIGNS else "movable",
"lord_sign": SIGNS[lord_sign_idx],
"lord_degree_in_sign": round(_ck_degree(lord, lons[lord]), 6) if lord in lons else None,
"lord_opposite_parity": lord_sign_idx % 2 != sign_idx % 2,
"dasha_years": years,
})
row["level_values"] = [
row["occupant_count"],
row["aspect_factor_count"],
(best, dignified),
{"movable": 0, "fixed": 1, "dual": 2}[row["modality"]],
row["lord_degree_in_sign"] if row["lord_degree_in_sign"] is not None else -1.0,
int(row["lord_opposite_parity"]),
years,
]
return row
def _atmakaraka(planet_lons: Dict[str, float]) -> Optional[str]:
lons = {planet: lon for planet, lon in _grahas(planet_lons).items() if planet != "Ketu"}
table = calc_chara_karaka_8(lons).get("karaka_table_8") or {}
for row in table.values():
if row.get("rank") == 1:
return row.get("planet")
return None
def compare_signs(first_idx: int, second_idx: int, planet_lons: Dict[str, float],
*, allow_book_example: bool = False) -> Dict[str, Any]:
"""Stronger of two signs by the seven general levels.
`allow_book_example`: only when all seven tie, the Indira example's AK
tie-break may decide; the result then carries `book_example_exception`.
"""
candidates = {first_idx: sign_strength(first_idx, planet_lons), second_idx: sign_strength(second_idx, planet_lons)}
base = {"candidates": {SIGNS[idx]: row for idx, row in candidates.items()},
"book_example_exception": None}
for level, name in enumerate(LEVELS):
a = candidates[first_idx]["level_values"][level]
b = candidates[second_idx]["level_values"][level]
if a != b:
winner = first_idx if a > b else second_idx
return {**base, "status": "selected", "selected_sign_idx": winner, "selected_sign": SIGNS[winner],
"strength_level_decided": level + 1, "strength_level_name": name}
if allow_book_example:
ak = _atmakaraka(planet_lons)
holders = [idx for idx in (first_idx, second_idx) if ak and nd._get_planet_sign(ak, planet_lons) == idx]
if len(holders) == 1:
winner = holders[0]
return {**base, "status": "selected", "selected_sign_idx": winner, "selected_sign": SIGNS[winner],
"strength_level_decided": None, "strength_level_name": "book_example_atmakaraka",
"book_example_exception": INDIRA_EXCEPTION, "atmakaraka": ak}
return {**base, "status": "tie", "reason": "all_seven_general_levels_tie",
"strength_level_decided": None, "strength_level_name": None}
def md_order(seed_sign_idx: int, planet_lons: Dict[str, float]) -> Dict[str, Any]:
"""MD sign order for one start sign, with the exception 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
method_note = None
if ketu:
order = _rath_ketu_table12_sequence(seed_sign_idx)
direction = "table12_ketu_reversed"
exception = "ketu_in_start_sign"
if saturn:
method_note = KETU_FIRST_NOTE
elif saturn:
order = nd.build_narayana_sign_sequence(seed_sign_idx, profile="rath_saturn_table11_v1")
direction = "table11_saturn_zodiacal"
exception = "saturn_in_start_sign"
else:
order = nd.build_narayana_sign_sequence(seed_sign_idx, profile="rath_general_table10_v1")
direction = "table10_general"
exception = None
return {"order": order, "direction": direction, "exception_applied": exception,
"method_note": method_note, "method_note_en": KETU_FIRST_NOTE_EN if method_note else None,
"saturn_in_seed": saturn, "ketu_in_seed": ketu}
def _ad_order_profile(md_sign_idx: int, planet_lons: Dict[str, float]) -> Tuple[str, Optional[str]]:
ketu = nd._get_planet_sign("Ketu", planet_lons) == md_sign_idx
saturn = nd._get_planet_sign("Saturn", planet_lons) == md_sign_idx
if ketu:
return "rath_ketu_table14_v1", (KETU_FIRST_NOTE if saturn else None)
if saturn:
return "rath_saturn_table11_v1", None
return "rath_table13_v1", None
# AD direction (round-6 ruling Q7): default by the odd/even of the AD start
# sign (Table 13 practice, as the book's Table 25 shows); exceptions p.51:
# Saturn in the MD sign -> always zodiacal (Table 11); Ketu in the MD sign ->
# reversed (Table 14); both -> Ketu first (product owner 2026-10-03). Two
# other statements in the book disagree with Table 25 and are reported, not
# applied (`book_divergence`).
AD_DIRECTION_RULE = {
"rath_table13_v1": ("按子运起点星座奇偶(Table 13)", "by the odd/even of the AD start sign (Table 13)"),
"rath_saturn_table11_v1": ("大运星座有土星:一律顺行(Table 11)", "Saturn in the MD sign: always zodiacal (Table 11)"),
"rath_ketu_table14_v1": ("大运星座有计都:反向(Table 14)", "Ketu in the MD sign: reversed (Table 14)"),
}
AD_BOOK_DIVERGENCE = (
("rath_p47_js_1_1_34", "Rath p.47 (J.S. 1.1.34)",
"p.47(J.S. 1.1.34)看大运星座奇偶", "p.47 (J.S. 1.1.34) by the odd/even of the MD sign"),
("js_2_4_31_32", "J.S. 2.4.31-32 (Rath p.47-48)",
"J.S. 2.4.31–32 看大运星座或其宫主所落星座", "J.S. 2.4.31-32 by the MD sign or the sign its lord occupies"),
)
def _is_odd_sign(sign_idx: int) -> bool:
return sign_idx % 2 == 0 # Aries (index 0) is the first, odd sign
def ad_direction(md_sign_idx: int, start_sign_idx: int, profile: str, planet_lons: Dict[str, float]) -> Dict[str, Any]:
"""Applied AD direction plus the two book statements it does not follow."""
order = nd.build_narayana_antardasha_order_sequence(start_sign_idx, profile=profile)
applied = "zodiacal" if order[1] == (start_sign_idx + 1) % 12 else "reverse"
lord_sign = nd._get_planet_sign(_lord(md_sign_idx, planet_lons), planet_lons)
by_rule = {
"rath_p47_js_1_1_34": "zodiacal" if _is_odd_sign(md_sign_idx) else "reverse",
"js_2_4_31_32": "zodiacal" if _is_odd_sign(md_sign_idx) or (lord_sign is not None and _is_odd_sign(lord_sign)) else "reverse",
}
zh, en = AD_DIRECTION_RULE[profile]
return {
"direction": applied,
"rule": profile,
"rule_text": zh,
"rule_text_en": en,
"book_divergence": [
{"id": key, "source": source, "rule_text": rule_zh, "rule_text_en": rule_en,
"direction": by_rule[key], "differs": by_rule[key] != applied, "applied": False}
for key, source, rule_zh, rule_en in AD_BOOK_DIVERGENCE
],
}
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 = _lord(sign_idx, planet_lons)
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]) -> Dict[str, Any]:
"""Twelve equal ADs: start from the sign of the lord of the stronger of
the MD sign and its 7th (round-5 Q4); order Table 13 / 11 / 14."""
md_sign_idx = md_row["sign_idx"]
seventh = (md_sign_idx + 6) % 12
comparison = compare_signs(md_sign_idx, seventh, planet_lons)
profile, note = _ad_order_profile(md_sign_idx, planet_lons)
if comparison["status"] != "selected":
return {"status": "blocked", "reason": comparison.get("reason"), "rows": [],
"antardasha_start_rule": {"rule": "stronger_of_md_sign_and_seventh_then_its_lord_sign",
"compared": [SIGNS[md_sign_idx], SIGNS[seventh]],
"strength_level_decided": None}}
stronger = comparison["selected_sign_idx"]
lord = _lord(stronger, planet_lons)
start = nd._get_planet_sign(lord, planet_lons)
if start is None:
return {"status": "blocked", "reason": "stronger_sign_lord_longitude_missing", "rows": []}
rows = nd.subdivide_narayana_equal_order(
md_row, start_sign_idx=start, order_profile=profile, level="AD",
parent_key="parent_md", parent_name=md_row["sign"],
)
for row in rows:
row["start_sign_source"] = "lord_of_stronger_md_or_seventh_sign"
return {
"status": "selected",
"rows": rows,
"antardasha_start_rule": {
"rule": "stronger_of_md_sign_and_seventh_then_its_lord_sign",
"compared": [SIGNS[md_sign_idx], SIGNS[seventh]],
"stronger_sign": SIGNS[stronger],
"strength_level_decided": comparison["strength_level_decided"],
"lord": lord,
"dual_lord_rule": DUAL_LORD_PROFILE if stronger in (7, 10) else None,
"start_sign": SIGNS[start],
"order_profile": profile,
"method_note": note,
"direction": ad_direction(md_sign_idx, start, profile, planet_lons),
},
}
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,
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); 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,
"dual_lord_rule": DUAL_LORD_PROFILE,
"source": SOURCE,
"lagna_sign": SIGNS[lagna_sign_idx],
"antardasha_start_rule": "stronger_of_md_sign_and_seventh_then_its_lord_sign (p.48, p.50 note 2)",
"known_book_differences": BOOK_SOFTWARE_DIFFERENCES,
}
if seed_sign_idx is not None:
selection = {"status": "explicit", "selected_sign_idx": seed_sign_idx,
"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)
base["seed_selection"] = selection
base["strength_level_decided"] = selection.get("strength_level_decided")
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" 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']}"
else:
status = "partial_verified"
basis = f"stronger_of_lagna_seventh:level_{selection['strength_level_decided']}_{selection['strength_level_name']}"
return {**base, "status": status, "seed_sign": seq["seed_sign"], "seed_basis": basis,
"direction": seq["direction"], "exception_applied": seq["exception_applied"],
"method_note": seq["method_note"], "method_note_en": seq["method_note_en"],
"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)
}
return {**base, "status": "parameter_sensitive", "reason": selection.get("reason"),
"seed_sign": None, "seed_basis": "tie_lagna_seventh_both_listed",
"direction": None, "exception_applied": None, "method_note": None, "method_note_en": None,
"mahadasha": None,
"alternatives": {sign: {"seed_sign": seq["seed_sign"], "direction": seq["direction"],
"exception_applied": seq["exception_applied"],
"mahadasha": seq["mahadasha"]} for sign, seq in alternatives.items()}}