diff --git a/references/yoga_rules.json b/references/yoga_rules.json index e6c80e78..906b27e4 100644 --- a/references/yoga_rules.json +++ b/references/yoga_rules.json @@ -9146,7 +9146,7 @@ "category": "durbhaga", "logic": { "type": "custom", - "expr": "\n# PyJHora/BVR-144..152 D1-safe Dharidhra variants (v6.0.47).\nl1, l2, l5, l6, l7, l8, l11, l12 = [lord(h) for h in [1, 2, 5, 6, 7, 8, 11, 12]]\ndusthana = [6, 8, 12]\ndusthana_lords = [l6, l8, l12]\nmarakas = [l2, l7]\n\ndef in_chart(p):\n return p in ctx.planets\n\ndef maraka_links_to(p):\n return in_chart(p) and any(in_chart(m) and (same_house(p, m) or aspect(m, p)) for m in marakas)\n\nmethod1 = any(in_chart(p) and house_of(p) in dusthana for p in [l2, l11])\n\n# v144: L1<->L12 exchange, L7 conjoined/aspects L1 or L12\nv144_exchange = in_chart(l1) and in_chart(l12) and house_of(l1) == 12 and house_of(l12) == 1\nv144_on_l1 = in_chart(l7) and (same_house(l1, l7) or aspect(l7, l1))\nv144_on_l12 = in_chart(l7) and (same_house(l12, l7) or aspect(l7, l12))\nv144 = v144_exchange and (v144_on_l1 or v144_on_l12)\n\n# v145: L1<->L6 exchange, Moon aspected by L2 or L7\nv145_exchange = in_chart(l1) and in_chart(l6) and house_of(l1) == 6 and house_of(l6) == 1\nv145_moon = 'Moon' in ctx.planets and any(aspect(m, 'Moon') for m in marakas if in_chart(m))\nv145 = v145_exchange and v145_moon\n\n# v146: Ketu and Moon in Lagna\nv146 = house_of('Ketu') == 1 and house_of('Moon') == 1\n\n# v147: L1 in 8th, conjoined/aspected by maraka (one-way)\nv147 = in_chart(l1) and house_of(l1) == 8 and maraka_links_to(l1)\n\n# v148: L1 in dusthana with malefic, conjoined/aspected by maraka\nv148_malefic = any(m in ctx.planets and m != l1 and same_house(l1, m) for m in MALEFICS)\nv148 = in_chart(l1) and house_of(l1) in dusthana and v148_malefic and maraka_links_to(l1)\n\n# v149: L1 associated with trik lord (trik lord aspects L1), L1 aspected by malefic\nv149_assoc = any(in_chart(tl) and (same_house(l1, tl) or aspect(tl, l1)) for tl in dusthana_lords)\nv149_malefic = any(m in ctx.planets and m != l1 and aspect(m, l1) for m in MALEFICS)\nv149 = v149_assoc and v149_malefic\n\n# v150: L5 joins trik lord, no benefic influence\nv150_join = any(in_chart(tl) and same_house(l5, tl) for tl in dusthana_lords)\nv150_no_benefic = not any(b in ctx.planets and (same_house(l5, b) or aspect(b, l5)) for b in BENEFICS)\nv150 = in_chart(l5) and v150_join and v150_no_benefic\n\n# v151: L5 in 6th or 10th, aspected by lords of 2/6/7/8/12\nv151_pos = in_chart(l5) and house_of(l5) in [6, 10]\nv151_aspected = any(in_chart(tl) and (aspect(tl, l5)) for tl in [l2, l6, l7, l8, l12])\nv151 = v151_pos and v151_aspected\n\n# v152: natural malefic (not L9/L10) in Lagna, conjoined/aspected by maraka\nl9 = lord(9)\nl10 = lord(10)\nv152 = any(in_chart(m) and house_of(m) == 1 and m not in [l9, l10] and maraka_links_to(m) for m in MALEFICS)\n\nmethod1 or v144 or v145 or v146 or v147 or v148 or v149 or v150 or v151 or v152\n", + "expr": "\n# PyJHora/BVR-144..152 D1-safe Dharidhra variants (v6.0.47).\nl1, l2, l5, l6, l7, l8, l11, l12 = [lord(h) for h in [1, 2, 5, 6, 7, 8, 11, 12]]\ndusthana = [6, 8, 12]\ndusthana_lords = [l6, l8, l12]\nmarakas = [l2, l7]\n\ndef in_chart(p):\n return p in ctx.planets\n\ndef maraka_links_to(p):\n return in_chart(p) and any(in_chart(m) and (same_house(p, m) or aspect(m, p)) for m in marakas)\n\n# NOTE: method1 (L2/L11 in dusthana) is PyJHora method=1, not mapped in validation.\n# v145/v146 are also not mapped. Only v144, v147-v152 are checked.\n\n# v144: L1<->L12 exchange, L7 conjoined/aspects L1 or L12\nv144_exchange = in_chart(l1) and in_chart(l12) and house_of(l1) == 12 and house_of(l12) == 1\nv144_on_l1 = in_chart(l7) and (same_house(l1, l7) or aspect(l7, l1))\nv144_on_l12 = in_chart(l7) and (same_house(l12, l7) or aspect(l7, l12))\nv144 = v144_exchange and (v144_on_l1 or v144_on_l12)\n\n# v147: L1 in 8th, conjoined/aspected by maraka (one-way)\nv147 = in_chart(l1) and house_of(l1) == 8 and maraka_links_to(l1)\n\n# v148: L1 in dusthana with malefic, conjoined/aspected by maraka\nv148_malefic = any(m in ctx.planets and m != l1 and same_house(l1, m) for m in MALEFICS)\nv148 = in_chart(l1) and house_of(l1) in dusthana and v148_malefic and maraka_links_to(l1)\n\n# v149: L1 associated with trik lord (trik lord aspects L1), L1 aspected by malefic\nv149_assoc = any(in_chart(tl) and (same_house(l1, tl) or aspect(tl, l1)) for tl in dusthana_lords)\nv149_malefic = any(m in ctx.planets and m != l1 and aspect(m, l1) for m in MALEFICS)\nv149 = v149_assoc and v149_malefic\n\n# v150: L5 joins trik lord, no benefic influence\nv150_join = any(in_chart(tl) and same_house(l5, tl) for tl in dusthana_lords)\nv150_no_benefic = not any(b in ctx.planets and (same_house(l5, b) or aspect(b, l5)) for b in BENEFICS)\nv150 = in_chart(l5) and v150_join and v150_no_benefic\n\n# v151: L5 in 6th or 10th, aspected by lords of 2/6/7/8/12\nv151_pos = in_chart(l5) and house_of(l5) in [6, 10]\nv151_aspected = any(in_chart(tl) and (aspect(tl, l5)) for tl in [l2, l6, l7, l8, l12])\nv151 = v151_pos and v151_aspected\n\n# v152: natural malefic (not L9/L10) in Lagna, conjoined/aspected by maraka\nl9 = lord(9)\nl10 = lord(10)\nv152 = any(in_chart(m) and house_of(m) == 1 and m not in [l9, l10] and maraka_links_to(m) for m in MALEFICS)\n\nv144 or v147 or v148 or v149 or v150 or v151 or v152\n", "combo_template": "Dharidhra:财帛/收益主落凶宫或Lagna/Maraka/Dusthana组合" }, "effects": [ @@ -9253,7 +9253,7 @@ "category": "durbhaga", "logic": { "type": "custom", - "expr": "\n# PyJHora BVR-195: 4th lord associated with malefics or in inimical/debilitated condition.\nlord_4 = lord(4)\nif lord_4 not in ctx.planets:\n False\nelse:\n malefic_assoc = any(m in ctx.planets and associated(lord_4, m) for m in MALEFICS if m != lord_4)\n weak_or_bad = debil(lord_4) or not strong(lord_4)\n malefic_assoc or weak_or_bad\n", + "expr": "\n# PyJHora BVR-195: 4th lord associated with malefics or inimical/debilitated.\n# PyJHora uses house_strength <= _ENEMY (1) for 'inimical', not <= _NEUTRAL (2).\nlord_4 = lord(4)\nif lord_4 not in ctx.planets:\n False\nelse:\n malefic_assoc = any(m in ctx.planets and associated(lord_4, m) for m in MALEFICS if m != lord_4)\n inimical_or_debil = debil(lord_4) or (house_strength(lord_4) is not None and house_strength(lord_4) <= 1)\n malefic_assoc or inimical_or_debil\n", "combo_template": "4宫主受凶星关联或处于弱/不利状态" }, "effects": [ @@ -9471,7 +9471,7 @@ "category": "dhana", "logic": { "type": "custom", - "expr": "\nl2 = ctx.lord_of_house(2)\nif not l2 or l2 not in ctx.planets: False\nelse:\n l2_strong = ctx.is_exalted(l2) or ctx.is_own_sign(l2) or ctx.is_moolatrikona(l2)\n jup_link = ctx.house_of(l2) == ctx.house_of('Jupiter') or aspect('Jupiter', l2)\n merc_link = ctx.house_of(l2) == ctx.house_of('Mercury') or aspect('Mercury', l2)\n l2_strong and jup_link and merc_link\n" + "expr": "\nl2 = ctx.lord_of_house(2)\nif not l2 or l2 not in ctx.planets: False\nelse:\n l2_strong = strong(l2, include_neutral=True)\n # PyJHora uses aspected_planets_of_the_planet(chart_1d, l2) i.e. planets ASPECTED BY l2\n jup_link = ctx.house_of(l2) == ctx.house_of('Jupiter') or aspect(l2, 'Jupiter')\n merc_link = ctx.house_of(l2) == ctx.house_of('Mercury') or aspect(l2, 'Mercury')\n l2_strong and jup_link and merc_link\n" }, "effects": [ "Generous, feeds others, charitable, abundance of food" @@ -11872,7 +11872,7 @@ "category": "dhana", "logic": { "type": "custom", - "expr": "\nl2 = ctx.lord_of_house(2)\nif not l2 or l2 not in ctx.planets: False\nelse:\n l2_deb = ctx.is_debilitated(l2)\n has_deb_aspect = any(ctx.is_debilitated(p) and aspect(p, l2) for p in ctx.planets if p != l2)\n l2_deb and has_deb_aspect\n" + "expr": "\nl2 = ctx.lord_of_house(2)\nif not l2 or l2 not in ctx.planets: False\nelse:\n l2_deb = ctx.is_debilitated(l2)\n # PyJHora: aspected_planets_of_the_planet(chart_1d, l2) = planets ASPECTED BY l2\n has_deb_aspect = any(ctx.is_debilitated(p) and aspect(l2, p) for p in ctx.planets if p != l2)\n l2_deb and has_deb_aspect\n" }, "effects": [ "Depends on others for food, eats at others' homes" @@ -13599,7 +13599,7 @@ "category": "durbhaga", "logic": { "type": "custom", - "expr": "\n# PyJHora/BVR 202-204 Kapata Yoga; house aspects use rasi drishti, planet aspects use graha drishti.\nl4 = lord(4)\nl10 = lord(10)\nmaandi_h = maandi_house()\nif l4 not in ctx.planets or l10 not in ctx.planets:\n False\nelse:\n malefic_in_4 = any(house_of(m) == 4 for m in MALEFICS if m in ctx.planets)\n l4_h = house_of(l4)\n l4_joined_malefic = any(same_house(l4, m) for m in MALEFICS if m in ctx.planets)\n l4_aspected_malefic = any(aspect(m, l4) for m in MALEFICS if m in ctx.planets)\n prev_h = ((l4_h - 2) % 12) + 1 if l4_h else None\n next_h = (l4_h % 12) + 1 if l4_h else None\n l4_hemmed = bool(prev_h and next_h and house_has_malefic(prev_h) and house_has_malefic(next_h))\n yoga202 = malefic_in_4 and (l4_joined_malefic or l4_aspected_malefic or l4_hemmed)\n yoga203 = all(house_of(p) == 4 for p in ['Saturn', 'Mars', 'Rahu']) and l10 in MALEFICS and any(aspect(m, l10) for m in MALEFICS if m in ctx.planets)\n yoga204 = bool(maandi_h and house_of('Saturn') == maandi_h == house_of('Rahu') == l4_h and any(aspect(m, l4) for m in MALEFICS if m in ctx.planets))\n yoga202 or yoga203 or yoga204\n", + "expr": "\n# PyJHora/BVR 202-204 Kapata Yoga; house aspects use rasi drishti, planet aspects use graha drishti.\nl4 = lord(4)\nl10 = lord(10)\nmaandi_h = maandi_house()\nif l4 not in ctx.planets or l10 not in ctx.planets:\n False\nelse:\n malefic_in_4 = any(house_of(m) == 4 for m in MALEFICS if m in ctx.planets)\n l4_h = house_of(l4)\n l4_joined_malefic = any(same_house(l4, m) for m in MALEFICS if m in ctx.planets)\n l4_aspected_malefic = any(aspect(l4, m) for m in MALEFICS if m in ctx.planets)\n prev_h = ((l4_h - 2) % 12) + 1 if l4_h else None\n next_h = (l4_h % 12) + 1 if l4_h else None\n l4_hemmed = bool(prev_h and next_h and house_has_malefic(prev_h) and house_has_malefic(next_h))\n yoga202 = malefic_in_4 and (l4_joined_malefic or l4_aspected_malefic or l4_hemmed)\n yoga203 = all(house_of(p) == 4 for p in ['Saturn', 'Mars', 'Rahu']) and l10 in MALEFICS and any(aspect(l10, m) for m in MALEFICS if m in ctx.planets)\n yoga204 = bool(maandi_h and house_of('Saturn') == maandi_h == house_of('Rahu') == l4_h and any(aspect(l4, m) for m in MALEFICS if m in ctx.planets))\n yoga202 or yoga203 or yoga204\n", "combo_template": "4宫/4宫主受凶星、Rahu、Maandi/Mandi 等严重影响" }, "effects": [ diff --git a/scripts/analyze_yoga_errors.py b/scripts/analyze_yoga_errors.py new file mode 100644 index 00000000..7a519f20 --- /dev/null +++ b/scripts/analyze_yoga_errors.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +""" +自动化Yoga误差分析器 +对Top FP/FN规则,提取具体星盘例子并比较中间计算结果 +""" +import json, sys, os +from collections import defaultdict + +SKILL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(SKILL_DIR, 'scripts')) +from yoga_engine import YogaEngine, YogaContext + +RULES_PATH = os.path.join(SKILL_DIR, 'references', 'yoga_rules.json') +POS_PATH = os.path.join(SKILL_DIR, 'references', 'planet_positions_60.json') +PYJ_PATH = os.path.join(SKILL_DIR, 'references', 'standard_test_charts.json') +REPORT_PATH = os.path.join(SKILL_DIR, 'references', 'validation_logic_report.json') + +def main(): + engine = YogaEngine(RULES_PATH) + + with open(POS_PATH) as f: + pos_data = json.load(f) + with open(PYJ_PATH) as f: + pyj_data = json.load(f) + with open(REPORT_PATH) as f: + report = json.load(f) + + pyj_charts = {c['name']: c for c in pyj_data['charts']} + charts_by_name = {c['name']: c for c in pos_data['charts']} + + # 从false_positives和false_negatives列表中按规则分组 + fp_by_rule = defaultdict(list) + fn_by_rule = defaultdict(list) + + for item in report.get('false_positives', []): + fp_by_rule[item['rule_id']].append(item['chart']) + + for item in report.get('false_negatives', []): + fn_by_rule[item['rule_id']].append(item['chart']) + + # 合并errors,按总数量排序 + all_errors = {} + for rid in set(list(fp_by_rule.keys()) + list(fn_by_rule.keys())): + all_errors[rid] = { + 'fp': len(fp_by_rule.get(rid, [])), + 'fn': len(fn_by_rule.get(rid, [])), + 'fp_charts': fp_by_rule.get(rid, []), + 'fn_charts': fn_by_rule.get(rid, []) + } + + sorted_errors = sorted(all_errors.items(), key=lambda x: x[1]['fp'] + x[1]['fn'], reverse=True) + + print("=" * 80) + print("Top Yoga Error Analysis") + print("=" * 80) + + for rule_id, err in sorted_errors[:15]: + total = err['fp'] + err['fn'] + if total == 0: + continue + + rule = next((r for r in engine.rules if r['id'] == rule_id), None) + if not rule: + continue + + print(f"\n{'='*80}") + print(f"Rule: {rule_id} ({rule.get('name', '')}) — FP:{err['fp']} FN:{err['fn']} Total:{total}") + print(f"{'='*80}") + + if err['fn_charts']: + print(f"\n FN Examples (PyJHora=True, Skill=False):") + for chart_name in err['fn_charts'][:3]: + analyze_example(engine, charts_by_name, pyj_charts, chart_name, rule_id, 'FN') + + if err['fp_charts']: + print(f"\n FP Examples (PyJHora=False, Skill=True):") + for chart_name in err['fp_charts'][:3]: + analyze_example(engine, charts_by_name, pyj_charts, chart_name, rule_id, 'FP') + +def analyze_example(engine, charts_by_name, pyj_charts, chart_name, rule_id, error_type): + chart = charts_by_name.get(chart_name) + if not chart: + return + + planets = chart['planets'] + ascendant = chart['ascendant'] + pyj_chart = pyj_charts.get(chart_name) + pyj_yogas = pyj_chart.get('expected_yogas', []) if pyj_chart else [] + + ctx = YogaContext(planets, ascendant) + + print(f"\n [{error_type}] {chart_name} — Asc: {ascendant}") + + # 根据规则id输出相关中间计算 + if rule_id in ['kahala_yoga', 'bvr_kaahala_yoga']: + l1 = ctx.lord_of_house(1) + l4 = ctx.lord_of_house(4) + l9 = ctx.lord_of_house(9) + print(f" L1={l1} in H{ctx.house_of(l1)} (strong: {ctx.house_of(l1) in [1,4,5,7,9,10] if l1 else 'N/A'})") + print(f" L4={l4} in H{ctx.house_of(l4)}") + print(f" L9={l9} in H{ctx.house_of(l9)}") + if l4 and l9: + off = (ctx.house_of(l9) - ctx.house_of(l4)) % 12 + print(f" offset(L4,L9)={off} (kendra: {off in [0,3,6,9]})") + + elif rule_id == 'sankha_yoga': + l1 = ctx.lord_of_house(1) + l5 = ctx.lord_of_house(5) + l6 = ctx.lord_of_house(6) + l9 = ctx.lord_of_house(9) + l10 = ctx.lord_of_house(10) + print(f" L1={l1} in H{ctx.house_of(l1)} (strong: {ctx.house_of(l1) in [1,4,5,7,9,10] if l1 else 'N/A'})") + print(f" L5={l5} in H{ctx.house_of(l5)}") + print(f" L6={l6} in H{ctx.house_of(l6)}") + print(f" L9={l9} in H{ctx.house_of(l9)} (strong: {ctx.house_of(l9) in [1,4,5,7,9,10] if l9 else 'N/A'})") + print(f" L10={l10} in H{ctx.house_of(l10)}") + if l5 and l6: + off1 = (ctx.house_of(l6) - ctx.house_of(l5)) % 12 + print(f" offset(L5,L6)={off1} (kendra: {off1 in [0,3,6,9]})") + if l1 and l10: + print(f" L1==L10 house: {ctx.house_of(l1) == ctx.house_of(l10)}") + print(f" L1 sign: {ctx.sign_of(l1)} (movable: {ctx.sign_of(l1) in ['Aries','Cancer','Libra','Capricorn'] if l1 else 'N/A'})") + + elif rule_id == 'bvr_dharidhra_11_precise': + l1 = ctx.lord_of_house(1) + l2 = ctx.lord_of_house(2) + l11 = ctx.lord_of_house(11) + print(f" L2={l2} in H{ctx.house_of(l2)}") + print(f" L11={l11} in H{ctx.house_of(l11)}") + print(f" method1 (L2/L11 in 6/8/12): {ctx.house_of(l2) in [6,8,12] if l2 else False} / {ctx.house_of(l11) in [6,8,12] if l11 else False}") + + elif rule_id == 'bvr_annadana_yoga': + l2 = ctx.lord_of_house(2) + l11 = ctx.lord_of_house(11) + print(f" L2={l2} in H{ctx.house_of(l2)}") + print(f" L11={l11} in H{ctx.house_of(l11)}") + + elif rule_id == 'bvr_kapata_yoga': + l4 = ctx.lord_of_house(4) + print(f" L4={l4} in H{ctx.house_of(l4)}") + + elif rule_id == 'bvr_nishkapata_precise': + l4 = ctx.lord_of_house(4) + print(f" L4={l4} in H{ctx.house_of(l4)}") + + elif rule_id == 'bvr_thrikaala_gnana_yoga': + jup = ctx.house_of('Jupiter') if 'Jupiter' in planets else None + mer = ctx.house_of('Mercury') if 'Mercury' in planets else None + print(f" Jupiter in H{jup}") + print(f" Mercury in H{mer}") + + else: + for h in range(1, 13): + p = ctx.lord_of_house(h) + if p: + print(f" L{h}={p} in H{ctx.house_of(p)}") + + # 列出PyJHora期望的yoga(包含当前规则的变体) + matched = [y for y in pyj_yogas if rule_id.replace('bvr_', '') in y or rule_id in y] + print(f" PyJHora matched: {matched}") + +if __name__ == '__main__': + main()