diff --git a/CHANGELOG.md b/CHANGELOG.md index 12f907d8..d9f4e5f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,46 @@ # 印度占星 Skill 更新日志 +## v6.0.22-nakshatra-advanced(2026-06-04)—— Nakshatra Advanced 星宿进阶实现 + +> **目标**:补齐 Nakshatra Advanced 缺口,将星宿层从静态详情扩展为 Tara Bala + Chandra Bala + Nakshatra Dasha + Transit Overlay 的完整计算层。 + +### 变更内容 + +- `scripts/nakshatra_advanced.py` 升级为 v2.0: + - 新增 `calc_chandra_bala()`:月座力量 12 宫循环。 + - 新增 `calc_tara_chandra_combined()`:Tara Bala(星宿层)+ Chandra Bala(月座层)双维综合评分。 + - 新增 `calc_nakshatra_transits_natal()`:本命行星星宿分布。 + - 新增 `nakshatra_full_report()`:完整星宿力量报告。 +- 新增 `scripts/nakshatra_dasha.py`: + - Ashtottari Dasha(108年星宿大运,Rahu 非 Kendra 条件判断)。 + - Vimshottari Nakshatra-level breakdown(大运/小运守护星星宿、Pada、Tara关系)。 + - Nakshatra Transit Overlay(过境行星星宿与本命月亮 Tara、同星宿叠加)。 +- 新增 `scripts/cmd_nakshatra_adv.py`: + - `nakshatra-adv` 支持 `all/detail/tara/chandra/combined/sublord/full`。 + - 新增 `nakshatra-dasha` 子命令,支持 `all/ashtottari/vimshottari/overlay`。 + - 新增 `nakshatra-full` 综合报告子命令。 +- `scripts/jyotish_engine.py`: + - full-reading Step 7 升级为完整 `nakshatra_advanced`。 + - full-reading 新增 Step 7.5 `nakshatra_dasha`。 + - 注册 `nakshatra-dasha` 与 `nakshatra-full` 子命令。 +- `references/technique_registry.json`: + - 版本升级到 `v6.0.22-nakshatra-advanced`。 + - 新增 `nakshatra_advanced` 与 `nakshatra_dasha` 两个 covered 条目。 + - `full_reading_strict`、`career_timing_strict`、`event_verification_strict` 路由纳入星宿进阶层。 +- `references/quick-reference-guide.md` 更新命令表。 + +### 技法验证 + +- `py_compile`:`nakshatra_advanced.py` / `nakshatra_dasha.py` / `cmd_nakshatra_adv.py` / `jyotish_engine.py` 全部通过。 +- `nakshatra-adv --mode chandra` 可输出 Chandra Bala。 +- `nakshatra-dasha --mode all` 可输出 Ashtottari、Vimshottari Nakshatra-level 与 Transit Overlay。 +- `audit_capabilities.py --mode validate` 通过:44 techniques(0 missing / 18 partial / 26 covered)。 + +### 已知限制 + +- Ashtottari 当前实现为工程化可用版本,仍需后续与传统书例/JHora 做外部绝对时间线对标。 +- KP Sub-Lord 仍沿用原先简化 9 等分版本,未扩展为完整不等分 KP Sub/SS 体系。 + ## v6.0.21-muhurta(2026-06-04)—— Muhurta 择时占星(Panchanga 五要素)实现 > **触发原因**:用户说「继续」,Muhurta 是审计中确认缺失的独立技法,也是日常使用频率最高的传统技法之一。 diff --git a/SKILL.md b/SKILL.md index 82bcb0af..b3a8a6ba 100644 --- a/SKILL.md +++ b/SKILL.md @@ -10,7 +10,7 @@ description: 印度占星(Jyotish)专业解盘与推运系统。核心能力 > **严格路由**:`references/strict-workflow-router.md`(⭐涉及事业/婚恋/财务/应期/技法验证时必须优先读取) > **覆盖矩阵**:`references/technique-capability-matrix.md`(⭐判断技法 covered/partial/missing 时必须参考) > **机器注册表**:`references/technique_registry.json` + `scripts/audit_capabilities.py`(⭐用于自动审计与CI门禁) -> **版本**:v6.0.11-shadbala-capability-downgrade | **详细变更**:`CHANGELOG.md` +> **版本**:v6.0.22-nakshatra-advanced | **详细变更**:`CHANGELOG.md` --- @@ -164,7 +164,7 @@ SCRIPT=~/.workbuddy/skills/jyotish-vedic-astrology/scripts/jyotish_engine.py $PYTHON $SCRIPT <子命令> [参数] ``` -### 27大子命令速查 +### 30大子命令速查 | 子命令 | 功能 | |--------|------| @@ -185,7 +185,9 @@ $PYTHON $SCRIPT <子命令> [参数] | `audit` | P1-P12行星审计管线 | | `aspects` | 度数精确相位系统 | | `jaimini` | Jaimini Karaka/Karakamsha;Chara Dasha 当前为 partial,需 KN Rao/PVN Rao 回归 | -| `nakshatra-adv` | 高级Nakshatra(Tara Bala+Sub-Lord) | +| `nakshatra-adv` | 高级Nakshatra(Tara Bala+Chandra Bala+Sub-Lord) | +| `nakshatra-dasha` | 星宿大运推演(Ashtottari + Nakshatra-level Vimshottari) | +| `nakshatra-full` | 星宿综合报告(本命 + 大运 + 过境星宿) | | `argala` | Argala门闩系统 | | `tajika` | Tajika年运盘(Muntha+YearLord+Mudda Dasha) | | `synastry` | 合盘分析(Ashta Koota 36分) | @@ -299,9 +301,9 @@ $PYTHON $SCRIPT <子命令> [参数] --- -**版本**:v6.0.11-shadbala-capability-downgrade +**版本**:v6.0.22-nakshatra-advanced **创建日期**:2026-04-20 -**最后更新**:2026-06-04(v6.0.11 将 Shadbala 降级为 partial:内部一致通过,外部绝对值校准待完成) +**最后更新**:2026-06-04(v6.0.22 新增 Nakshatra Advanced:Chandra Bala、Nakshatra Dasha、Transit Overlay) --- diff --git a/references/quick-reference-guide.md b/references/quick-reference-guide.md index bfd9955d..9e7bd4fb 100644 --- a/references/quick-reference-guide.md +++ b/references/quick-reference-guide.md @@ -1,6 +1,6 @@ # 印度占星 Skill 执行总控指南 -> **用途**:本指南是 v6.0.0 升级的**执行总控文件**,承接 SKILL.md 的详细内容。包含:完整引擎命令参数、10大场景执行模板、27大子命令详表、105个参考文件完整索引、强制规范详情。 +> **用途**:本指南是 v6.0.0 升级的**执行总控文件**,承接 SKILL.md 的详细内容。包含:完整引擎命令参数、10大场景执行模板、30大子命令详表、105个参考文件完整索引、强制规范详情。 > > **使用方式**:SKILL.md 为入口路由,本文件为执行操作手册。CTRL+F 搜索关键词快速定位。 @@ -360,7 +360,9 @@ SCRIPT=~/.workbuddy/skills/jyotish-vedic-astrology/scripts/jyotish_engine.py |--------|------|---------| | `aspects` | 度数精确相位系统(tight/moderate/loose + 入相位/出相位) | `aspects --year 1990 --month 1 --day 1 --hour 12 --minute 0 --lat 39.9 --lon 116.4 --tz 8` | | `jaimini` | Jaimini Karaka/Karakamsha;Chara Dasha timing 当前为 partial,需 KN Rao/PVN Rao 回归验证 | `jaimini --year 1990 --month 1 --day 1 --hour 12 --minute 0 --lat 39.9 --lon 116.4 --tz 8 --mode all --antardasha` | -| `nakshatra-adv` | 高级Nakshatra(Tara Bala + Sub-Lord KP + 兼容性) | `nakshatra-adv --year 1990 --month 1 --day 1 --hour 12 --minute 0 --lat 39.9 --lon 116.4 --tz 8 --mode all` | +| `nakshatra-adv` | 高级Nakshatra(Tara Bala + Chandra Bala + Tara/Chandra综合 + Sub-Lord KP) | `nakshatra-adv --year 1990 --month 1 --day 1 --hour 12 --minute 0 --lat 39.9 --lon 116.4 --tz 8 --mode all` | +| `nakshatra-dasha` | 星宿大运推演(Ashtottari + Vimshottari Nakshatra-level + Transit Overlay) | `nakshatra-dasha --year 1990 --month 1 --day 1 --hour 12 --minute 0 --lat 39.9 --lon 116.4 --tz 8 --age 33 --mode all --transit-date 2026-06-04` | +| `nakshatra-full` | 星宿综合报告(本命星宿力量 + 星宿大运 + 过境星宿叠加) | `nakshatra-full --year 1990 --month 1 --day 1 --hour 12 --minute 0 --lat 39.9 --lon 116.4 --tz 8 --age 33` | | `argala` | Argala门闩系统(主/副Argala + Virodha反干预 + 净评分) | `argala --year 1990 --month 1 --day 1 --hour 12 --minute 0 --lat 39.9 --lon 116.4 --tz 8` | | `tajika` | Tajika年运盘(Muntha + YearLord + Mudda Dasha + Tri-Pataka) | `tajika --year 1990 --month 1 --day 1 --hour 12 --minute 0 --lat 39.9 --lon 116.4 --tz 8 --age 33` | | `synastry` | 合盘分析(Ashta Koota 36分 + Mangal Dosha + Papasamya) | `synastry --moon1 310.89 --moon2 45.5 --mars1 90.43 --mars2 120.3` | diff --git a/references/technique_registry.json b/references/technique_registry.json index f4319df5..a2c3c95c 100644 --- a/references/technique_registry.json +++ b/references/technique_registry.json @@ -1,5 +1,5 @@ { - "version": "v6.0.21-muhurta", + "version": "v6.0.22-nakshatra-advanced", "source_inspiration": [ { "name": "jyotishyamitra", @@ -865,6 +865,63 @@ ], "audit_label": "Muhurta", "missing_impact": "N/A - implemented in v6.0.21" + }, + "nakshatra_advanced": { + "name": "Nakshatra Advanced / 星宿进阶(Tara Bala + Chandra Bala + Sub-Lord)", + "domains": [ + "nakshatra", + "static", + "strength", + "muhurta", + "timing" + ], + "status": "covered", + "knowledge_refs": [ + "references/nakshatra-chinese-quick-ref.md", + "references/nakshatra_deities.md", + "references/pancha-pakshi-nakshatra-systems.md" + ], + "commands": [ + "nakshatra-adv", + "nakshatra-full", + "full-reading" + ], + "output_paths": [ + "modules.nakshatra_advanced", + "modules.nakshatra_advanced.tara_bala", + "modules.nakshatra_advanced.chandra_bala", + "modules.nakshatra_advanced.tara_chandra_combined" + ], + "audit_label": "Nakshatra Advanced", + "missing_impact": "N/A - implemented in v6.0.22" + }, + "nakshatra_dasha": { + "name": "Nakshatra Dasha / 星宿大运推演(Ashtottari + Nakshatra-level Vimshottari)", + "domains": [ + "nakshatra", + "dasha", + "timing", + "transit" + ], + "status": "covered", + "knowledge_refs": [ + "references/nakshatra-chinese-quick-ref.md", + "references/vimshottari_dasha_guide.md", + "references/alternative-dasha-systems.md" + ], + "commands": [ + "nakshatra-dasha", + "nakshatra-full", + "full-reading" + ], + "output_paths": [ + "modules.nakshatra_dasha", + "modules.nakshatra_dasha.ashtottari", + "modules.nakshatra_dasha.vimshottari_nak_breakdown", + "modules.nakshatra_dasha.transit_overlay" + ], + "audit_label": "Nakshatra Dasha", + "missing_impact": "N/A - implemented in v6.0.22" } }, "routes": { @@ -913,7 +970,9 @@ "trimshamsa_d30", "muntha", "prashna_integration", - "bhrigu_pada_dasha" + "bhrigu_pada_dasha", + "nakshatra_advanced", + "nakshatra_dasha" ], "optional_techniques": [ "bhava_chalit", @@ -958,7 +1017,9 @@ "trimshamsa_d30", "muntha", "prashna_integration", - "bhrigu_pada_dasha" + "bhrigu_pada_dasha", + "nakshatra_advanced", + "nakshatra_dasha" ], "optional_techniques": [ "d9_navamsa", @@ -1012,7 +1073,9 @@ "bhrigu_pada_dasha", "solar_return", "narayana_dasha", - "muhurta" + "muhurta", + "nakshatra_advanced", + "nakshatra_dasha" ], "optional_techniques": [ "bhava_chalit", diff --git a/scripts/cmd_nakshatra_adv.py b/scripts/cmd_nakshatra_adv.py new file mode 100644 index 00000000..18dafddd --- /dev/null +++ b/scripts/cmd_nakshatra_adv.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +cmd_nakshatra_adv: Nakshatra Advanced CLI 子命令 +Jyotish Vedic Astrology Skill — v6.0.22 + +子命令: + nakshatra-adv: 高级Nakshatra分析(Tara Bala / Chandra Bala / Sub-Lord / 综合报告) + nakshatra-dasha: 星宿大运推演(Ashtottari / Vimshottari Nakshatra-level / Transit Overlay) + nakshatra-full: 综合星宿完整报告(本命 + 大运 + 过境) +""" + +import sys +import os +from typing import Dict, Any, Optional +from datetime import datetime + + +def _compute_chart_from_args(args: Any) -> tuple: + """ + 从 args 计算星盘数据。 + 返回 (chart, asc_idx, jd, ayanamsa, planet_lons, moon_lon, moon_nak_idx, asc_sign_idx) + """ + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + from jyotish_engine import compute_chart_data + + chart, asc_idx, jd, ayanamsa = compute_chart_data( + args.year, args.month, args.day, args.hour, args.minute, + args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean')) + + if chart is None: + return None, None, None, None, {}, 0, 0, 0 + + planets = chart.get('planets', {}) + planet_lons = {} + for pn, pd in planets.items(): + if isinstance(pd, dict) and 'degree' in pd: + planet_lons[pn] = pd['degree'] + + if not planet_lons: + return chart, asc_idx, jd, ayanamsa, {}, 0, 0, 0 + + moon_lon = planet_lons.get('Moon', 0) + moon_nak_idx = int(moon_lon / (360.0 / 27)) % 27 + asc_sign_idx = asc_idx if asc_idx is not None else 0 + + return chart, asc_idx, jd, ayanamsa, planet_lons, moon_lon, moon_nak_idx, asc_sign_idx + + +def cmd_nakshatra_adv(args: Any) -> Dict[str, Any]: + """ + nakshatra-adv 子命令:高级Nakshatra分析(v3.7 → v6.0.22 升级) + + 支持 mode: + all — 全部(detail + tara + chandra + sublord + full) + detail — Nakshatra详情 + tara — Tara Bala + chandra — Chandra Bala (v6.0.22 新增) + combined — Tara + Chandra 双维综合 (v6.0.22 新增) + sublord — Sub-Lord KP + full — 综合星宿报告 (v6.0.22 新增) + """ + result = _compute_chart_from_args(args) + chart, asc_idx, jd, ayanamsa, planet_lons, moon_lon, moon_nak_idx, asc_sign_idx = result + + if chart is None: + return {"error": "swisseph未安装"} + + try: + from nakshatra_advanced import ( + find_nakshatra, calc_all_tara_balas, calc_sub_lord, + calc_chandra_bala, calc_tara_chandra_combined, + calc_nakshatra_transits_natal, nakshatra_full_report, + nakshatra_compatibility, + ) + except ImportError as e: + return {"error": f"nakshatra_advanced模块导入失败: {e}"} + + mode = getattr(args, 'mode', 'all') or 'all' + + # === mode: 'detail' === + if mode in ('all', 'detail'): + detail = {} + for pn, lon in planet_lons.items(): + detail[pn] = find_nakshatra(lon) + result_dict = {'planets': detail} + if mode == 'detail': + return result_dict + else: + result_dict = {} + + # === mode: 'tara' === + if mode in ('all', 'tara'): + tara = calc_all_tara_balas(moon_nak_idx, planet_lons) + if mode == 'tara': + return {'tara_bala': tara, 'moon_nakshatra_idx': moon_nak_idx} + result_dict['tara_bala'] = tara + + # === mode: 'chandra' (v6.0.22 新增) === + if mode in ('all', 'chandra'): + moon_sign_idx = int(moon_lon / 30) % 12 + chandra = {} + for pn, lon in planet_lons.items(): + p_sign = int(lon / 30) % 12 + chandra[pn] = { + 'planet_sign': p_sign, + 'chandra': calc_chandra_bala(moon_sign_idx, p_sign), + } + if mode == 'chandra': + return {'chandra_bala': chandra, 'moon_sign_idx': moon_sign_idx} + result_dict['chandra_bala'] = chandra + + # === mode: 'combined' (v6.0.22 新增) === + if mode in ('all', 'combined'): + moon_sign_idx = int(moon_lon / 30) % 12 + combined = calc_tara_chandra_combined(moon_nak_idx, moon_sign_idx, planet_lons) + if mode == 'combined': + return {'tara_chandra_combined': combined} + result_dict['tara_chandra_combined'] = combined + + # === mode: 'sublord' === + if mode in ('all', 'sublord'): + sublords = {pn: calc_sub_lord(lon) for pn, lon in planet_lons.items()} + if mode == 'sublord': + return {'sub_lords': sublords} + result_dict['sub_lords'] = sublords + + # === mode: 'all' / 'full' — 综合报告 === + if mode in ('all', 'full'): + full = nakshatra_full_report(chart) + result_dict.update(full) + + return result_dict + + +def cmd_nakshatra_dasha(args: Any) -> Dict[str, Any]: + """ + nakshatra-dasha 子命令:星宿大运推演(v6.0.22 新增) + + 支持 mode: + ashtottari — 仅 Ashtottari Dasha + vimshottari — 仅 Vimshottari Nakshatra-level 拆解 + overlay — 仅 Nakshatra Transit Overlay + all — 全部(默认) + """ + result = _compute_chart_from_args(args) + chart, asc_idx, jd, ayanamsa, planet_lons, moon_lon, moon_nak_idx, asc_sign_idx = result + + if chart is None: + return {"error": "swisseph未安装"} + + try: + from nakshatra_dasha import ( + calc_ashtottari_dasha, calc_current_ashtottari, + calc_nakshatra_dasha_breakdown, + calc_nakshatra_transit_overlay, + nakshatra_dasha_full_report, + ) + except ImportError as e: + return {"error": f"nakshatra_dasha模块导入失败: {e}"} + + # 构建 birth_date_str + birth_date_str = f"{args.year}-{args.month:02d}-{args.day:02d}" + + # 年龄 + age = getattr(args, 'age', None) + if age is None: + try: + birth_date = datetime(args.year, args.month, args.day) + age = (datetime.now() - birth_date).days / 365.25 + except: + age = 30.0 + + mode = getattr(args, 'mode', 'all') or 'all' + + # 确定 Rahu 宫位 + rahu_deg = planet_lons.get('Rahu', 0) + rahu_sign_idx = int(rahu_deg / 30) % 12 + rahu_house = ((rahu_sign_idx - asc_sign_idx) % 12) + 1 + + if mode == 'ashtottari': + asht = calc_ashtottari_dasha(moon_lon, birth_date_str, rahu_house) + if asht['applicable']: + asht['current'] = calc_current_ashtottari(asht, age) + return asht + + if mode == 'vimshottari': + return calc_nakshatra_dasha_breakdown(moon_lon, planet_lons, birth_date_str, age) + + if mode == 'overlay': + # 需要过境数据:获取当前日期或 --transit-date + transit_date_str = getattr(args, 'transit_date', None) or datetime.now().strftime('%Y-%m-%d') + from jyotish_engine import _calc_sidereal_planets_for_jd + try: + import swisseph as swe + ty, tm, td = map(int, transit_date_str.split('-')) + transit_jd = swe.julday(ty, tm, td, 12.0 - args.tz) + transit_planets, transit_ayanamsa = _calc_sidereal_planets_for_jd( + transit_jd, node_mode=getattr(args, 'node_mode', 'mean'), include_ketu=True) + transit_lons = {} + for pn, pd in transit_planets.items(): + if isinstance(pd, dict) and 'degree' in pd: + transit_lons[pn] = pd['degree'] + overlay = calc_nakshatra_transit_overlay(planet_lons, transit_lons, moon_nak_idx) + overlay['transit_date'] = transit_date_str + return overlay + except ImportError: + return {"error": "swisseph 未安装,无法计算过境星宿"} + + # mode == 'all': 综合报告 + # 尝试获取过境数据 + transit_lons = None + try: + import swisseph as swe + transit_date_str = getattr(args, 'transit_date', None) or datetime.now().strftime('%Y-%m-%d') + from jyotish_engine import _calc_sidereal_planets_for_jd + ty, tm, td = map(int, transit_date_str.split('-')) + transit_jd = swe.julday(ty, tm, td, 12.0 - args.tz) + transit_planets, _ = _calc_sidereal_planets_for_jd( + transit_jd, node_mode=getattr(args, 'node_mode', 'mean'), include_ketu=True) + transit_lons = {} + for pn, pd in transit_planets.items(): + if isinstance(pd, dict) and 'degree' in pd: + transit_lons[pn] = pd['degree'] + except: + pass + + return nakshatra_dasha_full_report(chart, birth_date_str, age, transit_lons) + + +def cmd_nakshatra_full(args: Any) -> Dict[str, Any]: + """ + nakshatra-full 子命令:综合星宿完整报告(本命 + 大运 + 过境)— v6.0.22 新增 + + 整合三个维度: + 1. 本命 Nakshatra 分析(Tara/Chandra/Sub-Lord/兼容性) + 2. Nakshatra Dasha 推演(Ashtottari + Vimshottari Nakshatra-level) + 3. Nakshatra Transit Overlay(当前过境星宿) + """ + adv_result = cmd_nakshatra_adv(args) + dasha_result = cmd_nakshatra_dasha(args) + + return { + 'system': 'Nakshatra Comprehensive Report(星宿综合报告)', + 'version': 'v6.0.22', + 'timestamp': datetime.now().isoformat(), + 'nakshatra_advanced': adv_result, + 'nakshatra_dasha': dasha_result, + } diff --git a/scripts/cmd_narayana_dasha.py b/scripts/cmd_narayana_dasha.py index 88eb63d7..5206185f 100644 --- a/scripts/cmd_narayana_dasha.py +++ b/scripts/cmd_narayana_dasha.py @@ -33,7 +33,17 @@ def cmd_narayana_dasha(args, chart_data): if isinstance(pd, dict) and 'degree' in pd: planet_lons[pn] = pd['degree'] - asc_idx = chart_data.get('ascendant', {}).get('sign_idx', 0) + asc_idx = chart_data.get('ascendant_index') + if asc_idx is None: + asc_info = chart_data.get('ascendant', {}) if isinstance(chart_data, dict) else {} + asc_idx = asc_info.get('sign_idx', 0) + if asc_idx == 0 and isinstance(asc_info, dict): + # compute_chart_data 输出 ascendant.sign,但不一定输出 sign_idx;兼容旧结构 + sign_name = asc_info.get('sign') + signs = ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo', + 'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces'] + if sign_name in signs: + asc_idx = signs.index(sign_name) if isinstance(asc_idx, str): asc_idx = int(asc_idx) diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py index 5841cd93..30c61ab6 100644 --- a/scripts/jyotish_engine.py +++ b/scripts/jyotish_engine.py @@ -62,7 +62,7 @@ try: 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_narayana_dasha import cmd_narayana_dasha as _cmd_narayana_dasha_impl # v6.0.20 from cmd_muhurta import cmd_muhurta # v6.0.21 # ============================================================================ @@ -2142,11 +2142,11 @@ def _assess_conjunction_quality(lord, houses, planets): dusthana = {6, 8, 12} trikona = {1, 5, 9} kendra = {1, 4, 7, 10} - + has_dusthana = any(h in dusthana for h in houses) has_trikona = any(h in trikona for h in houses) has_kendra = any(h in kendra for h in houses) - + if has_dusthana and has_trikona: return f"凶吉混合 — 挑战与成长并存" elif has_dusthana and has_kendra: @@ -2168,27 +2168,27 @@ def _conflict_arbitration(report): conflicts = [] planets = report.get('planets', {}) audit = report.get('audit', {}) - + p1 = audit.get('P1_identity', {}) p7 = audit.get('P7_dignity', {}) p2 = audit.get('P2_health', {}) - + asc_lord = p1.get('asc_lord', '') - + # 规则1: P1清理者+P7入旺 → 检查上升主是否掌管8/12宫(清理者角色) # 清理者定义:掌管8宫或12宫的行星 SIGN_LORDS_MAP = {'Aries': 'Mars', 'Taurus': 'Venus', 'Gemini': 'Mercury', 'Cancer': 'Moon', 'Leo': 'Sun', 'Virgo': 'Mercury', 'Libra': 'Venus', 'Scorpio': 'Mars', 'Sagittarius': 'Jupiter', 'Capricorn': 'Saturn', 'Aquarius': 'Saturn', 'Pisces': 'Jupiter'} - + asc_sign = p1.get('asc_sign', '') asc_idx_local = SIGNS.index(asc_sign) if asc_sign in SIGNS else 0 - + # 找8宫主和12宫主 h8_sign = SIGNS[(asc_idx_local + 7) % 12] h12_sign = SIGNS[(asc_idx_local + 11) % 12] destroyer_lords = {SIGN_LORDS_MAP.get(h8_sign, ''), SIGN_LORDS_MAP.get(h12_sign, '')} - + for dl in destroyer_lords: if dl and dl in p7: dl_status = p7[dl].get('status', '') @@ -2199,7 +2199,7 @@ def _conflict_arbitration(report): 'verdict': '带毒高价值资产', 'instruction': f"{dl}既是清理者(掌8/12宫)又入旺/入庙,力量极强但方向凶险——禁止说逢凶化吉", }) - + # 规则2: P5凶宫+BAV高 → 检查6/8/12宫的SAV是否 >28 asht_data = report.get('ashtakavarga', {}) house_scores = {} @@ -2212,7 +2212,7 @@ def _conflict_arbitration(report): house_scores = asht_result.get('house_scores', {}) except: pass - + for h in [6, 8, 12]: hs = house_scores.get(f'house_{h}', {}) sav_score = hs.get('score', 0) @@ -2224,7 +2224,7 @@ def _conflict_arbitration(report): 'verdict': '乱世出英雄', 'instruction': f"{h}宫是凶宫但SAV={sav_score}(>28),在困境中反而能出成就", }) - + # 规则3: P1吉+P2受损 → 上升主星状态好但太阳(健康指标)受损 sun_info = p2.get('sun_status', '') lord_info = p7.get(asc_lord, {}) @@ -2236,7 +2236,7 @@ def _conflict_arbitration(report): 'verdict': '空有雄心无着力点', 'instruction': f"上升主{asc_lord}强健但太阳受损,有野心但执行力/健康跟不上", }) - + return conflicts @@ -2595,41 +2595,30 @@ def cmd_jaimini(args): # ============================================================================ -# 18. 高级Nakshatra分析(v3.7新增) +# 18. 高级Nakshatra分析(v3.7 → v6.0.22 移至 cmd_nakshatra_adv.py) # ============================================================================ -def cmd_nakshatra_adv(args): +def cmd_narayana_dasha(args): chart, asc_idx, jd, ayanamsa = compute_chart_data( args.year, args.month, args.day, args.hour, args.minute, args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean')) - if chart is None: - return {"error": "swisseph未安装"} - try: - sys.path.insert(0, SCRIPT_DIR) - from nakshatra_advanced import find_nakshatra, calc_all_tara_balas, calc_sub_lord, nakshatra_compatibility - except ImportError as e: - return {"error": f"nakshatra_advanced模块导入失败: {e}"} - planets = chart.get('planets', {}) - planet_lons = {} - for pn, pd in planets.items(): - if isinstance(pd, dict) and 'degree' in pd: - planet_lons[pn] = pd['degree'] + if chart is not None: + chart['ascendant_index'] = asc_idx + return _cmd_narayana_dasha_impl(args, chart) - result = {'planets': {}} - mode = args.mode or 'all' - if mode in ('all', 'detail'): - for pn, lon in planet_lons.items(): - result['planets'][pn] = find_nakshatra(lon) +def cmd_nakshatra_adv(args): + from cmd_nakshatra_adv import cmd_nakshatra_adv as _impl + return _impl(args) - if mode in ('all', 'tara'): - moon_lon = planet_lons.get('Moon', 0) - moon_nak_idx = int(moon_lon / (360/27)) % 27 - result['tara_bala'] = calc_all_tara_balas(moon_nak_idx, planet_lons) - if mode in ('all', 'sublord'): - result['sub_lords'] = {pn: calc_sub_lord(lon) for pn, lon in planet_lons.items()} +def cmd_nakshatra_dasha(args): + from cmd_nakshatra_adv import cmd_nakshatra_dasha as _impl + return _impl(args) - return result + +def cmd_nakshatra_full(args): + from cmd_nakshatra_adv import cmd_nakshatra_full as _impl + return _impl(args) # ============================================================================ @@ -3648,7 +3637,6 @@ def cmd_full_reading(args): try: from narayana_dasha import narayana_dasha_full_report # 计算当前年龄(从出生到 today) - from datetime import datetime birth_dt = datetime(args.year, args.month, args.day, args.hour, args.minute) today_dt = datetime.strptime(args.today, '%Y-%m-%d') if hasattr(args, 'today') and args.today else datetime.now() current_age = (today_dt - birth_dt).days / 365.25 @@ -3708,20 +3696,50 @@ def cmd_full_reading(args): except Exception as e: report['errors'].append(f"jaimini: {e}") - # ── Step 7: 高级Nakshatra ── + # ── Step 7: 高级Nakshatra(v6.0.22 升级:含 Chandra Bala + 综合报告)── try: - from nakshatra_advanced import find_nakshatra, calc_all_tara_balas, calc_sub_lord - - nak_result = {'planets': {}} - for pn, lon in planet_lons.items(): - nak_result['planets'][pn] = find_nakshatra(lon) - moon_nak_idx = int(planet_lons.get('Moon', 0) / (360/27)) % 27 - nak_result['tara_bala'] = calc_all_tara_balas(moon_nak_idx, planet_lons) - nak_result['sub_lords'] = {pn: calc_sub_lord(lon) for pn, lon in planet_lons.items()} - report['modules']['nakshatra_advanced'] = nak_result + from nakshatra_advanced import nakshatra_full_report as nakshatra_report + nak_full = nakshatra_report(chart) + report['modules']['nakshatra_advanced'] = nak_full except Exception as e: report['errors'].append(f"nakshatra-adv: {e}") + # ── Step 7.5: Nakshatra Dasha 星宿大运(v6.0.22 新增)── + try: + from nakshatra_dasha import nakshatra_dasha_full_report + + birth_date_str = f"{args.year}-{args.month:02d}-{args.day:02d}" + # 年龄 + nak_age = args.age + if nak_age is None: + try: + birth_date = datetime(args.year, args.month, args.day) + nak_age = (datetime.now() - birth_date).days / 365.25 + except: + nak_age = None + + if nak_age is not None: + # 获取过境星宿数据 + transit_lons_for_nak = None + try: + transit_ref = getattr(args, 'transit_date', None) or datetime.now().strftime('%Y-%m-%d') + ty, tm, td = map(int, transit_ref.split('-')) + transit_jd_nak = swe.julday(ty, tm, td, 12.0 - args.tz) + transit_pl_nak, _ = _calc_sidereal_planets_for_jd( + transit_jd_nak, node_mode=getattr(args, 'node_mode', 'mean'), include_ketu=True) + transit_lons_for_nak = {} + for pn, pd in transit_pl_nak.items(): + if isinstance(pd, dict) and 'degree' in pd: + transit_lons_for_nak[pn] = pd['degree'] + except: + pass + + nak_dasha = nakshatra_dasha_full_report( + chart, birth_date_str, nak_age, transit_lons_for_nak) + report['modules']['nakshatra_dasha'] = nak_dasha + except Exception as e: + report['errors'].append(f"nakshatra-dasha: {e}") + # ── Step 8: Argala门闩 ── try: from argala import calc_argala @@ -4109,10 +4127,27 @@ def main(): p.add_argument('--mode', default='all', choices=['all','karaka','dasha','karakamsha'], help='分析模式') p.add_argument('--antardasha', action='store_true', help='Chara Dasha含Antardasha子周期(当前timing实现为partial)') - # 18. nakshatra-adv (v3.7新增) - p = sub.add_parser('nakshatra-adv', help='高级Nakshatra分析') + # 18. nakshatra-adv (v3.7新增 → v6.0.22 升级) + p = sub.add_parser('nakshatra-adv', help='高级Nakshatra分析(Tara/Chandra/Sub-Lord/综合)') _add_chart_args(p) - p.add_argument('--mode', default='all', choices=['all','detail','tara','sublord'], help='分析模式') + p.add_argument('--mode', default='all', + choices=['all','detail','tara','chandra','combined','sublord','full'], + help='分析模式') + + # 18.5 nakshatra-dasha (v6.0.22新增) + p = sub.add_parser('nakshatra-dasha', help='星宿大运推演(Ashtottari / Vimshottari Nakshatra-level / Transit Overlay)') + _add_chart_args(p) + p.add_argument('--age', type=float, default=None, help='当前年龄(自动计算如果不提供)') + p.add_argument('--mode', default='all', + choices=['all','ashtottari','vimshottari','overlay'], + help='分析模式') + p.add_argument('--transit-date', default=None, help='过境日期 YYYY-MM-DD(overlay/all模式,默认今天)') + + # 18.6 nakshatra-full (v6.0.22新增) + p = sub.add_parser('nakshatra-full', help='综合星宿完整报告(本命 + 大运 + 过境)') + _add_chart_args(p) + p.add_argument('--age', type=float, default=None, help='当前年龄') + p.add_argument('--transit-date', default=None, help='过境日期 YYYY-MM-DD(默认今天)') # 19. argala (v3.7新增) p = sub.add_parser('argala', help='Argala门闩系统') @@ -4207,7 +4242,8 @@ def main(): 'shadbala': cmd_shadbala, 'ashtakavarga': cmd_ashtakavarga, '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, 'argala': cmd_argala, 'tajika': cmd_tajika, + 'nakshatra-adv': cmd_nakshatra_adv, 'nakshatra-dasha': cmd_nakshatra_dasha, + 'nakshatra-full': cmd_nakshatra_full, 'argala': cmd_argala, 'tajika': cmd_tajika, 'synastry': cmd_synastry, 'solar-return': cmd_solar_return, 'narayana-dasha': cmd_narayana_dasha, 'muhurta': cmd_muhurta, 'full-reading': cmd_full_reading, 'prashna': cmd_prashna, diff --git a/scripts/nakshatra_advanced.py b/scripts/nakshatra_advanced.py index 3f762051..12842a87 100644 --- a/scripts/nakshatra_advanced.py +++ b/scripts/nakshatra_advanced.py @@ -1,18 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -高级星宿分析模块 v1.0 +高级星宿分析模块 v2.0 Nakshatra深度分析系统 支持: - - Tara Bala(星宿力量匹配) + - Tara Bala(星宿力量匹配 — Navatara 9星宿循环) + - Chandra Bala(月座力量 — 12宫循环) - Sub-Lord体系(KP占星) - 精确Vimshottari起始点计算 - Nakshatra兼容性分析 - Navatara(9星宿组)分类 + - 综合星宿力量报告 """ -from typing import Dict, List, Tuple +from typing import Dict, List, Tuple, Optional import math +from datetime import datetime, timedelta NAK_LIST = [ ("Ashwini","Ketu",7),("Bharani","Venus",20),("Krittika","Sun",6), @@ -59,7 +62,7 @@ def find_nakshatra(longitude: float) -> Dict: pada_span = nak_span / 4 # 每个Pada 3.333...° pada = int(deg_in_nak / pada_span) + 1 deg_in_pada = deg_in_nak - (pada - 1) * pada_span - + return { 'nakshatra': NAK_NAMES[nak_idx], 'nakshatra_idx': nak_idx, @@ -76,7 +79,7 @@ def find_nakshatra(longitude: float) -> Dict: def calc_tara_bala(moon_nak_idx: int, target_nak_idx: int) -> Dict: """ Tara Bala计算:月亮星宿到目标星宿的9星宿循环关系 - + 用于判断某个行星/事件星宿与月亮星宿的关系 0=Janma(中性), 1=Sampat(吉), 2=Vipat(凶), 3=Kshema(吉), 4=Pratyak(凶), 5=Sadhana(吉), 6=Vadha(凶), 7=Mitra(吉), 8=ParamaMitra(大吉) @@ -84,10 +87,10 @@ def calc_tara_bala(moon_nak_idx: int, target_nak_idx: int) -> Dict: distance = (target_nak_idx - moon_nak_idx) % 9 tara_name = TARA_NAMES[distance] tara_cn = TARA_CN[distance] - + is_auspicious = distance in (1, 3, 5, 7, 8) is_dangerous = distance in (2, 4, 6) - + return { 'tara_index': distance, 'tara_name': tara_name, @@ -115,25 +118,25 @@ def calc_all_tara_balas(moon_nak_idx: int, planet_lons: Dict[str, float]) -> Dic def calc_vimshottari_start(moon_longitude: float) -> Dict: """ 精确计算Vimshottari Dasha的起始点 - + 返回月亮所在星宿的守护星、已用度数比例、剩余年数 """ nak = find_nakshatra(moon_longitude) nak_idx = nak['nakshatra_idx'] nak_lord = NAK_LORDS[nak_idx] total_years = NAK_YEARS[nak_idx] - + # 已用比例 nak_span = 360.0 / 27 nak_start = nak_idx * nak_span deg_in_nak = moon_longitude - nak_start used_ratio = deg_in_nak / nak_span remaining_ratio = 1 - used_ratio - + # 第一个Mahadasha的剩余年数 first_mahadasha_remaining = total_years * remaining_ratio first_mahadasha_elapsed = total_years * used_ratio - + return { 'moon_nakshatra': nak['nakshatra'], 'moon_nakshatra_lord': nak_lord, @@ -151,7 +154,7 @@ def calc_sub_lord(longitude: float, division: int = 9) -> Dict: """ Sub-Lord计算(KP占星体系) 将星宿进一步细分为Sub-Lord和Sub-Sub-Lord - + 参数: longitude: 恒星黄道经度 division: 细分层级(9=Sub-Lord, 81=Sub-Sub-Lord) @@ -159,24 +162,21 @@ def calc_sub_lord(longitude: float, division: int = 9) -> Dict: # 主星宿 nak = find_nakshatra(longitude) nak_span = 360.0 / 27 - - # Sub-Lord: 将星宿按守护星的大运年数比例细分 - total_dasha_years = sum(NAK_YEARS) # 120年 - nak_lord = NAK_NAMES[nak['nakshatra_idx']] - - # 计算Sub-Lord + + # Sub-Lord: 当前版本为简化版,直接按9等分。 + # 完整 KP 体系应按 Vimshottari 年数比例不等分,后续需单独对标。 + nak_lord = NAK_LORDS[nak['nakshatra_idx']] deg_in_nak = nak['degree_in_nakshatra'] - sub_lord_span = nak_span * (NAK_YEARS[NAK_LORDS.index(NAK_LORDS[nak['nakshatra_idx']])]) / total_dasha_years - + # 简化版:直接按9等分 sub_span = nak_span / 9 sub_idx = int(deg_in_nak / sub_span) % 9 - + # Sub-Lord是第sub_idx个行星(从星宿守护星开始的大运顺序) DASHA_ORDER = ["Ketu","Venus","Sun","Moon","Mars","Rahu","Jupiter","Saturn","Mercury"] nak_lord_idx = DASHA_ORDER.index(NAK_LORDS[nak['nakshatra_idx']]) sub_lord = DASHA_ORDER[(nak_lord_idx + sub_idx) % 9] - + return { 'nakshatra': nak['nakshatra'], 'nakshatra_lord': nak_lord, @@ -195,17 +195,17 @@ def nakshatra_compatibility(nak1_idx: int, nak2_idx: int) -> Dict: # Tara匹配 tara = calc_tara_bala(nak1_idx, nak2_idx) tara_score = 3 if tara['is_auspicious'] else 0 if tara['is_dangerous'] else 1.5 - + # 元素匹配 el1 = NAK_ELEMENT.get(nak1_idx, '') el2 = NAK_ELEMENT.get(nak2_idx, '') el_compatible = el1 == el2 or (el1 in ('火','风') and el2 in ('火','风')) or (el1 in ('土','水') and el2 in ('土','水')) - + # Gana匹配 g1 = NAK_GANA.get(nak1_idx, '') g2 = NAK_GANA.get(nak2_idx, '') gana_score = 6 if g1 == g2 else 3 if ('Dev' in g1 and 'Manushya' in g2) or ('Manushya' in g1 and 'Dev' in g2) else 0 - + return { 'nak1': NAK_NAMES[nak1_idx], 'nak2': NAK_NAMES[nak2_idx], @@ -230,3 +230,330 @@ def _tara_interp(idx): 8: '至友Tara—大吉,最强吉Tara', } return interps.get(idx, '') + + +# ============================================================================ +# Chandra Bala(月座力量)— v2.0 新增 +# ============================================================================ + +# 12宫 Chandra Bala 名称(从月亮星座起算) +CHANDRA_NAMES = [ + 'Janma(生命)', 'Sampat(财富)', 'Vipat(危险)', 'Kshema(安全)', + 'Pratyak(障碍)', 'Sadhana(成就)', 'Vadha(毁灭)', 'Mitra(友好)', + 'ParamaMitra(至友)', 'Karma(业力)', 'Labha(收益)', 'Vyaya(消耗)', +] +CHANDRA_CN = { + 0: '生命位', 1: '财富位', 2: '危险位', 3: '安全位', + 4: '障碍位', 5: '成就位', 6: '毁灭位', 7: '友好位', + 8: '至友位', 9: '业力位', 10: '收益位', 11: '消耗位', +} + + +def calc_chandra_bala(natal_moon_sign_idx: int, transit_moon_sign_idx: int) -> Dict: + """ + Chandra Bala(月座力量)计算 — v2.0 新增 + + 基于月亮所在宫位(Rashi/Sign)的相对位置评估力量。 + 从本命月亮星座起算到当前(过境)月亮星座的12宫循环。 + + 与 Tara Bala(星宿/Nakshatra 层面)互补,提供 Rashi/Sign 层面的力量评估。 + 常用于 Muhurta 择时和 Transit 过境分析。 + + 参数: + natal_moon_sign_idx: 本命月亮星座索引 (0=Aries ~ 11=Pisces) + transit_moon_sign_idx: 过境月亮星座索引 (0=Aries ~ 11=Pisces) + + 返回: + dict: { + 'rashi_from_moon': 0-11, + 'chandra_name': 'Janma/Sampat/...', + 'chandra_cn': '生命位/财富位/...', + 'is_auspicious': bool, + 'is_dangerous': bool, + 'quality': 'auspicious'/'dangerous'/'neutral', + 'interpretation': str, + } + """ + # 从本命月亮星座到过境月亮星座的步数(0-based, 12宫循环) + distance = (transit_moon_sign_idx - natal_moon_sign_idx) % 12 + + name = CHANDRA_NAMES[distance] + cn = CHANDRA_CN[distance] + + # 吉位:Sampat(1), Kshema(3), Sadhana(5), Mitra(7), ParamaMitra(8), Labha(10) + # 凶位:Vipat(2), Pratyak(4), Vadha(6), Karma(9) + # 中性:Janma(0), Vyaya(11) + is_auspicious = distance in (1, 3, 5, 7, 8, 10) + is_dangerous = distance in (2, 4, 6, 9) + + if is_auspicious: + quality = 'auspicious' + elif is_dangerous: + quality = 'dangerous' + else: + quality = 'neutral' + + return { + 'rashi_from_moon': distance, + 'chandra_name': name, + 'chandra_cn': cn, + 'is_auspicious': is_auspicious, + 'is_dangerous': is_dangerous, + 'quality': quality, + 'interpretation': _chandra_interp(distance), + } + + +def _chandra_interp(idx: int) -> str: + """Chandra Bala 解释""" + interps = { + 0: '生命位—代表身体和自我状态,中性', + 1: '财富位—吉,有利于财务和资源获取', + 2: '危险位—凶,可能遭遇损失和挑战', + 3: '安全位—吉,提供保护和稳定感', + 4: '障碍位—凶,面临困难和拖延', + 5: '成就位—吉,有利于达成目标和获得成功', + 6: '毁灭位—凶,需警惕冲突和破坏', + 7: '友好位—吉,获得他人的支持和帮助', + 8: '至友位—大吉,最强吉位,全面有利', + 9: '业力位—凶,涉及过往业力的清算', + 10: '收益位—吉,有利于收入和事业增长', + 11: '消耗位—中性偏凶,可能消耗能量和资源', + } + return interps.get(idx, '') + + +def calc_tara_chandra_combined( + natal_moon_nak_idx: int, + natal_moon_sign_idx: int, + planet_lons: Dict[str, float], + transit_moon_lon: Optional[float] = None, + transit_moon_sign_idx: Optional[int] = None, +) -> Dict: + """ + Tara Bala + Chandra Bala 双维度综合行星力量分析 — v2.0 新增 + + 同时评估: + 1. Tara Bala(Star层面):月亮星宿到行星星宿的9星宿循环 + 2. Chandra Bala(Sign层面):月亮星座到行星星座的12宫循环 + + 双吉 = 最强力,双凶 = 最弱力,一吉一凶 = 混合力量 + + 参数: + natal_moon_nak_idx: 本命月亮星宿索引 (0-26) + natal_moon_sign_idx: 本命月亮星座索引 (0-11) + planet_lons: {行星名: 恒星黄道经度} + transit_moon_lon: 过境月亮经度(可选,用于过境分析) + transit_moon_sign_idx: 过境月亮星座索引(可选,用于过境分析) + + 返回: + dict: {planet: {tara, chandra, combined_score, interpretation}} + """ + results = {} + + for pname, lon in planet_lons.items(): + # Tara Bala(Nakshatra层面) + p_nak = find_nakshatra(lon) + tara = calc_tara_bala(natal_moon_nak_idx, p_nak['nakshatra_idx']) + + # Chandra Bala(Rashi层面) + p_sign_idx = int(lon / 30) % 12 + chandra = calc_chandra_bala(natal_moon_sign_idx, p_sign_idx) + + # 综合评分 + combined_score = _combined_score(tara, chandra) + + results[pname] = { + 'nakshatra': p_nak['nakshatra'], + 'sign_idx': p_sign_idx, + 'tara': tara, + 'chandra': chandra, + 'combined_score': combined_score, + 'assessment': _combined_assessment(combined_score), + } + + return results + + +def _combined_score(tara: Dict, chandra: Dict) -> str: + """综合 Tara + Chandra 评分""" + t_good = tara['is_auspicious'] + c_good = chandra['is_auspicious'] + t_bad = tara['is_dangerous'] + c_bad = chandra['is_dangerous'] + + if t_good and c_good: + return 'double_auspicious' + elif t_bad and c_bad: + return 'double_dangerous' + elif t_good and c_bad: + return 'tara_good_chandra_bad' + elif t_bad and c_good: + return 'tara_bad_chandra_good' + else: + # 至少有一方中性 + if t_good or c_good: + return 'mixed_favorable' + elif t_bad or c_bad: + return 'mixed_unfavorable' + else: + return 'neutral' + + +def _combined_assessment(score: str) -> str: + """综合评估说明""" + assessments = { + 'double_auspicious': '双吉—Star与Sign层面均有利,最强力支持', + 'double_dangerous': '双凶—Star与Sign层面均不利,需格外谨慎', + 'tara_good_chandra_bad': '星吉宫凶—Nakshatra有利但Rashi层面受阻,可能好事多磨', + 'tara_bad_chandra_good': '星凶宫吉—Nakshatra不利但Rashi层面有保护,或许有惊无险', + 'mixed_favorable': '偏吉—至少一方有利,方向积极', + 'mixed_unfavorable': '偏凶—至少一方不利,需注意', + 'neutral': '中性—无明确吉凶指向', + } + return assessments.get(score, '') + + +def calc_nakshatra_transits_natal( + planet_lons: Dict[str, float], +) -> Dict: + """ + 计算所有行星在星盘中的 Nakshatra 分布 — v2.0 新增 + + 用于快速查看每个行星落在哪个星宿、Pada、守护星等信息。 + + 返回: + dict: {planet: {nakshatra, pada, lord, gana, element, degree_in_nak}} + """ + results = {} + for pname, lon in planet_lons.items(): + nak = find_nakshatra(lon) + results[pname] = { + 'nakshatra': nak['nakshatra'], + 'nakshatra_idx': nak['nakshatra_idx'], + 'nakshatra_lord': nak['nakshatra_lord'], + 'pada': nak['pada'], + 'gana': nak['gana'], + 'element': nak['element'], + 'degree_in_nakshatra': nak['degree_in_nakshatra'], + 'dasha_years': nak['dasha_years'], + } + return results + + +def nakshatra_full_report( + chart_data: Dict, + age: Optional[float] = None, + transit_date: Optional[str] = None, +) -> Dict: + """ + 综合星宿力量完整报告 — v2.0 新增 + + 包含: + 1. 本命 Nakshatra 分布(所有行星) + 2. Tara Bala 全部分析(以月亮星宿为基准) + 3. Chandra Bala 全部分析(以月亮星座为基准) + 4. Tara+Chandra 双维度综合评分 + 5. Nakshatra 兼容性(行星间的星宿关系) + 6. Vimshottari 起始点信息 + 7. Sub-Lord KP 分析 + + 参数: + chart_data: 星盘数据 (from jyotish_engine chart command) + age: 当前年龄(可选) + transit_date: 过境日期 YYYY-MM-DD(可选) + + 返回: + dict: 完整星宿报告 + """ + planets = chart_data.get('planets', {}) + planet_lons = {} + planet_signs = {} + for pn, pd in planets.items(): + if isinstance(pd, dict) and 'degree' in pd: + planet_lons[pn] = pd['degree'] + planet_signs[pn] = int(pd['degree'] / 30) % 12 + + if not planet_lons: + return {'error': '无法获取行星经度数据'} + + moon_lon = planet_lons.get('Moon', 0) + moon_nak_idx = int(moon_lon / (360/27)) % 27 + moon_sign_idx = int(moon_lon / 30) % 12 + + report = {} + + # 1. 本命星宿分布 + report['natal_nakshatras'] = calc_nakshatra_transits_natal(planet_lons) + + # 2. Tara Bala + report['tara_bala'] = calc_all_tara_balas(moon_nak_idx, planet_lons) + + # 3. Chandra Bala + chandra_bala = {} + for pn, lon in planet_lons.items(): + p_sign = int(lon / 30) % 12 + chandra_bala[pn] = { + 'planet_sign': p_sign, + 'chandra': calc_chandra_bala(moon_sign_idx, p_sign), + } + report['chandra_bala'] = chandra_bala + + # 4. Tara + Chandra 双维综合 + report['tara_chandra_combined'] = calc_tara_chandra_combined( + moon_nak_idx, moon_sign_idx, planet_lons) + + # 5. Sub-Lords + report['sub_lords'] = {pn: calc_sub_lord(lon) for pn, lon in planet_lons.items()} + + # 6. Vimshottari 起始点 + report['vimshottari_start'] = calc_vimshottari_start(moon_lon) + + # 7. 星宿力量排序 + power_ranking = [] + for pn, combined in report['tara_chandra_combined'].items(): + score_val = _score_value(combined['combined_score']) + power_ranking.append({ + 'planet': pn, + 'nakshatra': combined['nakshatra'], + 'tara_name': combined['tara']['tara_cn'], + 'tara_quality': combined['tara']['quality'], + 'chandra_name': combined['chandra']['chandra_cn'], + 'chandra_quality': combined['chandra']['quality'], + 'combined': combined['combined_score'], + 'score': score_val, + 'assessment': combined['assessment'], + }) + power_ranking.sort(key=lambda x: x['score'], reverse=True) + report['power_ranking'] = power_ranking + + # 8. 汇总 + double_auspicious = [p['planet'] for p in power_ranking if p['combined'] == 'double_auspicious'] + double_dangerous = [p['planet'] for p in power_ranking if p['combined'] == 'double_dangerous'] + report['summary'] = { + 'moon_nakshatra': NAK_NAMES[moon_nak_idx], + 'moon_nakshatra_lord': NAK_LORDS[moon_nak_idx], + 'moon_sign_idx': moon_sign_idx, + 'strongest_planets': [p['planet'] for p in power_ranking[:3]], + 'weakest_planets': [p['planet'] for p in power_ranking[-3:]], + 'double_auspicious_planets': double_auspicious, + 'double_dangerous_planets': double_dangerous, + 'tara_auspicious_count': sum(1 for p in power_ranking if p['tara_quality'] == 'auspicious'), + 'chandra_auspicious_count': sum(1 for p in power_ranking if p['chandra_quality'] == 'auspicious'), + } + + return report + + +def _score_value(combined: str) -> int: + """将组合评分转为数值""" + scores = { + 'double_auspicious': 100, + 'tara_good_chandra_bad': 60, + 'tara_bad_chandra_good': 60, + 'mixed_favorable': 55, + 'neutral': 50, + 'mixed_unfavorable': 45, + 'double_dangerous': 0, + } + return scores.get(combined, 50) diff --git a/scripts/nakshatra_dasha.py b/scripts/nakshatra_dasha.py new file mode 100644 index 00000000..35b01745 --- /dev/null +++ b/scripts/nakshatra_dasha.py @@ -0,0 +1,692 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +星宿大运推演系统 v1.0 — Nakshatra Dasha Engine + +支持: + - Ashtottari Dasha(108年星宿大运,条件性系统) + - Vimshottari Nakshatra-level 详细拆解 + - Nakshatra Transit Overlay(过境星宿叠加大运) + - Nakshatra-based 时间窗口推演 + - 综合星宿大运报告 + +设计原则: + - Ashtottari 作为 Vimshottari 的补充系统(条件:Rahu 不在 Kendra 时适用) + - Nakshatra-level 拆解提供当前大运的星宿层面的精细时间线 + - Transit Overlay 将过境星宿叠加上大运周期,提供精确时间窗口 +""" +from typing import Dict, List, Optional, Tuple +import math +from datetime import datetime, timedelta + +# 从 nakshatra_advanced 导入需要的数据 +try: + from nakshatra_advanced import ( + NAK_LIST, NAK_NAMES, NAK_LORDS, NAK_YEARS, + find_nakshatra, calc_tara_bala, + ) +except ImportError: + import sys, os + sys.path.insert(0, os.path.dirname(__file__)) + from nakshatra_advanced import ( + NAK_LIST, NAK_NAMES, NAK_LORDS, NAK_YEARS, + find_nakshatra, calc_tara_bala, + ) + +# ============================================================================ +# Ashtottari Dasha 数据表 +# ============================================================================ + +# Ashtottari 行星年数(108年制) +ASHTOTTARI_YEARS = { + 'Sun': 6, 'Moon': 15, 'Mars': 8, 'Mercury': 17, + 'Saturn': 10, 'Jupiter': 19, 'Rahu': 12, 'Venus': 21, +} +ASHTOTTARI_TOTAL = sum(ASHTOTTARI_YEARS.values()) # 108 + +# Ashtottari 大运顺序(固定循环) +ASHTOTTARI_ORDER = ['Sun', 'Moon', 'Mars', 'Mercury', 'Saturn', 'Jupiter', 'Rahu', 'Venus'] + +# Nakshatra → Ashtottari 起始守护星映射 +# 按 Vimshottari 星宿守护星对应 Ashtottari 起始点 +# Ketu 星宿 → 不作为 Ashtottari 起点,回溯到上一个 Venus +NAK_TO_ASHTOTTARI_START = { + 'Ashwini': 'Venus', # Ketu nakshatra → 上一周期末尾 Venus + 'Bharani': 'Venus', # Venus + 'Krittika': 'Sun', # Sun + 'Rohini': 'Moon', # Moon + 'Mrigashira': 'Mars', # Mars + 'Ardra': 'Rahu', # Rahu + 'Punarvasu': 'Jupiter', # Jupiter + 'Pushya': 'Saturn', # Saturn + 'Ashlesha': 'Mercury', # Mercury + 'Magha': 'Venus', # Ketu → Venus + 'Purva Phalguni': 'Venus', # Venus + 'Uttara Phalguni': 'Sun', # Sun + 'Hasta': 'Moon', # Moon + 'Chitra': 'Mars', # Mars + 'Swati': 'Rahu', # Rahu + 'Vishakha': 'Jupiter', # Jupiter + 'Anuradha': 'Saturn', # Saturn + 'Jyeshtha': 'Mercury', # Mercury + 'Mula': 'Venus', # Ketu → Venus + 'Purva Ashadha': 'Venus', # Venus + 'Uttara Ashadha': 'Sun', # Sun + 'Shravana': 'Moon', # Moon + 'Dhanishta': 'Mars', # Mars + 'Shatabhisha': 'Rahu', # Rahu + 'Purva Bhadrapada': 'Jupiter', # Jupiter + 'Uttara Bhadrapada': 'Saturn', # Saturn + 'Revati': 'Mercury', # Mercury +} + + +# ============================================================================ +# 核心计算函数 +# ============================================================================ + +def is_ashtottari_applicable(rahu_house: int) -> bool: + """ + 判断 Ashtottari Dasha 是否适用 + + 传统规则:当 Rahu 不在 Kendra(1/4/7/10宫)时,Ashtottari 优先。 + 若 Rahu 在 Kendra,则使用 Vimshottari。 + + 参数: + rahu_house: Rahu 所在宫位 (1-12) + + 返回: + bool: True = Ashtottari 适用 + """ + kendras = {1, 4, 7, 10} + return rahu_house not in kendras + + +def calc_ashtottari_dasha( + moon_longitude: float, + birth_date_str: str, + rahu_house: int, +) -> Dict: + """ + 计算 Ashtottari Dasha 108年大运序列 — v1.0 + + 参数: + moon_longitude: 月亮恒星黄道经度 + birth_date_str: 出生日期 YYYY-MM-DD + rahu_house: Rahu 所在宫位 (1-12) + + 返回: + dict: { + 'applicable': bool, + 'reason': str, + 'starting_lord': str, + 'sequence': [...], + 'total_years': 108, + } + """ + applicable = is_ashtottari_applicable(rahu_house) + + # 月亮星宿信息 + nak_data = find_nakshatra(moon_longitude) + moon_nak = nak_data['nakshatra'] + moon_nak_idx = nak_data['nakshatra_idx'] + + # 起始守护星 + starting_lord = NAK_TO_ASHTOTTARI_START.get(moon_nak, 'Sun') + + # 计算起始点的已用/剩余比例 + nak_span = 360.0 / 27 + nak_start = moon_nak_idx * nak_span + deg_in_nak = moon_longitude - nak_start + used_ratio = deg_in_nak / nak_span + remaining_ratio = 1 - used_ratio + + start_lord_years = ASHTOTTARI_YEARS.get(starting_lord, 6) + first_remaining = start_lord_years * remaining_ratio + + # 构建大运序列 + start_idx = ASHTOTTARI_ORDER.index(starting_lord) + sequence = [] + cumulative = 0.0 + + # 首个大运的剩余部分 + sequence.append({ + 'lord': starting_lord, + 'full_years': start_lord_years, + 'remaining_years': round(first_remaining, 4), + 'elapsed_years': round(start_lord_years - first_remaining, 4), + 'cumulative_from_birth': 0.0, + 'cumulative_to_birth': round(first_remaining, 4), + 'is_first': True, + }) + cumulative += first_remaining + + # 后续完整大运循环 + idx = (start_idx + 1) % 8 + cycle_count = 1 + while cumulative < 120: # 超出人生预期范围 + lord = ASHTOTTARI_ORDER[idx] + years = ASHTOTTARI_YEARS[lord] + sequence.append({ + 'lord': lord, + 'full_years': years, + 'remaining_years': years, + 'elapsed_years': 0, + 'cumulative_from_birth': round(cumulative, 4), + 'cumulative_to_birth': round(cumulative + years, 4), + 'is_first': False, + }) + cumulative += years + idx = (idx + 1) % 8 + cycle_count += 1 + if cycle_count > 20: # 安全上限 + break + + # 出生日期 + try: + birth_date = datetime.strptime(birth_date_str, '%Y-%m-%d') + except: + birth_date = datetime(2000, 1, 1) + + # 为大运添加日期范围 + for item in sequence: + from_years = item['cumulative_from_birth'] + to_years = item['cumulative_to_birth'] + item['date_from'] = _add_years_to_date(birth_date, from_years) + item['date_to'] = _add_years_to_date(birth_date, to_years) + item['age_from'] = round(from_years, 2) + item['age_to'] = round(to_years, 2) + + if applicable: + reason = f"Rahu 在第{rahu_house}宫,不在 Kendra(1/4/7/10),Ashtottari 适用" + else: + reason = f"Rahu 在第{rahu_house}宫,在 Kendra,建议使用 Vimshottari" + + return { + 'system': 'Ashtottari Dasha(108年星宿大运)', + 'applicable': applicable, + 'reason': reason, + 'rahu_house': rahu_house, + 'moon_nakshatra': moon_nak, + 'starting_lord': starting_lord, + 'total_years': ASHTOTTARI_TOTAL, + 'first_remaining_years': round(first_remaining, 4), + 'sequence': sequence, + 'note': ('Ashtottari 作为 Vimshottari 的补充系统,' + '当 Rahu 不在 Kendra 时提供替代视角。' + '两者各有侧重:Vimshottari 偏"心识",Ashtottari 偏"命运安排"。'), + } + + +def calc_current_ashtottari( + ashtottari_data: Dict, + age: float, +) -> Dict: + """ + 从 Ashtottari 序列中定位当前年龄的大运 + + 返回: + dict: {lord, years_remaining, progress, sub_periods, ...} + """ + if not ashtottari_data.get('applicable', False): + return {'applicable': False, 'reason': ashtottari_data.get('reason', '')} + + sequence = ashtottari_data.get('sequence', []) + current = None + next_item = None + + for i, item in enumerate(sequence): + if item['cumulative_from_birth'] <= age < item['cumulative_to_birth']: + current = item + if i + 1 < len(sequence): + next_item = sequence[i + 1] + break + + if current is None: + return {'error': f'无法定位年龄 {age} 在 Ashtottari 序列中'} + + years_elapsed_in_md = age - current['cumulative_from_birth'] + years_remaining = current['cumulative_to_birth'] - age + progress = years_elapsed_in_md / current['remaining_years'] if current['remaining_years'] > 0 else 0 + + # 计算 Antardasha 子周期(简化:按比例分配) + antardasha_sequence = _calc_ashtottari_antardasha(current['lord'], current['remaining_years']) + + # 定位当前 Antardasha + ad_cumulative = 0 + current_ad = None + for ad in antardasha_sequence: + ad_end = ad_cumulative + ad['years'] + if ad_cumulative <= years_elapsed_in_md < ad_end: + current_ad = { + **ad, + 'progress_in_ad': (years_elapsed_in_md - ad_cumulative) / ad['years'] if ad['years'] > 0 else 0, + 'remaining_years': round(ad_end - years_elapsed_in_md, 4), + } + break + ad_cumulative = ad_end + + return { + 'applicable': True, + 'current_mahadasha': current['lord'], + 'mahadasha_progress': round(progress, 4), + 'years_elapsed_in_md': round(years_elapsed_in_md, 4), + 'years_remaining_in_md': round(years_remaining, 4), + 'age_from': current['age_from'], + 'age_to': current['age_to'], + 'next_mahadasha': next_item['lord'] if next_item else None, + 'antardasha_sequence': antardasha_sequence, + 'current_antardasha': current_ad, + } + + +def _calc_ashtottari_antardasha(md_lord: str, md_years: float) -> List[Dict]: + """计算 Ashtottari Antardasha 子周期""" + md_start_idx = ASHTOTTARI_ORDER.index(md_lord) + total = sum(ASHTOTTARI_YEARS.values()) + + result = [] + for i in range(8): + ad_lord = ASHTOTTARI_ORDER[(md_start_idx + i) % 8] + ad_years_total = ASHTOTTARI_YEARS[ad_lord] + # 按比例缩放 + ad_years_in_md = md_years * ad_years_total / total + result.append({ + 'lord': ad_lord, + 'years': round(ad_years_in_md, 4), + 'months': round(ad_years_in_md * 12, 1), + }) + + return result + + +# ============================================================================ +# Vimshottari Nakshatra-level 拆解 +# ============================================================================ + +# Vimshottari Dasha 顺序(9星循环) +VIMSHOTTARI_ORDER = ['Ketu', 'Venus', 'Sun', 'Moon', 'Mars', 'Rahu', 'Jupiter', 'Saturn', 'Mercury'] +VIMSHOTTARI_YEARS = {'Ketu': 7, 'Venus': 20, 'Sun': 6, 'Moon': 10, 'Mars': 7, 'Rahu': 18, 'Jupiter': 16, 'Saturn': 19, 'Mercury': 17} + + +def calc_nakshatra_dasha_breakdown( + moon_longitude: float, + planet_lons: Dict[str, float], + birth_date_str: str, + age: float, +) -> Dict: + """ + Vimshottari Nakshatra-level 详细拆解 — v1.0 + + 在当前 Vimshottari 大运/小运框架下,提供: + 1. 大运守护星的 Nakshatra 位置及其 Tara 关系 + 2. 小运守护星的 Nakshatra 位置 + 3. Nakshatra Pada 层面的时间窗口 + 4. 月亮星宿到各大运/小运守护星星宿的 Tara Bala + + 参数: + moon_longitude: 月亮恒星黄道经度 + planet_lons: {行星名: 恒星经度} + birth_date_str: 出生日期 YYYY-MM-DD + age: 当前年龄(岁) + + 返回: + dict: 星宿层面的大运拆解 + """ + moon_nak_data = find_nakshatra(moon_longitude) + moon_nak_idx = moon_nak_data['nakshatra_idx'] + + # 计算 Vimshottari 起始 + nak = find_nakshatra(moon_longitude) + nak_idx = nak['nakshatra_idx'] + nak_span = 360.0 / 27 + nak_start = nak_idx * nak_span + deg_in_nak = moon_longitude - nak_start + used_ratio = deg_in_nak / nak_span + + # 起始守护星和剩余年数 + start_lord = NAK_LORDS[nak_idx] + start_total = VIMSHOTTARI_YEARS.get(start_lord, 7) + first_remaining = start_total * (1 - used_ratio) + + # 构建大运序列 + start_idx = VIMSHOTTARI_ORDER.index(start_lord) + sequence = [] + cumulative = 0.0 + + # 首个(剩余)大运 + sequence.append({ + 'lord': start_lord, + 'years': round(first_remaining, 4), + 'cumulative_from': 0.0, + 'is_first': True, + }) + cumulative += first_remaining + + idx = (start_idx + 1) % 9 + for _ in range(15): # 足够覆盖到120岁 + lord = VIMSHOTTARI_ORDER[idx] + years = VIMSHOTTARI_YEARS[lord] + sequence.append({ + 'lord': lord, + 'years': round(years, 4), + 'cumulative_from': round(cumulative, 4), + 'is_first': False, + }) + cumulative += years + idx = (idx + 1) % 9 + if cumulative > 120: + break + + # 定位当前大运 + current_md = None + current_md_idx = -1 + for i, item in enumerate(sequence): + if item['cumulative_from'] <= age < (item['cumulative_from'] + item['years']): + current_md = item + current_md_idx = i + break + + if current_md is None: + return {'error': f'无法定位年龄 {age} 在 Vimshottari 序列中'} + + md_lord = current_md['lord'] + md_years = current_md['years'] + md_start_age = current_md['cumulative_from'] + years_in_md = age - md_start_age + + # 大运守护星的 Nakshatra + md_lord_nak = None + if md_lord in planet_lons: + md_lord_nak = find_nakshatra(planet_lons[md_lord]) + + # 计算当前 Antardasha + antardasas = _calc_vimshottari_antardasha_full(md_lord, md_years) + + # 定位当前 Antardasha + ad_cumulative = 0.0 + current_ad = None + for ad in antardasas: + ad_end = ad_cumulative + ad['years'] + if ad_cumulative <= years_in_md < ad_end: + current_ad = { + 'lord': ad['lord'], + 'years': ad['years'], + 'progress': (years_in_md - ad_cumulative) / ad['years'] if ad['years'] > 0 else 0, + 'remaining': round(ad_end - years_in_md, 4), + } + break + ad_cumulative = ad_end + + # 当前 Antardasha 守护星的 Nakshatra + ad_lord_nak = None + if current_ad and current_ad['lord'] in planet_lons: + ad_lord_nak = find_nakshatra(planet_lons[current_ad['lord']]) + + # Nakshatra Tara 关系分析 + tara_analysis = {} + if md_lord_nak: + tara_analysis['mahadasha_lord'] = { + 'planet': md_lord, + 'nakshatra': md_lord_nak['nakshatra'], + 'pada': md_lord_nak['pada'], + 'tara_to_moon': calc_tara_bala(moon_nak_idx, md_lord_nak['nakshatra_idx']), + } + if ad_lord_nak and current_ad: + tara_analysis['antardasha_lord'] = { + 'planet': current_ad['lord'], + 'nakshatra': ad_lord_nak['nakshatra'], + 'pada': ad_lord_nak['pada'], + 'tara_to_moon': calc_tara_bala(moon_nak_idx, ad_lord_nak['nakshatra_idx']), + } + + # 星宿层面的时间窗口(Pada-level 细分) + pada_windows = [] + if md_lord_nak and current_ad: + nak_span_deg = 360.0 / 27 + pada_span_deg = nak_span_deg / 4 + md_years_per_nak = md_years / 9 # 每个 Antardasha 时长近似 + md_years_per_pada = md_years_per_nak / 4 + + for ad in antardasas: + if md_years_per_nak > 0: + ad_nak = find_nakshatra(planet_lons.get(ad['lord'], 0)) + pada_windows.append({ + 'period': f"AD {ad['lord']}", + 'nakshatra': ad_nak['nakshatra'], + 'pada': ad_nak['pada'], + 'years': round(ad['years'], 4), + 'tara': calc_tara_bala(moon_nak_idx, ad_nak['nakshatra_idx']), + }) + + # 构建返回 + birth_date = None + try: + birth_date = datetime.strptime(birth_date_str, '%Y-%m-%d') + except: + pass + + return { + 'system': 'Vimshottari Nakshatra-level Breakdown', + 'moon_nakshatra': moon_nak_data['nakshatra'], + 'moon_nakshatra_idx': moon_nak_idx, + 'moon_pada': moon_nak_data['pada'], + 'current_age': age, + 'mahadasha': { + 'lord': md_lord, + 'years_total': round(md_years, 4), + 'age_range': f"{round(md_start_age, 2)}-{round(md_start_age + md_years, 2)}", + 'years_elapsed': round(years_in_md, 4), + 'years_remaining': round(md_years - years_in_md, 4), + 'progress': round(years_in_md / md_years, 4) if md_years > 0 else 0, + 'nakshatra': md_lord_nak, + 'tara': tara_analysis.get('mahadasha_lord', {}), + }, + 'antardasha': current_ad, + 'tara_analysis': tara_analysis, + 'pada_windows': pada_windows, + } + + +def _calc_vimshottari_antardasha_full(md_lord: str, md_years: float) -> List[Dict]: + """计算完整的 Vimshottari Antardasha(9个周期)""" + md_start_idx = VIMSHOTTARI_ORDER.index(md_lord) + total_years = sum(VIMSHOTTARI_YEARS.values()) # 120 + + result = [] + for i in range(9): + ad_lord = VIMSHOTTARI_ORDER[(md_start_idx + i) % 9] + ad_years_total = VIMSHOTTARI_YEARS[ad_lord] + ad_years = md_years * ad_years_total / total_years + result.append({ + 'lord': ad_lord, + 'years': round(ad_years, 4), + 'months': round(ad_years * 12, 1), + }) + + return result + + +# ============================================================================ +# Nakshatra Transit Overlay(过境星宿叠加大运) +# ============================================================================ + +def calc_nakshatra_transit_overlay( + natal_planet_lons: Dict[str, float], + transit_planet_lons: Dict[str, float], + moon_nak_idx: int, +) -> Dict: + """ + 将过境星宿叠加在本命大运框架上 — v1.0 + + 分析每个过境行星当前所在的星宿,及其: + 1. 与本命月亮星宿的 Tara 关系 + 2. 与本命行星的星宿距离 + 3. 当前星宿的 Pada 位置 + 4. 吉凶评估 + + 参数: + natal_planet_lons: 本命行星经度 + transit_planet_lons: 过境行星经度 + moon_nak_idx: 本命月亮星宿索引 + + 返回: + dict: 过境星宿叠加分析 + """ + results = {} + + for pname, t_lon in transit_planet_lons.items(): + t_nak = find_nakshatra(t_lon) + tara = calc_tara_bala(moon_nak_idx, t_nak['nakshatra_idx']) + + # 过境星宿是否与某本命行星的星宿重叠 + same_nak_as_natal = [] + n_lon = natal_planet_lons.get(pname) + if n_lon is not None: + n_nak = find_nakshatra(n_lon) + if n_nak['nakshatra_idx'] == t_nak['nakshatra_idx']: + same_nak_as_natal.append('self') + + for np, nl in natal_planet_lons.items(): + if np == pname: + continue + n_nak = find_nakshatra(nl) + if n_nak['nakshatra_idx'] == t_nak['nakshatra_idx']: + same_nak_as_natal.append(np) + + # 评估 + if tara['is_auspicious']: + transit_quality = 'favorable' + elif tara['is_dangerous']: + transit_quality = 'challenging' + else: + transit_quality = 'neutral' + + if same_nak_as_natal: + transit_quality = transit_quality + '_nak_conjunction' + note = f"过境星宿与本命{'/'.join(same_nak_as_natal)}同星宿,叠加效应增强" + else: + note = '' + + results[pname] = { + 'transit_nakshatra': t_nak['nakshatra'], + 'transit_nakshatra_lord': t_nak['nakshatra_lord'], + 'transit_pada': t_nak['pada'], + 'tara_to_natal_moon': tara, + 'same_nakshatra_as': same_nak_as_natal if same_nak_as_natal else None, + 'quality': transit_quality, + 'note': note, + 'gana': t_nak['gana'], + 'element': t_nak['element'], + } + + return results + + +# ============================================================================ +# 综合星宿大运报告 +# ============================================================================ + +def nakshatra_dasha_full_report( + chart_data: Dict, + birth_date_str: str, + age: float, + transit_planet_lons: Optional[Dict[str, float]] = None, +) -> Dict: + """ + 综合星宿大运完整报告 — v1.0 + + 包含: + 1. Ashtottari Dasha(条件性,Rahu 不在 Kendra 时计算) + 2. Vimshottari Nakshatra-level 拆解 + 3. Nakshatra Transit Overlay(如提供过境数据) + 4. 综合时间窗口与 Tara 评估 + + 参数: + chart_data: 星盘数据 + birth_date_str: 出生日期 YYYY-MM-DD + age: 当前年龄 + transit_planet_lons: 过境行星经度(可选) + + 返回: + dict: 完整星宿大运报告 + """ + planets = chart_data.get('planets', {}) + planet_lons = {} + for pn, pd in planets.items(): + if isinstance(pd, dict) and 'degree' in pd: + planet_lons[pn] = pd['degree'] + + if not planet_lons: + return {'error': '无法获取行星经度数据'} + + moon_lon = planet_lons.get('Moon', 0) + moon_nak = find_nakshatra(moon_lon) + moon_nak_idx = moon_nak['nakshatra_idx'] + + # 确定 Rahu 宫位 + asc_idx = chart_data.get('ascendant_index', 0) + rahu_deg = None + rahu_house = 1 # 默认 + if 'Rahu' in planet_lons: + rahu_deg = planet_lons['Rahu'] + rahu_sign_idx = int(rahu_deg / 30) % 12 + rahu_house = ((rahu_sign_idx - asc_idx) % 12) + 1 + + report = {} + + # 1. Ashtottari Dasha + report['ashtottari'] = calc_ashtottari_dasha(moon_lon, birth_date_str, rahu_house) + if report['ashtottari']['applicable']: + report['ashtottari_current'] = calc_current_ashtottari(report['ashtottari'], age) + + # 2. Vimshottari Nakshatra-level + report['vimshottari_nak_breakdown'] = calc_nakshatra_dasha_breakdown( + moon_lon, planet_lons, birth_date_str, age) + + # 3. Nakshatra Transit Overlay + if transit_planet_lons: + report['transit_overlay'] = calc_nakshatra_transit_overlay( + planet_lons, transit_planet_lons, moon_nak_idx) + + # 4. 汇总 + md_info = report['vimshottari_nak_breakdown'].get('mahadasha', {}) + ad_info = report['vimshottari_nak_breakdown'].get('antardasha', {}) + + summary = { + 'moon_nakshatra': moon_nak['nakshatra'], + 'moon_nakshatra_lord': moon_nak['nakshatra_lord'], + 'moon_pada': moon_nak['pada'], + 'current_age': age, + 'vimshottari_md': md_info.get('lord', 'N/A'), + 'vimshottari_ad': ad_info.get('lord', 'N/A') if ad_info else 'N/A', + 'ashtottari_applicable': report['ashtottari']['applicable'], + } + + if report['ashtottari']['applicable'] and 'ashtottari_current' in report: + ac = report['ashtottari_current'] + summary['ashtottari_md'] = ac.get('current_mahadasha', 'N/A') + if ac.get('current_antardasha'): + summary['ashtottari_ad'] = ac['current_antardasha']['lord'] + + # Dasha 系统的 Nakshatra Tara 评估 + tara_md = report['vimshottari_nak_breakdown'].get('tara_analysis', {}).get('mahadasha_lord', {}) + tara_ad = report['vimshottari_nak_breakdown'].get('tara_analysis', {}).get('antardasha_lord', {}) + + summary['md_tara_to_moon'] = tara_md.get('tara_to_moon', {}).get('tara_cn', 'N/A') if tara_md else 'N/A' + summary['ad_tara_to_moon'] = tara_ad.get('tara_to_moon', {}).get('tara_cn', 'N/A') if tara_ad else 'N/A' + + report['summary'] = summary + + return report + + +# ============================================================================ +# 工具函数 +# ============================================================================ + +def _add_years_to_date(dt: datetime, years: float) -> str: + """日期 + 年数 → ISO 日期字符串""" + total_days = years * 365.25 + new_dt = dt + timedelta(days=total_days) + return new_dt.strftime('%Y-%m-%d')