1027 lines
42 KiB
Python
1027 lines
42 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
Divisional Charts Extended - D2-D60完整分盘宫位图计算器
|
||
印度占星 Shodasavarga(16分盘)+ 扩展分盘系统
|
||
|
||
核心功能:
|
||
- 计算D1-D60所有分盘的行星位置
|
||
- 生成每个分盘的12宫位行星分布图
|
||
- 支持BPHS标准算法
|
||
- 提供分盘上升点计算
|
||
- 输出可视化宫位图数据
|
||
|
||
参考文献:
|
||
- Brihat Parashara Hora Shastra (BPHS) Chapter 6
|
||
- Phaladeepika by Mantreswara
|
||
- Jataka Parijata by Vaidyanatha Dikshita
|
||
"""
|
||
|
||
from typing import Dict, List, Tuple
|
||
from enum import Enum
|
||
|
||
class VargaType(Enum):
|
||
"""分盘类型枚举"""
|
||
D1 = (1, "Rashi", "本命盘")
|
||
D2 = (2, "Hora", "财富")
|
||
D3 = (3, "Drekkana", "兄弟姐妹")
|
||
D4 = (4, "Chaturthamsa", "财产/运气")
|
||
D5 = (5, "Panchamsa", "名声/权力")
|
||
D6 = (6, "Shashthamsa", "健康/敌人")
|
||
D7 = (7, "Saptamsa", "子女")
|
||
D8 = (8, "Ashtamsa", "突发事件")
|
||
D9 = (9, "Navamsa", "配偶/灵性")
|
||
D10 = (10, "Dasamsa", "事业")
|
||
D11 = (11, "Rudramsa", "破坏/转化")
|
||
D12 = (12, "Dwadasamsa", "父母")
|
||
D16 = (16, "Shodasamsa", "交通工具/舒适")
|
||
D20 = (20, "Vimsamsa", "灵性修行")
|
||
D24 = (24, "Chaturvimsamsa", "教育/学习")
|
||
D27 = (27, "Bhamsa", "力量/弱点")
|
||
D30 = (30, "Trimsamsa", "不幸/困难")
|
||
D40 = (40, "Khavedamsa", "吉凶效果")
|
||
D45 = (45, "Akshavedamsa", "全面判断")
|
||
D60 = (60, "Shashtiamsa", "前世业力")
|
||
D81 = (81, "Navamsa-Navamsa", "D9之D9精微分盘")
|
||
D108 = (108, "Dwadasamsa-Navamsa", "D12之D9精微分盘")
|
||
D144 = (144, "Dwadasamsa-Dwadasamsa", "D12之D12精微分盘")
|
||
|
||
def __init__(self, division: int, name: str, meaning: str):
|
||
self.division = division
|
||
self.varga_name = name
|
||
self.meaning = meaning
|
||
|
||
class DivisionalChartsCalculator:
|
||
"""分盘计算器"""
|
||
|
||
SIGNS = ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
|
||
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"]
|
||
|
||
# 星座分类
|
||
MOVABLE_SIGNS = [0, 3, 6, 9] # Aries, Cancer, Libra, Capricorn
|
||
FIXED_SIGNS = [1, 4, 7, 10] # Taurus, Leo, Scorpio, Aquarius
|
||
DUAL_SIGNS = [2, 5, 8, 11] # Gemini, Virgo, Sagittarius, Pisces
|
||
|
||
ODD_SIGNS = [0, 2, 4, 6, 8, 10] # 奇数星座
|
||
EVEN_SIGNS = [1, 3, 5, 7, 9, 11] # 偶数星座
|
||
|
||
def __init__(self):
|
||
pass
|
||
|
||
def _position_parts(self, longitude: float) -> Tuple[int, float, float]:
|
||
"""Normalize a varga longitude into sign index, sign degree and 0-360 longitude."""
|
||
normalized = longitude % 360.0
|
||
sign_idx = int(normalized // 30) % 12
|
||
sign_degree = normalized % 30
|
||
return sign_idx, sign_degree, normalized
|
||
|
||
def calculate_all_vargas(self, planet_positions: Dict[str, float],
|
||
asc_degree: float) -> Dict[str, Dict]:
|
||
"""
|
||
计算所有分盘的行星位置
|
||
|
||
Args:
|
||
planet_positions: {行星名: 黄道度数(0-360)}
|
||
asc_degree: 上升点黄道度数(0-360)
|
||
|
||
Returns:
|
||
{分盘名: {
|
||
"planets": {行星名: {"sign": 星座, "degree": 度数, "house": 宫位}},
|
||
"ascendant": {"sign": 星座, "degree": 度数},
|
||
"house_chart": [宫位1的行星列表, 宫位2的行星列表, ...]
|
||
}}
|
||
"""
|
||
results = {}
|
||
|
||
# 计算所有分盘
|
||
for varga_type in VargaType:
|
||
varga_result = self._calculate_single_varga(
|
||
varga_type, planet_positions, asc_degree
|
||
)
|
||
results[varga_type.varga_name] = varga_result
|
||
|
||
return results
|
||
|
||
def _calculate_single_varga(self, varga_type: VargaType,
|
||
planet_positions: Dict[str, float],
|
||
asc_degree: float) -> Dict:
|
||
"""计算单个分盘"""
|
||
division = varga_type.division
|
||
|
||
# 计算分盘上升点
|
||
varga_asc = self._calculate_varga_position(asc_degree, division)
|
||
varga_asc_sign, varga_asc_degree, varga_asc_abs = self._position_parts(varga_asc)
|
||
|
||
# 计算所有行星的分盘位置
|
||
varga_planets = {}
|
||
for planet, degree in planet_positions.items():
|
||
varga_pos = self._calculate_varga_position(degree, division)
|
||
varga_sign, varga_degree, varga_abs = self._position_parts(varga_pos)
|
||
|
||
# 计算宫位(从上升点开始)
|
||
house = ((varga_sign - varga_asc_sign) % 12) + 1
|
||
|
||
varga_planets[planet] = {
|
||
"sign": self.SIGNS[varga_sign],
|
||
"sign_index": varga_sign,
|
||
"degree": round(varga_degree, 4),
|
||
"house": house,
|
||
"absolute_degree": round(varga_abs, 4)
|
||
}
|
||
|
||
# 生成宫位图(12个宫位,每个宫位包含的行星列表)
|
||
house_chart = [[] for _ in range(12)]
|
||
for planet, data in varga_planets.items():
|
||
house_chart[data["house"] - 1].append(planet)
|
||
|
||
return {
|
||
"division": division,
|
||
"name": varga_type.varga_name,
|
||
"meaning": varga_type.meaning,
|
||
"ascendant": {
|
||
"sign": self.SIGNS[varga_asc_sign],
|
||
"sign_index": varga_asc_sign,
|
||
"degree": round(varga_asc_degree, 4),
|
||
"absolute_degree": round(varga_asc_abs, 4)
|
||
},
|
||
"planets": varga_planets,
|
||
"house_chart": house_chart
|
||
}
|
||
|
||
def _calculate_varga_position(self, degree: float, division: int) -> float:
|
||
"""
|
||
计算分盘位置(BPHS标准算法)
|
||
|
||
Args:
|
||
degree: 黄道度数(0-360)
|
||
division: 分盘数(2-60)
|
||
|
||
Returns:
|
||
分盘中的黄道度数(0-360)
|
||
"""
|
||
# 获取星座索引和星座内度数
|
||
sign_index = int(degree // 30)
|
||
sign_degree = degree % 30
|
||
|
||
# 根据不同分盘使用不同算法
|
||
if division == 2:
|
||
return self._calculate_d2(sign_index, sign_degree)
|
||
elif division == 3:
|
||
return self._calculate_d3(sign_index, sign_degree)
|
||
elif division == 4:
|
||
return self._calculate_d4(sign_index, sign_degree)
|
||
elif division == 5:
|
||
return self._calculate_d5(sign_index, sign_degree)
|
||
elif division == 6:
|
||
return self._calculate_d6(sign_index, sign_degree)
|
||
elif division == 7:
|
||
return self._calculate_d7(sign_index, sign_degree)
|
||
elif division == 8:
|
||
return self._calculate_d8(sign_index, sign_degree)
|
||
elif division == 9:
|
||
return self._calculate_d9(sign_index, sign_degree)
|
||
elif division == 10:
|
||
return self._calculate_d10(sign_index, sign_degree)
|
||
elif division == 11:
|
||
return self._calculate_d11(sign_index, sign_degree)
|
||
elif division == 12:
|
||
return self._calculate_d12(sign_index, sign_degree)
|
||
elif division == 16:
|
||
return self._calculate_d16(sign_index, sign_degree)
|
||
elif division == 20:
|
||
return self._calculate_d20(sign_index, sign_degree)
|
||
elif division == 24:
|
||
return self._calculate_d24(sign_index, sign_degree)
|
||
elif division == 27:
|
||
return self._calculate_d27(sign_index, sign_degree)
|
||
elif division == 30:
|
||
return self._calculate_d30(sign_index, sign_degree)
|
||
elif division == 40:
|
||
return self._calculate_d40(sign_index, sign_degree)
|
||
elif division == 45:
|
||
return self._calculate_d45(sign_index, sign_degree)
|
||
elif division == 60:
|
||
return self._calculate_d60(sign_index, sign_degree)
|
||
elif division == 81:
|
||
return self._calculate_d81(sign_index, sign_degree)
|
||
elif division == 108:
|
||
return self._calculate_d108(sign_index, sign_degree)
|
||
elif division == 144:
|
||
return self._calculate_d144(sign_index, sign_degree)
|
||
else:
|
||
# 通用算法(适用于其他分盘)
|
||
return self._calculate_generic_varga(sign_index, sign_degree, division)
|
||
|
||
def _calculate_d2(self, sign_index: int, sign_degree: float) -> float:
|
||
"""D2 Hora - 财富分盘"""
|
||
# 奇数星座:0-15度→Leo,15-30度→Cancer
|
||
# 偶数星座:0-15度→Cancer,15-30度→Leo
|
||
if sign_index in self.ODD_SIGNS:
|
||
if sign_degree < 15:
|
||
return 4 * 30 + sign_degree * 2 # Leo
|
||
else:
|
||
return 3 * 30 + (sign_degree - 15) * 2 # Cancer
|
||
else:
|
||
if sign_degree < 15:
|
||
return 3 * 30 + sign_degree * 2 # Cancer
|
||
else:
|
||
return 4 * 30 + (sign_degree - 15) * 2 # Leo
|
||
|
||
def _calculate_d3(self, sign_index: int, sign_degree: float) -> float:
|
||
"""D3 Drekkana - 兄弟姐妹分盘"""
|
||
# 每个星座分为3个10度区间
|
||
# 第1个10度→本星座,第2个10度→第5个星座,第3个10度→第9个星座
|
||
drekkana = int(sign_degree // 10)
|
||
offset = [0, 4, 8][drekkana]
|
||
varga_sign = (sign_index + offset) % 12
|
||
varga_degree = (sign_degree % 10) * 3
|
||
return varga_sign * 30 + varga_degree
|
||
|
||
def _calculate_d4(self, sign_index: int, sign_degree: float) -> float:
|
||
"""D4 Chaturthamsa - 财产/运气分盘"""
|
||
# 每个星座分为4个7.5度区间
|
||
part = int(sign_degree // 7.5)
|
||
varga_sign = (sign_index + part * 3) % 12
|
||
varga_degree = (sign_degree % 7.5) * 4
|
||
return varga_sign * 30 + varga_degree
|
||
|
||
def _calculate_d7(self, sign_index: int, sign_degree: float) -> float:
|
||
"""D7 Saptamsa - 子女分盘"""
|
||
# 每个星座分为7个约4.286度区间
|
||
part = int(sign_degree // (30/7))
|
||
if sign_index in self.ODD_SIGNS:
|
||
varga_sign = (sign_index + part) % 12
|
||
else:
|
||
varga_sign = (sign_index + 6 + part) % 12
|
||
varga_degree = (sign_degree % (30/7)) * 7
|
||
return varga_sign * 30 + varga_degree
|
||
|
||
def _calculate_d9(self, sign_index: int, sign_degree: float) -> float:
|
||
"""D9 Navamsa - 配偶/灵性分盘(最重要的分盘)"""
|
||
# 每个星座分为9个3.333度区间
|
||
part = int(sign_degree // (30/9))
|
||
|
||
# 根据星座类型确定起始点 (BPHS标准)
|
||
# Movable(白羊/巨蟹/天秤/摩羯)=从本星座开始
|
||
# Fixed(金牛/狮子/天蝎/水瓶)=从第9个星座开始(+8)
|
||
# Dual(双子/处女/射手/双鱼)=从第5个星座开始(+4)
|
||
if sign_index in self.MOVABLE_SIGNS:
|
||
start = sign_index
|
||
elif sign_index in self.FIXED_SIGNS:
|
||
start = (sign_index + 8) % 12
|
||
else: # DUAL_SIGNS
|
||
start = (sign_index + 4) % 12
|
||
|
||
varga_sign = (start + part) % 12
|
||
varga_degree = (sign_degree % (30/9)) * 9
|
||
return varga_sign * 30 + varga_degree
|
||
|
||
def _calculate_d10(self, sign_index: int, sign_degree: float) -> float:
|
||
"""D10 Dasamsa - 事业分盘"""
|
||
# 每个星座分为10个3度区间
|
||
part = int(sign_degree // 3)
|
||
if sign_index in self.ODD_SIGNS:
|
||
varga_sign = (sign_index + part) % 12
|
||
else:
|
||
varga_sign = (sign_index + 8 + part) % 12
|
||
varga_degree = (sign_degree % 3) * 10
|
||
return varga_sign * 30 + varga_degree
|
||
|
||
def _calculate_d12(self, sign_index: int, sign_degree: float) -> float:
|
||
"""D12 Dwadasamsa - 父母分盘"""
|
||
# 每个星座分为12个2.5度区间
|
||
part = int(sign_degree // 2.5)
|
||
varga_sign = (sign_index + part) % 12
|
||
varga_degree = (sign_degree % 2.5) * 12
|
||
return varga_sign * 30 + varga_degree
|
||
|
||
def _calculate_d16(self, sign_index: int, sign_degree: float) -> float:
|
||
"""D16 Shodasamsa - 交通工具/舒适分盘"""
|
||
# 每个星座分为16个1.875度区间
|
||
part = int(sign_degree // 1.875)
|
||
if sign_index in self.MOVABLE_SIGNS:
|
||
start = sign_index
|
||
elif sign_index in self.FIXED_SIGNS:
|
||
start = (sign_index + 4) % 12
|
||
else:
|
||
start = (sign_index + 8) % 12
|
||
varga_sign = (start + part) % 12
|
||
varga_degree = (sign_degree % 1.875) * 16
|
||
return varga_sign * 30 + varga_degree
|
||
|
||
def _calculate_d20(self, sign_index: int, sign_degree: float) -> float:
|
||
"""D20 Vimsamsa - 灵性修行分盘"""
|
||
# 每个星座分为20个1.5度区间
|
||
part = int(sign_degree // 1.5)
|
||
if sign_index in self.MOVABLE_SIGNS:
|
||
start = sign_index
|
||
elif sign_index in self.FIXED_SIGNS:
|
||
start = (sign_index + 8) % 12
|
||
else:
|
||
start = (sign_index + 4) % 12
|
||
varga_sign = (start + part) % 12
|
||
varga_degree = (sign_degree % 1.5) * 20
|
||
return varga_sign * 30 + varga_degree
|
||
|
||
def _calculate_d24(self, sign_index: int, sign_degree: float) -> float:
|
||
"""D24 Chaturvimsamsa - 教育/学习分盘"""
|
||
# 每个星座分为24个1.25度区间
|
||
part = int(sign_degree // 1.25)
|
||
if sign_index in self.ODD_SIGNS:
|
||
varga_sign = (4 + part) % 12 # 从Leo开始
|
||
else:
|
||
varga_sign = (3 + part) % 12 # 从Cancer开始
|
||
varga_degree = (sign_degree % 1.25) * 24
|
||
return varga_sign * 30 + varga_degree
|
||
|
||
def _calculate_d27(self, sign_index: int, sign_degree: float) -> float:
|
||
"""D27 Bhamsa - 力量/弱点分盘"""
|
||
# 每个星座分为27个1.111度区间
|
||
part = int(sign_degree // (30/27))
|
||
if sign_index in self.ODD_SIGNS:
|
||
start = sign_index
|
||
else:
|
||
start = (sign_index + 8) % 12
|
||
varga_sign = (start + part) % 12
|
||
varga_degree = (sign_degree % (30/27)) * 27
|
||
return varga_sign * 30 + varga_degree
|
||
|
||
def _calculate_d30(self, sign_index: int, sign_degree: float) -> float:
|
||
"""D30 Trimsamsa - 不幸/困难分盘(特殊算法)"""
|
||
# D30使用特殊的不等分算法
|
||
# 奇数星座:Mars(5°), Saturn(5°), Jupiter(8°), Mercury(7°), Venus(5°)
|
||
# 偶数星座:Venus(5°), Mercury(7°), Jupiter(8°), Saturn(5°), Mars(5°)
|
||
|
||
if sign_index in self.ODD_SIGNS:
|
||
if sign_degree < 5:
|
||
varga_sign = 0 # Aries (Mars)
|
||
varga_degree = sign_degree * 6
|
||
elif sign_degree < 10:
|
||
varga_sign = 10 # Aquarius (Saturn)
|
||
varga_degree = (sign_degree - 5) * 6
|
||
elif sign_degree < 18:
|
||
varga_sign = 8 # Sagittarius (Jupiter)
|
||
varga_degree = (sign_degree - 10) * 3.75
|
||
elif sign_degree < 25:
|
||
varga_sign = 2 # Gemini (Mercury)
|
||
varga_degree = (sign_degree - 18) * 4.286
|
||
else:
|
||
varga_sign = 1 # Taurus (Venus)
|
||
varga_degree = (sign_degree - 25) * 6
|
||
else:
|
||
if sign_degree < 5:
|
||
varga_sign = 1 # Taurus (Venus)
|
||
varga_degree = sign_degree * 6
|
||
elif sign_degree < 12:
|
||
varga_sign = 2 # Gemini (Mercury)
|
||
varga_degree = (sign_degree - 5) * 4.286
|
||
elif sign_degree < 20:
|
||
varga_sign = 8 # Sagittarius (Jupiter)
|
||
varga_degree = (sign_degree - 12) * 3.75
|
||
elif sign_degree < 25:
|
||
varga_sign = 10 # Aquarius (Saturn)
|
||
varga_degree = (sign_degree - 20) * 6
|
||
else:
|
||
varga_sign = 0 # Aries (Mars)
|
||
varga_degree = (sign_degree - 25) * 6
|
||
|
||
return varga_sign * 30 + varga_degree
|
||
|
||
def _calculate_d40(self, sign_index: int, sign_degree: float) -> float:
|
||
"""D40 Khavedamsa - 吉凶效果分盘"""
|
||
# 每个星座分为40个0.75度区间
|
||
part = int(sign_degree // 0.75)
|
||
if sign_index in self.MOVABLE_SIGNS:
|
||
start = sign_index
|
||
elif sign_index in self.FIXED_SIGNS:
|
||
start = (sign_index + 8) % 12
|
||
else:
|
||
start = (sign_index + 4) % 12
|
||
varga_sign = (start + part) % 12
|
||
varga_degree = (sign_degree % 0.75) * 40
|
||
return varga_sign * 30 + varga_degree
|
||
|
||
def _calculate_d45(self, sign_index: int, sign_degree: float) -> float:
|
||
"""D45 Akshavedamsa - 全面判断分盘"""
|
||
# 每个星座分为45个0.667度区间
|
||
part = int(sign_degree // (30/45))
|
||
if sign_index in self.ODD_SIGNS:
|
||
start = sign_index
|
||
else:
|
||
start = (sign_index + 8) % 12
|
||
varga_sign = (start + part) % 12
|
||
varga_degree = (sign_degree % (30/45)) * 45
|
||
return varga_sign * 30 + varga_degree
|
||
|
||
def _calculate_d60(self, sign_index: int, sign_degree: float) -> float:
|
||
"""D60 Shashtiamsa - 前世业力分盘(最精细的分盘)"""
|
||
# 每个星座分为60个0.5度区间
|
||
part = int(sign_degree // 0.5)
|
||
varga_sign = (sign_index + part) % 12
|
||
varga_degree = (sign_degree % 0.5) * 60
|
||
return varga_sign * 30 + varga_degree
|
||
|
||
def _calculate_d81(self, sign_index: int, sign_degree: float) -> float:
|
||
"""D81 Navamsa-Navamsa — D9的D9精微分盘"""
|
||
# 先计算D9位置
|
||
d9_lon = self._calculate_d9(sign_index, sign_degree)
|
||
d9_sign = int(d9_lon / 30) % 12
|
||
d9_deg = d9_lon % 30
|
||
# 再对D9结果计算一次D9
|
||
return self._calculate_d9(d9_sign, d9_deg)
|
||
|
||
def _calculate_d108(self, sign_index: int, sign_degree: float) -> float:
|
||
"""D108 Dwadasamsa-Navamsa — D12的D9精微分盘"""
|
||
d9_lon = self._calculate_d9(sign_index, sign_degree)
|
||
d9_sign = int(d9_lon / 30) % 12
|
||
d9_deg = d9_lon % 30
|
||
return self._calculate_d12(d9_sign, d9_deg)
|
||
|
||
def _calculate_d144(self, sign_index: int, sign_degree: float) -> float:
|
||
"""D144 Dwadasamsa-Dwadasamsa — D12的D12精微分盘"""
|
||
d12_lon = self._calculate_d12(sign_index, sign_degree)
|
||
d12_sign = int(d12_lon / 30) % 12
|
||
d12_deg = d12_lon % 30
|
||
return self._calculate_d12(d12_sign, d12_deg)
|
||
|
||
def _calculate_d5(self, sign_index: int, sign_degree: float) -> float:
|
||
"""D5 Panchamsa - 名声/权力分盘"""
|
||
part = int(sign_degree // 6)
|
||
if sign_index in self.ODD_SIGNS:
|
||
varga_sign = (sign_index + part) % 12
|
||
else:
|
||
varga_sign = (sign_index + 8 + part) % 12
|
||
varga_degree = (sign_degree % 6) * 5
|
||
return varga_sign * 30 + varga_degree
|
||
|
||
def _calculate_d6(self, sign_index: int, sign_degree: float) -> float:
|
||
"""D6 Shashthamsa - 健康/敌人分盘"""
|
||
part = int(sign_degree // 5)
|
||
if sign_index in self.ODD_SIGNS:
|
||
varga_sign = (sign_index + part) % 12
|
||
else:
|
||
varga_sign = (sign_index + 6 + part) % 12
|
||
varga_degree = (sign_degree % 5) * 6
|
||
return varga_sign * 30 + varga_degree
|
||
|
||
def _calculate_d8(self, sign_index: int, sign_degree: float) -> float:
|
||
"""D8 Ashtamsa - 突发事件分盘"""
|
||
part = int(sign_degree // 3.75)
|
||
if sign_index in self.ODD_SIGNS:
|
||
varga_sign = (sign_index + part) % 12
|
||
else:
|
||
varga_sign = (sign_index + 8 + part) % 12
|
||
varga_degree = (sign_degree % 3.75) * 8
|
||
return varga_sign * 30 + varga_degree
|
||
|
||
def _calculate_d11(self, sign_index: int, sign_degree: float) -> float:
|
||
"""D11 Rudramsa - 破坏/转化分盘"""
|
||
part = int(sign_degree // (30/11))
|
||
if sign_index in self.ODD_SIGNS:
|
||
varga_sign = (sign_index + part) % 12
|
||
else:
|
||
varga_sign = (sign_index + 8 + part) % 12
|
||
varga_degree = (sign_degree % (30/11)) * 11
|
||
return varga_sign * 30 + varga_degree
|
||
|
||
def _calculate_generic_varga(self, sign_index: int, sign_degree: float, division: int) -> float:
|
||
"""通用分盘算法(适用于其他分盘)"""
|
||
part = int(sign_degree // (30/division))
|
||
varga_sign = (sign_index + part) % 12
|
||
varga_degree = (sign_degree % (30/division)) * division
|
||
return varga_sign * 30 + varga_degree
|
||
|
||
def generate_house_chart_ascii(self, house_chart: List[List[str]]) -> str:
|
||
"""
|
||
生成ASCII格式的宫位图
|
||
|
||
Args:
|
||
house_chart: 12个宫位的行星列表
|
||
|
||
Returns:
|
||
ASCII格式的宫位图字符串
|
||
"""
|
||
# 北印度风格宫位图(菱形)
|
||
chart = f"""
|
||
┌─────────┬─────────┬─────────┐
|
||
│ 12 │ 1 │ 2 │
|
||
│ {self._format_planets(house_chart[11]):7} │ {self._format_planets(house_chart[0]):7} │ {self._format_planets(house_chart[1]):7} │
|
||
├─────────┼─────────┼─────────┤
|
||
│ 11 │ │ 3 │
|
||
│ {self._format_planets(house_chart[10]):7} │ ASC │ {self._format_planets(house_chart[2]):7} │
|
||
├─────────┼─────────┼─────────┤
|
||
│ 10 │ 9 │ 4 │
|
||
│ {self._format_planets(house_chart[9]):7} │ {self._format_planets(house_chart[8]):7} │ {self._format_planets(house_chart[3]):7} │
|
||
├─────────┼─────────┼─────────┤
|
||
│ 9 │ 8 │ 5 │
|
||
│ {self._format_planets(house_chart[8]):7} │ {self._format_planets(house_chart[7]):7} │ {self._format_planets(house_chart[4]):7} │
|
||
├─────────┼─────────┼─────────┤
|
||
│ 8 │ 7 │ 6 │
|
||
│ {self._format_planets(house_chart[7]):7} │ {self._format_planets(house_chart[6]):7} │ {self._format_planets(house_chart[5]):7} │
|
||
└─────────┴─────────┴─────────┘
|
||
"""
|
||
return chart
|
||
|
||
def _format_planets(self, planets: List[str]) -> str:
|
||
"""格式化行星列表为简写"""
|
||
if not planets:
|
||
return " "
|
||
|
||
# 行星简写
|
||
abbrev = {
|
||
"Sun": "Su", "Moon": "Mo", "Mars": "Ma", "Mercury": "Me",
|
||
"Jupiter": "Ju", "Venus": "Ve", "Saturn": "Sa",
|
||
"Rahu": "Ra", "Ketu": "Ke"
|
||
}
|
||
|
||
short = [abbrev.get(p, p[:2]) for p in planets]
|
||
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, outer_deg, outer_abs = self._position_parts(outer_result)
|
||
|
||
# Step 2: Apply inner division to the outer result
|
||
inner_result = self._calculate_varga_position(outer_abs, inner_div)
|
||
inner_sign, inner_deg, inner_abs = self._position_parts(inner_result)
|
||
|
||
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_abs, 4),
|
||
'intermediate': {
|
||
'outer_sign': self.SIGNS[outer_sign],
|
||
'outer_sign_idx': 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, varga_deg, varga_abs = self._position_parts(varga_pos)
|
||
|
||
# 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_abs, 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, varga_deg, varga_abs = self._position_parts(varga_pos)
|
||
|
||
return {
|
||
'div': div,
|
||
'sign': self.SIGNS[varga_sign],
|
||
'sign_idx': varga_sign,
|
||
'degree': round(varga_deg, 4),
|
||
'variant': used_variant,
|
||
'absolute_degree': round(varga_abs, 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°
|
||
"Moon": 125.3, # Leo 5.3°
|
||
"Mars": 285.7, # Capricorn 15.7°
|
||
"Mercury": 25.2, # Aries 25.2°
|
||
"Jupiter": 95.8, # Cancer 5.8°
|
||
"Venus": 335.4, # Pisces 5.4°
|
||
"Saturn": 245.6, # Sagittarius 5.6°
|
||
"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)
|
||
for varga_name in ["Rashi", "Navamsa"]:
|
||
varga_data = all_vargas[varga_name]
|
||
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}")
|