fix(yoga): Pancha Mahapurusha has one main detector; combust/afflicted only reduced (T1, BUG-1207)

Astrologer ruling 甲5. scripts/pancha_mahapurusha.py is the only detector: kendras from
the Lagna; retrograde no longer cancels; combustion (shared orb table), malefic
conjunction and malefic graha drishti (Sun 7, Mars 4/7/8, Saturn 3/7/10) mark the
yoga reduced with reasons; D9 debilitation still cancels when navamsa_sign is given
(current behaviour, ruling silent, asked next round). _detect_yogas now passes the
Sun's longitude (combustion was never checked). mahapurusha_*, *_own, moolatrikona_*,
hamsa_trikona (Jupiter 1/5/9, not classical) and bvr_maalavya_yoga are reference_only;
yogas_doshas.calc_pancha_mahapurusha_yoga is kept as reference_yogas only. The rule
engine appends main-detector rows; cmd_yoga rows now carry lon + retrograde so the
report and card get the same combustion verdict. Card label lists 部分减弱 reasons.

Changed assertions (three columns in PROGRESS): self-conjunction tests (malefic in
same sign: invalid -> reduced), run_all t31/t32 (combust/retro: invalid -> reduced /
full), run_all t54 (evaluated rules >= 400 -> evaluated + reference_only >= 400).

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 12:26:22 +08:00
co-authored by Claude Opus 5.5
parent 6916bbaecc
commit de79390175
9 changed files with 421 additions and 69 deletions
+51 -17
View File
@@ -194,7 +194,9 @@
"strength_en": "strong",
"enabled": true,
"source_reference": "BVR-22: Mars in own/exaltation in Kendra",
"bvr_chapter": "Chapter 3"
"bvr_chapter": "Chapter 3",
"reference_only": true,
"reference_only_reason": "Astrologer rulings 2026-10-03 (甲5, BUG-1207): Pancha Mahapurusha has one main detector, scripts/pancha_mahapurusha.py (kendras from the Lagna only; retrograde does not cancel; combustion, malefic conjunction or aspect only reduce strength), emitted via scripts/main_yoga_detectors.py. This rule kept for reference only (hamsa_trikona: Jupiter in 1/5/9 is not classical)."
},
{
"id": "mahapurusha_mars_own",
@@ -240,7 +242,9 @@
"enabled": true,
"dedup_key": "Ruchaka Yoga",
"source_reference": "BVR-22: Mars in own/exaltation in Kendra",
"bvr_chapter": "Chapter 3"
"bvr_chapter": "Chapter 3",
"reference_only": true,
"reference_only_reason": "Astrologer rulings 2026-10-03 (甲5, BUG-1207): Pancha Mahapurusha has one main detector, scripts/pancha_mahapurusha.py (kendras from the Lagna only; retrograde does not cancel; combustion, malefic conjunction or aspect only reduce strength), emitted via scripts/main_yoga_detectors.py. This rule kept for reference only (hamsa_trikona: Jupiter in 1/5/9 is not classical)."
},
{
"id": "mahapurusha_mercury",
@@ -285,7 +289,9 @@
"strength_en": "strong",
"enabled": true,
"source_reference": "BVR-23: Mercury in own/exaltation in Kendra",
"bvr_chapter": "Chapter 3"
"bvr_chapter": "Chapter 3",
"reference_only": true,
"reference_only_reason": "Astrologer rulings 2026-10-03 (甲5, BUG-1207): Pancha Mahapurusha has one main detector, scripts/pancha_mahapurusha.py (kendras from the Lagna only; retrograde does not cancel; combustion, malefic conjunction or aspect only reduce strength), emitted via scripts/main_yoga_detectors.py. This rule kept for reference only (hamsa_trikona: Jupiter in 1/5/9 is not classical)."
},
{
"id": "mahapurusha_mercury_own",
@@ -331,7 +337,9 @@
"enabled": true,
"dedup_key": "Bhadra Yoga",
"source_reference": "BVR-23: Mercury in own/exaltation in Kendra",
"bvr_chapter": "Chapter 3"
"bvr_chapter": "Chapter 3",
"reference_only": true,
"reference_only_reason": "Astrologer rulings 2026-10-03 (甲5, BUG-1207): Pancha Mahapurusha has one main detector, scripts/pancha_mahapurusha.py (kendras from the Lagna only; retrograde does not cancel; combustion, malefic conjunction or aspect only reduce strength), emitted via scripts/main_yoga_detectors.py. This rule kept for reference only (hamsa_trikona: Jupiter in 1/5/9 is not classical)."
},
{
"id": "mahapurusha_jupiter",
@@ -376,7 +384,9 @@
"strength_en": "strong",
"enabled": true,
"source_reference": "BVR-19: Jupiter in own/exaltation in Kendra",
"bvr_chapter": "Chapter 3"
"bvr_chapter": "Chapter 3",
"reference_only": true,
"reference_only_reason": "Astrologer rulings 2026-10-03 (甲5, BUG-1207): Pancha Mahapurusha has one main detector, scripts/pancha_mahapurusha.py (kendras from the Lagna only; retrograde does not cancel; combustion, malefic conjunction or aspect only reduce strength), emitted via scripts/main_yoga_detectors.py. This rule kept for reference only (hamsa_trikona: Jupiter in 1/5/9 is not classical)."
},
{
"id": "mahapurusha_jupiter_own",
@@ -422,7 +432,9 @@
"enabled": true,
"dedup_key": "Hamsa Yoga",
"source_reference": "BVR-19: Jupiter in own/exaltation in Kendra",
"bvr_chapter": "Chapter 3"
"bvr_chapter": "Chapter 3",
"reference_only": true,
"reference_only_reason": "Astrologer rulings 2026-10-03 (甲5, BUG-1207): Pancha Mahapurusha has one main detector, scripts/pancha_mahapurusha.py (kendras from the Lagna only; retrograde does not cancel; combustion, malefic conjunction or aspect only reduce strength), emitted via scripts/main_yoga_detectors.py. This rule kept for reference only (hamsa_trikona: Jupiter in 1/5/9 is not classical)."
},
{
"id": "mahapurusha_venus",
@@ -467,7 +479,9 @@
"strength_en": "strong",
"enabled": true,
"source_reference": "BVR-20: Venus in own/exaltation in Kendra",
"bvr_chapter": "Chapter 3"
"bvr_chapter": "Chapter 3",
"reference_only": true,
"reference_only_reason": "Astrologer rulings 2026-10-03 (甲5, BUG-1207): Pancha Mahapurusha has one main detector, scripts/pancha_mahapurusha.py (kendras from the Lagna only; retrograde does not cancel; combustion, malefic conjunction or aspect only reduce strength), emitted via scripts/main_yoga_detectors.py. This rule kept for reference only (hamsa_trikona: Jupiter in 1/5/9 is not classical)."
},
{
"id": "mahapurusha_venus_own",
@@ -513,7 +527,9 @@
"enabled": true,
"dedup_key": "Malavya Yoga",
"source_reference": "BVR-20: Venus in own/exaltation in Kendra",
"bvr_chapter": "Chapter 3"
"bvr_chapter": "Chapter 3",
"reference_only": true,
"reference_only_reason": "Astrologer rulings 2026-10-03 (甲5, BUG-1207): Pancha Mahapurusha has one main detector, scripts/pancha_mahapurusha.py (kendras from the Lagna only; retrograde does not cancel; combustion, malefic conjunction or aspect only reduce strength), emitted via scripts/main_yoga_detectors.py. This rule kept for reference only (hamsa_trikona: Jupiter in 1/5/9 is not classical)."
},
{
"id": "mahapurusha_saturn",
@@ -558,7 +574,9 @@
"strength_en": "strong",
"enabled": true,
"source_reference": "BVR-21: Saturn in own/exaltation in Kendra",
"bvr_chapter": "Chapter 3"
"bvr_chapter": "Chapter 3",
"reference_only": true,
"reference_only_reason": "Astrologer rulings 2026-10-03 (甲5, BUG-1207): Pancha Mahapurusha has one main detector, scripts/pancha_mahapurusha.py (kendras from the Lagna only; retrograde does not cancel; combustion, malefic conjunction or aspect only reduce strength), emitted via scripts/main_yoga_detectors.py. This rule kept for reference only (hamsa_trikona: Jupiter in 1/5/9 is not classical)."
},
{
"id": "mahapurusha_saturn_own",
@@ -604,7 +622,9 @@
"enabled": true,
"dedup_key": "Sasa Yoga",
"source_reference": "BVR-21: Saturn in own/exaltation in Kendra",
"bvr_chapter": "Chapter 3"
"bvr_chapter": "Chapter 3",
"reference_only": true,
"reference_only_reason": "Astrologer rulings 2026-10-03 (甲5, BUG-1207): Pancha Mahapurusha has one main detector, scripts/pancha_mahapurusha.py (kendras from the Lagna only; retrograde does not cancel; combustion, malefic conjunction or aspect only reduce strength), emitted via scripts/main_yoga_detectors.py. This rule kept for reference only (hamsa_trikona: Jupiter in 1/5/9 is not classical)."
},
{
"id": "hamsa_trikona",
@@ -649,7 +669,9 @@
"enabled": true,
"dedup_key": "Hamsa Yoga",
"source_reference": "BVR-19: Jupiter in own/exaltation in Kendra",
"bvr_chapter": "Chapter 3"
"bvr_chapter": "Chapter 3",
"reference_only": true,
"reference_only_reason": "Astrologer rulings 2026-10-03 (甲5, BUG-1207): Pancha Mahapurusha has one main detector, scripts/pancha_mahapurusha.py (kendras from the Lagna only; retrograde does not cancel; combustion, malefic conjunction or aspect only reduce strength), emitted via scripts/main_yoga_detectors.py. This rule kept for reference only (hamsa_trikona: Jupiter in 1/5/9 is not classical)."
},
{
"id": "moolatrikona_mars",
@@ -694,7 +716,9 @@
"strength_en": "strong",
"enabled": true,
"source_reference": "BVR-22: Mars in own/exaltation in Kendra",
"bvr_chapter": "Chapter 3"
"bvr_chapter": "Chapter 3",
"reference_only": true,
"reference_only_reason": "Astrologer rulings 2026-10-03 (甲5, BUG-1207): Pancha Mahapurusha has one main detector, scripts/pancha_mahapurusha.py (kendras from the Lagna only; retrograde does not cancel; combustion, malefic conjunction or aspect only reduce strength), emitted via scripts/main_yoga_detectors.py. This rule kept for reference only (hamsa_trikona: Jupiter in 1/5/9 is not classical)."
},
{
"id": "moolatrikona_mercury",
@@ -739,7 +763,9 @@
"strength_en": "strong",
"enabled": true,
"source_reference": "BVR-23: Mercury in own/exaltation in Kendra",
"bvr_chapter": "Chapter 3"
"bvr_chapter": "Chapter 3",
"reference_only": true,
"reference_only_reason": "Astrologer rulings 2026-10-03 (甲5, BUG-1207): Pancha Mahapurusha has one main detector, scripts/pancha_mahapurusha.py (kendras from the Lagna only; retrograde does not cancel; combustion, malefic conjunction or aspect only reduce strength), emitted via scripts/main_yoga_detectors.py. This rule kept for reference only (hamsa_trikona: Jupiter in 1/5/9 is not classical)."
},
{
"id": "moolatrikona_jupiter",
@@ -784,7 +810,9 @@
"strength_en": "strong",
"enabled": true,
"source_reference": "BVR-19: Jupiter in own/exaltation in Kendra",
"bvr_chapter": "Chapter 3"
"bvr_chapter": "Chapter 3",
"reference_only": true,
"reference_only_reason": "Astrologer rulings 2026-10-03 (甲5, BUG-1207): Pancha Mahapurusha has one main detector, scripts/pancha_mahapurusha.py (kendras from the Lagna only; retrograde does not cancel; combustion, malefic conjunction or aspect only reduce strength), emitted via scripts/main_yoga_detectors.py. This rule kept for reference only (hamsa_trikona: Jupiter in 1/5/9 is not classical)."
},
{
"id": "moolatrikona_venus",
@@ -829,7 +857,9 @@
"strength_en": "strong",
"enabled": true,
"source_reference": "BVR-20: Venus in own/exaltation in Kendra",
"bvr_chapter": "Chapter 3"
"bvr_chapter": "Chapter 3",
"reference_only": true,
"reference_only_reason": "Astrologer rulings 2026-10-03 (甲5, BUG-1207): Pancha Mahapurusha has one main detector, scripts/pancha_mahapurusha.py (kendras from the Lagna only; retrograde does not cancel; combustion, malefic conjunction or aspect only reduce strength), emitted via scripts/main_yoga_detectors.py. This rule kept for reference only (hamsa_trikona: Jupiter in 1/5/9 is not classical)."
},
{
"id": "moolatrikona_saturn",
@@ -874,7 +904,9 @@
"strength_en": "strong",
"enabled": true,
"source_reference": "BVR-21: Saturn in own/exaltation in Kendra",
"bvr_chapter": "Chapter 3"
"bvr_chapter": "Chapter 3",
"reference_only": true,
"reference_only_reason": "Astrologer rulings 2026-10-03 (甲5, BUG-1207): Pancha Mahapurusha has one main detector, scripts/pancha_mahapurusha.py (kendras from the Lagna only; retrograde does not cancel; combustion, malefic conjunction or aspect only reduce strength), emitted via scripts/main_yoga_detectors.py. This rule kept for reference only (hamsa_trikona: Jupiter in 1/5/9 is not classical)."
},
{
"id": "gajakesari",
@@ -13784,7 +13816,9 @@
"strength": "strong",
"enabled": true,
"draft": false,
"evidence_note": "Source: B.V. Raman, BVR: BVR-20"
"evidence_note": "Source: B.V. Raman, BVR: BVR-20",
"reference_only": true,
"reference_only_reason": "Astrologer rulings 2026-10-03 (甲5, BUG-1207): Pancha Mahapurusha has one main detector, scripts/pancha_mahapurusha.py (kendras from the Lagna only; retrograde does not cancel; combustion, malefic conjunction or aspect only reduce strength), emitted via scripts/main_yoga_detectors.py. This BVR duplicate is kept for reference only."
},
{
"id": "bvr_maathru_saapa_sutakshaya_yoga",
+7 -5
View File
@@ -7118,11 +7118,13 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM
yogas = []
KENDRA = {1,4,7,10}
try:
from pancha_mahapurusha import detect_pancha_mahapurusha
pmc = detect_pancha_mahapurusha(planets)
for y in pmc:
if y['is_valid']:
yogas.append({'name': y['name'], 'planets': [y['planet']], 'category': 'PMC'})
# BUG-1207: the one Pancha Mahapurusha detector, with the Sun's
# longitude for combustion; reduced yogas stay formed.
from main_yoga_detectors import pancha_mahapurusha_rows
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']})
except Exception as e:
import logging
logging.warning(f"[api_server] pancha_mahapurusha detection failed: {e}")
+5
View File
@@ -12536,7 +12536,12 @@ def cmd_yoga(args):
"house": pd["house"],
"degree": pd.get("degree_in_sign", pd.get("degree")),
}
# Main yoga detectors (BUG-1207) read the longitude and the
# retrograde flag (combustion orb), as the consultation path does.
if pd.get("retrograde") is not None:
planets[pname]["retrograde"] = bool(pd.get("retrograde"))
if pd.get("degree_raw") is not None:
planets[pname]["lon"] = pd.get("degree_raw")
planet_lons_for_varga[pname] = pd.get("degree_raw")
try:
from varga import calc_all_vargas
+60 -8
View File
@@ -13,6 +13,10 @@ English reader edition).
- Neecha Bhanga (BUG-1205, 乙14): `yogas_doshas.calc_nicha_bhanga_raj_yoga`;
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).
- 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
@@ -29,9 +33,11 @@ from __future__ import annotations
from typing import Any, Dict, List, Optional
try: # scripts/ on sys.path
from pancha_mahapurusha import _longitude, detect_pancha_mahapurusha
from yoga_expansion import detect_kemadruma
from yogas_doshas import calc_nicha_bhanga_raj_yoga
except ImportError: # package import (scripts.main_yoga_detectors)
from scripts.pancha_mahapurusha import _longitude, detect_pancha_mahapurusha
from scripts.yoga_expansion import detect_kemadruma
from scripts.yogas_doshas import calc_nicha_bhanga_raj_yoga
@@ -55,13 +61,11 @@ NEECHA_BHANGA_NOTE_EN = 'Conditions only; no stand-alone conclusion'
def _lon(row: Any) -> Optional[float]:
if not isinstance(row, dict):
return None
for key in ('lon', 'degree_raw', 'longitude'):
value = row.get(key)
if isinstance(value, (int, float)):
return float(value) % 360.0
return None
"""Sidereal longitude; same reading as the Pancha Mahapurusha detector
(`lon` / `degree_raw` / `longitude`, else `degree` as a longitude when
>= 30 or as a degree in `sign`), so the report rows (degree in sign) and
the API rows (longitudes) give the same D9 and the same verdict."""
return _longitude(row) if isinstance(row, dict) else None
def navamsa_sign(lon: float) -> str:
@@ -144,6 +148,54 @@ def neecha_bhanga_rows(planets: Dict[str, Any]) -> List[Dict[str, Any]]:
return rows
# Names as the rule registry and the card closed list spell them.
MAHAPURUSHA_ROWS = {
'Mars': ('Ruchaka Yoga', '鲁查卡格局'),
'Mercury': ('Bhadra Yoga', '巴德拉格局'),
'Jupiter': ('Hamsa Yoga', '汉萨格局'),
'Venus': ('Malavya Yoga', '马拉维亚格局'),
'Saturn': ('Sasa Yoga', '萨萨格局'),
}
EXALTATION_SIGN = {'Mars': 'Capricorn', 'Mercury': 'Virgo', 'Jupiter': 'Cancer', 'Venus': 'Pisces', 'Saturn': 'Libra'}
def pancha_mahapurusha_rows(planets: Dict[str, Any], sun_degree: Optional[float] = None) -> List[Dict[str, Any]]:
"""Formed Pancha Mahapurusha yogas (full or reduced) from the main detector."""
rows: List[Dict[str, Any]] = []
for item in detect_pancha_mahapurusha(planets or {}, sun_degree):
if not item.get('is_valid'):
continue
planet = item['planet']
name, name_cn = MAHAPURUSHA_ROWS[planet]
sign = item.get('sign')
exalted = EXALTATION_SIGN.get(planet) == sign
reductions = list(item.get('reductions') or [])
reductions_en = list(item.get('reductions_en') or [])
combination = f"{_planet_zh(planet)}在第{item.get('house')}宫{_sign_zh(sign)}({'入旺' if exalted else '入庙'})"
combination_en = f"{planet} in house {item.get('house')}, {sign} ({'exalted' if exalted else 'own sign'})"
if reductions:
combination += ';部分减弱:' + ';'.join(reductions)
combination_en += '; partly weakened: ' + '; '.join(reductions_en)
rows.append({
'name': name,
'name_cn': name_cn,
'category': 'mahapurusha',
'planets': [planet],
'combination': combination,
'combination_en': combination_en,
'reductions': reductions,
'reductions_en': reductions_en,
'effects': ['卓越才能', '领域领军', '人格魅力'],
'effects_en': ['Outstanding abilities', "Leader in one's field", 'Personal charisma'],
'strength': '部分减弱' if reductions else '强',
'strength_en': 'partly weakened' if reductions else 'strong',
'source': 'BPHS',
'rule_id': f'main_detector:pancha_mahapurusha:{planet.lower()}',
'detector': 'pancha_mahapurusha.detect_pancha_mahapurusha',
})
return rows
def kemadruma_rows(planets: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Kemadruma from `yoga_expansion.detect_kemadruma` (no kendra cancellation)."""
result = detect_kemadruma(planets or {})
@@ -170,4 +222,4 @@ def kemadruma_rows(planets: Dict[str, Any]) -> List[Dict[str, Any]]:
def main_detector_rows(planets: Dict[str, Any], ascendant: Optional[str] = None) -> List[Dict[str, Any]]:
"""Every main-detector row for one D1 chart, in a fixed order."""
return kemadruma_rows(planets) + neecha_bhanga_rows(planets)
return pancha_mahapurusha_rows(planets) + kemadruma_rows(planets) + neecha_bhanga_rows(planets)
+93 -28
View File
@@ -11,7 +11,10 @@ Pancha Mahapurusha Yoga(五王瑜伽)完整检测模块 v1.0
- Malavya Yoga (Venus)
- Shasha Yoga (Saturn)
含完整的燃烧(Combustion)/逆行(Retrograde)/受克(Affliction)失效条件检测
破格口径按占星师定稿 2026-10-03 甲5(BUG-1207):逆行不破格;燃烧、与凶星
同宫或被照只记「部分减弱」;D9 落陷判不成立(现状,待问)。本模块是五大人格的
唯一主检测器,`yoga_rules.json` 同名规则与 `yogas_doshas.calc_pancha_mahapurusha_yoga`
为 reference_only。
"""
from typing import Dict, List, Optional
@@ -73,7 +76,7 @@ def _is_combust(planet: str, planet_degree: float, sun_degree: float, retrograde
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:
@@ -84,21 +87,73 @@ def _has_malefic_aspect(planet_sign: str, all_planets: Dict, planet: str = None)
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 _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) -> List[Dict]:
"""
检测所有Pancha Mahapurusha Yoga,含失效条件。
检测所有Pancha Mahapurusha Yoga(主检测器)。
占星师定稿 2026-10-03 甲5(BUG-1207):
- 只从命宫(Lagna)角宫计算:行星在上升起第 1/4/7/10 宫且入庙或入旺;
- 逆行不破格;
- 燃烧不破格,只记「部分减弱」(`strength: reduced`);
- 与凶星同宫或被凶星照只降低强度,不消灭格局;
- D9 落陷判不成立(`is_valid: False`)保留现状:只在传入 `navamsa_sign` 时检查,
产品路径不传;定稿未提及,待问占星师。
Args:
planets: {planet: {'sign': str, 'house': int, 'degree': float, ...}}
sun_degree: 太阳黄道经度(用于燃烧检测)
planets: {planet: {'sign': str, 'house': int, 'lon'|'degree': float, 'navamsa_sign'?: str}}
`house` 从上升数;经度取 `lon` / `degree_raw` / `longitude`,否则 `degree`
(≥30 视为经度,否则按星座内度数)。
sun_degree: 太阳黄道经度(燃烧检测);缺省时取 planets['Sun'] 的经度。
Returns:
检测到的PMC Yoga列表
检测到的PMC Yoga列表(含 strength: full / reduced / cancelled)
"""
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_PLANETS:
for planet in MAHAPURUSHA_ORDER:
pd = planets.get(planet)
if not pd:
continue
@@ -115,49 +170,57 @@ def detect_pancha_mahapurusha(planets: Dict, sun_degree: float = None) -> List[D
yoga_name = MAHAPURUSHA_NAMES[planet]
effect = MAHAPURUSHA_EFFECTS.get(yoga_name.replace(" Yoga", ""), "")
# 检查失效条件
cancellations = []
reductions = []
reductions_en = []
is_valid = True
# 1. 燃烧检测
if sun_deg is not None:
planet_deg = pd.get('degree', 0)
if _is_combust(planet, planet_deg, sun_deg, bool(pd.get('retrograde'))):
profile = COMBUSTION_ORB.get(planet, {'direct': 10})
threshold = profile.get('retrograde' if pd.get('retrograde') else 'direct', profile.get('direct', 10))
cancellations.append(f"{planet}被太阳燃烧(orb < {threshold}°) — Yoga效力大减")
is_valid = False
# 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. 逆行检测
if pd.get('retrograde', False):
cancellations.append(f"{planet}逆行 — Yoga效力减弱")
is_valid = False
# 2. 逆行:不破格,不减弱(定稿甲5)
# 3. 凶星受克
# 3. 凶星同宫 / 凶星照:只降低强度
malefics = _has_malefic_aspect(sign, planets, planet)
if malefics:
cancellations.append(f"{planet}受凶星合相: {', '.join(malefics)} — Yoga部分失效")
is_valid = False
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. Navamsa弱势(D9中落陷)
# 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:
# Check if debilitated in D9
deb_sign_idx = (SIGNS.index(EXALTATION[planet][0]) + 6) % 12 if planet in EXALTATION and planet != 'Rahu' and planet != 'Ketu' else -1
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
strength = 'cancelled' if not is_valid else ('reduced' if reductions else 'full')
yogas.append({
'name': yoga_name,
'planet': planet,
'sign': sign,
'house': house,
'is_valid': is_valid,
'strength': '完整' if is_valid else '削弱',
'strength': strength,
'strength_cn': {'full': '完整', 'reduced': '部分减弱', 'cancelled': '不成立'}[strength],
'effect': effect,
'reductions': reductions,
'reductions_en': reductions_en,
'cancellations': cancellations,
})
@@ -175,13 +238,14 @@ def assess_pmc_strength(planets: Dict, sun_degree: float = None) -> Dict:
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']
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)}受克而削弱"
assessment = f"瑜伽潜质存在但因{', '.join(st)}在 D9 落陷而不成立"
else:
assessment = "无Pancha Mahapurusha Yoga"
@@ -189,6 +253,7 @@ def assess_pmc_strength(planets: Dict, sun_degree: float = None) -> Dict:
'method': 'Pancha Mahapurusha Yoga 完整检测 v1.0',
'yogas': yogas,
'valid_count': len(valid),
'reduced': reduced,
'invalid_count': len(invalid),
'assessment': assessment,
}
+12 -1
View File
@@ -827,7 +827,18 @@ def calc_all_yogas_doshas(planets_data: Dict, houses: Dict,
# Yogas
results['raj_yogas'] = calc_raj_yogas(planets_data, houses)
results['dhana_yogas'] = calc_dhana_yogas(planets_data, houses)
results['pancha_mahapurusha'] = calc_pancha_mahapurusha_yoga(planets_data)
# BUG-1207 (甲5): Pancha Mahapurusha has one main detector
# (scripts/pancha_mahapurusha.py, via main_yoga_detectors). This older
# aggregate checks no affliction at all; its list is kept for reference only
# and is not counted as a formed yoga.
reference = calc_pancha_mahapurusha_yoga(planets_data)
results['pancha_mahapurusha'] = {
'status': 'reference_only',
'main_detector': 'scripts/pancha_mahapurusha.py',
'yogas': [],
'reference_yogas': reference['yogas'],
'summary': 'reference_only:五大人格以 scripts/pancha_mahapurusha.py 主检测器为准',
}
results['nicha_bhanga_raj'] = calc_nicha_bhanga_raj_yoga(planets_data, houses)
# Doshas
+13 -7
View File
@@ -139,19 +139,21 @@ def t30():
assert len(r) >= 1
# === v6.7.5 新增功能测试 ===
@test("PMC combustion cancellation")
# BUG-1207 (astrologer ruling 甲5): combustion only reduces the yoga and
# retrograde does not affect it. Was: both asserted is_valid == False.
@test("PMC combustion reduces, does not cancel")
def t31():
from pancha_mahapurusha import detect_pancha_mahapurusha
p = {'Mars': {'sign': 'Capricorn', 'house': 4, 'degree': 50}}
r = detect_pancha_mahapurusha(p, sun_degree=53)
assert r[0]['is_valid'] == False, "Mars too close to Sun should cancel PMC"
p = {'Mars': {'sign': 'Capricorn', 'house': 4, 'degree': 280}}
r = detect_pancha_mahapurusha(p, sun_degree=283)
assert r[0]['is_valid'] == True and r[0]['strength'] == 'reduced', "combust Mars keeps PMC, reduced"
@test("PMC retrograde cancellation")
@test("PMC retrograde does not cancel")
def t32():
from pancha_mahapurusha import detect_pancha_mahapurusha
p = {'Jupiter': {'sign': 'Cancer', 'house': 1, 'degree': 95, 'retrograde': True}}
r = detect_pancha_mahapurusha(p)
assert r[0]['is_valid'] == False
assert r[0]['is_valid'] == True and r[0]['strength'] == 'full'
@test("Bhava Bala computation")
def t33():
@@ -320,7 +322,11 @@ def t53():
def t54():
from yoga_engine import YogaEngine
engine = YogaEngine('references/yoga_rules.json')
assert len(engine.rules) >= 400, f"Should have 400+ rules, got {len(engine.rules)}"
# Astrologer rulings 2026-10-03 (BUG-1205..1207): 23 rules became reference_only
# (kept, not evaluated; their yogas come from one main detector each).
# Was: len(engine.rules) >= 400.
total = len(engine.rules) + len(engine.reference_only_rule_ids)
assert total >= 400, f"Should have 400+ rules, got {total}"
@test("Yoga engine solar lunar detection")
def t55():
@@ -0,0 +1,172 @@
"""Pancha Mahapurusha: one main detector, ruling 甲5 (BUG-1207).
`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
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
back on the card, the report or `chart.yogas` by another path. Placements are
fictional except the parametrised public AA figures.
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from scripts.consultation_native_layers import build_traditional_yogas, merge_detected_yogas
from scripts.jyotish_api_server import JyotishAPIHandler, _compact_yoga_entry
from scripts.jyotish_engine import compute_chart_data
from scripts.main_yoga_detectors import pancha_mahapurusha_rows
from scripts.pancha_mahapurusha import detect_pancha_mahapurusha
from scripts.yoga_engine import YogaEngine
from scripts.yogas_doshas import calc_all_yogas_doshas
ROOT = Path(__file__).resolve().parents[1]
RULES = ROOT / "references" / "yoga_rules.json"
SIGNS = ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"]
REFERENCE_IDS = {
*(f"mahapurusha_{planet}{suffix}" for planet in ("mars", "mercury", "jupiter", "venus", "saturn") for suffix in ("", "_own")),
*(f"moolatrikona_{planet}" for planet in ("mars", "mercury", "jupiter", "venus", "saturn")),
"hamsa_trikona", "bvr_maalavya_yoga",
}
def _row(sign: str, lon_in_sign: float, asc: str = "Cancer", **extra) -> dict:
return {"sign": sign, "house": (SIGNS.index(sign) - SIGNS.index(asc)) % 12 + 1,
"lon": SIGNS.index(sign) * 30 + lon_in_sign, "degree": lon_in_sign, **extra}
def _one(planets: dict, name: str) -> dict:
return next(item for item in detect_pancha_mahapurusha(planets) if item["name"] == name)
def test_retrograde_does_not_cancel() -> None:
hamsa = _one({"Jupiter": _row("Cancer", 5.0, retrograde=True)}, "Hamsa Yoga")
assert (hamsa["is_valid"], hamsa["strength"], hamsa["reductions"]) == (True, "full", [])
def test_combustion_reduces_using_the_suns_longitude_from_the_chart() -> None:
# API rows carry `degree` in sign; the longitude decides combustion.
planets = {"Jupiter": _row("Cancer", 5.0), "Sun": _row("Cancer", 10.0)}
hamsa = _one(planets, "Hamsa Yoga")
assert hamsa["is_valid"] is True and hamsa["strength"] == "reduced"
assert hamsa["reductions"][0].startswith("木星燃烧")
far = _one({"Jupiter": _row("Cancer", 5.0), "Sun": _row("Leo", 20.0)}, "Hamsa Yoga")
assert far["strength"] == "full"
def test_malefic_aspect_reduces() -> None:
# Saturn in Capricorn casts its 7th on Cancer.
hamsa = _one({"Jupiter": _row("Cancer", 5.0), "Saturn": _row("Capricorn", 10.0)}, "Hamsa Yoga")
assert hamsa["strength"] == "reduced"
assert hamsa["reductions"] == ["受凶星照:土星"]
# Rahu / Ketu cast no aspect (repository drishti rule); Ketu opposite is no reduction.
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_reference_only_rules_never_run() -> None:
rules = {rule["id"]: rule for rule in json.loads(RULES.read_text(encoding="utf-8"))["rules"]}
for rule_id in REFERENCE_IDS:
assert rules[rule_id]["reference_only"] is True and "BUG-1207" in rules[rule_id]["reference_only_reason"], rule_id
assert not REFERENCE_IDS & {rule["id"] for rule in YogaEngine(str(RULES)).rules}
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")
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"))
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],
[entry for entry in map(_compact_yoga_entry, engine_rows) if entry],
)
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")
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)}
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
# Saturn in Capricorn is the 7th from a Cancer Lagna: Sasa, full.
assert [(row["name"], row["strength"]) for row in all_rows] == [("Hamsa Yoga", "部分减弱"), ("Sasa Yoga", "强")]
rows = all_rows[:1]
entry = _compact_yoga_entry(rows[0])
layer = build_traditional_yogas({"raman_support": {"yogas": {"status": "executed", "hits": [], "ungoverned_detected": [entry]}}})
assert layer["names"] == ["Hamsa Yoga(Jupiter;部分减弱:受凶星照:土星)"]
FIGURES = (
("minute_rectification_development_v1.json", "steve_jobs_1955_development"),
("minute_rectification_holdout_v4.json", "barack_obama_1961_aa_v4_holdout"),
("minute_rectification_holdout_v4.json", "elizabeth_taylor_1932_aa_v4_holdout"),
("minute_rectification_holdout_v5.json", "marilyn_monroe_1926_aa_v5_holdout"),
("minute_rectification_holdout_v5.json", "judy_garland_1922_aa_v5_holdout"),
("minute_rectification_holdout_v5.json", "edith_piaf_1915_aa_v5_holdout"),
("minute_rectification_holdout_v5.json", "frida_kahlo_1907_aa_v5_holdout"),
("minute_rectification_holdout_v5.json", "george_w_bush_1946_aa_v5_holdout"),
("minute_rectification_holdout_v5.json", "zinedine_zidane_1972_aa_v5_holdout"),
)
@pytest.mark.parametrize("library,case_id", FIGURES)
def test_public_figures_engine_and_chart_paths_agree_with_the_main_detector(library: str, case_id: str) -> None:
cases = json.loads((ROOT / "references/real_case_calibration" / library).read_text(encoding="utf-8"))["cases"]
birth = next(case for case in cases if case["case_id"] == case_id)["birth"]
year, month, day = (int(part) for part in birth["date"].split("-"))
hour, minute = (int(part) for part in birth["time"].split(":"))
chart, *_ = compute_chart_data(year, month, day, hour, minute, birth["latitude"], birth["longitude"],
birth["timezone_offset"], ayanamsa_name="raman")
asc = chart["ascendant"]["sign"]
# Report path shape (degree in sign + lon) and API path shape (degree = lon).
report_planets = {name: {"sign": row["sign"], "house": row["house"], "degree": row["degree_in_sign"],
"lon": row["degree_raw"], "retrograde": row["retrograde"]}
for name, row in chart["planets"].items()}
api_planets = {name: {**row, "degree": row["lon"]} for name, row in report_planets.items()}
expected = {item["planet"]: item["strength"] for item in detect_pancha_mahapurusha(api_planets) if item["is_valid"]}
engine = {row["planets"][0]: row for row in YogaEngine(str(RULES)).detect(report_planets, asc) if row.get("category") == "mahapurusha"}
assert {planet: ("reduced" if row["reductions"] else "full") for planet, row in engine.items()} == expected
chart_rows = JyotishAPIHandler._detect_yogas(None, api_planets, SIGNS.index(asc))
assert {row["planets"][0] for row in chart_rows if row.get("category") == "PMC"} == set(expected)
def test_report_yoga_rows_carry_retrograde_and_longitude() -> None:
"""cmd_yoga (report path) hands the detectors the same longitude and
retrograde flag as the consultation path, so combustion orbs agree."""
from scripts.jyotish_engine import cmd_yoga
cases = json.loads((ROOT / "references/real_case_calibration/minute_rectification_holdout_v5.json").read_text(encoding="utf-8"))["cases"]
birth = next(case for case in cases if case["case_id"] == "judy_garland_1922_aa_v5_holdout")["birth"]
year, month, day = (int(part) for part in birth["date"].split("-"))
hour, minute = (int(part) for part in birth["time"].split(":"))
args = type("Args", (), {"planets": None, "year": year, "month": month, "day": day, "hour": hour, "minute": minute,
"lat": birth["latitude"], "lon": birth["longitude"], "tz": birth["timezone_offset"],
"node_mode": "mean", "second": 0, "ascendant": "Aries"})()
report = [row for row in cmd_yoga(args)["yogas"] if row.get("category") == "mahapurusha"]
chart, *_ = compute_chart_data(year, month, day, hour, minute, birth["latitude"], birth["longitude"], birth["timezone_offset"])
api = {name: {"sign": row["sign"], "house": row["house"], "lon": row["degree_raw"], "degree": row["degree_raw"],
"retrograde": row["retrograde"]} for name, row in chart["planets"].items()}
assert [(row["name"], row["reductions"]) for row in report] == [
(row["name"], row["reductions"]) for row in pancha_mahapurusha_rows(api)]
@@ -39,8 +39,11 @@ def test_another_malefic_in_the_same_sign_still_cancels_and_is_named() -> None:
"Saturn": {"sign": "Aries", "house": 1, "degree": 12.0},
}
ruchaka = _yoga(detect_pancha_mahapurusha(planets), "Ruchaka Yoga")
assert ruchaka["is_valid"] is False
assert any("Saturn" in reason and "Mars," not in reason for reason in ruchaka["cancellations"])
# BUG-1207 (ruling 甲5): a malefic in the same sign only reduces the yoga.
# Was: is_valid False with the reason in `cancellations`.
assert ruchaka["is_valid"] is True
assert ruchaka["strength"] == "reduced"
assert any("土星" in reason and "火星" not in reason for reason in ruchaka["reductions"])
assert _has_malefic_aspect("Aries", planets, "Mars") == ["Saturn"]
@@ -50,5 +53,7 @@ def test_benefic_yoga_planet_unchanged() -> None:
"Mars": {"sign": "Cancer", "house": 4, "degree": 100.0},
}
hamsa = _yoga(detect_pancha_mahapurusha(planets), "Hamsa Yoga")
assert hamsa["is_valid"] is False
# BUG-1207 (ruling 甲5): reduced, not cancelled (was is_valid False).
assert hamsa["is_valid"] is True
assert hamsa["strength"] == "reduced"
assert _has_malefic_aspect("Cancer", planets, "Jupiter") == ["Mars"]