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.
Files changed (172) hide show
  1. package/README.md +67 -0
  2. package/azure-function/function_app.py +93 -0
  3. package/azure-function/host.json +15 -0
  4. package/azure-function/kundali_bridge.py +952 -0
  5. package/azure-function/python/kundali_lib/__init__.py +1 -0
  6. package/azure-function/python/kundali_lib/__pycache__/__init__.cpython-313.pyc +0 -0
  7. package/azure-function/python/kundali_lib/__pycache__/ephemeris.cpython-313.pyc +0 -0
  8. package/azure-function/python/kundali_lib/__pycache__/geocoder.cpython-313.pyc +0 -0
  9. package/azure-function/python/kundali_lib/__pycache__/vedicastro_bridge.cpython-313.pyc +0 -0
  10. package/azure-function/python/kundali_lib/ephemeris.py +30 -0
  11. package/azure-function/python/kundali_lib/geocoder.py +82 -0
  12. package/azure-function/python/kundali_lib/vedic/__init__.py +1 -0
  13. package/azure-function/python/kundali_lib/vedic/__pycache__/__init__.cpython-313.pyc +0 -0
  14. package/azure-function/python/kundali_lib/vedic/__pycache__/arishta.cpython-313.pyc +0 -0
  15. package/azure-function/python/kundali_lib/vedic/__pycache__/ashtakavarga.cpython-313.pyc +0 -0
  16. package/azure-function/python/kundali_lib/vedic/__pycache__/avasthas.cpython-313.pyc +0 -0
  17. package/azure-function/python/kundali_lib/vedic/__pycache__/ayanamsa.cpython-313.pyc +0 -0
  18. package/azure-function/python/kundali_lib/vedic/__pycache__/bhava_chalit.cpython-313.pyc +0 -0
  19. package/azure-function/python/kundali_lib/vedic/__pycache__/char_dasha.cpython-313.pyc +0 -0
  20. package/azure-function/python/kundali_lib/vedic/__pycache__/chart.cpython-313.pyc +0 -0
  21. package/azure-function/python/kundali_lib/vedic/__pycache__/chart_types.cpython-313.pyc +0 -0
  22. package/azure-function/python/kundali_lib/vedic/__pycache__/compatibility.cpython-313.pyc +0 -0
  23. package/azure-function/python/kundali_lib/vedic/__pycache__/constants.cpython-313.pyc +0 -0
  24. package/azure-function/python/kundali_lib/vedic/__pycache__/dasha_extended.cpython-313.pyc +0 -0
  25. package/azure-function/python/kundali_lib/vedic/__pycache__/dasha_systems.cpython-313.pyc +0 -0
  26. package/azure-function/python/kundali_lib/vedic/__pycache__/doshas.cpython-313.pyc +0 -0
  27. package/azure-function/python/kundali_lib/vedic/__pycache__/gandanta.cpython-313.pyc +0 -0
  28. package/azure-function/python/kundali_lib/vedic/__pycache__/gochara.cpython-313.pyc +0 -0
  29. package/azure-function/python/kundali_lib/vedic/__pycache__/hora.cpython-313.pyc +0 -0
  30. package/azure-function/python/kundali_lib/vedic/__pycache__/houses.cpython-313.pyc +0 -0
  31. package/azure-function/python/kundali_lib/vedic/__pycache__/jaimini.cpython-313.pyc +0 -0
  32. package/azure-function/python/kundali_lib/vedic/__pycache__/kalachakra.cpython-313.pyc +0 -0
  33. package/azure-function/python/kundali_lib/vedic/__pycache__/kartari.cpython-313.pyc +0 -0
  34. package/azure-function/python/kundali_lib/vedic/__pycache__/kurmachakra.cpython-313.pyc +0 -0
  35. package/azure-function/python/kundali_lib/vedic/__pycache__/lunar_return.cpython-313.pyc +0 -0
  36. package/azure-function/python/kundali_lib/vedic/__pycache__/muhurta.cpython-313.pyc +0 -0
  37. package/azure-function/python/kundali_lib/vedic/__pycache__/nabhasha.cpython-313.pyc +0 -0
  38. package/azure-function/python/kundali_lib/vedic/__pycache__/nakshatra_details.cpython-313.pyc +0 -0
  39. package/azure-function/python/kundali_lib/vedic/__pycache__/panchanga.cpython-313.pyc +0 -0
  40. package/azure-function/python/kundali_lib/vedic/__pycache__/planets.cpython-313.pyc +0 -0
  41. package/azure-function/python/kundali_lib/vedic/__pycache__/shadbala.cpython-313.pyc +0 -0
  42. package/azure-function/python/kundali_lib/vedic/__pycache__/special_conditions.cpython-313.pyc +0 -0
  43. package/azure-function/python/kundali_lib/vedic/__pycache__/sudarshana.cpython-313.pyc +0 -0
  44. package/azure-function/python/kundali_lib/vedic/__pycache__/tajaka.cpython-313.pyc +0 -0
  45. package/azure-function/python/kundali_lib/vedic/__pycache__/upagrahas.cpython-313.pyc +0 -0
  46. package/azure-function/python/kundali_lib/vedic/__pycache__/varshaphal.cpython-313.pyc +0 -0
  47. package/azure-function/python/kundali_lib/vedic/__pycache__/yogas.cpython-313.pyc +0 -0
  48. package/azure-function/python/kundali_lib/vedic/__pycache__/zodiac.cpython-313.pyc +0 -0
  49. package/azure-function/python/kundali_lib/vedic/arishta.py +465 -0
  50. package/azure-function/python/kundali_lib/vedic/ashtakavarga.py +213 -0
  51. package/azure-function/python/kundali_lib/vedic/avasthas.py +292 -0
  52. package/azure-function/python/kundali_lib/vedic/ayanamsa.py +106 -0
  53. package/azure-function/python/kundali_lib/vedic/bhava_chalit.py +137 -0
  54. package/azure-function/python/kundali_lib/vedic/char_dasha.py +308 -0
  55. package/azure-function/python/kundali_lib/vedic/chart.py +126 -0
  56. package/azure-function/python/kundali_lib/vedic/chart_types.py +338 -0
  57. package/azure-function/python/kundali_lib/vedic/compatibility.py +705 -0
  58. package/azure-function/python/kundali_lib/vedic/constants.py +108 -0
  59. package/azure-function/python/kundali_lib/vedic/dasha_extended.py +262 -0
  60. package/azure-function/python/kundali_lib/vedic/dasha_systems.py +439 -0
  61. package/azure-function/python/kundali_lib/vedic/doshas.py +453 -0
  62. package/azure-function/python/kundali_lib/vedic/gandanta.py +213 -0
  63. package/azure-function/python/kundali_lib/vedic/gochara.py +277 -0
  64. package/azure-function/python/kundali_lib/vedic/hora.py +263 -0
  65. package/azure-function/python/kundali_lib/vedic/houses.py +30 -0
  66. package/azure-function/python/kundali_lib/vedic/jaimini.py +361 -0
  67. package/azure-function/python/kundali_lib/vedic/kalachakra.py +226 -0
  68. package/azure-function/python/kundali_lib/vedic/kartari.py +243 -0
  69. package/azure-function/python/kundali_lib/vedic/kurmachakra.py +383 -0
  70. package/azure-function/python/kundali_lib/vedic/lunar_return.py +402 -0
  71. package/azure-function/python/kundali_lib/vedic/muhurta.py +414 -0
  72. package/azure-function/python/kundali_lib/vedic/nabhasha.py +349 -0
  73. package/azure-function/python/kundali_lib/vedic/nakshatra_details.py +945 -0
  74. package/azure-function/python/kundali_lib/vedic/panchanga.py +297 -0
  75. package/azure-function/python/kundali_lib/vedic/planets.py +55 -0
  76. package/azure-function/python/kundali_lib/vedic/shadbala.py +500 -0
  77. package/azure-function/python/kundali_lib/vedic/special_conditions.py +319 -0
  78. package/azure-function/python/kundali_lib/vedic/sudarshana.py +232 -0
  79. package/azure-function/python/kundali_lib/vedic/tajaka.py +482 -0
  80. package/azure-function/python/kundali_lib/vedic/upagrahas.py +229 -0
  81. package/azure-function/python/kundali_lib/vedic/varshaphal.py +185 -0
  82. package/azure-function/python/kundali_lib/vedic/yogas.py +935 -0
  83. package/azure-function/python/kundali_lib/vedic/zodiac.py +42 -0
  84. package/azure-function/python/kundali_lib/vedicastro_bridge.py +198 -0
  85. package/azure-function/requirements.txt +9 -0
  86. package/index.js +747 -0
  87. package/kundali-chart-mcp.js +159 -0
  88. package/kundali_bridge.py +952 -0
  89. package/package.json +41 -0
  90. package/python/kundali_lib/__init__.py +1 -0
  91. package/python/kundali_lib/__pycache__/__init__.cpython-313.pyc +0 -0
  92. package/python/kundali_lib/__pycache__/ephemeris.cpython-313.pyc +0 -0
  93. package/python/kundali_lib/__pycache__/geocoder.cpython-313.pyc +0 -0
  94. package/python/kundali_lib/__pycache__/vedicastro_bridge.cpython-313.pyc +0 -0
  95. package/python/kundali_lib/ephemeris.py +30 -0
  96. package/python/kundali_lib/geocoder.py +82 -0
  97. package/python/kundali_lib/vedic/__init__.py +1 -0
  98. package/python/kundali_lib/vedic/__pycache__/__init__.cpython-313.pyc +0 -0
  99. package/python/kundali_lib/vedic/__pycache__/arishta.cpython-313.pyc +0 -0
  100. package/python/kundali_lib/vedic/__pycache__/ashtakavarga.cpython-313.pyc +0 -0
  101. package/python/kundali_lib/vedic/__pycache__/avasthas.cpython-313.pyc +0 -0
  102. package/python/kundali_lib/vedic/__pycache__/ayanamsa.cpython-313.pyc +0 -0
  103. package/python/kundali_lib/vedic/__pycache__/bhava_chalit.cpython-313.pyc +0 -0
  104. package/python/kundali_lib/vedic/__pycache__/char_dasha.cpython-313.pyc +0 -0
  105. package/python/kundali_lib/vedic/__pycache__/chart.cpython-313.pyc +0 -0
  106. package/python/kundali_lib/vedic/__pycache__/chart_types.cpython-313.pyc +0 -0
  107. package/python/kundali_lib/vedic/__pycache__/compatibility.cpython-313.pyc +0 -0
  108. package/python/kundali_lib/vedic/__pycache__/constants.cpython-313.pyc +0 -0
  109. package/python/kundali_lib/vedic/__pycache__/dasha_extended.cpython-313.pyc +0 -0
  110. package/python/kundali_lib/vedic/__pycache__/dasha_systems.cpython-313.pyc +0 -0
  111. package/python/kundali_lib/vedic/__pycache__/doshas.cpython-313.pyc +0 -0
  112. package/python/kundali_lib/vedic/__pycache__/gandanta.cpython-313.pyc +0 -0
  113. package/python/kundali_lib/vedic/__pycache__/gochara.cpython-313.pyc +0 -0
  114. package/python/kundali_lib/vedic/__pycache__/hora.cpython-313.pyc +0 -0
  115. package/python/kundali_lib/vedic/__pycache__/houses.cpython-313.pyc +0 -0
  116. package/python/kundali_lib/vedic/__pycache__/jaimini.cpython-313.pyc +0 -0
  117. package/python/kundali_lib/vedic/__pycache__/kalachakra.cpython-313.pyc +0 -0
  118. package/python/kundali_lib/vedic/__pycache__/kartari.cpython-313.pyc +0 -0
  119. package/python/kundali_lib/vedic/__pycache__/kurmachakra.cpython-313.pyc +0 -0
  120. package/python/kundali_lib/vedic/__pycache__/lunar_return.cpython-313.pyc +0 -0
  121. package/python/kundali_lib/vedic/__pycache__/muhurta.cpython-313.pyc +0 -0
  122. package/python/kundali_lib/vedic/__pycache__/nabhasha.cpython-313.pyc +0 -0
  123. package/python/kundali_lib/vedic/__pycache__/nakshatra_details.cpython-313.pyc +0 -0
  124. package/python/kundali_lib/vedic/__pycache__/panchanga.cpython-313.pyc +0 -0
  125. package/python/kundali_lib/vedic/__pycache__/planets.cpython-313.pyc +0 -0
  126. package/python/kundali_lib/vedic/__pycache__/shadbala.cpython-313.pyc +0 -0
  127. package/python/kundali_lib/vedic/__pycache__/special_conditions.cpython-313.pyc +0 -0
  128. package/python/kundali_lib/vedic/__pycache__/sudarshana.cpython-313.pyc +0 -0
  129. package/python/kundali_lib/vedic/__pycache__/tajaka.cpython-313.pyc +0 -0
  130. package/python/kundali_lib/vedic/__pycache__/upagrahas.cpython-313.pyc +0 -0
  131. package/python/kundali_lib/vedic/__pycache__/varshaphal.cpython-313.pyc +0 -0
  132. package/python/kundali_lib/vedic/__pycache__/yogas.cpython-313.pyc +0 -0
  133. package/python/kundali_lib/vedic/__pycache__/zodiac.cpython-313.pyc +0 -0
  134. package/python/kundali_lib/vedic/arishta.py +465 -0
  135. package/python/kundali_lib/vedic/ashtakavarga.py +213 -0
  136. package/python/kundali_lib/vedic/avasthas.py +292 -0
  137. package/python/kundali_lib/vedic/ayanamsa.py +106 -0
  138. package/python/kundali_lib/vedic/bhava_chalit.py +137 -0
  139. package/python/kundali_lib/vedic/char_dasha.py +308 -0
  140. package/python/kundali_lib/vedic/chart.py +126 -0
  141. package/python/kundali_lib/vedic/chart_types.py +338 -0
  142. package/python/kundali_lib/vedic/compatibility.py +705 -0
  143. package/python/kundali_lib/vedic/constants.py +108 -0
  144. package/python/kundali_lib/vedic/dasha_extended.py +262 -0
  145. package/python/kundali_lib/vedic/dasha_systems.py +439 -0
  146. package/python/kundali_lib/vedic/doshas.py +453 -0
  147. package/python/kundali_lib/vedic/gandanta.py +213 -0
  148. package/python/kundali_lib/vedic/gochara.py +277 -0
  149. package/python/kundali_lib/vedic/hora.py +263 -0
  150. package/python/kundali_lib/vedic/houses.py +30 -0
  151. package/python/kundali_lib/vedic/jaimini.py +361 -0
  152. package/python/kundali_lib/vedic/kalachakra.py +226 -0
  153. package/python/kundali_lib/vedic/kartari.py +243 -0
  154. package/python/kundali_lib/vedic/kurmachakra.py +383 -0
  155. package/python/kundali_lib/vedic/lunar_return.py +402 -0
  156. package/python/kundali_lib/vedic/muhurta.py +414 -0
  157. package/python/kundali_lib/vedic/nabhasha.py +349 -0
  158. package/python/kundali_lib/vedic/nakshatra_details.py +945 -0
  159. package/python/kundali_lib/vedic/panchanga.py +297 -0
  160. package/python/kundali_lib/vedic/planets.py +55 -0
  161. package/python/kundali_lib/vedic/shadbala.py +500 -0
  162. package/python/kundali_lib/vedic/special_conditions.py +319 -0
  163. package/python/kundali_lib/vedic/sudarshana.py +232 -0
  164. package/python/kundali_lib/vedic/tajaka.py +482 -0
  165. package/python/kundali_lib/vedic/upagrahas.py +229 -0
  166. package/python/kundali_lib/vedic/varshaphal.py +185 -0
  167. package/python/kundali_lib/vedic/yogas.py +935 -0
  168. package/python/kundali_lib/vedic/zodiac.py +42 -0
  169. package/python/kundali_lib/vedicastro_bridge.py +198 -0
  170. package/remote-server.js +590 -0
  171. package/requirements.txt +8 -0
  172. package/setup.sh +218 -0
