Files
Jyotisha/benchmarks/jyotish/scripts/run_ashtakavarga_book_examples.py
T
732642856 d2cd5a369f v6.1.9: Add public benchmarks and research roadmap
- 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
2026-06-08 13:16:42 +08:00

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()