fix(vimsopaka): score sign-owner relationships on a 20-point scale (BUG-1261, upstream f32472b5)

This commit is contained in:
jesse-ux
2026-10-07 16:43:34 +08:00
parent db68e90816
commit ab9e30560a
5 changed files with 109 additions and 70 deletions
+16
View File
@@ -16932,4 +16932,20 @@
- 防复发:上述手算例(11° 双子、19° 天蝎等)锁住四张分盘的目标星座
- 相关记录:BUG-1261
- 复发自:无
- 修复版本:本分支 `db68e908`
## BUG-1261 | Vimsopaka 把落陷取消当成尊贵分,且十六分盘权重加总不是 20
- 状态:resolved
- 首次发现:2026-10-07
- 最近更新:2026-10-07
- 影响面:`scripts/vimsopaka_calculator.py`;全盘报告里的 Vimsopaka 分数与分档
- 用户现象:全自宫的十六分盘加总不是 20;落陷取消被记成 9 分的尊贵;分档名称借用 Vaiseshikamsa 的计数头衔
- 触发条件:计算 Shodasavarga 或 Dasavarga 的 Vimsopaka
- 根因:十六分盘 D9 权重是 3.5(合计 20.5),十分盘 D9 是 3.0(合计不是 20);尊贵分把自宫记成 15,并把落陷、落陷取消放进同一张表;最终分除以已给权重而不是 20。上游 `f32472b5` 已改,本仓未跟上
- 修复:十六分盘 D9 改为 3.0、十分盘 D9 改为 1.5,两组权重都合计 20。尊贵分改为入庙 / 根本三角 / 自宫 20、极友 18、友 15、中性 10、敌 7、极敌 5。落陷和落陷取消不再入表,传入则报错。最终分除以 20。分档改为 High / Moderate / Limited / Low。全盘装配改传星主关系(`skip_debilitation`),盘面标签仍保留落陷取消。解释句只陈述加权关系,不写成功能吉凶或必然结果
- 验证:`tests/test_dasha.py` 全自宫等于 20,以及落陷取消报错;`tests/test_cli_smoke.py` 中 Jupiter 的 Vimsopaka 不再写 Neecha Bhanga
- 防复发:全自宫反例与 ValueError 断言
- 相关记录:BUG-1260、BUG-1205
- 复发自:无
- 修复版本:本分支待发布
+15 -10
View File
@@ -651,8 +651,11 @@ def _build_dignity_context(chart_data):
}
return context
def _get_dignity_level(planet, sign, deg_in_sign=None, planets_data=None):
"""判断行星在某星座的尊严等级 (用于 Vimsopaka 映射), 包含五重敌友和落陷取消"""
def _get_dignity_level(planet, sign, deg_in_sign=None, planets_data=None, skip_debilitation=False):
"""判断行星在某星座的尊严等级, 包含五重敌友;默认也含落陷取消。
skip_debilitation 只给 Varga Viswa:落陷和落陷取消不是星主关系。
"""
if EXALTATION.get(planet) == sign:
return 'EXALTED'
@@ -667,7 +670,7 @@ def _get_dignity_level(planet, sign, deg_in_sign=None, planets_data=None):
if SIGN_LORDS.get(sign) == planet:
return 'OWN_SIGN'
if DEBILITATION.get(planet) == sign:
if not skip_debilitation and DEBILITATION.get(planet) == sign:
if _check_neecha_bhanga(planet, sign, planets_data):
return 'NEECHA_BHANGA'
return 'DEBILITATED'
@@ -706,7 +709,7 @@ def _get_dignity_level(planet, sign, deg_in_sign=None, planets_data=None):
return 'NEUTRAL'
def varga_dignity_level(planet, varga_planets):
def varga_dignity_level(planet, varga_planets, skip_debilitation=False):
"""Dignity of a body inside a divisional chart (Vimsopaka and the chart page alike).
The temporary half of the compound relationship is read from that varga's
@@ -717,7 +720,10 @@ def varga_dignity_level(planet, varga_planets):
if not isinstance(pinfo, dict) or pinfo.get('sign') not in SIGN_LORDS:
return None
deg_in_sign = pinfo.get('degree_in_sign', pinfo.get('degree', 0) % 30)
return _get_dignity_level(planet, pinfo['sign'], deg_in_sign, _build_dignity_context(varga_planets))
return _get_dignity_level(
planet, pinfo['sign'], deg_in_sign, _build_dignity_context(varga_planets),
skip_debilitation=skip_debilitation,
)
def _get_planet_status_label(planet, sign, deg_in_sign=None, planets_data=None):
@@ -15756,7 +15762,6 @@ def cmd_full_reading(args):
'OWN_SIGN': DignityLevel.OWN_SIGN, 'GREAT_FRIEND': DignityLevel.GREAT_FRIEND,
'FRIEND': DignityLevel.FRIEND, 'NEUTRAL': DignityLevel.NEUTRAL,
'ENEMY': DignityLevel.ENEMY, 'GREAT_ENEMY': DignityLevel.GREAT_ENEMY,
'DEBILITATED': DignityLevel.DEBILITATED, 'NEECHA_BHANGA': DignityLevel.NEECHA_BHANGA,
}
# 建立 VargaType → varga_result key 的映射(修复:每个分盘独立计算尊贵)
_vt_div = {VimsVargaType.D1:1, VimsVargaType.D2:2, VimsVargaType.D3:3,
@@ -15785,15 +15790,15 @@ def cmd_full_reading(args):
if div == 1:
dl_key = _get_dignity_level(pn, pinfo['sign'],
pinfo.get('degree_in_sign', pinfo.get('degree', 0) % 30),
planets)
planets, skip_debilitation=True)
else:
dl_key = varga_dignity_level(pn, vdata)
dl_key = varga_dignity_level(pn, vdata, skip_debilitation=True)
planet_vargas_input[pn][vt] = dignity_map.get(dl_key, DignityLevel.NEUTRAL)
elif div == 1:
# D1 直接用本命盘数据
p_sign = SIGNS[int(planet_lons.get(pn, 0) / 30) % 12]
d_in_s = planet_lons.get(pn, 0) % 30
dl_key = _get_dignity_level(pn, p_sign, d_in_s, planets)
dl_key = _get_dignity_level(pn, p_sign, d_in_s, planets, skip_debilitation=True)
planet_vargas_input[pn][vt] = dignity_map.get(dl_key, DignityLevel.NEUTRAL)
else:
planet_vargas_input[pn][vt] = DignityLevel.NEUTRAL
@@ -15801,7 +15806,7 @@ def cmd_full_reading(args):
# fallback: D1 从 planet_lons 计算
p_sign = SIGNS[int(planet_lons.get(pn, 0) / 30) % 12]
d_in_s = planet_lons.get(pn, 0) % 30
dl_key = _get_dignity_level(pn, p_sign, d_in_s, planets)
dl_key = _get_dignity_level(pn, p_sign, d_in_s, planets, skip_debilitation=True)
planet_vargas_input[pn][vt] = dignity_map.get(dl_key, DignityLevel.NEUTRAL)
else:
planet_vargas_input[pn][vt] = DignityLevel.NEUTRAL
+39 -52
View File
@@ -60,7 +60,7 @@ class VimsopakaBalaCalculator:
VargaType.D3: 1.0, # Drekkana
VargaType.D4: 0.5, # Chaturthamsa
VargaType.D7: 0.5, # Saptamsa
VargaType.D9: 3.5, # Navamsa
VargaType.D9: 3.0, # Navamsa;十六分盘权重合计 20
VargaType.D10: 0.5, # Dasamsa
VargaType.D12: 0.5, # Dwadasamsa
VargaType.D16: 2.0, # Shodasamsa
@@ -79,7 +79,7 @@ class VimsopakaBalaCalculator:
VargaType.D2: 1.5,
VargaType.D3: 1.5,
VargaType.D7: 1.5,
VargaType.D9: 3.0,
VargaType.D9: 1.5,
VargaType.D10: 1.5,
VargaType.D12: 1.5,
VargaType.D16: 1.5,
@@ -87,31 +87,24 @@ class VimsopakaBalaCalculator:
VargaType.D60: 5.0
}
# 尊严等级对应的Virupas(力量单位)
# 星主关系对应的 Virupas。落陷和落陷取消不是这项的输入。
DIGNITY_VIRUPAS = {
DignityLevel.EXALTED: 20.0,
DignityLevel.MOOLATRIKONA: 18.0,
DignityLevel.OWN_SIGN: 15.0,
DignityLevel.GREAT_FRIEND: 12.5,
DignityLevel.FRIEND: 10.0,
DignityLevel.NEUTRAL: 7.5,
DignityLevel.ENEMY: 5.0,
DignityLevel.GREAT_ENEMY: 3.5,
DignityLevel.DEBILITATED: 2.0,
DignityLevel.NEECHA_BHANGA: 9.0,
DignityLevel.MOOLATRIKONA: 20.0,
DignityLevel.OWN_SIGN: 20.0,
DignityLevel.GREAT_FRIEND: 18.0,
DignityLevel.FRIEND: 15.0,
DignityLevel.NEUTRAL: 10.0,
DignityLevel.ENEMY: 7.0,
DignityLevel.GREAT_ENEMY: 5.0,
}
# 力量等级分类(基于总分)
# 数值分档。不再借用 Vaiseshikamsa 的分盘计数名称。
STRENGTH_CATEGORIES = {
"Parijatamsa": (18.0, 20.0), # 极强(18-20)
"Uttamamsa": (16.0, 18.0), # 优秀(16-18)
"Gopuramsa": (12.0, 16.0), # 良好(12-16)
"Simhasanamsa": (10.0, 12.0), # 中等偏上(10-12)
"Paravatamsa": (8.0, 10.0), # 中等(8-10)
"Devalokamsa": (6.0, 8.0), # 中等偏下(6-8)
"Brahmalokamsa": (4.0, 6.0), # 较弱(4-6)
"Airavata": (2.0, 4.0), # 弱(2-4)
"Vaiseshikamsa": (0.0, 2.0) # 极弱(0-2)
"High": (15.0, 20.000001),
"Moderate": (10.0, 15.0),
"Limited": (5.0, 10.0),
"Low": (0.0, 5.0),
}
def __init__(self, mode: str = "shodasavarga"):
@@ -156,8 +149,10 @@ class VimsopakaBalaCalculator:
# 计算每个分盘的得分
for varga_type, dignity in vargas.items():
if varga_type in self.weights:
# 获取尊严对应的Virupas
virupas = self.DIGNITY_VIRUPAS.get(dignity, 0.0)
# 只接受星主关系。落陷或落陷取消不能当作尊贵分。
if dignity not in self.DIGNITY_VIRUPAS:
raise ValueError('Varga Viswa requires a sign-owner relationship, not debility/cancellation')
virupas = self.DIGNITY_VIRUPAS[dignity]
# 获取分盘权重
weight = self.weights[varga_type]
@@ -175,9 +170,9 @@ class VimsopakaBalaCalculator:
total_weighted_score += weighted_score
total_weight += weight
# 计算最终得分(归一化到20分制)
# 分母固定为 20,不按已提供分盘的权重再归一。
if total_weight > 0:
final_score = total_weighted_score / total_weight
final_score = total_weighted_score / 20.0
else:
final_score = 0.0
@@ -193,7 +188,12 @@ class VimsopakaBalaCalculator:
"category": category,
"varga_scores": varga_scores,
"interpretation": interpretation,
"mode": self.mode
"mode": self.mode,
"calculation_profile": "bphs_7_21_27_varga_viswa_compound_relationships",
"included_weight": total_weight,
"complete": set(vargas).issuperset(self.weights),
"missing_vargas": [v.value for v in self.weights if v not in vargas],
"category_basis": "Numeric score band; not Vaiseshikamsa varga-count titles",
}
def _get_strength_category(self, score: float) -> str:
@@ -201,34 +201,21 @@ class VimsopakaBalaCalculator:
for category, (min_score, max_score) in self.STRENGTH_CATEGORIES.items():
if min_score <= score < max_score:
return category
return "Vaiseshikamsa" # 默认最低等级
return "Unclassified"
def _generate_interpretation(self, planet: str, score: float, category: str, varga_scores: Dict) -> str:
"""生成解读文本"""
interpretation = f"{planet}的Vimsopaka Bala得分为{score:.2f}/20,属于{category}等级。\n\n"
# 根据等级给出解读
if score >= 16:
interpretation += "这是极强的行星力量,表示该行星在生活中能够充分发挥其积极作用,带来显著的成果和成功。"
elif score >= 12:
interpretation += "这是良好的行星力量,表示该行星能够有效发挥作用,在其主管领域带来正面影响。"
elif score >= 8:
interpretation += "这是中等的行星力量,表示该行星的作用较为平衡,既有积极面也有挑战。"
elif score >= 4:
interpretation += "这是较弱的行星力量,表示该行星在发挥作用时会遇到一些困难和阻碍。"
else:
interpretation += "这是极弱的行星力量,表示该行星难以充分发挥其作用,需要通过补救措施来增强。"
# 找出最强和最弱的分盘
"""陈述加权后的星主关系,不把分数说成吉凶或应验。"""
text = (
f"{planet}的 Vimsopaka Bala 为 {score:.2f}/20,数值分档 {category}。"
"这项只计分盘里的星主关系,不是功能吉凶,也不表示必然结果。"
)
if varga_scores:
sorted_vargas = sorted(varga_scores.items(), key=lambda x: x[1]['weighted_score'], reverse=True)
strongest = sorted_vargas[0]
weakest = sorted_vargas[-1]
interpretation += f"\n\n最强分盘:{strongest[0]}({strongest[1]['dignity']},得分{strongest[1]['weighted_score']})"
interpretation += f"\n最弱分盘:{weakest[0]}({weakest[1]['dignity']},得分{weakest[1]['weighted_score']})"
return interpretation
ranked = sorted(varga_scores.items(), key=lambda item: item[1]['virupas'], reverse=True)
text += f" 关系分最高:{ranked[0][0]}({ranked[0][1]['virupas']:g}/20)。"
text += f" 关系分最低:{ranked[-1][0]}({ranked[-1][1]['virupas']:g}/20)。"
if len(varga_scores) < len(self.weights):
text += " 输入不完整,这里只是已计入分盘的合计,不是完整分数。"
return text
def compare_planets(self, results: Dict[str, Dict]) -> List[Tuple[str, float]]:
"""
+8 -3
View File
@@ -757,9 +757,14 @@ def test_full_reading_maps_advanced_dignity_labels_into_vimsopaka_scores() -> No
assert jupiter_d9["dignity"] == "NEECHA_BHANGA"
jupiter_navamsa = result["modules"]["vimsopaka"]["Jupiter"]["varga_scores"]["Navamsa"]
assert jupiter_navamsa["dignity"] == "Neecha Bhanga"
assert isinstance(jupiter_navamsa["virupas"], (int, float))
assert jupiter_navamsa["virupas"] > 2.0
# 原值: dignity == "Neecha Bhanga",virupas > 2.0
# 新值: 星主关系之一,virupas 属于 {20, 18, 15, 10, 7, 5}
# 原因: BUG-1261,Vimsopaka 不再把落陷取消当作尊贵分;D9 标签本身仍是 NEECHA_BHANGA
assert jupiter_navamsa["dignity"] in {
"Exalted", "Moolatrikona", "Own Sign", "Great Friend", "Friend", "Neutral", "Enemy", "Great Enemy",
}
assert jupiter_navamsa["virupas"] in {20.0, 18.0, 15.0, 10.0, 7.0, 5.0}
assert "不是功能吉凶" in result["modules"]["vimsopaka"]["Jupiter"]["interpretation"]
def test_varga_cli_outputs_d9_and_d10() -> None:
+31 -5
View File
@@ -134,19 +134,45 @@ class TestDignityLevel(unittest.TestCase):
calculator = VimsopakaBalaCalculator(mode="shodasavarga")
results = calculator.calculate_vimsopaka_bala({
'Jupiter': {
VargaType.D9: DignityLevel.NEECHA_BHANGA,
# 原值: D9 = NEECHA_BHANGA,virupas 9.0
# 新值: D9 = FRIEND,virupas 15.0
# 原因: BUG-1261,Varga Viswa 只收星主关系,不收落陷取消
VargaType.D9: DignityLevel.FRIEND,
VargaType.D1: DignityLevel.GREAT_FRIEND,
VargaType.D10: DignityLevel.GREAT_ENEMY,
}
})
jupiter = results['Jupiter']
assert jupiter['varga_scores']['Navamsa']['dignity'] == 'Neecha Bhanga'
assert jupiter['varga_scores']['Navamsa']['virupas'] == 9.0
# 原值: Neecha Bhanga / 9.0;Great Friend 12.5;Great Enemy 3.5
# 新值: Friend / 15.0;Great Friend 18.0;Great Enemy 5.0
# 原因: BUG-1261,尊贵分改为入旺自宫 20、极友 18、友 15、中 10、敌 7、极敌 5
assert jupiter['varga_scores']['Navamsa']['dignity'] == 'Friend'
assert jupiter['varga_scores']['Navamsa']['virupas'] == 15.0
assert jupiter['varga_scores']['Rashi']['dignity'] == 'Great Friend'
assert jupiter['varga_scores']['Rashi']['virupas'] == 12.5
assert jupiter['varga_scores']['Rashi']['virupas'] == 18.0
assert jupiter['varga_scores']['Dasamsa']['dignity'] == 'Great Enemy'
assert jupiter['varga_scores']['Dasamsa']['virupas'] == 3.5
assert jupiter['varga_scores']['Dasamsa']['virupas'] == 5.0
with self.assertRaises(ValueError):
calculator.calculate_vimsopaka_bala({'Jupiter': {VargaType.D9: DignityLevel.NEECHA_BHANGA}})
with self.assertRaises(ValueError):
calculator.calculate_vimsopaka_bala({'Jupiter': {VargaType.D1: DignityLevel.DEBILITATED}})
def test_all_own_sign_shodasavarga_scores_twenty(self):
from vimsopaka_calculator import DignityLevel, VimsopakaBalaCalculator
calculator = VimsopakaBalaCalculator(mode="shodasavarga")
own = {varga: DignityLevel.OWN_SIGN for varga in calculator.SHODASAVARGA_WEIGHTS}
result = calculator.calculate_vimsopaka_bala({'Sun': own})['Sun']
self.assertEqual(sum(calculator.SHODASAVARGA_WEIGHTS.values()), 20)
self.assertEqual(sum(VimsopakaBalaCalculator.DASAVARGA_WEIGHTS.values()), 20)
self.assertEqual(result['total_score'], 20)
self.assertEqual(result['category'], 'High')
self.assertTrue(result['complete'])
self.assertEqual(result['missing_vargas'], [])
self.assertEqual(result['calculation_profile'], 'bphs_7_21_27_varga_viswa_compound_relationships')
self.assertIn('不是功能吉凶', result['interpretation'])
self.assertNotIn('成功', result['interpretation'])
class TestDashaCalculation(unittest.TestCase):