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>
240 lines
10 KiB
Python
240 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""Source-bounded Shastihayani (60-year) Mahadasha producer.
|
|
|
|
This conditional Dasha uses a 28-nakshatra scheme that includes Abhijit and
|
|
alternates 3-star and 4-star groups. It derives the 28-star group position
|
|
from Moon longitude when explicit group inputs are not supplied.
|
|
"""
|
|
|
|
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
|
|
TOTAL_CYCLE = 60
|
|
GROUPS = (
|
|
{"lord": "Jupiter", "years": 10, "size": 3},
|
|
{"lord": "Sun", "years": 10, "size": 4},
|
|
{"lord": "Mars", "years": 10, "size": 3},
|
|
{"lord": "Moon", "years": 6, "size": 4},
|
|
{"lord": "Mercury", "years": 6, "size": 3},
|
|
{"lord": "Venus", "years": 6, "size": 4},
|
|
{"lord": "Saturn", "years": 6, "size": 3},
|
|
{"lord": "Rahu", "years": 6, "size": 4},
|
|
)
|
|
DASHA_SEQUENCE = tuple((str(group["lord"]), int(group["years"])) for group in GROUPS)
|
|
|
|
_STAR_SPANS = (
|
|
("Ashwini", 0.0, 13.3333333333, 0, 0),
|
|
("Bharani", 13.3333333333, 26.6666666667, 0, 1),
|
|
("Krittika", 26.6666666667, 40.0, 0, 2),
|
|
("Rohini", 40.0, 53.3333333333, 1, 0),
|
|
("Mrigashira", 53.3333333333, 66.6666666667, 1, 1),
|
|
("Ardra", 66.6666666667, 80.0, 1, 2),
|
|
("Punarvasu", 80.0, 93.3333333333, 1, 3),
|
|
("Pushya", 93.3333333333, 106.6666666667, 2, 0),
|
|
("Ashlesha", 106.6666666667, 120.0, 2, 1),
|
|
("Magha", 120.0, 133.3333333333, 2, 2),
|
|
("Purva Phalguni", 133.3333333333, 146.6666666667, 3, 0),
|
|
("Uttara Phalguni", 146.6666666667, 160.0, 3, 1),
|
|
("Hasta", 160.0, 173.3333333333, 3, 2),
|
|
("Chitra", 173.3333333333, 186.6666666667, 3, 3),
|
|
("Swati", 186.6666666667, 200.0, 4, 0),
|
|
("Vishakha", 200.0, 213.3333333333, 4, 1),
|
|
("Anuradha", 213.3333333333, 226.6666666667, 4, 2),
|
|
("Jyeshtha", 226.6666666667, 240.0, 5, 0),
|
|
("Mula", 240.0, 253.3333333333, 5, 1),
|
|
("Purva Ashadha", 253.3333333333, 266.6666666667, 5, 2),
|
|
("Uttara Ashadha", 266.6666666667, 276.6666666667, 5, 3),
|
|
("Abhijit", 276.6666666667, 280.8888888889, 6, 0),
|
|
("Shravana", 280.8888888889, 293.3333333333, 6, 1),
|
|
("Dhanishta", 293.3333333333, 306.6666666667, 6, 2),
|
|
("Shatabhisha", 306.6666666667, 320.0, 7, 0),
|
|
("Purva Bhadrapada", 320.0, 333.3333333333, 7, 1),
|
|
("Uttara Bhadrapada", 333.3333333333, 346.6666666667, 7, 2),
|
|
("Revati", 346.6666666667, 360.0, 7, 3),
|
|
)
|
|
|
|
|
|
def _sign_index(longitude: float) -> int:
|
|
return int(float(longitude) % 360.0 // 30.0)
|
|
|
|
|
|
def shastihayani_applicability(*, ascendant_longitude: float, sun_longitude: float) -> dict[str, Any]:
|
|
ascendant_sign = _sign_index(ascendant_longitude)
|
|
sun_sign = _sign_index(sun_longitude)
|
|
applicable = ascendant_sign == sun_sign
|
|
return {
|
|
"applicable": applicable,
|
|
"ascendant_sign_index": ascendant_sign,
|
|
"sun_sign_index": sun_sign,
|
|
"rule_family": "bphs_shastihayani_sun_in_lagna",
|
|
"reason": (
|
|
"Sun is in the Lagna sign."
|
|
if applicable
|
|
else "Shastihayani requires Sun in the Lagna sign."
|
|
),
|
|
}
|
|
|
|
|
|
def _sequence_from(group_index: int) -> list[dict[str, Any]]:
|
|
group_index %= len(GROUPS)
|
|
return list(GROUPS[group_index:] + GROUPS[:group_index])
|
|
|
|
|
|
def resolve_shastihayani_28_position(moon_longitude: float) -> dict[str, Any]:
|
|
"""Resolve the source-bounded 28-star position used by Shastihayani."""
|
|
longitude = float(moon_longitude) % 360.0
|
|
for star_name, start, end, group_index, within_group in _STAR_SPANS:
|
|
if start <= longitude < end or (end == 360.0 and longitude == 0.0):
|
|
fraction = (longitude - start) / (end - start)
|
|
return {
|
|
"moon_longitude": longitude,
|
|
"nakshatra": star_name,
|
|
"span_start_longitude": start,
|
|
"span_end_longitude": end,
|
|
"shastihayani_group_index": group_index,
|
|
"nakshatra_index_within_group": within_group,
|
|
"nakshatra_fraction_elapsed": fraction,
|
|
"source_locator": (
|
|
"Predicting Through Shasti Hayani Dasha: 28 nakshatras including Abhijit; "
|
|
"Uttara Ashadha curtailed to Capricorn 6°40, Abhijit spans Capricorn "
|
|
"6°40 to 10°53'20, and Shravana starts from Capricorn 10°53'20."
|
|
),
|
|
}
|
|
raise ValueError("moon_longitude could not be resolved into Shastihayani 28-star spans")
|
|
|
|
|
|
def _birth_lord_and_balance(
|
|
*, group_index: int, nakshatra_index_within_group: int, nakshatra_fraction_elapsed: float
|
|
) -> tuple[str, float]:
|
|
group = GROUPS[int(group_index) % len(GROUPS)]
|
|
within = int(nakshatra_index_within_group)
|
|
if not 0 <= within < int(group["size"]):
|
|
raise ValueError("nakshatra_index_within_group must be inside the selected Shastihayani group")
|
|
fraction = float(nakshatra_fraction_elapsed)
|
|
if not 0.0 <= fraction <= 1.0:
|
|
raise ValueError("nakshatra_fraction_elapsed must be between 0 and 1")
|
|
years_per_nakshatra = float(group["years"]) / int(group["size"])
|
|
remaining_current = years_per_nakshatra * (1.0 - fraction)
|
|
remaining_full_nakshatras = int(group["size"]) - within - 1
|
|
balance_years = remaining_current + remaining_full_nakshatras * years_per_nakshatra
|
|
return str(group["lord"]), balance_years
|
|
|
|
|
|
def calculate_shastihayani_dasha(birth_info: dict[str, Any]) -> dict[str, Any]:
|
|
"""Return birth-forward MD rows; child-period boundaries stay blocked."""
|
|
required = (
|
|
"birth_datetime",
|
|
"ascendant_longitude",
|
|
"sun_longitude",
|
|
"shastihayani_group_index",
|
|
"nakshatra_index_within_group",
|
|
"nakshatra_fraction_elapsed",
|
|
)
|
|
if any(birth_info.get(field) is None for field in (
|
|
"shastihayani_group_index",
|
|
"nakshatra_index_within_group",
|
|
"nakshatra_fraction_elapsed",
|
|
)) and birth_info.get("moon_longitude") is not None:
|
|
birth_info = {
|
|
**birth_info,
|
|
**resolve_shastihayani_28_position(float(birth_info["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 = shastihayani_applicability(
|
|
ascendant_longitude=float(birth_info["ascendant_longitude"]),
|
|
sun_longitude=float(birth_info["sun_longitude"]),
|
|
)
|
|
standard_table_profile = birth_info.get("standard_table_profile")
|
|
emit_standard_table = standard_table_profile == "pl9_reports_all_dasha_tables_v1"
|
|
if not applicability["applicable"] and not emit_standard_table:
|
|
return {
|
|
**applicability,
|
|
"execution_status": "not_applicable",
|
|
"confidence_status": "not_applicable",
|
|
"total_cycle": TOTAL_CYCLE,
|
|
"major": [],
|
|
}
|
|
group_index = int(birth_info["shastihayani_group_index"]) % len(GROUPS)
|
|
try:
|
|
lord, balance = _birth_lord_and_balance(
|
|
group_index=group_index,
|
|
nakshatra_index_within_group=int(birth_info["nakshatra_index_within_group"]),
|
|
nakshatra_fraction_elapsed=float(birth_info["nakshatra_fraction_elapsed"]),
|
|
)
|
|
except ValueError as exc:
|
|
return {
|
|
**applicability,
|
|
"execution_status": "blocked",
|
|
"confidence_status": "blocked",
|
|
"total_cycle": TOTAL_CYCLE,
|
|
"reason": str(exc),
|
|
"major": [],
|
|
}
|
|
year_days = float(birth_info.get("dasha_year_days") or YEAR_DAYS)
|
|
rows = []
|
|
cursor = birth
|
|
for index, group in enumerate(_sequence_from(group_index)):
|
|
years = balance if index == 0 else float(group["years"])
|
|
end = cursor + timedelta(days=years * year_days)
|
|
rows.append(
|
|
{
|
|
"level": "mahadasha",
|
|
"lord": group["lord"],
|
|
"planet": group["lord"],
|
|
"years": years,
|
|
"full_years": group["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, "applicable": True} if emit_standard_table else applicability),
|
|
"execution_status": "executed",
|
|
"confidence_status": "parameter_sensitive",
|
|
"verification_status": "source_bounded_local_candidate",
|
|
"classical_applicability": applicability,
|
|
"standard_table_profile": standard_table_profile or None,
|
|
"period_depth_status": period_depth_status,
|
|
"total_cycle": TOTAL_CYCLE,
|
|
"dasha_year_days": year_days,
|
|
"starting_lord": lord,
|
|
"shastihayani_group_index": group_index,
|
|
"nakshatra_index_within_group": int(birth_info["nakshatra_index_within_group"]),
|
|
"nakshatra_fraction_elapsed": float(birth_info["nakshatra_fraction_elapsed"]),
|
|
"dasha_balance_at_birth_years": balance,
|
|
"child_period_profile": child_profile or None,
|
|
"major": rows,
|
|
"antardasha": antardasha,
|
|
"pratyantardasha": pratyantardasha,
|
|
"reason": "Local source-bounded calculation with 28-nakshatra input; PL9 standard-table mode may emit rows even when the classical Sun-in-Lagna gate is not met.",
|
|
"source_locator": "Shastihayani source passages: Sun-in-Lagna gate, 28-nakshatra groups including Abhijit, 3/4 grouping, Jupiter-to-Rahu order, and 10/10/10/6/6/6/6/6 year allotments.",
|
|
}
|