fix(ayanamsa): keep a caller-selected mode across import and transit paths (BUG-1269, upstream 23be1807)
This commit is contained in:
@@ -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
|
||||
- 复发自:无
|
||||
- 修复版本:本分支待发布
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user