Clarify benchmark boundaries and improve annual workflow surfaces

This commit is contained in:
732642856
2026-06-27 19:09:31 +08:00
parent 4d875ea242
commit 04d901e1be
18 changed files with 506 additions and 156 deletions
+4 -4
View File
@@ -41,7 +41,7 @@ def tz_to_float(tz_str):
minutes = int(parts[1]) if len(parts) > 1 else 0
return sign * (hours + minutes / 60.0)
def _planet_dict_from_pyjhora_positions(positions, asc_sign):
def _planet_dict_from_external_benchmark_positions(positions, asc_sign):
"""Convert PyJHora planet positions to the skill validation schema."""
names = {
0: "Sun",
@@ -123,17 +123,17 @@ def compute_yogas(chart):
"d1": {
"ascendant": SIGNS[d1_asc_sign],
"ascendant_degree": d1_asc_degree,
"planets": _planet_dict_from_pyjhora_positions(d1_positions, d1_asc_sign),
"planets": _planet_dict_from_external_benchmark_positions(d1_positions, d1_asc_sign),
},
"d9": {
"ascendant": SIGNS[d9_asc_sign],
"ascendant_degree": d9_asc_degree,
"planets": _planet_dict_from_pyjhora_positions(d9_positions, d9_asc_sign),
"planets": _planet_dict_from_external_benchmark_positions(d9_positions, d9_asc_sign),
},
"d60": {
"ascendant": SIGNS[d60_asc_sign],
"ascendant_degree": d60_asc_degree,
"planets": _planet_dict_from_pyjhora_positions(d60_positions, d60_asc_sign),
"planets": _planet_dict_from_external_benchmark_positions(d60_positions, d60_asc_sign),
},
"panchanga": {
"tithi": tithi_no,
+12 -12
View File
@@ -101,8 +101,8 @@ def extract_skill_names(rules: list[dict]) -> tuple[set[str], dict[str, list[str
return keys, reverse
def extract_pyjhora_names(pyjhora_yoga_file: Path) -> set[str]:
content = pyjhora_yoga_file.read_text(encoding="utf-8", errors="ignore")
def extract_external_benchmark_names(external_benchmark_yoga_file: Path) -> set[str]:
content = external_benchmark_yoga_file.read_text(encoding="utf-8", errors="ignore")
funcs = re.findall(r"^def ([a-zA-Z_][a-zA-Z0-9_]*)\(", content, re.MULTILINE)
names: set[str] = set()
for fn in funcs:
@@ -126,7 +126,7 @@ def extract_pyjhora_names(pyjhora_yoga_file: Path) -> set[str]:
return names
def find_pyjhora_yoga_file(explicit: str | None = None) -> Path | None:
def find_external_benchmark_yoga_file(explicit: str | None = None) -> Path | None:
if explicit:
p = Path(explicit).expanduser().resolve()
return p if p.exists() else None
@@ -176,12 +176,12 @@ def main() -> int:
skill_keys, skill_reverse = extract_skill_names(rules)
pyjhora_file = find_pyjhora_yoga_file(args.pyjhora_yoga_file)
external_benchmark_file = find_external_benchmark_yoga_file(args.external_benchmark_yoga_file)
py_names: set[str] = set()
missing: list[str] = []
coverage_pct = None
if pyjhora_file:
py_names = extract_pyjhora_names(pyjhora_file)
if external_benchmark_file:
py_names = extract_external_benchmark_names(external_benchmark_file)
missing = sorted([name for name in py_names if not covered(name, skill_keys)])
coverage_pct = round((len(py_names) - len(missing)) / len(py_names) * 100, 2) if py_names else None
@@ -197,11 +197,11 @@ def main() -> int:
"categories": dict(categories.most_common()),
"strength_values": dict(strength_values),
"skill_normalized_name_keys": len(skill_keys),
"pyjhora_yoga_file": str(pyjhora_file) if pyjhora_file else None,
"pyjhora_unique_yoga_names": len(py_names) if pyjhora_file else None,
"matched_unique_yoga_names": (len(py_names) - len(missing)) if pyjhora_file else None,
"external_benchmark_yoga_file": str(external_benchmark_file) if external_benchmark_file else None,
"external_benchmark_unique_yoga_names": len(py_names) if external_benchmark_file else None,
"matched_unique_yoga_names": (len(py_names) - len(missing)) if external_benchmark_file else None,
"coverage_pct": coverage_pct,
"missing_count": len(missing) if pyjhora_file else None,
"missing_count": len(missing) if external_benchmark_file else None,
"missing": missing,
}
@@ -221,11 +221,11 @@ def main() -> int:
print(f" {cat:16s} {count:3d}")
print("\nPyJHora 对比:")
if not pyjhora_file:
if not external_benchmark_file:
print(" 未找到 PyJHora yoga.py;仅完成本地 JSON 统计。")
print(" 可用 --pyjhora-yoga-file 指定路径。")
else:
print(f" yoga.py: {pyjhora_file}")
print(f" yoga.py: {external_benchmark_file}")
print(f" PyJHora 唯一 Yoga 名称: {len(py_names)}")
print(f" 已匹配: {len(py_names) - len(missing)}")
print(f" 疑似缺失: {len(missing)}")
+3 -3
View File
@@ -74,7 +74,7 @@ CELEBRITY_CHARTS = [
{"name": "Paramahansa Yogananda", "date": "1893-01-05", "time": "20:38", "tz": "+05:30", "lat": 27.0360, "lon": 88.2627, "city": "Gorakhpur, India"},
]
def compute_pyjhora_yogas(chart):
def compute_external_benchmark_yogas(chart):
try:
result = subprocess.run(
[PYJHORA, HELPER],
@@ -98,12 +98,12 @@ if __name__ == "__main__":
}
for chart in CELEBRITY_CHARTS:
print(f" Computing {chart['name']}...", flush=True)
yogas = compute_pyjhora_yogas(chart)
yogas = compute_external_benchmark_yogas(chart)
entry = dict(chart)
entry["expected_yogas"] = yogas.get("yogas", [])
if "context" in yogas:
entry["context"] = yogas["context"]
entry["pyjhora_raw"] = yogas
entry["external_benchmark_raw"] = yogas
output["charts"].append(entry)
with open(outpath, "w") as f:
json.dump(output, f, indent=2, ensure_ascii=False)
+1
View File
@@ -8,6 +8,7 @@ mkdir -p "$WB/references"
mkdir -p "$WB/skills/jyotish-engine-modules"
mkdir -p "$WB/skills/jyotish-full-reading-integration"
cp "$ROOT/AGENTS.md" "$WB/AGENTS.md"
cp "$ROOT/SKILL.md" "$WB/SKILL.md"
cp "$ROOT/references/technique_registry.json" "$WB/references/technique_registry.json"
cp "$ROOT/references/quick-reference-guide.md" "$WB/references/quick-reference-guide.md"
+3 -3
View File
@@ -353,7 +353,7 @@ def main():
'chart': name,
'rule_id': rid,
'rule_name': rule_id_to_name.get(rid, '?'),
'pyjhora_names': orig_names,
'external_benchmark_names': orig_names,
})
# ==== 输出报告 ====
@@ -404,8 +404,8 @@ def main():
"charts_tested": 60,
"comparable_rules": len(comparable_rule_ids),
"skill_total": total_skill_comp,
"pyjhora_total": total_pyj_comp,
"unmapped_pyjhora": total_unmapped_pyj,
"external_benchmark_total": total_pyj_comp,
"unmapped_external_benchmark": total_unmapped_pyj,
"missing_mappings": {k: v for k, v in sorted(missing_mappings.items())},
"agreements": total_agreements,
"false_positives": total_false_positives,
+54 -54
View File
@@ -175,7 +175,7 @@ def skill_detect_yogas(planets, asc):
# ============================================================
# PyJhora 接口封装
# ============================================================
def init_pyjhora():
def init_external_benchmark():
"""初始化 PyJhora,返回是否成功"""
try:
import jhora.horoscope.chart.yoga as py_yoga
@@ -188,13 +188,13 @@ def init_pyjhora():
return False
def pyjhora_jd(year, month, day, hour_frac):
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 pyjhora_get_yogas(jd, lat, lon, tz, divisional_chart_factor=1):
def external_benchmark_get_yogas(jd, lat, lon, tz, divisional_chart_factor=1):
"""
调用 PyJhora 获取 D1 宫盘的 Yoga 检测结果。
返回:{yoga_function_name: {"name": ..., "desc": ..., "benefits": ...}}
@@ -329,10 +329,10 @@ def _canonical_name(key: str) -> str:
return CROSS_NAME_MAP.get(key, key)
def get_pyjhora_yoga_keys(pyjhora_results: Dict[str, dict]) -> Set[str]:
def get_external_benchmark_yoga_keys(external_benchmark_results: Dict[str, dict]) -> Set[str]:
"""从 PyJhora 检测结果中提取归一化名称集合"""
keys = set()
for fname in pyjhora_results.keys():
for fname in external_benchmark_results.keys():
# 去掉 _from_jd_place 等后缀
base = re.sub(r"_(from_jd_place|from_planet_positions|calculation|calc)$",
"", fname)
@@ -347,7 +347,7 @@ def get_pyjhora_yoga_keys(pyjhora_results: Dict[str, dict]) -> Set[str]:
# ============================================================
# 核心验证逻辑
# ============================================================
def validate_one_case(case: dict, run_pyjhora: bool = True) -> dict:
def validate_one_case(case: dict, run_external_benchmark: bool = True) -> dict:
"""
验证单个测试用例。
@@ -363,10 +363,10 @@ def validate_one_case(case: dict, run_pyjhora: bool = True) -> dict:
"birth": f"{case['year']}-{case['month']:02d}-{case['day']:02d} "
f"{case.get('hour', 12):02d}:{case.get('minute', 0):02d}",
"skill_yogas": [],
"pyjhora_yogas": [],
"external_benchmark_yogas": [],
"matched": [],
"skill_only": [], # false positive
"pyjhora_only": [], # false negative
"external_benchmark_only": [], # false negative
"error": None,
}
@@ -399,16 +399,16 @@ def validate_one_case(case: dict, run_pyjhora: bool = True) -> dict:
return result
# --- PyJhora 检测 ---
if run_pyjhora:
if run_external_benchmark:
try:
jd_py = pyjhora_jd(year, month, day, hour + minute / 60.0)
pyjhora_results = pyjhora_get_yogas(jd_py, lat, lon, tz,
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['pyjhora_yogas'] = list(pyjhora_results.keys())
result['pyjhora_count'] = len(pyjhora_results)
result['pyjhora_details'] = []
for fname, details in pyjhora_results.items():
result['pyjhora_details'].append({
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', ''),
@@ -419,11 +419,11 @@ def validate_one_case(case: dict, run_pyjhora: bool = True) -> dict:
# --- 对比 ---
skill_keys = get_skill_yoga_keys(skill_yogas)
pyjhora_keys = get_pyjhora_yoga_keys(pyjhora_results)
external_benchmark_keys = get_external_benchmark_yoga_keys(external_benchmark_results)
result['matched'] = sorted(skill_keys & pyjhora_keys)
result['skill_only'] = sorted(skill_keys - pyjhora_keys)
result['pyjhora_only'] = sorted(pyjhora_keys - skill_keys)
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()
@@ -439,17 +439,17 @@ def validate_one_case(case: dict, run_pyjhora: bool = True) -> dict:
return result
def run_validation(cases: List[dict], run_pyjhora: bool = True) -> dict:
def run_validation(cases: List[dict], run_external_benchmark: bool = True) -> dict:
"""运行批量验证"""
report = {
"total_cases": len(cases),
"cases": [],
"summary": {
"total_skill_yogas": 0,
"total_pyjhora_yogas": 0,
"total_external_benchmark_yogas": 0,
"total_matched": 0,
"total_skill_only": 0,
"total_pyjhora_only": 0,
"total_external_benchmark_only": 0,
"total_skill_only_functional": 0,
"total_skill_only_classic": 0,
}
@@ -458,7 +458,7 @@ def run_validation(cases: List[dict], run_pyjhora: bool = True) -> dict:
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_pyjhora=run_pyjhora)
r = validate_one_case(case, run_external_benchmark=run_external_benchmark)
report['cases'].append(r)
if r['error']:
@@ -466,21 +466,21 @@ def run_validation(cases: List[dict], run_pyjhora: bool = True) -> dict:
continue
sc = r.get('skill_count', 0)
pc = r.get('pyjhora_count', '?')
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['pyjhora_only'])}")
f"PyJhora独有: {len(r['external_benchmark_only'])}")
report['summary']['total_skill_yogas'] += sc
if isinstance(pc, int):
report['summary']['total_pyjhora_yogas'] += pc
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_pyjhora_only'] += len(r['pyjhora_only'])
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
@@ -535,27 +535,27 @@ def print_report(report: dict):
summary = report['summary']
total_skill = summary['total_skill_yogas']
total_pyjhora = summary['total_pyjhora_yogas']
total_external_benchmark = summary['total_external_benchmark_yogas']
matched = summary['total_matched']
skill_only = summary['total_skill_only']
pyjhora_only = summary['total_pyjhora_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_pyjhora}")
print(f" PyJhora 检测总数: {total_external_benchmark}")
print(f" 匹配总数: {matched}")
print(f" Skill 独有: {skill_only} (功能性{func_only}, 经典{classic_only})")
print(f" PyJhora 独有 (skill 缺失): {pyjhora_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_pyjhora > 0:
recall = matched / total_pyjhora * 100
if total_external_benchmark > 0:
recall = matched / total_external_benchmark * 100
print(f" 总体召回率 (Recall): {recall:.1f}%")
# --- 核心经典 Yoga 准确率(排除 skill 特色功能性 Yoga---
@@ -564,21 +564,21 @@ def print_report(report: dict):
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_pyjhora > 0:
classic_recall = matched / total_pyjhora * 100
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_pyjhora_missing = 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_pyjhora_missing.update(r.get('pyjhora_only', []))
all_external_benchmark_missing.update(r.get('external_benchmark_only', []))
print(f" Skill 经典 Yoga 不匹配(可能误判): {len(all_skill_classic)}")
if all_skill_classic:
@@ -586,9 +586,9 @@ def print_report(report: dict):
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_pyjhora_missing)}")
if all_pyjhora_missing:
print(f" {sorted(all_pyjhora_missing)[: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💡 改进建议:")
@@ -599,13 +599,13 @@ def print_report(report: dict):
if len(all_skill_classic) > 10:
print(f" ... 等共 {len(all_skill_classic)}")
missing_top = sorted(all_pyjhora_missing)[:20]
print(f"\n 2. 【规则补齐】以下 {len(all_pyjhora_missing)} 种 Yoga PyJhora 已实现但 skill 缺失,")
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_pyjhora_missing) > 15:
print(f" ... 等共 {len(all_pyjhora_missing)}")
if len(all_external_benchmark_missing) > 15:
print(f" ... 等共 {len(all_external_benchmark_missing)}")
print(f"\n 3. 【名称映射】当前 CROSS_NAME_MAP 已覆盖常见别名,")
print(f" 如仍有新别名发现,请添加到映射表中。")
@@ -617,20 +617,20 @@ def print_report(report: dict):
print(f"{r['error'][:300]}")
continue
sc = r.get('skill_count', 0)
pc = r.get('pyjhora_count', '?')
pc = r.get('external_benchmark_count', '?')
print(f" Skill ({sc}): {r['skill_yogas'][:5]}{'...' if len(r['skill_yogas']) > 5 else ''}")
if 'pyjhora_yogas' in r:
print(f" PyJhora ({pc}): {r['pyjhora_yogas'][:5]}{'...' if len(r['pyjhora_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['pyjhora_only'])}")
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('pyjhora_only'):
print(f" PyJhora 独有: {r['pyjhora_only'][:10]}")
if r.get('external_benchmark_only'):
print(f" PyJhora 独有: {r['external_benchmark_only'][:10]}")
def save_report(report: dict, output_file: str):
@@ -653,8 +653,8 @@ def main():
args = parser.parse_args()
# 检查 PyJhora
if not args.skip_pyjhora:
if not init_pyjhora():
if not args.skip_external_benchmark:
if not init_external_benchmark():
print("❌ PyJhora 不可用,请先安装: pip install pyjhora swisseph")
print(" 提示:也可用 --skip-pyjhora 只测试 skill 侧")
return 1
@@ -672,7 +672,7 @@ def main():
return 1
# 运行验证
report = run_validation(cases, run_pyjhora=not args.skip_pyjhora)
report = run_validation(cases, run_external_benchmark=not args.skip_external_benchmark)
# 输出报告
print_report(report)
+6 -6
View File
@@ -1298,7 +1298,7 @@ class YogaEngine:
return False
return offset(ctx.house_of(p), h) in (6, 3 if p == 'Mars' else -1, 7 if p == 'Mars' else -1, 4 if p == 'Jupiter' else -1, 8 if p == 'Jupiter' else -1, 2 if p == 'Saturn' else -1, 9 if p == 'Saturn' else -1)
def pyjhora_planets_aspecting_raasi(p, h):
def external_benchmark_planets_aspecting_raasi(p, h):
"""Replicate PyJHora house.planets_aspecting_the_raasi() behavior for source parity."""
if p not in ctx.planets or h is None:
return False
@@ -1314,7 +1314,7 @@ class YogaEngine:
]
return target_rasi_idx in planet_ids_in_aspected_signs
def pyjhora_aspected_planets_of_raasi(h):
def external_benchmark_aspected_planets_of_raasi(h):
"""Replicate PyJHora house.aspected_planets_of_the_raasi(): planets whose rasi drishti hits a target house."""
if h is None:
return []
@@ -1431,7 +1431,7 @@ class YogaEngine:
occupants = ctx.planets_in_house(target)
return bool(occupants) and all(p in BENEFICS for p in occupants)
def pyjhora_natural_benefics():
def external_benchmark_natural_benefics():
"""Replicate PyJHora yoga._get_natural_benefics(): Jupiter, Venus, plus benefic Mercury."""
benefics = [p for p in ["Jupiter", "Venus"] if p in ctx.planets]
mercury_house = ctx.house_of("Mercury")
@@ -1492,8 +1492,8 @@ class YogaEngine:
# v6.0.32: 同宫与相位检查(custom规则常用)
"same_house": same_house, "aspect": aspect, "aspects_house": aspects_house,
"graha_aspects_house": graha_aspects_house,
"pyjhora_planets_aspecting_raasi": pyjhora_planets_aspecting_raasi,
"pyjhora_aspected_planets_of_raasi": pyjhora_aspected_planets_of_raasi,
"external_benchmark_planets_aspecting_raasi": external_benchmark_planets_aspecting_raasi,
"external_benchmark_aspected_planets_of_raasi": external_benchmark_aspected_planets_of_raasi,
"rasi_drishti_signs_from": rasi_drishti_signs_from,
"rasi_aspects_house": rasi_aspects_house, "rasi_aspects": rasi_aspects,
"rasi_aspected_by_planets": rasi_aspected_by_planets,
@@ -1503,7 +1503,7 @@ class YogaEngine:
"only_malefics_in_house": only_malefics_in_house,
"house_has_benefic": house_has_benefic, "house_has_malefic": house_has_malefic,
"house_sign": house_sign, "movable_house": movable_house,
"pyjhora_natural_benefics": pyjhora_natural_benefics,
"external_benchmark_natural_benefics": external_benchmark_natural_benefics,
"d9_house_of": d9_house_of, "d9_sign_of": d9_sign_of,
"d9_lord_of_house": d9_lord_of_house, "navamsa_dispositor": navamsa_dispositor,
"tithi": tithi, "is_waning_moon": is_waning_moon,