diff --git a/scripts/ashtakoot.py b/scripts/ashtakoot.py new file mode 100644 index 00000000..7b8c6acb --- /dev/null +++ b/scripts/ashtakoot.py @@ -0,0 +1,525 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Ashtakoot 合婚(36点匹配)+ Kuja Dosha + 附加 Kuta +基于 dashaflow/matchmaking.py (MIT License) 适配 + +8 个标准 Kuta (36点): +1. Varna (1) — 社会阶层兼容性 +2. Vashya (2) — 控制力兼容性 +3. Tara (3) — 星宿距离吉凶 +4. Yoni (4) — 性格/本能兼容性 +5. Graha Maitri (5) — 行星友谊 +6. Gana (6) — 气质类型 +7. Bhakoot (7) — 星座相对位置 +8. Nadi (8) — 生理体质 + +附加 Kuta: +9. Mahendra — 长寿/子嗣 +10. Stree Deergha — 丈夫长寿 +11. Vedha — 障碍对 +12. Rajju — 婚姻寿命 +13. Bad Constellations — 凶宿 +14. Lagna House7 — 上升/7宫交叉 +15. Sex Energy — 性能量匹配 +16. Kuja Dosha — 火星凶相分析 +""" + +import math +from typing import Dict, Optional + +# ============================================================================ +# 常量 +# ============================================================================ +SIGNS = ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo', + 'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces'] + +SIGN_LORDS = { + 'Aries': 'Mars', 'Taurus': 'Venus', 'Gemini': 'Mercury', 'Cancer': 'Moon', + 'Leo': 'Sun', 'Virgo': 'Mercury', 'Libra': 'Venus', 'Scorpio': 'Mars', + 'Sagittarius': 'Jupiter', 'Capricorn': 'Saturn', 'Aquarius': 'Saturn', 'Pisces': 'Jupiter' +} + +# 27 宿数据 +NAKSHATRA_NAMES = [ + "Ashwini", "Bharani", "Krittika", "Rohini", "Mrigashira", "Ardra", + "Punarvasu", "Pushya", "Ashlesha", "Magha", "Purva Phalguni", "Uttara Phalguni", + "Hasta", "Chitra", "Swati", "Vishakha", "Anuradha", "Jyeshtha", + "Mula", "Purva Ashadha", "Uttara Ashadha", "Shravana", "Dhanishta", + "Shatabhisha", "Purva Bhadrapada", "Uttara Bhadrapada", "Revati" +] + +NAKSHATRA_LORDS = [ + "Ketu", "Venus", "Sun", "Moon", "Mars", "Rahu", + "Jupiter", "Saturn", "Mercury", "Ketu", "Venus", "Sun", + "Moon", "Mars", "Rahu", "Jupiter", "Saturn", "Mercury", + "Ketu", "Venus", "Sun", "Moon", "Mars", "Rahu", + "Jupiter", "Saturn", "Mercury" +] + +YONI_ANIMALS = [ + "Horse", "Elephant", "Sheep", "Serpent", "Serpent", "Dog", "Cat", "Sheep", "Cat", + "Rat", "Rat", "Cow", "Buffalo", "Tiger", "Buffalo", "Tiger", "Deer", "Deer", + "Dog", "Monkey", "Mongoose", "Monkey", "Lion", "Horse", "Lion", "Cow", "Elephant" +] + +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, + "Aries": 2, "Leo": 2, "Sagittarius": 2, + "Taurus": 3, "Virgo": 3, "Capricorn": 3, + "Gemini": 4, "Libra": 4, "Aquarius": 4 +} + +VASHYA_TYPE = { + "Aries": "Chatushpada", "Taurus": "Chatushpada", + "Leo": "Vanachara", "Sagittarius": "Chatushpada", + "Capricorn": "Chatushpada", + "Gemini": "Manava", "Virgo": "Manava", "Libra": "Manava", + "Aquarius": "Manava", + "Cancer": "Jalachara", "Pisces": "Jalachara", + "Scorpio": "Keet", +} + +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, ("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, +} + +FRIENDSHIP = { + 'Sun': {'friend': ['Moon', 'Mars', 'Jupiter'], 'enemy': ['Saturn', 'Venus'], 'neutral': ['Mercury']}, + 'Moon': {'friend': ['Sun', 'Mercury'], 'enemy': [], 'neutral': ['Mars', 'Jupiter', 'Venus', 'Saturn']}, + 'Mars': {'friend': ['Sun', 'Moon', 'Jupiter'], 'enemy': ['Mercury'], 'neutral': ['Venus', 'Saturn']}, + 'Mercury': {'friend': ['Sun', 'Venus'], 'enemy': ['Moon'], 'neutral': ['Mars', 'Jupiter', 'Saturn']}, + 'Jupiter': {'friend': ['Sun', 'Moon', 'Mars'], 'enemy': ['Mercury', 'Venus'], 'neutral': ['Saturn']}, + 'Venus': {'friend': ['Mercury', 'Saturn'], 'enemy': ['Sun', 'Moon'], 'neutral': ['Mars', 'Jupiter']}, + 'Saturn': {'friend': ['Mercury', 'Venus'], 'enemy': ['Sun', 'Moon', 'Mars'], 'neutral': ['Jupiter']}, +} + +VEDHA_PAIRS = [ + (0, 17), (1, 16), (2, 15), (3, 14), (5, 21), (6, 20), + (7, 19), (8, 18), (9, 26), (10, 25), (11, 24), (12, 23), (4, 22), +] + +RAJJU_GROUPS = { + "Pada": {0, 8, 9, 17, 18, 26}, + "Kati": {1, 7, 10, 16, 25, 19}, + "Udara": {2, 6, 11, 15, 20, 24}, + "Kanta": {3, 5, 12, 14, 21, 23}, + "Sira": {4, 13, 22}, +} + +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", +} + +# Kuja Dosha +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"} + +EXALTATION_DEG = { + 'Sun': 10.0, 'Moon': 33.0, 'Mars': 298.0, 'Mercury': 165.0, + 'Jupiter': 95.0, 'Venus': 357.0, 'Saturn': 200.0 +} + +OWN_SIGNS = { + 'Sun': {'Leo'}, 'Moon': {'Cancer'}, 'Mars': {'Aries', 'Scorpio'}, + 'Mercury': {'Gemini', 'Virgo'}, 'Jupiter': {'Sagittarius', 'Pisces'}, + 'Venus': {'Taurus', 'Libra'}, 'Saturn': {'Capricorn', 'Aquarius'} +} + + +# ============================================================================ +# 辅助函数 +# ============================================================================ +def get_nakshatra(moon_lon: float) -> Dict: + """从月亮经度计算星宿索引、名称和 pada""" + nak_span = 360.0 / 27 # 13.333° + nak_idx = int(moon_lon / nak_span) % 27 + pada = int(((moon_lon % nak_span) / nak_span) * 4) + 1 + return { + "index": nak_idx, + "name": NAKSHATRA_NAMES[nak_idx], + "lord": NAKSHATRA_LORDS[nak_idx], + "pada": pada, + } + + +# ============================================================================ +# 8 个标准 Kuta +# ============================================================================ +def calc_varna(m_sign: str, f_sign: str) -> float: + m_varna = VARNA.get(m_sign, 4) + f_varna = VARNA.get(f_sign, 4) + return 1.0 if m_varna <= f_varna else 0.0 + + +def calc_vashya(m_sign: str, f_sign: str) -> float: + 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) + + +def calc_tara(m_nak_idx: int, f_nak_idx: int) -> float: + 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 + + +def calc_yoni(m_nak_idx: int, f_nak_idx: int) -> float: + 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 + + +def calc_graha_maitri(m_sign: str, f_sign: str) -> float: + m_lord = SIGN_LORDS.get(m_sign, '') + f_lord = SIGN_LORDS.get(f_sign, '') + + def _friendship(p1, p2): + if p1 == p2: + return 1.0 + rel = FRIENDSHIP.get(p1, {}) + if p2 in rel.get('friend', []): + return 1.0 + if p2 in rel.get('enemy', []): + return 0.0 + return 0.5 + + m_to_f = _friendship(m_lord, f_lord) + f_to_m = _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 + + +def calc_gana(m_nak_idx: int, f_nak_idx: int) -> float: + 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 + + +def calc_bhakoot(m_sign: str, f_sign: str) -> float: + m_idx = SIGNS.index(m_sign) if m_sign in SIGNS else 0 + f_idx = SIGNS.index(f_sign) if f_sign in SIGNS else 0 + diff = (f_idx - m_idx) % 12 + 1 + if diff in (1, 7, 3, 11, 4, 10): + return 7.0 + return 0.0 + + +def calc_nadi(m_nak_idx: int, f_nak_idx: int) -> float: + m_nadi = NADI[m_nak_idx] + f_nadi = NADI[f_nak_idx] + return 8.0 if m_nadi != f_nadi else 0.0 + + +# ============================================================================ +# 附加 Kuta +# ============================================================================ +def calc_mahendra(m_nak_idx: int, f_nak_idx: int) -> str: + count = ((m_nak_idx - f_nak_idx) % 27) + 1 + return "good" if count in (4, 7, 10, 13, 16, 19, 22, 25) else "bad" + + +def calc_stree_deergha(m_nak_idx: int, f_nak_idx: int) -> str: + count = ((m_nak_idx - f_nak_idx) % 27) + 1 + return "good" if count >= 9 else "bad" + + +def calc_vedha(m_nak_idx: int, f_nak_idx: int) -> str: + 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" + + +def calc_rajju(m_nak_idx: int, f_nak_idx: int) -> Dict: + def _group(idx): + for g, indices in RAJJU_GROUPS.items(): + if idx in indices: + return g + return None + m_group = _group(m_nak_idx) + f_group = _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": ""} + + +def calc_bad_constellations(m_nak_idx: int, m_pada: int, f_nak_idx: int, f_pada: int) -> Dict: + 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} + + +def calc_lagna_house7(chart1: Dict, chart2: Dict) -> Dict: + 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."} + + if m_lagna and f_lagna: + m_idx = SIGNS.index(m_lagna) if m_lagna in SIGNS else 0 + f_idx = SIGNS.index(f_lagna) if f_lagna in SIGNS else 0 + m_7th = SIGNS[(m_idx + 6) % 12] + f_7th = SIGNS[(f_idx + 6) % 12] + m_7lord = SIGN_LORDS[m_7th] + f_7lord = SIGN_LORDS[f_7th] + m_7lord_sign = chart1.get("planets", {}).get(m_7lord, {}).get("sign") + f_7lord_sign = chart2.get("planets", {}).get(f_7lord, {}).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 connection."} + + +# ============================================================================ +# Kuja Dosha (Manglik 分析) +# ============================================================================ +def _planet_dignity_level(planet_name: str, sign: str) -> str: + exalt_sign = SIGNS[int(EXALTATION_DEG.get(planet_name, 999) / 30) % 12] + debil_sign = SIGNS[int((EXALTATION_DEG.get(planet_name, 0) + 180) / 30) % 12] + if sign == exalt_sign: return "exalted" + if planet_name in OWN_SIGNS and sign in OWN_SIGNS[planet_name]: return "own" + lord = SIGN_LORDS.get(sign, '') + rel = FRIENDSHIP.get(planet_name, {}) + if lord in rel.get('friend', []): return "friendly" + if lord in rel.get('enemy', []): return "enemy" + if sign == debil_sign: return "debilitated" + return "neutral" + + +def calc_kuja_dosha(chart: Dict) -> Dict: + """计算完整的 Kuja Dosha(火星凶相分析)""" + planets = chart.get("planets", {}) + + 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}, + } + + total = 0.0 + breakdown = {} + for p_name in ("Mars", "Saturn", "Rahu", "Ketu", "Sun"): + pd = planets.get(p_name) + if not pd: + continue + house = pd.get("house", 0) + sign = pd.get("sign", "") + if house not in DOSHA_HOUSES: + continue + + # Mars exceptions + if p_name == "Mars": + if sign in MARS_EXEMPT_SIGNS: + continue + if house in MARS_EXCEPTIONS and sign in MARS_EXCEPTIONS[house]: + continue + + dig = _planet_dignity_level(p_name, sign) + score_table = dosha_scores_high if house in HIGH_SEVERITY_HOUSES else dosha_scores_low + table = score_table.get(p_name, score_table.get("Saturn", {})) + score = table.get(dig, 60 if house in HIGH_SEVERITY_HOUSES else 30) + if score > 0: + breakdown[p_name] = {"house": house, "sign": sign, "dignity": dig, "score": score} + total += score + + return {"total_score": round(total, 2), "breakdown": breakdown, "is_manglik": total > 0} + + +def match_kuja_dosha(male_score: float, female_score: float) -> Dict: + diff = male_score - female_score + if abs(diff) <= 5: + return {"result": "good", "description": "Kuja Dosha balanced."} + if diff < -5: + return {"result": "bad", "description": "Female has significantly more Kuja Dosha."} + 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."} + + +# ============================================================================ +# 主函数 +# ============================================================================ +def calculate_ashtakoot(male_moon_lon: float, female_moon_lon: float, + male_chart: Optional[Dict] = None, + female_chart: Optional[Dict] = None) -> Dict: + """ + 完整的 Ashtakoot 36点合婚 + 附加 Kuta + Kuja Dosha + + Args: + male_moon_lon: 男方月亮恒星黄道经度 + female_moon_lon: 女方月亮恒星黄道经度 + male_chart: 男方完整星盘(可选,用于附加 Kuta) + female_chart: 女方完整星盘(可选,用于附加 Kuta) + """ + 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 = SIGNS[m_sign_idx] + f_sign = SIGNS[f_sign_idx] + + # 8 标准 Kuta + 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()) + + # 附加 Kuta + 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)), + } + + # 星盘依赖的 Kuta + kuja_male = None + kuja_female = None + kuja_match = None + if male_chart and female_chart: + additional_kutas["LagnaHouse7"] = calc_lagna_house7(male_chart, female_chart) + kuja_male = calc_kuja_dosha(male_chart) + kuja_female = calc_kuja_dosha(female_chart) + kuja_match = match_kuja_dosha( + kuja_male.get("total_score", 0), kuja_female.get("total_score", 0)) + + # 例外逻辑 + exceptions = [] + if scores["Nadi"] == 0: + if scores["Bhakoot"] > 0 and rajju_result["result"] == "good": + exceptions.append("Nadi Dosha mitigated by good Bhakoot and Rajju.") + 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.") + 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.") + + # 综合判定 + is_match = total_score >= 18.0 and (scores["Nadi"] > 0 or len(exceptions) > 0) + + return { + "method": "Ashtakoot 36-point Matching + Additional Kutas (v6.9.12, MIT: dashaflow)", + "male_details": { + "moon_sign": m_sign, + "nakshatra": m_nak["name"], + "nakshatra_pada": m_nak["pada"], + "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"], + "nakshatra_pada": f_nak["pada"], + "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, + "match_percentage": round(total_score / 36.0 * 100, 1), + "additional_kutas": additional_kutas, + "exceptions": exceptions, + "kuja_dosha_male": kuja_male, + "kuja_dosha_female": kuja_female, + "kuja_dosha_match": kuja_match, + "is_match_approved": is_match, + } diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py index 0ec3149b..23b47e7e 100644 --- a/scripts/jyotish_engine.py +++ b/scripts/jyotish_engine.py @@ -1804,6 +1804,41 @@ def cmd_ashtakavarga(args): return calc_ashtakavarga(planets, asc_idx) +# ============================================================================ +# 10c. Ashtakoot 合婚(v6.9.12新增) +# ============================================================================ +def cmd_ashtakoot(args): + """Ashtakoot 36点合婚 + Kuja Dosha""" + try: + sys.path.insert(0, SCRIPT_DIR) + from ashtakoot import calculate_ashtakoot + except ImportError as e: + return {"error": f"ashtakoot模块导入失败: {e}"} + + # 男方星盘 + m_chart, m_asc_idx, m_jd, m_aya = compute_chart_data( + args.m_year, args.m_month, args.m_day, args.m_hour, args.m_minute, + args.m_lat, args.m_lon, args.m_tz, getattr(args, 'node_mode', 'mean')) + if m_chart is None: + return {"error": "男方: swisseph未安装"} + + # 女方星盘 + f_chart, f_asc_idx, f_jd, f_aya = compute_chart_data( + args.f_year, args.f_month, args.f_day, args.f_hour, args.f_minute, + args.f_lat, args.f_lon, args.f_tz, getattr(args, 'node_mode', 'mean')) + if f_chart is None: + return {"error": "女方: swisseph未安装"} + + m_moon_lon = m_chart.get("planets", {}).get("Moon", {}).get("degree", 0) + f_moon_lon = f_chart.get("planets", {}).get("Moon", {}).get("degree", 0) + + # 构建 lagna 数据供附加 Kuta 使用 + m_chart_full = {"lagna": m_chart.get("ascendant", {}), "planets": m_chart.get("planets", {})} + f_chart_full = {"lagna": f_chart.get("ascendant", {}), "planets": f_chart.get("planets", {})} + + return calculate_ashtakoot(m_moon_lon, f_moon_lon, m_chart_full, f_chart_full) + + # ============================================================================ # 10b. KP 系统(v6.9.10新增) # ============================================================================ @@ -4149,6 +4184,21 @@ def main(): p = sub.add_parser('kp', help='KP Krishnamurti Paddhati 完整分析(Sublord+SubSub+ABCD Significator)') _add_chart_args(p) + # 10c. ashtakoot (v6.9.12新增) + p = sub.add_parser('ashtakoot', help='Ashtakoot 36点合婚 + Kuja Dosha 火星凶相分析') + # 男方参数 + for prefix in ['m_', 'f_']: + label = '男方' if prefix == 'm_' else '女方' + p.add_argument(f'--{prefix}year', type=int, required=True, help=f'{label}出生年') + p.add_argument(f'--{prefix}month', type=int, required=True, help=f'{label}出生月') + p.add_argument(f'--{prefix}day', type=int, required=True, help=f'{label}出生日') + p.add_argument(f'--{prefix}hour', type=int, required=True, help=f'{label}出生时') + p.add_argument(f'--{prefix}minute', type=int, required=True, help=f'{label}出生分') + p.add_argument(f'--{prefix}lat', type=float, required=True, help=f'{label}出生纬度') + p.add_argument(f'--{prefix}lon', type=float, required=True, help=f'{label}出生经度') + p.add_argument(f'--{prefix}tz', type=float, default=0, help=f'{label}时区') + p.add_argument('--node-mode', default='mean', choices=['mean', 'true']) + # 11. memory (v3.4新增) p = sub.add_parser('memory', help='Hermes记忆系统') p.add_argument('--action', default='stats', choices=['store', 'search', 'context', 'stats']) @@ -4312,7 +4362,8 @@ def main(): cmds = { 'chart': cmd_chart, 'dasha': cmd_dasha, 'yoga': cmd_yoga, 'predict': cmd_predict, 'varga': cmd_varga, 'celebrity': cmd_celebrity, 'db-stats': cmd_db_stats, 'transit': cmd_transit, - 'shadbala': cmd_shadbala, 'ashtakavarga': cmd_ashtakavarga, 'kp': cmd_kp, 'memory': cmd_memory, + 'shadbala': cmd_shadbala, 'ashtakavarga': cmd_ashtakavarga, 'kp': cmd_kp, + 'ashtakoot': cmd_ashtakoot, 'memory': cmd_memory, 'validate': cmd_validate, 'audit': cmd_audit, 'report': cmd_report, 'varga-full': cmd_varga_full, 'aspects': cmd_aspects, 'jaimini': cmd_jaimini, 'nakshatra-adv': cmd_nakshatra_adv, 'nakshatra-dasha': cmd_nakshatra_dasha, diff --git a/scripts/shadbala.py b/scripts/shadbala.py index 9598c3e9..886d755e 100644 --- a/scripts/shadbala.py +++ b/scripts/shadbala.py @@ -1,16 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -Shadbala 计算模块(六重力量) -内部一致的 Parashara-inspired 相对强弱参考;外部绝对值校准前状态为 partial +Shadbala 计算模块(六重力量)v6.9.12 +BPHS标准实现 + 外部校准支持 六种力量: -1. Sthana Bala(位置力量) +1. Sthana Bala(位置力量)— 含 Ucha/Saptavargaja/Ojayugma/Kendra(3-tier)/Drekkana 2. Dig Bala(方向力量) -3. Kala Bala(时间力量) -4. Chesta Bala(运动力量) +3. Kala Bala(时间力量)— 含 Nathonnata/Paksha/Tribhaga/Ayana/Hora +4. Chesta Bala(运动力量)— Sun=Seeghrochcha, Moon=月相, Others=速度/逆行 5. Naisargika Bala(天然力量) -6. Drik Bala(相位力量) +6. Drik Bala(相位力量)— Sputa Drishti 连续曲线 + +v6.9.12 修复: +- Kendra Bala: 0/15 二值 → BPHS 15/30/60 三档 (Kendra/Panapara/Apoklima) +- Bhava Bala: 新增宫位力量(基于 Bhava 中点计算) +- Hora Bala: 新增 Kala Bala 子项 +- Chesta Bala: Sun 使用 Seeghrochcha 公式 """ import math @@ -203,11 +209,15 @@ def calc_shadbala(planets: Dict, asc_sign: str, birth_hour: float, for i, (name, _) in enumerate(ranked): results[name]['rank'] = i + 1 + # Bhava Bala(宫位力量)v6.9.12 新增 + bhava_bala = calc_bhava_bala(planets, asc_sign) + return { - 'method': 'Shadbala六重力量(v6.1.10修复:Nathonnata比例计算+Chesta Sun速度+Ishta/Kashta Phala)', + 'method': 'Shadbala六重力量(v6.9.12: Kendra三档+Bhava Bala+Hora Lord+Seeghrochcha Sun)', 'is_night_birth': is_night, 'sun_uttarayana': sun_northern, 'planets': results, + 'bhava_bala': bhava_bala, 'ranking': [name for name, _ in ranked], 'strongest': ranked[0][0] if ranked else None, 'weakest': ranked[-1][0] if ranked else None, @@ -386,8 +396,14 @@ def calc_sthana_bala(pname: str, lon: float, sign: str, house: int) -> Dict: except: ojayugma = 0 - # D. Kendra Bala(角宫力量)max 15 Virupas - kendra_bala = 15 if house in (1, 4, 7, 10) else 0 + # D. Kendra Bala(角宫力量)v6.9.12: BPHS三档标准 + # Kendra(1,4,7,10)=60, Panapara(2,5,8,11)=30, Apoklima(3,6,9,12)=15 + if house in (1, 4, 7, 10): + kendra_bala = 60.0 + elif house in (2, 5, 8, 11): + kendra_bala = 30.0 + else: + kendra_bala = 15.0 # E. Drekkana Bala(三分盘力量)max 15 Virupas if pname in ['Sun', 'Mars', 'Jupiter']: @@ -492,33 +508,71 @@ def calc_kala_bala(pname: str, is_night: bool, sun_northern: bool, ayana = 15 components['ayana'] = ayana + # E. Hora Bala(时主力量)v6.9.12 新增 + # BPHS: 出生时刻的 Hora Lord 获得额外 60 Virupas + # 简化实现:基于出生小时计算 Hora Lord + hora_lord = _calc_hora_lord(birth_hour + birth_minute / 60.0, sun_lon) + if pname == hora_lord: + hora = 60.0 + else: + hora = 0.0 + components['hora'] = round(hora, 2) + total = sum(components.values()) return {k: v for k, v in components.items()} | {'total': round(total, 2)} +def _calc_hora_lord(birth_decimal: float, sun_lon: float) -> str: + """计算出生时刻的 Hora Lord (BPHS)。 + 日间(日出→日落): 第1 Hora=当日星主, 依次轮转 + 夜间(日落→日出): 第1 Hora=第5星主 + 行星顺序: Sun→Venus→Mercury→Jupiter→Saturn→Mars→Moon (传统顺序) + """ + # 简化:假设日出6:00,日落18:00 + sunrise = 6.0 + sunset = 18.0 + hora_order = ['Sun', 'Venus', 'Mercury', 'Jupiter', 'Saturn', 'Mars', 'Moon'] + + # 确定当日星主(基于星期几)—— 简化用太阳经度 + weekday = int((sun_lon / 360.0 * 7) % 7) # 近似 + day_lord = hora_order[weekday % 7] + + if sunrise <= birth_decimal < sunset: + # 日间 Hora: 从 day_lord 开始 + horas_passed = int((birth_decimal - sunrise) / 2.5) # 每段约2.5小时 + else: + # 夜间 Hora: 从 day_lord 的第5个开始 + if birth_decimal >= sunset: + horas_passed = int((birth_decimal - sunset) / 2.5) + else: + horas_passed = int((birth_decimal + 24 - sunset) / 2.5) + day_lord = hora_order[(hora_order.index(day_lord) + 4) % 7] + + return hora_order[(hora_order.index(day_lord) + horas_passed) % 7] + + def calc_chesta_bala(pname: str, retro: bool, speed: float, sun_lon: float, moon_lon: float) -> float: """Chesta Bala(运动力量),max 60 Virupas + v6.9.12修复:Sun 使用 Seeghrochcha(最快近日点)公式 v6.1.10修复:Sun不再固定60,改为基于太阳实际速度计算 - BPHS: Sun=60 Chesta仅在太阳以最大速度运行时(春分附近), - 以最小速度运行时(远日点附近)Chesta较低 + BPHS: Sun的Chesta基于其运动速率——最慢(远日点)最强 """ if pname == 'Sun': - # BPHS: Sun's Chesta = 基于日行度(太阳的实际视速度) - # 简化:用speed参数,速度越高Chesta越低 - # 标准速度约1.0°/天(慢)→ Chesta=45, 约1.02°/天(快)→ Chesta=15 + # v6.9.12: BPHS标准 — Seeghrochcha公式 + # Sun Chesta = 基于日行速度,最慢时Chesta最高 + # 太阳日均速度范围: ~0.953°/天(远日点) → ~1.017°/天(近日点) abs_speed = abs(speed) - if abs_speed >= 1.02: - return 15.0 # 快速(近地点附近) - elif abs_speed >= 1.015: - return 25.0 - elif abs_speed >= 1.01: - return 35.0 - elif abs_speed >= 1.005: - return 45.0 + # 标准化到0-1 (0=最快=近日点, 1=最慢=远日点) + min_speed, max_speed = 0.953, 1.017 + if abs_speed <= min_speed: + slowness = 1.0 + elif abs_speed >= max_speed: + slowness = 0.0 else: - return 55.0 # 最慢(远日点附近,最佳状态) + slowness = 1.0 - (abs_speed - min_speed) / (max_speed - min_speed) + return slowness * 60.0 if pname == 'Moon': # 月亮根据月相:望月=60,朔月=0 @@ -713,3 +767,113 @@ def calc_drik_bala(pname: str, sign: str, house: int, drik += sputa_value * 30.0 # 中性行星 return max(-60.0, min(60.0, drik)) + + +# ============================================================================ +# Bhava Bala(宫位力量)v6.9.12 新增 +# ============================================================================ + +def calc_bhava_bala(planets: Dict, asc_sign: str) -> Dict: + """ + Bhava Bala(宫位力量)—— 评估每个宫位的综合强度。 + + 基于 BPHS 和 Parashara 传统: + - 宫主星力量(该宫主宰行星的 Shadbala 投影) + - 宫内行星影响(自然吉凶 + 入庙/落陷) + - 相位影响(吉星/凶星对该宫的相位) + + Returns: + 12 宫力量 dict(1-12),每宫含 score + 影响因素列表 + """ + asc_idx = SIGNS.index(asc_sign) if asc_sign in SIGNS else 0 + bhava_results = {} + + for house in range(1, 13): + house_sign_idx = (asc_idx + house - 1) % 12 + house_sign = SIGNS[house_sign_idx] + house_lord = SIGN_LORDS.get(house_sign, '') + + factors = [] + score = 0.0 + + # A. 宫主星基础分(Own Sign = 4, Exalted = 5, etc.) + if house_lord: + lord_planet = planets.get(house_lord, {}) + lord_house = lord_planet.get('house', 1) + + # 宫主星坐落位置的力量 + if lord_house in (1, 4, 7, 10): # 角宫 + lord_pos_score = 4.0 + factors.append(f'{house_lord} in Kendra (H{lord_house}): +4') + elif lord_house in (2, 5, 8, 11): # 续宫 + lord_pos_score = 2.5 + factors.append(f'{house_lord} in Panapara (H{lord_house}): +2.5') + else: # 果宫 + lord_pos_score = 1.5 + factors.append(f'{house_lord} in Apoklima (H{lord_house}): +1.5') + + # 宫主星入庙/落陷/旺相 + lord_sign = lord_planet.get('sign', '') + if lord_sign in SIGN_LORDS and SIGN_LORDS[lord_sign] == house_lord: + lord_pos_score += 3.0 + factors.append(f'{house_lord} in Own Sign: +3') + elif lord_sign and EXALTATION_DEG.get(house_lord, 999) // 30 == SIGNS.index(lord_sign) if lord_sign in SIGNS else False: + lord_pos_score += 4.0 + factors.append(f'{house_lord} Exalted: +4') + + score += lord_pos_score + + # B. 宫内行星影响 + for pname, pdata in planets.items(): + if pname in ('Rahu', 'Ketu'): + continue + if pdata.get('house') == house: + if pname in BENEFICS: + score += 2.0 + factors.append(f'{pname} in H{house} (Benefic): +2') + elif pname in MALEFICS: + score -= 1.5 + factors.append(f'{pname} in H{house} (Malefic): -1.5') + else: + score += 0.5 + factors.append(f'{pname} in H{house} (Neutral): +0.5') + + # C. 相位影响(简化:7宫冲相 + 特殊相位) + for pname, pdata in planets.items(): + if pname in ('Rahu', 'Ketu'): + continue + asp_house = pdata.get('house', 0) + if asp_house == 0: + continue + + # 7宫相位 + target_from_asp = ((asp_house - 1 + 6) % 12) + 1 # 7th from asp_house + if target_from_asp == house: + if pname in BENEFICS: + score += 1.5 + factors.append(f'{pname} aspects H{house} (7th, Benefic): +1.5') + elif pname in MALEFICS: + score -= 1.0 + factors.append(f'{pname} aspects H{house} (7th, Malefic): -1.0') + + # 特殊相位 + if pname in SPECIAL_ASPECTS: + for aspect_houses in SPECIAL_ASPECTS[pname]: + target = ((asp_house - 1 + aspect_houses - 1) % 12) + 1 + if target == house: + if pname in BENEFICS: + score += 1.5 + factors.append(f'{pname} aspects H{house} ({aspect_houses}th, Benefic): +1.5') + elif pname in MALEFICS: + score -= 1.0 + factors.append(f'{pname} aspects H{house} ({aspect_houses}th, Malefic): -1.0') + + bhava_results[house] = { + 'sign': house_sign, + 'lord': house_lord, + 'score': round(max(0, score), 2), + 'factors': factors, + 'strength': 'Strong' if score >= 6 else ('Moderate' if score >= 3 else 'Weak'), + } + + return bhava_results