Quick gate: test_report_english_sweep_smoke counted 3 zh_han_function_glue
hits on chart #1, all new in 4b223eb8: the Matrunasa 199 trigger "Moon ->
Mercury -> Moon in house 8" (the renderer turns "house" into Chinese and
leaves "in") and the English ambiguous_cell_notes ("Lagna lord, ...").
- A custom rule's trigger item may be a (Chinese, English) pair; the Chinese
combination uses the Chinese items, combination_en and `triggers` the
English ones, `triggers_zh` the Chinese. Gajakesari, the variant and both
Matrunasa rules emit pairs (PLANET_ZH in the sandbox).
- ambiguous_cell_notes carry Chinese summary / basis plus summary_en /
basis_en, worded without the glue words.
No role or score changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbBXoyn7Hj3LCW3BUs5bZ8
175 lines
7.9 KiB
Python
175 lines
7.9 KiB
Python
#!/usr/bin/env python3
|
||
"""Functional benefic/malefic classification by ascendant."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Any
|
||
|
||
|
||
SIGNS = [
|
||
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
|
||
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
|
||
]
|
||
SIGN_TO_INDEX = {name: idx for idx, name in enumerate(SIGNS)}
|
||
SIGN_LORDS = {
|
||
"Aries": "Mars",
|
||
"Taurus": "Venus",
|
||
"Gemini": "Mercury",
|
||
"Cancer": "Moon",
|
||
"Leo": "Sun",
|
||
"Virgo": "Mercury",
|
||
"Libra": "Venus",
|
||
"Scorpio": "Mars",
|
||
"Sagittarius": "Jupiter",
|
||
"Capricorn": "Saturn",
|
||
"Aquarius": "Saturn",
|
||
"Pisces": "Jupiter",
|
||
}
|
||
PLANETS = ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn")
|
||
SOURCE = "strict_functional_benefic_malefic_v2"
|
||
# 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 = {
|
||
# BPHS 34.20 (Aries) and 34.35 (Scorpio) name Venus (sita) a malefic
|
||
# (BUG-1570, product decision 2026-11-04; scoring-13).
|
||
"Aries": {"Mars": "neutral", "Venus": "malefic"},
|
||
"Taurus": {"Venus": "malefic", "Sun": "benefic"},
|
||
# #166 Q4 (BUG-1590, astrologer 2026-11-04): a lord of a trine and of 6/8 is
|
||
# not a pure benefic unless the sign's own verse says so. Gemini Saturn (8, 9)
|
||
# and Virgo Saturn (5, 6) are mixed. 原值: 三方宫优先判 benefic;新值: neutral(混合)。
|
||
"Gemini": {"Jupiter": "malefic", "Saturn": "neutral"},
|
||
"Virgo": {"Jupiter": "malefic", "Saturn": "neutral"},
|
||
"Libra": {"Venus": "neutral", "Mars": "malefic"},
|
||
"Scorpio": {"Mars": "neutral", "Sun": "benefic", "Venus": "malefic"},
|
||
"Sagittarius": {"Jupiter": "neutral", "Mercury": "neutral"},
|
||
"Capricorn": {"Moon": "malefic"},
|
||
"Aquarius": {"Mars": "malefic", "Mercury": "neutral"},
|
||
"Pisces": {"Mercury": "malefic"},
|
||
}
|
||
|
||
|
||
# #166 Q4: the table the astrologer gave for the ambiguous cells. The role is the
|
||
# 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"): ("偏吉,保留 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"),
|
||
}
|
||
|
||
|
||
def normalize_sign(value: Any) -> str | None:
|
||
if not isinstance(value, str):
|
||
return None
|
||
lowered = value.strip().lower()
|
||
for sign in SIGNS:
|
||
if sign.lower() == lowered:
|
||
return sign
|
||
return None
|
||
|
||
|
||
def derive_functional_benefic_malefic(ascendant: Any) -> dict[str, Any]:
|
||
"""Return functional benefics, malefics and ownership roles for a Lagna."""
|
||
asc_sign = normalize_sign(ascendant)
|
||
if asc_sign is None:
|
||
return {
|
||
"status": "blocked",
|
||
"ascendant": ascendant if isinstance(ascendant, str) else None,
|
||
"functional_benefics": [],
|
||
"functional_malefics": [],
|
||
"functional_neutrals": [],
|
||
"yogakarakas": [],
|
||
"owned_houses": {},
|
||
"effect_on_confidence": "Functional layer blocked: unknown ascendant sign.",
|
||
"source": SOURCE,
|
||
}
|
||
|
||
asc_idx = SIGN_TO_INDEX[asc_sign]
|
||
owned_houses: dict[str, list[int]] = {}
|
||
for house_num in range(1, 13):
|
||
sign = SIGNS[(asc_idx + house_num - 1) % 12]
|
||
lord = SIGN_LORDS.get(sign)
|
||
if lord:
|
||
owned_houses.setdefault(lord, []).append(house_num)
|
||
|
||
trines = {1, 5, 9}
|
||
kendras = {1, 4, 7, 10}
|
||
challenging = {3, 6, 8, 11}
|
||
benefics: set[str] = set()
|
||
malefics: set[str] = set()
|
||
yogakarakas: set[str] = set()
|
||
neutrals: set[str] = set()
|
||
|
||
for planet in PLANETS:
|
||
houses = owned_houses.get(planet, [])
|
||
if not houses:
|
||
continue
|
||
owns_trine = any(house in trines for house in houses)
|
||
owns_kendra = any(house in kendras for house in houses)
|
||
owns_challenge = any(house in challenging for house in houses)
|
||
|
||
# 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)
|
||
elif owns_challenge and 1 not in houses:
|
||
malefics.add(planet)
|
||
elif owns_kendra and planet in {"Jupiter", "Venus", "Mercury", "Moon"}:
|
||
neutrals.add(planet)
|
||
else:
|
||
neutrals.add(planet)
|
||
|
||
eighth_lord = next((planet for planet, houses in owned_houses.items() if 8 in houses), None)
|
||
if eighth_lord in {"Sun", "Moon"} and eighth_lord in malefics:
|
||
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,
|
||
"functional_benefics": sorted(benefics),
|
||
"functional_malefics": sorted(malefics),
|
||
"functional_neutrals": sorted(neutrals - benefics - malefics),
|
||
"yogakarakas": sorted(yogakarakas),
|
||
"owned_houses": {planet: houses for planet, houses in sorted(owned_houses.items())},
|
||
"effect_on_confidence": (
|
||
"高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;"
|
||
"若功能属性与自然属性冲突,应降低置信度或显式标记冲突。"
|
||
),
|
||
"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",
|
||
"ambiguous_cell_notes": {
|
||
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, {})
|
||
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.",
|
||
],
|
||
}
|