Venus/Jupiter/Moon periods no longer collapse into one marriage-opportunity line. Full reading reports the ten BPHS conditional dasha families; Skill 6.9.16. Co-authored-by: Cursor <cursoragent@cursor.com>
121 lines
5.6 KiB
Python
121 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Source-bounded Satabdika (100-year) Mahadasha producer.
|
|
|
|
Satabdika is conditional on a vargottama Ascendant. The caller supplies the
|
|
D9 Ascendant sign explicitly so this module never guesses a divisional method.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timedelta
|
|
from typing import Any
|
|
|
|
try:
|
|
from scripts.source_bounded_child_periods import expand_parent_seeded_equal_split_children
|
|
except ModuleNotFoundError: # pragma: no cover - scripts/ on sys.path
|
|
from source_bounded_child_periods import expand_parent_seeded_equal_split_children
|
|
|
|
|
|
YEAR_DAYS = 365.25636
|
|
NAKSHATRA_SPAN = 360.0 / 27.0
|
|
REVATI_INDEX = 26
|
|
TOTAL_CYCLE = 100
|
|
DASHA_SEQUENCE = (
|
|
("Sun", 5), ("Moon", 5), ("Venus", 10), ("Mercury", 10),
|
|
("Jupiter", 20), ("Mars", 20), ("Saturn", 30),
|
|
)
|
|
_YEARS = dict(DASHA_SEQUENCE)
|
|
|
|
|
|
def _sign_index(longitude: float) -> int:
|
|
return int(float(longitude) % 360.0 // 30.0)
|
|
|
|
|
|
def satabdika_applicability(*, ascendant_longitude: float, navamsa_ascendant_sign_index: int) -> dict[str, Any]:
|
|
rasi = _sign_index(ascendant_longitude)
|
|
navamsa = int(navamsa_ascendant_sign_index) % 12
|
|
applicable = rasi == navamsa
|
|
return {
|
|
"applicable": applicable,
|
|
"ascendant_rasi_sign_index": rasi,
|
|
"ascendant_navamsa_sign_index": navamsa,
|
|
"rule_family": "bphs_satabdika_vargottama_lagna",
|
|
"reason": "Ascendant is vargottama." if applicable else "Satabdika requires a vargottama Ascendant (same Rasi and Navamsa sign).",
|
|
}
|
|
|
|
|
|
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 - REVATI_INDEX) % 27) % len(DASHA_SEQUENCE)][0]
|
|
balance = _YEARS[lord] * (1.0 - (moon % NAKSHATRA_SPAN) / NAKSHATRA_SPAN)
|
|
return lord, balance, nakshatra
|
|
|
|
|
|
def calculate_satabdika_dasha(birth_info: dict[str, Any]) -> dict[str, Any]:
|
|
"""Return birth-forward MD rows; child-period boundaries stay blocked."""
|
|
required = ("birth_datetime", "ascendant_longitude", "navamsa_ascendant_sign_index", "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 = satabdika_applicability(
|
|
ascendant_longitude=float(birth_info["ascendant_longitude"]),
|
|
navamsa_ascendant_sign_index=int(birth_info["navamsa_ascendant_sign_index"]),
|
|
)
|
|
if not applicability["applicable"]:
|
|
if birth_info.get("standard_table_profile") == "pl9_reports_all_dasha_tables_v1":
|
|
applicability = {
|
|
**applicability,
|
|
"classical_applicable": False,
|
|
"applicable": True,
|
|
"standard_table_profile": "pl9_reports_all_dasha_tables_v1",
|
|
"applicability_reason": applicability["reason"],
|
|
"reason": (
|
|
"PL9 standard-table profile requested: generate the report table while preserving "
|
|
"the unmet classical applicability gate in the audit fields."
|
|
),
|
|
}
|
|
else:
|
|
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
|
|
child_profile = birth_info.get("child_period_profile")
|
|
rows, antardasha, pratyantardasha, period_depth_status = expand_parent_seeded_equal_split_children(
|
|
rows,
|
|
DASHA_SEQUENCE,
|
|
profile=str(child_profile) if child_profile else None,
|
|
)
|
|
return {
|
|
**applicability, "execution_status": "executed", "confidence_status": "parameter_sensitive",
|
|
"verification_status": "source_bounded_local_candidate",
|
|
"period_depth_status": period_depth_status,
|
|
"total_cycle": TOTAL_CYCLE, "dasha_year_days": year_days, "moon_nakshatra_index": nakshatra,
|
|
"starting_lord": lord, "dasha_balance_at_birth_years": balance, "child_period_profile": child_profile or None,
|
|
"major": rows, "antardasha": antardasha, "pratyantardasha": pratyantardasha,
|
|
"reason": (
|
|
"Local source-bounded MD calculation with optional PyJHora-observed child-period profile."
|
|
if child_profile else
|
|
"Local source-bounded MD calculation; AD/PD are intentionally blocked pending row-level parity and runtime wiring."
|
|
),
|
|
"source_locator": "BPHS conditional Dasha passages: vargottama condition, Revati count, lord order, and 5/5/10/10/20/20/30 year allotments.",
|
|
}
|