diff --git a/CHANGELOG.md b/CHANGELOG.md index 35154a64..12f907d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,41 @@ # 印度占星 Skill 更新日志 +## v6.0.21-muhurta(2026-06-04)—— Muhurta 择时占星(Panchanga 五要素)实现 + +> **触发原因**:用户说「继续」,Muhurta 是审计中确认缺失的独立技法,也是日常使用频率最高的传统技法之一。 + +### 变更内容 + +- `scripts/muhurta.py`(新文件 v6.0.21):Muhurta 核心计算 + - `calc_tithi(sun_lon, moon_lon)` — 月相日(1-30,Shukla/Krishna) + - `calc_nakshatra_from_lon(lon)` — 星宿(27宿,Laghu/Sthira/Mridu/Ugra/Tikshna/Chara) + - `calc_yoga(sun_lon, moon_lon)` — 瑜伽(27 Yoga,日月之和) + - `calc_karana(sun_lon, moon_lon)` — 迦那(11 Karana,半 Tithi;含 Vishti/Bhadra 警告) + - `calc_vara(weekday)` — 周日(7 Vara,含 Hora 计算) + - `calc_panchanga(...)` — 五要素综合评分(吉凶百分比) + - `check_activity_muhurta(panchanga, activity)` — 活动适宜性检查(5类:婚礼/开业/出行/医疗/教育) + - `muhurta_full_report(...)` — 完整 Muhurta 报告 + - `_approx_sun_moon_lon(year, month, day)` — 近似算法(无 swisseph 时) +- `scripts/cmd_muhurta.py`(新文件 v6.0.21):`muhurta` 子命令 + - `--date` — 指定日期(默认今天) + - `--activity` — 活动类型过滤(marriage/business/travel/medical/education) + - `--scan-days` — 多天扫描模式 + - `--hour-from-sunrise` — 指定时段 +- `scripts/jyotish_engine.py` 更新:注册 `muhurta` 子命令 +- `references/technique_registry.json` 更新:新增 muhurta(covered, 41→42 entries) + +### 技法验证(2026-06-04 今日) + +| 要素 | 值 | 吉凶 | +|------|-----|------| +| Tithi | Krishna Tritiya | 吉 | +| Nakshatra | Purva Ashadha | 凶 | +| Yoga | Shukla | 吉 | +| Karana | Vishti(Bhadra)| 凶⚠️ | +| Vara | Thursday/Jupiter | 吉 | + +综合 50%(中等);适合婚礼/出行/医疗/学习,开业一般,注意 Vishti 时段。 + ## v6.0.20-narayana-dasha(2026-06-04)—— Narayana Dasha(Rishi Dasha)实现 > **触发原因**:用户说"继续",Narayana Dasha 是文章审计中确认缺失的独立大运系统,与 Vimshottari 互补。 diff --git a/SKILL.md b/SKILL.md index 9fb28500..82bcb0af 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,6 +1,6 @@ --- name: jyotish-vedic-astrology -version: 6.0.20 +version: 6.0.21 description: 印度占星(Jyotish)专业解盘与推运系统。核心能力:PDF星盘输入→严谨解盘→精确推运应期输出。触发词:印度占星、吠陀占星、Jyotish、解盘、推运、星盘分析、Dasha、Transit、Nakshatra、Yoga、出生时间矫正、PDF星盘、读取PDF、分析PDF星盘、现代解读、误判纠错、Varga分盘、综合分析、过境分析、合盘、婚姻匹配、年运盘、Prashna、Argala、Jaimini、Shadbala、Ashtakavarga、HTML报告、深度解盘。 --- diff --git a/references/technique_registry.json b/references/technique_registry.json index 1baf3e6f..f4319df5 100644 --- a/references/technique_registry.json +++ b/references/technique_registry.json @@ -1,5 +1,5 @@ { - "version": "v6.0.20-narayana-dasha", + "version": "v6.0.21-muhurta", "source_inspiration": [ { "name": "jyotishyamitra", @@ -840,6 +840,31 @@ ], "audit_label": "Narayana Dasha", "missing_impact": "N/A - implemented in v6.0.20" + }, + "muhurta": { + "name": "Muhurta (Panchanga / Auspicious Timing)", + "domains": [ + "muhurta", + "panchanga", + "electional", + "tithi", + "nakshatra", + "vara", + "yoga", + "karana" + ], + "status": "covered", + "knowledge_refs": [ + "muhurta-complete-guide.md" + ], + "commands": [ + "muhurta" + ], + "output_paths": [ + "modules.muhurta" + ], + "audit_label": "Muhurta", + "missing_impact": "N/A - implemented in v6.0.21" } }, "routes": { @@ -986,7 +1011,8 @@ "prashna_integration", "bhrigu_pada_dasha", "solar_return", - "narayana_dasha" + "narayana_dasha", + "muhurta" ], "optional_techniques": [ "bhava_chalit", diff --git a/scripts/cmd_muhurta.py b/scripts/cmd_muhurta.py new file mode 100644 index 00000000..0be26ed6 --- /dev/null +++ b/scripts/cmd_muhurta.py @@ -0,0 +1,178 @@ +""" +cmd_muhurta.py v6.0.21 — muhurta 子命令实现 + +用法: + python jyotish_engine.py muhurta --date 2026-06-15 --activity marriage + python jyotish_engine.py muhurta --date 2026-06-15 (全部活动) + python jyotish_engine.py muhurta --scan-days 7 --activity business (扫描7天) + +对于完整解盘(full-reading),也会附带当日 Muhurta 基本信息。 +""" + +from __future__ import annotations +from typing import Dict, Optional, List +from datetime import datetime, timedelta +import sys +import os + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + + +def _weekday_to_vara(py_weekday: int) -> int: + """Python weekday (0=Mon) → Vara index (0=Sun)""" + return (py_weekday + 1) % 7 + + +def _get_sun_moon_lons(year: int, month: int, day: int, + hour: int = 12) -> tuple: + """获取太阳/月亮恒星黄经。优先 swisseph,退化为近似算法。""" + try: + import swisseph as swe + swe.set_sid_mode(swe.SIDM_LAHIRI) + import math + jd_ut = swe.julday(year, month, day, hour) + sun_res = swe.calc_ut(jd_ut, swe.SUN, swe.FLG_SIDEREAL) + moon_res = swe.calc_ut(jd_ut, swe.MOON, swe.FLG_SIDEREAL) + return sun_res[0], moon_res[0], True + except Exception: + pass + # 近似算法 + from muhurta import _approx_sun_moon_lon + sun_lon, moon_lon = _approx_sun_moon_lon(year, month, day) + return sun_lon, moon_lon, False + + +def _format_panchanga_summary(panchanga: Dict) -> List[str]: + lines = [] + p = panchanga + lines.append(f" Tithi: {p['tithi']['full_name']} ({p['tithi']['quality']})") + lines.append(f" Nakshatra: {p['nakshatra']['nakshatra']} [{p['nakshatra']['type']}] ({p['nakshatra']['quality']})") + lines.append(f" Yoga: {p['yoga']['yoga']} ({p['yoga']['quality']})") + lines.append(f" Karana: {p['karana']['karana']} ({p['karana']['quality']})") + lines.append(f" Vara: {p['vara']['vara']} / {p['vara']['vara_lord']} ({p['vara']['quality']})") + lines.append(f" Hora: {p['hora']['hora_lord']} ({p['hora']['quality']})") + lines.append(f" 综合评分: {p['overall_quality']} ({p['overall_score']:.0%},吉 {p['auspicious_count']}/{p['total_elements']})") + if p['warnings']: + for w in p['warnings']: + lines.append(f" {w}") + return lines + + +def _format_activity_checks(activity_checks: Dict, target_activity: Optional[str] = None) -> List[str]: + lines = [] + ACTIVITY_NAMES_ZH = { + 'marriage': '婚礼/伴侣(Vivaha)', + 'business': '开业/签约(Vyapar)', + 'travel': '出行(Yatra)', + 'medical': '手术/医疗(Chikitsa)', + 'education': '学习/入学(Vidyarambha)', + } + for act, chk in activity_checks.items(): + if target_activity and act != target_activity: + continue + zh_name = ACTIVITY_NAMES_ZH.get(act, act) + verdict = chk.get('verdict', '未知') + icon = '✅' if '大吉' in verdict or ('吉' in verdict and '不宜' not in verdict) else \ + '⚠️' if '一般' in verdict or '中' in verdict else '❌' + lines.append(f" {icon} {zh_name}: {verdict}") + if chk.get('notes'): + for note in chk['notes'][:3]: + lines.append(f" → {note}") + return lines + + +def cmd_muhurta(args, chart_data: Optional[Dict] = None) -> int: + """ + Muhurta 子命令主入口。 + + args 属性: + args.date: '2026-06-15'(查询日期,默认今天) + args.activity: 活动类型(可选,默认全部) + args.scan_days: 扫描天数(可选,>1 时扫描多天) + args.hour_from_sunrise: 从日出起算的小时(默认 6h) + """ + from muhurta import muhurta_full_report, ACTIVITY_RULES + + # 解析日期 + if hasattr(args, 'date') and args.date: + try: + query_dt = datetime.strptime(args.date, '%Y-%m-%d') + except ValueError: + print(f"[ERROR] 日期格式错误: {args.date},请用 YYYY-MM-DD") + return 1 + else: + query_dt = datetime.now() + + hour_from_sunrise = getattr(args, 'hour_from_sunrise', 6.0) + target_activity = getattr(args, 'activity', None) + scan_days = getattr(args, 'scan_days', 1) + + activities = list(ACTIVITY_RULES.keys()) + if target_activity and target_activity not in activities: + print(f"[ERROR] 未知活动: {target_activity}。支持: {', '.join(activities)}") + return 1 + + print("=" * 64) + print("🕐 Muhurta 择时分析(印度吉凶历)") + print("=" * 64) + + dates_to_check = [query_dt + timedelta(days=i) for i in range(max(1, int(scan_days)))] + + for dt in dates_to_check: + sun_lon, moon_lon, has_swe = _get_sun_moon_lons( + dt.year, dt.month, dt.day, int(hour_from_sunrise + 6) # rough solar hour + ) + vara_idx = _weekday_to_vara(dt.weekday()) + date_str = dt.strftime('%Y-%m-%d') + + report = muhurta_full_report( + sun_lon=sun_lon, + moon_lon=moon_lon, + weekday=vara_idx, + hour_from_sunrise=hour_from_sunrise, + query_date_str=date_str, + activities=activities, + ) + + precision_note = '' if has_swe else '(近似值,±2°精度)' + print(f"\n📅 {date_str} {precision_note}") + print(f" Sun: {sun_lon:.1f}° Moon: {moon_lon:.1f}°") + print() + + print("── Panchanga 五要素 ──") + for line in _format_panchanga_summary(report['panchanga']): + print(line) + print() + + print("── Abhijit Muhurta ──") + abh = report['abhijit_muhurta'] + print(f" {abh['description']}") + print(f" 持续: {abh['duration_minutes']} 分钟 | {abh['warning']}") + print() + + print("── 活动适宜性 ──") + for line in _format_activity_checks(report['activity_checks'], target_activity): + print(line) + print() + + summary = report['summary'] + best = summary.get('best_activities', []) + avoid = summary.get('avoid_activities', []) + if best: + print(f" ✨ 适宜: {', '.join(best)}") + if avoid: + print(f" 🚫 不宜: {', '.join(avoid)}") + + if len(dates_to_check) > 1: + print("-" * 40) + + print() + print("━" * 64) + print("注意事项:") + print(" 1. 本分析基于恒星黄经(Lahiri Ayanamsa),为 Parashari 传统") + print(" 2. 精确 Muhurta 还需考虑出生盘 Lagna 与目标时间的相位关系") + print(" 3. 建议结合 Ascendant 力量、吉星位置做综合判断") + if not has_swe: + print(" 4. ⚠️ 当前使用近似算法(swisseph 未安装),精度约 ±2°") + print("━" * 64) + return 0 diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py index 867e639d..5841cd93 100644 --- a/scripts/jyotish_engine.py +++ b/scripts/jyotish_engine.py @@ -63,6 +63,7 @@ except ImportError: HAS_SWE = False from cmd_solar_return import cmd_solar_return # v6.0.18 from cmd_narayana_dasha import cmd_narayana_dasha # v6.0.20 +from cmd_muhurta import cmd_muhurta # v6.0.21 # ============================================================================ # 常量 @@ -4133,6 +4134,16 @@ def main(): _add_chart_args(p) p.add_argument('--age', type=float, default=None, help='当前年龄(用于定位大运位置)') + # 21.7 muhurta (v6.0.21新增) + p = sub.add_parser('muhurta', help='Muhurta 择时分析(Panchanga 五要素)') + p.add_argument('--date', default=None, help='查询日期 YYYY-MM-DD(默认今天)') + p.add_argument('--activity', default=None, + choices=['marriage', 'business', 'travel', 'medical', 'education'], + help='指定活动类型(默认检查所有)') + p.add_argument('--scan-days', type=int, default=1, help='扫描天数(默认1天,最多30天)') + p.add_argument('--hour-from-sunrise', type=float, default=6.0, + help='从日出起算的小时数(默认6h约正午)') + # 21. synastry (v3.7新增) p = sub.add_parser('synastry', help='合盘分析(Ashta Koota 36分制)') p.add_argument('--moon1', type=float, required=True, help='Person1月亮黄经') @@ -4197,7 +4208,8 @@ def main(): '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, 'argala': cmd_argala, 'tajika': cmd_tajika, - 'synastry': cmd_synastry, 'solar-return': cmd_solar_return, 'narayana-dasha': cmd_narayana_dasha, + 'synastry': cmd_synastry, 'solar-return': cmd_solar_return, + 'narayana-dasha': cmd_narayana_dasha, 'muhurta': cmd_muhurta, 'full-reading': cmd_full_reading, 'prashna': cmd_prashna, 'double-transit-pac': cmd_double_transit_pac, 'transit-ll7l': cmd_transit_ll7l, 'planetary-congregation': cmd_planetary_congregation, diff --git a/scripts/muhurta.py b/scripts/muhurta.py new file mode 100644 index 00000000..26d40dee --- /dev/null +++ b/scripts/muhurta.py @@ -0,0 +1,628 @@ +""" +muhurta.py v6.0.21 — Muhurta(择时占星)核心计算模块 + +Muhurta 是印度占星的择时系统,核心是 Panchanga 五要素: + 1. Tithi(月相日) — 月亮与太阳之间的角度 / 12° + 2. Vara(周日) — 星期对应的行星守护 + 3. Nakshatra(星宿)— 月亮所在星宿 + 4. Yoga(瑜伽) — 太阳 + 月亮黄经之和 / (360/27) + 5. Karana(半日) — 每半个 Tithi 为一 Karana + +每个元素都有吉(Subha)/ 凶(Asubha)/ 中性(Mixed)属性, +组合评分决定特定时间段是否适合某类活动。 +""" + +from __future__ import annotations +from typing import Dict, List, Optional, Tuple +from datetime import datetime, timedelta +import math + +# ── Vara(周日行星)────────────────────────────────────────────────── +VARA_LORDS = { + 0: ('Sunday', 'Sun', 'asubha'), # 周日 + 1: ('Monday', 'Moon', 'subha'), + 2: ('Tuesday', 'Mars', 'asubha'), + 3: ('Wednesday', 'Mercury', 'mixed'), + 4: ('Thursday', 'Jupiter', 'subha'), + 5: ('Friday', 'Venus', 'subha'), + 6: ('Saturday', 'Saturn', 'asubha'), +} + +# Hora(每小时行星)— 从日出起每小时依次排列 +# 顺序: Sun, Venus, Mercury, Moon, Saturn, Jupiter, Mars +HORA_ORDER = ['Sun', 'Venus', 'Mercury', 'Moon', 'Saturn', 'Jupiter', 'Mars'] +# 每日起始 Hora = Vara Lord 在 HORA_ORDER 中的位置 +VARA_START_IDX = { + 'Sun': 0, 'Venus': 1, 'Mercury': 2, 'Moon': 3, + 'Saturn': 4, 'Jupiter': 5, 'Mars': 6 +} + +# ── Tithi(月相日)──────────────────────────────────────────────────── +# 1-15 = Shukla Paksha, 16-30 = Krishna Paksha +TITHI_NAMES = [ + '', 'Pratipada', 'Dwitiya', 'Tritiya', 'Chaturthi', 'Panchami', + 'Shashthi', 'Saptami', 'Ashtami', 'Navami', 'Dashami', + 'Ekadashi', 'Dwadashi', 'Trayodashi', 'Chaturdashi', 'Purnima/Amavasya' +] +# 吉凶:1=subha, 0=asubha, 0.5=mixed +TITHI_QUALITY = { + 1: 'subha', 2: 'subha', 3: 'subha', 4: 'asubha', 5: 'subha', + 6: 'mixed', 7: 'subha', 8: 'asubha', 9: 'mixed', 10: 'subha', + 11: 'subha', 12: 'subha', 13: 'asubha', 14: 'asubha', + 15: 'subha', # Purnima = Shukla 15(满月) + 16: 'subha', # Pratipada Krishna + 17: 'subha', 18: 'subha', 19: 'asubha', 20: 'subha', + 21: 'mixed', 22: 'subha', 23: 'asubha', 24: 'mixed', 25: 'subha', + 26: 'subha', 27: 'subha', 28: 'asubha', 29: 'asubha', + 30: 'asubha' # Amavasya(新月) +} + +# ── Nakshatra(27 星宿)────────────────────────────────────────────── +NAKSHATRAS = [ + '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', 'Dhanishtha', + 'Shatabhisha', 'Purva Bhadrapada', 'Uttara Bhadrapada', 'Revati' +] +# Nakshatra 吉凶分类(Muhurta 视角) +NAKSHATRA_TYPE = { + 'Ashwini': 'laghu', # 轻快 → 手术、旅行 + 'Bharani': 'ugra', # 凶猛 → 不利开始 + 'Krittika': 'mixed', # 混合 + 'Rohini': 'sthira', # 固定/吉 → 种植、建筑 + 'Mrigashira': 'mridu', # 柔和 → 艺术、爱情 + 'Ardra': 'tikshna', # 尖锐 → 不宜重要事 + 'Punarvasu': 'chara', # 动态 → 旅行 + 'Pushya': 'laghu', # 最吉 → 几乎万能 + 'Ashlesha': 'tikshna', # 蛇宿 → 不宜 + 'Magha': 'ugra', # 凶 → 不宜 + 'Purva Phalguni': 'ugra',# 凶 + 'Uttara Phalguni': 'sthira', # 吉 + 'Hasta': 'laghu', # 轻快/吉 + 'Chitra': 'mridu', # 柔和 + 'Swati': 'chara', # 动态 + 'Vishakha': 'mixed', # 混合 + 'Anuradha': 'mridu', # 柔和 + 'Jyeshtha': 'tikshna', # 尖锐 + 'Mula': 'tikshna', # 最凶 → 不宜开始 + 'Purva Ashadha': 'ugra', # 凶 + 'Uttara Ashadha': 'sthira', # 吉 + 'Shravana': 'mridu', # 柔和/吉 + 'Dhanishtha': 'chara', # 动态 + 'Shatabhisha': 'chara', # 动态 + 'Purva Bhadrapada': 'ugra', # 凶 + 'Uttara Bhadrapada': 'sthira',# 吉 + 'Revati': 'mridu', # 柔和 +} +NAKSHATRA_QUALITY = { + 'laghu': 'subha', 'sthira': 'subha', 'mridu': 'subha', 'chara': 'mixed', + 'mixed': 'mixed', 'ugra': 'asubha', 'tikshna': 'asubha' +} + +# ── Yoga(27 瑜伽)──────────────────────────────────────────────────── +YOGA_NAMES = [ + 'Vishkambha', 'Priti', 'Ayushman', 'Saubhagya', 'Shobhana', + 'Atiganda', 'Sukarma', 'Dhriti', 'Shula', 'Ganda', + 'Vriddhi', 'Dhruva', 'Vyaghata', 'Harshana', 'Vajra', + 'Siddhi', 'Vyatipata', 'Variyana', 'Parigha', 'Shiva', + 'Siddha', 'Sadhya', 'Shubha', 'Shukla', 'Brahma', + 'Aindra', 'Vaidhriti' +] +YOGA_QUALITY = { + 'Vishkambha': 'asubha', 'Priti': 'subha', 'Ayushman': 'subha', + 'Saubhagya': 'subha', 'Shobhana': 'subha', 'Atiganda': 'asubha', + 'Sukarma': 'subha', 'Dhriti': 'subha', 'Shula': 'asubha', + 'Ganda': 'asubha', 'Vriddhi': 'subha', 'Dhruva': 'subha', + 'Vyaghata': 'asubha', 'Harshana': 'subha', 'Vajra': 'asubha', + 'Siddhi': 'subha', 'Vyatipata': 'asubha', 'Variyana': 'subha', + 'Parigha': 'asubha', 'Shiva': 'subha', 'Siddha': 'subha', + 'Sadhya': 'subha', 'Shubha': 'subha', 'Shukla': 'subha', + 'Brahma': 'subha', 'Aindra': 'subha', 'Vaidhriti': 'asubha' +} + +# ── Karana(11 迦那)──────────────────────────────────────────────── +# 7 个 movable + 4 个 fixed +KARANA_NAMES = [ + 'Bava', 'Balava', 'Kaulava', 'Taitila', 'Garija', + 'Vanija', 'Vishti', # 7 movable(循环8次) + 'Shakuni', 'Chatushpada', 'Naga', 'Kimstughna' # 4 fixed +] +KARANA_QUALITY = { + 'Bava': 'subha', 'Balava': 'subha', 'Kaulava': 'subha', + 'Taitila': 'subha', 'Garija': 'subha', 'Vanija': 'subha', + 'Vishti': 'asubha', # Bhadra(Vishti)最凶 + 'Shakuni': 'mixed', 'Chatushpada': 'mixed', + 'Naga': 'asubha', 'Kimstughna': 'subha' +} + + +# ── 核心计算函数 ───────────────────────────────────────────────────── + +def calc_tithi(sun_lon: float, moon_lon: float) -> Dict: + """计算 Tithi(月相日)。 + + sun_lon, moon_lon: 恒星黄经(Lahiri,0-360) + 返回: tithi_num(1-30), paksha, name, quality + """ + diff = (moon_lon - sun_lon) % 360 + tithi_num = int(diff / 12) + 1 # 1-30 + if tithi_num > 30: + tithi_num = 30 + + paksha = 'Shukla' if tithi_num <= 15 else 'Krishna' + tithi_in_paksha = tithi_num if tithi_num <= 15 else tithi_num - 15 + + name = TITHI_NAMES[min(tithi_in_paksha, 15)] + if tithi_num == 15: + name = 'Purnima' + elif tithi_num == 30: + name = 'Amavasya' + + quality = TITHI_QUALITY.get(tithi_num, 'mixed') + return { + 'tithi_num': tithi_num, + 'paksha': paksha, + 'tithi_in_paksha': tithi_in_paksha, + 'name': name, + 'full_name': f'{paksha} {name}', + 'quality': quality, + 'moon_sun_diff': round(diff, 2) + } + + +def calc_nakshatra_from_lon(lon: float) -> Dict: + """从黄经计算星宿。""" + lon = lon % 360 + idx = int(lon / (360 / 27)) + pada = int((lon % (360 / 27)) / (360 / 108)) + 1 + name = NAKSHATRAS[idx] + ntype = NAKSHATRA_TYPE.get(name, 'mixed') + quality = NAKSHATRA_QUALITY.get(ntype, 'mixed') + return { + 'nakshatra': name, + 'nakshatra_idx': idx, + 'pada': pada, + 'type': ntype, + 'quality': quality, + 'moon_lon': round(lon, 2) + } + + +def calc_yoga(sun_lon: float, moon_lon: float) -> Dict: + """计算 Yoga(日月之和的 27 分之一)。""" + total = (sun_lon + moon_lon) % 360 + idx = int(total / (360 / 27)) + if idx >= 27: + idx = 26 + name = YOGA_NAMES[idx] + quality = YOGA_QUALITY.get(name, 'mixed') + return { + 'yoga': name, + 'yoga_idx': idx, + 'quality': quality, + 'sun_moon_sum': round(total, 2) + } + + +def calc_karana(sun_lon: float, moon_lon: float) -> Dict: + """计算 Karana(半 Tithi)。 + + Karana 序列: + - Kimstughna(fixed, 只在 Krishna 30 Tithi 前半) + - 7 movable karanas × 8 = 56 + - Shakuni/Chatushpada/Naga/Kimstughna(fixed, 只在最后) + 共 60 个 half-tithis + """ + diff = (moon_lon - sun_lon) % 360 + half_tithi = diff / 6 # 0-60 + + # Karana 编号(0-59) + k_num = int(half_tithi) + + if k_num == 0: + name = 'Kimstughna' # Fixed, first half of Shukla 1 + elif 1 <= k_num <= 56: + idx = (k_num - 1) % 7 + name = KARANA_NAMES[idx] + elif k_num == 57: + name = 'Shakuni' + elif k_num == 58: + name = 'Chatushpada' + elif k_num == 59: + name = 'Naga' + else: + name = 'Kimstughna' + + quality = KARANA_QUALITY.get(name, 'mixed') + return { + 'karana': name, + 'karana_num': k_num, + 'quality': quality, + 'is_vishti': name == 'Vishti' # Vishti = Bhadra,最凶 + } + + +def calc_vara(weekday: int) -> Dict: + """计算 Vara(weekday: 0=Sun, 1=Mon, ..., 6=Sat)。""" + info = VARA_LORDS.get(weekday % 7, ('Unknown', 'Unknown', 'mixed')) + return { + 'vara': info[0], + 'vara_lord': info[1], + 'quality': info[2], + 'weekday_idx': weekday % 7 + } + + +def calc_hora(weekday: int, hour_from_sunrise: float) -> Dict: + """计算当前 Hora(日出后的小时序号)。 + + weekday: 0=Sun, ..., 6=Sat + hour_from_sunrise: 从日出起算的小时数(浮点) + """ + vara_lord = VARA_LORDS[weekday % 7][1] + start_idx = VARA_START_IDX.get(vara_lord, 0) + hora_offset = int(hour_from_sunrise) % 24 + hora_lord = HORA_ORDER[(start_idx + hora_offset) % 7] + hora_quality = 'subha' if hora_lord in ('Jupiter', 'Venus', 'Mercury') else \ + 'mixed' if hora_lord == 'Moon' else 'asubha' + return { + 'hora_lord': hora_lord, + 'hora_num': hora_offset + 1, + 'quality': hora_quality, + 'hora_from_sunrise': round(hour_from_sunrise, 2) + } + + +def calc_abhijit_muhurta(sunrise_ut: Optional[float] = None, + sunset_ut: Optional[float] = None) -> Dict: + """ + 计算 Abhijit Muhurta(最吉祥的时刻,正午±24分钟)。 + + Abhijit = 8/15 * daytime(从日出到日落的 8/15 处),持续约 48 分钟。 + 注意:周三(Wednesday)Abhijit 不吉,应避免使用。 + + 参数为 JD UT(可选),缺省时给出相对说明。 + """ + result = { + 'description': 'Abhijit Muhurta 是一天中最吉祥的时段(正午前后各24分钟)', + 'rule': '日升到日落共15个 muhurta,第8个(中间)即 Abhijit', + 'duration_minutes': 48, + 'warning': '周三(Wednesday/Budha Vara)不宜使用 Abhijit', + } + if sunrise_ut is not None and sunset_ut is not None: + day_dur = sunset_ut - sunrise_ut # in JD (days) + abhijit_start_jd = sunrise_ut + day_dur * (7 / 15) + abhijit_end_jd = sunrise_ut + day_dur * (8 / 15) + result['abhijit_start_jd'] = round(abhijit_start_jd, 6) + result['abhijit_end_jd'] = round(abhijit_end_jd, 6) + result['abhijit_start_offset_min'] = round(day_dur * (7 / 15) * 24 * 60, 1) + result['abhijit_end_offset_min'] = round(day_dur * (8 / 15) * 24 * 60, 1) + return result + + +def calc_panchanga(sun_lon: float, moon_lon: float, weekday: int, + hour_from_sunrise: float = 6.0) -> Dict: + """ + 计算 Panchanga 五要素(所有输入均为恒星坐标 Lahiri)。 + + 参数: + sun_lon: 太阳恒星黄经 + moon_lon: 月亮恒星黄经 + weekday: 0=Sun, ..., 6=Sat + hour_from_sunrise: 从日出起算的小时数(默认 6h,约正午) + """ + tithi = calc_tithi(sun_lon, moon_lon) + nakshatra = calc_nakshatra_from_lon(moon_lon) + yoga = calc_yoga(sun_lon, moon_lon) + karana = calc_karana(sun_lon, moon_lon) + vara = calc_vara(weekday) + hora = calc_hora(weekday, hour_from_sunrise) + + # 综合吉凶评分 + elements = [ + ('Tithi', tithi['quality']), + ('Vara', vara['quality']), + ('Nakshatra', nakshatra['quality']), + ('Yoga', yoga['quality']), + ('Karana', karana['quality']), + ('Hora', hora['quality']), + ] + score_map = {'subha': 1.0, 'mixed': 0.5, 'asubha': 0.0} + total_score = sum(score_map[q] for _, q in elements) + max_score = len(elements) + score_pct = total_score / max_score + + if score_pct >= 0.75: + overall = '吉(Subha)' + elif score_pct >= 0.5: + overall = '中(Mixed)' + else: + overall = '凶(Asubha)' + + # 特殊凶时段检查 + warnings = [] + if karana['is_vishti']: + warnings.append('⚠️ Vishti(Bhadra)时段——最凶,避免重要开始') + if tithi['name'] == 'Amavasya': + warnings.append('⚠️ Amavasya(新月)——不宜开始新事') + if nakshatra['type'] == 'tikshna': + warnings.append(f'⚠️ {nakshatra["nakshatra"]} 为 Tikshna(尖锐)星宿——不宜立约、开业') + if yoga['quality'] == 'asubha': + warnings.append(f'⚠️ {yoga["yoga"]} Yoga——不利时段') + + return { + 'tithi': tithi, + 'nakshatra': nakshatra, + 'yoga': yoga, + 'karana': karana, + 'vara': vara, + 'hora': hora, + 'overall_score': round(score_pct, 2), + 'overall_quality': overall, + 'warnings': warnings, + 'auspicious_count': sum(1 for _, q in elements if q == 'subha'), + 'total_elements': len(elements), + } + + +# ── 活动适宜性规则库 ────────────────────────────────────────────────── + +ACTIVITY_RULES = { + 'marriage': { + 'name': '婚礼(Vivaha)', + 'good_tithis': [2, 3, 5, 7, 10, 11, 12, 13, 15], # 吉 Tithi + 'bad_tithis': [4, 8, 9, 14, 29, 30], + 'good_nakshatras': ['Rohini', 'Mrigashira', 'Magha', 'Uttara Phalguni', + 'Hasta', 'Swati', 'Anuradha', 'Mula', 'Uttara Ashadha', + 'Uttara Bhadrapada', 'Revati'], + 'bad_nakshatras': ['Bharani', 'Ardra', 'Ashlesha', 'Jyeshtha'], + 'good_varas': ['Monday', 'Wednesday', 'Friday', 'Thursday'], + 'bad_varas': ['Tuesday', 'Saturday'], + }, + 'business': { + 'name': '开业/签约(Vyapar)', + 'good_tithis': [2, 3, 5, 7, 10, 11, 12], + 'bad_tithis': [4, 8, 9, 14, 29, 30], + 'good_nakshatras': ['Ashwini', 'Rohini', 'Mrigashira', 'Punarvasu', + 'Pushya', 'Hasta', 'Chitra', 'Swati', 'Anuradha', + 'Shravana', 'Dhanishtha', 'Revati'], + 'bad_nakshatras': ['Bharani', 'Ardra', 'Ashlesha', 'Magha', 'Mula', + 'Purva Ashadha', 'Purva Phalguni', 'Purva Bhadrapada'], + 'good_varas': ['Monday', 'Wednesday', 'Thursday', 'Friday'], + 'bad_varas': ['Tuesday', 'Saturday', 'Sunday'], + }, + 'travel': { + 'name': '出行(Yatra)', + 'good_tithis': [2, 3, 5, 7, 10, 12], + 'bad_tithis': [4, 8, 9, 14, 30], + 'good_nakshatras': ['Ashwini', 'Mrigashira', 'Punarvasu', 'Pushya', + 'Hasta', 'Chitra', 'Swati', 'Shravana', 'Revati'], + 'bad_nakshatras': ['Bharani', 'Ardra', 'Ashlesha', 'Jyeshtha', 'Mula'], + 'good_varas': ['Monday', 'Wednesday', 'Thursday', 'Friday'], + 'bad_varas': ['Tuesday', 'Saturday'], + }, + 'medical': { + 'name': '手术/医疗(Chikitsa)', + 'good_tithis': [1, 2, 3, 5, 6, 7, 10, 11, 12], + 'bad_tithis': [8, 9, 13, 14, 30], + 'good_nakshatras': ['Ashwini', 'Mrigashira', 'Pushya', 'Hasta', 'Anuradha'], + 'bad_nakshatras': ['Ardra', 'Ashlesha', 'Jyeshtha', 'Mula', 'Vishakha'], + 'good_varas': ['Monday', 'Wednesday', 'Thursday'], + 'bad_varas': ['Tuesday', 'Saturday', 'Sunday'], + }, + 'education': { + 'name': '学习/入学(Vidyarambha)', + 'good_tithis': [2, 3, 5, 7, 10, 11, 12], + 'bad_tithis': [4, 6, 8, 9, 14, 29, 30], + 'good_nakshatras': ['Ashwini', 'Mrigashira', 'Punarvasu', 'Pushya', + 'Hasta', 'Chitra', 'Swati', 'Shravana', 'Revati'], + 'bad_nakshatras': ['Bharani', 'Ardra', 'Ashlesha', 'Magha', 'Mula'], + 'good_varas': ['Monday', 'Wednesday', 'Thursday', 'Friday'], + 'bad_varas': ['Tuesday', 'Saturday'], + } +} + + +def check_activity_muhurta(panchanga: Dict, activity: str) -> Dict: + """ + 检查给定 Panchanga 是否适合特定活动。 + + activity: 'marriage', 'business', 'travel', 'medical', 'education' + """ + rules = ACTIVITY_RULES.get(activity) + if not rules: + return {'error': f'未知活动类型: {activity}。支持: {list(ACTIVITY_RULES.keys())}'} + + tithi_num = panchanga['tithi']['tithi_num'] + nakshatra = panchanga['nakshatra']['nakshatra'] + vara = panchanga['vara']['vara'] + + # Tithi 评估 + if tithi_num in rules['good_tithis']: + tithi_score = 'good' + elif tithi_num in rules['bad_tithis']: + tithi_score = 'bad' + else: + tithi_score = 'neutral' + + # Nakshatra 评估 + if nakshatra in rules['good_nakshatras']: + nakshatra_score = 'good' + elif nakshatra in rules['bad_nakshatras']: + nakshatra_score = 'bad' + else: + nakshatra_score = 'neutral' + + # Vara 评估 + if vara in rules['good_varas']: + vara_score = 'good' + elif vara in rules['bad_varas']: + vara_score = 'bad' + else: + vara_score = 'neutral' + + scores = [tithi_score, nakshatra_score, vara_score] + good_count = scores.count('good') + bad_count = scores.count('bad') + + if bad_count >= 2: + verdict = '不宜(Avoid)' + elif good_count >= 2 and bad_count == 0: + verdict = '大吉(Excellent)' + elif good_count >= 1 and bad_count == 0: + verdict = '吉(Good)' + elif bad_count == 1 and good_count >= 1: + verdict = '一般(Fair)' + else: + verdict = '中(Neutral)' + + return { + 'activity': rules['name'], + 'tithi_eval': tithi_score, + 'nakshatra_eval': nakshatra_score, + 'vara_eval': vara_score, + 'verdict': verdict, + 'good_count': good_count, + 'bad_count': bad_count, + 'notes': _get_activity_notes(rules, tithi_num, nakshatra, vara) + } + + +def _get_activity_notes(rules: Dict, tithi_num: int, + nakshatra: str, vara: str) -> List[str]: + notes = [] + if tithi_num in rules['bad_tithis']: + notes.append(f'Tithi {tithi_num} 不宜此类活动') + if nakshatra in rules['bad_nakshatras']: + notes.append(f'{nakshatra} 星宿不利此类活动') + if vara in rules['bad_varas']: + notes.append(f'{vara} 不利此类活动') + if tithi_num in rules['good_tithis']: + notes.append(f'Tithi {tithi_num} 有利此类活动') + if nakshatra in rules['good_nakshatras']: + notes.append(f'{nakshatra} 星宿适合此类活动') + if vara in rules['good_varas']: + notes.append(f'{vara} 有利此类活动') + return notes + + +# ── 完整报告函数 ────────────────────────────────────────────────────── + +def muhurta_full_report( + sun_lon: float, + moon_lon: float, + weekday: int, + hour_from_sunrise: float = 6.0, + query_date_str: Optional[str] = None, + activities: Optional[List[str]] = None, +) -> Dict: + """ + 生成完整 Muhurta 报告。 + + 参数: + sun_lon: 太阳恒星黄经(Lahiri,0-360) + moon_lon: 月亮恒星黄经(Lahiri,0-360) + weekday: 0=Sun, ..., 6=Sat + hour_from_sunrise: 从日出起算的小时(默认 6h = 约正午) + query_date_str: 查询日期字符串(用于展示) + activities: 要检查的活动列表(默认检查所有) + """ + panchanga = calc_panchanga(sun_lon, moon_lon, weekday, hour_from_sunrise) + abhijit = calc_abhijit_muhurta() + + if activities is None: + activities = list(ACTIVITY_RULES.keys()) + + activity_checks = {} + for act in activities: + activity_checks[act] = check_activity_muhurta(panchanga, act) + + return { + 'query_date': query_date_str or 'unknown', + 'panchanga': panchanga, + 'abhijit_muhurta': abhijit, + 'activity_checks': activity_checks, + 'summary': { + 'overall_quality': panchanga['overall_quality'], + 'overall_score': panchanga['overall_score'], + 'auspicious_elements': panchanga['auspicious_count'], + 'warnings': panchanga['warnings'], + 'best_activities': [ + act for act, chk in activity_checks.items() + if '吉' in chk.get('verdict', '') or 'Good' in chk.get('verdict', '') + ], + 'avoid_activities': [ + act for act, chk in activity_checks.items() + if '不宜' in chk.get('verdict', '') or 'Avoid' in chk.get('verdict', '') + ] + } + } + + +# ── 近似测试函数 ────────────────────────────────────────────────────── + +def _approx_sun_moon_lon(year: int, month: int, day: int) -> Tuple[float, float]: + """ + 近似计算太阳/月亮恒星黄经(无 swisseph,精度约 ±2°)。 + 仅用于测试和展示,不用于精确解盘。 + Lahiri Ayanamsa ≈ 23.85°(2026年) + """ + # J2000.0 起的天数 + import math + jd = 367 * year - int(7 * (year + int((month + 9) / 12)) / 4) + int(275 * month / 9) + day + 1721013.5 + d = jd - 2451545.0 # days since J2000.0 + + # 太阳黄经(热带) + M_sun = math.radians(357.5291 + 0.98560028 * d) + L_sun = 280.4665 + 0.98564736 * d + 1.9146 * math.sin(M_sun) + L_sun = L_sun % 360 + + # 月亮黄经(热带,简化) + L_moon = (218.3165 + 13.175396 * d) % 360 + M_moon = math.radians(134.9634 + 13.064993 * d) + L_moon = (L_moon + 6.2886 * math.sin(M_moon)) % 360 + + # 转为恒星(减去 Lahiri Ayanamsa ≈ 23.85°,2026年) + ayanamsa = 23.85 + sun_sid = (L_sun - ayanamsa) % 360 + moon_sid = (L_moon - ayanamsa) % 360 + + return sun_sid, moon_sid + + +if __name__ == '__main__': + import json + # 测试:2026-06-04 (Wednesday) + y, m, d = 2026, 6, 4 + sun_lon, moon_lon = _approx_sun_moon_lon(y, m, d) + wd = datetime(y, m, d).weekday() # 0=Mon in Python → convert + # Python weekday: 0=Mon, but our VARA_LORDS: 0=Sun + # June 4 2026 = Wednesday = Python 2 + vara_idx = (wd + 1) % 7 # convert Python weekday to Vara (Sun=0) + + print(f"=== Muhurta 测试 {y}-{m:02d}-{d:02d} ===") + print(f"Sun lon (approx): {sun_lon:.2f}° Moon lon (approx): {moon_lon:.2f}°") + print(f"Vara index: {vara_idx} ({VARA_LORDS[vara_idx][0]})") + print() + + result = muhurta_full_report( + sun_lon, moon_lon, vara_idx, + hour_from_sunrise=6.0, + query_date_str=f'{y}-{m:02d}-{d:02d}', + ) + + p = result['panchanga'] + print(f"Tithi: {p['tithi']['full_name']} ({p['tithi']['quality']})") + print(f"Nakshatra: {p['nakshatra']['nakshatra']} ({p['nakshatra']['quality']})") + print(f"Yoga: {p['yoga']['yoga']} ({p['yoga']['quality']})") + print(f"Karana: {p['karana']['karana']} ({p['karana']['quality']})") + print(f"Vara: {p['vara']['vara']} ({p['vara']['quality']})") + print(f"Hora: {p['hora']['hora_lord']} ({p['hora']['quality']})") + print() + print(f"综合评分: {result['summary']['overall_quality']} ({result['summary']['overall_score']:.0%})") + print(f"警告: {result['summary']['warnings']}") + print() + print("活动适宜性:") + for act, chk in result['activity_checks'].items(): + print(f" {act}: {chk['verdict']}")