fix(dignity): record Neecha Bhanga kendras and clip the first dasha from its full parent (BUG-1268, upstream 4a807e7d)
This commit is contained in:
@@ -17046,3 +17046,19 @@
|
||||
- 相关记录:BUG-1220
|
||||
- 复发自:无
|
||||
- 修复版本:本分支待发布
|
||||
|
||||
## BUG-1268 | 落陷取消只查两种条件,第一大运的小运从截断点起排
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-10-07
|
||||
- 最近更新:2026-10-07
|
||||
- 影响面:尊贵判断里的落陷取消;全盘报告的 Vimshottari 小运;Bhava Bala 表;八字 Chara Karaka 表
|
||||
- 用户现象:落陷取消只在定位星入庙或旺星同宫时成立。出生落在第一大运中途时,小运从截断后的起点重排,而不是从完整大运起点排完再裁。Bhava Bala 的合计行会被印成多一宫。Karaka 表没有用于排序的度数
|
||||
- 触发条件:判断落陷取消;渲染第一大运被出生截断后的小运;渲染 Bhava Bala 或 Karaka 表
|
||||
- 根因:尊贵路径只记录了定位星入庙、旺星与落陷星同宫。上游 `4a807e7d` 另加定位星在月亮的四正宫、定位星在上升的四正宫。小运以前用截断后的可见段长度重算。Bhava 渲染把 houses 列表整段打出,合计行没有宫号。Karaka 表只有盘面度数
|
||||
- 修复:尊贵证据的 `conditions_checked` 加入上述两条件,并把宫位送进尊贵上下文。命中只记取消条件,`royal_yoga_label_allowed` 为假,不标成 Raja Yoga。卡片仍走原有的 `calc_nicha_bhanga_raj_yoga`。第一大运的小运和更小运从完整父段起点排,显示时裁到出生之后,并保留 `unclipped_start_datetime`。Bhava 表改用带宫号的行。Karaka 表增加 Ranking degree 列,取 `effective_degree_in_sign`
|
||||
- 验证:`tests/test_neecha_bhanga_dignity_evidence.py`;`tests/test_neecha_bhanga_conditions.py`;`tests/test_bhava_bala_formal.py`;`tests/test_chara_karaka_8_bphs_order.py`
|
||||
- 防复发:上述测试锁住条件名、未裁起点和宫号过滤
|
||||
- 相关记录:BUG-1205、BUG-1261
|
||||
- 复发自:无
|
||||
- 修复版本:本分支待发布
|
||||
|
||||
+104
-28
@@ -612,32 +612,83 @@ def _build_planetary_friendship_snapshot(planets_data):
|
||||
),
|
||||
}
|
||||
|
||||
def _check_neecha_bhanga(planet, sign, planets_data):
|
||||
"""Check for Cancellation of Debilitation."""
|
||||
def _neecha_bhanga_evidence(planet, sign, planets_data):
|
||||
"""Record cancellation conditions. A hit is not a Raja Yoga."""
|
||||
evidence = {
|
||||
'planet': planet,
|
||||
'debilitation_sign': sign,
|
||||
'成立': False,
|
||||
'reasons': [],
|
||||
'conditions_checked': [
|
||||
'dispositor_exalted',
|
||||
'exaltation_lord_conjunct_debilitation_sign',
|
||||
'dispositor_kendra_from_moon',
|
||||
'dispositor_kendra_from_lagna',
|
||||
],
|
||||
'royal_yoga_label_allowed': False,
|
||||
'claim_boundary': (
|
||||
'Neecha Bhanga evidence records cancellation conditions only; it does not by itself prove a Raja Yoga, '
|
||||
'complete reversal of weakness, or a guaranteed outcome.'
|
||||
),
|
||||
}
|
||||
if not planets_data:
|
||||
return False
|
||||
|
||||
evidence['status'] = 'blocked_missing_planets'
|
||||
return evidence
|
||||
|
||||
sign_lord = SIGN_LORDS.get(sign)
|
||||
|
||||
# Condition 1: Dispositor is exalted.
|
||||
lord_data = planets_data.get(sign_lord, {}) if sign_lord else {}
|
||||
moon_sign = (planets_data.get('Moon') or {}).get('sign') if isinstance(planets_data.get('Moon'), dict) else None
|
||||
lord_sign = lord_data.get('sign') if isinstance(lord_data, dict) else None
|
||||
signs = list(SIGN_LORDS)
|
||||
if lord_sign in signs and moon_sign in signs:
|
||||
moon_house = (signs.index(lord_sign) - signs.index(moon_sign)) % 12 + 1
|
||||
if moon_house in (1, 4, 7, 10):
|
||||
evidence['成立'] = True
|
||||
evidence['reasons'].append({
|
||||
'condition': 'dispositor_kendra_from_moon',
|
||||
'planet': sign_lord,
|
||||
'house_from_moon': moon_house,
|
||||
})
|
||||
if isinstance(lord_data, dict) and lord_data.get('house') in (1, 4, 7, 10):
|
||||
evidence['成立'] = True
|
||||
evidence['reasons'].append({
|
||||
'condition': 'dispositor_kendra_from_lagna',
|
||||
'planet': sign_lord,
|
||||
'house': lord_data['house'],
|
||||
})
|
||||
|
||||
if sign_lord and sign_lord in planets_data:
|
||||
lord_sign = planets_data[sign_lord].get('sign')
|
||||
if lord_sign and EXALTATION.get(sign_lord) == lord_sign:
|
||||
return True
|
||||
|
||||
# Condition 2: Planet exalted in this sign is conjunct.
|
||||
evidence['成立'] = True
|
||||
evidence['reasons'].append({
|
||||
'condition': 'dispositor_exalted',
|
||||
'planet': sign_lord,
|
||||
'sign': lord_sign,
|
||||
})
|
||||
|
||||
exalted_planet = None
|
||||
for p, ex_sign in EXALTATION.items():
|
||||
if ex_sign == sign:
|
||||
exalted_planet = p
|
||||
break
|
||||
|
||||
if exalted_planet and exalted_planet in planets_data:
|
||||
ex_p_sign = planets_data[exalted_planet].get('sign')
|
||||
if ex_p_sign == sign:
|
||||
return True
|
||||
|
||||
return False
|
||||
evidence['成立'] = True
|
||||
evidence['reasons'].append({
|
||||
'condition': 'exaltation_lord_conjunct_debilitation_sign',
|
||||
'planet': exalted_planet,
|
||||
'sign': sign,
|
||||
})
|
||||
|
||||
evidence['status'] = 'executed'
|
||||
return evidence
|
||||
|
||||
|
||||
def _check_neecha_bhanga(planet, sign, planets_data):
|
||||
"""Boolean compatibility wrapper. Details live on the evidence helper."""
|
||||
return bool(_neecha_bhanga_evidence(planet, sign, planets_data).get('成立'))
|
||||
|
||||
|
||||
def _build_dignity_context(chart_data):
|
||||
@@ -652,6 +703,7 @@ def _build_dignity_context(chart_data):
|
||||
context[pn] = {
|
||||
'sign': pd.get('sign'),
|
||||
'degree_in_sign': pd.get('degree_in_sign', pd.get('degree', 0) % 30),
|
||||
'house': pd.get('house'),
|
||||
}
|
||||
return context
|
||||
|
||||
@@ -6808,7 +6860,8 @@ def render_pl9_markdown(packet: dict) -> str:
|
||||
bhava_bala = strength_sheet.get('bhava_bala') if isinstance(strength_sheet.get('bhava_bala'), dict) else {}
|
||||
strongest = _humanize_reader_token(shadbala.get('strongest')) if isinstance(shadbala, dict) else None
|
||||
weakest = _humanize_reader_token(shadbala.get('weakest')) if isinstance(shadbala, dict) else None
|
||||
houses = bhava_bala.get('houses') if isinstance(bhava_bala.get('houses'), list) else []
|
||||
from special_lagnas import bhava_house_rows
|
||||
houses = bhava_house_rows(bhava_bala)
|
||||
top_house = None
|
||||
top_score = None
|
||||
weak_house = None
|
||||
@@ -7653,7 +7706,8 @@ def render_pl9_markdown(packet: dict) -> str:
|
||||
)
|
||||
out.append('')
|
||||
if bhava_bala:
|
||||
rows = bhava_bala.get('houses') if isinstance(bhava_bala.get('houses'), list) else []
|
||||
from special_lagnas import bhava_house_rows
|
||||
rows = bhava_house_rows(bhava_bala)
|
||||
if rows:
|
||||
out.extend([
|
||||
'#### Bhava Bala(三分量)',
|
||||
@@ -12470,29 +12524,51 @@ def cmd_dasha(args):
|
||||
|
||||
today = datetime.strptime(args.today, "%Y-%m-%d") if args.today else datetime.now()
|
||||
current = None
|
||||
year_days = 365.25
|
||||
for d in timeline:
|
||||
ds = datetime.strptime(d["start"], "%Y-%m-%d"); de = datetime.strptime(d["end"], "%Y-%m-%d")
|
||||
total_days = (de - ds).days; li = DASHA_ORDER.index(d["lord"])
|
||||
# v3.7.2: 为所有 Mahadasha 计算 Antardasha(不仅当前大运)
|
||||
sub = []; sdt = ds
|
||||
# 出生只截断第一大运的可见段。小运和更小运从完整父段起点排,再裁到出生之后。
|
||||
parent_start = datetime.fromisoformat(d["start_datetime"])
|
||||
parent_end = datetime.fromisoformat(d["end_datetime"])
|
||||
visible_origin = birth_dt if d.get("is_balance") else parent_start
|
||||
total_duration = timedelta(days=float(d["full_years"]) * year_days)
|
||||
li = DASHA_ORDER.index(d["lord"])
|
||||
sub = []
|
||||
sdt = parent_start
|
||||
for j in range(9):
|
||||
sl = DASHA_ORDER[(li + j) % 9]; sd = total_days * DASHA_YEARS[sl] / 120
|
||||
se = sdt + timedelta(days=sd)
|
||||
is_cur = sdt <= today < se
|
||||
sl = DASHA_ORDER[(li + j) % 9]
|
||||
se = parent_end if j == 8 else sdt + (total_duration * (DASHA_YEARS[sl] / 120.0))
|
||||
if se <= visible_origin:
|
||||
sdt = se
|
||||
continue
|
||||
visible_start = max(sdt, visible_origin)
|
||||
children = _build_pratyantar_timeline(d["lord"], sl, sdt, se, reference_dt=today)
|
||||
visible_children = []
|
||||
for child in children:
|
||||
child_start = datetime.fromisoformat(child["start_datetime"])
|
||||
child_end = datetime.fromisoformat(child["end_datetime"])
|
||||
if child_end <= visible_origin:
|
||||
continue
|
||||
if child_start < visible_origin:
|
||||
child["unclipped_start_datetime"] = child_start.isoformat(timespec="seconds")
|
||||
child["start"] = visible_origin.strftime("%Y-%m-%d")
|
||||
child["start_datetime"] = visible_origin.isoformat(timespec="seconds")
|
||||
child["is_current"] = bool(visible_origin <= today < child_end)
|
||||
visible_children.append(child)
|
||||
sub.append({
|
||||
"lord": sl,
|
||||
"lord_cn": PLANET_CN[sl],
|
||||
"start": sdt.strftime("%Y-%m-%d"),
|
||||
"start": visible_start.strftime("%Y-%m-%d"),
|
||||
"end": se.strftime("%Y-%m-%d"),
|
||||
"start_datetime": sdt.isoformat(timespec="seconds"),
|
||||
"start_datetime": visible_start.isoformat(timespec="seconds"),
|
||||
"unclipped_start_datetime": sdt.isoformat(timespec="seconds"),
|
||||
"end_datetime": se.isoformat(timespec="seconds"),
|
||||
"is_current": is_cur,
|
||||
"pratyantar_dasha_timeline": _build_pratyantar_timeline(
|
||||
d["lord"], sl, sdt, se, reference_dt=today,
|
||||
),
|
||||
"is_current": visible_start <= today < se,
|
||||
"pratyantar_dasha_timeline": visible_children,
|
||||
})
|
||||
sdt = se
|
||||
d["antardasha_timeline"] = sub
|
||||
ds = datetime.strptime(d["start"], "%Y-%m-%d")
|
||||
de = datetime.strptime(d["end"], "%Y-%m-%d")
|
||||
if ds <= today < de:
|
||||
d["is_current"] = True
|
||||
# 从 antardasha 列表中提取当前正在运行的 antardasha
|
||||
|
||||
@@ -1197,11 +1197,11 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
)
|
||||
lines.append('')
|
||||
bhava_bala = _dict(strengths.get('bhava_bala'))
|
||||
bhava_rows = _list(bhava_bala.get('houses')) or [
|
||||
{'house': key, **value}
|
||||
for key, value in bhava_bala.items()
|
||||
if str(key).isdigit() and isinstance(value, dict)
|
||||
]
|
||||
try:
|
||||
from special_lagnas import bhava_house_rows
|
||||
except ImportError:
|
||||
from scripts.special_lagnas import bhava_house_rows
|
||||
bhava_rows = bhava_house_rows(bhava_bala)
|
||||
if bhava_rows:
|
||||
# BUG-1210 (甲13): the formal three components (scripts/bhava_bala.py);
|
||||
# the local +2 / -1.5 house score is never shown.
|
||||
@@ -1211,7 +1211,7 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
'| House | Sign | Lord | Bhavadhipati | Dig | Drishti | Total (Virupa) | Total (Rupa) |',
|
||||
'|-------|------|------|--------------|-----|---------|----------------|--------------|',
|
||||
])
|
||||
for row in bhava_rows[:12]:
|
||||
for row in bhava_rows:
|
||||
if not isinstance(row, dict) or row.get('total') is None:
|
||||
continue
|
||||
lines.append(
|
||||
@@ -1292,8 +1292,8 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
lines.extend(['### Jaimini Karaka、Arudha Pada 与特殊 Lagna', ''])
|
||||
if karaka_table:
|
||||
lines.extend([
|
||||
'| Karaka | Planet | Degree | Domain |',
|
||||
'|--------|--------|--------|--------|',
|
||||
'| Karaka | Planet | Degree | Ranking degree | Domain |',
|
||||
'|--------|--------|--------|----------------|--------|',
|
||||
])
|
||||
for name, row in karaka_table.items():
|
||||
if not isinstance(row, dict):
|
||||
@@ -1301,6 +1301,7 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
lines.append(
|
||||
f"| {_cell(row.get('cn_name') or name)} | {_cell(_token(row.get('planet')))} | "
|
||||
f"{_cell(_degree(row.get('degree_in_sign') or row.get('sign_degree')))} | "
|
||||
f"{_cell(_degree(row.get('effective_degree_in_sign', row.get('degree_in_sign'))))} | "
|
||||
f"{_cell(row.get('domain'))} |"
|
||||
)
|
||||
lines.append('')
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Dignity-path Neecha Bhanga records the two kendra conditions (BUG-1268)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from scripts.jyotish_engine import (
|
||||
_build_dignity_context,
|
||||
_get_dignity_level,
|
||||
_neecha_bhanga_evidence,
|
||||
cmd_dasha,
|
||||
)
|
||||
|
||||
|
||||
def _capricorn_jupiter(**extra_planets):
|
||||
planets = {
|
||||
"Jupiter": {"sign": "Capricorn", "degree": 10.0, "house": 10},
|
||||
"Moon": {"sign": "Gemini", "degree": 5.0, "house": 3},
|
||||
"Mars": {"sign": "Leo", "degree": 5.0, "house": 5},
|
||||
}
|
||||
planets.update(extra_planets)
|
||||
return planets
|
||||
|
||||
|
||||
def test_dispositor_kendra_from_lagna_is_checked_and_is_not_a_raja_yoga() -> None:
|
||||
planets = _capricorn_jupiter(Saturn={"sign": "Aries", "degree": 4.0, "house": 1})
|
||||
context = _build_dignity_context(planets)
|
||||
assert context["Saturn"]["house"] == 1
|
||||
evidence = _neecha_bhanga_evidence("Jupiter", "Capricorn", context)
|
||||
assert evidence["conditions_checked"] == [
|
||||
"dispositor_exalted",
|
||||
"exaltation_lord_conjunct_debilitation_sign",
|
||||
"dispositor_kendra_from_moon",
|
||||
"dispositor_kendra_from_lagna",
|
||||
]
|
||||
assert evidence["royal_yoga_label_allowed"] is False
|
||||
assert "Raja Yoga" in evidence["claim_boundary"]
|
||||
assert {row["condition"] for row in evidence["reasons"]} == {"dispositor_kendra_from_lagna"}
|
||||
assert _get_dignity_level("Jupiter", "Capricorn", 10.0, context) == "NEECHA_BHANGA"
|
||||
|
||||
|
||||
def test_dispositor_kendra_from_moon_is_checked() -> None:
|
||||
planets = _capricorn_jupiter(
|
||||
Saturn={"sign": "Cancer", "degree": 4.0, "house": 5},
|
||||
Moon={"sign": "Cancer", "degree": 8.0, "house": 5},
|
||||
)
|
||||
context = _build_dignity_context(planets)
|
||||
evidence = _neecha_bhanga_evidence("Jupiter", "Capricorn", context)
|
||||
assert {row["condition"] for row in evidence["reasons"]} == {"dispositor_kendra_from_moon"}
|
||||
assert evidence["reasons"][0]["house_from_moon"] == 1
|
||||
|
||||
|
||||
def test_first_mahadasha_antardasha_keeps_the_unclipped_parent_start() -> None:
|
||||
# 原值 / 新值 / 原因: 旧小运从截断后的可见段重排;新值保留未截断起点并裁掉出生前已结束的小运。
|
||||
args = type("Args", (), {
|
||||
"moon_lon": 79.0,
|
||||
"nakshatra": None,
|
||||
"pada": None,
|
||||
"birthdate": "1990-01-15",
|
||||
"today": "2026-05-04",
|
||||
})()
|
||||
first = cmd_dasha(args)["timeline"][0]
|
||||
birth = datetime(1990, 1, 15)
|
||||
assert first["is_balance"] is True
|
||||
ads = first["antardasha_timeline"]
|
||||
assert ads
|
||||
for row in ads:
|
||||
assert datetime.fromisoformat(row["start_datetime"]) >= birth
|
||||
assert datetime.fromisoformat(row["end_datetime"]) > birth
|
||||
assert "unclipped_start_datetime" in row
|
||||
straddling = [row for row in ads if row["unclipped_start_datetime"] != row["start_datetime"]]
|
||||
assert straddling
|
||||
assert datetime.fromisoformat(straddling[0]["unclipped_start_datetime"]) < birth
|
||||
Reference in New Issue
Block a user