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 <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Grok (agent loop)
Cursor
parent
e8a7f3ac22
commit
6cbae4ce23
+17
-2
@@ -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`。
|
||||
|
||||
+34
-33
@@ -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
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user