diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 2fa992f7..bed3ca81 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -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 +- 复发自:无 - 修复版本:本分支待发布 diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py index 8bc1d54e..5fd87a3f 100644 --- a/scripts/jyotish_engine.py +++ b/scripts/jyotish_engine.py @@ -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 diff --git a/scripts/vimsopaka_calculator.py b/scripts/vimsopaka_calculator.py index 7337fb53..bf2c69c1 100644 --- a/scripts/vimsopaka_calculator.py +++ b/scripts/vimsopaka_calculator.py @@ -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]]: """ diff --git a/tests/test_cli_smoke.py b/tests/test_cli_smoke.py index 33bede87..1dfb09e9 100644 --- a/tests/test_cli_smoke.py +++ b/tests/test_cli_smoke.py @@ -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: diff --git a/tests/test_dasha.py b/tests/test_dasha.py index 17d83682..cccccde1 100644 --- a/tests/test_dasha.py +++ b/tests/test_dasha.py @@ -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):