pyephemera 0.4.1__py3-none-win_amd64.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.
- ephemera/__init__.py +809 -0
- ephemera/_native.py +165 -0
- ephemera/data/eph-01as-chiron.bin +0 -0
- ephemera/data/eph-01as.bin +0 -0
- ephemera/data/eph-1as-chiron.bin +0 -0
- ephemera/data/eph-1as.bin +0 -0
- ephemera/data/eph-asteroids.bin +0 -0
- ephemera/data/eph-chiron.bin +0 -0
- ephemera/data/eph-stars.bin +0 -0
- ephemera/ephemera_native.dll +0 -0
- ephemera/py.typed +0 -0
- pyephemera-0.4.1.dist-info/METADATA +97 -0
- pyephemera-0.4.1.dist-info/RECORD +15 -0
- pyephemera-0.4.1.dist-info/WHEEL +5 -0
- pyephemera-0.4.1.dist-info/licenses/LICENSE +21 -0
ephemera/__init__.py
ADDED
|
@@ -0,0 +1,809 @@
|
|
|
1
|
+
"""Ephemera: a pocket-sized, MIT-licensed ephemeris for astrology and astronomy.
|
|
2
|
+
|
|
3
|
+
import ephemera as eph
|
|
4
|
+
|
|
5
|
+
jd = eph.julian_date(2026, 1, 1.5) # 1 Jan 2026, 12:00 UT
|
|
6
|
+
moon = eph.position(eph.Body.MOON, jd) # degrees, AU, per day
|
|
7
|
+
cusps = eph.houses(jd, -0.1278, 51.5074, eph.HouseSystem.PLACIDUS)
|
|
8
|
+
lahiri = eph.ayanamsa(eph.Ayanamsa.LAHIRI, eph.ut_to_tt(jd, 2026))
|
|
9
|
+
|
|
10
|
+
The 1" planetary series with Chiron (1550-2650) is built in. More data
|
|
11
|
+
files come with the package (``load_package_data('stars')``) or from the
|
|
12
|
+
repository (``load_data(path)``).
|
|
13
|
+
|
|
14
|
+
Angles are degrees, distances AU, times Julian days (UT or TT as named);
|
|
15
|
+
calls that find nothing return None. The data loaded is shared by the
|
|
16
|
+
whole process: load files before computing from several threads.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import datetime as _dt
|
|
22
|
+
import math
|
|
23
|
+
import os
|
|
24
|
+
from ctypes import byref, c_double, c_int
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from enum import IntEnum, IntFlag
|
|
27
|
+
from typing import List, NamedTuple, Optional, Union
|
|
28
|
+
|
|
29
|
+
from . import _native as _n
|
|
30
|
+
from ._native import lib as _lib
|
|
31
|
+
|
|
32
|
+
__version__ = "0.4.1"
|
|
33
|
+
|
|
34
|
+
DEG = 180.0 / math.pi
|
|
35
|
+
_RAD = math.pi / 180.0
|
|
36
|
+
MINOR_BASE = 1000
|
|
37
|
+
"""Body id of the first loaded minor planet (``minor_body`` gives the id)."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Body(IntEnum):
|
|
41
|
+
MERCURY = 0
|
|
42
|
+
VENUS = 1
|
|
43
|
+
EARTH = 2
|
|
44
|
+
MARS = 3
|
|
45
|
+
JUPITER = 4
|
|
46
|
+
SATURN = 5
|
|
47
|
+
URANUS = 6
|
|
48
|
+
NEPTUNE = 7
|
|
49
|
+
PLUTO = 8
|
|
50
|
+
SUN = 9
|
|
51
|
+
MOON = 10
|
|
52
|
+
CERES = 11
|
|
53
|
+
PALLAS = 12
|
|
54
|
+
JUNO = 13
|
|
55
|
+
VESTA = 14
|
|
56
|
+
CHIRON = 15
|
|
57
|
+
MEAN_NODE = 16
|
|
58
|
+
TRUE_NODE = 17
|
|
59
|
+
MEAN_LILITH = 18
|
|
60
|
+
OSCU_LILITH = 19
|
|
61
|
+
# The Hamburg school's Uranian points (Witte, Sieggrün).
|
|
62
|
+
CUPIDO = 20
|
|
63
|
+
HADES = 21
|
|
64
|
+
ZEUS = 22
|
|
65
|
+
KRONOS = 23
|
|
66
|
+
APOLLON = 24
|
|
67
|
+
ADMETOS = 25
|
|
68
|
+
VULKANUS = 26
|
|
69
|
+
POSEIDON = 27
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Ayanamsa(IntEnum):
|
|
73
|
+
LAHIRI = 0
|
|
74
|
+
FAGAN_BRADLEY = 1
|
|
75
|
+
RAMAN = 2
|
|
76
|
+
KRISHNAMURTI = 3
|
|
77
|
+
YUKTESHWAR = 4
|
|
78
|
+
DE_LUCE = 5
|
|
79
|
+
USHA_SHASHI = 6
|
|
80
|
+
DJWHAL_KHUL = 7
|
|
81
|
+
JN_BHASIN = 8
|
|
82
|
+
BABYLONIAN_KUGLER1 = 9
|
|
83
|
+
BABYLONIAN_KUGLER2 = 10
|
|
84
|
+
BABYLONIAN_KUGLER3 = 11
|
|
85
|
+
BABYLONIAN_HUBER = 12
|
|
86
|
+
BABYLONIAN_ETA_PISCIUM = 13
|
|
87
|
+
BABYLONIAN_ALDEBARAN = 14
|
|
88
|
+
HIPPARCHOS = 15
|
|
89
|
+
SASSANIAN = 16
|
|
90
|
+
J2000 = 17
|
|
91
|
+
J1900 = 18
|
|
92
|
+
B1950 = 19
|
|
93
|
+
SURYASIDDHANTA = 20
|
|
94
|
+
SURYASIDDHANTA_MEAN_SUN = 21
|
|
95
|
+
ARYABHATA = 22
|
|
96
|
+
ARYABHATA_MEAN_SUN = 23
|
|
97
|
+
SS_REVATI = 24
|
|
98
|
+
SS_CITRA = 25
|
|
99
|
+
SKYDRAM = 26
|
|
100
|
+
ARYABHATA_522 = 27
|
|
101
|
+
BABYLONIAN_BRITTON = 28
|
|
102
|
+
GALACTIC_EQUATOR_FIORENZA = 29
|
|
103
|
+
VETTIUS_VALENS = 30
|
|
104
|
+
LAHIRI_1940 = 31
|
|
105
|
+
LAHIRI_VP285 = 32
|
|
106
|
+
KRISHNAMURTI_SENTHILATHIBAN = 33
|
|
107
|
+
LAHIRI_ICRC = 34
|
|
108
|
+
# Star-based (need the stars: load_package_data('stars')).
|
|
109
|
+
TRUE_CITRA = 35
|
|
110
|
+
TRUE_REVATI = 36
|
|
111
|
+
TRUE_PUSHYA = 37
|
|
112
|
+
TRUE_MULA = 38
|
|
113
|
+
GALACTIC_CENTER = 39
|
|
114
|
+
GALACTIC_CENTER_BRAND = 40
|
|
115
|
+
GALACTIC_CENTER_COCHRANE = 41
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class HouseSystem(IntEnum):
|
|
119
|
+
WHOLE_SIGN = 0
|
|
120
|
+
EQUAL = 1
|
|
121
|
+
PORPHYRY = 2
|
|
122
|
+
PLACIDUS = 3
|
|
123
|
+
KOCH = 4
|
|
124
|
+
REGIOMONTANUS = 5
|
|
125
|
+
CAMPANUS = 6
|
|
126
|
+
ALCABITIUS = 7
|
|
127
|
+
TOPOCENTRIC = 8
|
|
128
|
+
VEHLOW = 9
|
|
129
|
+
EQUAL_MC = 10
|
|
130
|
+
EQUAL_ARIES = 11
|
|
131
|
+
MERIDIAN = 12
|
|
132
|
+
MORINUS = 13
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class OrbitMethod(IntEnum):
|
|
136
|
+
MEAN = 0
|
|
137
|
+
"""Secular mean elements (Simon et al. 1994 / Meeus ch. 31): Mercury..Neptune."""
|
|
138
|
+
OSCULATING = 1
|
|
139
|
+
"""From the position and velocity: the planets but the Earth, Pluto, minor bodies."""
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class EclipseType(IntFlag):
|
|
143
|
+
TOTAL = 1
|
|
144
|
+
ANNULAR = 2
|
|
145
|
+
HYBRID = 4
|
|
146
|
+
PARTIAL = 8
|
|
147
|
+
PENUMBRAL = 16
|
|
148
|
+
NON_CENTRAL = 32
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ---- results ---------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclass(frozen=True)
|
|
155
|
+
class Position:
|
|
156
|
+
longitude: float
|
|
157
|
+
latitude: float
|
|
158
|
+
distance: float
|
|
159
|
+
longitude_speed: float
|
|
160
|
+
latitude_speed: float
|
|
161
|
+
distance_speed: float
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def retrograde(self) -> bool:
|
|
165
|
+
return self.longitude_speed < 0
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class CalendarDate(NamedTuple):
|
|
169
|
+
year: int
|
|
170
|
+
month: int
|
|
171
|
+
day: float # with the fraction of the day
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class JulianDates(NamedTuple):
|
|
175
|
+
jd_tt: float
|
|
176
|
+
jd_ut: float
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class Nutation(NamedTuple):
|
|
180
|
+
longitude: float
|
|
181
|
+
obliquity: float
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class Equatorial(NamedTuple):
|
|
185
|
+
right_ascension: float
|
|
186
|
+
declination: float
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class Heliocentric(NamedTuple):
|
|
190
|
+
longitude: float
|
|
191
|
+
latitude: float
|
|
192
|
+
distance: float
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class AscMc(NamedTuple):
|
|
196
|
+
asc: float
|
|
197
|
+
mc: float
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class HousesInfo(NamedTuple):
|
|
201
|
+
cusps: List[float]
|
|
202
|
+
fallback: bool # the system is undefined here (polar): Porphyry was used
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@dataclass(frozen=True)
|
|
206
|
+
class HouseAngles:
|
|
207
|
+
ascendant: float
|
|
208
|
+
mc: float
|
|
209
|
+
armc: float
|
|
210
|
+
vertex: float
|
|
211
|
+
equatorial_ascendant: float
|
|
212
|
+
co_ascendant_koch: float
|
|
213
|
+
co_ascendant_munkasey: float
|
|
214
|
+
polar_ascendant: float
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@dataclass(frozen=True)
|
|
218
|
+
class Phenomena:
|
|
219
|
+
phase_angle: float
|
|
220
|
+
phase: float # illuminated fraction, 0..1
|
|
221
|
+
elongation: float
|
|
222
|
+
diameter: float
|
|
223
|
+
magnitude: float
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class CustomAyanamsa(NamedTuple):
|
|
227
|
+
"""A user-defined ayanamsa: its value (degrees) at a TT Julian day."""
|
|
228
|
+
jd_t0: float
|
|
229
|
+
value_at_t0: float
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class NodeCrossing(NamedTuple):
|
|
233
|
+
jd: float
|
|
234
|
+
node: str # 'ascending' or 'descending'
|
|
235
|
+
longitude: float
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class Station(NamedTuple):
|
|
239
|
+
jd: float
|
|
240
|
+
turns: str # 'retrograde' or 'direct'
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@dataclass(frozen=True)
|
|
244
|
+
class OrbitPoints:
|
|
245
|
+
ascending_node: Position
|
|
246
|
+
descending_node: Position
|
|
247
|
+
perihelion: Position
|
|
248
|
+
aphelion: Position
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@dataclass(frozen=True)
|
|
252
|
+
class Orbit:
|
|
253
|
+
"""A heliocentric orbit, mean ecliptic and equinox of date."""
|
|
254
|
+
semi_major_axis: float
|
|
255
|
+
eccentricity: float
|
|
256
|
+
inclination: float
|
|
257
|
+
node: float
|
|
258
|
+
perihelion: float
|
|
259
|
+
heliocentric: OrbitPoints
|
|
260
|
+
geocentric: OrbitPoints
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@dataclass(frozen=True)
|
|
264
|
+
class SolarEclipse:
|
|
265
|
+
"""Times are TT Julian days; 0 marks a phase that does not occur."""
|
|
266
|
+
type: EclipseType
|
|
267
|
+
maximum: float
|
|
268
|
+
gamma: float
|
|
269
|
+
magnitude: float
|
|
270
|
+
latitude: float
|
|
271
|
+
longitude: float
|
|
272
|
+
saros: int
|
|
273
|
+
partial_begin: float
|
|
274
|
+
partial_end: float
|
|
275
|
+
central_begin: float
|
|
276
|
+
central_end: float
|
|
277
|
+
center_line_begin: float
|
|
278
|
+
center_line_end: float
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@dataclass(frozen=True)
|
|
282
|
+
class LocalSolarEclipse:
|
|
283
|
+
type: EclipseType
|
|
284
|
+
maximum: float
|
|
285
|
+
first_contact: float
|
|
286
|
+
second_contact: float
|
|
287
|
+
third_contact: float
|
|
288
|
+
fourth_contact: float
|
|
289
|
+
magnitude: float
|
|
290
|
+
obscuration: float
|
|
291
|
+
altitude: float # of the Sun at maximum; below 0: not visible
|
|
292
|
+
saros: int
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@dataclass(frozen=True)
|
|
296
|
+
class LocalEclipseState:
|
|
297
|
+
type: EclipseType # 0: none
|
|
298
|
+
magnitude: float
|
|
299
|
+
obscuration: float
|
|
300
|
+
altitude: float
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@dataclass(frozen=True)
|
|
304
|
+
class LunarEclipse:
|
|
305
|
+
type: EclipseType
|
|
306
|
+
maximum: float
|
|
307
|
+
gamma: float
|
|
308
|
+
umbral_magnitude: float
|
|
309
|
+
penumbral_magnitude: float
|
|
310
|
+
saros: int
|
|
311
|
+
penumbral_begin: float
|
|
312
|
+
penumbral_end: float
|
|
313
|
+
partial_begin: float
|
|
314
|
+
partial_end: float
|
|
315
|
+
total_begin: float
|
|
316
|
+
total_end: float
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class GeoPoint(NamedTuple):
|
|
320
|
+
latitude: float
|
|
321
|
+
longitude: float
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class MinorPlanet(NamedTuple):
|
|
325
|
+
body: int
|
|
326
|
+
number: int
|
|
327
|
+
name: str
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
class SplitLongitude(NamedTuple):
|
|
331
|
+
sign: int # 0 = Aries .. 11 = Pisces
|
|
332
|
+
degrees: int
|
|
333
|
+
minutes: int
|
|
334
|
+
seconds: float
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# ---- helpers -----------------------------------------------------------------
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _year(jd: float) -> float:
|
|
341
|
+
return 2000.0 + (jd - 2451545.0) / 365.25
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _position(p: _n.Position) -> Position:
|
|
345
|
+
return Position(p.longitude * DEG, p.latitude * DEG, p.distance,
|
|
346
|
+
p.longitude_speed * DEG, p.latitude_speed * DEG, p.distance_speed)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _observer(lon: float, lat: float, elevation: float = 0.0) -> _n.Observer:
|
|
350
|
+
return _n.Observer(lon * _RAD, lat * _RAD, elevation)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _str(b: Optional[bytes]) -> str:
|
|
354
|
+
return b.decode("utf-8") if b else ""
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# ---- data files -------------------------------------------------------------
|
|
358
|
+
|
|
359
|
+
PACKAGE_DATA = ("1as", "1as-chiron", "01as", "01as-chiron", "chiron", "asteroids", "stars")
|
|
360
|
+
"""Data files shipped in the package (data/eph-<name>.bin)."""
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def data_path(name: str) -> str:
|
|
364
|
+
"""Path of a packaged data file, e.g. ``data_path('stars')``."""
|
|
365
|
+
if name not in PACKAGE_DATA:
|
|
366
|
+
raise ValueError(f"unknown data file {name!r}; packaged: {', '.join(PACKAGE_DATA)}")
|
|
367
|
+
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "data", f"eph-{name}.bin")
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def load_data(source: Union[bytes, bytearray, memoryview, str, os.PathLike]) -> bool:
|
|
371
|
+
"""Load a data file from its bytes or a path; False if it is rejected.
|
|
372
|
+
Later files override what earlier ones provide."""
|
|
373
|
+
if isinstance(source, (bytes, bytearray, memoryview)):
|
|
374
|
+
data = bytes(source)
|
|
375
|
+
return _lib.eph_load_data(data, len(data)) == 0 # the library copies it
|
|
376
|
+
return _lib.eph_load_file(os.fsencode(source)) == 0
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def load_package_data(*names: str) -> None:
|
|
380
|
+
"""Load packaged data files: '01as', 'chiron', 'asteroids', 'stars', ..."""
|
|
381
|
+
for name in names:
|
|
382
|
+
if not load_data(data_path(name)):
|
|
383
|
+
raise ValueError(f"cannot load data file {name!r}")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def unload_data() -> None:
|
|
387
|
+
"""Unload every data file (the built-in data too; see load_embedded)."""
|
|
388
|
+
_lib.eph_unload_data()
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def load_embedded() -> bool:
|
|
392
|
+
"""Reload the built-in data (the 1" series with Chiron)."""
|
|
393
|
+
return _lib.eph_load_embedded() == 0
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
# ---- time --------------------------------------------------------------------
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def julian_date(year: int, month: int, day: float) -> float:
|
|
400
|
+
"""Julian day of a calendar date (Gregorian; Julian before 1582-10-15);
|
|
401
|
+
``day`` carries the fraction of the day (1.5 = noon)."""
|
|
402
|
+
return _lib.eph_julian_date(year, month, day)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def calendar_date(jd: float) -> CalendarDate:
|
|
406
|
+
y, m, d = c_int(), c_int(), c_double()
|
|
407
|
+
_lib.eph_calendar_date(jd, byref(y), byref(m), byref(d))
|
|
408
|
+
return CalendarDate(y.value, m.value, d.value)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def delta_t(year: float) -> float:
|
|
412
|
+
"""TT - UT, seconds."""
|
|
413
|
+
return _lib.eph_delta_t(year)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def ut_to_tt(jd_ut: float, year: Optional[float] = None) -> float:
|
|
417
|
+
return _lib.eph_ut_to_tt(jd_ut, _year(jd_ut) if year is None else year)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def tt_to_ut(jd_tt: float, year: Optional[float] = None) -> float:
|
|
421
|
+
return _lib.eph_tt_to_ut(jd_tt, _year(jd_tt) if year is None else year)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def utc_to_jd(year: Union[int, _dt.datetime], month: int = 1, day: int = 1, hour: int = 0,
|
|
425
|
+
minute: int = 0, second: float = 0.0) -> Optional[JulianDates]:
|
|
426
|
+
"""Civil UTC (leap seconds from 1972) to TT and UT Julian days; None for an
|
|
427
|
+
invalid date. Also takes a datetime: an aware one is converted to UTC, a
|
|
428
|
+
naive one is taken as UTC."""
|
|
429
|
+
if isinstance(year, _dt.datetime):
|
|
430
|
+
t = year
|
|
431
|
+
if t.tzinfo is not None:
|
|
432
|
+
t = t.astimezone(_dt.timezone.utc)
|
|
433
|
+
year, month, day, hour, minute = t.year, t.month, t.day, t.hour, t.minute
|
|
434
|
+
second = t.second + t.microsecond / 1e6
|
|
435
|
+
tt, ut = c_double(), c_double()
|
|
436
|
+
if _lib.eph_utc_to_jd(year, month, day, hour, minute, second, byref(tt), byref(ut)) != 0:
|
|
437
|
+
return None
|
|
438
|
+
return JulianDates(tt.value, ut.value)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def tdb_minus_tt(jd_tt: float) -> float:
|
|
442
|
+
"""TDB - TT, seconds."""
|
|
443
|
+
return _lib.eph_tdb_minus_tt(jd_tt)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def sidereal_time(jd_ut: float) -> float:
|
|
447
|
+
"""Greenwich apparent sidereal time (IAU 2006), degrees."""
|
|
448
|
+
return _lib.eph_sidereal_time(jd_ut) * DEG
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def equation_of_time(jd_ut: float) -> Optional[float]:
|
|
452
|
+
"""Apparent minus mean solar time, minutes; None without the Sun's data."""
|
|
453
|
+
e = c_double()
|
|
454
|
+
return e.value * 1440.0 if _lib.eph_equation_of_time(jd_ut, byref(e)) == 0 else None
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
# ---- frames ----------------------------------------------------------------
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def mean_obliquity(jd_tt: float) -> float:
|
|
461
|
+
"""Mean obliquity of the ecliptic (IAU 2006), degrees."""
|
|
462
|
+
return _lib.eph_mean_obliquity(jd_tt) * DEG
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def nutation(jd_tt: float) -> Nutation:
|
|
466
|
+
"""Nutation in longitude and obliquity (IAU 2000B), degrees."""
|
|
467
|
+
dpsi, deps = c_double(), c_double()
|
|
468
|
+
_lib.eph_nutation(jd_tt, byref(dpsi), byref(deps))
|
|
469
|
+
return Nutation(dpsi.value * DEG, deps.value * DEG)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def ecliptic_to_equatorial(lon: float, lat: float, jd_tt: float) -> Equatorial:
|
|
473
|
+
"""Apparent ecliptic to equatorial coordinates (true obliquity), degrees."""
|
|
474
|
+
ra, dec = c_double(), c_double()
|
|
475
|
+
_lib.eph_ecliptic_to_equatorial(lon * _RAD, lat * _RAD, jd_tt, byref(ra), byref(dec))
|
|
476
|
+
return Equatorial(ra.value * DEG, dec.value * DEG)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
# ---- positions ---------------------------------------------------------------
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def position(body: int, jd_ut: float, apparent: bool = True,
|
|
483
|
+
year: Optional[float] = None) -> Optional[Position]:
|
|
484
|
+
"""Geocentric ecliptic position of date for a UT Julian day, apparent by
|
|
485
|
+
default; None without data. ``year`` (fractional) selects ΔT."""
|
|
486
|
+
out = _n.Position()
|
|
487
|
+
fn = _lib.eph_position_apparent_ut if apparent else _lib.eph_position_ut
|
|
488
|
+
rc = fn(body, jd_ut, _year(jd_ut) if year is None else year, byref(out))
|
|
489
|
+
return _position(out) if rc == 0 else None
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def position_tt(body: int, jd_tt: float, apparent: bool = True) -> Optional[Position]:
|
|
493
|
+
out = _n.Position()
|
|
494
|
+
fn = _lib.eph_position_apparent_tt if apparent else _lib.eph_position_tt
|
|
495
|
+
return _position(out) if fn(body, jd_tt, byref(out)) == 0 else None
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def heliocentric(body: int, jd_tt: float) -> Optional[Heliocentric]:
|
|
499
|
+
"""Heliocentric ecliptic of date; None for the Sun, the Moon or without data."""
|
|
500
|
+
lon, lat, dist = c_double(), c_double(), c_double()
|
|
501
|
+
if _lib.eph_heliocentric_tt(body, jd_tt, byref(lon), byref(lat), byref(dist)) != 0:
|
|
502
|
+
return None
|
|
503
|
+
return Heliocentric(lon.value * DEG, lat.value * DEG, dist.value)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def body_name(body: int) -> str:
|
|
507
|
+
"""'Sun', 'Moon', ... (minor planets: their names)."""
|
|
508
|
+
if body >= MINOR_BASE:
|
|
509
|
+
return minor_name(body)
|
|
510
|
+
return _str(_lib.eph_body_name(body))
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def topocentric(body: int, jd_ut: float, lon: float, lat: float, elevation: float = 0.0,
|
|
514
|
+
year: Optional[float] = None) -> Optional[Position]:
|
|
515
|
+
"""Apparent position seen from a place (east longitude, latitude in degrees,
|
|
516
|
+
elevation in metres)."""
|
|
517
|
+
out = _n.Position()
|
|
518
|
+
obs = _observer(lon, lat, elevation)
|
|
519
|
+
rc = _lib.eph_position_topocentric_ut(body, jd_ut, _year(jd_ut) if year is None else year,
|
|
520
|
+
byref(obs), byref(out))
|
|
521
|
+
return _position(out) if rc == 0 else None
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
# ---- houses ------------------------------------------------------------------
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def asc_mc(jd_ut: float, lon: float, lat: float) -> AscMc:
|
|
528
|
+
asc, mc = c_double(), c_double()
|
|
529
|
+
obs = _observer(lon, lat)
|
|
530
|
+
_lib.eph_asc_mc(jd_ut, byref(obs), byref(asc), byref(mc))
|
|
531
|
+
return AscMc(asc.value * DEG, mc.value * DEG)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def houses_info(jd_ut: float, lon: float, lat: float,
|
|
535
|
+
system: int = HouseSystem.PORPHYRY) -> Optional[HousesInfo]:
|
|
536
|
+
"""The twelve cusps, and whether the system is undefined at this latitude
|
|
537
|
+
(Placidus, Koch, Alcabitius inside the polar circles: Porphyry is used)."""
|
|
538
|
+
cusps = (c_double * 12)()
|
|
539
|
+
obs = _observer(lon, lat)
|
|
540
|
+
rc = _lib.eph_houses(jd_ut, byref(obs), system, cusps)
|
|
541
|
+
if rc < 0:
|
|
542
|
+
return None
|
|
543
|
+
return HousesInfo([c * DEG for c in cusps], rc == 1)
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def houses(jd_ut: float, lon: float, lat: float,
|
|
547
|
+
system: int = HouseSystem.PORPHYRY) -> Optional[List[float]]:
|
|
548
|
+
"""Twelve cusps (degrees); None on bad arguments."""
|
|
549
|
+
info = houses_info(jd_ut, lon, lat, system)
|
|
550
|
+
return info.cusps if info else None
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def house_angles(jd_ut: float, lon: float, lat: float) -> HouseAngles:
|
|
554
|
+
"""Ascendant, MC, ARMC, Vertex, equatorial Ascendant, co-Ascendants and
|
|
555
|
+
polar Ascendant, degrees."""
|
|
556
|
+
out = _n.Angles()
|
|
557
|
+
obs = _observer(lon, lat)
|
|
558
|
+
_lib.eph_house_angles(jd_ut, byref(obs), byref(out))
|
|
559
|
+
return HouseAngles(*(getattr(out, f) * DEG for f, _ in _n.Angles._fields_))
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def house_position(cusps: List[float], lon: float) -> float:
|
|
563
|
+
"""House 1.0 .. 12.999 of a longitude, given cusps from houses()."""
|
|
564
|
+
arr = (c_double * 12)(*(c * _RAD for c in cusps))
|
|
565
|
+
return _lib.eph_house_position(arr, lon * _RAD)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
# ---- phenomena -----------------------------------------------------------------
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def phenomena(body: int, jd_tt: float) -> Optional[Phenomena]:
|
|
572
|
+
"""Phase angle, illuminated fraction, elongation, apparent diameter
|
|
573
|
+
(degrees) and visual magnitude; None without data."""
|
|
574
|
+
out = _n.Phenomena()
|
|
575
|
+
if _lib.eph_phenomena_tt(body, jd_tt, byref(out)) != 0:
|
|
576
|
+
return None
|
|
577
|
+
return Phenomena(out.phase_angle * DEG, out.phase, out.elongation * DEG,
|
|
578
|
+
out.diameter * DEG, out.magnitude)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
# ---- searches --------------------------------------------------------------------
|
|
582
|
+
|
|
583
|
+
AyanamsaOption = Union[int, CustomAyanamsa]
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def longitude_crossing(body: int, lon: float, jd_ut: float, direction: int = 1,
|
|
587
|
+
max_days: float = 400, ayanamsa: Optional[AyanamsaOption] = None
|
|
588
|
+
) -> Optional[float]:
|
|
589
|
+
"""When the apparent longitude reaches ``lon`` (returns, ingresses); UT in
|
|
590
|
+
and out, None if not within max_days. With ``ayanamsa`` (an Ayanamsa or a
|
|
591
|
+
CustomAyanamsa) the longitude is sidereal, the ayanamsa taken at the
|
|
592
|
+
crossing itself (sidereal returns and ingresses)."""
|
|
593
|
+
out = c_double()
|
|
594
|
+
if ayanamsa is None:
|
|
595
|
+
rc = _lib.eph_longitude_crossing_ut(body, lon * _RAD, jd_ut, direction, max_days, byref(out))
|
|
596
|
+
return out.value if rc == 0 else None
|
|
597
|
+
jd_tt = ut_to_tt(jd_ut)
|
|
598
|
+
if isinstance(ayanamsa, CustomAyanamsa):
|
|
599
|
+
rc = _lib.eph_longitude_crossing_sidereal_custom(
|
|
600
|
+
body, lon * _RAD, ayanamsa.jd_t0, ayanamsa.value_at_t0 * _RAD, jd_tt, direction,
|
|
601
|
+
max_days, byref(out))
|
|
602
|
+
else:
|
|
603
|
+
rc = _lib.eph_longitude_crossing_sidereal(body, lon * _RAD, int(ayanamsa), jd_tt, direction,
|
|
604
|
+
max_days, byref(out))
|
|
605
|
+
return tt_to_ut(out.value) if rc == 0 else None
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def heliocentric_crossing(body: int, lon: float, jd_tt: float, direction: int = 1,
|
|
609
|
+
max_days: float = 40000, ayanamsa: Optional[AyanamsaOption] = None
|
|
610
|
+
) -> Optional[float]:
|
|
611
|
+
"""When the heliocentric longitude reaches ``lon``; TT in and out."""
|
|
612
|
+
out = c_double()
|
|
613
|
+
if ayanamsa is None:
|
|
614
|
+
rc = _lib.eph_heliocentric_crossing(body, lon * _RAD, jd_tt, direction, max_days, byref(out))
|
|
615
|
+
elif isinstance(ayanamsa, CustomAyanamsa):
|
|
616
|
+
rc = _lib.eph_heliocentric_crossing_sidereal_custom(
|
|
617
|
+
body, lon * _RAD, ayanamsa.jd_t0, ayanamsa.value_at_t0 * _RAD, jd_tt, direction,
|
|
618
|
+
max_days, byref(out))
|
|
619
|
+
else:
|
|
620
|
+
rc = _lib.eph_heliocentric_crossing_sidereal(body, lon * _RAD, int(ayanamsa), jd_tt,
|
|
621
|
+
direction, max_days, byref(out))
|
|
622
|
+
return out.value if rc == 0 else None
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def node_crossing(body: int, jd_tt: float, direction: int = 1,
|
|
626
|
+
max_days: float = 40) -> Optional[NodeCrossing]:
|
|
627
|
+
"""When the body crosses the ecliptic; TT in and out."""
|
|
628
|
+
jd, lon = c_double(), c_double()
|
|
629
|
+
rc = _lib.eph_node_crossing(body, jd_tt, direction, max_days, byref(jd), byref(lon))
|
|
630
|
+
if rc < 1:
|
|
631
|
+
return None
|
|
632
|
+
return NodeCrossing(jd.value, "ascending" if rc == 1 else "descending", lon.value * DEG)
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def station(body: int, jd_tt: float, direction: int = 1,
|
|
636
|
+
max_days: float = 800) -> Optional[Station]:
|
|
637
|
+
"""When the body turns retrograde or direct; TT in and out."""
|
|
638
|
+
jd = c_double()
|
|
639
|
+
rc = _lib.eph_station(body, jd_tt, direction, max_days, byref(jd))
|
|
640
|
+
if rc < 1:
|
|
641
|
+
return None
|
|
642
|
+
return Station(jd.value, "retrograde" if rc == 1 else "direct")
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
# ---- planetary nodes and apsides --------------------------------------------------
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def orbit(body: int, jd_tt: float, method: int = OrbitMethod.MEAN) -> Optional[Orbit]:
|
|
649
|
+
"""The orbit's elements (degrees, AU) and its nodes, perihelion and
|
|
650
|
+
aphelion, heliocentric and geocentric; None if unsupported or without data."""
|
|
651
|
+
out = _n.Orbit()
|
|
652
|
+
if _lib.eph_orbit_tt(body, jd_tt, method, byref(out)) != 0:
|
|
653
|
+
return None
|
|
654
|
+
|
|
655
|
+
def points(arr) -> OrbitPoints:
|
|
656
|
+
return OrbitPoints(*(_position(arr[i]) for i in range(4)))
|
|
657
|
+
|
|
658
|
+
return Orbit(out.semi_major_axis, out.eccentricity, out.inclination * DEG, out.node * DEG,
|
|
659
|
+
out.perihelion * DEG, points(out.heliocentric), points(out.geocentric))
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
# ---- eclipses ------------------------------------------------------------------------
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def solar_eclipse(jd_tt: float, direction: int = 1) -> Optional[SolarEclipse]:
|
|
666
|
+
"""The next (or previous, direction -1) solar eclipse anywhere; TT."""
|
|
667
|
+
o = _n.SolarEclipse()
|
|
668
|
+
if _lib.eph_solar_eclipse_next(jd_tt, direction, byref(o)) != 0:
|
|
669
|
+
return None
|
|
670
|
+
return SolarEclipse(EclipseType(o.type), o.maximum, o.gamma, o.magnitude, o.latitude * DEG,
|
|
671
|
+
o.longitude * DEG, o.saros, o.partial_begin, o.partial_end,
|
|
672
|
+
o.central_begin, o.central_end, o.center_line_begin, o.center_line_end)
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def solar_eclipse_local(jd_tt: float, lon: float, lat: float, elevation: float = 0.0,
|
|
676
|
+
direction: int = 1) -> Optional[LocalSolarEclipse]:
|
|
677
|
+
"""The next solar eclipse seen from a place (check ``altitude``: the Sun
|
|
678
|
+
may be below the horizon)."""
|
|
679
|
+
o = _n.LocalSolarEclipse()
|
|
680
|
+
obs = _observer(lon, lat, elevation)
|
|
681
|
+
if _lib.eph_solar_eclipse_local(jd_tt, byref(obs), direction, byref(o)) != 0:
|
|
682
|
+
return None
|
|
683
|
+
return LocalSolarEclipse(EclipseType(o.type), o.maximum, o.first_contact, o.second_contact,
|
|
684
|
+
o.third_contact, o.fourth_contact, o.magnitude, o.obscuration,
|
|
685
|
+
o.altitude * DEG, o.saros)
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def solar_eclipse_where(jd_tt: float) -> Optional[GeoPoint]:
|
|
689
|
+
"""Where the shadow axis meets the Earth; None if it misses."""
|
|
690
|
+
lat, lon = c_double(), c_double()
|
|
691
|
+
if _lib.eph_solar_eclipse_where(jd_tt, byref(lat), byref(lon)) != 0:
|
|
692
|
+
return None
|
|
693
|
+
return GeoPoint(lat.value * DEG, lon.value * DEG)
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def solar_eclipse_how(jd_tt: float, lon: float, lat: float,
|
|
697
|
+
elevation: float = 0.0) -> Optional[LocalEclipseState]:
|
|
698
|
+
"""The solar eclipse seen from a place at an instant (type 0: none)."""
|
|
699
|
+
o = _n.LocalEclipseState()
|
|
700
|
+
obs = _observer(lon, lat, elevation)
|
|
701
|
+
if _lib.eph_solar_eclipse_how(jd_tt, byref(obs), byref(o)) != 0:
|
|
702
|
+
return None
|
|
703
|
+
return LocalEclipseState(EclipseType(o.type), o.magnitude, o.obscuration, o.altitude * DEG)
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def lunar_eclipse(jd_tt: float, direction: int = 1) -> Optional[LunarEclipse]:
|
|
707
|
+
"""The next (or previous) lunar eclipse, penumbral ones included; TT."""
|
|
708
|
+
o = _n.LunarEclipse()
|
|
709
|
+
if _lib.eph_lunar_eclipse_next(jd_tt, direction, byref(o)) != 0:
|
|
710
|
+
return None
|
|
711
|
+
return LunarEclipse(EclipseType(o.type), o.maximum, o.gamma, o.umbral_magnitude,
|
|
712
|
+
o.penumbral_magnitude, o.saros, o.penumbral_begin, o.penumbral_end,
|
|
713
|
+
o.partial_begin, o.partial_end, o.total_begin, o.total_end)
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
# ---- minor planets ------------------------------------------------------------------
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def minor_body(name: Union[str, int]) -> Optional[int]:
|
|
720
|
+
"""The body id of a loaded minor planet: 433, "433", "Eros" or "433 Eros";
|
|
721
|
+
None if none is loaded. Works with position(), searches, phenomena(),
|
|
722
|
+
orbit() and heliocentric()."""
|
|
723
|
+
i = _lib.eph_minor_find(str(name).encode("utf-8"))
|
|
724
|
+
return None if i < 0 else MINOR_BASE + i
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def minor_count() -> int:
|
|
728
|
+
return _lib.eph_minor_count()
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def minor_name(body: int) -> str:
|
|
732
|
+
return _str(_lib.eph_minor_name(body - MINOR_BASE))
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
def minor_list() -> List[MinorPlanet]:
|
|
736
|
+
return [MinorPlanet(MINOR_BASE + i, _lib.eph_minor_number(i), _str(_lib.eph_minor_name(i)))
|
|
737
|
+
for i in range(_lib.eph_minor_count())]
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
# ---- fixed stars ------------------------------------------------------------------
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def star_find(name: str) -> Optional[int]:
|
|
744
|
+
"""Star index by name ("Regulus"), designation ("alf Leo") or "HIP 49669";
|
|
745
|
+
None if unknown (or the stars are not loaded)."""
|
|
746
|
+
i = _lib.eph_star_find(name.encode("utf-8"))
|
|
747
|
+
return None if i < 0 else i
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def star_count() -> int:
|
|
751
|
+
"""Stars loaded (0 without eph-stars.bin); indices 0 .. count - 1."""
|
|
752
|
+
return _lib.eph_star_count()
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def star_name(index: int) -> str:
|
|
756
|
+
return _str(_lib.eph_star_name(index))
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def star_designation(index: int) -> str:
|
|
760
|
+
return _str(_lib.eph_star_designation(index))
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def star_magnitude(index: int) -> float:
|
|
764
|
+
return _lib.eph_star_magnitude(index)
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def star_position(index: int, jd_ut: float, year: Optional[float] = None) -> Optional[Position]:
|
|
768
|
+
out = _n.Position()
|
|
769
|
+
rc = _lib.eph_star_position_apparent_ut(index, jd_ut, _year(jd_ut) if year is None else year,
|
|
770
|
+
byref(out))
|
|
771
|
+
return _position(out) if rc == 0 else None
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
# ---- ayanamsas --------------------------------------------------------------------
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def ayanamsa(system: int, jd_tt: float) -> float:
|
|
778
|
+
"""Ayanamsa in degrees: sidereal = tropical - ayanamsa."""
|
|
779
|
+
return _lib.eph_ayanamsa(system, jd_tt) * DEG
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def ayanamsa_custom(jd_t0: float, value_at_t0: float, jd_tt: float) -> float:
|
|
783
|
+
"""A user-defined ayanamsa (degrees at jd_t0) carried by the precession."""
|
|
784
|
+
return _lib.eph_ayanamsa_custom(jd_t0, value_at_t0 * _RAD, jd_tt) * DEG
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def ayanamsa_lahiri(jd_tt: float) -> float:
|
|
788
|
+
return _lib.eph_ayanamsa_lahiri(jd_tt) * DEG
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
def ayanamsa_name(system: int) -> str:
|
|
792
|
+
"""'Lahiri', 'Fagan/Bradley', ..."""
|
|
793
|
+
return _str(_lib.eph_ayanamsa_name(system))
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
# ---- utilities ---------------------------------------------------------------------
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def midpoint(a: float, b: float) -> float:
|
|
800
|
+
"""Midpoint of the shorter arc between two longitudes, degrees."""
|
|
801
|
+
return _lib.eph_midpoint(a * _RAD, b * _RAD) * DEG
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def split_longitude(lon: float) -> SplitLongitude:
|
|
805
|
+
"""Zodiac sign (0 = Aries .. 11) and the position within it."""
|
|
806
|
+
sign, deg, minute, sec = c_int(), c_int(), c_int(), c_double()
|
|
807
|
+
_lib.eph_split_longitude(lon * _RAD, byref(sign), byref(deg), byref(minute), byref(sec))
|
|
808
|
+
return SplitLongitude(sign.value, deg.value, minute.value, sec.value)
|
|
809
|
+
|
ephemera/_native.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""The native library and its C types (ctypes). Internal: use the functions
|
|
2
|
+
in the ``ephemera`` package, which take and return degrees."""
|
|
3
|
+
|
|
4
|
+
import ctypes
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from ctypes import POINTER, Structure, c_char_p, c_double, c_int, c_size_t, c_void_p
|
|
8
|
+
|
|
9
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _find_library() -> str:
|
|
13
|
+
if sys.platform == "win32":
|
|
14
|
+
names = ["ephemera_native.dll", "libephemera_native.dll"]
|
|
15
|
+
elif sys.platform == "darwin":
|
|
16
|
+
names = ["libephemera_native.dylib", "libephemera_native.so"]
|
|
17
|
+
else:
|
|
18
|
+
names = ["libephemera_native.so"]
|
|
19
|
+
for name in names:
|
|
20
|
+
path = os.path.join(_HERE, name)
|
|
21
|
+
if os.path.exists(path):
|
|
22
|
+
return path
|
|
23
|
+
raise ImportError(f"ephemera: native library not found in {_HERE} (looked for {', '.join(names)})")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
lib = ctypes.CDLL(_find_library())
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Position(Structure):
|
|
30
|
+
_fields_ = [(n, c_double) for n in ("longitude", "latitude", "distance",
|
|
31
|
+
"longitude_speed", "latitude_speed", "distance_speed")]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Observer(Structure):
|
|
35
|
+
_fields_ = [("longitude", c_double), ("latitude", c_double), ("elevation", c_double)]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Angles(Structure):
|
|
39
|
+
_fields_ = [(n, c_double) for n in ("ascendant", "mc", "armc", "vertex", "equatorial_ascendant",
|
|
40
|
+
"co_ascendant_koch", "co_ascendant_munkasey",
|
|
41
|
+
"polar_ascendant")]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Phenomena(Structure):
|
|
45
|
+
_fields_ = [(n, c_double) for n in ("phase_angle", "phase", "elongation", "diameter", "magnitude")]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SolarEclipse(Structure):
|
|
49
|
+
_fields_ = [("type", c_int), ("maximum", c_double), ("gamma", c_double), ("magnitude", c_double),
|
|
50
|
+
("latitude", c_double), ("longitude", c_double), ("saros", c_int),
|
|
51
|
+
("partial_begin", c_double), ("partial_end", c_double),
|
|
52
|
+
("central_begin", c_double), ("central_end", c_double),
|
|
53
|
+
("center_line_begin", c_double), ("center_line_end", c_double)]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class LocalEclipseState(Structure):
|
|
57
|
+
_fields_ = [("type", c_int), ("magnitude", c_double), ("obscuration", c_double),
|
|
58
|
+
("altitude", c_double)]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class LocalSolarEclipse(Structure):
|
|
62
|
+
_fields_ = [("type", c_int), ("maximum", c_double),
|
|
63
|
+
("first_contact", c_double), ("second_contact", c_double),
|
|
64
|
+
("third_contact", c_double), ("fourth_contact", c_double),
|
|
65
|
+
("magnitude", c_double), ("obscuration", c_double), ("altitude", c_double),
|
|
66
|
+
("saros", c_int)]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class LunarEclipse(Structure):
|
|
70
|
+
_fields_ = [("type", c_int), ("maximum", c_double), ("gamma", c_double),
|
|
71
|
+
("umbral_magnitude", c_double), ("penumbral_magnitude", c_double), ("saros", c_int),
|
|
72
|
+
("penumbral_begin", c_double), ("penumbral_end", c_double),
|
|
73
|
+
("partial_begin", c_double), ("partial_end", c_double),
|
|
74
|
+
("total_begin", c_double), ("total_end", c_double)]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Orbit(Structure):
|
|
78
|
+
_fields_ = [("semi_major_axis", c_double), ("eccentricity", c_double), ("inclination", c_double),
|
|
79
|
+
("node", c_double), ("perihelion", c_double),
|
|
80
|
+
("heliocentric", Position * 4), ("geocentric", Position * 4)]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
D = c_double
|
|
84
|
+
I = c_int
|
|
85
|
+
PD = POINTER(c_double)
|
|
86
|
+
PI = POINTER(c_int)
|
|
87
|
+
PPOS = POINTER(Position)
|
|
88
|
+
POBS = POINTER(Observer)
|
|
89
|
+
|
|
90
|
+
_SIGNATURES = {
|
|
91
|
+
# time
|
|
92
|
+
"eph_julian_date": (D, [I, I, D]),
|
|
93
|
+
"eph_calendar_date": (None, [D, PI, PI, PD]),
|
|
94
|
+
"eph_delta_t": (D, [D]),
|
|
95
|
+
"eph_tdb_minus_tt": (D, [D]),
|
|
96
|
+
"eph_ut_to_tt": (D, [D, D]),
|
|
97
|
+
"eph_tt_to_ut": (D, [D, D]),
|
|
98
|
+
"eph_utc_to_jd": (I, [I, I, I, I, I, D, PD, PD]),
|
|
99
|
+
"eph_sidereal_time": (D, [D]),
|
|
100
|
+
"eph_equation_of_time": (I, [D, PD]),
|
|
101
|
+
# frames
|
|
102
|
+
"eph_mean_obliquity": (D, [D]),
|
|
103
|
+
"eph_nutation": (None, [D, PD, PD]),
|
|
104
|
+
"eph_ecliptic_to_equatorial": (None, [D, D, D, PD, PD]),
|
|
105
|
+
# positions
|
|
106
|
+
"eph_position_tt": (I, [I, D, PPOS]),
|
|
107
|
+
"eph_position_apparent_tt": (I, [I, D, PPOS]),
|
|
108
|
+
"eph_position_ut": (I, [I, D, D, PPOS]),
|
|
109
|
+
"eph_position_apparent_ut": (I, [I, D, D, PPOS]),
|
|
110
|
+
"eph_heliocentric_tt": (I, [I, D, PD, PD, PD]),
|
|
111
|
+
"eph_position_topocentric_ut": (I, [I, D, D, POBS, PPOS]),
|
|
112
|
+
"eph_body_name": (c_char_p, [I]),
|
|
113
|
+
# sidereal zodiac and houses
|
|
114
|
+
"eph_ayanamsa": (D, [I, D]),
|
|
115
|
+
"eph_ayanamsa_name": (c_char_p, [I]),
|
|
116
|
+
"eph_ayanamsa_custom": (D, [D, D, D]),
|
|
117
|
+
"eph_ayanamsa_lahiri": (D, [D]),
|
|
118
|
+
"eph_asc_mc": (I, [D, POBS, PD, PD]),
|
|
119
|
+
"eph_houses": (I, [D, POBS, I, PD]),
|
|
120
|
+
"eph_house_angles": (I, [D, POBS, POINTER(Angles)]),
|
|
121
|
+
"eph_house_position": (D, [PD, D]),
|
|
122
|
+
# phenomena, eclipses
|
|
123
|
+
"eph_phenomena_tt": (I, [I, D, POINTER(Phenomena)]),
|
|
124
|
+
"eph_solar_eclipse_next": (I, [D, I, POINTER(SolarEclipse)]),
|
|
125
|
+
"eph_solar_eclipse_where": (I, [D, PD, PD]),
|
|
126
|
+
"eph_solar_eclipse_how": (I, [D, POBS, POINTER(LocalEclipseState)]),
|
|
127
|
+
"eph_solar_eclipse_local": (I, [D, POBS, I, POINTER(LocalSolarEclipse)]),
|
|
128
|
+
"eph_lunar_eclipse_next": (I, [D, I, POINTER(LunarEclipse)]),
|
|
129
|
+
# searches
|
|
130
|
+
"eph_longitude_crossing": (I, [I, D, D, I, D, PD]),
|
|
131
|
+
"eph_longitude_crossing_ut": (I, [I, D, D, I, D, PD]),
|
|
132
|
+
"eph_heliocentric_crossing": (I, [I, D, D, I, D, PD]),
|
|
133
|
+
"eph_longitude_crossing_sidereal": (I, [I, D, I, D, I, D, PD]),
|
|
134
|
+
"eph_longitude_crossing_sidereal_custom": (I, [I, D, D, D, D, I, D, PD]),
|
|
135
|
+
"eph_heliocentric_crossing_sidereal": (I, [I, D, I, D, I, D, PD]),
|
|
136
|
+
"eph_heliocentric_crossing_sidereal_custom": (I, [I, D, D, D, D, I, D, PD]),
|
|
137
|
+
"eph_node_crossing": (I, [I, D, I, D, PD, PD]),
|
|
138
|
+
"eph_station": (I, [I, D, I, D, PD]),
|
|
139
|
+
# minor planets, orbits
|
|
140
|
+
"eph_minor_count": (I, []),
|
|
141
|
+
"eph_minor_find": (I, [c_char_p]),
|
|
142
|
+
"eph_minor_number": (I, [I]),
|
|
143
|
+
"eph_minor_name": (c_char_p, [I]),
|
|
144
|
+
"eph_orbit_tt": (I, [I, D, I, POINTER(Orbit)]),
|
|
145
|
+
# utilities
|
|
146
|
+
"eph_midpoint": (D, [D, D]),
|
|
147
|
+
"eph_split_longitude": (None, [D, PI, PI, PI, PD]),
|
|
148
|
+
# fixed stars
|
|
149
|
+
"eph_star_count": (I, []),
|
|
150
|
+
"eph_star_find": (I, [c_char_p]),
|
|
151
|
+
"eph_star_name": (c_char_p, [I]),
|
|
152
|
+
"eph_star_designation": (c_char_p, [I]),
|
|
153
|
+
"eph_star_magnitude": (D, [I]),
|
|
154
|
+
"eph_star_position_apparent_ut": (I, [I, D, D, PPOS]),
|
|
155
|
+
# data files
|
|
156
|
+
"eph_load_data": (I, [c_void_p, c_size_t]),
|
|
157
|
+
"eph_load_file": (I, [c_char_p]),
|
|
158
|
+
"eph_load_embedded": (I, []),
|
|
159
|
+
"eph_unload_data": (None, []),
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for _name, (_restype, _argtypes) in _SIGNATURES.items():
|
|
163
|
+
_fn = getattr(lib, _name)
|
|
164
|
+
_fn.restype = _restype
|
|
165
|
+
_fn.argtypes = _argtypes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
ephemera/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyephemera
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: Pocket-sized ephemeris for astrology and astronomy: planets, Moon, Chiron, nodes, houses, ayanamsas, stars, eclipses, Uranian points and 1,000+ asteroids. No dependencies, MIT.
|
|
5
|
+
Keywords: ephemeris,astrology,astronomy,planets,houses,ayanamsa,sidereal,eclipse
|
|
6
|
+
Author: Intellecat
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: C++
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Project-URL: Homepage, https://github.com/intellecat/ephemera
|
|
18
|
+
Project-URL: Documentation, https://github.com/intellecat/ephemera/blob/main/docs/guide.md
|
|
19
|
+
Project-URL: Issues, https://github.com/intellecat/ephemera/issues
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
<p align="center"><img alt="Ephemera" src="https://raw.githubusercontent.com/intellecat/ephemera/main/assets/logo/lockup-horizontal-light.svg" width="420"></p>
|
|
24
|
+
|
|
25
|
+
# pyephemera
|
|
26
|
+
|
|
27
|
+
**A pocket-sized, MIT-licensed ephemeris for astrology and astronomy.**
|
|
28
|
+
JPL-grade planetary positions, houses, ayanamsas, eclipses, fixed stars and
|
|
29
|
+
1,000+ asteroids, from a small C library with no dependencies. Wheels for
|
|
30
|
+
Linux, macOS and Windows; Python 3.9+.
|
|
31
|
+
|
|
32
|
+
- **MIT licensed:** use it in closed-source and commercial apps.
|
|
33
|
+
- **Accurate:** within 0.6″ of NASA JPL's DE440 over 1550–2650 with the
|
|
34
|
+
built-in data, or 0.03″ with the high-precision tables.
|
|
35
|
+
- **Small:** the planets, the Moon and Chiron are built in (204 KB); more
|
|
36
|
+
data files come with the package.
|
|
37
|
+
- **Complete for astrology:** 14 house systems, 42 ayanamsas, nodes and
|
|
38
|
+
Lilith, Uranian points, eclipses, returns and stations, sidereal
|
|
39
|
+
returns, planetary nodes and apsides, and 411 fixed stars.
|
|
40
|
+
- **Independently verified** against JPL, NASA's eclipse canon and
|
|
41
|
+
India's official ephemeris. See the
|
|
42
|
+
[accuracy page](https://github.com/intellecat/ephemera/blob/main/docs/accuracy.md).
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
pip install pyephemera
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import ephemera as eph
|
|
50
|
+
|
|
51
|
+
jd = eph.julian_date(2026, 1, 1.5) # 1 Jan 2026, 12:00 UT
|
|
52
|
+
moon = eph.position(eph.Body.MOON, jd) # degrees, AU, per day
|
|
53
|
+
cusps = eph.houses(jd, -0.1278, 51.5074, eph.HouseSystem.PLACIDUS) # London
|
|
54
|
+
lahiri = eph.ayanamsa(eph.Ayanamsa.LAHIRI, eph.ut_to_tt(jd)) # sidereal = tropical − ayanamsa
|
|
55
|
+
eclipse = eph.solar_eclipse(eph.ut_to_tt(jd)) # the next solar eclipse
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Times
|
|
59
|
+
|
|
60
|
+
Positions take a **UT** Julian day; searches, eclipses and orbits take
|
|
61
|
+
**TT**. `utc_to_jd` converts civil time (with leap seconds) and accepts a
|
|
62
|
+
`datetime`:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from datetime import datetime
|
|
66
|
+
from zoneinfo import ZoneInfo
|
|
67
|
+
|
|
68
|
+
jd_tt, jd_ut = eph.utc_to_jd(datetime(1990, 6, 15, 14, 30, tzinfo=ZoneInfo("Europe/London")))
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Data files
|
|
72
|
+
|
|
73
|
+
The 1″ series with Chiron is built in. The package also carries:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
eph.load_package_data("stars") # 411 named stars, star-based ayanamsas
|
|
77
|
+
eph.load_package_data("asteroids") # Ceres, Pallas, Juno, Vesta (1600–2500)
|
|
78
|
+
eph.load_package_data("01as") # the 0.2″ series
|
|
79
|
+
eph.load_data("de440-2000.bin") # any file from the repository, by path or bytes
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The DE440 tables (0.002″) and the minor-planet files (1,000+ asteroids,
|
|
83
|
+
1900–2100) are downloads from the
|
|
84
|
+
[repository](https://github.com/intellecat/ephemera/blob/main/docs/data.md).
|
|
85
|
+
Loaded data is shared by the whole process.
|
|
86
|
+
|
|
87
|
+
## Documentation
|
|
88
|
+
|
|
89
|
+
The [guide](https://github.com/intellecat/ephemera/blob/main/docs/guide.md)
|
|
90
|
+
covers every feature. The Python names follow the JavaScript API in
|
|
91
|
+
snake_case: `houses_info`, `longitude_crossing`, `solar_eclipse_local`,
|
|
92
|
+
`minor_body`; calls that find nothing return `None`.
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT. Built from public data: NASA JPL, NASA's eclipse catalogues and
|
|
97
|
+
Hipparcos. No third-party code is included.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ephemera/__init__.py,sha256=cjbHg4N2T2VG4P_BM3tod08Erx-eIgCkff7BEUaj9sk,26155
|
|
2
|
+
ephemera/_native.py,sha256=6XBqze1dtw2rCc6pGiTwF6DdiyVkOdgI_17xipwPVvA,6545
|
|
3
|
+
ephemera/data/eph-01as-chiron.bin,sha256=wSNH46BEh-_yUytKW3sY1MdbE5nKkTURLzsuEaF4V4w,362108
|
|
4
|
+
ephemera/data/eph-01as.bin,sha256=XAK95lbyfxlkEHhD9yYHwv1HEnHLPj069QenOOjl9yI,302536
|
|
5
|
+
ephemera/data/eph-1as-chiron.bin,sha256=yj0-d5zwajplkr9PR-ht_gujzL9Mn6H_sjF6jpxE12s,204292
|
|
6
|
+
ephemera/data/eph-1as.bin,sha256=eVmz3dZ9tBF8aEVyxfmY4oLx6yMY91FZZO919HGbM-k,144720
|
|
7
|
+
ephemera/data/eph-asteroids.bin,sha256=mrlHhLU294RC1de0bdnOS1a0JYwaSqiuKQAifQchZLA,424916
|
|
8
|
+
ephemera/data/eph-chiron.bin,sha256=lW-KRjR93Q94gbTRxqduAvwAHAB5SJE1Msd7qrUu2ng,59644
|
|
9
|
+
ephemera/data/eph-stars.bin,sha256=9Tjq2gjkQn9Hsle5k_jt7TgQgTohAiQdhhTSQ-pLVlE,25853
|
|
10
|
+
ephemera/ephemera_native.dll,sha256=Vi6DoYij5smQ2gcwS1kDds6Wo46J7N9sRFoir3CHcHI,336384
|
|
11
|
+
ephemera/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
pyephemera-0.4.1.dist-info/METADATA,sha256=P4xdfYMHjVCrBHsnILROtgrzH4-Kz1MGPd-eEv5_2MA,4034
|
|
13
|
+
pyephemera-0.4.1.dist-info/WHEEL,sha256=H2-vNj0Uj93L_UlwWLAHSbPwt0nxqZmlgAGZZjm-2qY,102
|
|
14
|
+
pyephemera-0.4.1.dist-info/licenses/LICENSE,sha256=kp2mYkUsVBt5ZBrgioeyEdl_vuEdEZ-dvMZEoWeFBAY,1067
|
|
15
|
+
pyephemera-0.4.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Intellecat
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|