f83db2fac1
- add external validation reports and open-source comparison references - integrate Jaimini arudha/graha pada, enhanced argala, and additional synastry kutas - update skill docs and capability matrices - add smoke tests for open-source integrations
237 lines
8.1 KiB
Python
237 lines
8.1 KiB
Python
"""
|
|
Muhurtha — Electional Astrology (BPHS)
|
|
Evaluates auspiciousness of a given date/time for specific activities.
|
|
Uses Panchang elements, planetary positions, and classical rules.
|
|
"""
|
|
|
|
from .constants import ZODIAC_SIGNS, SIGN_LORDS
|
|
|
|
# ==========================================
|
|
# UNIVERSAL AVOIDANCE RULES
|
|
# ==========================================
|
|
|
|
# Inauspicious Panchang Yoga indices (0-indexed)
|
|
BAD_YOGAS = {0, 5, 8, 9, 12, 14, 16, 18, 26}
|
|
|
|
# Universally avoided lunar days (tithis)
|
|
BAD_TITHIS = {4, 6, 8, 12, 14, 30}
|
|
|
|
# Universally avoided nakshatras
|
|
BAD_NAKSHATRAS = {"Bharani", "Krittika"}
|
|
|
|
|
|
# ==========================================
|
|
# ACTIVITY-SPECIFIC RULES
|
|
# ==========================================
|
|
|
|
MARRIAGE_RULES = {
|
|
"good_nakshatras": {"Rohini", "Mrigashira", "Magha", "Uttara Phalguni", "Hasta",
|
|
"Swati", "Anuradha", "Moola", "Uttara Ashadha", "Uttara Bhadrapada", "Revati"},
|
|
"good_tithis": {2, 3, 5, 7, 10, 11, 13},
|
|
"good_lagnas": {"Taurus", "Gemini", "Cancer", "Virgo", "Libra", "Sagittarius"},
|
|
}
|
|
|
|
TRAVEL_RULES = {
|
|
"good_nakshatras": {"Ashwini", "Mrigashira", "Punarvasu", "Pushya", "Hasta",
|
|
"Anuradha", "Shravana", "Dhanishta", "Revati"},
|
|
"good_tithis": {2, 3, 5, 7, 10, 11, 13},
|
|
"good_lagnas": {"Aries", "Taurus", "Cancer", "Leo", "Libra", "Sagittarius"},
|
|
}
|
|
|
|
BUSINESS_RULES = {
|
|
"good_nakshatras": {"Ashwini", "Rohini", "Punarvasu", "Pushya", "Uttara Phalguni",
|
|
"Hasta", "Chitra", "Swati", "Anuradha", "Shravana", "Dhanishta", "Revati"},
|
|
"good_tithis": {2, 3, 5, 7, 10, 11, 13},
|
|
"good_weekdays": {"Monday", "Wednesday", "Thursday", "Friday"},
|
|
"moon_signs": {"Taurus", "Cancer", "Virgo", "Libra", "Sagittarius", "Pisces"},
|
|
}
|
|
|
|
EDUCATION_RULES = {
|
|
"good_nakshatras": {"Ashwini", "Punarvasu", "Pushya", "Hasta", "Chitra",
|
|
"Swati", "Shravana", "Dhanishta", "Shatabhisha", "Revati"},
|
|
"good_tithis": {2, 3, 5, 7, 10, 11, 13},
|
|
"good_lagnas": {"Gemini", "Virgo", "Sagittarius", "Pisces"},
|
|
}
|
|
|
|
HOUSE_ENTRY_RULES = {
|
|
"good_nakshatras": {"Rohini", "Uttara Phalguni", "Uttara Ashadha", "Uttara Bhadrapada",
|
|
"Shravana", "Dhanishta", "Revati", "Ashwini", "Mrigashira"},
|
|
"good_tithis": {2, 3, 5, 7, 10, 11, 13},
|
|
"good_weekdays": {"Monday", "Wednesday", "Thursday", "Friday"},
|
|
}
|
|
|
|
MEDICAL_RULES = {
|
|
"good_nakshatras": {"Ashwini", "Rohini", "Mrigashira", "Pushya", "Hasta",
|
|
"Chitra", "Swati", "Anuradha", "Shravana", "Revati"},
|
|
"good_tithis": {2, 3, 5, 7, 10, 11, 13},
|
|
"good_weekdays": {"Saturday", "Monday"},
|
|
}
|
|
|
|
ACTIVITY_RULES = {
|
|
"marriage": MARRIAGE_RULES,
|
|
"travel": TRAVEL_RULES,
|
|
"business": BUSINESS_RULES,
|
|
"education": EDUCATION_RULES,
|
|
"house_entry": HOUSE_ENTRY_RULES,
|
|
"medical": MEDICAL_RULES,
|
|
}
|
|
|
|
|
|
def _check_panchanga_suddhi(panchang):
|
|
"""Check universal Panchang purity (5-fold). Returns list of issues."""
|
|
issues = []
|
|
|
|
tithi_num = panchang.get("tithi", {}).get("number", 0)
|
|
if tithi_num in BAD_TITHIS:
|
|
issues.append(f"Inauspicious tithi: {panchang['tithi'].get('name', tithi_num)}")
|
|
|
|
nak_name = panchang.get("nakshatra", {}).get("name", "")
|
|
if nak_name in BAD_NAKSHATRAS:
|
|
issues.append(f"Inauspicious nakshatra: {nak_name}")
|
|
|
|
yoga_idx = panchang.get("yoga", {}).get("index", -1)
|
|
if yoga_idx in BAD_YOGAS:
|
|
issues.append(f"Inauspicious yoga: {panchang['yoga'].get('name', yoga_idx)}")
|
|
|
|
return issues
|
|
|
|
|
|
def _check_marriage_doshas(planets):
|
|
"""Check marriage-specific rejection doshas (per BPHS)."""
|
|
doshas = []
|
|
|
|
# Sagraha Dosha: Moon conjunct any planet
|
|
moon = planets.get("Moon", {})
|
|
moon_sign = moon.get("sign", "")
|
|
for p_name, pd in planets.items():
|
|
if p_name != "Moon" and pd.get("sign") == moon_sign:
|
|
doshas.append(f"Sagraha Dosha: Moon conjunct {p_name} in {moon_sign}")
|
|
break
|
|
|
|
# Moon in 6th, 8th, or 12th
|
|
moon_house = moon.get("house", 0)
|
|
if moon_house in (6, 8, 12):
|
|
doshas.append(f"Shashtashta Dosha: Moon in house {moon_house}")
|
|
|
|
# Venus in 6th
|
|
venus = planets.get("Venus", {})
|
|
if venus.get("house") == 6:
|
|
doshas.append("Bhrigupta Shatka: Venus in 6th house")
|
|
|
|
# Mars in 8th
|
|
mars = planets.get("Mars", {})
|
|
if mars.get("house") == 8:
|
|
doshas.append("Kujaasthama: Mars in 8th house")
|
|
|
|
return doshas
|
|
|
|
|
|
def evaluate_muhurtha(activity, panchang, planets=None, lagna_sign=None):
|
|
"""
|
|
Evaluate auspiciousness of a moment for a given activity.
|
|
|
|
Parameters
|
|
----------
|
|
activity : str
|
|
One of: 'marriage', 'travel', 'business', 'education', 'house_entry', 'medical'
|
|
panchang : dict
|
|
Panchang data from calculate_panchang()
|
|
planets : dict, optional
|
|
Planet positions (for marriage dosha checks)
|
|
lagna_sign : str, optional
|
|
Rising sign at the moment
|
|
|
|
Returns
|
|
-------
|
|
dict with verdict, reasons, and score
|
|
"""
|
|
rules = ACTIVITY_RULES.get(activity)
|
|
if not rules:
|
|
return {"verdict": "error", "reason": f"Unknown activity: {activity}",
|
|
"supported_activities": list(ACTIVITY_RULES.keys())}
|
|
|
|
positive = []
|
|
negative = []
|
|
|
|
# 1. Universal Panchanga Suddhi
|
|
panchang_issues = _check_panchanga_suddhi(panchang)
|
|
negative.extend(panchang_issues)
|
|
|
|
# 2. Activity-specific nakshatra
|
|
nak_name = panchang.get("nakshatra", {}).get("name", "")
|
|
good_naks = rules.get("good_nakshatras", set())
|
|
if nak_name in good_naks:
|
|
positive.append(f"Auspicious nakshatra for {activity}: {nak_name}")
|
|
elif nak_name and nak_name not in BAD_NAKSHATRAS:
|
|
negative.append(f"Nakshatra {nak_name} is not ideal for {activity}")
|
|
|
|
# 3. Activity-specific tithi
|
|
tithi_num = panchang.get("tithi", {}).get("number", 0)
|
|
good_tithis = rules.get("good_tithis", set())
|
|
if tithi_num in good_tithis:
|
|
positive.append(f"Auspicious tithi: {panchang['tithi'].get('name', tithi_num)}")
|
|
|
|
# 4. Weekday check
|
|
good_weekdays = rules.get("good_weekdays")
|
|
if good_weekdays:
|
|
vara = panchang.get("vara", {}).get("name", "")
|
|
if vara in good_weekdays:
|
|
positive.append(f"Auspicious weekday: {vara}")
|
|
else:
|
|
negative.append(f"Weekday {vara} is not ideal for {activity}")
|
|
|
|
# 5. Lagna check
|
|
good_lagnas = rules.get("good_lagnas")
|
|
if good_lagnas and lagna_sign:
|
|
if lagna_sign in good_lagnas:
|
|
positive.append(f"Auspicious Lagna: {lagna_sign}")
|
|
else:
|
|
negative.append(f"Lagna {lagna_sign} is not ideal for {activity}")
|
|
|
|
# 6. Moon sign check (for business)
|
|
moon_signs = rules.get("moon_signs")
|
|
if moon_signs and planets:
|
|
moon_sign = planets.get("Moon", {}).get("sign", "")
|
|
if moon_sign in moon_signs:
|
|
positive.append(f"Moon in auspicious sign for {activity}: {moon_sign}")
|
|
else:
|
|
negative.append(f"Moon in {moon_sign} is not ideal for {activity}")
|
|
|
|
# 7. Marriage-specific dosha checks
|
|
if activity == "marriage" and planets:
|
|
doshas = _check_marriage_doshas(planets)
|
|
for d in doshas:
|
|
negative.append(f"DOSHA: {d}")
|
|
|
|
# 8th house check (should be empty for marriage, medical, house_entry)
|
|
if activity in ("marriage", "medical", "house_entry") and planets:
|
|
for p_name, pd in planets.items():
|
|
if p_name not in ("Rahu", "Ketu") and pd.get("house") == 8:
|
|
negative.append(f"Planet in 8th house: {p_name}")
|
|
break
|
|
|
|
# Scoring
|
|
score = len(positive) * 10 - len(negative) * 15
|
|
has_hard_reject = any("DOSHA:" in n for n in negative)
|
|
|
|
if has_hard_reject:
|
|
verdict = "inauspicious"
|
|
elif len(negative) == 0 and len(positive) >= 2:
|
|
verdict = "auspicious"
|
|
elif len(positive) > len(negative):
|
|
verdict = "mixed_favorable"
|
|
elif len(negative) > len(positive):
|
|
verdict = "inauspicious"
|
|
else:
|
|
verdict = "mixed"
|
|
|
|
return {
|
|
"activity": activity,
|
|
"verdict": verdict,
|
|
"score": max(0, score),
|
|
"positive_factors": positive,
|
|
"negative_factors": negative,
|
|
"total_positive": len(positive),
|
|
"total_negative": len(negative),
|
|
}
|