Files
Jyotisha/scripts/muhurta_factor_probe.py
2026-07-23 12:03:58 +08:00

209 lines
7.6 KiB
Python

#!/usr/bin/env python3
"""Muhurta factor probe: observation only, no electional verdict."""
from __future__ import annotations
import argparse
import json
from datetime import datetime
from cmd_muhurta import _get_sun_moon_lons, _weekday_to_vara
from muhurta import calc_panchanga, calc_abhijit_muhurta
YAMAGANDA_SEGMENT_BY_WEEKDAY = {
0: 5, # Sunday
1: 4,
2: 3,
3: 2,
4: 1,
5: 7,
6: 6,
}
GULIKA_SEGMENT_BY_WEEKDAY = {
0: 7, # Sunday
1: 6,
2: 5,
3: 4,
4: 3,
5: 2,
6: 1,
}
def _tarabala(birth_moon_nakshatra_index: int | None, current_nakshatra_index: int) -> dict:
if birth_moon_nakshatra_index is None:
return {"status": "missing_birth_moon_nakshatra", "claim": "not_computed"}
tara_pos = ((current_nakshatra_index - birth_moon_nakshatra_index) % 27) + 1
tara_class = ((tara_pos - 1) % 9) + 1
favorable = tara_class in {2, 4, 6, 8, 9}
return {"status": "computed_rule_probe", "tara_position": tara_pos, "tara_class": tara_class, "favorable": favorable}
def _chandrabala(birth_moon_sign_index: int | None, current_moon_lon: float) -> dict:
if birth_moon_sign_index is None:
return {"status": "missing_birth_moon_sign", "claim": "not_computed"}
current_sign = int((current_moon_lon % 360) // 30)
relative_house = ((current_sign - birth_moon_sign_index) % 12) + 1
favorable = relative_house not in {6, 8, 12}
return {"status": "computed_rule_probe", "relative_house": relative_house, "favorable": favorable}
def _segment_window(segment: int, sunrise: str = "06:00", sunset: str = "18:00") -> dict:
def minutes(value: str) -> int:
hour, minute = [int(part) for part in value.split(":", 1)]
return hour * 60 + minute
def hhmm(value: float) -> str:
total = int(round(value)) % (24 * 60)
return f"{total // 60:02d}:{total % 60:02d}"
start_day = minutes(sunrise)
end_day = minutes(sunset)
if end_day <= start_day:
end_day += 24 * 60
length = (end_day - start_day) / 8
start = start_day + (segment - 1) * length
end = start + length
return {"segment": segment, "start": hhmm(start), "end": hhmm(end), "duration_minutes": round(length, 1)}
def _yamaganda(weekday_idx: int) -> dict:
return {
"status": "computed_rule_probe",
"quality": "avoid",
"window": _segment_window(YAMAGANDA_SEGMENT_BY_WEEKDAY[weekday_idx % 7]),
}
def _gulika_kalam(weekday_idx: int) -> dict:
return {
"status": "computed_rule_probe",
"quality": "avoid_or_handle_with_caution",
"window": _segment_window(GULIKA_SEGMENT_BY_WEEKDAY[weekday_idx % 7]),
}
def _panchaka(current_moon_lon: float) -> dict:
sign_index = int((current_moon_lon % 360) // 30)
active = sign_index in {10, 11} # Aquarius/Pisces observation rule
return {
"status": "computed_rule_probe",
"active": active,
"moon_sign_index": sign_index,
"quality": "avoid" if active else "neutral",
}
def _sankranti(sun_lon: float) -> dict:
degree_in_sign = sun_lon % 30
near_boundary = degree_in_sign <= 1 or degree_in_sign >= 29
return {
"status": "boundary_screen_observation",
"active_or_near": near_boundary,
"sun_degree_in_sign": round(degree_in_sign, 3),
"quality": "avoid_or_verify_exact_ingress" if near_boundary else "not_near_boundary",
}
def _yoga_flag(panchanga: dict, yoga_name: str) -> dict:
current = ((panchanga.get("yoga") or {}).get("yoga") or "")
active = current.casefold() == yoga_name.casefold()
return {
"status": "computed_rule_probe",
"active": active,
"current_yoga": current,
"quality": "avoid" if active else "not_active",
}
def _factor_scorecard(factors: dict) -> dict:
favorable = []
caution = []
for key, value in factors.items():
if not isinstance(value, dict):
continue
if value.get("favorable") is True or value.get("quality") in {"not_active", "not_near_boundary"}:
favorable.append(key)
if value.get("favorable") is False or value.get("quality") in {"avoid", "avoid_or_verify_exact_ingress", "avoid_or_handle_with_caution"}:
caution.append(key)
return {
"claim_status": "factor_only_not_final_muhurta_verdict",
"score_cap": "low",
"favorable_factor_count": len(favorable),
"caution_factor_count": len(caution),
"favorable_factors": favorable,
"caution_factors": caution,
"boundary": "Factor scorecard is for comparison only; it is not a final electional verdict.",
}
def build_probe(date_text: str, birth_moon_nakshatra_index: int | None, birth_moon_sign_index: int | None) -> dict:
dt = datetime.strptime(date_text, "%Y-%m-%d")
sun_lon, moon_lon, has_swiss = _get_sun_moon_lons(dt.year, dt.month, dt.day, 12)
weekday = _weekday_to_vara(dt.weekday())
panchanga = calc_panchanga(sun_lon=sun_lon, moon_lon=moon_lon, weekday=weekday, hour_from_sunrise=6.0)
factors = {
"panchanga": panchanga,
"tarabala": _tarabala(birth_moon_nakshatra_index, panchanga["nakshatra"]["nakshatra_idx"]),
"chandrabala": _chandrabala(birth_moon_sign_index, moon_lon),
"rahu_kalam": panchanga.get("rahu_kala"),
"yamaganda": _yamaganda(weekday),
"gulika_kalam": _gulika_kalam(weekday),
"abhijit_muhurta": calc_abhijit_muhurta(),
"panchaka": _panchaka(moon_lon),
"sankranti": _sankranti(sun_lon),
"vyatipata": _yoga_flag(panchanga, "Vyatipata"),
"vaidhriti": _yoga_flag(panchanga, "Vaidhriti"),
}
signals = []
blockers = []
if factors["tarabala"].get("favorable") is True:
signals.append("Tarabala")
elif factors["tarabala"].get("favorable") is False:
blockers.append("Tarabala")
if factors["chandrabala"].get("favorable") is True:
signals.append("Chandrabala")
elif factors["chandrabala"].get("favorable") is False:
blockers.append("Chandrabala")
if factors["abhijit_muhurta"]:
signals.append("Abhijit")
factor_scorecard = _factor_scorecard(factors)
return {
"scope": "muhurta_factor_probe",
"created_at": "2026-07-19",
"date": date_text,
"production_tuning_allowed": False,
"claim_status": "exploratory_muhurta_candidate",
"verified_muhurta_verdict": False,
"full_scoring_status": "factor_only_scoring_observation",
"final_muhurta_verdict_status": "blocked_until_oracle",
"ephemeris_source": "swisseph" if has_swiss else "approximate_fallback",
"factors": factors,
"factor_scorecard": factor_scorecard,
"candidate_windows": [
{
"label": "abhijit_reference_window",
"signals": signals,
"blockers": blockers,
"confidence_cap": "low",
"claim_status": "exploratory_muhurta_candidate"
}
],
"boundary": "Muhurta factors are observations only;未通过完整 worked examples 与负样本验证,不能作为确定择日承诺。"
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--date", required=True)
parser.add_argument("--birth-moon-nakshatra-index", type=int)
parser.add_argument("--birth-moon-sign-index", type=int)
args = parser.parse_args()
print(json.dumps(build_probe(args.date, args.birth_moon_nakshatra_index, args.birth_moon_sign_index), ensure_ascii=False, indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())