#!/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()