Files
Jyotisha/scripts/divisional_charts_extended.py
2026-06-25 18:33:26 +08:00

1027 lines
42 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Divisional Charts Extended - D2-D60完整分盘宫位图计算器
印度占星 Shodasavarga16分盘)+ 扩展分盘系统
核心功能:
- 计算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度→Leo15-30度→Cancer
# 偶数星座:0-15度→Cancer15-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}")