Files
Jyotisha/scripts/validate.py
732642856 2c3392703d feat: v3.6.0 全量同步 — 完整印度占星引擎+74篇参考资料+14子命令
## 核心升级(v1.6.0 → v3.6.0)

### 计算引擎(scripts/)
- jyotish_engine.py v3.6.0:14子命令统一引擎(chart/dasha/yoga/predict/varga/celebrity/db-stats/transit/shadbala/ashtakavarga/memory/validate/audit/report)
- 基于 Swiss Ephemeris + Lahiri Ayanamsa 恒星黄道
- shadbala.py:Shadbala六重力量计算(Sthana/Dig/Kala/Chesta/Naisargika/Drik Bala)
- ashtakavarga.py v2.0:BPHS完整8×8矩阵,SAV=337
- validate.py:R1-R10+R2b 数学验证(11项)
- event_prediction_model.py:三层验证事件预测规则引擎
- hermes_memory_core.py + hermes_bridge.py:Hermes记忆系统
- report_builder.py:MD→HTML报告生成器(羊皮纸主题)

### 参考资料(references/)— 74篇
- 新增35篇:AI解盘工作流/Argala/Dasa Convergence/替代推运系统/条件Dasha/Shodasavarga十六分盘/古典文献翻译/专业发展路径等
- 修改8篇:PDF读取v3.0/Ashtakavarga v2.0/名人案例库v2.0/KP占星v2.0等
- 覆盖:行星/星座/宫位/Nakshatra/Yoga/Dasha/Transit/关系占星/年运盘/Jaimini/KP/Varshaphala/补救措施

### 审计管线(P1-P12 + P3/P8/冲突仲裁)
- P3仓库耦合:双宫掌管命运捆绑分析
- P8年龄状态:行星度数→执行模式判定
- 冲突仲裁3条规则:矛盾信号裁决

### 其他
- CHANGELOG.md:完整版本历史
- .gitignore:排除缓存和数据库
- SKILL.md v3.6.0:触发词/子命令表/工作流完整更新
2026-04-24 22:36:57 +08:00

