Files
Jyotisha/scripts/upstream_technique_layers.py
T
ClaudeandClaude Opus 5.5 ac9be6f88c feat(techniques): port the astrologer upstream skill's missing techniques
Port from yinduzhanxing 5fb8379c (MIT): Naisargika, Kaala, Karaka, Tara,
Lagnamsaka, Yogardha, Mandooka, Paryaaya and Padanadhamsa dashas;
Sarvatobhadra, Kota and Kalanala chakras; Nakshatra Latta; Pachakadi;
Raman Amsayu and Pindayu. Producers and their upstream tests are copied;
scripts/upstream_technique_layers.py feeds them our natal and transit
positions and attaches chart.consultation_native_layers.upstream_techniques.

- dasha_registry: kaala and padanadhamsa keys (35 -> 37); the nine ported
  dashas are served by compute_dasha or fail closed with upstream's reason.
- technique_registry.json: 16 partial entries; README badges follow.
- Evidence lookup section `upstream_techniques` in the consultation tool.
- Boundaries: Kalanala carries no PyJHora (AGPL) grid coordinates;
  Padanadhamsa/Lagnamsaka run on our Narayana engine (no Rath Table 12,
  PVR co-lord variants or second cycle). PyJHora observers and the Amsayu
  worked-example scaffold live in scripts/research (root .py budget).
- PyJHora black-box cross-checks: Naisargika (lords/years) and Kaala
  (seed segment and fraction within 5e-4) on a public chart.

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

576 lines
31 KiB
Python

