Files
Jyotisha/scripts/functional_benefics.py
T
ClaudeandClaude Opus 5.5 5d64f71b81 fix: astrologer round-5 rulings 2/3/4/5/10/13 (BUG-1746..1752)
- Functional default = PVR Table 30 (functional_profile=pvr_table_30); the former
  product table is bphs_exception_profile, scored only when named (rectification
  names it, scores unchanged). profile_divergence shows both roles side by side;
  movable-lagna Moon by the table's paksha note; dual-lordship note; prior rulings
  BUG-1570/1590/#166 Q4 checked against Table 30.
- Shadbala: six open items labelled parameter_sensitive; every option combination
  computed; only planets whose pass/fail flips get minimum_check=parameter_sensitive.
- Vimshottari AD/PD rows carry parent_period_start / visible_period_start; the
  consult card cuts the balance MD from its full parent and clips at birth.
- KP DBA: full first parent, clipped at birth (no balance rescale).
- Mudda: balance fields + balance_method (angle default, time comparison apart),
  nominal_year_days, her crossing wording; Harsha combined blocked reason lists items.
- D30 _meta describes the unequal parts (as her r40).
- Golden recaptured with TTL=0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbBXoyn7Hj3LCW3BUs5bZ8
2026-11-08 11:57:22 +08:00

413 lines
22 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.
# 原值 "..._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.
FUNCTIONAL_PROFILE_PVR = "pvr_table_30"
FUNCTIONAL_PROFILE_BPHS = "bphs_exception_profile"
DEFAULT_FUNCTIONAL_PROFILE = FUNCTIONAL_PROFILE_PVR
FUNCTIONAL_PROFILES = (FUNCTIONAL_PROFILE_PVR, FUNCTIONAL_PROFILE_BPHS)
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,
"status": "derived_from_table_30_note" if moon_role else "paksha_unknown_not_classified",
"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
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_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.
"""
if functional_profile not in FUNCTIONAL_PROFILES:
return _blocked(ascendant, functional_profile, f"Functional layer blocked: unknown functional_profile {functional_profile!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, not classified"
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,
"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 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