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/lunar.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lunar node and apogee (Lilith) calculations for libephemeris.
|
|
3
|
+
|
|
4
|
+
This module computes:
|
|
5
|
+
- Mean Lunar Node: Average ascending node of Moon's orbit on ecliptic
|
|
6
|
+
- True Lunar Node: Instantaneous osculating ascending node
|
|
7
|
+
- Mean Lilith: Average lunar apogee (Black Moon Lilith)
|
|
8
|
+
- True Lilith: Instantaneous osculating lunar apogee
|
|
9
|
+
|
|
10
|
+
Formulas are based on:
|
|
11
|
+
- Jean Meeus "Astronomical Algorithms" (2nd ed., 1998), Chapter 47
|
|
12
|
+
- Skyfield orbital mechanics for osculating elements
|
|
13
|
+
|
|
14
|
+
References:
|
|
15
|
+
- Mean elements: Polynomial approximations (Meeus)
|
|
16
|
+
- True elements: Computed from instantaneous position/velocity vectors
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import math
|
|
20
|
+
from typing import Tuple
|
|
21
|
+
from .constants import SE_MEAN_NODE, SE_TRUE_NODE, SE_MEAN_APOG, SE_OSCU_APOG
|
|
22
|
+
from .state import get_timescale, get_planets
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def calc_mean_lunar_node(jd_tt: float) -> float:
|
|
26
|
+
"""
|
|
27
|
+
Calculate Mean Lunar Node (ascending node of lunar orbit on ecliptic).
|
|
28
|
+
|
|
29
|
+
Uses polynomial approximation from Meeus "Astronomical Algorithms" Ch. 47.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
jd_tt: Julian Day in Terrestrial Time (TT)
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
float: Ecliptic longitude of mean ascending node in degrees (0-360)
|
|
36
|
+
|
|
37
|
+
Precision:
|
|
38
|
+
Agreement with Swiss Ephemeris: < 0.01° (typically < 0.001°)
|
|
39
|
+
|
|
40
|
+
Note:
|
|
41
|
+
The mean node is a smoothed average that ignores short-period perturbations.
|
|
42
|
+
For instant precision, use calc_true_lunar_node() instead.
|
|
43
|
+
|
|
44
|
+
Formula: Ω = 125.0445479° - 1934.1362891°T + 0.0020754°T² + T³/467441 - T⁴/60616000
|
|
45
|
+
where T = Julian centuries since J2000.0
|
|
46
|
+
"""
|
|
47
|
+
T = (jd_tt - 2451545.0) / 36525.0 # Julian centuries from J2000.0
|
|
48
|
+
|
|
49
|
+
# FIXME: Precision - Meeus polynomial valid for ±several centuries from J2000
|
|
50
|
+
# Beyond this range, accuracy degrades. For distant dates, use numerical integration.
|
|
51
|
+
Omega = (
|
|
52
|
+
125.0445479
|
|
53
|
+
- 1934.1362891 * T
|
|
54
|
+
+ 0.0020754 * T**2
|
|
55
|
+
+ T**3 / 467441.0
|
|
56
|
+
- T**4 / 60616000.0
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return Omega % 360.0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def calc_true_lunar_node(jd_tt: float) -> Tuple[float, float, float]:
|
|
63
|
+
"""
|
|
64
|
+
Calculate True (osculating) Lunar Node from instantaneous Moon orbit.
|
|
65
|
+
|
|
66
|
+
Algorithm:
|
|
67
|
+
1. Get Moon geocentric position vector r and velocity vector v
|
|
68
|
+
2. Compute angular momentum h = r × v (perpendicular to orbital plane)
|
|
69
|
+
3. Node vector n = k × h (intersection of orbit with ecliptic)
|
|
70
|
+
4. Longitude = atan2(n_y, n_x)
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
jd_tt: Julian Day in Terrestrial Time (TT)
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Tuple[float, float, float]: (longitude, latitude, distance) where:
|
|
77
|
+
- longitude: Ecliptic longitude in degrees (0-360)
|
|
78
|
+
- latitude: Always 0.0 (node is on ecliptic by definition)
|
|
79
|
+
- distance: Placeholder 0.0 (nodes have no inherent distance)
|
|
80
|
+
|
|
81
|
+
Precision:
|
|
82
|
+
Agreement with Swiss Ephemeris: < 0.001° for modern dates
|
|
83
|
+
|
|
84
|
+
Note:
|
|
85
|
+
The true node varies rapidly (±10° from mean) due to solar/planetary perturbations.
|
|
86
|
+
Precession period: ~18.6 years (retrograde)
|
|
87
|
+
|
|
88
|
+
References:
|
|
89
|
+
Orbital mechanics: Vallado "Fundamentals of Astrodynamics" (2013)
|
|
90
|
+
"""
|
|
91
|
+
planets = get_planets()
|
|
92
|
+
ts = get_timescale()
|
|
93
|
+
t = ts.tt_jd(jd_tt)
|
|
94
|
+
|
|
95
|
+
earth = planets["earth"]
|
|
96
|
+
moon = planets["moon"]
|
|
97
|
+
|
|
98
|
+
# Get geocentric Moon state vectors (position, velocity)
|
|
99
|
+
moon_pos = moon.at(t).position.au
|
|
100
|
+
earth_pos = earth.at(t).position.au
|
|
101
|
+
moon_geo_pos = moon_pos - earth_pos
|
|
102
|
+
|
|
103
|
+
moon_vel = moon.at(t).velocity.au_per_d
|
|
104
|
+
earth_vel = earth.at(t).velocity.au_per_d
|
|
105
|
+
moon_geo_vel = moon_vel - earth_vel
|
|
106
|
+
|
|
107
|
+
# Angular momentum vector h = r × v (perpendicular to orbital plane)
|
|
108
|
+
h = [
|
|
109
|
+
moon_geo_pos[1] * moon_geo_vel[2] - moon_geo_pos[2] * moon_geo_vel[1],
|
|
110
|
+
moon_geo_pos[2] * moon_geo_vel[0] - moon_geo_pos[0] * moon_geo_vel[2],
|
|
111
|
+
moon_geo_pos[0] * moon_geo_vel[1] - moon_geo_pos[1] * moon_geo_vel[0],
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
# FIXME: Precision - Using fixed J2000 obliquity (23.4392911°)
|
|
115
|
+
# For highest precision over millennia, use time-variable obliquity.
|
|
116
|
+
eps = math.radians(23.4392911) # J2000.0 mean obliquity
|
|
117
|
+
|
|
118
|
+
# Rotate angular momentum vector from ICRS (equatorial) to ecliptic frame
|
|
119
|
+
h_ecl = [
|
|
120
|
+
h[0],
|
|
121
|
+
h[1] * math.cos(eps) + h[2] * math.sin(eps),
|
|
122
|
+
-h[1] * math.sin(eps) + h[2] * math.cos(eps),
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
# Node vector n = k × h where k = (0, 0, 1) is ecliptic pole
|
|
126
|
+
# n = (-h_y, h_x, 0), so longitude = atan2(h_x, -h_y)
|
|
127
|
+
node_lon = math.degrees(math.atan2(h_ecl[0], -h_ecl[1])) % 360.0
|
|
128
|
+
|
|
129
|
+
return node_lon, 0.0, 0.0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def calc_mean_lilith(jd_tt: float) -> float:
|
|
133
|
+
"""
|
|
134
|
+
Calculate Mean Lilith (Mean Lunar Apogee, also called Black Moon Lilith).
|
|
135
|
+
|
|
136
|
+
Uses polynomial approximation for mean lunar perigee, then adds 180°.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
jd_tt: Julian Day in Terrestrial Time (TT)
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
float: Ecliptic longitude of mean lunar apogee in degrees (0-360)
|
|
143
|
+
|
|
144
|
+
Precision:
|
|
145
|
+
Agreement with Swiss Ephemeris: < 0.01° (typically < 0.005°)
|
|
146
|
+
|
|
147
|
+
Note:
|
|
148
|
+
Mean Lilith is the time-averaged apogee, ignoring short-period variations.
|
|
149
|
+
The actual apogee oscillates ±5-10° from this mean position.
|
|
150
|
+
Apsidal precession period: ~8.85 years (prograde)
|
|
151
|
+
|
|
152
|
+
Formula: Apogee = Perigee + 180°
|
|
153
|
+
Perigee (Meeus): ω = 83.3532465° + 4069.0137287°T - 0.0103200°T² - T³/80053
|
|
154
|
+
"""
|
|
155
|
+
T = (jd_tt - 2451545.0) / 36525.0 # Julian centuries from J2000.0
|
|
156
|
+
|
|
157
|
+
# FIXME: Precision - Simplified formula omits planetary perturbations
|
|
158
|
+
# Full precision requires numerical integration of lunar orbit
|
|
159
|
+
# Current accuracy: ~0.005° for dates within ±200 years of J2000
|
|
160
|
+
|
|
161
|
+
# Mean longitude of lunar perigee (argument of perigee)
|
|
162
|
+
perigee = 83.3532465 + 4069.0137287 * T - 0.0103200 * T**2 - T**3 / 80053.0
|
|
163
|
+
|
|
164
|
+
# Apogee is 180° opposite to perigee
|
|
165
|
+
apogee = perigee + 180.0
|
|
166
|
+
|
|
167
|
+
return apogee % 360.0
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def calc_true_lilith(jd_tt: float) -> Tuple[float, float, float]:
|
|
171
|
+
"""
|
|
172
|
+
Calculate True Lilith (osculating lunar apogee).
|
|
173
|
+
|
|
174
|
+
Computes the apogee of the instantaneous ellipse fitted to Moon's orbit,
|
|
175
|
+
using orbital elements derived from position/velocity state vectors.
|
|
176
|
+
|
|
177
|
+
Algorithm:
|
|
178
|
+
1. Get Moon geocentric state vectors (r, v)
|
|
179
|
+
2. Compute eccentricity vector e = (v × h)/μ - r/|r|
|
|
180
|
+
3. Eccentricity vector points to perigee
|
|
181
|
+
4. Apogee = -e (opposite direction)
|
|
182
|
+
5. Convert to ecliptic longitude/latitude
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
jd_tt: Julian Day in Terrestrial Time (TT)
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Tuple[float, float, float]: (longitude, latitude, distance_scaled) where:
|
|
189
|
+
- longitude: Ecliptic longitude in degrees (0-360)
|
|
190
|
+
- latitude: Ecliptic latitude in degrees (typically < 5°)
|
|
191
|
+
- distance_scaled: Relative scale (eccentricity magnitude)
|
|
192
|
+
|
|
193
|
+
Precision:
|
|
194
|
+
Agreement with Swiss Ephemeris: < 0.01° for modern dates
|
|
195
|
+
May differ by 0.1° for complex perturbations
|
|
196
|
+
|
|
197
|
+
Note:
|
|
198
|
+
True Lilith can vary ±10° from mean Lilith in days/weeks.
|
|
199
|
+
The osculating apogee is sensitive to momentary perturbations.
|
|
200
|
+
|
|
201
|
+
μ (GM_Earth) = 398600.4418 km³/s² converted to AU³/day²
|
|
202
|
+
|
|
203
|
+
References:
|
|
204
|
+
Eccentricity vector method: Vallado "Fundamentals of Astrodynamics"
|
|
205
|
+
"""
|
|
206
|
+
planets = get_planets()
|
|
207
|
+
ts = get_timescale()
|
|
208
|
+
t = ts.tt_jd(jd_tt)
|
|
209
|
+
|
|
210
|
+
earth = planets["earth"]
|
|
211
|
+
moon = planets["moon"]
|
|
212
|
+
|
|
213
|
+
# Get geocentric Moon state vectors
|
|
214
|
+
moon_pos = moon.at(t).position.au
|
|
215
|
+
earth_pos = earth.at(t).position.au
|
|
216
|
+
r = moon_pos - earth_pos
|
|
217
|
+
|
|
218
|
+
moon_vel = moon.at(t).velocity.au_per_d
|
|
219
|
+
earth_vel = earth.at(t).velocity.au_per_d
|
|
220
|
+
v = moon_vel - earth_vel
|
|
221
|
+
|
|
222
|
+
# Calculate magnitudes
|
|
223
|
+
r_mag = math.sqrt(sum(x**2 for x in r))
|
|
224
|
+
v_mag = math.sqrt(sum(x**2 for x in v))
|
|
225
|
+
|
|
226
|
+
# Specific angular momentum h = r × v
|
|
227
|
+
h_vec = [
|
|
228
|
+
r[1] * v[2] - r[2] * v[1],
|
|
229
|
+
r[2] * v[0] - r[0] * v[2],
|
|
230
|
+
r[0] * v[1] - r[1] * v[0],
|
|
231
|
+
]
|
|
232
|
+
h_mag = math.sqrt(sum(x**2 for x in h_vec))
|
|
233
|
+
|
|
234
|
+
# FIXME: Precision - Using standard gravitational parameter for Earth
|
|
235
|
+
# This assumes 2-body problem (Earth-Moon). For highest precision,
|
|
236
|
+
# account for Sun's perturbations via 3-body dynamics.
|
|
237
|
+
# GM_Earth in AU³/day² (converted from km³/s²)
|
|
238
|
+
mu = 398600.4418 / (149597870.7**3) * (86400**2)
|
|
239
|
+
|
|
240
|
+
# Eccentricity vector e = (v × h)/μ - r/|r| (points toward perigee)
|
|
241
|
+
e_vec = [
|
|
242
|
+
(v[1] * h_vec[2] - v[2] * h_vec[1]) / mu - r[0] / r_mag,
|
|
243
|
+
(v[2] * h_vec[0] - v[0] * h_vec[2]) / mu - r[1] / r_mag,
|
|
244
|
+
(v[0] * h_vec[1] - v[1] * h_vec[0]) / mu - r[2] / r_mag,
|
|
245
|
+
]
|
|
246
|
+
|
|
247
|
+
# Apogee is opposite to perigee (180° from eccentricity vector)
|
|
248
|
+
apogee_vec = [-e for e in e_vec]
|
|
249
|
+
|
|
250
|
+
# FIXME: Precision - Using fixed J2000 obliquity
|
|
251
|
+
# For dates far from J2000, use time-variable obliquity
|
|
252
|
+
eps = math.radians(23.4392911) # J2000.0 mean obliquity
|
|
253
|
+
|
|
254
|
+
# Rotate from ICRS (equatorial) to ecliptic coordinates
|
|
255
|
+
apogee_ecl = [
|
|
256
|
+
apogee_vec[0],
|
|
257
|
+
apogee_vec[1] * math.cos(eps) + apogee_vec[2] * math.sin(eps),
|
|
258
|
+
-apogee_vec[1] * math.sin(eps) + apogee_vec[2] * math.cos(eps),
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
# Convert to spherical coordinates
|
|
262
|
+
lon = math.degrees(math.atan2(apogee_ecl[1], apogee_ecl[0])) % 360.0
|
|
263
|
+
lat = math.degrees(
|
|
264
|
+
math.asin(apogee_ecl[2] / math.sqrt(sum(x**2 for x in apogee_ecl)))
|
|
265
|
+
)
|
|
266
|
+
dist = math.sqrt(sum(x**2 for x in apogee_ecl))
|
|
267
|
+
|
|
268
|
+
return lon, lat, dist
|
|
269
|
+
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Minor body calculations for asteroids and Trans-Neptunian Objects (TNOs).
|
|
3
|
+
|
|
4
|
+
This module computes positions for:
|
|
5
|
+
- Main belt asteroids: Ceres, Pallas, Juno, Vesta
|
|
6
|
+
- Centaurs: Chiron, Pholus
|
|
7
|
+
- Trans-Neptunian Objects (TNOs): Eris, Sedna, Haumea, Makemake, Orcus, Quaoar, Ixion
|
|
8
|
+
|
|
9
|
+
Method: Keplerian orbital elements with 2-body dynamics (Sun-body only).
|
|
10
|
+
|
|
11
|
+
IMPORTANT PRECISION LIMITATIONS:
|
|
12
|
+
- Uses simplified Keplerian orbits (no perturbations from planets)
|
|
13
|
+
- Accuracies: 1-5 arcminutes (1-5') typical for asteroids
|
|
14
|
+
- TNOs: Lower precision due to longer periods and perturbations
|
|
15
|
+
- For research-grade precision, use full numerical integration (Swiss Ephemeris, JPL Horizons)
|
|
16
|
+
|
|
17
|
+
This is intentionally simpler than Swiss Ephemeris's full dynamical model for:
|
|
18
|
+
- Faster computation
|
|
19
|
+
- No external data files required
|
|
20
|
+
- Acceptable precision for most astrological applications
|
|
21
|
+
|
|
22
|
+
Orbital elements source: JPL Small-Body Database (epoch 2023.0)
|
|
23
|
+
Algorithm: Standard Keplerian orbital mechanics (Curtis, Vallado)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import math
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from typing import Tuple
|
|
29
|
+
from .constants import (
|
|
30
|
+
SE_CHIRON,
|
|
31
|
+
SE_PHOLUS,
|
|
32
|
+
SE_CERES,
|
|
33
|
+
SE_PALLAS,
|
|
34
|
+
SE_JUNO,
|
|
35
|
+
SE_VESTA,
|
|
36
|
+
SE_ERIS,
|
|
37
|
+
SE_SEDNA,
|
|
38
|
+
SE_HAUMEA,
|
|
39
|
+
SE_MAKEMAKE,
|
|
40
|
+
SE_IXION,
|
|
41
|
+
SE_ORCUS,
|
|
42
|
+
SE_QUAOAR,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class OrbitalElements:
|
|
48
|
+
"""
|
|
49
|
+
Classical Keplerian orbital elements for a minor body.
|
|
50
|
+
|
|
51
|
+
Attributes:
|
|
52
|
+
name: Body name
|
|
53
|
+
epoch: Reference epoch (Julian Day in TT)
|
|
54
|
+
a: Semi-major axis in AU
|
|
55
|
+
e: Eccentricity (0-1, dimensionless)
|
|
56
|
+
i: Inclination to ecliptic in degrees
|
|
57
|
+
omega: Argument of perihelion (ω) in degrees
|
|
58
|
+
Omega: Longitude of ascending node (Ω) in degrees
|
|
59
|
+
M0: Mean anomaly at epoch in degrees
|
|
60
|
+
n: Mean motion in degrees/day
|
|
61
|
+
|
|
62
|
+
Note:
|
|
63
|
+
These are osculating elements at the given epoch.
|
|
64
|
+
They drift over time due to planetary perturbations.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
name: str
|
|
68
|
+
epoch: float
|
|
69
|
+
a: float
|
|
70
|
+
e: float
|
|
71
|
+
i: float
|
|
72
|
+
omega: float
|
|
73
|
+
Omega: float
|
|
74
|
+
M0: float
|
|
75
|
+
n: float
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# =============================================================================
|
|
79
|
+
# ORBITAL ELEMENTS DATABASE (Epoch 2023.0 = JD 2459945.5)
|
|
80
|
+
# =============================================================================
|
|
81
|
+
# FIXME: Precision - Elements are osculating at epoch 2023.0
|
|
82
|
+
# Accuracy degrades ~10-50 arcsec/year due to secular perturbations
|
|
83
|
+
# For dates >10 years from epoch, update elements or use Swiss Ephemeris
|
|
84
|
+
|
|
85
|
+
MINOR_BODY_ELEMENTS = {
|
|
86
|
+
SE_CHIRON: OrbitalElements(
|
|
87
|
+
name="Chiron",
|
|
88
|
+
epoch=2459945.5,
|
|
89
|
+
a=13.633, # AU - between Saturn and Uranus
|
|
90
|
+
e=0.380,
|
|
91
|
+
i=6.935,
|
|
92
|
+
omega=209.35,
|
|
93
|
+
Omega=339.53,
|
|
94
|
+
M0=354.07,
|
|
95
|
+
n=0.0136, # ~72 year period
|
|
96
|
+
),
|
|
97
|
+
SE_PHOLUS: OrbitalElements(
|
|
98
|
+
name="Pholus",
|
|
99
|
+
epoch=2459945.5,
|
|
100
|
+
a=20.246,
|
|
101
|
+
e=0.574, # Highly eccentric
|
|
102
|
+
i=24.686,
|
|
103
|
+
omega=354.80,
|
|
104
|
+
Omega=119.48,
|
|
105
|
+
M0=286.10,
|
|
106
|
+
n=0.00763, # ~90 year period
|
|
107
|
+
),
|
|
108
|
+
SE_CERES: OrbitalElements(
|
|
109
|
+
name="Ceres",
|
|
110
|
+
epoch=2459945.5,
|
|
111
|
+
a=2.767,
|
|
112
|
+
e=0.076,
|
|
113
|
+
i=10.587,
|
|
114
|
+
omega=73.68,
|
|
115
|
+
Omega=80.31,
|
|
116
|
+
M0=352.21,
|
|
117
|
+
n=0.214, # ~4.6 year period
|
|
118
|
+
),
|
|
119
|
+
SE_PALLAS: OrbitalElements(
|
|
120
|
+
name="Pallas",
|
|
121
|
+
epoch=2459945.5,
|
|
122
|
+
a=2.772,
|
|
123
|
+
e=0.231,
|
|
124
|
+
i=34.841, # High inclination
|
|
125
|
+
omega=310.25,
|
|
126
|
+
Omega=173.14,
|
|
127
|
+
M0=180.73,
|
|
128
|
+
n=0.213,
|
|
129
|
+
),
|
|
130
|
+
SE_JUNO: OrbitalElements(
|
|
131
|
+
name="Juno",
|
|
132
|
+
epoch=2459945.5,
|
|
133
|
+
a=2.669,
|
|
134
|
+
e=0.257,
|
|
135
|
+
i=12.982,
|
|
136
|
+
omega=248.21,
|
|
137
|
+
Omega=170.13,
|
|
138
|
+
M0=92.57,
|
|
139
|
+
n=0.225, # ~4.4 year period
|
|
140
|
+
),
|
|
141
|
+
SE_VESTA: OrbitalElements(
|
|
142
|
+
name="Vesta",
|
|
143
|
+
epoch=2459945.5,
|
|
144
|
+
a=2.362,
|
|
145
|
+
e=0.089,
|
|
146
|
+
i=7.141,
|
|
147
|
+
omega=151.43,
|
|
148
|
+
Omega=103.91,
|
|
149
|
+
M0=205.66,
|
|
150
|
+
n=0.272, # ~3.6 year period
|
|
151
|
+
),
|
|
152
|
+
SE_ERIS: OrbitalElements(
|
|
153
|
+
name="Eris",
|
|
154
|
+
epoch=2459945.5,
|
|
155
|
+
a=67.781, # Highly distant
|
|
156
|
+
e=0.442,
|
|
157
|
+
i=44.040, # Extreme inclination
|
|
158
|
+
omega=151.64,
|
|
159
|
+
Omega=35.93,
|
|
160
|
+
M0=204.48,
|
|
161
|
+
n=0.00174, # ~558 year period
|
|
162
|
+
),
|
|
163
|
+
SE_SEDNA: OrbitalElements(
|
|
164
|
+
name="Sedna",
|
|
165
|
+
epoch=2459945.5,
|
|
166
|
+
a=506.0, # Extreme distance (detached object)
|
|
167
|
+
e=0.851, # Very eccentric
|
|
168
|
+
i=11.928,
|
|
169
|
+
omega=311.29,
|
|
170
|
+
Omega=144.25,
|
|
171
|
+
M0=358.10,
|
|
172
|
+
n=0.000155, # ~11,400 year period
|
|
173
|
+
),
|
|
174
|
+
SE_HAUMEA: OrbitalElements(
|
|
175
|
+
name="Haumea",
|
|
176
|
+
epoch=2459945.5,
|
|
177
|
+
a=43.116,
|
|
178
|
+
e=0.191,
|
|
179
|
+
i=28.214,
|
|
180
|
+
omega=239.23,
|
|
181
|
+
Omega=121.90,
|
|
182
|
+
M0=217.89,
|
|
183
|
+
n=0.00312, # ~283 year period
|
|
184
|
+
),
|
|
185
|
+
SE_MAKEMAKE: OrbitalElements(
|
|
186
|
+
name="Makemake",
|
|
187
|
+
epoch=2459945.5,
|
|
188
|
+
a=45.430,
|
|
189
|
+
e=0.158,
|
|
190
|
+
i=28.983,
|
|
191
|
+
omega=294.84,
|
|
192
|
+
Omega=79.37,
|
|
193
|
+
M0=160.33,
|
|
194
|
+
n=0.00287, # ~306 year period
|
|
195
|
+
),
|
|
196
|
+
SE_IXION: OrbitalElements(
|
|
197
|
+
name="Ixion",
|
|
198
|
+
epoch=2459945.5,
|
|
199
|
+
a=39.480, # Plutino (2:3 resonance with Neptune)
|
|
200
|
+
e=0.242,
|
|
201
|
+
i=19.593,
|
|
202
|
+
omega=299.24,
|
|
203
|
+
Omega=71.01,
|
|
204
|
+
M0=267.49,
|
|
205
|
+
n=0.00371, # ~248 year period
|
|
206
|
+
),
|
|
207
|
+
SE_ORCUS: OrbitalElements(
|
|
208
|
+
name="Orcus",
|
|
209
|
+
epoch=2459945.5,
|
|
210
|
+
a=39.177, # Plutino (anti-Pluto phase)
|
|
211
|
+
e=0.227,
|
|
212
|
+
i=20.573,
|
|
213
|
+
omega=73.28,
|
|
214
|
+
Omega=268.66,
|
|
215
|
+
M0=145.95,
|
|
216
|
+
n=0.00376, # ~245 year period
|
|
217
|
+
),
|
|
218
|
+
SE_QUAOAR: OrbitalElements(
|
|
219
|
+
name="Quaoar",
|
|
220
|
+
epoch=2459945.5,
|
|
221
|
+
a=43.406,
|
|
222
|
+
e=0.039, # Nearly circular
|
|
223
|
+
i=8.005,
|
|
224
|
+
omega=147.10,
|
|
225
|
+
Omega=188.85,
|
|
226
|
+
M0=21.70,
|
|
227
|
+
n=0.00307, # ~287 year period
|
|
228
|
+
),
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def solve_kepler_equation(M: float, e: float, tol: float = 1e-8) -> float:
|
|
233
|
+
"""
|
|
234
|
+
Solve Kepler's equation M = E - e·sin(E) for eccentric anomaly E.
|
|
235
|
+
|
|
236
|
+
Uses Newton-Raphson iteration for robust convergence.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
M: Mean anomaly in radians
|
|
240
|
+
e: Eccentricity (0 ≤ e < 1)
|
|
241
|
+
tol: Convergence tolerance (default 1e-8 ~ 0.002 arcsec)
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
float: Eccentric anomaly E in radians
|
|
245
|
+
|
|
246
|
+
Algorithm:
|
|
247
|
+
Newton-Raphson: E_{n+1} = E_n - f(E_n)/f'(E_n)
|
|
248
|
+
where f(E) = E - e·sin(E) - M
|
|
249
|
+
and f'(E) = 1 - e·cos(E)
|
|
250
|
+
|
|
251
|
+
Note:
|
|
252
|
+
Converges in ~3-6 iterations for typical eccentricities (e < 0.8).
|
|
253
|
+
Initial guess: M for e < 0.8, π for highly eccentric orbits.
|
|
254
|
+
|
|
255
|
+
References:
|
|
256
|
+
Curtis "Orbital Mechanics for Engineering Students" §3.1
|
|
257
|
+
Vallado "Fundamentals of Astrodynamics" Algorithm 2
|
|
258
|
+
"""
|
|
259
|
+
# FIXME: Precision - For parabolic/hyperbolic orbits (e ≥ 1), use different equation
|
|
260
|
+
# This implementation assumes elliptical orbits (0 ≤ e < 1)
|
|
261
|
+
E = M if e < 0.8 else math.pi
|
|
262
|
+
|
|
263
|
+
for _ in range(30):
|
|
264
|
+
f = E - e * math.sin(E) - M
|
|
265
|
+
fp = 1 - e * math.cos(E)
|
|
266
|
+
E_new = E - f / fp
|
|
267
|
+
|
|
268
|
+
if abs(E_new - E) < tol:
|
|
269
|
+
return E_new
|
|
270
|
+
E = E_new
|
|
271
|
+
|
|
272
|
+
return E
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def calc_minor_body_position(
|
|
276
|
+
elements: OrbitalElements, jd_tt: float
|
|
277
|
+
) -> Tuple[float, float, float]:
|
|
278
|
+
"""
|
|
279
|
+
Calculate heliocentric position using Keplerian orbital elements.
|
|
280
|
+
|
|
281
|
+
Propagates orbit from epoch to target time using mean motion.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
elements: Orbital elements at epoch
|
|
285
|
+
jd_tt: Target Julian Day in Terrestrial Time (TT)
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Tuple[float, float, float]: (x, y, z) heliocentric position in AU
|
|
289
|
+
Coordinates in ecliptic J2000.0 frame
|
|
290
|
+
|
|
291
|
+
Algorithm:
|
|
292
|
+
1. Propagate mean anomaly: M(t) = M0 + n·Δt
|
|
293
|
+
2. Solve Kepler's equation for eccentric anomaly E
|
|
294
|
+
3. Calculate true anomaly ν from E
|
|
295
|
+
4. Compute position in orbital plane
|
|
296
|
+
5. Rotate to ecliptic frame using Ω, i, ω
|
|
297
|
+
|
|
298
|
+
FIXME: Precision - 2-body Keplerian propagation ignores:
|
|
299
|
+
- Planetary perturbations (Jupiter, Saturn especially)
|
|
300
|
+
- Non-gravitational forces (radiation pressure, Yarkovsky)
|
|
301
|
+
- Relativistic effects (minor for asteroids)
|
|
302
|
+
Typical errors: 1-5 arcminutes for asteroids, worse for TNOs
|
|
303
|
+
|
|
304
|
+
References:
|
|
305
|
+
Curtis §3 (orbital elements)
|
|
306
|
+
Vallado §2.3 (coordinate transformations)
|
|
307
|
+
"""
|
|
308
|
+
dt = jd_tt - elements.epoch
|
|
309
|
+
|
|
310
|
+
# Propagate mean anomaly
|
|
311
|
+
M = math.radians((elements.M0 + elements.n * dt) % 360.0)
|
|
312
|
+
|
|
313
|
+
# Solve Kepler's equation
|
|
314
|
+
E = solve_kepler_equation(M, elements.e)
|
|
315
|
+
|
|
316
|
+
# True anomaly from eccentric anomaly
|
|
317
|
+
nu = 2.0 * math.atan2(
|
|
318
|
+
math.sqrt(1 + elements.e) * math.sin(E / 2),
|
|
319
|
+
math.sqrt(1 - elements.e) * math.cos(E / 2),
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Heliocentric distance
|
|
323
|
+
r = elements.a * (1 - elements.e * math.cos(E))
|
|
324
|
+
|
|
325
|
+
# Position in orbital plane (perifocal frame)
|
|
326
|
+
x_orb = r * math.cos(nu)
|
|
327
|
+
y_orb = r * math.sin(nu)
|
|
328
|
+
|
|
329
|
+
# Convert Euler angles to radians
|
|
330
|
+
omega_rad = math.radians(elements.omega)
|
|
331
|
+
Omega_rad = math.radians(elements.Omega)
|
|
332
|
+
i_rad = math.radians(elements.i)
|
|
333
|
+
|
|
334
|
+
# Precompute trig functions
|
|
335
|
+
cos_omega = math.cos(omega_rad)
|
|
336
|
+
sin_omega = math.sin(omega_rad)
|
|
337
|
+
cos_Omega = math.cos(Omega_rad)
|
|
338
|
+
sin_Omega = math.sin(Omega_rad)
|
|
339
|
+
cos_i = math.cos(i_rad)
|
|
340
|
+
sin_i = math.sin(i_rad)
|
|
341
|
+
|
|
342
|
+
# Rotation matrix from perifocal to ecliptic (standard transformation)
|
|
343
|
+
P11 = cos_omega * cos_Omega - sin_omega * sin_Omega * cos_i
|
|
344
|
+
P12 = -sin_omega * cos_Omega - cos_omega * sin_Omega * cos_i
|
|
345
|
+
P21 = cos_omega * sin_Omega + sin_omega * cos_Omega * cos_i
|
|
346
|
+
P22 = -sin_omega * sin_Omega + cos_omega * cos_Omega * cos_i
|
|
347
|
+
P31 = sin_omega * sin_i
|
|
348
|
+
P32 = cos_omega * sin_i
|
|
349
|
+
|
|
350
|
+
# Transform to heliocentric ecliptic J2000 coordinates
|
|
351
|
+
x = P11 * x_orb + P12 * y_orb
|
|
352
|
+
y = P21 * x_orb + P22 * y_orb
|
|
353
|
+
z = P31 * x_orb + P32 * y_orb
|
|
354
|
+
|
|
355
|
+
return x, y, z
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def calc_minor_body_heliocentric(
|
|
359
|
+
body_id: int, jd_tt: float
|
|
360
|
+
) -> Tuple[float, float, float]:
|
|
361
|
+
"""
|
|
362
|
+
Calculate heliocentric ecliptic coordinates for a minor body.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
body_id: Minor body identifier (SE_CHIRON, SE_ERIS, etc.)
|
|
366
|
+
jd_tt: Julian Day in Terrestrial Time (TT)
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
Tuple[float, float, float]: (longitude, latitude, distance) where:
|
|
370
|
+
- longitude: Ecliptic longitude in degrees (0-360)
|
|
371
|
+
- latitude: Ecliptic latitude in degrees (-90 to +90)
|
|
372
|
+
- distance: Heliocentric distance in AU
|
|
373
|
+
|
|
374
|
+
Raises:
|
|
375
|
+
ValueError: If body_id is not in the database
|
|
376
|
+
|
|
377
|
+
Note:
|
|
378
|
+
Returns HELIOCENTRIC coordinates. For geocentric, caller must
|
|
379
|
+
subtract Earth's heliocentric position (see planets.py).
|
|
380
|
+
|
|
381
|
+
Precision:
|
|
382
|
+
Asteroids (Ceres, etc.): ~1-3 arcminutes typical
|
|
383
|
+
TNOs (Eris, etc.): ~3-10 arcminutes typical
|
|
384
|
+
Errors increase with time from epoch (2023.0)
|
|
385
|
+
"""
|
|
386
|
+
if body_id not in MINOR_BODY_ELEMENTS:
|
|
387
|
+
raise ValueError(f"Unknown minor body ID: {body_id}")
|
|
388
|
+
|
|
389
|
+
elements = MINOR_BODY_ELEMENTS[body_id]
|
|
390
|
+
x, y, z = calc_minor_body_position(elements, jd_tt)
|
|
391
|
+
|
|
392
|
+
# Convert Cartesian to spherical coordinates
|
|
393
|
+
r = math.sqrt(x**2 + y**2 + z**2)
|
|
394
|
+
lon = math.degrees(math.atan2(y, x)) % 360.0
|
|
395
|
+
lat = math.degrees(math.asin(z / r))
|
|
396
|
+
|
|
397
|
+
return lon, lat, r
|
|
398
|
+
|