Files
Jyotisha/scripts/special_lagnas.py
T

502 lines
20 KiB
Python
Raw 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 -*-
"""
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()