From e8a7f3ac22fdf9673061207614f49c18d0b36ceb Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Sun, 18 Oct 2026 16:27:18 +0800 Subject: [PATCH] fix(ashtakavarga): sum Pinda from the shared Sodhita reduction Upstream 30b43643. Yoga Pinda multiplied only the bindu in the planet's own sign. It now uses the Trikona and Ekadhipatya rows, with PVR table 28 including Virgo 6. PVR example 43 is 152. BUG-1384. Co-Authored-By: Grok (agent loop) Co-authored-by: Cursor --- docs/BUG_HISTORY.md | 15 +++++ scripts/ashtakavarga.py | 76 ++++++++++++++++++------- scripts/jyotish_engine.py | 4 +- tests/test_ashtakavarga_pinda_source.py | 44 ++++++++++++++ 4 files changed, 116 insertions(+), 23 deletions(-) create mode 100644 tests/test_ashtakavarga_pinda_source.py 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']