fix(kp): place exact angular boundaries on the opening subdivision

Upstream 8eec9a54. Floating accumulation and a 120-degree remainder put star, pada, and sub-lord on different sides of the same boundary. Random longitudes away from those boundaries are unchanged. BUG-1383.

Co-Authored-By: Grok (agent loop)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jesse-ux
2026-10-18 16:20:35 +08:00
co-authored by Grok (agent loop) Cursor
parent 5be864a8e4
commit b5eb72343a
3 changed files with 91 additions and 33 deletions
+34 -33
View File
@@ -11,7 +11,10 @@ KP (Krishnamurti Paddhati) 占星系统模块
"""
import json
import math
from bisect import bisect_right
from datetime import datetime, timedelta
from fractions import Fraction
from pathlib import Path
from typing import Any, Dict, List, Tuple, Optional
@@ -44,6 +47,29 @@ VIMSHOTTARI_YEARS = dict(zip(KP_LORDS, VIMSHOTTARI_DURATION))
NAKSHATRA_SPAN = 360.0 / 27.0 # 13.333... degrees
def _angular_partition_starts():
"""Exact proportional starts, rounded once at the float API boundary."""
starts, owners = [], []
for star in range(27):
sub_start = Fraction(40 * star, 3)
for offset in range(9):
sub = (star + offset) % 9
sub_span = Fraction(40, 3) * VIMSHOTTARI_DURATION[sub] / 120
child_start = sub_start
for child_offset in range(9):
child = (sub + child_offset) % 9
starts.append(float(child_start))
owners.append((sub, child))
child_start += sub_span * VIMSHOTTARI_DURATION[child] / 120
sub_start += sub_span
return starts, owners
_KP_STARTS, _KP_OWNERS = _angular_partition_starts()
_STAR_STARTS = [float(Fraction(40 * i, 3)) for i in range(27)]
_PADA_STARTS = [float(Fraction(10 * i, 3)) for i in range(108)]
ROOT = Path(__file__).resolve().parents[1]
@@ -228,50 +254,25 @@ def get_kp_lords(degree: float) -> Dict:
Returns:
KP lords字典
"""
if isinstance(degree, bool) or not isinstance(degree, (int, float)) or not math.isfinite(degree):
raise ValueError('KP longitude must be a finite number')
deg = degree % 360
# 1. Sign lord
sign_index = int(deg // 30)
# 2. Nakshatra
nakshatra_index = int(deg // NAKSHATRA_SPAN) % 27
nakshatra_deg = deg % NAKSHATRA_SPAN
pada = int(nakshatra_deg // (NAKSHATRA_SPAN / 4)) + 1
# 3. Sublord & SubSubLord(KP核心算法)
# 将Vimshottari 120年周期按比例投影到度数上
deg_remainder = deg - 120 * int(deg / 120)
deg_cumulative = 0.0
for i in range(9):
deg_nl = NAKSHATRA_SPAN # 13.333... degrees per nakshatra
for j in range(i, i + 9):
j_mod = j % 9
deg_sl = deg_nl * VIMSHOTTARI_DURATION[j_mod] / 120.0
for k in range(j_mod, j_mod + 9):
k_mod = k % 9
deg_ss = deg_sl * VIMSHOTTARI_DURATION[k_mod] / 120.0
deg_cumulative += deg_ss
if deg_cumulative >= deg_remainder:
return {
'rasi_lord': SIGN_LORDS.get(SIGNS[sign_index], ''),
'sign': SIGNS[sign_index],
'nakshatra': NAKSHATRAS[nakshatra_index],
'nakshatra_lord': STAR_LORDS[nakshatra_index],
'pada': pada,
'sub_lord': KP_LORDS[j_mod],
'sub_sub_lord': KP_LORDS[k_mod],
}
# Fallback
# 2. Nakshatra. A shared boundary belongs to the subdivision that opens there.
nakshatra_index = bisect_right(_STAR_STARTS, deg) - 1
pada = (bisect_right(_PADA_STARTS, deg) - 1) % 4 + 1
sub, child = _KP_OWNERS[bisect_right(_KP_STARTS, deg) - 1]
return {
'rasi_lord': SIGN_LORDS.get(SIGNS[sign_index], ''),
'sign': SIGNS[sign_index],
'nakshatra': NAKSHATRAS[nakshatra_index],
'nakshatra_lord': STAR_LORDS[nakshatra_index],
'pada': pada,
'sub_lord': 'Unknown',
'sub_sub_lord': 'Unknown',
'sub_lord': KP_LORDS[sub],
'sub_sub_lord': KP_LORDS[child],
}