libephemeris 0.1.6__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.
@@ -0,0 +1,318 @@
1
+ """
2
+ Swiss Ephemeris API-compatible constants for libephemeris.
3
+
4
+ This module defines all constants used for planetary calculations, including:
5
+ - Planet/Body IDs: Numeric identifiers for celestial bodies
6
+ - Calculation Flags: Bitwise flags controlling observation parameters
7
+ - Sidereal Modes: Ayanamsha systems for sidereal astrology
8
+ - Calendar Systems: Julian vs Gregorian calendar selection
9
+ - Eclipse Types: Classification of solar and lunar eclipses
10
+
11
+ Constants are organized into logical groups for easy navigation.
12
+ Values match Swiss Ephemeris v2.x for API compatibility.
13
+ """
14
+
15
+ # =============================================================================
16
+ # PLANET AND BODY IDENTIFIERS
17
+ # =============================================================================
18
+
19
+ # Special values
20
+ SE_ECL_NUT: int = -1 # Nutation and obliquity calculation
21
+
22
+ # Major planets (traditional + modern)
23
+ SE_SUN: int = 0
24
+ SE_MOON: int = 1
25
+ SE_MERCURY: int = 2
26
+ SE_VENUS: int = 3
27
+ SE_MARS: int = 4
28
+ SE_JUPITER: int = 5
29
+ SE_SATURN: int = 6
30
+ SE_URANUS: int = 7
31
+ SE_NEPTUNE: int = 8
32
+ SE_PLUTO: int = 9
33
+
34
+ # Lunar nodes and apsides
35
+ SE_MEAN_NODE: int = 10 # Mean lunar node (Dragon's Head)
36
+ SE_TRUE_NODE: int = 11 # True (osculating) lunar node
37
+ SE_MEAN_APOG: int = 12 # Mean lunar apogee (Black Moon Lilith)
38
+ SE_OSCU_APOG: int = 13 # Osculating (true) lunar apogee
39
+
40
+ # Earth and centaurs
41
+ SE_EARTH: int = 14
42
+ SE_CHIRON: int = 15 # Centaur between Saturn and Uranus
43
+ SE_PHOLUS: int = 16 # Centaur beyond Saturn
44
+
45
+ # Main belt asteroids
46
+ SE_CERES: int = 17
47
+ SE_PALLAS: int = 18
48
+ SE_JUNO: int = 19
49
+ SE_VESTA: int = 20
50
+
51
+ # Interpolated lunar apsides
52
+ SE_INTP_APOG: int = 21 # Interpolated apogee
53
+ SE_INTP_PERG: int = 22 # Interpolated perigee
54
+
55
+ # Count and offsets
56
+ SE_NPLANETS: int = 23 # Total number of standard planet IDs
57
+ SE_AST_OFFSET: int = 10000 # Offset for asteroid catalog numbers
58
+ SE_VARUNA: int = SE_AST_OFFSET + 20000 # TNO Varuna
59
+ SE_FICT_OFFSET: int = 40 # Offset for fictitious bodies
60
+ SE_NFICT_ELEM: int = 15 # Number of fictitious elements
61
+ SE_COMET_OFFSET: int = 1000 # Offset for comet IDs
62
+ SE_NALL_NAT_POINTS: int = (
63
+ SE_NPLANETS + SE_NFICT_ELEM + SE_AST_OFFSET + SE_COMET_OFFSET
64
+ )
65
+
66
+ # Trans-Neptunian Objects (TNOs) - Catalog number + offset
67
+ SE_ERIS: int = 136199 + SE_AST_OFFSET # Largest known dwarf planet
68
+ SE_SEDNA: int = 90377 + SE_AST_OFFSET # Detached TNO
69
+ SE_HAUMEA: int = 136108 + SE_AST_OFFSET # Fast-rotating dwarf planet
70
+ SE_MAKEMAKE: int = 136472 + SE_AST_OFFSET # Classical Kuiper belt object
71
+ SE_IXION: int = 28978 + SE_AST_OFFSET # Plutino
72
+ SE_ORCUS: int = 90482 + SE_AST_OFFSET # Plutino, "anti-Pluto"
73
+ SE_QUAOAR: int = 50000 + SE_AST_OFFSET # Classical KBO
74
+
75
+ # =============================================================================
76
+ # VIRTUAL POINTS AND CALCULATED POSITIONS
77
+ # =============================================================================
78
+
79
+ # Fixed Stars (high offset to avoid ID collisions)
80
+ SE_FIXSTAR_OFFSET: int = 1000000
81
+ SE_REGULUS: int = SE_FIXSTAR_OFFSET + 1 # Alpha Leonis
82
+ SE_SPICA_STAR: int = SE_FIXSTAR_OFFSET + 2 # Alpha Virginis
83
+
84
+ # Astrological Angles (requires observer location)
85
+ SE_ANGLE_OFFSET: int = 2000000
86
+ SE_ASCENDANT: int = SE_ANGLE_OFFSET + 1 # Rising sign/degree
87
+ SE_MC: int = SE_ANGLE_OFFSET + 2 # Medium Coeli (Midheaven)
88
+ SE_DESCENDANT: int = SE_ANGLE_OFFSET + 3 # Setting point (Asc + 180°)
89
+ SE_IC: int = SE_ANGLE_OFFSET + 4 # Imum Coeli (MC + 180°)
90
+ SE_VERTEX: int = SE_ANGLE_OFFSET + 5 # Western intersection of prime vertical
91
+ SE_ANTIVERTEX: int = SE_ANGLE_OFFSET + 6 # Eastern intersection (Vertex + 180°)
92
+
93
+ # Arabic Parts (Lots) - Require pre-calculated planetary positions
94
+ SE_ARABIC_OFFSET: int = 3000000
95
+ SE_PARS_FORTUNAE: int = SE_ARABIC_OFFSET + 1 # Part of Fortune
96
+ SE_PARS_SPIRITUS: int = SE_ARABIC_OFFSET + 2 # Part of Spirit
97
+ SE_PARS_AMORIS: int = SE_ARABIC_OFFSET + 3 # Part of Eros/Love
98
+ SE_PARS_FIDEI: int = SE_ARABIC_OFFSET + 4 # Part of Faith
99
+
100
+ # =============================================================================
101
+ # CALCULATION FLAGS
102
+ # =============================================================================
103
+ # Ephemeris selection (currently only SWIEPH/JPL mode supported)
104
+ SEFLG_JPLEPH: int = 1 # Use JPL ephemeris
105
+ SEFLG_SWIEPH: int = 2 # Use Swiss Ephemeris (libephemeris uses Skyfield/JPL)
106
+ SEFLG_MOSEPH: int = 4 # Use Moshier ephemeris (not supported)
107
+
108
+ # Observer location and reference frame
109
+ SEFLG_HELCTR: int = 8 # Heliocentric position
110
+ SEFLG_TRUEPOS: int = 16 # True geometric position (no light time)
111
+ SEFLG_J2000: int = 32 # J2000.0 reference frame
112
+ SEFLG_NONUT: int = 64 # No nutation
113
+ SEFLG_SPEED3: int = 128 # High precision speed (3 calls)
114
+ SEFLG_SPEED: int = 256 # Calculate velocity
115
+ SEFLG_NOGDEFL: int = 512 # No gravitational deflection
116
+ SEFLG_NOABERR: int = 1024 # No aberration
117
+ SEFLG_ASTROMETRIC: int = SEFLG_NOABERR | SEFLG_NOGDEFL # Astrometric position
118
+ SEFLG_EQUATORIAL: int = 2048 # Equatorial coordinates (RA/Dec)
119
+ SEFLG_XYZ: int = 4096 # Cartesian coordinates
120
+ SEFLG_RADIANS: int = 8192 # Return angles in radians
121
+ SEFLG_BARYCTR: int = 16384 # Barycentric position
122
+ SEFLG_TOPOCTR: int = 32768 # Topocentric position (requires swe_set_topo)
123
+ SEFLG_SIDEREAL: int = 65536 # Sidereal positions
124
+ SEFLG_ICRS: int = 131072 # ICRS reference frame
125
+
126
+ # =============================================================================
127
+ # PYSWISSEPH-COMPATIBLE FLAG ALIASES (FLG_* instead of SEFLG_*)
128
+ # =============================================================================
129
+ # pyswisseph uses FLG_* prefix while Swiss Ephemeris C library uses SEFLG_*
130
+ # These aliases provide full API compatibility with pyswisseph
131
+
132
+ FLG_JPLEPH: int = SEFLG_JPLEPH
133
+ FLG_SWIEPH: int = SEFLG_SWIEPH
134
+ FLG_MOSEPH: int = SEFLG_MOSEPH
135
+ FLG_HELCTR: int = SEFLG_HELCTR
136
+ FLG_TRUEPOS: int = SEFLG_TRUEPOS
137
+ FLG_J2000: int = SEFLG_J2000
138
+ FLG_NONUT: int = SEFLG_NONUT
139
+ FLG_SPEED3: int = SEFLG_SPEED3
140
+ FLG_SPEED: int = SEFLG_SPEED
141
+ FLG_NOGDEFL: int = SEFLG_NOGDEFL
142
+ FLG_NOABERR: int = SEFLG_NOABERR
143
+ FLG_ASTROMETRIC: int = SEFLG_ASTROMETRIC
144
+ FLG_EQUATORIAL: int = SEFLG_EQUATORIAL # Equatorial coordinates (RA/Dec)
145
+ FLG_XYZ: int = SEFLG_XYZ
146
+ FLG_RADIANS: int = SEFLG_RADIANS
147
+ FLG_BARYCTR: int = SEFLG_BARYCTR
148
+ FLG_TOPOCTR: int = SEFLG_TOPOCTR
149
+ FLG_SIDEREAL: int = SEFLG_SIDEREAL
150
+ FLG_ICRS: int = SEFLG_ICRS
151
+
152
+ # Other aliases
153
+ AST_OFFSET: int = SE_AST_OFFSET
154
+
155
+ # =============================================================================
156
+ # SIDEREAL (AYANAMSHA) MODES
157
+ # =============================================================================
158
+
159
+ # Western sidereal traditions
160
+ SE_SIDM_FAGAN_BRADLEY: int = 0 # Fagan-Bradley (Synetic Vernal Point)
161
+ SE_SIDM_LAHIRI: int = 1 # Lahiri (Chitrapaksha, Indian standard)
162
+ SE_SIDM_DELUCE: int = 2 # De Luce
163
+ SE_SIDM_RAMAN: int = 3 # B.V. Raman
164
+ SE_SIDM_USHASHASHI: int = 4 # Ushashashi
165
+ SE_SIDM_KRISHNAMURTI: int = 5 # K.S. Krishnamurti (KP)
166
+ SE_SIDM_DJWHAL_KHUL: int = 6 # Djwhal Khul (Alice Bailey)
167
+ SE_SIDM_YUKTESHWAR: int = 7 # Yukteshwar
168
+ SE_SIDM_JN_BHASIN: int = 8 # J.N. Bhasin
169
+
170
+ # Babylonian traditions
171
+ SE_SIDM_BABYL_KUGLER1: int = 9 # Kugler variant 1
172
+ SE_SIDM_BABYL_KUGLER2: int = 10 # Kugler variant 2
173
+ SE_SIDM_BABYL_KUGLER3: int = 11 # Kugler variant 3
174
+ SE_SIDM_BABYL_HUBER: int = 12 # Huber
175
+ SE_SIDM_BABYL_ETPSC: int = 13 # ETPSC
176
+ SE_SIDM_BABYL_BRITTON: int = 38 # Britton
177
+
178
+ # Star-based ayanamshas
179
+ SE_SIDM_ALDEBARAN_15TAU: int = 14 # Aldebaran at 15° Taurus
180
+ SE_SIDM_TRUE_CITRA: int = 27 # True position of Spica (180° Citra)
181
+ SE_SIDM_TRUE_REVATI: int = 28 # True position of Revati
182
+ SE_SIDM_TRUE_PUSHYA: int = 29 # True position of Pushya (Cancri)
183
+ SE_SIDM_TRUE_MULA: int = 35 # True position of Mula (λ Scorpii)
184
+ SE_SIDM_TRUE_SHEORAN: int = 39 # True Sheoran
185
+
186
+ # Historical epochs
187
+ SE_SIDM_HIPPARCHOS: int = 15 # Hipparchos (128 BC)
188
+ SE_SIDM_SASSANIAN: int = 16 # Sassanian
189
+ SE_SIDM_J2000: int = 18 # J2000.0 (no ayanamsha)
190
+ SE_SIDM_J1900: int = 19 # J1900.0
191
+ SE_SIDM_B1950: int = 20 # B1950.0
192
+
193
+ # Indian traditions
194
+ SE_SIDM_SURYASIDDHANTA: int = 21 # Suryasiddhanta
195
+ SE_SIDM_SURYASIDDHANTA_MSUN: int = 22 # Suryasiddhanta (mean Sun)
196
+ SE_SIDM_ARYABHATA: int = 23 # Aryabhata
197
+ SE_SIDM_ARYABHATA_MSUN: int = 24 # Aryabhata (mean Sun)
198
+ SE_SIDM_ARYABHATA_522: int = 37 # Aryabhata 522
199
+ SE_SIDM_SS_REVATI: int = 25 # Suryasiddhanta Revati
200
+ SE_SIDM_SS_CITRA: int = 26 # Suryasiddhanta Citra
201
+
202
+ # Galactic alignment systems
203
+ SE_SIDM_GALCENT_0SAG: int = 17 # Galactic Center at 0° Sagittarius
204
+ SE_SIDM_GALCENT_RGILBRAND: int = 30 # Galactic Center (Gil Brand)
205
+ SE_SIDM_GALCENT_MULA_WILHELM: int = 36 # Galactic Center at Mula (Wilhelm)
206
+ SE_SIDM_GALCENT_COCHRANE: int = 40 # Galactic Center (Cochrane)
207
+ SE_SIDM_GALEQU_IAU1958: int = 31 # Galactic Equator (IAU 1958)
208
+ SE_SIDM_GALEQU_TRUE: int = 32 # Galactic Equator (True)
209
+ SE_SIDM_GALEQU_MULA: int = 33 # Galactic Equator at Mula
210
+ SE_SIDM_GALEQU_FIORENZA: int = 41 # Galactic Equator (Fiorenza)
211
+ SE_SIDM_GALALIGN_MARDYKS: int = 34 # Galactic Alignment (Mardyks)
212
+
213
+ # Other systems
214
+ SE_SIDM_VALENS_MOON: int = 42 # Vettius Valens (Moon-based)
215
+ SE_SIDM_USER: int = 255 # User-defined ayanamsha
216
+
217
+ # =============================================================================
218
+ # PYSWISSEPH-COMPATIBLE SIDEREAL MODE ALIASES (SIDM_* instead of SE_SIDM_*)
219
+ # =============================================================================
220
+ # pyswisseph uses SIDM_* prefix while Swiss Ephemeris C library uses SE_SIDM_*
221
+
222
+ # Western sidereal traditions
223
+ SIDM_FAGAN_BRADLEY: int = SE_SIDM_FAGAN_BRADLEY
224
+ SIDM_LAHIRI: int = SE_SIDM_LAHIRI
225
+ SIDM_DELUCE: int = SE_SIDM_DELUCE
226
+ SIDM_RAMAN: int = SE_SIDM_RAMAN
227
+ SIDM_USHASHASHI: int = SE_SIDM_USHASHASHI
228
+ SIDM_KRISHNAMURTI: int = SE_SIDM_KRISHNAMURTI
229
+ SIDM_DJWHAL_KHUL: int = SE_SIDM_DJWHAL_KHUL
230
+ SIDM_YUKTESHWAR: int = SE_SIDM_YUKTESHWAR
231
+ SIDM_JN_BHASIN: int = SE_SIDM_JN_BHASIN
232
+
233
+ # Babylonian traditions
234
+ SIDM_BABYL_KUGLER1: int = SE_SIDM_BABYL_KUGLER1
235
+ SIDM_BABYL_KUGLER2: int = SE_SIDM_BABYL_KUGLER2
236
+ SIDM_BABYL_KUGLER3: int = SE_SIDM_BABYL_KUGLER3
237
+ SIDM_BABYL_HUBER: int = SE_SIDM_BABYL_HUBER
238
+ SIDM_BABYL_ETPSC: int = SE_SIDM_BABYL_ETPSC
239
+ SIDM_BABYL_BRITTON: int = SE_SIDM_BABYL_BRITTON
240
+
241
+ # Star-based ayanamshas
242
+ SIDM_ALDEBARAN_15TAU: int = SE_SIDM_ALDEBARAN_15TAU
243
+ SIDM_TRUE_CITRA: int = SE_SIDM_TRUE_CITRA
244
+ SIDM_TRUE_REVATI: int = SE_SIDM_TRUE_REVATI
245
+ SIDM_TRUE_PUSHYA: int = SE_SIDM_TRUE_PUSHYA
246
+ SIDM_TRUE_MULA: int = SE_SIDM_TRUE_MULA
247
+ SIDM_TRUE_SHEORAN: int = SE_SIDM_TRUE_SHEORAN
248
+
249
+ # Historical epochs
250
+ SIDM_HIPPARCHOS: int = SE_SIDM_HIPPARCHOS
251
+ SIDM_SASSANIAN: int = SE_SIDM_SASSANIAN
252
+ SIDM_J2000: int = SE_SIDM_J2000
253
+ SIDM_J1900: int = SE_SIDM_J1900
254
+ SIDM_B1950: int = SE_SIDM_B1950
255
+
256
+ # Indian traditions
257
+ SIDM_SURYASIDDHANTA: int = SE_SIDM_SURYASIDDHANTA
258
+ SIDM_SURYASIDDHANTA_MSUN: int = SE_SIDM_SURYASIDDHANTA_MSUN
259
+ SIDM_ARYABHATA: int = SE_SIDM_ARYABHATA
260
+ SIDM_ARYABHATA_MSUN: int = SE_SIDM_ARYABHATA_MSUN
261
+ SIDM_ARYABHATA_522: int = SE_SIDM_ARYABHATA_522
262
+ SIDM_SS_REVATI: int = SE_SIDM_SS_REVATI
263
+ SIDM_SS_CITRA: int = SE_SIDM_SS_CITRA
264
+
265
+ # Galactic alignment systems
266
+ SIDM_GALCENT_0SAG: int = SE_SIDM_GALCENT_0SAG
267
+ SIDM_GALCENT_RGILBRAND: int = SE_SIDM_GALCENT_RGILBRAND
268
+ SIDM_GALCENT_MULA_WILHELM: int = SE_SIDM_GALCENT_MULA_WILHELM
269
+ SIDM_GALCENT_COCHRANE: int = SE_SIDM_GALCENT_COCHRANE
270
+ SIDM_GALEQU_IAU1958: int = SE_SIDM_GALEQU_IAU1958
271
+ SIDM_GALEQU_TRUE: int = SE_SIDM_GALEQU_TRUE
272
+ SIDM_GALEQU_MULA: int = SE_SIDM_GALEQU_MULA
273
+ SIDM_GALEQU_FIORENZA: int = SE_SIDM_GALEQU_FIORENZA
274
+ SIDM_GALALIGN_MARDYKS: int = SE_SIDM_GALALIGN_MARDYKS
275
+
276
+ # Other systems
277
+ SIDM_VALENS_MOON: int = SE_SIDM_VALENS_MOON
278
+ SIDM_USER: int = SE_SIDM_USER
279
+
280
+
281
+ # =============================================================================
282
+ # CALENDAR SYSTEMS
283
+ # =============================================================================
284
+
285
+ SE_JUL_CAL: int = 0 # Julian calendar
286
+ SE_GREG_CAL: int = 1 # Gregorian calendar
287
+
288
+ # =============================================================================
289
+ # ECLIPSE TYPES AND FLAGS
290
+ # =============================================================================
291
+ # Eclipse geometry types
292
+ SE_ECL_CENTRAL: int = 1 # Central eclipse (Moon's shadow axis crosses Earth)
293
+ SE_ECL_NONCENTRAL: int = 2 # Non-central eclipse
294
+ SE_ECL_TOTAL: int = 4 # Total eclipse (Sun/Earth completely covered)
295
+ SE_ECL_ANNULAR: int = 8 # Annular eclipse (ring of fire)
296
+ SE_ECL_PARTIAL: int = 16 # Partial eclipse
297
+ SE_ECL_ANNULAR_TOTAL: int = 32 # Hybrid eclipse (annular at some locations, total at others)
298
+ SE_ECL_PENUMBRAL: int = 64 # Penumbral lunar eclipse
299
+
300
+ # Composite eclipse type masks
301
+ SE_ECL_ALLTYPES_SOLAR: int = (
302
+ SE_ECL_CENTRAL
303
+ | SE_ECL_NONCENTRAL
304
+ | SE_ECL_TOTAL
305
+ | SE_ECL_ANNULAR
306
+ | SE_ECL_PARTIAL
307
+ | SE_ECL_ANNULAR_TOTAL
308
+ )
309
+ SE_ECL_ALLTYPES_LUNAR: int = SE_ECL_TOTAL | SE_ECL_PARTIAL | SE_ECL_PENUMBRAL
310
+
311
+ # Eclipse visibility and contact flags
312
+ SE_ECL_VISIBLE: int = 128 # Eclipse visible at location
313
+ SE_ECL_MAX_VISIBLE: int = 256 # Maximum phase visible
314
+ SE_ECL_1ST_VISIBLE: int = 512 # First contact visible
315
+ SE_ECL_2ND_VISIBLE: int = 1024 # Second contact visible
316
+ SE_ECL_3RD_VISIBLE: int = 2048 # Third contact visible
317
+ SE_ECL_4TH_VISIBLE: int = 4096 # Fourth contact visible
318
+ SE_ECL_ONE_TRY: int = 32768 # Try only once (optimization flag)
@@ -0,0 +1,326 @@
1
+ """
2
+ Crossing event calculations for libephemeris.
3
+
4
+ Finds exact times when the Sun or Moon cross specific ecliptic longitudes.
5
+ Uses Newton-Raphson iteration for sub-arcsecond precision.
6
+
7
+ Functions:
8
+ - swe_solcross_ut: Sun crossing events (e.g., ingresses, equinoxes)
9
+ - swe_mooncross_ut: Moon crossing events (for lunar mansion calculations)
10
+ - swe_cross_ut: Generic planet crossing
11
+
12
+ FIXME: Precision - Newton-Raphson convergence tolerance
13
+ Current tolerance: 1 arcsecond (1/3600°)
14
+ Iterations: 20-30 max
15
+ Typical convergence: 3-6 iterations for Sun, 5-10 for Moon
16
+
17
+ For sub-arcsecond precision, consider iterative refinement or
18
+ use Swiss Ephemeris swe_solcross_ut2 with higher tolerances.
19
+
20
+ Algorithm: Initial linear estimate + Newton-Raphson refinement
21
+ References: Meeus "Astronomical Algorithms" Ch. 5 (interpolation)
22
+ """
23
+
24
+ from typing import Tuple
25
+ from .constants import SEFLG_SWIEPH, SEFLG_SPEED, SE_SUN, SE_MOON
26
+ from .planets import swe_calc_ut
27
+
28
+
29
+ def swe_solcross_ut(
30
+ x2cross: float, jd_ut: float, flag: int = SEFLG_SWIEPH
31
+ ) -> float:
32
+ """
33
+ Find when the Sun crosses a specific ecliptic longitude.
34
+
35
+ Searches FORWARD in time for the next crossing after jd_ut.
36
+
37
+ Args:
38
+ x2cross: Target ecliptic longitude in degrees (0-360)
39
+ jd_ut: Julian Day (UT) to start search from
40
+ flag: Calculation flags (SEFLG_SWIEPH, etc.)
41
+
42
+ Returns:
43
+ float: Julian Day of crossing (UT)
44
+
45
+ Raises:
46
+ RuntimeError: If convergence fails or calculation error occurs
47
+
48
+ Algorithm:
49
+ 1. Get current Sun position and velocity
50
+ 2. Linear estimate: dt = (target - current) / velocity
51
+ 3. Refine with Newton-Raphson: jd_new = jd + (target - actual) / velocity
52
+ 4. Converge to < 1 arcsecond (~2.8 seconds of time)
53
+
54
+ Precision:
55
+ Typically < 1 arcsecond (< 5 seconds of time for Sun)
56
+
57
+ FIXME: Precision - Convergence tolerance 1 arcsec
58
+ For higher precision, reduce tolerance in line 74
59
+ Swiss Ephemeris achieves ~0.001 arcsec with tighter iteration
60
+
61
+ Example:
62
+ >>> # Find next Aries ingress (0°)
63
+ >>> jd_ingress = swe_solcross_ut(0.0, jd_now)
64
+ >>> # Find summer solstice (90°)
65
+ >>> jd_solstice = swe_solcross_ut(90.0, jd_now)
66
+ """
67
+ x2cross = x2cross % 360.0
68
+
69
+ try:
70
+ pos, _ = swe_calc_ut(jd_ut, SE_SUN, flag | SEFLG_SPEED)
71
+ lon_start = pos[0]
72
+ speed = pos[3] # degrees/day
73
+ except Exception as e:
74
+ raise RuntimeError(f"Failed to calculate Sun position: {e}")
75
+
76
+ # Calculate angular distance to target (always forward)
77
+ diff = (x2cross - lon_start) % 360.0
78
+
79
+ # Handle retrograde motion
80
+ if speed < 0 and diff > 0:
81
+ diff -= 360.0
82
+
83
+ # If already very close, look for next complete crossing
84
+ if abs(diff) < 1e-5:
85
+ if speed > 0:
86
+ diff += 360.0
87
+ else:
88
+ diff -= 360.0
89
+
90
+ # Initial time estimate (linear approximation)
91
+ if speed == 0:
92
+ speed = 0.9856 # Average Sun motion ~1°/day
93
+
94
+ dt_guess = diff / speed
95
+ jd_guess = jd_ut + dt_guess
96
+
97
+ # FIXME: Precision - Newton-Raphson max iterations = 20
98
+ # May fail for very slow bodies or near stationary points
99
+ jd = jd_guess
100
+ for iteration in range(20):
101
+ try:
102
+ pos, _ = swe_calc_ut(jd, SE_SUN, flag | SEFLG_SPEED)
103
+ lon = pos[0]
104
+ speed = pos[3]
105
+ except Exception as e:
106
+ raise RuntimeError(f"Failed to calculate Sun position during iteration: {e}")
107
+
108
+ # Angular difference to target
109
+ diff = (x2cross - lon) % 360.0
110
+ if diff > 180:
111
+ diff -= 360
112
+
113
+ # FIXME: Precision - Convergence tolerance 1 arcsecond
114
+ # Check convergence (< 1 arcsecond = 1/3600 degree)
115
+ if abs(diff) < 1.0 / 3600.0:
116
+ return jd
117
+
118
+ # Newton-Raphson step
119
+ if abs(speed) < 0.01:
120
+ speed = 0.9856
121
+
122
+ jd += diff / speed
123
+
124
+ # Safety: prevent divergence
125
+ if abs(jd - jd_guess) > 366:
126
+ raise RuntimeError("Solar crossing search diverged")
127
+
128
+ raise RuntimeError("Maximum iterations reached in solar crossing calculation")
129
+
130
+
131
+ def swe_mooncross_ut(
132
+ x2cross: float, jd_ut: float, flag: int = SEFLG_SWIEPH
133
+ ) -> float:
134
+ """
135
+ Find when the Moon crosses a specific ecliptic longitude.
136
+
137
+ Searches FORWARD in time for the next crossing after jd_ut.
138
+
139
+ Args:
140
+ x2cross: Target ecliptic longitude in degrees (0-360)
141
+ jd_ut: Julian Day (UT) to start search from
142
+ flag: Calculation flags (SEFLG_SWIEPH, etc.)
143
+
144
+ Returns:
145
+ float: Julian Day of crossing (UT)
146
+
147
+ Raises:
148
+ RuntimeError: If convergence fails or calculation error occurs
149
+
150
+ Note:
151
+ Moon moves ~13° per day (27.3 day cycle).
152
+ More variable speed than Sun due to orbit eccentricity.
153
+ Uses 30 iterations vs Sun's 20 for added robustness.
154
+
155
+ FIXME: Precision - Same 1 arcsec tolerance as Sun
156
+ Moon's higher speed means ~0.1 seconds time precision
157
+
158
+ Example:
159
+ >>> # Find next new moon (Sun-Moon conjunction at same longitude)
160
+ >>> sun_pos, _ = swe_calc_ut(jd_now, SE_SUN, SEFLG_SWIEPH)
161
+ >>> jd_new_moon = swe_mooncross_ut(sun_pos[0], jd_now)
162
+ """
163
+ x2cross = x2cross % 360.0
164
+
165
+ try:
166
+ pos, _ = swe_calc_ut(jd_ut, SE_MOON, flag | SEFLG_SPEED)
167
+ lon_start = pos[0]
168
+ speed = pos[3] # degrees/day
169
+ except Exception as e:
170
+ raise RuntimeError(f"Failed to calculate Moon position: {e}")
171
+
172
+ # Calculate initial guess for NEXT crossing
173
+ diff = (x2cross - lon_start) % 360.0
174
+
175
+ if speed < 0 and diff > 0:
176
+ diff -= 360.0
177
+
178
+ if abs(diff) < 1e-5:
179
+ if speed > 0:
180
+ diff += 360.0
181
+ else:
182
+ diff -= 360.0
183
+
184
+ # Initial time estimate
185
+ if speed == 0:
186
+ speed = 13.176 # Average Moon motion ~13.18°/day
187
+
188
+ dt_guess = diff / speed
189
+ jd_guess = jd_ut + dt_guess
190
+
191
+ # FIXME: Precision - Moon uses 30 iterations (vs Sun's 20)
192
+ # Higher iteration count for Moon's variable speed
193
+ jd = jd_guess
194
+ for iteration in range(30):
195
+ try:
196
+ pos, _ = swe_calc_ut(jd, SE_MOON, flag | SEFLG_SPEED)
197
+ lon = pos[0]
198
+ speed = pos[3]
199
+ except Exception as e:
200
+ raise RuntimeError(f"Failed to calculate Moon position during iteration: {e}")
201
+
202
+ # Difference to target
203
+ diff = (x2cross - lon) % 360.0
204
+ if diff > 180:
205
+ diff -= 360
206
+
207
+ # Check convergence (< 1 arcsecond)
208
+ if abs(diff) < 1.0 / 3600.0:
209
+ return jd
210
+
211
+ # Newton-Raphson step
212
+ if abs(speed) < 0.1:
213
+ speed = 13.176
214
+
215
+ jd += diff / speed
216
+
217
+ # Safety check
218
+ if abs(jd - jd_guess) > 31: # More than a month
219
+ raise RuntimeError("Moon crossing search diverged")
220
+
221
+ raise RuntimeError("Maximum iterations reached in moon crossing calculation")
222
+
223
+
224
+ def swe_cross_ut(
225
+ planet_id: int, x2cross: float, jd_ut: float, flag: int = SEFLG_SWIEPH
226
+ ) -> float:
227
+ """
228
+ Find when any planet crosses a specific ecliptic longitude.
229
+
230
+ Generic version for all planets (Mercury, Venus, Mars, etc.).
231
+
232
+ Args:
233
+ planet_id: Planet ID (SE_MERCURY, SE_VENUS, etc.)
234
+ x2cross: Target ecliptic longitude in degrees (0-360)
235
+ jd_ut: Julian Day (UT) to start search from
236
+ flag: Calculation flags
237
+
238
+ Returns:
239
+ float: Julian Day of crossing (UT)
240
+
241
+ Raises:
242
+ RuntimeError: If convergence fails or calculation error occurs
243
+
244
+ Note:
245
+ Uses adaptive iteration count based on typical planet speed.
246
+ Slower planets (Jupiter, Saturn) may need more iterations.
247
+
248
+ FIXME: Precision - Not optimized for retrograde stations
249
+ Near stationary points (speed ~ 0), convergence is slower
250
+ May need adaptive tolerance or damped Newton-Raphson
251
+
252
+ Example:
253
+ >>> # Mars ingress into Aries
254
+ >>> jd_mars_aries = swe_cross_ut(SE_MARS, 0.0, jd_now)
255
+ """
256
+ x2cross = x2cross % 360.0
257
+
258
+ try:
259
+ pos, _ = swe_calc_ut(jd_ut, planet_id, flag | SEFLG_SPEED)
260
+ lon_start = pos[0]
261
+ speed = pos[3]
262
+ except Exception as e:
263
+ raise RuntimeError(f"Failed to calculate planet position: {e}")
264
+
265
+ # Estimate typical speed if near zero
266
+ typical_speeds = {
267
+ 2: 1.4, # Mercury
268
+ 3: 1.2, # Venus
269
+ 4: 0.5, # Mars
270
+ 5: 0.08, # Jupiter
271
+ 6: 0.03, # Saturn
272
+ 7: 0.01, # Uranus
273
+ 8: 0.006, # Neptune
274
+ }
275
+ speed_default = typical_speeds.get(planet_id, 0.5)
276
+
277
+ # Calculate initial guess
278
+ diff = (x2cross - lon_start) % 360.0
279
+
280
+ if speed < 0 and diff > 0:
281
+ diff -= 360.0
282
+
283
+ if abs(diff) < 1e-5:
284
+ if speed > 0:
285
+ diff += 360.0
286
+ else:
287
+ diff -= 360.0
288
+
289
+ if abs(speed) < 0.001:
290
+ speed = speed_default
291
+
292
+ dt_guess = diff / speed
293
+ jd_guess = jd_ut + dt_guess
294
+
295
+ # Adaptive iteration count
296
+ max_iter = 40 if abs(speed) < 0.1 else 20
297
+
298
+ # FIXME: Precision - Adaptive iterations needed for slow planets
299
+ # Outer planets may need 40+ iterations near stationary points
300
+ jd = jd_guess
301
+ for iteration in range(max_iter):
302
+ try:
303
+ pos, _ = swe_calc_ut(jd, planet_id, flag | SEFLG_SPEED)
304
+ lon = pos[0]
305
+ speed = pos[3]
306
+ except Exception as e:
307
+ raise RuntimeError(f"Failed to calculate planet position during iteration: {e}")
308
+
309
+ diff = (x2cross - lon) % 360.0
310
+ if diff > 180:
311
+ diff -= 360
312
+
313
+ if abs(diff) < 1.0 / 3600.0:
314
+ return jd
315
+
316
+ if abs(speed) < 0.001:
317
+ speed = speed_default
318
+
319
+ jd += diff / speed
320
+
321
+ # Safety: longer range for slower planets
322
+ max_range = 400 if abs(speed_default) < 0.1 else 366
323
+ if abs(jd - jd_guess) > max_range:
324
+ raise RuntimeError("Planet crossing search diverged")
325
+
326
+ raise RuntimeError("Maximum iterations reached in planet crossing calculation")