Files
Jyotisha/scripts/pancha_mahapurusha.py
T
Jesse_ChenandClaude Opus 5.5 bc03bba26b fix(yoga): D9 debilitation only weakens Pancha Mahapurusha (BUG-1213)
Round-4 astrologer ruling 2026-10-03 Q1: the yoga stays formed with
d9_dignity_modifier='weakened' and the reason 'D9 落陷,兑现减弱'; only a failed
D1 condition is not_formed. The D9 sign is derived from the longitude when the
caller does not pass navamsa_sign, so the card, report and /api/pancha_mahapurusha
paths all check it. None of the nine public celebrity charts change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
2026-10-03 16:18:06 +08:00

290 lines
12 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 -*-
"""
Pancha Mahapurusha Yoga(五王瑜伽)完整检测模块 v1.0
基于BPHS和dashaflow (MIT) detect_yogas核心算法
五种瑜伽:
- Ruchaka Yoga (Mars)
- Bhadra Yoga (Mercury)
- Hamsa Yoga (Jupiter)
- Malavya Yoga (Venus)
- Shasha Yoga (Saturn)
破格口径按占星师定稿 2026-10-03 甲5(BUG-1207):逆行不破格;燃烧、与凶星
同宫或被照只记「部分减弱」。D9 落陷按占星师第四轮裁定(2026-10-03 问 1,BUG-1213)
只算减弱:格局仍成立(`status: formed`),加 `d9_dignity_modifier: "weakened"`;只有
D1 条件(命宫角宫 + 入庙/入旺)不成立时才是 `not_formed`。本模块是五大人格的
唯一主检测器,`yoga_rules.json` 同名规则与 `yogas_doshas.calc_pancha_mahapurusha_yoga`
为 reference_only。
"""
from typing import Dict, List, Optional
SIGNS = ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo',
'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces']
KENDRA_HOUSES = {1, 4, 7, 10}
MAHAPURUSHA_PLANETS = {"Mars", "Mercury", "Jupiter", "Venus", "Saturn"}
MAHAPURUSHA_NAMES = {
"Mars": "Ruchaka Yoga",
"Mercury": "Bhadra Yoga",
"Jupiter": "Hamsa Yoga",
"Venus": "Malavya Yoga",
"Saturn": "Shasha Yoga",
}
MAHAPURUSHA_EFFECTS = {
"Ruchaka": "军事领袖、建筑大师、体魄强健、勇敢果断",
"Bhadra": "演说家、学者、机智聪慧、商业头脑",
"Hamsa": "精神导师、哲学家、富足慷慨、智慧卓越",
"Malavya": "艺术家、享乐者、魅力超凡、审美敏锐",
"Shasha": "政治家、长者、坚韧执着、权威地位",
}
EXALTATION = {'Sun':('Aries',10),'Moon':('Taurus',3),'Mars':('Capricorn',28),'Mercury':('Virgo',15),'Jupiter':('Cancer',5),'Venus':('Pisces',27),'Saturn':('Libra',20)}
OWN_SIGNS = {'Sun':{'Leo'},'Moon':{'Cancer'},'Mars':{'Aries','Scorpio'},'Mercury':{'Gemini','Virgo'},'Jupiter':{'Sagittarius','Pisces'},'Venus':{'Taurus','Libra'},'Saturn':{'Capricorn','Aquarius'}}
# Combustion orb (degrees from Sun)
COMBUSTION_ORB = {
'Moon': {'direct': 12, 'retrograde': 12},
'Mars': {'direct': 17, 'retrograde': 10},
'Mercury': {'direct': 14, 'retrograde': 12},
'Jupiter': {'direct': 11, 'retrograde': 10},
'Venus': {'direct': 10, 'retrograde': 8},
'Saturn': {'direct': 15, 'retrograde': 10},
}
NATURAL_MALEFICS = {"Saturn", "Mars", "Sun", "Rahu", "Ketu"}
def _is_exalted_or_own(planet: str, sign: str) -> bool:
if planet in EXALTATION and EXALTATION[planet][0] == sign:
return True
if planet in OWN_SIGNS and sign in OWN_SIGNS[planet]:
return True
return False
def _is_combust(planet: str, planet_degree: float, sun_degree: float, retrograde: bool = False) -> bool:
"""检查行星是否被太阳燃烧"""
profile = COMBUSTION_ORB.get(planet, {'direct': 10})
orb = profile.get('retrograde' if retrograde else 'direct', profile.get('direct', 10))
diff = abs(planet_degree - sun_degree)
if diff > 180:
diff = 360 - diff
return diff <= orb
def _has_malefic_aspect(planet_sign: str, all_planets: Dict, planet: str = None) -> List[str]:
"""Natural malefics in the same sign as the yoga planet (the planet itself excluded, BUG-1180)."""
afflicting = []
for pn, pd in all_planets.items():
if pn == planet or pn not in NATURAL_MALEFICS:
continue
# 简单合相检查
if pd.get('sign') == planet_sign:
afflicting.append(pn)
return afflicting
# Parashari whole-sign graha drishti, as aspects.calc_house_aspects: every graha
# the 7th; Mars also 4 / 8, Saturn 3 / 10; Rahu / Ketu cast none.
MALEFIC_DRISHTI = {'Sun': (7,), 'Mars': (4, 7, 8), 'Saturn': (3, 7, 10)}
def _malefic_aspectors(planet_sign: str, all_planets: Dict, planet: str = None) -> List[str]:
"""Natural malefics whose graha drishti falls on the yoga planet's sign."""
if planet_sign not in SIGNS:
return []
target = SIGNS.index(planet_sign)
casters = []
for pn, offsets in MALEFIC_DRISHTI.items():
pd = all_planets.get(pn)
if pn == planet or not isinstance(pd, dict) or pd.get('sign') not in SIGNS:
continue
origin = SIGNS.index(pd['sign'])
if any((origin + offset - 1) % 12 == target for offset in offsets):
casters.append(pn)
return casters
def navamsa_sign_of(lon: float) -> str:
"""D9 sign of a sidereal longitude (Parashari navamsa, 3°20' each)."""
return SIGNS[int((float(lon) % 360.0) * 9.0 / 30.0) % 12]
def _debilitation_sign(planet: str) -> str:
if planet not in EXALTATION:
return ''
return SIGNS[(SIGNS.index(EXALTATION[planet][0]) + 6) % 12]
def _longitude(pd: Dict) -> Optional[float]:
"""Sidereal longitude of a row; `degree` may be a longitude or a degree in sign."""
for key in ('lon', 'degree_raw', 'longitude'):
value = pd.get(key)
if isinstance(value, (int, float)):
return float(value) % 360.0
degree = pd.get('degree')
if not isinstance(degree, (int, float)):
return None
if degree >= 30 or pd.get('sign') not in SIGNS:
return float(degree) % 360.0
return SIGNS.index(pd['sign']) * 30.0 + float(degree)
MAHAPURUSHA_ORDER = ('Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn')
PLANET_ZH = {'Sun': '太阳', 'Mars': '火星', 'Mercury': '水星', 'Jupiter': '木星', 'Venus': '金星',
'Saturn': '土星', 'Rahu': '罗睺', 'Ketu': '计都'}
def detect_pancha_mahapurusha(planets: Dict, sun_degree: float = None,
include_not_formed: bool = False) -> List[Dict]:
"""
检测所有Pancha Mahapurusha Yoga(主检测器)。
占星师定稿 2026-10-03 甲5(BUG-1207)与第四轮问 1(BUG-1213):
- 只从命宫(Lagna)角宫计算:行星在上升起第 1/4/7/10 宫且入庙或入旺;
这一条不满足才是 `not_formed`;
- 逆行不破格;
- 燃烧不破格,只记「部分减弱」(`strength: reduced`);
- 与凶星同宫或被凶星照只降低强度,不消灭格局;
- D9 落陷只算减弱:`status` 仍是 `formed`,加 `d9_dignity_modifier: "weakened"`,
原因列「D9 落陷,兑现减弱」。D9 星座取调用方给的 `navamsa_sign`,没给时由
黄道经度推出,所以各路径(咨询、报告、`/api/pancha_mahapurusha`)都检查 D9。
Args:
planets: {planet: {'sign': str, 'house': int, 'lon'|'degree': float, 'navamsa_sign'?: str}}
`house` 从上升数;经度取 `lon` / `degree_raw` / `longitude`,否则 `degree`
(≥30 视为经度,否则按星座内度数)。
sun_degree: 太阳黄道经度(燃烧检测);缺省时取 planets['Sun'] 的经度。
include_not_formed: 为 True 时,D1 条件不成立的五星也返回一行
(`status: not_formed`、`is_valid: False`)。
Returns:
检测到的PMC Yoga列表(含 status: formed / not_formed,strength: full / reduced)
"""
yogas = []
sun_deg = sun_degree
if sun_deg is None and isinstance(planets.get('Sun'), dict):
sun_deg = _longitude(planets['Sun'])
for planet in MAHAPURUSHA_ORDER:
pd = planets.get(planet)
if not pd:
continue
sign = pd.get('sign', '')
house = pd.get('house', 0)
# 基本条件:在Kendra + own/exalted(D1)。不满足即 not_formed。
if house not in KENDRA_HOUSES or not _is_exalted_or_own(planet, sign):
if include_not_formed:
yogas.append({
'name': MAHAPURUSHA_NAMES[planet],
'planet': planet,
'sign': sign,
'house': house,
'status': 'not_formed',
'is_valid': False,
'strength': 'not_formed',
'strength_cn': '不成立',
'reason': 'd1_kendra_own_or_exalted_not_met',
'reductions': [],
'reductions_en': [],
'cancellations': [],
})
continue
yoga_name = MAHAPURUSHA_NAMES[planet]
effect = MAHAPURUSHA_EFFECTS.get(yoga_name.replace(" Yoga", ""), "")
cancellations = []
reductions = []
reductions_en = []
# 1. 燃烧:部分减弱(容许度表与 jyotish_engine.COMBUSTION_ORBS 一致,
# tests/test_combustion_orb_tables_agree.py)
planet_deg = _longitude(pd)
retro = bool(pd.get('retrograde'))
if sun_deg is not None and planet_deg is not None and _is_combust(planet, planet_deg, sun_deg, retro):
profile = COMBUSTION_ORB.get(planet, {'direct': 10})
threshold = profile.get('retrograde' if retro else 'direct', profile.get('direct', 10))
reductions.append(f"{PLANET_ZH[planet]}燃烧(距太阳 {threshold}° 以内)")
reductions_en.append(f"{planet} combust (within {threshold}° of the Sun)")
# 2. 逆行:不破格,不减弱(定稿甲5)
# 3. 凶星同宫 / 凶星照:只降低强度
malefics = _has_malefic_aspect(sign, planets, planet)
if malefics:
reductions.append(f"与凶星同宫:{'、'.join(PLANET_ZH.get(m, m) for m in malefics)}")
reductions_en.append(f"conjunct malefic(s): {', '.join(malefics)}")
aspectors = _malefic_aspectors(sign, planets, planet)
if aspectors:
reductions.append(f"受凶星照:{'、'.join(PLANET_ZH.get(m, m) for m in aspectors)}")
reductions_en.append(f"aspected by malefic(s): {', '.join(aspectors)}")
# 4. D9 落陷:只算减弱(第四轮问 1,BUG-1213),格局仍成立。
navamsa_sign = pd.get('navamsa_sign') or ''
if not navamsa_sign and planet_deg is not None:
navamsa_sign = navamsa_sign_of(planet_deg)
d9_modifier = None
if navamsa_sign and navamsa_sign == _debilitation_sign(planet):
d9_modifier = 'weakened'
reductions.append('D9 落陷,兑现减弱')
reductions_en.append('debilitated in D9: delivery weakened')
strength = 'reduced' if reductions else 'full'
yogas.append({
'name': yoga_name,
'planet': planet,
'sign': sign,
'house': house,
'status': 'formed',
'is_valid': True,
'strength': strength,
'strength_cn': {'full': '完整', 'reduced': '部分减弱'}[strength],
'd9_sign': navamsa_sign or None,
'd9_dignity_modifier': d9_modifier,
'effect': effect,
'reductions': reductions,
'reductions_en': reductions_en,
'cancellations': cancellations,
})
return yogas
def assess_pmc_strength(planets: Dict, sun_degree: float = None) -> Dict:
"""
评估Pancha Mahapurusha Yoga总体强度。
Returns:
PMC评估结果
"""
yogas = detect_pancha_mahapurusha(planets, sun_degree)
valid = [y for y in yogas if y['is_valid']]
invalid = [y for y in yogas if not y['is_valid']]
reduced = [y['name'] for y in valid if y['strength'] == 'reduced']
d9_weakened = [y['name'] for y in valid if y.get('d9_dignity_modifier') == 'weakened']
if len(valid) >= 2:
assessment = "多重大王瑜伽 — 命主在不同领域均有杰出天赋"
elif len(valid) == 1:
assessment = f"{valid[0]['name']} — 命主在特定领域有卓越潜力"
else:
assessment = "无Pancha Mahapurusha Yoga"
return {
'method': 'Pancha Mahapurusha Yoga 完整检测 v1.0',
'yogas': yogas,
'valid_count': len(valid),
'reduced': reduced,
'd9_weakened': d9_weakened,
'invalid_count': len(invalid),
'assessment': assessment,
}