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,229 @@
1
+ """Upagrahas: Shadow planets and sub-planets in Vedic astrology."""
2
+
3
+ import math
4
+
5
+ try:
6
+ import swisseph as swe
7
+
8
+ _SWE_AVAILABLE = True
9
+ except ImportError: # pragma: no cover
10
+ _SWE_AVAILABLE = False
11
+
12
+ # ---------------------------------------------------------------------------
13
+ # Reference data
14
+ # ---------------------------------------------------------------------------
15
+
16
+ RASHIS = [
17
+ "Aries",
18
+ "Taurus",
19
+ "Gemini",
20
+ "Cancer",
21
+ "Leo",
22
+ "Virgo",
23
+ "Libra",
24
+ "Scorpio",
25
+ "Sagittarius",
26
+ "Capricorn",
27
+ "Aquarius",
28
+ "Pisces",
29
+ ]
30
+
31
+ NAKSHATRAS = [
32
+ "Ashwini",
33
+ "Bharani",
34
+ "Krittika",
35
+ "Rohini",
36
+ "Mrigashira",
37
+ "Ardra",
38
+ "Punarvasu",
39
+ "Pushya",
40
+ "Ashlesha",
41
+ "Magha",
42
+ "Purva Phalguni",
43
+ "Uttara Phalguni",
44
+ "Hasta",
45
+ "Chitra",
46
+ "Swati",
47
+ "Vishakha",
48
+ "Anuradha",
49
+ "Jyeshtha",
50
+ "Mula",
51
+ "Purva Ashadha",
52
+ "Uttara Ashadha",
53
+ "Shravana",
54
+ "Dhanishta",
55
+ "Shatabhisha",
56
+ "Purva Bhadrapada",
57
+ "Uttara Bhadrapada",
58
+ "Revati",
59
+ ]
60
+
61
+ # How many portions into the day each weekday's Gulika begins.
62
+ # Weekday index: 0=Sunday, 1=Monday, ..., 6=Saturday
63
+ # Each day is split into 8 equal portions; Gulika occupies one of them.
64
+ _GULIKA_WEEKDAY_PORTION = {
65
+ 0: 6, # Sunday — 7th portion (index 6)
66
+ 1: 0, # Monday — 1st portion (index 0)
67
+ 2: 1, # Tuesday — 2nd portion (index 1)
68
+ 3: 2, # Wednesday— 3rd portion (index 2)
69
+ 4: 3, # Thursday — 4th portion (index 3)
70
+ 5: 4, # Friday — 5th portion (index 4)
71
+ 6: 5, # Saturday — 6th portion (index 5)
72
+ }
73
+
74
+ # Mandi (start of portion, vs Gulika which is midpoint)
75
+ _MANDI_WEEKDAY_PORTION = _GULIKA_WEEKDAY_PORTION # same portions, different point
76
+
77
+ _UPAGRAHA_SIGNIFICANCE = {
78
+ "Gulika": "Most malefic upagraha, son of Saturn. Very important in KP and Nadi astrology.",
79
+ "Mandi": "Shadow of Gulika; marks the start of Saturn's portion. Strongly malefic.",
80
+ "Dhuma": "Smoky planet; Sun + 133°20'. Associated with obstacles and disruptions.",
81
+ "Vyatipata": "Calamity indicator; 360° − Dhuma. Highly inauspicious transit point.",
82
+ "Parivesha": "Encirclement; Vyatipata + 180°. Related to entrapment or enclosure.",
83
+ "Indra_Chapa": "Indra's bow; 360° − Parivesha (= Kodanda). Indicates ambition and pride.",
84
+ "Upaketu": "Comet-like sub-planet; Sun + 30°. Associated with sudden events.",
85
+ }
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # Helpers
89
+ # ---------------------------------------------------------------------------
90
+
91
+
92
+ def _rashi_from_lon(lon: float) -> str:
93
+ """Return the rashi name for an absolute sidereal longitude."""
94
+ return RASHIS[int(lon / 30) % 12]
95
+
96
+
97
+ def _nakshatra_from_lon(lon: float) -> str:
98
+ """Return the nakshatra name for an absolute sidereal longitude."""
99
+ return NAKSHATRAS[int(lon / (360 / 27)) % 27]
100
+
101
+
102
+ def _house_from_lon(lon: float, asc_lon: float) -> int:
103
+ """Return the house number (1–12) for a longitude given the ascendant longitude."""
104
+ diff = (lon - asc_lon) % 360
105
+ return int(diff / 30) + 1
106
+
107
+
108
+ def _normalise(lon: float) -> float:
109
+ """Bring a longitude into the [0, 360) range."""
110
+ return lon % 360
111
+
112
+
113
+ def _build_upagraha_entry(name: str, lon: float, asc_lon: float) -> dict:
114
+ lon = _normalise(lon)
115
+ return {
116
+ "longitude": round(lon, 4),
117
+ "rashi": _rashi_from_lon(lon),
118
+ "degree_in_sign": round(lon % 30, 4),
119
+ "nakshatra": _nakshatra_from_lon(lon),
120
+ "house": _house_from_lon(lon, asc_lon),
121
+ "significance": _UPAGRAHA_SIGNIFICANCE.get(name, ""),
122
+ }
123
+
124
+
125
+ def _get_sun_longitude(julian_day: float, ayanamsa_offset: float) -> float:
126
+ """Compute the Sun's sidereal longitude using swisseph (if available)."""
127
+ if _SWE_AVAILABLE:
128
+ flags = swe.FLG_SWIEPH | swe.FLG_SPEED | swe.FLG_SIDEREAL
129
+ result, _ = swe.calc_ut(julian_day, swe.SUN, flags)
130
+ return result[0] % 360
131
+ # Fallback: very rough tropical approximation minus ayanamsa
132
+ # (useful only if swisseph is genuinely absent)
133
+ T = (julian_day - 2451545.0) / 36525.0
134
+ lon_tropical = (280.46646 + 36000.76983 * T) % 360
135
+ return (lon_tropical - ayanamsa_offset) % 360
136
+
137
+
138
+ def _get_ascendant_longitude(
139
+ julian_day: float, birth_lat: float, birth_lon: float, ayanamsa_offset: float
140
+ ) -> float:
141
+ """Compute the sidereal ascendant longitude."""
142
+ if _SWE_AVAILABLE:
143
+ houses, ascmc = swe.houses(julian_day, birth_lat, birth_lon, b"P")
144
+ asc_tropical = ascmc[0]
145
+ return (asc_tropical - ayanamsa_offset) % 360
146
+ # Rough fallback: estimate LST-based ascendant
147
+ T = (julian_day - 2451545.0) / 36525.0
148
+ gst = (280.46061837 + 360.98564736629 * (julian_day - 2451545.0)) % 360
149
+ lst = (gst + birth_lon) % 360
150
+ return (lst - ayanamsa_offset) % 360
151
+
152
+
153
+ # ---------------------------------------------------------------------------
154
+ # Main function
155
+ # ---------------------------------------------------------------------------
156
+
157
+
158
+ def get_upagrahas(
159
+ julian_day: float, birth_lat: float, birth_lon: float, ayanamsa_offset: float
160
+ ) -> dict:
161
+ """Calculate all major upagrahas (shadow sub-planets).
162
+
163
+ Parameters
164
+ ----------
165
+ julian_day : float
166
+ Julian Day Number for the birth moment (UT).
167
+ birth_lat : float
168
+ Geographic latitude of birth location (degrees, N positive).
169
+ birth_lon : float
170
+ Geographic longitude of birth location (degrees, E positive).
171
+ ayanamsa_offset : float
172
+ Ayanamsa in degrees at the time of birth
173
+ (e.g. from ``swe.get_ayanamsa_ut(jd)``).
174
+
175
+ Returns
176
+ -------
177
+ dict
178
+ Keys: Gulika, Mandi, Dhuma, Vyatipata, Parivesha, Indra_Chapa, Upaketu.
179
+ Each value is a dict with longitude, rashi, degree_in_sign,
180
+ nakshatra, house, significance.
181
+ """
182
+ sun_lon = _get_sun_longitude(julian_day, ayanamsa_offset)
183
+ asc_lon = _get_ascendant_longitude(
184
+ julian_day, birth_lat, birth_lon, ayanamsa_offset
185
+ )
186
+
187
+ # ------------------------------------------------------------------
188
+ # Weekday from Julian Day
189
+ # jd 0.0 = Monday noon UT; int(jd + 1.5) % 7 gives 0=Sunday … 6=Sat
190
+ # ------------------------------------------------------------------
191
+ weekday = int(julian_day + 1.5) % 7 # 0=Sunday, 6=Saturday
192
+
193
+ # ------------------------------------------------------------------
194
+ # Gulika & Mandi
195
+ # Day duration assumed = 12 hours (simplified; sunrise 6 AM, sunset 6 PM).
196
+ # Split into 8 equal portions of 90 minutes each.
197
+ # Lagna (ascendant sign) changes roughly every 2 hours (120 min).
198
+ # Portion sign offset: each 90-min portion ≈ 90/120 × 30° = 22.5° from asc.
199
+ # Gulika longitude = ascendant + portion_index × (360/8)
200
+ # ------------------------------------------------------------------
201
+ day_duration_deg = 180.0 # 12 hours × 15°/hour (Sun travels 180° in a day)
202
+ portion_size_deg = day_duration_deg / 8 # 22.5° per portion
203
+
204
+ gulika_portion = _GULIKA_WEEKDAY_PORTION[weekday]
205
+ gulika_lon = _normalise(
206
+ asc_lon + gulika_portion * portion_size_deg + portion_size_deg / 2
207
+ )
208
+
209
+ mandi_portion = _MANDI_WEEKDAY_PORTION[weekday]
210
+ mandi_lon = _normalise(asc_lon + mandi_portion * portion_size_deg)
211
+
212
+ # ------------------------------------------------------------------
213
+ # Dhuma and derived upagrahas (purely Sun-based calculations)
214
+ # ------------------------------------------------------------------
215
+ dhuma_lon = _normalise(sun_lon + 133.333) # Sun + 133°20'
216
+ vyatipata_lon = _normalise(360.0 - dhuma_lon) # 360° − Dhuma
217
+ parivesha_lon = _normalise(vyatipata_lon + 180.0) # Vyatipata + 180°
218
+ indrachapa_lon = _normalise(360.0 - parivesha_lon) # 360° − Parivesha
219
+ upaketu_lon = _normalise(sun_lon + 30.0) # Sun + 30°
220
+
221
+ return {
222
+ "Gulika": _build_upagraha_entry("Gulika", gulika_lon, asc_lon),
223
+ "Mandi": _build_upagraha_entry("Mandi", mandi_lon, asc_lon),
224
+ "Dhuma": _build_upagraha_entry("Dhuma", dhuma_lon, asc_lon),
225
+ "Vyatipata": _build_upagraha_entry("Vyatipata", vyatipata_lon, asc_lon),
226
+ "Parivesha": _build_upagraha_entry("Parivesha", parivesha_lon, asc_lon),
227
+ "Indra_Chapa": _build_upagraha_entry("Indra_Chapa", indrachapa_lon, asc_lon),
228
+ "Upaketu": _build_upagraha_entry("Upaketu", upaketu_lon, asc_lon),
229
+ }
@@ -0,0 +1,185 @@
1
+ """Varshaphal: Vedic Solar Return chart for any given year of life.
2
+
3
+ The Varshaphal chart is cast for the exact moment the Sun returns to its
4
+ natal sidereal longitude each year (solar return). The chart is erected
5
+ for the querent's current place of residence rather than their birth place.
6
+ """
7
+
8
+ import calendar
9
+ from datetime import datetime
10
+
11
+ import pytz
12
+ import swisseph as swe
13
+
14
+ from kundali_lib.vedic.chart import _jd_ut, build_chart
15
+
16
+ _SIDEREAL_FLAGS = swe.FLG_SWIEPH | swe.FLG_SPEED | swe.FLG_SIDEREAL
17
+
18
+
19
+ # ── Internal helpers ─────────────────────────────────────────────────────────
20
+
21
+
22
+ def _sun_lon(jd: float) -> float:
23
+ """Sidereal (Lahiri) Sun longitude for a given Julian Day."""
24
+ swe.set_sid_mode(swe.SIDM_LAHIRI)
25
+ xx, _ = swe.calc_ut(jd, swe.SUN, _SIDEREAL_FLAGS)
26
+ return xx[0]
27
+
28
+
29
+ def _angular_diff(a: float, b: float) -> float:
30
+ """Signed difference (a − b) normalised to [−180, 180]."""
31
+ diff = (a - b) % 360.0
32
+ if diff > 180.0:
33
+ diff -= 360.0
34
+ return diff
35
+
36
+
37
+ def _find_solar_return_jd(
38
+ natal_sun_lon: float,
39
+ target_year: int,
40
+ birth_month: int,
41
+ birth_day: int,
42
+ ) -> float:
43
+ """Return the Julian Day when the sidereal Sun equals natal_sun_lon.
44
+
45
+ Strategy:
46
+ 1. Start scanning ~2 months before the expected solar-return date in
47
+ ``target_year`` (to be safely before the crossing).
48
+ 2. Walk day by day until a sign change in (sun_lon − natal_lon) is found.
49
+ 3. Refine with 60 iterations of binary search (sub-second accuracy).
50
+ """
51
+ start_month = birth_month - 2
52
+ start_year = target_year
53
+ if start_month < 1:
54
+ start_month += 12
55
+ start_year -= 1
56
+
57
+ # Clamp birth_day to a valid day in start_month
58
+ max_day = calendar.monthrange(start_year, start_month)[1]
59
+ start_day = min(birth_day, max_day)
60
+ start_jd = swe.julday(start_year, start_month, start_day, 0.0)
61
+
62
+ # ── Coarse day-level scan ────────────────────────────────────────────────
63
+ prev_diff = _angular_diff(_sun_lon(start_jd), natal_sun_lon)
64
+ lo_jd: float | None = None
65
+ hi_jd: float | None = None
66
+
67
+ for day_offset in range(1, 400):
68
+ jd = start_jd + day_offset
69
+ curr_diff = _angular_diff(_sun_lon(jd), natal_sun_lon)
70
+
71
+ if abs(curr_diff) < 0.001:
72
+ return jd # Landed almost exactly on the crossing
73
+
74
+ # Sun moves forward — we want the zero-crossing from negative → positive
75
+ if prev_diff < 0.0 and curr_diff >= 0.0:
76
+ lo_jd = jd - 1.0
77
+ hi_jd = jd
78
+ break
79
+
80
+ prev_diff = curr_diff
81
+
82
+ if lo_jd is None or hi_jd is None:
83
+ # Fallback (should not occur with a 400-day window)
84
+ return start_jd
85
+
86
+ # ── Fine binary search (~60 iterations → microsecond precision) ──────────
87
+ for _ in range(60):
88
+ mid = (lo_jd + hi_jd) / 2.0
89
+ diff = _angular_diff(_sun_lon(mid), natal_sun_lon)
90
+ if abs(diff) < 0.0001:
91
+ return mid
92
+ if diff < 0.0:
93
+ lo_jd = mid
94
+ else:
95
+ hi_jd = mid
96
+
97
+ return (lo_jd + hi_jd) / 2.0
98
+
99
+
100
+ def _jd_to_utc_datetime(jd: float) -> datetime:
101
+ """Convert a Julian Day number to a UTC-aware datetime."""
102
+ year, month, day, hour_frac = swe.revjul(jd, 1) # 1 = Gregorian calendar
103
+ hour = int(hour_frac)
104
+ minute_frac = (hour_frac - hour) * 60.0
105
+ minute = int(minute_frac)
106
+ second_frac = (minute_frac - minute) * 60.0
107
+ second = min(int(round(second_frac)), 59) # guard against rounding to 60
108
+ return datetime(
109
+ int(year), int(month), int(day), hour, minute, second, tzinfo=pytz.UTC
110
+ )
111
+
112
+
113
+ # ── Public API ───────────────────────────────────────────────────────────────
114
+
115
+
116
+ def get_varshaphal(
117
+ birth_dt: datetime,
118
+ birth_lat: float,
119
+ birth_lon: float,
120
+ birth_timezone: str,
121
+ year_of_life: int,
122
+ query_lat: float,
123
+ query_lon: float,
124
+ query_timezone: str,
125
+ ) -> dict:
126
+ """Calculate a Varshaphal (Solar Return) chart for a given year of life.
127
+
128
+ Args:
129
+ birth_dt: Naive local birth datetime.
130
+ birth_lat: Latitude of birth place.
131
+ birth_lon: Longitude of birth place.
132
+ birth_timezone: IANA timezone string for the birth place (e.g. "Asia/Kolkata").
133
+ year_of_life: Which solar return to compute (e.g. 26 for the 26th year).
134
+ Target calendar year = birth_year + year_of_life.
135
+ query_lat: Latitude of current residence.
136
+ query_lon: Longitude of current residence.
137
+ query_timezone: IANA timezone string for current residence.
138
+
139
+ Returns:
140
+ dict with solar-return timing and the full Varshaphal chart.
141
+ """
142
+ swe.set_ephe_path(None)
143
+ swe.set_sid_mode(swe.SIDM_LAHIRI)
144
+
145
+ # ── Step 1: natal Sun sidereal longitude ─────────────────────────────────
146
+ birth_jd = _jd_ut(birth_dt, birth_timezone)
147
+ xx, _ = swe.calc_ut(birth_jd, swe.SUN, _SIDEREAL_FLAGS)
148
+ natal_sun_lon: float = xx[0]
149
+
150
+ # ── Step 2: find JD of solar return in target year ───────────────────────
151
+ target_year = birth_dt.year + year_of_life
152
+ sr_jd = _find_solar_return_jd(
153
+ natal_sun_lon, target_year, birth_dt.month, birth_dt.day
154
+ )
155
+
156
+ # ── Step 3: convert to datetimes ─────────────────────────────────────────
157
+ sr_utc = _jd_to_utc_datetime(sr_jd)
158
+ query_tz = pytz.timezone(query_timezone)
159
+ sr_local_aware = sr_utc.astimezone(query_tz)
160
+ sr_local_naive = sr_local_aware.replace(tzinfo=None)
161
+
162
+ # ── Step 4: build chart at query location ────────────────────────────────
163
+ chart = build_chart(
164
+ name="Varshaphal",
165
+ birth_dt=sr_local_naive,
166
+ lat=query_lat,
167
+ lon=query_lon,
168
+ timezone=query_timezone,
169
+ )
170
+
171
+ return {
172
+ "year_of_life": year_of_life,
173
+ "solar_return_datetime_utc": sr_utc.isoformat(),
174
+ "solar_return_datetime_local": sr_local_aware.isoformat(),
175
+ "natal_sun_longitude": round(natal_sun_lon, 4),
176
+ "varshaphal_chart": {
177
+ "planetary_positions": chart["planetary_positions"],
178
+ "houses": chart["houses"],
179
+ "ascendant": chart["ascendant"],
180
+ "moon_sign": chart["moon_sign"],
181
+ "sun_sign": chart["sun_sign"],
182
+ "ayanamsa": chart["ayanamsa"],
183
+ "ayanamsa_name": chart["ayanamsa_name"],
184
+ },
185
+ }