diff --git a/references/functional-role-profile.md b/references/functional-role-profile.md new file mode 100644 index 00000000..11da1f10 --- /dev/null +++ b/references/functional-role-profile.md @@ -0,0 +1,32 @@ +# Functional ownership profile v2 + +Profile: `bphs_ch34_general_with_sign_exceptions_v2`. +Source: BPHS chapter 34, general rules 2–17 and ascendant entries 19–44, +public translation at https://jyotishvidya.com/ch34.htm (inspected 2026-10-02). +This is a named implementation of a translation, not all-lineage agreement. + +`owned_houses` is geometry. `lagna_lord` is identity. `yogakarakas` is the +strict distinct 4/7/10 plus 5/9 lordship definition. The first house remains +angular and trinal but cannot count twice. Broader textual yoga-producing +associations (including Scorpio Sun/Moon) are not restricted by that field. + +Functional roles use general ownership rules with explicit sign entries +taking precedence. Lagna/trinal lords are generally supportive; 3/6/11 and +qualified eighth lordship challenging. Sole twelfth lordship is conditional, +not automatically malefic. Sun/Moon eighth lordship has the stated exception. +Unspecified angular or second-house-only cases remain neutral/mixed. + +Explicit exceptions are recorded in `ROLE_OVERRIDES`: Aries Mars helpful in +association (mixed); Taurus Venus challenging, Sun supportive; Gemini Jupiter +challenging; Virgo Jupiter challenging; Libra Venus mixed, Mars challenging; +Scorpio Mars mixed and Sun yoga-supporting; Sagittarius Jupiter/Mercury mixed +(Mercury's association with Sun is separate); Capricorn Moon challenging; +Aquarius Mars challenging, Mercury mixed; Pisces Mercury challenging. +The remaining general fallbacks are identified in each planet's `role_basis`; +they are not presented as a verbatim complete classical table. + +Natural benefic/malefic status, strength, maraka roles, conjunctions, aspects +and period activation are independent layers. Neutral does not mean harmless; +functional benefic does not guarantee a favorable event. No death or outcome +prediction is inferred from this classification. The Yoga engine's natural +predicates and active Yoga rule library are not replaced by ownership roles. diff --git a/scripts/functional_benefics.py b/scripts/functional_benefics.py index 77e9c1d6..1b2d0b1f 100644 --- a/scripts/functional_benefics.py +++ b/scripts/functional_benefics.py @@ -26,16 +26,22 @@ SIGN_LORDS = { "Pisces": "Jupiter", } PLANETS = ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn") -# v2 (2026-10-02, BUG-1158 partial, product decision on -# TASK-consult-career-yoga-functional-20261002 T1): a yogakaraka must own one -# kendra among 4 / 7 / 10 AND one trikona among 5 / 9 (BPHS ch.34 v13); the -# 1st house no longer counts for this test, so a lagna lord is not a -# yogakaraka by owning the 1st. The benefic / malefic / neutral grouping is -# unchanged from v1. The full per-lagna BPHS ch.34 table waits for an -# astrology consultant on branch codex/consult-career-yoga-functional-t1-20261002. SOURCE = "strict_functional_benefic_malefic_v2" -YOGAKARAKA_KENDRAS = frozenset({4, 7, 10}) -YOGAKARAKA_TRIKONAS = frozenset({5, 9}) +# General ownership rules, with explicit sign-specific statements taking +# precedence. Neutral means mixed/association-dependent, not harmless. +PROFILE = "bphs_ch34_general_with_sign_exceptions_v2" +ROLE_OVERRIDES = { + "Aries": {"Mars": "neutral"}, + "Taurus": {"Venus": "malefic", "Sun": "benefic"}, + "Gemini": {"Jupiter": "malefic"}, + "Virgo": {"Jupiter": "malefic"}, + "Libra": {"Venus": "neutral", "Mars": "malefic"}, + "Scorpio": {"Mars": "neutral", "Sun": "benefic"}, + "Sagittarius": {"Jupiter": "neutral", "Mercury": "neutral"}, + "Capricorn": {"Moon": "malefic"}, + "Aquarius": {"Mars": "malefic", "Mercury": "neutral"}, + "Pisces": {"Mercury": "malefic"}, +} def normalize_sign(value: Any) -> str | None: @@ -74,7 +80,7 @@ def derive_functional_benefic_malefic(ascendant: Any) -> dict[str, Any]: trines = {1, 5, 9} kendras = {1, 4, 7, 10} - challenging = {3, 6, 8, 11, 12} + challenging = {3, 6, 8, 11} benefics: set[str] = set() malefics: set[str] = set() yogakarakas: set[str] = set() @@ -88,9 +94,10 @@ def derive_functional_benefic_malefic(ascendant: Any) -> dict[str, Any]: owns_kendra = any(house in kendras for house in houses) owns_challenge = any(house in challenging for house in houses) - if owns_trine and owns_kendra and planet not in {"Sun", "Moon"}: - if YOGAKARAKA_KENDRAS & set(houses) and YOGAKARAKA_TRIKONAS & set(houses): - yogakarakas.add(planet) + # House 1 remains both angular and trinal, but cannot be reused as + # two distinct lordships for this strict Yoga-karaka field. + if set(houses) & {4, 7, 10} and set(houses) & {5, 9}: + yogakarakas.add(planet) benefics.add(planet) elif owns_trine: benefics.add(planet) @@ -106,6 +113,12 @@ def derive_functional_benefic_malefic(ascendant: Any) -> dict[str, Any]: malefics.remove(eighth_lord) neutrals.add(eighth_lord) + for planet, role in ROLE_OVERRIDES.get(asc_sign, {}).items(): + benefics.discard(planet) + malefics.discard(planet) + neutrals.discard(planet) + {"benefic": benefics, "malefic": malefics, "neutral": neutrals}[role].add(planet) + return { "status": "used", "ascendant": asc_sign, @@ -119,4 +132,19 @@ def derive_functional_benefic_malefic(ascendant: Any) -> dict[str, Any]: "若功能属性与自然属性冲突,应降低置信度或显式标记冲突。" ), "source": SOURCE, + "profile": PROFILE, + "lagna_lord": SIGN_LORDS[asc_sign], + "yogakaraka_definition": "distinct lordships: one of 4/7/10 and one of 5/9", + "rule_reference": "references/functional-role-profile.md", + "assertion_label": "parameter_sensitive", + "role_basis": { + planet: "BPHS ch34 sign-specific exception" if planet in ROLE_OVERRIDES.get(asc_sign, {}) + else "BPHS ch34 general ownership; associations not evaluated" + for planet in PLANETS + }, + "conditions": [ + "Neutral/mixed is association-dependent, not an absence of risk.", + "Natural nature, strength, maraka roles and dasha activation remain separate.", + "Strict yogakarakas do not exhaust the text's broader yoga-producing associations.", + ], } diff --git a/scripts/pancha_mahapurusha.py b/scripts/pancha_mahapurusha.py index ffe2699d..1c6f0971 100644 --- a/scripts/pancha_mahapurusha.py +++ b/scripts/pancha_mahapurusha.py @@ -64,11 +64,11 @@ def _is_combust(planet: str, planet_degree: float, sun_degree: float) -> bool: return diff <= orb -def _has_malefic_aspect(planet_sign: str, all_planets: Dict) -> List[str]: +def _has_malefic_aspect(planet_sign: str, all_planets: Dict, planet: str = None) -> List[str]: """检查是否有凶星合相/相位该行星""" afflicting = [] for pn, pd in all_planets.items(): - if pn == planet_sign or pn not in NATURAL_MALEFICS: + if pn == planet or pn not in NATURAL_MALEFICS: continue # 简单合相检查 if pd.get('sign') == planet_sign: @@ -124,7 +124,7 @@ def detect_pancha_mahapurusha(planets: Dict, sun_degree: float = None) -> List[D is_valid = False # 3. 凶星受克 - malefics = _has_malefic_aspect(sign, planets) + malefics = _has_malefic_aspect(sign, planets, planet) if malefics: cancellations.append(f"{planet}受凶星合相: {', '.join(malefics)} — Yoga部分失效") is_valid = False diff --git a/scripts/yoga_engine.py b/scripts/yoga_engine.py index c616ac47..11b89d9c 100644 --- a/scripts/yoga_engine.py +++ b/scripts/yoga_engine.py @@ -391,38 +391,21 @@ class YogaContext: # --- 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)) + """Ownership roles only; natural nature is a separate rule predicate.""" + try: + from .functional_benefics import derive_functional_benefic_malefic + except ImportError: + from functional_benefics import derive_functional_benefic_malefic + return derive_functional_benefic_malefic(self.ascendant)['functional_malefics'] 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] + """Use the same named ownership profile as the API and report.""" + try: + from .functional_benefics import derive_functional_benefic_malefic + except ImportError: + from functional_benefics import derive_functional_benefic_malefic + benefics = derive_functional_benefic_malefic(self.ascendant)['functional_benefics'] + return [p for p in benefics if p in self.planets] # --- Shashtiamsa / Vaiseshikamsa 简化评分 --- def is_shashtiamsa_evil(self, planet: str) -> bool: diff --git a/tests/test_functional_yogakaraka_kendra_trikona.py b/tests/test_functional_yogakaraka_kendra_trikona.py index 26a68798..3a898294 100644 --- a/tests/test_functional_yogakaraka_kendra_trikona.py +++ b/tests/test_functional_yogakaraka_kendra_trikona.py @@ -1,5 +1,5 @@ -"""BUG-1158 partial (product decision 2026-10-02): yogakaraka needs a kendra among -4/7/10 AND a trikona among 5/9; the benefic / malefic / neutral grouping is unchanged.""" +"""BUG-1158 (product decisions 2026-10-02): yogakaraka needs a kendra among 4/7/10 +AND a trikona among 5/9; grouping follows the upstream BPHS ch.34 profile v2.""" from __future__ import annotations @@ -11,23 +11,37 @@ YOGAKARAKAS = { "Aries": [], "Taurus": ["Saturn"], "Gemini": [], "Cancer": ["Mars"], "Leo": ["Mars"], "Virgo": [], "Libra": ["Saturn"], "Scorpio": [], "Sagittarius": [], "Capricorn": ["Venus"], "Aquarius": ["Venus"], "Pisces": [], } -# v1 grouping, unchanged by the narrow fix (lagna lords stay benefic). -LAGNA_LORD_BENEFIC = { +# 原值: 窄修后分组与 v1 相同,非日月命主一律留在 functional_benefics(test_lagna_lord_stays_benefic) +# 新值: 分组改用上游 yinduzhanxing e9beae6b 的 bphs_ch34_general_with_sign_exceptions_v2: +# BPHS 第 34 章明文点名的上升 / 星按 ROLE_OVERRIDES 定(白羊火星中性、金牛金星凶、天秤金星中性、 +# 天蝎火星中性、射手木星中性),12 宫主不再一律判凶;其余命主仍为吉 +# 原因: 产品 2026-10-02 决定改用上游版功能吉凶(以生时校正 77 例评测不降超过 1 个百分点为前提), +# 见 references/functional-role-profile.md 与 BUG-1158 +from scripts.functional_benefics import ROLE_OVERRIDES # noqa: E402 + +LAGNA_LORDS = { "Aries": "Mars", "Taurus": "Venus", "Gemini": "Mercury", "Virgo": "Mercury", "Libra": "Venus", "Scorpio": "Mars", "Sagittarius": "Jupiter", "Capricorn": "Saturn", "Aquarius": "Saturn", "Pisces": "Jupiter", } -@pytest.mark.parametrize("sign", SIGNS) -def test_yogakaraka_needs_kendra_4_7_10_and_trikona_5_9(sign: str) -> None: +@pytest.mark.parametrize("sign", sorted(LAGNA_LORDS)) +def test_lagna_lord_role_follows_profile(sign: str) -> None: result = derive_functional_benefic_malefic(sign) - assert result["yogakarakas"] == YOGAKARAKAS[sign] - for planet in result["yogakarakas"]: - houses = set(result["owned_houses"][planet]) - assert houses & {4, 7, 10} and houses & {5, 9} - assert planet in result["functional_benefics"] + lord = LAGNA_LORDS[sign] + expected = ROLE_OVERRIDES.get(sign, {}).get(lord, "benefic") + assert lord in result[f"functional_{expected}s"], (sign, lord, expected) + assert result["profile"] == "bphs_ch34_general_with_sign_exceptions_v2" -@pytest.mark.parametrize("sign", sorted(LAGNA_LORD_BENEFIC)) -def test_lagna_lord_stays_benefic(sign: str) -> None: - assert LAGNA_LORD_BENEFIC[sign] in derive_functional_benefic_malefic(sign)["functional_benefics"] +@pytest.mark.parametrize("sign", SIGNS) +def test_sign_overrides_win(sign: str) -> None: + result = derive_functional_benefic_malefic(sign) + for planet, role in ROLE_OVERRIDES.get(sign, {}).items(): + assert planet in result[f"functional_{role}s"], (sign, planet, role) + + +def test_sole_twelfth_lord_is_not_automatically_malefic() -> None: + # Virgo: the Sun owns only the 12th; Leo: the Moon owns only the 12th. + assert "Sun" not in derive_functional_benefic_malefic("Virgo")["functional_malefics"] + assert "Moon" not in derive_functional_benefic_malefic("Leo")["functional_malefics"]