@@ -0,0 +1,453 @@
1
+ """Vedic astrological doshas: Mangal, Kaal Sarp, Sade Sati, and others."""
2
+
3
+ # ---------------------------------------------------------------------------
4
+ # Constants
5
+ # ---------------------------------------------------------------------------
6
+
7
+ RASHIS = [
8
+ "Aries",
9
+ "Taurus",
10
+ "Gemini",
11
+ "Cancer",
12
+ "Leo",
13
+ "Virgo",
14
+ "Libra",
15
+ "Scorpio",
16
+ "Sagittarius",
17
+ "Capricorn",
18
+ "Aquarius",
19
+ "Pisces",
20
+ ]
21
+
22
+ RASHI_INDEX = {r: i for i, r in enumerate(RASHIS)}
23
+
24
+ MANGAL_DOSHA_HOUSES = {1, 2, 4, 7, 8, 12}
25
+
26
+ DOSHA_LEVEL_MAP = {
27
+ 7: "High",
28
+ 8: "High",
29
+ 1: "Medium",
30
+ 4: "Medium",
31
+ 12: "Medium",
32
+ 2: "Low",
33
+ }
34
+
35
+ KAAL_SARP_NAMES = {
36
+ "Aries": "Anant",
37
+ "Taurus": "Kulik",
38
+ "Gemini": "Vasuki",
39
+ "Cancer": "Shankhpal",
40
+ "Leo": "Padma",
41
+ "Virgo": "Mahapadma",
42
+ "Libra": "Takshak",
43
+ "Scorpio": "Karkotak",
44
+ "Sagittarius": "Shankhnaad",
45
+ "Capricorn": "Patak",
46
+ "Aquarius": "Vishakta",
47
+ "Pisces": "Sheshnag",
48
+ }
49
+
50
+ # Combustion thresholds in degrees (direct, retrograde)
51
+ COMBUSTION_THRESHOLDS = {
52
+ "Moon": (12, 12),
53
+ "Mercury": (14, 12),
54
+ "Venus": (10, 8),
55
+ "Mars": (17, 17),
56
+ "Jupiter": (11, 11),
57
+ "Saturn": (15, 15),
58
+ }
59
+
60
+ MAIN_PLANETS = ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn"]
61
+ WAR_PLANETS = ["Mars", "Mercury", "Jupiter", "Venus", "Saturn"]
62
+
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # Helpers
66
+ # ---------------------------------------------------------------------------
67
+
68
+
69
+ def _get_planet(positions: list, name: str) -> dict | None:
70
+ for p in positions:
71
+ if p.get("name", "").lower() == name.lower():
72
+ return p
73
+ return None
74
+
75
+
76
+ def _angular_sep(lon_a: float, lon_b: float) -> float:
77
+ """Shortest angular distance between two longitudes (0-180)."""
78
+ diff = abs(lon_a - lon_b) % 360
79
+ return diff if diff <= 180 else 360 - diff
80
+
81
+
82
+ def _lon_between_clockwise(lon: float, start: float, end: float) -> bool:
83
+ """Return True if *lon* lies in the arc from *start* to *end* going clockwise
84
+ (i.e. increasing longitude modulo 360)."""
85
+ lon = lon % 360
86
+ start = start % 360
87
+ end = end % 360
88
+ if start <= end:
89
+ return start <= lon <= end
90
+ # Arc wraps around 0°
91
+ return lon >= start or lon <= end
92
+
93
+
94
+ def _rashi_from_lon(lon: float) -> str:
95
+ return RASHIS[int(lon % 360 / 30)]
96
+
97
+
98
+ # ---------------------------------------------------------------------------
99
+ # 1. Mangal Dosha
100
+ # ---------------------------------------------------------------------------
101
+
102
+
103
+ def check_mangal_dosha(planetary_positions: list, ascendant: dict) -> dict:
104
+ """Check for Mangal (Kuja) Dosha.
105
+
106
+ Parameters
107
+ ----------
108
+ planetary_positions : list
109
+ List of planet dicts from build_chart().
110
+ ascendant : dict
111
+ Ascendant dict (must contain at least a ``longitude`` key, or a
112
+ ``rashi`` key, from build_chart()).
113
+
114
+ Returns
115
+ -------
116
+ dict
117
+ has_dosha, mars_house, affected_houses, dosha_level, cancellation_notes,
118
+ severity.
119
+ """
120
+ mars = _get_planet(planetary_positions, "Mars")
121
+ if mars is None:
122
+ return {
123
+ "has_dosha": False,
124
+ "mars_house": None,
125
+ "affected_houses": list(MANGAL_DOSHA_HOUSES),
126
+ "dosha_level": "None",
127
+ "cancellation_notes": ["Mars not found in chart"],
128
+ "severity": "None",
129
+ }
130
+
131
+ mars_house = mars.get("house")
132
+ has_dosha = mars_house in MANGAL_DOSHA_HOUSES
133
+ dosha_level = DOSHA_LEVEL_MAP.get(mars_house, "None") if has_dosha else "None"
134
+
135
+ # --- Cancellation conditions ---
136
+ cancellation_notes: list[str] = []
137
+
138
+ mars_rashi = mars.get("rashi", "")
139
+ if mars_rashi in ("Aries", "Scorpio"):
140
+ cancellation_notes.append(
141
+ f"Mars is in its own sign ({mars_rashi}) — Dosha is cancelled."
142
+ )
143
+
144
+ if mars_rashi == "Capricorn":
145
+ cancellation_notes.append("Mars is exalted in Capricorn — Dosha is cancelled.")
146
+
147
+ # Jupiter aspecting Mars: Jupiter aspects house +5 and +9 (and its own house)
148
+ jupiter = _get_planet(planetary_positions, "Jupiter")
149
+ if jupiter is not None:
150
+ jup_house = jupiter.get("house", 0)
151
+ # Jupiter's 5th and 9th aspects (full aspect houses)
152
+ jup_aspects = {
153
+ jup_house,
154
+ (jup_house + 4) % 12 or 12,
155
+ (jup_house + 8) % 12 or 12,
156
+ }
157
+ if mars_house in jup_aspects:
158
+ cancellation_notes.append(
159
+ f"Jupiter (house {jup_house}) aspects Mars — Dosha is mitigated."
160
+ )
161
+
162
+ moon = _get_planet(planetary_positions, "Moon")
163
+ if moon is not None and moon.get("house") == mars_house:
164
+ cancellation_notes.append("Moon is conjunct Mars — Dosha is mitigated.")
165
+
166
+ severity: str
167
+ if cancellation_notes and any("cancelled" in n.lower() for n in cancellation_notes):
168
+ severity = "Cancelled"
169
+ elif not has_dosha:
170
+ severity = "None"
171
+ else:
172
+ severity = dosha_level # "High", "Medium", or "Low"
173
+
174
+ return {
175
+ "has_dosha": has_dosha,
176
+ "mars_house": mars_house,
177
+ "affected_houses": sorted(MANGAL_DOSHA_HOUSES),
178
+ "dosha_level": dosha_level,
179
+ "cancellation_notes": cancellation_notes,
180
+ "severity": severity,
181
+ }
182
+
183
+
184
+ # ---------------------------------------------------------------------------
185
+ # 2. Kaal Sarp Dosha
186
+ # ---------------------------------------------------------------------------
187
+
188
+
189
+ def check_kaal_sarp_dosha(planetary_positions: list) -> dict:
190
+ """Check for Kaal Sarp / Kaal Amrit Yoga.
191
+
192
+ Parameters
193
+ ----------
194
+ planetary_positions : list
195
+ List of planet dicts from build_chart().
196
+
197
+ Returns
198
+ -------
199
+ dict
200
+ has_dosha, dosha_type, rahu_longitude, ketu_longitude,
201
+ planets_inside, planets_outside, name.
202
+ """
203
+ rahu = _get_planet(planetary_positions, "Rahu")
204
+ if rahu is None:
205
+ return {
206
+ "has_dosha": False,
207
+ "dosha_type": "None",
208
+ "rahu_longitude": None,
209
+ "ketu_longitude": None,
210
+ "planets_inside": [],
211
+ "planets_outside": list(MAIN_PLANETS),
212
+ "name": "None",
213
+ }
214
+
215
+ rahu_lon = rahu.get("longitude", 0.0) % 360
216
+ ketu_lon = (rahu_lon + 180) % 360
217
+
218
+ planets_inside: list[str] = []
219
+ planets_outside: list[str] = []
220
+
221
+ for pname in MAIN_PLANETS:
222
+ p = _get_planet(planetary_positions, pname)
223
+ if p is None:
224
+ planets_outside.append(pname)
225
+ continue
226
+ plon = p.get("longitude", 0.0) % 360
227
+ if _lon_between_clockwise(plon, rahu_lon, ketu_lon):
228
+ planets_inside.append(pname)
229
+ else:
230
+ planets_outside.append(pname)
231
+
232
+ all_inside_clockwise = len(planets_outside) == 0
233
+ # Kaal Amrit: all planets in the arc from Ketu → Rahu (counter-clockwise)
234
+ planets_inside_ccw: list[str] = []
235
+ planets_outside_ccw: list[str] = []
236
+ for pname in MAIN_PLANETS:
237
+ p = _get_planet(planetary_positions, pname)
238
+ if p is None:
239
+ planets_outside_ccw.append(pname)
240
+ continue
241
+ plon = p.get("longitude", 0.0) % 360
242
+ if _lon_between_clockwise(plon, ketu_lon, rahu_lon):
243
+ planets_inside_ccw.append(pname)
244
+ else:
245
+ planets_outside_ccw.append(pname)
246
+
247
+ all_inside_ccw = len(planets_outside_ccw) == 0
248
+
249
+ rahu_rashi = _rashi_from_lon(rahu_lon)
250
+ ksd_name = KAAL_SARP_NAMES.get(rahu_rashi, "Unknown")
251
+
252
+ if all_inside_clockwise:
253
+ dosha_type = "Kaal Sarp"
254
+ has_dosha = True
255
+ elif all_inside_ccw:
256
+ dosha_type = "Kaal Amrit"
257
+ has_dosha = True
258
+ planets_inside = planets_inside_ccw
259
+ planets_outside = planets_outside_ccw
260
+ else:
261
+ dosha_type = "None"
262
+ has_dosha = False
263
+
264
+ return {
265
+ "has_dosha": has_dosha,
266
+ "dosha_type": dosha_type,
267
+ "rahu_longitude": round(rahu_lon, 4),
268
+ "ketu_longitude": round(ketu_lon, 4),
269
+ "planets_inside": planets_inside,
270
+ "planets_outside": planets_outside,
271
+ "name": ksd_name if has_dosha else "None",
272
+ }
273
+
274
+
275
+ # ---------------------------------------------------------------------------
276
+ # 3. Sade Sati
277
+ # ---------------------------------------------------------------------------
278
+
279
+
280
+ def check_sade_sati(natal_moon_rashi: str, current_saturn_rashi: str) -> dict:
281
+ """Check whether Saturn's transit triggers Sade Sati for the natal Moon sign.
282
+
283
+ Parameters
284
+ ----------
285
+ natal_moon_rashi : str
286
+ Rashi of Moon at birth (e.g. "Taurus").
287
+ current_saturn_rashi : str
288
+ Rashi Saturn currently occupies (transit chart).
289
+
290
+ Returns
291
+ -------
292
+ dict
293
+ is_sade_sati, phase, natal_moon_rashi, current_saturn_rashi, description.
294
+ """
295
+ if natal_moon_rashi not in RASHI_INDEX or current_saturn_rashi not in RASHI_INDEX:
296
+ return {
297
+ "is_sade_sati": False,
298
+ "phase": "None",
299
+ "natal_moon_rashi": natal_moon_rashi,
300
+ "current_saturn_rashi": current_saturn_rashi,
301
+ "description": "Invalid rashi name provided.",
302
+ }
303
+
304
+ moon_idx = RASHI_INDEX[natal_moon_rashi]
305
+ saturn_idx = RASHI_INDEX[current_saturn_rashi]
306
+
307
+ rising_rashi = RASHIS[(moon_idx - 1) % 12]
308
+ peak_rashi = natal_moon_rashi
309
+ setting_rashi = RASHIS[(moon_idx + 1) % 12]
310
+
311
+ if current_saturn_rashi == rising_rashi:
312
+ phase = "Rising"
313
+ description = (
314
+ f"Saturn is transiting {rising_rashi}, the sign before your natal Moon "
315
+ f"sign ({natal_moon_rashi}). This is the Rising phase of Sade Sati — "
316
+ "challenges begin to build gradually."
317
+ )
318
+ is_sade_sati = True
319
+ elif current_saturn_rashi == peak_rashi:
320
+ phase = "Peak"
321
+ description = (
322
+ f"Saturn is transiting your natal Moon sign ({natal_moon_rashi}). "
323
+ "This is the Peak phase of Sade Sati — the most intense period."
324
+ )
325
+ is_sade_sati = True
326
+ elif current_saturn_rashi == setting_rashi:
327
+ phase = "Setting"
328
+ description = (
329
+ f"Saturn is transiting {setting_rashi}, the sign after your natal Moon "
330
+ f"sign ({natal_moon_rashi}). This is the Setting phase of Sade Sati — "
331
+ "pressures gradually ease."
332
+ )
333
+ is_sade_sati = True
334
+ else:
335
+ phase = "None"
336
+ description = (
337
+ f"Saturn ({current_saturn_rashi}) is not transiting through any of the "
338
+ f"three signs of Sade Sati for Moon in {natal_moon_rashi} "
339
+ f"({rising_rashi}, {peak_rashi}, {setting_rashi})."
340
+ )
341
+ is_sade_sati = False
342
+
343
+ return {
344
+ "is_sade_sati": is_sade_sati,
345
+ "phase": phase,
346
+ "natal_moon_rashi": natal_moon_rashi,
347
+ "current_saturn_rashi": current_saturn_rashi,
348
+ "description": description,
349
+ }
350
+
351
+
352
+ # ---------------------------------------------------------------------------
353
+ # 4. Graha Yuddha (Planetary War)
354
+ # ---------------------------------------------------------------------------
355
+
356
+
357
+ def check_graha_yuddha(planetary_positions: list) -> list:
358
+ """Detect Graha Yuddha (planetary war) between non-luminary planets.
359
+
360
+ Two planets are at war when their longitudes are within 1 degree.
361
+ The planet with the lower longitude wins (or higher latitude, but we use
362
+ longitude as the primary metric since latitude data may be absent).
363
+
364
+ Parameters
365
+ ----------
366
+ planetary_positions : list
367
+ List of planet dicts from build_chart().
368
+
369
+ Returns
370
+ -------
371
+ list of dict
372
+ Each entry: planet1, planet2, longitude_diff, winner, loser.
373
+ """
374
+ wars: list[dict] = []
375
+ war_planets = []
376
+ for pname in WAR_PLANETS:
377
+ p = _get_planet(planetary_positions, pname)
378
+ if p is not None:
379
+ war_planets.append(p)
380
+
381
+ for i in range(len(war_planets)):
382
+ for j in range(i + 1, len(war_planets)):
383
+ p1 = war_planets[i]
384
+ p2 = war_planets[j]
385
+ lon1 = p1.get("longitude", 0.0)
386
+ lon2 = p2.get("longitude", 0.0)
387
+ sep = _angular_sep(lon1, lon2)
388
+ if sep <= 1.0:
389
+ # Winner = lower longitude (closer to 0° of the sign)
390
+ if lon1 <= lon2:
391
+ winner, loser = p1["name"], p2["name"]
392
+ else:
393
+ winner, loser = p2["name"], p1["name"]
394
+ wars.append(
395
+ {
396
+ "planet1": p1["name"],
397
+ "planet2": p2["name"],
398
+ "longitude_diff": round(sep, 4),
399
+ "winner": winner,
400
+ "loser": loser,
401
+ }
402
+ )
403
+
404
+ return wars
405
+
406
+
407
+ # ---------------------------------------------------------------------------
408
+ # 5. Combustion
409
+ # ---------------------------------------------------------------------------
410
+
411
+
412
+ def check_combustion(planetary_positions: list) -> list:
413
+ """Determine which planets are combust (too close to the Sun).
414
+
415
+ Parameters
416
+ ----------
417
+ planetary_positions : list
418
+ List of planet dicts from build_chart().
419
+
420
+ Returns
421
+ -------
422
+ list of dict
423
+ Each entry: planet, sun_longitude, planet_longitude,
424
+ angular_separation, threshold, is_combust.
425
+ """
426
+ sun = _get_planet(planetary_positions, "Sun")
427
+ if sun is None:
428
+ return []
429
+
430
+ sun_lon = sun.get("longitude", 0.0)
431
+ results: list[dict] = []
432
+
433
+ for pname, (thresh_direct, thresh_retro) in COMBUSTION_THRESHOLDS.items():
434
+ p = _get_planet(planetary_positions, pname)
435
+ if p is None:
436
+ continue
437
+ p_lon = p.get("longitude", 0.0)
438
+ is_retro = bool(p.get("is_retrograde", False))
439
+ threshold = thresh_retro if is_retro else thresh_direct
440
+ sep = _angular_sep(sun_lon, p_lon)
441
+
442
+ results.append(
443
+ {
444
+ "planet": pname,
445
+ "sun_longitude": round(sun_lon, 4),
446
+ "planet_longitude": round(p_lon, 4),
447
+ "angular_separation": round(sep, 4),
448
+ "threshold": threshold,
449
+ "is_combust": sep < threshold,
450
+ }
451
+ )
452
+
453
+ return results
@@ -0,0 +1,213 @@
1
+ """Gandanta: Junction points between water and fire signs — highly sensitive degrees."""
2
+
3
+ # ---------------------------------------------------------------------------
4
+ # Gandanta zone definitions
5
+ # ---------------------------------------------------------------------------
6
+ # Gandanta occurs at the three water→fire sign junctions.
7
+ # The sensitive zone extends GANDANTA_DEGREES on each side of the cusp.
8
+
9
+ GANDANTA_DEGREES = 3.333 # 3°20' = 1 nakshatra pada
10
+
11
+ # Each zone stores:
12
+ # from_sign / to_sign — the water and fire sign names
13
+ # water_end — absolute longitude of the water-sign cusp (end)
14
+ # fire_start — absolute longitude of the fire-sign cusp (start)
15
+ # nakshatras — (last nak of water sign, first nak of fire sign)
16
+ # description — brief spiritual significance
17
+ _GANDANTA_ZONES = [
18
+ {
19
+ "from_sign": "Cancer",
20
+ "to_sign": "Leo",
21
+ "water_end": 120.0, # end of Cancer (4 × 30)
22
+ "fire_start": 120.0, # start of Leo (5 × 30 — same cusp)
23
+ "nakshatras": ("Ashlesha", "Magha"),
24
+ "description": "Ashlesha–Magha junction: dissolution of ancestral karma and royalty.",
25
+ },
26
+ {
27
+ "from_sign": "Scorpio",
28
+ "to_sign": "Sagittarius",
29
+ "water_end": 240.0, # end of Scorpio (8 × 30)
30
+ "fire_start": 240.0, # start of Sagittarius (9 × 30)
31
+ "nakshatras": ("Jyeshtha", "Mula"),
32
+ "description": "Jyeshtha–Mula junction: most intense Gandanta; roots torn and replanted.",
33
+ },
34
+ {
35
+ "from_sign": "Pisces",
36
+ "to_sign": "Aries",
37
+ "water_end": 360.0, # end of Pisces (12 × 30 = 360 = 0)
38
+ "fire_start": 0.0, # start of Aries
39
+ "nakshatras": ("Revati", "Ashwini"),
40
+ "description": "Revati–Ashwini junction: end and beginning of the zodiac; liberation and rebirth.",
41
+ },
42
+ ]
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Helpers
46
+ # ---------------------------------------------------------------------------
47
+
48
+
49
+ def _normalise(lon: float) -> float:
50
+ return lon % 360
51
+
52
+
53
+ def _intensity_label(degrees_from_edge: float) -> str:
54
+ """Classify intensity based on distance from the exact junction."""
55
+ if degrees_from_edge < 1.0:
56
+ return "Severe"
57
+ if degrees_from_edge < 2.0:
58
+ return "Moderate"
59
+ return "Mild"
60
+
61
+
62
+ def _check_planet_in_gandanta(name: str, lon: float) -> dict | None:
63
+ """Return a Gandanta entry dict if *lon* falls in any Gandanta zone, else None."""
64
+ lon = _normalise(lon)
65
+ deg_in_sign = lon % 30
66
+
67
+ for zone in _GANDANTA_ZONES:
68
+ cusp = zone["water_end"] # water_end == fire_start (same longitude)
69
+
70
+ # --- Water-sign side: last GANDANTA_DEGREES of the water sign ---
71
+ # The water sign ends at `cusp`; a planet is in the water-side zone when
72
+ # its degree within its sign is > (30 - GANDANTA_DEGREES).
73
+ water_zone_start = cusp - GANDANTA_DEGREES # absolute
74
+ # Handle the Pisces→Aries wrap-around
75
+ if cusp == 360.0 or cusp == 0.0:
76
+ # water sign is Pisces; zone is 356.667° – 360°
77
+ in_water = 356.667 <= lon < 360.0
78
+ dist_water = 360.0 - lon if in_water else None
79
+ else:
80
+ in_water = water_zone_start <= lon < cusp
81
+ dist_water = cusp - lon if in_water else None
82
+
83
+ # --- Fire-sign side: first GANDANTA_DEGREES of the fire sign ---
84
+ if cusp == 360.0 or cusp == 0.0:
85
+ # fire sign is Aries; zone is 0° – 3.333°
86
+ fire_zone_end = GANDANTA_DEGREES
87
+ in_fire = 0.0 <= lon < fire_zone_end
88
+ dist_fire = lon if in_fire else None
89
+ else:
90
+ fire_zone_end = cusp + GANDANTA_DEGREES
91
+ in_fire = cusp <= lon < fire_zone_end
92
+ dist_fire = lon - cusp if in_fire else None
93
+
94
+ if in_water:
95
+ dist = dist_water
96
+ return {
97
+ "planet": name,
98
+ "longitude": round(lon, 4),
99
+ "rashi": zone["from_sign"],
100
+ "degree_in_sign": round(deg_in_sign, 4),
101
+ "gandanta_zone": f"{zone['from_sign']}–{zone['to_sign']} junction",
102
+ "in_water_side": True,
103
+ "in_fire_side": False,
104
+ "intensity": _intensity_label(dist),
105
+ "nakshatras": zone["nakshatras"],
106
+ "significance": zone["description"],
107
+ }
108
+
109
+ if in_fire:
110
+ dist = dist_fire
111
+ # For fire side, rashi is the fire sign
112
+ rashis = [
113
+ "Aries",
114
+ "Taurus",
115
+ "Gemini",
116
+ "Cancer",
117
+ "Leo",
118
+ "Virgo",
119
+ "Libra",
120
+ "Scorpio",
121
+ "Sagittarius",
122
+ "Capricorn",
123
+ "Aquarius",
124
+ "Pisces",
125
+ ]
126
+ rashi = rashis[int(lon / 30) % 12]
127
+ return {
128
+ "planet": name,
129
+ "longitude": round(lon, 4),
130
+ "rashi": rashi,
131
+ "degree_in_sign": round(deg_in_sign, 4),
132
+ "gandanta_zone": f"{zone['from_sign']}–{zone['to_sign']} junction",
133
+ "in_water_side": False,
134
+ "in_fire_side": True,
135
+ "intensity": _intensity_label(dist),
136
+ "nakshatras": zone["nakshatras"],
137
+ "significance": zone["description"],
138
+ }
139
+
140
+ return None
141
+
142
+
143
+ # ---------------------------------------------------------------------------
144
+ # Public API
145
+ # ---------------------------------------------------------------------------
146
+
147
+
148
+ def check_gandanta(planetary_positions: list, ascendant: dict = None) -> list:
149
+ """Check which planets (and optionally the ascendant) fall in Gandanta zones.
150
+
151
+ Parameters
152
+ ----------
153
+ planetary_positions : list
154
+ Each item is a planet dict with at least ``name`` and ``longitude``.
155
+ ascendant : dict, optional
156
+ Ascendant dict with at least ``longitude``. When supplied it is also
157
+ checked; it appears in the output as ``name="Ascendant"``.
158
+
159
+ Returns
160
+ -------
161
+ list of dict
162
+ One entry per planet/ascendant that lies in a Gandanta zone.
163
+ Empty list if none are in Gandanta.
164
+ """
165
+ results = []
166
+
167
+ for planet in planetary_positions:
168
+ entry = _check_planet_in_gandanta(planet["name"], planet["longitude"])
169
+ if entry:
170
+ results.append(entry)
171
+
172
+ if ascendant is not None:
173
+ entry = _check_planet_in_gandanta("Ascendant", ascendant["longitude"])
174
+ if entry:
175
+ results.append(entry)
176
+
177
+ return results
178
+
179
+
180
+ def get_gandanta_info() -> dict:
181
+ """Return educational information about all three Gandanta zones.
182
+
183
+ Returns
184
+ -------
185
+ dict
186
+ Keyed by ``"<WaterSign>-<FireSign>"``, each value holds zone metadata
187
+ including the boundary nakshatras, degrees affected, and significance.
188
+ """
189
+ info = {}
190
+ for zone in _GANDANTA_ZONES:
191
+ key = f"{zone['from_sign']}-{zone['to_sign']}"
192
+ info[key] = {
193
+ "water_sign": zone["from_sign"],
194
+ "fire_sign": zone["to_sign"],
195
+ "junction_longitude": zone["water_end"] % 360,
196
+ "affected_range": f"Last {GANDANTA_DEGREES}° of {zone['from_sign']} "
197
+ f"and first {GANDANTA_DEGREES}° of {zone['to_sign']}",
198
+ "boundary_nakshatras": zone["nakshatras"],
199
+ "last_nakshatra_water": zone["nakshatras"][0],
200
+ "first_nakshatra_fire": zone["nakshatras"][1],
201
+ "significance": zone["description"],
202
+ "intensity_guide": {
203
+ "Severe": "< 1° from junction cusp — highly problematic; remedies essential",
204
+ "Moderate": "1°–2° from junction cusp — notable challenges",
205
+ "Mild": "2°–3°20' from junction cusp — some instability; monitor",
206
+ },
207
+ "general_note": (
208
+ "Planets in Gandanta operate in a zone of karmic dissolution. "
209
+ "The native may face deep-rooted instability in the significations of "
210
+ "that planet unless strong remedies are undertaken."
211
+ ),
212
+ }
213
+ return info