Files
Jyotisha/scripts/functional_benefics.py
T
ClaudeandClaude Opus 5.5 7ff447ac67 fix(engine): keep the Chinese report free of English glue from #166 triggers and cell notes (BUG-1596, BUG-1597, BUG-1590)
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
2026-11-04 22:39:15 +08:00

175 lines
7.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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.",
],
}