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.
libephemeris/state.py ADDED
@@ -0,0 +1,213 @@
1
+ """
2
+ Global state management for libephemeris.
3
+
4
+ This module maintains the library's singleton state including:
5
+ - Ephemeris data loader (Skyfield Loader)
6
+ - Planetary ephemeris (DE421 or other JPL files)
7
+ - Timescale (for UTC/TT conversions)
8
+ - Observer topocentric location
9
+ - Sidereal mode configuration
10
+ - Cached angles for Arabic parts calculations
11
+
12
+ All state is stored in module-level globals to provide SwissEphemeris-compatible
13
+ stateful API behavior. This is thread-unsafe by design, matching SwissEph behavior.
14
+ """
15
+
16
+ import os
17
+ from typing import Optional, Union
18
+ from skyfield.api import Loader, Topos
19
+ from skyfield.timelib import Timescale
20
+ from skyfield.jpllib import SpiceKernel
21
+
22
+ # =============================================================================
23
+ # GLOBAL STATE VARIABLES
24
+ # =============================================================================
25
+
26
+ _EPHEMERIS_PATH: Optional[str] = None # Custom ephemeris directory
27
+ _LOADER: Optional[Loader] = None # Skyfield data loader
28
+ _PLANETS: Optional[SpiceKernel] = None # Loaded planetary ephemeris
29
+ _TS: Optional[Timescale] = None # Timescale object
30
+ _TOPO: Optional[Topos] = None # Observer location
31
+ _SIDEREAL_MODE: Optional[int] = None # Active sidereal mode ID
32
+ _SIDEREAL_AYAN_T0: float = 0.0 # Ayanamsha value at reference epoch
33
+ _SIDEREAL_T0: float = 0.0 # Reference epoch (JD) for ayanamsha
34
+ _ANGLES_CACHE: dict[str, float] = {} # Pre-calculated angles {name: longitude}
35
+
36
+
37
+ def get_loader() -> Loader:
38
+ """
39
+ Get or create the Skyfield data loader.
40
+
41
+ Returns:
42
+ Loader: Skyfield Loader instance for downloading/caching ephemeris files
43
+
44
+ Note:
45
+ Data files are cached in the parent directory of this module by default.
46
+ """
47
+ global _LOADER
48
+ if _LOADER is None:
49
+ data_dir = os.path.join(os.path.dirname(__file__), "..")
50
+ _LOADER = Loader(data_dir)
51
+ return _LOADER
52
+
53
+
54
+ def get_timescale() -> Timescale:
55
+ """
56
+ Get or create the Skyfield timescale object.
57
+
58
+ Returns:
59
+ Timescale: Skyfield timescale for time conversions (UTC, TT, etc.)
60
+
61
+ Note:
62
+ Automatically downloads IERS data for Delta T calculations if needed.
63
+ """
64
+ global _TS
65
+ if _TS is None:
66
+ load = get_loader()
67
+ _TS = load.timescale()
68
+ return _TS
69
+
70
+
71
+ def get_planets() -> SpiceKernel:
72
+ """
73
+ Get or load the planetary ephemeris (DE421 by default).
74
+
75
+ Returns:
76
+ SpiceKernel: Loaded JPL ephemeris kernel containing planetary positions
77
+
78
+ Raises:
79
+ FileNotFoundError: If de421.bsp cannot be found or downloaded
80
+
81
+ Note:
82
+ Searches for de421.bsp in the workspace root, then downloads if not found.
83
+ """
84
+ global _PLANETS
85
+ if _PLANETS is None:
86
+ load = get_loader()
87
+ base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
88
+ bsp_path = os.path.join(base_dir, "de421.bsp")
89
+ if not os.path.exists(bsp_path):
90
+ _PLANETS = load("de421.bsp")
91
+ else:
92
+ _PLANETS = load(bsp_path)
93
+ return _PLANETS
94
+
95
+
96
+ def set_topo(lon: float, lat: float, alt: float) -> None:
97
+ """
98
+ Set observer's topocentric location for planet calculations.
99
+
100
+ Args:
101
+ lon: Geographic longitude in degrees (East positive)
102
+ lat: Geographic latitude in degrees (North positive)
103
+ alt: Elevation above sea level in meters
104
+
105
+ Note:
106
+ Required for topocentric calculations (SEFLG_TOPOCTR),
107
+ angles (Ascendant, MC), and Arabic parts.
108
+ """
109
+ global _TOPO
110
+ _TOPO = Topos(latitude_degrees=lat, longitude_degrees=lon, elevation_m=alt)
111
+
112
+
113
+ def get_topo() -> Optional[Topos]:
114
+ """
115
+ Get the observer's topocentric location.
116
+
117
+ Returns:
118
+ Optional[Topos]: Current observer location or None if not set
119
+ """
120
+ return _TOPO
121
+
122
+
123
+ def set_sid_mode(mode: int, t0: float = 0.0, ayan_t0: float = 0.0) -> None:
124
+ """
125
+ Set the sidereal mode (ayanamsha system) for calculations.
126
+
127
+ Args:
128
+ mode: Sidereal mode ID (SE_SIDM_*) or 255 for custom
129
+ t0: Reference epoch (Julian Day) for custom ayanamsha (default: J2000.0)
130
+ ayan_t0: Ayanamsha value at t0 in degrees (for custom mode)
131
+
132
+ Note:
133
+ Affects all position calculations when SEFLG_SIDEREAL is set.
134
+ Default is Lahiri (SE_SIDM_LAHIRI) if never set.
135
+ """
136
+ global _SIDEREAL_MODE, _SIDEREAL_T0, _SIDEREAL_AYAN_T0
137
+ _SIDEREAL_MODE = mode
138
+ _SIDEREAL_T0 = t0 if t0 != 0.0 else 2451545.0
139
+ _SIDEREAL_AYAN_T0 = ayan_t0
140
+
141
+
142
+ def get_sid_mode(full: bool = False) -> Union[int, tuple[int, float, float]]:
143
+ """
144
+ Get the current sidereal mode configuration.
145
+
146
+ Args:
147
+ full: If True, return (mode, t0, ayan_t0); if False, return only mode ID
148
+
149
+ Returns:
150
+ int or tuple: Sidereal mode ID, or full configuration tuple
151
+
152
+ Note:
153
+ Returns SE_SIDM_LAHIRI (1) by default if never set.
154
+ """
155
+ if full:
156
+ return _SIDEREAL_MODE, _SIDEREAL_T0, _SIDEREAL_AYAN_T0
157
+ return _SIDEREAL_MODE if _SIDEREAL_MODE is not None else 1
158
+
159
+
160
+ def get_angles_cache() -> dict[str, float]:
161
+ """
162
+ Get cached astrological angles for the current calculation context.
163
+
164
+ Returns:
165
+ dict: Cached angles {name: longitude_degrees}
166
+
167
+ Note:
168
+ Used by Arabic parts calculations which require pre-calculated
169
+ planetary positions and angles.
170
+ """
171
+ return _ANGLES_CACHE
172
+
173
+
174
+ def set_angles_cache(angles: dict[str, float]) -> None:
175
+ """
176
+ Cache pre-calculated angles for use in Arabic parts and other virtual points.
177
+
178
+ Args:
179
+ angles: Dictionary of angles {name: longitude_degrees}
180
+ e.g., {"Sun": 120.5, "Moon": 240.3, "Asc": 15.7}
181
+
182
+ Note:
183
+ Creates a copy to prevent external mutation of cache.
184
+ """
185
+ global _ANGLES_CACHE
186
+ _ANGLES_CACHE = angles.copy()
187
+
188
+
189
+ def clear_angles_cache() -> None:
190
+ """
191
+ Clear the angles cache.
192
+
193
+ Use this between unrelated calculation contexts to prevent stale data.
194
+ """
195
+ global _ANGLES_CACHE
196
+ _ANGLES_CACHE = {}
197
+
198
+ _ANGLES_CACHE = {}
199
+
200
+
201
+ def set_ephe_path(path: Optional[str]) -> None:
202
+ """
203
+ Set the path for ephemeris files.
204
+
205
+ Args:
206
+ path: Path to directory containing ephemeris files.
207
+
208
+ Note:
209
+ This is currently a placeholder for compatibility.
210
+ libephemeris uses Skyfield which manages its own cache.
211
+ """
212
+ global _EPHEMERIS_PATH
213
+ _EPHEMERIS_PATH = path
@@ -0,0 +1,136 @@
1
+ """
2
+ Time conversion utilities for libephemeris.
3
+
4
+ Implements standard astronomical time functions for conversions between:
5
+ - Calendar dates and Julian Day numbers
6
+ - Gregorian and Julian calendar systems
7
+ - UT1 (Universal Time) and TT (Terrestrial Time)
8
+
9
+ Functions match the Swiss Ephemeris API for compatibility.
10
+ All algorithms follow Meeus "Astronomical Algorithms" (1998).
11
+ """
12
+
13
+ from .constants import SE_GREG_CAL
14
+ from .state import get_timescale
15
+
16
+
17
+ def swe_julday(
18
+ year: int, month: int, day: int, hour: float, gregflag: int = SE_GREG_CAL
19
+ ) -> float:
20
+ """
21
+ Convert calendar date to Julian Day number.
22
+
23
+ Args:
24
+ year: Calendar year (negative for BCE)
25
+ month: Month (1-12)
26
+ day: Day of month (1-31)
27
+ hour: Decimal hour (0.0-23.999...)
28
+ gregflag: SE_GREG_CAL (1) for Gregorian, SE_JUL_CAL (0) for Julian
29
+
30
+ Returns:
31
+ float: Julian Day number (days since JD 0.0 = noon Jan 1, 4713 BCE)
32
+
33
+ Note:
34
+ Transition date: Oct 15, 1582 (Gregorian) = Oct 5, 1582 (Julian)
35
+ JD 2451545.0 = Jan 1, 2000 12:00 TT (J2000.0 epoch)
36
+ """
37
+ if month <= 2:
38
+ year -= 1
39
+ month += 12
40
+
41
+ a = int(year / 100)
42
+
43
+ if gregflag == SE_GREG_CAL:
44
+ b = 2 - a + int(a / 4)
45
+ else:
46
+ b = 0
47
+
48
+ jd = (
49
+ int(365.25 * (year + 4716))
50
+ + int(30.6001 * (month + 1))
51
+ + day
52
+ + hour / 24.0
53
+ + b
54
+ - 1524.5
55
+ )
56
+ return jd
57
+
58
+
59
+ def swe_revjul(jd: float, gregflag: int = SE_GREG_CAL) -> tuple[int, int, int, float]:
60
+ """
61
+ Convert Julian Day number to calendar date.
62
+
63
+ Args:
64
+ jd: Julian Day number
65
+ gregflag: SE_GREG_CAL (1) for Gregorian, SE_JUL_CAL (0) for Julian
66
+
67
+ Returns:
68
+ tuple: (year, month, day, hour) where:
69
+ - year: Calendar year
70
+ - month: Month (1-12)
71
+ - day: Integer day of month
72
+ - hour: Decimal hour (0.0-23.999...)
73
+
74
+ Note:
75
+ Automatic Gregorian calendar used for JD >= 2299161 (Oct 15, 1582)
76
+ unless Julian calendar explicitly requested.
77
+ """
78
+ jd = jd + 0.5
79
+ z = int(jd)
80
+ f = jd - z
81
+
82
+ if z < 2299161:
83
+ a = z
84
+ else:
85
+ if gregflag == SE_GREG_CAL:
86
+ alpha = int((z - 1867216.25) / 36524.25)
87
+ a = z + 1 + alpha - int(alpha / 4)
88
+ else:
89
+ a = z
90
+
91
+ b = a + 1524
92
+ c = int((b - 122.1) / 365.25)
93
+ d = int(365.25 * c)
94
+ e = int((b - d) / 30.6001)
95
+
96
+ day = b - d - int(30.6001 * e) + f
97
+ if e < 14:
98
+ month = e - 1
99
+ else:
100
+ month = e - 13
101
+
102
+ if month > 2:
103
+ year = c - 4716
104
+ else:
105
+ year = c - 4715
106
+
107
+ d_int = int(day)
108
+ d_frac = day - d_int
109
+ hour = d_frac * 24.0
110
+ day = d_int
111
+
112
+ return year, month, day, hour
113
+
114
+
115
+ def swe_deltat(tjd: float) -> float:
116
+ """
117
+ Calculate Delta T (TT - UT1) for a given Julian Day.
118
+
119
+ Args:
120
+ tjd: Julian Day number in UT1
121
+
122
+ Returns:
123
+ float: Delta T in days (TT - UT1)
124
+
125
+ Note:
126
+ Delta T accounts for Earth's irregular rotation and is required
127
+ for high-precision planetary calculations. Values are obtained
128
+ from IERS (International Earth Rotation Service) data.
129
+
130
+ For modern dates: ~0.0008 days (~69 seconds as of 2024)
131
+ For historical dates: Calculated from polynomial models
132
+ """
133
+ ts = get_timescale()
134
+ t = ts.ut1_jd(tjd)
135
+ return t.delta_t
136
+
libephemeris/utils.py ADDED
@@ -0,0 +1,36 @@
1
+ """
2
+ Utility functions for libephemeris.
3
+
4
+ Provides helper functions compatible with pyswisseph API including
5
+ angular calculations and other mathematical utilities.
6
+ """
7
+
8
+
9
+ def difdeg2n(p1: float, p2: float) -> float:
10
+ """
11
+ Calculate distance in degrees p1 - p2 normalized to [-180;180].
12
+
13
+ Compatible with pyswisseph's swe.difdeg2n() function.
14
+ Computes the signed angular difference, handling 360° wrapping.
15
+
16
+ Args:
17
+ p1: First angle in degrees
18
+ p2: Second angle in degrees
19
+
20
+ Returns:
21
+ Normalized difference in range [-180, 180]
22
+
23
+ Examples:
24
+ >>> difdeg2n(10, 20)
25
+ -10.0
26
+ >>> difdeg2n(350, 10)
27
+ -20.0
28
+ >>> difdeg2n(10, 350)
29
+ 20.0
30
+ >>> difdeg2n(180, 0)
31
+ 180.0
32
+ """
33
+ diff = (p1 - p2) % 360.0
34
+ if diff > 180.0:
35
+ diff -= 360.0
36
+ return diff