249 lines
12 KiB
Python
249 lines
12 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
|
|
import csv
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SKILL_SCRIPT = Path(__file__).resolve().parents[3] / 'scripts' / 'jyotish_engine.py'
|
|
PYTHON = Path(__import__('sys').executable)
|
|
DATA = ROOT / 'data/benchmark_samples.json'
|
|
OUT = ROOT / 'outputs'
|
|
RAW = OUT / 'raw'
|
|
CANON = OUT / 'canonical'
|
|
|
|
PLANETS = ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu']
|
|
FIELDS = ['sign', 'house', 'degree_in_sign', 'nakshatra', 'nakshatra_pada', 'retrograde']
|
|
|
|
|
|
def safe_get(obj, *keys, default=None):
|
|
cur = obj
|
|
for key in keys:
|
|
if not isinstance(cur, dict) or key not in cur:
|
|
return default
|
|
cur = cur[key]
|
|
return cur
|
|
|
|
|
|
def run_sample(sample):
|
|
RAW.mkdir(parents=True, exist_ok=True)
|
|
CANON.mkdir(parents=True, exist_ok=True)
|
|
birth = sample['birth']
|
|
cmd = [
|
|
str(PYTHON), str(SKILL_SCRIPT), 'full-reading',
|
|
'--year', str(birth['year']),
|
|
'--month', str(birth['month']),
|
|
'--day', str(birth['day']),
|
|
'--hour', str(birth['hour']),
|
|
'--minute', str(birth['minute']),
|
|
'--lat', str(birth['lat']),
|
|
'--lon', str(birth['lon']),
|
|
'--tz', str(birth['tz']),
|
|
'--today', sample.get('today', '2026-06-03'),
|
|
]
|
|
proc = subprocess.run(cmd, text=True, capture_output=True)
|
|
raw_path = RAW / f"{sample['id']}.json"
|
|
if proc.returncode != 0:
|
|
raw_path.write_text(json.dumps({
|
|
'error': 'command_failed',
|
|
'returncode': proc.returncode,
|
|
'stderr': proc.stderr,
|
|
'stdout': proc.stdout,
|
|
'cmd': cmd,
|
|
}, ensure_ascii=False, indent=2))
|
|
return {'id': sample['id'], 'ok': False, 'error': proc.stderr.strip()[:500]}
|
|
try:
|
|
data = json.loads(proc.stdout)
|
|
except Exception as exc:
|
|
raw_path.write_text(json.dumps({
|
|
'error': 'json_parse_failed',
|
|
'exception': str(exc),
|
|
'stdout': proc.stdout[:2000],
|
|
'stderr': proc.stderr,
|
|
'cmd': cmd,
|
|
}, ensure_ascii=False, indent=2))
|
|
return {'id': sample['id'], 'ok': False, 'error': str(exc)}
|
|
raw_path.write_text(json.dumps(data, ensure_ascii=False, indent=2))
|
|
canon = canonicalize(sample, data)
|
|
(CANON / f"{sample['id']}.canonical.json").write_text(json.dumps(canon, ensure_ascii=False, indent=2))
|
|
return {'id': sample['id'], 'ok': True, 'canonical': canon}
|
|
|
|
|
|
def canonicalize(sample, data):
|
|
modules = data.get('modules', {})
|
|
chart = modules.get('chart', {})
|
|
planets = chart.get('planets', {})
|
|
d9 = safe_get(modules, 'varga_full', 'D9_Navamsa', default={}) or {}
|
|
d10 = safe_get(modules, 'varga_full', 'D10_Dasamsa', default={}) or {}
|
|
d2 = safe_get(modules, 'varga_full', 'D2_Hora', default={}) or {}
|
|
d4 = safe_get(modules, 'varga_full', 'D4_Turyamsa', default={}) or {}
|
|
current = safe_get(modules, 'dasha', 'current_dasha', default={}) or {}
|
|
ad = current.get('antardasha') or {}
|
|
special = modules.get('special_lagnas', {}) or {}
|
|
canonical = {
|
|
'sample_id': sample['id'],
|
|
'label': sample['label'],
|
|
'category': sample['category'],
|
|
'privacy': sample.get('privacy'),
|
|
'engine': 'local_jyotish_skill_v6_0_4',
|
|
'parameters': {
|
|
'zodiac': 'sidereal',
|
|
'ayanamsa': 'lahiri_assumed_by_skill',
|
|
'house': 'whole_sign_for_planet_house',
|
|
'today': sample.get('today'),
|
|
},
|
|
'birth': sample['birth'],
|
|
'ascendant': chart.get('ascendant'),
|
|
'planets': {},
|
|
'varga': {
|
|
'D2': {k: d2.get(k) for k in ['Ascendant'] + PLANETS},
|
|
'D4': {k: d4.get(k) for k in ['Ascendant'] + PLANETS},
|
|
'D9': {k: d9.get(k) for k in ['Ascendant'] + PLANETS},
|
|
'D10': {k: d10.get(k) for k in ['Ascendant'] + PLANETS},
|
|
},
|
|
'ashtakavarga': {
|
|
'bav': {name: safe_get(modules, 'ashtakavarga', 'bav', name, 'bindus', default=[]) for name in PLANETS[:7] + ['Lagna']},
|
|
'sav': [safe_get(modules, 'ashtakavarga', 'sav', 'scores', sign) for sign in ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo', 'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces']],
|
|
},
|
|
'shadbala': {name: safe_get(modules, 'shadbala', 'planets', name, 'total_virupas') for name in PLANETS[:7]},
|
|
'shadbala_components': {
|
|
name: {
|
|
'sthana': safe_get(modules, 'shadbala', 'planets', name, 'sthana_bala', 'total'),
|
|
'kala': safe_get(modules, 'shadbala', 'planets', name, 'kala_bala', 'total'),
|
|
'dig': safe_get(modules, 'shadbala', 'planets', name, 'dig_bala'),
|
|
'chesta': safe_get(modules, 'shadbala', 'planets', name, 'chesta_bala'),
|
|
'naisargika': safe_get(modules, 'shadbala', 'planets', name, 'naisargika_bala'),
|
|
'drik': safe_get(modules, 'shadbala', 'planets', name, 'drik_bala'),
|
|
}
|
|
for name in PLANETS[:7]
|
|
},
|
|
'dasha': {
|
|
'mahadasha_lord': current.get('lord'),
|
|
'mahadasha_start': current.get('start'),
|
|
'mahadasha_end': current.get('end'),
|
|
'antardasha_lord': ad.get('lord'),
|
|
'antardasha_start': ad.get('start'),
|
|
'antardasha_end': ad.get('end'),
|
|
},
|
|
'advanced': {
|
|
'A10_Karma_Pada': special.get('A10_Karma_Pada'),
|
|
'vargottama_true': {p: v for p, v in (modules.get('vargottama') or {}).items() if isinstance(v, dict) and v.get('is_vargottama')},
|
|
'pushkara_true': {p: v for p, v in (modules.get('pushkara') or {}).items() if isinstance(v, dict) and (v.get('pushkara_navamsa') or v.get('pushkara_bhaga'))},
|
|
'dasha_sandhi': modules.get('dasha_sandhi'),
|
|
},
|
|
'module_health': {
|
|
'module_count': len(modules),
|
|
'validation': modules.get('validation'),
|
|
'empty_modules': sorted([k for k, v in modules.items() if v in ({}, [], None)]),
|
|
}
|
|
}
|
|
for pname in PLANETS:
|
|
pdata = planets.get(pname, {}) or {}
|
|
canonical['planets'][pname] = {field: pdata.get(field) for field in FIELDS}
|
|
return canonical
|
|
|
|
|
|
def flatten_rows(results):
|
|
rows = []
|
|
for result in results:
|
|
if not result.get('ok'):
|
|
rows.append({'sample_id': result['id'], 'engine': 'local_jyotish_skill_v6_0_4', 'section': 'run', 'field': 'status', 'value': 'failed'})
|
|
continue
|
|
c = result['canonical']
|
|
rows.append({'sample_id': c['sample_id'], 'engine': c['engine'], 'section': 'ascendant', 'field': 'raw', 'value': json.dumps(c['ascendant'], ensure_ascii=False)})
|
|
for pname, pdata in c['planets'].items():
|
|
for field, value in pdata.items():
|
|
rows.append({'sample_id': c['sample_id'], 'engine': c['engine'], 'section': f'planet.{pname}', 'field': field, 'value': value})
|
|
for dkey, dval in c['dasha'].items():
|
|
rows.append({'sample_id': c['sample_id'], 'engine': c['engine'], 'section': 'dasha', 'field': dkey, 'value': dval})
|
|
for varga_name, varga in c['varga'].items():
|
|
for body, value in varga.items():
|
|
rows.append({'sample_id': c['sample_id'], 'engine': c['engine'], 'section': varga_name, 'field': body, 'value': json.dumps(value, ensure_ascii=False)})
|
|
rows.append({'sample_id': c['sample_id'], 'engine': c['engine'], 'section': 'advanced', 'field': 'A10_Karma_Pada', 'value': json.dumps(c['advanced']['A10_Karma_Pada'], ensure_ascii=False)})
|
|
rows.append({'sample_id': c['sample_id'], 'engine': c['engine'], 'section': 'health', 'field': 'module_count', 'value': c['module_health']['module_count']})
|
|
return rows
|
|
|
|
|
|
def write_report(samples, results):
|
|
ok_count = sum(1 for r in results if r.get('ok'))
|
|
lines = []
|
|
lines.append('# Jyotish benchmark 第一轮本地基线报告')
|
|
lines.append('')
|
|
lines.append('生成时间:2026-06-03')
|
|
lines.append('')
|
|
lines.append('## 1. 本轮范围')
|
|
lines.append('')
|
|
lines.append('- 本轮只建立当前 skill 的 canonical baseline。')
|
|
lines.append('- 样本全部为公开/虚构 smoke test,不包含用户个人出生资料。')
|
|
lines.append('- 还没有接入 PyJHora / VedAstro / jyotishyamitra 等外部引擎,因此本轮不能给最终可信度评分。')
|
|
lines.append('')
|
|
lines.append('## 2. 执行结果')
|
|
lines.append('')
|
|
lines.append(f'- 样本数:{len(samples)}')
|
|
lines.append(f'- 成功:{ok_count}')
|
|
lines.append(f'- 失败:{len(samples) - ok_count}')
|
|
lines.append('- 输出目录:`jyotish_benchmark/outputs/`')
|
|
lines.append('')
|
|
lines.append('## 3. 样本摘要')
|
|
lines.append('')
|
|
lines.append('| Sample | Ascendant | MD/AD | A10 | Modules | Empty modules |')
|
|
lines.append('|---|---|---|---|---:|---|')
|
|
for result in results:
|
|
if not result.get('ok'):
|
|
lines.append(f"| {result['id']} | failed | failed | failed | 0 | {result.get('error', '')} |")
|
|
continue
|
|
c = result['canonical']
|
|
asc = c['ascendant']
|
|
dasha = f"{c['dasha']['mahadasha_lord']} / {c['dasha']['antardasha_lord']}"
|
|
a10 = c['advanced']['A10_Karma_Pada']
|
|
a10_txt = a10.get('sign') if isinstance(a10, dict) else '-'
|
|
empty = ', '.join(c['module_health']['empty_modules']) or '-'
|
|
lines.append(f"| {c['sample_id']} | {asc} | {dasha} | {a10_txt} | {c['module_health']['module_count']} | {empty} |")
|
|
lines.append('')
|
|
lines.append('## 4. 发现')
|
|
lines.append('')
|
|
lines.append('- 当前 skill 对 10 个 smoke 样本都能生成 full-reading canonical JSON。')
|
|
lines.append('- 这证明内部输出契约具备批量 benchmark 的基础。')
|
|
lines.append('- 但这只是 baseline,不是外部可信度证明。')
|
|
lines.append('- 下一步必须接入至少 PyJHora 和 jyotishyamitra,形成 cross-engine matrix。')
|
|
lines.append('')
|
|
lines.append('## 5. 下一步')
|
|
lines.append('')
|
|
lines.append('1. 安装/隔离运行 PyJHora,抽取 D1/D9/D10/Dasha。')
|
|
lines.append('2. 安装/隔离运行 jyotishyamitra,抽取 JSON 输出。')
|
|
lines.append('3. 若 VedAstro API 可用,加入 API 对比;否则列为人工/半自动。')
|
|
lines.append('4. 生成 `cross_engine_matrix.csv`,按字段计算一致/不一致/不可比。')
|
|
lines.append('5. 对边界样本单独标注,避免误判。')
|
|
report = OUT / 'jyotish_benchmark_round1_local_baseline.md'
|
|
report.write_text('\n'.join(lines))
|
|
return report
|
|
|
|
|
|
def main():
|
|
OUT.mkdir(parents=True, exist_ok=True)
|
|
RAW.mkdir(parents=True, exist_ok=True)
|
|
CANON.mkdir(parents=True, exist_ok=True)
|
|
samples = json.loads(DATA.read_text())
|
|
results = [run_sample(sample) for sample in samples]
|
|
summary = {'total': len(results), 'ok': sum(1 for r in results if r.get('ok')), 'results': [{'id': r['id'], 'ok': r.get('ok'), 'error': r.get('error')} for r in results]}
|
|
(OUT / 'run_summary.json').write_text(json.dumps(summary, ensure_ascii=False, indent=2))
|
|
rows = flatten_rows(results)
|
|
csv_path = OUT / 'local_skill_canonical_matrix.csv'
|
|
with csv_path.open('w', newline='') as f:
|
|
writer = csv.DictWriter(f, fieldnames=['sample_id', 'engine', 'section', 'field', 'value'])
|
|
writer.writeheader()
|
|
writer.writerows(rows)
|
|
report = write_report(samples, results)
|
|
print(json.dumps({'summary': summary, 'matrix': str(csv_path), 'report': str(report)}, ensure_ascii=False, indent=2))
|
|
if summary['ok'] != summary['total']:
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|