Files
Jyotisha/scripts/chart_view_points/functional_roles.py
T
ClaudeandClaude Opus 5.5 578a361460 feat(chart): functional roles, 7/8 Chara Karaka and three new 特殊点 groups (#163 1-3)
/api/chart_points:
- karaka_scheme 7 (default) / 8 / both; shared ranking in
  chart_view_points/karaka_rank.py (unrounded in-sign degree, Rahu reversed
  in the 8 scheme, Ketu out). Bodies equal at full precision mark only the
  roles they span as tied; Goel uses the same ranking (BUG-1512).
- functional_roles: v2 BPHS ch.34 profile laid out per body, nodes pending,
  Yogakaraka / Badhaka / Kendradhipati (conditions not evaluated) as
  separate labels.
- sensitive_points (Bhrigu Bindu per chart node_mode, 64th Navamsa, 22nd
  Drekkana sign-only), sahams (PVR Table 74, nine first, all kept),
  goel_points (printed-text policy, worked example beside it).

盘面数据: 行星 gets Chara Karaka (AK · 自我, 七星/八星 switch), 自然属性,
管辖宫位, 功能角色, 补充身份; 特殊点 splits into 特殊上升 / 敏感点 / Saham /
Goel 特殊点 and the board's 特殊点 layer follows the open group.

jaimini.py, functional_benefics.py and varga.py are untouched (rectification
frozen identity).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbBXoyeZrYn6ryNcSe5Y2Y
2026-11-02 20:35:31 +08:00

101 lines
4.9 KiB
Python

"""Per-planet functional role table for the chart page (issue #163).
Reads ``functional_benefics.derive_functional_benefic_malefic`` (profile
``bphs_ch34_general_with_sign_exceptions_v2``) and lays it out one row per
body, with the natural nature and two independent identities beside it.
Nothing here reclassifies a planet:
- ``functional`` is the profile's class for the seven. ``neutral`` in that
profile means mixed / association-dependent; associations are not evaluated
there, so it is never a "cancelled out" verdict.
- Rahu and Ketu are not in the seven-lord table. Their role depends on
placement, dispositor and associations, which nothing here evaluates, so
``functional`` is ``pending``.
- ``yogakaraka`` is the profile's strict field: distinct lordships, one of
4/7/10 and one of 5/9.
- ``badhaka`` is the lord of ``yoga_engine.badhaka_house`` (movable 11,
fixed 9, dual 7). It is a separate label; it never changes ``functional``.
- ``kendradhipati`` marks a natural benefic that owns 4, 7 or 10 and none of
1, 5, 9. The dosha's conditions (associations, placement, maraka role) are
not evaluated anywhere in the engine, so the label is always
``conditions_not_evaluated``.
``functional_benefics.py`` is part of the rectification production identity,
so this layout lives in the chart page package.
"""
from __future__ import annotations
from typing import Any
try:
from functional_benefics import PROFILE, SIGN_LORDS, SIGNS, SOURCE, derive_functional_benefic_malefic, normalize_sign
from yoga_engine import BENEFICS, MALEFICS, badhaka_house
except ImportError: # package import (scripts.chart_view_points.functional_roles)
from scripts.functional_benefics import PROFILE, SIGN_LORDS, SIGNS, SOURCE, derive_functional_benefic_malefic, normalize_sign
from scripts.yoga_engine import BENEFICS, MALEFICS, badhaka_house
BODIES = ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu")
NODES = ("Rahu", "Ketu")
# The engine's natural lists treat Moon and Mercury as benefics without
# checking the Moon's phase or Mercury's company; say so instead of judging it.
NATURAL_CONDITION = {"Moon": "lunar_phase_not_evaluated", "Mercury": "association_not_evaluated"}
TRANSLATION = "BPHS ch.34, public English translation https://jyotishvidya.com/ch34.htm (inspected 2026-10-02)"
def functional_role_table(ascendant: Any) -> dict[str, Any]:
base = derive_functional_benefic_malefic(ascendant)
asc_sign = normalize_sign(ascendant)
if base.get("status") != "used" or asc_sign is None:
return {"status": "blocked", "reason": "unknown_ascendant_sign", "profile": PROFILE, "source": SOURCE}
benefics = set(base["functional_benefics"])
malefics = set(base["functional_malefics"])
yogakarakas = set(base["yogakarakas"])
owned = base["owned_houses"]
b_house = badhaka_house(asc_sign)
b_lord = SIGN_LORDS[SIGNS[(SIGNS.index(asc_sign) + b_house - 1) % 12]] if b_house else None
rows = []
for planet in BODIES:
natural = "benefic" if planet in BENEFICS else "malefic" if planet in MALEFICS else None
houses = list(owned.get(planet, []))
identities: list[dict[str, Any]] = []
if planet in NODES:
functional = "pending"
basis = "node_placement_dispositor_association_not_evaluated"
else:
functional = "benefic" if planet in benefics else "malefic" if planet in malefics else "neutral"
basis = base["role_basis"].get(planet)
if planet in yogakarakas:
identities.append({"key": "yogakaraka"})
if planet == b_lord:
identities.append({"key": "badhaka", "house": b_house})
if (natural == "benefic" and set(houses) & {4, 7, 10} and not set(houses) & {1, 5, 9}):
identities.append({"key": "kendradhipati", "status": "conditions_not_evaluated"})
conflict = functional in ("benefic", "malefic") and natural is not None and functional != natural
rows.append({
"planet": planet,
"natural": natural,
"natural_condition": NATURAL_CONDITION.get(planet),
"owned_houses": houses,
"functional": functional,
"role_basis": basis,
"identities": identities,
"natural_functional_conflict": conflict,
})
return {
"status": "computed",
"ascendant": asc_sign,
"profile": PROFILE,
"source": SOURCE,
"translation": TRANSLATION,
"rule_reference": base.get("rule_reference"),
"yogakaraka_definition": base.get("yogakaraka_definition"),
"badhaka_rule": "movable 11 / fixed 9 / dual 7 (yoga_engine.badhaka_house)",
"badhaka_house": b_house,
"badhaka_lord": b_lord,
"kendradhipati_rule": "natural benefic owning 4/7/10 and none of 1/5/9; dosha conditions not evaluated",
"neutral_meaning": "mixed / association-dependent; associations not evaluated",
"rows": rows,
}