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.
This commit is contained in:
@@ -104,9 +104,11 @@ def compute_yogas(chart):
|
||||
|
||||
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
|
||||
@@ -128,6 +130,11 @@ def compute_yogas(chart):
|
||||
"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,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
"""为 standard_test_charts.json 批量补充 D60 (Shashtiamsa) 数据。"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from _compute_one_chart import compute_yogas
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
STANDARD = ROOT / "references" / "standard_test_charts.json"
|
||||
|
||||
def main() -> int:
|
||||
data = json.loads(STANDARD.read_text(encoding="utf-8"))
|
||||
charts = data.get("charts", [])
|
||||
print(f"Processing {len(charts)} charts for D60 augmentation...")
|
||||
|
||||
updated = 0
|
||||
for i, chart in enumerate(charts):
|
||||
name = chart.get("name", f"chart_{i}")
|
||||
# Skip if already has d60
|
||||
if chart.get("context", {}).get("d60"):
|
||||
print(f" [{i+1}/{len(charts)}] {name}: already has D60, skipping")
|
||||
continue
|
||||
|
||||
# Call compute_yogas to get full context with D60
|
||||
result = compute_yogas(chart)
|
||||
if "error" in result:
|
||||
print(f" [{i+1}/{len(charts)}] {name}: ERROR - {result['error']}")
|
||||
continue
|
||||
|
||||
new_context = result.get("context", {})
|
||||
d60_data = new_context.get("d60")
|
||||
if d60_data:
|
||||
chart["context"]["d60"] = d60_data
|
||||
updated += 1
|
||||
print(f" [{i+1}/{len(charts)}] {name}: D60 added (asc={d60_data.get('ascendant')})")
|
||||
else:
|
||||
print(f" [{i+1}/{len(charts)}] {name}: no D60 data generated")
|
||||
|
||||
# Write back
|
||||
STANDARD.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
print(f"\nDone. Updated {updated}/{len(charts)} charts with D60 data.")
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -102,6 +102,7 @@ class YogaContext:
|
||||
self.asc_idx = SIGNS.index(ascendant) if ascendant in SIGNS else 0
|
||||
self.context = context or {}
|
||||
self.d9 = self.context.get("d9", {}) if isinstance(self.context, dict) else {}
|
||||
self.d60 = self.context.get("d60", {}) if isinstance(self.context, dict) else {}
|
||||
self.panchanga = self.context.get("panchanga", {}) if isinstance(self.context, dict) else {}
|
||||
self.upagraha = self.context.get("upagraha", {}) if isinstance(self.context, dict) else {}
|
||||
|
||||
@@ -122,6 +123,15 @@ class YogaContext:
|
||||
sign = SIGNS[(d9_asc_idx + h - 1) % 12]
|
||||
self._d9_house_lords[h] = self._resolve_sign_lord(sign)
|
||||
|
||||
# 预计算 D60 宫主星(用于依赖 Shashtiamsa 的 B.V. Raman Yoga)
|
||||
d60_asc = self.d60.get("ascendant")
|
||||
d60_asc_idx = SIGNS.index(d60_asc) if d60_asc in SIGNS else None
|
||||
self._d60_house_lords: Dict[int, str] = {}
|
||||
if d60_asc_idx is not None:
|
||||
for h in range(1, 13):
|
||||
sign = SIGNS[(d60_asc_idx + h - 1) % 12]
|
||||
self._d60_house_lords[h] = self._resolve_sign_lord(sign)
|
||||
|
||||
def _sign_index_of_planet(self, planet: str) -> Optional[int]:
|
||||
sign = self.sign_of(planet)
|
||||
return SIGNS.index(sign) if sign in SIGNS else None
|
||||
@@ -249,6 +259,142 @@ class YogaContext:
|
||||
sign = self.d9_sign_of(planet)
|
||||
return self._resolve_sign_lord(sign) if sign else None
|
||||
|
||||
# --- D60 / Shashtiamsa 扩展上下文 ---
|
||||
def d60_house_of(self, planet: str) -> Optional[int]:
|
||||
return self.d60.get("planets", {}).get(planet, {}).get("house")
|
||||
|
||||
def d60_sign_of(self, planet: str) -> Optional[str]:
|
||||
return self.d60.get("planets", {}).get(planet, {}).get("sign")
|
||||
|
||||
def d60_lord_of_house(self, house: int) -> Optional[str]:
|
||||
return self._d60_house_lords.get(house)
|
||||
|
||||
def shashtiamsa_dispositor(self, planet: str) -> Optional[str]:
|
||||
sign = self.d60_sign_of(planet)
|
||||
return self._resolve_sign_lord(sign) if sign else None
|
||||
|
||||
# --- D9 尊严查询(用于跨分盘 Yoga 判断) ---
|
||||
def is_exalted_in_d9(self, planet: str) -> bool:
|
||||
s = self.d9_sign_of(planet)
|
||||
return s is not None and EXALTATION.get(planet) == s
|
||||
|
||||
def is_debilitated_in_d9(self, planet: str) -> bool:
|
||||
s = self.d9_sign_of(planet)
|
||||
return s is not None and DEBILITATION.get(planet) == s
|
||||
|
||||
def is_own_sign_in_d9(self, planet: str) -> bool:
|
||||
s = self.d9_sign_of(planet)
|
||||
return s is not None and self._resolve_sign_lord(s) == planet
|
||||
|
||||
def is_moolatrikona_in_d9(self, planet: str) -> bool:
|
||||
s = self.d9_sign_of(planet)
|
||||
return s is not None and MOOLATRIKONA_SIGN.get(planet) == s
|
||||
|
||||
def dignity_in_d9(self, planet: str) -> str:
|
||||
s = self.d9_sign_of(planet)
|
||||
return _get_dignity_level(planet, s) if s else 'NEUTRAL'
|
||||
|
||||
def is_friendly_navamsa(self, planet: str) -> bool:
|
||||
"""Check if planet is in friendly sign in D9 (used by dharidhra method1)."""
|
||||
s = self.d9_sign_of(planet)
|
||||
if not s:
|
||||
return False
|
||||
lord = self._resolve_sign_lord(s)
|
||||
return lord in FRIENDLY_PLANETS.get(planet, [])
|
||||
|
||||
def is_unfriendly_navamsa(self, planet: str) -> bool:
|
||||
"""Check if planet is in unfriendly/enemy sign in D9."""
|
||||
s = self.d9_sign_of(planet)
|
||||
if not s:
|
||||
return False
|
||||
lord = self._resolve_sign_lord(s)
|
||||
# Enemy if not friendly and not self
|
||||
if lord == planet:
|
||||
return False
|
||||
return lord not in FRIENDLY_PLANETS.get(planet, [])
|
||||
|
||||
# --- Functional Malefic / Benefic(基于月亮盈亏和宫主关系) ---
|
||||
def functional_malefics(self) -> List[str]:
|
||||
"""
|
||||
Return functional malefics for this chart.
|
||||
Rules (per PyJHora/B.V. Raman):
|
||||
- Lords of 3, 6, 8, 11, 12 are functional malefics
|
||||
- Mercury is malefic if conjunct with malefic
|
||||
- Moon is malefic if waning (Krishna Paksha)
|
||||
"""
|
||||
fm = []
|
||||
dusthana_lords = [self.lord_of_house(h) for h in [3, 6, 8, 11, 12]]
|
||||
fm.extend([l for l in dusthana_lords if l])
|
||||
|
||||
# Mercury becomes functional malefic if conjunct with natural malefic
|
||||
if 'Mercury' in self.planets:
|
||||
merc_h = self.house_of('Mercury')
|
||||
merc_conj_malefic = any(
|
||||
m in self.planets and self.house_of(m) == merc_h
|
||||
for m in ['Mars', 'Saturn', 'Rahu', 'Ketu', 'Sun']
|
||||
)
|
||||
if merc_conj_malefic and 'Mercury' not in fm:
|
||||
fm.append('Mercury')
|
||||
|
||||
# Waning Moon is functional malefic
|
||||
if self.is_waning_moon() and 'Moon' in self.planets and 'Moon' not in fm:
|
||||
fm.append('Moon')
|
||||
|
||||
return list(set(fm))
|
||||
|
||||
def functional_benefics(self) -> List[str]:
|
||||
"""Return functional benefics = all planets minus functional malefics minus natural malefics."""
|
||||
fm = set(self.functional_malefics())
|
||||
nm = set(MALEFICS)
|
||||
return [p for p in ALL_PLANETS if p in self.planets and p not in fm and p not in nm]
|
||||
|
||||
# --- Shashtiamsa / Vaiseshikamsa 简化评分 ---
|
||||
def is_shashtiamsa_evil(self, planet: str) -> bool:
|
||||
"""
|
||||
Simplified evil shashtiamsa check.
|
||||
In D60, if planet is debilitated, in enemy sign, or in dusthana house → evil.
|
||||
"""
|
||||
s = self.d60_sign_of(planet)
|
||||
h = self.d60_house_of(planet)
|
||||
if s is None or h is None:
|
||||
return False
|
||||
debil = DEBILITATION.get(planet) == s
|
||||
lord = self._resolve_sign_lord(s)
|
||||
enemy = lord not in FRIENDLY_PLANETS.get(planet, []) and lord != planet
|
||||
dusthana = h in [6, 8, 12]
|
||||
return debil or enemy or dusthana
|
||||
|
||||
def is_shashtiamsa_good(self, planet: str) -> bool:
|
||||
"""Good shashtiamsa: exalted, own sign, or moolatrikona in D60."""
|
||||
s = self.d60_sign_of(planet)
|
||||
if s is None:
|
||||
return False
|
||||
return EXALTATION.get(planet) == s or self._resolve_sign_lord(s) == planet or MOOLATRIKONA_SIGN.get(planet) == s
|
||||
|
||||
def vaiseshikamsa_score(self, planet: str) -> int:
|
||||
"""
|
||||
Simplified Vaiseshikamsa scoring (0-20 scale, higher = better).
|
||||
Combines D1 dignity + D9 dignity + D60 dignity.
|
||||
"""
|
||||
score = 0
|
||||
# D1 dignity (0-8)
|
||||
d1_dignity = self.dignity(planet)
|
||||
d1_scores = {'EXALTED': 8, 'OWN_SIGN': 7, 'MOOLATRIKONA': 6, 'NEUTRAL': 3, 'DEBILITATED': 0}
|
||||
score += d1_scores.get(d1_dignity, 3)
|
||||
|
||||
# D9 dignity (0-7)
|
||||
d9_dignity = self.dignity_in_d9(planet)
|
||||
score += d1_scores.get(d9_dignity, 3) - 1 # slightly lower weight
|
||||
|
||||
# D60 dignity (0-5)
|
||||
s60 = self.d60_sign_of(planet)
|
||||
if s60:
|
||||
d60_dignity = _get_dignity_level(planet, s60)
|
||||
d60_scores = {'EXALTED': 5, 'OWN_SIGN': 4, 'MOOLATRIKONA': 3, 'NEUTRAL': 2, 'DEBILITATED': 0}
|
||||
score += d60_scores.get(d60_dignity, 2)
|
||||
|
||||
return score
|
||||
|
||||
def tithi(self) -> Optional[int]:
|
||||
return self.panchanga.get("tithi")
|
||||
|
||||
@@ -1291,6 +1437,26 @@ class YogaEngine:
|
||||
"upagraha_house": upagraha_house, "upagraha_sign": upagraha_sign,
|
||||
"gulika_house": gulika_house, "maandi_house": maandi_house,
|
||||
"gulika_sign": gulika_sign, "maandi_sign": maandi_sign,
|
||||
# v6.1.3: D60 / Shashtiamsa 扩展
|
||||
"d60_house_of": lambda p: ctx.d60_house_of(p),
|
||||
"d60_sign_of": lambda p: ctx.d60_sign_of(p),
|
||||
"d60_lord_of_house": lambda h: ctx.d60_lord_of_house(h),
|
||||
"shashtiamsa_dispositor": lambda p: ctx.shashtiamsa_dispositor(p),
|
||||
"is_shashtiamsa_evil": lambda p: ctx.is_shashtiamsa_evil(p),
|
||||
"is_shashtiamsa_good": lambda p: ctx.is_shashtiamsa_good(p),
|
||||
# v6.1.3: D9 尊严扩展
|
||||
"is_exalted_in_d9": lambda p: ctx.is_exalted_in_d9(p),
|
||||
"is_debilitated_in_d9": lambda p: ctx.is_debilitated_in_d9(p),
|
||||
"is_own_sign_in_d9": lambda p: ctx.is_own_sign_in_d9(p),
|
||||
"is_moolatrikona_in_d9": lambda p: ctx.is_moolatrikona_in_d9(p),
|
||||
"dignity_in_d9": lambda p: ctx.dignity_in_d9(p),
|
||||
"is_friendly_navamsa": lambda p: ctx.is_friendly_navamsa(p),
|
||||
"is_unfriendly_navamsa": lambda p: ctx.is_unfriendly_navamsa(p),
|
||||
# v6.1.3: Functional malefic / benefic
|
||||
"functional_malefics": lambda: ctx.functional_malefics(),
|
||||
"functional_benefics": lambda: ctx.functional_benefics(),
|
||||
# v6.1.3: Vaiseshikamsa scoring
|
||||
"vaiseshikamsa_score": lambda p: ctx.vaiseshikamsa_score(p),
|
||||
"exal": exal, "lord_of_house": lord_of_house, "sign": sign,
|
||||
"check_amala_from": check_amala_from,
|
||||
"Benefics": BENEFICS, "Malefics": MALEFICS,
|
||||
|
||||
Reference in New Issue
Block a user