kundali-chart-mcp 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -0
- package/azure-function/function_app.py +93 -0
- package/azure-function/host.json +15 -0
- package/azure-function/kundali_bridge.py +952 -0
- package/azure-function/python/kundali_lib/__init__.py +1 -0
- package/azure-function/python/kundali_lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/__pycache__/ephemeris.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/__pycache__/geocoder.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/__pycache__/vedicastro_bridge.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/ephemeris.py +30 -0
- package/azure-function/python/kundali_lib/geocoder.py +82 -0
- package/azure-function/python/kundali_lib/vedic/__init__.py +1 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/__init__.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/arishta.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/ashtakavarga.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/avasthas.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/ayanamsa.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/bhava_chalit.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/char_dasha.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/chart.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/chart_types.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/compatibility.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/constants.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/dasha_extended.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/dasha_systems.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/doshas.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/gandanta.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/gochara.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/hora.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/houses.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/jaimini.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/kalachakra.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/kartari.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/kurmachakra.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/lunar_return.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/muhurta.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/nabhasha.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/nakshatra_details.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/panchanga.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/planets.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/shadbala.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/special_conditions.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/sudarshana.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/tajaka.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/upagrahas.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/varshaphal.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/yogas.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/__pycache__/zodiac.cpython-313.pyc +0 -0
- package/azure-function/python/kundali_lib/vedic/arishta.py +465 -0
- package/azure-function/python/kundali_lib/vedic/ashtakavarga.py +213 -0
- package/azure-function/python/kundali_lib/vedic/avasthas.py +292 -0
- package/azure-function/python/kundali_lib/vedic/ayanamsa.py +106 -0
- package/azure-function/python/kundali_lib/vedic/bhava_chalit.py +137 -0
- package/azure-function/python/kundali_lib/vedic/char_dasha.py +308 -0
- package/azure-function/python/kundali_lib/vedic/chart.py +126 -0
- package/azure-function/python/kundali_lib/vedic/chart_types.py +338 -0
- package/azure-function/python/kundali_lib/vedic/compatibility.py +705 -0
- package/azure-function/python/kundali_lib/vedic/constants.py +108 -0
- package/azure-function/python/kundali_lib/vedic/dasha_extended.py +262 -0
- package/azure-function/python/kundali_lib/vedic/dasha_systems.py +439 -0
- package/azure-function/python/kundali_lib/vedic/doshas.py +453 -0
- package/azure-function/python/kundali_lib/vedic/gandanta.py +213 -0
- package/azure-function/python/kundali_lib/vedic/gochara.py +277 -0
- package/azure-function/python/kundali_lib/vedic/hora.py +263 -0
- package/azure-function/python/kundali_lib/vedic/houses.py +30 -0
- package/azure-function/python/kundali_lib/vedic/jaimini.py +361 -0
- package/azure-function/python/kundali_lib/vedic/kalachakra.py +226 -0
- package/azure-function/python/kundali_lib/vedic/kartari.py +243 -0
- package/azure-function/python/kundali_lib/vedic/kurmachakra.py +383 -0
- package/azure-function/python/kundali_lib/vedic/lunar_return.py +402 -0
- package/azure-function/python/kundali_lib/vedic/muhurta.py +414 -0
- package/azure-function/python/kundali_lib/vedic/nabhasha.py +349 -0
- package/azure-function/python/kundali_lib/vedic/nakshatra_details.py +945 -0
- package/azure-function/python/kundali_lib/vedic/panchanga.py +297 -0
- package/azure-function/python/kundali_lib/vedic/planets.py +55 -0
- package/azure-function/python/kundali_lib/vedic/shadbala.py +500 -0
- package/azure-function/python/kundali_lib/vedic/special_conditions.py +319 -0
- package/azure-function/python/kundali_lib/vedic/sudarshana.py +232 -0
- package/azure-function/python/kundali_lib/vedic/tajaka.py +482 -0
- package/azure-function/python/kundali_lib/vedic/upagrahas.py +229 -0
- package/azure-function/python/kundali_lib/vedic/varshaphal.py +185 -0
- package/azure-function/python/kundali_lib/vedic/yogas.py +935 -0
- package/azure-function/python/kundali_lib/vedic/zodiac.py +42 -0
- package/azure-function/python/kundali_lib/vedicastro_bridge.py +198 -0
- package/azure-function/requirements.txt +9 -0
- package/index.js +747 -0
- package/kundali-chart-mcp.js +159 -0
- package/kundali_bridge.py +952 -0
- package/package.json +41 -0
- package/python/kundali_lib/__init__.py +1 -0
- package/python/kundali_lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/python/kundali_lib/__pycache__/ephemeris.cpython-313.pyc +0 -0
- package/python/kundali_lib/__pycache__/geocoder.cpython-313.pyc +0 -0
- package/python/kundali_lib/__pycache__/vedicastro_bridge.cpython-313.pyc +0 -0
- package/python/kundali_lib/ephemeris.py +30 -0
- package/python/kundali_lib/geocoder.py +82 -0
- package/python/kundali_lib/vedic/__init__.py +1 -0
- package/python/kundali_lib/vedic/__pycache__/__init__.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/arishta.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/ashtakavarga.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/avasthas.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/ayanamsa.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/bhava_chalit.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/char_dasha.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/chart.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/chart_types.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/compatibility.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/constants.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/dasha_extended.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/dasha_systems.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/doshas.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/gandanta.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/gochara.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/hora.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/houses.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/jaimini.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/kalachakra.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/kartari.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/kurmachakra.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/lunar_return.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/muhurta.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/nabhasha.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/nakshatra_details.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/panchanga.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/planets.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/shadbala.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/special_conditions.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/sudarshana.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/tajaka.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/upagrahas.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/varshaphal.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/yogas.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/__pycache__/zodiac.cpython-313.pyc +0 -0
- package/python/kundali_lib/vedic/arishta.py +465 -0
- package/python/kundali_lib/vedic/ashtakavarga.py +213 -0
- package/python/kundali_lib/vedic/avasthas.py +292 -0
- package/python/kundali_lib/vedic/ayanamsa.py +106 -0
- package/python/kundali_lib/vedic/bhava_chalit.py +137 -0
- package/python/kundali_lib/vedic/char_dasha.py +308 -0
- package/python/kundali_lib/vedic/chart.py +126 -0
- package/python/kundali_lib/vedic/chart_types.py +338 -0
- package/python/kundali_lib/vedic/compatibility.py +705 -0
- package/python/kundali_lib/vedic/constants.py +108 -0
- package/python/kundali_lib/vedic/dasha_extended.py +262 -0
- package/python/kundali_lib/vedic/dasha_systems.py +439 -0
- package/python/kundali_lib/vedic/doshas.py +453 -0
- package/python/kundali_lib/vedic/gandanta.py +213 -0
- package/python/kundali_lib/vedic/gochara.py +277 -0
- package/python/kundali_lib/vedic/hora.py +263 -0
- package/python/kundali_lib/vedic/houses.py +30 -0
- package/python/kundali_lib/vedic/jaimini.py +361 -0
- package/python/kundali_lib/vedic/kalachakra.py +226 -0
- package/python/kundali_lib/vedic/kartari.py +243 -0
- package/python/kundali_lib/vedic/kurmachakra.py +383 -0
- package/python/kundali_lib/vedic/lunar_return.py +402 -0
- package/python/kundali_lib/vedic/muhurta.py +414 -0
- package/python/kundali_lib/vedic/nabhasha.py +349 -0
- package/python/kundali_lib/vedic/nakshatra_details.py +945 -0
- package/python/kundali_lib/vedic/panchanga.py +297 -0
- package/python/kundali_lib/vedic/planets.py +55 -0
- package/python/kundali_lib/vedic/shadbala.py +500 -0
- package/python/kundali_lib/vedic/special_conditions.py +319 -0
- package/python/kundali_lib/vedic/sudarshana.py +232 -0
- package/python/kundali_lib/vedic/tajaka.py +482 -0
- package/python/kundali_lib/vedic/upagrahas.py +229 -0
- package/python/kundali_lib/vedic/varshaphal.py +185 -0
- package/python/kundali_lib/vedic/yogas.py +935 -0
- package/python/kundali_lib/vedic/zodiac.py +42 -0
- package/python/kundali_lib/vedicastro_bridge.py +198 -0
- package/remote-server.js +590 -0
- package/requirements.txt +8 -0
- package/setup.sh +218 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"""Lunar Return (Chandraphal) and Prasna (Horary) charts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
|
|
7
|
+
import pytz
|
|
8
|
+
import swisseph as swe
|
|
9
|
+
|
|
10
|
+
from kundali_lib.vedic.chart import _jd_ut, build_chart
|
|
11
|
+
from kundali_lib.vedic.constants import RASHI_LORDS, RASHIS
|
|
12
|
+
from kundali_lib.vedic.zodiac import get_rashi
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# Reference data
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
RASHI_LORDS_MAP: dict[str, str] = {r: RASHI_LORDS[i] for i, r in enumerate(RASHIS)}
|
|
19
|
+
|
|
20
|
+
_SIDEREAL_FLAGS = swe.FLG_SWIEPH | swe.FLG_SIDEREAL
|
|
21
|
+
|
|
22
|
+
# Approximate days per sidereal month — used to bound the search window.
|
|
23
|
+
_SIDEREAL_MONTH_DAYS = 27.3217
|
|
24
|
+
|
|
25
|
+
# Moon returns every ~27.3 days; we search up to 32 days ahead from start.
|
|
26
|
+
_SEARCH_DAYS = 32
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Internal helpers
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _moon_longitude_at_jd(jd: float) -> float:
|
|
35
|
+
"""Sidereal (Lahiri) Moon longitude for a given Julian Day."""
|
|
36
|
+
swe.set_ephe_path(None)
|
|
37
|
+
swe.set_sid_mode(swe.SIDM_LAHIRI)
|
|
38
|
+
xx, _ = swe.calc_ut(jd, swe.MOON, swe.FLG_SWIEPH | swe.FLG_SIDEREAL)
|
|
39
|
+
return xx[0]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _angular_diff(a: float, b: float) -> float:
|
|
43
|
+
"""Signed difference (a − b) normalised to [−180, 180]."""
|
|
44
|
+
diff = (a - b) % 360.0
|
|
45
|
+
if diff > 180.0:
|
|
46
|
+
diff -= 360.0
|
|
47
|
+
return diff
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _jd_to_utc_datetime(jd: float) -> datetime:
|
|
51
|
+
"""Convert a Julian Day number to a UTC-aware datetime."""
|
|
52
|
+
year, month, day, hour_frac = swe.revjul(jd, 1)
|
|
53
|
+
hour = int(hour_frac)
|
|
54
|
+
minute_frac = (hour_frac - hour) * 60.0
|
|
55
|
+
minute = int(minute_frac)
|
|
56
|
+
second_frac = (minute_frac - minute) * 60.0
|
|
57
|
+
second = min(int(round(second_frac)), 59)
|
|
58
|
+
return datetime(
|
|
59
|
+
int(year), int(month), int(day), hour, minute, second, tzinfo=pytz.UTC
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _find_lunar_return(natal_moon_lon: float, start_jd: float) -> float:
|
|
64
|
+
"""Return the JD when the sidereal Moon longitude next equals natal_moon_lon.
|
|
65
|
+
|
|
66
|
+
Strategy:
|
|
67
|
+
1. Coarse scan every 6 hours until a zero-crossing is detected.
|
|
68
|
+
2. Binary-search the bracketed interval to sub-second precision.
|
|
69
|
+
"""
|
|
70
|
+
step = 0.25 # 6-hour steps
|
|
71
|
+
prev_diff = _angular_diff(_moon_longitude_at_jd(start_jd), natal_moon_lon)
|
|
72
|
+
lo_jd: float | None = None
|
|
73
|
+
hi_jd: float | None = None
|
|
74
|
+
|
|
75
|
+
for i in range(1, int(_SEARCH_DAYS / step) + 2):
|
|
76
|
+
jd = start_jd + i * step
|
|
77
|
+
curr_diff = _angular_diff(_moon_longitude_at_jd(jd), natal_moon_lon)
|
|
78
|
+
|
|
79
|
+
if abs(curr_diff) < 0.0001:
|
|
80
|
+
return jd # landed almost exactly on crossing
|
|
81
|
+
|
|
82
|
+
# Moon moves ~0.55°/6h forward; look for negative→positive zero-crossing.
|
|
83
|
+
if prev_diff < 0.0 and curr_diff >= 0.0:
|
|
84
|
+
lo_jd = jd - step
|
|
85
|
+
hi_jd = jd
|
|
86
|
+
break
|
|
87
|
+
|
|
88
|
+
prev_diff = curr_diff
|
|
89
|
+
|
|
90
|
+
if lo_jd is None or hi_jd is None:
|
|
91
|
+
# Fallback: return the point with smallest absolute difference
|
|
92
|
+
return start_jd
|
|
93
|
+
|
|
94
|
+
# Binary search — 60 iterations → microsecond-level precision
|
|
95
|
+
for _ in range(60):
|
|
96
|
+
mid = (lo_jd + hi_jd) / 2.0
|
|
97
|
+
diff = _angular_diff(_moon_longitude_at_jd(mid), natal_moon_lon)
|
|
98
|
+
if abs(diff) < 0.0001:
|
|
99
|
+
return mid
|
|
100
|
+
if diff < 0.0:
|
|
101
|
+
lo_jd = mid
|
|
102
|
+
else:
|
|
103
|
+
hi_jd = mid
|
|
104
|
+
|
|
105
|
+
return (lo_jd + hi_jd) / 2.0
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _get_planet(positions: list[dict], name: str) -> dict | None:
|
|
109
|
+
for p in positions:
|
|
110
|
+
if p["name"] == name:
|
|
111
|
+
return p
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _house_of_sign(sign: str, asc_sign: str) -> int:
|
|
116
|
+
asc_idx = RASHIS.index(asc_sign)
|
|
117
|
+
sign_idx = RASHIS.index(sign)
|
|
118
|
+
return (sign_idx - asc_idx) % 12 + 1
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
# Lunar Return month-theme interpreter
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
_LAGNA_THEMES: dict[str, str] = {
|
|
126
|
+
"Aries": "Action, initiation, and personal identity take centre stage.",
|
|
127
|
+
"Taurus": "Finances, stability, and material comfort are emphasised.",
|
|
128
|
+
"Gemini": "Communication, learning, and short journeys are prominent.",
|
|
129
|
+
"Cancer": "Home, emotions, and family matters dominate the month.",
|
|
130
|
+
"Leo": "Creativity, confidence, and self-expression are highlighted.",
|
|
131
|
+
"Virgo": "Health, work routines, and analytical thinking are in focus.",
|
|
132
|
+
"Libra": "Relationships, balance, and negotiations are key themes.",
|
|
133
|
+
"Scorpio": "Transformation, depth, and hidden matters come to the fore.",
|
|
134
|
+
"Sagittarius": "Philosophy, travel, and expansion of horizons are active.",
|
|
135
|
+
"Capricorn": "Career ambitions, discipline, and long-term goals dominate.",
|
|
136
|
+
"Aquarius": "Social networks, innovation, and humanitarian goals are active.",
|
|
137
|
+
"Pisces": "Spirituality, imagination, and introspection colour the month.",
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _month_theme(lr_asc_sign: str, moon_house: int) -> str:
|
|
142
|
+
base = _LAGNA_THEMES.get(lr_asc_sign, "A month of reflection and adjustment.")
|
|
143
|
+
moon_note = f" Moon in house {moon_house} brings focus to "
|
|
144
|
+
house_keywords = {
|
|
145
|
+
1: "the self and body",
|
|
146
|
+
2: "finances and family",
|
|
147
|
+
3: "communication and siblings",
|
|
148
|
+
4: "home and mother",
|
|
149
|
+
5: "creativity and children",
|
|
150
|
+
6: "health and service",
|
|
151
|
+
7: "relationships and partners",
|
|
152
|
+
8: "transformation and shared resources",
|
|
153
|
+
9: "luck, belief, and long journeys",
|
|
154
|
+
10: "career and public image",
|
|
155
|
+
11: "gains and social circles",
|
|
156
|
+
12: "solitude, losses, and spirituality",
|
|
157
|
+
}
|
|
158
|
+
moon_note += house_keywords.get(moon_house, "general matters") + "."
|
|
159
|
+
return base + moon_note
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
# Public API — Lunar Return
|
|
164
|
+
# ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def get_lunar_return(
|
|
168
|
+
birth_chart: dict,
|
|
169
|
+
birth_dt: datetime,
|
|
170
|
+
target_year: int,
|
|
171
|
+
target_month: int,
|
|
172
|
+
query_lat: float,
|
|
173
|
+
query_lon: float,
|
|
174
|
+
query_timezone: str,
|
|
175
|
+
) -> dict:
|
|
176
|
+
"""Calculate the Lunar Return (Chandraphal) chart for a given month.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
birth_chart: Output of ``build_chart()`` for the native's birth.
|
|
180
|
+
birth_dt: Native's local birth datetime (naive or aware).
|
|
181
|
+
target_year: Calendar year of the desired Lunar Return.
|
|
182
|
+
target_month: Calendar month (1-12) of the desired Lunar Return.
|
|
183
|
+
query_lat: Latitude for the Lunar Return chart.
|
|
184
|
+
query_lon: Longitude for the Lunar Return chart.
|
|
185
|
+
query_timezone: IANA timezone string for the query location.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
dict with Lunar Return timing, chart, and key highlights.
|
|
189
|
+
"""
|
|
190
|
+
swe.set_ephe_path(None)
|
|
191
|
+
swe.set_sid_mode(swe.SIDM_LAHIRI)
|
|
192
|
+
|
|
193
|
+
# ── Natal Moon longitude ──────────────────────────────────────────────────
|
|
194
|
+
birth_timezone = birth_chart.get("birth_details", {}).get("timezone", "UTC")
|
|
195
|
+
birth_jd = _jd_ut(birth_dt, birth_timezone)
|
|
196
|
+
xx, _ = swe.calc_ut(birth_jd, swe.MOON, _SIDEREAL_FLAGS)
|
|
197
|
+
natal_moon_lon: float = xx[0]
|
|
198
|
+
natal_moon_rashi = get_rashi(natal_moon_lon)
|
|
199
|
+
|
|
200
|
+
# ── Search start: 1st of target month ────────────────────────────────────
|
|
201
|
+
start_jd = swe.julday(target_year, target_month, 1, 0.0)
|
|
202
|
+
|
|
203
|
+
# ── Find the Lunar Return JD ──────────────────────────────────────────────
|
|
204
|
+
lr_jd = _find_lunar_return(natal_moon_lon, start_jd)
|
|
205
|
+
|
|
206
|
+
# ── Convert to datetimes ──────────────────────────────────────────────────
|
|
207
|
+
lr_utc = _jd_to_utc_datetime(lr_jd)
|
|
208
|
+
query_tz = pytz.timezone(query_timezone)
|
|
209
|
+
lr_local_aware = lr_utc.astimezone(query_tz)
|
|
210
|
+
lr_local_naive = lr_local_aware.replace(tzinfo=None)
|
|
211
|
+
|
|
212
|
+
# ── Next Lunar Return JD (for days_until_next_return) ────────────────────
|
|
213
|
+
next_lr_jd = _find_lunar_return(natal_moon_lon, lr_jd + 1.0)
|
|
214
|
+
days_until_next = round(next_lr_jd - lr_jd, 2)
|
|
215
|
+
|
|
216
|
+
# ── Build Lunar Return chart ──────────────────────────────────────────────
|
|
217
|
+
lr_chart = build_chart(
|
|
218
|
+
name="Lunar Return",
|
|
219
|
+
birth_dt=lr_local_naive,
|
|
220
|
+
lat=query_lat,
|
|
221
|
+
lon=query_lon,
|
|
222
|
+
timezone=query_timezone,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# ── Key highlights ────────────────────────────────────────────────────────
|
|
226
|
+
lr_asc_sign = lr_chart["ascendant"]["rashi"]
|
|
227
|
+
moon_planet = _get_planet(lr_chart["planetary_positions"], "Moon")
|
|
228
|
+
moon_house_lr = moon_planet.get("house", 0) if moon_planet else 0
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
"target_year": target_year,
|
|
232
|
+
"target_month": target_month,
|
|
233
|
+
"natal_moon_longitude": round(natal_moon_lon, 4),
|
|
234
|
+
"natal_moon_rashi": natal_moon_rashi,
|
|
235
|
+
"lunar_return_datetime_utc": lr_utc.isoformat(),
|
|
236
|
+
"lunar_return_datetime_local": lr_local_aware.isoformat(),
|
|
237
|
+
"days_until_next_return": days_until_next,
|
|
238
|
+
"lunar_return_chart": {
|
|
239
|
+
"planetary_positions": lr_chart["planetary_positions"],
|
|
240
|
+
"houses": lr_chart["houses"],
|
|
241
|
+
"ascendant": lr_chart["ascendant"],
|
|
242
|
+
"moon_sign": lr_chart["moon_sign"],
|
|
243
|
+
"sun_sign": lr_chart["sun_sign"],
|
|
244
|
+
},
|
|
245
|
+
"key_highlights": {
|
|
246
|
+
"lunar_return_ascendant": lr_asc_sign,
|
|
247
|
+
"month_theme": _month_theme(lr_asc_sign, moon_house_lr),
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# ---------------------------------------------------------------------------
|
|
253
|
+
# Public API — Prasna (Horary)
|
|
254
|
+
# ---------------------------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def get_prasna_chart(
|
|
258
|
+
query_dt_utc: datetime,
|
|
259
|
+
query_lat: float,
|
|
260
|
+
query_lon: float,
|
|
261
|
+
question: str = "",
|
|
262
|
+
) -> dict:
|
|
263
|
+
"""Calculate a Prasna (Horary) chart for the moment a question is asked.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
query_dt_utc: UTC-aware datetime of the question moment.
|
|
267
|
+
query_lat: Geographic latitude of the querent.
|
|
268
|
+
query_lon: Geographic longitude of the querent.
|
|
269
|
+
question: The question being asked (stored verbatim).
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
dict with the Prasna chart and key horary indicators.
|
|
273
|
+
"""
|
|
274
|
+
if query_dt_utc.tzinfo is None:
|
|
275
|
+
query_dt_utc = pytz.UTC.localize(query_dt_utc)
|
|
276
|
+
|
|
277
|
+
# ── Determine local timezone from coordinates ─────────────────────────────
|
|
278
|
+
try:
|
|
279
|
+
from timezonefinder import TimezoneFinder
|
|
280
|
+
|
|
281
|
+
tf = TimezoneFinder()
|
|
282
|
+
tz_name = tf.timezone_at(lat=query_lat, lng=query_lon) or "UTC"
|
|
283
|
+
except Exception:
|
|
284
|
+
tz_name = "UTC"
|
|
285
|
+
|
|
286
|
+
local_tz = pytz.timezone(tz_name)
|
|
287
|
+
query_dt_local = query_dt_utc.astimezone(local_tz)
|
|
288
|
+
query_dt_naive = query_dt_local.replace(tzinfo=None)
|
|
289
|
+
|
|
290
|
+
# ── Build chart at question moment ────────────────────────────────────────
|
|
291
|
+
prasna_chart = build_chart(
|
|
292
|
+
name="Prasna",
|
|
293
|
+
birth_dt=query_dt_naive,
|
|
294
|
+
lat=query_lat,
|
|
295
|
+
lon=query_lon,
|
|
296
|
+
timezone=tz_name,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
positions: list[dict] = prasna_chart["planetary_positions"]
|
|
300
|
+
asc_info: dict = prasna_chart["ascendant"]
|
|
301
|
+
asc_sign: str = asc_info.get("rashi", RASHIS[0])
|
|
302
|
+
|
|
303
|
+
# ── Lagna and Lagna lord ──────────────────────────────────────────────────
|
|
304
|
+
lagna_lord_name = RASHI_LORDS_MAP.get(asc_sign, "")
|
|
305
|
+
lagna_lord_planet = _get_planet(positions, lagna_lord_name)
|
|
306
|
+
lagna_lord_house = lagna_lord_planet.get("house", 0) if lagna_lord_planet else 0
|
|
307
|
+
|
|
308
|
+
# ── Moon ──────────────────────────────────────────────────────────────────
|
|
309
|
+
moon = _get_planet(positions, "Moon")
|
|
310
|
+
moon_rashi = moon.get("rashi", "") if moon else ""
|
|
311
|
+
moon_house = moon.get("house", 0) if moon else 0
|
|
312
|
+
|
|
313
|
+
# ── 7th house (the matter asked about) ───────────────────────────────────
|
|
314
|
+
asc_idx = RASHIS.index(asc_sign)
|
|
315
|
+
seventh_sign = RASHIS[(asc_idx + 6) % 12]
|
|
316
|
+
seventh_lord_name = RASHI_LORDS_MAP.get(seventh_sign, "")
|
|
317
|
+
seventh_lord_planet = _get_planet(positions, seventh_lord_name)
|
|
318
|
+
seventh_lord_house = (
|
|
319
|
+
seventh_lord_planet.get("house", 0) if seventh_lord_planet else 0
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# ── Is Moon approaching Lagna lord? (Ithasala check) ─────────────────────
|
|
323
|
+
moon_approaching_ll = False
|
|
324
|
+
if moon and lagna_lord_planet:
|
|
325
|
+
moon_speed = abs(moon.get("speed_longitude", 13.2))
|
|
326
|
+
ll_speed = abs(lagna_lord_planet.get("speed_longitude", 1.0))
|
|
327
|
+
if moon_speed > ll_speed:
|
|
328
|
+
diff = _angular_diff(lagna_lord_planet["longitude"], moon["longitude"])
|
|
329
|
+
moon_approaching_ll = 0 < diff <= 13
|
|
330
|
+
|
|
331
|
+
# ── Favourable and unfavourable indicators ────────────────────────────────
|
|
332
|
+
favourable: list[str] = []
|
|
333
|
+
unfavourable: list[str] = []
|
|
334
|
+
|
|
335
|
+
if moon_approaching_ll:
|
|
336
|
+
favourable.append(
|
|
337
|
+
"Moon approaching Lagna lord — positive indication for the matter"
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
if lagna_lord_house in {1, 4, 7, 10}:
|
|
341
|
+
favourable.append(f"Lagna lord in angular house {lagna_lord_house} — strong")
|
|
342
|
+
elif lagna_lord_house in {6, 8, 12}:
|
|
343
|
+
unfavourable.append(
|
|
344
|
+
f"Lagna lord in dusthana (house {lagna_lord_house}) — weakened"
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
if seventh_lord_house in {1, 4, 7, 10}:
|
|
348
|
+
favourable.append(
|
|
349
|
+
f"7th lord in angular house {seventh_lord_house} — matter is active"
|
|
350
|
+
)
|
|
351
|
+
elif seventh_lord_house in {6, 8, 12}:
|
|
352
|
+
unfavourable.append(f"7th lord in dusthana — obstacles for the matter")
|
|
353
|
+
|
|
354
|
+
if moon_house in {1, 4, 7, 10}:
|
|
355
|
+
favourable.append(f"Moon in kendra (house {moon_house}) — emotional strength")
|
|
356
|
+
elif moon_house in {6, 8, 12}:
|
|
357
|
+
unfavourable.append(f"Moon in dusthana (house {moon_house}) — anxiety or delay")
|
|
358
|
+
|
|
359
|
+
jupiter = _get_planet(positions, "Jupiter")
|
|
360
|
+
if jupiter and jupiter.get("house", 0) in {1, 5, 9}:
|
|
361
|
+
favourable.append("Jupiter in trikona — divine grace supports the query")
|
|
362
|
+
|
|
363
|
+
# ── General reading ───────────────────────────────────────────────────────
|
|
364
|
+
if len(favourable) > len(unfavourable):
|
|
365
|
+
outlook = "The chart generally favours a positive outcome."
|
|
366
|
+
elif len(unfavourable) > len(favourable):
|
|
367
|
+
outlook = "The chart indicates delays or obstacles; careful action needed."
|
|
368
|
+
else:
|
|
369
|
+
outlook = "The chart shows a mixed picture; outcome depends on effort."
|
|
370
|
+
|
|
371
|
+
general_reading = (
|
|
372
|
+
f"Prasna Lagna is {asc_sign}, ruled by {lagna_lord_name} "
|
|
373
|
+
f"(in house {lagna_lord_house}). "
|
|
374
|
+
f"Moon is in {moon_rashi} (house {moon_house}). "
|
|
375
|
+
f"The 7th house (matter asked about) is {seventh_sign}, "
|
|
376
|
+
f"ruled by {seventh_lord_name} (house {seventh_lord_house}). " + outlook
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
"question": question,
|
|
381
|
+
"datetime_utc": query_dt_utc.isoformat(),
|
|
382
|
+
"prasna_chart": {
|
|
383
|
+
"planetary_positions": positions,
|
|
384
|
+
"houses": prasna_chart["houses"],
|
|
385
|
+
"ascendant": asc_info,
|
|
386
|
+
"moon_sign": prasna_chart.get("moon_sign", moon_rashi),
|
|
387
|
+
},
|
|
388
|
+
"prasna_indicators": {
|
|
389
|
+
"lagna": asc_sign,
|
|
390
|
+
"lagna_lord": lagna_lord_name,
|
|
391
|
+
"lagna_lord_house": lagna_lord_house,
|
|
392
|
+
"moon_rashi": moon_rashi,
|
|
393
|
+
"moon_house": moon_house,
|
|
394
|
+
"seventh_house_sign": seventh_sign,
|
|
395
|
+
"seventh_lord": seventh_lord_name,
|
|
396
|
+
"seventh_lord_house": seventh_lord_house,
|
|
397
|
+
"moon_approaching_lagna_lord": moon_approaching_ll,
|
|
398
|
+
"favorable_indicators": favourable,
|
|
399
|
+
"unfavorable_indicators": unfavourable,
|
|
400
|
+
"general_reading": general_reading,
|
|
401
|
+
},
|
|
402
|
+
}
|