#!/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" ] # PVR Integrated Approach Table 39 angular arcs over the 27 named stars. # This profile includes the reduced Saturn arc and circular Rahu arc. # Applicability and child-period profiles remain separate choices. _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 # Table 39 has a circular Rahu arc: Uttara Bhadrapada through Bharani. # min/max would incorrectly turn that four-star arc into the full zodiac. starts = [idx for idx in matching if (idx - 1) % 27 not in matching] if len(starts) != 1: return None, None return starts[0], starts[0] + len(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 segment_start = start_nak_idx * 40.0 / 3.0 segment_end = (end_nak_idx + 1) * 40.0 / 3.0 longitude = float(moon_longitude) % 360.0 if segment_end > 360 and longitude < segment_start: longitude += 360 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 % 27], "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 _shift_period_tree(value, shift: timedelta): """Copy a period tree and move every stored instant by the same cycle shift.""" if isinstance(value, list): return [_shift_period_tree(item, shift) for item in value] if isinstance(value, dict): shifted = {} for key, item in value.items(): if key in ("start_date", "end_date") and isinstance(item, str): shifted[key] = (datetime.fromisoformat(item) + shift).isoformat() else: shifted[key] = _shift_period_tree(item, shift) return shifted return value def _current_period_on_cycle(major_periods: list, birth_dt: datetime, now: datetime) -> dict | None: """Second and later cycles move child dates with the parent. Birth row stays put.""" if not major_periods or now < birth_dt: return None cycle_duration = timedelta(days=TOTAL_CYCLE * 365.25) cycle_index = (now - birth_dt) // cycle_duration shift = cycle_index * cycle_duration for period in major_periods: start = datetime.fromisoformat(period["start_date"]) + shift end = datetime.fromisoformat(period["end_date"]) + shift if start <= now < end: current = _shift_period_tree(period, shift) current["elapsed_years"] = (now - start).total_seconds() / (86400 * 365.25) current["remaining_years"] = (end - now).total_seconds() / (86400 * 365.25) current["cycle_number"] = cycle_index + 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) # Later cycles repeat every 108 years. Child dates move with the parent. # The birth row in major stays on the original timeline. now = birth_info.get("as_of") or birth_info.get("today") or datetime.now() if isinstance(now, str): now = datetime.fromisoformat(now) current_period = _current_period_on_cycle(major_periods, birth_dt, now) result = { "applicable": True, "reference_date": now.isoformat(), "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']}")