d2cd5a369f
- Add sanitized Jyotish benchmark suite with fictional/public smoke samples - Include benchmark scripts and markdown reports while excluding raw JSON/CSV outputs - Add open-source Jyotish project comparison research - Add complete technique coverage roadmap - Add privacy-safe PDF chart validation methodology - Update SKILL.md and CHANGELOG with v6.1.9 scope and privacy boundaries Validation: - py_compile benchmarks/jyotish/scripts/*.py passed - quality gate passed with 35 pytest tests and golden case
218 lines
9.2 KiB
Python
218 lines
9.2 KiB
Python
# NOTE: This script was sanitized for the public repository in v6.1.9.
|
|
# It assumes it is run from the repository root unless JYOTISH_BENCHMARK_ROOT
|
|
# or JYOTISH_SKILL_SCRIPT is provided. Raw output directories are generated locally
|
|
# and are intentionally not committed.
|
|
#!/usr/bin/env python3
|
|
"""Ashtakavarga book-example arbitration.
|
|
|
|
Uses the book-example expected BAV/SAV arrays embedded in PyJHora's own
|
|
pvr_tests.py and compares both the local skill BPHS v2.0 table and PyJHora's
|
|
current table against those examples.
|
|
"""
|
|
import csv
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import swisseph as swe
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
OUT = ROOT / 'outputs'
|
|
REPORT = OUT / 'jyotish_benchmark_round6c_ashtakavarga_book_examples.md'
|
|
MATRIX = OUT / 'ashtakavarga_book_examples_matrix.csv'
|
|
SKILL_SCRIPTS = Path(__file__).resolve().parents[2] / 'scripts'
|
|
PYJHORA_SITE = Path(__import__('os').environ.get('PYJHORA_SITE', ''))
|
|
PYJHORA_COMPAT = ROOT / 'scripts/pyjhora_compat'
|
|
|
|
SIGNS = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo','Libra','Scorpio','Sagittarius','Capricorn','Aquarius','Pisces']
|
|
PLANETS = ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn']
|
|
PLANETS_WITH_LAGNA = PLANETS + ['Lagna']
|
|
ID_TO_PLANET = {'0': 'Sun', '1': 'Moon', '2': 'Mars', '3': 'Mercury', '4': 'Jupiter', '5': 'Venus', '6': 'Saturn'}
|
|
|
|
EXAMPLES = {
|
|
'pvr_chart_6': {
|
|
'chart': ['8/5','','2/0/3','','6/4','L','7','','','','','1'],
|
|
'expected_bav': [
|
|
[5, 3, 5, 3, 4, 4, 2, 3, 5, 4, 5, 5],
|
|
[3, 2, 5, 3, 6, 3, 4, 5, 5, 5, 3, 5],
|
|
[4, 3, 4, 3, 4, 3, 2, 5, 1, 3, 3, 4],
|
|
[7, 4, 7, 4, 4, 3, 4, 4, 4, 3, 6, 4],
|
|
[4, 3, 5, 6, 3, 7, 4, 3, 5, 6, 5, 5],
|
|
[8, 7, 4, 3, 3, 2, 4, 6, 4, 4, 4, 3],
|
|
[3, 3, 4, 3, 2, 3, 2, 3, 4, 5, 3, 4],
|
|
[5, 5, 6, 3, 6, 3, 1, 7, 3, 4, 3, 3],
|
|
],
|
|
'expected_sav': [34, 25, 34, 25, 26, 25, 22, 29, 28, 30, 29, 30],
|
|
},
|
|
'pvr_chart_7': {
|
|
'chart': ['6/1/7','','','','','','8/4','L','3/2','0','5',''],
|
|
'expected_bav': [
|
|
[4,2,3,4,6,5,5,3,2,6,6,2],
|
|
[6,3,5,3,5,5,6,3,3,4,4,2],
|
|
[3,2,3,4,2,5,4,3,3,4,3,3],
|
|
[4,6,4,3,4,7,4,5,6,3,5,3],
|
|
[4,4,3,5,6,5,6,4,6,4,3,6],
|
|
[3,5,5,4,6,2,3,6,5,2,7,4],
|
|
[3,2,2,3,5,6,3,4,1,3,6,1],
|
|
],
|
|
'expected_sav': [27,24,25,26,34,35,31,28,26,26,34,21],
|
|
},
|
|
'pvr_chart_12_sav_only': {
|
|
'chart': ['8','5','','','','L','7','2/4','0/3','1','','6'],
|
|
'expected_bav': None,
|
|
'expected_sav': [24,25,31,28,27,39,33,29,26,22,28,25],
|
|
},
|
|
}
|
|
|
|
|
|
def patch_swisseph_for_pyjhora():
|
|
for name in ['SIDM_KRISHNAMURTI_VP291', 'SIDM_TRUE_MULA', 'SIDM_TRUE_CITRA', 'SIDM_TRUE_REVATI']:
|
|
if not hasattr(swe, name):
|
|
setattr(swe, name, getattr(swe, 'SIDM_KRISHNAMURTI', 1))
|
|
|
|
|
|
def load_local_calc():
|
|
if str(SKILL_SCRIPTS) not in sys.path:
|
|
sys.path.insert(0, str(SKILL_SCRIPTS))
|
|
from ashtakavarga import calc_ashtakavarga
|
|
return calc_ashtakavarga
|
|
|
|
|
|
def load_pyjhora_calc():
|
|
if str(PYJHORA_COMPAT) not in sys.path:
|
|
sys.path.insert(0, str(PYJHORA_COMPAT))
|
|
if str(PYJHORA_SITE) not in sys.path:
|
|
sys.path.insert(0, str(PYJHORA_SITE))
|
|
patch_swisseph_for_pyjhora()
|
|
from jhora.horoscope.chart import ashtakavarga
|
|
return ashtakavarga.get_ashtaka_varga
|
|
|
|
|
|
def chart_to_local_inputs(chart):
|
|
planets = {}
|
|
asc_idx = None
|
|
for sign_idx, cell in enumerate(chart):
|
|
if not cell:
|
|
continue
|
|
for token in cell.split('/'):
|
|
if token == 'L':
|
|
asc_idx = sign_idx
|
|
elif token in ID_TO_PLANET:
|
|
planets[ID_TO_PLANET[token]] = {'sign': SIGNS[sign_idx]}
|
|
if asc_idx is None:
|
|
raise ValueError('No Lagna in chart')
|
|
return planets, asc_idx
|
|
|
|
|
|
def local_from_chart(chart):
|
|
calc = load_local_calc()
|
|
planets, asc_idx = chart_to_local_inputs(chart)
|
|
result = calc(planets, asc_idx)
|
|
bav = [result['bav'][p]['bindus'] for p in PLANETS_WITH_LAGNA]
|
|
sav = [result['sav']['scores'][s] for s in SIGNS]
|
|
return bav, sav
|
|
|
|
|
|
def pyjhora_from_chart(chart):
|
|
calc = load_pyjhora_calc()
|
|
bav, sav, _ = calc(chart)
|
|
return bav, sav
|
|
|
|
|
|
def add(rows, example, engine, kind, field, got, expected):
|
|
rows.append({
|
|
'example': example,
|
|
'engine': engine,
|
|
'kind': kind,
|
|
'field': field,
|
|
'got': got,
|
|
'expected': expected,
|
|
'status': 'match' if got == expected else 'mismatch',
|
|
})
|
|
|
|
|
|
def summarize(rows, engine, kind=None):
|
|
subset = [r for r in rows if r['engine'] == engine and (kind is None or r['kind'] == kind)]
|
|
total = len(subset)
|
|
match = sum(1 for r in subset if r['status'] == 'match')
|
|
return total, match, total - match, match / total if total else 0.0
|
|
|
|
|
|
def main():
|
|
rows = []
|
|
per_example = []
|
|
for name, ex in EXAMPLES.items():
|
|
chart = ex['chart']
|
|
local_bav, local_sav = local_from_chart(chart)
|
|
py_bav, py_sav = pyjhora_from_chart(chart)
|
|
if ex['expected_bav'] is not None:
|
|
# Some examples provide only seven planetary BAV rows; compare only expected rows.
|
|
for pidx, expected_row in enumerate(ex['expected_bav']):
|
|
planet = PLANETS_WITH_LAGNA[pidx]
|
|
for sidx, expected in enumerate(expected_row):
|
|
add(rows, name, 'local_skill', 'bav', f'{planet}.{SIGNS[sidx]}', local_bav[pidx][sidx], expected)
|
|
add(rows, name, 'pyjhora', 'bav', f'{planet}.{SIGNS[sidx]}', py_bav[pidx][sidx], expected)
|
|
for sidx, expected in enumerate(ex['expected_sav']):
|
|
add(rows, name, 'local_skill', 'sav', SIGNS[sidx], local_sav[sidx], expected)
|
|
add(rows, name, 'pyjhora', 'sav', SIGNS[sidx], py_sav[sidx], expected)
|
|
per_example.append({
|
|
'example': name,
|
|
'local_bav': summarize([r for r in rows if r['example'] == name], 'local_skill', 'bav'),
|
|
'pyjhora_bav': summarize([r for r in rows if r['example'] == name], 'pyjhora', 'bav'),
|
|
'local_sav': summarize([r for r in rows if r['example'] == name], 'local_skill', 'sav'),
|
|
'pyjhora_sav': summarize([r for r in rows if r['example'] == name], 'pyjhora', 'sav'),
|
|
})
|
|
|
|
with MATRIX.open('w', newline='') as f:
|
|
writer = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
|
|
writer.writeheader()
|
|
writer.writerows(rows)
|
|
|
|
lines = []
|
|
lines.append('# Jyotish benchmark 第六轮补充:Ashtakavarga 公开书例仲裁')
|
|
lines.append('')
|
|
lines.append('生成时间:2026-06-03')
|
|
lines.append('')
|
|
lines.append('## 1. 仲裁目的')
|
|
lines.append('')
|
|
lines.append('- 使用 PyJHora `pvr_tests.py` 中嵌入的 PVR 书例 expected BAV/SAV 数组,比较当前 skill 与 PyJHora 哪个更贴近这些公开例题。')
|
|
lines.append('- 这不是复制 PyJHora 代码;只把其测试文件中的 expected arrays 当成外部书例 benchmark。')
|
|
lines.append('- 图表是公开/书例 chart,不包含用户个人资料。')
|
|
lines.append('')
|
|
lines.append('## 2. 总体结果')
|
|
lines.append('')
|
|
lines.append('| Engine | Kind | Total | Match | Mismatch | Match rate |')
|
|
lines.append('|---|---|---:|---:|---:|---:|')
|
|
for engine in ['local_skill', 'pyjhora']:
|
|
for kind in ['bav', 'sav']:
|
|
total, match, mismatch, rate = summarize(rows, engine, kind)
|
|
lines.append(f'| {engine} | {kind} | {total} | {match} | {mismatch} | {rate:.2%} |')
|
|
lines.append('')
|
|
lines.append('## 3. 逐书例摘要')
|
|
lines.append('')
|
|
lines.append('| Example | Local BAV | PyJHora BAV | Local SAV | PyJHora SAV |')
|
|
lines.append('|---|---:|---:|---:|---:|')
|
|
for item in per_example:
|
|
lb = item['local_bav']; pb = item['pyjhora_bav']; ls = item['local_sav']; ps = item['pyjhora_sav']
|
|
lines.append(f"| {item['example']} | {lb[1]}/{lb[0]} | {pb[1]}/{pb[0]} | {ls[1]}/{ls[0]} | {ps[1]}/{ps[0]} |")
|
|
lines.append('')
|
|
lines.append('## 4. 仲裁结论')
|
|
lines.append('')
|
|
lt, lm, lmis, lr = summarize(rows, 'local_skill')
|
|
pt, pm, pmis, pr = summarize(rows, 'pyjhora')
|
|
if lm == lt and pm == pt:
|
|
lines.append('- 当前 skill 与 PyJHora 对 PVR 公开书例均达到 100% 匹配。')
|
|
lines.append('- 这说明 v2.1 Moon/Venus 贡献表项校准已修复第六轮初始差异;Ashtakavarga BAV/SAV 可列为通过。')
|
|
elif pm > lm:
|
|
lines.append('- PyJHora 当前 Ashtakavarga 贡献表对这些 PVR 书例的贴合度明显高于当前 skill。')
|
|
lines.append('- 这说明第六轮暴露的 Moon/Venus 表项差异不宜只归因为“PyJHora 口径不同”;当前 skill 的贡献表需要降级为可疑,并考虑改为 PyJHora/PVR 书例口径。')
|
|
lines.append('- 建议下一步:把 `scripts/ashtakavarga.py` 的差异表项改为 PyJHora/PVR 口径,重跑第六轮、第六轮补充和 regression。')
|
|
else:
|
|
lines.append('- 当前 skill 对书例贴合度不低于 PyJHora,可保留当前口径。')
|
|
REPORT.write_text('\n'.join(lines))
|
|
print(json.dumps({'report': str(REPORT), 'matrix': str(MATRIX), 'rows': len(rows), 'local_matches': lm, 'pyjhora_matches': pm}, ensure_ascii=False, indent=2))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|