"""Techniques ported from the astrologer's upstream skill, computed from our chart.
Upstream: /workspace/yinduzhanxing (yinduzhanxing, MIT) at 5fb8379c. Each
producer module (naisargika_dasha, kaala_dasha, karaka_dasha, tara_dasha,
lagnamsaka_dasha, yogardha_dasha, mandooka_dasha, paryaaya_dasha,
padanadhamsa, sarvatobhadra_chakra, kota_chakra, kalanala_chakra,
nakshatra_latta, pachakadi_relationships, amsayu_raman, pindayu_raman) is
upstream's own code; this file only feeds it our natal / transit positions
and keeps a compact view for consultations and reports.
Upstream keeps every school decision an explicit input and fails closed when
one is missing. That stays: where upstream has no source rule for an input
(Karaka durations, Mandooka / Paryaaya sequences, Tara strength order with
nodes, Rangacharya Jaimini Navamsa), the section is `blocked` with the
missing input named. Where we supply an input, the `inputs` field says what
was used. Nothing here is a verified prediction source: upstream sets
`customer_export_admitted = False` on all of these, and so do we.
Two upstream boundaries are narrowed here:
- Kalanala: upstream embeds PyJHora (AGPL) grid coordinates; we keep PyJHora
black-box only, so only the star slot is computed (see kalanala_chakra.py).
- Padanadhamsa / Lagnamsaka run on our own Narayana engine (the astrologer's
rulings), so upstream's PVR co-lord variants, Rath Table 12 (Ketu seed) and
second cycle are not available.
"""
from __future__ import annotations
import sys
from datetime import datetime, timedelta, timezone
from fractions import Fraction
from pathlib import Path
from typing import Any
SCHEMA = "jyotish.upstream_techniques.v1"
UPSTREAM = {"repo": "yinduzhanxing", "commit": "5fb8379c", "license": "MIT"}
GRAHAS = ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu")
SEVEN = GRAHAS[:7]
SIGN_NAMES = (
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
)
SIGN_LORDS = ("Mars", "Venus", "Mercury", "Moon", "Sun", "Mercury",
"Venus", "Mars", "Jupiter", "Saturn", "Saturn", "Jupiter")
EXALTATION_SIGN = {"Sun": 0, "Moon": 1, "Mars": 9, "Mercury": 5, "Jupiter": 3, "Venus": 11, "Saturn": 6}
NATURAL_MALEFICS = {"Sun", "Mars", "Saturn", "Rahu", "Ketu"}
# Sidereal year, the constant upstream's Naisargika / Karaka producers use.
YEAR_DAYS = 365.25636
STAR_SPAN = 40.0 / 3.0
BOUNDARY = (
"Ported from the astrologer's upstream skill; parameter_sensitive and not "
"event-validated. Supporting context only, never a stand-alone conclusion."
)
# Technique registry: one row per ported technique. `output_key` is the key in
# chart.consultation_native_layers.upstream_techniques.sections; `dasha_key`
# is the /api/dasha registry key when the technique is a dasha.
TECHNIQUE_REGISTRY: dict[str, dict[str, Any]] = {
"naisargika_dasha": {"name": "Naisargika Dasha", "kind": "dasha", "module": "naisargika_dasha", "dasha_key": "naisargika"},
"kaala_dasha": {"name": "Kaala Dasha", "kind": "dasha", "module": "kaala_dasha", "dasha_key": "kaala"},
"karaka_dasha": {"name": "Karaka Dasha", "kind": "dasha", "module": "karaka_dasha", "dasha_key": "karaka"},
"tara_dasha": {"name": "Tara Dasha (Rath Lesson 4)", "kind": "dasha", "module": "tara_dasha", "dasha_key": "tara"},
"lagnamsaka_dasha": {"name": "Lagnamsaka Dasha", "kind": "dasha", "module": "lagnamsaka_dasha", "dasha_key": "lagnamsaka"},
"yogardha_dasha": {"name": "Yogardha Dasha", "kind": "dasha", "module": "yogardha_dasha", "dasha_key": "yogardha"},
"mandooka_dasha": {"name": "Mandooka Dasha", "kind": "dasha", "module": "mandooka_dasha", "dasha_key": "mandooka"},
"paryaaya_dasha": {"name": "Paryaaya Dasha", "kind": "dasha", "module": "paryaaya_dasha", "dasha_key": "paryaaya"},
"padanadhamsa_dasha": {"name": "Padanadhamsa Dasha", "kind": "dasha", "module": "padanadhamsa", "dasha_key": "padanadhamsa"},
"sarvatobhadra_chakra": {"name": "Sarvatobhadra Chakra", "kind": "transit_chakra", "module": "sarvatobhadra_chakra"},
"kota_chakra": {"name": "Kota Chakra", "kind": "transit_chakra", "module": "kota_chakra"},
"kalanala_chakra": {"name": "Kalanala Chakra", "kind": "transit_chakra", "module": "kalanala_chakra"},
"nakshatra_latta": {"name": "Nakshatra Latta", "kind": "transit_chakra", "module": "nakshatra_latta"},
"pachakadi": {"name": "Pachakadi Sambandha", "kind": "natal", "module": "pachakadi_relationships"},
"amsayu": {"name": "Amsayu (Raman)", "kind": "longevity", "module": "amsayu_raman"},
"pindayu": {"name": "Pindayu (Raman + Aiyar Krurodaya)", "kind": "longevity", "module": "pindayu_raman"},
}
def _scripts_on_path() -> None:
scripts_dir = str(Path(__file__).resolve().parent)
if scripts_dir not in sys.path:
sys.path.insert(0, scripts_dir)
def _load(name: str):
_scripts_on_path()
try:
return __import__(f"scripts.{name}", fromlist=[name])
except ModuleNotFoundError: # pragma: no cover - scripts/ only on sys.path
return __import__(name)
def _record(value: Any) -> dict:
return value if isinstance(value, dict) else {}
def _blocked(reason: str, **extra: Any) -> dict:
return {"status": "blocked", "reason": reason, **extra}
def _lon(row: Any) -> float | None:
row = _record(row)
for key in ("lon", "degree_raw", "longitude"):
value = row.get(key)
if isinstance(value, (int, float)):
return float(value) % 360.0
return None
def star_of(lon: float) -> int:
return min(int(lon / STAR_SPAN), 26) + 1
def pada_of(lon: float) -> int:
return min(int((lon % STAR_SPAN) / (STAR_SPAN / 4)), 3) + 1
def _sign_of(lon: float) -> int:
return int(lon // 30) % 12
# ── inputs ──────────────────────────────────────────────────────────────────
def natal_inputs(planets: dict, ascendant: dict, *, birth_dt: datetime, tz: float,
lat: float | None, lon: float | None, ayanamsa: str, node_mode: str) -> dict:
"""Natal positions every ported producer reads; derived only from D1 longitudes."""
lons = {name: _lon(planets.get(name)) for name in GRAHAS}
missing = [name for name, value in lons.items() if value is None]
asc_lon = _lon(ascendant)
if missing or asc_lon is None:
raise ValueError("natal_longitudes_missing:" + ",".join(missing or ["Lagna"]))
varga = _load("varga")
jaimini = _load("jaimini")
d9 = {}
for name, value in lons.items():
row = varga.calc_varga(value, 9)
d9[name] = int(row["sign_idx"]) * 30 + float(row["degree_in_sign"])
d9_lagna = int(varga.calc_varga(asc_lon, 9)["sign_idx"]) % 12
d3_signs = {name: int(varga.calc_varga(value, 3)["sign_idx"]) % 12 for name, value in lons.items()}
lagna = _sign_of(asc_lon)
arudha = jaimini.calc_arudha_padas(lagna, dict(lons))
al = _record(_record(arudha.get("padas")).get("A1")).get("sign_idx")
return {
"lons": lons, "asc_lon": asc_lon, "lagna": lagna,
"signs": {name: _sign_of(value) for name, value in lons.items()},
"d9_lons": d9, "d9_lagna": d9_lagna, "d3_signs": d3_signs,
"al": int(al) if isinstance(al, int) else None,
"retrograde": {name: bool(_record(planets.get(name)).get("retrograde")) for name in GRAHAS},
"birth_dt": birth_dt, "tz": float(tz), "lat": lat, "lon": lon,
"ayanamsa": ayanamsa, "node_mode": node_mode,
}
def _aware_birth(inp: dict) -> datetime:
return inp["birth_dt"].replace(tzinfo=timezone(timedelta(hours=inp["tz"])))
def _day(value: Any) -> Any:
return value[:10] if isinstance(value, str) else value
def _age(value: Any) -> Any:
return round(float(value), 3) if isinstance(value, (int, float)) else value
# ── dashas ──────────────────────────────────────────────────────────────────
def naisargika(inp: dict) -> dict:
raw = _load("naisargika_dasha").calculate_naisargika_dasha(inp["birth_dt"])
return {
"status": raw["status"], "evidence_status": "parameter_sensitive",
"inputs": {"cycle_anchor": "birth moment", "balance_mode": raw["balance_mode"]},
"periods": [{"lord": p["lord"], "years": p["years"], "start": _day(p["start"]), "end": _day(p["end"])}
for p in raw["periods"]],
"children_status": raw["children_status"],
"limits": ["Whole 120-year natural cycle from birth; no birth balance, no sub-periods (upstream)."],
}
def kaala(inp: dict) -> dict:
if inp["lat"] is None or inp["lon"] is None:
return _blocked("birth_coordinates_missing")
raw = _load("kaala_dasha").calculate_kaala_dasha(
epoch=_aware_birth(inp).isoformat(), latitude=float(inp["lat"]), longitude=float(inp["lon"]),
year_days=YEAR_DAYS, boundary_policy="start_inclusive",
)
out = {
"status": raw["status"], "profile": raw["variant_profile_id"], "source": raw["source"],
"inputs": {"year_days": YEAR_DAYS, "boundary_policy": "start_inclusive",
"solar_events": raw.get("solar_event_profile")},
"child_status": raw.get("child_status"), "limits": raw.get("limits", []),
}
if raw["status"] == "blocked":
return {**out, "reason": raw.get("reason")}
out["seed"] = {"segment": raw["seed"]["segment"], "elapsed_fraction": round(raw["seed"]["elapsed_fraction"], 6)}
local = timezone(timedelta(hours=inp["tz"]))
def _local_day(stamp: str) -> str:
return datetime.fromisoformat(stamp).astimezone(local).strftime("%Y-%m-%d")
out["inputs"]["dates"] = "birth time zone"
out["periods"] = [{"cycle": p["cycle"], "lord": p["lord"], "years": round(p["years"], 4),
"start": _local_day(p["start_datetime_utc"]), "end": _local_day(p["end_datetime_utc"])}
for p in raw["periods"]]
return out
def karaka(inp: dict) -> dict:
raw = _load("karaka_dasha").calculate_karaka_dasha({p: inp["lons"][p] for p in SEVEN})
return {
"status": raw["status"], "blockers": raw.get("blockers", []),
"karaka_ranking": [{"planet": r["planet"], "karaka": r["karaka"],
"degree_in_sign": round(r["degree_in_sign"], 4)} for r in raw["karaka_ranking"]],
"limits": ["Upstream has no admitted Karaka Dasha duration profile; the ranking is real, the periods are not produced."],
}
def tara(inp: dict) -> dict:
module = _load("tara_dasha")
houses = {p: (inp["signs"][p] - inp["lagna"]) % 12 + 1 for p in GRAHAS}
kendra = [p for p in GRAHAS if (houses[p] - 1) % 3 == 0]
base = {"profile": module.PROFILE, "source": module.SOURCE, "houses": houses, "kendra_planets": kendra}
if len(kendra) < 4:
return {**base, "status": "not_applicable",
"reason": "rath_lesson4_requires_four_planets_in_lagna_kendras"}
return {**base, "status": "blocked", "reason": "strength_order_including_nodes_required",
"limits": ["Upstream needs an explicit nine-planet strength order and birth balance; "
"Shadbala does not rank the nodes, so no order is inferred."]}
def _narayana_rows(rows: list) -> list[dict]:
return [{"sign": r.get("sign") or SIGN_NAMES[int(r["sign_idx"])], "lord": r.get("lord"), "years": r.get("years"),
"start_age": _age(r.get("start_age")), "end_age": _age(r.get("end_age"))} for r in rows]
def lagnamsaka(inp: dict) -> dict:
raw = _load("lagnamsaka_dasha").calculate_lagnamsaka_dasha(inp["d9_lagna"], dict(inp["lons"]))
return {"status": raw["status"], "evidence_status": "parameter_sensitive",
"inputs": {"seed": f"D9 Lagna sign {SIGN_NAMES[inp['d9_lagna']]}", "planets": "D1 longitudes",
**raw["profiles"]},
"periods": _narayana_rows(raw["periods"]), "limits": [raw["claim_boundary"], "First cycle only (local Narayana engine)."]}
def _chara_sequence(inp: dict) -> list[tuple[int, int]]:
birth = inp["birth_dt"]
calculated = _load("jaimini").calc_chara_dasha_with_antardasha(
inp["lagna"], dict(inp["lons"]), birth.year, birth.month, birth.day, profile="kn_rao")
return [(int(r["sign_idx"]), int(r["duration_years"])) for r in calculated.get("dasha_sequence") or []][:12]
def yogardha(inp: dict) -> dict:
sequence = _chara_sequence(inp)
if len(sequence) != 12:
return _blocked("chara_sequence_incomplete")
sthira = _load("rashi_dashas_native").sthira_years
signs = [s for s, _ in sequence]
raw = _load("yogardha_dasha").calculate_yogardha_dasha(
signs, {s: y for s, y in sequence}, {s: sthira(s) for s in signs})
age = 0.0
periods = []
for p in raw["periods"]:
periods.append({"sign": SIGN_NAMES[p["sign_index"]], "chara_years": p["chara_years"], "sthira_years": p["sthira_years"],
"years": p["years"], "start_age": _age(age), "end_age": _age(age + p["years"])})
age += p["years"]
return {"status": raw["status"], "evidence_status": "parameter_sensitive",
"inputs": {"sequence": "our Chara Dasha order (K.N. Rao)", "chara_years": "our Chara Dasha (K.N. Rao)",
"sthira_years": "movable 7 / fixed 8 / dual 9"},
"periods": periods, "limits": [raw["claim_boundary"], "Sequence and spans are our choice of profile; unverified."]}
def mandooka(inp: dict) -> dict:
return {**_load("mandooka_dasha").calculate_mandooka_dasha(inp["lagna"]),
"limits": ["Upstream fails closed without a frog-leap sequence profile; the start sign shown is the Lagna, not a source rule."]}
def paryaaya(_inp: dict) -> dict:
return _blocked("sequence_durations_and_mode_required",
limits=["Upstream takes the Paryaaya sequence, sign spans and chara/sthira/ubhaya mode as explicit inputs; no source profile is admitted."])
def padanadhamsa(inp: dict) -> dict:
module = _load("padanadhamsa")
if inp["al"] is None:
return _blocked("arudha_lagna_missing")
profile = module.RATH_DUAL_PROFILE if inp["al"] in (7, 10) else module.RATH_PROFILE
rangacharya = _blocked("supplied_jaimini_navamsa_required",
note="Rangacharya profile needs a Jaimini Navamsa, not the ordinary D9.")
try:
report = module.build_padanadhamsa_rath_report(
arudha_lagna_sign_idx=inp["al"], navamsa_planet_longitudes=dict(inp["d9_lons"]),
duration_profile="jaimini_odd_footed_dignity_v2", dual_lord_profile="jaimini_dual_lord_source_v1",
profile=profile)
except ValueError as exc:
return {**_blocked(str(exc)), "profile": profile, "rangacharya": rangacharya}
raw = report["evidence_appendix"][0]
return {"status": raw["status"], "profile": profile, "source": raw["source"],
"inputs": {"arudha_lagna": SIGN_NAMES[inp["al"]], "planets": "D9 longitudes",
"duration": raw["duration_profile"], "sequence": raw["sequence_profile"]},
"al_lord": raw["al_lord"], "seed": SIGN_NAMES[raw["seed_selection"]["selected_sign_idx"]],
"periods": _narayana_rows(raw["periods"]), "limits": raw["limits"], "rangacharya": rangacharya}
DASHA_BUILDERS = {
"naisargika_dasha": naisargika, "kaala_dasha": kaala, "karaka_dasha": karaka, "tara_dasha": tara,
"lagnamsaka_dasha": lagnamsaka, "yogardha_dasha": yogardha, "mandooka_dasha": mandooka,
"paryaaya_dasha": paryaaya, "padanadhamsa_dasha": padanadhamsa,
}
# ── transit chakras ─────────────────────────────────────────────────────────
def _transit(positions: dict) -> dict:
lons = {name: _lon(positions.get(name)) for name in GRAHAS}
if any(value is None for value in lons.values()):
raise ValueError("transit_positions_missing")
speeds = {name: _record(positions.get(name)).get("speed") for name in GRAHAS}
return {"lons": lons, "stars": {p: star_of(v) for p, v in lons.items()}, "speeds": speeds}
def latta(inp: dict, tr: dict) -> dict:
module = _load("nakshatra_latta")
raw = module.calculate_latta(
transit_stars={p: tr["stars"][p] for p in module.RULES},
natal_moon_star=star_of(inp["lons"]["Moon"]), natal_lagna_star=star_of(inp["asc_lon"]),
profile=module.PROFILE)
return {"status": raw["status"], "profile": raw["variant_profile_id"], "source": raw["source"],
"rows": [{k: r[k] for k in ("planet", "source_index", "target_index", "direction", "matches")} for r in raw["rows"]],
"contacts": [r["planet"] for r in raw["rows"] if r["matches"]], "limits": raw["limits"]}
def sarvatobhadra(inp: dict, tr: dict) -> dict:
module = _load("sarvatobhadra_chakra")
targets = [
{"id": "natal_moon_star", "kind": "nakshatra", "value": star_of(inp["lons"]["Moon"])},
{"id": "natal_lagna_star", "kind": "nakshatra", "value": star_of(inp["asc_lon"])},
{"id": "natal_moon_rashi", "kind": "rashi", "value": inp["signs"]["Moon"] + 1},
{"id": "natal_lagna_rashi", "kind": "rashi", "value": inp["lagna"] + 1},
]
report = module.build_sarvatobhadra_from_longitudes(
longitudes={p: Fraction(tr["lons"][p]).limit_denominator(10 ** 9) for p in GRAHAS}, targets=targets,
target_star_basis="nakshatra_27", ayanamsa=inp["ayanamsa"],
longitude_profile_id=module.PVR_LONGITUDE_PROFILE, variant_profile_id=module.PVR_PROFILE)
raw = report["evidence_appendix"][0]
return {"status": raw["status"], "profile": raw["variant_profile_id"], "source": raw["sources"][0],
"inputs": {"transits": "noon of the reference day", "targets": [t["id"] for t in targets]},
"planet_stars": {r["planet"]: r["star_name"] for r in raw["rows"]},
"target_summary": [{k: t[k] for k in ("target_id", "kind", "value", "hit_planets", "blocked_planets")}
for t in raw["target_summary"]],
"completeness": raw["calculation_completeness"], "limits": raw["limits"]}
def kota(inp: dict, tr: dict) -> dict:
module = _load("kota_chakra")
motions = {}
for planet in GRAHAS:
speed = tr["speeds"].get(planet)
motions[planet] = "direct" if planet in ("Sun", "Moon") else (
"retrograde" if isinstance(speed, (int, float)) and speed < 0 else "direct")
moon = inp["lons"]["Moon"]
report = module.build_kota_report(
stars=dict(tr["stars"]), motions=motions, birth_star=star_of(moon), birth_pada=pada_of(moon),
moon_sign=inp["signs"]["Moon"] + 1, star_basis="nakshatra_27", ayanamsa=inp["ayanamsa"],
variant_profile_id=module.PADA_PROFILE)
raw = report["evidence_appendix"][0]
return {"status": raw["status"], "profile": raw["variant_profile_id"], "source": raw["source_url"],
"inputs": {"transits": "noon of the reference day", "motions": "sign of daily speed"},
"kota_swami": raw["guardians"]["swami"]["planet"], "kota_pala": raw["guardians"]["pala"]["planet"],
"placements": {p: {k: r[k] for k in ("ring_star_index", "layer", "road", "motion", "travel")}
for p, r in raw["placements"].items()},
"placement_rule_checks": [{"rule_id": c["rule_id"], "matched": c["matched"]} for c in raw["placement_rule_checks"]],
"limits": raw["limitations"]}
def kalanala(inp: dict, tr: dict) -> dict:
module = _load("kalanala_chakra")
out: dict[str, Any] = {"status": "partial", "inputs": {
"planets": "transit stars, noon of the reference day",
"base_star": "transit Sun star (surya) / transit Moon star (chandra), 28-star ring — our assumption, not an upstream rule"}}
for kind, body in (("surya", "Sun"), ("chandra", "Moon")):
star = tr["stars"][body]
base = star + 1 if star > 21 else star
raw = module.build_kalanala_report(stars=dict(tr["stars"]), base_star=base, ayanamsa=inp["ayanamsa"], kind=kind)
evidence = raw["evidence_appendix"][0]
out[kind] = {"base_star": base, "grid_status": evidence["grid_status"],
"slots": {r["planet"]: r["layout_slot"] for r in evidence["rows"]}}
out["limits"] = ["Slot placement only; PyJHora grid coordinates are not carried (AGPL boundary). No Vedha or timing claim."]
return out
CHAKRA_BUILDERS = {"sarvatobhadra_chakra": sarvatobhadra, "kota_chakra": kota,
"kalanala_chakra": kalanala, "nakshatra_latta": latta}
# ── natal relationship / longevity ──────────────────────────────────────────
def pachakadi(inp: dict) -> dict:
module = _load("pachakadi_relationships")
by_reference = {}
for reference in GRAHAS:
raw = module.calculate_pachakadi(dict(inp["signs"]), reference=reference)
by_reference[reference] = {name: row["planets"] for name, row in raw["relationships"].items()}
return {"status": "executed", "offset_profile": dict(module.DEFAULT_OFFSETS), "by_reference": by_reference,
"limits": ["Relationship layer only; not permanent or temporary friendship (upstream)."]}
def _aspects(caster: str, from_sign: int, to_sign: int) -> bool:
distance = (to_sign - from_sign) % 12 + 1
return distance == 7 or distance in {"Mars": (4, 8), "Jupiter": (5, 9), "Saturn": (3, 10)}.get(caster, ())
def aayu_conditions(engine, inp: dict, chart_planets: dict) -> tuple[dict, dict, dict]:
"""Raman Amsayu / Pindayu conditions read from our chart (upstream takes them supplied)."""
labels_to_level = {label: level for level, label in getattr(engine, "DIGNITY_LABELS", {}).items()}
sun = inp["lons"]["Sun"]
conditions, levels = {}, {}
for planet in SEVEN:
sign = inp["signs"][planet]
label = _record(chart_planets.get(planet)).get("status")
level = labels_to_level.get(label) if isinstance(label, str) else None
if level is None:
level = engine._get_dignity_level(planet, SIGN_NAMES[sign], inp["lons"][planet] % 30, chart_planets)
levels[planet] = level
row = {
"bhava": (sign - inp["lagna"]) % 12 + 1,
"exalted": EXALTATION_SIGN[planet] == sign,
"retrograde": planet not in ("Sun", "Moon") and inp["retrograde"][planet],
"vargottama": _sign_of(inp["d9_lons"][planet]) == sign,
"own_rasi": SIGN_LORDS[sign] == planet,
"own_navamsa": SIGN_LORDS[_sign_of(inp["d9_lons"][planet])] == planet,
"own_drekkana": SIGN_LORDS[inp["d3_signs"][planet]] == planet,
"inimical_sign": level in ("ENEMY", "GREAT_ENEMY"),
"combust": planet != "Sun" and bool(engine.is_combust(planet, inp["lons"][planet], sun, inp["retrograde"][planet])),
}
if planet == "Mercury":
joined = [p for p in GRAHAS if p != "Mercury" and inp["signs"][p] == sign and p in NATURAL_MALEFICS]
row["nature"] = "malefic" if joined else "benefic"
conditions[planet] = row
benefics = ("Moon", "Jupiter", "Venus") + (("Mercury",) if conditions["Mercury"]["nature"] == "benefic" else ())
benefic_aspects = {p: any(_aspects(b, inp["signs"][b], inp["signs"][p]) for b in benefics if b != p) for p in SEVEN}
return conditions, benefic_aspects, levels
def _minutes(inp: dict) -> dict:
out = {p: int(inp["lons"][p] * 60) % 21600 for p in SEVEN}
out["Lagna"] = int(inp["asc_lon"] * 60) % 21600
return out
def _aayu_view(raw: dict, *, adjusted_key: str) -> dict:
view = {"status": raw["status"], "evidence_status": raw["evidence_status"], "profile": raw["profile_id"],
"contributions": {}, "total_years": raw["total"]["display_360_day_term"] if raw.get("total") else None,
"total_years_exact": raw["total"]["years_exact"] if raw.get("total") else None,
"blockers": raw["blockers"]}
for row in raw["contributions"]:
adjusted = row.get(adjusted_key)
view["contributions"][row["body"]] = {
"base": row["base"]["display_360_day_term"],
"adjusted": adjusted["display_360_day_term"] if adjusted else None, "status": row["status"]}
return view
def _with_strength_branches(module, calculate, candidates, request: dict, adjusted_key: str) -> dict:
raw = calculate(**request)
view = _aayu_view(raw, adjusted_key=adjusted_key)
if raw["status"] == "blocked" and any(b.startswith("strongest_by_bhava") for b in raw["blockers"]):
report = candidates(**request)["evidence_appendix"][0]
view["strength_branches"] = [
{"assumed_strongest_by_bhava": b["strongest_by_bhava"], "status": b["result"]["status"],
"total_years": b["result"]["total"]["display_360_day_term"] if b["result"].get("total") else None}
for b in report["branches"]]
view["status"] = report["status"]
view["limits"] = raw["limits"]
return view
def amsayu(engine, inp: dict, chart_planets: dict) -> dict:
module = _load("amsayu_raman")
conditions, _aspect, levels = aayu_conditions(engine, inp, chart_planets)
request = {"longitude_minutes": _minutes(inp), "conditions": {p: {k: v for k, v in c.items() if k in module.FLAGS or k in ("bhava", "nature")} for p, c in conditions.items()},
"condition_profile": "jyotisha_chart_derived_whole_sign_compound_enemy_v1"}
view = _with_strength_branches(module, module.calculate_amsayu_raman, module.build_amsayu_strength_candidates_report,
request, "adjusted")
view["conditions"] = request["conditions"]
view["inputs"] = {"bhava": "whole-sign house", "inimical_sign": "compound (five-fold) enemy of the sign lord",
"combust": "jyotish_engine.is_combust", "d1_dignity": levels}
return view
def pindayu(engine, inp: dict, chart_planets: dict) -> dict:
module = _load("pindayu_raman")
rules = _load("raman_aayu_rules")
conditions, benefic_aspects, _levels = aayu_conditions(engine, inp, chart_planets)
request = {"longitude_minutes": _minutes(inp),
"conditions": {p: {k: v for k, v in c.items() if k in rules.HARANA_FLAGS or k in ("bhava", "nature")} for p, c in conditions.items()},
"condition_profile": "jyotisha_chart_derived_whole_sign_compound_enemy_v1",
"krurodaya_profile": module.KRURA_PROFILE, "benefic_aspects": benefic_aspects}
view = _with_strength_branches(module, module.calculate_pindayu_raman, module.build_pindayu_strength_candidates_report,
request, "adjusted_before_krurodaya")
view["conditions"] = request["conditions"]
view["benefic_aspects"] = benefic_aspects
view["inputs"] = {"benefic_aspects": "whole-sign graha drishti from Moon / Jupiter / Venus (+ Mercury when benefic)"}
return view
# ── assembly ────────────────────────────────────────────────────────────────
def _guard(builder, *args) -> dict:
try:
result = builder(*args)
return result if isinstance(result, dict) else _blocked("producer_returned_non_dict")
except Exception as exc: # each ported producer stays isolated
detail = str(exc) if isinstance(exc, ValueError) else exc.__class__.__name__
return _blocked(detail)
def build_upstream_techniques(engine, chart: dict, *, birth_payload: dict, reference: datetime,
transit_positions: dict | None) -> dict:
"""Every ported technique for one chart; one blocked section never stops the others."""
planets = _record(chart.get("planets"))
birth = _record(chart.get("birth"))
try:
birth_dt = datetime(int(birth_payload["year"]), int(birth_payload["month"]), int(birth_payload["day"]),
int(birth_payload["hour"]), int(birth_payload["minute"]), int(birth_payload.get("second") or 0))
except (KeyError, TypeError, ValueError):
return {"schema": SCHEMA, **_blocked("birth_moment_missing")}
lat, lon = birth_payload.get("lat"), birth_payload.get("lon")
inp = natal_inputs(
planets, _record(chart.get("ascendant")), birth_dt=birth_dt, tz=float(birth_payload.get("tz") or 0),
lat=float(lat) if isinstance(lat, (int, float)) else None,
lon=float(lon) if isinstance(lon, (int, float)) else None,
ayanamsa=str(birth.get("ayanamsa_name") or birth_payload.get("ayanamsa") or "raman"),
node_mode=str(birth.get("node_mode") or birth_payload.get("node_mode") or "mean"),
)
sections: dict[str, dict] = {key: _guard(builder, inp) for key, builder in DASHA_BUILDERS.items()}
try:
tr = _transit(_record(transit_positions))
except ValueError as exc:
tr = None
for key in CHAKRA_BUILDERS:
sections[key] = _blocked(str(exc))
if tr is not None:
for key, builder in CHAKRA_BUILDERS.items():
sections[key] = {**_guard(builder, inp, tr), "reference_date": reference.strftime("%Y-%m-%d")}
sections["pachakadi"] = _guard(pachakadi, inp)
sections["amsayu"] = _guard(amsayu, engine, inp, planets)
sections["pindayu"] = _guard(pindayu, engine, inp, planets)
return {
"schema": SCHEMA, "status": "executed", "upstream": dict(UPSTREAM),
"customer_export_admitted": False, "boundary": BOUNDARY,
"registry": {key: {"name": row["name"], "kind": row["kind"], "status": sections[key].get("status")}
for key, row in TECHNIQUE_REGISTRY.items()},
"sections": sections,
}
# ── /api/dasha registry entry point ────────────────────────────────────────
def compute_registry_dasha(key: str, *, birth_dt: datetime, planets: dict, ascendant: dict, body: dict) -> dict:
"""Periods for one ported dasha; raises ValueError(reason) when upstream fails closed."""
technique = next((k for k, row in TECHNIQUE_REGISTRY.items() if row.get("dasha_key") == key), None)
if technique is None:
raise KeyError(key)
lat, lon = body.get("lat", body.get("latitude")), body.get("lon", body.get("longitude"))
tz = body.get("tz", body.get("timezone"))
inp = natal_inputs(
planets, ascendant, birth_dt=birth_dt, tz=float(tz) if isinstance(tz, (int, float)) else 0.0,
lat=float(lat) if isinstance(lat, (int, float)) else None,
lon=float(lon) if isinstance(lon, (int, float)) else None,
ayanamsa=str(body.get("ayanamsa") or "raman"), node_mode=str(body.get("node_mode") or "mean"),
)
if technique == "kaala_dasha" and not isinstance(tz, (int, float)):
raise ValueError("kaala dasha needs tz, lat and lon")
section = DASHA_BUILDERS[technique](inp)
if section.get("status") in ("blocked", "not_applicable") or not section.get("periods"):
raise ValueError(str(section.get("reason") or ",".join(section.get("blockers") or []) or section.get("status")))
return section