diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 2650c216..cc627d5f 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -36593,3 +36593,18 @@ - 相关记录:无。 - 复发自:无。 - 修复版本:分支 `codex/upstream-sync8-20261017`。上游提交 `e7301feb`、`86935c0a`、`4f1536f2`。 + +## BUG-1387 | Saham 的 +30° 例外看错了操作数 + +- 状态:resolved +- 首次发现 / 最近更新:2026-10-17 / 2026-10-18 +- 来源:issue #80,任务书 T9。上游 `ce9fed15`。检索「add_30_degrees」「Punya_Saham」,无既有编号。 +- 现象:公式 Saham 在上升不落在第一操作数到第二操作数的顺行弧上时加 30°。规则文件七条公式的第三项都是上升,所以大量结果和 PVR 的判法差 30°。 +- 触发条件:`_calc_formula_saham`。本仓原先没有 trace 入口。 +- 根因:例外检查的是上升是否落在 A→B,PVR §28.8.1 检查的是第三操作数 C 是否落在 B→A。 +- 修复:两个入口都改为 `(C - B) % 360 <= (A - B) % 360`,不满足则结果加 30°。`references/saham_rules.json` 的描述改为同一条。补上 `_formula_saham_trace`,与数值入口使用同一判法。 +- 验证:`tests/test_saham_directed_operand_contract.py`。`tests/test_tajika.py` 三处断言按三栏改过:Punya 原值 85 / 新值 115;Karma 原值 40 / 新值 70;跨零点例子原值 340 / 新值 310。年运快照测试没有锁这三条公式的旧经度。 +- 防复发:契约测试覆盖 C 不是上升的公式,以及 trace 与数值入口一致。 +- 相关记录:无。 +- 复发自:无。 +- 修复版本:分支 `codex/upstream-sync8-20261017`。上游提交 `ce9fed15`。 diff --git a/references/saham_rules.json b/references/saham_rules.json index 3e6c87a2..0f90dd94 100644 --- a/references/saham_rules.json +++ b/references/saham_rules.json @@ -46,6 +46,6 @@ } }, "exception_rules": { - "add_30_degrees": "If the Ascendant does not fall between the first and the second point in the zodiacal order, add 30 degrees (1 sign) to the final result." + "add_30_degrees": "For A - B + C, add 30 degrees when C does not lie on the forward zodiacal arc from B to A. PVR Integrated Approach section 28.8.1, source page 394." } } diff --git a/scripts/tajika.py b/scripts/tajika.py index 73a5dc5c..f99ef5f9 100644 --- a/scripts/tajika.py +++ b/scripts/tajika.py @@ -96,14 +96,36 @@ def _calc_formula_saham( second = _resolve_saham_operand(formula[1], planet_lons, asc_lon, computed) third = _resolve_saham_operand(formula[2], planet_lons, asc_lon, computed) result = (third + (first - second)) % 360 - # references/saham_rules.json: add one sign when Ascendant is not on the - # forward zodiacal arc from the first formula point to the second. - ascendant_between_points = ( - first <= asc_lon <= second - if first <= second - else asc_lon >= first or asc_lon <= second - ) - return result if ascendant_between_points else (result + 30.0) % 360 + # PVR §28.8.1, p394: in A-B+C, C must lie on the forward arc B -> A. + third_between_points = (third - second) % 360 <= (first - second) % 360 + return result if third_between_points else (result + 30.0) % 360 + + +def _formula_saham_trace( + rule_name: str, + planet_lons: Dict[str, float], + asc_lon: float, + is_day: bool, + computed: Dict[str, float], +) -> Dict: + """Calculate a rule-backed Saham while preserving its sensitive convention.""" + rule = _SAHAM_RULES['sahams'][rule_name] + formula = rule['formula_day'] if is_day else rule['formula_night'] + first = _resolve_saham_operand(formula[0], planet_lons, asc_lon, computed) + second = _resolve_saham_operand(formula[1], planet_lons, asc_lon, computed) + third = _resolve_saham_operand(formula[2], planet_lons, asc_lon, computed) + base_longitude = (third + (first - second)) % 360 + third_between_points = (third - second) % 360 <= (first - second) % 360 + exception_applied = not third_between_points + return { + 'longitude': (base_longitude + 30.0) % 360 if exception_applied else base_longitude, + 'base_longitude': base_longitude, + 'rule_name': rule_name, + 'formula_operands': formula, + 'formula_variant': 'day' if is_day else 'night', + 'zodiacal_order_exception_applied': exception_applied, + 'rule_source': 'references/saham_rules.json', + } def calc_muntha(birth_asc_idx: int, age: int) -> Dict: diff --git a/tests/test_saham_directed_operand_contract.py b/tests/test_saham_directed_operand_contract.py new file mode 100644 index 00000000..127f3d7e --- /dev/null +++ b/tests/test_saham_directed_operand_contract.py @@ -0,0 +1,26 @@ +import pytest +from scripts import tajika + + +@pytest.mark.parametrize('sun,moon,asc,expected,exception', [ + (45, 120, 10, 115, True), + (45, 120, 60, 135, False), + (200, 100, 50, 310, False), + (200, 100, 150, 80, True), +]) +def test_punya_matches_pvr_directed_arc(sun, moon, asc, expected, exception): + points = {'Sun': sun, 'Moon': moon} + assert tajika._calc_formula_saham('Punya_Saham', points, asc, True, {}) == expected + result = tajika._formula_saham_trace('Punya_Saham', points, asc, True, {}) + assert result['longitude'] == expected + assert result['zodiacal_order_exception_applied'] is exception + + +def test_third_operand_is_used_even_when_not_ascendant(monkeypatch): + monkeypatch.setitem(tajika._SAHAM_RULES['sahams'], 'TestThird', { + 'formula_day': ['Jupiter', 'Moon', 'Venus'], + 'formula_night': ['Moon', 'Jupiter', 'Venus'], + }) + points = {'Jupiter': 200., 'Moon': 50., 'Venus': 100.} + assert tajika._calc_formula_saham('TestThird', points, 60., True, {}) == 250. + assert tajika._calc_formula_saham('TestThird', points, 60., False, {}) == 340. diff --git a/tests/test_tajika.py b/tests/test_tajika.py index d21667d9..5afbb228 100644 --- a/tests/test_tajika.py +++ b/tests/test_tajika.py @@ -285,7 +285,8 @@ class TestSahams: def test_punya_saham_formula(self): # Punya = Asc + (Moon - Sun), sourced from references/saham_rules.json sun, moon, asc = 45.0, 120.0, 10.0 - expected = (asc + (moon - sun)) % 360 # = 10 + 75 = 85 + # 原值 85 / 新值 115 / 原因:C=Asc10 不在 B=Sun45 → A=Moon120 的顺行弧上,按 PVR p394 加 30°。 + expected = 115.0 planet_lons = {'Sun': sun, 'Moon': moon, 'Mars': 90.0, 'Mercury': 60.0, 'Jupiter': 180.0, 'Venus': 210.0, 'Saturn': 300.0, 'Rahu': 150.0, 'Ketu': 330.0} @@ -298,14 +299,15 @@ class TestSahams: planet_lons = {'Sun': 45.0, 'Moon': 120.0, 'Mars': 90.0, 'Mercury': 60.0, 'Jupiter': 180.0, 'Venus': 210.0, 'Saturn': 300.0, 'Rahu': 150.0, 'Ketu': 330.0} - expected = (asc + (90.0 - 60.0)) % 360 + # 原值 40 / 新值 70 / 原因:C=Asc10 不在 B=Mercury60 → A=Mars90 的顺行弧上,加 30°。 + expected = 70.0 result = calc_all_sahams(planet_lons, asc, datetime(1990, 6, 15, 12, 0), lat=39.9042, lon=116.4074, tz=8) assert abs(result['karma_saham']['longitude'] - expected) < 0.01 def test_formula_saham_applies_reference_add_30_exception(self): from tajika import _calc_formula_saham - # Asc 50 is outside the forward 100 -> 200 zodiacal arc. + # 原值 340 / 新值 310 / 原因:C=Asc50 落在 B=Sun200 → A=Moon100 的跨零点顺行弧上,不加 30°。 result = _calc_formula_saham( 'Punya_Saham', {'Sun': 200.0, 'Moon': 100.0}, @@ -313,7 +315,7 @@ class TestSahams: True, {}, ) - assert result == 340.0 + assert result == 310.0 def test_is_faster_moon_vs_sun(self): assert _is_faster('Moon', 'Sun')