Files
Jyotisha/scripts/ashtottari_dasha.py
T
Jesse_Chen f224146348
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled
feat(upstream): merge a6f47abd engine, MCP, and orchestrator
Three-way-merge calculation modules and pl9-export into the product fork while keeping commercial API routes, Raman ayanamsa, and the consultation contract as a keypath superset.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-03 20:28:57 +08:00

524 lines
19 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
ashtottari_dasha.py — Ashtottari Dasha System (108-Year Cycle)
Reference: Brihat Parashara Hora Shastra (BPHS), Chapter 19
Ashtottari Dasha is a conditional dasha system with a total cycle of 108 years.
It is applicable only when the Moon is in specific Nakshatras:
- Krishna Paksha (waning Moon): Rohini, Ardra, Pushya, Ashlesha, Magha, Revati
- Shukla Paksha (waxing Moon): Ashwini, Mrigashira, Punarvasu, Chitra, Shravana, Dhanishta
The 8 planetary lords and their year allotments:
1. Sun — 6 years
2. Moon — 15 years
3. Mars — 8 years
4. Mercury — 17 years
5. Saturn — 10 years
6. Jupiter — 19 years
7. Rahu — 12 years
8. Venus — 21 years
Total: 6 + 15 + 8 + 17 + 10 + 19 + 12 + 21 = 108 years.
The dasha sequence always runs: Sun → Moon → Mars → Mercury → Saturn → Jupiter → Rahu → Venus.
The starting lord is determined by the Moon's Nakshatra at birth.
"""
from datetime import datetime, timedelta
from ashtottari_rule_profiles import (
RULE_FAMILY_MOON_NAKSHATRA_PLUS_PAKSHA,
RULE_FAMILY_RAHU_FROM_LAGNA_LORD,
SHUKLA_PAKSHA_NAKSHATRAS,
KRISHNA_PAKSHA_NAKSHATRAS,
evaluate_moon_nakshatra_plus_paksha_rule,
evaluate_rahu_from_lagna_lord_rule,
)
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
# Nakshatra indices (0-based): Ashwini=0, Bharani=1, ..., Revati=26
NAKSHATRAS = [
"Ashwini", "Bharani", "Krittika", "Rohini", "Mrigashira", "Ardra",
"Punarvasu", "Pushya", "Ashlesha", "Magha", "Purva Phalguni",
"Uttara Phalguni", "Hasta", "Chitra", "Swati", "Vishakha",
"Anuradha", "Jyeshtha", "Mula", "Purva Ashadha", "Uttara Ashadha",
"Shravana", "Dhanishta", "Shatabhisha", "Purva Bhadrapada",
"Uttara Bhadrapada", "Revati"
]
# Candidate 28-nakshatra cycle used by the PL9 sample-admitted path.
# This preserves the sample's Mula -> Mercury balance note while leaving the
# legacy Moon-whitelist rule available for research comparison.
_CANDIDATE_NAKSHATRA_STARTING_LORDS = {
"Ardra": "Sun",
"Punarvasu": "Sun",
"Pushya": "Sun",
"Ashlesha": "Sun",
"Magha": "Moon",
"Purva Phalguni": "Moon",
"Uttara Phalguni": "Moon",
"Hasta": "Mars",
"Chitra": "Mars",
"Swati": "Mars",
"Vishakha": "Mars",
"Anuradha": "Mercury",
"Jyeshtha": "Mercury",
"Mula": "Mercury",
"Purva Ashadha": "Saturn",
"Uttara Ashadha": "Saturn",
"Shravana": "Saturn",
"Dhanishta": "Jupiter",
"Shatabhisha": "Jupiter",
"Purva Bhadrapada": "Jupiter",
"Uttara Bhadrapada": "Rahu",
"Revati": "Rahu",
"Ashwini": "Rahu",
"Bharani": "Rahu",
"Krittika": "Venus",
"Rohini": "Venus",
"Mrigashira": "Venus",
}
# Ashtottari planetary sequence and year allotments (BPHS Ch.19)
DASHA_SEQUENCE = [
{"planet": "Sun", "years": 6},
{"planet": "Moon", "years": 15},
{"planet": "Mars", "years": 8},
{"planet": "Mercury", "years": 17},
{"planet": "Saturn", "years": 10},
{"planet": "Jupiter", "years": 19},
{"planet": "Rahu", "years": 12},
{"planet": "Venus", "years": 21},
]
TOTAL_CYCLE = 108 # years
_PLANET_ABBR = {
"Sun": "Su",
"Moon": "Mo",
"Mars": "Ma",
"Mercury": "Me",
"Saturn": "Sa",
"Jupiter": "Ju",
"Rahu": "Ra",
"Venus": "Ve",
}
# Mapping from starting Nakshatra index to first dasha lord index in DASHA_SEQUENCE
# Per BPHS Ch.19:
# - Rohini (3), Ardra (5), Pushya (7), Ashlesha (8), Magha (9), Revati (26)
# → start from Sun (0)
# - Ashwini (0), Mrigashira (4), Punarvasu (6)
# → start from Mars (2)
# - Chitra (13), Shravana (20), Dhanishta (22)
# → start from Jupiter (5)
STARTING_LORD_MAP = {
3: 0, # Rohini -> Sun
5: 0, # Ardra -> Sun
7: 0, # Pushya -> Sun
8: 0, # Ashlesha -> Sun
9: 0, # Magha -> Sun
26: 0, # Revati -> Sun
0: 2, # Ashwini -> Mars
4: 2, # Mrigashira -> Mars
6: 2, # Punarvasu -> Mars
13: 5, # Chitra -> Jupiter
20: 5, # Shravana -> Jupiter
22: 5, # Dhanishta -> Jupiter
}
# ---------------------------------------------------------------------------
# Core Functions
# ---------------------------------------------------------------------------
def is_ashtottari_applicable(moon_nakshatra_index: int, is_shukla_paksha: bool) -> bool:
"""
Check whether Ashtottari Dasha is applicable for the given birth conditions.
Args:
moon_nakshatra_index: 0-based index of Moon's Nakshatra (0 = Ashwini).
is_shukla_paksha: True if birth is in Shukla Paksha (waxing Moon), else False.
Returns:
bool: True if Ashtottari Dasha applies.
"""
applicable, _ = evaluate_moon_nakshatra_plus_paksha_rule(moon_nakshatra_index, is_shukla_paksha)
return applicable
def _moon_nakshatra_name(moon_nakshatra_index: int) -> str:
if moon_nakshatra_index is None:
return "unknown"
try:
return NAKSHATRAS[moon_nakshatra_index]
except Exception:
return "unknown"
def _candidate_nakshatra_starting_lord(moon_nakshatra_index: int) -> str:
return _CANDIDATE_NAKSHATRA_STARTING_LORDS.get(_moon_nakshatra_name(moon_nakshatra_index), "Sun")
def _candidate_lord_nakshatra_span(starting_planet: str) -> tuple[int, int] | tuple[None, None]:
matching = [
idx for idx, name in enumerate(NAKSHATRAS)
if _CANDIDATE_NAKSHATRA_STARTING_LORDS.get(name) == starting_planet
]
if not matching:
return None, None
return matching[0], matching[-1]
def _years_to_balance_parts(years: float) -> dict:
total_days = max(years, 0.0) * 365.256364
whole_years = int(total_days // 365.256364)
remaining_days = total_days - whole_years * 365.256364
whole_months = int(remaining_days // 30.0)
residual_days = remaining_days - whole_months * 30.0
day_count = int(round(residual_days))
if residual_days > 0:
day_count += 1
if day_count >= 30:
whole_months += day_count // 30
day_count = day_count % 30
if whole_months >= 12:
whole_years += whole_months // 12
whole_months = whole_months % 12
return {
"years": years,
"years_whole": whole_years,
"months_whole": whole_months,
"days_whole": day_count,
}
def _years_to_balance_parts_360(years: float) -> dict:
total_days = max(years, 0.0) * 360.0
whole_years = int(total_days // 360.0)
remaining_days = total_days - whole_years * 360.0
whole_months = int(remaining_days // 30.0)
residual_days = remaining_days - whole_months * 30.0
day_count = int(round(residual_days))
if residual_days > 0:
day_count += 1
if day_count >= 30:
whole_months += day_count // 30
day_count = day_count % 30
if whole_months >= 12:
whole_years += whole_months // 12
whole_months = whole_months % 12
return {
"years": years,
"years_whole": whole_years,
"months_whole": whole_months,
"days_whole": day_count,
}
def _calc_candidate_balance_and_birth_chain(moon_longitude: float, starting_planet: str) -> tuple[dict | None, dict | None]:
start_nak_idx, end_nak_idx = _candidate_lord_nakshatra_span(starting_planet)
if start_nak_idx is None or end_nak_idx is None:
return None, None
one_star = 360.0 / 27.0
segment_start = start_nak_idx * one_star
segment_end = (end_nak_idx + 1) * one_star
longitude = float(moon_longitude) % 360.0
if not (segment_start <= longitude <= segment_end):
return None, None
span = segment_end - segment_start
elapsed = longitude - segment_start
remaining_ratio = max(0.0, min(1.0, (segment_end - longitude) / span))
md_years = next(lord["years"] for lord in DASHA_SEQUENCE if lord["planet"] == starting_planet)
remaining_years = md_years * remaining_ratio
def _sequence_from(lord: str) -> list[str]:
idx = next(i for i, item in enumerate(DASHA_SEQUENCE) if item["planet"] == lord)
planets = [item["planet"] for item in DASHA_SEQUENCE]
return planets[idx:] + planets[:idx]
def _child_periods(parent_lord: str, parent_duration_years: float) -> list[dict]:
rows = []
for child_lord in _sequence_from(parent_lord):
child_years = parent_duration_years * next(
item["years"] for item in DASHA_SEQUENCE if item["planet"] == child_lord
) / TOTAL_CYCLE
rows.append({"lord": child_lord, "years": child_years})
return rows
elapsed_in_md = md_years - remaining_years
hierarchy = [starting_planet]
parent_lord = starting_planet
parent_duration = md_years
elapsed_within_parent = elapsed_in_md
for _depth in range(4):
rows = _child_periods(parent_lord, parent_duration)
cursor = 0.0
selected = rows[-1]
for row in rows:
next_cursor = cursor + row["years"]
if cursor <= elapsed_within_parent < next_cursor:
selected = row
elapsed_within_parent = elapsed_within_parent - cursor
break
cursor = next_cursor
hierarchy.append(selected["lord"])
parent_lord = selected["lord"]
parent_duration = selected["years"]
balance = {
"planet": starting_planet,
"segment_start_nakshatra": NAKSHATRAS[start_nak_idx],
"segment_end_nakshatra": NAKSHATRAS[end_nak_idx],
"segment_span_nakshatras": end_nak_idx - start_nak_idx + 1,
"display_calendar": "360_day_traditional",
**_years_to_balance_parts_360(remaining_years),
}
birth_chain = {
"lords": hierarchy,
"compact": "-".join(_PLANET_ABBR.get(lord, lord[:2]) for lord in hierarchy),
}
return balance, birth_chain
def _build_major_periods(start_lord_idx: int, birth_date: datetime) -> list:
"""
Build the full list of major periods (Mahadashas) given the starting lord.
Returns a list of dicts with keys: planet, years, start_date, end_date.
"""
periods = []
seq_len = len(DASHA_SEQUENCE)
current_date = birth_date
for i in range(seq_len):
idx = (start_lord_idx + i) % seq_len
lord = DASHA_SEQUENCE[idx]
end_date = current_date + timedelta(days=lord["years"] * 365.25)
periods.append({
"planet": lord["planet"],
"years": lord["years"],
"start_date": current_date.isoformat(),
"end_date": end_date.isoformat(),
})
current_date = end_date
return periods
def _sequence_from_lord(starting_planet: str) -> list[dict]:
start_idx = next(
idx for idx, lord in enumerate(DASHA_SEQUENCE) if lord["planet"] == starting_planet
)
return DASHA_SEQUENCE[start_idx:] + DASHA_SEQUENCE[:start_idx]
def _build_subperiods(parent_period: dict, level_key: str) -> list[dict]:
start_dt = datetime.fromisoformat(parent_period["start_date"])
current_dt = start_dt
child_periods = []
for lord in _sequence_from_lord(parent_period["planet"]):
child_years = float(parent_period["years"]) * float(lord["years"]) / float(TOTAL_CYCLE)
end_dt = current_dt + timedelta(days=child_years * 365.25)
child_periods.append({
"level": level_key,
"planet": lord["planet"],
"lord": lord["planet"],
"years": child_years,
"start_date": current_dt.isoformat(),
"end_date": end_dt.isoformat(),
"status": "parameter_sensitive",
"derivation": "native_ashtottari_recursive_proportional_variant",
})
current_dt = end_dt
if child_periods:
child_periods[-1]["end_date"] = parent_period["end_date"]
return child_periods
def _attach_recursive_subperiods(major_periods: list[dict]) -> tuple[list[dict], list[dict]]:
all_antardashas = []
all_pratyantardashas = []
for period in major_periods:
antardasha = _build_subperiods(period, "antardasha")
period["antardasha"] = antardasha
all_antardashas.extend(antardasha)
for child in antardasha:
pratyantardasha = _build_subperiods(child, "pratyantardasha")
child["pratyantardasha"] = pratyantardasha
all_pratyantardashas.extend(pratyantardasha)
return all_antardashas, all_pratyantardashas
def calculate_ashtottari_dasha(birth_info: dict) -> dict:
"""
Calculate Ashtottari Dasha for a native.
Args:
birth_info: dict containing:
- "moon_nakshatra_index": int (0-26)
- "is_shukla_paksha": bool
- "birth_datetime": datetime object or ISO-format string
Returns:
dict with keys:
- "applicable": bool — whether this dasha system applies
- "major": list of major period dicts (planet, years, start/end dates)
- "current": dict — the currently running major period
- "total_cycle": int — 108
- "starting_planet": str — first dasha lord
"""
moon_idx = birth_info.get("moon_nakshatra_index")
is_shukla = birth_info.get("is_shukla_paksha", True)
birth_dt = birth_info.get("birth_datetime")
moon_longitude = birth_info.get("moon_longitude")
rule_family = birth_info.get("applicability_rule_family") or RULE_FAMILY_MOON_NAKSHATRA_PLUS_PAKSHA
if isinstance(birth_dt, str):
birth_dt = datetime.fromisoformat(birth_dt)
if birth_dt is None:
birth_dt = datetime.now()
if rule_family == RULE_FAMILY_RAHU_FROM_LAGNA_LORD:
applicable, applicability_reason = evaluate_rahu_from_lagna_lord_rule(birth_info)
starting_planet = _candidate_nakshatra_starting_lord(moon_idx)
else:
applicable, applicability_reason = evaluate_moon_nakshatra_plus_paksha_rule(moon_idx, is_shukla)
starting_planet = None
if not applicable:
result = {
"applicable": False,
"major": [],
"current": None,
"total_cycle": TOTAL_CYCLE,
"starting_planet": None,
"reason": f"Moon Nakshatra '{NAKSHATRAS[moon_idx]}' does not qualify for Ashtottari Dasha under {'Shukla' if is_shukla else 'Krishna'} Paksha.",
}
if rule_family == RULE_FAMILY_RAHU_FROM_LAGNA_LORD:
result.update({
"execution_status": "blocked",
"confidence_status": "blocked",
"verification_status": "unverified",
})
return result
if starting_planet is None:
start_lord_idx = STARTING_LORD_MAP.get(moon_idx, 0)
starting_planet = DASHA_SEQUENCE[start_lord_idx]["planet"]
else:
start_lord_idx = next(
idx for idx, lord in enumerate(DASHA_SEQUENCE) if lord["planet"] == starting_planet
)
major_periods = _build_major_periods(start_lord_idx, birth_dt)
antardasha_periods, pratyantardasha_periods = _attach_recursive_subperiods(major_periods)
balance_at_birth = None
dasha_at_birth = None
if moon_longitude is not None and rule_family == RULE_FAMILY_RAHU_FROM_LAGNA_LORD:
balance_at_birth, dasha_at_birth = _calc_candidate_balance_and_birth_chain(
moon_longitude=float(moon_longitude),
starting_planet=starting_planet,
)
# Determine current period. Major periods repeat every 108 years; older natives
# should still return a current period instead of None after the first cycle.
now = datetime.now()
current_period = None
age_years = max((now - birth_dt).days / 365.25, 0)
current_in_cycle = age_years % TOTAL_CYCLE
cumulative = 0.0
for p in major_periods:
years = p["years"]
if cumulative <= current_in_cycle < cumulative + years:
cycle_start = birth_dt + timedelta(days=(age_years - current_in_cycle + cumulative) * 365.25)
cycle_end = cycle_start + timedelta(days=years * 365.25)
current_period = p.copy()
current_period["start_date"] = cycle_start.isoformat()
current_period["end_date"] = cycle_end.isoformat()
current_period["elapsed_years"] = (now - cycle_start).days / 365.25
current_period["remaining_years"] = (cycle_end - now).days / 365.25
current_period["cycle_number"] = int(age_years // TOTAL_CYCLE) + 1
break
cumulative += years
result = {
"applicable": True,
"major": major_periods,
"antardasha": antardasha_periods,
"pratyantardasha": pratyantardasha_periods,
"current": current_period,
"total_cycle": TOTAL_CYCLE,
"starting_planet": starting_planet,
}
if rule_family == RULE_FAMILY_RAHU_FROM_LAGNA_LORD:
result.update({
"execution_status": "executed",
"confidence_status": "parameter_sensitive",
"verification_status": "unverified",
})
return result
# ---------------------------------------------------------------------------
# Self-test
# ---------------------------------------------------------------------------
if __name__ == "__main__":
# Example 1: Applicable case — Moon in Rohini, Krishna Paksha
test_birth_1 = {
"moon_nakshatra_index": 3, # Rohini
"is_shukla_paksha": False, # Krishna Paksha
"birth_datetime": datetime(1990, 6, 15, 10, 30),
}
result_1 = calculate_ashtottari_dasha(test_birth_1)
print("=" * 60)
print("Test 1: Moon in Rohini, Krishna Paksha")
print("=" * 60)
print(f"Applicable: {result_1['applicable']}")
print(f"Starting Planet: {result_1['starting_planet']}")
print(f"Total Cycle: {result_1['total_cycle']} years")
print("Major Periods:")
for p in result_1["major"]:
print(f" {p['planet']:10s} | {p['years']:2d} years | {p['start_date'][:10]}{p['end_date'][:10]}")
print(f"Current Period: {result_1['current']}")
print()
# Example 2: Applicable case — Moon in Ashwini, Shukla Paksha
test_birth_2 = {
"moon_nakshatra_index": 0, # Ashwini
"is_shukla_paksha": True, # Shukla Paksha
"birth_datetime": datetime(1985, 3, 20, 8, 0),
}
result_2 = calculate_ashtottari_dasha(test_birth_2)
print("=" * 60)
print("Test 2: Moon in Ashwini, Shukla Paksha")
print("=" * 60)
print(f"Applicable: {result_2['applicable']}")
print(f"Starting Planet: {result_2['starting_planet']}")
print(f"Total Cycle: {result_2['total_cycle']} years")
for p in result_2["major"]:
print(f" {p['planet']:10s} | {p['years']:2d} years | {p['start_date'][:10]}{p['end_date'][:10]}")
print(f"Current Period: {result_2['current']}")
print()
# Example 3: Non-applicable case — Moon in Bharani, Shukla Paksha
test_birth_3 = {
"moon_nakshatra_index": 1, # Bharani
"is_shukla_paksha": True,
"birth_datetime": datetime(2000, 1, 1, 0, 0),
}
result_3 = calculate_ashtottari_dasha(test_birth_3)
print("=" * 60)
print("Test 3: Moon in Bharani, Shukla Paksha (Non-applicable)")
print("=" * 60)
print(f"Applicable: {result_3['applicable']}")
print(f"Reason: {result_3['reason']}")