Files
Jyotisha/scripts/padanadhamsa.py
T
ClaudeandClaude Opus 5.5 ac9be6f88c feat(techniques): port the astrologer upstream skill's missing techniques
Port from yinduzhanxing 5fb8379c (MIT): Naisargika, Kaala, Karaka, Tara,
Lagnamsaka, Yogardha, Mandooka, Paryaaya and Padanadhamsa dashas;
Sarvatobhadra, Kota and Kalanala chakras; Nakshatra Latta; Pachakadi;
Raman Amsayu and Pindayu. Producers and their upstream tests are copied;
scripts/upstream_technique_layers.py feeds them our natal and transit
positions and attaches chart.consultation_native_layers.upstream_techniques.

- dasha_registry: kaala and padanadhamsa keys (35 -> 37); the nine ported
  dashas are served by compute_dasha or fail closed with upstream's reason.
- technique_registry.json: 16 partial entries; README badges follow.
- Evidence lookup section `upstream_techniques` in the consultation tool.
- Boundaries: Kalanala carries no PyJHora (AGPL) grid coordinates;
  Padanadhamsa/Lagnamsaka run on our Narayana engine (no Rath Table 12,
  PVR co-lord variants or second cycle). PyJHora observers and the Amsayu
  worked-example scaffold live in scripts/research (root .py budget).
- PyJHora black-box cross-checks: Naisargika (lords/years) and Kaala
  (seed segment and fraction within 5e-4) on a public chart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbBXoyn7Hj3LCW3BUs5bZ8
2026-11-07 14:44:37 +08:00

193 lines
12 KiB
Python

