feat(functional): adopt upstream BPHS ch.34 profile v2; fix Pancha Mahapurusha self-conjunction; yoga engine reads the same functional roles
Ported verbatim from the upstream skill repo (yinduzhanxing e9beae6b) with product authorization 2026-10-02: scripts/functional_benefics.py, scripts/pancha_mahapurusha.py, references/functional-role-profile.md; only the two functional-role methods of scripts/yoga_engine.py (our file diverged). v5 77-case rectification eval: 5 cells up (to +7.8pp), one cell -1.3pp (one case); product accepted. Rectification memoization golden still to be regenerated (scores change with the grouping). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f85794661b
commit
57782aea8a
@@ -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.
|
||||
@@ -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.",
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+13
-30
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user