Files
Jyotisha/scripts/punarphoo.py
T
Jesse_ChenandCursor 04ad3325d5
Independent Staging Quality Gate / validate (push) Successful in 11m26s
Independent Staging Quality Gate / publish (push) Successful in 13m24s
fix(relationship): freeze marriage event classes and add conditional dashas (BUG-608)
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>
2026-09-09 16:04:52 +08:00

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,
}