"""Rangacharya article MD scope using supplied Jaimini Navamsa signs."""
from copy import deepcopy
import math
import json
from itertools import product
PROFILE = 'padanadhamsa_rangacharya_movable_fixed_md_v1'
SOURCE = 'https://studyres.com/doc/6420886/padanadhamsa-dasa---saptarishis-magazine'
LORDS = ('Mars','Venus','Mercury','Moon','Sun','Mercury',
'Venus','Mars','Jupiter','Saturn','Saturn','Jupiter')
RATH_PROFILE = 'padanadhamsa_rath_single_al_lord_narayana_md_v1'
RATH_DUAL_PROFILE = 'padanadhamsa_rath_dual_al_sign_strength_md_v1'
RATH_SOURCE = 'https://scienceoflight.net/wp-content/uploads/extra/Narayana%20Dasa%20by%20Sanjay%20Rath.pdf#page=192'
RATH_AD_PROFILE = 'padanadhamsa_rath_p50_general_ad_v1'
RATH_TWO_CYCLE_PROFILE = 'padanadhamsa_rath_two_cycle_md_ad_v1'
# Not ported from upstream (yinduzhanxing 5fb8379c): the PVR conditional
# co-lord timelines, the PVR co-lord report and the Rath p50 antardasa overlay.
# They need upstream Narayana entries ours does not carry
# (calculate_pvr_co_lord_period_candidates, resolve_pvr_co_lord,
# calculate_narayana_second_cycle_years, Rath Table 12 / Table 25 profiles).
# Our Narayana engine follows the astrologer's own rulings (BUG-1601 and
# later); it is not replaced by upstream's variant here.
LOCAL_STRENGTH_PROFILE = 'jaimini_lagna_seventh_strength_v1'
def build_padanadhamsa_rath_report(*, arudha_lagna_sign_idx, navamsa_planet_longitudes,
duration_profile, dual_lord_profile, profile,
strength_profile='jaimini_lagna_seventh_strength_v1', cycle_count=1):
"""Rath chapter Special Narayana Dasa: D9 AL-lord sign/seventh seed.
Uses existing native Narayana rules, not Rangacharya inclusive durations.
"""
try:
from scripts.narayana_dasha import calc_narayana_mahadasha, select_narayana_seed_sign
except ModuleNotFoundError: # pragma: no cover - scripts/ on sys.path
from narayana_dasha import calc_narayana_mahadasha, select_narayana_seed_sign
if profile not in {RATH_PROFILE, RATH_DUAL_PROFILE}:
raise ValueError('explicit Rath Padanadhamsa profile required')
if type(cycle_count) is not int or cycle_count not in (1, 2):
raise ValueError('cycle_count must be integer 1 or 2')
if cycle_count != 1:
raise ValueError('second Padanadhamsa cycle not ported; local Narayana engine has no cycle_count')
if strength_profile != LOCAL_STRENGTH_PROFILE:
raise ValueError(f'only {LOCAL_STRENGTH_PROFILE} is available in the local Narayana engine')
al = _sign(arudha_lagna_sign_idx)
if al in {7, 10} and profile == RATH_PROFILE:
raise ValueError('Scorpio/Aquarius AL requires both AL lords and additional D9 strength arbitration')
required = set(LORDS) | {'Rahu', 'Ketu'}
if not isinstance(navamsa_planet_longitudes, dict) or set(navamsa_planet_longitudes) != required:
raise ValueError('all nine D9 planetary longitudes required; no missing-lord fallback')
for value in navamsa_planet_longitudes.values():
if isinstance(value, bool) or not isinstance(value, (int,float)) or not math.isfinite(value) or not 0 <= value < 360:
raise ValueError('D9 longitudes must be finite in [0,360)')
if duration_profile not in {'jaimini_odd_footed_v1', 'jaimini_odd_footed_dignity_v2'}:
raise ValueError('explicit native Jaimini duration profile required')
al_lords = {7: ('Mars', 'Ketu'), 10: ('Saturn', 'Rahu')}.get(al, (LORDS[al],))
lord_candidates = []
for lord in al_lords:
candidate_sign = int(navamsa_planet_longitudes[lord] // 30)
# Reuse the native score data even if that sign/seventh comparison ties.
candidate = select_narayana_seed_sign(candidate_sign, navamsa_planet_longitudes)['candidates'][candidate_sign]
lord_candidates.append({**candidate, 'sign_lord': candidate['lord'],
'lord': lord, 'sign_idx': candidate_sign})
winner = lord_candidates[0]
reason = 'single_al_lord'
if len(lord_candidates) == 2:
a, b = lord_candidates
if a['sign_idx'] == b['sign_idx']:
reason = 'both_al_lords_in_same_d9_sign'
else:
comparison = select_narayana_seed_sign(a['sign_idx'], navamsa_planet_longitudes,
comparison_sign_idx=b['sign_idx'])
if comparison['status'] != 'selected':
raise ValueError('dual AL lord signs tied; later sign-strength rules required')
winner = next(c for c in lord_candidates if c['sign_idx'] == comparison['selected_sign_idx'])
for candidate in lord_candidates:
candidate['strength_comparison'] = comparison
reason = 'occupant_count_then_' + strength_profile
anchor = winner['sign_idx']
selection = select_narayana_seed_sign(anchor, navamsa_planet_longitudes)
if selection['status'] != 'selected':
raise ValueError(selection['reason'])
seed = selection['selected_sign_idx']
has_ketu = int(navamsa_planet_longitudes['Ketu']//30)==seed
has_saturn = int(navamsa_planet_longitudes['Saturn']//30)==seed
if has_ketu and has_saturn:
raise ValueError('joint Saturn/Ketu seed exception precedence unresolved')
if has_ketu:
# Same boundary as narayana_dasha (BUG-1601 family): Rath Table 12 is
# not a local sequence profile, so a Ketu seed stays blocked.
raise ValueError('rath_ketu_table12_v1_not_in_local_sequence_profiles')
sequence_profile = 'rath_saturn_table11_v1' if has_saturn else 'rath_general_table10_v1'
rows = calc_narayana_mahadasha(seed, navamsa_planet_longitudes,
duration_profile=duration_profile, dual_lord_profile=dual_lord_profile,
sequence_profile=sequence_profile, seed_profile='legacy_lagna_v0')
for row in rows:
row['active'] = row['years'] > 0
row.setdefault('cycle_index', 1)
result = dict(status='parameter_sensitive', variant_profile_id=profile, source=RATH_SOURCE,
tradition_profile='sanjay_rath_special_narayana', al_lord=winner['lord'],
al_lord_selection=dict(candidates=lord_candidates, reason=reason,
selected_sign_idx=anchor, selected_lord=winner['lord']),
al_lord_navamsa_sign_idx=anchor, seed_selection=selection,
duration_profile=duration_profile, dual_lord_profile=dual_lord_profile,
sequence_profile=sequence_profile, strength_profile=strength_profile, cycle_count=cycle_count,
periods=rows, limits=['Supplied D1 AL and consistently computed D9 longitudes; not computed by this entry',
'AL-lord and seed comparison follows the explicit strength_profile; unresolved ties and joint Saturn/Ketu precedence remain blocked',
'Chart47/Table53 first nine published MD durations reproduced; remaining rows not independently verified',
'First-cycle MD age-years only; no AD, calendar dates or automatic customer export'])
lines=['# Padanadhamsa — Rath D9 Narayana MD', '', f'Source: {RATH_SOURCE}',
f'Profile: {profile}', f'Duration: {duration_profile}; dual lord: {dual_lord_profile}',
f'Sequence: {sequence_profile}; strength: {strength_profile}',
f'Cycles: {cycle_count}',
f'AL lords: {list(al_lords)}; anchor decision: {reason}; selected lord: {winner["lord"]}',
f'AL lord D9 sign: {anchor}; selected seed: {seed}', '',
]
lines += ['| Cycle | Sign | Years | Start age | End age | Active |', '| --- | --- | --- | --- | --- | --- |']
lines += ['| '+' | '.join(str(r[k]) for k in ('cycle_index','sign','years','start_age','end_age','active'))+' |' for r in rows]
lines += ['', *result['limits']]
return dict(status=result['status'],customer_export_admitted=False,evidence_appendix=[result],
exports=dict(markdown='\n'.join(lines),ai_evidence_bundle=deepcopy(result)))
def _sign(value):
if type(value) is not int or not 0 <= value < 12:
raise ValueError('sign indices must be integers in [0,12)')
return value
def calculate_padanadhamsa_duration(*, sign_idx, lord_sign_idx, profile):
"""Article's inclusive parity rule, usable independently of seed order.
The caller supplies the sign of the resolved classical lord in Jaimini
Navamsa. This also works for dual signs without inventing their MD order.
"""
if profile != PROFILE:
raise ValueError('explicit Rangacharya text-rule profile required')
sign, lord = _sign(sign_idx), _sign(lord_sign_idx)
direction = 1 if sign % 2 == 0 else -1
distance = ((lord - sign) * direction) % 12
exception = 'own_sign_12' if distance == 0 else 'opposite_sign_10' if distance == 6 else 'none'
years = 12 if distance == 0 else 10 if distance == 6 else distance + 1
return dict(status='parameter_sensitive', variant_profile_id=PROFILE,
source=SOURCE, sign_idx=sign, lord_sign_idx=lord, direction=direction,
inclusive_count=distance+1, exception=exception, years=years,
claim_boundary='Article text-rule duration only; published Scorpio example conflict unresolved')
def build_padanadhamsa_report(*, arudha_lagna_sign_idx, jaimini_navamsa_signs,
navamsa_profile, profile):
"""Seven classical lords; AL and Jaimini Navamsa supplied, not reconstructed."""
if profile != PROFILE or navamsa_profile != 'supplied_rangacharya_jaimini_navamsa':
raise ValueError('explicit Rangacharya and supplied Jaimini Navamsa profiles required')
al = _sign(arudha_lagna_sign_idx)
if not isinstance(jaimini_navamsa_signs, dict) or set(jaimini_navamsa_signs) != set(LORDS):
raise ValueError('exactly seven classical planetary Jaimini Navamsa signs required')
for value in jaimini_navamsa_signs.values():
_sign(value)
seed = jaimini_navamsa_signs[LORDS[al]]
if seed % 3 == 2:
raise ValueError('dual-sign trine-group continuation needs an explicit worked sequence')
direction = 1 if seed % 2 == 0 else -1
step = 1 if seed % 3 == 0 else 5
rows, elapsed = [], 0
for i in range(12):
sign = (seed + i * step * direction) % 12
lord = LORDS[sign]
lord_sign = jaimini_navamsa_signs[lord]
duration = calculate_padanadhamsa_duration(sign_idx=sign, lord_sign_idx=lord_sign, profile=profile)
years = duration['years']
rows.append(dict(sign_idx=sign, lord=lord, lord_sign_idx=lord_sign,
duration_rule=duration,
years=years, start_age_years=elapsed, end_age_years=elapsed+years))
elapsed += years
result = dict(status='parameter_sensitive', variant_profile_id=PROFILE,
tradition_profile='iranganti_rangacharya_article', source=SOURCE,
navamsa_profile=navamsa_profile, seed_sign_idx=seed, seed_lord=LORDS[al],
periods=rows, limits=['Supplied AL and Jaimini Navamsa, not ordinary Parasara D9',
'Movable/fixed seed only; seven classical lords, no node co-lord substitution',
'No birth proration, AD, calendar dates or dual-seed continuation claimed',
'Article text and Indira table conflict: Scorpio text-rule 4 years versus printed 10; subsequent ages differ by six years',
'Authored article mirrored by third party; published table reproduction is not event verification'])
lines=['# Padanadhamsa — Rangacharya movable/fixed MD', '', f'Source: {SOURCE}',
f'Profile: {PROFILE}', f'Navamsa: {navamsa_profile}', '',
'| Sign index | Lord | Years | Start age | End age |', '| --- | --- | --- | --- | --- |']
lines += ['| '+' | '.join(str(r[k]) for k in ('sign_idx','lord','years','start_age_years','end_age_years'))+' |' for r in rows]
lines += ['', *result['limits']]
return dict(status=result['status'],customer_export_admitted=False,
evidence_appendix=[result],exports=dict(markdown='\n'.join(lines),ai_evidence_bundle=deepcopy(result)))