235 lines
8.9 KiB
Python
235 lines
8.9 KiB
Python
"""
|
||
JHora 基准校准测试套件 v1.0
|
||
对比当前引擎的 Shadbala 输出与 JHora/PyJHora 的已知参考值
|
||
|
||
测试数据集来源:
|
||
- BV Raman (作者本人) — PyJHora 标准测试用例
|
||
- PVR Rao — PyJHora PVR 教材测试用例
|
||
"""
|
||
|
||
import json
|
||
import sys
|
||
import os
|
||
|
||
# 确保可以导入 scripts 模块
|
||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))
|
||
|
||
from shadbala import calc_shadbala
|
||
|
||
# ============================================================================
|
||
# 标准测试数据集
|
||
# ============================================================================
|
||
# 来源:PyJHora test suite + PVR 教材
|
||
TEST_CASES = [
|
||
{
|
||
"name": "BV Raman (作者本人)",
|
||
"data": {
|
||
"year": 1918, "month": 10, "day": 16,
|
||
"hour": 14, "minute": 22, "second": 16,
|
||
"lat": 12.97, "lon": 77.58, # 班加罗尔
|
||
"tz": 5.5,
|
||
},
|
||
# 预期值来源:PyJHora Drik Bala for BV Raman (Raman Ayanamsa)
|
||
"expected": {
|
||
"drik_bala_sunny": 15.86, # 示例值,需从 JHora 确认
|
||
}
|
||
},
|
||
{
|
||
"name": "PVR Rao (教材示例)",
|
||
"data": {
|
||
"year": 1981, "month": 9, "day": 13,
|
||
"hour": 1, "minute": 30, "second": 0,
|
||
"lat": 28.65, "lon": 77.22, # 德里
|
||
"tz": 5.5,
|
||
},
|
||
"expected": {}
|
||
},
|
||
]
|
||
|
||
# ============================================================================
|
||
# 参考值:BPHS Shadbala 最低要求(Rupas)
|
||
# ============================================================================
|
||
BPHS_MIN_REQUIRED = {
|
||
"Sun": 5.0, "Moon": 6.0, "Mars": 5.0,
|
||
"Mercury": 7.0, "Jupiter": 6.5, "Venus": 5.5, "Saturn": 5.0
|
||
}
|
||
|
||
# PyJHora 使用的 Naisargika Bala(Shashtiamshas)
|
||
NAISARGIKA_REF = {
|
||
"Sun": 60.0, "Moon": 51.43, "Venus": 42.86,
|
||
"Jupiter": 34.29, "Mercury": 25.71, "Mars": 17.14, "Saturn": 8.57
|
||
}
|
||
|
||
# ============================================================================
|
||
# 内部一致性测试:检查 Shadbala 子项和是否等于总分
|
||
# ============================================================================
|
||
def _check_internal_consistency(result):
|
||
"""检查每个行星的6个子项之和是否等于 total_virupas"""
|
||
errors = []
|
||
for pname, pd in result.get("planets", {}).items():
|
||
sthana_total = pd.get("sthana_bala", {}).get("total", 0)
|
||
dig = pd.get("dig_bala", 0)
|
||
kala_total = pd.get("kala_bala", {}).get("total", 0)
|
||
chesta = pd.get("chesta_bala", 0)
|
||
naisargika = pd.get("naisargika_bala", 0)
|
||
drik = pd.get("drik_bala", 0)
|
||
total = pd.get("total_virupas", 0)
|
||
|
||
computed = sthana_total + dig + kala_total + chesta + naisargika + drik
|
||
diff = abs(computed - total)
|
||
if diff > 0.1:
|
||
errors.append(f"{pname}: 子项和={computed:.2f} ≠ total_virupas={total:.2f} (差{diff:.2f})")
|
||
return errors
|
||
|
||
|
||
# ============================================================================
|
||
# 外部一致性测试:检查是否满足 BPHS 最低要求
|
||
# ============================================================================
|
||
def _check_min_required(result):
|
||
"""检查每个行星是否满足 BPHS 最低 Shadbala Rupas 要求"""
|
||
errors = []
|
||
for pname, pd in result.get("planets", {}).items():
|
||
total_rupas = pd.get("total_rupas", 0)
|
||
min_req = BPHS_MIN_REQUIRED.get(pname, 5.0)
|
||
if total_rupas < min_req:
|
||
errors.append(f"{pname}: {total_rupas:.2f} Rupas < 最低要求 {min_req:.2f}")
|
||
return errors
|
||
|
||
|
||
# ============================================================================
|
||
# Naisargika Bala 对比
|
||
# ============================================================================
|
||
def _check_naisargika(result):
|
||
"""检查 Naisargika Bala 是否匹配参考值"""
|
||
errors = []
|
||
for pname, pd in result.get("planets", {}).items():
|
||
actual = pd.get("naisargika_bala", 0)
|
||
expected = NAISARGIKA_REF.get(pname, 0)
|
||
if abs(actual - expected) > 0.1:
|
||
errors.append(f"{pname}: Naisargika={actual:.2f} ≠ 参考值{expected:.2f}")
|
||
return errors
|
||
|
||
|
||
# ============================================================================
|
||
# 排序合理性测试
|
||
# ============================================================================
|
||
def _check_ranking_sanity(result):
|
||
"""检查排名是否合理(擢升行星应比落陷行星强)"""
|
||
planets = result.get("planets", {})
|
||
ranking = result.get("ranking", [])
|
||
errors = []
|
||
|
||
# 如果有擢升行星,应排在非擢升行星之前
|
||
if len(ranking) >= 2:
|
||
pass # 无法自动判断合理排序,只做记录
|
||
|
||
return errors
|
||
|
||
|
||
# ============================================================================
|
||
# 运行全部测试
|
||
# ============================================================================
|
||
def run_all_tests():
|
||
results_summary = []
|
||
total_tests = 0
|
||
total_errors = 0
|
||
|
||
for tc in TEST_CASES:
|
||
d = tc["data"]
|
||
print(f"\n{'='*60}")
|
||
print(f"测试: {tc['name']}")
|
||
print(f" 出生: {d['year']}-{d['month']:02d}-{d['day']:02d} {d['hour']:02d}:{d['minute']:02d}")
|
||
print(f" 地点: {d['lat']}°, {d['lon']}°, tz={d['tz']}")
|
||
print(f"{'='*60}")
|
||
|
||
# 简化:不实际调用占星引擎(引擎需要完整行星数据)
|
||
# 实际运行需要先计算 chart 获取完整行星数据
|
||
# 这里先占位,后续接入 jyotish_engine.py 的 calculate_full_chart
|
||
|
||
case_result = {
|
||
"name": tc["name"],
|
||
"tests": {},
|
||
}
|
||
|
||
# 测试1: 内部一致性
|
||
# TODO: 接入完整计算链后启用
|
||
# errors = test_internal_consistency(result)
|
||
|
||
# 测试2: Naisargika Bala 对比
|
||
# TODO: 接入
|
||
|
||
results_summary.append(case_result)
|
||
|
||
return results_summary
|
||
|
||
|
||
# ============================================================================
|
||
# 手动单用例测试函数(用于快速验证特定命盘)
|
||
# ============================================================================
|
||
def verify_shadbala_for_chart(planets, asc_sign, birth_hour, sun_lon, moon_lon,
|
||
birth_minute=0, chart_name="测试盘"):
|
||
"""对给定命盘运行完整的 Shadbala 验证"""
|
||
result = calc_shadbala(planets, asc_sign, birth_hour, sun_lon, moon_lon,
|
||
birth_minute)
|
||
|
||
print(f"\n{'='*60}")
|
||
print(f"Shadbala 验证: {chart_name}")
|
||
print(f"{'='*60}")
|
||
print(f"方法: {result['method']}")
|
||
print(f"排名: {' > '.join(result['ranking'])}")
|
||
print(f"最强: {result['strongest']}, 最弱: {result['weakest']}")
|
||
print(f"\n{'行星':>8} | {'Rupas':>6} | {'最低要求':>8} | {'达标?':>5} | {'Chesta':>6} | {'Ishta':>6} | {'Kashta':>6}")
|
||
print("-" * 65)
|
||
|
||
errors = []
|
||
for pname in ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn']:
|
||
pd = result.get("planets", {}).get(pname, {})
|
||
rupas = pd.get("total_rupas", 0)
|
||
min_req = BPHS_MIN_REQUIRED.get(pname, 5.0)
|
||
passed = "✅" if rupas >= min_req else "❌"
|
||
chesta = pd.get("chesta_bala", 0)
|
||
ishta = pd.get("ishta_phala", 0)
|
||
kashta = pd.get("kashta_phala", 0)
|
||
print(f"{pname:>8} | {rupas:>6.2f} | {min_req:>8.2f} | {passed:>5} | {chesta:>6.1f} | {ishta:>6.1f} | {kashta:>6.1f}")
|
||
if rupas < min_req:
|
||
errors.append(f"{pname}: {rupas:.2f} < {min_req:.2f}")
|
||
|
||
# 内部一致性测试
|
||
consistency = _check_internal_consistency(result)
|
||
for e in consistency:
|
||
errors.append(e)
|
||
|
||
# Naisargika 测试
|
||
naisargika = _check_naisargika(result)
|
||
for e in naisargika:
|
||
errors.append(e)
|
||
|
||
print("-" * 65)
|
||
if errors:
|
||
print(f"\n⚠️ 发现 {len(errors)} 个问题:")
|
||
for e in errors:
|
||
print(f" • {e}")
|
||
else:
|
||
print(f"\n✅ 全部检查通过")
|
||
|
||
return result, errors
|
||
|
||
|
||
if __name__ == "__main__":
|
||
# 使用PRIVATE_REDACTED_CASE的出生数据运行验证
|
||
planets = {
|
||
'Sun': {'sign': 'Aries', 'degree': 27.5, 'house': 9, 'retrograde': False, 'speed': 0.98},
|
||
'Moon': {'sign': 'Aquarius', 'degree': 10.0, 'house': 7, 'retrograde': False, 'speed': 13.5},
|
||
'Mars': {'sign': 'Cancer', 'degree': 5.0, 'house': 12, 'retrograde': False, 'speed': 0.65},
|
||
'Mercury': {'sign': 'Pisces', 'degree': 22.0, 'house': 8, 'retrograde': False, 'speed': 1.2},
|
||
'Jupiter': {'sign': 'Virgo', 'degree': 15.0, 'house': 2, 'retrograde': True, 'speed': -0.08},
|
||
'Venus': {'sign': 'Pisces', 'degree': 5.0, 'house': 8, 'retrograde': False, 'speed': 1.1},
|
||
'Saturn': {'sign': 'Aquarius', 'degree': 28.0, 'house': 7, 'retrograde': False, 'speed': 0.05},
|
||
}
|
||
|
||
verify_shadbala_for_chart(
|
||
planets, 'Leo', 14.75, 27.5, 310.0,
|
||
birth_minute=45,
|
||
chart_name="PRIVATE_REDACTED_CASE (public sample birth datetime public sample)"
|
||
)
|