diff --git a/references/yoga_rules.json b/references/yoga_rules.json index d356de17..23b168db 100644 --- a/references/yoga_rules.json +++ b/references/yoga_rules.json @@ -946,7 +946,7 @@ }, { "type": "custom", - "expr": "\n# #166 Q5 (BUG-1596): PVR strict definition. Jupiter in a kendra from the Moon (previous condition),\n# conjoined or aspected by a benefic, not debilitated, not combust, not in a natural enemy's sign.\n# The strong-Jupiter substitute is a separate rule (gajakesari_strong_jupiter_variant).\ntriggers = []\nbenefic_links = [b for b in ['Venus', 'Mercury'] if b in ctx.planets and (same_house(b, 'Jupiter') or aspect(b, 'Jupiter'))]\nok = bool(benefic_links) and not debil('Jupiter') and not combust('Jupiter') and not in_natural_enemy_sign('Jupiter')\nif ok:\n triggers = ['benefic association: ' + ', '.join(benefic_links)]\n__result__ = ok\n", + "expr": "\n# #166 Q5 (BUG-1596): PVR strict definition. Jupiter in a kendra from the Moon (previous condition),\n# conjoined or aspected by a benefic, not debilitated, not combust, not in a natural enemy's sign.\n# The strong-Jupiter substitute is a separate rule (gajakesari_strong_jupiter_variant).\ntriggers = []\nbenefic_links = [b for b in ['Venus', 'Mercury'] if b in ctx.planets and (same_house(b, 'Jupiter') or aspect(b, 'Jupiter'))]\nok = bool(benefic_links) and not debil('Jupiter') and not combust('Jupiter') and not in_natural_enemy_sign('Jupiter')\nif ok:\n triggers = [('吉星关联:' + '、'.join(PLANET_ZH.get(p, p) for p in benefic_links), 'benefic association: ' + ', '.join(benefic_links))]\n__result__ = ok\n", "combo_template": "严格定义:木星受吉星合相或相照,且不落陷、不燃烧、不落敌宫", "combo_template_en": "Strict definition: Jupiter conjoined or aspected by a benefic, not debilitated, combust or in an enemy sign" } @@ -990,7 +990,7 @@ }, { "type": "custom", - "expr": "\n# #166 Q5: variant only when the strict definition does not hold.\nbenefic_links = [b for b in ['Venus', 'Mercury'] if b in ctx.planets and (same_house(b, 'Jupiter') or aspect(b, 'Jupiter'))]\nstrict = bool(benefic_links) and not debil('Jupiter') and not combust('Jupiter') and not in_natural_enemy_sign('Jupiter')\nstrong_jup = exalted('Jupiter') or own('Jupiter') or moola('Jupiter')\ntriggers = [f\"strong Jupiter: {dignity('Jupiter')}\"] if strong_jup and not strict else []\n__result__ = strong_jup and not strict and not debil('Jupiter')\n", + "expr": "\n# #166 Q5: variant only when the strict definition does not hold.\nbenefic_links = [b for b in ['Venus', 'Mercury'] if b in ctx.planets and (same_house(b, 'Jupiter') or aspect(b, 'Jupiter'))]\nstrict = bool(benefic_links) and not debil('Jupiter') and not combust('Jupiter') and not in_natural_enemy_sign('Jupiter')\nstrong_jup = exalted('Jupiter') or own('Jupiter') or moola('Jupiter')\ntriggers = [(f\"木星强:{dignity('Jupiter')}\", f\"strong Jupiter: {dignity('Jupiter')}\")] if strong_jup and not strict else []\n__result__ = strong_jup and not strict and not debil('Jupiter')\n", "combo_template": "变体:木星强(入旺 / 本宫 / 根本三角)代替吉星关联", "combo_template_en": "Variant: a strong Jupiter (exalted / own / moolatrikona) in place of benefic association" } @@ -11578,7 +11578,7 @@ "category": "durbhaga", "logic": { "type": "custom", - "expr": "\n# #166 Q6 (BUG-1597): Raman 300 Combinations no. 198 only (no. 199 is bvr_matrunasa_199).\n# Hemmed by, associated with, or aspected by natural malefics (OR). Aspects are named Parashari graha\n# aspects (7th; Mars 4/8, Jupiter 5/9, Saturn 3/10); Rahu / Ketu count for conjunction and hemming,\n# not for aspect (implementation choice recorded in BUG-1597). No Jaimini rasi drishti.\ntriggers = []\nif 'Moon' in ctx.planets:\n moon_h = house_of('Moon')\n prev_h = ((moon_h - 2) % 12) + 1\n next_h = (moon_h % 12) + 1\n prev_m = [p for p in MALEFICS if p in ctx.planets and house_of(p) == prev_h]\n next_m = [p for p in MALEFICS if p in ctx.planets and house_of(p) == next_h]\n if prev_m and next_m:\n triggers.append('hemmed: ' + ', '.join(prev_m + next_m))\n conj = [p for p in MALEFICS if p in ctx.planets and same_house('Moon', p)]\n if conj:\n triggers.append('with Moon: ' + ', '.join(conj))\n asp = [p for p in ('Sun', 'Mars', 'Saturn') if p in ctx.planets and not same_house('Moon', p) and aspect(p, 'Moon')]\n if asp:\n triggers.append('aspects Moon: ' + ', '.join(asp))\n__result__ = bool(triggers)\n", + "expr": "\n# #166 Q6 (BUG-1597): Raman 300 Combinations no. 198 only (no. 199 is bvr_matrunasa_199).\n# Hemmed by, associated with, or aspected by natural malefics (OR). Aspects are named Parashari graha\n# aspects (7th; Mars 4/8, Jupiter 5/9, Saturn 3/10); Rahu / Ketu count for conjunction and hemming,\n# not for aspect (implementation choice recorded in BUG-1597). No Jaimini rasi drishti.\ntriggers = []\nif 'Moon' in ctx.planets:\n moon_h = house_of('Moon')\n prev_h = ((moon_h - 2) % 12) + 1\n next_h = (moon_h % 12) + 1\n prev_m = [p for p in MALEFICS if p in ctx.planets and house_of(p) == prev_h]\n next_m = [p for p in MALEFICS if p in ctx.planets and house_of(p) == next_h]\n if prev_m and next_m:\n triggers.append(('凶星夹制:' + '、'.join(PLANET_ZH.get(p, p) for p in prev_m + next_m), 'hemmed: ' + ', '.join(prev_m + next_m)))\n conj = [p for p in MALEFICS if p in ctx.planets and same_house('Moon', p)]\n if conj:\n triggers.append(('与月亮同宫:' + '、'.join(PLANET_ZH.get(p, p) for p in conj), 'with Moon: ' + ', '.join(conj)))\n asp = [p for p in ('Sun', 'Mars', 'Saturn') if p in ctx.planets and not same_house('Moon', p) and aspect(p, 'Moon')]\n if asp:\n triggers.append(('相照月亮:' + '、'.join(PLANET_ZH.get(p, p) for p in asp), 'aspects Moon: ' + ', '.join(asp)))\n__result__ = bool(triggers)\n", "combo_template": "Raman 198:月亮受凶星夹制、合相或相照", "combo_template_en": "Raman 198: Moon hemmed, conjoined or aspected by malefics" }, @@ -11603,7 +11603,7 @@ "category": "durbhaga", "logic": { "type": "custom", - "expr": "\n# #166 Q6: Raman no. 199. Lord of the D9 sign holding the 4th lord; then the lord of the D9 sign\n# that planet holds; that final planet in the 6th / 8th / 12th of D1.\ntriggers = []\nl4 = lord(4)\nnav_lord = navamsa_dispositor(l4) if l4 else None\nfinal_planet = navamsa_dispositor(nav_lord) if nav_lord else None\nif final_planet in ctx.planets and house_of(final_planet) in [6, 8, 12]:\n triggers.append(f'{l4} -> {nav_lord} -> {final_planet} in house {house_of(final_planet)}')\n__result__ = bool(triggers)\n", + "expr": "\n# #166 Q6: Raman no. 199. Lord of the D9 sign holding the 4th lord; then the lord of the D9 sign\n# that planet holds; that final planet in the 6th / 8th / 12th of D1.\ntriggers = []\nl4 = lord(4)\nnav_lord = navamsa_dispositor(l4) if l4 else None\nfinal_planet = navamsa_dispositor(nav_lord) if nav_lord else None\nif final_planet in ctx.planets and house_of(final_planet) in [6, 8, 12]:\n triggers.append((f'{PLANET_ZH.get(l4, l4)} → {PLANET_ZH.get(nav_lord, nav_lord)} → {PLANET_ZH.get(final_planet, final_planet)},落第 {house_of(final_planet)} 宫', f'{l4} -> {nav_lord} -> {final_planet}, house {house_of(final_planet)}'))\n__result__ = bool(triggers)\n", "combo_template": "Raman 199:4宫主的 Navamsa 链最终落 6 / 8 / 12 宫", "combo_template_en": "Raman 199: the 4th lord's Navamsa chain ends in the 6th / 8th / 12th" }, diff --git a/scripts/functional_benefics.py b/scripts/functional_benefics.py index a08a1157..a8de3ed9 100644 --- a/scripts/functional_benefics.py +++ b/scripts/functional_benefics.py @@ -53,14 +53,14 @@ ROLE_OVERRIDES = { # summary; the other lordship is kept as a duty, never dropped. Basis says whether # the sign's own verse states it or it is derived from the general rules. AMBIGUOUS_CELL_NOTES = { - ("Taurus", "Mercury"): ("mildly benefic; keeps 2nd-lord duties", "sign verse: somewhat auspicious"), - ("Gemini", "Mercury"): ("Lagna lord, supportive; not a Yogakaraka", "general rules"), - ("Gemini", "Saturn"): ("mixed; 9th-lord support, not a pure benefic", "general rules with sign context"), - ("Sagittarius", "Saturn"): ("challenging (3rd lord); Maraka (2nd lord) listed separately", "3rd-house rule plus sign verse"), - ("Cancer", "Jupiter"): ("mildly benefic; keeps 6th-lord duties", "sign verse"), - ("Leo", "Jupiter"): ("mildly benefic; keeps 8th-lord duties", "sign verse"), - ("Virgo", "Saturn"): ("mixed; not a pure benefic on the 5th lordship alone", "sign verse does not name it a benefic"), - ("Capricorn", "Mercury"): ("mildly benefic; keeps 6th-lord duties", "sign verse"), + ("Taurus", "Mercury"): ("偏吉,保留 2 宫主职责", "专条:有所吉利", "mildly benefic; keeps 2nd-lord duties", "sign verse: somewhat auspicious"), + ("Gemini", "Mercury"): ("上升主,支持;不自动认定 Yogakaraka", "通用规则推导", "Ascendant ruler, supportive; not a Yogakaraka", "general rules"), + ("Gemini", "Saturn"): ("混合,9 宫支持,不标纯吉", "通用规则与专条语境并列", "mixed; 9th-house support, not a pure benefic", "general rules with sign context"), + ("Sagittarius", "Saturn"): ("功能挑战(3 宫主);Maraka(2 宫主)另列", "3 宫通则加专条身份", "challenging (3rd house); Maraka (2nd house) listed separately", "3rd-house rule plus sign verse"), + ("Cancer", "Jupiter"): ("偏吉,保留 6 宫主职责", "专条明确支持", "mildly benefic; keeps 6th-house duties", "sign verse"), + ("Leo", "Jupiter"): ("偏吉,保留 8 宫主职责", "专条明确支持", "mildly benefic; keeps 8th-house duties", "sign verse"), + ("Virgo", "Saturn"): ("混合,不只凭 5 宫判纯吉", "专条未明确列为吉星", "mixed; not a pure benefic on the 5th house alone", "sign verse does not name it a benefic"), + ("Capricorn", "Mercury"): ("偏吉,保留 6 宫主职责", "专条明确支持", "mildly benefic; keeps 6th-house duties", "sign verse"), } @@ -158,8 +158,8 @@ def derive_functional_benefic_malefic(ascendant: Any) -> dict[str, Any]: "rule_reference": "references/functional-role-profile.md", "assertion_label": "parameter_sensitive", "ambiguous_cell_notes": { - planet: {"summary": note, "basis": basis} - for (sign, planet), (note, basis) in AMBIGUOUS_CELL_NOTES.items() if sign == asc_sign + planet: {"summary": note, "basis": basis, "summary_en": note_en, "basis_en": basis_en} + for (sign, planet), (note, basis, note_en, basis_en) in AMBIGUOUS_CELL_NOTES.items() if sign == asc_sign }, "role_basis": { planet: "BPHS ch34 sign-specific exception" if planet in ROLE_OVERRIDES.get(asc_sign, {}) diff --git a/scripts/yoga_engine.py b/scripts/yoga_engine.py index e61030d5..9035c580 100644 --- a/scripts/yoga_engine.py +++ b/scripts/yoga_engine.py @@ -65,6 +65,9 @@ NATURAL_ENEMIES = { # Combustion orbs (direct motion), same values as jyotish_engine.COMBUSTION_ORBS # direct column; used only when the chart row carries no `combust` flag. COMBUSTION_ORBS_DIRECT = {'Moon': 12.0, 'Mars': 17.0, 'Mercury': 14.0, 'Jupiter': 11.0, 'Venus': 10.0, 'Saturn': 15.0} +# Short Chinese planet names for bilingual rule triggers (#166 Q5 / Q6). +PLANET_ZH = {'Sun': '太阳', 'Moon': '月亮', 'Mars': '火星', 'Mercury': '水星', 'Jupiter': '木星', + 'Venus': '金星', 'Saturn': '土星', 'Rahu': '罗睺', 'Ketu': '计都'} PLANET_CN = { "Ketu": "南交点Ketu", "Venus": "金星Venus", "Sun": "太阳Sun", "Moon": "月亮Moon", "Mars": "火星Mars", "Rahu": "北交点Rahu", @@ -823,6 +826,7 @@ class YogaEngine: "category": rule.get("category", ""), "rule_id": rule.get("id", ""), **({"triggers": m["triggers"]} if m.get("triggers") else {}), + **({"triggers_zh": m["triggers_zh"]} if m.get("triggers_zh") else {}), **({"canonical_mapping": rule["canonical_mapping"]} if rule.get("canonical_mapping") else {}), # English twins for the reader's English edition; the Chinese # fields above are unchanged. @@ -1011,7 +1015,8 @@ class YogaEngine: "combination": merged_combo, "strength": sm.get("strength", match.get("strength")), "bindings": {**match.get("bindings", {}), **sm.get("bindings", {})}, - **({"triggers": (match.get("triggers") or []) + (sm.get("triggers") or [])} + **({"triggers": (match.get("triggers") or []) + (sm.get("triggers") or []), + "triggers_zh": (match.get("triggers_zh") or []) + (sm.get("triggers_zh") or [])} if match.get("triggers") or sm.get("triggers") else {}), }) results = new_results @@ -1538,11 +1543,15 @@ class YogaEngine: strength = strength(ctx, bindings) # #166 Q6 (BUG-1597): a rule may set `triggers` (list of short strings) # to name what actually fired; they are appended to the combination. + # An item is either one string (used in both editions) or a + # (Chinese, English) pair, so the Chinese report never carries English glue. triggers = exec_globals.get("triggers") if isinstance(triggers, list) and triggers: - triggers = [str(item) for item in triggers] - combo = BiText(f"{combo}:{';'.join(triggers)}", f"{_en_text(combo)}: {'; '.join(triggers)}") - return [{"combination": combo, "strength": strength, "triggers": triggers}] + pairs = [(item[0], item[1]) if isinstance(item, (list, tuple)) and len(item) == 2 else (item, item) for item in triggers] + zh_items = [str(zh) for zh, _ in pairs] + en_items = [str(en) for _, en in pairs] + combo = BiText(f"{combo}:{';'.join(zh_items)}", f"{_en_text(combo)}: {'; '.join(en_items)}") + return [{"combination": combo, "strength": strength, "triggers": en_items, "triggers_zh": zh_items}] return [{"combination": combo, "strength": strength}] return [] @@ -1835,6 +1844,7 @@ class YogaEngine: "house_strength": house_strength, "strong": strong, "weak": weak, "associated": associated, "temporal_friend": temporal_friend, "natural_friend": natural_friend, "combust": combust, "in_natural_enemy_sign": in_natural_enemy_sign, + "PLANET_ZH": PLANET_ZH, "occupants": occupants, "only_benefics_in_house": only_benefics_in_house, "only_malefics_in_house": only_malefics_in_house, "house_has_benefic": house_has_benefic, "house_has_malefic": house_has_malefic, diff --git a/tests/test_astrologer_rulings_166_batch2.py b/tests/test_astrologer_rulings_166_batch2.py index fbc043b9..1313dd57 100644 --- a/tests/test_astrologer_rulings_166_batch2.py +++ b/tests/test_astrologer_rulings_166_batch2.py @@ -122,6 +122,7 @@ def test_q5_strict_gajakesari_holds_with_benefic_and_clean_jupiter() -> None: assert "Gajakesari Yoga" in rows # Venus conjoined; Mercury in Virgo aspects Pisces (7th). assert rows["Gajakesari Yoga"]["triggers"] == ["benefic association: Venus, Mercury"] + assert rows["Gajakesari Yoga"]["triggers_zh"] == ["吉星关联:金星、水星"] assert "Strong-Jupiter Gajakesari Variant" not in rows @@ -142,4 +143,5 @@ def test_q6_matrunasa_198_rasi_drishti_alone_no_longer_fires_and_triggers_are_na rows = {row["name"]: row for row in _detect(planets, "Aries")} hit = rows.get("Matrunasa Yoga") assert hit is not None and hit["triggers"] == ["aspects Moon: Saturn"] + assert hit["triggers_zh"] == ["相照月亮:土星"] assert hit["effects"] and "健康" not in hit["effects"][0].replace("不作母亲健康", "")