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,319 @@
1
+ """Special planetary conditions: Vargottama, Jaimini Karakas, Dig Bala, Pushkara Navamsha."""
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
+ # Jaimini Chara Karaka planets (Rahu included as 8th)
23
+ KARAKA_PLANETS = [
24
+ "Sun",
25
+ "Moon",
26
+ "Mars",
27
+ "Mercury",
28
+ "Jupiter",
29
+ "Venus",
30
+ "Saturn",
31
+ "Rahu",
32
+ ]
33
+
34
+ KARAKA_NAMES = [
35
+ "Atmakaraka", # AK — highest degree
36
+ "Amatyakaraka", # AmK
37
+ "Bhratrukaraka", # BK
38
+ "Matrukaraka", # MK
39
+ "Putrakaraka", # PK
40
+ "Gnatikaraka", # GK
41
+ "Darakaraka", # DK — lowest degree (7th)
42
+ ]
43
+
44
+ # Dig Bala: planet → best house (1-based)
45
+ DIG_BALA_BEST = {
46
+ "Sun": 10,
47
+ "Mars": 10,
48
+ "Moon": 4,
49
+ "Venus": 4,
50
+ "Mercury": 1,
51
+ "Jupiter": 1,
52
+ "Saturn": 7,
53
+ }
54
+
55
+ KENDRA_HOUSES = {1, 4, 7, 10}
56
+
57
+ # Pushkara Navamsha degree ranges per rashi (inclusive lower, exclusive upper)
58
+ # Source: classical texts — the benefic Navamsha within each sign
59
+ PUSHKARA_RANGES: dict[str, tuple[float, float]] = {
60
+ "Aries": (21.0, 23.2),
61
+ "Taurus": (10.0, 13.3),
62
+ "Gemini": (28.0, 30.0),
63
+ "Cancer": (25.0, 27.2),
64
+ "Leo": (22.0, 24.4),
65
+ "Virgo": (19.0, 21.7),
66
+ "Libra": (16.0, 18.9),
67
+ "Scorpio": (13.0, 16.0),
68
+ "Sagittarius": (10.0, 13.3),
69
+ "Capricorn": (7.0, 10.6),
70
+ "Aquarius": (4.0, 7.8),
71
+ "Pisces": (1.0, 4.4),
72
+ }
73
+
74
+
75
+ # ---------------------------------------------------------------------------
76
+ # Helpers
77
+ # ---------------------------------------------------------------------------
78
+
79
+
80
+ def _get_planet(positions: list, name: str) -> dict | None:
81
+ for p in positions:
82
+ if p.get("name", "").lower() == name.lower():
83
+ return p
84
+ return None
85
+
86
+
87
+ def _degree_in_sign(longitude: float) -> float:
88
+ """Return the degrees elapsed within the current sign (0-30)."""
89
+ return longitude % 30
90
+
91
+
92
+ def _rashi_from_lon(lon: float) -> str:
93
+ return RASHIS[int(lon % 360 / 30)]
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # 1. Vargottama
98
+ # ---------------------------------------------------------------------------
99
+
100
+
101
+ def check_vargottama(planetary_positions: list, d9_positions: list) -> list:
102
+ """Identify Vargottama planets (same rashi in D1 and D9).
103
+
104
+ Parameters
105
+ ----------
106
+ planetary_positions : list
107
+ D1 (natal/rashi) planet dicts from build_chart().
108
+ d9_positions : list
109
+ D9 (Navamsa) planet dicts. Must contain at least ``name`` and
110
+ ``rashi`` (or ``longitude``) fields.
111
+
112
+ Returns
113
+ -------
114
+ list of dict
115
+ Each entry: planet, d1_rashi, d9_rashi, is_vargottama.
116
+ """
117
+ results: list[dict] = []
118
+ all_planet_names = {p["name"] for p in planetary_positions if "name" in p}
119
+
120
+ for pname in all_planet_names:
121
+ d1 = _get_planet(planetary_positions, pname)
122
+ d9 = _get_planet(d9_positions, pname)
123
+
124
+ d1_rashi = d1.get("rashi") if d1 else None
125
+ if d9 is not None:
126
+ d9_rashi = d9.get("rashi") or (
127
+ _rashi_from_lon(d9["longitude"]) if "longitude" in d9 else None
128
+ )
129
+ else:
130
+ d9_rashi = None
131
+
132
+ is_vargottama = (
133
+ d1_rashi is not None and d9_rashi is not None and d1_rashi == d9_rashi
134
+ )
135
+
136
+ results.append(
137
+ {
138
+ "planet": pname,
139
+ "d1_rashi": d1_rashi,
140
+ "d9_rashi": d9_rashi,
141
+ "is_vargottama": is_vargottama,
142
+ }
143
+ )
144
+
145
+ # Stable ordering
146
+ results.sort(key=lambda x: x["planet"])
147
+ return results
148
+
149
+
150
+ # ---------------------------------------------------------------------------
151
+ # 2. Jaimini Chara Karakas
152
+ # ---------------------------------------------------------------------------
153
+
154
+
155
+ def get_jaimini_karakas(planetary_positions: list) -> dict:
156
+ """Calculate the 7 Jaimini Chara Karakas from 8 significator planets.
157
+
158
+ Rahu's degree is reversed: effective_degree = 30 - degree_in_sign.
159
+ All 8 planets are sorted by effective degree (descending); the top 7
160
+ receive Karaka titles from Atmakaraka down to Darakaraka.
161
+
162
+ Parameters
163
+ ----------
164
+ planetary_positions : list
165
+ List of planet dicts from build_chart().
166
+
167
+ Returns
168
+ -------
169
+ dict
170
+ Keys are Karaka names (Atmakaraka … Darakaraka); values are dicts
171
+ with ``planet``, ``degree_in_sign``, ``rashi``.
172
+ """
173
+ planet_data: list[tuple[float, str, float, str]] = []
174
+ # (effective_degree, name, raw_degree_in_sign, rashi)
175
+
176
+ for pname in KARAKA_PLANETS:
177
+ p = _get_planet(planetary_positions, pname)
178
+ if p is None:
179
+ continue
180
+
181
+ raw_deg = p.get("degree", None)
182
+ if raw_deg is None:
183
+ lon = p.get("longitude", 0.0)
184
+ raw_deg = _degree_in_sign(lon)
185
+
186
+ rashi = p.get("rashi") or _rashi_from_lon(p.get("longitude", 0.0))
187
+
188
+ if pname == "Rahu":
189
+ effective = 30.0 - (raw_deg % 30)
190
+ else:
191
+ effective = raw_deg % 30
192
+
193
+ planet_data.append((effective, pname, raw_deg % 30, rashi))
194
+
195
+ # Sort descending by effective degree; secondary sort by name for stability
196
+ planet_data.sort(key=lambda x: (-x[0], x[1]))
197
+
198
+ result: dict = {}
199
+ for rank, karaka_name in enumerate(KARAKA_NAMES):
200
+ if rank < len(planet_data):
201
+ eff, pname, raw_deg, rashi = planet_data[rank]
202
+ result[karaka_name] = {
203
+ "planet": pname,
204
+ "degree_in_sign": round(raw_deg, 4),
205
+ "rashi": rashi,
206
+ }
207
+ else:
208
+ result[karaka_name] = None
209
+
210
+ return result
211
+
212
+
213
+ # ---------------------------------------------------------------------------
214
+ # 3. Dig Bala
215
+ # ---------------------------------------------------------------------------
216
+
217
+
218
+ def get_dig_bala(planetary_positions: list) -> list:
219
+ """Assess directional strength (Dig Bala) for each planet.
220
+
221
+ Parameters
222
+ ----------
223
+ planetary_positions : list
224
+ List of planet dicts from build_chart().
225
+
226
+ Returns
227
+ -------
228
+ list of dict
229
+ Each entry: planet, house, best_house, dig_bala_strength.
230
+ Strength: "Full" (in best house), "Half" (any other kendra), "Weak" (other).
231
+ """
232
+ results: list[dict] = []
233
+
234
+ for pname, best_house in DIG_BALA_BEST.items():
235
+ p = _get_planet(planetary_positions, pname)
236
+ if p is None:
237
+ continue
238
+
239
+ house = p.get("house")
240
+ if house is None:
241
+ strength = "Unknown"
242
+ elif house == best_house:
243
+ strength = "Full"
244
+ elif house in KENDRA_HOUSES:
245
+ strength = "Half"
246
+ else:
247
+ strength = "Weak"
248
+
249
+ results.append(
250
+ {
251
+ "planet": pname,
252
+ "house": house,
253
+ "best_house": best_house,
254
+ "dig_bala_strength": strength,
255
+ }
256
+ )
257
+
258
+ return results
259
+
260
+
261
+ # ---------------------------------------------------------------------------
262
+ # 4. Pushkara Navamsha
263
+ # ---------------------------------------------------------------------------
264
+
265
+
266
+ def check_pushkara_navamsha(planetary_positions: list) -> list:
267
+ """Identify planets occupying a Pushkara Navamsha degree.
268
+
269
+ Parameters
270
+ ----------
271
+ planetary_positions : list
272
+ List of planet dicts from build_chart().
273
+
274
+ Returns
275
+ -------
276
+ list of dict
277
+ Each entry: planet, rashi, degree_in_sign, is_pushkara.
278
+ """
279
+ results: list[dict] = []
280
+
281
+ for p in planetary_positions:
282
+ pname = p.get("name", "Unknown")
283
+ rashi = p.get("rashi")
284
+
285
+ lon = p.get("longitude", None)
286
+ raw_deg = p.get("degree", None)
287
+
288
+ if raw_deg is None and lon is not None:
289
+ raw_deg = _degree_in_sign(float(lon))
290
+ elif raw_deg is not None:
291
+ raw_deg = float(raw_deg) % 30 # ensure within sign
292
+
293
+ if rashi is None and lon is not None:
294
+ rashi = _rashi_from_lon(float(lon))
295
+
296
+ if rashi is None or raw_deg is None:
297
+ results.append(
298
+ {
299
+ "planet": pname,
300
+ "rashi": rashi,
301
+ "degree_in_sign": raw_deg,
302
+ "is_pushkara": False,
303
+ }
304
+ )
305
+ continue
306
+
307
+ lo, hi = PUSHKARA_RANGES.get(rashi, (0.0, 0.0))
308
+ is_pushkara = lo <= raw_deg < hi
309
+
310
+ results.append(
311
+ {
312
+ "planet": pname,
313
+ "rashi": rashi,
314
+ "degree_in_sign": round(raw_deg, 4),
315
+ "is_pushkara": is_pushkara,
316
+ }
317
+ )
318
+
319
+ return results
@@ -0,0 +1,232 @@
1
+ """Sudarshana Chakra: Triple chart analysis from Lagna, Moon, and Sun simultaneously."""
2
+
3
+ from datetime import datetime
4
+
5
+ from kundali_lib.vedic.constants import RASHIS
6
+
7
+ # Houses considered auspicious / inauspicious in classical Vedic astrology
8
+ _TRIKONA = [1, 5, 9] # Trine houses — dharma, prosperity
9
+ _KENDRA = [1, 4, 7, 10] # Angular houses — strength, visibility
10
+ _DUSTHANA = [6, 8, 12] # Dusthana — suffering, obstacles, loss
11
+ _UPACHAYA = [3, 6, 10, 11] # Growth houses
12
+
13
+
14
+ def _house_from_reference(planet_rashi: str, reference_rashi: str) -> int:
15
+ """Return the 1-based house number of planet_rashi counted from reference_rashi."""
16
+ ref_i = RASHIS.index(reference_rashi)
17
+ pla_i = RASHIS.index(planet_rashi)
18
+ return (pla_i - ref_i) % 12 + 1
19
+
20
+
21
+ def _build_view(
22
+ reference_label: str,
23
+ reference_rashi: str,
24
+ planetary_positions: list[dict],
25
+ ) -> dict:
26
+ """Build one of the three Sudarshana views (Lagna, Chandra, or Surya).
27
+
28
+ Args:
29
+ reference_label: Human-readable label, e.g. ``"Lagna in Leo"``.
30
+ reference_rashi: The sign that acts as House 1 for this view.
31
+ planetary_positions: From ``base_chart["planetary_positions"]``.
32
+ """
33
+ positions: list[dict] = []
34
+ for p in planetary_positions:
35
+ house = _house_from_reference(p["rashi"], reference_rashi)
36
+ positions.append(
37
+ {
38
+ "planet": p["name"],
39
+ "rashi": p["rashi"],
40
+ "house_from_reference": house,
41
+ }
42
+ )
43
+
44
+ return {
45
+ "reference": reference_label,
46
+ "reference_rashi": reference_rashi,
47
+ "planetary_positions": positions,
48
+ "key_houses": {
49
+ "trikona": _TRIKONA,
50
+ "kendra": _KENDRA,
51
+ "dusthana": _DUSTHANA,
52
+ },
53
+ }
54
+
55
+
56
+ def _score_house(house: int) -> int:
57
+ """Simple scoring: trikona/kendra positive, dusthana negative, rest neutral."""
58
+ if house in _TRIKONA or house in _KENDRA:
59
+ return 1
60
+ if house in _DUSTHANA:
61
+ return -1
62
+ return 0
63
+
64
+
65
+ def _consolidated_analysis(
66
+ lagna_view: dict,
67
+ chandra_view: dict,
68
+ surya_view: dict,
69
+ ) -> dict:
70
+ """Derive cross-view insights from all three Sudarshana perspectives."""
71
+
72
+ # Build per-house strength score across all three views
73
+ house_scores: dict[int, int] = {h: 0 for h in range(1, 13)}
74
+
75
+ for view in (lagna_view, chandra_view, surya_view):
76
+ for pos in view["planetary_positions"]:
77
+ h = pos["house_from_reference"]
78
+ house_scores[h] += _score_house(h)
79
+
80
+ strongest_house = max(house_scores, key=lambda h: house_scores[h])
81
+ weakest_house = min(house_scores, key=lambda h: house_scores[h])
82
+
83
+ # Per-planet consistency check
84
+ planet_scores: dict[str, int] = {}
85
+ for view in (lagna_view, chandra_view, surya_view):
86
+ for pos in view["planetary_positions"]:
87
+ pname = pos["planet"]
88
+ score = _score_house(pos["house_from_reference"])
89
+ planet_scores[pname] = planet_scores.get(pname, 0) + score
90
+
91
+ # Consistently well-placed: positive score in all 3 views (score == 3)
92
+ well_placed = [p for p, s in planet_scores.items() if s == 3]
93
+ # Consistently afflicted: negative score in all 3 views (score == -3)
94
+ afflicted = [p for p, s in planet_scores.items() if s == -3]
95
+
96
+ if well_placed and not afflicted:
97
+ summary = (
98
+ f"The Sudarshana Chakra shows strong harmony. "
99
+ f"{', '.join(well_placed)} "
100
+ f"{'are' if len(well_placed) > 1 else 'is'} well-placed across all three "
101
+ f"perspectives (Lagna, Moon, Sun), indicating consistent support in the "
102
+ f"significations of house {strongest_house}."
103
+ )
104
+ elif afflicted and not well_placed:
105
+ summary = (
106
+ f"Significant challenges indicated. "
107
+ f"{', '.join(afflicted)} "
108
+ f"{'are' if len(afflicted) > 1 else 'is'} afflicted across all three "
109
+ f"perspectives, requiring careful attention and remedial measures."
110
+ )
111
+ elif well_placed and afflicted:
112
+ summary = (
113
+ f"Mixed Sudarshana pattern. {', '.join(well_placed)} consistently strong; "
114
+ f"{', '.join(afflicted)} consistently under stress. Balance strengths against "
115
+ f"weaknesses in decision-making."
116
+ )
117
+ else:
118
+ summary = (
119
+ "Balanced Sudarshana Chakra — no planet dominates strongly or weakly across "
120
+ "all three views. Results depend on active dashas and Gochara."
121
+ )
122
+
123
+ return {
124
+ "strongest_house": strongest_house,
125
+ "weakest_house": weakest_house,
126
+ "planets_consistently_well_placed": well_placed,
127
+ "planets_consistently_afflicted": afflicted,
128
+ "summary": summary,
129
+ }
130
+
131
+
132
+ def _sudarshana_year_analysis(
133
+ birth_year: int,
134
+ current_year: int,
135
+ lagna_view: dict,
136
+ chandra_view: dict,
137
+ surya_view: dict,
138
+ ) -> dict:
139
+ """Compute the Sudarshana year and activated houses for each view.
140
+
141
+ The Sudarshana year cycles through the 12 houses annually.
142
+ Year 1 of life → House 1, Year 2 → House 2, etc. (cycles every 12 years).
143
+ """
144
+ age = current_year - birth_year
145
+ if age < 0:
146
+ age = 0
147
+ year_house = (age % 12) + 1 # 1-based
148
+
149
+ def _planets_in_house(view: dict, house: int) -> list[str]:
150
+ return [
151
+ pos["planet"]
152
+ for pos in view["planetary_positions"]
153
+ if pos["house_from_reference"] == house
154
+ ]
155
+
156
+ return {
157
+ "age": age,
158
+ "sudarshana_year_number": age + 1,
159
+ "activated_house": year_house,
160
+ "lagna_view_activated_planets": _planets_in_house(lagna_view, year_house),
161
+ "chandra_view_activated_planets": _planets_in_house(chandra_view, year_house),
162
+ "surya_view_activated_planets": _planets_in_house(surya_view, year_house),
163
+ "note": (
164
+ f"Currently in Sudarshana year {age + 1} (House {year_house} activated). "
165
+ "The themes of this house are emphasised across all three chart perspectives."
166
+ ),
167
+ }
168
+
169
+
170
+ def get_sudarshana_chakra(
171
+ base_chart: dict,
172
+ current_year: int | None = None,
173
+ ) -> dict:
174
+ """Generate the full Sudarshana Chakra analysis.
175
+
176
+ Args:
177
+ base_chart: Output of ``build_chart()``. Must contain
178
+ ``planetary_positions``, ``ascendant``, ``moon_sign``,
179
+ ``sun_sign``, and ``birth_details``.
180
+ current_year: The Gregorian year to use for the Sudarshana year
181
+ calculation. Defaults to the current year if omitted.
182
+
183
+ Returns:
184
+ A dict with ``lagna_view``, ``chandra_view``, ``surya_view``,
185
+ ``consolidated_analysis``, and ``sudarshana_year``.
186
+ """
187
+ if current_year is None:
188
+ current_year = datetime.utcnow().year
189
+
190
+ planetary_positions: list[dict] = base_chart["planetary_positions"]
191
+ lagna_rashi: str = base_chart["ascendant"]["rashi"]
192
+ moon_rashi: str = base_chart["moon_sign"]
193
+ sun_rashi: str = base_chart["sun_sign"]
194
+
195
+ # --- Three Views ---
196
+ lagna_view = _build_view(
197
+ reference_label=f"Lagna in {lagna_rashi}",
198
+ reference_rashi=lagna_rashi,
199
+ planetary_positions=planetary_positions,
200
+ )
201
+ chandra_view = _build_view(
202
+ reference_label=f"Chandra (Moon) in {moon_rashi}",
203
+ reference_rashi=moon_rashi,
204
+ planetary_positions=planetary_positions,
205
+ )
206
+ surya_view = _build_view(
207
+ reference_label=f"Surya (Sun) in {sun_rashi}",
208
+ reference_rashi=sun_rashi,
209
+ planetary_positions=planetary_positions,
210
+ )
211
+
212
+ # --- Consolidated Analysis ---
213
+ consolidated = _consolidated_analysis(lagna_view, chandra_view, surya_view)
214
+
215
+ # --- Sudarshana Year ---
216
+ birth_year_str = base_chart.get("birth_details", {}).get("datetime", "")
217
+ try:
218
+ birth_year = int(birth_year_str[:4])
219
+ except (ValueError, TypeError):
220
+ birth_year = current_year
221
+
222
+ sudarshana_year = _sudarshana_year_analysis(
223
+ birth_year, current_year, lagna_view, chandra_view, surya_view
224
+ )
225
+
226
+ return {
227
+ "lagna_view": lagna_view,
228
+ "chandra_view": chandra_view,
229
+ "surya_view": surya_view,
230
+ "consolidated_analysis": consolidated,
231
+ "sudarshana_year": sudarshana_year,
232
+ }