#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Shadbala 计算模块(六重力量)v6.9.12 BPHS标准实现 + 外部校准支持 六种力量: 1. Sthana Bala(位置力量)— 含 Ucha/Saptavargaja/Ojayugma/Kendra(3-tier)/Drekkana 2. Dig Bala(方向力量) 3. Kala Bala(时间力量)— 含 Nathonnata/Paksha/Tribhaga/Ayana/Hora 4. Chesta Bala(运动力量)— Sun=Seeghrochcha, Moon=月相, Others=速度/逆行 5. Naisargika Bala(天然力量) 6. Drik Bala(相位力量)— Sputa Drishti 连续曲线 v6.9.12 修复: - Kendra Bala: 0/15 二值 → BPHS 15/30/60 三档 (Kendra/Panapara/Apoklima) - Bhava Bala: 新增宫位力量(基于 Bhava 中点计算) - Hora Bala: 新增 Kala Bala 子项 - Chesta Bala: Sun 使用 Seeghrochcha 公式 """ import math import swisseph as swe import sys import os import json from typing import Dict, Tuple # v6.1.10: 导入实际Varga计算器(支持脚本和包两种调用方式) _script_dir = os.path.dirname(os.path.abspath(__file__)) if _script_dir not in sys.path: sys.path.insert(0, _script_dir) from varga import varga_map, SIGNS as VARGA_SIGNS, SIGN_LORDS as VARGA_SIGN_LORDS # ============================================================================ # 常量 # ============================================================================ SIGNS = ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo', 'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces'] SIGN_LORDS = { 'Aries': 'Mars', 'Taurus': 'Venus', 'Gemini': 'Mercury', 'Cancer': 'Moon', 'Leo': 'Sun', 'Virgo': 'Mercury', 'Libra': 'Venus', 'Scorpio': 'Mars', 'Sagittarius': 'Jupiter', 'Capricorn': 'Saturn', 'Aquarius': 'Saturn', 'Pisces': 'Jupiter' } SHADBALA_CONSTANTS_PATH = os.path.join(_script_dir, '..', 'references', 'shat_bala_constants.json') def _to_longitude(entry: Dict[str, float]) -> float: return float(entry['sign']) * 30 + float(entry['degree']) def _load_shadbala_constants() -> Dict: with open(SHADBALA_CONSTANTS_PATH, 'r', encoding='utf-8') as handle: return json.load(handle) _SHADBALA_CONSTANTS = _load_shadbala_constants() # 入庙度数(sign_idx * 30 + degree) EXALTATION_DEG = { planet: _to_longitude(entry) for planet, entry in _SHADBALA_CONSTANTS['exaltation_degrees'].items() } # 落陷度数(由参考 JSON 直接提供,避免隐式推导与外部真值漂移) DEBILITATION_DEG = { planet: _to_longitude(entry) for planet, entry in _SHADBALA_CONSTANTS['debilitation_degrees'].items() } # 行星友好/敌对关系(静态真值表) FRIENDSHIP = { planet: { 'friend': relation['friends'], 'enemy': relation['enemies'], 'neutral': relation['neutrals'], } for planet, relation in _SHADBALA_CONSTANTS['natural_relationships'].items() } # Dig Bala 最强宫位 DIG_BALA_HOUSE = { 'Sun': 10, 'Mars': 10, # Midheaven 'Moon': 4, 'Venus': 4, # Nadir 'Jupiter': 1, 'Mercury': 1, # Ascendant 'Saturn': 7, # Descendant } # Naisargika Bala(天然力量,单位 Shashtiamshas) # v6.1.10: 改为PyJHora/JHora标准值(60-8.57递减序列) # 来源:PyJHora strength.py, PVR Rao, BPHS第9章 NAISARGIKA_BALA = { 'Sun': 60.0, 'Moon': 51.43, 'Venus': 42.86, 'Jupiter': 34.29, 'Mercury': 25.71, 'Mars': 17.14, 'Saturn': 8.57 } # Shadbala 最低要求(Rupas) MIN_REQUIRED = { 'Sun': 5.0, 'Moon': 6.0, 'Mars': 5.0, 'Mercury': 7.0, 'Jupiter': 6.5, 'Venus': 5.5, 'Saturn': 5.0 } # 昼强/夜强行星 DIURNAL_STRONG = ['Sun', 'Jupiter', 'Venus'] NOCTURNAL_STRONG = ['Moon', 'Mars', 'Saturn'] # 吉星/凶星 BENEFICS = ['Jupiter', 'Venus', 'Mercury'] MALEFICS = ['Saturn', 'Mars', 'Sun'] # 行星相位规则(所有行星都有7宫相位,特殊相位如下) SPECIAL_ASPECTS = { 'Mars': [4, 8], # 火星额外看4宫和8宫 'Jupiter': [5, 9], # 木星额外看5宫和9宫 'Saturn': [3, 10], # 土星额外看3宫和10宫 } # Virupas → Rupas 转换(60 Virupas = 1 Rupa) VIRUPAS_PER_RUPA = 60.0 _DIG_POWERLESS_HOUSE = {"Sun": 3, "Moon": 9, "Mars": 3, "Mercury": 6, "Jupiter": 6, "Venus": 9, "Saturn": 0} _MOOLATRIKONA = { "Sun": (4, 0.0, 20.0), "Moon": (1, 4.0, 30.0), "Mars": (0, 0.0, 12.0), "Mercury": (5, 16.0, 20.0), "Jupiter": (8, 0.0, 10.0), "Venus": (6, 0.0, 15.0), "Saturn": (10, 0.0, 20.0), } _PLANET_INDEX = {name: index for index, name in enumerate(("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn"))} _INDEX_PLANET = {index: name for name, index in _PLANET_INDEX.items()} _ABDA_WEEKDAY_PLANETS = ("Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Sun", "Moon") _HORA_ORDER = ("Saturn", "Jupiter", "Mars", "Sun", "Venus", "Mercury", "Moon") _SURYA_CIVIL_DAYS = 1_577_917_828 _SURYA_REVOLUTIONS = { "Sun": 4_320_000, "Mars": 2_296_832, "Mercury": 17_937_060, "Jupiter": 364_220, "Venus": 7_022_376, "Saturn": 146_568, } _UJJAIN_LONGITUDE = 75.7885 def _solar_event_local_hour(jd_ut: float, lat: float, lon: float, timezone: float, event: int) -> float: local_jd = jd_ut + timezone / 24.0 year, month, day, _ = swe.revjul(local_jd, swe.GREG_CAL) local_midnight = swe.julday(year, month, day, 0.0, swe.GREG_CAL) utc_midnight = local_midnight - timezone / 24.0 _, times = swe.rise_trans( utc_midnight, swe.SUN, event | swe.BIT_HINDU_RISING, (float(lon), float(lat), 0.0), 0.0, 0.0, swe.FLG_SWIEPH | swe.FLG_SIDEREAL | swe.FLG_NONUT, ) return (times[0] + timezone / 24.0 - local_midnight) * 24.0 def build_shadbala_context(jd_ut: float, lat: float, lon: float, ayanamsa: str = "lahiri", timezone: float = 0.0) -> Dict: """Build precise sidereal house context from standard birth inputs.""" sid_mode = swe.SIDM_LAHIRI if str(ayanamsa).lower() in {"raman"}: sid_mode = swe.SIDM_RAMAN elif str(ayanamsa).lower() in {"kp", "krishnamurti"}: sid_mode = swe.SIDM_KRISHNAMURTI swe.set_sid_mode(sid_mode) cusps, _ = swe.houses_ex(float(jd_ut), float(lat), float(lon), b"P", swe.FLG_SIDEREAL) local_jd = float(jd_ut) + float(timezone) / 24.0 year, month, day, local_hour = swe.revjul(local_jd, swe.GREG_CAL) sunrise = _solar_event_local_hour(jd_ut, lat, lon, timezone, swe.CALC_RISE) sunset = _solar_event_local_hour(jd_ut, lat, lon, timezone, swe.CALC_SET) previous_sunset = _solar_event_local_hour(jd_ut - 1.0, lat, lon, timezone, swe.CALC_SET) return { "jd_ut": float(jd_ut), "jd_local": local_jd, "lat": float(lat), "lon": float(lon), "timezone": float(timezone), "year": int(year), "month": int(month), "day": int(day), "local_hour": float(local_hour), "ayanamsa": str(ayanamsa), "ayanamsa_degrees": float(swe.get_ayanamsa_ut(float(jd_ut))), "sunrise_hour": sunrise, "sunset_hour": sunset, "previous_sunset_hour": previous_sunset, "house_midpoints": [float(value) % 360 for value in cusps], } def calc_dig_bala_precise(pname: str, planet_lon: float, house_midpoints: list[float]) -> float: """Classical directional strength from the powerless bhava midpoint.""" powerless = house_midpoints[_DIG_POWERLESS_HOUSE[pname]] return round(abs(float(powerless) - (float(planet_lon) % 360)) / 3.0, 2) def _planet_longitude(name: str, planets: Dict) -> float: data = planets[name] degree = float(data.get("degree", 0.0)) if degree >= 30.0 or data.get("sign") not in SIGNS: return degree % 360.0 return (SIGNS.index(data["sign"]) * 30.0 + degree) % 360.0 def classify_drik_planets(planets: Dict) -> tuple[set[str], set[str]]: """Classify the seven planets for Drik Bala from lunar phase and Mercury's company.""" benefics = {name for name in ("Jupiter", "Venus") if name in planets} malefics = {name for name in ("Sun", "Mars", "Saturn") if name in planets} if "Moon" in planets and "Sun" in planets: target = benefics if (_planet_longitude("Moon", planets) - _planet_longitude("Sun", planets)) % 360.0 <= 180.0 else malefics target.add("Moon") if "Mercury" in planets: mercury_lon = _planet_longitude("Mercury", planets) mercury_sign = int(mercury_lon // 30) companions = [ name for name in benefics | malefics if name != "Mercury" and int(_planet_longitude(name, planets) // 30) == mercury_sign ] benefic_count = sum(name in benefics for name in companions) malefic_count = sum(name in malefics for name in companions) if benefic_count >= malefic_count and (benefic_count != malefic_count or not companions): benefics.add("Mercury") elif malefic_count > benefic_count: malefics.add("Mercury") else: nearest = min(companions, key=lambda name: abs(_planet_longitude(name, planets) - mercury_lon)) (benefics if nearest in benefics else malefics).add("Mercury") return benefics, malefics def _sphuta_drishti_virupas(angle: float, aspecting_planet: str) -> float: angle = round(float(angle) % 360.0, 2) if angle < 30.0: strength = 0.0 elif angle < 60.0: strength = (angle - 30.0) / 2.0 elif angle < 90.0: strength = angle - 45.0 + (45.0 if aspecting_planet == "Saturn" else 0.0) elif angle < 120.0: strength = (120.0 - angle) / 2.0 + 30.0 + (15.0 if aspecting_planet == "Mars" else 0.0) elif angle < 150.0: strength = 150.0 - angle + (30.0 if aspecting_planet == "Jupiter" else 0.0) elif angle < 180.0: strength = 2.0 * (angle - 150.0) elif angle < 300.0: strength = (300.0 - angle) / 2.0 if aspecting_planet == "Mars" and 210.0 <= angle < 240.0: strength += 15.0 elif aspecting_planet == "Jupiter" and 240.0 <= angle < 270.0: strength += 30.0 elif aspecting_planet == "Saturn" and 270.0 <= angle < 300.0: strength += 45.0 else: strength = 0.0 return round(strength, 2) def calc_drik_bala_precise(pname: str, all_planets: Dict) -> float: """Continuous Sphuta Drishti, quarter-weighted by natural benefic/malefic status.""" if pname not in all_planets: return 0.0 benefics, malefics = classify_drik_planets(all_planets) target_lon = _planet_longitude(pname, all_planets) total = 0.0 for other_name in benefics | malefics: if other_name == pname: continue strength = _sphuta_drishti_virupas(target_lon - _planet_longitude(other_name, all_planets), other_name) total += strength if other_name in benefics else -strength return round(total / 4.0, 2) def _d30_sign(sign_idx: int, degree: float) -> int: ranges = ( ((5, 0), (10, 10), (18, 8), (25, 2), (30, 6)) if sign_idx % 2 == 0 else ((5, 1), (12, 5), (20, 11), (25, 9), (30, 7)) ) return next(sign for upper, sign in ranges if degree <= upper) def _saptavarga_signs(planets: Dict) -> Dict[int, Dict[str, int]]: result = {division: {} for division in (1, 2, 3, 7, 9, 12, 30)} for name in set(planets) & set(_MOOLATRIKONA): longitude = _planet_longitude(name, planets) sign_idx, degree = int(longitude // 30), longitude % 30.0 result[1][name] = sign_idx result[2][name] = (4 if sign_idx % 2 == 0 else 3) if degree < 15.0 else (3 if sign_idx % 2 == 0 else 4) for division in (3, 7, 9, 12): result[division][name] = varga_map(sign_idx, min(division - 1, int(degree / (30.0 / division))), division) result[30][name] = _d30_sign(sign_idx, degree) return result def _compound_relation_score(planet: str, owner: str, d1_signs: Dict[str, int]) -> float: natural = "friend" if owner in FRIENDSHIP[planet]["friend"] else "enemy" if owner in FRIENDSHIP[planet]["enemy"] else "neutral" if owner not in d1_signs: return {"friend": 15.0, "neutral": 7.5, "enemy": 3.75}[natural] separation = (d1_signs[owner] - d1_signs[planet]) % 12 temporary = "friend" if separation in {1, 2, 3, 9, 10, 11} else "enemy" return { ("friend", "friend"): 22.5, ("neutral", "friend"): 15.0, ("enemy", "friend"): 7.5, ("friend", "enemy"): 7.5, ("neutral", "enemy"): 3.75, ("enemy", "enemy"): 1.875, }[(natural, temporary)] def calc_sthana_bala_precise(pname: str, all_planets: Dict, house: int) -> Dict: longitude = _planet_longitude(pname, all_planets) degree = longitude % 30.0 vargas = _saptavarga_signs(all_planets) scores = {} for division, positions in vargas.items(): sign_idx = positions[pname] owner = SIGN_LORDS[SIGNS[sign_idx]] mt_sign, mt_start, mt_end = _MOOLATRIKONA[pname] if division == 1 and sign_idx == mt_sign and mt_start <= degree < mt_end: score = 45.0 elif owner == pname: score = 30.0 else: score = _compound_relation_score(pname, owner, vargas[1]) scores[f"sapta_d{division}"] = score offset = (longitude - DEBILITATION_DEG[pname]) % 360.0 ucha_bala = round(min(offset, 360.0 - offset) / 3.0, 2) wants_even = pname in {"Moon", "Venus"} ojayugma = 15.0 * sum((vargas[division][pname] % 2 == 1) == wants_even for division in (1, 9)) kendra_bala = 60.0 if house in (1, 4, 7, 10) else 30.0 if house in (2, 5, 8, 11) else 15.0 drekkana_bala = 15.0 if ( (pname in {"Sun", "Mars", "Jupiter"} and degree < 10.0) or (pname in {"Mercury", "Saturn"} and 10.0 <= degree < 20.0) or (pname in {"Moon", "Venus"} and degree >= 20.0) ) else 0.0 sapta_score = round(sum(scores.values()), 2) return { "ucha_bala": ucha_bala, **scores, "sapta_score": sapta_score, "ojayugma_bala": ojayugma, "kendra_bala": kendra_bala, "drekkana_bala": drekkana_bala, "total": round(ucha_bala + sapta_score + ojayugma + kendra_bala + drekkana_bala, 2), } def _lagrange_interpolate(xs: tuple[float, ...], ys: tuple[float, ...], value: float) -> float: total = 0.0 for i, x_i in enumerate(xs): term = ys[i] for j, x_j in enumerate(xs): if i != j: term *= (value - x_j) / (x_i - x_j) total += term return total def _ayana_bala(pname: str, longitude: float, ayanamsa: float) -> tuple[float, float]: raw_tropical = longitude + ayanamsa tropical = raw_tropical % 360.0 north = raw_tropical < 180.0 folded = tropical if 90.0 < tropical < 180.0: folded = 180.0 - tropical elif 180.0 < tropical < 270.0: folded = tropical - 180.0 elif tropical > 270.0: folded = 360.0 - tropical sign = 1.0 if north and pname in {"Moon", "Saturn"}: sign = -1.0 elif not north and pname in {"Sun", "Mars", "Jupiter", "Venus"}: sign = -1.0 if pname == "Mercury": sign = 1.0 declination = sign * _lagrange_interpolate( (0.0, 15.0, 30.0, 45.0, 60.0, 75.0, 90.0), (0.0, 362 / 60.0, 703 / 60.0, 1002 / 60.0, 1238 / 60.0, 1388 / 60.0, 24.0), round(folded, 2), ) bala = round((24.0 + declination) * 1.25, 2) if pname == "Sun": bala = round(bala * 2.0, 2) return bala, round(declination, 4) def _days_elapsed_since_base(year: int, base_year: int = 1951, base_days: int = 174) -> int: years = year - base_year leap_years = sum(1 for value in range(base_year + 1, year + 1) if value % 4 == 0 and (value % 100 != 0 or value % 400 == 0)) return base_days + leap_years * 366 + (years - leap_years) * 365 def _calendar_lords(context: Dict) -> Dict[str, str]: year, month, day = context["year"], context["month"], context["day"] local_jd = context["jd_local"] year_start = swe.julday(year, 1, 1, 0.0, swe.GREG_CAL) elapsed = int(local_jd - year_start + 1.0) ahargana = _days_elapsed_since_base(year - 1) + elapsed abda = _ABDA_WEEKDAY_PLANETS[(int(ahargana // 360) * 3 + 1) % 7] masa = _ABDA_WEEKDAY_PLANETS[(int(ahargana // 30) * 2 + 1) % 7] vaara_ahargana = _days_elapsed_since_base(year - 1, 1827, 244) + elapsed if context["local_hour"] < context["sunrise_hour"]: vaara_ahargana -= 1 vaara = _ABDA_WEEKDAY_PLANETS[int(vaara_ahargana) % 7] civil_weekday = int(math.ceil(local_jd + 1.0) % 7) local_hour = context["local_hour"] if local_hour < context["sunrise_hour"]: civil_weekday = (civil_weekday - 1) % 7 local_hour += 24.0 hora_index = (int(local_hour - context["sunrise_hour"]) + civil_weekday + 1) % 7 return {"abda": abda, "masa": masa, "vaara": vaara, "hora": _HORA_ORDER[hora_index]} def calc_kala_bala_precise(pname: str, all_planets: Dict, context: Dict) -> Dict: """Classical temporal strength from local solar events and calendar lords.""" local_hour = context["local_hour"] midnight = (context["sunrise_hour"] + context["previous_sunset_hour"]) / 2.0 midnight = 12.0 - midnight if midnight < 12.0 else midnight - 12.0 unnata = (local_hour - midnight) * 5.0 if local_hour < 12.0 else (24.0 + midnight - local_hour) * 5.0 if pname == "Mercury": nathonnata = 60.0 elif pname in {"Sun", "Jupiter", "Venus"}: nathonnata = round(unnata, 2) else: nathonnata = round(60.0 - unnata, 2) benefics, malefics = classify_drik_planets(all_planets) phase = round(abs(_planet_longitude("Sun", all_planets) - _planet_longitude("Moon", all_planets)) / 3.0, 2) paksha = phase if pname in benefics else round(60.0 - phase, 2) if pname in malefics else 0.0 if pname == "Moon": paksha = round(paksha * 2.0, 2) sunrise, sunset = context["sunrise_hour"], context["sunset_hour"] day_length = sunset - sunrise night_length = 24.0 - day_length tribhaga_lord = None if sunrise <= local_hour < sunset: tribhaga_lord = ("Mercury", "Sun", "Saturn")[min(2, int((local_hour - sunrise) / (day_length / 3.0)))] else: since_sunset = (local_hour - sunset) % 24.0 tribhaga_lord = ("Moon", "Venus", "Mars")[min(2, int(since_sunset / (night_length / 3.0)))] tribhaga = 60.0 if pname in {"Jupiter", tribhaga_lord} else 0.0 lords = _calendar_lords(context) calendar = (15.0 if pname == lords["abda"] else 0.0) + (30.0 if pname == lords["masa"] else 0.0) + (45.0 if pname == lords["vaara"] else 0.0) + (60.0 if pname == lords["hora"] else 0.0) ayana, declination = _ayana_bala(pname, _planet_longitude(pname, all_planets), context["ayanamsa_degrees"]) total = round(nathonnata + paksha + tribhaga + calendar + ayana, 2) return { "nathonnata": nathonnata, "paksha": paksha, "tribhaga": tribhaga, "abda": 15.0 if pname == lords["abda"] else 0.0, "masa": 30.0 if pname == lords["masa"] else 0.0, "vaara": 45.0 if pname == lords["vaara"] else 0.0, "hora": 60.0 if pname == lords["hora"] else 0.0, "ayana": ayana, "declination": declination, "yuddha": 0.0, "total": total, } def _surya_mean_longitude(pname: str, context: Dict) -> float: """Surya Siddhanta mean motion with desantara correction from Ujjain.""" local_jd = context["jd_local"] kali_days = int(local_jd - 588_465.0) weekday = int(math.ceil(local_jd + 1.0) % 7) kali_days += weekday - 5 - kali_days % 7 daily_motion = round(_SURYA_REVOLUTIONS[pname] / _SURYA_CIVIL_DAYS * 360.0, 7) mean_longitude = (kali_days * daily_motion) % 360.0 desantara = (_UJJAIN_LONGITUDE - context["lon"]) / 360.0 * daily_motion return (mean_longitude + desantara) % 360.0 def calc_chesta_bala_precise(pname: str, all_planets: Dict, kala: Dict, context: Dict) -> float: """BPHS bounded Chesta Bala using mean motion and Seeghrochcha. Mean-motion/Seeghrochcha structure and constants are adapted from the MIT jyotishganit reference retained under references/open_source_sources. """ if pname == "Sun": return round(float(kala["ayana"]), 2) if pname == "Moon": return round(float(kala["paksha"]), 2) sun_mean = _surya_mean_longitude("Sun", context) planet_mean = _surya_mean_longitude(pname, context) if pname in {"Mercury", "Venus"}: seeghrochcha, mean_longitude = planet_mean, sun_mean else: seeghrochcha, mean_longitude = sun_mean, planet_mean average_longitude = (_planet_longitude(pname, all_planets) + mean_longitude) / 2.0 chesta_kendra = abs(seeghrochcha - average_longitude) if chesta_kendra > 180.0: chesta_kendra = 360.0 - chesta_kendra return round(max(0.0, chesta_kendra) / 3.0, 2) def calc_shadbala(planets: Dict, asc_sign: str, birth_hour: float, sun_lon: float, moon_lon: float, birth_minute: float = 0.0, context: Dict | None = None) -> Dict: """ 计算 Shadbala 相对强弱参考(covered;外部绝对值校准前保留置信度上限) Args: planets: 行星数据 dict,每颗行星需要 {sign, degree, house, retrograde, speed} asc_sign: 上升星座名称 birth_hour: 出生时间(当地时间,24小时制) sun_lon: 太阳恒星黄道经度 moon_lon: 月亮恒星黄道经度 Returns: Shadbala 计算结果(内部一致的相对强弱参考) """ results = {} is_night = birth_hour < 6.0 or birth_hour >= 18.0 sun_northern = sun_lon >= 270 or sun_lon < 90 # 第一轮:计算所有原始值 raw_totals = {} for pname in ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn']: if pname not in planets: continue p = planets[pname] lon = p.get('degree', 0) sign = p.get('sign', 'Aries') house = p.get('house', 1) retro = p.get('retrograde', False) speed = p.get('speed', 1.0) sthana = calc_sthana_bala_precise(pname, planets, house) dig = calc_dig_bala_precise(pname, lon, context["house_midpoints"]) if context and context.get("house_midpoints") else calc_dig_bala(pname, house) kala = calc_kala_bala_precise(pname, planets, context) if context else calc_kala_bala(pname, is_night, sun_northern, sun_lon, moon_lon, birth_hour, birth_minute) chesta = calc_chesta_bala_precise(pname, planets, kala, context) if context else calc_chesta_bala(pname, retro, speed, sun_lon, moon_lon) naisargika = NAISARGIKA_BALA.get(pname, 30.0) drik = calc_drik_bala(pname, sign, house, planets) raw = sthana['total'] + dig + kala['total'] + chesta + naisargika + drik raw_totals[pname] = max(1.0, raw) # 第二轮:按六项子力绝对值生成结果。 # Shadbala 的绝对 Rupas 必须保留子项合计,不做七星总和归一。 # 旧的 1200 Virupas 全局不变量会把七颗星总 Rupa 固定到 20, # 低于 BPHS 最低要求合计 40 Rupa,导致 JHora/PDF 对标系统性偏低。 for pname in ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn']: if pname not in planets or pname not in raw_totals: continue p = planets[pname] lon = p.get('degree', 0) sign = p.get('sign', 'Aries') house = p.get('house', 1) retro = p.get('retrograde', False) speed = p.get('speed', 1.0) sthana = calc_sthana_bala_precise(pname, planets, house) dig = calc_dig_bala_precise(pname, lon, context["house_midpoints"]) if context and context.get("house_midpoints") else calc_dig_bala(pname, house) kala = calc_kala_bala_precise(pname, planets, context) if context else calc_kala_bala(pname, is_night, sun_northern, sun_lon, moon_lon, birth_hour, birth_minute) chesta = calc_chesta_bala_precise(pname, planets, kala, context) if context else calc_chesta_bala(pname, retro, speed, sun_lon, moon_lon) naisargika = NAISARGIKA_BALA.get(pname, 30.0) drik = calc_drik_bala(pname, sign, house, planets) raw = raw_totals[pname] total_virupas = raw total_rupas = total_virupas / VIRUPAS_PER_RUPA min_req = MIN_REQUIRED.get(pname, 5.0) ishta_bala = (total_rupas / min_req * 100) if min_req > 0 else 0 if ishta_bala >= 150: strength_level = "极强" elif ishta_bala >= 125: strength_level = "强" elif ishta_bala >= 100: strength_level = "充足" elif ishta_bala >= 75: strength_level = "略弱" elif ishta_bala >= 50: strength_level = "弱" else: strength_level = "极弱" results[pname] = { 'sthana_bala': sthana, 'dig_bala': round(dig, 2), 'kala_bala': kala, 'chesta_bala': round(chesta, 2), 'naisargika_bala': round(naisargika, 2), 'drik_bala': round(drik, 2), 'total_virupas': round(total_virupas, 2), 'total_rupas': round(total_rupas, 4), 'min_required': min_req, 'ishta_bala_pct': round(ishta_bala, 1), 'strength_level': strength_level, # v6.1.10: Ishta/Kashta Phala 'ishta_phala': round(math.sqrt(max(0, sthana['ucha_bala'] * chesta)), 2), 'kashta_phala': round(math.sqrt(max(0, (60 - sthana['ucha_bala']) * (60 - chesta))), 2), } # 排名 ranked = sorted(results.items(), key=lambda x: x[1]['total_rupas'], reverse=True) for i, (name, _) in enumerate(ranked): results[name]['rank'] = i + 1 # Bhava Bala(宫位力量)v6.9.12 新增 bhava_bala = calc_bhava_bala(planets, asc_sign) return { 'method': 'Shadbala六重力量(absolute Virupas; precise Sthana/Dig/Kala/Drik; bounded BPHS Chesta)', 'method_variants': { 'kala': 'bphs_local_solar_events_ahargana_declination' if context else 'legacy_approximation', 'chesta': 'bphs_bounded_surya_mean_motion_seeghrochcha' if context else 'legacy_speed_bands', }, 'external_parity_boundary': 'Chesta remains cross-engine method-conflicted; totals inherit that boundary.', 'is_night_birth': is_night, 'sun_uttarayana': sun_northern, 'planets': results, 'bhava_bala': bhava_bala, 'ranking': [name for name, _ in ranked], 'strongest': ranked[0][0] if ranked else None, 'weakest': ranked[-1][0] if ranked else None, } def _dignity_score(pname: str, sign: str) -> float: """计算单层Varga中的尊严分数(BPHS标准) Own Sign=45, Exalted=50 (不超过45 unless specifically exalted degrees), Great Friend=40, Friend=35, Neutral=25, Enemy=15, Great Enemy=5 """ lord = SIGN_LORDS.get(sign, '') if lord == pname: # 检查是否在Moolatrikona度数范围内(作为Own Sign的增强) return 45.0 # 获取行星对该sign lord的关系 rel = FRIENDSHIP.get(pname, {}) friends_of_pname = rel.get('friend', []) enemies_of_pname = rel.get('enemy', []) neutrals_of_pname = rel.get('neutral', []) # 反向关系:sign lord对pname的态度 lord_rel = FRIENDSHIP.get(lord, {}) lord_friends = lord_rel.get('friend', []) lord_enemies = lord_rel.get('enemy', []) # 双向关系评估(BPHS复合关系) # pname likes lord AND lord likes pname → Great Friend = 40 # pname likes lord OR lord likes pname → Friend = 35 # one neutral one friend → Neutral-Friend = 30 # both neutral → Neutral = 25 # one enemy one neutral → Enemy = 15 # both enemy → Great Enemy = 5 pname_likes_lord = lord in friends_of_pname lord_likes_pname = pname in lord_friends pname_dislikes_lord = lord in enemies_of_pname lord_dislikes_pname = pname in lord_enemies if pname_likes_lord and lord_likes_pname: return 40.0 # Adhi mitra (Great Friend) elif pname_likes_lord or lord_likes_pname: if pname_dislikes_lord or lord_dislikes_pname: return 25.0 # Mixed → Neutral return 35.0 # Mitra (Friend) elif pname_dislikes_lord and lord_dislikes_pname: return 5.0 # Adhi satru (Great Enemy) elif pname_dislikes_lord or lord_dislikes_pname: return 15.0 # Satru (Enemy) else: return 25.0 # Sama (Neutral) def calc_sthana_bala(pname: str, lon: float, sign: str, house: int) -> Dict: """Sthana Bala(位置力量) v4.5.0: 修复Saptavargaja Bala为完整7层计算 """ # A. Ucha Bala(入庙力量)max 60 Virupas debilit_deg = DEBILITATION_DEG.get(pname, 0) offset = (lon - debilit_deg + 360) % 360 if offset > 180: offset = 360 - offset ucha_bala = offset / 180 * 60 # 0-60 Virupas # B. Saptavargaja Bala(七分盘力量)max ~315 Virupas (7 × 45) # v6.1.10: 调用varga.py实际分盘计算替代简化算法 sign_idx = SIGNS.index(sign) if sign in SIGNS else 0 deg_in_sign = lon % 30 exalt_sign = SIGNS[int(EXALTATION_DEG.get(pname, 0) / 30) % 12] debilit_sign = SIGNS[int(DEBILITATION_DEG.get(pname, 0) / 30) % 12] # D1 (Rashi) — 直接用当前sign,检查入庙/落陷 d1_score = _dignity_score(pname, sign) if sign == exalt_sign: d1_score = 50.0 elif sign == debilit_sign: d1_score = 5.0 # D2 (Hora) — 调用varga_map d2_part = int(deg_in_sign / 15) # 30/2=15 d2_sign_idx = varga_map(sign_idx, d2_part, 2) d2_sign = VARGA_SIGNS[d2_sign_idx] if pname == VARGA_SIGN_LORDS.get(d2_sign, ''): d2_score = 45.0 elif d2_sign == exalt_sign: d2_score = 50.0 elif d2_sign == debilit_sign: d2_score = 5.0 else: d2_score = _dignity_score(pname, d2_sign) # D3 (Drekkana) — 调用varga_map d3_part = int(deg_in_sign / 10) # 30/3=10 d3_sign_idx = varga_map(sign_idx, d3_part, 3) d3_sign = VARGA_SIGNS[d3_sign_idx] if pname == VARGA_SIGN_LORDS.get(d3_sign, ''): d3_score = 30.0 elif d3_sign == exalt_sign: d3_score = 45.0 elif d3_sign == debilit_sign: d3_score = 5.0 else: d3_score = _dignity_score(pname, d3_sign) # D4 (Turyamsa/Chaturthamsa) — 调用varga_map d4_part = int(deg_in_sign / 7.5) # 30/4=7.5 # 边界处理:最后一份 if d4_part >= 4: d4_part = 3 d4_sign_idx = varga_map(sign_idx, d4_part, 4) d4_sign = VARGA_SIGNS[d4_sign_idx] if pname == VARGA_SIGN_LORDS.get(d4_sign, ''): d4_score = 45.0 elif d4_sign == exalt_sign: d4_score = 50.0 elif d4_sign == debilit_sign: d4_score = 5.0 else: d4_score = _dignity_score(pname, d4_sign) # D7 (Saptamsa) — 调用varga_map d7_part = int(deg_in_sign / (30 / 7)) if d7_part >= 7: d7_part = 6 d7_sign_idx = varga_map(sign_idx, d7_part, 7) d7_sign = VARGA_SIGNS[d7_sign_idx] if pname == VARGA_SIGN_LORDS.get(d7_sign, ''): d7_score = 45.0 elif d7_sign == exalt_sign: d7_score = 50.0 elif d7_sign == debilit_sign: d7_score = 5.0 else: d7_score = _dignity_score(pname, d7_sign) # D9 (Navamsa) — 调用varga_map d9_part = int(deg_in_sign / (30 / 9)) if d9_part >= 9: d9_part = 8 d9_sign_idx = varga_map(sign_idx, d9_part, 9) d9_sign = VARGA_SIGNS[d9_sign_idx] if pname == VARGA_SIGN_LORDS.get(d9_sign, ''): d9_score = 45.0 elif d9_sign == exalt_sign: d9_score = 50.0 elif d9_sign == debilit_sign: d9_score = 5.0 else: d9_score = _dignity_score(pname, d9_sign) # D12 (Dwadashamsa) — 调用varga_map d12_part = int(deg_in_sign / 2.5) # 30/12=2.5 if d12_part >= 12: d12_part = 11 d12_sign_idx = varga_map(sign_idx, d12_part, 12) d12_sign = VARGA_SIGNS[d12_sign_idx] if pname == VARGA_SIGN_LORDS.get(d12_sign, ''): d12_score = 45.0 elif d12_sign == exalt_sign: d12_score = 50.0 elif d12_sign == debilit_sign: d12_score = 5.0 else: d12_score = _dignity_score(pname, d12_sign) sapta_score = d1_score + d2_score + d3_score + d4_score + d7_score + d9_score + d12_score # C. Ojayugma Bala(奇偶宫力量)max 15 Virupas # v4.5.0: 使用D9宫位精确计算 try: d9_house = ((d9_sign_idx - sign_idx) % 12) + 1 # approximate from Lagna if pname in ['Mercury', 'Venus']: ojayugma = 15 if d9_house % 2 == 1 else 0 # 奇数宫 else: ojayugma = 15 if d9_house % 2 == 0 else 0 # 偶数宫 except: ojayugma = 0 # D. Kendra Bala(角宫力量)v6.9.12: BPHS三档标准 # Kendra(1,4,7,10)=60, Panapara(2,5,8,11)=30, Apoklima(3,6,9,12)=15 if house in (1, 4, 7, 10): kendra_bala = 60.0 elif house in (2, 5, 8, 11): kendra_bala = 30.0 else: kendra_bala = 15.0 # E. Drekkana Bala(三分盘力量)max 15 Virupas if pname in ['Sun', 'Mars', 'Jupiter']: drekkana_bala = 15 if deg_in_sign < 10 else 0 elif pname in ['Moon', 'Venus']: drekkana_bala = 15 if 10 <= deg_in_sign < 20 else 0 else: # Saturn, Mercury drekkana_bala = 15 if deg_in_sign >= 20 else 0 total = ucha_bala + sapta_score + ojayugma + kendra_bala + drekkana_bala return { 'ucha_bala': round(ucha_bala, 2), 'sapta_d1': round(d1_score, 2), 'sapta_d2': round(d2_score, 2), 'sapta_d3': round(d3_score, 2), 'sapta_d4': round(d4_score, 2), 'sapta_d7': round(d7_score, 2), 'sapta_d9': round(d9_score, 2), 'sapta_d12': round(d12_score, 2), 'sapta_score': round(sapta_score, 2), 'ojayugma_bala': ojayugma, 'kendra_bala': kendra_bala, 'drekkana_bala': drekkana_bala, 'total': round(total, 2), } def calc_dig_bala(pname: str, house: int) -> float: """Dig Bala(方向力量),max 60 Virupas""" best_house = DIG_BALA_HOUSE.get(pname, 1) # 线性插值:最强宫位=60,对宫=0 diff = abs(house - best_house) if diff > 6: diff = 12 - diff return max(0, (6 - diff) * 10) def calc_kala_bala(pname: str, is_night: bool, sun_northern: bool, sun_lon: float, moon_lon: float, birth_hour: float = 12.0, birth_minute: float = 0.0) -> Dict: """Kala Bala(时间力量) v6.1.10: Nathonnata升级为BPHS比例计算(渐变0-60非二值) v6.1.10: 添加Abda/Masa/Dina/Hora子项 """ components = {} # A. Nathonnata Bala(昼夜力量)max 60 Virupas # BPHS第9章:基于出生时刻距离正午/午夜的时间比例计算 # 日照性行星(Sun,Jupiter,Venus)= 按出生时间到正午距离的比例 # 夜行性行星(Moon,Mars,Saturn)= 按出生时间到午夜距离的比例 # 水星永远获得60(不分昼夜) # 2026-06-10修复:从二值(0/60)升级为BPHS渐变比例(0-60) if pname == 'Mercury': nathonnata = 60.0 else: birth_decimal = birth_hour + birth_minute / 60.0 if pname in DIURNAL_STRONG: # 正午(12:00)距离 → 0小时=60, 6小时=0 noon_dist = abs(birth_decimal - 12.0) noon_dist = min(noon_dist, 24.0 - noon_dist) nathonnata = max(0.0, (6.0 - noon_dist) / 6.0 * 60.0) else: # 午夜(0:00)距离 → 0小时=60, 6小时=0 midnight_dist = abs(birth_decimal - 0.0) midnight_dist = min(midnight_dist, 24.0 - midnight_dist) nathonnata = max(0.0, (6.0 - midnight_dist) / 6.0 * 60.0) components['nathonnata'] = round(nathonnata, 2) # B. Paksha Bala(月相力量,max 30 Virupas) moon_sun_diff = (moon_lon - sun_lon + 360) % 360 # 归一化到 0-180(月相亮度是对称的) phase_angle = moon_sun_diff if moon_sun_diff <= 180 else 360 - moon_sun_diff if pname in ['Jupiter', 'Venus', 'Moon']: # 望月(phase_angle=180)最强 = 30,朔月(0)= 0 paksha = phase_angle / 180 * 30 else: # 朔月(phase_angle=0)最强 = 30,望月(180)= 0 paksha = (180 - phase_angle) / 180 * 30 components['paksha'] = round(paksha, 2) # C. Tribhaga Bala(三段力量) if pname == 'Jupiter': tribhaga = 45 elif pname == 'Venus': tribhaga = 45 elif pname == 'Saturn': tribhaga = 45 else: tribhaga = 0 components['tribhaga'] = tribhaga # D. Ayana Bala(太阳南北行) if pname == 'Mercury': ayana = 30 elif sun_northern and pname in ['Sun', 'Mars', 'Moon']: ayana = 30 elif not sun_northern and pname in ['Jupiter', 'Venus', 'Saturn']: ayana = 30 else: ayana = 15 components['ayana'] = ayana # E. Hora Bala(时主力量)v6.9.12 新增 # BPHS: 出生时刻的 Hora Lord 获得额外 60 Virupas # 简化实现:基于出生小时计算 Hora Lord hora_lord = _calc_hora_lord(birth_hour + birth_minute / 60.0, sun_lon) if pname == hora_lord: hora = 60.0 else: hora = 0.0 components['hora'] = round(hora, 2) total = sum(components.values()) return {k: v for k, v in components.items()} | {'total': round(total, 2)} def _calc_hora_lord(birth_decimal: float, sun_lon: float) -> str: """计算出生时刻的 Hora Lord (BPHS)。 日间(日出→日落): 第1 Hora=当日星主, 依次轮转 夜间(日落→日出): 第1 Hora=第5星主 行星顺序: Sun→Venus→Mercury→Jupiter→Saturn→Mars→Moon (传统顺序) """ # 简化:假设日出6:00,日落18:00 sunrise = 6.0 sunset = 18.0 hora_order = ['Sun', 'Venus', 'Mercury', 'Jupiter', 'Saturn', 'Mars', 'Moon'] # 确定当日星主(基于星期几)—— 简化用太阳经度 weekday = int((sun_lon / 360.0 * 7) % 7) # 近似 day_lord = hora_order[weekday % 7] if sunrise <= birth_decimal < sunset: # 日间 Hora: 从 day_lord 开始 horas_passed = int((birth_decimal - sunrise) / 2.5) # 每段约2.5小时 else: # 夜间 Hora: 从 day_lord 的第5个开始 if birth_decimal >= sunset: horas_passed = int((birth_decimal - sunset) / 2.5) else: horas_passed = int((birth_decimal + 24 - sunset) / 2.5) day_lord = hora_order[(hora_order.index(day_lord) + 4) % 7] return hora_order[(hora_order.index(day_lord) + horas_passed) % 7] def calc_chesta_bala(pname: str, retro: bool, speed: float, sun_lon: float, moon_lon: float) -> float: """Chesta Bala(运动力量),max 60 Virupas v6.9.12修复:Sun 使用 Seeghrochcha(最快近日点)公式 v6.1.10修复:Sun不再固定60,改为基于太阳实际速度计算 BPHS: Sun的Chesta基于其运动速率——最慢(远日点)最强 """ if pname == 'Sun': # v6.9.12: BPHS标准 — Seeghrochcha公式 # Sun Chesta = 基于日行速度,最慢时Chesta最高 # 太阳日均速度范围: ~0.953°/天(远日点) → ~1.017°/天(近日点) abs_speed = abs(speed) # 标准化到0-1 (0=最快=近日点, 1=最慢=远日点) min_speed, max_speed = 0.953, 1.017 if abs_speed <= min_speed: slowness = 1.0 elif abs_speed >= max_speed: slowness = 0.0 else: slowness = 1.0 - (abs_speed - min_speed) / (max_speed - min_speed) return slowness * 60.0 if pname == 'Moon': # 月亮根据月相:望月=60,朔月=0 # BPHS: Chesta Bala 与月相亮面比例成正比 # diff 取 0-180 范围(>180 时用 360-diff,因为月相是对称的) moon_sun_diff = (moon_lon - sun_lon + 360) % 360 if moon_sun_diff > 180: moon_sun_diff = 360 - moon_sun_diff return moon_sun_diff / 180 * 60 # 其他行星 if retro: return 60.0 # 速度判断(简化:用speed的绝对值) abs_speed = abs(speed) if abs_speed > 1.0: # 快速直行 return 50.0 elif abs_speed > 0.5: return 35.0 elif abs_speed > 0.1: return 20.0 else: return 10.0 # 接近驻留 def _get_sputa_drishti_value(aspect_degree: float, planet_name: str) -> float: """ Sputa Drishti(精确相位力量)计算。 基于jyotishganit (MIT License) 算法,将行星间角距离转换为连续相位力量值。 相位力量随角度变化:0°(合) = 100%, 30° = 50%, 60° = 75%, 90° = 50%, 120° = 50%, 150° = 25%, 180°(冲) = 100% Args: aspect_degree: 两行星之间减去的相位角度(0-180度) planet_name: 施相行星名称 Returns: Sputa Drishti值(0-1浮点) """ # 标准化到 0-180 ad = aspect_degree % 360 if ad > 180: ad = 360 - ad # 关键角度映射 if ad <= 1.0: return 1.0 # 紧密合相 elif ad <= 3.0: return 0.95 elif ad <= 7.0: return 0.85 elif ad <= 10.0: return 0.75 elif ad <= 15.0: return 0.675 elif ad <= 20.0: return 0.5 elif ad <= 25.0: return 0.375 elif ad <= 30.0: return 0.25 elif ad <= 40.0: return 0.2 elif ad <= 50.0: return 0.15 elif ad <= 60.0: return 0.1 elif ad <= 75.0: return 0.075 elif ad <= 90.0: return 0.05 elif ad <= 120.0: return 0.0375 elif ad <= 150.0: return 0.025 # >150度几乎没有相位力量 return 0.0 def calc_yuddha_bala(planets: Dict) -> Dict: """ Yuddha Bala(行星战争力量调整)。 基于jyotishganit (MIT License) 算法。 当两颗行星相距 < 1° 时发生行星战争,胜者获得力量加成, 败者减去相应力量。基于 Shadbala 总力量 + 行星直径比率判定。 Args: planets: 行星数据 dict,需要包含经度信息和已计算的shadbala Returns: yuddha调整字典(planet_name → adjustment_value) """ SEVEN_PLANETS = ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn'] PLANET_DISK_SIZE = { 'Sun': 0.533, 'Moon': 0.518, 'Mars': 0.033, 'Mercury': 0.022, 'Jupiter': 0.137, 'Venus': 0.054, 'Saturn': 0.103 } WAR_ORB = 1.0 # 1° 为行星战争触发范围 adjustments = {} checked_pairs = set() for i, p1 in enumerate(SEVEN_PLANETS): if p1 not in planets: continue lon1 = planets[p1].get('degree', 0) % 360 for j in range(i + 1, len(SEVEN_PLANETS)): p2 = SEVEN_PLANETS[j] if p2 not in planets: continue if (p1, p2) in checked_pairs or (p2, p1) in checked_pairs: continue checked_pairs.add((p1, p2)) lon2 = planets[p2].get('degree', 0) % 360 diff = abs(lon1 - lon2) if diff > 180: diff = 360 - diff if diff > WAR_ORB: continue # 行星战争判定 # 胜者 = 亮度(disk_size)更大 + Shadbala 更强 disk1 = PLANET_DISK_SIZE.get(p1, 0.05) disk2 = PLANET_DISK_SIZE.get(p2, 0.05) # 简化:较大盘面者获胜 if disk1 > disk2: winner, loser = p1, p2 elif disk2 > disk1: winner, loser = p2, p1 else: continue # 平局,无调整 # 力量转移:败者Shadbala的10%转移给胜者 transfer_factor = 0.1 adjustments[winner] = adjustments.get(winner, 0) + transfer_factor * 60 adjustments[loser] = adjustments.get(loser, 0) - transfer_factor * 60 return adjustments def calc_drik_bala(pname: str, sign: str, house: int, all_planets: Dict) -> float: """Compatibility wrapper for the continuous Sphuta Drishti calculation.""" return calc_drik_bala_precise(pname, all_planets) # ============================================================================ # Bhava Bala(宫位力量)v6.9.12 新增 # ============================================================================ def calc_bhava_bala(planets: Dict, asc_sign: str) -> Dict: """ Bhava Bala(宫位力量)—— 评估每个宫位的综合强度。 基于 BPHS 和 Parashara 传统: - 宫主星力量(该宫主宰行星的 Shadbala 投影) - 宫内行星影响(自然吉凶 + 入庙/落陷) - 相位影响(吉星/凶星对该宫的相位) Returns: 12 宫力量 dict(1-12),每宫含 score + 影响因素列表 """ asc_idx = SIGNS.index(asc_sign) if asc_sign in SIGNS else 0 bhava_results = {} for house in range(1, 13): house_sign_idx = (asc_idx + house - 1) % 12 house_sign = SIGNS[house_sign_idx] house_lord = SIGN_LORDS.get(house_sign, '') factors = [] score = 0.0 # A. 宫主星基础分(Own Sign = 4, Exalted = 5, etc.) if house_lord: lord_planet = planets.get(house_lord, {}) lord_house = lord_planet.get('house', 1) # 宫主星坐落位置的力量 if lord_house in (1, 4, 7, 10): # 角宫 lord_pos_score = 4.0 factors.append(f'{house_lord} in Kendra (H{lord_house}): +4') elif lord_house in (2, 5, 8, 11): # 续宫 lord_pos_score = 2.5 factors.append(f'{house_lord} in Panapara (H{lord_house}): +2.5') else: # 果宫 lord_pos_score = 1.5 factors.append(f'{house_lord} in Apoklima (H{lord_house}): +1.5') # 宫主星入庙/落陷/旺相 lord_sign = lord_planet.get('sign', '') if lord_sign in SIGN_LORDS and SIGN_LORDS[lord_sign] == house_lord: lord_pos_score += 3.0 factors.append(f'{house_lord} in Own Sign: +3') elif lord_sign and EXALTATION_DEG.get(house_lord, 999) // 30 == SIGNS.index(lord_sign) if lord_sign in SIGNS else False: lord_pos_score += 4.0 factors.append(f'{house_lord} Exalted: +4') score += lord_pos_score # B. 宫内行星影响 for pname, pdata in planets.items(): if pname in ('Rahu', 'Ketu'): continue if pdata.get('house') == house: if pname in BENEFICS: score += 2.0 factors.append(f'{pname} in H{house} (Benefic): +2') elif pname in MALEFICS: score -= 1.5 factors.append(f'{pname} in H{house} (Malefic): -1.5') else: score += 0.5 factors.append(f'{pname} in H{house} (Neutral): +0.5') # C. 相位影响(简化:7宫冲相 + 特殊相位) for pname, pdata in planets.items(): if pname in ('Rahu', 'Ketu'): continue asp_house = pdata.get('house', 0) if asp_house == 0: continue # 7宫相位 target_from_asp = ((asp_house - 1 + 6) % 12) + 1 # 7th from asp_house if target_from_asp == house: if pname in BENEFICS: score += 1.5 factors.append(f'{pname} aspects H{house} (7th, Benefic): +1.5') elif pname in MALEFICS: score -= 1.0 factors.append(f'{pname} aspects H{house} (7th, Malefic): -1.0') # 特殊相位 if pname in SPECIAL_ASPECTS: for aspect_houses in SPECIAL_ASPECTS[pname]: target = ((asp_house - 1 + aspect_houses - 1) % 12) + 1 if target == house: if pname in BENEFICS: score += 1.5 factors.append(f'{pname} aspects H{house} ({aspect_houses}th, Benefic): +1.5') elif pname in MALEFICS: score -= 1.0 factors.append(f'{pname} aspects H{house} ({aspect_houses}th, Malefic): -1.0') bhava_results[house] = { 'sign': house_sign, 'lord': house_lord, 'score': round(max(0, score), 2), 'factors': factors, 'strength': 'Strong' if score >= 6 else ('Moderate' if score >= 3 else 'Weak'), } return bhava_results