diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 433b432e..153c6d4c 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -36548,3 +36548,18 @@ - 相关记录:BUG-1381。 - 复发自:无。 - 修复版本:分支 `codex/upstream-sync8-20261017`。上游提交 `8eec9a54`。 + +## BUG-1384 | Yoga Pinda 只乘了本座 bindu + +- 状态:resolved +- 首次发现 / 最近更新:2026-10-17 / 2026-10-18 +- 来源:issue #80,任务书 T4。上游 `30b43643`。检索「calc_yoga_pinda」「rashi_weight」,没有更早的同类记录。BUG-1209 已经有标准削减链,Pinda 没接上。 +- 现象:Pinda 只拿行星所在星座的 bindu,乘 4 再乘行星权重。PVR 例 43 应为 152,这条捷径算不出来。强弱排序因此偏掉。 +- 触发条件:报告和 Ashtakavarga 接口调用 `calc_yoga_pinda`。 +- 根因:没有做三方净化和单宫主净化,也没有把十二座、七曜分别加权。 +- 修复:七行齐全时直接复用 `sodhita_chain`。星座权重按 PVR 表 28(处女座 6)。同值且一占一空时清空空宫,这是 Raman 15(c),原削减函数已经这样做。报告表头改成同一口径。 +- 验证:`tests/test_ashtakavarga_pinda_source.py`。例 43 的 Mercury 为 77 + 75 = 152。整盘的 Pinda 削减行与 Sodhita 链相同。 +- 防复发:上述测试。 +- 相关记录:BUG-1209。 +- 复发自:无。 +- 修复版本:分支 `codex/upstream-sync8-20261017`。上游提交 `30b43643`。 diff --git a/scripts/ashtakavarga.py b/scripts/ashtakavarga.py index 7822b3e1..230f960b 100644 --- a/scripts/ashtakavarga.py +++ b/scripts/ashtakavarga.py @@ -162,6 +162,8 @@ PLANET_WEIGHTS = { 'Sun': 5, 'Moon': 5, 'Mars': 8, 'Mercury': 5, 'Jupiter': 10, 'Venus': 7, 'Saturn': 5 } +# PVR §12.7 表 28。处女座是 6。不改用 Prasna Marga 的 5。 +RASHI_PINDA_WEIGHTS = (7, 10, 8, 4, 10, 6, 7, 8, 9, 5, 11, 12) # 行星列表 SEVEN_PLANETS = ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn'] @@ -431,13 +433,17 @@ def calc_prastara_av(planets: Dict, asc_sign_idx: int) -> Dict: def calc_yoga_pinda(bav_results: Dict, planets: Dict, asc_sign_idx: int) -> Dict: - """ - 计算 Yoga Pinda / Shodhya Pinda 汇总。 + """Yoga Pinda from the same Trikona and Ekadhipatya rows as the Sodhita table. - 本项目沿用既有 v2.1 口径:取行星本星座的 BAV bindu,分别乘以星座权重与行星权重; - 两者相加作为 Yoga Pinda。该函数把原本嵌在 calc_ashtakavarga() 内部的结果提升为 - 一等契约,便于 API、前端与测试直接引用。 + PVR §12.7 tables 28–29: twelve reduced signs times rashi weights (Virgo 6), + plus the seven planets' weights at the signs they occupy. A full seven-row + chart reuses ``sodhita_chain`` so the report cannot show two reductions. """ + try: + from ashtakavarga_shodhana import occupied_signs, sodhita_chain + except ImportError: + from scripts.ashtakavarga_shodhana import occupied_signs, sodhita_chain + source_sign_idx = {} for pname in SEVEN_PLANETS: if pname in planets: @@ -445,27 +451,51 @@ def calc_yoga_pinda(bav_results: Dict, planets: Dict, asc_sign_idx: int) -> Dict if sign in SIGNS: source_sign_idx[pname] = SIGNS.index(sign) source_sign_idx['Lagna'] = asc_sign_idx + note = ( + 'PVR §12.7 表 28–29:十二座削减值乘星座权重(处女座 6),' + '再加七曜所在座的行星权重。与 Sodhita 表同一份削减。' + '同值且一占一空时按 Raman 15(c) 清空空宫。' + ) + if any(pname not in source_sign_idx for pname in SEVEN_PLANETS): + return { + 'method': 'Yoga Pinda', 'version': '2.0', + 'profile': 'pvr_trikona_ekadhipatya_tables28_29', + 'planets': {}, 'summary': {}, 'all_valid': False, + 'validation_note': note + ' 七曜星座不全,不算 Graha Pinda。', + } + + available = {} + for pname, row in bav_results.items(): + bindus = row.get('bindus') if isinstance(row, dict) else None + if isinstance(bindus, list) and len(bindus) == 12: + available[pname] = {'bindus': bindus} + if set(SEVEN_PLANETS).issubset(available): + chain = sodhita_chain(available, planets) + trikona_rows = chain['trikona_bav'] + reduced_rows = chain['sodhita_bav'] + else: + trikona_rows = { + pname: calc_trikona_shodhana(row['bindus'])['reduced_scores'] + for pname, row in available.items() + } + reduced_rows = calc_ekadhipatya_shodhana(trikona_rows, occupied_signs(planets))['rows'] rows = {} all_valid = True for pname in SEVEN_PLANETS: - bav_row = bav_results.get(pname, {}) - bindus = bav_row.get('bindus', []) + reduced = reduced_rows.get(pname) own_sign = source_sign_idx.get(pname) - if own_sign is None or len(bindus) != 12: + bindus = (available.get(pname) or {}).get('bindus') + if reduced is None or own_sign is None or bindus is None: all_valid = False continue - - bindu_at_own = bindus[own_sign] - sign_weight = 4 - planet_weight = PLANET_WEIGHTS.get(pname, 5) - rashi_pinda = bindu_at_own * sign_weight - graha_pinda = bindu_at_own * planet_weight + rashi_pinda = sum(value * weight for value, weight in zip(reduced, RASHI_PINDA_WEIGHTS)) + graha_pinda = sum( + reduced[source_sign_idx[body]] * PLANET_WEIGHTS[body] for body in SEVEN_PLANETS + ) rows[pname] = { 'sign': SIGNS[own_sign], - 'bindu_at_own_sign': bindu_at_own, - 'rashi_weight': sign_weight, - 'planet_weight': planet_weight, + 'bindu_at_own_sign': bindus[own_sign], 'rashi_pinda': rashi_pinda, 'graha_pinda': graha_pinda, 'yoga_pinda': rashi_pinda + graha_pinda, @@ -475,10 +505,14 @@ def calc_yoga_pinda(bav_results: Dict, planets: Dict, asc_sign_idx: int) -> Dict strongest = ranked[0][0] if ranked else None weakest = ranked[-1][0] if ranked else None total = sum(item['yoga_pinda'] for item in rows.values()) - return { 'method': 'Yoga Pinda', - 'version': '1.0', + 'version': '2.0', + 'profile': 'pvr_trikona_ekadhipatya_tables28_29', + 'rashi_weights': list(RASHI_PINDA_WEIGHTS), + 'planet_weights': dict(PLANET_WEIGHTS), + 'trikona_rows': trikona_rows, + 'sodhita_rows': reduced_rows, 'planets': rows, 'summary': { 'strongest_planet': strongest, @@ -487,7 +521,7 @@ def calc_yoga_pinda(bav_results: Dict, planets: Dict, asc_sign_idx: int) -> Dict 'average_yoga_pinda': round(total / len(rows), 2) if rows else 0, }, 'all_valid': all_valid and len(rows) == len(SEVEN_PLANETS), - 'validation_note': 'Yoga Pinda = Rashi Pinda + Graha Pinda;当前复用既有 v2.1 Shodhya Pinda 权重口径。', + 'validation_note': note, } @@ -569,5 +603,5 @@ def calc_ekadhipatya_shodhana( 'occupied_signs': sorted(occupied_signs), 'audit': audit, 'status': 'executed', - 'claim_boundary': 'Applied after Trikona Shodhana; source profile follows PyJHora/PVR rule ordering.', + 'claim_boundary': 'Applied after Trikona Shodhana. One occupied and one vacant sign with the same value clears the vacant sign (Raman 15(c)).', } diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py index 06ef4782..c5411b92 100644 --- a/scripts/jyotish_engine.py +++ b/scripts/jyotish_engine.py @@ -7254,9 +7254,9 @@ def render_pl9_markdown(packet: dict) -> str: if shodhya_pinda: out.extend([ '', - '#### Shodhya / Yoga Pinda(本地补充分值)', + '#### Shodhya / Yoga Pinda(PVR §12.7)', '', - '本表透传本地 Bindu 权重计算的 Rashi/Graha/Yoga Pinda。不等同于 PL9 第 54 页的 Trikona / Ekadhipatya reduction 前后矩阵;该页逐阶段结构仍未闭环,所有行仅作 parameter_sensitive 补充数据。', + '本表与 Sodhita 表用同一份削减:先三方净化,再单宫主净化。同值且一占一空时按 Raman 15(c) 清空空宫。星座权重按 PVR 表 28(处女座 6),七曜权重按表 29。这是具名口径,不是经典定论。', '', '| 行星 | Rashi Pinda | Graha Pinda | Yoga Pinda | 本命星座 Bindu | 状态 |', '|------|-------------|-------------|------------|----------------|------|', diff --git a/tests/test_ashtakavarga_pinda_source.py b/tests/test_ashtakavarga_pinda_source.py new file mode 100644 index 00000000..e75e4fb5 --- /dev/null +++ b/tests/test_ashtakavarga_pinda_source.py @@ -0,0 +1,44 @@ +from scripts.ashtakavarga import SEVEN_PLANETS, calc_ashtakavarga, calc_ekadhipatya_shodhana, calc_yoga_pinda +from scripts.ashtakavarga_shodhana import sodhita_chain + + +def test_pvr_example_43_full_sums_not_own_sign_shortcut(): + # PVR examples 40–43. This Mercury row is already reduced and stays put. + bav = {'Mercury': {'bindus': [3, 1, 3, 0, 0, 0, 0, 0, 0, 0, 2, 0]}} + planets = {name: {'sign': 'Leo'} for name in SEVEN_PLANETS} + for name in ('Sun', 'Mars', 'Mercury'): + planets[name] = {'sign': 'Gemini'} + planets['Venus'] = {'sign': 'Aries'} + row = calc_yoga_pinda(bav, planets, 0)['planets']['Mercury'] + assert (row['rashi_pinda'], row['graha_pinda'], row['yoga_pinda']) == (77, 75, 152) + + +def test_virgo_multiplier_and_zero_trine_convention(): + bav = {'Mercury': {'bindus': [0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0]}} + planets = {name: {'sign': 'Leo'} for name in SEVEN_PLANETS} + result = calc_yoga_pinda(bav, planets, 0) + assert result['sodhita_rows']['Mercury'][5] == 2 + assert result['planets']['Mercury']['rashi_pinda'] == 12 + assert result['planets']['Mercury']['graha_pinda'] == 0 + + +def test_missing_planet_does_not_silently_emit_partial_graha_sum(): + result = calc_yoga_pinda({'Sun': {'bindus': [1] * 12}}, {'Sun': {'sign': 'Leo'}}, 0) + assert result['all_valid'] is False + assert result['planets'] == {} + + +def test_raman_rule_15c_equal_pair_removes_only_vacant_value(): + row = [0] * 12 + row[0] = row[7] = 2 + result = calc_ekadhipatya_shodhana({'Moon': row}, {0})['rows']['Moon'] + assert result[0] == 2 + assert result[7] == 0 + + +def test_full_chart_pinda_reuses_the_sodhita_chain(): + planets = {name: {'sign': 'Leo'} for name in SEVEN_PLANETS} + bav = calc_ashtakavarga(planets, asc_sign_idx=0)['bav'] + pinda = calc_yoga_pinda(bav, planets, 0) + chain = sodhita_chain(bav, planets) + assert pinda['sodhita_rows'] == chain['sodhita_bav']