fix(ayanamsa): keep a caller-selected mode across import and transit paths (BUG-1269, upstream 23be1807)

This commit is contained in:
jesse-ux
2026-10-07 17:05:18 +08:00
parent 4b9c019814
commit 4e134b9b9f
6 changed files with 183 additions and 24 deletions
+16
View File
@@ -16998,3 +16998,19 @@
- 相关记录:BUG-1190
- 复发自:无
- 修复版本:本分支待发布
## BUG-1269 | 导入引擎时的岁差引导盖掉调用方已选的模式
- 状态:resolved
- 首次发现:2026-10-07
- 最近更新:2026-10-07
- 影响面:`scripts/ayanamsa_utils.py`、`scripts/jyotish_engine.py`、`scripts/bhava_chalit.py`、`scripts/cmd_nakshatra_adv.py`
- 用户现象:调用方先选定岁差,再导入引擎,实际计算回到产品默认的 Raman。Bhava Chalit 和星宿命令丢掉请求的岁差与出生秒
- 触发条件:先 `apply_ayanamsa` 再导入 `jyotish_engine`;或调用 bhava-chalit / nakshatra / transit 时传入非默认岁差
- 根因:引擎导入时无条件 `apply_ayanamsa(DEFAULT_AYANAMSA_NAME)`。本文件作为 `__main__` 跑过之后,Bhava Chalit 和星宿命令再次按模块名导入,引导会再跑一遍。行运行星计算不读请求的岁差。Bhava Chalit 与星宿命令调用 `compute_chart_data` 时不传 `second` 和 `ayanamsa_name`,宫位时刻也不加秒
- 修复:`ayanamsa_utils` 增加进程闩 `AYANAMSA_APPLIED_IN_PROCESS`。已有人设过岁差时,导入引导不再覆盖;还没人设过时仍用本仓默认 Raman。行运在计算前应用请求的岁差,并在策略里记下 requested 与 applied。Bhava Chalit 与星宿命令把岁差和出生秒传进星盘,Bhava 的宫位时刻带上秒
- 验证:`tests/test_ayanamsa_requested_applied.py`。六种岁差(lahiri、raman、krishnamurti、fagan_bradley、true_citra、true_pushya)在本命、行运、Bhava Chalit、星宿命令上 requested 等于 applied。子进程先设 lahiri 再导入引擎,名字仍是 lahiri
- 防复发:上述测试
- 相关记录:BUG-1265
- 复发自:无
- 修复版本:本分支待发布
+11 -1
View File
@@ -45,6 +45,10 @@ AYANAMSA_DISPLAY_NAMES = {
DEFAULT_AYANAMSA_NAME = 'raman'
ACTIVE_AYANAMSA_NAME = DEFAULT_AYANAMSA_NAME
# Flipped the first time any caller applies a mode. Import bootstraps read this
# so they do not replace a mode that was already selected. This module is
# imported once; jyotish_engine can be loaded a second time as its own module.
AYANAMSA_APPLIED_IN_PROCESS = False
class UnsupportedAyanamsaError(ValueError):
@@ -92,7 +96,7 @@ def current_ayanamsa_name(args: Any = None) -> str:
def apply_ayanamsa(name: Any = None, swe_module: Any = None) -> bool:
"""Set Swiss Ephemeris sidereal mode explicitly."""
global ACTIVE_AYANAMSA_NAME
global ACTIVE_AYANAMSA_NAME, AYANAMSA_APPLIED_IN_PROCESS
module = swe_module or swe
if module is None:
return False
@@ -102,9 +106,15 @@ def apply_ayanamsa(name: Any = None, swe_module: Any = None) -> bool:
except Exception:
return False
ACTIVE_AYANAMSA_NAME = key
AYANAMSA_APPLIED_IN_PROCESS = True
return True
def ayanamsa_already_applied() -> bool:
"""True once any caller has applied an ayanamsa in this process."""
return AYANAMSA_APPLIED_IN_PROCESS
@contextmanager
def temporary_ayanamsa(name: Any = None, swe_module: Any = None) -> Iterator[str]:
"""Apply a sidereal mode and restore the previous process-wide mode."""
+18 -8
View File
@@ -473,9 +473,12 @@ def cmd_bhava_chalit(args):
if not HAS_SWE:
return {"error": "swisseph 未安装, 无法计算"}
second = int(getattr(args, 'second', 0) or 0)
chart, asc_idx, jd, ayanamsa = compute_chart_data(
args.year, args.month, args.day, args.hour, args.minute,
args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean'))
args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean'),
second=second,
ayanamsa_name=getattr(args, 'ayanamsa', None))
if chart is None:
return {"error": "星盘计算失败"}
@@ -490,7 +493,7 @@ def cmd_bhava_chalit(args):
asc_lon = chart['ascendant']['degree_raw']
# MC: 从 swisseph 重新获取
import swisseph as swe
hour_decimal = args.hour + args.minute / 60.0 - args.tz
hour_decimal = args.hour + args.minute / 60.0 + second / 3600.0 - args.tz
jd_val = swe.julday(args.year, args.month, args.day, hour_decimal)
cusps_raw, ascmc = swe.houses(jd_val, args.lat, args.lon, b'A')
mc_tropical = ascmc[1] # MC
@@ -500,15 +503,22 @@ def cmd_bhava_chalit(args):
calc = BhavaChalitCalculator()
mode = getattr(args, 'mode', 'compare')
applied_ayanamsa = (chart.get('birth_info') or {}).get('ayanamsa_name')
def _stamp(payload):
if isinstance(payload, dict):
payload['requested_ayanamsa'] = getattr(args, 'ayanamsa', None)
payload['applied_ayanamsa'] = applied_ayanamsa
return payload
if mode == 'boundaries':
return calc.calculate_bhava_boundaries(
asc_lon, mc_lon, house_system, jd_val, args.lat, args.lon)
return _stamp(calc.calculate_bhava_boundaries(
asc_lon, mc_lon, house_system, jd_val, args.lat, args.lon))
elif mode == 'chart':
return calc.get_bhava_chalit_chart(
return _stamp(calc.get_bhava_chalit_chart(
planet_lons, asc_lon, mc_lon, house_system,
jd_val, args.lat, args.lon)
jd_val, args.lat, args.lon))
else: # compare
return calc.compare_rashi_vs_bhava(
return _stamp(calc.compare_rashi_vs_bhava(
planet_lons, asc_lon, mc_lon, house_system,
jd_val, args.lat, args.lon)
jd_val, args.lat, args.lon))
+21 -9
View File
@@ -26,7 +26,9 @@ def _compute_chart_from_args(args: Any) -> tuple:
chart, asc_idx, jd, ayanamsa = compute_chart_data(
args.year, args.month, args.day, args.hour, args.minute,
args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean'))
args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean'),
second=int(getattr(args, 'second', 0) or 0),
ayanamsa_name=getattr(args, 'ayanamsa', None))
if chart is None:
return None, None, None, None, {}, 0, 0, 0
@@ -66,6 +68,14 @@ def cmd_nakshatra_adv(args: Any) -> Dict[str, Any]:
if chart is None:
return {"error": "swisseph未安装"}
applied_ayanamsa = (chart.get('birth_info') or {}).get('ayanamsa_name') if isinstance(chart, dict) else None
def _stamp(payload):
if isinstance(payload, dict):
payload['requested_ayanamsa'] = getattr(args, 'ayanamsa', None)
payload['applied_ayanamsa'] = applied_ayanamsa
return payload
try:
from nakshatra_advanced import (
find_nakshatra, calc_all_tara_balas, calc_sub_lord,
@@ -85,7 +95,7 @@ def cmd_nakshatra_adv(args: Any) -> Dict[str, Any]:
detail[pn] = find_nakshatra(lon)
result_dict = {'planets': detail}
if mode == 'detail':
return result_dict
return _stamp(result_dict)
else:
result_dict = {}
@@ -93,7 +103,7 @@ def cmd_nakshatra_adv(args: Any) -> Dict[str, Any]:
if mode in ('all', 'tara'):
tara = calc_all_tara_balas(moon_nak_idx, planet_lons)
if mode == 'tara':
return {'tara_bala': tara, 'moon_nakshatra_idx': moon_nak_idx}
return _stamp({'tara_bala': tara, 'moon_nakshatra_idx': moon_nak_idx})
result_dict['tara_bala'] = tara
# === mode: 'chandra' (v6.0.22 新增) ===
@@ -107,7 +117,7 @@ def cmd_nakshatra_adv(args: Any) -> Dict[str, Any]:
'chandra': calc_chandra_bala(moon_sign_idx, p_sign),
}
if mode == 'chandra':
return {'chandra_bala': chandra, 'moon_sign_idx': moon_sign_idx}
return _stamp({'chandra_bala': chandra, 'moon_sign_idx': moon_sign_idx})
result_dict['chandra_bala'] = chandra
# === mode: 'combined' (v6.0.22 新增) ===
@@ -115,14 +125,14 @@ def cmd_nakshatra_adv(args: Any) -> Dict[str, Any]:
moon_sign_idx = int(moon_lon / 30) % 12
combined = calc_tara_chandra_combined(moon_nak_idx, moon_sign_idx, planet_lons)
if mode == 'combined':
return {'tara_chandra_combined': combined}
return _stamp({'tara_chandra_combined': combined})
result_dict['tara_chandra_combined'] = combined
# === mode: 'sublord' ===
if mode in ('all', 'sublord'):
sublords = {pn: calc_sub_lord(lon) for pn, lon in planet_lons.items()}
if mode == 'sublord':
return {'sub_lords': sublords}
return _stamp({'sub_lords': sublords})
result_dict['sub_lords'] = sublords
# === mode: 'all' / 'full' — 综合报告 ===
@@ -130,7 +140,7 @@ def cmd_nakshatra_adv(args: Any) -> Dict[str, Any]:
full = nakshatra_full_report(chart)
result_dict.update(full)
return result_dict
return _stamp(result_dict)
def cmd_nakshatra_dasha(args: Any) -> Dict[str, Any]:
@@ -196,7 +206,8 @@ def cmd_nakshatra_dasha(args: Any) -> Dict[str, Any]:
ty, tm, td = map(int, transit_date_str.split('-'))
transit_jd = swe.julday(ty, tm, td, 12.0 - args.tz)
transit_planets, transit_ayanamsa = _calc_sidereal_planets_for_jd(
transit_jd, node_mode=getattr(args, 'node_mode', 'mean'), include_ketu=True)
transit_jd, node_mode=getattr(args, 'node_mode', 'mean'), include_ketu=True,
ayanamsa_name=getattr(args, 'ayanamsa', None))
transit_lons = {}
for pn, pd in transit_planets.items():
if isinstance(pd, dict) and 'degree' in pd:
@@ -217,7 +228,8 @@ def cmd_nakshatra_dasha(args: Any) -> Dict[str, Any]:
ty, tm, td = map(int, transit_date_str.split('-'))
transit_jd = swe.julday(ty, tm, td, 12.0 - args.tz)
transit_planets, _ = _calc_sidereal_planets_for_jd(
transit_jd, node_mode=getattr(args, 'node_mode', 'mean'), include_ketu=True)
transit_jd, node_mode=getattr(args, 'node_mode', 'mean'), include_ketu=True,
ayanamsa_name=getattr(args, 'ayanamsa', None))
transit_lons = {}
for pn, pd in transit_planets.items():
if isinstance(pd, dict) and 'degree' in pd:
+25 -6
View File
@@ -93,6 +93,7 @@ from ayanamsa_utils import (
DEFAULT_AYANAMSA_NAME,
UnsupportedAyanamsaError,
apply_ayanamsa,
ayanamsa_already_applied,
ayanamsa_display_name,
current_ayanamsa_name,
)
@@ -145,7 +146,10 @@ load_local_env(ROOT_DIR)
try:
import swisseph as swe
apply_ayanamsa(DEFAULT_AYANAMSA_NAME, swe) # default Raman sidereal mode; callers pass explicit names
# Default stays Raman. Skip when this process already applied a mode, including
# a second import of this file under the name jyotish_engine.
if not ayanamsa_already_applied():
apply_ayanamsa(DEFAULT_AYANAMSA_NAME, swe)
HAS_SWE = True
except ImportError:
HAS_SWE = False
@@ -772,10 +776,15 @@ def _sidereal_calculation_profile(jd, position_mode):
return mode, ephemeris_flags, ayanamsa
def _calc_sidereal_planets_for_jd(jd, node_mode='mean', include_ketu=True):
"""使用 Swiss Ephemeris 计算指定 Julian Day 的恒星黄道行星位置。"""
def _calc_sidereal_planets_for_jd(jd, node_mode='mean', include_ketu=True, ayanamsa_name=None):
"""使用 Swiss Ephemeris 计算指定 Julian Day 的恒星黄道行星位置。
ayanamsa_name 显式指定岁差。省略时沿用进程当前模式。
"""
if not HAS_SWE:
return {}, None
if ayanamsa_name:
_apply_ayanamsa(ayanamsa_name)
ayanamsa = swe.get_ayanamsa(jd)
calc_flags = getattr(swe, 'FLG_SWIEPH', 2)
data = {}
@@ -12867,11 +12876,16 @@ def cmd_db_stats(args):
# ============================================================================
# 8. 过境查询
# ============================================================================
def _transit_ayanamsa_mixed_review_policy(command_name, *, requested_ayanamsa=None, transit_ayanamsa=None):
def _transit_ayanamsa_mixed_review_policy(command_name, *, requested_ayanamsa=None, transit_ayanamsa=None,
applied_ayanamsa=None):
requested_key = str(requested_ayanamsa).strip().lower().replace('-', '_') if requested_ayanamsa else None
applied_key = str(applied_ayanamsa).strip().lower().replace('-', '_') if applied_ayanamsa else None
return {
'status': 'mixed_review',
'command': command_name,
'requested_ayanamsa': requested_ayanamsa,
'applied_ayanamsa': applied_ayanamsa,
'same_ayanamsa_transit_parity': bool(requested_key and requested_key == applied_key),
'transit_ayanamsa': round(transit_ayanamsa, 4) if isinstance(transit_ayanamsa, (int, float)) else transit_ayanamsa,
'degree_claim_policy': 'sign_house_month_only_until_same_ayanamsa_transit_parity_closes',
'runtime_note': (
@@ -12891,6 +12905,9 @@ def cmd_transit(args):
return {"error": "swisseph未安装,无法计算实时Transit"}
swe.set_ephe_path('')
requested_ayanamsa = getattr(args, 'ayanamsa', None)
if requested_ayanamsa:
_apply_ayanamsa(requested_ayanamsa)
# --- 参数解析 ---
t_year = args.year
@@ -12912,7 +12929,8 @@ def cmd_transit(args):
# --- 计算行星位置 ---
node_mode = getattr(args, 'node_mode', 'mean')
transit_data, ayanamsa = _calc_sidereal_planets_for_jd(jd_mid, node_mode=node_mode, include_ketu=True)
transit_data, ayanamsa = _calc_sidereal_planets_for_jd(
jd_mid, node_mode=node_mode, include_ketu=True, ayanamsa_name=requested_ayanamsa)
if target_planets:
target_set = set(target_planets)
transit_data = {p: d for p, d in transit_data.items() if p in target_set}
@@ -12971,7 +12989,8 @@ def cmd_transit(args):
'data_layer': 'true_transit_positions',
'transit_ayanamsa_policy': _transit_ayanamsa_mixed_review_policy(
'transit',
requested_ayanamsa=getattr(args, 'ayanamsa', None),
requested_ayanamsa=requested_ayanamsa,
applied_ayanamsa=_current_ayanamsa_name(args),
transit_ayanamsa=ayanamsa,
),
'planets': transit_data,
+92
View File
@@ -0,0 +1,92 @@
"""BUG-1269: import bootstrap must not clobber a chosen ayanamsa, and the
natal, transit, bhava-chalit, and nakshatra paths must apply the requested mode.
"""
from __future__ import annotations
import subprocess
import sys
from types import SimpleNamespace
import pytest
from scripts.ayanamsa_utils import AYANAMSA_MODES
from scripts.bhava_chalit import cmd_bhava_chalit
from scripts.cmd_nakshatra_adv import cmd_nakshatra_adv
from scripts.jyotish_engine import cmd_transit, compute_chart_data
MODES = ("lahiri", "raman", "krishnamurti", "fagan_bradley", "true_citra", "true_pushya")
def test_mode_names_exist():
for name in MODES:
assert name in AYANAMSA_MODES
def _birth(mode: str, second: int = 0):
return SimpleNamespace(
year=1955, month=2, day=24, hour=19, minute=15, second=second,
lat=37.7749, lon=-122.4194, tz=-8.0,
ayanamsa=mode, node_mode="mean",
mode="chart", house_system="sripati", planet=None,
retrograde_calendar=False,
)
@pytest.mark.parametrize("mode", MODES)
def test_requested_ayanamsa_is_applied_on_four_paths(mode: str):
chart, _asc, _jd, _value = compute_chart_data(
1955, 2, 24, 19, 15, 37.7749, -122.4194, -8.0, "mean",
second=0, ayanamsa_name=mode,
)
assert chart["birth_info"]["ayanamsa_name"] == mode
transit = cmd_transit(SimpleNamespace(
year=2026, month=10, day=5, tz=-8.0, ayanamsa=mode,
node_mode="mean", planet=None, retrograde_calendar=False,
))
policy = transit["transit_ayanamsa_policy"]
assert policy["requested_ayanamsa"] == mode
assert policy["applied_ayanamsa"] == mode
assert policy["same_ayanamsa_transit_parity"] is True
bhava = cmd_bhava_chalit(_birth(mode))
assert bhava["requested_ayanamsa"] == mode
assert bhava["applied_ayanamsa"] == mode
nak_args = _birth(mode)
nak_args.mode = "detail"
nakshatra = cmd_nakshatra_adv(nak_args)
assert nakshatra["requested_ayanamsa"] == mode
assert nakshatra["applied_ayanamsa"] == mode
def test_lahiri_and_raman_longitudes_differ():
lahiri, *_ = compute_chart_data(1955, 2, 24, 19, 15, 37.7749, -122.4194, -8.0, "mean", ayanamsa_name="lahiri")
raman, *_ = compute_chart_data(1955, 2, 24, 19, 15, 37.7749, -122.4194, -8.0, "mean", ayanamsa_name="raman")
assert lahiri["planets"]["Sun"]["degree_raw"] != pytest.approx(raman["planets"]["Sun"]["degree_raw"], abs=0.2)
lahiri_t = cmd_transit(SimpleNamespace(year=2026, month=10, day=5, tz=-8.0, ayanamsa="lahiri", node_mode="mean", planet=None, retrograde_calendar=False))
raman_t = cmd_transit(SimpleNamespace(year=2026, month=10, day=5, tz=-8.0, ayanamsa="raman", node_mode="mean", planet=None, retrograde_calendar=False))
assert lahiri_t["planets"]["Sun"]["degree_raw"] != pytest.approx(raman_t["planets"]["Sun"]["degree_raw"], abs=0.2)
def test_bhava_chalit_uses_birth_second():
zero = cmd_bhava_chalit(_birth("lahiri", 0))
later = cmd_bhava_chalit(_birth("lahiri", 40))
assert abs(later["ascendant_lon"] - zero["ascendant_lon"]) > 0.05
def test_import_bootstrap_does_not_clobber_explicit_mode():
probe = """
import sys
sys.path.insert(0, "scripts")
import ayanamsa_utils as au
au.apply_ayanamsa("lahiri")
import jyotish_engine
assert au.ACTIVE_AYANAMSA_NAME == "lahiri", au.ACTIVE_AYANAMSA_NAME
print("OK")
"""
proc = subprocess.run([sys.executable, "-c", probe], capture_output=True, text=True, cwd=str(__import__("pathlib").Path(__file__).resolve().parents[1]))
assert proc.returncode == 0, proc.stderr[-2000:]
assert proc.stdout.strip().endswith("OK")