Files
Jyotisha/scripts/validate_yoga_accuracy.py

706 lines
26 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 -*-
"""
Yoga 准确率验证框架 v1.0
功能:
1. 用同一组出生数据,分别调用 PyJhora 和 skill 的 Yoga 检测
2. 对比结果,生成准确率报告
3. 支持批量测试用例(JSON 文件)
用法:
python3 validate_yoga_accuracy.py --benchmark
python3 validate_yoga_accuracy.py --test-cases test_cases.json
python3 validate_yoga_accuracy.py --interactive
"""
import json
import re
import sys
import glob
import argparse
import traceback
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Set
from ayanamsa_utils import sidereal_flags
# ============================================================
# 路径设置
# ============================================================
SCRIPT_DIR = Path(__file__).resolve().parent
SKILL_ROOT = SCRIPT_DIR.parent
# 确保 skill 脚本在 sys.path 中
if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR))
# ============================================================
# 常量(自包含,与 yoga_engine.py 一致)
# ============================================================
SIGNS = ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo',
'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces']
SIGN_LORDS = {
'Aries': 'Mars', 'Taurus': 'Venus', 'Gemini': 'Mercury', 'Cancer': 'Moon',
'Leo': 'Sun', 'Virgo': 'Mercury', 'Libra': 'Venus', 'Scorpio': 'Mars',
'Sagittarius': 'Jupiter', 'Capricorn': 'Saturn', 'Aquarius': 'Saturn', 'Pisces': 'Jupiter'
}
# ============================================================
# Skill 功能性 Yoga CategoryPyJhora 中无对应规则,属于 skill 特色)
# ============================================================
FUNCTIONAL_CATEGORIES = {
'dhana', 'kalatra', 'putra', 'ayur', 'vidya', 'shakti',
'karma', 'asha', 'moksha', 'aryama', 'guru', 'ari', 'varga',
# raja 虽然是经典概念,但 PyJhora 没有单独函数,skill 按功能性实现
'raja',
}
# 加载 skill 的 yoga_rules.json,建立 name -> category 映射
def _load_skill_rule_categories() -> Dict[str, str]:
"""读取 yoga_rules.json,返回 {归一化名称: category}"""
rules_path = SKILL_ROOT / "references" / "yoga_rules.json"
if not rules_path.exists():
return {}
try:
with open(rules_path, 'r', encoding='utf-8') as f:
data = json.load(f)
mapping = {}
for r in data.get('rules', []):
if not r.get('enabled', True):
continue
key = normalize_name(r.get('name', ''))
if key:
mapping[key] = r.get('category', 'unknown')
return mapping
except Exception:
return {}
# 延迟加载,避免在模块导入时读取文件
_SKILL_RULE_CATEGORIES: Optional[Dict[str, str]] = None
def get_skill_rule_categories() -> Dict[str, str]:
global _SKILL_RULE_CATEGORIES
if _SKILL_RULE_CATEGORIES is None:
_SKILL_RULE_CATEGORIES = _load_skill_rule_categories()
return _SKILL_RULE_CATEGORIES
# ============================================================
# Swiss Ephemeris 星盘计算(独立实现,不依赖 jyotish_engine
# ============================================================
def skill_compute_chart(year, month, day, hour, minute, lat, lon, tz,
node_mode='mean'):
"""
用 Swiss Ephemeris 计算星盘。
返回:(planets_dict, asc_idx, jd, ayanamsa)
"""
import swisseph as swe
# Julian Day(本地时间 → UTC
hour_decimal = hour + minute / 60.0 - tz
jd = swe.julday(year, month, day, hour_decimal)
# Ayanamsa
flags = sidereal_flags(swe, 'lahiri')
ayanamsa = swe.get_ayanamsa_ut(jd)
# Ascendant
asc_info = swe.houses(jd, lat, lon, b'A') # 'A' = equal houses (Vedic)
asc_long = asc_info[0][0]
asc_deg = (asc_long - ayanamsa) % 360
asc_sign = int(asc_deg / 30)
asc_idx = asc_sign + 1 # 1-based
# 行星
planets = {}
planet_map = [
('Sun', swe.SUN), ('Moon', swe.MOON), ('Mars', swe.MARS),
('Mercury', swe.MERCURY), ('Jupiter', swe.JUPITER),
('Venus', swe.VENUS), ('Saturn', swe.SATURN),
]
for pname, pid in planet_map:
res = swe.calc_ut(jd, pid, flags)
long = res[0][0] % 360
sign = int(long / 30)
deg = long % 30.0
speed = res[0][3]
is_retro = speed < 0
house = ((sign - asc_sign) % 12) + 1
planets[pname] = {
'longitude': long, 'sign': SIGNS[sign],
'degree': deg, 'house': house,
'is_retro': is_retro, 'is_combust': False,
}
# Rahu / Ketu
if node_mode == 'true':
rahu_res = swe.calc_ut(jd, swe.TRUE_NODE, flags)
else:
rahu_res = swe.calc_ut(jd, swe.MEAN_NODE, flags)
rahu_long = rahu_res[0][0] % 360
ketu_long = (rahu_long + 180.0) % 360
for name, long in [('Rahu', rahu_long), ('Ketu', ketu_long)]:
sign = int(long / 30)
deg = long % 30.0
house = ((sign - asc_sign) % 12) + 1
planets[name] = {
'longitude': long, 'sign': SIGNS[sign],
'degree': deg, 'house': house,
'is_retro': False, 'is_combust': False,
}
# 燃烧判断(简化)
sun_long = planets['Sun']['longitude']
combust_limits = {
'Moon': 12.0, 'Mars': 17.0, 'Mercury': 14.0,
'Jupiter': 11.0, 'Venus': 10.0, 'Saturn': 15.0,
}
for pname, limit in combust_limits.items():
p_long = planets[pname]['longitude']
diff = abs((p_long - sun_long + 180) % 360 - 180)
planets[pname]['is_combust'] = diff < limit
return planets, asc_idx, jd, ayanamsa
def skill_detect_yogas(planets, asc):
"""调用 skill 的 Yoga 引擎检测 Yoga"""
from yoga_engine import detect_yogas
return detect_yogas(planets, asc)
# ============================================================
# PyJhora 接口封装
# ============================================================
def init_external_benchmark():
"""初始化 PyJhora,返回是否成功"""
try:
import jhora.horoscope.chart.yoga as py_yoga
import jhora.panchanga.drik as drik
import jhora.horoscope.chart.charts as charts
import jhora.const as const
return True
except ImportError as e:
print(f"⚠️ PyJhora 导入失败: {e}")
return False
def external_benchmark_jd(year, month, day, hour_frac):
"""计算 Julian Day(与 PyJhora 一致)"""
import swisseph as swe
return swe.julday(year, month, day, hour_frac)
def external_benchmark_get_yogas(jd, lat, lon, tz, divisional_chart_factor=1):
"""
调用 PyJhora 获取 D1 宫盘的 Yoga 检测结果。
返回:{yoga_function_name: {"name": ..., "desc": ..., "benefits": ...}}
"""
import jhora.horoscope.chart.yoga as py_yoga
import jhora.panchanga.drik as drik
place = drik.Place("TestPlace", lat, lon, tz)
yoga_results, _, _ = py_yoga.get_yoga_details(
jd, place, divisional_chart_factor=divisional_chart_factor, language='en'
)
# yoga_results: {yoga_function_name: [chart_id, yoga_name, yoga_description, yoga_benefits]}
result = {}
for fname, details in yoga_results.items():
result[fname] = {
"name": details[1] if len(details) > 1 else fname,
"desc": details[2] if len(details) > 2 else "",
"benefits": details[3] if len(details) > 3 else "",
"chart_id": details[0] if len(details) > 0 else "",
}
return result
# ============================================================
# 名称归一化(用于对比)
# ============================================================
def normalize_name(name: str) -> str:
"""归一化 Yoga 名称(与 benchmark_yoga_coverage.py 一致)"""
s = name.lower()
s = re.sub(r"\([^)]*\)", " ", s)
s = s.replace("&", " and ")
s = re.sub(r"[^a-z0-9]+", "_", s)
s = re.sub(r"_+", "_", s).strip("_")
for suffix in ("_yoga", "_graha", "_classic", "_calculation", "_calc"):
if s.endswith(suffix):
s = s[: -len(suffix)]
s = re.sub(r"_\d+$", "", s)
return s
# 单向名称映射:PyJhora 归一化名称 -> skill 标准归一化名称
# 键是 PyJhora 的归一化名称,值是 skill 的标准归一化名称
# 两边都会通过 _canonical_name() 映射到统一的标准名称空间
CROSS_NAME_MAP = {
# --- 经典 Yoga 别名(PyJhora -> skill ---
"nipuna": "budhaditya",
"gaja_kesari": "gajakesari",
"maalavya": "malavya",
"shasha": "sasa",
"murdha": "moordha",
"kedara": "kedaara",
"kaahala": "kahala",
"daama": "dama",
"veenaa": "veena",
"paasa": "pasa",
"mridang": "mridanga",
"sankh": "sankha",
"chatusagar": "chatussagara",
"rajalakshan": "rajalakshana",
"bhaarathi": "bharati",
"saraswathi": "saraswati",
"kalpadrum": "kalpadruma",
"trikala_gnana": "thrikaala_gnana",
"yukti_samanwithavagmi": "yukthi_samanwithavagmi",
"mathibhraman": "mathibhramana",
"swaviryaddhana": "swaveeryaddhana",
"antya_vayasi_dhana": "anthya_vayasi_dhana",
"madhya_vayasi_dhana": "madhya_vayasi_dhana",
"matrumuladdhana": "matrumooladdhana",
"kalanirdesat_puthranaasa": "kaalanirdesat_puthranaasa",
"dvadasa_sahodara": "dwadasa_sahodara",
"vahan": "vahana",
"parakram": "parakrama",
"yuddha_pravin": "yuddha_praveena",
"sumukh": "sumukha",
"durmukh": "durmukha",
"kapat": "kapata",
"nishkapat": "nishkapata",
"asatyavad": "asatyavadi",
"bhratruvriddh": "bhratruvriddhi",
"bandhubhisthyakt": "bandhubhisthyaktha",
"bandhu_pujy": "bandhu_pujya",
"matrunas": "matrunasa",
"matru_sneh": "matru_sneha",
"bahu_puthr": "bahu_puthra",
"bahu_stri": "bahu_sthree",
"eka_puthr": "eka_puthra",
"satkalatr": "satkalatra",
"sareera_soukh": "sareera_soukhya",
"rogagrasth": "rogagrastha",
"dehasthoul": "dehasthoulya",
"dehapusht": "dehapushti",
"krisang": "krisanga",
"sada_sanchar": "sada_sanchara",
"annadan": "annadana",
"amal": "amala",
"asubh": "asubha",
"guhyarog": "guhyaroga",
"dharidhr": "dharidhra",
"vanchana_chora_bheeth": "vanchana_chora_bheethi",
"rajabhrasht": "rajabhrashta",
"vran": "vrana",
"buddhimatur": "buddhimaturya",
"ayatna_griha_prapt": "ayatna_griha_prapta",
}
def get_skill_yoga_keys(yoga_list: List[Dict]) -> Set[str]:
"""从 skill 检测结果中提取归一化名称集合"""
keys = set()
for y in yoga_list:
for field in ['name', 'name_cn']:
val = y.get(field, "")
if not val or not isinstance(val, str):
continue
# 排除纯数字、空字符串、太短的名字
stripped = val.strip()
if not stripped or stripped.replace('_', '').replace('-', '').isdigit():
continue
if len(stripped) <= 2:
continue
key = normalize_name(val)
if not key or key.replace('_', '').isdigit():
continue
key = _canonical_name(key)
keys.add(key)
return keys
def _canonical_name(key: str) -> str:
"""通过 CROSS_NAME_MAP 将别名映射到统一的标准名称"""
return CROSS_NAME_MAP.get(key, key)
def get_external_benchmark_yoga_keys(external_benchmark_results: Dict[str, dict]) -> Set[str]:
"""从 PyJhora 检测结果中提取归一化名称集合"""
keys = set()
for fname in external_benchmark_results.keys():
# 去掉 _from_jd_place 等后缀
base = re.sub(r"_(from_jd_place|from_planet_positions|calculation|calc)$",
"", fname)
base = base.replace("_yoga", "")
key = normalize_name(base)
# 映射到标准名称
key = _canonical_name(key)
keys.add(key)
return keys
# ============================================================
# 核心验证逻辑
# ============================================================
def validate_one_case(case: dict, run_external_benchmark: bool = True) -> dict:
"""
验证单个测试用例。
case = {
"name": "Einstein",
"year": 1879, "month": 3, "day": 14,
"hour": 10, "minute": 30,
"lat": 48.0, "lon": 11.0, "tz": 1.0
}
"""
result = {
"case_name": case.get("name", "unknown"),
"birth": f"{case['year']}-{case['month']:02d}-{case['day']:02d} "
f"{case.get('hour', 12):02d}:{case.get('minute', 0):02d}",
"skill_yogas": [],
"external_benchmark_yogas": [],
"matched": [],
"skill_only": [], # false positive
"external_benchmark_only": [], # false negative
"error": None,
}
year = case['year']
month = case['month']
day = case['day']
hour = case.get('hour', 12)
minute = case.get('minute', 0)
lat = case['lat']
lon = case['lon']
tz = case.get('tz', 0)
# --- Skill 检测 ---
try:
planets, asc, jd, ayanamsa = skill_compute_chart(
year, month, day, hour, minute, lat, lon, tz
)
skill_yogas = skill_detect_yogas(planets, asc)
result['skill_yogas'] = [y.get('name', y.get('id', '?')) for y in skill_yogas]
result['skill_count'] = len(skill_yogas)
result['skill_details'] = []
for y in skill_yogas:
result['skill_details'].append({
'name': y.get('name', ''),
'id': y.get('id', ''),
'strength': y.get('strength', ''),
})
except Exception as e:
result['error'] = f"Skill error: {e}\n{traceback.format_exc()}"
return result
# --- PyJhora 检测 ---
if run_external_benchmark:
try:
jd_py = external_benchmark_jd(year, month, day, hour + minute / 60.0)
external_benchmark_results = external_benchmark_get_yogas(jd_py, lat, lon, tz,
divisional_chart_factor=1)
result['external_benchmark_yogas'] = list(external_benchmark_results.keys())
result['external_benchmark_count'] = len(external_benchmark_results)
result['external_benchmark_details'] = []
for fname, details in external_benchmark_results.items():
result['external_benchmark_details'].append({
'function': fname,
'name': details.get('name', ''),
'desc': details.get('desc', ''),
})
except Exception as e:
result['error'] = f"PyJhora error: {e}\n{traceback.format_exc()}"
return result
# --- 对比 ---
skill_keys = get_skill_yoga_keys(skill_yogas)
external_benchmark_keys = get_external_benchmark_yoga_keys(external_benchmark_results)
result['matched'] = sorted(skill_keys & external_benchmark_keys)
result['skill_only'] = sorted(skill_keys - external_benchmark_keys)
result['external_benchmark_only'] = sorted(external_benchmark_keys - skill_keys)
# --- 分类:功能性 Yoga vs 经典 Yoga ---
cat_map = get_skill_rule_categories()
result['skill_only_functional'] = []
result['skill_only_classic'] = []
for key in result['skill_only']:
cat = cat_map.get(key, 'unknown')
if cat in FUNCTIONAL_CATEGORIES:
result['skill_only_functional'].append(key)
else:
result['skill_only_classic'].append(key)
return result
def run_validation(cases: List[dict], run_external_benchmark: bool = True) -> dict:
"""运行批量验证"""
report = {
"total_cases": len(cases),
"cases": [],
"summary": {
"total_skill_yogas": 0,
"total_external_benchmark_yogas": 0,
"total_matched": 0,
"total_skill_only": 0,
"total_external_benchmark_only": 0,
"total_skill_only_functional": 0,
"total_skill_only_classic": 0,
}
}
for case in cases:
name = case.get('name', 'unknown')
print(f"🔍 验证: {name} ({case['year']}-{case['month']:02d}-{case['day']:02d})")
r = validate_one_case(case, run_external_benchmark=run_external_benchmark)
report['cases'].append(r)
if r['error']:
print(f" ❌ 错误: {r['error'][:300]}")
continue
sc = r.get('skill_count', 0)
pc = r.get('external_benchmark_count', '?')
func_n = len(r.get('skill_only_functional', []))
classic_n = len(r.get('skill_only_classic', []))
print(f" Skill: {sc} | PyJhora: {pc}")
print(f" 匹配: {len(r['matched'])} | "
f"Skill独有: {len(r['skill_only'])} (功能性{func_n}, 经典{classic_n}) | "
f"PyJhora独有: {len(r['external_benchmark_only'])}")
report['summary']['total_skill_yogas'] += sc
if isinstance(pc, int):
report['summary']['total_external_benchmark_yogas'] += pc
report['summary']['total_matched'] += len(r['matched'])
report['summary']['total_skill_only'] += len(r['skill_only'])
if isinstance(pc, int):
report['summary']['total_external_benchmark_only'] += len(r['external_benchmark_only'])
report['summary']['total_skill_only_functional'] += func_n
report['summary']['total_skill_only_classic'] += classic_n
return report
# ============================================================
# 测试用例
# ============================================================
BENCHMARK_CASES = [
{
"name": "Einstein",
"year": 1879, "month": 3, "day": 14,
"hour": 10, "minute": 30,
"lat": 48.0, "lon": 11.0, "tz": 1.0,
},
{
"name": "Steve Jobs",
"year": 1955, "month": 2, "day": 24,
"hour": 19, "minute": 30,
"lat": 37.0, "lon": -122.0, "tz": -8.0,
},
{
"name": "NM Gandhi",
"year": 1869, "month": 10, "day": 2,
"hour": 8, "minute": 52,
"lat": 21.0, "lon": 73.0, "tz": 5.5,
},
{
"name": "APJ Abdul Kalam",
"year": 1931, "month": 10, "day": 15,
"hour": 6, "minute": 0,
"lat": 8.0, "lon": 78.0, "tz": 5.5,
},
{
"name": "Queen Elizabeth II",
"year": 1926, "month": 4, "day": 21,
"hour": 2, "minute": 40,
"lat": 51.0, "lon": 0.0, "tz": 0.0,
},
]
# ============================================================
# 报告输出
# ============================================================
def print_report(report: dict):
"""打印验证报告"""
print("\n" + "=" * 70)
print("🧘 Yoga 准确率验证报告")
print("=" * 70)
summary = report['summary']
total_skill = summary['total_skill_yogas']
total_external_benchmark = summary['total_external_benchmark_yogas']
matched = summary['total_matched']
skill_only = summary['total_skill_only']
external_benchmark_only = summary['total_external_benchmark_only']
func_only = summary.get('total_skill_only_functional', 0)
classic_only = summary.get('total_skill_only_classic', 0)
print(f"\n📊 汇总:")
print(f" 测试用例数: {report['total_cases']}")
print(f" Skill 检测总数: {total_skill}")
print(f" PyJhora 检测总数: {total_external_benchmark}")
print(f" 匹配总数: {matched}")
print(f" Skill 独有: {skill_only} (功能性{func_only}, 经典{classic_only})")
print(f" PyJhora 独有 (skill 缺失): {external_benchmark_only}")
if total_skill > 0:
precision = matched / total_skill * 100
print(f"\n 总体精确率 (Precision): {precision:.1f}%")
if total_external_benchmark > 0:
recall = matched / total_external_benchmark * 100
print(f" 总体召回率 (Recall): {recall:.1f}%")
# --- 核心经典 Yoga 准确率(排除 skill 特色功能性 Yoga---
classic_skill_total = total_skill - func_only
if classic_skill_total > 0:
classic_precision = matched / classic_skill_total * 100
print(f"\n 🎯 核心经典 Yoga 精确率: {classic_precision:.1f}%")
print(f" (排除 {func_only} 条功能性 Yoga 后: {matched}/{classic_skill_total})")
if total_external_benchmark > 0:
classic_recall = matched / total_external_benchmark * 100
print(f" 🎯 核心经典 Yoga 召回率: {classic_recall:.1f}%")
# --- 全局不匹配统计 ---
print(f"\n🔍 全局不匹配分析:")
all_skill_classic = set()
all_skill_func = set()
all_external_benchmark_missing = set()
for r in report['cases']:
if r.get('error'):
continue
all_skill_classic.update(r.get('skill_only_classic', []))
all_skill_func.update(r.get('skill_only_functional', []))
all_external_benchmark_missing.update(r.get('external_benchmark_only', []))
print(f" Skill 经典 Yoga 不匹配(可能误判): {len(all_skill_classic)}")
if all_skill_classic:
print(f" {sorted(all_skill_classic)[:15]}")
print(f" Skill 功能性 YogaPyJhora 无对应,属 skill 特色): {len(all_skill_func)}")
if all_skill_func:
print(f" {sorted(all_skill_func)[:15]}")
print(f" PyJhora 有但 Skill 缺失的 Yoga: {len(all_external_benchmark_missing)}")
if all_external_benchmark_missing:
print(f" {sorted(all_external_benchmark_missing)[:15]}")
# --- 改进建议 ---
print(f"\n💡 改进建议:")
print(f" 1. 【规则校准】以下 {len(all_skill_classic)} 种经典 Yoga 两边实现不一致,")
print(f" 建议逐条核对 BPHS 原文,修正 skill 规则条件:")
for name in sorted(all_skill_classic)[:10]:
print(f" - {name}")
if len(all_skill_classic) > 10:
print(f" ... 等共 {len(all_skill_classic)}")
missing_top = sorted(all_external_benchmark_missing)[:20]
print(f"\n 2. 【规则补齐】以下 {len(all_external_benchmark_missing)} 种 Yoga PyJhora 已实现但 skill 缺失,")
print(f" 建议按优先级补充(推荐先补充高频出现的):")
for name in missing_top[:15]:
print(f" - {name}")
if len(all_external_benchmark_missing) > 15:
print(f" ... 等共 {len(all_external_benchmark_missing)}")
print(f"\n 3. 【名称映射】当前 CROSS_NAME_MAP 已覆盖常见别名,")
print(f" 如仍有新别名发现,请添加到映射表中。")
print(f"\n📋 逐案例详情:")
for r in report['cases']:
print(f"\n{r['case_name']}{r['birth']}")
if r.get('error'):
print(f"{r['error'][:300]}")
continue
sc = r.get('skill_count', 0)
pc = r.get('external_benchmark_count', '?')
print(f" Skill ({sc}): {r['skill_yogas'][:5]}{'...' if len(r['skill_yogas']) > 5 else ''}")
if 'external_benchmark_yogas' in r:
print(f" PyJhora ({pc}): {r['external_benchmark_yogas'][:5]}{'...' if len(r['external_benchmark_yogas']) > 5 else ''}")
func_n = len(r.get('skill_only_functional', []))
cls_n = len(r.get('skill_only_classic', []))
print(f" 匹配: {len(r['matched'])} | Skill独有: {len(r['skill_only'])}(功能{func_n},经典{cls_n}) | PyJhora独有: {len(r['external_benchmark_only'])}")
if r.get('skill_only_classic'):
print(f" Skill 经典不匹配: {r['skill_only_classic'][:10]}")
if r.get('skill_only_functional'):
print(f" Skill 功能性: {r['skill_only_functional'][:10]}")
if r.get('external_benchmark_only'):
print(f" PyJhora 独有: {r['external_benchmark_only'][:10]}")
def save_report(report: dict, output_file: str):
"""保存报告为 JSON"""
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(report, f, ensure_ascii=False, indent=2)
print(f"\n💾 报告已保存: {output_file}")
# ============================================================
# 主程序
# ============================================================
def main():
parser = argparse.ArgumentParser(description="Yoga 准确率验证框架")
parser.add_argument('--test-cases', help='测试用例 JSON 文件路径')
parser.add_argument('--interactive', action='store_true', help='手动输入出生数据')
parser.add_argument('--benchmark', action='store_true', help='运行内置基准测试')
parser.add_argument('--output', '-o', default='validation_report.json', help='输出报告文件')
parser.add_argument('--skip-pyjhora', action='store_true', help='跳过 PyJhora(只测试 skill')
args = parser.parse_args()
# 检查 PyJhora
if not args.skip_external_benchmark:
if not init_external_benchmark():
print("❌ PyJhora 不可用,请先安装: pip install pyjhora swisseph")
print(" 提示:也可用 --skip-pyjhora 只测试 skill 侧")
return 1
# 加载测试用例
if args.test_cases:
with open(args.test_cases, 'r', encoding='utf-8') as f:
cases = json.load(f)
elif args.benchmark:
cases = BENCHMARK_CASES
elif args.interactive:
cases = [interactive_input()]
else:
print("使用 --benchmark 运行内置测试,或 --test-cases 指定测试用例文件")
return 1
# 运行验证
report = run_validation(cases, run_external_benchmark=not args.skip_external_benchmark)
# 输出报告
print_report(report)
save_report(report, args.output)
return 0
def interactive_input() -> dict:
"""手动输入出生数据"""
print("\n请输入出生数据:")
name = input(" 姓名 (可选): ").strip() or "Test"
year = int(input(" 年: "))
month = int(input(" 月: "))
day = int(input(" 日: "))
hour = int(input(" 时 (24小时制): "))
minute = int(input(" 分: "))
lat = float(input(" 纬度 (度, 北正南负): "))
lon = float(input(" 经度 (度, 东正西负): "))
tz = float(input(" 时区 (小时, 东正西负): "))
return {
"name": name, "year": year, "month": month, "day": day,
"hour": hour, "minute": minute,
"lat": lat, "lon": lon, "tz": tz,
}
if __name__ == '__main__':
sys.exit(main())