Files
732642856 f83db2fac1 Enhance Jyotish validation and Jaimini modules
- 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
2026-06-10 20:50:52 +08:00

509 lines
19 KiB
Python

from .constants import ZODIAC_SIGNS, SIGN_LORDS, NATURAL_FRIENDS, NATURAL_ENEMIES, EXALTATION, DEBILITATION, OWN_SIGNS
from .nakshatra import get_nakshatra
import math
# --- Data Tables ---
# 0-26 indexed Nakshatras
YONI_ANIMALS = [
"Horse", "Elephant", "Sheep", "Serpent", "Serpent", "Dog", "Cat", "Sheep", "Cat", # 0-8
"Rat", "Rat", "Cow", "Buffalo", "Tiger", "Buffalo", "Tiger", "Deer", "Deer", # 9-17
"Dog", "Monkey", "Mongoose", "Monkey", "Lion", "Horse", "Lion", "Cow", "Elephant" # 18-26
]
YONI_ENEMIES = {
"Horse": "Buffalo", "Buffalo": "Horse",
"Elephant": "Lion", "Lion": "Elephant",
"Sheep": "Monkey", "Monkey": "Sheep",
"Serpent": "Mongoose", "Mongoose": "Serpent",
"Dog": "Deer", "Deer": "Dog",
"Cat": "Rat", "Rat": "Cat",
"Cow": "Tiger", "Tiger": "Cow"
}
GANA = [
"Deva", "Manushya", "Rakshasa", "Manushya", "Deva", "Manushya", "Deva", "Deva", "Rakshasa",
"Rakshasa", "Manushya", "Manushya", "Deva", "Rakshasa", "Deva", "Rakshasa", "Deva", "Rakshasa",
"Rakshasa", "Manushya", "Manushya", "Deva", "Rakshasa", "Rakshasa", "Manushya", "Manushya", "Deva"
]
NADI = [
"Adi", "Madhya", "Antya", "Antya", "Madhya", "Adi", "Adi", "Madhya", "Antya",
"Antya", "Madhya", "Adi", "Adi", "Madhya", "Antya", "Antya", "Madhya", "Adi",
"Adi", "Madhya", "Antya", "Antya", "Madhya", "Adi", "Adi", "Madhya", "Antya"
]
VARNA = {
"Cancer": 1, "Scorpio": 1, "Pisces": 1, # Brahmin
"Aries": 2, "Leo": 2, "Sagittarius": 2, # Kshatriya
"Taurus": 3, "Virgo": 3, "Capricorn": 3, # Vaishya
"Gemini": 4, "Libra": 4, "Aquarius": 4 # Shudra
}
# 1. Varna (1 point)
def calc_varna(m_sign, f_sign):
m_varna = VARNA[m_sign]
f_varna = VARNA[f_sign]
return 1.0 if m_varna <= f_varna else 0.0
# 2. Vashya (2 points) — Full BPHS classification
# Sign categories: Chatushpada (quadruped), Manava (human), Jalachara (water),
# Vanachara (wild/forest), Keet (insect/reptile)
VASHYA_TYPE = {
"Aries": "Chatushpada", "Taurus": "Chatushpada",
"Leo": "Vanachara", "Sagittarius": "Chatushpada", # 2nd half is Manava
"Capricorn": "Chatushpada", # 1st half is Chatushpada
"Gemini": "Manava", "Virgo": "Manava", "Libra": "Manava",
"Aquarius": "Manava", # 1st half is Manava
"Cancer": "Jalachara", "Pisces": "Jalachara",
"Scorpio": "Keet",
}
# Vashya compatibility scoring matrix
VASHYA_MATRIX = {
("Chatushpada", "Chatushpada"): 2.0,
("Manava", "Manava"): 2.0,
("Jalachara", "Jalachara"): 2.0,
("Vanachara", "Vanachara"): 2.0,
("Keet", "Keet"): 2.0,
("Chatushpada", "Manava"): 0.5,
("Manava", "Chatushpada"): 0.5,
("Manava", "Jalachara"): 1.0,
("Jalachara", "Manava"): 1.0,
("Chatushpada", "Jalachara"): 0.5,
("Jalachara", "Chatushpada"): 0.5,
("Vanachara", "Chatushpada"): 0.0, # Wild eats quadruped
("Chatushpada", "Vanachara"): 0.0,
("Keet", "Chatushpada"): 0.0,
("Chatushpada", "Keet"): 0.0,
("Vanachara", "Manava"): 0.5,
("Manava", "Vanachara"): 0.5,
("Keet", "Manava"): 0.5,
("Manava", "Keet"): 0.5,
("Vanachara", "Jalachara"): 0.5,
("Jalachara", "Vanachara"): 0.5,
("Keet", "Jalachara"): 1.0,
("Jalachara", "Keet"): 1.0,
("Vanachara", "Keet"): 1.0,
("Keet", "Vanachara"): 1.0,
}
def calc_vashya(m_sign, f_sign):
if m_sign == f_sign:
return 2.0
m_type = VASHYA_TYPE.get(m_sign, "Manava")
f_type = VASHYA_TYPE.get(f_sign, "Manava")
return VASHYA_MATRIX.get((m_type, f_type), 1.0)
# 3. Tara (3 points)
def calc_tara(m_nak_idx, f_nak_idx):
m_to_f = (f_nak_idx - m_nak_idx) % 9
f_to_m = (m_nak_idx - f_nak_idx) % 9
pts = 0.0
if m_to_f not in (2, 4, 6): pts += 1.5
if f_to_m not in (2, 4, 6): pts += 1.5
return pts
# 4. Yoni (4 points)
def calc_yoni(m_nak_idx, f_nak_idx):
m_yoni = YONI_ANIMALS[m_nak_idx]
f_yoni = YONI_ANIMALS[f_nak_idx]
if m_yoni == f_yoni:
return 4.0
if YONI_ENEMIES.get(m_yoni) == f_yoni:
return 0.0
return 2.0 # Neutral
# 5. Graha Maitri (5 points)
def check_friendship(p1, p2):
if p1 == p2:
return 1.0 # Same lord
if p2 in NATURAL_FRIENDS.get(p1, []):
return 1.0
if p2 in NATURAL_ENEMIES.get(p1, []):
return 0.0
return 0.5 # Neutral
def calc_graha_maitri(m_sign, f_sign):
m_lord = SIGN_LORDS[m_sign]
f_lord = SIGN_LORDS[f_sign]
m_to_f = check_friendship(m_lord, f_lord)
f_to_m = check_friendship(f_lord, m_lord)
total = m_to_f + f_to_m
if total == 2.0: return 5.0
if total == 1.5: return 4.0
if total == 1.0: return 3.0
if total == 0.5: return 1.0
return 0.0
# 6. Gana (6 points)
def calc_gana(m_nak_idx, f_nak_idx):
m_gana = GANA[m_nak_idx]
f_gana = GANA[f_nak_idx]
if m_gana == f_gana: return 6.0
if m_gana == "Deva" and f_gana == "Manushya": return 6.0
if m_gana == "Manushya" and f_gana == "Deva": return 5.0
if m_gana == "Rakshasa" and f_gana == "Manushya": return 0.0
if f_gana == "Rakshasa" and m_gana == "Manushya": return 0.0
if m_gana == "Rakshasa" and f_gana == "Deva": return 1.0
if f_gana == "Rakshasa" and m_gana == "Deva": return 0.0
return 0.0
# 7. Bhakoot (7 points)
def calc_bhakoot(m_sign, f_sign):
m_idx = ZODIAC_SIGNS.index(m_sign)
f_idx = ZODIAC_SIGNS.index(f_sign)
diff = (f_idx - m_idx) % 12 + 1
if diff in (1, 7, 3, 11, 4, 10):
return 7.0
return 0.0
# 8. Nadi (8 points)
def calc_nadi(m_nak_idx, f_nak_idx):
m_nadi = NADI[m_nak_idx]
f_nadi = NADI[f_nak_idx]
if m_nadi != f_nadi:
return 8.0
return 0.0 # Nadi Dosha
# ==========================================
# ADDITIONAL KUTAS (beyond 36-point Ashtakoot)
# ==========================================
# 9. Mahendra Kuta — longevity and well-being
def calc_mahendra(m_nak_idx, f_nak_idx):
"""Male's nakshatra counted from female's. Auspicious if 4,7,10,13,16,19,22,25."""
count = ((m_nak_idx - f_nak_idx) % 27) + 1
return "good" if count in (4, 7, 10, 13, 16, 19, 22, 25) else "bad"
# 10. Stree Deergha — husband's longevity
def calc_stree_deergha(m_nak_idx, f_nak_idx):
"""Male's nakshatra must be >= 9 nakshatras from female's (counted f→m)."""
count = ((m_nak_idx - f_nak_idx) % 27) + 1
return "good" if count >= 9 else "bad"
# 11. Vedha Kuta — obstruction pairs
VEDHA_PAIRS = [
(0, 17), # Ashwini - Jyeshtha
(1, 16), # Bharani - Anuradha
(2, 15), # Krittika - Vishakha
(3, 14), # Rohini - Swati
(5, 21), # Ardra - Shravana
(6, 20), # Punarvasu - Uttara Ashadha
(7, 19), # Pushya - Purva Ashadha
(8, 18), # Ashlesha - Mula
(9, 26), # Magha - Revati
(10, 25), # Purva Phalguni - Uttara Bhadrapada
(11, 24), # Uttara Phalguni - Purva Bhadrapada
(12, 23), # Hasta - Shatabhisha
(4, 22), # Mrigashira - Dhanishta
]
def calc_vedha(m_nak_idx, f_nak_idx):
"""Check if male and female nakshatras form a hostile Vedha pair."""
for a, b in VEDHA_PAIRS:
if (m_nak_idx == a and f_nak_idx == b) or (m_nak_idx == b and f_nak_idx == a):
return "bad"
return "good"
# 12. Kuja Dosha (Manglik) — Mars affliction analysis
_DOSHA_HOUSES = {2, 4, 7, 8, 12}
_HIGH_SEVERITY_HOUSES = {7, 8}
_MARS_EXCEPTIONS = {
2: {"Gemini", "Virgo"},
12: {"Taurus", "Libra"},
4: {"Aries", "Scorpio"},
7: {"Capricorn", "Cancer"},
8: {"Sagittarius", "Pisces"},
}
_MARS_EXEMPT_SIGNS = {"Aquarius", "Leo"}
def _planet_dignity_level(planet_name, sign):
"""Return dignity level for Kuja Dosha scoring."""
if planet_name in EXALTATION and EXALTATION[planet_name][0] == sign:
return "exalted"
if planet_name in OWN_SIGNS and sign in OWN_SIGNS[planet_name]:
return "own"
lord = SIGN_LORDS.get(sign)
if lord and planet_name in NATURAL_FRIENDS.get(lord, []):
return "friendly"
if lord and planet_name in NATURAL_ENEMIES.get(lord, []):
return "enemy"
if planet_name in DEBILITATION and DEBILITATION[planet_name][0] == sign:
return "debilitated"
return "neutral"
_DOSHA_SCORES_HIGH = {
"Mars": {"debilitated": 100, "enemy": 90, "neutral": 80, "friendly": 70, "own": 60, "exalted": 50},
"Saturn": {"debilitated": 75, "enemy": 67.5, "neutral": 60, "friendly": 52.5, "own": 45, "exalted": 37.5},
"Sun": {"debilitated": 50, "enemy": 45, "neutral": 40, "friendly": 35, "own": 30, "exalted": 25},
}
_DOSHA_SCORES_LOW = {
"Mars": {"debilitated": 50, "enemy": 45, "neutral": 40, "friendly": 35, "own": 30, "exalted": 25},
"Saturn": {"debilitated": 37.5, "enemy": 33.75, "neutral": 30, "friendly": 26.25, "own": 22.5, "exalted": 18.75},
"Sun": {"debilitated": 25, "enemy": 22.5, "neutral": 20, "friendly": 17.5, "own": 15, "exalted": 12.5},
}
def _calc_dosha_score(planet_name, house, sign):
"""Calculate Kuja Dosha score for a single planet placement."""
if house not in _DOSHA_HOUSES:
return 0.0
# Mars-specific exceptions
if planet_name == "Mars":
if sign in _MARS_EXEMPT_SIGNS:
return 0.0
if house in _MARS_EXCEPTIONS and sign in _MARS_EXCEPTIONS[house]:
return 0.0
dig = _planet_dignity_level(planet_name, sign)
score_planet = planet_name if planet_name in _DOSHA_SCORES_HIGH else "Saturn" # Rahu/Ketu use Saturn table
if house in _HIGH_SEVERITY_HOUSES:
return _DOSHA_SCORES_HIGH.get(score_planet, _DOSHA_SCORES_HIGH["Saturn"]).get(dig, 60)
else:
return _DOSHA_SCORES_LOW.get(score_planet, _DOSHA_SCORES_LOW["Saturn"]).get(dig, 30)
def calc_kuja_dosha(chart):
"""
Calculate total Kuja Dosha score for a chart.
Checks Mars, Saturn, Rahu, Ketu, Sun in houses 2,4,7,8,12.
chart: output from calculate_vedic_chart (needs planets with house and sign).
Returns dict with total score and per-planet breakdown.
"""
planets = chart.get("planets", {})
total = 0.0
breakdown = {}
for p_name in ("Mars", "Saturn", "Rahu", "Ketu", "Sun"):
pd = planets.get(p_name)
if not pd:
continue
score = _calc_dosha_score(p_name, pd["house"], pd["sign"])
if score > 0:
breakdown[p_name] = {"house": pd["house"], "sign": pd["sign"], "score": score}
total += score
return {"total_score": round(total, 2), "breakdown": breakdown, "is_manglik": total > 0}
def match_kuja_dosha(male_score, female_score):
"""
Compare Kuja Dosha between male and female.
|diff| <= 5: good. Female > male by > 5: bad. Male > female by > 5: check 25% threshold.
"""
diff = male_score - female_score
if abs(diff) <= 5:
return {"result": "good", "description": "Kuja Dosha balanced between partners."}
if diff < -5:
return {"result": "bad", "description": "Female has significantly more Kuja Dosha."}
# male > female by > 5
if female_score > 0 and diff < female_score * 0.25:
return {"result": "acceptable", "description": "Male has more Kuja Dosha but within tolerance."}
return {"result": "bad", "description": "Male has significantly more Kuja Dosha."}
# 13. Rajju Kuta — marital longevity based on nakshatra body-part group
RAJJU_GROUPS = {
"Pada": {0, 8, 9, 17, 18, 26}, # Ashwini, Ashlesha, Magha, Jyeshtha, Mula, Revati
"Kati": {1, 7, 10, 16, 25, 19}, # Bharani, Pushya, P.Phalguni, Anuradha, U.Bhadra, P.Ashadha
"Udara": {2, 6, 11, 15, 20, 24}, # Krittika, Punarvasu, U.Phalguni, Vishakha, U.Ashadha, P.Bhadra
"Kanta": {3, 5, 12, 14, 21, 23}, # Rohini, Ardra, Hasta, Swati, Shravana, Shatabhisha
"Sira": {4, 13, 22}, # Mrigashira, Chitra, Dhanishta
}
RAJJU_EFFECTS = {
"Sira": "head — risk to husband's longevity",
"Kanta": "neck — risk to wife's longevity",
"Udara": "stomach — risk to children",
"Kati": "waist — poverty may ensue",
"Pada": "foot — couple may be always wandering",
}
def _get_rajju_group(nak_idx):
for group, indices in RAJJU_GROUPS.items():
if nak_idx in indices:
return group
return None
def calc_rajju(m_nak_idx, f_nak_idx):
"""Same Rajju group = bad; different = good."""
m_group = _get_rajju_group(m_nak_idx)
f_group = _get_rajju_group(f_nak_idx)
if m_group and f_group and m_group == f_group:
return {"result": "bad", "group": m_group, "effect": RAJJU_EFFECTS.get(m_group, "")}
return {"result": "good", "group": None, "effect": ""}
# 14. Bad Constellations — specific nakshatra quarters considered destructive
def calc_bad_constellations(m_nak_idx, m_pada, f_nak_idx, f_pada):
"""
Only first pada of Moola/Ashlesha/Jyeshtha and 4th pada of Vishakha are bad.
Ashlesha/Jyeshtha/Vishakha destructive only for females.
"""
issues = []
if m_nak_idx == 18 and m_pada == 1:
issues.append("Male born in Moola 1st pada — risk to father-in-law.")
if f_nak_idx == 18 and f_pada == 1:
issues.append("Female born in Moola 1st pada — risk to father-in-law.")
if f_nak_idx == 8 and f_pada == 1:
issues.append("Female born in Ashlesha 1st pada — risk to husband's mother.")
if f_nak_idx == 17 and f_pada == 1:
issues.append("Female born in Jyeshtha 1st pada — risk to husband's elder brother.")
if f_nak_idx == 15 and f_pada == 4:
issues.append("Female born in Vishakha 4th pada — risk to husband's younger brother.")
return {"result": "bad" if issues else "good", "issues": issues}
# 15. Lagna and House 7 — cross-Lagna compatibility
def calc_lagna_house7(chart1, chart2):
"""
Good if female's Moon sign = male's Lagna OR male's Moon sign = female's Lagna,
OR if 7th house lords are exchanged.
"""
m_lagna = chart1.get("lagna", {}).get("sign")
f_lagna = chart2.get("lagna", {}).get("sign")
m_moon = chart1.get("planets", {}).get("Moon", {}).get("sign")
f_moon = chart2.get("planets", {}).get("Moon", {}).get("sign")
if (f_moon and m_lagna and f_moon == m_lagna) or (m_moon and f_lagna and m_moon == f_lagna):
return {"result": "good", "description": "Moon-Lagna cross match — mutual understanding and affection."}
m_lagna_idx = ZODIAC_SIGNS.index(m_lagna) if m_lagna else None
f_lagna_idx = ZODIAC_SIGNS.index(f_lagna) if f_lagna else None
if m_lagna_idx is not None and f_lagna_idx is not None:
m_7th_sign = ZODIAC_SIGNS[(m_lagna_idx + 6) % 12]
f_7th_sign = ZODIAC_SIGNS[(f_lagna_idx + 6) % 12]
m_7th_lord = SIGN_LORDS[m_7th_sign]
f_7th_lord = SIGN_LORDS[f_7th_sign]
m_7lord_sign = chart1.get("planets", {}).get(m_7th_lord, {}).get("sign")
f_7lord_sign = chart2.get("planets", {}).get(f_7th_lord, {}).get("sign")
if m_7lord_sign == f_lagna or f_7lord_sign == m_lagna:
return {"result": "good", "description": "7th house lord cross-placement — marriage stability."}
return {"result": "neutral", "description": "No special Lagna-7th house connection found."}
# 16. Sex Energy — based on planets in 7th house
def calc_sex_energy(chart1, chart2):
"""
Mars/Venus in 7th = strong sex drive. Mercury/Jupiter in 7th = moderate.
Mismatch between partners = potential incompatibility.
"""
def _classify(chart):
planets = chart.get("planets", {})
strong = any(planets.get(p, {}).get("house") == 7 for p in ("Mars", "Venus"))
moderate = any(planets.get(p, {}).get("house") == 7 for p in ("Mercury", "Jupiter"))
if strong and not moderate:
return "strong"
if moderate and not strong:
return "moderate"
if strong and moderate:
return "mixed"
return "unknown"
m_type = _classify(chart1)
f_type = _classify(chart2)
if m_type in ("unknown", "mixed") or f_type in ("unknown", "mixed"):
return {"result": "neutral", "male": m_type, "female": f_type,
"description": "Insufficient data for sex energy assessment."}
if m_type == f_type:
return {"result": "good", "male": m_type, "female": f_type,
"description": f"Both partners have {m_type} sex energy — compatible."}
return {"result": "bad", "male": m_type, "female": f_type,
"description": f"Male has {m_type} and female has {f_type} sex energy — potential mismatch."}
def calculate_ashtakoot(male_moon_lon: float, female_moon_lon: float,
male_chart=None, female_chart=None):
"""
Calculates the 36-point Ashtakoot compatibility matching
plus additional kutas (Mahendra, Stree Deergha, Vedha, Rajju, etc.).
"""
m_nak = get_nakshatra(male_moon_lon)
f_nak = get_nakshatra(female_moon_lon)
m_nak_idx = m_nak["index"]
f_nak_idx = f_nak["index"]
m_sign_idx = int((male_moon_lon % 360) / 30)
f_sign_idx = int((female_moon_lon % 360) / 30)
m_sign = ZODIAC_SIGNS[m_sign_idx]
f_sign = ZODIAC_SIGNS[f_sign_idx]
scores = {
"Varna": calc_varna(m_sign, f_sign),
"Vashya": calc_vashya(m_sign, f_sign),
"Tara": calc_tara(m_nak_idx, f_nak_idx),
"Yoni": calc_yoni(m_nak_idx, f_nak_idx),
"GrahaMaitri": calc_graha_maitri(m_sign, f_sign),
"Gana": calc_gana(m_nak_idx, f_nak_idx),
"Bhakoot": calc_bhakoot(m_sign, f_sign),
"Nadi": calc_nadi(m_nak_idx, f_nak_idx),
}
total_score = sum(scores.values())
# Additional kutas
rajju_result = calc_rajju(m_nak_idx, f_nak_idx)
additional_kutas = {
"Mahendra": calc_mahendra(m_nak_idx, f_nak_idx),
"StreeDeergha": calc_stree_deergha(m_nak_idx, f_nak_idx),
"Vedha": calc_vedha(m_nak_idx, f_nak_idx),
"Rajju": rajju_result,
"BadConstellations": calc_bad_constellations(
m_nak_idx, m_nak.get("pada", 0), f_nak_idx, f_nak.get("pada", 0)),
}
# Chart-dependent kutas (need full chart data)
if male_chart and female_chart:
additional_kutas["LagnaHouse7"] = calc_lagna_house7(male_chart, female_chart)
additional_kutas["SexEnergy"] = calc_sex_energy(male_chart, female_chart)
# Exception logic (per VedAstro/BPHS):
exceptions = []
# 1. Bad Nadi neutralized if Bhakoot + Rajju both good
if scores["Nadi"] == 0:
if scores["Bhakoot"] > 0 and rajju_result["result"] == "good":
exceptions.append("Nadi Dosha mitigated by good Bhakoot and Rajju.")
# 2. Bad Rajju neutralized if GrahaMaitri + Bhakoot + Tara + Mahendra all good
if rajju_result["result"] == "bad":
if (scores["GrahaMaitri"] >= 4.0 and scores["Bhakoot"] > 0 and
scores["Tara"] >= 1.5 and additional_kutas["Mahendra"] == "good"):
exceptions.append("Rajju Dosha mitigated by good Graha Maitri, Bhakoot, Tara, and Mahendra.")
# 3. Bad Stree Deergha neutralized if Bhakoot + GrahaMaitri both good
if additional_kutas["StreeDeergha"] == "bad":
if scores["Bhakoot"] > 0 and scores["GrahaMaitri"] >= 4.0:
exceptions.append("Stree Deergha Dosha mitigated by good Bhakoot and Graha Maitri.")
return {
"male_details": {
"moon_sign": m_sign,
"nakshatra": m_nak["name"],
"gana": GANA[m_nak_idx],
"nadi": NADI[m_nak_idx],
"yoni": YONI_ANIMALS[m_nak_idx]
},
"female_details": {
"moon_sign": f_sign,
"nakshatra": f_nak["name"],
"gana": GANA[f_nak_idx],
"nadi": NADI[f_nak_idx],
"yoni": YONI_ANIMALS[f_nak_idx]
},
"scores": scores,
"total_score": total_score,
"max_score": 36.0,
"additional_kutas": additional_kutas,
"exceptions": exceptions,
"is_match_approved": total_score >= 18.0 and (scores["Nadi"] > 0 or len(exceptions) > 0)
}