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
212 lines
7.9 KiB
Python
212 lines
7.9 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 math
|
||
from datetime import datetime, timezone, timedelta
|
||
from pathlib import Path
|
||
|
||
import swisseph as swe
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
DATA = ROOT / 'data/benchmark_samples.json'
|
||
OUT = ROOT / 'outputs'
|
||
SWISS_OUT = OUT / 'swiss_direct'
|
||
CANON = OUT / 'canonical'
|
||
|
||
PLANETS = {
|
||
'Sun': swe.SUN,
|
||
'Moon': swe.MOON,
|
||
'Mars': swe.MARS,
|
||
'Mercury': swe.MERCURY,
|
||
'Jupiter': swe.JUPITER,
|
||
'Venus': swe.VENUS,
|
||
'Saturn': swe.SATURN,
|
||
'Rahu': swe.MEAN_NODE,
|
||
}
|
||
SIGNS = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo','Libra','Scorpio','Sagittarius','Capricorn','Aquarius','Pisces']
|
||
NAKSHATRAS = [
|
||
'Ashwini','Bharani','Krittika','Rohini','Mrigashira','Ardra','Punarvasu','Pushya','Ashlesha',
|
||
'Magha','Purva Phalguni','Uttara Phalguni','Hasta','Chitra','Swati','Vishakha','Anuradha','Jyeshtha',
|
||
'Mula','Purva Ashadha','Uttara Ashadha','Shravana','Dhanishta','Shatabhisha','Purva Bhadrapada','Uttara Bhadrapada','Revati'
|
||
]
|
||
|
||
|
||
def normalize(deg):
|
||
return deg % 360.0
|
||
|
||
|
||
def sign_of(lon):
|
||
idx = int(normalize(lon) // 30)
|
||
return SIGNS[idx]
|
||
|
||
|
||
def degree_in_sign(lon):
|
||
return normalize(lon) % 30
|
||
|
||
|
||
def nakshatra_of(lon):
|
||
unit = 360.0 / 27.0
|
||
pos = normalize(lon) / unit
|
||
idx = int(math.floor(pos))
|
||
pada = int(math.floor((pos - idx) * 4)) + 1
|
||
if pada > 4:
|
||
pada = 4
|
||
return NAKSHATRAS[idx], pada
|
||
|
||
|
||
def julian_day_utc(birth):
|
||
tz = float(birth['tz'])
|
||
local = datetime(int(birth['year']), int(birth['month']), int(birth['day']), int(birth['hour']), int(birth['minute']))
|
||
utc_dt = local - timedelta(hours=tz)
|
||
hour = utc_dt.hour + utc_dt.minute / 60 + utc_dt.second / 3600
|
||
return swe.julday(utc_dt.year, utc_dt.month, utc_dt.day, hour, swe.GREG_CAL)
|
||
|
||
|
||
def calc_swiss(sample):
|
||
birth = sample['birth']
|
||
jd = julian_day_utc(birth)
|
||
swe.set_sid_mode(swe.SIDM_LAHIRI, 0, 0)
|
||
flags = swe.FLG_SWIEPH | swe.FLG_SIDEREAL | swe.FLG_SPEED
|
||
planets = {}
|
||
for name, pid in PLANETS.items():
|
||
res, ret = swe.calc_ut(jd, pid, flags)
|
||
lon = normalize(res[0])
|
||
nak, pada = nakshatra_of(lon)
|
||
planets[name] = {
|
||
'longitude': round(lon, 6),
|
||
'sign': sign_of(lon),
|
||
'degree_in_sign': round(degree_in_sign(lon), 6),
|
||
'nakshatra': nak,
|
||
'nakshatra_pada': pada,
|
||
'retrograde': bool(res[3] < 0),
|
||
}
|
||
rahu_lon = planets['Rahu']['longitude']
|
||
ketu_lon = normalize(rahu_lon + 180)
|
||
nak, pada = nakshatra_of(ketu_lon)
|
||
planets['Ketu'] = {
|
||
'longitude': round(ketu_lon, 6),
|
||
'sign': sign_of(ketu_lon),
|
||
'degree_in_sign': round(degree_in_sign(ketu_lon), 6),
|
||
'nakshatra': nak,
|
||
'nakshatra_pada': pada,
|
||
'retrograde': planets['Rahu']['retrograde'],
|
||
}
|
||
return {
|
||
'sample_id': sample['id'],
|
||
'engine': 'swiss_direct_lahiri_mean_node',
|
||
'julian_day_ut': jd,
|
||
'parameters': {'ayanamsa': 'Lahiri', 'node': 'Mean Node', 'flags': int(flags)},
|
||
'planets': planets,
|
||
}
|
||
|
||
|
||
def compare_sample(sample_id, swiss):
|
||
local_path = CANON / f'{sample_id}.canonical.json'
|
||
local = json.loads(local_path.read_text())
|
||
rows = []
|
||
for pname in ['Sun','Moon','Mars','Mercury','Jupiter','Venus','Saturn','Rahu','Ketu']:
|
||
s = swiss['planets'][pname]
|
||
l = local['planets'][pname]
|
||
for field in ['sign','degree_in_sign','nakshatra','nakshatra_pada','retrograde']:
|
||
sv = s.get(field)
|
||
lv = l.get(field)
|
||
status = 'match'
|
||
delta = ''
|
||
if field == 'degree_in_sign':
|
||
try:
|
||
delta_val = abs(float(sv) - float(lv))
|
||
delta = round(delta_val, 6)
|
||
status = 'match' if delta_val <= 0.1 else 'mismatch'
|
||
except Exception:
|
||
status = 'not_comparable'
|
||
else:
|
||
status = 'match' if sv == lv else 'mismatch'
|
||
rows.append({
|
||
'sample_id': sample_id,
|
||
'body': pname,
|
||
'field': field,
|
||
'local_skill': lv,
|
||
'swiss_direct': sv,
|
||
'delta': delta,
|
||
'status': status,
|
||
})
|
||
return rows
|
||
|
||
|
||
def write_report(rows):
|
||
total = len(rows)
|
||
matches = sum(1 for r in rows if r['status'] == 'match')
|
||
mismatches = [r for r in rows if r['status'] == 'mismatch']
|
||
by_field = {}
|
||
for r in rows:
|
||
by_field.setdefault(r['field'], {'total':0, 'match':0, 'mismatch':0})
|
||
by_field[r['field']]['total'] += 1
|
||
by_field[r['field']][r['status']] = by_field[r['field']].get(r['status'], 0) + 1
|
||
lines = []
|
||
lines.append('# Jyotish benchmark 第一轮 Swiss direct 对比报告')
|
||
lines.append('')
|
||
lines.append('生成时间:2026-06-03')
|
||
lines.append('')
|
||
lines.append('## 1. 范围')
|
||
lines.append('')
|
||
lines.append('- 对比对象:当前 skill canonical baseline vs 直接调用 Swiss Ephemeris。')
|
||
lines.append('- 配置:Sidereal Lahiri,Mean Node,行星黄经与 Nakshatra 字段。')
|
||
lines.append('- 本轮不比较上升、宫位、D9/D10、大运;这些留给下一轮多引擎/参数冻结测试。')
|
||
lines.append('')
|
||
lines.append('## 2. 总体结果')
|
||
lines.append('')
|
||
lines.append(f'- 字段总数:{total}')
|
||
lines.append(f'- 匹配:{matches}')
|
||
lines.append(f'- 不匹配:{len(mismatches)}')
|
||
lines.append(f'- 匹配率:{matches / total:.2%}' if total else '- 匹配率:N/A')
|
||
lines.append('')
|
||
lines.append('## 3. 分字段结果')
|
||
lines.append('')
|
||
lines.append('| Field | Total | Match | Mismatch |')
|
||
lines.append('|---|---:|---:|---:|')
|
||
for field, stat in sorted(by_field.items()):
|
||
lines.append(f"| {field} | {stat['total']} | {stat.get('match',0)} | {stat.get('mismatch',0)} |")
|
||
lines.append('')
|
||
if mismatches:
|
||
lines.append('## 4. 不匹配样例')
|
||
lines.append('')
|
||
lines.append('| Sample | Body | Field | Local skill | Swiss direct | Delta |')
|
||
lines.append('|---|---|---|---|---|---:|')
|
||
for r in mismatches[:80]:
|
||
lines.append(f"| {r['sample_id']} | {r['body']} | {r['field']} | {r['local_skill']} | {r['swiss_direct']} | {r['delta']} |")
|
||
lines.append('')
|
||
lines.append('## 5. 解释')
|
||
lines.append('')
|
||
lines.append('- 若 sign/nakshatra 大量一致,说明当前 skill 的核心 Lahiri 行星计算大方向可信。')
|
||
lines.append('- 若 degree_in_sign 出现系统性差异,优先检查 ayanamsa、True/Mean Node、UTC换算、Swiss flags。')
|
||
lines.append('- 本轮发现的问题只约束计算层,不直接评价解释和预测能力。')
|
||
report = OUT / 'jyotish_benchmark_round1_swiss_direct_compare.md'
|
||
report.write_text('\n'.join(lines))
|
||
return report
|
||
|
||
|
||
def main():
|
||
OUT.mkdir(parents=True, exist_ok=True)
|
||
SWISS_OUT.mkdir(parents=True, exist_ok=True)
|
||
samples = json.loads(DATA.read_text())
|
||
all_rows = []
|
||
for sample in samples:
|
||
swiss = calc_swiss(sample)
|
||
(SWISS_OUT / f"{sample['id']}.swiss_direct.json").write_text(json.dumps(swiss, ensure_ascii=False, indent=2))
|
||
all_rows.extend(compare_sample(sample['id'], swiss))
|
||
csv_path = OUT / 'swiss_direct_comparison_matrix.csv'
|
||
with csv_path.open('w', newline='') as f:
|
||
writer = csv.DictWriter(f, fieldnames=['sample_id','body','field','local_skill','swiss_direct','delta','status'])
|
||
writer.writeheader()
|
||
writer.writerows(all_rows)
|
||
report = write_report(all_rows)
|
||
print(json.dumps({'matrix': str(csv_path), 'report': str(report), 'rows': len(all_rows)}, ensure_ascii=False, indent=2))
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|