Files
Jyotisha/scripts/dasha_registry.py
T
jesse-uxandGrok (agent loop) 5ac88e58f5 让大运接口走完整解读的同一套算法,不再返回编出来的周期。
Ashtottari 在适用时把出生前已经走过的部分算进时间线。

Co-Authored-By: Grok (agent loop)
2026-10-16 00:52:29 +08:00

343 lines
15 KiB
Python

"""HTTP dasha registry.
`DASHA_REGISTRY_METADATA` is the old `extended_dashas.DASHA_REGISTRY` moved
verbatim so `/api/chart` `available_dashas` stays byte-identical.
`compute_dasha` calls the same modules `cmd_full_reading` calls, with the same
birth-info fields. Systems without that module return "not implemented"
instead of a fabricated timeline.
This module must not import `jyotish_api_server` or PyJHora.
"""
from __future__ import annotations
from datetime import datetime
from typing import Any
try:
from source_bounded_child_periods import PYJHORA_PARENT_SEEDED_EQUAL_SPLIT_PROFILE
except ModuleNotFoundError: # pragma: no cover - package import
from scripts.source_bounded_child_periods import PYJHORA_PARENT_SEEDED_EQUAL_SPLIT_PROFILE
DASHA_REGISTRY_METADATA = {
"vimshottari": {"name": "Vimshottari", "years": 120, "type": "nakshatra"},
"ashtottari": {"name": "Ashtottari", "years": 108, "type": "nakshatra"},
"yogini": {"name": "Yogini", "years": 36, "type": "nakshatra"},
"kalachakra": {"name": "Kalachakra", "years": 94, "type": "nakshatra"},
"tara": {"name": "Tara Dasha", "years": 81, "type": "nakshatra"},
"shodasottari": {"name": "Shodasottari", "years": 128, "type": "nakshatra"},
"panchottari": {"name": "Panchottari", "years": 105, "type": "nakshatra"},
"satabdika": {"name": "Satabdika", "years": 100, "type": "nakshatra"},
"chara": {"name": "Chara (Jaimini)", "years": 36, "type": "rasi"},
"narayana": {"name": "Narayana", "years": 36, "type": "rasi"},
"shoola": {"name": "Shoola Dasha", "years": 9, "type": "rasi"},
"sudasa": {"name": "Sudasa", "years": 36, "type": "rasi"},
"drig": {"name": "Drig Dasha", "years": 36, "type": "rasi"},
"sthira": {"name": "Sthira Dasha", "years": 36, "type": "rasi"},
"mandooka": {"name": "Mandooka Dasha", "years": 36, "type": "rasi"},
"lagnamsaka": {"name": "Lagnamsaka Dasha", "years": 36, "type": "rasi"},
"yogardha": {"name": "Yogardha Dasha", "years": 36, "type": "rasi"},
"brahma": {"name": "Brahma Dasha", "years": 36, "type": "rasi"},
"dwisaptati": {"name": "Dwisaptati Sama", "years": 72, "type": "conditional"},
"shattrimsa": {"name": "Shattrimsa Sama", "years": 36, "type": "conditional"},
"dwadashottari": {"name": "Dwadashottari", "years": 112, "type": "conditional"},
"shasti_hayani": {"name": "Shasti-Hayani", "years": 60, "type": "conditional"},
"chaturaaseeti": {"name": "Chaturaaseeti Sama", "years": 84, "type": "conditional"},
"tithi_ashtottari": {"name": "Tithi Ashtottari", "years": 108, "type": "conditional"},
"tithi_yogini": {"name": "Tithi Yogini", "years": 36, "type": "conditional"},
"patyayini": {"name": "Patyayini", "years": 36, "type": "conditional"},
"navamsa": {"name": "Navamsa Dasha", "years": 36, "type": "varga"},
"kendradi": {"name": "Kendradi", "years": 38, "type": "bhav"},
"paryaaya": {"name": "Paryaaya Dasha", "years": 36, "type": "varga"},
"rashmi": {"name": "Rashmi Dasha", "years": 36, "type": "special"},
"naisargika": {"name": "Naisargika Dasha", "years": 120, "type": "special"},
"aayu": {"name": "Aayu Dasha", "years": 36, "type": "special"},
"karaka": {"name": "Karaka Dasha", "years": 36, "type": "special"},
"panchasvara": {"name": "Panchasvara Dasha", "years": 36, "type": "special"},
"sudarsana": {"name": "Sudarsana Chakra Dasha", "years": 36, "type": "special"},
}
_CONDITIONAL_CALLS = {
"shodasottari": ("shodashottari_dasha", "calculate_shodashottari_dasha", {}),
"dwadashottari": (
"dwadashottari_dasha",
"calculate_dwadashottari_dasha",
{"sukramsaka_profile": "navamsa_lagna_taurus_or_libra"},
),
"dwisaptati": ("dwisaptatisama_dasha", "calculate_dwisaptatisama_dasha", {}),
"shattrimsa": ("shattrimshatsama_dasha", "calculate_shattrimshatsama_dasha", {}),
"panchottari": ("panchottari_dasha", "calculate_panchottari_dasha", {}),
"satabdika": ("satabdika_dasha", "calculate_satabdika_dasha", {}),
"chaturaaseeti": ("chaturshitisama_dasha", "calculate_chaturshitisama_dasha", {}),
}
SIGNS = [
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
]
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",
}
class DashaNotImplemented(Exception):
"""The registry key exists, but there is no engine module behind it."""
def available_dasha_rows() -> list[dict[str, Any]]:
"""`/api/chart` `available_dashas` rows, same order and fields as before."""
return [
{"key": key, "name": info["name"], "years": info["years"], "type": info["type"]}
for key, info in DASHA_REGISTRY_METADATA.items()
]
def _load(module_name: str):
try:
return __import__(module_name)
except ModuleNotFoundError: # pragma: no cover - package import
return __import__(f"scripts.{module_name}", fromlist=[module_name])
def _longitude(value: Any) -> float | None:
if isinstance(value, (int, float)):
return float(value) % 360.0
if not isinstance(value, dict):
return None
for key in ("lon", "longitude", "degree"):
raw = value.get(key)
if isinstance(raw, (int, float)):
return float(raw) % 360.0
sign_idx = value.get("sign_idx")
if sign_idx is None and value.get("sign") in SIGNS:
sign_idx = SIGNS.index(value["sign"])
degree = value.get("degree_in_sign", value.get("degree", 0))
try:
if sign_idx is None:
return None
return (int(sign_idx) * 30 + float(degree or 0)) % 360.0
except (TypeError, ValueError):
return None
def _planet_longitudes(planets: dict) -> dict[str, float]:
longitudes = {}
if not isinstance(planets, dict):
return longitudes
for name, data in planets.items():
longitude = _longitude(data)
if longitude is not None:
longitudes[str(name)] = longitude
return longitudes
def _optional_float(body: dict, *keys: str) -> float | None:
for key in keys:
raw = body.get(key)
if isinstance(raw, (int, float)):
return float(raw)
return None
def _reference_datetime(birth_dt: datetime, body: dict) -> datetime:
raw = body.get("today") or body.get("current_date")
if isinstance(raw, datetime):
return raw
if isinstance(raw, str) and raw.strip():
return datetime.strptime(raw.strip()[:10], "%Y-%m-%d")
return datetime.now()
def build_reading_birth_info(
birth_dt: datetime,
planets: dict,
ascendant: dict | None,
body: dict | None = None,
) -> tuple[dict[str, Any], dict[str, Any], dict[str, float]]:
"""Same fields `cmd_full_reading` puts in `conditional_birth_info`.
Returns `(alt_dasha_info, conditional_birth_info, planet_longitudes)`.
Kept here, not extracted from the engine, so the full-reading dict
construction and its output stay untouched.
"""
body = body or {}
ascendant = ascendant or {}
planet_lons = _planet_longitudes(planets)
moon_lon = planet_lons.get("Moon", _longitude(body.get("moon_lon")) or 0.0)
sun_lon = planet_lons.get("Sun", _longitude(body.get("sun_lon")) or 0.0)
asc_lon = _longitude(ascendant)
if asc_lon is None:
asc_lon = _longitude(body.get("ascendant_longitude"))
if asc_lon is None:
sign_idx = ascendant.get("sign_idx", body.get("asc_sign_idx", 0))
try:
asc_idx = int(sign_idx) % 12
except (TypeError, ValueError):
asc_idx = 0
asc_lon = float(asc_idx * 30)
asc_idx = int(float(asc_lon) / 30) % 12
moon_nakshatra_index = int(moon_lon / (360 / 27)) % 27
moon_pada = int((moon_lon % (360 / 27)) / (360 / 108)) + 1
tithi_number = int(((moon_lon - sun_lon) % 360) / 12) + 1
lagna_lord_name = SIGN_LORDS[SIGNS[asc_idx]]
tenth_lord_name = SIGN_LORDS[SIGNS[(asc_idx + 9) % 12]]
try:
varga = _load("varga")
navamsa_ascendant_sign_index = int(varga.calc_varga(asc_lon, 9).get("sign_idx", 0)) % 12
venus_navamsa_sign_index = int(varga.calc_varga(float(planet_lons.get("Venus", 0)), 9).get("sign_idx", 0)) % 12
except Exception:
navamsa_ascendant_sign_index = None
venus_navamsa_sign_index = None
degree_in_sign = float(asc_lon) % 30.0
ascendant_hora_lord = (
"Sun"
if (asc_idx % 2 == 0 and degree_in_sign < 15.0)
or (asc_idx % 2 == 1 and degree_in_sign >= 15.0)
else "Moon"
)
birth_is_daytime = None
lat = _optional_float(body, "lat", "latitude")
lon = _optional_float(body, "lon", "longitude")
tz = _optional_float(body, "tz", "timezone")
if lat is not None and lon is not None and tz is not None:
try:
daynight = _load("saham_daynight")
birth_is_daytime = bool(daynight.determine_daytime(birth_dt, lat=lat, lon=lon, tz=tz).get("is_daytime"))
except Exception:
birth_is_daytime = None
applicability_rule_family = (
"rahu_from_lagna_lord_kendra_trikona_plus_paksha_daynight"
if birth_is_daytime is not None
else "moon_nakshatra_plus_paksha"
)
alt = {
"birth_datetime": birth_dt,
"moon_nakshatra_index": moon_nakshatra_index,
"moon_pada": moon_pada,
"is_shukla_paksha": 1 <= tithi_number <= 15,
"lagna_rashi_index": asc_idx,
}
conditional = {
**alt,
"ascendant_longitude": asc_lon,
"sun_longitude": sun_lon,
"moon_longitude": moon_lon,
"birth_is_daytime": birth_is_daytime,
"ascendant_sign_index": asc_idx,
"ascendant_hora_lord": ascendant_hora_lord,
"navamsa_ascendant_sign_index": navamsa_ascendant_sign_index,
"venus_rasi_sign_index": int(float(planet_lons.get("Venus", 0)) / 30) % 12,
"venus_navamsa_sign_index": venus_navamsa_sign_index,
"tenth_lord_sign_index": int(float(planet_lons.get(tenth_lord_name, 0)) / 30) % 12,
"rahu_sign_index": int(float(planet_lons.get("Rahu", 0)) / 30) % 12,
"lagna_lord_sign_index": int(float(planet_lons.get(lagna_lord_name, 0)) / 30) % 12,
"applicability_rule_family": applicability_rule_family,
}
return alt, conditional, planet_lons
def _major_rows(payload: Any) -> list:
if isinstance(payload, dict):
majors = payload.get("major")
if isinstance(majors, list):
return majors
periods = payload.get("periods")
if isinstance(periods, list):
return periods
sequence = payload.get("mahadasha_sequence")
if isinstance(sequence, list):
return sequence
return []
if isinstance(payload, list):
return payload
return []
def _conditional_payload(key: str, conditional: dict) -> dict:
module_name, function_name, extra = _CONDITIONAL_CALLS[key]
module = _load(module_name)
birth_info = {
**conditional,
**extra,
"child_period_profile": PYJHORA_PARENT_SEEDED_EQUAL_SPLIT_PROFILE,
}
return getattr(module, function_name)(birth_info)
def compute_dasha(
key: str,
*,
birth_dt: datetime,
planets: dict,
ascendant: dict | None,
body: dict | None = None,
) -> dict[str, Any]:
"""Return engine major periods for one registry key.
Raises `DashaNotImplemented` when the key has no engine module. Vimshottari
stays on the handler's existing canonical branch and is not computed here.
"""
if key not in DASHA_REGISTRY_METADATA:
raise DashaNotImplemented("Unknown dasha system")
if key == "chara":
raise DashaNotImplemented("chara dasha is served at /api/dasha/chara")
body = body or {}
alt, conditional, planet_lons = build_reading_birth_info(birth_dt, planets or {}, ascendant, body)
if key == "yogini":
payload = _load("yogini_dasha").calculate_yogini_dasha({**alt, "moon_longitude": conditional["moon_longitude"]})
return {"periods": _major_rows(payload)}
if key == "ashtottari":
payload = _load("ashtottari_dasha").calculate_ashtottari_dasha({
**alt,
"moon_longitude": conditional["moon_longitude"],
})
return {"periods": _major_rows(payload)}
if key == "kalachakra":
payload = _load("kalachakra_dasha").calculate_kalachakra_dasha(alt)
return {"periods": _major_rows(payload)}
if key == "shasti_hayani":
birth_info = dict(conditional)
birth_info["child_period_profile"] = PYJHORA_PARENT_SEEDED_EQUAL_SPLIT_PROFILE
payload = _load("shastihayani_dasha").calculate_shastihayani_dasha(birth_info)
return {"periods": _major_rows(payload)}
if key in _CONDITIONAL_CALLS:
return {"periods": _major_rows(_conditional_payload(key, conditional))}
if key == "narayana":
asc_idx = int(conditional["ascendant_sign_index"])
reference = _reference_datetime(birth_dt, body)
current_age = (reference - birth_dt).days / 365.25
birth_year = int(body.get("year") or birth_dt.year)
narayana = _load("narayana_dasha")
report = narayana.narayana_dasha_full_report(
lagna_sign_idx=asc_idx,
planet_lons=planet_lons,
current_age=current_age,
birth_year=birth_year,
)
labeled = _load("narayana_legacy_label").label_legacy_narayana(
report,
lagna_sign_idx=asc_idx,
planet_lons=planet_lons,
current_age=current_age,
birth_year=birth_year,
)
return {
"periods": _major_rows(labeled),
"algorithm_label": labeled.get("algorithm_label") if isinstance(labeled, dict) else None,
"algorithm_note": labeled.get("algorithm_note") if isinstance(labeled, dict) else None,
"profile": labeled.get("profile") if isinstance(labeled, dict) else None,
}
if key == "shoola":
rashi = _load("rashi_dashas_native")
try:
section = rashi.calculate_rashi_dashas(
birth_dt.date(), conditional["ascendant_longitude"], planet_lons
)["shoola"]
except ValueError as exc:
raise DashaNotImplemented(str(exc)) from exc
return {"periods": _major_rows(section)}
raise DashaNotImplemented("dasha system not implemented")