Ashtottari 在适用时把出生前已经走过的部分算进时间线。 Co-Authored-By: Grok (agent loop)
343 lines
15 KiB
Python
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")
|