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,500 @@
1
+ """Shadbala: six-fold planetary strength calculation."""
2
+
3
+ import math
4
+
5
+ # ---------------------------------------------------------------------------
6
+ # Constant tables
7
+ # ---------------------------------------------------------------------------
8
+
9
+ PLANETS = ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn"]
10
+
11
+ # Absolute sidereal longitudes for exaltation / debilitation
12
+ EXALTATION_DEG = {
13
+ "Sun": 10,
14
+ "Moon": 33,
15
+ "Mars": 298,
16
+ "Mercury": 165,
17
+ "Jupiter": 95,
18
+ "Venus": 357,
19
+ "Saturn": 200,
20
+ }
21
+ DEBILITATION_DEG = {
22
+ "Sun": 190,
23
+ "Moon": 213,
24
+ "Mars": 118,
25
+ "Mercury": 345,
26
+ "Jupiter": 275,
27
+ "Venus": 177,
28
+ "Saturn": 20,
29
+ }
30
+
31
+ OWN_SIGNS = {
32
+ "Sun": ["Leo"],
33
+ "Moon": ["Cancer"],
34
+ "Mars": ["Aries", "Scorpio"],
35
+ "Mercury": ["Gemini", "Virgo"],
36
+ "Jupiter": ["Sagittarius", "Pisces"],
37
+ "Venus": ["Taurus", "Libra"],
38
+ "Saturn": ["Capricorn", "Aquarius"],
39
+ }
40
+
41
+ MOOLATRIKONA = {
42
+ "Sun": "Leo",
43
+ "Moon": "Taurus",
44
+ "Mars": "Aries",
45
+ "Mercury": "Virgo",
46
+ "Jupiter": "Sagittarius",
47
+ "Venus": "Libra",
48
+ "Saturn": "Aquarius",
49
+ }
50
+
51
+ # Rashi index → name
52
+ RASHIS = [
53
+ "Aries",
54
+ "Taurus",
55
+ "Gemini",
56
+ "Cancer",
57
+ "Leo",
58
+ "Virgo",
59
+ "Libra",
60
+ "Scorpio",
61
+ "Sagittarius",
62
+ "Capricorn",
63
+ "Aquarius",
64
+ "Pisces",
65
+ ]
66
+
67
+ # Navamsha sign lookup: given absolute longitude, return D9 rashi name
68
+ # Each rashi (30°) is divided into 9 navamshas of 3°20' each.
69
+ # The 108 navamshas cycle through the 12 rashis starting from Aries for
70
+ # fire signs, Cancer for earth, Libra for air, Capricorn for water.
71
+ _NAVAMSHA_START = {
72
+ 0: 0,
73
+ 1: 9,
74
+ 2: 6,
75
+ 3: 3,
76
+ 4: 0,
77
+ 5: 9,
78
+ 6: 6,
79
+ 7: 3,
80
+ 8: 0,
81
+ 9: 9,
82
+ 10: 6,
83
+ 11: 3,
84
+ }
85
+
86
+
87
+ def _navamsha_rashi(lon: float) -> str:
88
+ """Return navamsha (D9) sign for a given sidereal longitude."""
89
+ lon = lon % 360
90
+ rashi_idx = int(lon / 30)
91
+ degree_in_sign = lon - rashi_idx * 30
92
+ navamsha_num = int(degree_in_sign / (10 / 3)) # 0-8
93
+ navamsha_num = min(navamsha_num, 8)
94
+ start = _NAVAMSHA_START[rashi_idx]
95
+ d9_rashi_idx = (start + navamsha_num) % 12
96
+ return RASHIS[d9_rashi_idx]
97
+
98
+
99
+ # Planetary relationships (for saptavargaja — simplified to friend/enemy/neutral)
100
+ FRIENDS = {
101
+ "Sun": ["Moon", "Mars", "Jupiter"],
102
+ "Moon": ["Sun", "Mercury"],
103
+ "Mars": ["Sun", "Moon", "Jupiter"],
104
+ "Mercury": ["Sun", "Venus"],
105
+ "Jupiter": ["Sun", "Moon", "Mars"],
106
+ "Venus": ["Mercury", "Saturn"],
107
+ "Saturn": ["Mercury", "Venus"],
108
+ }
109
+ ENEMIES = {
110
+ "Sun": ["Venus", "Saturn"],
111
+ "Moon": ["None"],
112
+ "Mars": ["Mercury"],
113
+ "Mercury": ["Moon"],
114
+ "Jupiter": ["Mercury", "Venus"],
115
+ "Venus": ["Sun", "Moon"],
116
+ "Saturn": ["Sun", "Moon", "Mars"],
117
+ }
118
+
119
+ # Sign rulerships (lord → sign index list)
120
+ _SIGN_LORDS = {
121
+ "Aries": "Mars",
122
+ "Taurus": "Venus",
123
+ "Gemini": "Mercury",
124
+ "Cancer": "Moon",
125
+ "Leo": "Sun",
126
+ "Virgo": "Mercury",
127
+ "Libra": "Venus",
128
+ "Scorpio": "Mars",
129
+ "Sagittarius": "Jupiter",
130
+ "Capricorn": "Saturn",
131
+ "Aquarius": "Saturn",
132
+ "Pisces": "Jupiter",
133
+ }
134
+
135
+ # Exaltation sign names
136
+ _EXALT_SIGN = {p: RASHIS[int(EXALTATION_DEG[p] / 30)] for p in PLANETS}
137
+
138
+ # Best house for Dig Bala
139
+ DIG_BALA_BEST_HOUSE = {
140
+ "Sun": 10,
141
+ "Moon": 4,
142
+ "Mars": 10,
143
+ "Mercury": 1,
144
+ "Jupiter": 1,
145
+ "Venus": 4,
146
+ "Saturn": 7,
147
+ }
148
+
149
+ # Natural (Naisargika) Bala in Shashtiamsas
150
+ NAISARGIKA_BALA = {
151
+ "Sun": 60.0,
152
+ "Moon": 51.43,
153
+ "Mars": 17.14,
154
+ "Mercury": 25.71,
155
+ "Jupiter": 34.29,
156
+ "Venus": 42.86,
157
+ "Saturn": 8.57,
158
+ }
159
+
160
+ # Mean daily motion in degrees
161
+ MEAN_MOTION = {
162
+ "Sun": 1.0,
163
+ "Moon": 13.0,
164
+ "Mars": 0.524,
165
+ "Mercury": 1.383,
166
+ "Jupiter": 0.083,
167
+ "Venus": 1.2,
168
+ "Saturn": 0.033,
169
+ }
170
+
171
+ # Required Rupas (minimum for "Strong" classification)
172
+ REQUIRED_RUPAS = {
173
+ "Sun": 6.5,
174
+ "Moon": 6.0,
175
+ "Mars": 5.0,
176
+ "Mercury": 7.0,
177
+ "Jupiter": 6.5,
178
+ "Venus": 5.5,
179
+ "Saturn": 5.0,
180
+ }
181
+
182
+ # Natural benefics / malefics
183
+ NATURAL_BENEFICS = {"Moon", "Mercury", "Jupiter", "Venus"}
184
+ NATURAL_MALEFICS = {"Sun", "Mars", "Saturn", "Rahu", "Ketu"}
185
+
186
+ # ---------------------------------------------------------------------------
187
+ # Sthana Bala components
188
+ # ---------------------------------------------------------------------------
189
+
190
+
191
+ def _uchcha_bala(planet: str, lon: float) -> float:
192
+ """Exaltation strength (0-60 Shashtiamsas)."""
193
+ debil = DEBILITATION_DEG[planet]
194
+ diff = abs(lon - debil) % 360
195
+ if diff > 180:
196
+ diff = 360 - diff
197
+ return min(60.0, diff / 3.0)
198
+
199
+
200
+ def _saptavargaja_bala(planet: str, lon: float) -> float:
201
+ """
202
+ Simplified Saptavargaja Bala using D1 and D9 only.
203
+ Returns combined score (max ~40 per chart = 80 total).
204
+ """
205
+ total = 0.0
206
+ for rashi in [RASHIS[int(lon / 30) % 12], _navamsha_rashi(lon)]:
207
+ lord = _SIGN_LORDS.get(rashi, "")
208
+ exalt_sign = _EXALT_SIGN.get(planet, "")
209
+ if rashi == exalt_sign:
210
+ total += 20.0
211
+ elif rashi in OWN_SIGNS.get(planet, []):
212
+ total += 15.0
213
+ elif rashi == MOOLATRIKONA.get(planet, ""):
214
+ total += 15.0
215
+ elif lord in FRIENDS.get(planet, []):
216
+ total += 10.0
217
+ elif lord in ENEMIES.get(planet, []):
218
+ total += 3.75
219
+ else:
220
+ total += 7.5
221
+ return total
222
+
223
+
224
+ def _ojhayugma_bala(planet: str, lon: float) -> float:
225
+ """Odd/Even sign strength (0 or 15 Shashtiamsas)."""
226
+ rashi_idx = int(lon / 30) % 12
227
+ is_odd = rashi_idx % 2 == 0 # Aries=0 is odd, Taurus=1 is even, etc.
228
+ male_planets = {"Sun", "Mars", "Jupiter"}
229
+ female_planets = {"Moon", "Venus"}
230
+ if planet in male_planets and is_odd:
231
+ return 15.0
232
+ if planet in female_planets and not is_odd:
233
+ return 15.0
234
+ if planet == "Saturn":
235
+ return 15.0 # simplified
236
+ return 0.0
237
+
238
+
239
+ def _kendradi_bala(house: int) -> float:
240
+ """Angular/succedent/cadent house strength."""
241
+ if house in (1, 4, 7, 10):
242
+ return 60.0
243
+ if house in (2, 5, 8, 11):
244
+ return 30.0
245
+ return 15.0
246
+
247
+
248
+ def _drekkana_bala(planet: str, lon: float) -> float:
249
+ """Decanate (drekkana) strength (0 or 15 Shashtiamsas)."""
250
+ degree_in_sign = lon % 30
251
+ male_planets = {"Sun", "Mars", "Jupiter"}
252
+ female_planets = {"Moon", "Venus"}
253
+ if planet in male_planets and degree_in_sign < 10:
254
+ return 15.0
255
+ if planet in female_planets and degree_in_sign >= 20:
256
+ return 15.0
257
+ if planet == "Mercury" and 10 <= degree_in_sign < 20:
258
+ return 15.0
259
+ return 0.0
260
+
261
+
262
+ def _sthana_bala(planet: str, lon: float, house: int) -> float:
263
+ """Total positional strength."""
264
+ return (
265
+ _uchcha_bala(planet, lon)
266
+ + _saptavargaja_bala(planet, lon)
267
+ + _ojhayugma_bala(planet, lon)
268
+ + _kendradi_bala(house)
269
+ + _drekkana_bala(planet, lon)
270
+ )
271
+
272
+
273
+ # ---------------------------------------------------------------------------
274
+ # Dig Bala
275
+ # ---------------------------------------------------------------------------
276
+
277
+
278
+ def _dig_bala(planet: str, house: int) -> float:
279
+ """Directional strength (0-60 Shashtiamsas)."""
280
+ best = DIG_BALA_BEST_HOUSE[planet]
281
+ # Minimum circular distance in houses (1-12 circle)
282
+ dist = abs(house - best)
283
+ dist = min(dist, 12 - dist)
284
+ # Convert house distance to degrees (1 house = 30°), normalize to 0-60
285
+ angular_dist_deg = dist * 30 # 0-180
286
+ return max(0.0, 60.0 - (angular_dist_deg * 60.0 / 180.0))
287
+
288
+
289
+ # ---------------------------------------------------------------------------
290
+ # Kala Bala (temporal strength)
291
+ # ---------------------------------------------------------------------------
292
+
293
+
294
+ def _nathonnatha_bala(planet: str, jd: float) -> float:
295
+ """Day/night strength based on Julian Day fraction."""
296
+ # JD fraction: 0.5 = noon UT; <0.5 or >0.5 used to approximate day/night.
297
+ # Simplified: treat JD fraction 0.25–0.75 as daytime.
298
+ frac = jd % 1.0
299
+ is_daytime = 0.25 <= frac < 0.75
300
+ diurnal = {"Sun", "Venus", "Jupiter"}
301
+ nocturnal = {"Moon", "Mars", "Saturn"}
302
+ if planet == "Mercury":
303
+ return 30.0
304
+ if planet in diurnal:
305
+ return 30.0 if is_daytime else 0.0
306
+ if planet in nocturnal:
307
+ return 30.0 if not is_daytime else 0.0
308
+ return 15.0
309
+
310
+
311
+ def _paksha_bala(planet: str, sun_lon: float, moon_lon: float) -> float:
312
+ """Lunar phase (Paksha) strength."""
313
+ diff = (moon_lon - sun_lon) % 360
314
+ benefics = {"Moon", "Mercury", "Jupiter", "Venus"}
315
+ if planet in benefics:
316
+ return min(60.0, diff / 6.0)
317
+ else:
318
+ return min(60.0, (360.0 - diff) / 6.0)
319
+
320
+
321
+ def _kala_bala(planet: str, jd: float, sun_lon: float, moon_lon: float) -> float:
322
+ """Combined temporal strength (Nathonnatha + Paksha)."""
323
+ return _nathonnatha_bala(planet, jd) + _paksha_bala(planet, sun_lon, moon_lon)
324
+
325
+
326
+ # ---------------------------------------------------------------------------
327
+ # Chesta Bala (motional strength)
328
+ # ---------------------------------------------------------------------------
329
+
330
+
331
+ def _chesta_bala(planet: str, speed: float) -> float:
332
+ """Motional strength based on longitudinal speed."""
333
+ if planet == "Moon":
334
+ # Moon chesta bala = similar to paksha bala for simplicity; return 30
335
+ return 30.0
336
+ mean = MEAN_MOTION.get(planet, 1.0)
337
+ if speed < 0: # retrograde
338
+ return 60.0
339
+ if speed == 0:
340
+ return 60.0
341
+ if speed <= mean:
342
+ # Slower than mean → stronger (approaching retrogression)
343
+ ratio = speed / mean
344
+ return max(0.0, 60.0 * (1.0 - ratio * 0.5))
345
+ else:
346
+ # Faster than mean
347
+ return 45.0
348
+
349
+
350
+ # ---------------------------------------------------------------------------
351
+ # Drik Bala (aspectual strength)
352
+ # ---------------------------------------------------------------------------
353
+
354
+
355
+ def _drik_bala(planet: str, all_positions: list) -> float:
356
+ """
357
+ Simplified aspectual strength.
358
+ Full aspect = planets in 7th house from each other (opposition).
359
+ Conjunction = same house.
360
+ Each benefic aspect: +15, each malefic aspect: -15. Cap at ±60.
361
+ """
362
+ target = next((p for p in all_positions if p["name"] == planet), None)
363
+ if target is None:
364
+ return 0.0
365
+
366
+ target_house = target["house"]
367
+ score = 0.0
368
+
369
+ for p in all_positions:
370
+ if p["name"] == planet:
371
+ continue
372
+ p_name = p["name"]
373
+ p_house = p["house"]
374
+
375
+ # Check if p aspects target: conjunction or 7th house opposition
376
+ diff = abs(p_house - target_house)
377
+ is_aspecting = (diff == 0) or (diff == 6)
378
+
379
+ if is_aspecting:
380
+ if p_name in NATURAL_BENEFICS:
381
+ score += 15.0
382
+ elif p_name in NATURAL_MALEFICS:
383
+ score -= 15.0
384
+
385
+ return max(-60.0, min(60.0, score))
386
+
387
+
388
+ # ---------------------------------------------------------------------------
389
+ # Ishta / Kashta Phala
390
+ # ---------------------------------------------------------------------------
391
+
392
+
393
+ def _ishta_kashta(chesta: float, uchcha: float) -> tuple:
394
+ """
395
+ Ishta Phala (benefic strength) and Kashta Phala (malefic strength).
396
+ Ishta = sqrt(uchcha_bala * chesta_bala)
397
+ Kashta = sqrt((60 - uchcha_bala) * (60 - chesta_bala))
398
+ """
399
+ uchcha_norm = min(60.0, max(0.0, uchcha))
400
+ chesta_norm = min(60.0, max(0.0, chesta))
401
+ ishta = math.sqrt(uchcha_norm * chesta_norm)
402
+ kashta = math.sqrt((60.0 - uchcha_norm) * (60.0 - chesta_norm))
403
+ return round(ishta, 2), round(kashta, 2)
404
+
405
+
406
+ # ---------------------------------------------------------------------------
407
+ # Main entry point
408
+ # ---------------------------------------------------------------------------
409
+
410
+
411
+ def get_shadbala(base_chart: dict) -> dict:
412
+ """
413
+ Calculate Shadbala (six-fold planetary strength) for all seven classical planets.
414
+
415
+ Parameters
416
+ ----------
417
+ base_chart : dict
418
+ Output of ``build_chart()``. Must contain:
419
+ - ``planetary_positions``: list of planet dicts (see module docstring).
420
+ - ``julian_day``: float (JD at birth moment).
421
+
422
+ Returns
423
+ -------
424
+ dict
425
+ ``planets`` mapping each planet name to its strength breakdown,
426
+ plus a ``summary`` string.
427
+ """
428
+ positions: list = base_chart.get("planetary_positions", [])
429
+ jd: float = base_chart.get("julian_day", 0.0)
430
+
431
+ # Build quick lookup
432
+ pos_map = {p["name"]: p for p in positions}
433
+
434
+ sun_lon = pos_map.get("Sun", {}).get("longitude", 0.0)
435
+ moon_lon = pos_map.get("Moon", {}).get("longitude", 0.0)
436
+
437
+ results = {}
438
+
439
+ for planet in PLANETS:
440
+ p = pos_map.get(planet)
441
+ if p is None:
442
+ continue
443
+
444
+ lon = p["longitude"]
445
+ house = p["house"]
446
+ speed = p.get("speed_longitude", 0.0)
447
+ is_retro = p.get("is_retrograde", False)
448
+ if is_retro:
449
+ speed = -abs(speed)
450
+
451
+ # --- Six strengths ---
452
+ sthana = _sthana_bala(planet, lon, house)
453
+ dig = _dig_bala(planet, house)
454
+ kala = _kala_bala(planet, jd, sun_lon, moon_lon)
455
+ chesta = _chesta_bala(planet, speed)
456
+ naisargika = NAISARGIKA_BALA[planet]
457
+ drik = _drik_bala(planet, positions)
458
+
459
+ total = sthana + dig + kala + chesta + naisargika + drik
460
+ total_rupas = round(total / 60.0, 3)
461
+ required = REQUIRED_RUPAS[planet]
462
+
463
+ uchcha = _uchcha_bala(planet, lon)
464
+ ishta, kashta = _ishta_kashta(chesta, uchcha)
465
+
466
+ if total_rupas >= required * 1.2:
467
+ grade = "Strong"
468
+ elif total_rupas >= required * 0.8:
469
+ grade = "Average"
470
+ else:
471
+ grade = "Weak"
472
+
473
+ results[planet] = {
474
+ "sthana_bala": round(sthana, 2),
475
+ "dig_bala": round(dig, 2),
476
+ "kala_bala": round(kala, 2),
477
+ "chesta_bala": round(chesta, 2),
478
+ "naisargika_bala": round(naisargika, 2),
479
+ "drik_bala": round(drik, 2),
480
+ "total_shadbala": round(total, 2),
481
+ "total_rupas": total_rupas,
482
+ "required_rupas": required,
483
+ "ishta_phala": ishta,
484
+ "kashta_phala": kashta,
485
+ "strength_grade": grade,
486
+ }
487
+
488
+ # Build summary
489
+ strong = [p for p, v in results.items() if v["strength_grade"] == "Strong"]
490
+ weak = [p for p, v in results.items() if v["strength_grade"] == "Weak"]
491
+ summary_parts = []
492
+ if strong:
493
+ summary_parts.append(f"Strong planets: {', '.join(strong)}.")
494
+ if weak:
495
+ summary_parts.append(f"Weak planets: {', '.join(weak)}.")
496
+ if not summary_parts:
497
+ summary_parts.append("All planets are of average strength.")
498
+ summary = " ".join(summary_parts)
499
+
500
+ return {"planets": results, "summary": summary}