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,465 @@
1
+ """Arishta Yogas: Longevity assessment and affliction indicators."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from kundali_lib.vedic.constants import RASHI_LORDS, RASHIS
6
+
7
+ # ---------------------------------------------------------------------------
8
+ # Reference data
9
+ # ---------------------------------------------------------------------------
10
+
11
+ RASHI_LORDS_MAP: dict[str, str] = {r: RASHI_LORDS[i] for i, r in enumerate(RASHIS)}
12
+
13
+ EXALTATION: dict[str, str] = {
14
+ "Sun": "Aries",
15
+ "Moon": "Taurus",
16
+ "Mars": "Capricorn",
17
+ "Mercury": "Virgo",
18
+ "Jupiter": "Cancer",
19
+ "Venus": "Pisces",
20
+ "Saturn": "Libra",
21
+ }
22
+
23
+ DEBILITATION: dict[str, str] = {
24
+ "Sun": "Libra",
25
+ "Moon": "Scorpio",
26
+ "Mars": "Cancer",
27
+ "Mercury": "Pisces",
28
+ "Jupiter": "Capricorn",
29
+ "Venus": "Virgo",
30
+ "Saturn": "Aries",
31
+ }
32
+
33
+ OWN_SIGNS: dict[str, list[str]] = {
34
+ "Sun": ["Leo"],
35
+ "Moon": ["Cancer"],
36
+ "Mars": ["Aries", "Scorpio"],
37
+ "Mercury": ["Gemini", "Virgo"],
38
+ "Jupiter": ["Sagittarius", "Pisces"],
39
+ "Venus": ["Taurus", "Libra"],
40
+ "Saturn": ["Capricorn", "Aquarius"],
41
+ }
42
+
43
+ FRIENDLY: dict[str, set[str]] = {
44
+ "Sun": {"Moon", "Mars", "Jupiter"},
45
+ "Moon": {"Sun", "Mercury"},
46
+ "Mars": {"Sun", "Moon", "Jupiter"},
47
+ "Mercury": {"Sun", "Venus"},
48
+ "Jupiter": {"Sun", "Moon", "Mars"},
49
+ "Venus": {"Mercury", "Saturn"},
50
+ "Saturn": {"Mercury", "Venus"},
51
+ }
52
+
53
+ ENEMY: dict[str, set[str]] = {
54
+ "Sun": {"Venus", "Saturn"},
55
+ "Moon": {"Rahu", "Ketu"},
56
+ "Mars": {"Mercury"},
57
+ "Mercury": {"Moon"},
58
+ "Jupiter": {"Mercury", "Venus"},
59
+ "Venus": {"Sun", "Moon"},
60
+ "Saturn": {"Sun", "Moon", "Mars"},
61
+ }
62
+
63
+ MALEFIC_PLANETS = {"Sun", "Mars", "Saturn", "Rahu", "Ketu"}
64
+ BENEFIC_PLANETS = {"Moon", "Mercury", "Jupiter", "Venus"}
65
+ DUSTHANA_HOUSES = {6, 8, 12}
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # Pindayu years contribution
69
+ # ---------------------------------------------------------------------------
70
+
71
+ PINDAYU_YEARS: dict[str, dict[str, float]] = {
72
+ "Sun": {
73
+ "exalt": 19,
74
+ "own": 19,
75
+ "friendly": 15,
76
+ "neutral": 12,
77
+ "enemy": 9,
78
+ "debil": 6,
79
+ },
80
+ "Moon": {
81
+ "exalt": 25,
82
+ "own": 25,
83
+ "friendly": 21,
84
+ "neutral": 16,
85
+ "enemy": 12,
86
+ "debil": 8,
87
+ },
88
+ "Mars": {
89
+ "exalt": 15,
90
+ "own": 15,
91
+ "friendly": 12,
92
+ "neutral": 9,
93
+ "enemy": 7,
94
+ "debil": 5,
95
+ },
96
+ "Mercury": {
97
+ "exalt": 20,
98
+ "own": 20,
99
+ "friendly": 16,
100
+ "neutral": 12,
101
+ "enemy": 9,
102
+ "debil": 7,
103
+ },
104
+ "Jupiter": {
105
+ "exalt": 18,
106
+ "own": 18,
107
+ "friendly": 14,
108
+ "neutral": 11,
109
+ "enemy": 8,
110
+ "debil": 6,
111
+ },
112
+ "Venus": {
113
+ "exalt": 20,
114
+ "own": 20,
115
+ "friendly": 16,
116
+ "neutral": 12,
117
+ "enemy": 9,
118
+ "debil": 7,
119
+ },
120
+ "Saturn": {
121
+ "exalt": 17,
122
+ "own": 17,
123
+ "friendly": 14,
124
+ "neutral": 11,
125
+ "enemy": 8,
126
+ "debil": 5,
127
+ },
128
+ }
129
+
130
+ # ---------------------------------------------------------------------------
131
+ # Balarishta conditions
132
+ # ---------------------------------------------------------------------------
133
+
134
+ BALARISHTA_CONDITIONS: list[dict] = [
135
+ {
136
+ "name": "Moon in Papakartari",
137
+ "check": "Moon hemmed between malefics in 6th/8th from Moon",
138
+ "severity": "High",
139
+ },
140
+ {
141
+ "name": "Moon with Rahu or Ketu at birth",
142
+ "check": "Moon conjunct Rahu or Ketu (same house)",
143
+ "severity": "High",
144
+ },
145
+ {
146
+ "name": "Malefics in 1, 8, 12 without benefic aspect",
147
+ "check": "Sun/Mars/Saturn/Rahu/Ketu in 1st/8th/12th houses",
148
+ "severity": "Medium",
149
+ },
150
+ {
151
+ "name": "Moon in dusthana in weak state",
152
+ "check": "Moon in 6th, 8th, or 12th in debilitation or enemy's sign",
153
+ "severity": "Medium",
154
+ },
155
+ {
156
+ "name": "Exchange between 1st and 8th lords",
157
+ "check": "Lord of 8th in 1st or Lord of 1st in 8th",
158
+ "severity": "High",
159
+ },
160
+ ]
161
+
162
+
163
+ # ---------------------------------------------------------------------------
164
+ # Internal helpers
165
+ # ---------------------------------------------------------------------------
166
+
167
+
168
+ def _get_planet(positions: list[dict], name: str) -> dict | None:
169
+ for p in positions:
170
+ if p["name"] == name:
171
+ return p
172
+ return None
173
+
174
+
175
+ def _planet_sign_relationship(planet_name: str, rashi: str) -> str:
176
+ """Return 'exalt', 'debil', 'own', 'friendly', 'enemy', or 'neutral'."""
177
+ if EXALTATION.get(planet_name) == rashi:
178
+ return "exalt"
179
+ if DEBILITATION.get(planet_name) == rashi:
180
+ return "debil"
181
+ if rashi in OWN_SIGNS.get(planet_name, []):
182
+ return "own"
183
+ lord = RASHI_LORDS_MAP.get(rashi)
184
+ if lord in FRIENDLY.get(planet_name, set()):
185
+ return "friendly"
186
+ if lord in ENEMY.get(planet_name, set()):
187
+ return "enemy"
188
+ return "neutral"
189
+
190
+
191
+ def _pindayu_for(planet: dict) -> float:
192
+ name = planet["name"]
193
+ if name not in PINDAYU_YEARS:
194
+ return 0.0
195
+ rel = _planet_sign_relationship(name, planet.get("rashi", ""))
196
+ return float(PINDAYU_YEARS[name].get(rel, PINDAYU_YEARS[name]["neutral"]))
197
+
198
+
199
+ def _planets_in_house(positions: list[dict], house: int) -> list[str]:
200
+ return [p["name"] for p in positions if p.get("house", 0) == house]
201
+
202
+
203
+ def _has_benefic_aspect(positions: list[dict], house: int, asc_sign: str) -> bool:
204
+ """Simplified: check if any benefic planet aspects the given house (7th aspect)."""
205
+ for p in positions:
206
+ if p["name"] in BENEFIC_PLANETS:
207
+ planet_house = p.get("house", 0)
208
+ if (planet_house - 1) % 12 == (house - 7) % 12:
209
+ return True
210
+ return False
211
+
212
+
213
+ # ---------------------------------------------------------------------------
214
+ # Balarishta condition evaluators
215
+ # ---------------------------------------------------------------------------
216
+
217
+
218
+ def _eval_moon_papakartari(positions: list[dict]) -> bool:
219
+ """Moon hemmed between malefics (malefic in 6th and 8th from Moon)."""
220
+ moon = _get_planet(positions, "Moon")
221
+ if not moon:
222
+ return False
223
+ moon_house = moon.get("house", 0)
224
+ house_6_from_moon = (moon_house + 4) % 12 + 1 # 6th from Moon
225
+ house_8_from_moon = (moon_house + 6) % 12 + 1 # 8th from Moon
226
+ planets_6 = _planets_in_house(positions, house_6_from_moon)
227
+ planets_8 = _planets_in_house(positions, house_8_from_moon)
228
+ has_malefic_6 = any(n in MALEFIC_PLANETS for n in planets_6)
229
+ has_malefic_8 = any(n in MALEFIC_PLANETS for n in planets_8)
230
+ return has_malefic_6 and has_malefic_8
231
+
232
+
233
+ def _eval_moon_with_nodes(positions: list[dict]) -> bool:
234
+ moon = _get_planet(positions, "Moon")
235
+ if not moon:
236
+ return False
237
+ moon_house = moon.get("house", 0)
238
+ return any(
239
+ _get_planet(positions, n)
240
+ and _get_planet(positions, n).get("house", 0) == moon_house
241
+ for n in ("Rahu", "Ketu")
242
+ )
243
+
244
+
245
+ def _eval_malefics_in_1_8_12(positions: list[dict], asc_sign: str) -> bool:
246
+ afflicted_houses = set()
247
+ for p in positions:
248
+ if p["name"] in MALEFIC_PLANETS and p.get("house", 0) in {1, 8, 12}:
249
+ if not _has_benefic_aspect(positions, p.get("house", 0), asc_sign):
250
+ afflicted_houses.add(p.get("house", 0))
251
+ return len(afflicted_houses) >= 2
252
+
253
+
254
+ def _eval_moon_in_dusthana_weak(positions: list[dict]) -> bool:
255
+ moon = _get_planet(positions, "Moon")
256
+ if not moon:
257
+ return False
258
+ house = moon.get("house", 0)
259
+ if house not in DUSTHANA_HOUSES:
260
+ return False
261
+ rel = _planet_sign_relationship("Moon", moon.get("rashi", ""))
262
+ return rel in ("debil", "enemy")
263
+
264
+
265
+ def _eval_1st_8th_exchange(positions: list[dict], asc_sign: str) -> bool:
266
+ asc_idx = RASHIS.index(asc_sign)
267
+ eighth_sign = RASHIS[(asc_idx + 7) % 12]
268
+ first_lord = RASHI_LORDS_MAP.get(asc_sign)
269
+ eighth_lord = RASHI_LORDS_MAP.get(eighth_sign)
270
+ if not first_lord or not eighth_lord:
271
+ return False
272
+ first_lord_planet = _get_planet(positions, first_lord)
273
+ eighth_lord_planet = _get_planet(positions, eighth_lord)
274
+ if not first_lord_planet or not eighth_lord_planet:
275
+ return False
276
+ # Exchange: 1st lord in 8th house or 8th lord in 1st house
277
+ return (
278
+ first_lord_planet.get("house", 0) == 8
279
+ or eighth_lord_planet.get("house", 0) == 1
280
+ )
281
+
282
+
283
+ # ---------------------------------------------------------------------------
284
+ # Public API
285
+ # ---------------------------------------------------------------------------
286
+
287
+
288
+ def get_arishta_yogas(base_chart: dict) -> dict:
289
+ """Assess longevity and Arishta yogas from a birth chart.
290
+
291
+ Args:
292
+ base_chart: Output of ``build_chart()``.
293
+
294
+ Returns:
295
+ dict with Balarishta conditions, longevity category, Pindayu years
296
+ estimate, 8th house analysis, and overall Arishta assessment.
297
+ """
298
+ positions: list[dict] = base_chart.get("planetary_positions", [])
299
+ asc_info: dict = base_chart.get("ascendant", {})
300
+ asc_sign: str = asc_info.get("rashi", RASHIS[0])
301
+ asc_idx = RASHIS.index(asc_sign)
302
+
303
+ # ── Balarishta evaluation ─────────────────────────────────────────────────
304
+ evaluators = [
305
+ _eval_moon_papakartari,
306
+ _eval_moon_with_nodes,
307
+ lambda pos: _eval_malefics_in_1_8_12(pos, asc_sign),
308
+ _eval_moon_in_dusthana_weak,
309
+ lambda pos: _eval_1st_8th_exchange(pos, asc_sign),
310
+ ]
311
+
312
+ balarishta_results: list[dict] = []
313
+ for condition, evaluator in zip(BALARISHTA_CONDITIONS, evaluators):
314
+ present = evaluator(positions)
315
+ balarishta_results.append(
316
+ {
317
+ "name": condition["name"],
318
+ "present": present,
319
+ "description": condition["check"],
320
+ "severity": condition["severity"],
321
+ }
322
+ )
323
+
324
+ high_count = sum(
325
+ 1 for b in balarishta_results if b["present"] and b["severity"] == "High"
326
+ )
327
+ med_count = sum(
328
+ 1 for b in balarishta_results if b["present"] and b["severity"] == "Medium"
329
+ )
330
+ arishta_present = high_count >= 1 or med_count >= 2
331
+
332
+ # ── Arishta type ──────────────────────────────────────────────────────────
333
+ alparishta = False
334
+ # Alparishta: malefics simultaneously in 1, 8, 12
335
+ houses_with_malefics = {
336
+ p.get("house", 0) for p in positions if p["name"] in MALEFIC_PLANETS
337
+ }
338
+ if houses_with_malefics.issuperset({1, 8, 12}):
339
+ alparishta = True
340
+ # Classic Alparishta: Saturn in Lagna, Moon in 8th, Sun in 7th
341
+ saturn = _get_planet(positions, "Saturn")
342
+ moon = _get_planet(positions, "Moon")
343
+ sun = _get_planet(positions, "Sun")
344
+ if (
345
+ saturn
346
+ and saturn.get("house", 0) == 1
347
+ and moon
348
+ and moon.get("house", 0) == 8
349
+ and sun
350
+ and sun.get("house", 0) == 7
351
+ ):
352
+ alparishta = True
353
+
354
+ if arishta_present and alparishta:
355
+ arishta_type: str | None = "Alparishta (short life threat, 0-32 years)"
356
+ elif arishta_present and high_count >= 1:
357
+ arishta_type = "Balarishta (early life affliction)"
358
+ elif arishta_present:
359
+ arishta_type = "Madhyarishta (medium life affliction)"
360
+ else:
361
+ arishta_type = None
362
+
363
+ # ── Pindayu calculation (Sun + Moon + Lagna lord) ─────────────────────────
364
+ lagna_lord_name = RASHI_LORDS_MAP.get(asc_sign, "")
365
+ sun_planet = _get_planet(positions, "Sun")
366
+ moon_planet = _get_planet(positions, "Moon")
367
+ lagna_lord_planet = _get_planet(positions, lagna_lord_name)
368
+
369
+ pindayu_sun = _pindayu_for(sun_planet) if sun_planet else 0.0
370
+ pindayu_moon = _pindayu_for(moon_planet) if moon_planet else 0.0
371
+ pindayu_ll = _pindayu_for(lagna_lord_planet) if lagna_lord_planet else 0.0
372
+ pindayu_total = pindayu_sun + pindayu_moon + pindayu_ll
373
+
374
+ # ── Longevity category ────────────────────────────────────────────────────
375
+ if pindayu_total >= 66 and not arishta_present:
376
+ longevity_category = "Long (75+ years)"
377
+ elif pindayu_total >= 36:
378
+ longevity_category = "Medium (36-75 years)"
379
+ else:
380
+ longevity_category = "Short (<36 years)"
381
+
382
+ # ── Eighth house analysis ─────────────────────────────────────────────────
383
+ eighth_sign = RASHIS[(asc_idx + 7) % 12]
384
+ eighth_lord_name = RASHI_LORDS_MAP[eighth_sign]
385
+ eighth_lord_planet = _get_planet(positions, eighth_lord_name)
386
+ eighth_lord_position = (
387
+ f"House {eighth_lord_planet.get('house', '?')} in {eighth_lord_planet.get('rashi', '?')}"
388
+ if eighth_lord_planet
389
+ else "Unknown"
390
+ )
391
+ planets_in_8 = _planets_in_house(positions, 8)
392
+
393
+ eighth_rel = _planet_sign_relationship(
394
+ eighth_lord_name,
395
+ eighth_lord_planet.get("rashi", "") if eighth_lord_planet else "",
396
+ )
397
+ eighth_strength = (
398
+ "Strong"
399
+ if eighth_rel in ("exalt", "own")
400
+ else "Weak"
401
+ if eighth_rel in ("debil", "enemy")
402
+ else "Moderate"
403
+ )
404
+
405
+ # ── Cancellation factors ──────────────────────────────────────────────────
406
+ cancellation: list[str] = []
407
+ jupiter = _get_planet(positions, "Jupiter")
408
+ if jupiter and jupiter.get("house", 0) in {1, 5, 9}:
409
+ cancellation.append("Jupiter in trikona mitigates Arishta")
410
+ if (
411
+ moon_planet
412
+ and _planet_sign_relationship("Moon", moon_planet.get("rashi", "")) == "exalt"
413
+ ):
414
+ cancellation.append("Exalted Moon reduces early affliction severity")
415
+ if lagna_lord_planet and lagna_lord_planet.get("house", 0) in {1, 4, 7, 10}:
416
+ cancellation.append("Lagna lord in kendra provides resilience")
417
+ if not cancellation and arishta_present:
418
+ cancellation.append("No strong cancellation factors found")
419
+ elif not cancellation:
420
+ cancellation.append("Chart has no significant Arishta; no cancellation needed")
421
+
422
+ # ── Summary ───────────────────────────────────────────────────────────────
423
+ active_balarishta = [b["name"] for b in balarishta_results if b["present"]]
424
+ summary_parts = [
425
+ f"Longevity category: {longevity_category}.",
426
+ f"Pindayu indicator: Sun={pindayu_sun}, Moon={pindayu_moon}, "
427
+ f"Lagna lord={pindayu_ll} → Total={pindayu_total} years (indicative).",
428
+ ]
429
+ if arishta_present:
430
+ summary_parts.append(
431
+ f"Arishta type: {arishta_type}. Active conditions: {', '.join(active_balarishta)}."
432
+ )
433
+ else:
434
+ summary_parts.append("No significant Arishta yoga detected.")
435
+ if cancellation:
436
+ summary_parts.append(f"Mitigating factors: {'; '.join(cancellation)}.")
437
+
438
+ return {
439
+ "balarishta_conditions": balarishta_results,
440
+ "longevity_category": longevity_category,
441
+ "pindayu_years": {
442
+ "Sun": pindayu_sun,
443
+ "Moon": pindayu_moon,
444
+ "Lagna_lord": pindayu_ll,
445
+ "total_indicator": pindayu_total,
446
+ "note": (
447
+ "Pindayu sum for Sun + Moon + Lagna lord only. "
448
+ "Full Pindayu includes all seven planets with various divisors."
449
+ ),
450
+ },
451
+ "eighth_house_analysis": {
452
+ "eighth_lord": eighth_lord_name,
453
+ "eighth_lord_position": eighth_lord_position,
454
+ "planets_in_eighth": planets_in_8,
455
+ "strength": eighth_strength,
456
+ },
457
+ "arishta_present": arishta_present,
458
+ "arishta_type": arishta_type,
459
+ "cancellation_factors": cancellation,
460
+ "summary": " ".join(summary_parts),
461
+ "disclaimer": (
462
+ "Longevity calculation is indicative only. "
463
+ "Modern Vedic astrologers use this as one of many tools."
464
+ ),
465
+ }
@@ -0,0 +1,213 @@
1
+ """Ashtakavarga: mathematical benefic point system for transit predictions.
2
+
3
+ Each of 7 classical planets (Sun through Saturn) receives benefic points (0 or 1)
4
+ contributed by every other planet plus the Lagna, based on traditional Parashari
5
+ tables. The resulting per-planet scores (Bhinnashtakavarga, BAV) are summed into
6
+ a 12-house total (Sarvashtakavarga, SAV) which guides transit interpretation.
7
+ """
8
+
9
+ # ── Parashari contribution tables ────────────────────────────────────────────
10
+ # For each target planet the dict maps contributor → list of house offsets
11
+ # (1-based, measured FROM the contributor) that receive one benefic point.
12
+
13
+ SUN_CONTRIBUTIONS: dict[str, list[int]] = {
14
+ "Sun": [1, 2, 4, 7, 8, 9, 10, 11],
15
+ "Moon": [3, 6, 10, 11],
16
+ "Mars": [1, 2, 4, 7, 8, 9, 10, 11],
17
+ "Mercury": [3, 5, 6, 9, 12],
18
+ "Jupiter": [5, 6, 9, 11],
19
+ "Venus": [6, 7, 12],
20
+ "Saturn": [1, 2, 4, 7, 8, 9, 10, 11],
21
+ "Lagna": [3, 4, 6, 10, 11, 12],
22
+ }
23
+
24
+ MOON_CONTRIBUTIONS: dict[str, list[int]] = {
25
+ "Sun": [3, 6, 7, 8, 10, 11],
26
+ "Moon": [1, 3, 6, 7, 10, 11],
27
+ "Mars": [2, 3, 5, 6, 9, 10, 11],
28
+ "Mercury": [1, 3, 4, 5, 7, 8, 10, 11],
29
+ "Jupiter": [1, 4, 7, 8, 10, 11, 12],
30
+ "Venus": [3, 4, 5, 7, 9, 10, 11],
31
+ "Saturn": [3, 5, 6, 11],
32
+ "Lagna": [3, 6, 10, 11],
33
+ }
34
+
35
+ MARS_CONTRIBUTIONS: dict[str, list[int]] = {
36
+ "Sun": [3, 5, 6, 10, 11],
37
+ "Moon": [3, 6, 11],
38
+ "Mars": [1, 2, 4, 7, 8, 10, 11],
39
+ "Mercury": [3, 5, 6],
40
+ "Jupiter": [6, 10, 11, 12],
41
+ "Venus": [6, 8, 11, 12],
42
+ "Saturn": [1, 4, 7, 8, 9, 10, 11],
43
+ "Lagna": [1, 3, 6, 10, 11],
44
+ }
45
+
46
+ MERCURY_CONTRIBUTIONS: dict[str, list[int]] = {
47
+ "Sun": [5, 6, 9, 11, 12],
48
+ "Moon": [2, 4, 6, 8, 10, 11],
49
+ "Mars": [1, 2, 4, 7, 8, 9, 10, 11],
50
+ "Mercury": [1, 3, 5, 6, 9, 10, 11, 12],
51
+ "Jupiter": [6, 8, 11, 12],
52
+ "Venus": [1, 2, 3, 4, 5, 8, 9, 11],
53
+ "Saturn": [1, 2, 4, 7, 8, 9, 10, 11],
54
+ "Lagna": [1, 2, 4, 6, 8, 10, 11],
55
+ }
56
+
57
+ JUPITER_CONTRIBUTIONS: dict[str, list[int]] = {
58
+ "Sun": [1, 2, 3, 4, 7, 8, 9, 10, 11],
59
+ "Moon": [2, 5, 7, 9, 11],
60
+ "Mars": [1, 2, 4, 7, 8, 10, 11],
61
+ "Mercury": [1, 2, 4, 5, 6, 9, 10, 11],
62
+ "Jupiter": [1, 2, 3, 4, 7, 8, 10, 11],
63
+ "Venus": [2, 5, 6, 9, 10, 11],
64
+ "Saturn": [3, 5, 6, 12],
65
+ "Lagna": [1, 2, 4, 5, 6, 7, 9, 10, 11],
66
+ }
67
+
68
+ VENUS_CONTRIBUTIONS: dict[str, list[int]] = {
69
+ "Sun": [8, 11, 12],
70
+ "Moon": [1, 2, 3, 4, 5, 8, 9, 11, 12],
71
+ "Mars": [3, 4, 6, 9, 11, 12],
72
+ "Mercury": [3, 5, 6, 9, 11],
73
+ "Jupiter": [5, 8, 9, 10, 11],
74
+ "Venus": [1, 2, 3, 4, 5, 8, 9, 10, 11],
75
+ "Saturn": [3, 4, 5, 8, 9, 10, 11],
76
+ "Lagna": [1, 2, 3, 4, 5, 8, 9, 11],
77
+ }
78
+
79
+ SATURN_CONTRIBUTIONS: dict[str, list[int]] = {
80
+ "Sun": [1, 2, 4, 7, 8, 10, 11],
81
+ "Moon": [3, 6, 11],
82
+ "Mars": [3, 5, 6, 10, 11, 12],
83
+ "Mercury": [6, 8, 9, 10, 11, 12],
84
+ "Jupiter": [5, 6, 11, 12],
85
+ "Venus": [6, 11, 12],
86
+ "Saturn": [3, 5, 6, 11],
87
+ "Lagna": [1, 3, 4, 6, 10, 11],
88
+ }
89
+
90
+ # Ordered list of target planets and their contribution tables
91
+ _TARGET_PLANETS: list[str] = [
92
+ "Sun",
93
+ "Moon",
94
+ "Mars",
95
+ "Mercury",
96
+ "Jupiter",
97
+ "Venus",
98
+ "Saturn",
99
+ ]
100
+
101
+ _CONTRIBUTIONS: dict[str, dict[str, list[int]]] = {
102
+ "Sun": SUN_CONTRIBUTIONS,
103
+ "Moon": MOON_CONTRIBUTIONS,
104
+ "Mars": MARS_CONTRIBUTIONS,
105
+ "Mercury": MERCURY_CONTRIBUTIONS,
106
+ "Jupiter": JUPITER_CONTRIBUTIONS,
107
+ "Venus": VENUS_CONTRIBUTIONS,
108
+ "Saturn": SATURN_CONTRIBUTIONS,
109
+ }
110
+
111
+ # BAV strength thresholds (points out of 8 per house)
112
+ _BAV_STRONG = 5
113
+ _BAV_MODERATE = 4
114
+ # SAV strength thresholds (sum of 7 planets = max ~337 per house)
115
+ _SAV_EXCELLENT = 28
116
+ _SAV_GOOD = 22
117
+ _SAV_AVERAGE = 16
118
+
119
+
120
+ # ── Private helpers ───────────────────────────────────────────────────────────
121
+
122
+
123
+ def _empty_house_scores() -> dict[str, int]:
124
+ return {f"House_{h}": 0 for h in range(1, 13)}
125
+
126
+
127
+ def _compute_bav(
128
+ target_planet: str,
129
+ planet_houses: dict[str, int],
130
+ lagna_house: int = 1,
131
+ ) -> dict[str, int]:
132
+ """Compute Bhinnashtakavarga (BAV) for one target planet.
133
+
134
+ For each contributing planet / Lagna, for each benefic offset in its table,
135
+ the house that lands at (contributor_house + offset - 1) receives +1 point.
136
+ """
137
+ scores = _empty_house_scores()
138
+ table = _CONTRIBUTIONS[target_planet]
139
+
140
+ for contributor, offsets in table.items():
141
+ if contributor == "Lagna":
142
+ contrib_house = lagna_house
143
+ else:
144
+ contrib_house = planet_houses.get(contributor)
145
+ if contrib_house is None:
146
+ continue # planet absent from chart (e.g. outer planets)
147
+
148
+ for offset in offsets:
149
+ # offset=1 → same house as contributor; offset=2 → next house; etc.
150
+ target_house = (contrib_house - 1 + offset - 1) % 12 + 1
151
+ scores[f"House_{target_house}"] += 1
152
+
153
+ scores["total"] = sum(scores[f"House_{h}"] for h in range(1, 13))
154
+ return scores
155
+
156
+
157
+ def _sav_interpretation(score: int) -> str:
158
+ if score >= _SAV_EXCELLENT:
159
+ return "Excellent (>=28)"
160
+ if score >= _SAV_GOOD:
161
+ return "Good (22-27)"
162
+ if score >= _SAV_AVERAGE:
163
+ return "Average (16-21)"
164
+ return "Weak (<16)"
165
+
166
+
167
+ # ── Public API ────────────────────────────────────────────────────────────────
168
+
169
+
170
+ def get_ashtakavarga(base_chart: dict) -> dict:
171
+ """Calculate Ashtakavarga from a built chart dict.
172
+
173
+ Args:
174
+ base_chart: Output of ``build_chart()`` — must contain
175
+ ``planetary_positions`` (list with ``name`` and ``house`` keys).
176
+
177
+ Returns:
178
+ dict with three keys:
179
+
180
+ * ``bhinnashtakavarga``: per-planet 12-house benefic scores + total.
181
+ * ``sarvashtakavarga``: sum of all 7 planets per house + total.
182
+ * ``interpretation``: SAV strength label per house.
183
+ """
184
+ # Extract house numbers for each planet
185
+ planet_houses: dict[str, int] = {
186
+ p["name"]: p["house"] for p in base_chart["planetary_positions"]
187
+ }
188
+
189
+ # Lagna always occupies house 1 by definition
190
+ lagna_house = 1
191
+
192
+ # ── Bhinnashtakavarga (BAV) ──────────────────────────────────────────────
193
+ bav: dict[str, dict[str, int]] = {}
194
+ for target_planet in _TARGET_PLANETS:
195
+ bav[target_planet] = _compute_bav(target_planet, planet_houses, lagna_house)
196
+
197
+ # ── Sarvashtakavarga (SAV) ───────────────────────────────────────────────
198
+ sav = _empty_house_scores()
199
+ for planet in _TARGET_PLANETS:
200
+ for h in range(1, 13):
201
+ sav[f"House_{h}"] += bav[planet][f"House_{h}"]
202
+ sav["total"] = sum(sav[f"House_{h}"] for h in range(1, 13))
203
+
204
+ # ── Interpretation ───────────────────────────────────────────────────────
205
+ interpretation: dict[str, str] = {
206
+ f"House_{h}": _sav_interpretation(sav[f"House_{h}"]) for h in range(1, 13)
207
+ }
208
+
209
+ return {
210
+ "bhinnashtakavarga": bav,
211
+ "sarvashtakavarga": sav,
212
+ "interpretation": interpretation,
213
+ }