#!/usr/bin/env python3 """Source-bounded Tribhagi Dasha producer. Tribhagi divides each Vimshottari lord's years across its three nakshatras. The full 27-nakshatra traversal is 120 years; each nine-nakshatra bhaga is 40 years. This module emits birth-forward major rows only. """ from __future__ import annotations from datetime import datetime, timedelta from typing import Any YEAR_DAYS = 365.25636 NAKSHATRA_SPAN = 360.0 / 27.0 VIMSHOTTARI_SEQUENCE = ( ("Ketu", 7), ("Venus", 20), ("Sun", 6), ("Moon", 10), ("Mars", 7), ("Rahu", 18), ("Jupiter", 16), ("Saturn", 19), ("Mercury", 17), ) NAKSHATRA_NAMES = ( "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", "Dhanishtha", "Shatabhisha", "Purva Bhadrapada", "Uttara Bhadrapada", "Revati", ) FULL_CYCLE_YEARS = 120 BHAGA_CYCLE_YEARS = 40 def _nakshatra_index(moon_longitude: float) -> int: return int(float(moon_longitude) % 360.0 // NAKSHATRA_SPAN) def _nakshatra_fraction_elapsed(moon_longitude: float) -> float: return (float(moon_longitude) % NAKSHATRA_SPAN) / NAKSHATRA_SPAN def _lord_for_nakshatra(index: int) -> tuple[str, float]: lord, years = VIMSHOTTARI_SEQUENCE[index % len(VIMSHOTTARI_SEQUENCE)] return lord, years / 3.0 def _cycle_indices(start: int, mode: str) -> list[int]: if mode == "full_120": return [(start + offset) % 27 for offset in range(27)] if mode == "single_bhaga_40": bhaga_start = (start // 9) * 9 return [(start + offset - bhaga_start) % 9 + bhaga_start for offset in range(9)] raise ValueError("mode must be full_120 or single_bhaga_40") def calculate_tribhagi_dasha(birth_info: dict[str, Any]) -> dict[str, Any]: """Return birth-forward Tribhagi rows; child-period boundaries stay blocked.""" required = ("birth_datetime", "moon_longitude") missing = [field for field in required if birth_info.get(field) is None] if missing: return { "applicable": False, "execution_status": "blocked", "confidence_status": "blocked", "reason": "Missing required input: " + ", ".join(missing), "major": [], } mode = str(birth_info.get("mode") or "full_120") try: indices = _cycle_indices(_nakshatra_index(float(birth_info["moon_longitude"])), mode) except ValueError as exc: return { "applicable": False, "execution_status": "blocked", "confidence_status": "blocked", "reason": str(exc), "major": [], } birth = birth_info["birth_datetime"] if isinstance(birth, str): birth = datetime.fromisoformat(birth) year_days = float(birth_info.get("dasha_year_days") or YEAR_DAYS) fraction_elapsed = _nakshatra_fraction_elapsed(float(birth_info["moon_longitude"])) rows = [] cursor = birth for offset, nakshatra_index in enumerate(indices): lord, full_years = _lord_for_nakshatra(nakshatra_index) years = full_years * (1.0 - fraction_elapsed) if offset == 0 else full_years end = cursor + timedelta(days=years * year_days) rows.append( { "level": "mahadasha", "lord": lord, "planet": lord, "nakshatra_index": nakshatra_index, "nakshatra": NAKSHATRA_NAMES[nakshatra_index], "years": years, "full_years": full_years, "is_birth_balance": offset == 0, "start_date": cursor.isoformat(timespec="seconds"), "end_date": end.isoformat(timespec="seconds"), } ) cursor = end return { "schema": "tribhagi_dasha_v1", "applicable": True, "execution_status": "executed", "confidence_status": "parameter_sensitive", "verification_status": "source_bounded_local_candidate", "period_depth_status": "mahadasha_executed_ad_pd_blocked_pending_row_level_replay", "mode": mode, "total_cycle": FULL_CYCLE_YEARS if mode == "full_120" else BHAGA_CYCLE_YEARS, "dasha_year_days": year_days, "starting_nakshatra_index": indices[0], "starting_lord": rows[0]["lord"] if rows else None, "dasha_balance_at_birth_years": rows[0]["years"] if rows else 0.0, "major": rows, "reason": "Local source-bounded Tribhagi MD calculation; AD/PD and PL9 semantic mapping remain blocked pending row-level replay.", "source_locator": "Public Tribhagi explanations: Vimshottari lord years divided across three ruled nakshatras; full 120-year cycle or single 40-year bhaga view.", }