feat: add yoga context validation fixtures
This commit is contained in:
@@ -1,5 +1,41 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## v6.0.36(2026-06-05)—— Yoga 验证数据结构升级与 D9/Panchanga/Upagraha 上下文接入
|
||||
|
||||
> **目标**:为继续提升 Yoga 逻辑准确率补齐验证数据层,不再把所有 B.V. Raman / PyJHora 条件硬塞进 D1-only 近似,优先让标准测试集携带 D9/Navamsa、tithi/waning Moon、Gulika/Maandi 等上下文。
|
||||
|
||||
### 验证数据 schema 2.0
|
||||
|
||||
- 升级 `references/standard_test_charts.json` 为 `schema_version: "2.0"`,每张名人星盘除 PyJHora D1/Rasi Yoga 结果外,新增 `context`。
|
||||
- 新增上下文字段:
|
||||
- `context.d1`:D1 上升、上升度数、九行星星座/宫位/度数。
|
||||
- `context.d9`:D9/Navamsa 上升、上升度数、九行星星座/宫位/度数。
|
||||
- `context.panchanga`:tithi、paksha、`is_waning_moon`、原始 tithi 输出。
|
||||
- `context.upagraha`:Gulika、Maandi 的 D1 星座/宫位/度数。
|
||||
- 新增 `scripts/build_planet_positions_60.py`,从标准测试集的 context 重建 Skill-side `references/planet_positions_60.json`,保证 Skill 与 PyJHora 使用同一套 fixture 来源。
|
||||
- 修正 `_compute_one_chart.py` 输出星座名时使用英文 `SIGNS` 常量,避免 PyJHora `utils.RAASI_LIST` 中的星座符号污染 JSON;同时修正 D9 上升计算为 `d1_asc_sign * 30 + d1_asc_degree` 后再分入 Navamsa。
|
||||
|
||||
### Yoga 引擎上下文接入
|
||||
|
||||
- `YogaContext` 新增可选 `context` 参数,并提供 D9/Panchanga/Upagraha 查询接口:`d9_house_of()`、`d9_sign_of()`、`d9_lord_of_house()`、`navamsa_dispositor()`、`tithi()`、`is_waning_moon()`、`upagraha_house()`、`upagraha_sign()`。
|
||||
- `YogaEngine.detect()`、`detect_yogas()`、`detect_yogas_from_json()` 支持透传扩展 context。
|
||||
- `validate_logic_v2.py` 现在读取 `chart["context"]` 并传给 Yoga 引擎,后续 D9/tithi/Gulika 规则修复可以在同一验证框架里直接评估。
|
||||
- custom Yoga 沙箱新增上下文 helper:`d9_house_of`、`d9_sign_of`、`d9_lord_of_house`、`navamsa_dispositor`、`tithi`、`is_waning_moon`、`upagraha_house`、`gulika_house`、`maandi_house` 等。
|
||||
- 首批语义条件从 D1 近似升级为 context-aware:`navamsa_lagna_in_dry_planet_sign`、`lagna_lord_and_navamsa_lord_both_in_watery_signs`、`saturn_in_12th_with_waning_moon`、`sun_and_mandi_in_house`、若干 Navamsa dispositor 条件与 Gulika 相关条件。
|
||||
|
||||
### 当前验证结果
|
||||
|
||||
- `scripts/validate_logic_v2.py`:Precision **83.84%**,Recall **69.64%**,F1 **76.08%**。
|
||||
- 本轮重点是把验证框架升级到可承载 D9/Panchanga/Upagraha 规则,指标暂与 v6.0.35 持平;这说明上下文接入没有制造新回归,后续可以开始逐条修依赖扩展上下文的 Top FN/FP。
|
||||
- `scripts/run_quality_gate.py --skip-yoga-logic`:通过;完整 pytest **35 passed**,BPHS/Ashtakavarga 不变量 **18/18** 通过,golden case 通过。
|
||||
|
||||
### 后续优化点
|
||||
|
||||
- 下一轮可直接针对 context-aware 队列修复:`mathibhramana`、`matrunasa`、`nishkapata`、`bahu_puthra`、`vanchana_chora_bheethi` 等。
|
||||
- 可继续减少 D1-only 近似,把 PyJHora/B.V. Raman 中明确依赖 Navamsa、tithi、Gulika/Maandi 的条件逐条迁移到扩展上下文。
|
||||
|
||||
---
|
||||
|
||||
## v6.0.35(2026-06-05)—— Yoga 精度二次提升与 Top FP 收窄
|
||||
|
||||
> **目标**:在 v6.0.34 已大幅提升 F1 后,继续按验证报告 Top FP/FN 队列修正高影响规则,优先降低过宽误报,同时保持质量门禁通过。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: jyotish-vedic-astrology
|
||||
version: 6.0.35
|
||||
version: 6.0.36
|
||||
description: 印度占星(Jyotish)专业解盘与推运系统。核心能力:PDF星盘输入→严谨解盘→精确推运应期输出。触发词:印度占星、吠陀占星、Jyotish、解盘、推运、星盘分析、Dasha、Transit、Nakshatra、Yoga、出生时间矫正、PDF星盘、读取PDF、分析PDF星盘、现代解读、误判纠错、Varga分盘、综合分析、过境分析、合盘、婚姻匹配、年运盘、Prashna、Argala、Jaimini、Shadbala、Ashtakavarga、HTML报告、深度解盘。
|
||||
---
|
||||
|
||||
@@ -10,7 +10,7 @@ description: 印度占星(Jyotish)专业解盘与推运系统。核心能力
|
||||
> **严格路由**:`references/strict-workflow-router.md`(⭐涉及事业/婚恋/财务/应期/技法验证时必须优先读取)
|
||||
> **覆盖矩阵**:`references/technique-capability-matrix.md`(⭐判断技法 covered/partial/missing 时必须参考)
|
||||
> **机器注册表**:`references/technique_registry.json` + `scripts/audit_capabilities.py`(⭐用于自动审计与CI门禁)
|
||||
> **版本**:v6.0.35-yoga-precision-pass | **详细变更**:`CHANGELOG.md`
|
||||
> **版本**:v6.0.36-yoga-context-fixtures | **详细变更**:`CHANGELOG.md`
|
||||
|
||||
---
|
||||
|
||||
@@ -301,9 +301,9 @@ $PYTHON $SCRIPT <子命令> [参数]
|
||||
|
||||
---
|
||||
|
||||
**版本**:v6.0.35-yoga-precision-pass
|
||||
**版本**:v6.0.36-yoga-context-fixtures
|
||||
**创建日期**:2026-04-20
|
||||
**最后更新**:2026-06-05(v6.0.35 继续对齐 PyJHora/B.V. Raman 高影响 Yoga 条件,重点收窄 Top FP 并修复 Kahala 映射;当前 473 条 JSON 规则、379 条启用规则,D1/Rasi 逻辑验证 F1=76.08%,报告以 `references/validation_logic_report.json` 为准)
|
||||
**最后更新**:2026-06-05(v6.0.36 将标准 Yoga 验证数据升级为 schema 2.0,加入 D1/D9/Panchanga/Gulika/Maandi 扩展上下文,并让 YogaEngine/validate_logic_v2 接收 context;当前 473 条 JSON 规则、379 条启用规则,D1/Rasi 逻辑验证 F1=76.08%,报告以 `references/validation_logic_report.json` 为准)
|
||||
|
||||
---
|
||||
|
||||
|
||||
+8030
-61
File diff suppressed because it is too large
Load Diff
+15998
-62
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""用PyJhora计算单张星盘的Yoga列表 - 被build_standard_test_charts.py调用"""
|
||||
import sys, json, os, datetime, io
|
||||
import sys, json, datetime, io
|
||||
|
||||
# 抑制PyJhora的所有输出(stdout + stderr + logging)
|
||||
class SuppressAll:
|
||||
@@ -14,9 +14,15 @@ class SuppressAll:
|
||||
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
|
||||
PYJHORA_OK = True
|
||||
@@ -34,8 +40,43 @@ def tz_to_float(tz_str):
|
||||
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"""
|
||||
"""计算给定星盘的所有Yoga,并输出 D1/D9/tithi/Gulika 验证上下文。"""
|
||||
if not PYJHORA_OK:
|
||||
return {"error": f"PyJhora import failed: {IMPORT_ERR}"}
|
||||
try:
|
||||
@@ -43,21 +84,60 @@ def compute_yogas(chart):
|
||||
# 构造Place对象(timezone是浮点数小时)
|
||||
tz = tz_to_float(chart["tz"])
|
||||
place = Place(chart["city"], chart["lat"], chart["lon"], tz)
|
||||
|
||||
|
||||
# 构造datetime并转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")
|
||||
jd = utils.gregorian_to_jd(dt)
|
||||
|
||||
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 []
|
||||
|
||||
return {"yogas": yogas, "count": len(yogas), "status": "ok"}
|
||||
|
||||
d1_positions = drik.dhasavarga(jd, place, 1)[:9]
|
||||
d9_positions = drik.dhasavarga(jd, place, 9)[: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)
|
||||
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),
|
||||
},
|
||||
"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)}
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build Skill-side planet position fixtures from standard_test_charts context."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
STANDARD = ROOT / "references" / "standard_test_charts.json"
|
||||
OUTPUT = ROOT / "references" / "planet_positions_60.json"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
data = json.loads(STANDARD.read_text(encoding="utf-8"))
|
||||
charts = []
|
||||
for chart in data.get("charts", []):
|
||||
context = chart.get("context", {})
|
||||
d1 = context.get("d1", {})
|
||||
if not d1.get("planets") or not d1.get("ascendant"):
|
||||
raise ValueError(f"Missing D1 context for {chart.get('name')}")
|
||||
charts.append(
|
||||
{
|
||||
"name": chart["name"],
|
||||
"planets": d1["planets"],
|
||||
"ascendant": d1["ascendant"],
|
||||
"context": context,
|
||||
}
|
||||
)
|
||||
|
||||
output = {
|
||||
"schema_version": "2.0",
|
||||
"description": "Skill-side Yoga validation positions with D1/D9/Panchanga/Upagraha context",
|
||||
"charts": charts,
|
||||
}
|
||||
OUTPUT.write_text(json.dumps(output, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
print(f"Saved {len(charts)} charts to {OUTPUT}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -91,12 +91,18 @@ if __name__ == "__main__":
|
||||
print(f"Standard Test Charts Builder - {len(CELEBRITY_CHARTS)} charts")
|
||||
outpath = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "references", "standard_test_charts.json")
|
||||
os.makedirs(os.path.dirname(outpath), exist_ok=True)
|
||||
output = {"schema_version": "1.0", "description": "Standard test charts for yoga validation", "charts": []}
|
||||
output = {
|
||||
"schema_version": "2.0",
|
||||
"description": "Standard test charts for yoga validation with D1/D9/Panchanga/Upagraha context",
|
||||
"charts": [],
|
||||
}
|
||||
for chart in CELEBRITY_CHARTS:
|
||||
print(f" Computing {chart['name']}...", flush=True)
|
||||
yogas = compute_pyjhora_yogas(chart)
|
||||
entry = dict(chart)
|
||||
entry["expected_yogas"] = yogas.get("yogas", [])
|
||||
if "context" in yogas:
|
||||
entry["context"] = yogas["context"]
|
||||
entry["pyjhora_raw"] = yogas
|
||||
output["charts"].append(entry)
|
||||
with open(outpath, "w") as f:
|
||||
|
||||
@@ -21,6 +21,9 @@ COMPILE_TARGETS = [
|
||||
ROOT / "scripts" / "yoga_engine.py",
|
||||
ROOT / "scripts" / "audit_capabilities.py",
|
||||
ROOT / "scripts" / "validate_bphs_invariants.py",
|
||||
ROOT / "scripts" / "_compute_one_chart.py",
|
||||
ROOT / "scripts" / "build_standard_test_charts.py",
|
||||
ROOT / "scripts" / "build_planet_positions_60.py",
|
||||
ROOT / "tests" / "run_golden_cases.py",
|
||||
]
|
||||
|
||||
|
||||
@@ -279,9 +279,10 @@ def main():
|
||||
name = chart['name']
|
||||
planets = chart['planets']
|
||||
ascendant = chart['ascendant']
|
||||
context = chart.get('context')
|
||||
|
||||
# Skill 引擎检测
|
||||
skill_results = engine.detect(planets, ascendant)
|
||||
skill_results = engine.detect(planets, ascendant, context=context)
|
||||
skill_yoga_ids = set(r['rule_id'] for r in skill_results)
|
||||
skill_comp = skill_yoga_ids & comparable_rule_ids # 只保留可对比的
|
||||
total_skill_comp += len(skill_comp)
|
||||
|
||||
+136
-11
@@ -67,17 +67,30 @@ def _get_dignity_level(planet: str, sign: str) -> str:
|
||||
class YogaContext:
|
||||
"""Yoga 检测上下文。封装行星数据并提供各类查询方法。"""
|
||||
|
||||
def __init__(self, planets: Dict[str, Dict], ascendant: str):
|
||||
def __init__(self, planets: Dict[str, Dict], ascendant: str, context: Optional[Dict] = None):
|
||||
self.planets = planets # name -> {sign, house, degree, ...}
|
||||
self.ascendant = ascendant
|
||||
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.panchanga = self.context.get("panchanga", {}) if isinstance(self.context, dict) else {}
|
||||
self.upagraha = self.context.get("upagraha", {}) if isinstance(self.context, dict) else {}
|
||||
|
||||
# 预计算宫主星
|
||||
# 预计算 D1 宫主星
|
||||
self._house_lords: Dict[int, str] = {}
|
||||
for h in range(1, 13):
|
||||
sign = SIGNS[(self.asc_idx + h - 1) % 12]
|
||||
self._house_lords[h] = SIGN_LORDS[sign]
|
||||
|
||||
# 预计算 D9 宫主星(用于依赖 Navamsa 的 B.V. Raman Yoga)
|
||||
d9_asc = self.d9.get("ascendant")
|
||||
d9_asc_idx = SIGNS.index(d9_asc) if d9_asc in SIGNS else None
|
||||
self._d9_house_lords: Dict[int, str] = {}
|
||||
if d9_asc_idx is not None:
|
||||
for h in range(1, 13):
|
||||
sign = SIGNS[(d9_asc_idx + h - 1) % 12]
|
||||
self._d9_house_lords[h] = SIGN_LORDS[sign]
|
||||
|
||||
# --- 基础查询 ---
|
||||
def house_of(self, planet: str) -> Optional[int]:
|
||||
return self.planets.get(planet, {}).get("house")
|
||||
@@ -94,6 +107,34 @@ class YogaContext:
|
||||
def planets_in_house(self, house: int) -> List[str]:
|
||||
return [p for p, info in self.planets.items() if info.get("house") == house]
|
||||
|
||||
# --- D9 / Panchanga / Upagraha 扩展上下文 ---
|
||||
def d9_house_of(self, planet: str) -> Optional[int]:
|
||||
return self.d9.get("planets", {}).get(planet, {}).get("house")
|
||||
|
||||
def d9_sign_of(self, planet: str) -> Optional[str]:
|
||||
return self.d9.get("planets", {}).get(planet, {}).get("sign")
|
||||
|
||||
def d9_lord_of_house(self, house: int) -> Optional[str]:
|
||||
return self._d9_house_lords.get(house)
|
||||
|
||||
def navamsa_dispositor(self, planet: str) -> Optional[str]:
|
||||
sign = self.d9_sign_of(planet)
|
||||
return SIGN_LORDS.get(sign) if sign else None
|
||||
|
||||
def tithi(self) -> Optional[int]:
|
||||
return self.panchanga.get("tithi")
|
||||
|
||||
def is_waning_moon(self) -> bool:
|
||||
return bool(self.panchanga.get("is_waning_moon"))
|
||||
|
||||
def upagraha_house(self, name: str) -> Optional[int]:
|
||||
payload = self.upagraha.get(str(name).lower(), {})
|
||||
return payload.get("house")
|
||||
|
||||
def upagraha_sign(self, name: str) -> Optional[str]:
|
||||
payload = self.upagraha.get(str(name).lower(), {})
|
||||
return payload.get("sign")
|
||||
|
||||
# --- 尊严 ---
|
||||
def is_exalted(self, planet: str) -> bool:
|
||||
s = self.sign_of(planet)
|
||||
@@ -241,12 +282,12 @@ class YogaEngine:
|
||||
self.schema_version = data.get("schema_version", "1.0")
|
||||
self.rules = [r for r in data.get("rules", []) if r.get("enabled", True)]
|
||||
|
||||
def detect(self, planets: Dict[str, Dict], ascendant: str) -> List[Dict]:
|
||||
def detect(self, planets: Dict[str, Dict], ascendant: str, context: Optional[Dict] = None) -> List[Dict]:
|
||||
"""
|
||||
检测 Yoga。
|
||||
返回: [{name, name_cn, combination, effects, strength, source, bvr_id, category}, ...]
|
||||
"""
|
||||
ctx = YogaContext(planets, ascendant)
|
||||
ctx = YogaContext(planets, ascendant, context=context)
|
||||
results = []
|
||||
seen_dedup = set()
|
||||
|
||||
@@ -947,6 +988,19 @@ class YogaEngine:
|
||||
def movable_house(h):
|
||||
return house_sign(h) in MOVABLE_SIGNS
|
||||
|
||||
def d9_house_of(p): return ctx.d9_house_of(p)
|
||||
def d9_sign_of(p): return ctx.d9_sign_of(p)
|
||||
def d9_lord_of_house(h): return ctx.d9_lord_of_house(h)
|
||||
def navamsa_dispositor(p): return ctx.navamsa_dispositor(p)
|
||||
def tithi(): return ctx.tithi()
|
||||
def is_waning_moon(): return ctx.is_waning_moon()
|
||||
def upagraha_house(name): return ctx.upagraha_house(name)
|
||||
def upagraha_sign(name): return ctx.upagraha_sign(name)
|
||||
def gulika_house(): return ctx.upagraha_house("gulika")
|
||||
def maandi_house(): return ctx.upagraha_house("maandi")
|
||||
def gulika_sign(): return ctx.upagraha_sign("gulika")
|
||||
def maandi_sign(): return ctx.upagraha_sign("maandi")
|
||||
|
||||
def exal(p): return ctx.is_exalted(p)
|
||||
def lord_of_house(h): return ctx.lord_of_house(h)
|
||||
def sign(p): return ctx.sign_of(p)
|
||||
@@ -1008,6 +1062,12 @@ class YogaEngine:
|
||||
"only_malefics_in_house": only_malefics_in_house,
|
||||
"house_has_benefic": house_has_benefic, "house_has_malefic": house_has_malefic,
|
||||
"house_sign": house_sign, "movable_house": movable_house,
|
||||
"d9_house_of": d9_house_of, "d9_sign_of": d9_sign_of,
|
||||
"d9_lord_of_house": d9_lord_of_house, "navamsa_dispositor": navamsa_dispositor,
|
||||
"tithi": tithi, "is_waning_moon": is_waning_moon,
|
||||
"upagraha_house": upagraha_house, "upagraha_sign": upagraha_sign,
|
||||
"gulika_house": gulika_house, "maandi_house": maandi_house,
|
||||
"gulika_sign": gulika_sign, "maandi_sign": maandi_sign,
|
||||
"exal": exal, "lord_of_house": lord_of_house, "sign": sign,
|
||||
"check_amala_from": check_amala_from,
|
||||
"Benefics": BENEFICS, "Malefics": MALEFICS,
|
||||
@@ -1187,6 +1247,39 @@ class YogaEngine:
|
||||
p = cond.get("planet") or cond.get("to")
|
||||
by = cond.get("by") or cond.get("from")
|
||||
return success() if p in ctx.planets and by in ctx.planets and aspects(by, p) else []
|
||||
if ctype == "navamsa_lord_of_2_5_11_exalted_joins_9th_lord":
|
||||
l9 = house_lord(9)
|
||||
for h in (2, 5, 11):
|
||||
lord_h = house_lord(h)
|
||||
disp = ctx.navamsa_dispositor(lord_h) if lord_h else None
|
||||
if disp and l9 and disp in ctx.planets and ctx.is_exalted(disp) and related(disp, l9):
|
||||
return success()
|
||||
return []
|
||||
if ctype == "navamsa_lord_of_moon_exalted_in_rasi":
|
||||
disp = ctx.navamsa_dispositor("Moon")
|
||||
return success() if disp and disp in ctx.planets and ctx.is_exalted(disp) else []
|
||||
if ctype == "navamsa_dispositor_exalted_in_10th_with_lagna_lord":
|
||||
ref = cond.get("reference_planet")
|
||||
if ref == "10th_lord":
|
||||
ref_planet = house_lord(10)
|
||||
else:
|
||||
ref_planet = ref if ref in ctx.planets else None
|
||||
disp = ctx.navamsa_dispositor(ref_planet) if ref_planet else None
|
||||
l1 = house_lord(1)
|
||||
return success() if disp and l1 and disp in ctx.planets and ctx.is_exalted(disp) and ctx.house_of(disp) == 10 and related(disp, l1) else []
|
||||
if ctype == "navamsa_lord_of_4th_lord_in_12th":
|
||||
l4 = house_lord(4)
|
||||
disp = ctx.navamsa_dispositor(l4) if l4 else None
|
||||
return success() if disp and disp in ctx.planets and ctx.house_of(disp) == 12 else []
|
||||
if ctype == "moon_with_malefics_in_navamsa_cancer_scorpio":
|
||||
moon_sign = ctx.d9_sign_of("Moon")
|
||||
moon_house = ctx.d9_house_of("Moon")
|
||||
return success() if moon_sign in {"Cancer", "Scorpio"} and moon_house and any(ctx.d9_house_of(p) == moon_house for p in MALEFICS if p in ctx.planets and p != "Moon") else []
|
||||
if ctype == "sun_and_mandi_in_house":
|
||||
target = cond.get("house") or cond.get("in_house")
|
||||
if target is None and cond.get("house_offset") is not None:
|
||||
target = int(cond.get("house_offset")) + 1
|
||||
return success() if target and ctx.house_of("Sun") == target and ctx.upagraha_house("maandi") == target else []
|
||||
if ctype == "planet_conjunct_planet":
|
||||
a = cond.get("planet") or cond.get("a")
|
||||
b = cond.get("with") or cond.get("b")
|
||||
@@ -1233,7 +1326,13 @@ class YogaEngine:
|
||||
return success() if p and (related(p, "Saturn") or related(p, "Rahu")) else []
|
||||
if ctype == "malefic_in_lagna_or_gulika_in_trine_OR_gulika_with_kendra_trine_lord_OR_l1_with_rahu_sat_ket":
|
||||
l1 = house_lord(1)
|
||||
return success() if any(ctx.house_of(p) == 1 for p in MALEFICS if p in ctx.planets) or (l1 and any(related(l1, p) for p in ["Rahu", "Saturn", "Ketu"] if p in ctx.planets)) else []
|
||||
gh = ctx.upagraha_house("gulika")
|
||||
gulika_in_trine = gh in (1, 5, 9)
|
||||
kendra_trine_lords = {house_lord(h) for h in (1, 4, 5, 7, 9, 10)}
|
||||
gulika_with_kendra_trine_lord = gh is not None and any(
|
||||
lord and lord in ctx.planets and ctx.house_of(lord) == gh for lord in kendra_trine_lords
|
||||
)
|
||||
return success() if any(ctx.house_of(p) == 1 for p in MALEFICS if p in ctx.planets) or gulika_in_trine or gulika_with_kendra_trine_lord or (l1 and any(related(l1, p) for p in ["Rahu", "Saturn", "Ketu"] if p in ctx.planets)) else []
|
||||
if ctype == "same_lord_for_1st_and_4th":
|
||||
return success() if house_lord(1) == house_lord(4) else []
|
||||
if ctype == "1st_and_4th_lords_are_natural_or_temporal_friends":
|
||||
@@ -1265,10 +1364,14 @@ class YogaEngine:
|
||||
dry_signs = {"Aries", "Gemini", "Leo", "Virgo", "Sagittarius", "Capricorn"}
|
||||
return success() if l1 and ctx.sign_of(l1) in dry_signs else []
|
||||
if ctype == "navamsa_lagna_in_dry_planet_sign":
|
||||
return [] # 需要D9上下文,当前D1 Yoga验证中不强行近似
|
||||
d9_asc = ctx.d9.get("ascendant")
|
||||
dry_lords = {"Sun", "Mars", "Saturn"}
|
||||
return success() if d9_asc and SIGN_LORDS.get(d9_asc) in dry_lords else []
|
||||
if ctype == "lagna_lord_and_navamsa_lord_both_in_watery_signs":
|
||||
l1 = house_lord(1)
|
||||
return success() if l1 and ctx.sign_of(l1) in {"Cancer", "Scorpio", "Pisces"} else []
|
||||
d9_lagna_lord = SIGN_LORDS.get(ctx.d9.get("ascendant"))
|
||||
watery = {"Cancer", "Scorpio", "Pisces"}
|
||||
return success() if l1 and d9_lagna_lord and ctx.sign_of(l1) in watery and ctx.sign_of(d9_lagna_lord) in watery else []
|
||||
if ctype == "jupiter_in_lagna_or_aspects_lagna_from_watery":
|
||||
return success() if "Jupiter" in ctx.planets and (ctx.house_of("Jupiter") == 1 or (ctx.sign_of("Jupiter") in {"Cancer", "Scorpio", "Pisces"} and aspects("Jupiter", 1))) else []
|
||||
if ctype == "lagna_watery_with_benefics_or_lagna_lord_watery":
|
||||
@@ -1280,9 +1383,30 @@ class YogaEngine:
|
||||
if ctype == "saturn_in_lagna_mars_in_5_7_9":
|
||||
return success() if ctx.house_of("Saturn") == 1 and ctx.house_of("Mars") in (5, 7, 9) else []
|
||||
if ctype == "saturn_in_12th_with_waning_moon":
|
||||
return success() if ctx.house_of("Saturn") == 12 and ctx.house_of("Moon") == 12 else []
|
||||
return success() if ctx.house_of("Saturn") == 12 and ctx.is_waning_moon() else []
|
||||
if ctype == "moon_mercury_in_kendra_with_planet":
|
||||
return success() if ctx.house_of("Moon") == ctx.house_of("Mercury") and ctx.is_kendra(ctx.house_of("Moon")) and len(ctx.planets_in_house(ctx.house_of("Moon"))) >= 3 else []
|
||||
if ctype == "four_planet_chain_all_in_kendra_trine_or_exaltation":
|
||||
l1 = house_lord(1)
|
||||
if not l1:
|
||||
return []
|
||||
chain = [l1]
|
||||
current = l1
|
||||
for _ in range(2):
|
||||
sign_current = ctx.sign_of(current)
|
||||
current = SIGN_LORDS.get(sign_current) if sign_current else None
|
||||
if not current:
|
||||
return []
|
||||
chain.append(current)
|
||||
d9_disp = ctx.navamsa_dispositor(current)
|
||||
if not d9_disp:
|
||||
return []
|
||||
chain.append(d9_disp)
|
||||
ok = all(
|
||||
p in ctx.planets and (ctx.is_kendra(ctx.house_of(p)) or ctx.is_trikona(ctx.house_of(p)) or ctx.is_exalted(p))
|
||||
for p in chain
|
||||
)
|
||||
return success() if ok else []
|
||||
if ctype == "planets_in_relative_houses":
|
||||
base = house(cond.get("from", "lagna")) or 1
|
||||
offsets = cond.get("houses") or cond.get("offsets") or []
|
||||
@@ -1321,7 +1445,8 @@ class YogaEngine:
|
||||
# 便捷入口
|
||||
# ============================================================================
|
||||
def detect_yogas(planets: Dict[str, Dict], ascendant: str,
|
||||
rules_path: Optional[str] = None) -> List[Dict]:
|
||||
rules_path: Optional[str] = None,
|
||||
context: Optional[Dict] = None) -> List[Dict]:
|
||||
"""便捷函数:检测 Yoga"""
|
||||
if rules_path is None:
|
||||
# 自动查找规则文件
|
||||
@@ -1329,14 +1454,14 @@ def detect_yogas(planets: Dict[str, Dict], ascendant: str,
|
||||
skill_dir = os.path.dirname(script_dir)
|
||||
rules_path = os.path.join(skill_dir, "references", "yoga_rules.json")
|
||||
engine = YogaEngine(rules_path)
|
||||
return engine.detect(planets, ascendant)
|
||||
return engine.detect(planets, ascendant, context=context)
|
||||
|
||||
|
||||
def detect_yogas_from_json(chart_json: Dict, rules_path: Optional[str] = None) -> List[Dict]:
|
||||
"""从 chart JSON 结构中提取 planets + ascendant 并检测"""
|
||||
planets = chart_json.get("planets", {})
|
||||
asc = chart_json.get("ascendant", chart_json.get("ascendant_sign", "Aries"))
|
||||
return detect_yogas(planets, asc, rules_path)
|
||||
return detect_yogas(planets, asc, rules_path, context=chart_json.get("context"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user