293 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
R1-R10 数学验证模块
对星盘计算结果执行 10 项硬性校验规则(来自 CNWU16 审计框架)
规则清单:
R1: SAV 总和 = 3377行星BAV之和)
R2: 各 BAV 行常数(Sun=48, Moon=49, Mars=39, Mercury=54, Jupiter=56, Venus=52, Saturn=39, Lagna=49
R2b: BAV列→SAV列校验(每个星座的7行星BAV之和 = 该星座SAV分数)
R3: 水星最大延伸角 ≤ 28°(不远离太阳)
R4: 金星最大延伸角 ≤ 47°(不远离太阳)
R5: Rahu-Ketu 严格 180° 对冲
R6: 逆行合法性(只有火星/木星/土星/金星/水星可能逆行,Rahu/Ketu永远逆行,太阳/月亮不逆行)
R7: Dasha 大运年限链总和 = 120 年
R8: 行星完整性(7行星 + Rahu + Ketu + Lagna 全部存在)
R9: 星座度数范围 [0, 30)
R10: 宫位连续性(1-12宫无断点)
"""
from typing import Dict, List, Tuple
import math
SIGNS = ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo',
'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces']
DASHA_YEARS = {
"Ketu": 7, "Venus": 20, "Sun": 6, "Moon": 10,
"Mars": 7, "Rahu": 18, "Jupiter": 16, "Saturn": 19, "Mercury": 17
}
EXPECTED_PLANETS = ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu']
BAV_TOTALS = {
'Sun': 48, 'Moon': 49, 'Mars': 39, 'Mercury': 54,
'Jupiter': 56, 'Venus': 52, 'Saturn': 39, 'Lagna': 49
}
SEVEN_PLANETS = ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn']
def validate_chart(chart_data: Dict, ashtakavarga_data: Dict = None) -> Dict:
"""
执行 R1-R10 全部校验
Args:
chart_data: jyotish_engine chart 命令的完整输出
ashtakavarga_data: ashtakavarga 命令的输出(可选,用于 R1/R2)
Returns:
{
"valid": bool, # 全部通过
"total_checks": int,
"passed": int,
"failed": int,
"results": [{"rule": "R1", "name": "...", "passed": bool, "detail": "..."}, ...]
}
"""
results = []
planets = chart_data.get('planets', {})
asc = chart_data.get('ascendant', {})
# R1: SAV 总和 = 337
if ashtakavarga_data:
sav_total = ashtakavarga_data.get('sav', {}).get('total', 0)
sav_valid = ashtakavarga_data.get('sav', {}).get('valid', False)
results.append({
'rule': 'R1', 'name': 'SAV总和=337',
'passed': sav_valid,
'detail': f"SAV total = {sav_total}, expected 337"
})
else:
results.append({
'rule': 'R1', 'name': 'SAV总和=337',
'passed': None,
'detail': '未提供 ashtakavarga 数据,跳过'
})
# R2: 各 BAV 行常数
if ashtakavarga_data:
bav_results = ashtakavarga_data.get('bav', {})
all_bav_valid = ashtakavarga_data.get('all_bav_valid', False)
details = []
for pname in SEVEN_PLANETS + ['Lagna']:
if pname in bav_results:
actual = bav_results[pname].get('total', 0)
expected = BAV_TOTALS.get(pname, 0)
details.append(f"{pname}={actual}/{expected}")
results.append({
'rule': 'R2', 'name': 'BAV行常数校验',
'passed': all_bav_valid,
'detail': '; '.join(details) if details else '无BAV数据'
})
else:
results.append({
'rule': 'R2', 'name': 'BAV行常数校验',
'passed': None,
'detail': '未提供 ashtakavarga 数据,跳过'
})
# R2b: BAV列→SAV列校验(每个星座的7行星BAV之和 = 该星座SAV分数)
if ashtakavarga_data:
r2b_passed, r2b_detail = _check_bav_column_sav(ashtakavarga_data)
results.append({'rule': 'R2b', 'name': 'BAV列→SAV列校验', 'passed': r2b_passed, 'detail': r2b_detail})
else:
results.append({'rule': 'R2b', 'name': 'BAV列→SAV列校验', 'passed': None, 'detail': '未提供 ashtakavarga 数据,跳过'})
# R3: 水星最大延伸角 ≤ 28°
r3_passed, r3_detail = _check_elongation(planets, 'Mercury', 'Sun', 28)
results.append({'rule': 'R3', 'name': '水星延伸角≤28°', 'passed': r3_passed, 'detail': r3_detail})
# R4: 金星最大延伸角 ≤ 47°
r4_passed, r4_detail = _check_elongation(planets, 'Venus', 'Sun', 47)
results.append({'rule': 'R4', 'name': '金星延伸角≤47°', 'passed': r4_passed, 'detail': r4_detail})
# R5: Rahu-Ketu 严格 180° 对冲
r5_passed, r5_detail = _check_rahu_ketu_opposition(planets)
results.append({'rule': 'R5', 'name': 'Rahu-Ketu 180°对冲', 'passed': r5_passed, 'detail': r5_detail})
# R6: 逆行合法性
r6_passed, r6_detail = _check_retrograde_legitimacy(planets)
results.append({'rule': 'R6', 'name': '逆行合法性', 'passed': r6_passed, 'detail': r6_detail})
# R7: Dasha 大运年限链总和 = 120 年
dasha_sum = sum(DASHA_YEARS.values())
results.append({
'rule': 'R7', 'name': 'Dasha年限总和=120',
'passed': dasha_sum == 120,
'detail': f"Dasha year chain sum = {dasha_sum}"
})
# R8: 行星完整性
r8_passed, r8_detail = _check_planet_completeness(planets, asc)
results.append({'rule': 'R8', 'name': '行星完整性', 'passed': r8_passed, 'detail': r8_detail})
# R9: 星座度数范围 [0, 30)
r9_passed, r9_detail = _check_degree_range(planets)
results.append({'rule': 'R9', 'name': '星座度数范围', 'passed': r9_passed, 'detail': r9_detail})
# R10: 宫位连续性(1-12宫无断点)
r10_passed, r10_detail = _check_house_continuity(chart_data)
results.append({'rule': 'R10', 'name': '宫位连续性', 'passed': r10_passed, 'detail': r10_detail})
# 汇总
checked = [r for r in results if r['passed'] is not None]
passed_count = sum(1 for r in checked if r['passed'])
failed_count = sum(1 for r in checked if not r['passed'])
return {
'valid': failed_count == 0,
'total_checks': len(results),
'checked': len(checked),
'passed': passed_count,
'failed': failed_count,
'skipped': len(results) - len(checked),
'results': results,
}
def _check_elongation(planets: Dict, planet: str, reference: str, max_deg: float) -> Tuple[bool, str]:
"""检查行星与参考行星的延伸角"""
p1 = planets.get(planet, {})
p2 = planets.get(reference, {})
deg1 = p1.get('degree', None)
deg2 = p2.get('degree', None)
if deg1 is None or deg2 is None:
return None, f'{planet}{reference}度数缺失'
elongation = abs(deg1 - deg2)
if elongation > 180:
elongation = 360 - elongation
ok = elongation <= max_deg
return ok, f"{planet}延伸角 = {elongation:.2f}°, 阈值 {max_deg}°"
def _check_rahu_ketu_opposition(planets: Dict) -> Tuple[bool, str]:
"""检查 Rahu-Ketu 严格 180° 对冲"""
rahu = planets.get('Rahu', {})
ketu = planets.get('Ketu', {})
rahu_deg = rahu.get('degree', None)
ketu_deg = ketu.get('degree', None)
if rahu_deg is None or ketu_deg is None:
return None, 'Rahu或Ketu度数缺失'
diff = abs(rahu_deg - ketu_deg)
deviation = abs(diff - 180)
ok = deviation < 0.01 # 允许极小数值误差
return ok, f"Rahu={rahu_deg:.4f}°, Ketu={ketu_deg:.4f}°, 偏差={deviation:.4f}°"
def _check_retrograde_legitimacy(planets: Dict) -> Tuple[bool, str]:
"""检查逆行合法性"""
# 太阳和月亮不应逆行
never_retro = ['Sun', 'Moon']
# Rahu/Ketu 永远逆行
always_retro = ['Rahu', 'Ketu']
# 火星/木星/土星/金星/水星 可能逆行也可能不逆行
can_retro = ['Mars', 'Jupiter', 'Saturn', 'Venus', 'Mercury']
issues = []
for pname in never_retro:
pd = planets.get(pname, {})
if pd.get('retrograde', False):
issues.append(f"{pname}不应逆行")
for pname in always_retro:
pd = planets.get(pname, {})
if not pd.get('retrograde', True):
issues.append(f"{pname}应永远逆行")
ok = len(issues) == 0
detail = '; '.join(issues) if issues else '所有行星逆行状态合法'
return ok, detail
def _check_planet_completeness(planets: Dict, asc: Dict) -> Tuple[bool, str]:
"""检查行星完整性"""
missing = []
for pname in EXPECTED_PLANETS:
if pname not in planets:
missing.append(pname)
if not asc:
missing.append('Ascendant(Lagna)')
ok = len(missing) == 0
detail = f"缺失: {', '.join(missing)}" if missing else f"完整:{len(EXPECTED_PLANETS)}行星 + Ascendant"
return ok, detail
def _check_degree_range(planets: Dict) -> Tuple[bool, str]:
"""检查星座度数范围"""
issues = []
for pname, pd in planets.items():
deg_in_sign = pd.get('degree_in_sign', None)
if deg_in_sign is not None:
if deg_in_sign < 0 or deg_in_sign >= 30:
issues.append(f"{pname}度数={deg_in_sign}° 超出[0,30)范围")
ok = len(issues) == 0
detail = '; '.join(issues) if issues else '所有行星度数在合法范围'
return ok, detail
def _check_house_continuity(chart_data: Dict) -> Tuple[bool, str]:
"""检查宫位连续性"""
houses = chart_data.get('houses', {})
house_nums = set()
for key in houses:
if key.startswith('house_'):
num = int(key.replace('house_', ''))
house_nums.add(num)
expected = set(range(1, 13))
missing = expected - house_nums
ok = len(missing) == 0
detail = f"缺失宫位: {sorted(missing)}" if missing else f"1-12宫完整({len(house_nums)}宫)"
return ok, detail
def _check_bav_column_sav(ashtakavarga_data: Dict) -> Tuple[bool, str]:
"""
R2b: BAV列→SAV列校验
每个星座的7行星BAV bindus之和必须等于该星座的SAV分数。
这验证了行总和(R2)和列总和的交叉一致性。
"""
bav_results = ashtakavarga_data.get('bav', {})
house_scores = ashtakavarga_data.get('house_scores', {})
sav_scores = ashtakavarga_data.get('sav', {})
issues = []
# 从每个7行星的 BAV bindus 列向量求和,对比 SAV house_scores
for sign_i in range(12):
# 计算该星座位置(第 sign_i 列)的 7行星 BAV 之和
col_sum = 0
for pname in SEVEN_PLANETS:
if pname in bav_results:
bindus = bav_results[pname].get('bindus', [])
if len(bindus) > sign_i:
col_sum += bindus[sign_i]
# 获取该星座/宫位的 SAV 分数
house_key = f'house_{sign_i + 1}'
hs = house_scores.get(house_key, {})
sav_score = hs.get('score', None)
if sav_score is not None and col_sum != sav_score:
issues.append(f"星座{SIGNS[sign_i]}列: BAV列和={col_sum}, SAV={sav_score}")
# 同时验证 SAV 逐宫总分是否与 house_scores 一致
if not issues:
ok = True
detail = "所有12星座 BAV列和 与 SAV分数一致 ✅"
else:
ok = False
detail = f"不一致: {'; '.join(issues)}"
return ok, detail