Files
Jyotisha/benchmarks/jyotish/scripts/run_skill_baseline.py
T
2026-07-17 19:39:45 +08:00

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