fix(nakshatra): keep the first mahadasha's antardashas on the full clock
Upstream 8643bc9a. The nakshatra breakdown was restarting the nine antardashas from the birth remainder, so mid-Ashwini reported Ketu instead of Rahu. Reuse cmd_dasha and clip to birth. BUG-1380. Co-Authored-By: Grok (agent loop) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Grok (agent loop)
Cursor
parent
e7bf60c1f2
commit
68762ccec5
@@ -36488,3 +36488,18 @@
|
||||
- 相关记录:BUG-1361、BUG-1362。
|
||||
- 复发自:无。
|
||||
- 修复版本:分支 `codex/beta-safety-net-20261014`。本机没有装到生产主机,也没有点生产按钮。
|
||||
|
||||
## BUG-1380 | 星宿拆解把第一大运剩余年数重新切成九段小运
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现 / 最近更新:2026-10-17 / 2026-10-18
|
||||
- 来源:issue #80,任务书 `docs/tasks/TASK-upstream-sync8-20261017.md` T1。上游 `8643bc9a`。检索「first_remaining」「antardasha_full」,没有更早的同类记录。BUG-1268 修了 `cmd_dasha` 的同一类错误,这条拆解函数当时没跟上。
|
||||
- 现象:月亮在 Ashwini 中点时,出生当下的小运被报成 Ketu。按完整大运时钟应为 Rahu。
|
||||
- 触发条件:`calc_nakshatra_dasha_breakdown` 把第一大运的剩余年数交给按整段比例切分的小运函数。完整报告的星宿拆解走这条路径。
|
||||
- 根因:剩余段被当成一段新的完整大运,九段小运从大运主星重新起算。
|
||||
- 修复:小运改走 `cmd_dasha` 已有时间线:从完整大运真实起点排,再裁到出生之后。缺本命经度的行星不再用 0° 填星宿。
|
||||
- 验证:`tests/test_nakshatra_native_dasha_clock.py` 的 Ashwini 中点用例通过,当前小运为 Rahu,起点裁在出生日时。
|
||||
- 防复发:上述测试。不得再把第一大运余额交给「按余额重切九段」的函数。
|
||||
- 相关记录:BUG-1268。
|
||||
- 复发自:无。
|
||||
- 修复版本:分支 `codex/upstream-sync8-20261017`。上游提交 `8643bc9a`。
|
||||
|
||||
+43
-96
@@ -304,13 +304,6 @@ def _calc_ashtottari_antardasha(md_lord: str, md_years: float) -> List[Dict]:
|
||||
# Vimshottari Nakshatra-level 拆解
|
||||
# ============================================================================
|
||||
|
||||
# Vimshottari Dasha 顺序(9星循环)
|
||||
try:
|
||||
from dasha_common import VIMSHOTTARI_ORDER, VIMSHOTTARI_YEARS
|
||||
except ModuleNotFoundError: # pragma: no cover - package import
|
||||
from scripts.dasha_common import VIMSHOTTARI_ORDER, VIMSHOTTARI_YEARS
|
||||
|
||||
|
||||
def calc_nakshatra_dasha_breakdown(
|
||||
moon_longitude: float,
|
||||
planet_lons: Dict[str, float],
|
||||
@@ -338,87 +331,58 @@ def calc_nakshatra_dasha_breakdown(
|
||||
moon_nak_data = find_nakshatra(moon_longitude)
|
||||
moon_nak_idx = moon_nak_data['nakshatra_idx']
|
||||
|
||||
# 计算 Vimshottari 起始
|
||||
nak = find_nakshatra(moon_longitude)
|
||||
nak_idx = nak['nakshatra_idx']
|
||||
nak_span = 360.0 / 27
|
||||
nak_start = nak_idx * nak_span
|
||||
deg_in_nak = moon_longitude - nak_start
|
||||
used_ratio = deg_in_nak / nak_span
|
||||
|
||||
# 起始守护星和剩余年数
|
||||
start_lord = NAK_LORDS[nak_idx]
|
||||
start_total = VIMSHOTTARI_YEARS.get(start_lord, 7)
|
||||
first_remaining = start_total * (1 - used_ratio)
|
||||
|
||||
# 构建大运序列
|
||||
start_idx = VIMSHOTTARI_ORDER.index(start_lord)
|
||||
sequence = []
|
||||
cumulative = 0.0
|
||||
|
||||
# 首个(剩余)大运
|
||||
sequence.append({
|
||||
'lord': start_lord,
|
||||
'years': round(first_remaining, 4),
|
||||
'cumulative_from': 0.0,
|
||||
'is_first': True,
|
||||
})
|
||||
cumulative += first_remaining
|
||||
|
||||
idx = (start_idx + 1) % 9
|
||||
for _ in range(15): # 足够覆盖到120岁
|
||||
lord = VIMSHOTTARI_ORDER[idx]
|
||||
years = VIMSHOTTARI_YEARS[lord]
|
||||
sequence.append({
|
||||
'lord': lord,
|
||||
'years': round(years, 4),
|
||||
'cumulative_from': round(cumulative, 4),
|
||||
'is_first': False,
|
||||
})
|
||||
cumulative += years
|
||||
idx = (idx + 1) % 9
|
||||
if cumulative > 120:
|
||||
break
|
||||
|
||||
# 定位当前大运
|
||||
current_md = None
|
||||
current_md_idx = -1
|
||||
for i, item in enumerate(sequence):
|
||||
if item['cumulative_from'] <= age < (item['cumulative_from'] + item['years']):
|
||||
current_md = item
|
||||
current_md_idx = i
|
||||
break
|
||||
|
||||
# 小运沿用 cmd_dasha:从完整大运真实起点排,再裁到出生之后。
|
||||
# 把第一大运的剩余年数当成完整大运会把早期小运整段重切(BUG-1380)。
|
||||
from types import SimpleNamespace
|
||||
try:
|
||||
from jyotish_engine import cmd_dasha
|
||||
except ImportError:
|
||||
from scripts.jyotish_engine import cmd_dasha
|
||||
if not math.isfinite(age) or age < 0:
|
||||
raise ValueError('age must be finite and nonnegative')
|
||||
birth = datetime.fromisoformat(birth_date_str)
|
||||
reference = birth + timedelta(days=age * 365.25)
|
||||
packet = cmd_dasha(SimpleNamespace(
|
||||
moon_lon=moon_longitude, nakshatra=None, pada=None,
|
||||
birthdate=birth.date().isoformat(), year=birth.year, month=birth.month,
|
||||
day=birth.day, hour=birth.hour, minute=birth.minute, second=birth.second,
|
||||
lat=None, lon=None, tz=0, today=reference.date().isoformat(),
|
||||
dasha_year_days=365.25))
|
||||
current_md = next((row for row in packet['timeline']
|
||||
if datetime.fromisoformat(row['start_datetime']) <= reference
|
||||
< datetime.fromisoformat(row['end_datetime'])), None)
|
||||
if current_md is None:
|
||||
return {'error': f'无法定位年龄 {age} 在 Vimshottari 序列中'}
|
||||
|
||||
md_lord = current_md['lord']
|
||||
md_years = current_md['years']
|
||||
md_start_age = current_md['cumulative_from']
|
||||
years_in_md = age - md_start_age
|
||||
md_start = datetime.fromisoformat(current_md['start_datetime'])
|
||||
md_end = datetime.fromisoformat(current_md['end_datetime'])
|
||||
md_years = (md_end - md_start).total_seconds() / (86400 * 365.25)
|
||||
md_start_age = (md_start - birth).total_seconds() / (86400 * 365.25)
|
||||
years_in_md = (reference - md_start).total_seconds() / (86400 * 365.25)
|
||||
|
||||
# 大运守护星的 Nakshatra
|
||||
md_lord_nak = None
|
||||
if md_lord in planet_lons:
|
||||
md_lord_nak = find_nakshatra(planet_lons[md_lord])
|
||||
|
||||
# 计算当前 Antardasha
|
||||
antardasas = _calc_vimshottari_antardasha_full(md_lord, md_years)
|
||||
|
||||
# 定位当前 Antardasha
|
||||
ad_cumulative = 0.0
|
||||
antardasas = []
|
||||
current_ad = None
|
||||
for ad in antardasas:
|
||||
ad_end = ad_cumulative + ad['years']
|
||||
if ad_cumulative <= years_in_md < ad_end:
|
||||
for ad in current_md['antardasha_timeline']:
|
||||
ad_start = datetime.fromisoformat(ad['start_datetime'])
|
||||
ad_end = datetime.fromisoformat(ad['end_datetime'])
|
||||
years = (ad_end - ad_start).total_seconds() / (86400 * 365.25)
|
||||
antardasas.append({'lord': ad['lord'], 'years': years})
|
||||
if ad_start <= reference < ad_end:
|
||||
nominal_start = datetime.fromisoformat(ad['unclipped_start_datetime'])
|
||||
span = (ad_end - nominal_start).total_seconds()
|
||||
current_ad = {
|
||||
'lord': ad['lord'],
|
||||
'years': ad['years'],
|
||||
'progress': (years_in_md - ad_cumulative) / ad['years'] if ad['years'] > 0 else 0,
|
||||
'remaining': round(ad_end - years_in_md, 4),
|
||||
'years': round(years, 4),
|
||||
'progress': (reference - nominal_start).total_seconds() / span if span else 0,
|
||||
'remaining': round((ad_end - reference).total_seconds() / (86400 * 365.25), 4),
|
||||
'start_datetime': ad['start_datetime'],
|
||||
'end_datetime': ad['end_datetime'],
|
||||
}
|
||||
break
|
||||
ad_cumulative = ad_end
|
||||
|
||||
# 当前 Antardasha 守护星的 Nakshatra
|
||||
ad_lord_nak = None
|
||||
@@ -451,8 +415,8 @@ def calc_nakshatra_dasha_breakdown(
|
||||
md_years_per_pada = md_years_per_nak / 4
|
||||
|
||||
for ad in antardasas:
|
||||
if md_years_per_nak > 0:
|
||||
ad_nak = find_nakshatra(planet_lons.get(ad['lord'], 0))
|
||||
if md_years_per_nak > 0 and ad['lord'] in planet_lons:
|
||||
ad_nak = find_nakshatra(planet_lons[ad['lord']])
|
||||
pada_windows.append({
|
||||
'period': f"AD {ad['lord']}",
|
||||
'nakshatra': ad_nak['nakshatra'],
|
||||
@@ -470,6 +434,8 @@ def calc_nakshatra_dasha_breakdown(
|
||||
|
||||
return {
|
||||
'system': 'Vimshottari Nakshatra-level Breakdown',
|
||||
'period_source': 'jyotish_engine.cmd_dasha native timeline',
|
||||
'reference_datetime': reference.isoformat(timespec='seconds'),
|
||||
'moon_nakshatra': moon_nak_data['nakshatra'],
|
||||
'moon_nakshatra_idx': moon_nak_idx,
|
||||
'moon_pada': moon_nak_data['pada'],
|
||||
@@ -490,25 +456,6 @@ def calc_nakshatra_dasha_breakdown(
|
||||
}
|
||||
|
||||
|
||||
def _calc_vimshottari_antardasha_full(md_lord: str, md_years: float) -> List[Dict]:
|
||||
"""计算完整的 Vimshottari Antardasha(9个周期)"""
|
||||
md_start_idx = VIMSHOTTARI_ORDER.index(md_lord)
|
||||
total_years = sum(VIMSHOTTARI_YEARS.values()) # 120
|
||||
|
||||
result = []
|
||||
for i in range(9):
|
||||
ad_lord = VIMSHOTTARI_ORDER[(md_start_idx + i) % 9]
|
||||
ad_years_total = VIMSHOTTARI_YEARS[ad_lord]
|
||||
ad_years = md_years * ad_years_total / total_years
|
||||
result.append({
|
||||
'lord': ad_lord,
|
||||
'years': round(ad_years, 4),
|
||||
'months': round(ad_years * 12, 1),
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Nakshatra Transit Overlay(过境星宿叠加大运)
|
||||
# ============================================================================
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
"""The interpretation helper must consume the real, unclipped MD/AD clock."""
|
||||
from scripts.nakshatra_dasha import calc_nakshatra_dasha_breakdown
|
||||
|
||||
|
||||
def test_first_remaining_md_is_not_repartitioned_into_new_ads():
|
||||
# Mid-Ashwini: half of Ketu's seven years elapsed. The active AD is Rahu,
|
||||
# not Ketu; the helper must not restart the first parent's child clock.
|
||||
result = calc_nakshatra_dasha_breakdown(
|
||||
20 / 3, {'Ketu': 1., 'Rahu': 181.},
|
||||
'2000-01-01T12:34:56', 0.)
|
||||
assert result['mahadasha']['lord'] == 'Ketu'
|
||||
assert result['antardasha']['lord'] == 'Rahu'
|
||||
assert result['antardasha']['start_datetime'] == '2000-01-01T12:34:56'
|
||||
assert result['reference_datetime'] == '2000-01-01T12:34:56'
|
||||
assert result['antardasha']['progress'] > 0
|
||||
# Missing natal positions are never silently fabricated as 0 degrees.
|
||||
assert {row['period'] for row in result['pada_windows']} <= {'AD Ketu', 'AD Rahu'}
|
||||
Reference in New Issue
Block a user