Files
Jyotisha/scripts/ashtottari_dasha.py
T
jesse-uxandGrok (agent loop) 5ac88e58f5 让大运接口走完整解读的同一套算法,不再返回编出来的周期。
Ashtottari 在适用时把出生前已经走过的部分算进时间线。

Co-Authored-By: Grok (agent loop)
2026-10-16 00:52:29 +08:00

574 lines
21 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 _timeline_start_with_birth_balance(
birth_dt: datetime,
moon_longitude,
starting_planet: str,
) -> tuple[datetime, bool]:
"""Shift the first mahadasha so birth falls inside the elapsed portion.
Both applicability families use the starting lord's nakshatra group. When
the Moon longitude is missing, or it does not fall in that group, the
timeline stays on the birth instant.
"""
if moon_longitude is None:
return birth_dt, False
balance, _birth_chain = _calc_candidate_balance_and_birth_chain(
moon_longitude=float(moon_longitude),
starting_planet=starting_planet,
)
if balance is None:
return birth_dt, False
md_years = next(lord["years"] for lord in DASHA_SEQUENCE if lord["planet"] == starting_planet)
elapsed_years = md_years - float(balance["years"])
if elapsed_years == 0:
return birth_dt, False
return birth_dt - timedelta(days=elapsed_years * 365.25), True
def _current_period_from_shifted_majors(major_periods: list, now: datetime) -> dict | None:
if not major_periods:
return None
first = datetime.fromisoformat(major_periods[0]["start_date"])
last = datetime.fromisoformat(major_periods[-1]["end_date"])
span = last - first
if span.total_seconds() <= 0 or now < first:
return None
cycles = int((now - first).total_seconds() // span.total_seconds())
for period in major_periods:
start = datetime.fromisoformat(period["start_date"]) + cycles * span
end = datetime.fromisoformat(period["end_date"]) + cycles * span
if start <= now < end:
current = period.copy()
current["start_date"] = start.isoformat()
current["end_date"] = end.isoformat()
current["elapsed_years"] = (now - start).days / 365.25
current["remaining_years"] = (end - now).days / 365.25
current["cycle_number"] = cycles + 1
return current
return None
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
)
timeline_start, balance_shifted = _timeline_start_with_birth_balance(
birth_dt, moon_longitude, starting_planet,
)
major_periods = _build_major_periods(start_lord_idx, timeline_start)
antardasha_periods, pratyantardasha_periods = _attach_recursive_subperiods(major_periods)
# Determine current period. Major periods repeat every 108 years; older natives
# should still return a current period instead of None after the first cycle.
# A shifted timeline is read from the period dates. An unshifted timeline keeps
# the previous birth-anchored walk so charts without a Moon longitude stay put.
now = datetime.now()
current_period = None
if balance_shifted:
current_period = _current_period_from_shifted_majors(major_periods, now)
else:
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']}")