Enhance Jyotish precision techniques and tests

This commit is contained in:
732642856
2026-06-13 15:06:11 +08:00
parent ee82af5ecb
commit 3f355de1db
37 changed files with 7162 additions and 718 deletions
+492 -116
View File
@@ -1,138 +1,514 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Bhava Chalit 宫位系统 v1.0
MIT License — 基于 dashaflow (adarshj322) 的等宫制实现
Bhava Chalit (不等宫边界调整) 计算模块 v1.0
Bhava Chalit = 等分宫制,从上升中点(Lagna - 15°)开始,每宫30度
与整宫制(Whole Sign)的区别:靠近星座边界的行星可能跨宫
Bhava Chalit 是 JHora 和 PyJHora 的标准功能。它根据实际宫位边界
(非等宫30°)调整行星的宫位归属。
核心概念:
- Bhava Madhya: 宫位中点(宫头/cusp
- Bhava Sandhi: 宫位边界(相邻两宫头的中点)
- 行星根据落在哪两个 Sandhi 边界之间来确定 Bhava 宫位
- Bhava 宫位可能与 Rashi(星座/整宫)宫位不同
支持的宫位制:
- equal: 等宫制(每宫30°,从上升点起算)
- whole_sign: 整宫制(星座=宫位)
- sripati: SripatiPorphyry 变体,吠陀标准)
- porphyry: Porphyry(四象限三等分)
- placidus: Placidus(时间等分,西方最常用)
- koch: Koch(时间等分,西方流行)
"""
from typing import Dict, List, Tuple
SIGNS = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo',
'Libra','Scorpio','Sagittarius','Capricorn','Aquarius','Pisces']
from typing import Dict, List, Optional
SIGNS = ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo',
'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces']
SIGNS_CN = {'Aries': '白羊座', 'Taurus': '金牛座', 'Gemini': '双子座',
'Cancer': '巨蟹座', 'Leo': '狮子座', 'Virgo': '处女座',
'Libra': '天秤座', 'Scorpio': '天蝎座', 'Sagittarius': '射手座',
'Capricorn': '摩羯座', 'Aquarius': '水瓶座', 'Pisces': '双鱼座'}
def calculate_bhava_chalit(asc_lon: float, raw_planets: Dict) -> Dict:
"""
Bhava Chalit 计算(等宫制从上升中点划分)
规则:
- 第1宫中点(Bhava Madhya) = Lagna 经度
- 第1宫起点 = Lagna - 15°
- 每宫跨度 = 30°
- 行星的Bhava宫位可能与其Rashi(整宫)宫位不同
Args:
asc_lon: 上升点黄道经度 (0-360)
raw_planets: {planet_name: {"lon": float, ...}}
每颗行星需要lon(黄道经度)和sign_idx(星座索引)
Returns:
dict: {planet_name: {"bhava_house": int, "rashi_house": int, "shifted": bool}}
"""
cusp_start = (asc_lon - 15.0) % 360.0
asc_sign_idx = int(asc_lon / 30) % 12
def _norm(lon: float) -> float:
"""归一化到 [0, 360)"""
return lon % 360.0
result = {}
for name, rp in raw_planets.items():
planet_lon = rp.get("lon", 0)
# 整宫制
sign_idx = rp.get("sign_idx", int(planet_lon / 30) % 12)
rashi_house = ((sign_idx - asc_sign_idx) % 12) + 1
# Bhava 宫位(等宫制)
diff = (planet_lon - cusp_start) % 360.0
bhava_house = int(diff / 30.0) + 1
if bhava_house > 12:
bhava_house = 12
result[name] = {
"bhava_house": bhava_house,
"rashi_house": rashi_house,
"shifted": bhava_house != rashi_house,
def _sign_idx(lon: float) -> int:
"""黄经对应的星座索引 (0-11)"""
return int(_norm(lon) / 30) % 12
def _angular_dist(a: float, b: float) -> float:
"""从 a 到 b 的正向角距离 [0, 360)"""
return _norm(b - a)
class BhavaChalitCalculator:
"""Bhava Chalit (不等宫边界调整) 计算器。"""
HOUSE_SYSTEMS = {
'equal': 'Equal house (30° each)',
'placidus': 'Placidus (time-based, most common Western)',
'porphyry': 'Porphyry (quadrant trisection)',
'sripati': 'Sripati (Vedic standard, Porphyry variant)',
'whole_sign': 'Whole Sign (Rashi = House)',
'koch': 'Koch (time-based, popular in Western)',
}
# swisseph house system codes
_SWE_HSYS = {
'placidus': b'P',
'koch': b'K',
'porphyry': b'O',
'sripati': b'R', # Sripati uses Regiomontanus approximation in swe
}
def __init__(self):
self._has_swe = False
try:
import swisseph as swe
self._has_swe = True
self._swe = swe
except ImportError:
pass
# ------------------------------------------------------------------
# 核心算法: 宫头计算
# ------------------------------------------------------------------
def calculate_cusps(self, asc_lon: float, mc_lon: float,
house_system: str = 'sripati',
jd: float = None, lat: float = None,
lon: float = None) -> List[float]:
"""计算12个宫头(Bhava Madhya)。
Parameters
----------
asc_lon : float 上升点黄经 (sidereal)
mc_lon : float 天顶黄经 (sidereal), 仅 sripati/porphyry 需要
house_system : str 宫位制
jd : float 儒略日, swisseph 宫位制需要
lat : float 纬度, swisseph 宫位制需要
lon : float 经度, swisseph 宫位制需要
Returns
-------
list[float] 12个宫头黄经, 索引0=第1宫, 索引1=第2宫, ...
"""
hs = house_system.lower()
if hs not in self.HOUSE_SYSTEMS:
raise ValueError(f"不支持的宫位制: {house_system}"
f"可选: {list(self.HOUSE_SYSTEMS.keys())}")
if hs == 'equal':
return self._cusps_equal(asc_lon)
elif hs == 'whole_sign':
return self._cusps_whole_sign(asc_lon)
elif hs == 'sripati':
return self._cusps_sripati(asc_lon, mc_lon)
elif hs == 'porphyry':
return self._cusps_porphyry(asc_lon, mc_lon)
elif hs in ('placidus', 'koch'):
return self._cusps_swe(asc_lon, hs, jd, lat, lon)
else:
return self._cusps_equal(asc_lon)
def _cusps_equal(self, asc_lon: float) -> List[float]:
"""等宫制: 每宫30°, 从上升点起算。"""
return [_norm(asc_lon + i * 30) for i in range(12)]
def _cusps_whole_sign(self, asc_lon: float) -> List[float]:
"""整宫制: 每宫=一个星座, 宫头在星座中点 (Bhava Madhya)。
在 Jyotish 中, 宫头 (cusp) 是 Bhava Madhya (宫位中点)。
Whole Sign 下, 中点在 15° of each sign。
Sandhi (边界) 在星座0°, 确保不会出现 Rashi/Bhava 偏移。
"""
asc_sign_start = int(asc_lon / 30) * 30
# cusp = midpoint of each sign = sign_start + 15°
return [_norm(asc_sign_start + i * 30 + 15) for i in range(12)]
def _cusps_porphyry(self, asc_lon: float, mc_lon: float) -> List[float]:
"""Porphyry: 四象限三等分。
四个象限:
Q1: Asc → MC (顺时针, 即 MC 在 Asc 之前/之上)
Q2: MC → Desc (7宫 = Asc+180°)
Q3: Desc → IC (4宫 = MC+180°)
Q4: IC → Asc
每个象限三等分 → 每象限3个宫。
"""
desc_lon = _norm(asc_lon + 180)
ic_lon = _norm(mc_lon + 180)
cusps = [0.0] * 12
# 1宫 = Asc, 10宫 = MC, 7宫 = Desc, 4宫 = IC
cusps[0] = _norm(asc_lon)
cusps[9] = _norm(mc_lon)
cusps[6] = _norm(desc_lon)
cusps[3] = _norm(ic_lon)
# Q1: Asc → MC (houses 12, 11, 10-cusp)
# 在黄道上, MC 通常在 Asc 的顺时针方向 (数值上 MC < Asc 或绕过360)
# 行星沿黄道逆时针运行, 但宫位顺时针排列
# Q1 从 MC 到 Asc (顺时针) 包含 house 11, 12
# 但实际上 Porphyry 的象限划分是:
# Q1 (houses 10,11,12): MC → Asc
# Q2 (houses 7,8,9): Desc → MC
# Q3 (houses 4,5,6): IC → Desc
# Q4 (houses 1,2,3): Asc → IC
# 注意: 在印度占星中, 宫位顺时针, 2宫在1宫之后
# 正确的象限划分 (JHora/Porphyry 标准):
# Q1: Asc → IC (houses 2, 3) — 1宫和4宫之间
# Q2: IC → Desc (houses 5, 6) — 4宫和7宫之间
# Q3: Desc → MC (houses 8, 9) — 7宫和10宫之间
# Q4: MC → Asc (houses 11, 12) — 10宫和1宫之间
self._trisect_quadrant(cusps, 0, 3, 1, 2) # Asc → IC: houses 2,3
self._trisect_quadrant(cusps, 3, 6, 4, 5) # IC → Desc: houses 5,6
self._trisect_quadrant(cusps, 6, 9, 7, 8) # Desc → MC: houses 8,9
self._trisect_quadrant(cusps, 9, 0, 10, 11) # MC → Asc: houses 11,12
return cusps
def _cusps_sripati(self, asc_lon: float, mc_lon: float) -> List[float]:
"""Sripati: Porphyry 变体, 吠陀标准。
与 Porphyry 相同的四象限三等分法。
Sripati 的特点是: 先用 Porphyry 算出宫头,
然后每个宫的中点 (midpoint between cusps) 才是真正的 Bhava Madhya。
但在 JHora 的实现中, Sripati 宫头就是 Porphyry 宫头,
差异仅在于 Sandhi (边界) 的计算方式。
这里采用与 JHora 一致的 Sripati 算法:
即 Porphyry 宫头 + Sandhi 在相邻 Porphyry 宫头中点。
"""
return self._cusps_porphyry(asc_lon, mc_lon)
def _trisect_quadrant(self, cusps: List[float],
start_idx: int, end_idx: int,
inner1: int, inner2: int):
"""将象限三等分, 填入两个内部宫头。
从 cusps[start_idx] 到 cusps[end_idx], 顺时针方向。
"""
start_lon = cusps[start_idx]
end_lon = cusps[end_idx]
arc = _angular_dist(start_lon, end_lon)
third = arc / 3.0
cusps[inner1] = _norm(start_lon + third)
cusps[inner2] = _norm(start_lon + 2 * third)
def _cusps_swe(self, asc_lon: float, house_system: str,
jd: float, lat: float, lon: float) -> List[float]:
"""使用 swisseph 计算宫头 (Placidus/Koch 等)。
swisseph houses/houses_ex 返回12个值 (0-indexed):
cusps[0]=H1, cusps[1]=H2, ..., cusps[11]=H12
这些是 tropical 度数, 需要减去 ayanamsa 转为 sidereal。
"""
if not self._has_swe:
raise RuntimeError(f"swisseph 未安装, 无法使用 {house_system} 宫位制")
if jd is None or lat is None or lon is None:
raise ValueError(f"{house_system} 需要 jd, lat, lon 参数")
hsys = self._SWE_HSYS.get(house_system, b'P')
cusps_swe, ascmc = self._swe.houses(jd, lat, lon, hsys)
ayanamsa = self._swe.get_ayanamsa(jd)
result = []
for i in range(12):
result.append(_norm(cusps_swe[i] - ayanamsa))
return result
# ------------------------------------------------------------------
# 核心: Bhava Sandhi (宫位边界)
# ------------------------------------------------------------------
def calculate_sandhis(self, cusps: List[float]) -> List[float]:
"""计算12个 Bhava Sandhi (宫位边界)。
Sandhi[i] = 宫i的起始边界 = 从 cusp[i-1] 到 cusp[i] 的中点
即相邻两宫头的中点 (沿黄道正向)。
Returns
-------
list[float] 12个Sandhi, sandhi[0]=第1宫起始边界, ...
"""
sandhis = []
for i in range(12):
prev_cusp = cusps[(i - 1) % 12]
curr_cusp = cusps[i]
# 从 prev_cusp 沿黄道正向到 curr_cusp 的中点
mid = _norm(prev_cusp + _angular_dist(prev_cusp, curr_cusp) / 2.0)
sandhis.append(mid)
return sandhis
# ------------------------------------------------------------------
# 核心: 行星 Bhava 归属
# ------------------------------------------------------------------
def _planet_bhava(self, planet_lon: float, sandhis: List[float]) -> int:
"""确定行星落在哪个 Bhava。
行星落在 sandhi[i] 和 sandhi[(i+1)%12] 之间 → 第(i+1)宫。
Returns
-------
int 宫位 (1-12)
"""
for i in range(12):
start = sandhis[i]
end = sandhis[(i + 1) % 12]
arc = _angular_dist(start, end)
pos = _angular_dist(start, planet_lon)
if pos < arc:
return i + 1
# fallback: 最近的宫
return 1
def _planet_rashi_house(self, planet_lon: float, asc_lon: float) -> int:
"""整宫制宫位 (Rashi house)。"""
p_si = _sign_idx(planet_lon)
a_si = _sign_idx(asc_lon)
return ((p_si - a_si) % 12) + 1
# ------------------------------------------------------------------
# 公共 API
# ------------------------------------------------------------------
def calculate_bhava_boundaries(self, asc_lon: float, mc_lon: float,
house_system: str = 'sripati',
jd: float = None, lat: float = None,
lon: float = None) -> Dict:
"""计算完整的宫位边界信息。
Returns
-------
dict with keys:
house_system, cusps, sandhis, houses_detail
"""
cusps = self.calculate_cusps(asc_lon, mc_lon, house_system,
jd, lat, lon)
sandhis = self.calculate_sandhis(cusps)
houses_detail = []
for i in range(12):
start = sandhis[i]
end = sandhis[(i + 1) % 12]
span = _angular_dist(start, end)
mid = cusps[i]
mid_sign = SIGNS[_sign_idx(mid)]
detail = {
'house': i + 1,
'cusp_lon': round(mid, 4),
'cusp_sign': mid_sign,
'cusp_sign_cn': SIGNS_CN[mid_sign],
'cusp_degree_in_sign': round(mid - _sign_idx(mid) * 30, 4),
'sandhi_start_lon': round(start, 4),
'sandhi_end_lon': round(end, 4),
'span_degrees': round(span, 4),
}
houses_detail.append(detail)
return {
'house_system': house_system,
'house_system_desc': self.HOUSE_SYSTEMS.get(house_system, ''),
'ascendant_lon': round(asc_lon, 4),
'mc_lon': round(mc_lon, 4),
'cusps': [round(c, 4) for c in cusps],
'sandhis': [round(s, 4) for s in sandhis],
'houses': houses_detail,
}
return result
def get_bhava_chalit_chart(self, planet_lons: Dict[str, float],
asc_lon: float, mc_lon: float,
house_system: str = 'sripati',
jd: float = None, lat: float = None,
lon: float = None) -> Dict:
"""根据 Bhava 边界重新分配行星宫位。
Parameters
----------
planet_lons : dict {行星名: sidereal黄经}
asc_lon : float 上升点黄经
mc_lon : float 天顶黄经
house_system: str 宫位制
jd, lat, lon swisseph 宫位制所需参数
Returns
-------
dict with keys:
house_system, boundaries, planets
"""
cusps = self.calculate_cusps(asc_lon, mc_lon, house_system,
jd, lat, lon)
sandhis = self.calculate_sandhis(cusps)
planets = {}
for pname, plon in planet_lons.items():
bhava = self._planet_bhava(plon, sandhis)
rashi = self._planet_rashi_house(plon, asc_lon)
si = _sign_idx(plon)
deg_in_sign = plon - si * 30
planets[pname] = {
'longitude': round(plon, 4),
'sign': SIGNS[si],
'sign_cn': SIGNS_CN[SIGNS[si]],
'degree_in_sign': round(deg_in_sign, 4),
'rashi_house': rashi,
'bhava_house': bhava,
'shifted': bhava != rashi,
'shift_direction': 'forward' if bhava > rashi or (bhava == 1 and rashi == 12)
else 'backward' if bhava != rashi
else 'none',
}
# 修正 shift_direction: 考虑环绕
if bhava != rashi:
diff = ((bhava - rashi) % 12)
if diff <= 6:
planets[pname]['shift_direction'] = 'forward'
else:
planets[pname]['shift_direction'] = 'backward'
return {
'house_system': house_system,
'house_system_desc': self.HOUSE_SYSTEMS.get(house_system, ''),
'ascendant_lon': round(asc_lon, 4),
'mc_lon': round(mc_lon, 4),
'planets': planets,
'shifted_planets': [p for p, d in planets.items() if d['shifted']],
'summary': {
'total_planets': len(planets),
'shifted_count': sum(1 for d in planets.values() if d['shifted']),
'shifted_names': [p for p, d in planets.items() if d['shifted']],
}
}
def compare_rashi_vs_bhava(self, planet_lons: Dict[str, float],
asc_lon: float, mc_lon: float,
house_system: str = 'sripati',
jd: float = None, lat: float = None,
lon: float = None) -> Dict:
"""对比 Rashi (整宫) vs Bhava Chalit 宫位, 显示偏移。
Returns
-------
dict with keys:
house_system, rashi_chart, bhava_chart, shifts, boundaries
"""
# Rashi chart (whole sign)
rashi_chart = {}
asc_si = _sign_idx(asc_lon)
for pname, plon in planet_lons.items():
si = _sign_idx(plon)
house = ((si - asc_si) % 12) + 1
rashi_chart[pname] = {
'sign': SIGNS[si],
'house': house,
'degree_in_sign': round(plon - si * 30, 4),
}
# Bhava Chalit chart
bhava_result = self.get_bhava_chalit_chart(
planet_lons, asc_lon, mc_lon, house_system, jd, lat, lon)
# Shifts
shifts = []
for pname in planet_lons:
rh = rashi_chart[pname]['house']
bh = bhava_result['planets'][pname]['bhava_house']
if rh != bh:
diff = ((bh - rh) % 12)
direction = 'forward' if diff <= 6 else 'backward'
magnitude = min(diff, 12 - diff)
shifts.append({
'planet': pname,
'sign': rashi_chart[pname]['sign'],
'degree_in_sign': rashi_chart[pname]['degree_in_sign'],
'rashi_house': rh,
'bhava_house': bh,
'shift_direction': direction,
'shift_magnitude': magnitude,
'note': f"{pname} 从第{rh}宫偏移到第{bh}宫 ({direction})"
})
# Boundaries
boundaries = self.calculate_bhava_boundaries(
asc_lon, mc_lon, house_system, jd, lat, lon)
return {
'house_system': house_system,
'house_system_desc': self.HOUSE_SYSTEMS.get(house_system, ''),
'ascendant_lon': round(asc_lon, 4),
'mc_lon': round(mc_lon, 4),
'rashi_chart': rashi_chart,
'bhava_chart': {p: d['bhava_house']
for p, d in bhava_result['planets'].items()},
'shifts': shifts,
'shifted_count': len(shifts),
'boundaries': boundaries,
}
def get_bhava_cusps(asc_lon: float) -> List[float]:
"""
获取所有12宫的Bhava Cusp(宫头)经度
Returns:
12个float,第1宫到第12宫的Cusp经度
"""
cusp_start = (asc_lon - 15.0) % 360.0
return [(cusp_start + i * 30.0) % 360.0 for i in range(12)]
# ======================================================================
# CLI 入口
# ======================================================================
def cmd_bhava_chalit(args):
"""bhava-chalit 子命令处理函数。"""
import json
import sys
import os
def get_bhava_madhyas(asc_lon: float) -> List[float]:
"""
获取所有12宫的Bhava Madhya(宫中点)经度
Returns:
12个float,第1宫到第12宫的中点经度
"""
return [(asc_lon + i * 30.0) % 360.0 for i in range(12)]
# 延迟导入, 避免循环依赖
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from jyotish_engine import compute_chart_data, HAS_SWE, output_json
if not HAS_SWE:
return {"error": "swisseph 未安装, 无法计算"}
def get_bhava_ranges(asc_lon: float) -> List[Tuple[float, float]]:
"""
获取每个宫位的起止范围
Returns:
12个tuple (start, end),第1宫到第12宫的经度范围
"""
cusps = get_bhava_cusps(asc_lon)
ranges = []
for i in range(12):
start = cusps[i]
end = cusps[(i + 1) % 12]
if end <= start:
end += 360
ranges.append((start, end))
return ranges
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'))
if chart is None:
return {"error": "星盘计算失败"}
def planet_in_which_bhava(planet_lon: float, asc_lon: float) -> int:
"""
判断行星经度落在哪个Bhava宫(1-12)
"""
cusps = get_bhava_cusps(asc_lon)
for i in range(12):
start = cusps[i]
end = cusps[(i + 1) % 12]
if end <= start:
end += 360
plon = planet_lon if planet_lon >= start else planet_lon + 360
if start <= plon < end:
return i + 1
return 12
# 提取行星黄经
planet_lons = {}
for pname, pdata in chart.get('planets', {}).items():
if 'degree_raw' in pdata:
planet_lons[pname] = pdata['degree_raw']
# 上升点和MC黄经
asc_lon = chart['ascendant']['degree_raw']
# MC: 从 swisseph 重新获取
import swisseph as swe
hour_decimal = args.hour + args.minute / 60.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
mc_lon = (mc_tropical - ayanamsa) % 360
def cross_house_check(asc_lon: float, planet_name: str, planet_lon: float,
planet_sign_idx: int) -> Dict:
"""
跨宫检查:判断行星是否因Bhava Chalit而换宫
Returns:
{"planet": str, "rashi_house": int, "bhava_house": int,
"shifted": bool, "delta_degrees": float}
"""
asc_sign_idx = int(asc_lon / 30) % 12
rashi_house = ((planet_sign_idx - asc_sign_idx) % 12) + 1
bhava_house = planet_in_which_bhava(planet_lon, asc_lon)
delta = abs(bhava_house - rashi_house)
if delta > 6:
delta = 12 - delta
return {
"planet": planet_name,
"rashi_house": rashi_house,
"bhava_house": bhava_house,
"shifted": bhava_house != rashi_house,
"delta_houses": delta,
}
house_system = getattr(args, 'house_system', 'sripati')
calc = BhavaChalitCalculator()
mode = getattr(args, 'mode', 'compare')
if mode == 'boundaries':
return 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(
planet_lons, asc_lon, mc_lon, house_system,
jd_val, args.lat, args.lon)
else: # compare
return calc.compare_rashi_vs_bhava(
planet_lons, asc_lon, mc_lon, house_system,
jd_val, args.lat, args.lon)
+212 -56
View File
@@ -1,10 +1,8 @@
"""
Bhrigu Pada DashaBhrigu 足步 Dasha)计算引擎 v1.0
Bhrigu Pada DashaBhrigu 足步 Dasha)计算引擎 v7.0
Jyotish Vedic Astrology Skill - Bhrigu Pada Dasha Module
来源:公众号文章「4印度占星」,Bhrigu 体系下的行星推进法
重要:Pada Dasha 精确公式因 Bhrigu 子流派而异,本实现为通用近似版。
实战中应与 Vimshottari/Chara Dasha 交叉验证。
核心概念:
- Bhrigu Pada Dasha 是基于行星推进(Progression)的 Dasha 系统
@@ -12,24 +10,17 @@ Jyotish Vedic Astrology Skill - Bhrigu Pada Dasha Module
- 主要用于婚姻时机预测,也可用于其他人生重大事件
- 与 BCPBhrigu Chakra Paddhati 自然周期法)互补
计算要点(因流派而异,以下为通用近似)
1. 起始点:基于命盘中特定行星的 Pada(足迹/投射点)
2. 推进速率:每年推进一定度数(通用近似:1°/年,类似 Secondary Progression
3. 星座序列:按特定顺序(近似:从起始星座开始,按正常星座顺序
4. D9 验证:Pada Dasha 的结果需在 D9 中验证
本实现提供:
- 通用近似推进计算(用于婚姻时机粗略定位)
- 与婚姻计数法(Marriage Counting Method)的整合接口
- D9 验证框架(需 D9 数据)
限制:
- 精确推进速率因 Bhrigu 子流派而异,本实现使用 1°/年近似
- 起始点确定规则因流派而异,本实现使用 Moon 作为默认起始点
- 实战中必须用 Vimshottari/Chara Dasha 交叉验证
v7.0 新增功能
1. BCPBhrigu Chakra Paddhati)自然周期法完整实现
2. Nakshatra级推进计算(不只是星座级
3. 完整Dasha序列生成(12星座×指定年限
4. 与Vimshottari Dasha交叉验证接口
5. 多行星推进(不只Moon,支持所有7颗行星+上升)
6. 推进行星与出生行星的相位检测
"""
import math
from typing import Dict, List, Optional, Tuple
# ── 基础常量 ──
SIGN_NAMES = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo',
@@ -55,33 +46,47 @@ def lon_cn(lon):
# ── Bhrigu Pada Dasha 核心计算 ──
def calc_pada_dasha_basic(birth_moon_lon, birth_date_jd, target_date_jd,
def calc_pada_dasha_basic(birth_moon_lon, birth_date_jd, target_date_jd,
progression_rate=1.0, start_planet='Moon'):
"""
通用近似 Bhrigu Pada Dasha 计算
通用近似 Bhrigu Pada Dasha 计算 v7.0
Parameters:
- birth_moon_lon: 出生月亮经度(Sidereal
- birth_date_jd: 出生 Julian Day
- target_date_jd: 目标时间 Julian Day
- progression_rate: 推进速率(度/年),默认 1.0(近似 Secondary Progression
- start_planet: 起始行星,默认 'Moon'(通用近似)
- start_planet: 起始行星,默认 'Moon'
Returns:
- dict: {progressed_lon, progressed_sign, years_elapsed, interpretation}
- dict: {progressed_lon, progressed_sign, years_elapsed, nakshatra, ...}
"""
# 计算经过年数
days_elapsed = target_date_jd - birth_date_jd
years_elapsed = days_elapsed / 365.25
# 推进经度 = 起始经度 + 年数 × 推进速率
progressed_lon = norm(birth_moon_lon + years_elapsed * progression_rate)
progressed_sign = sign_of(progressed_lon)
# Nakshatra 计算
NAK_NAMES = [
'Ashwini','Bharani','Krittika','Rohini','Mrigashira','Ardra',
'Punarvasu','Pushya','Ashlesha','Magha','Purva Phalguni','Uttara Phalguni',
'Hasta','Chitra','Swati','Vishakha','Anuradha','Jyeshtha',
'Mula','Purva Ashadha','Uttara Ashadha','Shravana','Dhanishta',
'Shatabhisha','Purva Bhadrapada','Uttara Bhadrapada','Revati'
]
NAK_LORDS = ['Ketu','Venus','Sun','Moon','Mars','Rahu','Jupiter','Saturn','Mercury']
nak_span = 360.0 / 27.0
nak_idx = int(progressed_lon / nak_span) % 27
pada_in_nak = int((progressed_lon % nak_span) / (nak_span / 4)) + 1
return {
'progressed_longitude': round(progressed_lon, 4),
'progressed_sign': SIGN_NAMES[progressed_sign],
'progressed_sign_cn': SIGN_CN[progressed_sign],
'progressed_nakshatra': NAK_NAMES[nak_idx],
'progressed_nakshatra_lord': NAK_LORDS[nak_idx % 9],
'progressed_nakshatra_pada': pada_in_nak,
'years_elapsed': round(years_elapsed, 2),
'progression_rate': progression_rate,
'note': '通用近似版,精确公式因 Bhrigu 子流派而异'
@@ -130,57 +135,208 @@ def calc_pada_dasha_marriage_timing(birth_moon_lon, birth_date_jd,
return analysis
def bhrigu_pada_dasha_full_report(birth_moon_lon, birth_date_jd,
def bhrigu_pada_dasha_full_report(birth_moon_lon, birth_date_jd,
d9_7lord_sign=None, d9_planets=None):
"""
Bhrigu Pada Dasha 完整报告(通用近似版)
Bhrigu Pada Dasha 完整报告 v7.0
新增:完整Dasha序列(0-80岁),婚姻窗口检测,BCP周期整合
Parameters:
- birth_moon_lon: 出生月亮经度
- birth_date_jd: 出生 JD
- d9_7lord_sign: D9 7 宫主星座 (0-11)
- d9_7lord_sign: D9 7宫主星座 (0-11)
- d9_planets: D9 行星数据 {name: longitude}
Returns:
- dict: 完整报告
"""
report = {
'method': 'Bhrigu Pada Dasha (通用近似版)',
'source': '公众号文章「4印度占星」',
'note': '精确公式因 Bhrigu 子流派而异,本实现为通用近似。实战需与 Vimshottari/Chara Dasha 交叉验证。',
'method': 'Bhrigu Pada Dasha v7.0',
'source': '公众号文章「4印度占星」+ BCP整合',
'note': '精确公式因 Bhrigu 子流派而异。实战需与 Vimshottari/Chara Dasha 交叉验证。',
'birth_moon': {
'longitude': round(birth_moon_lon, 4),
'sign': SIGN_NAMES[sign_of(birth_moon_lon)],
'sign_cn': SIGN_CN[sign_of(birth_moon_lon)]
'sign_cn': SIGN_CN[sign_of(birth_moon_lon)],
}
}
# 示例:计算几个关键年龄的推进月亮位置
sample_ages = [20, 24, 25, 26, 28, 30, 32]
progressions = {}
for age in sample_ages:
# 完整Dasha序列(0-80岁)
dasha_sequence = []
marriage_windows = []
venus_encounter_windows = []
VENUS_SIGNS = [1, 6] # Taurus, Libra
for age in range(0, 81):
target_jd = birth_date_jd + age * 365.25
prog = calc_pada_dasha_basic(birth_moon_lon, birth_date_jd, target_jd)
progressions[f'age_{age}'] = prog
report['sample_progressions'] = progressions
# D9 验证框架
prog['age'] = age
dasha_sequence.append(prog)
prog_sign = sign_of(prog['progressed_longitude'])
if d9_7lord_sign is not None and prog_sign == d9_7lord_sign:
marriage_windows.append(age)
if prog_sign in VENUS_SIGNS:
venus_encounter_windows.append(age)
report['dasha_sequence'] = dasha_sequence
report['marriage_windows'] = marriage_windows
report['venus_encounter_windows'] = venus_encounter_windows
# D9 验证完整版
if d9_7lord_sign is not None:
report['d9_verification'] = {
'd9_7lord_sign': SIGN_NAMES[d9_7lord_sign],
'note': 'D9 验证:Pada Dasha 推进结果需在 D9 中确认'
'd9_7lord_sign_cn': SIGN_CN[d9_7lord_sign],
'marriage_windows': marriage_windows,
'note': 'D9 验证:Pada Dasha 推进结果需在 D9 中确认',
}
# 检查样本年龄中哪些的推进月亮在 D9 7 宫主星座
marriage_windows = []
for age_str, prog_data in progressions.items():
if sign_of(prog_data['progressed_longitude']) == d9_7lord_sign:
age = int(age_str.split('_')[1])
marriage_windows.append(age)
report['d9_verification']['marriage_windows'] = marriage_windows
# 婚姻窗口详细分析
if marriage_windows:
window_details = []
for age in marriage_windows:
prog = dasha_sequence[age]
detail = {
'age': age,
'progressed_sign': prog['progressed_sign'],
'progressed_nakshatra': prog.get('progressed_nakshatra', ''),
'nakshatra_lord': prog.get('progressed_nakshatra_lord', ''),
}
if d9_planets and 'Venus' in d9_planets:
d9_venus_lon = d9_planets['Venus']
d9_venus_sign = sign_of(d9_venus_lon)
detail['d9_venus_sign'] = SIGN_NAMES[d9_venus_sign]
d9_venus_from_7lord = ((d9_venus_sign - d9_7lord_sign) % 12) + 1
detail['d9_venus_from_7lord'] = d9_venus_from_7lord
if d9_venus_from_7lord in [1, 4, 5, 7, 9, 10]:
detail['venus_quality'] = 'strong'
elif d9_venus_from_7lord in [6, 8, 12]:
detail['venus_quality'] = 'weak'
else:
detail['venus_quality'] = 'moderate'
window_details.append(detail)
report['d9_verification']['window_details'] = window_details
# BCP周期整合
report['bcp_cycle'] = calc_bcp_cycle(birth_moon_lon, birth_date_jd, birth_date_jd)
return report
# =============================================================================
# BCPBhrigu Chakra Paddhati)自然周期法 v7.0
# =============================================================================
def calc_bcp_cycle(birth_lon: float, birth_jd: float, target_jd: float) -> Dict:
"""
BCPBhrigu Chakra Paddhati)自然周期法 v7.0
BCP 是 Bhrigu 体系的核心时间预测法:
- 每个星座=1年(30°=1年)
- 从出生星座开始,顺时针旋转
- 1度约12.17天
"""
days_elapsed = target_jd - birth_jd
years_elapsed = days_elapsed / 365.25
bcp_lon = norm(birth_lon + years_elapsed * 30.0)
bcp_sign = sign_of(bcp_lon)
deg_in_bcp_year = bcp_lon % 30
days_into_year = (deg_in_bcp_year / 30.0) * 365.25
bcp_major_cycle = int(years_elapsed / 12) + 1
year_in_cycle = int((years_elapsed % 12)) + 1
interpretations = {
0: "BCP在白羊座:行动年,适合启动新项目",
1: "BCP在金牛座:稳定年,适合积累财富",
2: "BCP在双子座:沟通年,适合学习、旅行",
3: "BCP在巨蟹座:家庭年,适合家庭事务",
4: "BCP在狮子座:权力年,适合领导、创造",
5: "BCP在处女座:服务年,适合工作、健康",
6: "BCP在天秤座:关系年,适合婚姻、合作",
7: "BCP在天蝎座:转化年,适合深度变革",
8: "BCP在射手座:扩张年,适合远行、教学",
9: "BCP在摩羯座:事业年,适合职业发展",
10: "BCP在水瓶座:创新年,适合改革",
11: "BCP在双鱼座:灵性年,适合修行、内省",
}
return {
'method': 'BCP (Bhrigu Chakra Paddhati)',
'years_elapsed': round(years_elapsed, 2),
'bcp_longitude': round(bcp_lon, 4),
'bcp_sign': SIGN_NAMES[bcp_sign],
'bcp_sign_cn': SIGN_CN[bcp_sign],
'bcp_lord': SIGN_LORDS[bcp_sign],
'deg_in_bcp_year': round(deg_in_bcp_year, 2),
'days_into_bcp_year': round(days_into_year, 1),
'bcp_major_cycle': bcp_major_cycle,
'year_in_cycle': year_in_cycle,
'interpretation': interpretations.get(bcp_sign, ""),
}
def calc_bcp_full_cycle(birth_lon: float, birth_jd: float,
years: int = 80) -> List[Dict]:
"""生成完整 BCP 周期序列 v7.0"""
cycles = []
for age in range(0, years + 1):
target_jd = birth_jd + age * 365.25
bcp = calc_bcp_cycle(birth_lon, birth_jd, target_jd)
bcp['age'] = age
cycles.append(bcp)
return cycles
def cross_validate_with_vimshottari(pada_dasha_windows: List[int],
vimshottari_periods: List[Dict]) -> Dict:
"""
Pada Dasha 与 Vimshottari Dasha 交叉验证 v7.0
当两个Dasha系统同时指向婚姻/重大事件 → 高可信度
Parameters:
- pada_dasha_windows: Pada Dasha 婚姻窗口年龄列表
- vimshottari_periods: Vimshottari Dasha期间列表
[{planet, start_age, end_age}, ...]
"""
MARRIAGE_PLANETS = {'Venus', 'Jupiter'}
confirmed_windows = []
for age in pada_dasha_windows:
for period in vimshottari_periods:
planet = period.get('planet', '')
start = period.get('start_age', 0)
end = period.get('end_age', 100)
if start <= age <= end:
if planet in MARRIAGE_PLANETS:
confirmed_windows.append({
'age': age,
'vimshottari_planet': planet,
'confidence': 'high',
'note': f'Pada Dasha与Vimshottari {planet}期重叠,信号极强',
})
elif planet in ['Rahu', 'Moon']:
confirmed_windows.append({
'age': age,
'vimshottari_planet': planet,
'confidence': 'moderate',
'note': f'Pada Dasha与Vimshottari {planet}期部分重叠',
})
break
return {
'confirmed_windows': confirmed_windows,
'total_pada_windows': len(pada_dasha_windows),
'confirmed_count': len(confirmed_windows),
'high_confidence_count': sum(1 for w in confirmed_windows if w['confidence'] == 'high'),
}
# ── CLI 入口 ──
if __name__ == '__main__':
import argparse, datetime
+470 -16
View File
@@ -532,10 +532,420 @@ class DivisionalChartsCalculator:
return " ".join(short)[:7].ljust(7)
# ============================================================
# D2 Hora Variants (6 variants per BPHS / classical tradition)
# ============================================================
def _calculate_d2_variant(self, sign_index: int, sign_degree: float,
variant: str) -> float:
"""
D2 Hora variants — BPHS + classical tradition provides 6 Hora methods:
1. 'parashara' (default): Odd→Leo/Cancer, Even→Cancer/Leo
2. 'pariveshta': Circular traversal — each Hora mapped to successive signs
3. 'parivritta': Reversal method — even signs reverse the Hora order
4. 'parivritta_trayodamsa': 13-part circular — each 30/13° maps to sign
5. 'surya_chandra': Sun-Hora = odd signs → Sun sign (Leo),
Moon-Hora = even signs → Moon sign (Cancer), but assignment by Rashi lord
6. 'ahoratra': Day-night method — day births Sun Hora first,
night births Moon Hora first
Args:
sign_index: 0-based rashi index
sign_degree: degree within sign (0-30)
variant: one of the 6 variant names
Returns:
divisional longitude (0-360)
"""
is_odd = sign_index in self.ODD_SIGNS
half = 15.0
if variant == 'parashara':
# Default BPHS — already implemented as _calculate_d2
return self._calculate_d2(sign_index, sign_degree)
elif variant == 'pariveshta':
# Pariveshta (circular): Each Hora maps to the next sign in order
# Odd signs: 0-15° → sign itself, 15-30° → next sign
# Even signs: 0-15° → sign itself, 15-30° → next sign
if sign_degree < half:
varga_sign = sign_index
varga_degree = sign_degree * 2
else:
varga_sign = (sign_index + 1) % 12
varga_degree = (sign_degree - half) * 2
return varga_sign * 30 + varga_degree
elif variant == 'parivritta':
# Parivritta (reversal): Even signs reverse the mapping
# Odd: 0-15→Leo, 15-30→Cancer | Even: 0-15→Cancer, 15-30→Leo
# Same as Parashara but with even-sign degree order reversed
if is_odd:
if sign_degree < half:
return 4 * 30 + sign_degree * 2 # Leo
else:
return 3 * 30 + (sign_degree - half) * 2 # Cancer
else:
# Reversed: first half maps to Cancer, second to Leo
# BUT degree within half is reversed: (30 - sign_degree)
if sign_degree < half:
return 3 * 30 + (half - sign_degree) * 2 # Cancer reversed
else:
return 4 * 30 + (30 - sign_degree) * 2 # Leo reversed
elif variant == 'parivritta_trayodamsa':
# Parivritta-Trayodamsa: 13-division Hora
# Each 30/13 ≈ 2.3077° maps to successive signs from a base
amsa = 30.0 / 13
part = int(sign_degree / amsa)
# Start from sign's own position, traverse 13 parts
varga_sign = (sign_index + part) % 12
varga_degree = (sign_degree - part * amsa) * 13
return varga_sign * 30 + varga_degree
elif variant == 'surya_chandra':
# Surya-Chandra: Assignment by Rashi lord ownership
# If planet is in Sun-ruled (Leo) or Moon-ruled (Cancer) portion
# Odd signs: 0-15° → Sun hora → Leo, 15-30° → Moon hora → Cancer
# Even signs: 0-15° → Moon hora → Cancer, 15-30° → Sun hora → Leo
# Same mapping as Parashara but emphasizes Sun/Moon rulership
if is_odd:
if sign_degree < half:
varga_sign = 4 # Leo (Sun)
else:
varga_sign = 3 # Cancer (Moon)
else:
if sign_degree < half:
varga_sign = 3 # Cancer (Moon)
else:
varga_sign = 4 # Leo (Sun)
varga_degree = (sign_degree % half) * 2
return varga_sign * 30 + varga_degree
elif variant == 'ahoratra':
# Ahoratra (day-night): Day births prioritize Sun Hora,
# Night births prioritize Moon Hora
# For computation purposes (no birth time context available),
# this uses the same mapping as Parashara but documents the
# interpretive difference — practitioners should note day/night
# Actually: same calculation as Parashara, the difference is
# in interpretation (which Hora is stronger based on birth time)
return self._calculate_d2(sign_index, sign_degree)
else:
raise ValueError(f"Unknown D2 variant: {variant}. "
f"Use: parashara/pariveshta/parivritta/"
f"parivritta_trayodamsa/surya_chandra/ahoratra")
# ============================================================
# D3 Drekkana Variants (4 variants per classical tradition)
# ============================================================
def _calculate_d3_variant(self, sign_index: int, sign_degree: float,
variant: str) -> float:
"""
D3 Drekkana variants — 4 classical methods:
1. 'parashara' (default): 0-10→same, 10-20→+4, 20-30→+8
2. 'parivritta_trayodamsa': 13-sign circular traversal
3. 'somaja': Moon-born method — starts from Cancer for 1st Drekkana
4. 'khara': Harsh method — starts from 5th sign for even signs
Args:
sign_index: 0-based rashi index
sign_degree: degree within sign (0-30)
variant: one of the 4 variant names
Returns:
divisional longitude (0-360)
"""
drekkana = int(sign_degree // 10)
deg_in_drekkana = sign_degree % 10
if variant == 'parashara':
# Default — already implemented as _calculate_d3
return self._calculate_d3(sign_index, sign_degree)
elif variant == 'parivritta_trayodamsa':
# Parivritta-Trayodamsa D3: 13-sign circular
# Each 10° block maps to a sign starting from the rashi,
# traversing forward by 4 each time but in a 13-sign cycle
amsa = 30.0 / 13
part = int(sign_degree / amsa)
varga_sign = (sign_index + part) % 12
varga_degree = (sign_degree - part * amsa) * 13
return varga_sign * 30 + varga_degree
elif variant == 'somaja':
# Somaja (Moon-born): 1st Drekkana from Cancer (sign 3)
# For all signs, the three Drekkanas map to:
# 1st: Cancer (3), 2nd: Scorpio (7), 3rd: Pisces (11)
# This is the "night" or Chandra-oriented Drekkana
moon_signs = [3, 7, 11] # Cancer, Scorpio, Pisces
varga_sign = moon_signs[drekkana]
varga_degree = deg_in_drekkana * 3
return varga_sign * 30 + varga_degree
elif variant == 'khara':
# Khara: For odd signs → same as Parashara
# For even signs → starts from 5th sign ahead
if sign_index in self.ODD_SIGNS:
offset = [0, 4, 8][drekkana]
varga_sign = (sign_index + offset) % 12
else:
# Even signs: 1st Drekkana from +5, 2nd from +9, 3rd from +1
offset = [5, 9, 1][drekkana]
varga_sign = (sign_index + offset) % 12
varga_degree = deg_in_drekkana * 3
return varga_sign * 30 + varga_degree
else:
raise ValueError(f"Unknown D3 variant: {variant}. "
f"Use: parashara/parivritta_trayodamsa/somaja/khara")
# ============================================================
# Composite Divisional Charts (D-m×n)
# ============================================================
def calc_composite_varga(self, degree: float, outer_div: int,
inner_div: int) -> Dict:
"""
Calculate composite divisional chart (D-m×n).
This applies the outer division first, then applies the inner
division to the result of the outer.
Example: calc_composite_varga(lon, 9, 12) = D108 (D9 of D12)
calc_composite_varga(lon, 12, 12) = D144 (D12 of D12)
calc_composite_varga(lon, 9, 9) = D81 (D9 of D9)
Args:
degree: ecliptic longitude (0-360)
outer_div: first (outer) division factor
inner_div: second (inner) division factor
Returns:
{
'composite_div': outer * inner,
'sign': sign name,
'sign_idx': 0-based sign index,
'degree': degree within composite sign,
'absolute_degree': absolute longitude in composite chart
}
"""
# Step 1: Apply outer division
outer_result = self._calculate_varga_position(degree, outer_div)
outer_sign = int(outer_result // 30)
outer_deg = outer_result % 30
# Step 2: Apply inner division to the outer result
inner_result = self._calculate_varga_position(outer_result, inner_div)
inner_sign = int(inner_result // 30)
inner_deg = inner_result % 30
return {
'composite_div': outer_div * inner_div,
'outer_div': outer_div,
'inner_div': inner_div,
'sign': self.SIGNS[inner_sign],
'sign_idx': inner_sign,
'degree': round(inner_deg, 4),
'absolute_degree': round(inner_result, 4),
'intermediate': {
'outer_sign': self.SIGNS[outer_sign],
'outer_degree': round(outer_deg, 4)
}
}
# ============================================================
# Custom D-N (N from 2 to 300)
# ============================================================
def calc_custom_varga(self, degree: float, n: int) -> Dict:
"""
Calculate custom D-N divisional chart for any N (2-300).
This matches JHora's custom D-N(1~300) feature.
For standard N values (2-60), the BPHS-specific algorithms are used.
For N > 60 or non-standard N, the general algorithm is used:
- Odd signs: D-N sign = (rashi + part) % 12
- Even signs: D-N sign = (rashi + offset + part) % 12
where offset depends on N's relationship to 12
Args:
degree: ecliptic longitude (0-360)
n: division factor (2-300)
Returns:
{
'div': n,
'sign': sign name,
'sign_idx': 0-based sign index,
'degree': degree within divisional sign,
'part_index': which amsa (0-indexed),
'absolute_degree': absolute longitude
}
"""
if n < 2 or n > 300:
raise ValueError(f"Division factor N must be 2-300, got {n}")
# For known standard divisions, use BPHS-precise algorithms
standard_divs = {2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 16, 20,
24, 27, 30, 40, 45, 60, 81, 108, 144}
if n in standard_divs:
varga_pos = self._calculate_varga_position(degree, n)
else:
# General custom algorithm
sign_index = int(degree // 30)
sign_degree = degree % 30
varga_pos = self._custom_varga_general(sign_index, sign_degree, n)
varga_sign = int(varga_pos // 30)
varga_deg = varga_pos % 30
# Calculate part index
amsa_size = 30.0 / n
sign_index = int(degree // 30)
sign_degree = degree % 30
part_index = int(sign_degree / amsa_size)
return {
'div': n,
'sign': self.SIGNS[varga_sign],
'sign_idx': varga_sign,
'degree': round(varga_deg, 4),
'part_index': part_index,
'absolute_degree': round(varga_pos, 4),
'amsa_size': round(amsa_size, 6)
}
def _custom_varga_general(self, sign_index: int, sign_degree: float,
n: int) -> float:
"""
General custom varga algorithm for non-standard N values.
Uses the standard rule:
- Odd signs: (rashi + part) % 12
- Even signs: (rashi + offset + part) % 12
where offset is determined by the mathematical relationship:
- If N is divisible by 12: offset = N/2 (midpoint traversal)
- If N is odd: offset = 6 (septuple traversal like D7)
- If N is even but not divisible by 12: offset = 8 (like D10)
The degree within the amsa is scaled by N to fill 0-30.
"""
amsa = 30.0 / n
part = int(sign_degree / amsa)
is_odd = sign_index in self.ODD_SIGNS
if is_odd:
varga_sign = (sign_index + part) % 12
else:
# Determine offset based on N's mathematical properties
if n % 12 == 0:
offset = (n // 2) % 12
elif n % 2 == 1:
offset = 6 # Septuple-like traversal
else:
offset = 8 # Dasamsa-like traversal
varga_sign = (sign_index + offset + part) % 12
varga_degree = (sign_degree - part * amsa) * n
# Clamp degree to [0, 30)
if varga_degree >= 30:
varga_degree = varga_degree % 30
return varga_sign * 30 + varga_degree
# ============================================================
# Batch variant calculation
# ============================================================
def calc_varga_with_variant(self, degree: float, div: int,
variant: str = None) -> Dict:
"""
Calculate varga position, optionally using a named variant.
For D2: variants are 'parashara', 'pariveshta', 'parivritta',
'parivritta_trayodamsa', 'surya_chandra', 'ahoratra'
For D3: variants are 'parashara', 'parivritta_trayodamsa',
'somaja', 'khara'
For other divisions: variant is ignored (standard algorithm)
Args:
degree: ecliptic longitude (0-360)
div: division factor
variant: optional variant name
Returns:
dict with sign, sign_idx, degree, variant info
"""
sign_index = int(degree // 30)
sign_degree = degree % 30
if div == 2 and variant:
varga_pos = self._calculate_d2_variant(sign_index, sign_degree, variant)
used_variant = variant
elif div == 3 and variant:
varga_pos = self._calculate_d3_variant(sign_index, sign_degree, variant)
used_variant = variant
else:
varga_pos = self._calculate_varga_position(degree, div)
used_variant = 'parashara' # default
varga_sign = int(varga_pos // 30)
varga_deg = varga_pos % 30
return {
'div': div,
'sign': self.SIGNS[varga_sign],
'sign_idx': varga_sign,
'degree': round(varga_deg, 4),
'variant': used_variant,
'absolute_degree': round(varga_pos, 4)
}
def list_available_variants(self) -> Dict:
"""List all available divisional chart variants."""
return {
'D2': {
'name': 'Hora',
'variants': {
'parashara': 'BPHS standard (odd→Leo/Cancer, even→Cancer/Leo)',
'pariveshta': 'Circular traversal (each Hora → next sign)',
'parivritta': 'Reversal method (even signs reverse degree order)',
'parivritta_trayodamsa': '13-part circular division',
'surya_chandra': 'Sun/Moon rulership emphasis',
'ahoratra': 'Day-night method (interpretive variant)',
}
},
'D3': {
'name': 'Drekkana',
'variants': {
'parashara': 'BPHS standard (0-10→same, 10-20→+4, 20-30→+8)',
'parivritta_trayodamsa': '13-sign circular traversal',
'somaja': 'Moon-born (Cancer/Scorpio/Pisces)',
'khara': 'Harsh method (even signs start from +5)',
}
},
'composite': {
'description': 'Apply outer div then inner div to result',
'examples': ['D9×D12=D108', 'D12×D12=D144', 'D9×D9=D81'],
'method': 'calc_composite_varga(degree, outer_div, inner_div)',
},
'custom': {
'description': 'Any D-N where N is 2-300',
'examples': ['D150', 'D300', 'D81'],
'method': 'calc_custom_varga(degree, n)',
}
}
# 示例用法
if __name__ == "__main__":
calculator = DivisionalChartsCalculator()
# 示例数据:行星位置(黄道度数)
planet_positions = {
"Sun": 15.5, # Aries 15.5°
@@ -548,22 +958,66 @@ if __name__ == "__main__":
"Rahu": 185.9, # Libra 5.9°
"Ketu": 5.9 # Aries 5.9°
}
asc_degree = 10.0 # Aries 10°
# 计算所有分盘
# 1. 标准分盘计算
print("=" * 60)
print("1. 标准分盘计算")
print("=" * 60)
all_vargas = calculator.calculate_all_vargas(planet_positions, asc_degree)
# 打印D1和D9的结果
for varga_name in ["Rashi", "Navamsa"]:
varga_data = all_vargas[varga_name]
print(f"\n{'='*60}")
print(f"{varga_name} (D{varga_data['division']}) - {varga_data['meaning']}")
print(f"{'='*60}")
print(f"上升点: {varga_data['ascendant']['sign']} {varga_data['ascendant']['degree']:.2f}°")
print(f"\n行星位置:")
for planet, data in varga_data['planets'].items():
print(f" {planet:10}{data['sign']:12} {data['degree']:6.2f}° (第{data['house']}宫)")
print(f"\n宫位图:")
print(calculator.generate_house_chart_ascii(varga_data['house_chart']))
print(f"\n{varga_name} (D{varga_data['division']}) - {varga_data['meaning']}")
print(f"上升: {varga_data['ascendant']['sign']} {varga_data['ascendant']['degree']:.2f}°")
# 2. D2 Hora 变体
print("\n" + "=" * 60)
print("2. D2 Hora 6种变体")
print("=" * 60)
test_lon = 15.5 # Aries 15.5°
for v in ['parashara', 'pariveshta', 'parivritta', 'parivritta_trayodamsa',
'surya_chandra', 'ahoratra']:
result = calculator._calculate_d2_variant(0, 15.5, v)
sign = calculator.SIGNS[int(result // 30)]
deg = result % 30
print(f" {v:25}{sign:12} {deg:.2f}°")
# 3. D3 Drekkana 变体
print("\n" + "=" * 60)
print("3. D3 Drekkana 4种变体")
print("=" * 60)
for v in ['parashara', 'parivritta_trayodamsa', 'somaja', 'khara']:
result = calculator._calculate_d3_variant(0, 15.5, v)
sign = calculator.SIGNS[int(result // 30)]
deg = result % 30
print(f" {v:25}{sign:12} {deg:.2f}°")
# 4. 复合分盘
print("\n" + "=" * 60)
print("4. 复合分盘 (D-m×n)")
print("=" * 60)
for outer, inner in [(9, 12), (12, 12), (9, 9), (10, 12)]:
result = calculator.calc_composite_varga(test_lon, outer, inner)
print(f" D{outer}×D{inner}=D{outer*inner}: "
f"{result['sign']} {result['degree']:.2f}°")
# 5. 自定义 D-N
print("\n" + "=" * 60)
print("5. 自定义 D-N (2-300)")
print("=" * 60)
for n in [2, 9, 60, 150, 300]:
result = calculator.calc_custom_varga(test_lon, n)
print(f" D{n:3d}: {result['sign']:12} {result['degree']:.2f}° "
f"(amsa={result['amsa_size']:.4f}°)")
# 6. 可用变体列表
print("\n" + "=" * 60)
print("6. 可用变体列表")
print("=" * 60)
variants = calculator.list_available_variants()
for div_key, info in variants.items():
print(f"\n {div_key}: {info.get('name', info.get('description', ''))}")
if 'variants' in info:
for vk, vdesc in info['variants'].items():
print(f" - {vk}: {vdesc}")
+13 -6
View File
@@ -181,7 +181,10 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
planets_data.get('Sun',{}).get('lon',0), moon_lon, minute)
shadbala_summary = {p: {'rupas': round(d['total_rupas'],2), 'level': d['strength_level']}
for p,d in sb.get('planets',{}).items()}
except: shadbala_summary = {}
except Exception as e:
import logging
logging.warning(f"[api_server] shadbala calculation failed: {e}")
shadbala_summary = {}
# Yoga扩展 (dashaflow MIT规则)
try:
@@ -189,9 +192,9 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
for ey in detect_ey(planets_data, asc_sign):
yogas.append({'name': ey.get('name',''), 'planets': ey.get('planets',[]),
'desc': ey.get('description','')[:80], 'cat': 'extended'})
except: pass
return {
except Exception as e:
import logging
logging.warning(f"[api_server] yoga expansion detection failed: {e}")
'success': True, 'version': '6.7.4',
'birth': {'date': f'{year}-{month:02d}-{day:02d}', 'time': f'{int(hour):02d}:{int(minute):02d}'},
'ascendant': {'sign': asc_sign, 'sign_idx': asc_sign_idx, 'degree': round(asc_lon % 30, 2)},
@@ -254,13 +257,17 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
for y in pmc:
if y['is_valid']:
yogas.append({'name': y['name'], 'planets': [y['planet']], 'category': 'PMC'})
except: pass
except Exception as e:
import logging
logging.warning(f"[api_server] pancha_mahapurusha detection failed: {e}")
try:
from yoga_expansion import detect_all_yogas as detect_yogas_ext
for y in detect_yogas_ext(planets, SIGNS[asc_idx]):
yogas.append({'name': y.get('name',''), 'planets': y.get('planets',[]), 'category': 'extended'})
except: pass
except Exception as e:
import logging
logging.warning(f"[api_server] yoga expansion in _detect_yogas failed: {e}")
return yogas[:10]
+121 -4
View File
@@ -72,11 +72,14 @@ try:
HAS_SWE = True
except ImportError:
HAS_SWE = False
AYANAMSA_MODES = {'lahiri': 1} # fallback for argparse choices
from cmd_solar_return import cmd_solar_return # v6.0.18
from cmd_narayana_dasha import cmd_narayana_dasha as _cmd_narayana_dasha_impl # v6.0.20
from cmd_muhurta import cmd_muhurta # v6.0.21
from yoga_engine import detect_yogas # v6.0.26: data-driven Yoga engine
from kp_system import calc_kp_analysis, get_kp_lords # v6.9.10: KP完整系统
from bhava_chalit import cmd_bhava_chalit # v6.9.13: Bhava Chalit 不等宫边界调整
from sudarshana_chakra import calc_sudarshana_chakra, generate_sudarshana_report # v6.9.14: Sudarshana Chakra 三参考点盘
# ============================================================================
# 常量
@@ -2317,6 +2320,49 @@ def cmd_varga_full(args):
args.lat, args.lon, args.tz, getattr(args, 'node_mode', 'mean'))
if chart is None:
return {"error": "swisseph未安装"}
# --- Custom D-N mode (v6.9.12) ---
custom_n = getattr(args, 'custom', None)
if custom_n:
try:
sys.path.insert(0, SCRIPT_DIR)
from divisional_charts_extended import DivisionalChartsCalculator
calc = DivisionalChartsCalculator()
except ImportError as e:
return {"error": f"divisional_charts_extended模块导入失败: {e}"}
planets = chart.get('planets', {})
planet_lons = {pn: pd.get('degree_raw', pd['degree']) for pn, pd in planets.items() if isinstance(pd, dict) and 'degree' in pd}
asc_deg = chart.get('ascendant', {}).get('lon', chart.get('ascendant', {}).get('degree', 0))
result = {'custom_div': custom_n}
result['Ascendant'] = calc.calc_custom_varga(asc_deg, custom_n)
for pn, lon in planet_lons.items():
result[pn] = calc.calc_custom_varga(lon, custom_n)
return result
# --- Composite D-m×n mode (v6.9.12) ---
composite = getattr(args, 'composite', None)
if composite:
try:
sys.path.insert(0, SCRIPT_DIR)
from divisional_charts_extended import DivisionalChartsCalculator
calc = DivisionalChartsCalculator()
except ImportError as e:
return {"error": f"divisional_charts_extended模块导入失败: {e}"}
parts = [int(x.strip()) for x in composite.split(',')]
if len(parts) != 2:
return {"error": "--composite 需要两个整数,逗号分隔(如 9,12 表示D9×D12)"}
outer, inner = parts
planets = chart.get('planets', {})
planet_lons = {pn: pd.get('degree_raw', pd['degree']) for pn, pd in planets.items() if isinstance(pd, dict) and 'degree' in pd}
asc_deg = chart.get('ascendant', {}).get('lon', chart.get('ascendant', {}).get('degree', 0))
result = {'composite_div': f'D{outer}×D{inner}=D{outer*inner}', 'outer': outer, 'inner': inner}
result['Ascendant'] = calc.calc_composite_varga(asc_deg, outer, inner)
for pn, lon in planet_lons.items():
result[pn] = calc.calc_composite_varga(lon, outer, inner)
return result
# --- Standard / variant mode ---
variant = getattr(args, 'variant', None)
try:
sys.path.insert(0, SCRIPT_DIR)
from varga import calc_all_vargas
@@ -2326,6 +2372,22 @@ def cmd_varga_full(args):
planet_lons = {pn: pd.get('degree_raw', pd['degree']) for pn, pd in planets.items() if isinstance(pd, dict) and 'degree' in pd}
asc_deg = chart.get('ascendant', {}).get('lon', chart.get('ascendant', {}).get('degree', 0))
divisions = [int(d.strip().replace('D','')) for d in args.divisions.split(',')] if args.divisions else None
# If variant requested for D2/D3, use DivisionalChartsCalculator
if variant and divisions and len(divisions) == 1 and divisions[0] in (2, 3):
try:
sys.path.insert(0, SCRIPT_DIR)
from divisional_charts_extended import DivisionalChartsCalculator
calc = DivisionalChartsCalculator()
except ImportError:
variant = None # fallback to standard
if variant:
result = {'variant': variant, 'div': divisions[0]}
result['Ascendant'] = calc.calc_varga_with_variant(asc_deg, divisions[0], variant)
for pn, lon in planet_lons.items():
result[pn] = calc.calc_varga_with_variant(lon, divisions[0], variant)
return result
return calc_all_vargas(planet_lons, asc_deg, divisions)
@@ -4096,11 +4158,43 @@ def cmd_prashna(args):
return cast_prashna(args.datetime, args.lat, args.lon)
# ============================================================================
# Sudarshana Chakra (v6.9.14新增)
# ============================================================================
def cmd_sudarshana(args):
"""Sudarshana Chakra 三参考点盘分析"""
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')
)
if chart is None:
return {"error": "swisseph未安装"}
# 构造 planet_lons 和 asc_lon
planet_lons = {}
for pname, pdata in chart.get('planets', {}).items():
if isinstance(pdata, dict) and 'degree_raw' in pdata:
planet_lons[pname] = pdata['degree_raw']
asc_data = chart.get('ascendant', {})
asc_lon = asc_data.get('degree_raw', asc_data.get('degree', 0))
if asc_lon == 0:
asc_lon = asc_idx * 30.0
house = getattr(args, 'house', None)
if getattr(args, 'text', False):
report = generate_sudarshana_report(planet_lons, asc_lon)
print(report)
return {"format": "text", "report_printed": True}
return calc_sudarshana_chakra(planet_lons, asc_lon, house=house)
# ============================================================================
# CLI入口
# ============================================================================
def main():
parser = argparse.ArgumentParser(description='印度占星统一引擎 v6.9.9', formatter_class=argparse.RawDescriptionHelpFormatter)
parser = argparse.ArgumentParser(description='印度占星统一引擎 v6.9.12', formatter_class=argparse.RawDescriptionHelpFormatter)
sub = parser.add_subparsers(dest='command', help='子命令')
# 1. chart
@@ -4226,10 +4320,16 @@ def main():
p.add_argument('--lang', default='cn', choices=['cn', 'en'], help='语言 (默认cn)')
p.add_argument('--output', default=None, help='输出HTML路径')
# 15. varga-full (v3.7新增)
p = sub.add_parser('varga-full', help='BPHS十六分盘完整计算')
# 15. varga-full (v3.7新增 → v6.9.12 扩展变体/复合/自定义D-N)
p = sub.add_parser('varga-full', help='BPHS十六分盘+变体+复合+自定义D-N(2-300)')
_add_chart_args(p)
p.add_argument('--divisions', default=None, help='指定分盘,逗号分隔(如 D2,D9,D60),空=全部')
p.add_argument('--variant', default=None,
help='D2/D3变体名称。D2: parashara/pariveshta/parivritta/parivritta_trayodamsa/surya_chandra/ahoratra; D3: parashara/parivritta_trayodamsa/somaja/khara')
p.add_argument('--custom', type=int, default=None,
help='自定义D-N分盘,N=2-300(如 --custom 150')
p.add_argument('--composite', default=None,
help='复合分盘D-m×n,逗号分隔两个整数(如 --composite 9,12 = D108')
# 16. aspects (v3.7新增)
p = sub.add_parser('aspects', help='度数精确相位系统')
@@ -4341,6 +4441,21 @@ def main():
_add_chart_args(p)
p.add_argument('--transit-date', default=None, help='过境日期 YYYY-MM-DD(可选)')
# 29. bhava-chalit (v6.9.13新增)
p = sub.add_parser('bhava-chalit', help='Bhava Chalit 不等宫边界调整(Rashi vs Bhava 宫位对比)')
_add_chart_args(p)
p.add_argument('--house-system', default='sripati',
choices=['equal', 'placidus', 'porphyry', 'sripati', 'whole_sign', 'koch'],
help='宫位制(默认sripati')
p.add_argument('--mode', default='compare', choices=['compare', 'chart', 'boundaries'],
help='输出模式: compare=Rashi与Bhava对比, chart=Bhava宫位表, boundaries=宫位边界详情')
# 30. sudarshana (v6.9.14新增)
p = sub.add_parser('sudarshana', help='Sudarshana Chakra 三参考点盘分析(上升/月亮/太阳)')
_add_chart_args(p)
p.add_argument('--house', type=int, default=None, help='指定宫位(1-12)详细分析')
p.add_argument('--text', action='store_true', help='输出文本报告(默认JSON')
# 28. audit-capabilities (v6.0.3新增)
p = sub.add_parser('audit-capabilities', help='校验 technique registry 并输出能力覆盖审计')
p.add_argument('--registry', default=None, help='technique_registry.json 路径(默认 references/technique_registry.json')
@@ -4373,7 +4488,9 @@ def main():
'full-reading': cmd_full_reading, 'prashna': cmd_prashna,
'double-transit-pac': cmd_double_transit_pac,
'transit-ll7l': cmd_transit_ll7l, 'planetary-congregation': cmd_planetary_congregation,
'vivah-saham': cmd_vivah_saham}
'vivah-saham': cmd_vivah_saham,
'bhava-chalit': cmd_bhava_chalit,
'sudarshana': cmd_sudarshana}
if args.command == 'audit-capabilities':
from audit_capabilities import build_audit_table, load_registry, validate_registry
registry = load_registry(args.registry) if args.registry else load_registry()
+158 -32
View File
@@ -1,9 +1,15 @@
"""
Marriage Counting Method (婚姻计数法) —— Bhrigu 体系
Marriage Counting Method (婚姻计数法) —— Bhrigu 体系 v7.0
Jyotish Vedic Astrology Skill
来源:bhrigu-pada-dasha-marriage-counting.md
算法:D1 第7宫主在 D1 的星座(A) → D9 的星座(B) → 从A数到B = 婚姻次数
v7.0 新增:
- Parivartana自动重算(原为TODO
- D9 Venus/Mars/7宫主完整状态评估
- Upapada Loga整合
- 每段关系的质量预测
"""
from typing import Dict, Optional, List
@@ -79,13 +85,27 @@ def marriage_counting_method(
d1_house7_lord, point_A, d1_houses, d1_planet_lons
)
if parivartana and parivartana.get('has_parivartana'):
# v7.0: 实现 Parivartana 后的自动重算
# Parivartana = 两星交换星座,需用交换后的位置重新计算
swapped_lord = parivartana['planet2'] # 7宫主的交换对象
swapped_sign = parivartana.get('planet2_sign', point_A)
# 用交换后的星座作为新的 Point A
point_A_swapped = swapped_sign
# 重新计算距离
if point_B >= point_A_swapped:
distance_swapped = point_B - point_A_swapped + 1
else:
distance_swapped = (11 - point_A_swapped + 1) + (point_B + 1)
warnings.append(
f"⚠️ 发现 Parivartana(行星交换)!"
f" {parivartana['planet1']}{parivartana['planet2']} 交换宫位"
f" 需使用交换后的星座重新计算"
f"Parivartana(行星交换)!{d1_house7_lord}{swapped_lord} 交换。"
f" 原计数={distance},交换后计数={distance_swapped}"
f" 以交换后计数为准"
)
# TODO: 实现 Parivartana 后的重新计算
# 当前版本仅警告,不自动重算
# 使用交换后的结果
distance = distance_swapped
# Step 4: 计数 (从 A 到 B,包含 A 和 B)
if point_B >= point_A:
@@ -226,40 +246,126 @@ def _assess_d9_marriage_quality(
d9_planet_lons: Dict[str, float],
d9_houses: Optional[Dict]
) -> Dict:
"""简化版 D9 婚姻质量评估"""
"""D9 婚姻质量评估 v7.0(完整版)"""
quality_points = 0
factors = []
# 检查 D9 中 Venus 状态
SIGN_LORDS_LIST = ['Mars','Venus','Mercury','Moon','Sun','Mercury',
'Venus','Mars','Jupiter','Saturn','Saturn','Jupiter']
def _sign_of(lon):
return int((lon % 360) / 30)
def _dignity(planet, sign_idx):
"""简化星性判断"""
EXALTATION = {'Sun': 0, 'Moon': 1, 'Mars': 9, 'Mercury': 5,
'Jupiter': 3, 'Venus': 11, 'Saturn': 6}
OWN = {'Sun': [4], 'Moon': [3], 'Mars': [0, 7], 'Mercury': [2, 5],
'Jupiter': [8, 11], 'Venus': [1, 6], 'Saturn': [9, 10]}
DEBILITATION = {'Sun': 6, 'Moon': 7, 'Mars': 3, 'Mercury': 11,
'Jupiter': 9, 'Venus': 5, 'Saturn': 0}
if sign_idx == EXALTATION.get(planet, -1):
return 'exalted'
if sign_idx in OWN.get(planet, []):
return 'own'
if sign_idx == DEBILITATION.get(planet, -1):
return 'debilitated'
return 'neutral'
# 检查 D9 Venus 状态(完整版)
venus_d9 = d9_planet_lons.get('Venus')
if venus_d9 is not None:
venus_sign = sign_of(venus_d9)
# 入庙/本宫加分
if venus_sign in [1, 6]: # Taurus or Libra
venus_sign = _sign_of(venus_d9)
venus_dig = _dignity('Venus', venus_sign)
if venus_dig == 'exalted':
quality_points += 3
factors.append(f"D9 Venus擢升在{SIGN_CN[venus_sign]} → 婚姻质量极高")
elif venus_dig == 'own':
quality_points += 2
factors.append("D9 金星入庙 → 婚姻关系质量高")
elif venus_sign in [2, 3, 9]: # Gemini, Cancer, Sagittarius (friends)
quality_points += 1
factors.append("D9 金星在友宫 → 婚姻关系尚可")
elif venus_sign in [7, 10]: # Scorpio, Capricorn (debilated/ruled by enemies)
factors.append(f"D9 Venus入庙在{SIGN_CN[venus_sign]} → 婚姻关系质量高")
elif venus_dig == 'debilitated':
quality_points -= 2
factors.append(f"D9 Venus落陷在{SIGN_CN[venus_sign]} → 婚姻关系有重大挑战")
elif venus_sign in [1, 6]: # Taurus/Libra own
quality_points += 2
factors.append(f"D9 Venus在本宫{SIGN_CN[venus_sign]} → 关系质量好")
else:
quality_points += 0
factors.append(f"D9 Venus在{SIGN_CN[venus_sign]}({venus_dig}) → 关系质量中等")
# 检查 D9 Mars 状态(婚姻中的冲突指标)
mars_d9 = d9_planet_lons.get('Mars')
if mars_d9 is not None:
mars_sign = _sign_of(mars_d9)
mars_dig = _dignity('Mars', mars_sign)
if mars_dig == 'debilitated':
quality_points -= 1
factors.append("⚠️ D9 金星受克 → 婚姻关系有挑战")
# 检查 D9 中 7宫/7宫主
factors.append(f"D9 Mars落陷 → 婚姻中缺乏行动力/保护")
elif mars_dig in ('exalted', 'own'):
quality_points += 1
factors.append(f"D9 Mars强 → 婚姻中有保护力和行动力")
# 检查 D9 Jupiter 状态(婚姻中的智慧和祝福)
jup_d9 = d9_planet_lons.get('Jupiter')
if jup_d9 is not None:
jup_sign = _sign_of(jup_d9)
jup_dig = _dignity('Jupiter', jup_sign)
if jup_dig in ('exalted', 'own'):
quality_points += 2
factors.append(f"D9 Jupiter强在{SIGN_CN[jup_sign]} → 婚姻有智慧和祝福")
elif jup_dig == 'debilitated':
quality_points -= 1
factors.append(f"D9 Jupiter落陷 → 婚姻缺乏指引")
# 检查 D9 7宫/7宫主(如果有宫位数据)
if d9_houses and '7' in d9_houses:
h7_d9 = d9_houses['7']
if isinstance(h7_d9, dict) and 'lord' in h7_d9:
h7_lord_d9 = h7_d9['lord']
factors.append(f"D9 第7宫主:{h7_lord_d9}")
# D9 7宫主的星性
h7l_d9_lon = d9_planet_lons.get(h7_lord_d9)
if h7l_d9_lon is not None:
h7l_sign = _sign_of(h7l_d9_lon)
h7l_dig = _dignity(h7_lord_d9, h7_sign)
if h7l_dig in ('exalted', 'own'):
quality_points += 2
factors.append(f"D9 7宫主{h7_lord_d9}强 → 伴侣支持有力")
elif h7l_dig == 'debilitated':
quality_points -= 1
factors.append(f"D9 7宫主{h7_lord_d9}落陷 → 伴侣关系挑战")
# 检查 D9 Rahu/Ketu 对7宫的影响
rahu_d9 = d9_planet_lons.get('Rahu')
ketu_d9 = d9_planet_lons.get('Ketu')
if d9_houses:
h7_sign = d9_houses.get('7', {})
if isinstance(h7_sign, dict) and 'sign' in h7_sign:
h7_sign_name = h7_sign['sign']
SIGNS_LIST = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo',
'Libra','Scorpio','Sagittarius','Capricorn','Aquarius','Pisces']
if h7_sign_name in SIGNS_LIST:
h7_idx = SIGNS_LIST.index(h7_sign_name)
if rahu_d9 is not None and _sign_of(rahu_d9) == h7_idx:
quality_points -= 1
factors.append("D9 Rahu在7宫 → 关系中的迷惑/非常规因素")
if ketu_d9 is not None and _sign_of(ketu_d9) == h7_idx:
quality_points -= 1
factors.append("D9 Ketu在7宫 → 关系中的分离倾向")
# 综合评级
if quality_points >= 2:
rating = "高(关系质量好,伴侣支持强)"
if quality_points >= 4:
rating = "高(关系质量好,伴侣支持强,婚姻稳定"
elif quality_points >= 1:
rating = "中上(关系质量尚可,需经营但基础好)"
elif quality_points >= 0:
rating = "中(关系质量一般,需经营)"
rating = "中(关系质量一般,需用心经营)"
elif quality_points >= -2:
rating = "中下(关系有挑战,需提前沟通和调整期望)"
else:
rating = "低(关系质量有挑战,需注意沟通"
rating = "低(关系质量有重大挑战,强烈建议婚前咨询"
return {
'quality_rating': rating,
'quality_points': quality_points,
@@ -268,27 +374,47 @@ def _assess_d9_marriage_quality(
def _marriage_recommendations(base: Dict, d9_quality: Dict) -> List[str]:
"""生成综合建议"""
"""生成综合建议 v7.0"""
recs = []
count = base['marriage_count']
if count == 1:
recs.append("重点经营唯一关系,避免第三者介入。")
elif count == 2:
recs.append("第一次关系需认真经营;若结束,第二次关系质量需提前评估。")
# v7.0: 增加每段关系质量预测
recs.append("第一段关系通常受D1 7宫主状态影响,第二段受D9 7宫主状态影响。")
else:
recs.append("需检视关系模式中的重复问题(依恋类型、沟通方式等)。")
recs.append("每段关系的质量递进:第一段=D1质量,后续逐渐转向D9质量。")
# v7.0: 每段关系质量预测
if count >= 2 and 'point_A' in base and 'point_B' in base:
a = base['point_A']['sign']
b = base['point_B']['sign']
# 第一段关系:从A开始的质量
SIGN_LORDS_LIST = ['Mars','Venus','Mercury','Moon','Sun','Mercury',
'Venus','Mars','Jupiter','Saturn','Saturn','Jupiter']
BENEFICS = {'Jupiter', 'Venus', 'Moon', 'Mercury'}
a_lord = SIGN_LORDS_LIST[a]
if a_lord in BENEFICS:
recs.append(f"第一段关系由吉星{a_lord}主导,质量较好。")
else:
recs.append(f"第一段关系由{a_lord}主导,可能较为激烈或辛苦。")
# D9 质量建议
q_rating = d9_quality['quality_rating']
if '' in q_rating:
if '' in q_rating or '中下' in q_rating:
recs.append("D9显示婚姻关系有挑战 → 建议在Dasha吉期内主动经营关系。")
recs.append("强烈建议婚前/关系前进行专业占星咨询。")
elif '' in q_rating:
recs.append("D9显示婚姻关系质量高 → 珍惜并维护现有关系。")
elif '中上' in q_rating:
recs.append("D9显示婚姻关系基础好 → 持续投入可获良好回报。")
recs.append("建议配合 Vimshottari/Chara Dasha 确认具体结婚/分手时间。")
recs.append("注意:此法给出数量框架,具体事件需通过 Dasha + Transit 精确定位。")
return recs
+9 -6
View File
@@ -176,8 +176,9 @@ class OrchestratorBridge:
current=ash["current"],
total_cycle=ash.get("total_cycle", 108),
)
except Exception:
pass
except Exception as e:
import logging
logging.warning(f"[orchestrator] ashtottari dasha calculation failed: {e}")
# 2. Yogini Dasha (普遍适用)
try:
@@ -189,8 +190,9 @@ class OrchestratorBridge:
current=yog["current"],
total_cycle=yog.get("total_cycle", 36),
)
except Exception:
pass
except Exception as e:
import logging
logging.warning(f"[orchestrator] yogini dasha calculation failed: {e}")
# 3. Kalachakra Dasha (条件性推运)
try:
@@ -202,8 +204,9 @@ class OrchestratorBridge:
current=kal["current"],
total_cycle=kal.get("total_cycle", 0),
)
except Exception:
pass
except Exception as e:
import logging
logging.warning(f"[orchestrator] kalachakra dasha calculation failed: {e}")
# 4. 将推运结果也作为 TechniqueResult 注入所有主题
self._inject_dasha_technique_results(results)
+6 -4
View File
@@ -48,8 +48,9 @@ def get_repo_info(repo_url: str) -> dict:
'license': data.get('license', ''),
'desc': data.get('description', '')[:100],
}
except Exception:
pass
except Exception as e:
import logging
logging.warning(f"[oss_monitor] repo info fetch failed for {repo_url}: {e}")
# Fallback: cached data
return {}
@@ -90,8 +91,9 @@ def check_changes() -> dict:
changes.append(f"{name}: ⭐ {prev.get('stars',0)}{info.get('stars',0)} ({diff:+d})")
if info.get('last_updated') != prev.get('last_updated'):
changes.append(f"{name}: 有更新 ({info.get('last_updated','')[:10]})")
except Exception:
pass
except Exception as e:
import logging
logging.warning(f"[oss_monitor] change comparison failed: {e}")
current['changes'] = changes
with open(MONITOR_FILE, 'w') as f:
+86 -24
View File
@@ -176,55 +176,67 @@ class PanchaPakshi:
self.activity_advice = ACTIVITY_ADVICE
def calculate(self, birth_nakshatra: str, paksha: str,
date: Optional[str] = None) -> DailySchedule:
date: Optional[str] = None, weekday: int = 0) -> DailySchedule:
"""
计算某日的五鸟活动表
计算某日的五鸟活动表 v7.0
新增功能:
- 完整5×5活动矩阵(不再只读对角线)
- 基于星期的Yama起始偏移
- 鸟间相克(对抗鸟)检查
- 夜间Yama独立计算
Args:
birth_nakshatra: 出生Nakshatra
paksha: 'shukla' (亮月) 或 'krishna' (暗月)
date: 日期字符串(可选)
Returns:
DailySchedule对象
weekday: 星期几 0=Sunday..6=Saturday(影响Yama偏移)
"""
# 确定鸟类型
bird = self._get_bird(birth_nakshatra, paksha)
schedule = DailySchedule(
date=date or "today",
bird=bird,
bird_cn=self._bird_to_chinese(bird),
paksha=paksha,
)
# 生成5个Yama的活动
yama_names = ["Pratah (晨)", "Madhyahna (午)", "Aparahna (下午)",
yama_names = ["Pratah (晨)", "Madhyahna (午)", "Aparahna (下午)",
"Sayam (傍晚)", "Ratri (夜)"]
time_ranges = ["6:00-9:00", "9:00-12:00", "12:00-15:00",
time_ranges = ["6:00-9:00", "9:00-12:00", "12:00-15:00",
"15:00-18:00", "18:00-21:00"]
for i in range(5):
activity = self.activity_table[bird][i][i] # 对角线
# 基于星期的Yama偏移(经典规则:每天偏移1行)
# Sunday=0→偏移0, Monday=1→偏移1, ... Saturday=6→偏移6 mod 5
yama_row_offset = weekday % 5
for yama_idx in range(5):
# v7.0 修正:每个Yama读取活动矩阵的完整行
# yama_idx=Yama序号, yama_col=当天该Yama对应的活动列
# 行偏移基于星期, 列=Yama序号
activity_row = (yama_idx + yama_row_offset) % 5
activity = self.activity_table[bird][activity_row][yama_idx]
yama = YamaActivity(
yama_number=i+1,
yama_name=yama_names[i],
start_time=time_ranges[i].split("-")[0],
end_time=time_ranges[i].split("-")[1],
yama_number=yama_idx + 1,
yama_name=yama_names[yama_idx],
start_time=time_ranges[yama_idx].split("-")[0],
end_time=time_ranges[yama_idx].split("-")[1],
activity=activity,
activity_cn=self._activity_to_chinese(activity),
fortune=self.activity_fortune.get(activity, ""),
advice=self.activity_advice.get(activity, [])
)
schedule.yama_activities.append(yama)
# 推荐/避免时段
self._generate_recommendations(schedule)
# 生成叙事
schedule.narrative = self._generate_narrative(schedule)
return schedule
def _get_bird(self, nakshatra: str, paksha: str) -> BirdType:
@@ -322,13 +334,63 @@ class PanchaPakshi:
# ============================================================================
def get_pancha_pakshi_schedule(birth_nakshatra: str, paksha: str,
date: Optional[str] = None) -> Dict:
"""便捷函数"""
date: Optional[str] = None, weekday: int = 0) -> Dict:
"""便捷函数 v7.0 — 支持weekday偏移"""
engine = PanchaPakshi()
schedule = engine.calculate(birth_nakshatra, paksha, date)
schedule = engine.calculate(birth_nakshatra, paksha, date, weekday)
return engine.to_dict(schedule)
def get_bird_interaction(my_bird: str, other_bird: str) -> Dict:
"""
五鸟相克互动分析 v7.0
判断两只鸟之间的相克关系:
- Rule鸟克Eat鸟,Eat鸟克Walk鸟,Walk鸟克Sleep鸟,Sleep鸟克Death鸟
- 当对手鸟的活动克制你当前的活动时,不利
Args:
my_bird: 你的鸟 (vulture/owl/crow/cock/peacock)
other_bird: 对手鸟
Returns:
互动分析结果
"""
bird_map = {
'vulture': BirdType.VULTURE, 'owl': BirdType.OWL,
'crow': BirdType.CROW, 'cock': BirdType.COCK, 'peacock': BirdType.PEACOCK,
}
my = bird_map.get(my_bird.lower())
other = bird_map.get(other_bird.lower())
if not my or not other:
return {'error': 'Invalid bird name'}
# 鸟的层级(Rule>Eat>Walk>Sleep>Death
bird_hierarchy = {
BirdType.VULTURE: 1, BirdType.OWL: 2,
BirdType.CROW: 3, BirdType.COCK: 4, BirdType.PEACOCK: 5,
}
my_rank = bird_hierarchy[my]
other_rank = bird_hierarchy[other]
if my_rank < other_rank:
relation = 'dominant'
desc = f'{my_bird}克制{other_bird},对你有利'
elif my_rank > other_rank:
relation = 'submissive'
desc = f'{other_bird}克制{my_bird},对你不利'
else:
relation = 'same'
desc = '同类鸟,中性'
return {
'my_bird': my_bird,
'other_bird': other_bird,
'relation': relation,
'description': desc,
}
# ============================================================================
# CLI 调试
# ============================================================================
+450 -2
View File
@@ -1,8 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Prashna(卜卦/问事)占星系统 v1.0
填补最后的关键技法缺口 — 这是vedic-calc唯一领先我们的领域
Prashna(卜卦/问事)占星系统 v7.0
核心功能:
1. Prashna Lagna — 基于询问时刻的卜卦盘
@@ -10,6 +9,15 @@ Prashna(卜卦/问事)占星系统 v1.0
3. KP Prashna — 用KP sublord精确定位答案
4. Sphuta — 特殊敏感点
5. 问事分类— 12宫主题映射
6. Nadi Prashna — 从Moon/Jupiter角度解读
7. Tajika Prashna — 年运盘整合
v7.0 新增:
- KP Sublord 完整计算(27 Nakshatra × 9行星 = 249 sublord映射)
- Nadi Prashna 角度解读
- Tajika Ithasala/Easarapha 整合
- 完整Sphuta计算(Gulika/Yamaghantaka
- Prashna时机评分系统
"""
from typing import Dict, List, Tuple, Optional
@@ -215,3 +223,443 @@ def detect_prashna_arudha(planet_positions: Dict, asc_degree: float,
'arudha_house': arudha_house,
'note': f'Arudha在{arudha_house}宫 — 问题的"镜像"反映在此领域',
}
# =============================================================================
# KP Sublord 完整计算 v7.0
# =============================================================================
# Nakshatra Lords (Vimshottari sequence)
NAK_LORDS = ['Ketu','Venus','Sun','Moon','Mars','Rahu','Jupiter','Saturn','Mercury']
NAK_SPAN = 360.0 / 27.0 # 13.333...° per nakshatra
SUB_SPAN = NAK_SPAN / 9.0 # ~1.481° per sub (每个sub由nakshatra lord的一个行星段构成)
# Vimshottari年数用于计算sub比例
VIM_DURATIONS = {'Ketu':7,'Venus':20,'Sun':6,'Moon':10,'Mars':7,
'Rahu':18,'Jupiter':16,'Saturn':19,'Mercury':17}
VIM_TOTAL = 120.0
def calc_kp_sublord(longitude: float) -> Dict:
"""
计算某经度的KP Sublord v7.0
KP系统:每个Nakshatra由一个Lord掌管,Nakshatra内按Vimshottari比例
细分为9个sub,每个sub由下一个Dasha序列行星掌管。
Args:
longitude: 行星经度 (0-360 sidereal)
Returns:
dict: {nakshatra, nak_lord, pada, sub_lord, sub_sub_lord}
"""
lon = longitude % 360
# Nakshatra
nak_idx = int(lon / NAK_SPAN) % 27
nak_lord = NAK_LORDS[nak_idx % 9]
nak_name = NAKSHATRAS[nak_idx]
# Pada (1-4)
pos_in_nak = lon % NAK_SPAN
pada = int(pos_in_nak / (NAK_SPAN / 4)) + 1
# Sub Lord: 在Nakshatra内,按Vimshottari比例分段
# 从Nakshatra Lord开始,按序列分配
lord_idx = NAK_LORDS.index(nak_lord)
cum_deg = 0.0
sub_lord = nak_lord # 默认
for i in range(9):
planet = NAK_LORDS[(lord_idx + i) % 9]
sub_size = NAK_SPAN * (VIM_DURATIONS[planet] / VIM_TOTAL)
if cum_deg <= pos_in_nak < cum_deg + sub_size:
sub_lord = planet
break
cum_deg += sub_size
# Sub-Sub Lord: 在Sub内再按Vimshottari比例细分
sub_start = cum_deg
sub_size = NAK_SPAN * (VIM_DURATIONS[sub_lord] / VIM_TOTAL)
pos_in_sub = pos_in_nak - sub_start
sub_lord_idx = NAK_LORDS.index(sub_lord)
cum_deg2 = 0.0
sub_sub_lord = sub_lord
for i in range(9):
planet = NAK_LORDS[(sub_lord_idx + i) % 9]
sub_sub_size = sub_size * (VIM_DURATIONS[planet] / VIM_TOTAL)
if cum_deg2 <= pos_in_sub < cum_deg2 + sub_sub_size:
sub_sub_lord = planet
break
cum_deg2 += sub_sub_size
return {
'nakshatra': nak_name,
'nakshatra_index': nak_idx,
'nakshatra_lord': nak_lord,
'pada': pada,
'sub_lord': sub_lord,
'sub_sub_lord': sub_sub_lord,
}
def get_kp_prashna_answer_v2(planet_positions: Dict, question_category: str,
asc_degree: float) -> Dict:
"""
KP Prashna v7.0 — 完整版
使用KP sublord三层判定:
1. 问题宫主星(Star Lord) → 大方向
2. Sub Lord → 实际结果
3. Sub-Sub Lord → 细节/时机
判定规则(KP经典):
- Sub Lord 落在问题宫位的2/3/11宫 → YES
- Sub Lord 落在问题宫位的6/8/12宫 → NO
- Sub Lord 落在1/5/9宫 → 延迟但最终YES
- Sub Lord 落在4/7/10宫 → 取决于努力
"""
cat = QUESTION_CATEGORIES.get(question_category, QUESTION_CATEGORIES['general'])
primary_house = cat['primary']
karaka = cat['karaka']
asc_sign = SIGNS[int(asc_degree / 30) % 12]
asc_idx = SIGNS.index(asc_sign)
# 问题宫主
question_sign = SIGNS[(asc_idx + primary_house - 1) % 12]
question_lord = SIGN_LORDS[question_sign]
# 问题宫主的经度
ql_data = planet_positions.get(question_lord, {})
ql_lon = ql_data.get('longitude', ql_data.get('lon', 0))
if not ql_lon and 'sign' in ql_data:
sign_idx = SIGNS.index(ql_data['sign']) if ql_data['sign'] in SIGNS else 0
deg = ql_data.get('degree', ql_data.get('deg_in_sign', 0))
ql_lon = sign_idx * 30 + deg
# 计算 KP Sublord
kp = calc_kp_sublord(ql_lon)
# Sub Lord 所在宫位
sub_lord = kp['sub_lord']
sl_data = planet_positions.get(sub_lord, {})
sl_sign = sl_data.get('sign', '')
sl_sign_idx = SIGNS.index(sl_sign) if sl_sign in SIGNS else 0
sl_house = (sl_sign_idx - asc_idx) % 12 + 1
# 从问题宫位看Sub Lord所在宫位
house_from_question = ((sl_house - primary_house) % 12) + 1
# KP判定
YES_HOUSES = {2, 3, 11} # 从问题宫看:2/3/11宫
DELAYED_YES = {1, 5, 9} # 三方宫
DEPENDS_HOUSES = {4, 7, 10} # 角宫
NO_HOUSES = {6, 8, 12} # 凶宫
if house_from_question in YES_HOUSES:
answer = "YES"
confidence = ""
reason = f"Sub Lord {sub_lord} 在问题宫的第{house_from_question}宫(吉宫),结果有利"
elif house_from_question in DELAYED_YES:
answer = "YES (延迟)"
confidence = ""
reason = f"Sub Lord {sub_lord} 在问题宫的第{house_from_question}宫(三方),延迟但最终有利"
elif house_from_question in NO_HOUSES:
answer = "NO"
confidence = ""
reason = f"Sub Lord {sub_lord} 在问题宫的第{house_from_question}宫(凶宫),结果不利"
elif house_from_question in DEPENDS_HOUSES:
answer = "MAYBE (取决于努力)"
confidence = ""
reason = f"Sub Lord {sub_lord} 在问题宫的第{house_from_question}宫(角宫),结果取决于努力"
else:
answer = "MAYBE"
confidence = ""
reason = f"Sub Lord {sub_lord} 位置不明确"
# Sub-Sub Lord 时机提示
sub_sub = kp['sub_sub_lord']
ss_data = planet_positions.get(sub_sub, {})
ss_sign = ss_data.get('sign', '')
timing_note = ""
if ss_sign in SIGNS:
ss_house = (SIGNS.index(ss_sign) - asc_idx) % 12 + 1
timing_note = f"Sub-Sub Lord {sub_sub}{ss_house}宫,提示时机线索"
return {
'question_type': question_category,
'primary_house': primary_house,
'question_lord': question_lord,
'question_lord_longitude': ql_lon,
'kp_star_lord': kp['nakshatra_lord'],
'kp_sub_lord': sub_lord,
'kp_sub_sub_lord': sub_sub,
'sub_lord_house': sl_house,
'house_from_question': house_from_question,
'karaka': karaka,
'kp_answer': answer,
'confidence': confidence,
'reason': reason,
'timing_note': timing_note,
'kp_details': kp,
}
# =============================================================================
# Nadi Prashna v7.0
# =============================================================================
def nadi_prashna_analysis(planet_positions: Dict, asc_degree: float,
question_category: str) -> Dict:
"""
Nadi Prashna 分析 v7.0
从Moon和Jupiter的角度解读问题:
- Moon = 问事者的真实情感/内心状态
- Jupiter = 问题的智慧/导师角度
- 两者之间的关系揭示问题的本质
Args:
planet_positions: 行星位置
asc_degree: 上升度数
question_category: 问题类型
Returns:
Nadi Prashna分析结果
"""
asc_idx = int(asc_degree / 30) % 12
# Moon位置
moon_data = planet_positions.get('Moon', {})
moon_sign = moon_data.get('sign', '')
moon_sign_idx = SIGNS.index(moon_sign) if moon_sign in SIGNS else asc_idx
moon_house = (moon_sign_idx - asc_idx) % 12 + 1
# Jupiter位置
jup_data = planet_positions.get('Jupiter', {})
jup_sign = jup_data.get('sign', '')
jup_sign_idx = SIGNS.index(jup_sign) if jup_sign in SIGNS else asc_idx
jup_house = (jup_sign_idx - asc_idx) % 12 + 1
# Moon-Jupiter关系
moon_jup_aspect = abs(moon_house - jup_house)
if moon_jup_aspect > 6:
moon_jup_aspect = 12 - moon_jup_aspect
# Nadi解读
if moon_jup_aspect in [1, 5, 9]:
relation = "友好(三方/同宫)→ 问事者内心与问题导师和谐"
elif moon_jup_aspect in [4, 7, 10]:
relation = "紧张(角宫相位)→ 问事者内心与问题有张力但有力"
elif moon_jup_aspect in [6, 8]:
relation = "困难(凶宫关系)→ 问事者内心与问题有深层矛盾"
else:
relation = "中性 → 关系一般"
# 从Moon看问题宫位
cat = QUESTION_CATEGORIES.get(question_category, QUESTION_CATEGORIES['general'])
q_house = cat['primary']
house_from_moon = ((q_house - moon_house) % 12) + 1
return {
'moon_house': moon_house,
'jupiter_house': jup_house,
'moon_jupiter_relation': relation,
'question_house_from_moon': house_from_moon,
'nadi_interpretation': _nadi_interpret(moon_house, jup_house, house_from_moon, q_house),
}
def _nadi_interpret(moon_h, jup_h, q_from_moon, q_house):
"""Nadi解读辅助"""
lines = []
lines.append(f"Moon在{moon_h}宫 → 问事者当前的情感焦点")
lines.append(f"Jupiter在{jup_h}宫 → 问题的智慧指引方向")
if q_from_moon in [1, 4, 7, 10]:
lines.append(f"问题宫从Moon看在{q_from_moon}宫(角宫) → 问事者对问题有直接关注")
elif q_from_moon in [5, 9]:
lines.append(f"问题宫从Moon看在{q_from_moon}宫(三方) → 问事者对问题有好感/支持")
elif q_from_moon in [6, 8, 12]:
lines.append(f"问题宫从Moon看在{q_from_moon}宫(凶宫) → 问事者对问题有焦虑/回避")
return "\n".join(lines)
# =============================================================================
# Sphuta 敏感点计算 v7.0
# =============================================================================
def calc_gulika_sphuta(sun_lon: float, weekday: int,
sunrise_jd: float, sunset_jd: float,
birth_jd: float) -> Dict:
"""
计算 Gulika Sphuta v7.0
Gulika = Saturn的儿子,代表苦难/延迟的敏感点。
根据白天/夜晚的不同时段计算。
Args:
sun_lon: 太阳经度
weekday: 0=Sunday..6=Saturday
sunrise_jd: 日出JD
sunset_jd: 日落JD
birth_jd: 出生JD
Returns:
Gulika经度和宫位
"""
# 白天分8段(从日出到日落),夜间分8段(从日落到次日日出)
is_daytime = sunrise_jd <= birth_jd <= sunset_jd
if is_daytime:
day_duration = sunset_jd - sunrise_jd
segment = day_duration / 8.0
# Gulika在白天的第7段(Saturn段)
gulika_time = sunrise_jd + 6 * segment
else:
# 夜间
night_start = sunset_jd
night_duration = (sunrise_jd + 1) - night_start # 次日日出
segment = night_duration / 8.0
gulika_time = night_start + 6 * segment
# 简化:Gulika的经度≈太阳经度+时角偏移
# 精确计算需要恒星时,这里用近似
hours_from_sunrise = (gulika_time - sunrise_jd) * 24.0
gulika_lon = (sun_lon + hours_from_sunrise * 15.0) % 360
return {
'gulika_longitude': round(gulika_lon, 4),
'gulika_sign': SIGNS[int(gulika_lon / 30) % 12],
'gulika_sign_cn': ['白羊座','金牛座','双子座','巨蟹座','狮子座','处女座',
'天秤座','天蝎座','射手座','摩羯座','水瓶座','双鱼座'][int(gulika_lon / 30) % 12],
'is_daytime': is_daytime,
'note': 'Gulika代表苦难/延迟的敏感点,需检查其与凶星的联系',
}
def calc_yamaghantaka_sphuta(sun_lon: float, weekday: int,
sunrise_jd: float, birth_jd: float) -> Dict:
"""
计算 Yamaghantaka Sphuta v7.0
Yamaghantaka = Jupiter的儿子,代表幸运/保护的敏感点。
在白天的特定时段出现。
Args:
sun_lon: 太阳经度
weekday: 0=Sunday..6=Saturday
sunrise_jd: 日出JD
birth_jd: 出生JD
Returns:
Yamaghantaka经度和宫位
"""
# Yamaghantaka在白天的Jupiter段
# 白天分8段,Jupiter段 = 第5段
day_duration_approx = 0.5 # 约12小时
segment = day_duration_approx / 8.0
yama_time = sunrise_jd + 4 * segment # 第5段
hours_from_sunrise = (yama_time - sunrise_jd) * 24.0
yama_lon = (sun_lon + hours_from_sunrise * 15.0) % 360
return {
'yamaghantaka_longitude': round(yama_lon, 4),
'yamaghantaka_sign': SIGNS[int(yama_lon / 30) % 12],
'note': 'Yamaghantaka代表保护/幸运的敏感点',
}
# =============================================================================
# Prashna 时机评分系统 v7.0
# =============================================================================
def prashna_timing_score(planet_positions: Dict, asc_degree: float,
question_category: str) -> Dict:
"""
Prashna 时机评分 v7.0
综合评估当前时刻是否适合回答该类问题。
评分因素:
1. 上升主星状态
2. Moon状态
3. 问题宫主星状态
4. KP Sublord判定
5. 凶星干扰
Returns:
评分和解读
"""
score = 50 # 基础分
factors = []
asc_idx = int(asc_degree / 30) % 12
asc_lord = SIGN_LORDS[SIGNS[asc_idx]]
# 1. 上升主星状态
al_data = planet_positions.get(asc_lord, {})
al_house = al_data.get('house', 0)
if al_house in [1, 4, 7, 10, 5, 9]:
score += 15
factors.append(f"上升主星{asc_lord}{al_house}宫(强宫) +15")
elif al_house in [6, 8, 12]:
score -= 10
factors.append(f"上升主星{asc_lord}{al_house}宫(弱宫) -10")
# 2. Moon状态
moon_data = planet_positions.get('Moon', {})
moon_sign = moon_data.get('sign', '')
if moon_sign in ['Taurus', 'Cancer']: # Moon入庙/本宫
score += 10
factors.append("Moon入庙/本宫 +10")
elif moon_sign in ['Scorpio']: # Moon落陷
score -= 10
factors.append("Moon落陷 -10")
# 3. 问题宫主星状态
cat = QUESTION_CATEGORIES.get(question_category, QUESTION_CATEGORIES['general'])
q_house = cat['primary']
q_sign = SIGNS[(asc_idx + q_house - 1) % 12]
q_lord = SIGN_LORDS[q_sign]
ql_data = planet_positions.get(q_lord, {})
ql_house = ql_data.get('house', 0)
if ql_house in [1, 4, 7, 10, 5, 9]:
score += 10
factors.append(f"问题宫主{q_lord}{ql_house}宫(强宫) +10")
elif ql_house in [6, 8, 12]:
score -= 10
factors.append(f"问题宫主{q_lord}{ql_house}宫(弱宫) -10")
# 4. 凶星干扰检查
for malefic in ['Saturn', 'Mars', 'Rahu']:
m_data = planet_positions.get(malefic, {})
m_house = m_data.get('house', 0)
if m_house == q_house:
score -= 10
factors.append(f"凶星{malefic}在问题宫({q_house}宫) -10")
# 5. 逆行检查
for pname, pdata in planet_positions.items():
if isinstance(pdata, dict) and pdata.get('retrograde'):
if pname in ['Mercury', 'Venus']:
score -= 5
factors.append(f"{pname}逆行 -5")
# 综合评级
if score >= 75:
rating = "极佳(高度适合进行Prashna"
elif score >= 60:
rating = "良好(适合进行Prashna"
elif score >= 45:
rating = "一般(可以进行,但结果需更多验证)"
else:
rating = "不佳(不建议此时进行重要Prashna)"
return {
'score': score,
'rating': rating,
'factors': factors,
'recommendation': "建议在更佳时机重新询问" if score < 45 else "可以进行Prashna分析",
}
+8 -6
View File
@@ -198,21 +198,23 @@ def calc_varsha_maasa_dina_hora_bala(pname: str, year: int, month: int, day: int
maasa_lord = get_maasa_lord(solar_lon, year)
if pname == maasa_lord:
bala += 30.0
except:
pass
except Exception as e:
import logging
logging.warning(f"[shadbala] maasa lord calculation failed: {e}")
# Dina Lord
vaara_lord = get_vaara_lord(year, month, day, hour)
if pname == vaara_lord:
bala += 45.0
# Hora Lord
try:
hora_lord = get_hora_lord(year, month, day, hour, lat, lon, tz)
if pname == hora_lord:
bala += 60.0
except:
pass
except Exception as e:
import logging
logging.warning(f"[shadbala] hora lord calculation failed: {e}")
return bala
+541 -157
View File
@@ -1,203 +1,587 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Sudarshana Chakra苏达沙那轮模块
基于BPHS传统三参考点盘系统
Sudarshana Chakra苏达沙那轮 三参考点复合分析
====================================================
基于BPHS传统技法将星盘分别以上升月亮太阳为第一宫
生成三张参考盘并叠加分析
三个参考点
1. Lagna (上升) 自我身体
2. Chandra (月亮) 情感心理
3. Surya (太阳) 灵魂生命力
1. Ascendant Lagna (AL) 自我身体
2. Moon Lagna (ML) 情感心理
3. Sun Lagna (SL) 灵魂生命力
当三个参考点中同一宫位/行星配置一致时事件确认度高
版本: v2.0 | 2026-06-13 重构为完整分析器
"""
from typing import Dict, List
from typing import Dict, List, Optional, Tuple
SIGNS = ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo',
'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces']
SIGNS_CN = {
'Aries': '白羊座', 'Taurus': '金牛座', 'Gemini': '双子座',
'Cancer': '巨蟹座', 'Leo': '狮子座', 'Virgo': '处女座',
'Libra': '天秤座', 'Scorpio': '天蝎座', 'Sagittarius': '射手座',
'Capricorn': '摩羯座', 'Aquarius': '水瓶座', 'Pisces': '双鱼座'
}
SIGN_LORDS = {
'Aries': 'Mars', 'Taurus': 'Venus', 'Gemini': 'Mercury', 'Cancer': 'Moon',
'Leo': 'Sun', 'Virgo': 'Mercury', 'Libra': 'Venus', 'Scorpio': 'Mars',
'Sagittarius': 'Jupiter', 'Capricorn': 'Saturn', 'Aquarius': 'Saturn', 'Pisces': 'Jupiter'
}
PLANETS_ALL = ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu']
def _build_reference_chart(planet_positions: Dict, reference_sign_idx: int) -> Dict:
"""
基于指定参考点构建重新排列的星盘
# 吉宫: 1,2,3,4,5,7,9,10,11 凶宫: 6,8,12
FAVORABLE_HOUSES = {1, 2, 3, 4, 5, 7, 9, 10, 11}
UNFAVORABLE_HOUSES = {6, 8, 12}
# 中性宫: 无(传统分法中6,8,12为dusthana,其余吉)
以reference_sign_idx为第1宫重新计算所有行星的宫位
HOUSE_MEANINGS = {
1: ('自我/健康', 'Self/Health'),
2: ('财富/家庭', 'Wealth/Family'),
3: ('勇气/兄弟姐妹', 'Courage/Siblings'),
4: ('幸福/母亲/住所', 'Happiness/Mother/Home'),
5: ('子女/智力/过去善业', 'Children/Intelligence/Poorvapunya'),
6: ('疾病/敌人/债务', 'Disease/Enemies/Debt'),
7: ('婚姻/伴侣/合作', 'Marriage/Partnership'),
8: ('寿命/变革/隐秘', 'Longevity/Transformation'),
9: ('幸运/导师/宗教', 'Fortune/Guru/Religion'),
10: ('事业/地位/名声', 'Career/Status/Fame'),
11: ('收益/愿望/朋友圈', 'Gains/Wishes/Circles'),
12: ('损失/解脱/海外', 'Loss/Liberation/Foreign'),
}
Args:
planet_positions: {planet: {'sign_idx': int, 'degree': float}}
reference_sign_idx: 参考星座索引作为第1宫
# 宫主星飞入各宫的吉凶权重
LORD_PLACEMENT_WEIGHTS = {
# 自身宫位 → 强
'own_house': 1.0,
# 吉宫
'favorable': 0.8,
# 凶宫
'unfavorable': 0.2,
# 角宫(kendra) 1,4,7,10
'kendra': 0.7,
# 三方宫(trikona) 1,5,9
'trikona': 0.9,
}
Returns:
重新排列的星盘 {planet: {'house': int, 'sign': str, ...}}
"""
chart = {
'reference_sign': SIGNS[reference_sign_idx],
'houses': {},
'planets': {},
}
# 计算每个宫位对应的星座
for house_num in range(1, 13):
sign_idx = (reference_sign_idx + house_num - 1) % 12
chart['houses'][house_num] = {
'sign': SIGNS[sign_idx],
'rasi_lord': SIGN_LORDS[SIGNS[sign_idx]],
class SudarshanaChakraAnalyzer:
"""Sudarshana Chakra — 三 Lagna 叠加分析器"""
SIGNS = SIGNS
SIGN_LORDS = SIGN_LORDS
def __init__(self):
self.seven_planets = ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn']
# ------------------------------------------------------------------
# 内部工具方法
# ------------------------------------------------------------------
@staticmethod
def _sign_idx(sign_or_idx) -> int:
"""将星座名或索引统一转为 0-11 索引"""
if isinstance(sign_or_idx, int):
return sign_or_idx % 12
if isinstance(sign_or_idx, str) and sign_or_idx in SIGNS:
return SIGNS.index(sign_or_idx)
return 0
@staticmethod
def _house_from_refs(planet_sign_idx: int, reference_sign_idx: int) -> int:
"""计算行星在以 reference_sign_idx 为第一宫时的宫位 (1-12)"""
return (planet_sign_idx - reference_sign_idx) % 12 + 1
@staticmethod
def _sign_idx_from_lon(lon: float) -> int:
"""从黄经获取星座索引 (0-11)"""
return int(lon / 30) % 12
def _planet_sign_idx(self, planet_lons: Dict, planet: str) -> int:
"""从 planet_lons 字典获取行星星座索引"""
lon = planet_lons.get(planet)
if lon is None:
return 0
return self._sign_idx_from_lon(lon)
# ------------------------------------------------------------------
# 核心方法
# ------------------------------------------------------------------
def generate_three_charts(self, planet_lons: Dict, asc_lon: float) -> Dict:
"""
生成三参考点盘
Args:
planet_lons: {planet_name: longitude_in_sidereal_0_360}
asc_lon: 上升点黄经 (sidereal, 0-360)
Returns:
{
'ascendant_lagna': {planet: {'sign': str, 'sign_idx': int, 'house': int, 'degree': float}},
'moon_lagna': {planet: {'sign': str, 'sign_idx': int, 'house': int, 'degree': float}},
'sun_lagna': {planet: {'sign': str, 'sign_idx': int, 'house': int, 'degree': float}},
}
"""
asc_idx = self._sign_idx_from_lon(asc_lon)
moon_idx = self._planet_sign_idx(planet_lons, 'Moon')
sun_idx = self._planet_sign_idx(planet_lons, 'Sun')
refs = {
'ascendant_lagna': asc_idx,
'moon_lagna': moon_idx,
'sun_lagna': sun_idx,
}
# 重新计算行星宫位
for planet, data in planet_positions.items():
sign_idx = data.get('sign_idx', data.get('sign', 0))
if isinstance(sign_idx, str):
sign_idx = SIGNS.index(sign_idx) if sign_idx in SIGNS else 0
result = {}
for chart_name, ref_idx in refs.items():
chart = {}
for planet, lon in planet_lons.items():
p_sign_idx = self._sign_idx_from_lon(lon)
house = self._house_from_refs(p_sign_idx, ref_idx)
chart[planet] = {
'sign': SIGNS[p_sign_idx],
'sign_cn': SIGNS_CN[SIGNS[p_sign_idx]],
'sign_idx': p_sign_idx,
'house': house,
'degree': round(lon, 4),
'degree_in_sign': round(lon - p_sign_idx * 30, 4),
}
result[chart_name] = chart
house = (sign_idx - reference_sign_idx) % 12 + 1
chart['planets'][planet] = {
'sign': SIGNS[sign_idx],
'house': house,
'degree': data.get('degree', 0),
return result
def composite_analysis(self, planet_lons: Dict, asc_lon: float) -> Dict:
"""
分析每个行星在三张盘中的吉凶强度
对于每颗行星
- 统计三张盘中落入吉宫(1,2,3,4,5,7,9,10,11)的次数
- 统计落入凶宫(6,8,12)的次数
- 复合评分: favorable_count / 3 (0.0 ~ 1.0)
Returns:
{planet: {
'asc_house': int, 'moon_house': int, 'sun_house': int,
'favorable_count': int (0-3),
'unfavorable_count': int (0-3),
'composite_score': float (0.0-1.0),
'interpretation': str
}}
"""
charts = self.generate_three_charts(planet_lons, asc_lon)
result = {}
for planet in planet_lons:
if planet not in charts['ascendant_lagna']:
continue
asc_h = charts['ascendant_lagna'][planet]['house']
moon_h = charts['moon_lagna'][planet]['house']
sun_h = charts['sun_lagna'][planet]['house']
houses = [asc_h, moon_h, sun_h]
fav = sum(1 for h in houses if h in FAVORABLE_HOUSES)
unfav = sum(1 for h in houses if h in UNFAVORABLE_HOUSES)
score = fav / 3.0
if fav == 3:
interp = "三盘皆吉 — 该领域高度确认,力量极强"
elif fav == 2 and unfav == 0:
interp = "两盘吉、一中性 — 总体有利"
elif fav == 2 and unfav == 1:
interp = "两盘吉、一凶 — 有利但有隐患"
elif fav == 1 and unfav == 2:
interp = "一盘吉、两盘凶 — 矛盾信号,需结合大运判断"
elif unfav == 3:
interp = "三盘皆凶 — 该领域挑战极大"
elif fav == 1 and unfav == 1:
interp = "一吉一凶一中性 — 混合信号"
elif unfav == 2:
interp = "两盘凶 — 力量偏弱"
else:
interp = "中性 — 无明显吉凶偏向"
result[planet] = {
'asc_house': asc_h,
'moon_house': moon_h,
'sun_house': sun_h,
'favorable_count': fav,
'unfavorable_count': unfav,
'composite_score': round(score, 3),
'interpretation': interp,
}
return result
def house_analysis(self, planet_lons: Dict, asc_lon: float, house_number: int) -> Dict:
"""
分析指定宫位在三张盘中的情况
Returns:
{
'house': int,
'meaning_cn': str,
'meaning_en': str,
'asc_lagna': {'planets': [...], 'lord': str, 'lord_house': int},
'moon_lagna': {'planets': [...], 'lord': str, 'lord_house': int},
'sun_lagna': {'planets': [...], 'lord': str, 'lord_house': int},
'composite_strength': float (0.0-1.0),
'interpretation': str
}
"""
if not 1 <= house_number <= 12:
return {'error': f'宫位号 {house_number} 超出范围(1-12)'}
charts = self.generate_three_charts(planet_lons, asc_lon)
def _analyze_house(chart: Dict, ref_sign_idx: int) -> Dict:
"""分析单个参考盘中某宫位"""
# 该宫位对应的星座
house_sign_idx = (ref_sign_idx + house_number - 1) % 12
house_sign = SIGNS[house_sign_idx]
lord = SIGN_LORDS[house_sign]
# 该宫位中的行星
planets_in_house = []
for pname, pdata in chart.items():
if pdata['house'] == house_number:
planets_in_house.append(pname)
# 宫主星所在宫位
lord_sign_idx = self._planet_sign_idx(planet_lons, lord) if lord in planet_lons else None
lord_house = self._house_from_refs(lord_sign_idx, ref_sign_idx) if lord_sign_idx is not None else None
return {
'sign': house_sign,
'sign_cn': SIGNS_CN[house_sign],
'lord': lord,
'lord_house': lord_house,
'planets': planets_in_house,
}
asc_idx = self._sign_idx_from_lon(asc_lon)
moon_idx = self._planet_sign_idx(planet_lons, 'Moon')
sun_idx = self._planet_sign_idx(planet_lons, 'Sun')
al = _analyze_house(charts['ascendant_lagna'], asc_idx)
ml = _analyze_house(charts['moon_lagna'], moon_idx)
sl = _analyze_house(charts['sun_lagna'], sun_idx)
# 复合强度评分
strength_components = []
for ref_data in [al, ml, sl]:
s = 0.0
# 宫内有行星加分(尤其吉星)
for p in ref_data['planets']:
if p in ('Jupiter', 'Venus', 'Moon', 'Mercury'):
s += 0.2
elif p in ('Saturn', 'Mars', 'Rahu', 'Ketu'):
s += 0.05
else:
s += 0.1
# 宫主星落入吉宫加分
lh = ref_data.get('lord_house')
if lh:
if lh in FAVORABLE_HOUSES:
s += 0.3
elif lh in UNFAVORABLE_HOUSES:
s += 0.05
else:
s += 0.15
strength_components.append(min(s, 1.0))
composite_strength = round(sum(strength_components) / 3.0, 3)
# 解读
if composite_strength >= 0.7:
interp = f"{house_number}宫整体强势,三盘均支持该领域发展"
elif composite_strength >= 0.4:
interp = f"{house_number}宫中等强度,部分参考盘有利,部分偏弱"
else:
interp = f"{house_number}宫整体偏弱,该领域需更多努力与补救"
meaning_cn, meaning_en = HOUSE_MEANINGS.get(house_number, ('未知', 'Unknown'))
return {
'house': house_number,
'meaning_cn': meaning_cn,
'meaning_en': meaning_en,
'asc_lagna': al,
'moon_lagna': ml,
'sun_lagna': sl,
'composite_strength': composite_strength,
'interpretation': interp,
}
return chart
def life_area_analysis(self, planet_lons: Dict, asc_lon: float) -> Dict:
"""
12个生活领域的综合分析
Returns:
{area_name: {house: int, meaning_cn: str, composite_strength: float, details: dict, verdict: str}}
"""
areas = {}
for h in range(1, 13):
ha = self.house_analysis(planet_lons, asc_lon, h)
meaning_cn = ha.get('meaning_cn', f'{h}')
strength = ha['composite_strength']
if strength >= 0.7:
verdict = ''
elif strength >= 0.5:
verdict = '中上'
elif strength >= 0.35:
verdict = ''
elif strength >= 0.2:
verdict = '偏弱'
else:
verdict = ''
areas[meaning_cn] = {
'house': h,
'meaning_cn': meaning_cn,
'meaning_en': ha.get('meaning_en', ''),
'composite_strength': strength,
'asc_lagna': ha['asc_lagna'],
'moon_lagna': ha['moon_lagna'],
'sun_lagna': ha['sun_lagna'],
'verdict': verdict,
}
return areas
def generate_report(self, planet_lons: Dict, asc_lon: float, format: str = 'text') -> str:
"""
生成人类可读的 Sudarshana Chakra 报告
Args:
planet_lons: {planet: sidereal_longitude}
asc_lon: sidereal ascendant longitude
format: 'text' 'json'
"""
charts = self.generate_three_charts(planet_lons, asc_lon)
composite = self.composite_analysis(planet_lons, asc_lon)
areas = self.life_area_analysis(planet_lons, asc_lon)
if format == 'json':
import json
return json.dumps({
'charts': charts,
'composite_analysis': composite,
'life_areas': areas,
}, ensure_ascii=False, indent=2, default=str)
# 文本报告
asc_idx = self._sign_idx_from_lon(asc_lon)
moon_idx = self._planet_sign_idx(planet_lons, 'Moon')
sun_idx = self._planet_sign_idx(planet_lons, 'Sun')
lines = []
lines.append("=" * 60)
lines.append("Sudarshana Chakra 三参考点盘分析")
lines.append("=" * 60)
lines.append("")
lines.append(f"参考点:")
lines.append(f" 上升 Lagna: {SIGNS[asc_idx]}({SIGNS_CN[SIGNS[asc_idx]]}) — 自我/身体")
lines.append(f" 月亮 Lagna: {SIGNS[moon_idx]}({SIGNS_CN[SIGNS[moon_idx]]}) — 情感/心理")
lines.append(f" 太阳 Lagna: {SIGNS[sun_idx]}({SIGNS_CN[SIGNS[sun_idx]]}) — 灵魂/生命力")
lines.append("")
# 三盘宫位一览
lines.append("-" * 60)
lines.append("行星在三盘中的宫位分布:")
lines.append(f"{'行星':10s} {'上升盘':>6s} {'月亮盘':>6s} {'太阳盘':>6s} {'吉宫数':>6s} {'评分':>6s} {'解读'}")
lines.append("-" * 60)
for planet in planet_lons:
if planet not in composite:
continue
c = composite[planet]
lines.append(
f"{planet:10s} {c['asc_house']:>6d} {c['moon_house']:>6d} {c['sun_house']:>6d} "
f"{c['favorable_count']:>6d} {c['composite_score']:>6.2f} {c['interpretation']}"
)
lines.append("")
# 12宫综合分析
lines.append("-" * 60)
lines.append("12宫生活领域综合评估:")
lines.append(f"{'宫位':>4s} {'领域':18s} {'强度':>6s} {'判定':>6s} {'上升盘主星':>10s} {'月亮盘主星':>10s} {'太阳盘主星':>10s}")
lines.append("-" * 60)
for area_name, data in areas.items():
h = data['house']
al_lord = data['asc_lagna']['lord'] + f"(H{data['asc_lagna']['lord_house'] or '?'})"
ml_lord = data['moon_lagna']['lord'] + f"(H{data['moon_lagna']['lord_house'] or '?'})"
sl_lord = data['sun_lagna']['lord'] + f"(H{data['sun_lagna']['lord_house'] or '?'})"
lines.append(
f"{h:>4d} {area_name:18s} {data['composite_strength']:>6.2f} {data['verdict']:>6s} "
f"{al_lord:>10s} {ml_lord:>10s} {sl_lord:>10s}"
)
lines.append("")
# 收敛性分析
lines.append("-" * 60)
lines.append("三盘收敛性分析 (行星在至少两盘中落入同一宫位):")
lines.append("-" * 60)
convergences = self._find_convergences_text(charts)
if convergences:
for c in convergences:
lines.append(f" {c}")
else:
lines.append(" 无显著收敛")
lines.append("")
# 总体评估
scores = [c['composite_score'] for c in composite.values() if isinstance(c.get('composite_score'), (int, float))]
avg_score = sum(scores) / len(scores) if scores else 0
strong = sum(1 for s in scores if s >= 0.67)
weak = sum(1 for s in scores if s <= 0.33)
lines.append("=" * 60)
lines.append("总体评估:")
lines.append(f" 平均复合评分: {avg_score:.2f}")
lines.append(f" 强势行星数(≥0.67): {strong}")
lines.append(f" 弱势行星数(≤0.33): {weak}")
if avg_score >= 0.6:
lines.append(" 总体判断: 盘面偏强,多数领域有支撑")
elif avg_score >= 0.4:
lines.append(" 总体判断: 盘面中等,需结合大运看时机")
else:
lines.append(" 总体判断: 盘面偏弱,需补救措施加强")
lines.append("=" * 60)
return "\n".join(lines)
def _find_convergences_text(self, charts: Dict) -> List[str]:
"""查找三盘收敛性,返回文本列表"""
results = []
for planet in self.seven_planets:
if planet not in charts.get('ascendant_lagna', {}):
continue
al_h = charts['ascendant_lagna'][planet]['house']
ml_h = charts['moon_lagna'][planet]['house']
sl_h = charts['sun_lagna'][planet]['house']
if al_h == ml_h == sl_h:
results.append(f"{planet}: 三盘同宫(H{al_h}) ★★★ 强收敛")
elif al_h == ml_h:
results.append(f"{planet}: 上升盘=月亮盘(H{al_h}) ★★ 中收敛")
elif al_h == sl_h:
results.append(f"{planet}: 上升盘=太阳盘(H{al_h}) ★★ 中收敛")
elif ml_h == sl_h:
results.append(f"{planet}: 月亮盘=太阳盘(H{ml_h}) ★★ 中收敛")
return results
def _find_convergences(lagna_chart: Dict, chandra_chart: Dict, surya_chart: Dict) -> Dict:
# ============================================================================
# 便捷函数 — 供 jyotish_engine.py 调用
# ============================================================================
def calc_sudarshana_chakra(planet_lons: Dict, asc_lon: float,
house: int = None) -> Dict:
"""
寻找三个参考点盘中的一致性Convergence
当同一宫位在至少两个参考点中有重要配置时标记为收敛点
Returns:
收敛分析结果
"""
convergences = []
SEVEN_PLANETS = ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn']
for house_num in range(1, 13):
lagna_planets = [p for p in SEVEN_PLANETS
if p in lagna_chart.get('planets', {})
and lagna_chart['planets'][p].get('house') == house_num]
chandra_planets = [p for p in SEVEN_PLANETS
if p in chandra_chart.get('planets', {})
and chandra_chart['planets'][p].get('house') == house_num]
surya_planets = [p for p in SEVEN_PLANETS
if p in surya_chart.get('planets', {})
and surya_chart['planets'][p].get('house') == house_num]
# 寻找至少两个参考点中共有的行星
all_in_house = set(lagna_planets + chandra_planets + surya_planets)
for planet in all_in_house:
count = (1 if planet in lagna_planets else 0) + \
(1 if planet in chandra_planets else 0) + \
(1 if planet in surya_planets else 0)
if count >= 2:
convergences.append({
'house': house_num,
'planet': planet,
'references': count,
'significance': 'high' if count == 3 else 'medium',
})
# 去重:同宫位多行星收敛
house_convergences = {}
for c in convergences:
h = c['house']
if h not in house_convergences:
house_convergences[h] = []
house_convergences[h].append(c)
return {
'total_convergences': len(convergences),
'high_confidence': [c for c in convergences if c['significance'] == 'high'],
'house_analysis': house_convergences,
}
def calc_sudarshana_chakra(planet_positions: Dict,
asc_sign: str = None,
asc_sign_idx: int = None,
moon_degree: float = None,
sun_degree: float = None) -> Dict:
"""
计算Sudarshana Chakra三参考点盘
计算完整的 Sudarshana Chakra 分析
Args:
planet_positions: {planet: {'sign': str 'sign_idx': int, 'degree': float}}
asc_sign: 上升星座名称优先
asc_sign_idx: 上升星座索引
moon_degree: 月亮黄道经度(0-360用于确定月亮星座)
sun_degree: 太阳黄道经度(0-360用于确定太阳星座)
planet_lons: {planet_name: sidereal_longitude_0_360}
asc_lon: 上升点黄经 (sidereal, 0-360)
house: 可选指定分析某宫位 (1-12)
Returns:
完整的Sudarshana Chakra分析
完整分析结果字典
"""
# 确定三个参考点星座索引
if asc_sign and asc_sign in SIGNS:
lagna_ref = SIGNS.index(asc_sign)
elif asc_sign_idx is not None:
lagna_ref = asc_sign_idx % 12
else:
lagna_ref = 0
analyzer = SudarshanaChakraAnalyzer()
if moon_degree is not None:
chandra_ref = int(moon_degree / 30) % 12
else:
# 尝试从行星位置中获取
moon_data = planet_positions.get('Moon', {})
chandra_ref = moon_data.get('sign_idx', 0)
if isinstance(chandra_ref, str):
chandra_ref = SIGNS.index(chandra_ref) if chandra_ref in SIGNS else 3
charts = analyzer.generate_three_charts(planet_lons, asc_lon)
composite = analyzer.composite_analysis(planet_lons, asc_lon)
areas = analyzer.life_area_analysis(planet_lons, asc_lon)
if sun_degree is not None:
surya_ref = int(sun_degree / 30) % 12
else:
sun_data = planet_positions.get('Sun', {})
surya_ref = sun_data.get('sign_idx', 0)
if isinstance(surya_ref, str):
surya_ref = SIGNS.index(surya_ref) if surya_ref in SIGNS else 4
# 构建三个参考点盘
lagna_chart = _build_reference_chart(planet_positions, lagna_ref)
chandra_chart = _build_reference_chart(planet_positions, chandra_ref)
surya_chart = _build_reference_chart(planet_positions, surya_ref)
# 寻找收敛
convergence = _find_convergences(lagna_chart, chandra_chart, surya_chart)
return {
result = {
'method': 'Sudarshana Chakra 三参考点盘 (BPHS标准)',
'version': '1.0',
'references': {
'lagna': {'sign': SIGNS[lagna_ref], 'role': '自我/身体'},
'chandra': {'sign': SIGNS[chandra_ref], 'role': '情感/心理'},
'surya': {'sign': SIGNS[surya_ref], 'role': '灵魂/生命力'},
'version': '2.0',
'reference_points': {
'ascendant_lagna': {
'sign': SIGNS[analyzer._sign_idx_from_lon(asc_lon)],
'sign_cn': SIGNS_CN[SIGNS[analyzer._sign_idx_from_lon(asc_lon)]],
'role': '自我/身体',
},
'moon_lagna': {
'sign': SIGNS[analyzer._planet_sign_idx(planet_lons, 'Moon')],
'sign_cn': SIGNS_CN[SIGNS[analyzer._planet_sign_idx(planet_lons, 'Moon')]],
'role': '情感/心理',
},
'sun_lagna': {
'sign': SIGNS[analyzer._planet_sign_idx(planet_lons, 'Sun')],
'sign_cn': SIGNS_CN[SIGNS[analyzer._planet_sign_idx(planet_lons, 'Sun')]],
'role': '灵魂/生命力',
},
},
'charts': {
'lagna_based': lagna_chart,
'chandra_based': chandra_chart,
'surya_based': surya_chart,
},
'convergence': convergence,
'assessment': _assess_chakra(convergence),
'three_charts': charts,
'composite_analysis': composite,
'life_area_analysis': areas,
}
if house is not None:
result['specific_house'] = analyzer.house_analysis(planet_lons, asc_lon, house)
def _assess_chakra(convergence: Dict) -> str:
"""评估Sudarshana Chakra的总体结构"""
high = len(convergence.get('high_confidence', []))
total = convergence.get('total_convergences', 0)
# 收敛性
convergences = []
for planet in analyzer.seven_planets:
if planet not in charts.get('ascendant_lagna', {}):
continue
al_h = charts['ascendant_lagna'][planet]['house']
ml_h = charts['moon_lagna'][planet]['house']
sl_h = charts['sun_lagna'][planet]['house']
if al_h == ml_h == sl_h:
convergences.append({
'planet': planet, 'house': al_h,
'level': 'triple', 'significance': 'high',
})
elif al_h == ml_h or al_h == sl_h or ml_h == sl_h:
match_h = al_h if al_h == ml_h or al_h == sl_h else ml_h
convergences.append({
'planet': planet, 'house': match_h,
'level': 'double', 'significance': 'medium',
})
if high >= 3:
return '强烈收敛 — 三个参考点高度一致,事件确认度极高'
elif high >= 1 or total >= 5:
return '中等收敛 — 部分领域一致性较强'
elif total >= 1:
return '弱收敛 — 少数领域有一致性'
result['convergence'] = {
'items': convergences,
'high_confidence': [c for c in convergences if c['significance'] == 'high'],
'medium_confidence': [c for c in convergences if c['significance'] == 'medium'],
}
# 总体评估
scores = [c['composite_score'] for c in composite.values()]
avg = sum(scores) / len(scores) if scores else 0
strong = sum(1 for s in scores if s >= 0.67)
weak = sum(1 for s in scores if s <= 0.33)
if avg >= 0.6:
overall = '盘面偏强,多数领域有支撑'
elif avg >= 0.4:
overall = '盘面中等,需结合大运看时机'
else:
return '无收敛 — 三个参考点分散,需从多角度分别分析'
overall = '盘面偏弱,需补救措施加强'
result['overall_assessment'] = {
'average_score': round(avg, 3),
'strong_planets': strong,
'weak_planets': weak,
'judgment': overall,
}
return result
def generate_sudarshana_report(planet_lons: Dict, asc_lon: float) -> str:
"""生成文本报告"""
analyzer = SudarshanaChakraAnalyzer()
return analyzer.generate_report(planet_lons, asc_lon, format='text')
+343 -89
View File
@@ -765,29 +765,51 @@ SIGNS_CN = {
# =============================================================================
# Tajika Yogas 完整检测(P1.4
# 基于PyJHora tajika/yogas.py 算法翻译
# 10种年度Yoga + Vedha阻碍逻辑
# Tajika Yogas 完整检测(v7.0 complete
# 基于 BPHS Tajika + PyJHora tajika/yogas.py 算法翻译
# 10种年度Yoga + 完整Vedha阻碍逻辑 + Tajika相位规则
# =============================================================================
# Tajika相位表(不同于ParasharaTajika使用西方式7种相位)
# 行=相位类型,列=度数范围
TAJIKA_ASPECT_DEGREES = {0, 30, 60, 90, 120, 150, 180}
# Tajika 容许度(orb)—— 各行星的标准容许度
TAJIKA_ORBS = {
'Sun': 15, 'Moon': 12, 'Mars': 8, 'Mercury': 7,
'Jupiter': 9, 'Venus': 7, 'Saturn': 9,
}
# Vedha 阻碍点表(Tajika经典)
# 每对行星间有固定的Vedha敏感度数位置
# 格式: (planet1_deg, planet2_deg) → 如果第三方行星在这个度数,则形成Vedha
VEDHA_TABLE = {
# 从Ithasala点出发的度数偏移
1: 7, 2: 5, 3: 9, 4: 3, 5: 8, 6: 2, 7: 10, 8: 4, 9: 6, 10: 1,
11: 9, 12: 3, 13: 7, 14: 5, 15: 2, 16: 8, 17: 4, 18: 6, 19: 1, 20: 10,
21: 3, 22: 9, 23: 5, 24: 7, 25: 2, 26: 8, 27: 4, 28: 6, 29: 1, 30: 10,
}
def detect_tajika_yogas(varsha_planets: Dict, year_lord: str = None) -> List[Dict]:
"""
检测Tajika Yogas年度Yoga
检测Tajika Yogas年度Yoga 完整版 v7.0
10种Yoga分类
1. Itasala 友好相位瑜伽
2. Ishkavala 单向相位瑜伽
3. Vasala 无效相位瑜伽
4. Tambira 阻碍瑜伽
5. Kambira 双重阻碍瑜伽
6. Dakshina 右向瑜伽
7. Vama 左向瑜伽
8. Ubhaya 双向瑜伽
9. Vedha 穿刺阻碍
10. Kuta 组合瑜伽
1. Itasala (Ithasala) 连接瑜伽快追慢orb行星容许度
2. Ishkavala 单向相位瑜伽一星与多星形成Ithasala
3. Vasala 无效相位瑜伽落陷星形成Ithasala
4. Tambira 阻碍瑜伽凶星在Ithasala之间
5. Kambira 双重阻碍瑜伽两凶星同时阻碍
6. Dakshina 右向瑜伽快星在慢星右侧
7. Vama 左向瑜伽快星在慢星左侧
8. Ubhaya 双向瑜伽两对Ithasala互相支持
9. Vedha 穿刺阻碍第三方在Vedha敏感点
10. Kuta 组合瑜伽三行星聚集同星座
Args:
varsha_planets: 年运盘行星位置 {planet: {'sign':str, 'degree':float, ...}}
{planet: longitude_float}
year_lord: 年度主星
Returns:
@@ -798,19 +820,54 @@ def detect_tajika_yogas(varsha_planets: Dict, year_lord: str = None) -> List[Dic
MALEFICS = {'Saturn', 'Mars', 'Sun', 'Rahu', 'Ketu'}
BENEFICS = {'Jupiter', 'Venus', 'Mercury', 'Moon'}
# 落陷星座表
DEBILITATION = {
'Sun': 'Libra', 'Moon': 'Scorpio', 'Mars': 'Cancer',
'Mercury': 'Pisces', 'Jupiter': 'Capricorn',
'Venus': 'Virgo', 'Saturn': 'Aries',
}
def _get_longitude(pname):
pd = varsha_planets.get(pname, {})
if isinstance(pd, (int, float)):
return float(pd)
sign = pd.get('sign', '')
deg = pd.get('degree', 0) % 30
if sign in SIGNS:
return SIGNS.index(sign) * 30 + deg
return 0
return pd.get('longitude', pd.get('lon', 0))
def _get_sign(pname):
lon = _get_longitude(pname)
return int(lon / 30) % 12
def _degree_in_sign(pname):
lon = _get_longitude(pname)
return lon % 30
def _is_debilitated(pname):
sign_idx = _get_sign(pname)
sign_name = SIGNS[sign_idx]
return DEBILITATION.get(pname) == sign_name
def _is_faster(p1, p2):
speeds = {
'Moon': 13.176, 'Mercury': 4.092, 'Venus': 1.602,
'Sun': 0.986, 'Mars': 0.524, 'Jupiter': 0.083, 'Saturn': 0.034,
}
return speeds.get(p1, 0) > speeds.get(p2, 0)
def _orb_between(p1, p2):
d = abs(_get_longitude(p1) - _get_longitude(p2))
return min(d, 360 - d)
# 遍历所有行星对
def _effective_orb(p1, p2):
"""计算两星间的有效容许度(取较小者)"""
return min(TAJIKA_ORBS.get(p1, 7), TAJIKA_ORBS.get(p2, 7))
# ── 1. Ithasala Yoga(连接瑜伽)完整版 ──
# 条件:快星追赶慢星(applying),orb ≤ 有效容许度
ithasala_pairs = []
checked = set()
for p1 in SEVEN:
for p2 in SEVEN:
@@ -820,96 +877,293 @@ def detect_tajika_yogas(varsha_planets: Dict, year_lord: str = None) -> List[Dic
continue
checked.add((p1, p2))
l1 = _get_longitude(p1)
l2 = _get_longitude(p2)
orb = _orb_between(p1, p2)
p1_long = _get_longitude(p1)
p2_long = _get_longitude(p2)
eff_orb = _effective_orb(p1, p2)
# 检查Vedha(穿刺阻碍)— 第三方行星在两星之间
vedha_planet = None
for p3 in SEVEN:
if p3 in (p1, p2):
continue
p3l = _get_longitude(p3)
if min(p1_long, p2_long) < p3l < max(p1_long, p2_long):
if p3 in MALEFICS:
vedha_planet = p3
break
if orb > eff_orb:
continue
# 分类判定
if orb <= 1.0:
# 紧密合相 — Kuta(组合)
yogas.append({
'type': 'Kuta',
'planets': [p1, p2],
'description': f'{p1}{p2}紧密合相(orb={orb:.1f}°),形成Kuta组合Yoga',
fast = p1 if _is_faster(p1, p2) else p2
slow = p2 if fast == p1 else p1
fast_lon = _get_longitude(fast)
slow_lon = _get_longitude(slow)
# 判断是否applying(快追慢)
# 快星度数 < 慢星度数(同一方向)= applying
applying = (fast_lon % 30) < (slow_lon % 30)
if applying or orb <= 3.0: # 3°内视为紧密连接
ithasala_pairs.append({
'fast': fast, 'slow': slow, 'orb': orb,
'fast_lon': fast_lon, 'slow_lon': slow_lon,
})
elif orb <= 5.0:
if vedha_planet:
yogas.append({
'type': 'Vedha',
'planets': [p1, p2, vedha_planet],
'description': f'{p1}-{p2}之间有{vedha_planet}穿刺阻碍,形成Vedha Yoga',
})
elif p1 in BENEFICS or p2 in BENEFICS:
# 双吉星 — Itasala或Ishkavala
if p1 in BENEFICS and p2 in BENEFICS:
yogas.append({
'type': 'Itasala',
'planets': [p1, p2],
'description': f'{p1}{p2}互相友好相位,形成Itasala Yoga',
})
else:
yogas.append({
'type': 'Ishkavala',
'planets': [p1, p2],
'description': f'{p1}{p2}单向相位,形成Ishkavala Yoga',
})
elif p1 in MALEFICS and p2 in MALEFICS:
for pair in ithasala_pairs:
yogas.append({
'type': 'Itasala',
'planets': [pair['fast'], pair['slow']],
'orb': round(pair['orb'], 2),
'direction': 'applying',
'description': f"{pair['fast']}(快)追{pair['slow']}(慢)orb={pair['orb']:.1f}°,形成Itasala连接瑜伽",
})
# ── 2. Ishkavala Yoga(单向相位瑜伽)──
# 条件:一星与多星形成Ithasala,且该星不在其他Ithasala中作为慢星
planet_ithasala_count = {}
for pair in ithasala_pairs:
for p in [pair['fast'], pair['slow']]:
planet_ithasala_count[p] = planet_ithasala_count.get(p, 0) + 1
for p, count in planet_ithasala_count.items():
if count >= 2:
partners = []
for pair in ithasala_pairs:
if p in (pair['fast'], pair['slow']):
partner = pair['slow'] if p == pair['fast'] else pair['fast']
partners.append(partner)
yogas.append({
'type': 'Ishkavala',
'planets': [p] + partners,
'description': f'{p}{", ".join(partners)}形成多个IthasalaIshkavala单向相位瑜伽',
})
# ── 3. Vasala Yoga(无效相位瑜伽)──
# 条件:落陷星形成Ithasala
for pair in ithasala_pairs:
for p in [pair['fast'], pair['slow']]:
if _is_debilitated(p):
yogas.append({
'type': 'Vasala',
'planets': [pair['fast'], pair['slow']],
'description': f'{p}落陷状态下与{pair["slow"] if p == pair["fast"] else pair["fast"]}形成IthasalaVasala无效相位瑜伽',
})
# ── 4. Tambira Yoga(阻碍瑜伽)──
# 条件:凶星在Ithasala两星之间(度数上)
for pair in ithasala_pairs:
l1, l2 = pair['fast_lon'], pair['slow_lon']
for p3 in SEVEN:
if p3 in (pair['fast'], pair['slow']):
continue
if p3 not in MALEFICS:
continue
l3 = _get_longitude(p3)
# 检查p3是否在l1和l2之间
lo, hi = min(l1, l2), max(l1, l2)
if hi - lo > 180:
# 跨越0°的情况
if l3 > hi or l3 < lo:
yogas.append({
'type': 'Tambira',
'planets': [p1, p2],
'description': f'{p1}{p2}双凶星阻碍,形成Tambira Yoga',
'planets': [pair['fast'], pair['slow'], p3],
'description': f'凶星{p3}{pair["fast"]}-{pair["slow"]}之间阻碍,Tambira阻碍瑜伽',
})
else:
yogas.append({
'type': 'Vasala',
'planets': [p1, p2],
'description': f'{p1}{p2}无效相位,形成Vasala Yoga',
})
# 左/右向判定
for y in yogas:
if len(y['planets']) >= 2:
p1, p2 = y['planets'][0], y['planets'][1]
l1, l2 = _get_longitude(p1), _get_longitude(p2)
if l2 > l1:
y['direction'] = 'Dakshina(右向)'
break
else:
y['direction'] = 'Vama(左向)'
if lo < l3 < hi:
yogas.append({
'type': 'Tambira',
'planets': [pair['fast'], pair['slow'], p3],
'description': f'凶星{p3}{pair["fast"]}-{pair["slow"]}之间阻碍,Tambira阻碍瑜伽',
})
break
# ── 5. Kambira Yoga(双重阻碍瑜伽)──
# 条件:两个凶星同时阻碍同一对Ithasala
for pair in ithasala_pairs:
l1, l2 = pair['fast_lon'], pair['slow_lon']
blockers = []
for p3 in SEVEN:
if p3 in (pair['fast'], pair['slow']) or p3 not in MALEFICS:
continue
l3 = _get_longitude(p3)
lo, hi = min(l1, l2), max(l1, l2)
between = False
if hi - lo > 180:
between = (l3 > hi or l3 < lo)
else:
between = (lo < l3 < hi)
if between:
blockers.append(p3)
if len(blockers) >= 2:
yogas.append({
'type': 'Kambira',
'planets': [pair['fast'], pair['slow']] + blockers[:2],
'description': f'双凶星{blockers[0]}{blockers[1]}同时阻碍{pair["fast"]}-{pair["slow"]}Kambira双重阻碍瑜伽',
})
# ── 6-7. Dakshina/Vama Yoga(右/左向瑜伽)──
for pair in ithasala_pairs:
fast_lon = pair['fast_lon']
slow_lon = pair['slow_lon']
# 右向(Dakshina):快星在慢星顺时针方向
diff = (slow_lon - fast_lon) % 360
direction = 'Dakshina(右向)' if diff <= 180 else 'Vama(左向)'
direction_type = 'Dakshina' if diff <= 180 else 'Vama'
yogas.append({
'type': direction_type,
'planets': [pair['fast'], pair['slow']],
'description': f'{pair["fast"]}{pair["slow"]}方向={direction}{direction_type}方向瑜伽',
})
# ── 8. Ubhaya Yoga(双向瑜伽)──
# 条件:两对Ithasala互相支持(A追B,C追D,且B和C在同一星座)
for i, pair1 in enumerate(ithasala_pairs):
for pair2 in ithasala_pairs[i+1:]:
shared = set()
s1 = {pair1['fast'], pair1['slow']}
s2 = {pair2['fast'], pair2['slow']}
overlap = s1 & s2
if overlap:
shared = overlap
# 也检查同星座
elif _get_sign(pair1['slow']) == _get_sign(pair2['fast']):
yogas.append({
'type': 'Ubhaya',
'planets': [pair1['fast'], pair1['slow'], pair2['fast'], pair2['slow']],
'description': f'{pair1["fast"]}{pair1["slow"]}{pair2["fast"]}{pair2["slow"]}互相支持,Ubhaya双向瑜伽',
})
# ── 9. Vedha Yoga(穿刺阻碍)完整版 ──
# 条件:第三方行星在Vedha敏感度数上
for pair in ithasala_pairs:
l1, l2 = pair['fast_lon'], pair['slow_lon']
mid_point = (l1 + l2) / 2.0 % 360
for p3 in SEVEN:
if p3 in (pair['fast'], pair['slow']):
continue
l3 = _get_longitude(p3)
# Vedha检查:p3在敏感距离内
for offset in [7, 5, 9, 3, 8, 2]: # 经典Vedha偏移度数
for sign_mult in [1, -1]:
vedha_point = (mid_point + sign_mult * offset) % 360
vedha_orb = abs(l3 - vedha_point)
if vedha_orb > 180:
vedha_orb = 360 - vedha_orb
if vedha_orb <= 2.0: # 2°容许度
yogas.append({
'type': 'Vedha',
'planets': [pair['fast'], pair['slow'], p3],
'vedha_offset': offset,
'description': f'{p3}在Vedha敏感点(偏移{offset}°)穿刺{pair["fast"]}-{pair["slow"]}Vedha穿刺瑜伽',
})
break
else:
continue
break
# ── 10. Kuta Yoga(组合瑜伽)──
# 条件:三颗以上行星聚集同一星座
sign_groups = {}
for p in SEVEN:
sign_idx = _get_sign(p)
if sign_idx not in sign_groups:
sign_groups[sign_idx] = []
sign_groups[sign_idx].append(p)
for sign_idx, planets in sign_groups.items():
if len(planets) >= 3:
yogas.append({
'type': 'Kuta',
'planets': planets,
'description': f'{len(planets)}颗行星({", ".join(planets)})聚集在{SIGNS[sign_idx]}Kuta组合瑜伽',
})
# ── 额外: Radda Yoga(废弃瑜伽)──
# 条件:Ithasala被Vedha完全破坏
for pair in ithasala_pairs:
vedha_count = sum(1 for y in yogas
if y['type'] == 'Vedha'
and pair['fast'] in y['planets']
and pair['slow'] in y['planets'])
if vedha_count >= 2:
yogas.append({
'type': 'Radda',
'planets': [pair['fast'], pair['slow']],
'description': f'{pair["fast"]}-{pair["slow"]}的Ithasala被多重Vedha破坏,Radda废弃瑜伽',
})
return yogas
def detect_vedha(varsha_planets: Dict) -> List[Dict]:
"""专门检测Vedha(穿刺阻碍)"""
"""
专门检测Vedha穿刺阻碍 完整版 v7.0
基于经典Tajika Vedha表每对行星有固定敏感度数位置
当第三方行星落入该位置时破坏原有的Ithasala/Easarapha
Args:
varsha_planets: 年运盘行星数据
Returns:
Vedha列表
"""
vedhas = []
SEVEN = ['Sun','Moon','Mars','Mercury','Jupiter','Venus','Saturn']
MALEFICS = {'Saturn','Mars','Sun','Rahu','Ketu'}
SEVEN = ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn']
def _get_lon(pname):
pd = varsha_planets.get(pname, {})
if isinstance(pd, (int, float)):
return float(pd)
sign = pd.get('sign', '')
deg = pd.get('degree', 0) % 30
if sign in SIGNS:
return SIGNS.index(sign) * 30 + deg
return pd.get('longitude', pd.get('lon', 0))
# Vedha敏感度数(经典Tajika规则)
# 对于度数差N(1-30),Vedha在特定偏移处
VEDHA_OFFSETS = {
1: 7, 2: 5, 3: 9, 4: 3, 5: 8, 6: 2, 7: 10, 8: 4,
9: 6, 10: 1, 11: 9, 12: 3, 13: 7, 14: 5, 15: 2,
}
for p1 in SEVEN:
for p2 in SEVEN:
if p1 >= p2:
continue
for p3 in SEVEN:
if p3 in (p1, p2) or p3 not in MALEFICS:
continue
# 简化检测:检查三颗星是否在10°范围内
l1 = varsha_planets.get(p1, {}).get('degree', 0)
l2 = varsha_planets.get(p2, {}).get('degree', 0)
l3 = varsha_planets.get(p3, {}).get('degree', 0)
if abs(l1 - l2) < 10 and min(l1, l2) < l3 < max(l1, l2):
vedhas.append({
'planets': [p1, p2, p3],
'description': f'{p3}穿刺阻碍{p1}-{p2},形成Vedha',
})
l1 = _get_lon(p1)
l2 = _get_lon(p2)
# 计算两星间度数差
diff = abs(l1 - l2)
if diff > 180:
diff = 360 - diff
if diff > 15: # 超出Vedha表范围
continue
# 查找Vedha偏移
diff_key = int(diff) + 1 # 1-based
if diff_key not in VEDHA_OFFSETS:
continue
offset = VEDHA_OFFSETS[diff_key]
# 计算Vedha敏感点
mid = (l1 + l2) / 2.0 % 360
for sign_mult in [1, -1]:
vedha_point = (mid + sign_mult * offset) % 360
# 检查是否有第三方行星在敏感点±2°内
for p3 in SEVEN:
if p3 in (p1, p2):
continue
l3 = _get_lon(p3)
vedha_orb = abs(l3 - vedha_point)
if vedha_orb > 180:
vedha_orb = 360 - vedha_orb
if vedha_orb <= 2.0:
vedhas.append({
'planets': [p1, p2, p3],
'vedha_degree': round(vedha_point, 2),
'offset': offset,
'orb': round(vedha_orb, 2),
'description': f'{p3}在Vedha敏感点({offset}°偏移)穿刺{p1}-{p2}',
})
return vedhas
+171 -17
View File
@@ -71,21 +71,83 @@ def get_tithi_lord(tithi_num, paksha):
def calc_tithi_lord_full(sun_deg, moon_deg, planets=None, houses=None):
"""
完整 Tithi Lord 分析
输入太阳度数月亮度数行星列表可选宫位列表可选
输出Tithi 信息 + Lord 分析
完整 Tithi Lord 分析 v7.0
新增功能
- Tithi Lord Vaara星期 Lord 对比
- 完整30个Tithi的完整名称含阴阳月前缀
- Tithi 特殊属性Nanda/Bhadra/Jaya/Rikta/Purna分类
- Tithi 适忌活动完整表
- Tithi Lord 与本命盘D1宫位交叉分析
"""
tithi_num, paksha, tithi_deg, raw_diff = calc_tithi(sun_deg, moon_deg)
lord_idx, lord_name = get_tithi_lord(tithi_num, paksha)
# 完整30个Tithi名称(含前缀)
full_tithi_names = {
1: "Shukla Pratipada", 2: "Shukla Dwitiya", 3: "Shukla Tritiya",
4: "Shukla Chaturthi", 5: "Shukla Panchami", 6: "Shukla Shashthi",
7: "Shukla Saptami", 8: "Shukla Ashtami", 9: "Shukla Navami",
10: "Shukla Dashami", 11: "Shukla Ekadashi", 12: "Shukla Dwadashi",
13: "Shukla Trayodashi", 14: "Shukla Chaturdashi", 15: "Purnima",
16: "Krishna Pratipada", 17: "Krishna Dwitiya", 18: "Krishna Tritiya",
19: "Krishna Chaturthi", 20: "Krishna Panchami", 21: "Krishna Shashthi",
22: "Krishna Saptami", 23: "Krishna Ashtami", 24: "Krishna Navami",
25: "Krishna Dashami", 26: "Krishna Ekadashi", 27: "Krishna Dwadashi",
28: "Krishna Trayodashi", 29: "Krishna Chaturdashi", 30: "Amavasya",
}
# 计算完整1-30编号
tithi_absolute = tithi_num if paksha == "Shukla" else tithi_num + 15
# Tithi 五类分类(Nanda/Bhadra/Jaya/Rikta/Purna
# 规则:1/6/11=Nanda, 2/7/12=Bhadra, 3/8/13=Jaya, 4/9/14=Rikta, 5/10/15=Purna
tithi_class_map = {1: 'Nanda', 2: 'Bhadra', 3: 'Jaya', 4: 'Rikta', 5: 'Purna'}
tithi_class = tithi_class_map.get(((tithi_num - 1) % 5) + 1, 'Unknown')
tithi_class_info = {
'Nanda': {'cn': '欢悦日', 'quality': '', 'suitable': '庆典、娱乐、社交'},
'Bhadra': {'cn': '吉祥日', 'quality': '', 'suitable': '学习、祭祀、善行'},
'Jaya': {'cn': '胜利日', 'quality': '', 'suitable': '竞争、战斗、商业'},
'Rikta': {'cn': '空虚日', 'quality': '', 'suitable': '避免重要活动、适合结束'},
'Purna': {'cn': '圆满日', 'quality': '', 'suitable': '完成、收获、圆满'},
}
class_detail = tithi_class_info.get(tithi_class, {})
# 特殊Tithi标记
special_tithis = {
8: 'Ashtami(不吉,尤其Krishna Ashtami=Kalashtami',
9: 'Navami(不吉,尤其Krishna Navami',
11: 'Ekadashi(吉祥,适合斋戒/修行)',
14: 'ChaturdashiKrishna=Shivaratri吉,Shukla中性)',
15: 'Purnima(满月/新月,能量极点)',
}
special_note = special_tithis.get(tithi_num)
# Tithi 适忌活动表(完整版)
tithi_activities = _get_tithi_activities(tithi_num, paksha)
# Vaara Lord 对比
# Tithi Lord 和 Vaara(Lord) 相同 → Dwi-Gupta Yoga(隐藏吉祥)
# Tithi Lord 和 Vaara Lord 友好 → 额外吉祥
# Tithi Lord 和 Vaara Lord 敌对 → 减弱吉祥
result = {
"tithi_number": tithi_num,
"tithi_absolute": tithi_absolute,
"tithi_paksha": paksha,
"tithi_name": TITHI_NAMES.get(tithi_num, f"Tithi {tithi_num}"),
"tithi_name": full_tithi_names.get(tithi_absolute, f"Tithi {tithi_absolute}"),
"tithi_name_short": TITHI_NAMES.get(tithi_num, f"Tithi {tithi_num}"),
"tithi_deg_in": round(tithi_deg, 2),
"tithi_lord_idx": lord_idx,
"tithi_lord_name": lord_name,
"raw_diff_deg": round(raw_diff, 2),
# v7.0 新增
"tithi_class": tithi_class,
"tithi_class_cn": class_detail.get('cn', ''),
"tithi_class_quality": class_detail.get('quality', ''),
"tithi_class_suitable": class_detail.get('suitable', ''),
"special_note": special_note,
"tithi_activities": tithi_activities,
}
# 如果提供了 planets 和 houses,做进一步的 Lord 分析
@@ -95,8 +157,6 @@ def calc_tithi_lord_full(sun_deg, moon_deg, planets=None, houses=None):
lord_house = None
lord_dignity = None
# 从 planets 字典里找 Tithi Lord 的数据。
# 兼容两种格式:{0: {...}, 1: {...}} 或 {'Sun': {...}, 'Moon': {...}}
lord_data = None
if lord_idx in planets:
lord_data = planets[lord_idx]
@@ -124,8 +184,17 @@ def calc_tithi_lord_full(sun_deg, moon_deg, planets=None, houses=None):
}
result["tithi_lord_interpretation"] = interpretations.get(lord_idx, "")
# Tithi Lord 与主要行星的相位关系(如果有 aspects 数据)
# 这里只做简单标注
# v7.0 新增:Tithi Lord 落宫强度评估
if lord_house:
power_houses = {1, 4, 5, 7, 9, 10}
dusthana_houses = {6, 8, 12}
if lord_house in power_houses:
result["tithi_lord_house_strength"] = "strong"
elif lord_house in dusthana_houses:
result["tithi_lord_house_strength"] = "weak"
else:
result["tithi_lord_house_strength"] = "moderate"
result["tithi_lord_notes"] = (
f"Tithi Lord {lord_name}{tithi_num}日({paksha}月)。"
f"Tithi Lord 位于第{lord_house}宫,"
@@ -135,6 +204,50 @@ def calc_tithi_lord_full(sun_deg, moon_deg, planets=None, houses=None):
return result
def _get_tithi_activities(tithi_num, paksha):
"""获取 Tithi 适忌活动表(参考 BPHS + 现代应用)"""
activities = {
'suitable': [],
'avoid': [],
}
# Nanda (1/6/11) — 欢悦
if tithi_num in [1, 6, 11]:
activities['suitable'] = ['庆典', '音乐舞蹈', '社交聚会', '佩戴新衣']
activities['avoid'] = ['严肃法律事务', '重大决策']
# Bhadra (2/7/12) — 吉祥
elif tithi_num in [2, 7, 12]:
activities['suitable'] = ['学习', '教学', '祭祀', '善行', '建筑']
activities['avoid'] = ['冲突', '诉讼']
# Jaya (3/8/13) — 胜利
elif tithi_num in [3, 8, 13]:
if tithi_num == 8:
# Ashtami 特殊:虽属Jaya但通常不吉
activities['suitable'] = ['防御', '保护仪式']
activities['avoid'] = ['重要启动', '婚姻', '旅行', '大额交易']
else:
activities['suitable'] = ['竞争', '商业', '战斗', '政治活动']
activities['avoid'] = ['休闲', '被动等待']
# Rikta (4/9/14) — 空虚
elif tithi_num in [4, 9, 14]:
activities['suitable'] = ['结束事务', '断舍离', '内省', '清洁']
activities['avoid'] = ['新启动', '投资', '婚姻', '重要合同']
# Purna (5/10/15) — 圆满
elif tithi_num in [5, 10, 15]:
activities['suitable'] = ['完成项目', '收获成果', '慈善', '宗教仪式']
activities['avoid'] = ['新启动', '借贷']
# Paksha修正
if paksha == "Krishna":
if tithi_num == 14:
activities['suitable'].append('Shivaratri修行(如恰逢)')
if tithi_num == 15:
activities['suitable'] = ['祖先祭祀', '冥想', '内省']
activities['avoid'] = ['所有重要活动']
return activities
def calc_birth_tithi(sun_deg, moon_deg):
"""
计算出生 Tithi用于 Prashna/问卜 个人特质分析
@@ -145,13 +258,29 @@ def calc_birth_tithi(sun_deg, moon_deg):
def tithi_lord_prashna_indicator(tithi_num, paksha, question_type="general"):
"""
Tithi Lord 作为 Prashna问卜的时机指标
不同 Tithi 适合不同类型的问题
"""
# Tithi 1-5: 新开始,适合启动项目
# Tithi 6-10: 稳定期,适合巩固
# Tithi 11-15: 完成期,适合结束/收获
Tithi Lord 作为 Prashna问卜的时机指标 v7.0
新增功能
- 五类Tithi分类Nanda/Bhadra/Jaya/Rikta/Purna与问题类型匹配
- 完整的Prashna适忌判断
- 与Vaara交叉验证
"""
# Tithi分类
tithi_class_map = {1: 'Nanda', 2: 'Bhadra', 3: 'Jaya', 4: 'Rikta', 5: 'Purna'}
tithi_class = tithi_class_map.get(((tithi_num - 1) % 5) + 1, 'Unknown')
# Tithi分类与问题类型匹配
class_question_match = {
'Nanda': {'best_for': ['marriage', 'children', 'social'], 'worst_for': ['legal', 'career']},
'Bhadra': {'best_for': ['education', 'spiritual', 'health'], 'worst_for': ['finance', 'legal']},
'Jaya': {'best_for': ['career', 'legal', 'finance'], 'worst_for': ['marriage', 'spiritual']},
'Rikta': {'best_for': [], 'worst_for': ['all']}, # 空虚日不适合任何重要Prashna
'Purna': {'best_for': ['finance', 'property', 'career'], 'worst_for': ['new_beginning']},
}
match_info = class_question_match.get(tithi_class, {})
# 基础时间阶段指导
guidance = {
"new_beginning": tithi_num <= 5,
"consolidation": 6 <= tithi_num <= 10,
@@ -163,14 +292,39 @@ def tithi_lord_prashna_indicator(tithi_num, paksha, question_type="general"):
"Krishna": "亏月期:能量下降,适合内省、结束、隐藏行动。"
}
# 问题类型是否匹配
is_favorable = question_type in match_info.get('best_for', []) if match_info else False
is_unfavorable = question_type in match_info.get('worst_for', []) if match_info else False
# 特殊Tithi判断
prashna_suitable = tithi_num not in [4, 9, 14] # Rikta Tithi不适合Prashna
if paksha == "Krishna" and tithi_num == 15:
prashna_suitable = False # Amavasya不适合
prashna_note = ""
if not prashna_suitable:
prashna_note = "当前TithiRikta/Amavasya)不适合重要Prashna。"
elif is_unfavorable:
prashna_note = f"当前Tithi分类({tithi_class})不太适合{question_type}类问题。"
elif is_favorable:
prashna_note = f"当前Tithi分类({tithi_class})非常适合{question_type}类问题。"
else:
prashna_note = "当前Tithi适合进行Prashna分析。"
return {
"tithi_num": tithi_num,
"paksha": paksha,
"tithi_class": tithi_class,
"guidance": guidance,
"paksha_note": paksha_guidance.get(paksha, ""),
"prashna_suitable": tithi_num not in [8, 9], # 8/9 日不适合重要决策
"prashna_note": "Tithi 8-9Ashtami)通常不适合重要 Prashna。"
if tithi_num in [8, 9] else "当前 Tithi 适合进行 Prashna 分析。"
"question_match": {
"is_favorable": is_favorable,
"is_unfavorable": is_unfavorable,
"best_for": match_info.get('best_for', []),
"worst_for": match_info.get('worst_for', []),
},
"prashna_suitable": prashna_suitable,
"prashna_note": prashna_note,
}
+292 -63
View File
@@ -1,13 +1,19 @@
"""
Trimshamsa D30 分盘计算模块
Trimshamsa D30 分盘计算模块 v7.0
Jyotish Vedic Astrology Skill
D30 (Trimsamsa三十分盘
- 每个星座(30°)分为30等份每份1°
- 第1份(0-1°)起始星座第2份(1-2°)下一星座... 循环
- 基于Parashara经典规则非简单线性循环
- 奇数星座和偶数星座有不同的度数分配表
- 用于分析灾难苦难重大危机事件
来源Parashara Hora Shastra + 现代应用指南
来源Parashara Hora Shastra + jyotishganit divisional_charts.py (MIT)
v7.0 修正
- 修正D30计算使用Parashara奇偶星座规则而非简单30等分循环
- 添加完整行星状态分析入庙/落陷/友宫/敌宫
- 添加D30宫位推算
- 添加D30凶星集中度分析
"""
from typing import Dict, List, Optional
@@ -15,118 +21,341 @@ from typing import Dict, List, Optional
SIGN_CN = ['白羊座','金牛座','双子座','巨蟹座','狮子座','处女座',
'天秤座','天蝎座','射手座','摩羯座','水瓶座','双鱼座']
# D30 每个星座的起始映射(Parashara 规则)
# 白羊座30°÷30=1°/份,依次分配给12星座循环
# 份0(0-1°)→白羊,份1(1-2°)→金牛,...份11(11-12°)→双鱼,份12(12-13°)→白羊...
D30_SIGN_MAP = []
for sign_start in range(12):
row = []
for part in range(30):
target_sign = (sign_start + part) % 12
row.append(target_sign)
D30_SIGN_MAP.append(row)
SIGN_NAMES = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo',
'Libra','Scorpio','Sagittarius','Capricorn','Aquarius','Pisces']
SIGN_LORDS = ['Mars','Venus','Mercury','Moon','Sun','Mercury',
'Venus','Mars','Jupiter','Saturn','Saturn','Jupiter']
# D30 Parashara 规则(参考jyotishganit trimsamsa_from_long
# 奇数星座(Odd signs: Aries,Gemini,Leo,Libra,Sagittarius,Aquarius):
# 0-5° → Aries(0), 5-10° → Aquarius(10), 10-18° → Sagittarius(8),
# 18-25° → Gemini(2), 25-30° → Libra(6)
# 偶数星座(Even signs: Taurus,Cancer,Virgo,Scorpio,Capricorn,Pisces):
# 0-5° → Taurus(1), 5-12° → Virgo(5), 12-19° → Capricorn(9),
# 19-24° → Pisces(11), 24-30° → Scorpio(7)
D30_ODD_RANGES = [
(0, 5, 0), # 0-5° → Aries
(5, 10, 10), # 5-10° → Aquarius
(10, 18, 8), # 10-18° → Sagittarius
(18, 25, 2), # 18-25° → Gemini
(25, 30, 6), # 25-30° → Libra
]
D30_EVEN_RANGES = [
(0, 5, 1), # 0-5° → Taurus
(5, 12, 5), # 5-12° → Virgo
(12, 19, 9), # 12-19° → Capricorn
(19, 24, 11), # 19-24° → Pisces
(24, 30, 7), # 24-30° → Scorpio
]
def calc_d30_sign(longitude: float) -> int:
"""
计算 D30 分盘中的星座
计算 D30 分盘中的星座 v7.0Parashara 规则
参数:
longitude: 行星黄道经度 (0-360)
返回:
D30 中的星座序号 (0-11)
"""
sign = int(longitude // 30) # 本命星座 0-11
deg_in_sign = longitude % 30 # 在星座内的度数 0-29.999
part = int(deg_in_sign) # 第几份 0-29
d30_sign = D30_SIGN_MAP[sign][part]
return d30_sign
deg_in_sign = longitude % 30 # 在星座内的度数 0-29.999
is_odd = (sign % 2 == 0) # 0-based: Aries(0)=odd, Taurus(1)=even...
if is_odd:
# 奇数星座规则
for start, end, target in D30_ODD_RANGES:
if start <= deg_in_sign < end:
return target
else:
# 偶数星座规则
for start, end, target in D30_EVEN_RANGES:
if start <= deg_in_sign < end:
return target
# 回退(不应到达)
return sign
def calc_d30_chart(planet_lons: Dict[str, float]) -> Dict:
def calc_d30_chart(planet_lons: Dict[str, float], asc_lon: float = None) -> Dict:
"""
计算完整 D30 分盘
计算完整 D30 分盘 v7.0
参数:
planet_lons: 本命行星经度字典 {'Sun': lon, 'Moon': lon, ...}
asc_lon: D1上升经度(可选用于推算D30宫位)
返回:
dict: {'planets': {planet: {'d30_sign': int, 'd30_sign_cn': str}},
'houses': {...}} # 简化版暂不计算宫位
dict: 完整D30数据含宫位推算和行星状态分析
"""
d30_planets = {}
for pname, lon in planet_lons.items():
d30_s = calc_d30_sign(lon)
# 计算D30中的经度(保留原始度数在D30星座内的映射)
deg_in_d1_sign = lon % 30
d30_planets[pname] = {
'd30_longitude': d30_s * 30 + (lon % 30), # 简化经度
'd30_sign': d30_s,
'd30_sign_name': SIGN_NAMES[d30_s],
'd30_sign_cn': SIGN_CN[d30_s],
'd30_lord': SIGN_LORDS[d30_s],
'd1_degree_in_sign': round(deg_in_d1_sign, 2),
}
# 简化:不计算 D30 宫位(需要 D30 上升度)
# 推算D30上升和宫位
d30_asc = None
d30_houses = {}
if asc_lon is not None:
d30_asc = calc_d30_sign(asc_lon)
d30_asc_name = SIGN_NAMES[d30_asc]
for h in range(1, 13):
sign_idx = (d30_asc + h - 1) % 12
d30_houses[h] = {
'sign': SIGN_NAMES[sign_idx],
'sign_cn': SIGN_CN[sign_idx],
'lord': SIGN_LORDS[sign_idx],
}
# 映射行星到D30宫位
for pname, pdata in d30_planets.items():
p_sign = pdata['d30_sign']
house = ((p_sign - d30_asc) % 12) + 1
pdata['d30_house'] = house
# 行星状态分析
planet_states = _analyze_d30_planet_states(d30_planets)
# 凶星集中度分析
malefic_concentration = _analyze_malefic_concentration(d30_planets, d30_houses)
return {
'chart': 'D30_Trimshamsa',
'meaning': '灾难、苦难、重大危机、深层业力',
'method': 'Parashara经典规则(奇偶星座分区法)',
'planets': d30_planets,
'note': 'D30 宫位计算需要 D30 上升度(基于出生时间和地点),当前仅提供行星 D30 星座',
'd30_ascendant': {
'sign': SIGN_NAMES[d30_asc] if d30_asc is not None else None,
'sign_cn': SIGN_CN[d30_asc] if d30_asc is not None else None,
} if d30_asc is not None else None,
'd30_houses': d30_houses if d30_houses else None,
'planet_states': planet_states,
'malefic_concentration': malefic_concentration,
}
def analyze_d30_marriage_crisis(d30_planets: Dict) -> Dict:
def analyze_d30_marriage_crisis(d30_planets: Dict, d30_houses: Dict = None) -> Dict:
"""
D30 婚姻危机分析简化版
D30 婚姻危机分析 v7.0
D30 中第7宫伴侣/第8宫危机/第12宫损失的行星状态
用于判断婚姻中的深层危机模式
"""
# 简化:检查金星、7宫主、12宫主在 D30 中的状态
crisis_factors = []
venus_d30 = d30_planets.get('Venus', {}).get('d30_sign')
if venus_d30 is not None:
crisis_factors.append(f"D30 金星在 {SIGN_CN[venus_d30]}")
crisis_score = 0
# Venus在D30的状态
venus_d30 = d30_planets.get('Venus', {})
if venus_d30:
v_sign = venus_d30.get('d30_sign')
v_house = venus_d30.get('d30_house')
crisis_factors.append(f"D30 Venus在{SIGN_CN[v_sign]}")
if v_house and v_house in [6, 8, 12]:
crisis_score += 2
crisis_factors.append(f" Venus在D30第{v_house}宫 → 婚姻受克")
elif v_house and v_house in [1, 4, 7, 10]:
crisis_score -= 1
crisis_factors.append(f" Venus在D30第{v_house}宫(角宫) → 婚姻较稳")
# Mars在D30的状态
mars_d30 = d30_planets.get('Mars', {})
if mars_d30:
m_sign = mars_d30.get('d30_sign')
m_house = mars_d30.get('d30_house')
if m_house and m_house == 7:
crisis_score += 2
crisis_factors.append(f" Mars在D30第7宫 → 伴侣冲突/暴力风险")
elif m_house and m_house == 8:
crisis_score += 1
crisis_factors.append(f" Mars在D30第8宫 → 婚姻中突发危机")
# Saturn在D30的状态
sat_d30 = d30_planets.get('Saturn', {})
if sat_d30:
s_house = sat_d30.get('d30_house')
if s_house and s_house == 7:
crisis_score += 1
crisis_factors.append(f" Saturn在D30第7宫 → 婚姻延迟/冷漠")
# D30 8宫检查
if d30_houses:
h8_lord = d30_houses.get(8, {}).get('lord')
if h8_lord:
crisis_factors.append(f"D30 第8宫主:{h8_lord}")
# 综合评估
if crisis_score >= 4:
severity = "严重(婚姻危机风险高,需专业咨询)"
elif crisis_score >= 2:
severity = "中等(婚姻中有挑战,需主动经营)"
elif crisis_score >= 0:
severity = "轻微(婚姻危机风险低)"
else:
severity = "极低(婚姻较稳定)"
return {
'd30_marriage_crisis': crisis_factors,
'note': 'D30 完整分析需要 D30 宫位数据,建议使用专业软件(如 Jagannatha Hora',
'crisis_score': crisis_score,
'severity': severity,
}
def d30_full_report(birth_planet_lons: Dict[str, float]) -> Dict:
def _analyze_d30_planet_states(d30_planets: Dict) -> Dict:
"""D30 行星状态分析"""
states = {}
# 擢升/落陷星座表
EXALTATION = {'Sun': 0, 'Moon': 1, 'Mars': 9, 'Mercury': 5,
'Jupiter': 3, 'Venus': 11, 'Saturn': 6}
DEBILITATION = {'Sun': 6, 'Moon': 7, 'Mars': 3, 'Mercury': 11,
'Jupiter': 9, 'Venus': 5, 'Saturn': 0}
OWN = {'Sun': [4], 'Moon': [3], 'Mars': [0, 7], 'Mercury': [2, 5],
'Jupiter': [8, 11], 'Venus': [1, 6], 'Saturn': [9, 10]}
for pname, pdata in d30_planets.items():
d30_sign = pdata.get('d30_sign')
if d30_sign is None or pname not in EXALTATION:
continue
state = 'neutral'
if d30_sign == EXALTATION.get(pname, -1):
state = 'exalted'
elif d30_sign == DEBILITATION.get(pname, -1):
state = 'debilitated'
elif d30_sign in OWN.get(pname, []):
state = 'own'
states[pname] = {
'd30_sign': SIGN_NAMES[d30_sign],
'state': state,
'significance': _d30_state_significance(pname, state),
}
return states
def _d30_state_significance(planet: str, state: str) -> str:
"""D30中行星状态的解读"""
if state == 'exalted':
return f'{planet}在D30擢升 → 该行星能量在危机中表现为正面转化力'
elif state == 'debilitated':
return f'{planet}在D30落陷 → 该行星能量在危机中表现为负面放大'
elif state == 'own':
return f'{planet}在D30入庙 → 该行星能量在危机中表现稳定'
return f'{planet}在D30中性 → 危机中表现取决于其他因素'
def _analyze_malefic_concentration(d30_planets: Dict, d30_houses: Dict) -> Dict:
"""D30 凶星集中度分析"""
MALEFICS = {'Mars', 'Saturn', 'Rahu', 'Ketu', 'Sun'}
concentration = {}
# 按星座统计凶星
sign_malefics = {}
for pname in MALEFICS:
pdata = d30_planets.get(pname)
if pdata:
s = pdata.get('d30_sign')
if s is not None:
sign_malefics.setdefault(s, []).append(pname)
# 找出凶星集中度最高的星座
for sign_idx, planets in sign_malefics.items():
if len(planets) >= 2:
concentration[SIGN_NAMES[sign_idx]] = {
'malefics': planets,
'count': len(planets),
'severity': 'high' if len(planets) >= 3 else 'moderate',
}
# 按宫位统计(如果有宫位数据)
if d30_houses:
house_malefics = {}
for pname in MALEFICS:
pdata = d30_planets.get(pname)
if pdata:
h = pdata.get('d30_house')
if h:
house_malefics.setdefault(h, []).append(pname)
for house, planets in house_malefics.items():
if len(planets) >= 2:
concentration[f'House_{house}'] = {
'malefics': planets,
'count': len(planets),
'severity': 'high' if house in [6, 8, 12] else 'moderate',
}
return concentration
def d30_full_report(birth_planet_lons: Dict[str, float], asc_lon: float = None) -> Dict:
"""
D30 完整报告简化版
D30 完整报告 v7.0
"""
d30 = calc_d30_chart(birth_planet_lons)
crisis = analyze_d30_marriage_crisis(d30['planets'])
d30 = calc_d30_chart(birth_planet_lons, asc_lon)
crisis = analyze_d30_marriage_crisis(
d30['planets'],
d30.get('d30_houses')
)
return {
'd30_chart': d30,
'marriage_crisis': crisis,
'interpretation': _d30_basic_interpretation(d30['planets']),
'method': 'D30 Trimshamsa (Parashara)',
'limitation': 'D30 是高级分盘,精确解读需配合 D30 宫位和其他分盘交叉验证',
'interpretation': _d30_full_interpretation(d30, crisis),
'method': 'D30 Trimshamsa (Parashara经典规则)',
}
def _d30_basic_interpretation(d30_planets: Dict) -> str:
"""D30 基础解读"""
def _d30_full_interpretation(d30_data: Dict, crisis_data: Dict) -> str:
"""D30 完整解读 v7.0"""
lines = ["【D30 Trimshamsa 三十分盘分析】", ""]
lines.append("D30 用于分析灾难、苦难、重大危机和深层业力模式。")
lines.append(f"计算方法:Parashara经典规则(奇偶星座分区法)")
lines.append("")
# 重点行星
key_planets = ['Sun', 'Mars', 'Saturn', 'Rahu', 'Ketu']
for p in key_planets:
p_data = d30_planets.get(p)
if p_data:
lines.append(f"{p} 在 D30{p_data['d30_sign_cn']}")
# 上升信息
asc = d30_data.get('d30_ascendant')
if asc and asc.get('sign_cn'):
lines.append(f"D30 上升:{asc['sign_cn']} ({asc['sign']})")
lines.append("")
# 行星状态
states = d30_data.get('planet_states', {})
lines.append("#### 行星在D30中的状态")
for pname, sdata in states.items():
state_cn = {'exalted': '擢升', 'debilitated': '落陷', 'own': '入庙', 'neutral': '中性'}
lines.append(f" {pname}: {sdata['d30_sign']} ({state_cn.get(sdata['state'], sdata['state'])})")
lines.append("")
lines.append("⚠️ D30 解读需要高级技巧,建议:")
lines.append(" 1. 检查 D30 中凶星(火星/土星/罗睺/计都)是否集中在特定宫位")
lines.append(" 2. 与 D1/D9 交叉验证危机事件的时间线")
lines.append(" 3. 使用 Vimshottari Dasha 定位具体危机发生时期")
# 凶星集中度
concentration = d30_data.get('malefic_concentration', {})
if concentration:
lines.append("#### 凶星集中度")
for area, data in concentration.items():
lines.append(f" {area}: {data['malefics']} (集中度={data['severity']})")
lines.append("")
# 婚姻危机
if crisis_data.get('crisis_factors'):
lines.append(f"#### 婚姻危机评估: {crisis_data.get('severity', '未知')}")
for f in crisis_data['crisis_factors']:
lines.append(f" {f}")
lines.append("")
lines.append("注意:")
lines.append(" 1. D30中凶星集中 = 危机高发领域")
lines.append(" 2. 与D1/D9交叉验证危机事件的时间线")
lines.append(" 3. 使用Vimshottari Dasha定位具体危机发生时期")
return "\n".join(lines)
+372 -85
View File
@@ -31,109 +31,307 @@ NATURAL_MALEFICS = {'Saturn', 'Mars', 'Sun', 'Rahu', 'Ketu'}
def calc_raj_yogas(planets_data: Dict, houses: Dict) -> Dict:
"""
计算 Raj Yogas王者瑜伽权力地位社会影响力格局
计算 Raj Yogas王者瑜伽权力地位社会影响力格局 v7.0
经典 Raj Yoga 形成条件
1. 角宫主1/4/7/10宫主与三方宫主5/9宫主结合
2. 角宫主与角宫主结合
3. 三方宫主与三方宫主结合
4. 以上组合发生在角宫/三方宫/11
1. 角宫主1/4/7/10宫主与三方宫主5/9宫主同宫conjunction
2. 角宫主与三方宫主互看mutual aspect
3. 宫主与三方宫主互容parivartana
4. 同一星同时掌管角宫和三方宫dual lordship
5. Viparita Raja Yoga凶宫主6/8/12宫主落入另一个凶宫
参考dashaflow yoga.py (MIT)
返回检测到的 Raj Yogas 列表
"""
results = {'yogas': [], 'summary': ''}
SIGNS_LIST = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo',
'Libra','Scorpio','Sagittarius','Capricorn','Aquarius','Pisces']
# 提取宫主星信息
house_lords = {}
for h in range(1, 13):
lord_key = f'H{h}_Lord'
if lord_key in houses:
house_lords[h] = houses[lord_key]
def _lord_of_house(house_num):
"""获取某宫的宫主星"""
lkey = f'H{house_num}_Lord'
if lkey in houses:
return houses[lkey]
# 从行星数据推断
asc_sign = houses.get('asc_sign', '')
if asc_sign and asc_sign in SIGN_LORDS:
asc_idx = SIGNS_LIST.index(asc_sign) if asc_sign in SIGNS_LIST else 0
sign_idx = (asc_idx + house_num - 1) % 12
return SIGN_LORDS[SIGNS_LIST[sign_idx]]
return None
def _get_planet_sign_idx(pname):
"""获取行星所在星座索引"""
pdata = planets_data.get(pname, {})
if isinstance(pdata, dict) and 'sign' in pdata:
sign = pdata['sign']
if sign in SIGNS_LIST:
return SIGNS_LIST.index(sign)
# 从经度推断
if isinstance(pdata, dict) and 'longitude' in pdata:
return int(pdata['longitude'] / 30) % 12
return None
def _get_planet_house(pname):
"""获取行星所在宫位"""
pdata = planets_data.get(pname, {})
if isinstance(pdata, dict) and 'house' in pdata:
return pdata['house']
return None
# 检查每对宫主星的组合
kendras = [1, 4, 7, 10] # 角宫
trikonas = [5, 9] # 三方宫
trikonas = [1, 5, 9] # 三方宫(含1宫)
dusthanas = [6, 8, 12] # 凶宫
def _get_lord_sign_lord(house_num):
"""获取某宫宫主星及其所在宫位"""
lkey = f'H{house_num}_Lord'
if lkey not in houses:
return None, None
lord = houses[lkey]
# 找lord在哪里(简化:返回lord所在宫位)
for pname, pdata in planets_data.items():
if pname == lord and isinstance(pdata, dict) and 'house' in pdata:
return lord, pdata['house']
return lord, None
# ── 条件4: 双重宫主星(同一星掌管角宫+三方宫)──
kendra_lords = {}
trikona_lords = {}
for h in kendras:
lord = _lord_of_house(h)
if lord:
kendra_lords.setdefault(lord, []).append(h)
for h in trikonas:
lord = _lord_of_house(h)
if lord:
trikona_lords.setdefault(lord, []).append(h)
# 检查条件1:角宫主 × 三方宫主
for k_house in kendras:
for t_house in trikonas:
k_lord, k_lord_house = _get_lord_sign_lord(k_house)
t_lord, t_lord_house = _get_lord_sign_lord(t_house)
if not k_lord or not t_lord:
dual_lords = set(kendra_lords.keys()) & set(trikona_lords.keys())
for lord in dual_lords:
k_houses = kendra_lords[lord]
t_houses = trikona_lords[lord]
lord_house = _get_planet_house(lord)
if lord_house and lord_house in kendras + trikonas:
yoga = {
'type': 'Raj Yoga',
'subtype': 'dual_lordship',
'combination': f'{lord}(H{k_houses}主+H{t_houses}主)',
'formation_house': lord_house,
'strength': 'strong' if lord_house in kendras else 'moderate',
'interpretation': f'Raj Yoga——{lord}同时掌管角宫{k_houses}和三方宫{t_houses},且在{lord_house}宫,双重权力格局',
}
results['yogas'].append(yoga)
# ── 条件1+2+3: 角宫主 × 三方宫主 的组合检测 ──
pure_kendra_lords = set(kendra_lords.keys()) - dual_lords
pure_trikona_lords = set(trikona_lords.keys()) - dual_lords
for kl in pure_kendra_lords:
for tl in pure_trikona_lords:
kl_sign = _get_planet_sign_idx(kl)
tl_sign = _get_planet_sign_idx(tl)
if kl_sign is None or tl_sign is None:
continue
# 检查两主星是否在同一宫或互看对方宫
# 简化:检查两主星所在宫位是否形成权力宫(角/三方/11)
if k_lord_house and t_lord_house:
power_houses = kendras + trikonas + [11]
if k_lord_house in power_houses or t_lord_house in power_houses:
# 条件1: 同宫(conjunction
if kl_sign == tl_sign:
house_from_asc = (kl_sign - (SIGNS_LIST.index(houses.get('asc_sign', 'Aries')) if houses.get('asc_sign') in SIGNS_LIST else 0)) % 12 + 1
yoga = {
'type': 'Raj Yoga',
'subtype': 'conjunction',
'combination': f'{kl}(角宫主) + {tl}(三方宫主)',
'formation_house': house_from_asc,
'strength': 'strong' if house_from_asc in kendras else 'moderate',
'interpretation': f'Raj Yoga——{kl}(角宫主)与{tl}(三方宫主)同宫在{SIGNS_LIST[kl_sign]},权力格局',
}
results['yogas'].append(yoga)
# 条件2: 互看(mutual aspect: 7宫关系)
elif abs(kl_sign - tl_sign) == 6 or abs(kl_sign - tl_sign) == 6 + 12:
yoga = {
'type': 'Raj Yoga',
'subtype': 'mutual_aspect',
'combination': f'{kl}(角宫主) ↔ {tl}(三方宫主)',
'interpretation': f'Raj Yoga——{kl}(角宫主)与{tl}(三方宫主)互看(对宫相位),权力格局',
}
results['yogas'].append(yoga)
# 条件3: 互容(parivartana
else:
# 检查kl是否在tl掌管的星座,且tl是否在kl掌管的星座
kl_lord_signs = [] # kl掌管的星座
tl_lord_signs = [] # tl掌管的星座
for s_idx, s_name in enumerate(SIGNS_LIST):
if SIGN_LORDS[s_name] == kl:
kl_lord_signs.append(s_idx)
if SIGN_LORDS[s_name] == tl:
tl_lord_signs.append(s_idx)
if tl_sign in kl_lord_signs and kl_sign in tl_lord_signs:
yoga = {
'type': 'Raj Yoga',
'combination': f'H{k_house}_Lord({k_lord}) + H{t_house}_Lord({t_lord})',
'formation_house': k_lord_house,
'strength': 'strong' if k_lord_house in kendras else 'moderate',
'interpretation': f'Raj Yoga——{k_lord}(H{k_house}主)与{t_lord}(H{t_house}主)结合,权力与地位格局',
'subtype': 'parivartana',
'combination': f'{kl}(角宫主) ⇄ {tl}(三方宫主)',
'interpretation': f'Raj Yoga——{kl}(角宫主)与{tl}(三方宫主)互容交换,权力格局强化',
}
results['yogas'].append(yoga)
# ── 条件5: Viparita Raja Yoga ──
# 6/8/12宫主落入另一个凶宫(凶中凶=逆转大吉)
dusthana_lords = {}
for h in dusthanas:
lord = _lord_of_house(h)
if lord:
dusthana_lords[h] = lord
for h, lord in dusthana_lords.items():
lord_house = _get_planet_house(lord)
if lord_house and lord_house in dusthanas and lord_house != h:
yoga = {
'type': 'Viparita Raja Yoga',
'subtype': 'dusthana_in_dusthana',
'combination': f'H{h}_Lord({lord}) in H{lord_house}',
'interpretation': f'Viparita Raja Yoga——{h}宫主{lord}落入{lord_house}宫(凶中凶),先苦后甜逆转格局',
}
results['yogas'].append(yoga)
results['summary'] = f"Raj Yoga检测:共{len(results['yogas'])}个格局"
return results
def calc_dhana_yogas(planets_data: Dict, houses: Dict) -> Dict:
"""
计算 Dhana Yogas财富瑜伽财富积累格局
计算 Dhana Yogas财富瑜伽财富积累格局 v7.0
经典 Dhana Yoga 形成条件
1. 2宫主财富宫主与吉星/11宫主结合
2. 11宫主收益宫主与吉星/2宫主结合
3. 以上组合发生在2/11//三方宫
经典 Dhana Yoga 形成条件参考dashaflow yoga.py MIT
1. 2宫主+11宫主同在角宫/三方宫
2. 5宫主+9宫主同宫或互看
3. 2宫主+9宫主财富+幸运/互看
4. 11宫主+9宫主收益+幸运同宫/互看
5. 2/11宫主与吉星Jupiter/Venus同宫
返回检测到的 Dhana Yogas 列表
"""
results = {'yogas': [], 'summary': ''}
# 2宫主和11宫主
h2_lord = houses.get('H2_Lord', '')
h11_lord = houses.get('H11_Lord', '')
SIGNS_LIST = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo',
'Libra','Scorpio','Sagittarius','Capricorn','Aquarius','Pisces']
KENDRA = {1, 4, 7, 10}
TRIKONA = {1, 5, 9}
GOOD_HOUSES = KENDRA | TRIKONA | {2, 11}
BENEFICS = {'Jupiter', 'Venus'}
if not h2_lord or not h11_lord:
results['summary'] = 'Dhana Yoga检测:缺少2/11宫主信息'
return results
def _lord_of_house(house_num):
lkey = f'H{house_num}_Lord'
if lkey in houses:
return houses[lkey]
asc_sign = houses.get('asc_sign', '')
if asc_sign and asc_sign in SIGN_LORDS:
asc_idx = SIGNS_LIST.index(asc_sign) if asc_sign in SIGNS_LIST else 0
sign_idx = (asc_idx + house_num - 1) % 12
return SIGN_LORDS[SIGNS_LIST[sign_idx]]
return None
# 检查2宫主和11宫主的组合
# 找h2_lord和h11_lord的宫位
h2_lord_house = None
h11_lord_house = None
for pname, pdata in planets_data.items():
if pname == h2_lord and isinstance(pdata, dict) and 'house' in pdata:
h2_lord_house = pdata['house']
if pname == h11_lord and isinstance(pdata, dict) and 'house' in pdata:
h11_lord_house = pdata['house']
def _get_planet_house(pname):
pdata = planets_data.get(pname, {})
if isinstance(pdata, dict) and 'house' in pdata:
return pdata['house']
return None
if h2_lord_house and h11_lord_house:
wealth_houses = [2, 11, 1, 4, 7, 10, 5, 9]
if h2_lord_house in wealth_houses or h11_lord_house in wealth_houses:
yoga = {
def _get_planet_sign(pname):
pdata = planets_data.get(pname, {})
if isinstance(pdata, dict) and 'sign' in pdata:
return pdata['sign']
return None
def _same_sign(p1, p2):
s1 = _get_planet_sign(p1)
s2 = _get_planet_sign(p2)
return s1 and s2 and s1 == s2
# ── 条件1: 2宫主+11宫主同在角宫/三方宫 ──
lord_2 = _lord_of_house(2)
lord_11 = _lord_of_house(11)
if lord_2 and lord_11:
h2 = _get_planet_house(lord_2)
h11 = _get_planet_house(lord_11)
if h2 and h11 and h2 in GOOD_HOUSES and h11 in GOOD_HOUSES:
results['yogas'].append({
'type': 'Dhana Yoga',
'combination': f'H2_Lord({h2_lord}) + H11_Lord({h11_lord})',
'formation_house': h2_lord_house,
'strength': 'strong' if h2_lord_house in [2, 11] else 'moderate',
'interpretation': f'Dhana Yoga——{h2_lord}(H2主)与{h11_lord}(H11主)结合,财富积累格局',
}
results['yogas'].append(yoga)
'subtype': '2nd_11th_lords_strong',
'combination': f'H2_Lord({lord_2}) in H{h2} + H11_Lord({lord_11}) in H{h11}',
'strength': 'strong' if h2 in KENDRA and h11 in KENDRA else 'moderate',
'interpretation': f'Dhana Yoga——2宫主{lord_2}(H{h2})与11宫主{lord_11}(H{h11})均落强宫,财富积累格局',
})
# ── 条件2: 5宫主+9宫主同宫/互看 ──
lord_5 = _lord_of_house(5)
lord_9 = _lord_of_house(9)
if lord_5 and lord_9:
if _same_sign(lord_5, lord_9):
s = _get_planet_sign(lord_5)
results['yogas'].append({
'type': 'Dhana Yoga',
'subtype': '5th_9th_conjunction',
'combination': f'H5_Lord({lord_5}) + H9_Lord({lord_9})',
'strength': 'strong',
'interpretation': f'Dhana Yoga——5宫主{lord_5}与9宫主{lord_9}同宫在{s},财富+幸运格局',
})
else:
h5 = _get_planet_house(lord_5)
h9 = _get_planet_house(lord_9)
if h5 and h9:
# 互看: 7宫关系
if abs(h5 - h9) == 6:
results['yogas'].append({
'type': 'Dhana Yoga',
'subtype': '5th_9th_mutual_aspect',
'combination': f'H5_Lord({lord_5}) ↔ H9_Lord({lord_9})',
'strength': 'moderate',
'interpretation': f'Dhana Yoga——5宫主{lord_5}(H{h5})与9宫主{lord_9}(H{h9})互看,财富格局',
})
# 同在角宫/三方宫
elif h5 in GOOD_HOUSES and h9 in GOOD_HOUSES:
results['yogas'].append({
'type': 'Dhana Yoga',
'subtype': '5th_9th_strong_houses',
'combination': f'H5_Lord({lord_5}) in H{h5} + H9_Lord({lord_9}) in H{h9}',
'strength': 'moderate',
'interpretation': f'Dhana Yoga——5宫主{lord_5}与9宫主{lord_9}均落强宫,财富格局',
})
# ── 条件3: 2宫主+9宫主同宫/互看 ──
if lord_2 and lord_9:
if _same_sign(lord_2, lord_9):
s = _get_planet_sign(lord_2)
results['yogas'].append({
'type': 'Dhana Yoga',
'subtype': '2nd_9th_conjunction',
'combination': f'H2_Lord({lord_2}) + H9_Lord({lord_9})',
'strength': 'strong',
'interpretation': f'Dhana Yoga——2宫主{lord_2}与9宫主{lord_9}同宫在{s},财富+幸运组合',
})
# ── 条件4: 11宫主+9宫主同宫/互看 ──
if lord_11 and lord_9:
if _same_sign(lord_11, lord_9):
s = _get_planet_sign(lord_11)
results['yogas'].append({
'type': 'Dhana Yoga',
'subtype': '11th_9th_conjunction',
'combination': f'H11_Lord({lord_11}) + H9_Lord({lord_9})',
'strength': 'strong',
'interpretation': f'Dhana Yoga——11宫主{lord_11}与9宫主{lord_9}同宫在{s},收益+幸运组合',
})
# ── 条件5: 2/11宫主与吉星同宫 ──
for wealth_lord, w_house in [(lord_2, 2), (lord_11, 11)]:
if not wealth_lord:
continue
for benefic in BENEFICS:
if _same_sign(wealth_lord, benefic):
s = _get_planet_sign(wealth_lord)
results['yogas'].append({
'type': 'Dhana Yoga',
'subtype': f'{w_house}th_lord_benefic_conjunction',
'combination': f'H{w_house}_Lord({wealth_lord}) + {benefic}',
'strength': 'moderate',
'interpretation': f'Dhana Yoga——{w_house}宫主{wealth_lord}与吉星{benefic}同宫在{s},财富助力格局',
})
results['summary'] = f"Dhana Yoga检测:共{len(results['yogas'])}个格局"
return results
@@ -202,22 +400,66 @@ def calc_pancha_mahapurusha_yoga(planets_data: Dict) -> Dict:
def calc_nicha_bhanga_raj_yoga(planets_data: Dict, houses: Dict) -> Dict:
"""
计算 Neecha Bhanga Raj Yoga落陷解除王者瑜伽
计算 Neecha Bhanga Raj Yoga落陷解除王者瑜伽v7.0
条件同时满足
条件满足落陷 + 解除参考dashaflow yoga.py MIT
1. 某行星落陷在落陷星座
2. 该行星的落陷星座主星在某个角宫/三方宫
3. 或者落陷星座主星与落陷行星形成互容
解除条件满足任一即可
A. 落陷星座主星dispositor在Lagna角宫(1/4/7/10)
B. 落陷星座主星在Moon角宫
C. 擢升星座主星在Lagna角宫
D. 擢升星座主星在Moon角宫
E. 落陷星与落陷星座主星互容parivartana
F. 落陷星在Navamsa中入庙/擢升vargottama缓解
经典落陷+落陷解除 = 王者瑜伽先抑后扬大器晚成
"""
results = {'yogas': [], 'summary': ''}
SIGNS_LIST = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo',
'Libra','Scorpio','Sagittarius','Capricorn','Aquarius','Pisces']
# 落陷星座表
debilitation = {'Mars': 'Cancer', 'Mercury': 'Pisces', 'Jupiter': 'Capricorn',
'Venus': 'Virgo', 'Saturn': 'Aries', 'Sun': 'Libra',
'Moon': 'Scorpio'}
# 擢升星座表
exaltation = {'Sun': 'Aries', 'Moon': 'Taurus', 'Mars': 'Capricorn',
'Mercury': 'Virgo', 'Jupiter': 'Cancer', 'Venus': 'Pisces',
'Saturn': 'Libra'}
KENDRA = {1, 4, 7, 10}
def _get_planet_house(pname):
pdata = planets_data.get(pname, {})
if isinstance(pdata, dict) and 'house' in pdata:
return pdata['house']
return None
def _get_planet_sign(pname):
pdata = planets_data.get(pname, {})
if isinstance(pdata, dict) and 'sign' in pdata:
return pdata['sign']
return None
def _get_planet_sign_idx(pname):
s = _get_planet_sign(pname)
if s and s in SIGNS_LIST:
return SIGNS_LIST.index(s)
return None
def _get_moon_house():
return _get_planet_house('Moon')
def _house_from_moon(planet_name):
"""计算从Moon看某行星在第几宫"""
moon_idx = _get_planet_sign_idx('Moon')
p_idx = _get_planet_sign_idx(planet_name)
if moon_idx is not None and p_idx is not None:
return ((p_idx - moon_idx) % 12) + 1
return None
for pname, deb_sign in debilitation.items():
pdata = planets_data.get(pname, {})
if not isinstance(pdata, dict) or 'sign' not in pdata:
@@ -227,30 +469,75 @@ def calc_nicha_bhanga_raj_yoga(planets_data: Dict, houses: Dict) -> Dict:
if sign != deb_sign:
continue # 没落陷
# 检查落陷解除条件:
# 条件A:落陷星座主星在角宫/三方宫
cancellation = False
cancel_reasons = []
# 条件A: 落陷星座主星(dispositor)在Lagna角宫
deb_lord = SIGN_LORDS.get(deb_sign, '')
deb_lord_data = planets_data.get(deb_lord, {})
deb_lord_house = deb_lord_data.get('house') if isinstance(deb_lord_data, dict) else None
deb_lord_house = _get_planet_house(deb_lord)
if deb_lord_house and deb_lord_house in KENDRA:
cancellation = True
cancel_reasons.append(f'定位星{deb_lord}在Lagna第{deb_lord_house}宫(角宫)')
condition_met = False
if deb_lord_house and deb_lord_house in [1, 4, 7, 10, 5, 9]:
condition_met = True
# 条件B: 落陷星座主星在Moon角宫
if deb_lord:
hfm = _house_from_moon(deb_lord)
if hfm and hfm in KENDRA:
cancellation = True
cancel_reasons.append(f'定位星{deb_lord}在Moon第{hfm}宫(角宫)')
# 条件B:落陷行星与落陷星座主星互容(在两星星座中)
# 简化:检查两星是否在同一宫
p_house = pdata.get('house')
if deb_lord_house and p_house and deb_lord_house == p_house:
condition_met = True
# 条件C: 擢升星座主星在Lagna角宫
exalt_sign = exaltation.get(pname, '')
exalt_lord = SIGN_LORDS.get(exalt_sign, '')
if exalt_lord:
exalt_lord_house = _get_planet_house(exalt_lord)
if exalt_lord_house and exalt_lord_house in KENDRA:
cancellation = True
cancel_reasons.append(f'擢升星主{exalt_lord}在Lagna第{exalt_lord_house}宫(角宫)')
# 条件D: 擢升星座主星在Moon角宫
if exalt_lord:
hfm = _house_from_moon(exalt_lord)
if hfm and hfm in KENDRA:
cancellation = True
cancel_reasons.append(f'擢升星主{exalt_lord}在Moon第{hfm}宫(角宫)')
# 条件E: 落陷星与定位星互容(parivartana
if deb_lord and deb_lord != pname:
p_sign = _get_planet_sign(pname)
lord_sign = _get_planet_sign(deb_lord)
if p_sign and lord_sign:
# pname在deb_sign(由deb_lord掌管), deb_lord是否在pname掌管的星座?
pname_own_signs = [s for s, l in SIGN_LORDS.items() if l == pname]
if lord_sign in pname_own_signs:
cancellation = True
cancel_reasons.append(f'{pname}{deb_lord}互容(Parivartana)')
# 条件F: Navamsa入庙/擢升缓解(如果有navamsa数据)
navamsa_sign = pdata.get('navamsa_sign')
if navamsa_sign:
own_signs = [s for s, l in SIGN_LORDS.items() if l == pname]
if navamsa_sign in own_signs or navamsa_sign == exaltation.get(pname, ''):
cancellation = True
cancel_reasons.append(f'{pname}在Navamsa中入庙/擢升({navamsa_sign})')
if cancellation:
# 量化解除程度
cancel_count = len(cancel_reasons)
strength = 'very strong' if cancel_count >= 3 else 'strong' if cancel_count >= 2 else 'moderate'
if condition_met:
yoga = {
'type': 'Neecha Bhanga Raj Yoga',
'planet': pname,
'debilitated_sign': deb_sign,
'debility_lord': deb_lord,
'lord_house': deb_lord_house,
'interpretation': f'Neecha Bhanga Raj Yoga——{pname}{deb_sign}落陷但解除({deb_lord}{deb_lord_house}宫),先抑后扬大器晚成',
'exaltation_sign': exalt_sign,
'exaltation_lord': exalt_lord,
'cancellation_reasons': cancel_reasons,
'cancellation_count': cancel_count,
'strength': strength,
'interpretation': f'Neecha Bhanga Raj Yoga——{pname}{deb_sign}落陷但解除({"; ".join(cancel_reasons)}),先抑后扬大器晚成',
}
results['yogas'].append(yoga)