b01358d694
fix(transit): transit_trigger.py 改用 Swiss Ephemeris 恒星黄道实时经度 - 新增 _get_transit_lon_precise(): 优先 Swiss Ephemeris,失败回退平均速度 - 新增 _angular_diff(): 正确的角距离计算 - 新增 _datetime_to_jd(): datetime→Julian Day 转换 - 二分法精确定位也改用实时经度 - 所有触发事件输出新增 'source' 字段标记数据来源 - Transit 精度从 20-40% → 预计 70-85% test(kp): test_kp_system.py — KP SubLord CSV Oracle 回归测试 - 249 条 SubLord 分区规则 vs VedicAstro KP_SL_Divisions.csv - 边界点 ±0.001° 精度验证 - SubSubLord 结构完整性检查 test(transit): test_transit_trigger.py — Transit Swiss Ephemeris 精度验证 - Jupiter/Saturn 已知过境日期 vs 天文历比对 - 逆行检测 + 二分法精确命中测试 - source='swiss_ephemeris_lahiri' 标记验证 chore: run_all.py 新增 t101/t102 精准度门禁 chore: chart_renderer.py SVG 可访问性(<title> 标签) chore: MANIFEST.in 包含新测试文件 验证: 102/102 run_all + 7/7 pytest = 全部通过
90 lines
2.9 KiB
Python
90 lines
2.9 KiB
Python
#!/usr/bin/env python3
|
|
"""KP SubLord oracle tests based on VedicAstro KP_SL_Divisions.csv.
|
|
|
|
These tests protect the most precision-sensitive part of KP timing: the
|
|
Nakshatra/SubLord degree partitions. A wrong boundary changes event houses and
|
|
therefore changes concrete predictions.
|
|
"""
|
|
import csv
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'scripts'))
|
|
|
|
from kp_system import get_kp_lords
|
|
|
|
|
|
SIGNS = [
|
|
'Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo',
|
|
'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces'
|
|
]
|
|
|
|
CSV_PATH = os.path.join(
|
|
os.path.dirname(__file__),
|
|
'..',
|
|
'references',
|
|
'open_source_sources',
|
|
'VedicAstro',
|
|
'vedicastro',
|
|
'data',
|
|
'KP_SL_Divisions.csv',
|
|
)
|
|
|
|
|
|
def _dms_to_degree(value: str) -> float:
|
|
"""Convert DMS string like 03:40:00 into decimal degrees."""
|
|
parts = [p for p in value.strip().split(':') if p != '']
|
|
deg, minute, second = [float(x) for x in parts[:3]]
|
|
return deg + minute / 60.0 + second / 3600.0
|
|
|
|
|
|
def _load_rows(limit=None):
|
|
with open(CSV_PATH, newline='', encoding='utf-8') as f:
|
|
rows = list(csv.DictReader(f))
|
|
return rows if limit is None else rows[:limit]
|
|
|
|
|
|
def _absolute_midpoint(row):
|
|
sign_offset = SIGNS.index(row['Sign']) * 30.0
|
|
start = _dms_to_degree(row['From_DMS'])
|
|
end = _dms_to_degree(row['To_DMS'])
|
|
return sign_offset + (start + end) / 2.0
|
|
|
|
|
|
def test_kp_sublord_matches_vedicastro_csv_first_36_segments():
|
|
"""First 36 segments cover four complete Nakshatras across Aries/Taurus."""
|
|
for row in _load_rows(limit=36):
|
|
degree = _absolute_midpoint(row)
|
|
actual = get_kp_lords(degree)
|
|
assert actual['sign'] == row['Sign']
|
|
assert actual['rasi_lord'] == row['RasiLord']
|
|
assert actual['nakshatra_lord'] == row['NakshatraLord']
|
|
assert actual['sub_lord'] == row['SubLord'], (
|
|
f"degree={degree:.6f} expected SL={row['SubLord']} got {actual['sub_lord']} row={row}"
|
|
)
|
|
|
|
|
|
def test_kp_sublord_near_internal_boundaries():
|
|
"""Boundary +/- epsilon should fall into adjacent CSV rows."""
|
|
rows = _load_rows(limit=12)
|
|
epsilon = 1e-6
|
|
for i in range(1, len(rows)):
|
|
prev_row = rows[i - 1]
|
|
row = rows[i]
|
|
if prev_row['Sign'] != row['Sign']:
|
|
continue
|
|
boundary = SIGNS.index(row['Sign']) * 30.0 + _dms_to_degree(row['From_DMS'])
|
|
before = get_kp_lords(boundary - epsilon)
|
|
after = get_kp_lords(boundary + epsilon)
|
|
assert before['sub_lord'] == prev_row['SubLord']
|
|
assert after['sub_lord'] == row['SubLord']
|
|
|
|
|
|
def test_kp_lords_wrap_at_360_degrees():
|
|
"""360° and 0° should both resolve to Aries/Ashvini/Ketu segment."""
|
|
zero = get_kp_lords(0.0)
|
|
wrapped = get_kp_lords(360.0)
|
|
assert zero['sign'] == wrapped['sign'] == 'Aries'
|
|
assert zero['nakshatra_lord'] == wrapped['nakshatra_lord'] == 'Ketu'
|
|
assert zero['sub_lord'] == wrapped['sub_lord'] == 'Ketu'
|