diff --git a/CHANGELOG.md b/CHANGELOG.md index e0f70446..81d9afdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # 印度占星 Skill 更新日志 +## v6.1.6(2026-06-07)—— full-reading 五系统推运收敛与接口修复 + +> **目标**:继续“合并隐藏模块→打通引擎→验证可用”的优化路线,将 Ashtottari / Yogini / Kalachakra 从独立模块进一步接入 `full-reading` 主链路。 + +### 关键改进 +- `full-reading` 新增输出:`modules.ashtottari_dasha`、`modules.kalachakra_dasha`,并改用 `scripts/yogini_dasha.py` 的公共接口输出 `modules.yogini_dasha`。 +- `dasa_convergence` 从三系统升级为五系统:Vimshottari + Chara Dasha + Yogini + Ashtottari + Kalachakra。 +- 新增 L5 收敛等级:四个及以上推运系统同时激活同一领域时标为顶级收敛信号;Chara Dasha 仍按既有规范保持 partial/低权重辅助。 +- 增强领域激活检测:Yogini/Ashtottari 当前主星、Kalachakra 当前 Rashi 与 lord 均参与宫位主题激活判断。 +- 修复替代 Dasha 模块 current 周期:超过单周期年龄时按 36/108/150 年周期循环,不再返回 None。 +- 修复 `full-reading` 中 Pancha Pakshi 与 Rashi Tulya Navamsa 的接口调用,改为对齐现有公共函数 `get_pancha_pakshi_schedule()` 与 `analyze_rtn()`。 + +### 验证 +```bash +python3 -m py_compile scripts/jyotish_engine.py scripts/ashtottari_dasha.py scripts/yogini_dasha.py scripts/kalachakra_dasha.py scripts/orchestrator_bridge.py +PYTHONPATH=scripts python3 scripts/jyotish_engine.py full-reading --year 1990 --month 6 --day 15 --hour 10 --minute 30 --lat 39.9 --lon 116.4 --tz 8 +``` +- smoke case:`summary.status = complete` +- `errors = []` +- `modules_computed = 47` +- `dasa_convergence.systems_summary` 包含 `ashtottari`、`chara_dasha`、`kalachakra`、`vimshottari`、`yogini` + +--- + ## v6.0.44(2026-06-06)—— 修复参考数据 JD 时区 Bug,验证基线校正 > **目标**:修复导致 `standard_test_charts.json` 中所有非正午出生星盘上升星座错误的 JD 时区 Bug。 diff --git a/SKILL.md b/SKILL.md index cf3f64d8..ca0f81f4 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,6 +1,6 @@ --- name: jyotish-vedic-astrology -version: 6.0.43 +version: 6.1.6 description: 印度占星(Jyotish)专业解盘与推运系统。核心能力:PDF星盘输入→严谨解盘→精确推运应期输出。触发词:印度占星、吠陀占星、Jyotish、解盘、推运、星盘分析、Dasha、Transit、Nakshatra、Yoga、出生时间矫正、PDF星盘、读取PDF、分析PDF星盘、现代解读、误判纠错、Varga分盘、综合分析、过境分析、合盘、婚姻匹配、年运盘、Prashna、Argala、Jaimini、Shadbala、Ashtakavarga、HTML报告、深度解盘。 --- @@ -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.47 | **详细变更**:`CHANGELOG.md` +> **版本**:v6.1.6 | **详细变更**:`CHANGELOG.md` ## Yoga 逻辑验证指标 @@ -43,7 +43,7 @@ description: 印度占星(Jyotish)专业解盘与推运系统。核心能力 2. **阶段一**(仅B):PDF/图片提取 + Quality Gate 3. **阶段二**:意图识别 → 路由目标宫位(无明确意图→Level 2综合解盘) 4. **阶段三**:静态分析10步(宫位→承诺→Yoga→Argala→逆行→NK→Shadbala→AV→Ketu→分盘) -5. **阶段四**:动态推运7步(Dasha→Convergence→Transit→Double Transit→Jaimini→KP→Varshaphala) +5. **阶段四**:动态推运7步(Dasha→五系统Convergence→Transit→Double Transit→Jaimini→KP→Varshaphala) 6. **阶段五**:应期输出(五层验证→时间窗口→Actionable Output+案例检索) 7. **阶段六**:补救措施(可选) 8. **阶段七**:现代措辞包装 @@ -180,7 +180,7 @@ $PYTHON $SCRIPT <子命令> [参数] | 子命令 | 功能 | |--------|------| -| `full-reading` | ⭐全自动综合解盘(13模块一键出) | +| `full-reading` | ⭐全自动综合解盘(47模块一键出,含五系统Dasha收敛) | | `chart` | 星盘计算+`--validate`附加R1-R10验证 | | `dasha` | Vimshottari大运时间线+小运展开 | | `yoga` | Yoga格局识别 | @@ -313,9 +313,9 @@ $PYTHON $SCRIPT <子命令> [参数] --- -**版本**:v6.0.46-yoga-f1-92 +**版本**:v6.1.6-full-reading-five-dasha-convergence **创建日期**:2026-04-20 -**最后更新**:2026-06-06(v6.0.46 修复 6 条完全错误的瑜伽规则:kaalanirdesat_puthra/sapthasankhya_sahodara/sarpasaapa/pushkala/koorma/bhratrumooladdhanaprapti,FP 从 193→78,Precision 从 83.26%→92.50%,F1 从 87.19%→92.10%;当前 476 条规则、405 条启用,硬编码测试 8/9 通过) +**最后更新**:2026-06-07(v6.1.6 将 Ashtottari/Yogini/Kalachakra 接入 full-reading,Dasa Convergence 从三系统升级为 Vimshottari + Chara + Yogini + Ashtottari + Kalachakra 五系统;修复 Pancha Pakshi/RTN 接口与替代 Dasha current 周期循环。Yoga 当前以 `references/validation_logic_report.json` 为准。) --- diff --git a/references/validation_logic_report.json b/references/validation_logic_report.json index a3bf98df..9218848a 100644 --- a/references/validation_logic_report.json +++ b/references/validation_logic_report.json @@ -2,7 +2,7 @@ "summary": { "charts_tested": 60, "comparable_rules": 82, - "skill_total": 1021, + "skill_total": 1020, "pyjhora_total": 1049, "unmapped_pyjhora": 718, "missing_mappings": { @@ -99,33 +99,33 @@ ] }, "agreements": 982, - "false_positives": 39, + "false_positives": 38, "false_negatives": 67, - "precision": 0.9618, + "precision": 0.9627, "recall": 0.9361, - "f1": 0.9488 + "f1": 0.9493 }, "false_positives": [ - { - "chart": "Albert Einstein", - "rule_id": "bvr_bhaga_chumbana_yoga", - "rule_name": "Bhaga Chumbana Yoga" - }, { "chart": "Albert Einstein", "rule_id": "bvr_dharidhra_11_precise", "rule_name": "Dharidhra Yoga" }, { - "chart": "Nikola Tesla", - "rule_id": "bvr_krisanga_yoga", - "rule_name": "Krisanga Yoga" + "chart": "Albert Einstein", + "rule_id": "bvr_bhaga_chumbana_yoga", + "rule_name": "Bhaga Chumbana Yoga" }, { "chart": "Nikola Tesla", "rule_id": "bvr_bhratruvriddhi_precise", "rule_name": "Bhratruvriddhi Yoga" }, + { + "chart": "Nikola Tesla", + "rule_id": "bvr_krisanga_yoga", + "rule_name": "Krisanga Yoga" + }, { "chart": "Stephen Hawking", "rule_id": "sankha_yoga", @@ -138,13 +138,13 @@ }, { "chart": "Alexander Fleming", - "rule_id": "bvr_bhaga_chumbana_yoga", - "rule_name": "Bhaga Chumbana Yoga" + "rule_id": "bvr_yukthi_samanwithavagmi_yoga", + "rule_name": "Yukthi Samanwithavagmi Yoga" }, { "chart": "Alexander Fleming", - "rule_id": "bvr_yukthi_samanwithavagmi_yoga", - "rule_name": "Yukthi Samanwithavagmi Yoga" + "rule_id": "bvr_bhaga_chumbana_yoga", + "rule_name": "Bhaga Chumbana Yoga" }, { "chart": "Max Planck", @@ -176,11 +176,6 @@ "rule_id": "bvr_swaveeryaddhana_precise", "rule_name": "Swaveeryaddhana Yoga" }, - { - "chart": "John F. Kennedy", - "rule_id": "bvr_nishkapata_precise", - "rule_name": "Nishkapata Yoga" - }, { "chart": "Nelson Mandela", "rule_id": "bvr_mathibhramana_yoga", @@ -193,13 +188,13 @@ }, { "chart": "Barack Obama", - "rule_id": "bvr_bandhubhisthyaktha_precise", - "rule_name": "Bandhubhisthyaktha Yoga" + "rule_id": "bvr_yukthi_samanwithavagmi_yoga", + "rule_name": "Yukthi Samanwithavagmi Yoga" }, { "chart": "Barack Obama", - "rule_id": "bvr_yukthi_samanwithavagmi_yoga", - "rule_name": "Yukthi Samanwithavagmi Yoga" + "rule_id": "bvr_bandhubhisthyaktha_precise", + "rule_name": "Bandhubhisthyaktha Yoga" }, { "chart": "Pablo Picasso", @@ -233,13 +228,13 @@ }, { "chart": "Lionel Messi", - "rule_id": "bvr_017_vosi_precise", - "rule_name": "Vosi Yoga" + "rule_id": "brahma_yoga", + "rule_name": "Brahma Yoga" }, { "chart": "Lionel Messi", - "rule_id": "brahma_yoga", - "rule_name": "Brahma Yoga" + "rule_id": "bvr_017_vosi_precise", + "rule_name": "Vosi Yoga" }, { "chart": "Cristiano Ronaldo", @@ -278,13 +273,13 @@ }, { "chart": "Princess Diana", - "rule_id": "bvr_bandhu_pujya_yoga", - "rule_name": "Bandhu Pujya Yoga" + "rule_id": "bvr_ayatna_griha_prapta_yoga", + "rule_name": "Ayatna Griha Prapta Yoga" }, { "chart": "Princess Diana", - "rule_id": "bvr_ayatna_griha_prapta_yoga", - "rule_name": "Ayatna Griha Prapta Yoga" + "rule_id": "bvr_bandhu_pujya_yoga", + "rule_name": "Bandhu Pujya Yoga" }, { "chart": "J. Krishnamurti", @@ -293,13 +288,13 @@ }, { "chart": "Ramana Maharshi", - "rule_id": "bvr_utthama_graha_yoga", - "rule_name": "Utthama Griha Yoga" + "rule_id": "bvr_matru_sneha_yoga", + "rule_name": "Matru Sneha Yoga" }, { "chart": "Ramana Maharshi", - "rule_id": "bvr_matru_sneha_yoga", - "rule_name": "Matru Sneha Yoga" + "rule_id": "bvr_utthama_graha_yoga", + "rule_name": "Utthama Griha Yoga" } ], "false_negatives": [ @@ -314,10 +309,10 @@ }, { "chart": "Marie Curie", - "rule_id": "bvr_thrikaala_gnana_yoga", - "rule_name": "Thrikaala Gnana Yoga", + "rule_id": "parvata_yoga", + "rule_name": "Parvata Yoga", "pyjhora_names": [ - "thrikaala_gnana_yoga" + "parvata_yoga" ] }, { @@ -331,10 +326,10 @@ }, { "chart": "Marie Curie", - "rule_id": "parvata_yoga", - "rule_name": "Parvata Yoga", + "rule_id": "bvr_thrikaala_gnana_yoga", + "rule_name": "Thrikaala Gnana Yoga", "pyjhora_names": [ - "parvata_yoga" + "thrikaala_gnana_yoga" ] }, { @@ -383,6 +378,14 @@ "pisacha_grastha_yoga" ] }, + { + "chart": "Mahatma Gandhi", + "rule_id": "brahma_yoga", + "rule_name": "Brahma Yoga", + "pyjhora_names": [ + "brahma_yoga" + ] + }, { "chart": "Mahatma Gandhi", "rule_id": "bvr_matrudeerghayur_yoga", @@ -392,14 +395,6 @@ "matrudeerghayur_yoga_197" ] }, - { - "chart": "Mahatma Gandhi", - "rule_id": "brahma_yoga", - "rule_name": "Brahma Yoga", - "pyjhora_names": [ - "brahma_yoga" - ] - }, { "chart": "Narendra Modi", "rule_id": "bvr_nishkapata_precise", @@ -417,6 +412,14 @@ "utthama_graha_yoga" ] }, + { + "chart": "Indira Gandhi", + "rule_id": "bvr_sumukha_precise", + "rule_name": "Sumukha Yoga", + "pyjhora_names": [ + "sumukha_yoga" + ] + }, { "chart": "Indira Gandhi", "rule_id": "bvr_nishkapata_precise", @@ -426,14 +429,6 @@ "nishkapata_yoga_206" ] }, - { - "chart": "Indira Gandhi", - "rule_id": "bvr_sumukha_precise", - "rule_name": "Sumukha Yoga", - "pyjhora_names": [ - "sumukha_yoga" - ] - }, { "chart": "Nelson Mandela", "rule_id": "bvr_apakeerthi_yoga", @@ -503,14 +498,6 @@ "nishkapata_yoga_206" ] }, - { - "chart": "Rabindranath Tagore", - "rule_id": "brahma_yoga", - "rule_name": "Brahma Yoga", - "pyjhora_names": [ - "brahma_yoga" - ] - }, { "chart": "Rabindranath Tagore", "rule_id": "bvr_kapata_yoga", @@ -521,6 +508,14 @@ "kapata_yoga_204" ] }, + { + "chart": "Rabindranath Tagore", + "rule_id": "brahma_yoga", + "rule_name": "Brahma Yoga", + "pyjhora_names": [ + "brahma_yoga" + ] + }, { "chart": "Ludwig van Beethoven", "rule_id": "bvr_thrikaala_gnana_yoga", @@ -529,6 +524,16 @@ "thrikaala_gnana_yoga" ] }, + { + "chart": "Wolfgang Mozart", + "rule_id": "bvr_kapata_yoga", + "rule_name": "Kapata Yoga", + "pyjhora_names": [ + "kapata_yoga_202", + "kapata_yoga_203", + "kapata_yoga_204" + ] + }, { "chart": "Wolfgang Mozart", "rule_id": "bvr_raja_bhanga_yoga", @@ -538,24 +543,6 @@ "raja_bhanga_yoga_299" ] }, - { - "chart": "Wolfgang Mozart", - "rule_id": "bvr_pittharoga_yoga", - "rule_name": "Pittharoga Yoga", - "pyjhora_names": [ - "pittharoga_yoga" - ] - }, - { - "chart": "Wolfgang Mozart", - "rule_id": "bvr_kapata_yoga", - "rule_name": "Kapata Yoga", - "pyjhora_names": [ - "kapata_yoga_202", - "kapata_yoga_203", - "kapata_yoga_204" - ] - }, { "chart": "Wolfgang Mozart", "rule_id": "bvr_theevrabuddhi_yoga", @@ -564,6 +551,14 @@ "theevrabuddhi_yoga" ] }, + { + "chart": "Wolfgang Mozart", + "rule_id": "bvr_pittharoga_yoga", + "rule_name": "Pittharoga Yoga", + "pyjhora_names": [ + "pittharoga_yoga" + ] + }, { "chart": "Pablo Picasso", "rule_id": "bvr_raja_bhanga_yoga", @@ -650,15 +645,6 @@ "bahu_sthree_yoga" ] }, - { - "chart": "Azim Premji", - "rule_id": "bvr_nishkapata_precise", - "rule_name": "Nishkapata Yoga", - "pyjhora_names": [ - "nishkapata_yoga_205", - "nishkapata_yoga_206" - ] - }, { "chart": "Azim Premji", "rule_id": "bvr_dharidhra_11_precise", @@ -673,6 +659,15 @@ "dharidhra_yoga_152" ] }, + { + "chart": "Azim Premji", + "rule_id": "bvr_ayatna_griha_prapta_yoga", + "rule_name": "Ayatna Griha Prapta Yoga", + "pyjhora_names": [ + "ayatna_griha_prapta_yoga_189", + "ayatna_griha_prapta_yoga_190" + ] + }, { "chart": "Azim Premji", "rule_id": "bvr_bahu_puthra_precise", @@ -684,11 +679,11 @@ }, { "chart": "Azim Premji", - "rule_id": "bvr_ayatna_griha_prapta_yoga", - "rule_name": "Ayatna Griha Prapta Yoga", + "rule_id": "bvr_nishkapata_precise", + "rule_name": "Nishkapata Yoga", "pyjhora_names": [ - "ayatna_griha_prapta_yoga_189", - "ayatna_griha_prapta_yoga_190" + "nishkapata_yoga_205", + "nishkapata_yoga_206" ] }, { @@ -699,6 +694,14 @@ "pushkala_yoga" ] }, + { + "chart": "Michael Jordan", + "rule_id": "bvr_bandhubhisthyaktha_precise", + "rule_name": "Bandhubhisthyaktha Yoga", + "pyjhora_names": [ + "bandhubhisthyaktha_yoga" + ] + }, { "chart": "Michael Jordan", "rule_id": "bvr_krisanga_yoga", @@ -708,14 +711,6 @@ "krisanga_yoga_113" ] }, - { - "chart": "Michael Jordan", - "rule_id": "bvr_bandhubhisthyaktha_precise", - "rule_name": "Bandhubhisthyaktha Yoga", - "pyjhora_names": [ - "bandhubhisthyaktha_yoga" - ] - }, { "chart": "Lionel Messi", "rule_id": "bvr_matrunasa_precise", @@ -725,6 +720,16 @@ "matrunasa_yoga_199" ] }, + { + "chart": "Lionel Messi", + "rule_id": "bvr_kapata_yoga", + "rule_name": "Kapata Yoga", + "pyjhora_names": [ + "kapata_yoga_202", + "kapata_yoga_203", + "kapata_yoga_204" + ] + }, { "chart": "Lionel Messi", "rule_id": "bvr_dharidhra_11_precise", @@ -748,16 +753,6 @@ "matrudeerghayur_yoga_197" ] }, - { - "chart": "Lionel Messi", - "rule_id": "bvr_kapata_yoga", - "rule_name": "Kapata Yoga", - "pyjhora_names": [ - "kapata_yoga_202", - "kapata_yoga_203", - "kapata_yoga_204" - ] - }, { "chart": "Cristiano Ronaldo", "rule_id": "bvr_matru_sneha_yoga", @@ -815,14 +810,6 @@ "matrudeerghayur_yoga_197" ] }, - { - "chart": "Dalai Lama", - "rule_id": "bvr_utthama_graha_yoga", - "rule_name": "Utthama Griha Yoga", - "pyjhora_names": [ - "utthama_graha_yoga" - ] - }, { "chart": "Dalai Lama", "rule_id": "brahma_yoga", @@ -831,6 +818,14 @@ "brahma_yoga" ] }, + { + "chart": "Dalai Lama", + "rule_id": "bvr_utthama_graha_yoga", + "rule_name": "Utthama Griha Yoga", + "pyjhora_names": [ + "utthama_graha_yoga" + ] + }, { "chart": "Pope Francis", "rule_id": "bvr_raja_bhanga_yoga", @@ -873,15 +868,6 @@ "pittharoga_yoga" ] }, - { - "chart": "Paramahansa Yogananda", - "rule_id": "bvr_dattha_puthra_yoga", - "rule_name": "Dattha Puthra Yoga", - "pyjhora_names": [ - "dattha_puthra_yoga_222", - "dattha_puthra_yoga_223" - ] - }, { "chart": "Paramahansa Yogananda", "rule_id": "bvr_matrunasa_precise", @@ -898,6 +884,15 @@ "pyjhora_names": [ "bandhubhisthyaktha_yoga" ] + }, + { + "chart": "Paramahansa Yogananda", + "rule_id": "bvr_dattha_puthra_yoga", + "rule_name": "Dattha Puthra Yoga", + "pyjhora_names": [ + "dattha_puthra_yoga_222", + "dattha_puthra_yoga_223" + ] } ] } \ No newline at end of file diff --git a/references/yoga_rules.json b/references/yoga_rules.json index 041c957d..d156c040 100644 --- a/references/yoga_rules.json +++ b/references/yoga_rules.json @@ -1,7 +1,7 @@ { "schema_version": "1.0", "description": "Yoga 规则库 - 数据驱动架构 v1.0", - "total_rules": 475, + "total_rules": 476, "rules": [ { "id": "raja_yoga", @@ -9295,7 +9295,7 @@ "category": "auspicious", "logic": { "type": "custom", - "expr": "\n# PyJHora/BVR 205-206 Nishkapata Yoga.\nnatural_benefics = {'Jupiter', 'Venus'}\nbenefic_signs = {'Taurus', 'Gemini', 'Cancer', 'Virgo', 'Libra', 'Sagittarius', 'Pisces'}\nplanets_in_4 = occupants(4)\nhas_benefic = any(p in natural_benefics for p in planets_in_4)\nhas_strong_planet = any(exalted(p) or own(p) or moola(p) for p in planets_in_4)\nfourth_benefic_sign = house_sign(4) in benefic_signs\ny205 = has_benefic or has_strong_planet or fourth_benefic_sign\nl1 = lord(1)\ny206 = False\nif l1 in ctx.planets and house_of(l1) == 4:\n l1_with_benefic = any(p != l1 and same_house(l1, p) for p in natural_benefics if p in ctx.planets)\n l1_aspected_benefic = any(aspect(p, l1) for p in natural_benefics if p in ctx.planets and p != l1)\n y206 = l1_with_benefic or l1_aspected_benefic\ny205 or y206 or vaiseshikamsa_score(lord(1)) >= 15\n", + "expr": "\n# PyJHora/BVR 205-206 Nishkapata Yoga.\nnatural_benefics = {'Jupiter', 'Venus'}\nbenefic_signs = {'Taurus', 'Gemini', 'Cancer', 'Virgo', 'Libra', 'Sagittarius', 'Pisces'}\nplanets_in_4 = occupants(4)\nhas_benefic = any(p in natural_benefics for p in planets_in_4)\nhas_strong_planet = any(exalted(p) or own(p) or moola(p) for p in planets_in_4)\nfourth_benefic_sign = house_sign(4) in benefic_signs\ny205 = has_benefic or has_strong_planet or fourth_benefic_sign\nl1 = lord(1)\ny206 = False\nif l1 in ctx.planets and house_of(l1) == 4:\n l1_with_benefic = any(p != l1 and same_house(l1, p) for p in natural_benefics if p in ctx.planets)\n l1_aspected_benefic = any(aspect(p, l1) for p in natural_benefics if p in ctx.planets and p != l1)\n y206 = l1_with_benefic or l1_aspected_benefic\ny205 or y206 or vaiseshikamsa_score(lord(1)) >= 16\n", "combo_template": "第4宫有天然吉星/强星/吉性星座,或上升主入4宫且受吉星影响" }, "effects": [ @@ -13634,5 +13634,5 @@ "source_reference": "BVR-170: Malefics in 2nd + 2nd lord weak" } ], - "total_enabled_rules": 381 -} \ No newline at end of file + "total_enabled_rules": 405 +} diff --git a/scripts/ashtottari_dasha.py b/scripts/ashtottari_dasha.py index 01d65f76..6f52ad92 100644 --- a/scripts/ashtottari_dasha.py +++ b/scripts/ashtottari_dasha.py @@ -174,17 +174,26 @@ def calculate_ashtottari_dasha(birth_info: dict) -> dict: major_periods = _build_major_periods(start_lord_idx, birth_dt) - # Determine current period + # Determine current period. Major periods repeat every 108 years; older natives + # should still return a current period instead of None after the first cycle. now = datetime.now() current_period = None + age_years = max((now - birth_dt).days / 365.25, 0) + current_in_cycle = age_years % TOTAL_CYCLE + cumulative = 0.0 for p in major_periods: - p_start = datetime.fromisoformat(p["start_date"]) - p_end = datetime.fromisoformat(p["end_date"]) - if p_start <= now < p_end: + years = p["years"] + if cumulative <= current_in_cycle < cumulative + years: + cycle_start = birth_dt + timedelta(days=(age_years - current_in_cycle + cumulative) * 365.25) + cycle_end = cycle_start + timedelta(days=years * 365.25) current_period = p.copy() - current_period["elapsed_years"] = (now - p_start).days / 365.25 - current_period["remaining_years"] = (p_end - now).days / 365.25 + current_period["start_date"] = cycle_start.isoformat() + current_period["end_date"] = cycle_end.isoformat() + current_period["elapsed_years"] = (now - cycle_start).days / 365.25 + current_period["remaining_years"] = (cycle_end - now).days / 365.25 + current_period["cycle_number"] = int(age_years // TOTAL_CYCLE) + 1 break + cumulative += years return { "applicable": True, diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py index e88777fc..3c34d686 100644 --- a/scripts/jyotish_engine.py +++ b/scripts/jyotish_engine.py @@ -2506,9 +2506,9 @@ def _calc_transit_multi_reference(planets, asc_idx, asc_deg, planet_lons, transi # ============================================================================ -# Dasa Convergence 三系统交叉验证(v4.5.0 P1补齐) +# Dasa Convergence 多系统交叉验证(v6.1.6) # 基于 dasa-convergence-methodology.md -# 三系统:Vimshottari + Chara Dasha + Yogini +# 系统:Vimshottari + Chara Dasha + Yogini + Ashtottari + Kalachakra # ============================================================================ # Yogini Dasha 常量 YOGINI_ORDER = ['Mangala', 'Pingala', 'Dhanya', 'Bhramari', 'Bhadrika', 'Ulka', 'Siddha', 'Sankata'] @@ -2589,10 +2589,11 @@ def _calc_yogini_dasha(moon_lon, birthdate_str): } -def _calc_dasa_convergence(dasha_result, chara_dasha_result, yogini_result, planet_lons, asc_idx): +def _calc_dasa_convergence(dasha_result, chara_dasha_result, yogini_result, planet_lons, asc_idx, ashtottari_result=None, kalachakra_result=None): """ - ⭐ v4.5.0: Dasa Convergence 三系统交叉验证 - Vimshottari + Chara Dasha + Yogini 三系统同时激活同一生活领域时,概率大幅提升 + ⭐ v6.1.6: Dasa Convergence 多系统交叉验证 + Vimshottari + Chara Dasha + Yogini + Ashtottari + Kalachakra 同时激活同一生活领域时,概率大幅提升。 + Chara Dasha 当前仍按 skill 规范降级为 partial,只作为低权重辅助。 """ # 提取各系统当前周期 convergence_data = {'systems': {}} @@ -2628,11 +2629,33 @@ def _calc_dasa_convergence(dasha_result, chara_dasha_result, yogini_result, plan # 系统3: Yogini if isinstance(yogini_result, dict): - cur_yog = yogini_result.get('current_yogini', {}) + cur_yog = yogini_result.get('current_yogini') or yogini_result.get('current') or {} convergence_data['systems']['yogini'] = { 'yogini': cur_yog.get('yogini') if isinstance(cur_yog, dict) else None, - 'years': cur_yog.get('full_years') if isinstance(cur_yog, dict) else None, - 'basis': 'Nakshatra (8-goddess cycle)', + 'planet': cur_yog.get('planet') if isinstance(cur_yog, dict) else None, + 'years': cur_yog.get('full_years', cur_yog.get('years')) if isinstance(cur_yog, dict) else None, + 'basis': 'Nakshatra/Lagna 36-year cycle', + } + + # 系统4: Ashtottari Dasha(条件性) + if isinstance(ashtottari_result, dict): + cur_ash = ashtottari_result.get('current') or {} + convergence_data['systems']['ashtottari'] = { + 'applicable': ashtottari_result.get('applicable', True), + 'planet': cur_ash.get('planet') if isinstance(cur_ash, dict) else None, + 'years': cur_ash.get('years') if isinstance(cur_ash, dict) else None, + 'basis': 'Conditional Nakshatra/Paksha 108-year cycle', + } + + # 系统5: Kalachakra Dasha + if isinstance(kalachakra_result, dict): + cur_kal = kalachakra_result.get('current') or {} + convergence_data['systems']['kalachakra'] = { + 'mode': kalachakra_result.get('mode'), + 'lord': cur_kal.get('lord') if isinstance(cur_kal, dict) else None, + 'rashi': cur_kal.get('rashi') if isinstance(cur_kal, dict) else None, + 'years': cur_kal.get('years') if isinstance(cur_kal, dict) else None, + 'basis': 'Moon Nakshatra Pada / Rashi-year cycle', } # 宫位主题映射 @@ -2691,6 +2714,73 @@ def _calc_dasa_convergence(dasha_result, chara_dasha_result, yogini_result, plan 'reason': f'Chara大运星座{cd_sign}是{house}宫', }) + # Yogini / Ashtottari: 当前行星是否掌管或落入该宫 + for system_key, system_label, planet_key in [ + ('yogini', 'Yogini', 'planet'), + ('ashtottari', 'Ashtottari', 'planet'), + ]: + sys_data = convergence_data['systems'].get(system_key, {}) + planet = sys_data.get(planet_key) + if not planet or not isinstance(planet, str): + continue + target_sign_idx = (asc_idx + house - 1) % 12 + target_sign = SIGNS[target_sign_idx] + target_lord = SIGN_LORDS.get(target_sign, '') + if planet == target_lord: + activations.append({ + 'system': system_label, + 'level': 'maha', + 'planet': planet, + 'reason': f'{system_label}当前主星{planet}是{house}宫({target_sign})的宫主星', + }) + if planet in planet_lons: + p_sign_idx = int(planet_lons.get(planet, 0) / 30) % 12 + p_house = ((p_sign_idx - asc_idx) % 12) + 1 + if p_house == house: + activations.append({ + 'system': system_label, + 'level': 'maha', + 'planet': planet, + 'reason': f'{system_label}当前主星{planet}落在{house}宫', + }) + + # Kalachakra: 当前 Rashi 是否关联该宫,当前 lord 是否掌管或落入该宫 + kal = convergence_data['systems'].get('kalachakra', {}) + if kal: + kal_rashi = kal.get('rashi') + if kal_rashi in SIGNS: + kal_sign_idx = SIGNS.index(kal_rashi) + kal_house_from_asc = ((kal_sign_idx - asc_idx) % 12) + 1 + if kal_house_from_asc == house: + activations.append({ + 'system': 'Kalachakra', + 'level': 'maha_rashi', + 'sign': kal_rashi, + 'reason': f'Kalachakra当前推运星座{kal_rashi}是{house}宫', + }) + kal_lord = kal.get('lord') + if kal_lord and isinstance(kal_lord, str): + target_sign_idx = (asc_idx + house - 1) % 12 + target_sign = SIGNS[target_sign_idx] + target_lord = SIGN_LORDS.get(target_sign, '') + if kal_lord == target_lord: + activations.append({ + 'system': 'Kalachakra', + 'level': 'maha_lord', + 'planet': kal_lord, + 'reason': f'Kalachakra当前主星{kal_lord}是{house}宫({target_sign})的宫主星', + }) + if kal_lord in planet_lons: + p_sign_idx = int(planet_lons.get(kal_lord, 0) / 30) % 12 + p_house = ((p_sign_idx - asc_idx) % 12) + 1 + if p_house == house: + activations.append({ + 'system': 'Kalachakra', + 'level': 'maha_lord', + 'planet': kal_lord, + 'reason': f'Kalachakra当前主星{kal_lord}落在{house}宫', + }) + if activations: domain_activations[domain] = { 'house': house, @@ -2701,7 +2791,11 @@ def _calc_dasa_convergence(dasha_result, chara_dasha_result, yogini_result, plan # 收敛等级评估 for domain, info in domain_activations.items(): sc = info['system_count'] - if sc >= 3: + if sc >= 4: + info['convergence_level'] = 'L5' + info['probability'] = '85-92%' + info['interpretation'] = '四个及以上推运系统同时激活,顶级收敛信号' + elif sc >= 3: info['convergence_level'] = 'L4' info['probability'] = '75-85%' info['interpretation'] = '三系统同时激活,极强信号' @@ -2721,7 +2815,7 @@ def _calc_dasa_convergence(dasha_result, chara_dasha_result, yogini_result, plan 'systems_summary': convergence_data['systems'], 'domain_activations': domain_activations, 'top_convergent_domains': [(d, info['convergence_level']) for d, info in top_domains], - 'protocol': 'v4.5.0 Dasa Convergence 三系统交叉验证。收敛等级: L1(单系统)→L3(双系统)→L4(三系统)。所有预测必须标注收敛等级。', + 'protocol': 'v6.1.6 Dasa Convergence 多系统交叉验证。收敛等级: L1(单系统)→L3(双系统)→L4(三系统)→L5(四个及以上系统)。Chara Dasha 当前为 partial,所有预测仍必须由 Vimshottari/Transit/Varga 等独立层确认。', } @@ -3194,36 +3288,61 @@ def cmd_full_reading(args): try: # Pancha Pakshi(五鸟系统,需要出生 Nakshatra) - # 先尝试从 nakshatra_advanced 获取出生 Nakshatra - nakshatra_num = None - if 'moon_nakshatra' in dir() or 'moon_nak' in locals(): - pass # 动态获取 - # 从 planets 数据推算 Nakshatra(Moon 的度数为基准) + # v6.1.6: 对齐 pancha_pakshi.py 现有公共接口 get_pancha_pakshi_schedule() moon_deg = planet_lons.get('Moon', 0) - nak_num = int(moon_deg / 13.3333333) + 1 + nak_num = int(moon_deg / (360.0 / 27)) + 1 if nak_num > 27: nak_num = 27 - from pancha_pakshi import calc_pakshi_full_analysis - pk_result = calc_pakshi_full_analysis(nak_num, target_weekday=0, target_period=0) + nak_name = NAKSHATRA_LIST[nak_num - 1][0] + tithi_number = int(((moon_deg - planet_lons.get('Sun', 0)) % 360) / 12) + 1 + paksha = 'shukla' if 1 <= tithi_number <= 15 else 'krishna' + from pancha_pakshi import get_pancha_pakshi_schedule + pk_result = get_pancha_pakshi_schedule( + birth_nakshatra=nak_name, + paksha=paksha, + date=f"{args.year}-{args.month:02d}-{args.day:02d}", + ) + pk_result['input_context'] = { + 'moon_nakshatra_index': nak_num - 1, + 'moon_nakshatra': nak_name, + 'tithi_number': tithi_number, + 'paksha': paksha, + } report['modules']['pancha_pakshi'] = pk_result except Exception as e: report['errors'].append(f"pancha-pakshi: {e}") try: # Rashi Tulya Navamsa(D1 与 D9 同宫对比分析) - from rashi_tulya_navamsa import analyze_rashi_tulya_navamsa, rashi_tulya_navamsa_summary + # v6.1.6: 对齐 rashi_tulya_navamsa.py 现有公共接口 analyze_rtn(chart_data) + from rashi_tulya_navamsa import analyze_rtn varga_full = report['modules'].get('varga_full', {}) d9_data = varga_full.get('D9_Navamsa', {}) - if d9_data and d9_data.get('planets') and d9_data.get('houses'): - rt_result = analyze_rashi_tulya_navamsa( - planets, houses, - d9_data['planets'], d9_data['houses'] - ) - rt_summary = rashi_tulya_navamsa_summary(rt_result) - report['modules']['rashi_tulya_navamsa'] = { - 'analysis': rt_result, - 'summary': rt_summary - } + if d9_data: + d9_planets = d9_data.get('planets') if isinstance(d9_data, dict) else None + if not d9_planets and isinstance(d9_data, dict): + d9_planets = { + pn: pd for pn, pd in d9_data.items() + if isinstance(pd, dict) and pn not in ('_meta', 'Ascendant') and 'sign' in pd + } + if d9_planets: + rt_chart = { + 'ascendant': chart.get('ascendant', {}), + 'planets': planets, + 'context': {'navamsa_planets': d9_planets}, + } + rt_result = analyze_rtn(rt_chart) + report['modules']['rashi_tulya_navamsa'] = { + 'analysis': rt_result, + 'summary': { + 'strength_score': rt_result.get('strength_score'), + 'weakness_score': rt_result.get('weakness_score'), + 'exalted_cancelled_count': len(rt_result.get('exalted_cancelled', [])), + 'debilitated_cancelled_count': len(rt_result.get('debilitated_cancelled', [])), + }, + } + else: + report['modules']['rashi_tulya_navamsa'] = {'note': 'D9 planet data incomplete, skip Rashi Tulya Navamsa'} else: report['modules']['rashi_tulya_navamsa'] = {'note': 'D9 data incomplete, skip Rashi Tulya Navamsa'} except Exception as e: @@ -3585,15 +3704,56 @@ def cmd_full_reading(args): except Exception as e: report['errors'].append(f"transit-multi-ref: {e}") - # ── Step 18: Dasa Convergence 三系统交叉验证 (v4.5.0 P1) ── + # ── Step 18: Dasa Convergence 多系统交叉验证 (v6.1.6) ── try: dasha_data = report['modules'].get('dasha', {}) jaimini_data = report['modules'].get('jaimini', {}) chara_dasha_data = jaimini_data.get('chara_dasha', {}) if isinstance(jaimini_data, dict) else {} - birthdate_str = f"{args.year}-{args.month:02d}-{args.day:02d}" - yogini_data = _calc_yogini_dasha(planet_lons.get('Moon', 0), birthdate_str) - report['modules']['yogini_dasha'] = yogini_data - convergence = _calc_dasa_convergence(dasha_data, chara_dasha_data, yogini_data, planet_lons, asc_idx) + moon_lon = planet_lons.get('Moon', 0) + moon_nakshatra_index = int(moon_lon / (360 / 27)) % 27 + moon_pada = int((moon_lon % (360 / 27)) / (360 / 108)) + 1 + tithi_number = int(((moon_lon - planet_lons.get('Sun', 0)) % 360) / 12) + 1 + birth_info_for_alt_dasha = { + 'birth_datetime': datetime(args.year, args.month, args.day, args.hour, args.minute), + 'moon_nakshatra_index': moon_nakshatra_index, + 'moon_pada': moon_pada, + 'is_shukla_paksha': 1 <= tithi_number <= 15, + 'lagna_rashi_index': asc_idx, + } + + try: + from yogini_dasha import calculate_yogini_dasha + yogini_data = calculate_yogini_dasha(birth_info_for_alt_dasha) + report['modules']['yogini_dasha'] = yogini_data + except Exception as alt_e: + yogini_data = {'error': str(alt_e)} + report['modules']['yogini_dasha'] = yogini_data + + try: + from ashtottari_dasha import calculate_ashtottari_dasha + ashtottari_data = calculate_ashtottari_dasha(birth_info_for_alt_dasha) + report['modules']['ashtottari_dasha'] = ashtottari_data + except Exception as alt_e: + ashtottari_data = {'error': str(alt_e)} + report['modules']['ashtottari_dasha'] = ashtottari_data + + try: + from kalachakra_dasha import calculate_kalachakra_dasha + kalachakra_data = calculate_kalachakra_dasha(birth_info_for_alt_dasha) + report['modules']['kalachakra_dasha'] = kalachakra_data + except Exception as alt_e: + kalachakra_data = {'error': str(alt_e)} + report['modules']['kalachakra_dasha'] = kalachakra_data + + convergence = _calc_dasa_convergence( + dasha_data, + chara_dasha_data, + yogini_data, + planet_lons, + asc_idx, + ashtottari_result=ashtottari_data, + kalachakra_result=kalachakra_data, + ) report['modules']['dasa_convergence'] = convergence except Exception as e: report['errors'].append(f"dasa-convergence: {e}") @@ -3651,7 +3811,7 @@ def cmd_full_reading(args): 'modules_computed': module_count, 'errors': error_count, 'status': 'complete' if error_count == 0 else f'{error_count} errors', - 'next_step': '⭐ v4.5.0: P1缺口已补齐。新增 transit_multi_reference(四参考点) + dasa_convergence(三系统交叉) + yogini_dasha + d9_navamsa_expanded(逐行星尊严展开)。AI必须使用四参考点分析Transit,Dasa预测必须标注收敛等级。', + 'next_step': '⭐ v6.1.6: full-reading 已输出 transit_multi_reference(四参考点) + dasa_convergence(五系统交叉) + yogini_dasha + ashtottari_dasha + kalachakra_dasha + d9_navamsa_expanded。AI必须使用四参考点分析Transit,Dasa预测必须标注多系统收敛等级。', } return report diff --git a/scripts/kalachakra_dasha.py b/scripts/kalachakra_dasha.py index 5c6462bb..ca3e56da 100644 --- a/scripts/kalachakra_dasha.py +++ b/scripts/kalachakra_dasha.py @@ -217,17 +217,26 @@ def calculate_kalachakra_dasha(birth_info: dict) -> dict: starting_lord = major_periods[0]["lord"] if major_periods else None starting_rashi = major_periods[0]["rashi"] if major_periods else None - # Determine current period + # Determine current period. Kalachakra cycles through the generated Rashi-year + # sequence repeatedly; older natives should still return a current period. now = datetime.now() current_period = None + age_years = max((now - birth_dt).days / 365.25, 0) + current_in_cycle = age_years % total_cycle if total_cycle else age_years + cumulative = 0.0 for p in major_periods: - p_start = datetime.fromisoformat(p["start_date"]) - p_end = datetime.fromisoformat(p["end_date"]) - if p_start <= now < p_end: + years = p["years"] + if cumulative <= current_in_cycle < cumulative + years: + cycle_start = birth_dt + timedelta(days=(age_years - current_in_cycle + cumulative) * 365.25) + cycle_end = cycle_start + timedelta(days=years * 365.25) current_period = p.copy() - current_period["elapsed_years"] = (now - p_start).days / 365.25 - current_period["remaining_years"] = (p_end - now).days / 365.25 + current_period["start_date"] = cycle_start.isoformat() + current_period["end_date"] = cycle_end.isoformat() + current_period["elapsed_years"] = (now - cycle_start).days / 365.25 + current_period["remaining_years"] = (cycle_end - now).days / 365.25 + current_period["cycle_number"] = int(age_years // total_cycle) + 1 if total_cycle else 1 break + cumulative += years return { "mode": mode, diff --git a/scripts/yogini_dasha.py b/scripts/yogini_dasha.py index 1429c781..a34f66cd 100644 --- a/scripts/yogini_dasha.py +++ b/scripts/yogini_dasha.py @@ -136,17 +136,26 @@ def calculate_yogini_dasha(birth_info: dict) -> dict: major_periods = _build_major_periods(start_idx, birth_dt) - # Determine current period + # Determine current period. Yogini repeats every 36 years; older natives + # should still return a current period instead of None after the first cycle. now = datetime.now() current_period = None + age_years = max((now - birth_dt).days / 365.25, 0) + current_in_cycle = age_years % TOTAL_CYCLE + cumulative = 0.0 for p in major_periods: - p_start = datetime.fromisoformat(p["start_date"]) - p_end = datetime.fromisoformat(p["end_date"]) - if p_start <= now < p_end: + years = p["years"] + if cumulative <= current_in_cycle < cumulative + years: + cycle_start = birth_dt + timedelta(days=(age_years - current_in_cycle + cumulative) * 365.25) + cycle_end = cycle_start + timedelta(days=years * 365.25) current_period = p.copy() - current_period["elapsed_years"] = (now - p_start).days / 365.25 - current_period["remaining_years"] = (p_end - now).days / 365.25 + current_period["start_date"] = cycle_start.isoformat() + current_period["end_date"] = cycle_end.isoformat() + current_period["elapsed_years"] = (now - cycle_start).days / 365.25 + current_period["remaining_years"] = (cycle_end - now).days / 365.25 + current_period["cycle_number"] = int(age_years // TOTAL_CYCLE) + 1 break + cumulative += years return { "major": major_periods,