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
This commit is contained in:
Jesse_Chen
2026-10-03 16:18:06 +08:00
co-authored by Claude Opus 5.5
parent 52b3061fe5
commit bc03bba26b
4 changed files with 117 additions and 47 deletions
+2 -1
View File
@@ -7127,7 +7127,8 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM
sun_lon = (planets.get('Sun') or {}).get('lon')
for y in pancha_mahapurusha_rows(planets, sun_degree=sun_lon):
yogas.append({'name': y['name'], 'planets': y['planets'], 'category': 'PMC',
'reductions': y['reductions']})
'reductions': y['reductions'],
'd9_dignity_modifier': y.get('d9_dignity_modifier')})
except Exception as e:
import logging
logging.warning(f"[api_server] pancha_mahapurusha detection failed: {e}")
+4 -2
View File
@@ -15,8 +15,9 @@ English reader edition).
only the cancellation conditions that hold are listed, never called Raja Yoga.
- Pancha Mahapurusha (BUG-1207, 甲5): `pancha_mahapurusha.detect_pancha_mahapurusha`;
kendras from the Lagna only, retrograde does not cancel, combustion / malefic
conjunction or aspect only mark the yoga `reduced` (部分减弱), D9 debilitation
still cancels it (current behaviour; not in the ruling, asked next round).
conjunction or aspect only mark the yoga `reduced` (部分减弱); D9 debilitation
only weakens it (round-4 ruling 2026-10-03 Q1, BUG-1213): the yoga stays formed
with `d9_dignity_modifier: "weakened"` and the reason 「D9 落陷,兑现减弱」.
- Kemadruma (BUG-1206, 甲7): `yoga_expansion.detect_kemadruma`; the 2nd / 12th
from the Moon count only Mars, Mercury, Jupiter, Venus, Saturn, and there is no
kendra cancellation (planets in kendras, including only Rahu / Ketu there, do
@@ -185,6 +186,7 @@ def pancha_mahapurusha_rows(planets: Dict[str, Any], sun_degree: Optional[float]
'combination_en': combination_en,
'reductions': reductions,
'reductions_en': reductions_en,
'd9_dignity_modifier': item.get('d9_dignity_modifier'),
'effects': ['卓越才能', '领域领军', '人格魅力'],
'effects_en': ['Outstanding abilities', "Leader in one's field", 'Personal charisma'],
'strength': '部分减弱' if reductions else '强',
+58 -28
View File
@@ -12,7 +12,9 @@ Pancha Mahapurusha Yoga(五王瑜伽)完整检测模块 v1.0
- Shasha Yoga (Saturn)
破格口径按占星师定稿 2026-10-03 甲5(BUG-1207):逆行不破格;燃烧、与凶星
同宫或被照只记「部分减弱」;D9 落陷判不成立(现状,待问)。本模块是五大人格的
同宫或被照只记「部分减弱」。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。
"""
@@ -108,6 +110,17 @@ def _malefic_aspectors(planet_sign: str, all_planets: Dict, planet: str = None)
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'):
@@ -127,26 +140,31 @@ PLANET_ZH = {'Sun': '太阳', 'Mars': '火星', 'Mercury': '水星', 'Jupiter':
'Saturn': '土星', 'Rahu': '罗睺', 'Ketu': '计都'}
def detect_pancha_mahapurusha(planets: Dict, sun_degree: float = None) -> List[Dict]:
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):
占星师定稿 2026-10-03 甲5(BUG-1207)与第四轮问 1(BUG-1213):
- 只从命宫(Lagna)角宫计算:行星在上升起第 1/4/7/10 宫且入庙或入旺;
这一条不满足才是 `not_formed`;
- 逆行不破格;
- 燃烧不破格,只记「部分减弱」(`strength: reduced`);
- 与凶星同宫或被凶星照只降低强度,不消灭格局;
- D9 落陷判不成立(`is_valid: False`)保留现状:只在传入 `navamsa_sign` 时检查,
产品路径不传;定稿未提及,待问占星师。
- 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列表(含 strength: full / reduced / cancelled)
检测到的PMC Yoga列表(含 status: formed / not_formed,strength: full / reduced)
"""
yogas = []
sun_deg = sun_degree
@@ -161,10 +179,23 @@ def detect_pancha_mahapurusha(planets: Dict, sun_degree: float = None) -> List[D
sign = pd.get('sign', '')
house = pd.get('house', 0)
# 基本条件:在Kendra + own/exalted
if house not in KENDRA_HOUSES:
continue
if not _is_exalted_or_own(planet, sign):
# 基本条件:在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]
@@ -173,7 +204,6 @@ def detect_pancha_mahapurusha(planets: Dict, sun_degree: float = None) -> List[D
cancellations = []
reductions = []
reductions_en = []
is_valid = True
# 1. 燃烧:部分减弱(容许度表与 jyotish_engine.COMBUSTION_ORBS 一致,
# tests/test_combustion_orb_tables_agree.py)
@@ -197,27 +227,28 @@ def detect_pancha_mahapurusha(planets: Dict, sun_degree: float = None) -> List[D
reductions.append(f"受凶星照:{'、'.join(PLANET_ZH.get(m, m) for m in aspectors)}")
reductions_en.append(f"aspected by malefic(s): {', '.join(aspectors)}")
# 4. Navamsa弱势(D9中落陷):不成立。现状保留——只在调用方给出
# `navamsa_sign` 时检查;产品路径(chart.yogas、规则引擎、报告)都不传,
# 所以这一条目前不生效。定稿未提及,记为下轮问题,不在本单启用。
navamsa_sign = pd.get('navamsa_sign', '')
if navamsa_sign in OWN_SIGNS.get(planet, set()) or (planet in EXALTATION and EXALTATION[planet][0] == navamsa_sign):
pass # D9 also strong → yoga reinforced
elif navamsa_sign:
deb_sign_idx = (SIGNS.index(EXALTATION[planet][0]) + 6) % 12 if planet in EXALTATION else -1
if deb_sign_idx >= 0 and navamsa_sign == SIGNS[deb_sign_idx]:
cancellations.append(f"{planet}在Navamsa落陷 — Yoga根基不稳")
is_valid = False
# 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 = 'cancelled' if not is_valid else ('reduced' if reductions else 'full')
strength = 'reduced' if reductions else 'full'
yogas.append({
'name': yoga_name,
'planet': planet,
'sign': sign,
'house': house,
'is_valid': is_valid,
'status': 'formed',
'is_valid': True,
'strength': strength,
'strength_cn': {'full': '完整', 'reduced': '部分减弱', 'cancelled': '不成立'}[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,
@@ -239,13 +270,11 @@ def assess_pmc_strength(planets: Dict, sun_degree: float = None) -> Dict:
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']} — 命主在特定领域有卓越潜力"
elif len(invalid) > 0:
st = [y['planet'] for y in invalid]
assessment = f"瑜伽潜质存在但因{', '.join(st)}在 D9 落陷而不成立"
else:
assessment = "无Pancha Mahapurusha Yoga"
@@ -254,6 +283,7 @@ def assess_pmc_strength(planets: Dict, sun_degree: float = None) -> Dict:
'yogas': yogas,
'valid_count': len(valid),
'reduced': reduced,
'd9_weakened': d9_weakened,
'invalid_count': len(invalid),
'assessment': assessment,
}
+53 -16
View File
@@ -2,7 +2,7 @@
`scripts/pancha_mahapurusha.py` is the only detector: kendras from the Lagna,
retrograde does not cancel, combustion and malefic conjunction / aspect only
reduce the yoga, D9 debilitation still cancels it (current behaviour). The
reduce the yoga, D9 debilitation only weakens it (round-4 ruling Q1, BUG-1213). The
data-driven `mahapurusha_*`, `*_own`, `moolatrikona_*`, `hamsa_trikona` and the
BVR Maalavya duplicate are `reference_only`, and `yogas_doshas` keeps its old
aggregate for reference only, so a yoga the main detector rejects cannot come
@@ -69,12 +69,42 @@ def test_malefic_aspect_reduces() -> None:
assert _one({"Jupiter": _row("Cancer", 5.0), "Ketu": _row("Capricorn", 10.0)}, "Hamsa Yoga")["strength"] == "full"
def test_d9_debilitation_still_cancels_when_the_caller_gives_d9() -> None:
# Current behaviour kept (ruling silent): checked only when `navamsa_sign`
# is supplied; product paths do not supply it.
hamsa = _one({"Jupiter": _row("Cancer", 21.0, navamsa_sign="Capricorn")}, "Hamsa Yoga")
assert hamsa["is_valid"] is False and hamsa["strength"] == "cancelled"
assert _one({"Jupiter": _row("Cancer", 21.0)}, "Hamsa Yoga")["is_valid"] is True
def test_d9_debilitation_only_weakens() -> None:
# 原值:传 navamsa_sign 落陷 → is_valid False、strength cancelled(不传则不检查)
# 新值:formed 不变,d9_dignity_modifier = weakened,原因「D9 落陷,兑现减弱」;
# 不传 navamsa_sign 时由经度推出 D9(巨蟹 21° 的 D9 是摩羯)
# 原因:占星师第四轮裁定 2026-10-03 问 1(BUG-1213)
for row in (_row("Cancer", 21.0, navamsa_sign="Capricorn"), _row("Cancer", 21.0)):
hamsa = _one({"Jupiter": row}, "Hamsa Yoga")
assert (hamsa["status"], hamsa["is_valid"], hamsa["strength"]) == ("formed", True, "reduced")
assert hamsa["d9_dignity_modifier"] == "weakened" and hamsa["d9_sign"] == "Capricorn"
assert hamsa["reductions"] == ["D9 落陷,兑现减弱"]
strong = _one({"Jupiter": _row("Cancer", 5.0)}, "Hamsa Yoga")
assert strong["d9_dignity_modifier"] is None and strong["strength"] == "full"
def test_d1_condition_not_met_is_not_formed() -> None:
# Jupiter exalted but in the 9th from a Pisces Lagna (trikona, not kendra).
rows = detect_pancha_mahapurusha({"Jupiter": _row("Cancer", 5.0, asc="Pisces")}, include_not_formed=True)
hamsa = next(item for item in rows if item["planet"] == "Jupiter")
assert (hamsa["status"], hamsa["is_valid"]) == ("not_formed", False)
# Kendra but neither own nor exalted.
rows = detect_pancha_mahapurusha({"Jupiter": _row("Leo", 5.0, asc="Leo")}, include_not_formed=True)
assert next(item for item in rows if item["planet"] == "Jupiter")["status"] == "not_formed"
assert detect_pancha_mahapurusha({"Jupiter": _row("Leo", 5.0, asc="Leo")}) == []
def test_d9_weakened_yoga_reaches_the_card_and_the_report_rows_alike() -> None:
planets = {"Jupiter": _row("Cancer", 21.0), "Moon": _row("Taurus", 3.0)}
rows = pancha_mahapurusha_rows(planets)
assert rows[0]["d9_dignity_modifier"] == "weakened"
assert "D9 落陷,兑现减弱" in rows[0]["combination"]
engine_rows = [row for row in YogaEngine(str(RULES)).detect(planets, "Cancer") if row.get("category") == "mahapurusha"]
assert engine_rows == rows
chart_rows = JyotishAPIHandler._detect_yogas(None, planets, SIGNS.index("Cancer"))
entry = _compact_yoga_entry(next(row for row in chart_rows if row.get("category") == "PMC"))
layer = build_traditional_yogas({"raman_support": {"yogas": {"status": "executed", "hits": [], "ungoverned_detected": [entry]}}})
assert layer["names"] == ["Hamsa Yoga(Jupiter;部分减弱:D9 落陷,兑现减弱)"]
def test_reference_only_rules_never_run() -> None:
@@ -85,14 +115,17 @@ def test_reference_only_rules_never_run() -> None:
def test_a_rejected_yoga_does_not_come_back_by_another_path() -> None:
# Jupiter exalted in the Lagna (Cancer) but debilitated in D9; also in a
# trikona, which the retired hamsa_trikona rule used to accept.
planets = {"Jupiter": _row("Cancer", 21.0, navamsa_sign="Capricorn"), "Moon": _row("Taurus", 3.0), "Sun": _row("Aries", 10.0),
"Mars": _row("Gemini", 1.0), "Mercury": _row("Pisces", 2.0), "Venus": _row("Aries", 2.0),
"Saturn": _row("Leo", 2.0), "Rahu": _row("Virgo", 2.0), "Ketu": _row("Pisces", 2.0)}
engine_rows = YogaEngine(str(RULES)).detect(planets, "Cancer")
# 原值:木星在上升巨蟹入旺但 D9 落陷(被判不成立)作为「被拒格局」
# 新值:木星入旺巨蟹但在双鱼上升的第 9 宫(三方宫,非角宫),D1 条件不成立
# 原因:第四轮问 1(BUG-1213)D9 落陷不再拒绝格局;被拒样例改为 D1 不成立,
# 仍是已退役的 hamsa_trikona 规则曾接受的三方宫
asc = "Pisces"
planets = {"Jupiter": _row("Cancer", 5.0, asc=asc), "Moon": _row("Taurus", 3.0, asc=asc), "Sun": _row("Aries", 10.0, asc=asc),
"Mars": _row("Gemini", 1.0, asc=asc), "Mercury": _row("Aquarius", 2.0, asc=asc), "Venus": _row("Aries", 2.0, asc=asc),
"Saturn": _row("Leo", 2.0, asc=asc), "Rahu": _row("Virgo", 2.0, asc=asc), "Ketu": _row("Pisces", 2.0, asc=asc)}
engine_rows = YogaEngine(str(RULES)).detect(planets, asc)
assert not [row for row in engine_rows if "Hamsa" in row["name"] or "汉萨" in row.get("name_cn", "")]
chart_rows = JyotishAPIHandler._detect_yogas(None, planets, SIGNS.index("Cancer"))
chart_rows = JyotishAPIHandler._detect_yogas(None, planets, SIGNS.index(asc))
assert not [row for row in chart_rows if "Hamsa" in row["name"]]
merged = merge_detected_yogas(
[entry for entry in map(_compact_yoga_entry, chart_rows) if entry],
@@ -100,13 +133,17 @@ def test_a_rejected_yoga_does_not_come_back_by_another_path() -> None:
)
layer = build_traditional_yogas({"raman_support": {"yogas": {"status": "executed", "hits": [], "ungoverned_detected": merged}}})
assert not [name for name in layer["names"] if "Hamsa" in name]
aggregate = calc_all_yogas_doshas(planets, {}, "Cancer", "Moon", "Taurus", 11, 12, 10, "Gemini")
aggregate = calc_all_yogas_doshas(planets, {}, asc, "Moon", "Taurus", 11, 12, 10, "Gemini")
assert aggregate["pancha_mahapurusha"]["status"] == "reference_only"
assert aggregate["pancha_mahapurusha"]["yogas"] == []
def test_reduced_yoga_reaches_the_card_with_its_reasons() -> None:
planets = {"Jupiter": _row("Cancer", 5.0), "Saturn": _row("Capricorn", 10.0)}
# 原值:土星摩羯 10°(D9 白羊,落陷;旧规则只在给 navamsa_sign 时检查)
# 新值:土星摩羯 2°(D9 摩羯,本宫),断言不变
# 原因:BUG-1213 起 D9 由经度推出,摩羯 10° 的萨萨会多一条「D9 落陷」减弱;
# 本用例只测凶星照的原因上卡,换成 D9 不落陷的度数
planets = {"Jupiter": _row("Cancer", 5.0), "Saturn": _row("Capricorn", 2.0)}
all_rows = pancha_mahapurusha_rows(planets)
engine_rows = [row for row in YogaEngine(str(RULES)).detect(planets, "Cancer") if row.get("category") == "mahapurusha"]
assert engine_rows == all_rows