From 6cbae4ce237a99a9ce8690cb79aeecea65ce02e8 Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Sun, 18 Oct 2026 16:32:26 +0800 Subject: [PATCH] fix(bhava): use the named mirrored equal-house strength profile Upstream a4d4a249. Dig tables, Capricorn's first half, the sputa formula, and the benefic set now match jyotishganit_mirrored_equal_house_v2. Mercury is neutral. The result names that profile and does not call it a classical ruling. BUG-1385. Co-Authored-By: Grok (agent loop) Co-authored-by: Cursor --- docs/BUG_HISTORY.md | 19 ++++++- scripts/bhava_bala.py | 67 +++++++++++------------ tests/test_bhava_bala_vendor_profile.py | 71 +++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 35 deletions(-) create mode 100644 tests/test_bhava_bala_vendor_profile.py diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 153c6d4c..1119c9de 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -16309,7 +16309,7 @@ - 修复:`6feb1a4a`。报告改用正式三分量(Bhavadhipati = 宫主 Shadbala Virupa、Bhava Dig、Bhava Drishti;等宫),列 Total(Virupa / Rupa);本地评分改名、不再出现在任何结果里。其他使用者:无(补救、健康叙述只读 Shadbala Rupa)。 - 验证:`tests/test_bhava_bala_formal.py`;`tests/test_report_longform_gaps2.py`、`tests/test_shadbala_complete.py` 按三栏更新。 - 防复发:同上测试对报告全文 grep「(Benefic): +2」。 -- 相关记录:BUG-1208。 +- 相关记录:BUG-1208、BUG-1385(2026-10-18 三分量内部改成具名实现 jyotishganit_mirrored_equal_house_v2)。 - 复发自:无 - 修复版本:待发布 @@ -34407,7 +34407,7 @@ - 修复:`6feb1a4a`。报告改用正式三分量(Bhavadhipati = 宫主 Shadbala Virupa、Bhava Dig、Bhava Drishti;等宫),列 Total(Virupa / Rupa);本地评分改名、不再出现在任何结果里。其他使用者:无(补救、健康叙述只读 Shadbala Rupa)。 - 验证:`tests/test_bhava_bala_formal.py`;`tests/test_report_longform_gaps2.py`、`tests/test_shadbala_complete.py` 按三栏更新。 - 防复发:同上测试对报告全文 grep「(Benefic): +2」。 -- 相关记录:BUG-1208。 +- 相关记录:BUG-1208、BUG-1385(2026-10-18 三分量内部改成具名实现 jyotishganit_mirrored_equal_house_v2)。 - 复发自:无 - 修复版本:待发布 @@ -36563,3 +36563,18 @@ - 相关记录:BUG-1209。 - 复发自:无。 - 修复版本:分支 `codex/upstream-sync8-20261017`。上游提交 `30b43643`。 + +## BUG-1385 | Bhava Bala 的方向表、相位和吉星集合与所标来源不一致 + +- 状态:resolved +- 首次发现 / 最近更新:2026-10-17 / 2026-10-18 +- 来源:issue #80,任务书 T5。上游 `a4d4a249`。检索「BHAVA_STRENGTH_FROM_SIGN_NATURE」「vanaspati」,关联 BUG-1210。 +- 现象:报告和咨询卡的每宫 Bhava Bala 用错了方向表。人象和四足共用一张表,还多一档植物。摩羯前半被当成水象。相位是阶梯表,没有火、木、土的特殊角度。吉星拿了水星,没拿月亮。 +- 触发条件:`calc_bhava_bala` 和 `bhava_bala_from_chart`。 +- 根因:标成 jyotishganit,实际表和相位公式不是那套镜像角实现。 +- 修复:Dig 表、摩羯前半改四足、BPHS 分段相位、火木土特殊相位、吉星改为月亮木星金星。结果标明 `jyotishganit_mirrored_equal_house_v2`,并写明这不是经典定论。 +- 验证:`tests/test_bhava_bala_vendor_profile.py` 与供应商公式逐角对照。`tests/test_bhava_bala_formal.py` 通过。 +- 防复发:上述对照测试。 +- 相关记录:BUG-1210。 +- 复发自:无。 +- 修复版本:分支 `codex/upstream-sync8-20261017`。上游提交 `a4d4a249`。 diff --git a/scripts/bhava_bala.py b/scripts/bhava_bala.py index 77aa0ce9..79c9da88 100644 --- a/scripts/bhava_bala.py +++ b/scripts/bhava_bala.py @@ -35,36 +35,34 @@ SIGN_NATURE = { # 宫位力量基于星座性质(BPHS标准,单位Virupas) # 索引0-11对应第1-12宫 BHAVA_STRENGTH_FROM_SIGN_NATURE = { - 'nara': [30, 20, 10, 20, 10, 20, 30, 20, 10, 20, 10, 20], - 'chatushpada': [30, 20, 10, 20, 10, 20, 30, 20, 10, 20, 10, 20], - 'jalachara': [20, 30, 10, 30, 10, 30, 20, 30, 10, 30, 10, 30], - 'keeta': [10, 20, 30, 20, 30, 20, 10, 20, 30, 20, 30, 20], - 'vanaspati': [20, 10, 20, 10, 20, 30, 20, 10, 20, 10, 20, 30], + 'nara': [60, 50, 40, 30, 20, 10, 0, 10, 20, 30, 40, 50], + 'chatushpada': [30, 20, 10, 0, 10, 20, 30, 40, 50, 60, 50, 40], + 'jalachara': [30, 40, 50, 60, 50, 40, 30, 20, 10, 0, 10, 20], + 'keeta': [0, 10, 20, 30, 40, 50, 60, 50, 40, 30, 20, 10], } -# Sputa Drishti值表(基于角距离的连续相位力量) -def _get_sputa_drishti(angle_diff: float) -> float: - """基于jxotishganit MIT算法,将精确角距离转换为Sputa Drishti值(0-60)""" - ad = angle_diff % 360 +def _get_sputa_drishti(angle_diff: float, planet: str = 'Sun') -> float: + """jyotishganit 镜像角公式。火、木、土在特殊相位上另有一段。不是本仓方向性 Shadbala。""" + ad = abs(angle_diff) % 360 if ad > 180: ad = 360 - ad - - if ad <= 1.0: return 60.0 - if ad <= 3.0: return 57.0 - if ad <= 7.0: return 51.0 - if ad <= 10.0: return 45.0 - if ad <= 15.0: return 40.5 - if ad <= 20.0: return 30.0 - if ad <= 25.0: return 22.5 - if ad <= 30.0: return 15.0 - if ad <= 40.0: return 12.0 - if ad <= 50.0: return 9.0 - if ad <= 60.0: return 6.0 - if ad <= 75.0: return 4.5 - if ad <= 90.0: return 3.0 - if ad <= 120.0: return 2.25 - if ad <= 150.0: return 1.5 - return 0.0 + if ad < 30: + strength = 0.0 + elif ad < 60: + strength = (ad - 30) / 2 + elif ad < 90: + strength = ad - 45 + elif ad < 120: + strength = 30 + (120 - ad) / 2 + elif ad < 150: + strength = 150 - ad + else: + strength = 2 * (ad - 150) + for special in {'Mars': (90, 210), 'Jupiter': (120, 240), 'Saturn': (60, 270)}.get(planet, ()): + distance = abs(ad - special) + if distance <= 15: + strength = max(strength, 60 * (1 - distance / 15)) + return strength def calc_bhava_adhipathi_bala(house_signs: List[str], planet_shadbala: Dict) -> List[float]: @@ -109,8 +107,8 @@ def calc_bhava_dig_bala(house_signs: List[str], house_degrees: List[float]) -> L # 处理双性星座 if 'first_half' in nature_key: - # Sagittarius前半=nara, Capricorn前半=jalachara - real_nature = 'nara' if 'Sagittarius' in nature_key else 'jalachara' + # Sagittarius 前半是人象,Capricorn 前半是四足,不是水象。 + real_nature = 'nara' if 'Sagittarius' in nature_key else 'chatushpada' elif 'second_half' in nature_key: real_nature = 'chatushpada' if 'Sagittarius' in nature_key else 'jalachara' else: @@ -143,7 +141,7 @@ def calc_bhava_drik_bala(house_signs: List[str], house_degrees: List[float], Returns: 12个宫位的drik bala值 """ - BENEFICS = ['Jupiter', 'Venus', 'Mercury'] + BENEFICS = ['Jupiter', 'Venus', 'Moon'] MALEFICS = ['Saturn', 'Mars', 'Sun'] asc_sign_idx = SIGNS.index(asc_sign) if asc_sign in SIGNS else 0 @@ -175,7 +173,7 @@ def calc_bhava_drik_bala(house_signs: List[str], house_degrees: List[float], dist_deg = abs(diff) # Sputa Drishti值 - sputa = _get_sputa_drishti(dist_deg) + sputa = _get_sputa_drishti(dist_deg, planet_name) if planet_name in BENEFICS: benefic_sputa += sputa @@ -238,8 +236,11 @@ def calc_bhava_bala(house_signs: List[str], house_degrees: List[float], }) return { - 'method': 'Bhava Bala三元力量(基于jyotishganit MIT算法)', - 'version': '1.0', + 'method': 'jyotishganit_mirrored_equal_house_v2', + 'version': '2.0', + 'score_unit': 'Virupas', + 'method_boundary': '具名实现:镜像相位、吉星固定为月亮木星金星、水星中性、上升等宫中点。不是本仓方向性 Shadbala,也不是经典定论。', + 'claim_boundary': 'Bhava Bala 只是三分量原始分数,不能单独证明某项人生结果。', 'houses': houses, 'strongest_house': max(houses, key=lambda h: h['total'])['house'], 'weakest_house': min(houses, key=lambda h: h['total'])['house'], @@ -281,7 +282,7 @@ def bhava_bala_from_chart(planets: Dict, asc_sign: str, asc_degree: float, shadb house['lord'] = SIGN_LORDS.get(house['sign']) house['total_rupas'] = round(house['total'] / 60.0, 2) house.pop('level', None) - result['method'] = 'Bhava Bala 三分量:Bhavadhipati(宫主 Shadbala)+ Bhava Dig + Bhava Drishti(jyotishganit MIT 算法,等宫)' + result['method'] = 'jyotishganit_mirrored_equal_house_v2:宫主 Shadbala + Dig + Drishti。吉星为月亮、木星、金星,水星中性。不是经典定论。' result['unit'] = 'virupa' result['source'] = 'scripts/bhava_bala.py' return result diff --git a/tests/test_bhava_bala_vendor_profile.py b/tests/test_bhava_bala_vendor_profile.py new file mode 100644 index 00000000..195b2ef8 --- /dev/null +++ b/tests/test_bhava_bala_vendor_profile.py @@ -0,0 +1,71 @@ +"""Replay the vendored pure formula functions, not a second handwritten oracle. + +AST isolation avoids importing the vendor's unrelated Skyfield/network stack. +This establishes bounded formula parity, not full-engine or classical authority. +""" +import ast +import importlib.util +from pathlib import Path +from types import SimpleNamespace +import sys + +import pytest +from scripts import bhava_bala as local + +VENDOR=Path(__file__).resolve().parents[1]/'references/open_source_sources/jyotishganit/jyotishganit' + + +@pytest.fixture +def vendor(monkeypatch): + name='jyotishganit.core.constants' + spec=importlib.util.spec_from_file_location(name,VENDOR/'core/constants.py') + constants=importlib.util.module_from_spec(spec);spec.loader.exec_module(constants) + monkeypatch.setitem(sys.modules,name,constants) + names={'normalize','planet_longitude_from_sign','get_sputa_drishti_degree', + '_calculate_general_sputa','_calculate_planet_sputa', + 'compute_bhava_adhipathi_bala','compute_bhava_dig_bala','compute_bhava_drik_bala', + 'get_angular_distance_between_planets_and_house'} + path=VENDOR/'components/strengths.py' + parsed=ast.parse(path.read_text()) + functions=[node for node in parsed.body if isinstance(node,ast.FunctionDef) and node.name in names] + assert {f.name for f in functions}==names + module=ast.Module(body=[ast.ImportFrom(module='__future__',names=[ast.alias(name='annotations')],level=0),*functions],type_ignores=[]) + namespace=dict(vars(constants)) + exec(compile(ast.fix_missing_locations(module),str(path),'exec'),namespace) + return SimpleNamespace(**namespace) + + +@pytest.mark.parametrize('planet',['Sun','Moon','Mars','Mercury','Jupiter','Venus','Saturn']) +def test_aspect_boundaries_against_vendor(vendor,planet): + angles=[-360,-210,0,1,15,29.99999,30,45,59.99999,60,75,89.99999,90, + 105,119.99999,120,135,149.99999,150,165,179.99999,180,210,240,270,300,359.99999,360] + for angle in angles: + assert local._get_sputa_drishti(angle,planet)==pytest.approx(vendor.get_sputa_drishti_degree(angle,planet)) + + +@pytest.mark.parametrize('degree',[0.0001,14.9999,15,29.9999]) +def test_all_signs_at_all_house_indices(vendor,degree): + for sign in local.SIGNS: + chart=SimpleNamespace(houses=[SimpleNamespace(sign=sign,sign_degrees=degree) for _ in range(12)]) + vendor.compute_bhava_dig_bala(chart) + assert local.calc_bhava_dig_bala([sign]*12,[degree]*12)==[h.bhava_dig_bala for h in chart.houses] + + +def test_zero_degree_capricorn_uses_first_half(): + # Vendor's `(sign_degrees or 15)` misclassifies exact zero; honor the + # explicit first-half boundary rather than copying that input bug. + assert local.calc_bhava_dig_bala(['Capricorn']*12,[0]*12)==local.BHAVA_STRENGTH_FROM_SIGN_NATURE['chatushpada'] + + +def test_full_component_replay_on_synthetic_chart(vendor): + positions={p:{'sign':local.SIGNS[i],'degree':3.75+i*2} for i,p in enumerate(('Sun','Moon','Mars','Mercury','Jupiter','Venus','Saturn'))} + strengths={p:300+i*20 for i,p in enumerate(positions)} + planets=[SimpleNamespace(celestial_body=p,sign=row['sign'],sign_degrees=row['degree'],shadbala={'Shadbala':{'Total':strengths[p]}}) for p,row in positions.items()] + houses=[SimpleNamespace(sign=s,sign_degrees=12.3,lord=local.SIGN_LORDS[s]) for s in local.SIGNS] + chart=SimpleNamespace(planets=planets,houses=houses) + vendor.compute_bhava_adhipathi_bala(chart);vendor.compute_bhava_dig_bala(chart);vendor.compute_bhava_drik_bala(chart) + got=local.calc_bhava_bala(local.SIGNS,[12.3]*12,'Aries',12.3,positions,strengths) + for row,h in zip(got['houses'],houses): + for key,expected in [('adhipathi_bala',h.bhava_bala_adhipathi),('dig_bala',h.bhava_dig_bala),('drik_bala',h.bhava_drik_bala)]: + assert row[key]==round(expected,2) + assert row['total']==round(h.bhava_bala_adhipathi+h.bhava_dig_bala+h.bhava_drik_bala,2)