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/__init__.py +151 -0
- libephemeris/angles.py +106 -0
- libephemeris/arabic_parts.py +232 -0
- libephemeris/constants.py +318 -0
- libephemeris/crossing.py +326 -0
- libephemeris/fixed_stars.py +228 -0
- libephemeris/houses.py +1625 -0
- libephemeris/lunar.py +269 -0
- libephemeris/minor_bodies.py +398 -0
- libephemeris/planets.py +1228 -0
- libephemeris/state.py +213 -0
- libephemeris/time_utils.py +136 -0
- libephemeris/utils.py +36 -0
- libephemeris-0.1.6.dist-info/METADATA +376 -0
- libephemeris-0.1.6.dist-info/RECORD +18 -0
- libephemeris-0.1.6.dist-info/WHEEL +5 -0
- libephemeris-0.1.6.dist-info/licenses/LICENSE +13 -0
- libephemeris-0.1.6.dist-info/top_level.txt +1 -0
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
|