"""`/api/chart_points`: the chart page's data tables and point layers (issue #114). Everything here is an existing engine producer called directly, so the chart page does not have to run the full reading: - upagrahas: ``gulika.calculate_upagrahas`` (solar five + six time-segment points) - special lagnas: ``native_special_lagnas.native_special_lagna_rows`` (Swiss Ephemeris sunrise; not ``jaimini.calc_sunrise_utc_hours``, BUG-1462) - Yogi / Avayogi: ``annual_native.yogi_sphuta.build_yogi_sphuta_local`` - arudha padas A1–A11 and UL: ``jaimini.calc_arudha_padas`` - friendship (natural, temporary, compound): ``jyotish_engine._build_planetary_friendship_snapshot`` - Chara karakas: ``chart_view_points.karaka_rank.rank_chara_karakas`` (the ranking Goel uses too), seven-karaka by default; ``karaka_scheme`` ``"8"`` gives the eight-karaka table (Rahu reversed, Ketu out) and ``"both"`` adds it as ``chara_karakas_8``. Bodies equal at full precision mark only the roles they span as tied; the other roles keep their planet (issue #163) - functional roles: ``chart_view_points.functional_roles.functional_role_table`` over ``functional_benefics`` profile ``bphs_ch34_general_with_sign_exceptions_v2`` - sensitive points: Bhrigu Bindu from ``jyotish_engine._calc_sensitive_points`` (the chart's own Rahu, so it follows ``node_mode``), 64th Navamsa from the Moon and 22nd Drekkana from the Lagna (``varga``); the last two are a sign and its lord only - Sahams: ``tajika.calc_all_sahams`` (PVR §28.8.1 Table 74; day or night from the birth time and place, ``saham_daynight`` Hindu rising) - Goel special points: ``goel_jaimini_strength.build_goel_chart_evidence`` under the printed-text node policy, with the worked-example policy beside it Each block fails on its own (``status: blocked`` + reason); one failure never fails the response. """ from __future__ import annotations from datetime import datetime from typing import Any, Callable SIGNS = ( "Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces", ) class ChartPointsInputError(ValueError): """The request is missing the birth moment or the natal chart.""" def _number(body: dict[str, Any], key: str) -> float: value = body.get(key) if isinstance(value, bool) or not isinstance(value, (int, float)): raise ChartPointsInputError(f"{key} must be a number") return float(value) def _guard(build: Callable[[], Any]) -> Any: try: return build() except Exception as exc: # each block is independent return {"status": "blocked", "reason": f"{exc.__class__.__name__}: {exc}"} def _longitude(planet: Any) -> float | None: if not isinstance(planet, dict): return None for key in ("lon", "degree_raw", "longitude"): value = planet.get(key) if isinstance(value, (int, float)) and not isinstance(value, bool): return float(value) % 360.0 return None def _nakshatra(longitude: float) -> dict[str, Any]: from jyotish_engine import _nakshatra_fields return _nakshatra_fields(longitude) def _point(key: str, longitude: float, asc_sign: int, **extra: Any) -> dict[str, Any]: longitude = longitude % 360.0 sign = int(longitude // 30) % 12 return { **_nakshatra(longitude), "key": key, "longitude": round(longitude, 4), "sign": SIGNS[sign], "degree_in_sign": round(longitude % 30, 4), "house": (sign - asc_sign) % 12 + 1, **extra, } UPAGRAHA_ORDER = ( "Dhuma", "Vyatipata", "Parivesha", "Indrachapa", "Upaketu", "Kaala", "Mrityu", "Artha_Praharaka", "Yama_Ghantaka", "Gulika", "Maandi", ) SPECIAL_ORDER = ( "Bhava_Lagna", "Hora_Lagna", "Ghati_Lagna", "ViGhati_Lagna", "Pranapada_Lagna", "Sree_Lagna", "Indu_Lagna", "Varnada_Lagna", ) SEVEN = ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn") NINE = SEVEN + ("Rahu", "Ketu") ARUDHA_ORDER = ("A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "A10", "A11", "UL") # Interface order for the Saham group: these nine first, the rest under 「全部」. SAHAM_PRIMARY = ( "Punya_Saham", "Vidya_Saham", "Yashas_Saham", "Mitra_Saham", "Vivaha_Saham", "Artha_Saham", "Labha_Saham", "Karyasiddhi_Saham", "Paradesa_Saham", ) GOEL_POINTS = ("Brahma", "Maheshwara", "Rudra") GOEL_POLICIES = ("include_nodes_text", "exclude_nodes_worked_example") def _karaka_scheme(body: dict[str, Any]) -> str: value = str(body.get("karaka_scheme") or "7").strip() if value not in ("7", "8", "both"): raise ChartPointsInputError("karaka_scheme must be 7, 8 or both") return value def compute_chart_points(body: dict[str, Any]) -> dict[str, Any]: if not isinstance(body, dict): raise ChartPointsInputError("request body must be an object") planets = body.get("planets") ascendant = body.get("ascendant") if not isinstance(planets, dict) or not isinstance(ascendant, dict): raise ChartPointsInputError("planets and ascendant from /api/chart are required") asc_lon = _longitude(ascendant) sun_lon = _longitude(planets.get("Sun")) if asc_lon is None or sun_lon is None: raise ChartPointsInputError("ascendant and Sun longitudes are required") year, month, day = int(_number(body, "year")), int(_number(body, "month")), int(_number(body, "day")) hour, minute = int(_number(body, "hour")), int(_number(body, "minute")) second = int(body.get("second") or 0) lat, lon, tz = _number(body, "lat"), _number(body, "lon"), _number(body, "tz") ayanamsa = str(body.get("ayanamsa") or "raman") node_mode = str(body.get("node_mode") or "unspecified") karaka_scheme = _karaka_scheme(body) longitudes = {name: value for name in NINE if (value := _longitude(planets.get(name))) is not None} birth = datetime(year, month, day, hour, minute, second) asc_sign = int(asc_lon // 30) % 12 def upagrahas() -> dict[str, Any]: from gulika import calculate_upagrahas packet = calculate_upagrahas(birth, lat=lat, lon=lon, tz=tz, sun_longitude=sun_lon, ayanamsa=ayanamsa) raw = packet.get("raw") or {} rows = [ _point(name, float(raw[name]["longitude"]), asc_sign, kind="solar" if raw[name].get("source") == "solar_longitude_formula" else "time_segment") for name in UPAGRAHA_ORDER if isinstance(raw.get(name), dict) and "longitude" in raw[name] ] return {"status": "computed", "rows": rows, "boundary": packet.get("boundary")} def special() -> dict[str, Any]: from native_special_lagnas import native_special_lagna_rows rows = native_special_lagna_rows(birth, lat, lon, tz, asc_lon, ayanamsa) out = [ _point(key, float(rows[key]["longitude"]), asc_sign) for key in SPECIAL_ORDER if isinstance(rows.get(key), dict) and rows[key].get("status") == "computed" ] if not out: reasons = {row.get("reason") for row in rows.values() if isinstance(row, dict)} return {"status": "blocked", "reason": ";".join(sorted(str(r) for r in reasons if r))} return {"status": "computed", "rows": out} def yogi() -> dict[str, Any]: from annual_native.yogi_sphuta import build_yogi_sphuta_local packet = build_yogi_sphuta_local({"planets": planets, "ascendant": ascendant}) rows = [] for key, name in (("yogi", "Yogi_Point"), ("ava_yogi", "Avayogi_Point")): part = packet.get(key) if isinstance(part, dict) and isinstance(part.get("longitude"), (int, float)): rows.append(_point(name, float(part["longitude"]), asc_sign)) if not rows: return {"status": "blocked", "reason": str(packet.get("reason") or "yogi_points_missing")} return {"status": "computed", "rows": rows, "formula": packet.get("formula")} def arudha() -> dict[str, Any]: from jaimini import calc_arudha_padas longitudes = {name: value for name, value in ((n, _longitude(p)) for n, p in planets.items()) if value is not None} padas = calc_arudha_padas(asc_sign, longitudes).get("padas") or {} rows = [] for key in ARUDHA_ORDER: pada = padas.get(key) if not isinstance(pada, dict) or pada.get("sign") not in SIGNS: continue sign = SIGNS.index(pada["sign"]) rows.append({ "key": key, "sign": pada["sign"], "house": (sign - asc_sign) % 12 + 1, "source_house": int(pada.get("house_num") or ARUDHA_ORDER.index(key) + 1), "source_lord": pada.get("source_house_lord"), "exception": bool(pada.get("exception_triggered")), }) return {"status": "computed", "rows": rows} def karakas(scheme: int) -> dict[str, Any]: from chart_view_points.karaka_rank import rank_chara_karakas ranking = rank_chara_karakas({name: lon % 30 for name, lon in longitudes.items()}, scheme) if ranking["status"] != "computed": return {"status": "blocked", "reason": ranking["reason"], "scheme": f"chara_{scheme}"} rows = [ { "key": row["key"], "name": row["name"], "rank": row["rank"], "planet": row["planet"], "tied": row["tied"], "candidates": row["candidates"], "degree_in_sign": round(row["degree_in_sign"], 6), "sort_degree": round(row["sort_degree"], 6), } for row in ranking["rows"] ] return { "status": "computed", "scheme": f"chara_{scheme}", "rows": rows, "ties": ranking["ties"], "sort_basis": ranking["sort_basis"], "tie_tolerance": ranking["tie_tolerance"], } def functional() -> dict[str, Any]: from chart_view_points.functional_roles import functional_role_table return functional_role_table(SIGNS[asc_sign]) def sensitive() -> dict[str, Any]: from jyotish_engine import _calc_sensitive_points from varga import calc_22nd_drekkana, calc_64th_navamsa rows = [] bhrigu = _calc_sensitive_points(planets).get("bhrigu_bindu") if isinstance(bhrigu, dict) and isinstance(bhrigu.get("longitude"), (int, float)): rows.append(_point( "Bhrigu_Bindu", float(bhrigu["longitude"]), asc_sign, precision="longitude", lord=bhrigu.get("lord"), reference={"from": "Rahu", "to": "Moon", "arc": bhrigu.get("arc_mode"), "node_mode": node_mode}, )) moon = longitudes.get("Moon") if moon is not None: n64 = calc_64th_navamsa(moon) sign = SIGNS.index(n64["sign"]) rows.append({ "key": "Navamsa_64", "sign": n64["sign"], "house": (sign - asc_sign) % 12 + 1, "lord": n64["lord"], "precision": "sign", "reference": {"from": "Moon", "base_sign": n64["base_navamsa_sign"], "division": "D9", "count": 64}, }) d22 = calc_22nd_drekkana(asc_lon) sign = SIGNS.index(d22["sign"]) rows.append({ "key": "Drekkana_22", "sign": d22["sign"], "house": (sign - asc_sign) % 12 + 1, "lord": d22["lord"], "precision": "sign", "reference": {"from": "Lagna", "base_sign": d22["base_drekkana_sign"], "division": "D3", "count": 22}, }) return {"status": "computed", "rows": rows, "node_mode": node_mode} def sahams() -> dict[str, Any]: from tajika import calc_all_sahams if len(longitudes) < len(NINE): return {"status": "blocked", "reason": "planets_missing"} packet = calc_all_sahams(dict(longitudes), asc_lon, birth, "natal", lat=lat, lon=lon, tz=tz) if packet.get("status") == "blocked": return {"status": "blocked", "reason": str(packet.get("reason"))} rows = [] for value in packet.values(): if not isinstance(value, dict) or not isinstance(value.get("longitude"), (int, float)): continue rule = value.get("formula_rule") key = rule or str(value.get("name") or "").replace(" ", "_") extra: dict[str, Any] = { "name": value.get("name"), "rule_backed": bool(rule), "primary": key in SAHAM_PRIMARY, } if rule: extra.update({ "formula_operands": value.get("formula_operands"), "formula_variant": value.get("formula_variant"), "plus_30_applied": value.get("zodiacal_order_exception_applied"), }) rows.append(_point(key, float(value["longitude"]), asc_sign, **extra)) order = {key: index for index, key in enumerate(SAHAM_PRIMARY)} rows.sort(key=lambda row: (order.get(row["key"], len(order)), not row["rule_backed"])) daynight = packet.get("daynight_evidence") or {} return { "status": "computed", "rows": rows, "is_daytime": daynight.get("is_daytime"), "daynight_method": daynight.get("method"), "source": "PVR Narasimha Rao, Vedic Astrology: An Integrated Approach, §28.8.1 Table 74", } def goel() -> dict[str, Any]: try: from scripts.goel_jaimini_strength import PROFILE, SOURCE, build_goel_chart_evidence except ImportError: from goel_jaimini_strength import PROFILE, SOURCE, build_goel_chart_evidence if len(longitudes) < len(NINE): return {"status": "blocked", "reason": "planets_missing"} runs = { policy: build_goel_chart_evidence(dict(longitudes), asc_lon, node_occupancy_policy=policy) for policy in GOEL_POLICIES } main, compare = (runs[policy] for policy in GOEL_POLICIES) if main["strength"].get("status") != "executed": return { "status": "blocked", "reason": str(main["strength"].get("reason")), "karaka_ties": main["strength"].get("karaka_ties"), "profile": PROFILE, } rows = [] for name in GOEL_POINTS: point, other = main["special_points"].get(name) or {}, compare["special_points"].get(name) or {} planet = point.get("value") if point.get("status") == "executed" else None row: dict[str, Any] = { "key": f"Goel_{name}", "point": name, "status": point.get("status"), "planet": planet, "rule": point.get("rule"), "reason": point.get("reason"), "candidates": point.get("candidates"), "compare": { "status": other.get("status"), "planet": other.get("value") if other.get("status") == "executed" else None, "reason": other.get("reason"), }, } row["differs"] = (row["status"], planet) != (row["compare"]["status"], row["compare"]["planet"]) if planet in longitudes: sign = int(longitudes[planet] // 30) % 12 row.update({"sign": SIGNS[sign], "house": (sign - asc_sign) % 12 + 1}) rows.append(row) return { "status": "computed", "rows": rows, "policy": GOEL_POLICIES[0], "compare_policy": GOEL_POLICIES[1], "lagna_seventh_strength": {policy: runs[policy].get("lagna_seventh_strength") for policy in GOEL_POLICIES}, "karaka_scheme": "chara_7", "profile": PROFILE, "source": SOURCE, } def friendship() -> dict[str, Any]: from jyotish_engine import _build_planetary_friendship_snapshot return _build_planetary_friendship_snapshot(planets) return { "success": True, "endpoint": "chart_points", "ascendant_sign": SIGNS[asc_sign], "upagrahas": _guard(upagrahas), "special_lagnas": _guard(special), "yogi_points": _guard(yogi), "arudha_padas": _guard(arudha), "friendship": _guard(friendship), "chara_karakas": _guard(lambda: karakas(8 if karaka_scheme == "8" else 7)), **({"chara_karakas_8": _guard(lambda: karakas(8))} if karaka_scheme == "both" else {}), "functional_roles": _guard(functional), "sensitive_points": _guard(sensitive), "sahams": _guard(sahams), "goel_points": _guard(goel), }