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,414 @@
1
+ """Muhurta: auspicious timing selection using Panchanga and planetary conditions."""
2
+
3
+ import math
4
+ from datetime import datetime, timedelta, timezone
5
+
6
+ import swisseph as swe
7
+
8
+ from kundali_lib.vedic.panchanga import get_panchanga
9
+
10
+ # ---------------------------------------------------------------------------
11
+ # Scoring tables
12
+ # ---------------------------------------------------------------------------
13
+
14
+ # Tithi numbers considered auspicious / inauspicious (1-indexed, 30-tithi cycle)
15
+ AUSPICIOUS_TITHIS = {2, 3, 5, 7, 10, 11, 13}
16
+ INAUSPICIOUS_TITHIS = {4, 8, 9, 12, 14, 30}
17
+
18
+ VARA_SCORES: dict[str, dict[str, float]] = {
19
+ "marriage": {
20
+ "Sunday": 6,
21
+ "Monday": 10,
22
+ "Tuesday": 2,
23
+ "Wednesday": 8,
24
+ "Thursday": 10,
25
+ "Friday": 10,
26
+ "Saturday": 4,
27
+ },
28
+ "business": {
29
+ "Sunday": 8,
30
+ "Monday": 6,
31
+ "Tuesday": 4,
32
+ "Wednesday": 10,
33
+ "Thursday": 10,
34
+ "Friday": 8,
35
+ "Saturday": 2,
36
+ },
37
+ "travel": {
38
+ "Sunday": 8,
39
+ "Monday": 8,
40
+ "Tuesday": 4,
41
+ "Wednesday": 10,
42
+ "Thursday": 8,
43
+ "Friday": 6,
44
+ "Saturday": 2,
45
+ },
46
+ "housewarming": {
47
+ "Sunday": 6,
48
+ "Monday": 10,
49
+ "Tuesday": 2,
50
+ "Wednesday": 8,
51
+ "Thursday": 10,
52
+ "Friday": 8,
53
+ "Saturday": 4,
54
+ },
55
+ "education": {
56
+ "Sunday": 6,
57
+ "Monday": 8,
58
+ "Tuesday": 4,
59
+ "Wednesday": 10,
60
+ "Thursday": 10,
61
+ "Friday": 6,
62
+ "Saturday": 4,
63
+ },
64
+ "general": {
65
+ "Sunday": 6,
66
+ "Monday": 8,
67
+ "Tuesday": 4,
68
+ "Wednesday": 10,
69
+ "Thursday": 10,
70
+ "Friday": 8,
71
+ "Saturday": 4,
72
+ },
73
+ }
74
+
75
+ NAKSHATRA_NATURE: dict[str, str] = {
76
+ "Ashwini": "Fixed",
77
+ "Bharani": "Fierce",
78
+ "Krittika": "Mixed",
79
+ "Rohini": "Fixed",
80
+ "Mrigashira": "Soft",
81
+ "Ardra": "Fierce",
82
+ "Punarvasu": "Movable",
83
+ "Pushya": "Fixed",
84
+ "Ashlesha": "Fierce",
85
+ "Magha": "Fierce",
86
+ "Purva Phalguni": "Fierce",
87
+ "Uttara Phalguni": "Fixed",
88
+ "Hasta": "Movable",
89
+ "Chitra": "Soft",
90
+ "Swati": "Movable",
91
+ "Vishakha": "Mixed",
92
+ "Anuradha": "Soft",
93
+ "Jyeshtha": "Fierce",
94
+ "Mula": "Fierce",
95
+ "Purva Ashadha": "Fierce",
96
+ "Uttara Ashadha": "Fixed",
97
+ "Shravana": "Movable",
98
+ "Dhanishta": "Movable",
99
+ "Shatabhisha": "Movable",
100
+ "Purva Bhadrapada": "Fierce",
101
+ "Uttara Bhadrapada": "Fixed",
102
+ "Revati": "Soft",
103
+ }
104
+
105
+ # Best nakshatra natures per event type
106
+ _NAKSHATRA_BEST: dict[str, list[str]] = {
107
+ "marriage": ["Fixed", "Soft"],
108
+ "business": ["Fixed", "Movable"],
109
+ "travel": ["Movable"],
110
+ "housewarming": ["Fixed"],
111
+ "education": ["Fixed", "Soft", "Movable"],
112
+ "general": ["Fixed", "Soft", "Movable"],
113
+ }
114
+
115
+ # Inauspicious Yoga names (to be avoided)
116
+ INAUSPICIOUS_YOGAS = {
117
+ "Vishkumbha",
118
+ "Atiganda",
119
+ "Shoola",
120
+ "Ganda",
121
+ "Vyaghata",
122
+ "Vajra",
123
+ "Vyatipata",
124
+ "Parigha",
125
+ "Vaidhriti",
126
+ }
127
+
128
+ # Auspicious movable Karanas
129
+ AUSPICIOUS_KARANAS = {"Bava", "Balava", "Kaulava", "Taitila", "Garija", "Vanija"}
130
+ INAUSPICIOUS_KARANAS = {"Vishti"} # Bhadra Karana
131
+
132
+ # Scoring weights (sum = 1.0)
133
+ WEIGHTS = {
134
+ "nakshatra": 0.30,
135
+ "vara": 0.25,
136
+ "tithi": 0.25,
137
+ "yoga": 0.10,
138
+ "karana": 0.10,
139
+ }
140
+
141
+ # Ayanamsa constant for sidereal computation
142
+ _AYANAMSA_MODE = swe.SIDM_LAHIRI
143
+
144
+ # ---------------------------------------------------------------------------
145
+ # Internal helpers
146
+ # ---------------------------------------------------------------------------
147
+
148
+
149
+ def _score_tithi(
150
+ tithi_number: int, event_type: str
151
+ ) -> tuple[float, list[str], list[str]]:
152
+ """Return (score 0-10, auspicious_factors, inauspicious_factors)."""
153
+ auspicious: list[str] = []
154
+ inauspicious: list[str] = []
155
+ if tithi_number in AUSPICIOUS_TITHIS:
156
+ auspicious.append(f"Tithi {tithi_number} is auspicious for most events.")
157
+ score = 9.0
158
+ elif tithi_number in INAUSPICIOUS_TITHIS:
159
+ inauspicious.append(
160
+ f"Tithi {tithi_number} is inauspicious — avoid for {event_type}."
161
+ )
162
+ score = 2.0
163
+ else:
164
+ score = 6.0 # neutral
165
+ return score, auspicious, inauspicious
166
+
167
+
168
+ def _score_vara(vara_name: str, event_type: str) -> tuple[float, list[str], list[str]]:
169
+ """Return (score 0-10, auspicious_factors, inauspicious_factors)."""
170
+ auspicious: list[str] = []
171
+ inauspicious: list[str] = []
172
+ table = VARA_SCORES.get(event_type, VARA_SCORES["general"])
173
+ raw = table.get(vara_name, 5)
174
+ if raw >= 8:
175
+ auspicious.append(f"{vara_name} is a favorable day for {event_type}.")
176
+ elif raw <= 4:
177
+ inauspicious.append(f"{vara_name} is not recommended for {event_type}.")
178
+ return float(raw), auspicious, inauspicious
179
+
180
+
181
+ def _score_nakshatra(
182
+ nakshatra_name: str, event_type: str
183
+ ) -> tuple[float, list[str], list[str]]:
184
+ """Return (score 0-10, auspicious_factors, inauspicious_factors)."""
185
+ auspicious: list[str] = []
186
+ inauspicious: list[str] = []
187
+ nature = NAKSHATRA_NATURE.get(nakshatra_name, "Mixed")
188
+ best = _NAKSHATRA_BEST.get(event_type, ["Fixed", "Soft", "Movable"])
189
+ if nature == "Fierce":
190
+ inauspicious.append(
191
+ f"{nakshatra_name} (Fierce nakshatra) should be avoided for {event_type}."
192
+ )
193
+ score = 2.0
194
+ elif nature in best:
195
+ auspicious.append(
196
+ f"{nakshatra_name} ({nature} nakshatra) is well-suited for {event_type}."
197
+ )
198
+ score = 9.0
199
+ else:
200
+ score = 6.0
201
+ return score, auspicious, inauspicious
202
+
203
+
204
+ def _score_yoga(yoga_name: str) -> tuple[float, list[str], list[str]]:
205
+ """Return (score 0-10, auspicious_factors, inauspicious_factors)."""
206
+ auspicious: list[str] = []
207
+ inauspicious: list[str] = []
208
+ if yoga_name in INAUSPICIOUS_YOGAS:
209
+ inauspicious.append(
210
+ f"{yoga_name} yoga is inauspicious — avoid important events."
211
+ )
212
+ score = 1.0
213
+ else:
214
+ auspicious.append(f"{yoga_name} yoga is auspicious.")
215
+ score = 8.0
216
+ return score, auspicious, inauspicious
217
+
218
+
219
+ def _score_karana(karana_name: str) -> tuple[float, list[str], list[str]]:
220
+ """Return (score 0-10, auspicious_factors, inauspicious_factors)."""
221
+ auspicious: list[str] = []
222
+ inauspicious: list[str] = []
223
+ if karana_name in AUSPICIOUS_KARANAS:
224
+ auspicious.append(f"{karana_name} karana is auspicious.")
225
+ score = 9.0
226
+ elif karana_name in INAUSPICIOUS_KARANAS:
227
+ inauspicious.append(
228
+ f"{karana_name} (Bhadra/Vishti) karana — avoid auspicious events."
229
+ )
230
+ score = 1.0
231
+ else:
232
+ score = 5.0
233
+ return score, auspicious, inauspicious
234
+
235
+
236
+ def _grade(score: float) -> str:
237
+ if score >= 80:
238
+ return "Excellent"
239
+ if score >= 60:
240
+ return "Good"
241
+ if score >= 40:
242
+ return "Average"
243
+ return "Avoid"
244
+
245
+
246
+ # ---------------------------------------------------------------------------
247
+ # Public API
248
+ # ---------------------------------------------------------------------------
249
+
250
+
251
+ def get_muhurta_score(
252
+ panchanga: dict,
253
+ planetary_positions: list,
254
+ event_type: str,
255
+ ) -> dict:
256
+ """
257
+ Score a given Panchanga moment for a specific type of event.
258
+
259
+ Parameters
260
+ ----------
261
+ panchanga : dict
262
+ Output of ``get_panchanga()``.
263
+ planetary_positions : list
264
+ Planet dicts from ``build_chart()`` (used for future planetary
265
+ condition checks; reserved for extensibility).
266
+ event_type : str
267
+ One of: "marriage", "business", "travel", "housewarming",
268
+ "education", "general".
269
+
270
+ Returns
271
+ -------
272
+ dict
273
+ Detailed scores, grade, and recommendation.
274
+ """
275
+ if event_type not in VARA_SCORES:
276
+ event_type = "general"
277
+
278
+ tithi_num = panchanga["tithi"]["number"]
279
+ vara_name = panchanga["vara"]["name"]
280
+ nak_name = panchanga["nakshatra"]["name"]
281
+ yoga_name = panchanga["yoga"]["name"]
282
+ kar_name = panchanga["karana"]["name"]
283
+
284
+ t_score, t_aus, t_in = _score_tithi(tithi_num, event_type)
285
+ v_score, v_aus, v_in = _score_vara(vara_name, event_type)
286
+ n_score, n_aus, n_in = _score_nakshatra(nak_name, event_type)
287
+ y_score, y_aus, y_in = _score_yoga(yoga_name)
288
+ k_score, k_aus, k_in = _score_karana(kar_name)
289
+
290
+ # Weighted overall score (each component is 0-10; normalise to 0-100)
291
+ overall = (
292
+ n_score * WEIGHTS["nakshatra"]
293
+ + v_score * WEIGHTS["vara"]
294
+ + t_score * WEIGHTS["tithi"]
295
+ + y_score * WEIGHTS["yoga"]
296
+ + k_score * WEIGHTS["karana"]
297
+ ) * 10.0 # scale 0-10 → 0-100
298
+
299
+ overall = round(min(100.0, max(0.0, overall)), 2)
300
+ grade = _grade(overall)
301
+
302
+ auspicious_factors = t_aus + v_aus + n_aus + y_aus + k_aus
303
+ inauspicious_factors = t_in + v_in + n_in + y_in + k_in
304
+
305
+ if grade in ("Excellent", "Good"):
306
+ rec = (
307
+ f"This is a {grade.lower()} time for {event_type}. "
308
+ f"{nak_name} nakshatra on {vara_name} during Tithi {tithi_num} "
309
+ f"under {yoga_name} yoga is supportive."
310
+ )
311
+ elif grade == "Average":
312
+ rec = (
313
+ f"This time is average for {event_type}. Proceed with awareness of "
314
+ f"the inauspicious factors: {'; '.join(inauspicious_factors) if inauspicious_factors else 'none critical'}."
315
+ )
316
+ else:
317
+ rec = (
318
+ f"Avoid scheduling {event_type} at this time. "
319
+ f"Key concerns: {'; '.join(inauspicious_factors) if inauspicious_factors else 'multiple weak factors'}."
320
+ )
321
+
322
+ return {
323
+ "event_type": event_type,
324
+ "overall_score": overall,
325
+ "grade": grade,
326
+ "tithi_score": round(t_score * 10, 2),
327
+ "vara_score": round(v_score * 10, 2),
328
+ "nakshatra_score": round(n_score * 10, 2),
329
+ "yoga_score": round(y_score * 10, 2),
330
+ "karana_score": round(k_score * 10, 2),
331
+ "auspicious_factors": auspicious_factors,
332
+ "inauspicious_factors": inauspicious_factors,
333
+ "recommendation": rec,
334
+ }
335
+
336
+
337
+ def get_best_muhurta_in_range(
338
+ start_dt: datetime,
339
+ end_dt: datetime,
340
+ location_lat: float,
341
+ location_lon: float,
342
+ event_type: str,
343
+ ) -> list:
344
+ """
345
+ Scan every hour between start_dt and end_dt and return the top-5
346
+ most auspicious windows for the given event type.
347
+
348
+ Parameters
349
+ ----------
350
+ start_dt : datetime
351
+ Range start (timezone-aware or naive UTC).
352
+ end_dt : datetime
353
+ Range end (timezone-aware or naive UTC).
354
+ location_lat : float
355
+ Geographic latitude (degrees).
356
+ location_lon : float
357
+ Geographic longitude (degrees).
358
+ event_type : str
359
+ One of the supported event types.
360
+
361
+ Returns
362
+ -------
363
+ list
364
+ Up to 5 dicts, each with datetime (ISO), score, grade,
365
+ tithi, nakshatra, vara, and yoga.
366
+ """
367
+ # Ensure datetimes are UTC-aware
368
+ if start_dt.tzinfo is None:
369
+ start_dt = start_dt.replace(tzinfo=timezone.utc)
370
+ if end_dt.tzinfo is None:
371
+ end_dt = end_dt.replace(tzinfo=timezone.utc)
372
+
373
+ swe.set_sid_mode(_AYANAMSA_MODE)
374
+
375
+ candidates: list[dict] = []
376
+ current = start_dt
377
+
378
+ while current <= end_dt:
379
+ # Convert to Julian Day (UT)
380
+ h = current.hour + current.minute / 60.0 + current.second / 3600.0
381
+ jd = swe.julday(current.year, current.month, current.day, h)
382
+
383
+ try:
384
+ # Sun sidereal longitude
385
+ sun_flags = swe.FLG_SIDEREAL | swe.FLG_SPEED
386
+ sun_result = swe.calc_ut(jd, swe.SUN, sun_flags)
387
+ sun_lon = sun_result[0][0] % 360
388
+
389
+ # Moon sidereal longitude
390
+ moon_result = swe.calc_ut(jd, swe.MOON, sun_flags)
391
+ moon_lon = moon_result[0][0] % 360
392
+
393
+ panchanga = get_panchanga(sun_lon, moon_lon, jd)
394
+ score_data = get_muhurta_score(panchanga, [], event_type)
395
+
396
+ candidates.append(
397
+ {
398
+ "datetime": current.isoformat(),
399
+ "score": score_data["overall_score"],
400
+ "grade": score_data["grade"],
401
+ "tithi": panchanga["tithi"]["name"],
402
+ "nakshatra": panchanga["nakshatra"]["name"],
403
+ "vara": panchanga["vara"]["name"],
404
+ "yoga": panchanga["yoga"]["name"],
405
+ }
406
+ )
407
+ except Exception:
408
+ pass # skip hours where ephemeris data is unavailable
409
+
410
+ current += timedelta(hours=1)
411
+
412
+ # Sort descending by score, return top 5
413
+ candidates.sort(key=lambda x: x["score"], reverse=True)
414
+ return candidates[:5]