diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index e2e95680..e16ccbe4 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -17735,3 +17735,18 @@ - 复发自:无。 - 修复版本:分支 `codex/upstream-sync7-20261014`,尚未合入 staging。 +## BUG-1319 | KP 落宫沿用本命中点判宫,传了宫头也不声明口径 + +- 状态:fixed-pending-verify +- 首次发现 / 最近更新:2026-10-14 / 2026-10-14 +- 来源:issue #56。上游 `0337b77f`。任务书 T2。 +- 现象:KP 用的行星落宫和本命 Bhava Chalit 表一样,按相邻宫头的中点判宫。宫头传进 KP 分析时,结果不写明判宫口径。 +- 触发条件:打开 KP。本命 Bhava Chalit 表不走这条。 +- 根因:同步第六轮漏了夹在上游隐私提交里的判宫修正。本命表和 KP 共用了中点函数。 +- 修复:KP 改成宫头到下一宫头。本命表默认仍是中点,宫位数字不变。传了宫头却不声明口径就拒绝;没传宫头记整宫代理。KP 报告写出判宫口径。 +- 验证:`tests/test_kp_cusp_house_assignment.py` 3 通过。同一颗星在两种口径下落入不同宫;默认中点与改前一致。 +- 防复发:未知口径直接报错;报告正文必须出现 House occupancy basis。 +- 相关记录:BUG-1318。 +- 复发自:无。 +- 修复版本:分支 `codex/upstream-sync7-20261014`,尚未合入 staging。 + diff --git a/scripts/bhava_chalit.py b/scripts/bhava_chalit.py index 17412e0a..54a0bf18 100644 --- a/scripts/bhava_chalit.py +++ b/scripts/bhava_chalit.py @@ -272,6 +272,20 @@ class BhavaChalitCalculator: # fallback: 最近的宫 return 1 + def _planet_bhava_from_cusps(self, planet_lon: float, cusps: List[float]) -> int: + """KP 宫头到宫头:宫 i(0 起)的范围是 cusp[i] → cusp[(i+1)%12]。 + + 落在下一宫头上的点归下一宫。本命 Bhava Chalit 表不用这条。 + """ + for i in range(12): + start = cusps[i] + end = cusps[(i + 1) % 12] + arc = _angular_dist(start, end) + pos = _angular_dist(start, planet_lon) + if arc > 0 and pos < arc: + return i + 1 + return 1 + def _planet_rashi_house(self, planet_lon: float, asc_lon: float) -> int: """整宫制宫位 (Rashi house)。""" p_si = _sign_idx(planet_lon) @@ -330,7 +344,8 @@ class BhavaChalitCalculator: asc_lon: float, mc_lon: float, house_system: str = 'sripati', jd: float = None, lat: float = None, - lon: float = None) -> Dict: + lon: float = None, + house_assignment: str = 'sandhi') -> Dict: """根据 Bhava 边界重新分配行星宫位。 Parameters @@ -346,13 +361,18 @@ class BhavaChalitCalculator: dict with keys: house_system, boundaries, planets """ + if house_assignment not in ('sandhi', 'cusp_to_cusp'): + raise ValueError("house_assignment must be 'sandhi' or 'cusp_to_cusp'") cusps = self.calculate_cusps(asc_lon, mc_lon, house_system, jd, lat, lon) sandhis = self.calculate_sandhis(cusps) planets = {} for pname, plon in planet_lons.items(): - bhava = self._planet_bhava(plon, sandhis) + if house_assignment == 'cusp_to_cusp': + bhava = self._planet_bhava_from_cusps(plon, cusps) + else: + bhava = self._planet_bhava(plon, sandhis) rashi = self._planet_rashi_house(plon, asc_lon) si = _sign_idx(plon) deg_in_sign = plon - si * 30 @@ -380,6 +400,7 @@ class BhavaChalitCalculator: return { 'house_system': house_system, 'house_system_desc': self.HOUSE_SYSTEMS.get(house_system, ''), + 'house_assignment': house_assignment, 'ascendant_lon': round(asc_lon, 4), 'mc_lon': round(mc_lon, 4), 'planets': planets, diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py index 4d85c653..e2f14143 100644 --- a/scripts/jyotish_engine.py +++ b/scripts/jyotish_engine.py @@ -13963,6 +13963,7 @@ def cmd_kp(args): ) bhava_chart = kp_house_calculator.get_bhava_chalit_chart( planet_lons, float(asc_lon), 0.0, 'placidus', jd, args.lat, args.lon, + house_assignment='cusp_to_cusp', ) bhava_planets = bhava_chart.get('planets') if isinstance(bhava_chart.get('planets'), dict) else {} @@ -13977,7 +13978,12 @@ def cmd_kp(args): 'house': bhava_row.get('bhava_house', pdata.get('house', 1)), } - result = calc_kp_analysis(planet_positions, asc_sign, house_cusps=boundaries.get('cusps')) + result = calc_kp_analysis( + planet_positions, + asc_sign, + house_cusps=boundaries.get('cusps'), + house_occupancy_basis='cusp_to_cusp', + ) moon_lon = planets.get('Moon', {}).get('degree_raw', planets.get('Moon', {}).get('degree', 0.0)) if isinstance(planets.get('Moon'), dict) else 0.0 try: birth_moment = datetime(int(args.year), int(args.month), int(args.day), int(args.hour), int(getattr(args, 'minute', 0) or 0)) diff --git a/scripts/kp_system.py b/scripts/kp_system.py index fcd1cbfb..09067377 100644 --- a/scripts/kp_system.py +++ b/scripts/kp_system.py @@ -371,6 +371,7 @@ def calc_kp_analysis( planet_positions: Dict, asc_sign: str = 'Aries', house_cusps: Optional[List[float]] = None, + house_occupancy_basis: Optional[str] = None, ) -> Dict: """ 完整KP分析(基于 diliprk/VedicAstro MIT 算法)。 @@ -379,6 +380,7 @@ def calc_kp_analysis( planet_positions: 行星位置 {planet: {'sign': str, 'degree': float, 'house': int}} asc_sign: 上升星座名称(无显式宫头时用于 whole-sign 代理) house_cusps: 可选的 12 个实际宫头黄经。提供时优先用于 KP 宫头与显著星。 + house_occupancy_basis: 传了宫头就必须声明判宫口径;没传宫头时记 whole_sign_proxy。 Returns: 完整KP分析结果 @@ -405,8 +407,11 @@ def calc_kp_analysis( } explicit_cusps = house_cusps is not None + if explicit_cusps and not house_occupancy_basis: + raise ValueError('house_occupancy_basis is required when house_cusps are provided') if explicit_cusps and len(house_cusps) != 12: raise ValueError('house_cusps must contain exactly 12 longitudes') + occupancy_basis = house_occupancy_basis if explicit_cusps else 'whole_sign_proxy' # 2. 构建宫位信息(含 KP lords)。无实际宫头时保留历史 whole-sign 中点代理。 houses = [] @@ -438,6 +443,7 @@ def calc_kp_analysis( 'source': 'diliprk/VedicAstro MIT License', 'maturity_profile': kp_maturity_profile(), 'house_basis': 'explicit_cusps' if explicit_cusps else 'whole_sign_proxy', + 'house_occupancy_basis': occupancy_basis, 'planets': {pname: {'kp_lords': data['kp_lords'], 'significators': planet_sig.get(pname, {})} for pname, data in kp_planets.items()}, 'houses': {h['house']: {'sign': h['sign'], 'cusp_longitude': h['cusp_longitude'], 'kp_lords': h['kp_lords'], 'significators': house_sig.get(h['house'], {})} @@ -494,6 +500,7 @@ def build_kp_report_pack( f"- claim_status: `{maturity.get('claim_status')}`", f"- truth_matrix_allowed: `{maturity.get('truth_matrix_allowed')}`", f"- closed_oracle_parity_assets: `{', '.join(closed_parity_assets.keys())}`", + f"- House occupancy basis: `{kp_analysis.get('house_occupancy_basis') or 'whole_sign_proxy'}`", f"- planet_count: `{len(planets)}`", f"- house_count: `{len(houses)}`", f"- dba_period_count: `{len(periods)}`", diff --git a/scripts/pl9_reader_export.py b/scripts/pl9_reader_export.py index 830ba468..f0ec9f66 100644 --- a/scripts/pl9_reader_export.py +++ b/scripts/pl9_reader_export.py @@ -2140,6 +2140,7 @@ def render_pl9_parity_markdown(packet: dict) -> str: '### p123 KP Chart / Cusps', '', f"- House basis:{_cell(kp.get('house_basis'))}", + f"- House occupancy basis:{_cell(kp.get('house_occupancy_basis'))}", '', '| House | Sign | Cusp longitude | KP lord chain |', '|-------|------|----------------|---------------|', diff --git a/tests/test_kp_cusp_house_assignment.py b/tests/test_kp_cusp_house_assignment.py new file mode 100644 index 00000000..e33fa041 --- /dev/null +++ b/tests/test_kp_cusp_house_assignment.py @@ -0,0 +1,58 @@ +"""BUG-1319: KP house occupancy is cusp-to-cusp and must be declared. + +Upstream 0337b77f. Natal Bhava Chalit stays on the sandhi midpoint rule. +""" + +from __future__ import annotations + +import os +import sys + +import pytest + +SCRIPTS = os.path.join(os.path.dirname(__file__), "..", "scripts") +if SCRIPTS not in sys.path: + sys.path.insert(0, SCRIPTS) + +from bhava_chalit import BhavaChalitCalculator # noqa: E402 +from kp_system import build_kp_report_pack, calc_kp_analysis # noqa: E402 + + +def test_cusp_to_cusp_differs_from_sandhi_and_default_houses_stay() -> None: + calc = BhavaChalitCalculator() + planets = {"Sun": 40.0} + sandhi = calc.get_bhava_chalit_chart(planets, 15.0, 270.0, "equal") + explicit = calc.get_bhava_chalit_chart( + planets, 15.0, 270.0, "equal", house_assignment="sandhi", + ) + cusp = calc.get_bhava_chalit_chart( + planets, 15.0, 270.0, "equal", house_assignment="cusp_to_cusp", + ) + assert sandhi["house_assignment"] == "sandhi" + assert explicit["planets"]["Sun"]["bhava_house"] == sandhi["planets"]["Sun"]["bhava_house"] == 2 + assert cusp["house_assignment"] == "cusp_to_cusp" + assert cusp["planets"]["Sun"]["bhava_house"] == 1 + assert cusp["planets"]["Sun"]["bhava_house"] != sandhi["planets"]["Sun"]["bhava_house"] + + +def test_unknown_house_assignment_is_rejected() -> None: + calc = BhavaChalitCalculator() + with pytest.raises(ValueError, match="house_assignment"): + calc.get_bhava_chalit_chart({"Sun": 10.0}, 15.0, 270.0, "equal", house_assignment="whole") + + +def test_kp_cusps_require_an_occupancy_basis_and_the_report_names_it() -> None: + planets = {"Sun": {"sign": "Taurus", "degree": 10.0, "house": 2}} + cusps = [15.0 + 30.0 * index for index in range(12)] + with pytest.raises(ValueError, match="house_occupancy_basis"): + calc_kp_analysis(planets, "Aries", house_cusps=cusps) + with pytest.raises(ValueError, match="house_occupancy_basis"): + calc_kp_analysis(planets, "Aries", house_cusps=cusps, house_occupancy_basis="") + declared = calc_kp_analysis( + planets, "Aries", house_cusps=cusps, house_occupancy_basis="cusp_to_cusp", + ) + assert declared["house_occupancy_basis"] == "cusp_to_cusp" + proxy = calc_kp_analysis(planets, "Aries") + assert proxy["house_occupancy_basis"] == "whole_sign_proxy" + markdown = build_kp_report_pack(declared)["exports"]["markdown"] + assert "House occupancy basis: `cusp_to_cusp`" in markdown