v6.9.11: Transit 精度升级 + KP Oracle 测试 + 精准度门禁
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 = 全部通过
This commit is contained in:
@@ -111,7 +111,7 @@ def render_south_indian_chart(planets: Dict, asc_sign: str, title: str = "D1 —
|
||||
if p_sign == sign_name:
|
||||
symbol = PLANET_SYMBOLS.get(pname, pname[:2])
|
||||
color = PLANET_COLORS.get(pname, '#333')
|
||||
lines.append(f'<text x="{x+CELL_SIZE/2}" y="{planet_y}" text-anchor="middle" font-size="13" fill="{color}">{symbol}{p_deg}</text>')
|
||||
lines.append(f'<text x="{x+CELL_SIZE/2}" y="{planet_y}" text-anchor="middle" font-size="13" fill="{color}" data-planet="{pname}"><title>{pname}</title>{symbol}{p_deg}</text>')
|
||||
planet_y += 16
|
||||
|
||||
# 中心区域(传统上写星盘信息)
|
||||
|
||||
+51
-23
@@ -26,15 +26,25 @@ EXACT_ORB = 0.1 # 精确接触
|
||||
|
||||
|
||||
def _get_transit_lon(planet: str, base_date: datetime, days_offset: float) -> float:
|
||||
"""计算行星在指定日期的过境经度(简化模型,基于平均速度)"""
|
||||
"""计算行星在指定日期的过境经度(简化模型,仅作为 Swiss Ephemeris 不可用时的回退)。"""
|
||||
speed = PLANET_SPEED.get(planet, 0.5)
|
||||
# 从base_date的初始位置推算
|
||||
# 注意:实际应使用Swiss Ephemeris,此为近似值
|
||||
return (base_date.toordinal() * speed + days_offset * 360 / 365.25) % 360
|
||||
|
||||
|
||||
def _get_planet_lon_swe(planet_name: str, jd: float) -> float:
|
||||
"""使用Swiss Ephemeris计算行星经度(如果可用)"""
|
||||
def _datetime_to_jd(dt: datetime) -> float:
|
||||
"""Convert a datetime to Julian day UT."""
|
||||
import swisseph as swe
|
||||
hour = dt.hour + dt.minute / 60.0 + dt.second / 3600.0 + dt.microsecond / 3_600_000_000.0
|
||||
return swe.julday(dt.year, dt.month, dt.day, hour)
|
||||
|
||||
|
||||
def _angular_diff(a: float, b: float) -> float:
|
||||
"""Smallest angular distance in degrees."""
|
||||
return abs((a - b + 180.0) % 360.0 - 180.0)
|
||||
|
||||
|
||||
def _get_planet_lon_swe(planet_name: str, jd: float, sidereal: bool = True) -> float:
|
||||
"""使用 Swiss Ephemeris 计算行星经度(默认 Lahiri 恒星黄道)。"""
|
||||
try:
|
||||
import swisseph as swe
|
||||
planet_ids = {
|
||||
@@ -44,17 +54,33 @@ def _get_planet_lon_swe(planet_name: str, jd: float) -> float:
|
||||
'Rahu': swe.MEAN_NODE, 'Ketu': swe.MEAN_NODE,
|
||||
}
|
||||
pid = planet_ids.get(planet_name)
|
||||
if pid:
|
||||
result = swe.calc_ut(jd, pid, swe.FLG_SWIEPH)
|
||||
lon = result[0][0]
|
||||
if planet_name == 'Ketu':
|
||||
lon = (lon + 180) % 360
|
||||
return lon
|
||||
if pid is None:
|
||||
return None
|
||||
flags = swe.FLG_SWIEPH
|
||||
if sidereal:
|
||||
swe.set_sid_mode(swe.SIDM_LAHIRI)
|
||||
flags |= swe.FLG_SIDEREAL
|
||||
result = swe.calc_ut(jd, pid, flags)
|
||||
lon = result[0][0]
|
||||
if planet_name == 'Ketu':
|
||||
lon = (lon + 180) % 360
|
||||
return lon % 360
|
||||
except (ImportError, Exception):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _get_transit_lon_precise(planet: str, dt: datetime, base_date: datetime) -> Tuple[float, str]:
|
||||
"""Return transit longitude and calculation source."""
|
||||
try:
|
||||
lon = _get_planet_lon_swe(planet, _datetime_to_jd(dt))
|
||||
if lon is not None:
|
||||
return lon, 'swiss_ephemeris_lahiri'
|
||||
except Exception:
|
||||
pass
|
||||
return _get_transit_lon(planet, base_date, (dt - base_date).total_seconds() / 86400.0), 'mean_speed_fallback'
|
||||
|
||||
|
||||
def search_transit_triggers(
|
||||
planet: str,
|
||||
target_longitude: float,
|
||||
@@ -97,9 +123,10 @@ def search_transit_triggers(
|
||||
prev_orb = None
|
||||
prev_sign = None
|
||||
|
||||
source = 'unknown'
|
||||
while current_date <= end_date:
|
||||
lon = _get_transit_lon(planet, start_date, (current_date - start_date).days)
|
||||
diff = min(abs(lon - target_longitude), 360 - abs(lon - target_longitude))
|
||||
lon, source = _get_transit_lon_precise(planet, current_date, start_date)
|
||||
diff = _angular_diff(lon, target_longitude)
|
||||
|
||||
if diff <= orb:
|
||||
# 检测是否是进入/离开接触
|
||||
@@ -110,6 +137,7 @@ def search_transit_triggers(
|
||||
'orb': round(diff, 2),
|
||||
'event': 'entering',
|
||||
'type': 'transit_contact',
|
||||
'source': source,
|
||||
})
|
||||
elif prev_orb is None:
|
||||
if diff <= EXACT_ORB:
|
||||
@@ -119,6 +147,7 @@ def search_transit_triggers(
|
||||
'orb': round(diff, 2),
|
||||
'event': 'exact',
|
||||
'type': 'exact_hit',
|
||||
'source': source,
|
||||
})
|
||||
|
||||
prev_orb = diff
|
||||
@@ -152,6 +181,7 @@ def _merge_contact_intervals(triggers: List[Dict], planet: str, target: float) -
|
||||
'duration_days': (period_end - entry['date']).days,
|
||||
'event': f'{planet} transit over {target:.1f}°',
|
||||
'type': 'transit_period',
|
||||
'source': entry.get('source', 'unknown'),
|
||||
})
|
||||
i = j
|
||||
else:
|
||||
@@ -163,6 +193,7 @@ def _merge_contact_intervals(triggers: List[Dict], planet: str, target: float) -
|
||||
'duration_days': 1,
|
||||
'event': f'{planet} exact on {target:.1f}°',
|
||||
'type': 'exact_hit',
|
||||
'source': entry.get('source', 'unknown'),
|
||||
})
|
||||
i += 1
|
||||
return merged
|
||||
@@ -285,24 +316,21 @@ def find_exact_transit_date(
|
||||
lo_days = 0.0
|
||||
hi_days = (end_date - start_date).days
|
||||
|
||||
lo_lon = _get_transit_lon(planet, start_date, 0)
|
||||
hi_lon = _get_transit_lon(planet, start_date, hi_days)
|
||||
|
||||
# 判断目标是否在区间内(考虑360°环绕)
|
||||
def angle_between(target, a, b):
|
||||
a, b, target = sorted([a % 360, b % 360, target % 360])
|
||||
return target == b # target在中间
|
||||
lo_lon, source = _get_transit_lon_precise(planet, start_date, start_date)
|
||||
hi_lon, _ = _get_transit_lon_precise(planet, end_date, start_date)
|
||||
|
||||
for _ in range(30): # 30次迭代精度 ≈ 1分钟
|
||||
mid_days = (lo_days + hi_days) / 2.0
|
||||
mid_lon = _get_transit_lon(planet, start_date, mid_days)
|
||||
mid_dt = start_date + timedelta(days=mid_days)
|
||||
mid_lon, source = _get_transit_lon_precise(planet, mid_dt, start_date)
|
||||
|
||||
if abs(mid_lon - target_longitude) < EXACT_ORB:
|
||||
if _angular_diff(mid_lon, target_longitude) < EXACT_ORB:
|
||||
return {
|
||||
'planet': planet,
|
||||
'target_degree': round(target_longitude, 1),
|
||||
'date': (start_date + timedelta(days=mid_days)).strftime('%Y-%m-%d %H:%M'),
|
||||
'date': mid_dt.strftime('%Y-%m-%d %H:%M'),
|
||||
'exact_degree': round(mid_lon, 2),
|
||||
'source': source,
|
||||
}
|
||||
|
||||
if (mid_lon - lo_lon) % 360 < (target_longitude - lo_lon) % 360:
|
||||
|
||||
Reference in New Issue
Block a user