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:
jesse-ux
2026-10-18 16:08:31 +08:00
co-authored by Grok (agent loop) Cursor
parent e7bf60c1f2
commit 68762ccec5
3 changed files with 75 additions and 96 deletions
+43 -96
View File
@@ -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(过境星宿叠加大运)
# ============================================================================