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,935 @@
1
+ """Identification of major Vedic astrological Yogas from a birth chart."""
2
+
3
+ # ---------------------------------------------------------------------------
4
+ # Reference data
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_LORDS = {
23
+ "Aries": "Mars",
24
+ "Taurus": "Venus",
25
+ "Gemini": "Mercury",
26
+ "Cancer": "Moon",
27
+ "Leo": "Sun",
28
+ "Virgo": "Mercury",
29
+ "Libra": "Venus",
30
+ "Scorpio": "Mars",
31
+ "Sagittarius": "Jupiter",
32
+ "Capricorn": "Saturn",
33
+ "Aquarius": "Saturn",
34
+ "Pisces": "Jupiter",
35
+ }
36
+
37
+ EXALTATION = {
38
+ "Sun": "Aries",
39
+ "Moon": "Taurus",
40
+ "Mars": "Capricorn",
41
+ "Mercury": "Virgo",
42
+ "Jupiter": "Cancer",
43
+ "Venus": "Pisces",
44
+ "Saturn": "Libra",
45
+ }
46
+
47
+ DEBILITATION = {
48
+ "Sun": "Libra",
49
+ "Moon": "Scorpio",
50
+ "Mars": "Cancer",
51
+ "Mercury": "Pisces",
52
+ "Jupiter": "Capricorn",
53
+ "Venus": "Virgo",
54
+ "Saturn": "Aries",
55
+ }
56
+
57
+ # Reverse map: which planet gets exalted in a given sign
58
+ EXALTED_IN_SIGN = {v: k for k, v in EXALTATION.items()}
59
+
60
+ KENDRA_HOUSES = {1, 4, 7, 10}
61
+ TRIKONA_HOUSES = {1, 5, 9}
62
+ DUSTHANA_HOUSES = {6, 8, 12}
63
+
64
+ CLASSICAL_PLANETS = {"Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn"}
65
+ BENEFIC_PLANETS = {"Moon", "Mercury", "Jupiter", "Venus"}
66
+ NODES = {"Rahu", "Ketu"}
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # Internal helpers
71
+ # ---------------------------------------------------------------------------
72
+
73
+
74
+ def _get_planet(positions: list, name: str) -> dict | None:
75
+ for p in positions:
76
+ if p["name"] == name:
77
+ return p
78
+ return None
79
+
80
+
81
+ def _planet_house(positions: list, name: str) -> int | None:
82
+ p = _get_planet(positions, name)
83
+ return p["house"] if p else None
84
+
85
+
86
+ def _planet_rashi(positions: list, name: str) -> str | None:
87
+ p = _get_planet(positions, name)
88
+ return p.get("rashi") if p else None
89
+
90
+
91
+ def _house_lord(houses: dict, house_num: int) -> str:
92
+ """Return the planet that lords the given house number."""
93
+ rashi = houses.get(f"House_{house_num}")
94
+ if rashi:
95
+ return RASHI_LORDS.get(rashi, "")
96
+ return ""
97
+
98
+
99
+ def _house_rashi(houses: dict, house_num: int) -> str:
100
+ return houses.get(f"House_{house_num}", "")
101
+
102
+
103
+ def _nth_house_from(base: int, n: int) -> int:
104
+ """Return the house number that is n houses away from base (1-indexed)."""
105
+ return ((base - 1 + n - 1) % 12) + 1
106
+
107
+
108
+ def _planets_in_house(positions: list, house_num: int) -> list:
109
+ return [p["name"] for p in positions if p["house"] == house_num]
110
+
111
+
112
+ def _kendra_houses_from(base_house: int) -> set:
113
+ return {
114
+ _nth_house_from(base_house, 1),
115
+ _nth_house_from(base_house, 4),
116
+ _nth_house_from(base_house, 7),
117
+ _nth_house_from(base_house, 10),
118
+ }
119
+
120
+
121
+ def _yoga_dict(
122
+ name: str,
123
+ yoga_type: str,
124
+ description: str,
125
+ planets: list,
126
+ present: bool,
127
+ strength: str,
128
+ ) -> dict:
129
+ return {
130
+ "name": name,
131
+ "type": yoga_type,
132
+ "description": description,
133
+ "planets_involved": planets,
134
+ "present": present,
135
+ "strength": strength,
136
+ }
137
+
138
+
139
+ def _strength_from_flag(strong: bool, moderate: bool) -> str:
140
+ if strong:
141
+ return "Strong"
142
+ if moderate:
143
+ return "Moderate"
144
+ return "Weak"
145
+
146
+
147
+ # ---------------------------------------------------------------------------
148
+ # Pancha Mahapurusha Yogas
149
+ # ---------------------------------------------------------------------------
150
+
151
+
152
+ def _check_pancha_mahapurusha(
153
+ positions: list, planet_name: str, own_signs: set, exalt_sign: str | None
154
+ ) -> tuple:
155
+ """Return (present, strength)."""
156
+ p = _get_planet(positions, planet_name)
157
+ if not p:
158
+ return False, "Weak"
159
+ rashi = p.get("rashi", "")
160
+ house = p.get("house")
161
+ if house is None:
162
+ return False, "Weak"
163
+ in_own = rashi in own_signs
164
+ in_exalt = rashi == exalt_sign
165
+ in_kendra = house in KENDRA_HOUSES
166
+ present = (in_own or in_exalt) and in_kendra
167
+ if present:
168
+ strength = "Strong" if in_exalt else "Moderate"
169
+ else:
170
+ strength = "Weak"
171
+ return present, strength
172
+
173
+
174
+ def _pancha_mahapurusha_yogas(positions: list) -> list:
175
+ yogas = []
176
+
177
+ configs = [
178
+ (
179
+ "Ruchaka",
180
+ "Mars",
181
+ {"Aries", "Scorpio"},
182
+ "Capricorn",
183
+ "Mars in own sign (Aries/Scorpio) or exaltation (Capricorn) in a kendra house.",
184
+ ),
185
+ (
186
+ "Bhadra",
187
+ "Mercury",
188
+ {"Gemini", "Virgo"},
189
+ "Virgo",
190
+ "Mercury in own sign (Gemini/Virgo) or exaltation (Virgo) in a kendra house.",
191
+ ),
192
+ (
193
+ "Hamsa",
194
+ "Jupiter",
195
+ {"Sagittarius", "Pisces"},
196
+ "Cancer",
197
+ "Jupiter in own sign (Sagittarius/Pisces) or exaltation (Cancer) in a kendra house.",
198
+ ),
199
+ (
200
+ "Malavya",
201
+ "Venus",
202
+ {"Taurus", "Libra"},
203
+ "Pisces",
204
+ "Venus in own sign (Taurus/Libra) or exaltation (Pisces) in a kendra house.",
205
+ ),
206
+ (
207
+ "Sasa",
208
+ "Saturn",
209
+ {"Capricorn", "Aquarius"},
210
+ "Libra",
211
+ "Saturn in own sign (Capricorn/Aquarius) or exaltation (Libra) in a kendra house.",
212
+ ),
213
+ ]
214
+
215
+ for yoga_name, planet, own_signs, exalt_sign, desc in configs:
216
+ present, strength = _check_pancha_mahapurusha(
217
+ positions, planet, own_signs, exalt_sign
218
+ )
219
+ yogas.append(
220
+ _yoga_dict(
221
+ yoga_name,
222
+ "Benefic",
223
+ desc,
224
+ [planet],
225
+ present,
226
+ strength if present else "Weak",
227
+ )
228
+ )
229
+ return yogas
230
+
231
+
232
+ # ---------------------------------------------------------------------------
233
+ # Sun / Moon yogas
234
+ # ---------------------------------------------------------------------------
235
+
236
+
237
+ def _sun_moon_yogas(positions: list) -> list:
238
+ yogas = []
239
+
240
+ sun = _get_planet(positions, "Sun")
241
+ moon = _get_planet(positions, "Moon")
242
+ mercury = _get_planet(positions, "Mercury")
243
+ jupiter = _get_planet(positions, "Jupiter")
244
+ mars = _get_planet(positions, "Mars")
245
+
246
+ # Budha-Aditya Yoga
247
+ if sun and mercury:
248
+ present = sun["house"] == mercury["house"]
249
+ # Weaker if Mercury is combust (very close to Sun); rough proxy: combust if within 8°
250
+ combust = (
251
+ present and abs(sun.get("longitude", 0) - mercury.get("longitude", 0)) < 8
252
+ )
253
+ strength = "Moderate" if combust else ("Strong" if present else "Weak")
254
+ yogas.append(
255
+ _yoga_dict(
256
+ "Budha-Aditya Yoga",
257
+ "Benefic",
258
+ "Sun and Mercury conjunction. Confers intelligence and communication skills.",
259
+ ["Sun", "Mercury"],
260
+ present,
261
+ strength,
262
+ )
263
+ )
264
+
265
+ # Gaja Kesari Yoga
266
+ if moon and jupiter:
267
+ diff = (moon["house"] - jupiter["house"]) % 12
268
+ present = diff in {0, 3, 6, 9}
269
+ strength = "Strong" if diff == 0 else ("Moderate" if present else "Weak")
270
+ yogas.append(
271
+ _yoga_dict(
272
+ "Gaja Kesari Yoga",
273
+ "Benefic",
274
+ "Moon and Jupiter in kendra from each other. Bestows fame, wisdom, and prosperity.",
275
+ ["Moon", "Jupiter"],
276
+ present,
277
+ strength,
278
+ )
279
+ )
280
+
281
+ # Chandra-Mangala Yoga
282
+ if moon and mars:
283
+ present = moon["house"] == mars["house"]
284
+ yogas.append(
285
+ _yoga_dict(
286
+ "Chandra-Mangala Yoga",
287
+ "Benefic",
288
+ "Moon and Mars conjunction. Gives financial acumen and strong will.",
289
+ ["Moon", "Mars"],
290
+ present,
291
+ "Moderate" if present else "Weak",
292
+ )
293
+ )
294
+
295
+ # Sunapha / Anapha / Durudhura / Kemadruma (all require Moon)
296
+ if moon:
297
+ moon_house = moon["house"]
298
+ second_from_moon = _nth_house_from(moon_house, 2)
299
+ twelfth_from_moon = _nth_house_from(moon_house, 12)
300
+
301
+ # Planets in 2nd/12th from Moon (excluding Sun)
302
+ in_2nd = [
303
+ p["name"]
304
+ for p in positions
305
+ if p["house"] == second_from_moon and p["name"] != "Sun"
306
+ ]
307
+ in_12th = [
308
+ p["name"]
309
+ for p in positions
310
+ if p["house"] == twelfth_from_moon and p["name"] != "Sun"
311
+ ]
312
+
313
+ # Sunapha
314
+ present_sunapha = len(in_2nd) > 0
315
+ yogas.append(
316
+ _yoga_dict(
317
+ "Sunapha Yoga",
318
+ "Benefic",
319
+ "Planet (other than Sun) in 2nd house from Moon. Brings wealth and self-reliance.",
320
+ ["Moon"] + in_2nd,
321
+ present_sunapha,
322
+ "Moderate" if present_sunapha else "Weak",
323
+ )
324
+ )
325
+
326
+ # Anapha
327
+ present_anapha = len(in_12th) > 0
328
+ yogas.append(
329
+ _yoga_dict(
330
+ "Anapha Yoga",
331
+ "Benefic",
332
+ "Planet (other than Sun) in 12th house from Moon. Confers health and generosity.",
333
+ ["Moon"] + in_12th,
334
+ present_anapha,
335
+ "Moderate" if present_anapha else "Weak",
336
+ )
337
+ )
338
+
339
+ # Durudhura
340
+ present_duru = present_sunapha and present_anapha
341
+ yogas.append(
342
+ _yoga_dict(
343
+ "Durudhura Yoga",
344
+ "Benefic",
345
+ "Planets in both 2nd and 12th from Moon. Grants abundance and enjoyment of life.",
346
+ ["Moon"] + list(set(in_2nd + in_12th)),
347
+ present_duru,
348
+ "Strong" if present_duru else "Weak",
349
+ )
350
+ )
351
+
352
+ # Kemadruma (Malefic) — no planet in 2nd or 12th from Moon AND Moon not in kendra
353
+ moon_in_kendra = moon_house in KENDRA_HOUSES
354
+ present_kema = (
355
+ (not present_sunapha) and (not present_anapha) and (not moon_in_kendra)
356
+ )
357
+ yogas.append(
358
+ _yoga_dict(
359
+ "Kemadruma Yoga",
360
+ "Malefic",
361
+ "No planet in 2nd or 12th from Moon and Moon not in kendra. Weakens the Moon significantly.",
362
+ ["Moon"],
363
+ present_kema,
364
+ "Strong" if present_kema else "Weak",
365
+ )
366
+ )
367
+
368
+ return yogas
369
+
370
+
371
+ # ---------------------------------------------------------------------------
372
+ # Wealth / Raj Yogas
373
+ # ---------------------------------------------------------------------------
374
+
375
+
376
+ def _raj_and_wealth_yogas(positions: list, houses: dict) -> list:
377
+ yogas = []
378
+
379
+ # --- Raj Yoga ---
380
+ kendra_houses = [1, 4, 7, 10]
381
+ trikona_houses_list = [1, 5, 9]
382
+ kendra_lord_map = {h: _house_lord(houses, h) for h in kendra_houses}
383
+ trikona_lord_map = {h: _house_lord(houses, h) for h in trikona_houses_list}
384
+
385
+ raj_pairs = []
386
+ for kh in kendra_houses:
387
+ kl = kendra_lord_map[kh]
388
+ if not kl:
389
+ continue
390
+ for th in trikona_houses_list:
391
+ if kh == th:
392
+ continue
393
+ tl = trikona_lord_map[th]
394
+ if not tl:
395
+ continue
396
+ if kl == tl:
397
+ # Same planet is yoga karaka (lords both kendra & trikona)
398
+ raj_pairs.append((kl, tl))
399
+ continue
400
+ kl_house = _planet_house(positions, kl)
401
+ tl_house = _planet_house(positions, tl)
402
+ # Conjunction
403
+ if kl_house is not None and tl_house is not None and kl_house == tl_house:
404
+ raj_pairs.append((kl, tl))
405
+ continue
406
+ # Exchange
407
+ kl_rashi = _planet_rashi(positions, kl)
408
+ tl_rashi = _planet_rashi(positions, tl)
409
+ if kl_rashi and tl_rashi:
410
+ if RASHI_LORDS.get(kl_rashi) == tl and RASHI_LORDS.get(tl_rashi) == kl:
411
+ raj_pairs.append((kl, tl))
412
+
413
+ raj_present = len(raj_pairs) > 0
414
+ raj_planets = list({p for pair in raj_pairs for p in pair})
415
+ yogas.append(
416
+ _yoga_dict(
417
+ "Raj Yoga",
418
+ "Benefic",
419
+ "Lord of a kendra house (1,4,7,10) conjunct or exchange with lord of a trikona house (1,5,9). "
420
+ "Confers power, authority, and success.",
421
+ raj_planets,
422
+ raj_present,
423
+ "Strong" if len(raj_pairs) > 1 else ("Moderate" if raj_present else "Weak"),
424
+ )
425
+ )
426
+
427
+ # --- Dhana Yoga ---
428
+ lord_2 = _house_lord(houses, 2)
429
+ lord_11 = _house_lord(houses, 11)
430
+ dhana_present = False
431
+ if lord_2 and lord_11 and lord_2 != lord_11:
432
+ h2 = _planet_house(positions, lord_2)
433
+ h11 = _planet_house(positions, lord_11)
434
+ if h2 is not None and h11 is not None:
435
+ if h2 == h11:
436
+ dhana_present = True
437
+ else:
438
+ # Mutual kendra or trikona
439
+ diff = (h2 - h11) % 12
440
+ if diff in {0, 3, 6, 9}:
441
+ dhana_present = True
442
+ elif lord_2 and lord_11 and lord_2 == lord_11:
443
+ dhana_present = True # Same planet lords both 2nd and 11th
444
+ yogas.append(
445
+ _yoga_dict(
446
+ "Dhana Yoga",
447
+ "Benefic",
448
+ "Lords of 2nd and 11th conjoined or in mutual kendra/trikona. Brings great wealth.",
449
+ [lord_2, lord_11] if lord_2 and lord_11 else [],
450
+ dhana_present,
451
+ "Moderate" if dhana_present else "Weak",
452
+ )
453
+ )
454
+
455
+ # --- Lakshmi Yoga ---
456
+ lord_5 = _house_lord(houses, 5)
457
+ lord_9 = _house_lord(houses, 9)
458
+ venus = _get_planet(positions, "Venus")
459
+ lakshmi_present = False
460
+ lakshmi_strength = "Weak"
461
+ if venus:
462
+ venus_rashi = venus.get("rashi", "")
463
+ venus_house = venus.get("house")
464
+ venus_in_own = venus_rashi in {"Taurus", "Libra"}
465
+ venus_in_exalt = venus_rashi == "Pisces"
466
+ venus_in_kendra_or_trikona = venus_house in (KENDRA_HOUSES | TRIKONA_HOUSES)
467
+ venus_is_lord_5_or_9 = "Venus" in {lord_5, lord_9}
468
+ if (
469
+ venus_is_lord_5_or_9
470
+ and (venus_in_own or venus_in_exalt)
471
+ and venus_in_kendra_or_trikona
472
+ ):
473
+ lakshmi_present = True
474
+ lakshmi_strength = "Strong" if venus_in_exalt else "Moderate"
475
+ yogas.append(
476
+ _yoga_dict(
477
+ "Lakshmi Yoga",
478
+ "Benefic",
479
+ "Venus (lord of 5th or 9th) in own/exaltation sign in kendra or trikona. "
480
+ "Blesses with beauty, luxury, and spiritual grace.",
481
+ ["Venus"],
482
+ lakshmi_present,
483
+ lakshmi_strength,
484
+ )
485
+ )
486
+
487
+ # --- Saraswati Yoga ---
488
+ saraswati_houses = KENDRA_HOUSES | TRIKONA_HOUSES | {2, 11}
489
+ sj = _planet_house(positions, "Jupiter")
490
+ sv = _planet_house(positions, "Venus")
491
+ sm = _planet_house(positions, "Mercury")
492
+ saraswati_present = (
493
+ sj in saraswati_houses and sv in saraswati_houses and sm in saraswati_houses
494
+ )
495
+ yogas.append(
496
+ _yoga_dict(
497
+ "Saraswati Yoga",
498
+ "Benefic",
499
+ "Jupiter, Venus, and Mercury all in kendra or trikona (including 2nd/11th). "
500
+ "Confers great learning, eloquence, and artistic talent.",
501
+ ["Jupiter", "Venus", "Mercury"],
502
+ saraswati_present,
503
+ "Strong" if saraswati_present else "Weak",
504
+ )
505
+ )
506
+
507
+ return yogas
508
+
509
+
510
+ # ---------------------------------------------------------------------------
511
+ # Benefic Yogas (Vesi / Voshi / Obhayachari / Amala / Pancha-Mahabhuta)
512
+ # ---------------------------------------------------------------------------
513
+
514
+
515
+ def _benefic_yogas(positions: list, houses: dict) -> list:
516
+ yogas = []
517
+
518
+ sun = _get_planet(positions, "Sun")
519
+
520
+ # Vesi / Voshi / Obhayachari
521
+ if sun:
522
+ sun_house = sun["house"]
523
+ second_from_sun = _nth_house_from(sun_house, 2)
524
+ twelfth_from_sun = _nth_house_from(sun_house, 12)
525
+
526
+ in_2nd_sun = [
527
+ p["name"]
528
+ for p in positions
529
+ if p["house"] == second_from_sun and p["name"] != "Moon"
530
+ ]
531
+ in_12th_sun = [
532
+ p["name"]
533
+ for p in positions
534
+ if p["house"] == twelfth_from_sun and p["name"] != "Moon"
535
+ ]
536
+
537
+ present_vesi = len(in_2nd_sun) > 0
538
+ yogas.append(
539
+ _yoga_dict(
540
+ "Vesi Yoga",
541
+ "Benefic",
542
+ "Planet (other than Moon) in 2nd house from Sun. Grants wealth and political success.",
543
+ ["Sun"] + in_2nd_sun,
544
+ present_vesi,
545
+ "Moderate" if present_vesi else "Weak",
546
+ )
547
+ )
548
+
549
+ present_voshi = len(in_12th_sun) > 0
550
+ yogas.append(
551
+ _yoga_dict(
552
+ "Voshi Yoga",
553
+ "Benefic",
554
+ "Planet (other than Moon) in 12th house from Sun. Brings fame and noble character.",
555
+ ["Sun"] + in_12th_sun,
556
+ present_voshi,
557
+ "Moderate" if present_voshi else "Weak",
558
+ )
559
+ )
560
+
561
+ present_obhaya = present_vesi and present_voshi
562
+ yogas.append(
563
+ _yoga_dict(
564
+ "Obhayachari Yoga",
565
+ "Benefic",
566
+ "Planets in both 2nd and 12th from Sun. Bestows kingly attributes and leadership.",
567
+ ["Sun"] + list(set(in_2nd_sun + in_12th_sun)),
568
+ present_obhaya,
569
+ "Strong" if present_obhaya else "Weak",
570
+ )
571
+ )
572
+
573
+ # Amala Yoga — only benefics in 10th from Lagna or Moon
574
+ tenth_from_lagna = 10
575
+ planets_in_10th = _planets_in_house(positions, tenth_from_lagna)
576
+ amala_lagna = len(planets_in_10th) > 0 and all(
577
+ p in BENEFIC_PLANETS for p in planets_in_10th
578
+ )
579
+
580
+ moon = _get_planet(positions, "Moon")
581
+ amala_moon = False
582
+ if moon:
583
+ tenth_from_moon = _nth_house_from(moon["house"], 10)
584
+ planets_in_10th_moon = _planets_in_house(positions, tenth_from_moon)
585
+ amala_moon = len(planets_in_10th_moon) > 0 and all(
586
+ p in BENEFIC_PLANETS for p in planets_in_10th_moon
587
+ )
588
+ amala_present = amala_lagna or amala_moon
589
+ yogas.append(
590
+ _yoga_dict(
591
+ "Amala Yoga",
592
+ "Benefic",
593
+ "Only benefic planets (Moon, Mercury, Jupiter, Venus) occupy the 10th from Lagna or Moon. "
594
+ "Ensures a spotless reputation and virtuous career.",
595
+ list(set(planets_in_10th)),
596
+ amala_present,
597
+ "Strong" if amala_present else "Weak",
598
+ )
599
+ )
600
+
601
+ # Pancha-Mahabhuta Yoga — all kendras occupied
602
+ kendra_occupied = all(
603
+ len(_planets_in_house(positions, h)) > 0 for h in KENDRA_HOUSES
604
+ )
605
+ all_kendra_planets = [
606
+ p for h in KENDRA_HOUSES for p in _planets_in_house(positions, h)
607
+ ]
608
+ yogas.append(
609
+ _yoga_dict(
610
+ "Pancha-Mahabhuta Yoga",
611
+ "Benefic",
612
+ "All kendra houses (1,4,7,10) are occupied by planets, representing all 5 elements. "
613
+ "Bestows a well-rounded, powerful personality.",
614
+ all_kendra_planets,
615
+ kendra_occupied,
616
+ "Strong" if kendra_occupied else "Weak",
617
+ )
618
+ )
619
+
620
+ return yogas
621
+
622
+
623
+ # ---------------------------------------------------------------------------
624
+ # Malefic Yogas
625
+ # ---------------------------------------------------------------------------
626
+
627
+
628
+ def _malefic_yogas(positions: list, houses: dict) -> list:
629
+ yogas = []
630
+
631
+ # --- Daridra Yoga ---
632
+ good_houses = [1, 2, 4, 5, 7, 9, 10, 11]
633
+ dusthana_lords = {
634
+ _house_lord(houses, h) for h in [6, 8, 12] if _house_lord(houses, h)
635
+ }
636
+ good_lords = {_house_lord(houses, h) for h in good_houses if _house_lord(houses, h)}
637
+ daridra_planets = []
638
+ for dl in dusthana_lords:
639
+ for gl in good_lords:
640
+ if dl == gl:
641
+ continue
642
+ dl_house = _planet_house(positions, dl)
643
+ gl_house = _planet_house(positions, gl)
644
+ if dl_house is not None and gl_house is not None and dl_house == gl_house:
645
+ daridra_planets.extend([dl, gl])
646
+ daridra_present = len(daridra_planets) > 0
647
+ yogas.append(
648
+ _yoga_dict(
649
+ "Daridra Yoga",
650
+ "Malefic",
651
+ "Lords of dusthana houses (6th, 8th, 12th) conjunct with lords of good houses. "
652
+ "Can bring poverty, debts, and misfortune.",
653
+ list(set(daridra_planets)),
654
+ daridra_present,
655
+ "Strong" if daridra_present else "Weak",
656
+ )
657
+ )
658
+
659
+ # --- Shakata Yoga ---
660
+ moon = _get_planet(positions, "Moon")
661
+ jupiter = _get_planet(positions, "Jupiter")
662
+ shakata_present = False
663
+ if moon and jupiter:
664
+ diff_from_moon = (jupiter["house"] - moon["house"]) % 12 + 1
665
+ shakata_present = diff_from_moon in {6, 8, 12}
666
+ yogas.append(
667
+ _yoga_dict(
668
+ "Shakata Yoga",
669
+ "Malefic",
670
+ "Jupiter in 6th, 8th, or 12th house from Moon. Causes fluctuating fortune and obstacles.",
671
+ ["Moon", "Jupiter"],
672
+ shakata_present,
673
+ "Strong" if shakata_present else "Weak",
674
+ )
675
+ )
676
+
677
+ # --- Guru Chandal Yoga ---
678
+ rahu = _get_planet(positions, "Rahu")
679
+ ketu = _get_planet(positions, "Ketu")
680
+ guru_chandal_planets = ["Jupiter"]
681
+ guru_chandal = False
682
+ if jupiter:
683
+ if rahu and rahu["house"] == jupiter["house"]:
684
+ guru_chandal = True
685
+ guru_chandal_planets.append("Rahu")
686
+ if ketu and ketu["house"] == jupiter["house"]:
687
+ guru_chandal = True
688
+ guru_chandal_planets.append("Ketu")
689
+ yogas.append(
690
+ _yoga_dict(
691
+ "Guru Chandal Yoga",
692
+ "Malefic",
693
+ "Jupiter conjunct Rahu or Ketu. Can corrupt Jupiter's wisdom and cause ethical confusion.",
694
+ guru_chandal_planets,
695
+ guru_chandal,
696
+ "Strong" if guru_chandal else "Weak",
697
+ )
698
+ )
699
+
700
+ # --- Grahan Yoga ---
701
+ sun = _get_planet(positions, "Sun")
702
+ moon_p = _get_planet(positions, "Moon")
703
+ grahan_planets = []
704
+ if sun and rahu and sun["house"] == rahu["house"]:
705
+ grahan_planets.extend(["Sun", "Rahu"])
706
+ if sun and ketu and sun["house"] == ketu["house"]:
707
+ grahan_planets.extend(["Sun", "Ketu"])
708
+ if moon_p and rahu and moon_p["house"] == rahu["house"]:
709
+ grahan_planets.extend(["Moon", "Rahu"])
710
+ if moon_p and ketu and moon_p["house"] == ketu["house"]:
711
+ grahan_planets.extend(["Moon", "Ketu"])
712
+ grahan_present = len(grahan_planets) > 0
713
+ yogas.append(
714
+ _yoga_dict(
715
+ "Grahan Yoga",
716
+ "Malefic",
717
+ "Sun or Moon conjunct Rahu or Ketu (eclipse yoga). Afflicts the luminaries and causes setbacks.",
718
+ list(set(grahan_planets)),
719
+ grahan_present,
720
+ "Strong" if grahan_present else "Weak",
721
+ )
722
+ )
723
+
724
+ return yogas
725
+
726
+
727
+ # ---------------------------------------------------------------------------
728
+ # Other Notable Yogas
729
+ # ---------------------------------------------------------------------------
730
+
731
+
732
+ def _other_yogas(positions: list, houses: dict) -> list:
733
+ yogas = []
734
+
735
+ moon = _get_planet(positions, "Moon")
736
+
737
+ # --- Neecha Bhanga Raj Yoga ---
738
+ neecha_bhanga_planets = []
739
+ for planet, debil_sign in DEBILITATION.items():
740
+ p = _get_planet(positions, planet)
741
+ if not p:
742
+ continue
743
+ if p.get("rashi") != debil_sign:
744
+ continue
745
+ # Planet is debilitated; check cancellation conditions
746
+ # Condition 1: Lord of the debilitation sign is in kendra from Lagna
747
+ debil_lord = RASHI_LORDS.get(debil_sign, "")
748
+ debil_lord_house = _planet_house(positions, debil_lord)
749
+ lord_in_kendra = (
750
+ debil_lord_house in KENDRA_HOUSES if debil_lord_house else False
751
+ )
752
+
753
+ # Condition 2: Planet exalted in the same sign is in kendra from Lagna or Moon
754
+ exalt_planet = EXALTED_IN_SIGN.get(debil_sign, "")
755
+ exalt_planet_house = (
756
+ _planet_house(positions, exalt_planet) if exalt_planet else None
757
+ )
758
+ exalt_in_kendra_lagna = (
759
+ (exalt_planet_house in KENDRA_HOUSES) if exalt_planet_house else False
760
+ )
761
+ exalt_in_kendra_moon = False
762
+ if moon and exalt_planet_house:
763
+ moon_kendras = _kendra_houses_from(moon["house"])
764
+ exalt_in_kendra_moon = exalt_planet_house in moon_kendras
765
+
766
+ if lord_in_kendra or exalt_in_kendra_lagna or exalt_in_kendra_moon:
767
+ neecha_bhanga_planets.append(planet)
768
+
769
+ nbr_present = len(neecha_bhanga_planets) > 0
770
+ yogas.append(
771
+ _yoga_dict(
772
+ "Neecha Bhanga Raj Yoga",
773
+ "Benefic",
774
+ "Debilitated planet's weakness is cancelled by the lord of debilitation sign or the "
775
+ "exaltation ruler being in kendra. Turns weakness into a source of strength.",
776
+ neecha_bhanga_planets,
777
+ nbr_present,
778
+ "Strong"
779
+ if len(neecha_bhanga_planets) > 1
780
+ else ("Moderate" if nbr_present else "Weak"),
781
+ )
782
+ )
783
+
784
+ # --- Viparita Raj Yoga ---
785
+ lord_6 = _house_lord(houses, 6)
786
+ lord_8 = _house_lord(houses, 8)
787
+ lord_12 = _house_lord(houses, 12)
788
+ l6h = _planet_house(positions, lord_6) if lord_6 else None
789
+ l8h = _planet_house(positions, lord_8) if lord_8 else None
790
+ l12h = _planet_house(positions, lord_12) if lord_12 else None
791
+
792
+ viparita_conditions = [
793
+ l6h in {8, 12} if l6h else False,
794
+ l8h in {6, 12} if l8h else False,
795
+ l12h in {6, 8} if l12h else False,
796
+ ]
797
+ vip_count = sum(viparita_conditions)
798
+ vip_present = vip_count > 0
799
+ vip_planets = []
800
+ if viparita_conditions[0] and lord_6:
801
+ vip_planets.append(lord_6)
802
+ if viparita_conditions[1] and lord_8:
803
+ vip_planets.append(lord_8)
804
+ if viparita_conditions[2] and lord_12:
805
+ vip_planets.append(lord_12)
806
+ yogas.append(
807
+ _yoga_dict(
808
+ "Viparita Raj Yoga",
809
+ "Benefic",
810
+ "Lord of 6th in 8th/12th, lord of 8th in 6th/12th, or lord of 12th in 6th/8th. "
811
+ "Adversity ultimately turns into power and success.",
812
+ vip_planets,
813
+ vip_present,
814
+ "Strong" if vip_count >= 2 else ("Moderate" if vip_present else "Weak"),
815
+ )
816
+ )
817
+
818
+ # --- Parivartana Yoga (Exchange) ---
819
+ planet_data = {p["name"]: p for p in positions if p["name"] in CLASSICAL_PLANETS}
820
+ sorted_names = sorted(planet_data.keys())
821
+ parivartana_yogas = []
822
+
823
+ for i, name1 in enumerate(sorted_names):
824
+ for name2 in sorted_names[i + 1 :]:
825
+ p1 = planet_data[name1]
826
+ p2 = planet_data[name2]
827
+ p1_rashi = p1.get("rashi", "")
828
+ p2_rashi = p2.get("rashi", "")
829
+ # Check mutual exchange
830
+ if (
831
+ RASHI_LORDS.get(p1_rashi) == name2
832
+ and RASHI_LORDS.get(p2_rashi) == name1
833
+ ):
834
+ h1 = p1["house"]
835
+ h2 = p2["house"]
836
+ is_kendra_1 = h1 in KENDRA_HOUSES
837
+ is_kendra_2 = h2 in KENDRA_HOUSES
838
+ is_trikona_1 = h1 in TRIKONA_HOUSES
839
+ is_trikona_2 = h2 in TRIKONA_HOUSES
840
+ is_dusthana_1 = h1 in DUSTHANA_HOUSES
841
+ is_dusthana_2 = h2 in DUSTHANA_HOUSES
842
+
843
+ if (
844
+ (is_kendra_1 and is_kendra_2)
845
+ or (is_kendra_1 and is_trikona_2)
846
+ or (is_trikona_1 and is_kendra_2)
847
+ ):
848
+ subtype = "Maha Parivartana Yoga"
849
+ ytype = "Benefic"
850
+ elif (is_kendra_1 and is_dusthana_2) or (is_dusthana_1 and is_kendra_2):
851
+ subtype = "Kahala Parivartana Yoga"
852
+ ytype = "Neutral"
853
+ elif is_dusthana_1 or is_dusthana_2:
854
+ subtype = "Dainya Parivartana Yoga"
855
+ ytype = "Malefic"
856
+ else:
857
+ subtype = "Parivartana Yoga"
858
+ ytype = "Neutral"
859
+
860
+ parivartana_yogas.append(
861
+ _yoga_dict(
862
+ subtype,
863
+ ytype,
864
+ f"Exchange between {name1} (house {h1}) and {name2} (house {h2}). "
865
+ "Planets act as if in each other's signs, amplifying their combined energy.",
866
+ [name1, name2],
867
+ True,
868
+ "Moderate",
869
+ )
870
+ )
871
+
872
+ yogas.extend(parivartana_yogas)
873
+ return yogas
874
+
875
+
876
+ # ---------------------------------------------------------------------------
877
+ # Main public function
878
+ # ---------------------------------------------------------------------------
879
+
880
+
881
+ def get_yogas(base_chart: dict) -> dict:
882
+ """
883
+ Identify major Vedic astrological Yogas from a birth chart.
884
+
885
+ Args:
886
+ base_chart: Full chart dict with keys:
887
+ planetary_positions, ascendant, houses, moon_sign, sun_sign
888
+
889
+ Returns:
890
+ dict with benefic_yogas, malefic_yogas, and summary.
891
+ """
892
+ positions = base_chart.get("planetary_positions", [])
893
+ houses = base_chart.get("houses", {})
894
+
895
+ all_yogas: list[dict] = []
896
+ all_yogas.extend(_pancha_mahapurusha_yogas(positions))
897
+ all_yogas.extend(_sun_moon_yogas(positions))
898
+ all_yogas.extend(_raj_and_wealth_yogas(positions, houses))
899
+ all_yogas.extend(_benefic_yogas(positions, houses))
900
+ all_yogas.extend(_malefic_yogas(positions, houses))
901
+ all_yogas.extend(_other_yogas(positions, houses))
902
+
903
+ benefic_yogas = [y for y in all_yogas if y["type"] == "Benefic" and y["present"]]
904
+ malefic_yogas = [y for y in all_yogas if y["type"] == "Malefic" and y["present"]]
905
+ neutral_present = [y for y in all_yogas if y["type"] == "Neutral" and y["present"]]
906
+
907
+ total_present = len(benefic_yogas) + len(malefic_yogas) + len(neutral_present)
908
+
909
+ if len(benefic_yogas) > len(malefic_yogas) * 2:
910
+ quality = "highly auspicious"
911
+ elif len(benefic_yogas) > len(malefic_yogas):
912
+ quality = "largely auspicious"
913
+ elif len(malefic_yogas) > len(benefic_yogas):
914
+ quality = "challenging with notable malefic influences"
915
+ else:
916
+ quality = "mixed"
917
+
918
+ strong_benefic = [y["name"] for y in benefic_yogas if y["strength"] == "Strong"]
919
+ strong_malefic = [y["name"] for y in malefic_yogas if y["strength"] == "Strong"]
920
+
921
+ summary_parts = [
922
+ f"Found {total_present} active yoga(s): "
923
+ f"{len(benefic_yogas)} benefic, {len(malefic_yogas)} malefic, "
924
+ f"{len(neutral_present)} neutral. The overall chart is {quality}."
925
+ ]
926
+ if strong_benefic:
927
+ summary_parts.append(f"Strong benefic yogas: {', '.join(strong_benefic)}.")
928
+ if strong_malefic:
929
+ summary_parts.append(f"Strong malefic yogas: {', '.join(strong_malefic)}.")
930
+
931
+ return {
932
+ "benefic_yogas": benefic_yogas,
933
+ "malefic_yogas": malefic_yogas,
934
+ "summary": " ".join(summary_parts),
935
+ }