libaditya 0.3.4__py2.py3-none-any.whl

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 (131) hide show
  1. libaditya/__init__.py +46 -0
  2. libaditya/_version.py +29 -0
  3. libaditya/calc/__init__.py +9 -0
  4. libaditya/calc/api.py +30 -0
  5. libaditya/calc/avasthas.py +346 -0
  6. libaditya/calc/docs/jaimini-get-arch.md +95 -0
  7. libaditya/calc/hellenistic.py +37 -0
  8. libaditya/calc/jaimini.py +335 -0
  9. libaditya/calc/jaimini_get.py +240 -0
  10. libaditya/calc/kala.py +73 -0
  11. libaditya/calc/panchanga.py +422 -0
  12. libaditya/calc/rashi.py +612 -0
  13. libaditya/calc/returns.py +50 -0
  14. libaditya/calc/swe_functions.py +118 -0
  15. libaditya/calc/varga.py +287 -0
  16. libaditya/calc/vimshottari.py +431 -0
  17. libaditya/cards/__init__.py +5 -0
  18. libaditya/cards/card.py +90 -0
  19. libaditya/cards/cards_constants.py +115 -0
  20. libaditya/cards/cards_of_truth.py +323 -0
  21. libaditya/cards/cot.py +142 -0
  22. libaditya/cards/deck.py +52 -0
  23. libaditya/charts/__init__.py +6 -0
  24. libaditya/charts/api.py +36 -0
  25. libaditya/charts/bodygraph.py +80 -0
  26. libaditya/charts/chart.py +286 -0
  27. libaditya/charts/jaimini.py +83 -0
  28. libaditya/charts/tajika.py +36 -0
  29. libaditya/constants.py +1776 -0
  30. libaditya/draw/__init__.py +20 -0
  31. libaditya/draw/draw_bodygraph.py +489 -0
  32. libaditya/draw/draw_sbc.py +712 -0
  33. libaditya/draw/themes/default-theme.hd +53 -0
  34. libaditya/ephe/ast0/se00010s.se1 +0 -0
  35. libaditya/ephe/seas_00.se1 +0 -0
  36. libaditya/ephe/seas_06.se1 +0 -0
  37. libaditya/ephe/seas_12.se1 +0 -0
  38. libaditya/ephe/seas_18.se1 +0 -0
  39. libaditya/ephe/seas_24.se1 +0 -0
  40. libaditya/ephe/seas_30.se1 +0 -0
  41. libaditya/ephe/seas_36.se1 +0 -0
  42. libaditya/ephe/seas_42.se1 +0 -0
  43. libaditya/ephe/seas_48.se1 +0 -0
  44. libaditya/ephe/seasm06.se1 +0 -0
  45. libaditya/ephe/seasm12.se1 +0 -0
  46. libaditya/ephe/seasm18.se1 +0 -0
  47. libaditya/ephe/seasm24.se1 +0 -0
  48. libaditya/ephe/seasm30.se1 +0 -0
  49. libaditya/ephe/seasm36.se1 +0 -0
  50. libaditya/ephe/seasm42.se1 +0 -0
  51. libaditya/ephe/seasm48.se1 +0 -0
  52. libaditya/ephe/seasm54.se1 +0 -0
  53. libaditya/ephe/sefstars.txt +4245 -0
  54. libaditya/ephe/semo_00.se1 +0 -0
  55. libaditya/ephe/semo_06.se1 +0 -0
  56. libaditya/ephe/semo_12.se1 +0 -0
  57. libaditya/ephe/semo_18.se1 +0 -0
  58. libaditya/ephe/semo_24.se1 +0 -0
  59. libaditya/ephe/semo_30.se1 +0 -0
  60. libaditya/ephe/semo_36.se1 +0 -0
  61. libaditya/ephe/semo_42.se1 +0 -0
  62. libaditya/ephe/semo_48.se1 +0 -0
  63. libaditya/ephe/semom06.se1 +0 -0
  64. libaditya/ephe/semom12.se1 +0 -0
  65. libaditya/ephe/semom18.se1 +0 -0
  66. libaditya/ephe/semom24.se1 +0 -0
  67. libaditya/ephe/semom30.se1 +0 -0
  68. libaditya/ephe/semom36.se1 +0 -0
  69. libaditya/ephe/semom42.se1 +0 -0
  70. libaditya/ephe/semom48.se1 +0 -0
  71. libaditya/ephe/semom54.se1 +0 -0
  72. libaditya/ephe/sepl_00.se1 +0 -0
  73. libaditya/ephe/sepl_06.se1 +0 -0
  74. libaditya/ephe/sepl_12.se1 +0 -0
  75. libaditya/ephe/sepl_18.se1 +0 -0
  76. libaditya/ephe/sepl_24.se1 +0 -0
  77. libaditya/ephe/sepl_30.se1 +0 -0
  78. libaditya/ephe/sepl_36.se1 +0 -0
  79. libaditya/ephe/sepl_42.se1 +0 -0
  80. libaditya/ephe/sepl_48.se1 +0 -0
  81. libaditya/ephe/seplm06.se1 +0 -0
  82. libaditya/ephe/seplm12.se1 +0 -0
  83. libaditya/ephe/seplm18.se1 +0 -0
  84. libaditya/ephe/seplm24.se1 +0 -0
  85. libaditya/ephe/seplm30.se1 +0 -0
  86. libaditya/ephe/seplm36.se1 +0 -0
  87. libaditya/ephe/seplm42.se1 +0 -0
  88. libaditya/ephe/seplm48.se1 +0 -0
  89. libaditya/ephe/seplm54.se1 +0 -0
  90. libaditya/hd/__init__.py +8 -0
  91. libaditya/hd/calc.py +55 -0
  92. libaditya/hd/constants.py +127 -0
  93. libaditya/hd/longitude.py +271 -0
  94. libaditya/objects/__init__.py +19 -0
  95. libaditya/objects/celestial_object.py +206 -0
  96. libaditya/objects/context.py +133 -0
  97. libaditya/objects/cusps.py +259 -0
  98. libaditya/objects/julian_day.py +265 -0
  99. libaditya/objects/location.py +167 -0
  100. libaditya/objects/longitude.py +987 -0
  101. libaditya/objects/nakshatras.py +285 -0
  102. libaditya/objects/planets.py +2059 -0
  103. libaditya/objects/shadbala.py +398 -0
  104. libaditya/objects/signs.py +690 -0
  105. libaditya/objects/swe_functions.py +75 -0
  106. libaditya/print_functions.py +548 -0
  107. libaditya/read.py +665 -0
  108. libaditya/stars/__init__.py +4 -0
  109. libaditya/stars/fixed_star.py +216 -0
  110. libaditya/stars/make_swe_stars.py +407 -0
  111. libaditya/stars/star-sign-boundaries +44 -0
  112. libaditya/stars/stellarium/__init__.py +6 -0
  113. libaditya/stars/stellarium/remote_control/__init__.py +22 -0
  114. libaditya/stars/stellarium/remote_control/location.py +82 -0
  115. libaditya/stars/stellarium/remote_control/locationsearch.py +35 -0
  116. libaditya/stars/stellarium/remote_control/main.py +165 -0
  117. libaditya/stars/stellarium/remote_control/objects.py +63 -0
  118. libaditya/stars/stellarium/remote_control/scripts.py +81 -0
  119. libaditya/stars/stellarium/remote_control/simbad.py +20 -0
  120. libaditya/stars/stellarium/remote_control/stelaction.py +31 -0
  121. libaditya/stars/stellarium/remote_control/stelproperty.py +30 -0
  122. libaditya/stars/stellarium/remote_control/view.py +85 -0
  123. libaditya/stars/stellarium/stellarium.py +92 -0
  124. libaditya/stars/the_stars.py +522 -0
  125. libaditya/stars/utilities.py +539 -0
  126. libaditya/utils.py +431 -0
  127. libaditya/write.py +67 -0
  128. libaditya-0.3.4.dist-info/METADATA +588 -0
  129. libaditya-0.3.4.dist-info/RECORD +131 -0
  130. libaditya-0.3.4.dist-info/WHEEL +5 -0
  131. libaditya-0.3.4.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,612 @@
