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
576 lines
31 KiB
Python
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
|