Files
Jyotisha/scripts/_compute_one_chart.py
T
732642856 0406723b7e v6.1.3: D9/D60 engine upgrade + 5 bottleneck rules tuned
Engine (yoga_engine.py):
- Added D60/Shashtiamsa context injection (d60_house_of, d60_sign_of, etc.)
- Added D9 dignity queries (is_exalted_in_d9, is_own_sign_in_d9, etc.)
- Added functional_malefics() / functional_benefics()
- Added is_shashtiamsa_evil/good() and vaiseshikamsa_score()
- Exposed all new methods to rule expression sandbox

Data (_compute_one_chart.py):
- Generate D60 positions alongside D1/D9
- Inject D60 into context for all computed charts

Rules (yoga_rules.json):
- thrikaala_gnana: D1 strong = exalted/own/moola only (kendra/trikona removed)
  + D9 strong check (at least 1 of 3 in D9 exalted/own/moola)
  FP: 12→0, FN: stable
- dharidhra: restored method1 (L2/L11 in dusthana + unfriendly navamsa + malefic aspect)
  FN reduced
- bandhubhisthyaktha: added is_shashtiamsa_evil(lord_4)
- nishkapata: added vaiseshikamsa_score(lord(1)) >= 15
- kapata: reverted functional_malefics (caused 22 FN), back to MALEFICS

Validation:
- FP: 45→39 (-6)
- FN: 72→67 (-5)
- F1: 94.35%→94.88% (+0.53%)
- Precision: 95.60%→96.18% (+0.58%)

New scripts:
- scripts/add_d60_to_test_charts.py: batch D60 augmentation
- scripts/ashtottari_dasha.py, yogini_dasha.py, kalachakra_dasha.py
- scripts/report_orchestrator.py

D60 data added to all 60 standard test charts.
2026-06-07 14:44:53 +08:00

166 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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_pyjhora_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_pyjhora_positions(d1_positions, d1_asc_sign),
},
"d9": {
"ascendant": SIGNS[d9_asc_sign],
"ascendant_degree": d9_asc_degree,
"planets": _planet_dict_from_pyjhora_positions(d9_positions, d9_asc_sign),
},
"d60": {
"ascendant": SIGNS[d60_asc_sign],
"ascendant_degree": d60_asc_degree,
"planets": _planet_dict_from_pyjhora_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))