1
+ # This file is part of libaditya.
2
+ #
3
+ # Copyright (c) 2025 Josh Harper <humanhaven@substack.com>
4
+ #
5
+ # libaditya is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU Affero General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # libaditya is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU Affero General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Affero General Public License
16
+ # along with libaditya. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import swisseph as swe
19
+ from dataclasses import dataclass, replace
20
+
21
+ from libaditya.objects import Planet, RashiBala
22
+ from libaditya.calc import Varga
23
+ from libaditya.calc import vimshottari
24
+ from libaditya.calc.panchanga import Panchanga
25
+ from libaditya.calc.swe_functions import SWERashi
26
+ from libaditya.calc.hellenistic import Hellenistic
27
+ from libaditya.calc.returns import Returns
28
+ from libaditya.calc.avasthas import (LajjitaadiAvasthas, BaladiAvasthas,
29
+ JagradadiAvasthas, DeeptadiAvasthas,
30
+ ShayanadiAvasthas)
31
+ from libaditya.draw.draw_sbc import DrawSBC
32
+
33
+ from .jaimini_get import JaiminiGet
34
+
35
+
36
+ @dataclass
37
+ class AkritiYoga:
38
+ name: str
39
+ translation: str
40
+ to_move: int
41
+ houses: tuple
42
+
43
+ def __str__(self):
44
+ return f"{self.name} ({self.translation}): {self.to_move} to move — houses {self.houses}"
45
+
46
+ def __repr__(self):
47
+ return f"AkritiYoga({self.name!r}, to_move={self.to_move})"
48
+
49
+
50
+ @dataclass
51
+ class NabhasaYoga:
52
+ name: str
53
+ translation: str
54
+ category: str
55
+ to_move: int
56
+ condition: str
57
+
58
+ def __str__(self):
59
+ return f"{self.name} ({self.translation}): {self.to_move} to move — {self.condition}"
60
+
61
+ def __repr__(self):
62
+ return f"NabhasaYoga({self.name!r}, category={self.category!r}, to_move={self.to_move})"
63
+
64
+
65
+ @dataclass
66
+ class MahapurushaYoga:
67
+ name: str
68
+ translation: str
69
+ planet: str
70
+ present: bool
71
+ house: int
72
+ dignity: str
73
+
74
+ def __str__(self):
75
+ if self.present:
76
+ return f"{self.name} ({self.translation}): {self.planet} in house {self.house}, {self.dignity}"
77
+ return f"{self.name} ({self.translation}): not present"
78
+
79
+ def __repr__(self):
80
+ return f"MahapurushaYoga({self.name!r}, planet={self.planet!r}, present={self.present})"
81
+
82
+
83
+ @dataclass
84
+ class SolarYoga:
85
+ name: str
86
+ planets: list
87
+ present: bool
88
+
89
+ def __str__(self):
90
+ if self.present:
91
+ return f"{self.name}: {', '.join(self.planets)}"
92
+ return f"{self.name}: not present"
93
+
94
+ def __repr__(self):
95
+ return f"SolarYoga({self.name!r}, present={self.present})"
96
+
97
+
98
+ @dataclass
99
+ class LunarYoga:
100
+ name: str
101
+ planets: list
102
+ present: bool
103
+
104
+ def __str__(self):
105
+ if self.present:
106
+ return f"{self.name}: {', '.join(self.planets)}"
107
+ return f"{self.name}: not present"
108
+
109
+ def __repr__(self):
110
+ return f"LunarYoga({self.name!r}, present={self.present})"
111
+
112
+
113
+ class Rashi(Varga,SWERashi,JaiminiGet,RashiBala,DrawSBC,Hellenistic,Returns,LajjitaadiAvasthas,BaladiAvasthas,JagradadiAvasthas,DeeptadiAvasthas,ShayanadiAvasthas):
114
+
115
+ def __init__(self,context,chart):
116
+ self.master = chart
117
+ super().__init__(context=context,amsha=1)
118
+ self._dig_balas = self.init_dig_balas()
119
+ self._saptavargaja_balas = self.init_saptavargaja_balas()
120
+ self._sama_visama_balas = self.init_sama_visama_balas()
121
+ self._kendradi_balas = self.init_kendradi_balas()
122
+ self._drig_balas = self.init_drig_balas()
123
+ self._lajjitaadi_avasthas = self.lajjitaadi_avasthas()
124
+ self._baladi_avasthas = self.baladi_avasthas()
125
+ self._jagradadi_avasthas = self.jagradadi_avasthas()
126
+ self._deeptadi_avasthas = self.deeptadi_avasthas()
127
+ self._shayanadi_avasthas = self.shayanadi_avasthas()
128
+
129
+ def planets(self):
130
+ return self._planets
131
+
132
+ def ecliptic(self):
133
+ return self._ecliptic
134
+
135
+ def cusps(self):
136
+ return self._cusps
137
+
138
+ def signs(self):
139
+ return self._signs
140
+
141
+ def avasthas(self):
142
+ from libaditya import print_functions as printf
143
+ printf.print_avasthas(self._lajjitaadi_avasthas,
144
+ self._baladi_avasthas,
145
+ self._jagradadi_avasthas,
146
+ self._deeptadi_avasthas,
147
+ self._shayanadi_avasthas)
148
+
149
+ def house_position(self, planet: str, hsys=None) -> float:
150
+ """
151
+ a wrapper for the function swe.house_pos()
152
+ planet is the Planet.identity(), i.e., the english name of the desired planet
153
+ """
154
+ armc = self.cusps().armc()
155
+ lat = self.context.location.latitude()
156
+ eo = self.context.timeJD.ecliptic_obliquity()
157
+ planet = self.planets()[planet]
158
+ planet_coords = (planet.ecliptic_longitude(),planet.latitude())
159
+ if hsys == None:
160
+ hsys = self.context.hsys
161
+ return swe.house_pos(armc,lat,eo,planet_coords,hsys.encode())
162
+
163
+ def Master(self):
164
+ """
165
+ return the Chart that spawned it
166
+ """
167
+ return self.master
168
+
169
+ def panchanga(self, **kwargs):
170
+ """
171
+ panchanga is here because it has to do with the sun and moon, with time
172
+ that is the Rashi chart essentially
173
+ none of the other vargas really need a separate class, because they are really subsections of this class
174
+ which is why Rashi can hopefully reach all the Vargas
175
+
176
+ Panchanga.__str__ prints the basic panchagna, and also the panchanga addendum
177
+ __repr__ prints only the basic time and panchanga information
178
+ """
179
+ context = replace(self.context,**kwargs)
180
+ return Panchanga(context)
181
+
182
+ def print_current_vimshottari_dasha(self,yrlen="saura",levels=5):
183
+ vimshottari.print_current_vdasha(self.context,yrlen,levels)
184
+ return
185
+
186
+ def rashi_argala(self) -> [[Planet],[Planet],[Planet]]:
187
+ """
188
+ it returns a list a lists, where each sublist is the combinatino of the corresponding argala lists of lagna and seventh
189
+
190
+ this combines the argala formed to both 1st and 7th
191
+ """
192
+ lagna_arg = self.argala(self.signs().lagna())
193
+ seventh_arg = self.argala(self.signs()[self.signs().lagna().astrological_signs_forward(7)])
194
+ ret = [[],[],[]]
195
+ for n,arg in enumerate(lagna_arg):
196
+ for planet in lagna_arg[n]:
197
+ ret[n].append(planet)
198
+ for planet in seventh_arg[n]:
199
+ ret[n].append(planet)
200
+ return ret
201
+ # each arg has three elements; put all of lagna_arg[0] together with seventh_arg[0], etc.
202
+
203
+
204
+ def akriti_yogas(self):
205
+ """
206
+ find the 20 akriti yogas in this Rashi varga
207
+ these are rarely found perfectly or completely, so we find
208
+ how many planets would need to be moved to create each
209
+ for these yogas, planets means the seven embodied planets, i.e., karakas (Sun-Saturn)
210
+ returns a list of AkritiYoga dataclasses sorted by to_move ascending
211
+ """
212
+ akritis = []
213
+ lagna_sign = self.signs().lagna()
214
+ # karaka counts per house (1-indexed by house number from lagna)
215
+ h = {}
216
+ for i in range(1, 13):
217
+ h[i] = self.signs()[lagna_sign.astrological_signs_forward(i)].how_many_karakas()
218
+
219
+ # for vajra/yava: count benefic and malefic karakas per house
220
+ # uses fixed natural classification: Moon, Mercury, Jupiter, Venus = benefic
221
+ # Sun, Mars, Saturn = malefic
222
+ FIXED_MALEFICS = {"Sun", "Mars", "Saturn"}
223
+ bh = {} # benefic karaka count per house
224
+ mh = {} # malefic karaka count per house
225
+ for i in range(1, 13):
226
+ sign = self.signs()[lagna_sign.astrological_signs_forward(i)]
227
+ karakas = sign.karakas()
228
+ b = 0
229
+ m = 0
230
+ for k in karakas:
231
+ if k.identity() in FIXED_MALEFICS:
232
+ m += 1
233
+ else:
234
+ b += 1
235
+ bh[i] = b
236
+ mh[i] = m
237
+
238
+ def tm(*houses):
239
+ """to_move: how many of the 7 karakas are outside the given houses"""
240
+ return 7 - sum(h[i] for i in houses)
241
+
242
+ def tm_dist(*houses):
243
+ """to_move for yogas requiring distribution across houses.
244
+ accounts for both planets outside AND empty required houses,
245
+ since a planet must be moved into each empty house."""
246
+ outside = 7 - sum(h[i] for i in houses)
247
+ empty = sum(1 for i in houses if h[i] == 0)
248
+ return max(outside, empty)
249
+
250
+ # --- trine yogas ---
251
+ akritis.append(AkritiYoga("Sringataka", "mountain having three peaks",
252
+ tm(1, 5, 9), (1, 5, 9)))
253
+ akritis.append(AkritiYoga("Hala Artha", "plough",
254
+ tm(2, 6, 10), (2, 6, 10)))
255
+ akritis.append(AkritiYoga("Hala Kama", "plough",
256
+ tm(3, 7, 11), (3, 7, 11)))
257
+ akritis.append(AkritiYoga("Hala Moksha", "plough",
258
+ tm(4, 8, 12), (4, 8, 12)))
259
+
260
+ # --- gada yogas: all planets in two successive angles ---
261
+ akritis.append(AkritiYoga("Gada 1/4", "mace",
262
+ tm(1, 4), (1, 4)))
263
+ akritis.append(AkritiYoga("Gada 4/7", "mace",
264
+ tm(4, 7), (4, 7)))
265
+ akritis.append(AkritiYoga("Gada 7/10", "mace",
266
+ tm(7, 10), (7, 10)))
267
+ akritis.append(AkritiYoga("Gada 10/1", "mace",
268
+ tm(10, 1), (10, 1)))
269
+
270
+ # --- two-house angle yogas ---
271
+ akritis.append(AkritiYoga("Sakata", "cart",
272
+ tm(1, 7), (1, 7)))
273
+ akritis.append(AkritiYoga("Vihaga", "skygoer",
274
+ tm(4, 10), (4, 10)))
275
+
276
+ # --- four-house yogas ---
277
+ akritis.append(AkritiYoga("Kamala", "lotus",
278
+ tm(1, 4, 7, 10), (1, 4, 7, 10)))
279
+ akritis.append(AkritiYoga("Vapi Panaphara", "pond",
280
+ tm(2, 5, 8, 11), (2, 5, 8, 11)))
281
+ akritis.append(AkritiYoga("Vapi Apoklima", "pond",
282
+ tm(3, 6, 9, 12), (3, 6, 9, 12)))
283
+
284
+ # --- vajra and yava: benefic/malefic distribution in angles ---
285
+ # vajra: all benefics in 1 & 7, all malefics in 4 & 10
286
+ vajra_correct = (bh[1] + bh[7]) + (mh[4] + mh[10])
287
+ akritis.append(AkritiYoga("Vajra", "thunderbolt",
288
+ 7 - vajra_correct, (1, 4, 7, 10)))
289
+ # yava: all malefics in 1 & 7, all benefics in 4 & 10
290
+ yava_correct = (mh[1] + mh[7]) + (bh[4] + bh[10])
291
+ akritis.append(AkritiYoga("Yava", "barleycorn",
292
+ 7 - yava_correct, (1, 4, 7, 10)))
293
+
294
+ # --- four consecutive house yogas ---
295
+ akritis.append(AkritiYoga("Yupa", "sacrificial pillar",
296
+ tm(1, 2, 3, 4), (1, 2, 3, 4)))
297
+ akritis.append(AkritiYoga("Sara", "arrow",
298
+ tm(4, 5, 6, 7), (4, 5, 6, 7)))
299
+ akritis.append(AkritiYoga("Shakti", "power",
300
+ tm(7, 8, 9, 10), (7, 8, 9, 10)))
301
+ akritis.append(AkritiYoga("Danda", "rod",
302
+ tm(10, 11, 12, 1), (10, 11, 12, 1)))
303
+
304
+ # --- seven consecutive house yogas (use tm_dist for distribution) ---
305
+ akritis.append(AkritiYoga("Nauka", "boat",
306
+ tm_dist(1, 2, 3, 4, 5, 6, 7), (1, 2, 3, 4, 5, 6, 7)))
307
+ akritis.append(AkritiYoga("Kuta", "peak",
308
+ tm_dist(4, 5, 6, 7, 8, 9, 10), (4, 5, 6, 7, 8, 9, 10)))
309
+ akritis.append(AkritiYoga("Chatra", "parasol",
310
+ tm_dist(7, 8, 9, 10, 11, 12, 1), (7, 8, 9, 10, 11, 12, 1)))
311
+ akritis.append(AkritiYoga("Chapa", "bow",
312
+ tm_dist(10, 11, 12, 1, 2, 3, 4), (10, 11, 12, 1, 2, 3, 4)))
313
+
314
+ # --- ardha chandra: seven consecutive from a panaphara or apoklima ---
315
+ akritis.append(AkritiYoga("Ardha Chandra Artha Panaphara", "half moon",
316
+ tm_dist(2, 3, 4, 5, 6, 7, 8), (2, 3, 4, 5, 6, 7, 8)))
317
+ akritis.append(AkritiYoga("Ardha Chandra Dharma Panaphara", "half moon",
318
+ tm_dist(5, 6, 7, 8, 9, 10, 11), (5, 6, 7, 8, 9, 10, 11)))
319
+ akritis.append(AkritiYoga("Ardha Chandra Moksha Panaphara", "half moon",
320
+ tm_dist(8, 9, 10, 11, 12, 1, 2), (8, 9, 10, 11, 12, 1, 2)))
321
+ akritis.append(AkritiYoga("Ardha Chandra Kama Panaphara", "half moon",
322
+ tm_dist(11, 12, 1, 2, 3, 4, 5), (11, 12, 1, 2, 3, 4, 5)))
323
+ akritis.append(AkritiYoga("Ardha Chandra Kama Apoklima", "half moon",
324
+ tm_dist(3, 4, 5, 6, 7, 8, 9), (3, 4, 5, 6, 7, 8, 9)))
325
+ akritis.append(AkritiYoga("Ardha Chandra Artha Apoklima", "half moon",
326
+ tm_dist(6, 7, 8, 9, 10, 11, 12), (6, 7, 8, 9, 10, 11, 12)))
327
+ akritis.append(AkritiYoga("Ardha Chandra Dharma Apoklima", "half moon",
328
+ tm_dist(9, 10, 11, 12, 1, 2, 3), (9, 10, 11, 12, 1, 2, 3)))
329
+ akritis.append(AkritiYoga("Ardha Chandra Moksha Apoklima", "half moon",
330
+ tm_dist(12, 1, 2, 3, 4, 5, 6), (12, 1, 2, 3, 4, 5, 6)))
331
+
332
+ # --- six alternate house yogas (use tm_dist for distribution) ---
333
+ akritis.append(AkritiYoga("Chakra", "wheel",
334
+ tm_dist(1, 3, 5, 7, 9, 11), (1, 3, 5, 7, 9, 11)))
335
+ akritis.append(AkritiYoga("Samudra", "sea",
336
+ tm_dist(2, 4, 6, 8, 10, 12), (2, 4, 6, 8, 10, 12)))
337
+
338
+ akritis.sort(key=lambda y: y.to_move)
339
+ return akritis
340
+
341
+
342
+ def ashraya_yogas(self):
343
+ """
344
+ Ashraya ("resting place") yogas: 3 yogas based on the modality
345
+ of the signs the 7 karakas rest in.
346
+ Rajju = all in movable, Musala = all in fixed, Nala = all in dual.
347
+ """
348
+ lagna_sign = self.signs().lagna()
349
+ movable = 0
350
+ fixed = 0
351
+ dual = 0
352
+ for i in range(1, 13):
353
+ sign = self.signs()[lagna_sign.astrological_signs_forward(i)]
354
+ n = sign.how_many_karakas()
355
+ mod = sign.modality()
356
+ if mod == "Moveable":
357
+ movable += n
358
+ elif mod == "Fixed":
359
+ fixed += n
360
+ else:
361
+ dual += n
362
+
363
+ yogas = [
364
+ NabhasaYoga("Rajju", "rope", "Ashraya", 7 - movable,
365
+ "all planets in movable signs"),
366
+ NabhasaYoga("Musala", "pestle", "Ashraya", 7 - fixed,
367
+ "all planets in fixed signs"),
368
+ NabhasaYoga("Nala", "reed", "Ashraya", 7 - dual,
369
+ "all planets in dual signs"),
370
+ ]
371
+ yogas.sort(key=lambda y: y.to_move)
372
+ return yogas
373
+
374
+
375
+ def dala_yogas(self):
376
+ """
377
+ Dala ("petal") yogas: 2 yogas based on benefic/malefic occupation
378
+ of the kendras (houses 1, 4, 7, 10).
379
+ Mala = all benefics in kendras, Sarpa = all malefics in kendras.
380
+ Uses each planet's actual nature (Moon varies by phase).
381
+ """
382
+ lagna_sign = self.signs().lagna()
383
+ kendras = {1, 4, 7, 10}
384
+ benefics_in_kendras = 0
385
+ malefics_in_kendras = 0
386
+ total_benefics = 0
387
+ total_malefics = 0
388
+
389
+ for i in range(1, 13):
390
+ sign = self.signs()[lagna_sign.astrological_signs_forward(i)]
391
+ for planet in sign.karakas():
392
+ if planet.nature() == "Benefic":
393
+ total_benefics += 1
394
+ if i in kendras:
395
+ benefics_in_kendras += 1
396
+ else:
397
+ total_malefics += 1
398
+ if i in kendras:
399
+ malefics_in_kendras += 1
400
+
401
+ yogas = [
402
+ NabhasaYoga("Mala", "garland", "Dala",
403
+ total_benefics - benefics_in_kendras,
404
+ "all benefics in kendras"),
405
+ NabhasaYoga("Sarpa", "serpent", "Dala",
406
+ total_malefics - malefics_in_kendras,
407
+ "all malefics in kendras"),
408
+ ]
409
+ yogas.sort(key=lambda y: y.to_move)
410
+ return yogas
411
+
412
+
413
+ def sankhya_yogas(self):
414
+ """
415
+ Sankhya ("number") yogas: 7 yogas based on how many houses
416
+ the 7 karakas occupy. Exactly one is always active (to_move=0).
417
+ """
418
+ lagna_sign = self.signs().lagna()
419
+ occupied = 0
420
+ for i in range(1, 13):
421
+ if self.signs()[lagna_sign.astrological_signs_forward(i)].how_many_karakas() > 0:
422
+ occupied += 1
423
+
424
+ sankhya_defs = [
425
+ ("Veena", "lute", 7),
426
+ ("Dama", "garland", 6),
427
+ ("Pasa", "noose", 5),
428
+ ("Kedara", "field", 4),
429
+ ("Sula", "spike", 3),
430
+ ("Yuga", "yoke", 2),
431
+ ("Gola", "globe", 1),
432
+ ]
433
+ yogas = []
434
+ for name, trans, required in sankhya_defs:
435
+ yogas.append(NabhasaYoga(name, trans, "Sankhya",
436
+ abs(occupied - required),
437
+ f"planets in {required} houses"))
438
+ yogas.sort(key=lambda y: y.to_move)
439
+ return yogas
440
+
441
+
442
+ def nabhasa_yogas(self):
443
+ """
444
+ All 32 Nabhasa yogas: Ashraya (3) + Dala (2) + Sankhya (7) + Akriti (20).
445
+ Returns a list of mixed NabhasaYoga and AkritiYoga dataclasses,
446
+ sorted by to_move ascending.
447
+ """
448
+ all_yogas = self.ashraya_yogas() + self.dala_yogas() + self.sankhya_yogas() + self.akriti_yogas()
449
+ all_yogas.sort(key=lambda y: y.to_move)
450
+ return all_yogas
451
+
452
+
453
+ def panchamahapurusha_yogas(self):
454
+ """
455
+ Panchamahapurusha yogas: Mars, Mercury, Jupiter, Venus, or Saturn
456
+ in an angle (house 1, 4, 7, 10) AND in own, moolatrikona, or exaltation sign.
457
+ Returns a list of 5 MahapurushaYoga dataclasses.
458
+ """
459
+ defs = [
460
+ ("Ruchaka", "radiant", "Mars"),
461
+ ("Bhadra", "blessed", "Mercury"),
462
+ ("Hamsa", "swan", "Jupiter"),
463
+ ("Malavya", "of Malava", "Venus"),
464
+ ("Sasa", "rabbit", "Saturn"),
465
+ ]
466
+ yogas = []
467
+ for name, trans, planet_name in defs:
468
+ sign = self.signs().where_is(planet_name)
469
+ house = sign.rashis_from_lagna()
470
+ dig = self.planets()[planet_name].dignity()
471
+ in_angle = house in (1, 4, 7, 10)
472
+ in_own_or_ex = dig in ("OH", "EX", "MT")
473
+ present = in_angle and in_own_or_ex
474
+ yogas.append(MahapurushaYoga(name, trans, planet_name,
475
+ present, house, dig))
476
+ return yogas
477
+
478
+
479
+ def solar_yogas(self):
480
+ """
481
+ Solar yogas based on starry planets (Mars, Mercury, Jupiter, Venus, Saturn)
482
+ — NOT Moon, Rahu, or Ketu — in the 2nd and/or 12th from the Sun.
483
+ Vosi: planet(s) in the 12th from Sun (rises before Sun)
484
+ Vesi: planet(s) in the 2nd from Sun (sets after Sun)
485
+ Ubhayachari: planets in both 2nd and 12th from Sun
486
+ """
487
+ sun_sign = self.signs().where_is("Sun")
488
+ sun_house = sun_sign.rashis_from_lagna()
489
+ lagna_sign = self.signs().lagna()
490
+ # house numbers of 2nd and 12th from Sun
491
+ h12_from_sun = ((sun_house - 2) % 12) + 1 # 12th from Sun
492
+ h2_from_sun = (sun_house % 12) + 1 # 2nd from Sun
493
+
494
+ STARRY = {"Mars", "Mercury", "Jupiter", "Venus", "Saturn"}
495
+ sign_12 = self.signs()[lagna_sign.astrological_signs_forward(h12_from_sun)]
496
+ sign_2 = self.signs()[lagna_sign.astrological_signs_forward(h2_from_sun)]
497
+
498
+ vosi_planets = [p.identity() for p in sign_12.karakas()
499
+ if p.identity() in STARRY]
500
+ vesi_planets = [p.identity() for p in sign_2.karakas()
501
+ if p.identity() in STARRY]
502
+
503
+ yogas = [
504
+ SolarYoga("Vosi", vosi_planets, len(vosi_planets) > 0),
505
+ SolarYoga("Vesi", vesi_planets, len(vesi_planets) > 0),
506
+ SolarYoga("Ubhayachari", vosi_planets + vesi_planets,
507
+ len(vosi_planets) > 0 and len(vesi_planets) > 0),
508
+ ]
509
+ return yogas
510
+
511
+
512
+ def lunar_yogas(self):
513
+ """
514
+ Lunar yogas based on planets (not Sun, Rahu, Ketu) in the
515
+ 2nd and/or 12th from the Moon.
516
+ Anapha: planet(s) in the 12th from Moon
517
+ Sunapha: planet(s) in the 2nd from Moon
518
+ Durudhara: planets in both 2nd and 12th from Moon
519
+ Kemadruma: none of the above
520
+ """
521
+ moon_sign = self.signs().where_is("Moon")
522
+ moon_house = moon_sign.rashis_from_lagna()
523
+ lagna_sign = self.signs().lagna()
524
+ h12_from_moon = ((moon_house - 2) % 12) + 1
525
+ h2_from_moon = (moon_house % 12) + 1
526
+
527
+ ELIGIBLE = {"Mars", "Mercury", "Jupiter", "Venus", "Saturn"}
528
+ sign_12 = self.signs()[lagna_sign.astrological_signs_forward(h12_from_moon)]
529
+ sign_2 = self.signs()[lagna_sign.astrological_signs_forward(h2_from_moon)]
530
+
531
+ anapha_planets = [p.identity() for p in sign_12.karakas()
532
+ if p.identity() in ELIGIBLE]
533
+ sunapha_planets = [p.identity() for p in sign_2.karakas()
534
+ if p.identity() in ELIGIBLE]
535
+
536
+ has_anapha = len(anapha_planets) > 0
537
+ has_sunapha = len(sunapha_planets) > 0
538
+
539
+ yogas = [
540
+ LunarYoga("Anapha", anapha_planets, has_anapha),
541
+ LunarYoga("Sunapha", sunapha_planets, has_sunapha),
542
+ LunarYoga("Durudhara", anapha_planets + sunapha_planets,
543
+ has_anapha and has_sunapha),
544
+ LunarYoga("Kemadruma", [], not has_anapha and not has_sunapha),
545
+ ]
546
+ return yogas
547
+
548
+ def jaimini_kemadruma(self):
549
+ """
550
+ Jaimini Kemadruma Yoga (1.2.119-120)
551
+
552
+ from the svamsha (AK in D9), lagna, or pada:
553
+ if there are an equal number of malefics in the 2nd and 8th,
554
+ with no benefics in either house, that is kemadruma yoga
555
+
556
+ count backward if the reference sign is even (jaimini direction rule)
557
+ malefics are planets where .nature() == "Malefic"
558
+
559
+ also checks from AK in rashi (D1)
560
+
561
+ moon aspecting the malefics makes it more severe (1.2.120)
562
+
563
+ returns a list of dicts, one per reference point where kemadruma is present
564
+ each dict has: reference, second_malefics, eighth_malefics, moon_aspects, sign
565
+ """
566
+ ak = self.planets().jaimini_karakas()[0]
567
+ d9 = self.master.varga(9)
568
+
569
+ # reference points: (name, sign, varga to use)
570
+ refs = [
571
+ ("lagna", self.lagna(), self),
572
+ ("AK in D1", self.signs().where_is(ak.identity()), self),
573
+ ("pada", self.pada(), self),
574
+ ("svamsha", d9.where_is(ak.identity()), d9),
575
+ ]
576
+
577
+ moon_sign = self.signs()[self.planets().moon().sign()]
578
+
579
+ results = []
580
+ for name, ref_sign, varga in refs:
581
+ direction = 1 if ref_sign.sign() % 2 == 1 else -1
582
+ second = varga.signs()[ref_sign.astrological_signs_forward(2 * direction)]
583
+ eighth = varga.signs()[ref_sign.astrological_signs_forward(8 * direction)]
584
+
585
+ second_malefics = [p for p in second.grahas() if p.nature() == "Malefic"]
586
+ eighth_malefics = [p for p in eighth.grahas() if p.nature() == "Malefic"]
587
+ second_benefics = [p for p in second.grahas() if p.nature() == "Benefic"]
588
+ eighth_benefics = [p for p in eighth.grahas() if p.nature() == "Benefic"]
589
+
590
+ if (len(second_malefics) == 0 or len(eighth_malefics) == 0):
591
+ continue
592
+ if len(second_malefics) != len(eighth_malefics):
593
+ continue
594
+ if second_benefics or eighth_benefics:
595
+ continue
596
+
597
+ # check if moon aspects either house (1.2.120 — more severe)
598
+ moon_aspects_second = self.signs().rashi_aspect_from_to(moon_sign, second)
599
+ moon_aspects_eighth = self.signs().rashi_aspect_from_to(moon_sign, eighth)
600
+
601
+ results.append({
602
+ "reference": name,
603
+ "sign": ref_sign,
604
+ "second": second,
605
+ "second_malefics": second_malefics,
606
+ "eighth": eighth,
607
+ "eighth_malefics": eighth_malefics,
608
+ "moon_aspects": moon_aspects_second or moon_aspects_eighth,
609
+ })
610
+
611
+ return results
612
+
@@ -0,0 +1,50 @@
1
+ # This file is part of libaditya.
2
+ #
3
+ # Copyright (c) 2025 Josh Harper <humanhaven@substack.com>
4
+ #
5
+ # libaditya is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU Affero General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # libaditya is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU Affero General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Affero General Public License
16
+ # along with libaditya. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ from libaditya.objects import EphContext, Sun
19
+
20
+ class Returns:
21
+ """
22
+ inherits unto Rashi
23
+ provides calculations for returns, e.g., solar return, etc.
24
+
25
+ TODO:
26
+ """
27
+
28
+ def solar_return(self, year: int = None) -> EphContext:
29
+ """
30
+ return EphContext for the next time the Sun is at the same degree at it is in this Rashi
31
+
32
+ if year is None, solar_return will calculate for the current year of the persons life
33
+ i.e., year=0 is the birth year; year=1 is the next time it is at that degree
34
+
35
+ of if you are 36, Rashi().solar_return() will give the current solar return chart for the time
36
+ in between being 36 and 37
37
+ """
38
+ if year == None:
39
+ year = int(self.context.timeJD.current_age())
40
+ context = self.context
41
+ original_sun = self.planets().sun()
42
+ return_longitude = original_sun.ecliptic_longitude()
43
+ while year > 0:
44
+ this_sun = Sun(context)
45
+ # move it forward slightly, so that when we ingress it to its ecliptic_longitude, its find the next time it is there
46
+ this_sun = this_sun.ingress(this_sun.ecliptic_longitude()+.1)
47
+ next_sun = this_sun.ingress(return_longitude)
48
+ context = next_sun.context
49
+ year -= 1
50
+ return self.master._new_context(context)