- seed rath_highest_occupant_dignity_v1 (named convention; equal tier below own sign blocks the seed) - AD direction profiles rath_p50_md_parity_v1 / rath_stronger_lord_start_parity_v1 (default, pending choice) - Saturn+Ketu in seed / MD sign blocked; PyJHora Ketu priority only as named reference profile - 0-year MD rows flagged, never current, skipped on the annual timeline; years profile rath_table8_phalita_dignity_v1 - KP DBA year_days 365.25 default, parent/AD/full/visible fields, boundaries equal main Vimshottari - functional profile pvr_table30_v1 (aliases), Moon-only blocked without paksha; profile through API, cache keys, audit; rectification asserts one profile per run - golden recaptured with TTL=0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DbBXoyn7Hj3LCW3BUs5bZ8
458 lines
24 KiB
Python
458 lines
24 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.
|
||
# 原值 "..._v2"。新值 "..._v3_166q4":上游 v3 的表(白羊 / 天蝎金星判凶,BUG-1570)
|
||
# 加 #166 Q4 双子 / 处女土星判混合(BUG-1590)。原因:三方对照 2026-11-07:产品侧
|
||
# 实现缺陷——标签写 v2,实际用的表既不是上游 v2 也不是 v3。
|
||
PROFILE = "bphs_ch34_general_with_sign_exceptions_v3_166q4"
|
||
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"),
|
||
}
|
||
|
||
|
||
# 占星师 2026-11-07 Q7: a separate, named profile — PVR, "Vedic Astrology: An Integrated
|
||
# Approach" §13.2 Table 30 (printed pp. 166-167, table on p. 167 / PDF p. 179; copy in her
|
||
# references/book_corpus/books/PVR_Integrated_Approach.md). It is NOT the product profile above
|
||
# (PROFILE = BPHS ch. 34 general ownership rules, trine-ownership first, plus sign-specific
|
||
# overrides). The two agree on Virgo (Sun, Saturn neutral) and differ on 6 cells: Aries Mars,
|
||
# Gemini Mercury, Cancer Saturn, Libra Venus, Scorpio Sun, Pisces Jupiter. Moon is not listed
|
||
# by PVR for movable lagnas (depends on waxing / waning).
|
||
# 原值:只供对照(reference_only),不进打分。新值:默认档 functional_profile = "pvr_table_30";
|
||
# 上面的产品表另存为 "bphs_exception_profile",只有被明确选中时才参与打分。
|
||
# 原因:占星师 2026-11-08 第五轮裁定 3。
|
||
PVR_TABLE30_PROFILE = "pvr_integrated_approach_13_2_table_30"
|
||
PVR_TABLE30_SOURCE = "PVR Narasimha Rao, Vedic Astrology: An Integrated Approach, §13.2 Table 30 (p. 167; PDF p. 179)"
|
||
PVR_TABLE30 = {
|
||
# lagna: (yogakaraka, benefics, neutrals, malefics)
|
||
"Aries": ((), ("Sun", "Mars", "Jupiter"), (), ("Mercury", "Venus", "Saturn")),
|
||
"Taurus": (("Saturn",), ("Sun", "Mercury", "Saturn"), ("Mars",), ("Moon", "Jupiter", "Venus")),
|
||
"Gemini": ((), ("Venus",), ("Moon", "Mercury", "Saturn"), ("Sun", "Mars", "Jupiter")),
|
||
"Cancer": (("Mars",), ("Moon", "Mars", "Jupiter"), ("Sun", "Saturn"), ("Mercury", "Venus")),
|
||
"Leo": (("Mars",), ("Sun", "Mars", "Jupiter"), ("Moon",), ("Mercury", "Venus", "Saturn")),
|
||
"Virgo": ((), ("Mercury", "Venus"), ("Sun", "Saturn"), ("Moon", "Mars", "Jupiter")),
|
||
"Libra": (("Saturn",), ("Mercury", "Venus", "Saturn"), (), ("Sun", "Mars", "Jupiter")),
|
||
"Scorpio": ((), ("Moon", "Jupiter"), ("Sun", "Mars"), ("Mercury", "Venus", "Saturn")),
|
||
"Sagittarius": ((), ("Sun", "Mars"), ("Moon", "Mercury", "Jupiter"), ("Venus", "Saturn")),
|
||
"Capricorn": (("Venus",), ("Venus", "Mercury", "Saturn"), ("Sun",), ("Mars", "Jupiter")),
|
||
"Aquarius": (("Venus",), ("Venus", "Saturn"), ("Sun", "Mercury"), ("Moon", "Mars", "Jupiter")),
|
||
"Pisces": ((), ("Moon", "Mars"), ("Jupiter",), ("Sun", "Mercury", "Venus", "Saturn")),
|
||
}
|
||
|
||
|
||
def pvr_table30_roles(ascendant: Any) -> dict[str, Any]:
|
||
"""PVR Table 30 roles for a lagna, as a separately named reference profile."""
|
||
sign = normalize_sign(ascendant)
|
||
if sign is None:
|
||
return {"status": "blocked", "profile": PVR_TABLE30_PROFILE, "source": PVR_TABLE30_SOURCE}
|
||
yk, ben, neu, mal = PVR_TABLE30[sign]
|
||
return {"status": "reference_only", "profile": PVR_TABLE30_PROFILE, "source": PVR_TABLE30_SOURCE,
|
||
"ascendant": sign, "yogakarakas": list(yk), "functional_benefics": sorted(ben),
|
||
"functional_neutrals": sorted(neu), "functional_malefics": sorted(mal)}
|
||
|
||
|
||
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
|
||
|
||
|
||
# ── 占星师 2026-11-08 第五轮裁定 3:两套具名功能吉凶体系 ─────────────────────────────
|
||
# Default: PVR §13.2 Table 30. The product table above (BPHS ch. 34 general rules, trine first,
|
||
# plus sign exceptions) is kept whole as "bphs_exception_profile" and scores only when a caller
|
||
# names it. Where the two differ the output carries both roles side by side, never merged.
|
||
# 原值:"pvr_table_30"。新值:占星师的 profile id "pvr_table30_v1";旧名作别名继续接受。
|
||
# 原因:占星师 2026-11-08 第五轮逐项答复 K2/K3(同一 profile 在两仓同名,写进查询、缓存、审计与封存标签)。
|
||
FUNCTIONAL_PROFILE_PVR = "pvr_table30_v1"
|
||
FUNCTIONAL_PROFILE_BPHS = "bphs_exception_profile"
|
||
DEFAULT_FUNCTIONAL_PROFILE = FUNCTIONAL_PROFILE_PVR
|
||
FUNCTIONAL_PROFILES = (FUNCTIONAL_PROFILE_PVR, FUNCTIONAL_PROFILE_BPHS)
|
||
FUNCTIONAL_PROFILE_ALIASES = {
|
||
"pvr_table_30": FUNCTIONAL_PROFILE_PVR,
|
||
"pvr_integrated_approach_13_2_table_30": FUNCTIONAL_PROFILE_PVR,
|
||
"bphs_ch34_general_with_sign_exceptions_v3": FUNCTIONAL_PROFILE_BPHS,
|
||
"bphs_ch34_general_with_sign_exceptions_v3_166q4": FUNCTIONAL_PROFILE_BPHS,
|
||
}
|
||
|
||
|
||
class MixedFunctionalProfiles(ValueError):
|
||
"""One scoring run used more than one functional profile (K3: never mix tables)."""
|
||
|
||
|
||
def assert_single_functional_profile(layers: Any, *, run: str = "rectification") -> str | None:
|
||
"""The one functional profile of ``layers`` (functional dicts); raise when they differ.
|
||
|
||
占星师 2026-11-08 第五轮逐项答复 K3: one rectification scoring run never mixes tables.
|
||
"""
|
||
names = {str(layer.get("functional_profile")) for layer in layers or []
|
||
if isinstance(layer, dict) and layer.get("functional_profile")}
|
||
if len(names) > 1:
|
||
raise MixedFunctionalProfiles(f"{run}_mixed_functional_profiles: {sorted(names)}")
|
||
return next(iter(names), None)
|
||
|
||
|
||
def resolve_functional_profile(value: Any) -> str | None:
|
||
"""Canonical profile id for a name or alias; ``None`` / "" = the default; unknown -> ``None``."""
|
||
if value is None or (isinstance(value, str) and not value.strip()):
|
||
return DEFAULT_FUNCTIONAL_PROFILE
|
||
if not isinstance(value, str):
|
||
return None
|
||
name = value.strip()
|
||
name = FUNCTIONAL_PROFILE_ALIASES.get(name, name)
|
||
return name if name in FUNCTIONAL_PROFILES else None
|
||
PROFILE_IDS = {FUNCTIONAL_PROFILE_PVR: PVR_TABLE30_PROFILE, FUNCTIONAL_PROFILE_BPHS: PROFILE}
|
||
PROFILE_RULING = "占星师 2026-11-08 第五轮裁定 3:默认 PVR 表 30;BPHS 特殊格另存为具名 profile,只在被明确选中时参与打分"
|
||
|
||
# Movable lagnas whose Moon PVR Table 30 leaves out. Cancer is movable too, but there the Moon is the
|
||
# lagna lord and the table lists him as a benefic.
|
||
PVR_MOON_UNLISTED_LAGNAS = ("Aries", "Libra", "Capricorn")
|
||
# PVR §13.2 (printed pp. 166-167; PDF pp. 178-179), the note under Table 30.
|
||
PVR_MOVABLE_MOON_NOTE = (
|
||
"Moon is not listed for movable rasis, because his functional nature depends on whether he is waxing or "
|
||
"waning. Waxing Moon is a natural benefic and he becomes a functional malefic with quadrant ownership. "
|
||
"Waning Moon, on the other hand, is a natural malefic and quadrant ownership makes him functionally neutral."
|
||
)
|
||
PVR_MOVABLE_MOON_NOTE_ZH = "PVR 表 30 不列动星座上升的月亮:盈月(自然吉星)管角宫 → 功能凶;亏月(自然凶星)管角宫 → 功能中性"
|
||
# PVR §13.2: "In the case of planets owning two rasis, we need to judiciously combine the two indications."
|
||
DUAL_LORDSHIP_NOTE = (
|
||
"PVR §13.2:管两个星座的行星要把两宫的指示综合判断;表 30 是综合后的摘要,不是只看一个宫位的机械规则。"
|
||
)
|
||
DUAL_LORDSHIP_NOTE_EN = (
|
||
"PVR §13.2: for a planet owning two rasis the two indications must be judiciously combined; "
|
||
"Table 30 is that integrated summary, not a mechanical one-house rule."
|
||
)
|
||
# Earlier astrologer rulings on the product table (BUG-1570, BUG-1590, #166 Q4 cell notes), checked
|
||
# against Table 30 so none is silently dropped. Values: (ruling, product role, note).
|
||
PRIOR_RULINGS = {
|
||
("Aries", "Venus"): ("BUG-1570(BPHS 34.20)", "malefic", "两档一致:功能凶"),
|
||
("Scorpio", "Venus"): ("BUG-1570(BPHS 34.35)", "malefic", "两档一致:功能凶"),
|
||
("Gemini", "Saturn"): ("BUG-1590(#166 Q4)", "neutral", "两档一致:混合 / 中性"),
|
||
("Virgo", "Saturn"): ("BUG-1590(#166 Q4)", "neutral", "两档一致:混合 / 中性"),
|
||
("Gemini", "Mercury"): ("#166 Q4 歧义格说明", "benefic",
|
||
"冲突:#166 Q4 说明「上升主,支持」,产品表判功能吉;PVR 表 30 判功能中性。"
|
||
"按裁定 3 分档解决:默认 PVR 中性,bphs_exception_profile 保留功能吉,两者并列显示"),
|
||
}
|
||
|
||
|
||
def moon_paksha_from_longitudes(sun_longitude: Any, moon_longitude: Any) -> str | None:
|
||
"""'waxing' when the Moon is 0-180 degrees ahead of the Sun, 'waning' otherwise; None without data."""
|
||
try:
|
||
elongation = (float(moon_longitude) - float(sun_longitude)) % 360.0
|
||
except (TypeError, ValueError):
|
||
return None
|
||
return "waxing" if elongation < 180.0 else "waning"
|
||
|
||
|
||
def moon_paksha_from_planets(planets: Any) -> str | None:
|
||
"""Paksha from a planets mapping whose rows carry an absolute longitude."""
|
||
if not isinstance(planets, dict):
|
||
return None
|
||
|
||
def lon(name: str) -> Any:
|
||
row = planets.get(name)
|
||
if isinstance(row, (int, float)):
|
||
return row
|
||
if isinstance(row, dict):
|
||
for key in ("lon", "longitude", "degree_raw", "abs_degree"):
|
||
if isinstance(row.get(key), (int, float)):
|
||
return row[key]
|
||
sign, degree = normalize_sign(row.get("sign")), row.get("degree")
|
||
if sign is not None and isinstance(degree, (int, float)):
|
||
return SIGN_TO_INDEX[sign] * 30.0 + float(degree) if 0 <= degree < 30 else degree
|
||
return None
|
||
|
||
return moon_paksha_from_longitudes(lon("Sun"), lon("Moon"))
|
||
|
||
|
||
def _owned_houses(asc_sign: str) -> dict[str, list[int]]:
|
||
asc_idx = SIGN_TO_INDEX[asc_sign]
|
||
owned: dict[str, list[int]] = {}
|
||
for house_num in range(1, 13):
|
||
lord = SIGN_LORDS.get(SIGNS[(asc_idx + house_num - 1) % 12])
|
||
if lord:
|
||
owned.setdefault(lord, []).append(house_num)
|
||
return owned
|
||
|
||
|
||
def _bphs_exception_roles(asc_sign: str) -> dict[str, str]:
|
||
"""The product table (PROFILE): BPHS ch. 34 general rules plus sign exceptions. Unchanged logic."""
|
||
owned_houses = _owned_houses(asc_sign)
|
||
trines = {1, 5, 9}
|
||
kendras = {1, 4, 7, 10}
|
||
challenging = {3, 6, 8, 11}
|
||
roles: dict[str, str] = {}
|
||
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}:
|
||
roles[planet] = "benefic"
|
||
elif owns_trine:
|
||
roles[planet] = "benefic"
|
||
elif owns_challenge and 1 not in houses:
|
||
roles[planet] = "malefic"
|
||
elif owns_kendra and planet in {"Jupiter", "Venus", "Mercury", "Moon"}:
|
||
roles[planet] = "neutral"
|
||
else:
|
||
roles[planet] = "neutral"
|
||
eighth_lord = next((planet for planet, houses in owned_houses.items() if 8 in houses), None)
|
||
if eighth_lord in {"Sun", "Moon"} and roles.get(eighth_lord) == "malefic":
|
||
roles[eighth_lord] = "neutral"
|
||
for planet, role in ROLE_OVERRIDES.get(asc_sign, {}).items():
|
||
roles[planet] = role
|
||
return roles
|
||
|
||
|
||
def _bphs_yogakarakas(asc_sign: str) -> list[str]:
|
||
return sorted(planet for planet, houses in _owned_houses(asc_sign).items()
|
||
if set(houses) & {4, 7, 10} and set(houses) & {5, 9})
|
||
|
||
|
||
def _pvr_roles(asc_sign: str, moon_paksha: str | None) -> tuple[dict[str, str], dict[str, Any] | None]:
|
||
"""Table 30 roles; for an unlisted movable-lagna Moon the table's own paksha note decides."""
|
||
_yk, ben, neu, mal = PVR_TABLE30[asc_sign]
|
||
roles: dict[str, str] = {}
|
||
for group, role in ((ben, "benefic"), (neu, "neutral"), (mal, "malefic")):
|
||
for planet in group:
|
||
roles[planet] = role
|
||
moon_note = None
|
||
if asc_sign in PVR_MOON_UNLISTED_LAGNAS:
|
||
moon_role = {"waxing": "malefic", "waning": "neutral"}.get(str(moon_paksha or ""))
|
||
moon_note = {
|
||
"listed_in_table_30": False,
|
||
"moon_paksha": moon_paksha if moon_role else None,
|
||
"role": moon_role,
|
||
# 原值 "paksha_unknown_not_classified";新值 "blocked"(只有月亮 blocked,其他分类照旧)。
|
||
# 原因:占星师 2026-11-08 第五轮逐项答复 K2:未给月相时仅月亮标 blocked。
|
||
"status": "derived_from_table_30_note" if moon_role else "blocked",
|
||
"rule": PVR_MOVABLE_MOON_NOTE,
|
||
"rule_zh": PVR_MOVABLE_MOON_NOTE_ZH,
|
||
"source": "PVR §13.2 note under Table 30 (printed p. 166-167; PDF p. 178-179)",
|
||
}
|
||
if moon_role:
|
||
roles["Moon"] = moon_role
|
||
else:
|
||
moon_note["reason"] = "moon_paksha_missing"
|
||
return roles, moon_note
|
||
|
||
|
||
def _profile_roles(asc_sign: str, profile: str, moon_paksha: str | None) -> tuple[dict[str, str], list[str], dict[str, Any] | None]:
|
||
if profile == FUNCTIONAL_PROFILE_PVR:
|
||
roles, moon_note = _pvr_roles(asc_sign, moon_paksha)
|
||
return roles, sorted(PVR_TABLE30[asc_sign][0]), moon_note
|
||
return _bphs_exception_roles(asc_sign), _bphs_yogakarakas(asc_sign), None
|
||
|
||
|
||
def _blocked(ascendant: Any, profile: str, reason: str) -> dict[str, Any]:
|
||
return {
|
||
"status": "blocked",
|
||
"ascendant": ascendant if isinstance(ascendant, str) else None,
|
||
"functional_profile": profile,
|
||
"functional_benefics": [],
|
||
"functional_malefics": [],
|
||
"functional_neutrals": [],
|
||
"yogakarakas": [],
|
||
"owned_houses": {},
|
||
"effect_on_confidence": reason,
|
||
"source": SOURCE,
|
||
}
|
||
|
||
|
||
def derive_functional_benefic_malefic(
|
||
ascendant: Any,
|
||
*,
|
||
functional_profile: str = DEFAULT_FUNCTIONAL_PROFILE,
|
||
moon_paksha: str | None = None,
|
||
) -> dict[str, Any]:
|
||
"""Functional benefics, malefics and ownership roles for a Lagna under one named profile.
|
||
|
||
``functional_profile``: ``"pvr_table30_v1"`` (alias ``"pvr_table_30"``; default, 占星师 2026-11-08 第五轮裁定 3) or
|
||
``"bphs_exception_profile"`` (the former product table; scores only when a caller names it).
|
||
``moon_paksha`` (``"waxing"`` / ``"waning"``) classifies the Moon for Aries / Libra / Capricorn
|
||
lagnas under Table 30's own note; without it that Moon is left unclassified, never guessed.
|
||
"""
|
||
requested = functional_profile
|
||
functional_profile = resolve_functional_profile(functional_profile)
|
||
if functional_profile is None:
|
||
return _blocked(ascendant, requested, f"Functional layer blocked: unknown functional_profile {requested!r}.")
|
||
asc_sign = normalize_sign(ascendant)
|
||
if asc_sign is None:
|
||
return _blocked(ascendant, functional_profile, "Functional layer blocked: unknown ascendant sign.")
|
||
|
||
other = FUNCTIONAL_PROFILE_BPHS if functional_profile == FUNCTIONAL_PROFILE_PVR else FUNCTIONAL_PROFILE_PVR
|
||
roles, yogakarakas, moon_note = _profile_roles(asc_sign, functional_profile, moon_paksha)
|
||
other_roles, other_yogakarakas, other_moon_note = _profile_roles(asc_sign, other, moon_paksha)
|
||
owned_houses = _owned_houses(asc_sign)
|
||
by_role = {role: sorted(p for p, r in roles.items() if r == role) for role in ("benefic", "malefic", "neutral")}
|
||
other_by_role = {role: sorted(p for p, r in other_roles.items() if r == role) for role in ("benefic", "malefic", "neutral")}
|
||
|
||
divergence = []
|
||
for planet in PLANETS:
|
||
mine, theirs = roles.get(planet), other_roles.get(planet)
|
||
if mine != theirs:
|
||
divergence.append({
|
||
"planet": planet,
|
||
functional_profile: mine or "not_classified",
|
||
other: theirs or "not_classified",
|
||
"kind": "movable_lagna_moon_paksha" if planet == "Moon" and asc_sign in PVR_MOON_UNLISTED_LAGNAS else "table_cell",
|
||
})
|
||
|
||
if functional_profile == FUNCTIONAL_PROFILE_PVR:
|
||
role_basis = {
|
||
planet: ("PVR §13.2 Table 30 note: movable-lagna Moon by paksha" if planet == "Moon" and moon_note
|
||
else "PVR §13.2 Table 30")
|
||
for planet in PLANETS
|
||
}
|
||
if moon_note and not moon_note["role"]:
|
||
role_basis["Moon"] = "PVR §13.2 Table 30 does not list this Moon; paksha unknown: blocked (moon_paksha_missing)"
|
||
profile_source = PVR_TABLE30_SOURCE
|
||
else:
|
||
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
|
||
}
|
||
profile_source = "BPHS ch. 34 general ownership rules plus sign exceptions (references/functional-role-profile.md)"
|
||
|
||
prior = []
|
||
for (sign, planet), (ruling, product_role, note) in PRIOR_RULINGS.items():
|
||
if sign != asc_sign:
|
||
continue
|
||
pvr_role = (roles if functional_profile == FUNCTIONAL_PROFILE_PVR else other_roles).get(planet)
|
||
prior.append({"planet": planet, "ruling": ruling, FUNCTIONAL_PROFILE_BPHS: product_role,
|
||
FUNCTIONAL_PROFILE_PVR: pvr_role, "agrees": pvr_role == product_role, "resolution": note})
|
||
|
||
result: dict[str, Any] = {
|
||
"status": "used",
|
||
"ascendant": asc_sign,
|
||
"functional_profile": functional_profile,
|
||
"functional_profile_ruling": PROFILE_RULING,
|
||
# Planets this profile cannot classify for lack of input (K2: only the Moon, only without paksha).
|
||
"blocked_planets": ["Moon"] if moon_note and not moon_note["role"] and functional_profile == FUNCTIONAL_PROFILE_PVR else [],
|
||
"functional_benefics": by_role["benefic"],
|
||
"functional_malefics": by_role["malefic"],
|
||
"functional_neutrals": by_role["neutral"],
|
||
"yogakarakas": yogakarakas,
|
||
"owned_houses": {planet: houses for planet, houses in sorted(owned_houses.items())},
|
||
"effect_on_confidence": (
|
||
"高严谨模式下必须叠加功能性宫主吉凶与自然吉凶;"
|
||
"若功能属性与自然属性冲突,应降低置信度或显式标记冲突。"
|
||
),
|
||
"source": SOURCE,
|
||
"profile": PROFILE_IDS[functional_profile],
|
||
"profile_source": profile_source,
|
||
"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",
|
||
"profile_is_not": PROFILE_IDS[other],
|
||
"assertion_label": "parameter_sensitive",
|
||
"dual_lordship_note": DUAL_LORDSHIP_NOTE,
|
||
"dual_lordship_note_en": DUAL_LORDSHIP_NOTE_EN,
|
||
"role_basis": role_basis,
|
||
# Both profiles side by side where they differ; the other profile never scores here.
|
||
"profile_divergence": {
|
||
"status": "differs" if divergence else "same",
|
||
"selected_profile": functional_profile,
|
||
"other_profile": other,
|
||
"other_profile_scored": False,
|
||
"display_rule": "两档不同时并列显示,不合并成一张表;默认档参与判断,另一档只作对照",
|
||
"cells": divergence,
|
||
"other_profile_roles": {
|
||
"functional_benefics": other_by_role["benefic"],
|
||
"functional_malefics": other_by_role["malefic"],
|
||
"functional_neutrals": other_by_role["neutral"],
|
||
"yogakarakas": other_yogakarakas,
|
||
},
|
||
},
|
||
"prior_rulings": prior,
|
||
"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.",
|
||
"Dual lordship needs an integrated judgment of both houses (PVR §13.2).",
|
||
],
|
||
}
|
||
if requested not in (None, "", functional_profile):
|
||
result["functional_profile_requested"] = requested # an alias, recorded as given
|
||
if moon_note or other_moon_note:
|
||
result["movable_lagna_moon"] = moon_note or other_moon_note
|
||
if functional_profile == FUNCTIONAL_PROFILE_BPHS:
|
||
result["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
|
||
}
|
||
else:
|
||
# #166 Q4 cell notes belong to the product table; under Table 30 they stay visible as notes on
|
||
# the other profile, with the Table 30 role beside them.
|
||
result["ambiguous_cell_notes"] = {
|
||
planet: {"summary": note, "basis": basis, "summary_en": note_en, "basis_en": basis_en,
|
||
"applies_to_profile": FUNCTIONAL_PROFILE_BPHS, "pvr_table_30_role": roles.get(planet)}
|
||
for (sign, planet), (note, basis, note_en, basis_en) in AMBIGUOUS_CELL_NOTES.items() if sign == asc_sign
|
||
}
|
||
return result
|