#!/usr/bin/env python3 """Source-bounded Shattrimshatsama (36-year) Mahadasha producer.""" from __future__ import annotations from datetime import datetime, timedelta from typing import Any YEAR_DAYS = 365.25636 NAKSHATRA_SPAN = 360.0 / 27.0 SRAVANA_INDEX = 21 TOTAL_CYCLE = 36 DASHA_SEQUENCE = ( ("Moon", 1), ("Sun", 2), ("Jupiter", 3), ("Mars", 4), ("Mercury", 5), ("Saturn", 6), ("Venus", 7), ("Rahu", 8), ) _YEARS = dict(DASHA_SEQUENCE) def shattrimshatsama_applicability(*, birth_is_daytime: bool, ascendant_hora_lord: str) -> dict[str, Any]: """Apply the day/Sun-Hora or night/Moon-Hora condition.""" hora_lord = str(ascendant_hora_lord).strip().title() applicable = (bool(birth_is_daytime) and hora_lord == "Sun") or ( not bool(birth_is_daytime) and hora_lord == "Moon" ) return { "applicable": applicable, "birth_is_daytime": bool(birth_is_daytime), "ascendant_hora_lord": hora_lord, "rule_family": "bphs_shattrimshatsama_day_sun_hora_or_night_moon_hora", "reason": ( "Birth matches the day/Sun-Hora or night/Moon-Hora condition." if applicable else "Shattrimshatsama requires day birth with Sun Hora, or night birth with Moon Hora." ), } def _sequence_from(lord: str) -> list[tuple[str, int]]: start = next(index for index, (item, _years) in enumerate(DASHA_SEQUENCE) if item == lord) return list(DASHA_SEQUENCE[start:] + DASHA_SEQUENCE[:start]) def _birth_lord_and_balance(moon_longitude: float) -> tuple[str, float, int]: moon = float(moon_longitude) % 360.0 nakshatra = int(moon // NAKSHATRA_SPAN) lord = DASHA_SEQUENCE[((nakshatra - SRAVANA_INDEX) % 27) % len(DASHA_SEQUENCE)][0] balance = _YEARS[lord] * (1.0 - (moon % NAKSHATRA_SPAN) / NAKSHATRA_SPAN) return lord, balance, nakshatra def calculate_shattrimshatsama_dasha(birth_info: dict[str, Any]) -> dict[str, Any]: """Return birth-forward MD rows; child-period boundaries stay blocked.""" required = ("birth_datetime", "birth_is_daytime", "ascendant_hora_lord", "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": [], } birth = birth_info["birth_datetime"] if isinstance(birth, str): birth = datetime.fromisoformat(birth) applicability = shattrimshatsama_applicability( birth_is_daytime=bool(birth_info["birth_is_daytime"]), ascendant_hora_lord=str(birth_info["ascendant_hora_lord"]), ) if not applicability["applicable"]: return { **applicability, "execution_status": "not_applicable", "confidence_status": "not_applicable", "total_cycle": TOTAL_CYCLE, "major": [], } year_days = float(birth_info.get("dasha_year_days") or YEAR_DAYS) lord, balance, nakshatra = _birth_lord_and_balance(float(birth_info["moon_longitude"])) cursor = birth rows = [] for index, (row_lord, full_years) in enumerate(_sequence_from(lord)): years = balance if index == 0 else float(full_years) end = cursor + timedelta(days=years * year_days) rows.append( { "level": "mahadasha", "lord": row_lord, "planet": row_lord, "years": years, "full_years": full_years, "is_birth_balance": index == 0, "start_date": cursor.isoformat(timespec="seconds"), "end_date": end.isoformat(timespec="seconds"), } ) cursor = end return { **applicability, "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", "total_cycle": TOTAL_CYCLE, "dasha_year_days": year_days, "moon_nakshatra_index": nakshatra, "starting_lord": lord, "dasha_balance_at_birth_years": balance, "major": rows, "reason": "Local source-bounded MD calculation; AD/PD are intentionally blocked pending row-level parity and runtime wiring.", "source_locator": "BPHS conditional Dasha passages: day/Sun-Hora or night/Moon-Hora gate, Sravana count, Moon-to-Rahu order, and 1-8 year allotments.", }