feat(annual): localise the solar-return instant, relocated returns, compact PL9 export (BUG-1095..1097)

Upstream yinduzhanxing 41d1c740 / 59c9139f, hand-merged; 089858c0 verified not applicable.

- solar_return: dt_local was the UT instant verbatim (no caller ever added the
  offset). Localise with ZoneInfo at the return instant (IANA id) or the fixed
  offset; expose annual_location; Sahams day/night at the true instant;
  optional current_location return charts.
- cmd_solar_return / annual_tajika_pack / engine: pass birth IANA zone and
  --solar-return-location-mode / --current-* through; external PyJHora
  replay blocked when relocated.
- output_json keeps the upstream stream= signature; PL9 exports written
  compact via dumps (measured faster than json.dump streaming), 9.57 -> 4.61 MB.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
Jesse_Chen
2026-09-29 14:59:08 +08:00
co-authored by Claude Fable 5.1
parent 15be6a0631
commit 7c5ea7ae5a
9 changed files with 516 additions and 18 deletions
+49 -7
View File
@@ -15,7 +15,8 @@ Jyotish Vedic Astrology Skill — v6.0.18
"""
from typing import Dict, Optional, Tuple, List
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo
import math
import sys
import os
@@ -261,6 +262,7 @@ def _find_solar_return_approx(
birth_sun_lon: float,
target_year: int,
tz: float,
timezone_id: str | None = None,
) -> Dict:
"""
近似计算太阳返照时间(不使用 swisseph)。
@@ -272,7 +274,7 @@ def _find_solar_return_approx(
except ValueError:
approx_dt_utc = datetime(target_year, birth_dt.month, 1, 12, 0, 0)
dt_local = approx_dt_utc + timedelta(hours=tz)
dt_local = _localize_return_datetime(approx_dt_utc, tz, timezone_id).replace(tzinfo=None)
return {
'jd_ut': _datetime_to_jd_ut(approx_dt_utc),
@@ -291,12 +293,27 @@ def _find_solar_return_approx(
# 生成完整太阳返照盘
# =========================================================================
def _localize_return_datetime(dt_ut, utc_offset, timezone_id=None):
"""Convert the return instant using the zone's offset at that instant.
With an IANA ``timezone_id`` the offset is taken from the zone rules at the
return instant (DST-aware). Without one, the fixed ``utc_offset`` is used.
Upstream yinduzhanxing 41d1c740.
"""
zone = ZoneInfo(timezone_id) if timezone_id else timezone(timedelta(hours=float(utc_offset)))
return dt_ut.replace(tzinfo=timezone.utc).astimezone(zone)
def calc_solar_return_chart(
birth_year: int, birth_month: int, birth_day: int,
birth_hour: int, birth_minute: int,
birth_lat: float, birth_lon: float, birth_tz: float,
target_year: int,
ayanamsa_name: str = 'raman',
local_timezone_id: str | None = None,
return_lat: float | None = None,
return_lon: float | None = None,
return_tz: float | None = None,
) -> Dict:
"""
计算太阳返照盘(Varshaphala)。
@@ -325,6 +342,13 @@ def calc_solar_return_chart(
# → 改为由调用方传入 birth_sun_lon,或从出生盘数据里取
# 这里我们先做:调用 engine 的 compute_chart_data 算出生盘
try:
if (return_lat is None) != (return_lon is None):
raise ValueError('return_location_incomplete')
if return_lat is not None and return_tz is None and not local_timezone_id:
raise ValueError('return_location_timezone_missing')
annual_lat = float(birth_lat if return_lat is None else return_lat)
annual_lon = float(birth_lon if return_lon is None else return_lon)
annual_tz = float(birth_tz if return_tz is None else return_tz)
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from jyotish_engine import compute_chart_data, HAS_SWE as ENGINE_HAS_SWE
@@ -337,7 +361,8 @@ def calc_solar_return_chart(
birth_hour, birth_minute, 0)),
0, # birth_sun_lon unknown
target_year,
birth_tz
annual_tz,
timezone_id=local_timezone_id,
),
}
@@ -368,6 +393,14 @@ def calc_solar_return_chart(
sr_jd_ut = sr_result['jd_ut']
sr_dt_ut = sr_result['dt_ut']
# 返照时刻本地化:按返照瞬间的时区偏移(IANA 区支持夏令时),而不是
# 把 UT 直接当本地时间(旧行为 dt_local == dt_ut)。
local_dt = _localize_return_datetime(sr_dt_ut, annual_tz, local_timezone_id)
annual_tz = local_dt.utcoffset().total_seconds() / 3600
sr_result['dt_local'] = local_dt.replace(tzinfo=None)
sr_result['annual_location'] = {'latitude': annual_lat, 'longitude': annual_lon,
'utc_offset': annual_tz, 'timezone_id': local_timezone_id}
# Step 3: 以返照时刻计算星盘
# compute_chart_data 接受 year/month/day/hour/minute
sr_year, sr_month, sr_day = sr_dt_ut.year, sr_dt_ut.month, sr_dt_ut.day
@@ -378,7 +411,7 @@ def calc_solar_return_chart(
sr_chart, sr_asc_idx, sr_jd, sr_ayanamsa = compute_chart_data(
sr_year, sr_month, sr_day,
sr_hour_int, sr_minute_int,
birth_lat, birth_lon, 0, # UT 时间,时区=0
annual_lat, annual_lon, 0, # UT 时间,时区=0
'mean',
ayanamsa_name=ayanamsa_name,
)
@@ -404,6 +437,8 @@ def calc_solar_return_chart(
'birth_asc_sign_idx': birth_asc_idx,
'target_year': target_year,
'birth_year': birth_year,
'annual_location': {'latitude': annual_lat, 'longitude': annual_lon,
'utc_offset': annual_tz, 'timezone_id': local_timezone_id},
'age': target_year - birth_year,
'note': f'太阳返照盘 {target_year} 年(年龄 {target_year-birth_year} 岁)',
}
@@ -422,6 +457,10 @@ def solar_return_full_report(
birth_lat: float, birth_lon: float, birth_tz: float,
target_year: int,
ayanamsa_name: str = 'raman',
local_timezone_id: str | None = None,
return_lat: float | None = None,
return_lon: float | None = None,
return_tz: float | None = None,
) -> Dict:
"""
太阳返照盘完整报告(Varshaphala 年运分析)。
@@ -449,6 +488,8 @@ def solar_return_full_report(
birth_lat, birth_lon, birth_tz,
target_year,
ayanamsa_name=ayanamsa_name,
local_timezone_id=local_timezone_id,
return_lat=return_lat, return_lon=return_lon, return_tz=return_tz,
)
if 'error' in sr:
@@ -469,6 +510,7 @@ def solar_return_full_report(
}
else:
result['solar_return'] = sr['solar_return']
result['annual_location'] = dict(sr.get('annual_location') or {})
result['sr_chart_info'] = {
'asc_sign': SIGNS[sr['asc_sign_idx']],
'asc_sign_idx': sr['asc_sign_idx'],
@@ -549,9 +591,9 @@ def solar_return_full_report(
asc_lon,
sr_dt_local,
chart_type='varsha',
lat=birth_lat,
lon=birth_lon,
tz=birth_tz,
lat=sr.get('annual_location', {}).get('latitude', birth_lat),
lon=sr.get('annual_location', {}).get('longitude', birth_lon),
tz=sr.get('annual_location', {}).get('utc_offset', birth_tz),
)
result['sahams'] = sahams
except Exception as e: