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
268 lines
10 KiB
Python
268 lines
10 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 datetime import datetime, timedelta
|
||
from pathlib import Path
|
||
|
||
import swisseph as swe
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
SKILL_SCRIPT = Path(__file__).resolve().parents[2] / 'scripts' / 'jyotish_engine.py'
|
||
PYTHON = Path(sys.executable)
|
||
DATA = ROOT / 'data/benchmark_samples.json'
|
||
OUT = ROOT / 'outputs'
|
||
RAW = OUT / 'raw'
|
||
TRANSIT_OUT = OUT / 'transit_true'
|
||
|
||
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']
|
||
|
||
|
||
def normalize(deg):
|
||
return deg % 360.0
|
||
|
||
|
||
def sign_of(lon):
|
||
return SIGNS[int(normalize(lon) // 30)]
|
||
|
||
|
||
def degree_in_sign(lon):
|
||
return normalize(lon) % 30
|
||
|
||
|
||
def julian_day_for_transit(date_str, tz):
|
||
y, m, d = map(int, date_str.split('-'))
|
||
local = datetime(y, m, d, 12, 0)
|
||
utc_dt = local - timedelta(hours=float(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_transit(date_str, tz):
|
||
jd = julian_day_for_transit(date_str, tz)
|
||
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])
|
||
planets[name] = {
|
||
'longitude': round(lon, 6),
|
||
'sign': sign_of(lon),
|
||
'degree_in_sign': round(degree_in_sign(lon), 6),
|
||
'retrograde': bool(res[3] < 0),
|
||
}
|
||
rahu_lon = planets['Rahu']['longitude']
|
||
ketu_lon = normalize(rahu_lon + 180)
|
||
planets['Ketu'] = {
|
||
'longitude': round(ketu_lon, 6),
|
||
'sign': sign_of(ketu_lon),
|
||
'degree_in_sign': round(degree_in_sign(ketu_lon), 6),
|
||
'retrograde': planets['Rahu']['retrograde'],
|
||
}
|
||
return {'julian_day_ut': jd, 'planets': planets, 'parameters': {'ayanamsa': 'Lahiri', 'node': 'Mean Node'}}
|
||
|
||
|
||
def run_full_reading(sample):
|
||
birth = sample['birth']
|
||
transit_date = sample.get('today', '2026-06-03')
|
||
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', transit_date,
|
||
'--transit-date', transit_date,
|
||
'--node-mode', 'mean',
|
||
]
|
||
proc = subprocess.run(cmd, text=True, capture_output=True)
|
||
raw_path = RAW / f"{sample['id']}.transit_full_reading.json"
|
||
if proc.returncode != 0:
|
||
raw_path.write_text(json.dumps({'cmd': cmd, 'returncode': proc.returncode, 'stdout': proc.stdout, 'stderr': proc.stderr}, ensure_ascii=False, indent=2))
|
||
raise RuntimeError(proc.stderr[:500])
|
||
data = json.loads(proc.stdout)
|
||
raw_path.write_text(json.dumps(data, ensure_ascii=False, indent=2))
|
||
return data
|
||
|
||
|
||
def compare_sample(sample):
|
||
transit_date = sample.get('today', '2026-06-03')
|
||
data = run_full_reading(sample)
|
||
modules = data.get('modules', {})
|
||
local_transit = modules.get('transit_positions', {})
|
||
local_multi = modules.get('transit_multi_reference', {})
|
||
swiss = calc_swiss_transit(transit_date, sample['birth']['tz'])
|
||
(TRANSIT_OUT / f"{sample['id']}.swiss_transit.json").write_text(json.dumps(swiss, ensure_ascii=False, indent=2))
|
||
|
||
rows = []
|
||
rows.append({
|
||
'sample_id': sample['id'],
|
||
'body': 'module',
|
||
'field': 'transit_positions.data_layer',
|
||
'local_skill': local_transit.get('data_layer'),
|
||
'swiss_direct': 'true_transit_positions',
|
||
'delta': '',
|
||
'status': 'match' if local_transit.get('data_layer') == 'true_transit_positions' else 'mismatch',
|
||
})
|
||
rows.append({
|
||
'sample_id': sample['id'],
|
||
'body': 'module',
|
||
'field': 'transit_multi_reference.data_layer',
|
||
'local_skill': local_multi.get('data_layer'),
|
||
'swiss_direct': 'true_transit_positions',
|
||
'delta': '',
|
||
'status': 'match' if local_multi.get('data_layer') == 'true_transit_positions' else 'mismatch',
|
||
})
|
||
rows.append({
|
||
'sample_id': sample['id'],
|
||
'body': 'module',
|
||
'field': 'transit_multi_reference.target_date',
|
||
'local_skill': local_multi.get('target_date'),
|
||
'swiss_direct': transit_date,
|
||
'delta': '',
|
||
'status': 'match' if local_multi.get('target_date') == transit_date else 'mismatch',
|
||
})
|
||
|
||
local_planets = local_transit.get('planets', {})
|
||
multi_analysis = local_multi.get('transit_analysis', {})
|
||
for pname in ['Sun','Moon','Mars','Mercury','Jupiter','Venus','Saturn','Rahu','Ketu']:
|
||
s = swiss['planets'][pname]
|
||
l = local_planets.get(pname, {})
|
||
for field in ['sign','degree_in_sign','retrograde']:
|
||
sv = s.get(field)
|
||
lv = l.get(field)
|
||
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.01 else 'mismatch'
|
||
except Exception:
|
||
status = 'not_comparable'
|
||
else:
|
||
status = 'match' if sv == lv else 'mismatch'
|
||
rows.append({
|
||
'sample_id': sample['id'],
|
||
'body': pname,
|
||
'field': f'transit_positions.{field}',
|
||
'local_skill': lv,
|
||
'swiss_direct': sv,
|
||
'delta': delta,
|
||
'status': status,
|
||
})
|
||
if pname in ['Jupiter','Saturn','Rahu','Ketu']:
|
||
mv = (multi_analysis.get(pname) or {}).get('sign')
|
||
rows.append({
|
||
'sample_id': sample['id'],
|
||
'body': pname,
|
||
'field': 'transit_multi_reference.sign',
|
||
'local_skill': mv,
|
||
'swiss_direct': s.get('sign'),
|
||
'delta': '',
|
||
'status': 'match' if mv == s.get('sign') else 'mismatch',
|
||
})
|
||
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, 'not_comparable': 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 第八轮 Transit 真实过境对比报告')
|
||
lines.append('')
|
||
lines.append('生成时间:2026-06-03')
|
||
lines.append('')
|
||
lines.append('## 1. 范围')
|
||
lines.append('')
|
||
lines.append('- 对比对象:full-reading.modules.transit_positions / transit_multi_reference vs 直接调用 Swiss Ephemeris。')
|
||
lines.append('- 样本:10个公开/虚构 smoke case,不包含真实用户个人资料。')
|
||
lines.append('- 配置:Sidereal Lahiri,Mean Node,transit date 使用样本 today 字段。')
|
||
lines.append('- 重点:确认 full-reading 的多参考点 Transit 不再使用 natal positions fallback,而是使用真实过境行星位置。')
|
||
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('')
|
||
if mismatches:
|
||
lines.append('- Transit 真实过境链路仍存在不匹配,需继续检查 UTC换算、node mode 或输出路径。')
|
||
else:
|
||
lines.append('- full-reading 的 Transit 输出已明确使用 true_transit_positions。')
|
||
lines.append('- transit_positions 与 Swiss direct 完全对齐;transit_multi_reference 的 Jupiter/Saturn/Rahu/Ketu 星座也与真实过境一致。')
|
||
report = OUT / 'jyotish_benchmark_round8_transit_true_compare.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)
|
||
TRANSIT_OUT.mkdir(parents=True, exist_ok=True)
|
||
samples = json.loads(DATA.read_text())
|
||
all_rows = []
|
||
for sample in samples:
|
||
all_rows.extend(compare_sample(sample))
|
||
csv_path = OUT / 'transit_true_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({
|
||
'rows': len(all_rows),
|
||
'matches': sum(1 for r in all_rows if r['status'] == 'match'),
|
||
'mismatches': sum(1 for r in all_rows if r['status'] == 'mismatch'),
|
||
'csv': str(csv_path),
|
||
'report': str(report),
|
||
}, ensure_ascii=False, indent=2))
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|