Files
Jyotisha/benchmarks/jyotish/scripts/run_swiss_direct_compare.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

212 lines
7.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 LahiriMean 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()