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