166 lines
6.2 KiB
Python
166 lines
6.2 KiB
Python
#!/usr/bin/env python3
|
||
"""用PyJhora计算单张星盘的Yoga列表 - 被build_standard_test_charts.py调用"""
|
||
import sys, json, datetime, io
|
||
|
||
# 抑制PyJhora的所有输出(stdout + stderr + logging)
|
||
class SuppressAll:
|
||
def __enter__(self):
|
||
self._stdout = sys.stdout
|
||
self._stderr = sys.stderr
|
||
sys.stdout = io.StringIO()
|
||
sys.stderr = io.StringIO()
|
||
return self
|
||
def __exit__(self, *a):
|
||
sys.stdout = self._stdout
|
||
sys.stderr = self._stderr
|
||
|
||
SIGNS = [
|
||
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
|
||
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
|
||
]
|
||
|
||
try:
|
||
with SuppressAll():
|
||
from jhora.horoscope.chart import yoga as yoga_mod
|
||
from jhora.panchanga import drik
|
||
from jhora.panchanga.drik import Place
|
||
import jhora.utils as utils
|
||
import swisseph as swe
|
||
PYJHORA_OK = True
|
||
except Exception as e:
|
||
PYJHORA_OK = False
|
||
IMPORT_ERR = str(e)
|
||
|
||
def tz_to_float(tz_str):
|
||
"""将 +05:30 格式转为 5.5 小时浮点数"""
|
||
tz_str = tz_str.strip()
|
||
sign = -1 if tz_str.startswith('-') else 1
|
||
tz_str = tz_str.lstrip('+-')
|
||
parts = tz_str.split(':')
|
||
hours = int(parts[0])
|
||
minutes = int(parts[1]) if len(parts) > 1 else 0
|
||
return sign * (hours + minutes / 60.0)
|
||
|
||
def _planet_dict_from_external_benchmark_positions(positions, asc_sign):
|
||
"""Convert PyJHora planet positions to the skill validation schema."""
|
||
names = {
|
||
0: "Sun",
|
||
1: "Moon",
|
||
2: "Mars",
|
||
3: "Mercury",
|
||
4: "Jupiter",
|
||
5: "Venus",
|
||
6: "Saturn",
|
||
7: "Rahu",
|
||
8: "Ketu",
|
||
}
|
||
out = {}
|
||
for pid, (sign_idx, degree) in positions:
|
||
if pid not in names:
|
||
continue
|
||
out[names[pid]] = {
|
||
"sign": SIGNS[sign_idx],
|
||
"house": ((sign_idx - asc_sign) % 12) + 1,
|
||
"degree": degree,
|
||
}
|
||
return out
|
||
|
||
|
||
def _upagraha_payload(name, longitude_pair, asc_sign):
|
||
sign_idx, degree = longitude_pair
|
||
return {
|
||
"name": name,
|
||
"sign": SIGNS[sign_idx],
|
||
"house": ((sign_idx - asc_sign) % 12) + 1,
|
||
"degree": degree,
|
||
}
|
||
|
||
|
||
def compute_yogas(chart):
|
||
"""计算给定星盘的所有Yoga,并输出 D1/D9/tithi/Gulika 验证上下文。"""
|
||
if not PYJHORA_OK:
|
||
return {"error": f"PyJhora import failed: {IMPORT_ERR}"}
|
||
try:
|
||
with SuppressAll():
|
||
# 构造Place对象(timezone是浮点数小时)
|
||
tz = tz_to_float(chart["tz"])
|
||
place = Place(chart["city"], chart["lat"], chart["lon"], tz)
|
||
|
||
# 构造datetime,转正确UTC JD(修复:PyJHora的gregorian_to_jd返回正午JD,无视时间)
|
||
date_str = chart["date"] + " " + chart["time"]
|
||
if date_str.count(':') == 1:
|
||
date_str += ":00"
|
||
dt = datetime.datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S")
|
||
utc_hour = dt.hour - tz + dt.minute / 60.0 + dt.second / 3600.0
|
||
jd = swe.julday(dt.year, dt.month, dt.day, utc_hour)
|
||
dob = drik.Date(dt.year, dt.month, dt.day)
|
||
tob = (dt.hour, dt.minute, dt.second)
|
||
|
||
# 计算 D1/Rasi Yoga。v2 逻辑正确性验证必须与 Skill 的 D1 Yoga 引擎逐层对齐;
|
||
# 不能使用 get_yoga_details_for_all_charts(),否则会把 D2/D9/D10 等分盘 Yoga 混入,
|
||
# 造成大量“Skill 漏检”的假阴性。
|
||
result = yoga_mod.get_yoga_details(jd, place, divisional_chart_factor=1)
|
||
yogas = list(result[0].keys()) if result and len(result) > 0 else []
|
||
|
||
d1_positions = drik.dhasavarga(jd, place, 1)[:9]
|
||
d9_positions = drik.dhasavarga(jd, place, 9)[:9]
|
||
d60_positions = drik.dhasavarga(jd, place, 60)[:9]
|
||
asc_info = drik.ascendant(jd, place)
|
||
d1_asc_sign, d1_asc_degree = asc_info[0], asc_info[1]
|
||
d9_asc_sign, d9_asc_degree = drik.dasavarga_from_long(d1_asc_sign * 30 + d1_asc_degree, 9)
|
||
d60_asc_sign, d60_asc_degree = drik.dasavarga_from_long(d1_asc_sign * 30 + d1_asc_degree, 60)
|
||
tithi_info = drik.tithi(jd, place)
|
||
tithi_no = int(tithi_info[0]) if tithi_info else None
|
||
lunar_phase = None
|
||
if tithi_no:
|
||
lunar_phase = "waxing" if tithi_no <= 15 else "waning"
|
||
gulika = drik.gulika_longitude(dob, tob, place, divisional_chart_factor=1)
|
||
maandi = drik.maandi_longitude(dob, tob, place, divisional_chart_factor=1)
|
||
|
||
context = {
|
||
"schema_version": "2.0",
|
||
"jd": jd,
|
||
"d1": {
|
||
"ascendant": SIGNS[d1_asc_sign],
|
||
"ascendant_degree": d1_asc_degree,
|
||
"planets": _planet_dict_from_external_benchmark_positions(d1_positions, d1_asc_sign),
|
||
},
|
||
"d9": {
|
||
"ascendant": SIGNS[d9_asc_sign],
|
||
"ascendant_degree": d9_asc_degree,
|
||
"planets": _planet_dict_from_external_benchmark_positions(d9_positions, d9_asc_sign),
|
||
},
|
||
"d60": {
|
||
"ascendant": SIGNS[d60_asc_sign],
|
||
"ascendant_degree": d60_asc_degree,
|
||
"planets": _planet_dict_from_external_benchmark_positions(d60_positions, d60_asc_sign),
|
||
},
|
||
"panchanga": {
|
||
"tithi": tithi_no,
|
||
"paksha": lunar_phase,
|
||
"is_waning_moon": lunar_phase == "waning",
|
||
"raw_tithi": tithi_info,
|
||
},
|
||
"upagraha": {
|
||
"gulika": _upagraha_payload("Gulika", gulika, d1_asc_sign),
|
||
"maandi": _upagraha_payload("Maandi", maandi, d1_asc_sign),
|
||
},
|
||
}
|
||
return {"yogas": yogas, "count": len(yogas), "status": "ok", "context": context}
|
||
except Exception as e:
|
||
return {"error": str(e)}
|
||
|
||
if __name__ == "__main__":
|
||
if len(sys.argv) >= 2:
|
||
chart = json.loads(sys.argv[1])
|
||
else:
|
||
# Read from stdin
|
||
raw = sys.stdin.read()
|
||
if not raw.strip():
|
||
print(json.dumps({"error": "No chart data"}))
|
||
sys.exit(1)
|
||
chart = json.loads(raw)
|
||
result = compute_yogas(chart)
|
||
# 最后一行必须是纯净JSON
|
||
print(json.dumps(result, ensure_ascii=False))
|