Files
Jyotisha/scripts/main_yoga_detectors.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

228 lines
11 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
"""One main detector per classical yoga (astrologer rulings 2026-10-03).
The same yoga used to be judged by several implementations (local code,
`references/yoga_rules.json`, upstream ports), merged by name, so whichever
arrived first won. Each yoga below now has exactly one detector; the data-driven
rules of the same name are `reference_only` in `yoga_rules.json` and the rule
engine (`yoga_engine.YogaEngine.detect`) appends the rows built here instead, so
the reader report, the consultation sieve and `chart.yogas` carry one verdict.
Rows are shaped like `yoga_engine` rows (Chinese fields plus `*_en` twins for the
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
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
not cancel it).
Inputs are D1 rows `{sign, house, lon?, retrograde?, navamsa_sign?}`. A missing
`navamsa_sign` is derived from the sidereal longitude (Parashari navamsa: each
3°20' from the movable / fixed / dual starting sign), so every caller that has
longitudes gets the same verdict.
"""
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
SIGNS = ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo',
'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces']
SIGN_ZH = {
'Aries': '白羊座', 'Taurus': '金牛座', 'Gemini': '双子座', 'Cancer': '巨蟹座',
'Leo': '狮子座', 'Virgo': '处女座', 'Libra': '天秤座', 'Scorpio': '天蝎座',
'Sagittarius': '射手座', 'Capricorn': '摩羯座', 'Aquarius': '水瓶座', 'Pisces': '双鱼座',
}
PLANET_ZH = {
'Sun': '太阳', 'Moon': '月亮', 'Mars': '火星', 'Mercury': '水星', 'Jupiter': '木星',
'Venus': '金星', 'Saturn': '土星', 'Rahu': '罗睺', 'Ketu': '计都',
}
NEECHA_BHANGA_NAME = 'Neecha Bhanga'
NEECHA_BHANGA_NAME_ZH = '落陷取消'
NEECHA_BHANGA_CATEGORY = 'neecha_bhanga'
NEECHA_BHANGA_NOTE_ZH = '只列成立的取消条件,不单独下结论'
NEECHA_BHANGA_NOTE_EN = 'Conditions only; no stand-alone conclusion'
def _lon(row: Any) -> Optional[float]:
"""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:
"""D9 sign of a sidereal longitude (Parashari navamsa)."""
return SIGNS[int((lon % 360.0) * 9.0 / 30.0) % 12]
def with_navamsa(planets: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
"""Copy of `planets` with `navamsa_sign` filled from the longitude when missing."""
rows: Dict[str, Dict[str, Any]] = {}
for name, row in (planets or {}).items():
if not isinstance(row, dict):
continue
copy = dict(row)
lon = _lon(row)
if not copy.get('navamsa_sign') and lon is not None:
copy['navamsa_sign'] = navamsa_sign(lon)
rows[name] = copy
return rows
def _planet_zh(name: Any) -> str:
return PLANET_ZH.get(str(name), str(name))
def _sign_zh(name: Any) -> str:
return SIGN_ZH.get(str(name), str(name))
def _neecha_condition_text(item: Dict[str, Any], debilitated: str) -> tuple[str, str]:
code = item.get('code')
planet = item.get('planet')
house = item.get('house')
frame_zh = '上升' if item.get('frame') == 'lagna' else '月亮'
frame_en = 'the Lagna' if item.get('frame') == 'lagna' else 'the Moon'
if code in ('A', 'B'):
return (f'落陷星座主星{_planet_zh(planet)}在{frame_zh}起第{house}宫(角宫)',
f'dispositor {planet} in house {house} from {frame_en} (kendra)')
if code in ('C', 'D'):
return (f'该星入旺星座的主星{_planet_zh(planet)}在{frame_zh}起第{house}宫(角宫)',
f'lord of its exaltation sign, {planet}, in house {house} from {frame_en} (kendra)')
if code == 'E':
return (f'{_planet_zh(debilitated)}与落陷星座主星{_planet_zh(planet)}互换星座',
f'{debilitated} and its dispositor {planet} exchange signs')
if code == 'F':
return (f'{_planet_zh(planet)}在 D9 入庙或入旺({_sign_zh(item.get("sign"))})',
f'{planet} own or exalted in D9 ({item.get("sign")})')
return (str(item), str(item))
def neecha_bhanga_rows(planets: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Formed Neecha Bhanga rows; conditions from `calc_nicha_bhanga_raj_yoga`."""
result = calc_nicha_bhanga_raj_yoga(with_navamsa(planets), {})
rows: List[Dict[str, Any]] = []
for item in result.get('yogas') or []:
planet = item.get('planet')
texts = [_neecha_condition_text(cond, planet) for cond in item.get('cancellation_conditions') or []]
if not texts:
continue
zh = [text[0] for text in texts]
en = [text[1] for text in texts]
sign = item.get('debilitated_sign')
rows.append({
'name': NEECHA_BHANGA_NAME,
'name_cn': NEECHA_BHANGA_NAME_ZH,
'category': NEECHA_BHANGA_CATEGORY,
'planets': [planet],
'combination': f'{_planet_zh(planet)}落陷于{_sign_zh(sign)};成立条件:' + ';'.join(zh),
'combination_en': f'{planet} debilitated in {sign}; conditions met: ' + '; '.join(en),
'conditions': zh,
'conditions_en': en,
'effects': [NEECHA_BHANGA_NOTE_ZH],
'effects_en': [NEECHA_BHANGA_NOTE_EN],
'strength': f'成立条件 {len(zh)} 条',
'strength_en': f'{len(zh)} condition(s) met',
'source': 'BPHS',
'rule_id': 'main_detector:neecha_bhanga',
'detector': 'yogas_doshas.calc_nicha_bhanga_raj_yoga',
})
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,
'd9_dignity_modifier': item.get('d9_dignity_modifier'),
'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 {})
if not result.get('present'):
return []
moon_sign = (planets.get('Moon') or {}).get('sign')
return [{
'name': 'Kemadruma Yoga',
'name_cn': '空劫瑜伽',
'category': 'durbhaga',
'planets': ['Moon'],
'combination': f'月亮在{_sign_zh(moon_sign)},第2、12宫无火水木金土(太阳与罗计不计;不启用角宫解除)',
'combination_en': (f'Moon in {moon_sign}; none of Mars to Saturn in the 2nd or 12th from it '
'(the Sun and the nodes are not counted; no kendra cancellation)'),
'effects': ['人生艰辛', '需自力更生', '精神挑战'],
'effects_en': ['Hard life', 'Must rely on oneself', 'Mental and spiritual challenges'],
'strength': '弱',
'strength_en': 'weak',
'source': 'BPHS',
'rule_id': 'main_detector:kemadruma',
'detector': 'yoga_expansion.detect_kemadruma',
}]
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 pancha_mahapurusha_rows(planets) + kemadruma_rows(planets) + neecha_bhanga_rows(planets)