Files
Jyotisha/scripts/nakshatra_dasha.py
2026-06-04 12:19:11 +08:00

693 lines
23 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
星宿大运推演系统 v1.0 — Nakshatra Dasha Engine
支持:
- Ashtottari Dasha108年星宿大运,条件性系统)
- Vimshottari Nakshatra-level 详细拆解
- Nakshatra Transit Overlay(过境星宿叠加大运)
- Nakshatra-based 时间窗口推演
- 综合星宿大运报告
设计原则:
- Ashtottari 作为 Vimshottari 的补充系统(条件:Rahu 不在 Kendra 时适用)
- Nakshatra-level 拆解提供当前大运的星宿层面的精细时间线
- Transit Overlay 将过境星宿叠加上大运周期,提供精确时间窗口
"""
from typing import Dict, List, Optional, Tuple
import math
from datetime import datetime, timedelta
# 从 nakshatra_advanced 导入需要的数据
try:
from nakshatra_advanced import (
NAK_LIST, NAK_NAMES, NAK_LORDS, NAK_YEARS,
find_nakshatra, calc_tara_bala,
)
except ImportError:
import sys, os
sys.path.insert(0, os.path.dirname(__file__))
from nakshatra_advanced import (
NAK_LIST, NAK_NAMES, NAK_LORDS, NAK_YEARS,
find_nakshatra, calc_tara_bala,
)
# ============================================================================
# Ashtottari Dasha 数据表
# ============================================================================
# Ashtottari 行星年数(108年制)
ASHTOTTARI_YEARS = {
'Sun': 6, 'Moon': 15, 'Mars': 8, 'Mercury': 17,
'Saturn': 10, 'Jupiter': 19, 'Rahu': 12, 'Venus': 21,
}
ASHTOTTARI_TOTAL = sum(ASHTOTTARI_YEARS.values()) # 108
# Ashtottari 大运顺序(固定循环)
ASHTOTTARI_ORDER = ['Sun', 'Moon', 'Mars', 'Mercury', 'Saturn', 'Jupiter', 'Rahu', 'Venus']
# Nakshatra → Ashtottari 起始守护星映射
# 按 Vimshottari 星宿守护星对应 Ashtottari 起始点
# Ketu 星宿 → 不作为 Ashtottari 起点,回溯到上一个 Venus
NAK_TO_ASHTOTTARI_START = {
'Ashwini': 'Venus', # Ketu nakshatra → 上一周期末尾 Venus
'Bharani': 'Venus', # Venus
'Krittika': 'Sun', # Sun
'Rohini': 'Moon', # Moon
'Mrigashira': 'Mars', # Mars
'Ardra': 'Rahu', # Rahu
'Punarvasu': 'Jupiter', # Jupiter
'Pushya': 'Saturn', # Saturn
'Ashlesha': 'Mercury', # Mercury
'Magha': 'Venus', # Ketu → Venus
'Purva Phalguni': 'Venus', # Venus
'Uttara Phalguni': 'Sun', # Sun
'Hasta': 'Moon', # Moon
'Chitra': 'Mars', # Mars
'Swati': 'Rahu', # Rahu
'Vishakha': 'Jupiter', # Jupiter
'Anuradha': 'Saturn', # Saturn
'Jyeshtha': 'Mercury', # Mercury
'Mula': 'Venus', # Ketu → Venus
'Purva Ashadha': 'Venus', # Venus
'Uttara Ashadha': 'Sun', # Sun
'Shravana': 'Moon', # Moon
'Dhanishta': 'Mars', # Mars
'Shatabhisha': 'Rahu', # Rahu
'Purva Bhadrapada': 'Jupiter', # Jupiter
'Uttara Bhadrapada': 'Saturn', # Saturn
'Revati': 'Mercury', # Mercury
}
# ============================================================================
# 核心计算函数
# ============================================================================
def is_ashtottari_applicable(rahu_house: int) -> bool:
"""
判断 Ashtottari Dasha 是否适用
传统规则:当 Rahu 不在 Kendra1/4/7/10宫)时,Ashtottari 优先。
若 Rahu 在 Kendra,则使用 Vimshottari。
参数:
rahu_house: Rahu 所在宫位 (1-12)
返回:
bool: True = Ashtottari 适用
"""
kendras = {1, 4, 7, 10}
return rahu_house not in kendras
def calc_ashtottari_dasha(
moon_longitude: float,
birth_date_str: str,
rahu_house: int,
) -> Dict:
"""
计算 Ashtottari Dasha 108年大运序列 — v1.0
参数:
moon_longitude: 月亮恒星黄道经度
birth_date_str: 出生日期 YYYY-MM-DD
rahu_house: Rahu 所在宫位 (1-12)
返回:
dict: {
'applicable': bool,
'reason': str,
'starting_lord': str,
'sequence': [...],
'total_years': 108,
}
"""
applicable = is_ashtottari_applicable(rahu_house)
# 月亮星宿信息
nak_data = find_nakshatra(moon_longitude)
moon_nak = nak_data['nakshatra']
moon_nak_idx = nak_data['nakshatra_idx']
# 起始守护星
starting_lord = NAK_TO_ASHTOTTARI_START.get(moon_nak, 'Sun')
# 计算起始点的已用/剩余比例
nak_span = 360.0 / 27
nak_start = moon_nak_idx * nak_span
deg_in_nak = moon_longitude - nak_start
used_ratio = deg_in_nak / nak_span
remaining_ratio = 1 - used_ratio
start_lord_years = ASHTOTTARI_YEARS.get(starting_lord, 6)
first_remaining = start_lord_years * remaining_ratio
# 构建大运序列
start_idx = ASHTOTTARI_ORDER.index(starting_lord)
sequence = []
cumulative = 0.0
# 首个大运的剩余部分
sequence.append({
'lord': starting_lord,
'full_years': start_lord_years,
'remaining_years': round(first_remaining, 4),
'elapsed_years': round(start_lord_years - first_remaining, 4),
'cumulative_from_birth': 0.0,
'cumulative_to_birth': round(first_remaining, 4),
'is_first': True,
})
cumulative += first_remaining
# 后续完整大运循环
idx = (start_idx + 1) % 8
cycle_count = 1
while cumulative < 120: # 超出人生预期范围
lord = ASHTOTTARI_ORDER[idx]
years = ASHTOTTARI_YEARS[lord]
sequence.append({
'lord': lord,
'full_years': years,
'remaining_years': years,
'elapsed_years': 0,
'cumulative_from_birth': round(cumulative, 4),
'cumulative_to_birth': round(cumulative + years, 4),
'is_first': False,
})
cumulative += years
idx = (idx + 1) % 8
cycle_count += 1
if cycle_count > 20: # 安全上限
break
# 出生日期
try:
birth_date = datetime.strptime(birth_date_str, '%Y-%m-%d')
except:
birth_date = datetime(2000, 1, 1)
# 为大运添加日期范围
for item in sequence:
from_years = item['cumulative_from_birth']
to_years = item['cumulative_to_birth']
item['date_from'] = _add_years_to_date(birth_date, from_years)
item['date_to'] = _add_years_to_date(birth_date, to_years)
item['age_from'] = round(from_years, 2)
item['age_to'] = round(to_years, 2)
if applicable:
reason = f"Rahu 在第{rahu_house}宫,不在 Kendra(1/4/7/10)Ashtottari 适用"
else:
reason = f"Rahu 在第{rahu_house}宫,在 Kendra,建议使用 Vimshottari"
return {
'system': 'Ashtottari Dasha108年星宿大运)',
'applicable': applicable,
'reason': reason,
'rahu_house': rahu_house,
'moon_nakshatra': moon_nak,
'starting_lord': starting_lord,
'total_years': ASHTOTTARI_TOTAL,
'first_remaining_years': round(first_remaining, 4),
'sequence': sequence,
'note': ('Ashtottari 作为 Vimshottari 的补充系统,'
'当 Rahu 不在 Kendra 时提供替代视角。'
'两者各有侧重:Vimshottari 偏"心识"Ashtottari 偏"命运安排"'),
}
def calc_current_ashtottari(
ashtottari_data: Dict,
age: float,
) -> Dict:
"""
从 Ashtottari 序列中定位当前年龄的大运
返回:
dict: {lord, years_remaining, progress, sub_periods, ...}
"""
if not ashtottari_data.get('applicable', False):
return {'applicable': False, 'reason': ashtottari_data.get('reason', '')}
sequence = ashtottari_data.get('sequence', [])
current = None
next_item = None
for i, item in enumerate(sequence):
if item['cumulative_from_birth'] <= age < item['cumulative_to_birth']:
current = item
if i + 1 < len(sequence):
next_item = sequence[i + 1]
break
if current is None:
return {'error': f'无法定位年龄 {age} 在 Ashtottari 序列中'}
years_elapsed_in_md = age - current['cumulative_from_birth']
years_remaining = current['cumulative_to_birth'] - age
progress = years_elapsed_in_md / current['remaining_years'] if current['remaining_years'] > 0 else 0
# 计算 Antardasha 子周期(简化:按比例分配)
antardasha_sequence = _calc_ashtottari_antardasha(current['lord'], current['remaining_years'])
# 定位当前 Antardasha
ad_cumulative = 0
current_ad = None
for ad in antardasha_sequence:
ad_end = ad_cumulative + ad['years']
if ad_cumulative <= years_elapsed_in_md < ad_end:
current_ad = {
**ad,
'progress_in_ad': (years_elapsed_in_md - ad_cumulative) / ad['years'] if ad['years'] > 0 else 0,
'remaining_years': round(ad_end - years_elapsed_in_md, 4),
}
break
ad_cumulative = ad_end
return {
'applicable': True,
'current_mahadasha': current['lord'],
'mahadasha_progress': round(progress, 4),
'years_elapsed_in_md': round(years_elapsed_in_md, 4),
'years_remaining_in_md': round(years_remaining, 4),
'age_from': current['age_from'],
'age_to': current['age_to'],
'next_mahadasha': next_item['lord'] if next_item else None,
'antardasha_sequence': antardasha_sequence,
'current_antardasha': current_ad,
}
def _calc_ashtottari_antardasha(md_lord: str, md_years: float) -> List[Dict]:
"""计算 Ashtottari Antardasha 子周期"""
md_start_idx = ASHTOTTARI_ORDER.index(md_lord)
total = sum(ASHTOTTARI_YEARS.values())
result = []
for i in range(8):
ad_lord = ASHTOTTARI_ORDER[(md_start_idx + i) % 8]
ad_years_total = ASHTOTTARI_YEARS[ad_lord]
# 按比例缩放
ad_years_in_md = md_years * ad_years_total / total
result.append({
'lord': ad_lord,
'years': round(ad_years_in_md, 4),
'months': round(ad_years_in_md * 12, 1),
})
return result
# ============================================================================
# Vimshottari Nakshatra-level 拆解
# ============================================================================
# Vimshottari Dasha 顺序(9星循环)
VIMSHOTTARI_ORDER = ['Ketu', 'Venus', 'Sun', 'Moon', 'Mars', 'Rahu', 'Jupiter', 'Saturn', 'Mercury']
VIMSHOTTARI_YEARS = {'Ketu': 7, 'Venus': 20, 'Sun': 6, 'Moon': 10, 'Mars': 7, 'Rahu': 18, 'Jupiter': 16, 'Saturn': 19, 'Mercury': 17}
def calc_nakshatra_dasha_breakdown(
moon_longitude: float,
planet_lons: Dict[str, float],
birth_date_str: str,
age: float,
) -> Dict:
"""
Vimshottari Nakshatra-level 详细拆解 — v1.0
在当前 Vimshottari 大运/小运框架下,提供:
1. 大运守护星的 Nakshatra 位置及其 Tara 关系
2. 小运守护星的 Nakshatra 位置
3. Nakshatra Pada 层面的时间窗口
4. 月亮星宿到各大运/小运守护星星宿的 Tara Bala
参数:
moon_longitude: 月亮恒星黄道经度
planet_lons: {行星名: 恒星经度}
birth_date_str: 出生日期 YYYY-MM-DD
age: 当前年龄(岁)
返回:
dict: 星宿层面的大运拆解
"""
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
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
# 大运守护星的 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
current_ad = None
for ad in antardasas:
ad_end = ad_cumulative + ad['years']
if ad_cumulative <= years_in_md < ad_end:
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),
}
break
ad_cumulative = ad_end
# 当前 Antardasha 守护星的 Nakshatra
ad_lord_nak = None
if current_ad and current_ad['lord'] in planet_lons:
ad_lord_nak = find_nakshatra(planet_lons[current_ad['lord']])
# Nakshatra Tara 关系分析
tara_analysis = {}
if md_lord_nak:
tara_analysis['mahadasha_lord'] = {
'planet': md_lord,
'nakshatra': md_lord_nak['nakshatra'],
'pada': md_lord_nak['pada'],
'tara_to_moon': calc_tara_bala(moon_nak_idx, md_lord_nak['nakshatra_idx']),
}
if ad_lord_nak and current_ad:
tara_analysis['antardasha_lord'] = {
'planet': current_ad['lord'],
'nakshatra': ad_lord_nak['nakshatra'],
'pada': ad_lord_nak['pada'],
'tara_to_moon': calc_tara_bala(moon_nak_idx, ad_lord_nak['nakshatra_idx']),
}
# 星宿层面的时间窗口(Pada-level 细分)
pada_windows = []
if md_lord_nak and current_ad:
nak_span_deg = 360.0 / 27
pada_span_deg = nak_span_deg / 4
md_years_per_nak = md_years / 9 # 每个 Antardasha 时长近似
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))
pada_windows.append({
'period': f"AD {ad['lord']}",
'nakshatra': ad_nak['nakshatra'],
'pada': ad_nak['pada'],
'years': round(ad['years'], 4),
'tara': calc_tara_bala(moon_nak_idx, ad_nak['nakshatra_idx']),
})
# 构建返回
birth_date = None
try:
birth_date = datetime.strptime(birth_date_str, '%Y-%m-%d')
except:
pass
return {
'system': 'Vimshottari Nakshatra-level Breakdown',
'moon_nakshatra': moon_nak_data['nakshatra'],
'moon_nakshatra_idx': moon_nak_idx,
'moon_pada': moon_nak_data['pada'],
'current_age': age,
'mahadasha': {
'lord': md_lord,
'years_total': round(md_years, 4),
'age_range': f"{round(md_start_age, 2)}-{round(md_start_age + md_years, 2)}",
'years_elapsed': round(years_in_md, 4),
'years_remaining': round(md_years - years_in_md, 4),
'progress': round(years_in_md / md_years, 4) if md_years > 0 else 0,
'nakshatra': md_lord_nak,
'tara': tara_analysis.get('mahadasha_lord', {}),
},
'antardasha': current_ad,
'tara_analysis': tara_analysis,
'pada_windows': pada_windows,
}
def _calc_vimshottari_antardasha_full(md_lord: str, md_years: float) -> List[Dict]:
"""计算完整的 Vimshottari Antardasha9个周期)"""
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(过境星宿叠加大运)
# ============================================================================
def calc_nakshatra_transit_overlay(
natal_planet_lons: Dict[str, float],
transit_planet_lons: Dict[str, float],
moon_nak_idx: int,
) -> Dict:
"""
将过境星宿叠加在本命大运框架上 — v1.0
分析每个过境行星当前所在的星宿,及其:
1. 与本命月亮星宿的 Tara 关系
2. 与本命行星的星宿距离
3. 当前星宿的 Pada 位置
4. 吉凶评估
参数:
natal_planet_lons: 本命行星经度
transit_planet_lons: 过境行星经度
moon_nak_idx: 本命月亮星宿索引
返回:
dict: 过境星宿叠加分析
"""
results = {}
for pname, t_lon in transit_planet_lons.items():
t_nak = find_nakshatra(t_lon)
tara = calc_tara_bala(moon_nak_idx, t_nak['nakshatra_idx'])
# 过境星宿是否与某本命行星的星宿重叠
same_nak_as_natal = []
n_lon = natal_planet_lons.get(pname)
if n_lon is not None:
n_nak = find_nakshatra(n_lon)
if n_nak['nakshatra_idx'] == t_nak['nakshatra_idx']:
same_nak_as_natal.append('self')
for np, nl in natal_planet_lons.items():
if np == pname:
continue
n_nak = find_nakshatra(nl)
if n_nak['nakshatra_idx'] == t_nak['nakshatra_idx']:
same_nak_as_natal.append(np)
# 评估
if tara['is_auspicious']:
transit_quality = 'favorable'
elif tara['is_dangerous']:
transit_quality = 'challenging'
else:
transit_quality = 'neutral'
if same_nak_as_natal:
transit_quality = transit_quality + '_nak_conjunction'
note = f"过境星宿与本命{'/'.join(same_nak_as_natal)}同星宿,叠加效应增强"
else:
note = ''
results[pname] = {
'transit_nakshatra': t_nak['nakshatra'],
'transit_nakshatra_lord': t_nak['nakshatra_lord'],
'transit_pada': t_nak['pada'],
'tara_to_natal_moon': tara,
'same_nakshatra_as': same_nak_as_natal if same_nak_as_natal else None,
'quality': transit_quality,
'note': note,
'gana': t_nak['gana'],
'element': t_nak['element'],
}
return results
# ============================================================================
# 综合星宿大运报告
# ============================================================================
def nakshatra_dasha_full_report(
chart_data: Dict,
birth_date_str: str,
age: float,
transit_planet_lons: Optional[Dict[str, float]] = None,
) -> Dict:
"""
综合星宿大运完整报告 — v1.0
包含:
1. Ashtottari Dasha(条件性,Rahu 不在 Kendra 时计算)
2. Vimshottari Nakshatra-level 拆解
3. Nakshatra Transit Overlay(如提供过境数据)
4. 综合时间窗口与 Tara 评估
参数:
chart_data: 星盘数据
birth_date_str: 出生日期 YYYY-MM-DD
age: 当前年龄
transit_planet_lons: 过境行星经度(可选)
返回:
dict: 完整星宿大运报告
"""
planets = chart_data.get('planets', {})
planet_lons = {}
for pn, pd in planets.items():
if isinstance(pd, dict) and 'degree' in pd:
planet_lons[pn] = pd['degree']
if not planet_lons:
return {'error': '无法获取行星经度数据'}
moon_lon = planet_lons.get('Moon', 0)
moon_nak = find_nakshatra(moon_lon)
moon_nak_idx = moon_nak['nakshatra_idx']
# 确定 Rahu 宫位
asc_idx = chart_data.get('ascendant_index', 0)
rahu_deg = None
rahu_house = 1 # 默认
if 'Rahu' in planet_lons:
rahu_deg = planet_lons['Rahu']
rahu_sign_idx = int(rahu_deg / 30) % 12
rahu_house = ((rahu_sign_idx - asc_idx) % 12) + 1
report = {}
# 1. Ashtottari Dasha
report['ashtottari'] = calc_ashtottari_dasha(moon_lon, birth_date_str, rahu_house)
if report['ashtottari']['applicable']:
report['ashtottari_current'] = calc_current_ashtottari(report['ashtottari'], age)
# 2. Vimshottari Nakshatra-level
report['vimshottari_nak_breakdown'] = calc_nakshatra_dasha_breakdown(
moon_lon, planet_lons, birth_date_str, age)
# 3. Nakshatra Transit Overlay
if transit_planet_lons:
report['transit_overlay'] = calc_nakshatra_transit_overlay(
planet_lons, transit_planet_lons, moon_nak_idx)
# 4. 汇总
md_info = report['vimshottari_nak_breakdown'].get('mahadasha', {})
ad_info = report['vimshottari_nak_breakdown'].get('antardasha', {})
summary = {
'moon_nakshatra': moon_nak['nakshatra'],
'moon_nakshatra_lord': moon_nak['nakshatra_lord'],
'moon_pada': moon_nak['pada'],
'current_age': age,
'vimshottari_md': md_info.get('lord', 'N/A'),
'vimshottari_ad': ad_info.get('lord', 'N/A') if ad_info else 'N/A',
'ashtottari_applicable': report['ashtottari']['applicable'],
}
if report['ashtottari']['applicable'] and 'ashtottari_current' in report:
ac = report['ashtottari_current']
summary['ashtottari_md'] = ac.get('current_mahadasha', 'N/A')
if ac.get('current_antardasha'):
summary['ashtottari_ad'] = ac['current_antardasha']['lord']
# Dasha 系统的 Nakshatra Tara 评估
tara_md = report['vimshottari_nak_breakdown'].get('tara_analysis', {}).get('mahadasha_lord', {})
tara_ad = report['vimshottari_nak_breakdown'].get('tara_analysis', {}).get('antardasha_lord', {})
summary['md_tara_to_moon'] = tara_md.get('tara_to_moon', {}).get('tara_cn', 'N/A') if tara_md else 'N/A'
summary['ad_tara_to_moon'] = tara_ad.get('tara_to_moon', {}).get('tara_cn', 'N/A') if tara_ad else 'N/A'
report['summary'] = summary
return report
# ============================================================================
# 工具函数
# ============================================================================
def _add_years_to_date(dt: datetime, years: float) -> str:
"""日期 + 年数 → ISO 日期字符串"""
total_days = years * 365.25
new_dt = dt + timedelta(days=total_days)
return new_dt.strftime('%Y-%m-%d')