502 lines
20 KiB
Python
502 lines
20 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
Special Lagnas Calculator - 特殊上升点计算器
|
||
印度占星特殊上升点(Bhava/Hora/Ghati/Arudha/Upapada Lagna)
|
||
|
||
核心功能:
|
||
- Bhava Lagna(宫位上升):反映物质层面的实际状况
|
||
- Hora Lagna(时辰上升):财富和物质收益的指示器
|
||
- Ghati Lagna(Ghati上升):基于Ghati单位的精确上升点
|
||
- Arudha Lagna(映像上升):公众形象和社会地位
|
||
- Upapada Lagna(配偶映像上升):配偶和婚姻的公众形象
|
||
"""
|
||
|
||
from typing import Dict, Tuple
|
||
from datetime import datetime, timedelta
|
||
import io
|
||
from contextlib import redirect_stdout
|
||
|
||
|
||
def jaimini_special_lagna_view(points):
|
||
"""Project canonical birth points without recalculating approximate aliases."""
|
||
aliases = {
|
||
'HL': 'Hora_Lagna',
|
||
'GL': 'Ghati_Lagna',
|
||
'ViL': 'ViGhati_Lagna',
|
||
'PP': 'Pranapada_Lagna',
|
||
'VL': 'Varnada_Lagna',
|
||
}
|
||
return {
|
||
alias: dict(points.get(key) or {'status': 'blocked', 'reason': 'birth_point_unavailable'})
|
||
for alias, key in aliases.items()
|
||
}
|
||
|
||
|
||
def bhava_house_rows(bhava):
|
||
"""Return only numbered house records; summaries are not additional houses."""
|
||
rows = bhava.get('houses') if isinstance(bhava, dict) else None
|
||
if isinstance(rows, list):
|
||
return [
|
||
dict(row) for row in rows
|
||
if isinstance(row, dict) and str(row.get('house')).isdigit() and 1 <= int(row['house']) <= 12
|
||
]
|
||
mapping = rows if isinstance(rows, dict) else bhava
|
||
if not isinstance(mapping, dict):
|
||
return []
|
||
return [
|
||
dict(row, house=int(key))
|
||
for key, row in mapping.items()
|
||
if str(key).isdigit() and 1 <= int(key) <= 12 and isinstance(row, dict)
|
||
]
|
||
|
||
|
||
def _pyjhora_ayanamsa_mode(name):
|
||
key = str(name or '').strip().lower().replace('-', '_').replace(' ', '_')
|
||
return {
|
||
'lahiri': 'LAHIRI',
|
||
'raman': 'RAMAN',
|
||
'kp': 'KP',
|
||
'krishnamurti': 'KP',
|
||
'fagan_bradley': 'FAGAN',
|
||
'fagan': 'FAGAN',
|
||
'true_citra': 'TRUE_CITRA',
|
||
'true_chitra': 'TRUE_CITRA',
|
||
'true_pushya': 'TRUE_PUSHYA',
|
||
}.get(key)
|
||
|
||
|
||
class SpecialLagnasCalculator:
|
||
"""特殊上升点计算器"""
|
||
|
||
SIGNS = ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
|
||
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"]
|
||
|
||
def __init__(self):
|
||
pass
|
||
|
||
def calculate_all_lagnas(self, asc_degree: float, sun_degree: float,
|
||
moon_degree: float, birth_time: datetime,
|
||
sunrise_time: datetime, lat: float | None = None,
|
||
lon: float | None = None, tz_offset: float | None = None,
|
||
ayanamsa: str | None = None) -> Dict[str, Dict]:
|
||
"""
|
||
计算所有特殊上升点
|
||
|
||
Args:
|
||
asc_degree: 上升点度数(0-360)
|
||
sun_degree: 太阳度数(0-360)
|
||
moon_degree: 月亮度数(0-360)
|
||
birth_time: 出生时间
|
||
sunrise_time: 当天日出时间
|
||
|
||
Returns:
|
||
所有特殊上升点的字典
|
||
"""
|
||
result = {}
|
||
|
||
# 1. Bhava Lagna
|
||
result["Bhava_Lagna"] = self.calculate_bhava_lagna(asc_degree, sun_degree, moon_degree)
|
||
|
||
# 2. Hora Lagna
|
||
result["Hora_Lagna"] = self.calculate_hora_lagna(asc_degree, sun_degree, birth_time, sunrise_time)
|
||
|
||
# 3. Ghati Lagna
|
||
result["Ghati_Lagna"] = self.calculate_ghati_lagna(asc_degree, birth_time, sunrise_time)
|
||
|
||
# 4/5. Arudha Lagna、Upapada Lagna 只有一个来源 jaimini.calc_arudha_padas(BUG-1190),
|
||
# 由 full reading 用它覆盖这两个占位。
|
||
result["Arudha_Lagna"] = {"note": "Computed by jaimini.calc_arudha_padas"}
|
||
result["Upapada_Lagna"] = {"note": "Computed by jaimini.calc_arudha_padas"}
|
||
pyjhora = self.calculate_pyjhora_special_lagnas(
|
||
birth_time=birth_time,
|
||
lat=lat,
|
||
lon=lon,
|
||
tz_offset=tz_offset,
|
||
asc_degree=asc_degree,
|
||
ayanamsa=ayanamsa,
|
||
)
|
||
if lat is None or lon is None or tz_offset is None:
|
||
for key in ("ViGhati_Lagna", "Sree_Lagna", "Indu_Lagna", "Pranapada_Lagna", "Varnada_Lagna"):
|
||
result[key] = pyjhora[key]
|
||
else:
|
||
result.update(pyjhora)
|
||
return result
|
||
|
||
def _format_special_lagna_payload(self, label: str, raw, asc_degree: float, method: str) -> Dict:
|
||
if isinstance(raw, dict):
|
||
return raw
|
||
if isinstance(raw, (list, tuple)) and len(raw) >= 2:
|
||
sign_idx = int(raw[0]) % 12
|
||
degree = float(raw[1]) % 30
|
||
longitude = sign_idx * 30 + degree
|
||
else:
|
||
longitude = float(raw) % 360
|
||
sign_idx = int(longitude // 30) % 12
|
||
degree = longitude % 30
|
||
return {
|
||
"full_name": label,
|
||
"degree": round(longitude, 4),
|
||
"longitude": round(longitude, 4),
|
||
"sign": self.SIGNS[sign_idx],
|
||
"sign_degree": round(degree, 4),
|
||
"house": ((sign_idx - int(asc_degree // 30)) % 12) + 1,
|
||
"method": method,
|
||
"source": "pyjhora_drik",
|
||
"status": "computed",
|
||
}
|
||
|
||
def calculate_pyjhora_special_lagnas(
|
||
self,
|
||
birth_time: datetime,
|
||
lat: float | None,
|
||
lon: float | None,
|
||
tz_offset: float | None,
|
||
asc_degree: float,
|
||
ayanamsa: str | None = None,
|
||
) -> Dict[str, Dict]:
|
||
names = (
|
||
("Bhava_Lagna", "Bhava Lagna"),
|
||
("Hora_Lagna", "Hora Lagna"),
|
||
("Ghati_Lagna", "Ghati Lagna"),
|
||
("ViGhati_Lagna", "Vighati Lagna"),
|
||
("Sree_Lagna", "Sree Lagna"),
|
||
("Indu_Lagna", "Indu Lagna"),
|
||
("Pranapada_Lagna", "Pranapada Lagna"),
|
||
("Varnada_Lagna", "Varnada Lagna"),
|
||
)
|
||
blocked = {
|
||
key: {"full_name": full_name, "status": "blocked", "source": "pyjhora_drik"}
|
||
for key, full_name in names
|
||
}
|
||
if lat is None or lon is None or tz_offset is None:
|
||
for row in blocked.values():
|
||
row["reason"] = "birth_coordinates_or_timezone_missing"
|
||
return blocked
|
||
producer = (
|
||
("Bhava_Lagna", "bhava_lagna", "Bhava Lagna"),
|
||
("Hora_Lagna", "hora_lagna", "Hora Lagna"),
|
||
("Ghati_Lagna", "ghati_lagna", "Ghati Lagna"),
|
||
("ViGhati_Lagna", "vighati_lagna", "Vighati Lagna"),
|
||
("Sree_Lagna", "sree_lagna", "Sree Lagna"),
|
||
("Indu_Lagna", "indu_lagna", "Indu Lagna"),
|
||
("Pranapada_Lagna", "pranapada_lagna", "Pranapada Lagna"),
|
||
)
|
||
try:
|
||
with redirect_stdout(io.StringIO()):
|
||
import swisseph as swe
|
||
from jhora import const as jhora_const
|
||
from jhora.panchanga import drik
|
||
from jhora.panchanga.drik import Place
|
||
except Exception as exc:
|
||
for row in blocked.values():
|
||
row["reason"] = f"pyjhora_unavailable:{exc}"
|
||
return blocked
|
||
|
||
mode = None
|
||
if ayanamsa is not None:
|
||
mode = _pyjhora_ayanamsa_mode(ayanamsa)
|
||
if mode is None:
|
||
for row in blocked.values():
|
||
row["reason"] = f"pyjhora_ayanamsa_unsupported:{ayanamsa}"
|
||
return blocked
|
||
|
||
previous_pyjhora = getattr(jhora_const, "_DEFAULT_AYANAMSA_MODE", None)
|
||
previous_swe = None
|
||
apply_ayanamsa = None
|
||
try:
|
||
from ayanamsa_utils import ACTIVE_AYANAMSA_NAME, apply_ayanamsa as _apply
|
||
previous_swe = ACTIVE_AYANAMSA_NAME
|
||
apply_ayanamsa = _apply
|
||
except Exception:
|
||
apply_ayanamsa = None
|
||
|
||
results = {}
|
||
try:
|
||
with redirect_stdout(io.StringIO()):
|
||
local_hour = (
|
||
birth_time.hour
|
||
+ birth_time.minute / 60.0
|
||
+ birth_time.second / 3600.0
|
||
+ birth_time.microsecond / 3_600_000_000.0
|
||
)
|
||
jd_local = swe.julday(birth_time.year, birth_time.month, birth_time.day, local_hour)
|
||
place = Place("birth_place", lat, lon, tz_offset)
|
||
if mode is not None:
|
||
drik.set_ayanamsa_mode(mode)
|
||
for output_key, func_name, full_name in producer:
|
||
func = getattr(drik, func_name, None)
|
||
if func is None:
|
||
results[output_key] = {
|
||
"full_name": full_name,
|
||
"status": "blocked",
|
||
"source": "pyjhora_drik",
|
||
"reason": f"pyjhora_missing_function:{func_name}",
|
||
}
|
||
continue
|
||
try:
|
||
raw = func(jd_local, place)
|
||
results[output_key] = self._format_special_lagna_payload(
|
||
full_name, raw, asc_degree, method=f"PyJHora drik.{func_name}",
|
||
)
|
||
except Exception as exc:
|
||
results[output_key] = {
|
||
"full_name": full_name,
|
||
"status": "blocked",
|
||
"source": "pyjhora_drik",
|
||
"reason": str(exc),
|
||
}
|
||
try:
|
||
from jhora.horoscope.chart import charts
|
||
if not hasattr(charts, "varnada_lagna"):
|
||
raise AttributeError("pyjhora_missing_function:varnada_lagna")
|
||
raw = charts.varnada_lagna(
|
||
(birth_time.year, birth_time.month, birth_time.day),
|
||
(birth_time.hour, birth_time.minute, birth_time.second),
|
||
place,
|
||
varnada_method=1,
|
||
)
|
||
results["Varnada_Lagna"] = self._format_special_lagna_payload(
|
||
"Varnada Lagna", raw, asc_degree, "PyJHora varnada method 1",
|
||
)
|
||
except Exception as exc:
|
||
reason = str(exc) or "pyjhora_missing_function:varnada_lagna"
|
||
results["Varnada_Lagna"] = {
|
||
"full_name": "Varnada Lagna",
|
||
"status": "blocked",
|
||
"source": "pyjhora_drik",
|
||
"reason": reason,
|
||
}
|
||
if ayanamsa is not None:
|
||
for row in results.values():
|
||
row["ayanamsa"] = ayanamsa
|
||
finally:
|
||
with redirect_stdout(io.StringIO()):
|
||
if previous_pyjhora:
|
||
try:
|
||
drik.set_ayanamsa_mode(previous_pyjhora)
|
||
except Exception:
|
||
jhora_const._DEFAULT_AYANAMSA_MODE = previous_pyjhora
|
||
if apply_ayanamsa is not None and previous_swe:
|
||
apply_ayanamsa(previous_swe)
|
||
return results
|
||
|
||
def calculate_bhava_lagna(self, asc_degree: float, sun_degree: float,
|
||
moon_degree: float) -> Dict:
|
||
"""
|
||
计算Bhava Lagna(宫位上升)
|
||
|
||
公式:Bhava Lagna = Asc + (Sun - Moon)
|
||
|
||
含义:
|
||
- 反映物质层面的实际状况
|
||
- 显示真实的生活环境和物质条件
|
||
- 与上升点的差异揭示表象与实质的差距
|
||
"""
|
||
# 计算太阳-月亮的度数差
|
||
sun_moon_diff = sun_degree - moon_degree
|
||
|
||
# 如果差值为负,加360度
|
||
if sun_moon_diff < 0:
|
||
sun_moon_diff += 360
|
||
|
||
# 计算Bhava Lagna
|
||
bhava_lagna = (asc_degree + sun_moon_diff) % 360
|
||
|
||
# 转换为星座和度数
|
||
sign_index = int(bhava_lagna // 30)
|
||
sign_degree = bhava_lagna % 30
|
||
|
||
return {
|
||
"degree": round(bhava_lagna, 4),
|
||
"sign": self.SIGNS[sign_index],
|
||
"sign_degree": round(sign_degree, 4),
|
||
"house": ((sign_index - int(asc_degree // 30)) % 12) + 1,
|
||
"formula": "Asc + (Sun - Moon)",
|
||
"meaning": "物质层面的实际状况,真实的生活环境"
|
||
}
|
||
|
||
def calculate_hora_lagna(self, asc_degree: float, sun_degree: float,
|
||
birth_time: datetime, sunrise_time: datetime) -> Dict:
|
||
"""
|
||
计算Hora Lagna(时辰上升)
|
||
|
||
公式:Hora Lagna = Asc + (出生时间距日出的Hora数 × 15度)
|
||
|
||
含义:
|
||
- 财富和物质收益的指示器
|
||
- 显示金钱来源和财富积累方式
|
||
- 与2宫(财富宫)密切相关
|
||
"""
|
||
# 计算出生时间距日出的小时数
|
||
time_diff = birth_time - sunrise_time
|
||
hours_from_sunrise = time_diff.total_seconds() / 3600
|
||
|
||
# 计算Hora数(每个Hora = 1小时)
|
||
hora_count = hours_from_sunrise
|
||
|
||
# 计算Hora Lagna(每个Hora推进15度)
|
||
hora_lagna = (asc_degree + (hora_count * 15)) % 360
|
||
|
||
# 转换为星座和度数
|
||
sign_index = int(hora_lagna // 30)
|
||
sign_degree = hora_lagna % 30
|
||
|
||
return {
|
||
"degree": round(hora_lagna, 4),
|
||
"sign": self.SIGNS[sign_index],
|
||
"sign_degree": round(sign_degree, 4),
|
||
"house": ((sign_index - int(asc_degree // 30)) % 12) + 1,
|
||
"hours_from_sunrise": round(hours_from_sunrise, 2),
|
||
"formula": "Asc + (Hora数 × 15°)",
|
||
"meaning": "财富和物质收益的指示器"
|
||
}
|
||
|
||
def calculate_ghati_lagna(self, asc_degree: float, birth_time: datetime,
|
||
sunrise_time: datetime) -> Dict:
|
||
"""
|
||
计算Ghati Lagna(Ghati上升)
|
||
|
||
公式:Ghati Lagna = Asc + (Ghati数 × 6度)
|
||
|
||
Ghati单位:
|
||
- 1 Ghati = 24分钟
|
||
- 1天 = 60 Ghati
|
||
- 每个Ghati推进6度(360度 / 60 Ghati = 6度)
|
||
|
||
含义:
|
||
- 基于Ghati单位的精确上升点
|
||
- 用于精确的时间矫正和事件预测
|
||
- 在Prashna(问事占星)中特别重要
|
||
"""
|
||
# 计算出生时间距日出的分钟数
|
||
time_diff = birth_time - sunrise_time
|
||
minutes_from_sunrise = time_diff.total_seconds() / 60
|
||
|
||
# 计算Ghati数(1 Ghati = 24分钟)
|
||
ghati_count = minutes_from_sunrise / 24
|
||
|
||
# 计算Ghati Lagna(每个Ghati推进6度)
|
||
ghati_lagna = (asc_degree + (ghati_count * 6)) % 360
|
||
|
||
# 转换为星座和度数
|
||
sign_index = int(ghati_lagna // 30)
|
||
sign_degree = ghati_lagna % 30
|
||
|
||
return {
|
||
"degree": round(ghati_lagna, 4),
|
||
"sign": self.SIGNS[sign_index],
|
||
"sign_degree": round(sign_degree, 4),
|
||
"house": ((sign_index - int(asc_degree // 30)) % 12) + 1,
|
||
"ghati_count": round(ghati_count, 2),
|
||
"formula": "Asc + (Ghati数 × 6°)",
|
||
"meaning": "基于Ghati单位的精确上升点,用于精确预测"
|
||
}
|
||
|
||
def calculate_arudha_pada(self, asc_degree: float, source_house: int, source_lord_degree: float,
|
||
label: str = "Arudha_Pada") -> Dict:
|
||
"""
|
||
计算任意宫位的 Arudha Pada(A1-A12)。
|
||
|
||
Args:
|
||
asc_degree: 上升点度数(0-360)
|
||
source_house: 源宫位(1-12);例如 10 = A10 / Karma Pada / Rajya Pada
|
||
source_lord_degree: 源宫主星度数(0-360)
|
||
label: 输出标签
|
||
|
||
Formula:
|
||
1. 找到源宫位星座。
|
||
2. 计算源宫主星距离源宫位的星座数。
|
||
3. 从源宫主星再数同样距离得到 Pada。
|
||
4. 若 Pada 落回源宫或源宫第7宫,按 Jaimini 例外规则改取该位置第10宫。
|
||
"""
|
||
if source_house < 1 or source_house > 12:
|
||
raise ValueError("source_house must be between 1 and 12")
|
||
|
||
asc_sign = int(asc_degree // 30)
|
||
source_sign = (asc_sign + source_house - 1) % 12
|
||
lord_sign = int(source_lord_degree // 30)
|
||
|
||
distance = (lord_sign - source_sign) % 12
|
||
pada_sign = (lord_sign + distance) % 12
|
||
|
||
exception_applied = False
|
||
if pada_sign == source_sign or pada_sign == (source_sign + 6) % 12:
|
||
pada_sign = (pada_sign + 9) % 12
|
||
exception_applied = True
|
||
|
||
pada_degree = (pada_sign * 30 + (source_lord_degree % 30)) % 360
|
||
return {
|
||
"label": label,
|
||
"source_house": source_house,
|
||
"degree": round(pada_degree, 4),
|
||
"sign": self.SIGNS[pada_sign],
|
||
"sign_degree": round(pada_degree % 30, 4),
|
||
"house": ((pada_sign - asc_sign) % 12) + 1,
|
||
"source_sign": self.SIGNS[source_sign],
|
||
"source_lord_sign": self.SIGNS[lord_sign],
|
||
"distance_from_source": distance if distance != 0 else 12,
|
||
"exception_applied": exception_applied,
|
||
"formula": f"A{source_house}: from house lord count the same distance from source house; apply 1/7 exception",
|
||
"meaning": "Arudha Pada:该宫位主题在外界/社会层面的显化"
|
||
}
|
||
|
||
def calculate_a10(self, asc_degree: float, tenth_lord_degree: float) -> Dict:
|
||
"""计算 A10 / Karma Pada / Rajya Pada:事业名声、职业可见度、社会身份。"""
|
||
result = self.calculate_arudha_pada(asc_degree, 10, tenth_lord_degree, "A10_Karma_Pada")
|
||
result["meaning"] = "A10/Karma Pada/Rajya Pada:事业名声、职业可见度、社会身份与公众层面的职业结果"
|
||
return result
|
||
|
||
def format_output(self, lagnas: Dict) -> str:
|
||
"""格式化输出所有特殊上升点"""
|
||
lines = ["=" * 60, "Special Lagnas Calculation", "=" * 60, ""]
|
||
|
||
for lagna_name, data in lagnas.items():
|
||
lines.append(f"{lagna_name}:")
|
||
if "note" in data:
|
||
lines.append(f" {data['note']}")
|
||
else:
|
||
lines.append(f" Position: {data['sign']} {data['sign_degree']:.2f}° ({data['degree']:.2f}°)")
|
||
lines.append(f" Formula: {data['formula']}")
|
||
lines.append(f" Meaning: {data['meaning']}")
|
||
lines.append("")
|
||
|
||
lines.append("=" * 60)
|
||
return "\n".join(lines)
|
||
|
||
|
||
def main():
|
||
"""命令行接口"""
|
||
import argparse
|
||
from datetime import datetime
|
||
|
||
parser = argparse.ArgumentParser(description="Calculate Special Lagnas")
|
||
parser.add_argument("--asc", type=float, required=True, help="Ascendant degree (0-360)")
|
||
parser.add_argument("--sun", type=float, required=True, help="Sun degree (0-360)")
|
||
parser.add_argument("--moon", type=float, required=True, help="Moon degree (0-360)")
|
||
parser.add_argument("--birth-time", type=str, help="Birth time (YYYY-MM-DD HH:MM:SS)")
|
||
parser.add_argument("--sunrise-time", type=str, help="Sunrise time (YYYY-MM-DD HH:MM:SS)")
|
||
parser.add_argument("--tenth-lord", type=float, help="10th house lord degree (for A10/Karma Pada)")
|
||
|
||
args = parser.parse_args()
|
||
|
||
calc = SpecialLagnasCalculator()
|
||
|
||
# 解析时间
|
||
birth_time = datetime.strptime(args.birth_time, "%Y-%m-%d %H:%M:%S") if args.birth_time else datetime.now()
|
||
sunrise_time = datetime.strptime(args.sunrise_time, "%Y-%m-%d %H:%M:%S") if args.sunrise_time else birth_time.replace(hour=6, minute=0, second=0)
|
||
|
||
# 计算所有特殊上升点
|
||
lagnas = calc.calculate_all_lagnas(args.asc, args.sun, args.moon, birth_time, sunrise_time)
|
||
|
||
# 如果提供了10宫主星位置,计算A10/Karma Pada
|
||
if args.tenth_lord is not None:
|
||
lagnas["A10_Karma_Pada"] = calc.calculate_a10(args.asc, args.tenth_lord)
|
||
|
||
# 输出结果
|
||
print(calc.format_output(lagnas))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|