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>
103 lines
3.5 KiB
Python
103 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""Punarphoo observation detector.
|
|
|
|
Natal rule: Moon-Saturn conjunction with circular orb <= 10°.
|
|
Timing observation: transiting Saturn returning to natal Moon.
|
|
|
|
Claim boundary (hard):
|
|
- This module is observation-only.
|
|
- Punarphoo natal presence or Saturn-return-to-natal-Moon is not marriage.
|
|
- Do not promote to truth / legal_marriage before negative holdout passes.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Dict, Optional
|
|
|
|
|
|
PUNARPHOO_ORB_DEG = 10.0
|
|
SATURN_RETURN_TO_MOON_ORB_DEG = 1.0
|
|
CLAIM_BOUNDARY = (
|
|
"Punarphoo return is not marriage. Observation only; "
|
|
"negative holdout not passed; do not promote to truth."
|
|
)
|
|
|
|
|
|
def circular_degree_gap(a: float, b: float) -> float:
|
|
gap = abs(float(a) - float(b)) % 360.0
|
|
return min(gap, 360.0 - gap)
|
|
|
|
|
|
def _longitude(pdata: Any) -> Optional[float]:
|
|
if not isinstance(pdata, dict):
|
|
return None
|
|
for key in ("longitude", "lon", "degree"):
|
|
value = pdata.get(key)
|
|
if isinstance(value, (int, float)):
|
|
return float(value)
|
|
return None
|
|
|
|
|
|
def detect_natal_punarphoo(planets: Dict[str, Any] | None, orb_limit: float = PUNARPHOO_ORB_DEG) -> Dict[str, Any]:
|
|
planets = planets if isinstance(planets, dict) else {}
|
|
moon = planets.get("Moon") if isinstance(planets.get("Moon"), dict) else {}
|
|
saturn = planets.get("Saturn") if isinstance(planets.get("Saturn"), dict) else {}
|
|
moon_lon = _longitude(moon)
|
|
saturn_lon = _longitude(saturn)
|
|
same_house = moon.get("house") is not None and moon.get("house") == saturn.get("house")
|
|
orb = circular_degree_gap(moon_lon, saturn_lon) if moon_lon is not None and saturn_lon is not None else None
|
|
present = orb is not None and orb <= orb_limit
|
|
return {
|
|
"status": "observation_only",
|
|
"natal_present": present,
|
|
"orb_deg": orb,
|
|
"orb_limit_deg": orb_limit,
|
|
"same_house": bool(same_house),
|
|
"moon_house": moon.get("house"),
|
|
"saturn_house": saturn.get("house"),
|
|
"claim_boundary": CLAIM_BOUNDARY,
|
|
"source": "punarphoo_observation_v1",
|
|
"production_tuning_allowed": False,
|
|
}
|
|
|
|
|
|
def detect_saturn_return_to_natal_moon(
|
|
natal_moon_longitude: Optional[float],
|
|
transit_saturn_longitude: Optional[float],
|
|
orb_limit: float = SATURN_RETURN_TO_MOON_ORB_DEG,
|
|
) -> Dict[str, Any]:
|
|
if natal_moon_longitude is None or transit_saturn_longitude is None:
|
|
return {
|
|
"status": "observation_only",
|
|
"active": False,
|
|
"orb_deg": None,
|
|
"orb_limit_deg": orb_limit,
|
|
"claim_boundary": CLAIM_BOUNDARY,
|
|
"source": "punarphoo_observation_v1",
|
|
}
|
|
orb = circular_degree_gap(natal_moon_longitude, transit_saturn_longitude)
|
|
return {
|
|
"status": "observation_only",
|
|
"active": orb <= orb_limit,
|
|
"orb_deg": orb,
|
|
"orb_limit_deg": orb_limit,
|
|
"claim_boundary": CLAIM_BOUNDARY,
|
|
"source": "punarphoo_observation_v1",
|
|
}
|
|
|
|
|
|
def detect_punarphoo(
|
|
planets: Dict[str, Any] | None,
|
|
transit_saturn_longitude: Optional[float] = None,
|
|
) -> Dict[str, Any]:
|
|
natal = detect_natal_punarphoo(planets)
|
|
moon_lon = _longitude((planets or {}).get("Moon")) if isinstance(planets, dict) else None
|
|
transit = detect_saturn_return_to_natal_moon(moon_lon, transit_saturn_longitude)
|
|
return {
|
|
**natal,
|
|
"saturn_return_to_natal_moon": transit,
|
|
"timing_observation_active": bool(transit.get("active")),
|
|
"claim_boundary": CLAIM_BOUNDARY,
|
|
}
|