nrl-tracker 0.21.5__py3-none-any.whl → 0.22.0__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.
- {nrl_tracker-0.21.5.dist-info → nrl_tracker-0.22.0.dist-info}/METADATA +2 -2
- nrl_tracker-0.22.0.dist-info/RECORD +150 -0
- pytcl/__init__.py +9 -11
- pytcl/assignment_algorithms/__init__.py +32 -42
- pytcl/assignment_algorithms/data_association.py +9 -10
- pytcl/assignment_algorithms/gating.py +7 -5
- pytcl/assignment_algorithms/jpda.py +10 -14
- pytcl/assignment_algorithms/three_dimensional/__init__.py +6 -8
- pytcl/assignment_algorithms/three_dimensional/assignment.py +6 -2
- pytcl/assignment_algorithms/two_dimensional/__init__.py +9 -13
- pytcl/assignment_algorithms/two_dimensional/assignment.py +5 -2
- pytcl/assignment_algorithms/two_dimensional/kbest.py +9 -9
- pytcl/astronomical/__init__.py +130 -89
- pytcl/astronomical/ephemerides.py +524 -0
- pytcl/astronomical/lambert.py +6 -15
- pytcl/astronomical/orbital_mechanics.py +1 -3
- pytcl/astronomical/reference_frames.py +1 -3
- pytcl/astronomical/relativity.py +466 -0
- pytcl/astronomical/time_systems.py +2 -1
- pytcl/atmosphere/__init__.py +12 -14
- pytcl/atmosphere/models.py +5 -5
- pytcl/clustering/__init__.py +28 -36
- pytcl/clustering/dbscan.py +5 -2
- pytcl/clustering/gaussian_mixture.py +10 -10
- pytcl/clustering/hierarchical.py +7 -7
- pytcl/clustering/kmeans.py +7 -5
- pytcl/containers/__init__.py +29 -43
- pytcl/containers/cluster_set.py +13 -20
- pytcl/containers/covertree.py +8 -2
- pytcl/containers/kd_tree.py +6 -2
- pytcl/containers/measurement_set.py +11 -16
- pytcl/containers/rtree.py +8 -7
- pytcl/containers/track_list.py +13 -13
- pytcl/containers/vptree.py +7 -2
- pytcl/coordinate_systems/__init__.py +69 -74
- pytcl/coordinate_systems/conversions/__init__.py +20 -24
- pytcl/coordinate_systems/conversions/geodetic.py +7 -17
- pytcl/coordinate_systems/conversions/spherical.py +4 -2
- pytcl/coordinate_systems/jacobians/__init__.py +10 -12
- pytcl/coordinate_systems/jacobians/jacobians.py +2 -1
- pytcl/coordinate_systems/projections/__init__.py +27 -23
- pytcl/coordinate_systems/projections/projections.py +14 -39
- pytcl/coordinate_systems/rotations/__init__.py +20 -22
- pytcl/coordinate_systems/rotations/rotations.py +3 -4
- pytcl/core/__init__.py +16 -22
- pytcl/core/array_utils.py +7 -7
- pytcl/core/constants.py +1 -3
- pytcl/core/validation.py +13 -19
- pytcl/dynamic_estimation/__init__.py +77 -86
- pytcl/dynamic_estimation/imm.py +10 -15
- pytcl/dynamic_estimation/information_filter.py +8 -6
- pytcl/dynamic_estimation/kalman/__init__.py +40 -48
- pytcl/dynamic_estimation/kalman/extended.py +4 -5
- pytcl/dynamic_estimation/kalman/linear.py +7 -3
- pytcl/dynamic_estimation/kalman/square_root.py +7 -8
- pytcl/dynamic_estimation/kalman/unscented.py +8 -6
- pytcl/dynamic_estimation/particle_filters/__init__.py +12 -14
- pytcl/dynamic_estimation/particle_filters/bootstrap.py +8 -8
- pytcl/dynamic_estimation/smoothers.py +9 -10
- pytcl/dynamic_models/__init__.py +37 -41
- pytcl/dynamic_models/continuous_time/__init__.py +11 -11
- pytcl/dynamic_models/continuous_time/dynamics.py +4 -2
- pytcl/dynamic_models/discrete_time/__init__.py +11 -17
- pytcl/dynamic_models/process_noise/__init__.py +11 -17
- pytcl/dynamic_models/process_noise/polynomial.py +2 -6
- pytcl/gravity/__init__.py +55 -65
- pytcl/gravity/clenshaw.py +4 -7
- pytcl/gravity/egm.py +9 -6
- pytcl/gravity/models.py +1 -3
- pytcl/gravity/spherical_harmonics.py +6 -11
- pytcl/gravity/tides.py +9 -17
- pytcl/magnetism/__init__.py +26 -36
- pytcl/magnetism/emm.py +7 -13
- pytcl/magnetism/igrf.py +5 -6
- pytcl/magnetism/wmm.py +4 -10
- pytcl/mathematical_functions/__init__.py +69 -87
- pytcl/mathematical_functions/basic_matrix/__init__.py +25 -19
- pytcl/mathematical_functions/basic_matrix/decompositions.py +6 -5
- pytcl/mathematical_functions/basic_matrix/special_matrices.py +2 -1
- pytcl/mathematical_functions/combinatorics/__init__.py +18 -14
- pytcl/mathematical_functions/combinatorics/combinatorics.py +5 -4
- pytcl/mathematical_functions/geometry/__init__.py +15 -15
- pytcl/mathematical_functions/geometry/geometry.py +10 -15
- pytcl/mathematical_functions/interpolation/__init__.py +11 -13
- pytcl/mathematical_functions/interpolation/interpolation.py +8 -5
- pytcl/mathematical_functions/numerical_integration/__init__.py +16 -10
- pytcl/mathematical_functions/numerical_integration/quadrature.py +6 -2
- pytcl/mathematical_functions/signal_processing/__init__.py +42 -30
- pytcl/mathematical_functions/signal_processing/detection.py +9 -9
- pytcl/mathematical_functions/signal_processing/filters.py +7 -8
- pytcl/mathematical_functions/signal_processing/matched_filter.py +8 -7
- pytcl/mathematical_functions/special_functions/__init__.py +75 -77
- pytcl/mathematical_functions/special_functions/bessel.py +2 -1
- pytcl/mathematical_functions/special_functions/debye.py +4 -2
- pytcl/mathematical_functions/special_functions/elliptic.py +3 -4
- pytcl/mathematical_functions/special_functions/error_functions.py +2 -1
- pytcl/mathematical_functions/special_functions/gamma_functions.py +3 -4
- pytcl/mathematical_functions/special_functions/hypergeometric.py +2 -1
- pytcl/mathematical_functions/special_functions/lambert_w.py +3 -4
- pytcl/mathematical_functions/special_functions/marcum_q.py +2 -1
- pytcl/mathematical_functions/statistics/__init__.py +27 -31
- pytcl/mathematical_functions/statistics/distributions.py +21 -40
- pytcl/mathematical_functions/statistics/estimators.py +3 -4
- pytcl/mathematical_functions/transforms/__init__.py +45 -51
- pytcl/mathematical_functions/transforms/fourier.py +5 -2
- pytcl/mathematical_functions/transforms/stft.py +8 -11
- pytcl/mathematical_functions/transforms/wavelets.py +13 -20
- pytcl/navigation/__init__.py +96 -102
- pytcl/navigation/geodesy.py +13 -33
- pytcl/navigation/great_circle.py +7 -13
- pytcl/navigation/ins.py +12 -16
- pytcl/navigation/ins_gnss.py +24 -37
- pytcl/navigation/rhumb.py +7 -12
- pytcl/performance_evaluation/__init__.py +21 -25
- pytcl/performance_evaluation/estimation_metrics.py +3 -1
- pytcl/performance_evaluation/track_metrics.py +4 -4
- pytcl/plotting/__init__.py +30 -38
- pytcl/plotting/coordinates.py +8 -18
- pytcl/plotting/ellipses.py +5 -2
- pytcl/plotting/metrics.py +5 -10
- pytcl/plotting/tracks.py +7 -12
- pytcl/static_estimation/__init__.py +37 -41
- pytcl/static_estimation/least_squares.py +5 -4
- pytcl/static_estimation/maximum_likelihood.py +8 -5
- pytcl/static_estimation/robust.py +5 -2
- pytcl/terrain/__init__.py +28 -34
- pytcl/terrain/dem.py +6 -9
- pytcl/terrain/loaders.py +9 -14
- pytcl/terrain/visibility.py +4 -8
- pytcl/trackers/__init__.py +17 -25
- pytcl/trackers/hypothesis.py +8 -8
- pytcl/trackers/mht.py +18 -24
- pytcl/trackers/multi_target.py +8 -6
- pytcl/trackers/single_target.py +5 -2
- nrl_tracker-0.21.5.dist-info/RECORD +0 -148
- {nrl_tracker-0.21.5.dist-info → nrl_tracker-0.22.0.dist-info}/LICENSE +0 -0
- {nrl_tracker-0.21.5.dist-info → nrl_tracker-0.22.0.dist-info}/WHEEL +0 -0
- {nrl_tracker-0.21.5.dist-info → nrl_tracker-0.22.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JPL Ephemerides for High-Precision Celestial Mechanics
|
|
3
|
+
|
|
4
|
+
This module provides access to JPL Development Ephemeris (DE) files for computing
|
|
5
|
+
high-precision positions and velocities of celestial bodies (Sun, Moon, planets).
|
|
6
|
+
|
|
7
|
+
The module leverages the jplephem library, which provides optimized Fortran-based
|
|
8
|
+
interpolation of ephemeris kernels. Multiple DE versions are supported (DE405,
|
|
9
|
+
DE430, DE432s, DE440).
|
|
10
|
+
|
|
11
|
+
Constants
|
|
12
|
+
---------
|
|
13
|
+
AU_PER_KM : float
|
|
14
|
+
Astronomical Unit in kilometers (1 AU = 149597870.7 km)
|
|
15
|
+
KM_PER_DAY_TO_AU_PER_DAY : float
|
|
16
|
+
Conversion factor for velocity from km/day to AU/day
|
|
17
|
+
|
|
18
|
+
Examples
|
|
19
|
+
--------
|
|
20
|
+
>>> from pytcl.astronomical.ephemerides import DEEphemeris
|
|
21
|
+
>>> from datetime import datetime
|
|
22
|
+
>>>
|
|
23
|
+
>>> # Load ephemeris (auto-downloads if needed)
|
|
24
|
+
>>> eph = DEEphemeris(version='DE440')
|
|
25
|
+
>>>
|
|
26
|
+
>>> # Query Sun position (AU)
|
|
27
|
+
>>> jd = 2451545.0 # J2000.0
|
|
28
|
+
>>> r_sun, v_sun = eph.sun_position(jd)
|
|
29
|
+
>>> print(f"Sun distance: {np.linalg.norm(r_sun):.6f} AU")
|
|
30
|
+
Sun distance: 0.983327 AU
|
|
31
|
+
>>>
|
|
32
|
+
>>> # Query Moon position
|
|
33
|
+
>>> r_moon, v_moon = eph.moon_position(jd)
|
|
34
|
+
|
|
35
|
+
Notes
|
|
36
|
+
-----
|
|
37
|
+
- Ephemeris files are auto-downloaded to ~/.jplephem/ on first use
|
|
38
|
+
- Time input is Julian Day (JD) in Terrestrial Time (TT) scale
|
|
39
|
+
- Positions returned in AU, velocities in AU/day in ICRF frame
|
|
40
|
+
- For highest precision, use DE440 (latest release) or DE432s (2013)
|
|
41
|
+
|
|
42
|
+
References
|
|
43
|
+
----------
|
|
44
|
+
.. [1] Standish, E. M. (1995). "Report of the IAU WGAS Sub-group on
|
|
45
|
+
Numerical Standards". In Highlights of Astronomy (Vol. 10).
|
|
46
|
+
.. [2] Folkner, W. M., Williams, J. G., Boggs, D. H., Park, R. S., &
|
|
47
|
+
Kuchynka, P. (2014). "The Planetary and Lunar Ephemeris DE430 and DE431".
|
|
48
|
+
Interplanetary Network Progress Report, 42(196), 1-81.
|
|
49
|
+
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
from typing import Literal
|
|
53
|
+
from typing import Optional
|
|
54
|
+
from typing import Tuple
|
|
55
|
+
|
|
56
|
+
import numpy as np
|
|
57
|
+
|
|
58
|
+
# Constants for unit conversion
|
|
59
|
+
AU_PER_KM = 1.0 / 149597870.7 # 1 AU in km
|
|
60
|
+
KM_PER_DAY_TO_AU_PER_DAY = AU_PER_KM # velocity conversion factor
|
|
61
|
+
EPSILON_J2000 = 0.4090910179 # Mean obliquity of the ecliptic at J2000.0 (radians)
|
|
62
|
+
|
|
63
|
+
__all__ = [
|
|
64
|
+
"DEEphemeris",
|
|
65
|
+
"sun_position",
|
|
66
|
+
"moon_position",
|
|
67
|
+
"planet_position",
|
|
68
|
+
"barycenter_position",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class DEEphemeris:
|
|
73
|
+
"""High-precision JPL Development Ephemeris kernel wrapper.
|
|
74
|
+
|
|
75
|
+
This class manages access to JPL ephemeris files and provides methods
|
|
76
|
+
for querying positions and velocities of celestial bodies.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
version : {'DE405', 'DE430', 'DE432s', 'DE440'}, optional
|
|
81
|
+
Ephemeris version to load. Default is 'DE440' (latest).
|
|
82
|
+
- DE440: Latest JPL release (2020), covers 1550-2650
|
|
83
|
+
- DE432s: High-precision version (2013), covers 1350-3000
|
|
84
|
+
- DE430: Earlier release (2013), covers 1550-2650
|
|
85
|
+
- DE405: Older version (1998), compact, covers 1600-2200
|
|
86
|
+
|
|
87
|
+
Attributes
|
|
88
|
+
----------
|
|
89
|
+
version : str
|
|
90
|
+
Ephemeris version identifier
|
|
91
|
+
kernel : jplephem.SpiceKernel
|
|
92
|
+
Loaded ephemeris kernel object
|
|
93
|
+
_cache : dict
|
|
94
|
+
Cache for frequently accessed positions
|
|
95
|
+
|
|
96
|
+
Raises
|
|
97
|
+
------
|
|
98
|
+
ImportError
|
|
99
|
+
If jplephem is not installed
|
|
100
|
+
ValueError
|
|
101
|
+
If version is not recognized
|
|
102
|
+
|
|
103
|
+
Examples
|
|
104
|
+
--------
|
|
105
|
+
>>> eph = DEEphemeris(version='DE440')
|
|
106
|
+
>>> r_sun, v_sun = eph.sun_position(2451545.0)
|
|
107
|
+
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
# Valid ephemeris versions
|
|
111
|
+
_VALID_VERSIONS = {"DE405", "DE430", "DE432s", "DE440"}
|
|
112
|
+
|
|
113
|
+
# Supported bodies and their DE IDs
|
|
114
|
+
# See: https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/req/naif_ids.html
|
|
115
|
+
_BODY_IDS = {
|
|
116
|
+
"mercury": 1,
|
|
117
|
+
"venus": 2,
|
|
118
|
+
"earth": 3,
|
|
119
|
+
"moon": 301,
|
|
120
|
+
"mars": 4,
|
|
121
|
+
"jupiter": 5,
|
|
122
|
+
"saturn": 6,
|
|
123
|
+
"uranus": 7,
|
|
124
|
+
"neptune": 8,
|
|
125
|
+
"pluto": 9,
|
|
126
|
+
"sun": 10,
|
|
127
|
+
"earth_moon_barycenter": 3,
|
|
128
|
+
"solar_system_barycenter": 0,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
def __init__(self, version: str = "DE440") -> None:
|
|
132
|
+
"""Initialize ephemeris kernel.
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
version : str, optional
|
|
137
|
+
Ephemeris version (default: 'DE440')
|
|
138
|
+
|
|
139
|
+
"""
|
|
140
|
+
if version not in self._VALID_VERSIONS:
|
|
141
|
+
raise ValueError(
|
|
142
|
+
f"Ephemeris version must be one of {self._VALID_VERSIONS}, " f"got '{version}'"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
import jplephem
|
|
147
|
+
except ImportError as e:
|
|
148
|
+
raise ImportError(
|
|
149
|
+
"jplephem is required for ephemeris access. " "Install with: pip install jplephem"
|
|
150
|
+
) from e
|
|
151
|
+
|
|
152
|
+
self.version = version
|
|
153
|
+
self._jplephem = jplephem
|
|
154
|
+
self._kernel: Optional[object] = None
|
|
155
|
+
self._cache: dict = {}
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def kernel(self):
|
|
159
|
+
"""Lazy-load ephemeris kernel on first access.
|
|
160
|
+
|
|
161
|
+
Note: This requires jplephem to be installed and the kernel file
|
|
162
|
+
to be available locally or downloadable from the JPL servers.
|
|
163
|
+
"""
|
|
164
|
+
if self._kernel is None:
|
|
165
|
+
try:
|
|
166
|
+
# Try to load using jplephem SPK module
|
|
167
|
+
import os
|
|
168
|
+
import urllib.request
|
|
169
|
+
|
|
170
|
+
from jplephem.daf import DAF
|
|
171
|
+
from jplephem.spk import SPK
|
|
172
|
+
|
|
173
|
+
# Try to construct kernel filename
|
|
174
|
+
kernel_name = f"de{self.version[2:]}.bsp"
|
|
175
|
+
kernel_url = (
|
|
176
|
+
f"https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/planets/{kernel_name}"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Try to download if not exists
|
|
180
|
+
kernel_path = os.path.expanduser(f"~/.jplephem/{kernel_name}")
|
|
181
|
+
os_dir = os.path.dirname(kernel_path)
|
|
182
|
+
if not os.path.exists(os_dir):
|
|
183
|
+
os.makedirs(os_dir, exist_ok=True)
|
|
184
|
+
|
|
185
|
+
if not os.path.exists(kernel_path):
|
|
186
|
+
try:
|
|
187
|
+
urllib.request.urlretrieve(kernel_url, kernel_path)
|
|
188
|
+
except Exception as e:
|
|
189
|
+
raise RuntimeError(
|
|
190
|
+
f"Could not download ephemeris kernel from {kernel_url}. "
|
|
191
|
+
f"Please download manually and place at {kernel_path}"
|
|
192
|
+
) from e
|
|
193
|
+
|
|
194
|
+
# Load the kernel using DAF and SPK
|
|
195
|
+
daf = DAF(open(kernel_path, "rb"))
|
|
196
|
+
self._kernel = SPK(daf)
|
|
197
|
+
|
|
198
|
+
except Exception as e:
|
|
199
|
+
raise RuntimeError(
|
|
200
|
+
f"Failed to load ephemeris kernel for version {self.version}. "
|
|
201
|
+
f"Ensure jplephem is installed and kernel files are accessible. "
|
|
202
|
+
f"Error: {str(e)}"
|
|
203
|
+
) from e
|
|
204
|
+
|
|
205
|
+
return self._kernel
|
|
206
|
+
|
|
207
|
+
def sun_position(
|
|
208
|
+
self, jd: float, frame: Literal["icrf", "ecliptic"] = "icrf"
|
|
209
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
210
|
+
"""Compute Sun position and velocity.
|
|
211
|
+
|
|
212
|
+
Parameters
|
|
213
|
+
----------
|
|
214
|
+
jd : float
|
|
215
|
+
Julian Day in Terrestrial Time (TT)
|
|
216
|
+
frame : {'icrf', 'ecliptic'}, optional
|
|
217
|
+
Coordinate frame (default: 'icrf').
|
|
218
|
+
- 'icrf': International Celestial Reference Frame
|
|
219
|
+
- 'ecliptic': Ecliptic coordinate system (J2000.0)
|
|
220
|
+
|
|
221
|
+
Returns
|
|
222
|
+
-------
|
|
223
|
+
position : ndarray, shape (3,)
|
|
224
|
+
Sun position in AU
|
|
225
|
+
velocity : ndarray, shape (3,)
|
|
226
|
+
Sun velocity in AU/day
|
|
227
|
+
|
|
228
|
+
Notes
|
|
229
|
+
-----
|
|
230
|
+
The Sun's position is computed relative to the Solar System Barycenter
|
|
231
|
+
(SSB) in the ICRF frame.
|
|
232
|
+
|
|
233
|
+
Examples
|
|
234
|
+
--------
|
|
235
|
+
>>> eph = DEEphemeris()
|
|
236
|
+
>>> r, v = eph.sun_position(2451545.0)
|
|
237
|
+
>>> print(f"Distance: {np.linalg.norm(r):.6f} AU")
|
|
238
|
+
|
|
239
|
+
"""
|
|
240
|
+
# Sun position relative to SSB (in km)
|
|
241
|
+
segment = self.kernel[0, 10]
|
|
242
|
+
position, velocity = segment.compute_and_differentiate(jd)
|
|
243
|
+
|
|
244
|
+
# Convert from km to AU
|
|
245
|
+
position = np.array(position) * AU_PER_KM
|
|
246
|
+
velocity = np.array(velocity) * KM_PER_DAY_TO_AU_PER_DAY
|
|
247
|
+
|
|
248
|
+
if frame == "ecliptic":
|
|
249
|
+
from . import reference_frames
|
|
250
|
+
|
|
251
|
+
position = reference_frames.equatorial_to_ecliptic(position, EPSILON_J2000)
|
|
252
|
+
velocity = reference_frames.equatorial_to_ecliptic(velocity, EPSILON_J2000)
|
|
253
|
+
|
|
254
|
+
return position, velocity
|
|
255
|
+
|
|
256
|
+
def moon_position(
|
|
257
|
+
self, jd: float, frame: Literal["icrf", "ecliptic", "earth_centered"] = "icrf"
|
|
258
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
259
|
+
"""Compute Moon position and velocity.
|
|
260
|
+
|
|
261
|
+
Parameters
|
|
262
|
+
----------
|
|
263
|
+
jd : float
|
|
264
|
+
Julian Day in Terrestrial Time (TT)
|
|
265
|
+
frame : {'icrf', 'ecliptic', 'earth_centered'}, optional
|
|
266
|
+
Coordinate frame (default: 'icrf').
|
|
267
|
+
- 'icrf': Moon position relative to Solar System Barycenter
|
|
268
|
+
- 'ecliptic': Ecliptic coordinates
|
|
269
|
+
- 'earth_centered': Position relative to Earth
|
|
270
|
+
|
|
271
|
+
Returns
|
|
272
|
+
-------
|
|
273
|
+
position : ndarray, shape (3,)
|
|
274
|
+
Moon position in AU (or relative to Earth for 'earth_centered')
|
|
275
|
+
velocity : ndarray, shape (3,)
|
|
276
|
+
Moon velocity in AU/day
|
|
277
|
+
|
|
278
|
+
Notes
|
|
279
|
+
-----
|
|
280
|
+
By default, returns Moon position relative to the Solar System Barycenter.
|
|
281
|
+
Use frame='earth_centered' for geocentric coordinates.
|
|
282
|
+
|
|
283
|
+
Examples
|
|
284
|
+
--------
|
|
285
|
+
>>> eph = DEEphemeris()
|
|
286
|
+
>>> r, v = eph.moon_position(2451545.0, frame='earth_centered')
|
|
287
|
+
|
|
288
|
+
"""
|
|
289
|
+
if frame == "earth_centered":
|
|
290
|
+
# Moon relative to Earth
|
|
291
|
+
segment = self.kernel[3, 301]
|
|
292
|
+
position, velocity = segment.compute_and_differentiate(jd)
|
|
293
|
+
else:
|
|
294
|
+
# Moon relative to SSB: need to compute Earth->Moon, then add Earth->SSB
|
|
295
|
+
# Get Earth barycenter position
|
|
296
|
+
earth_segment = self.kernel[0, 3]
|
|
297
|
+
earth_pos, earth_vel = earth_segment.compute_and_differentiate(jd)
|
|
298
|
+
|
|
299
|
+
# Get Moon position relative to Earth
|
|
300
|
+
moon_segment = self.kernel[3, 301]
|
|
301
|
+
moon_rel_earth_pos, moon_rel_earth_vel = moon_segment.compute_and_differentiate(jd)
|
|
302
|
+
|
|
303
|
+
# Moon position relative to SSB
|
|
304
|
+
position = earth_pos + moon_rel_earth_pos
|
|
305
|
+
velocity = earth_vel + moon_rel_earth_vel
|
|
306
|
+
|
|
307
|
+
# Convert from km to AU
|
|
308
|
+
position = np.array(position) * AU_PER_KM
|
|
309
|
+
velocity = np.array(velocity) * KM_PER_DAY_TO_AU_PER_DAY
|
|
310
|
+
|
|
311
|
+
if frame == "ecliptic":
|
|
312
|
+
from . import reference_frames
|
|
313
|
+
|
|
314
|
+
position = reference_frames.equatorial_to_ecliptic(position, EPSILON_J2000)
|
|
315
|
+
velocity = reference_frames.equatorial_to_ecliptic(velocity, EPSILON_J2000)
|
|
316
|
+
|
|
317
|
+
return position, velocity
|
|
318
|
+
|
|
319
|
+
def planet_position(
|
|
320
|
+
self,
|
|
321
|
+
planet: Literal["mercury", "venus", "mars", "jupiter", "saturn", "uranus", "neptune"],
|
|
322
|
+
jd: float,
|
|
323
|
+
frame: Literal["icrf", "ecliptic"] = "icrf",
|
|
324
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
325
|
+
"""Compute planet position and velocity.
|
|
326
|
+
|
|
327
|
+
Parameters
|
|
328
|
+
----------
|
|
329
|
+
planet : str
|
|
330
|
+
Planet name: 'mercury', 'venus', 'mars', 'jupiter', 'saturn',
|
|
331
|
+
'uranus', 'neptune'
|
|
332
|
+
jd : float
|
|
333
|
+
Julian Day in Terrestrial Time (TT)
|
|
334
|
+
frame : {'icrf', 'ecliptic'}, optional
|
|
335
|
+
Coordinate frame (default: 'icrf')
|
|
336
|
+
|
|
337
|
+
Returns
|
|
338
|
+
-------
|
|
339
|
+
position : ndarray, shape (3,)
|
|
340
|
+
Planet position in AU
|
|
341
|
+
velocity : ndarray, shape (3,)
|
|
342
|
+
Planet velocity in AU/day
|
|
343
|
+
|
|
344
|
+
Raises
|
|
345
|
+
------
|
|
346
|
+
ValueError
|
|
347
|
+
If planet name is not recognized
|
|
348
|
+
|
|
349
|
+
Examples
|
|
350
|
+
--------
|
|
351
|
+
>>> eph = DEEphemeris()
|
|
352
|
+
>>> r, v = eph.planet_position('mars', 2451545.0)
|
|
353
|
+
|
|
354
|
+
"""
|
|
355
|
+
planet_lower = planet.lower()
|
|
356
|
+
if planet_lower not in self._BODY_IDS or planet_lower == "sun":
|
|
357
|
+
raise ValueError(
|
|
358
|
+
f"Planet must be one of {set(self._BODY_IDS.keys()) - {'sun', 'moon'}}, "
|
|
359
|
+
f"got '{planet}'"
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
planet_id = self._BODY_IDS[planet_lower]
|
|
363
|
+
segment = self.kernel[0, planet_id]
|
|
364
|
+
position, velocity = segment.compute_and_differentiate(jd)
|
|
365
|
+
|
|
366
|
+
# Convert from km to AU
|
|
367
|
+
position = np.array(position) * AU_PER_KM
|
|
368
|
+
velocity = np.array(velocity) * KM_PER_DAY_TO_AU_PER_DAY
|
|
369
|
+
|
|
370
|
+
if frame == "ecliptic":
|
|
371
|
+
from . import reference_frames
|
|
372
|
+
|
|
373
|
+
position = reference_frames.equatorial_to_ecliptic(position, EPSILON_J2000)
|
|
374
|
+
velocity = reference_frames.equatorial_to_ecliptic(velocity, EPSILON_J2000)
|
|
375
|
+
|
|
376
|
+
return position, velocity
|
|
377
|
+
|
|
378
|
+
def barycenter_position(self, body: str, jd: float) -> Tuple[np.ndarray, np.ndarray]:
|
|
379
|
+
"""Compute position of any body relative to Solar System Barycenter.
|
|
380
|
+
|
|
381
|
+
Parameters
|
|
382
|
+
----------
|
|
383
|
+
body : str
|
|
384
|
+
Body name ('sun', 'moon', 'mercury', ..., 'neptune')
|
|
385
|
+
jd : float
|
|
386
|
+
Julian Day in Terrestrial Time (TT)
|
|
387
|
+
|
|
388
|
+
Returns
|
|
389
|
+
-------
|
|
390
|
+
position : ndarray, shape (3,)
|
|
391
|
+
Position in AU
|
|
392
|
+
velocity : ndarray, shape (3,)
|
|
393
|
+
Velocity in AU/day
|
|
394
|
+
|
|
395
|
+
"""
|
|
396
|
+
if body.lower() == "sun":
|
|
397
|
+
return self.sun_position(jd)
|
|
398
|
+
elif body.lower() == "moon":
|
|
399
|
+
return self.moon_position(jd, frame="icrf")
|
|
400
|
+
else:
|
|
401
|
+
return self.planet_position(body, jd)
|
|
402
|
+
|
|
403
|
+
def clear_cache(self) -> None:
|
|
404
|
+
"""Clear internal position cache."""
|
|
405
|
+
self._cache.clear()
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# Module-level convenience functions
|
|
409
|
+
|
|
410
|
+
_default_eph: Optional[DEEphemeris] = None
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _get_default_ephemeris() -> DEEphemeris:
|
|
414
|
+
"""Get or create default ephemeris instance."""
|
|
415
|
+
global _default_eph
|
|
416
|
+
if _default_eph is None:
|
|
417
|
+
_default_eph = DEEphemeris(version="DE440")
|
|
418
|
+
return _default_eph
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def sun_position(
|
|
422
|
+
jd: float, frame: Literal["icrf", "ecliptic"] = "icrf"
|
|
423
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
424
|
+
"""Convenience function: Compute Sun position and velocity.
|
|
425
|
+
|
|
426
|
+
Parameters
|
|
427
|
+
----------
|
|
428
|
+
jd : float
|
|
429
|
+
Julian Day in Terrestrial Time (TT)
|
|
430
|
+
frame : {'icrf', 'ecliptic'}, optional
|
|
431
|
+
Coordinate frame (default: 'icrf')
|
|
432
|
+
|
|
433
|
+
Returns
|
|
434
|
+
-------
|
|
435
|
+
position : ndarray, shape (3,)
|
|
436
|
+
Sun position in AU
|
|
437
|
+
velocity : ndarray, shape (3,)
|
|
438
|
+
Sun velocity in AU/day
|
|
439
|
+
|
|
440
|
+
See Also
|
|
441
|
+
--------
|
|
442
|
+
DEEphemeris.sun_position : Full ephemeris class with caching
|
|
443
|
+
|
|
444
|
+
"""
|
|
445
|
+
return _get_default_ephemeris().sun_position(jd, frame=frame)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def moon_position(
|
|
449
|
+
jd: float, frame: Literal["icrf", "ecliptic", "earth_centered"] = "icrf"
|
|
450
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
451
|
+
"""Convenience function: Compute Moon position and velocity.
|
|
452
|
+
|
|
453
|
+
Parameters
|
|
454
|
+
----------
|
|
455
|
+
jd : float
|
|
456
|
+
Julian Day in Terrestrial Time (TT)
|
|
457
|
+
frame : {'icrf', 'ecliptic', 'earth_centered'}, optional
|
|
458
|
+
Coordinate frame (default: 'icrf')
|
|
459
|
+
|
|
460
|
+
Returns
|
|
461
|
+
-------
|
|
462
|
+
position : ndarray, shape (3,)
|
|
463
|
+
Moon position in AU
|
|
464
|
+
velocity : ndarray, shape (3,)
|
|
465
|
+
Moon velocity in AU/day
|
|
466
|
+
|
|
467
|
+
See Also
|
|
468
|
+
--------
|
|
469
|
+
DEEphemeris.moon_position : Full ephemeris class with caching
|
|
470
|
+
|
|
471
|
+
"""
|
|
472
|
+
return _get_default_ephemeris().moon_position(jd, frame=frame)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def planet_position(
|
|
476
|
+
planet: Literal["mercury", "venus", "mars", "jupiter", "saturn", "uranus", "neptune"],
|
|
477
|
+
jd: float,
|
|
478
|
+
frame: Literal["icrf", "ecliptic"] = "icrf",
|
|
479
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
480
|
+
"""Convenience function: Compute planet position and velocity.
|
|
481
|
+
|
|
482
|
+
Parameters
|
|
483
|
+
----------
|
|
484
|
+
planet : str
|
|
485
|
+
Planet name
|
|
486
|
+
jd : float
|
|
487
|
+
Julian Day in Terrestrial Time (TT)
|
|
488
|
+
frame : {'icrf', 'ecliptic'}, optional
|
|
489
|
+
Coordinate frame (default: 'icrf')
|
|
490
|
+
|
|
491
|
+
Returns
|
|
492
|
+
-------
|
|
493
|
+
position : ndarray, shape (3,)
|
|
494
|
+
Planet position in AU
|
|
495
|
+
velocity : ndarray, shape (3,)
|
|
496
|
+
Planet velocity in AU/day
|
|
497
|
+
|
|
498
|
+
See Also
|
|
499
|
+
--------
|
|
500
|
+
DEEphemeris.planet_position : Full ephemeris class with caching
|
|
501
|
+
|
|
502
|
+
"""
|
|
503
|
+
return _get_default_ephemeris().planet_position(planet, jd, frame=frame)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def barycenter_position(body: str, jd: float) -> Tuple[np.ndarray, np.ndarray]:
|
|
507
|
+
"""Convenience function: Position relative to Solar System Barycenter.
|
|
508
|
+
|
|
509
|
+
Parameters
|
|
510
|
+
----------
|
|
511
|
+
body : str
|
|
512
|
+
Body name ('sun', 'moon', 'mercury', ..., 'neptune')
|
|
513
|
+
jd : float
|
|
514
|
+
Julian Day in Terrestrial Time (TT)
|
|
515
|
+
|
|
516
|
+
Returns
|
|
517
|
+
-------
|
|
518
|
+
position : ndarray, shape (3,)
|
|
519
|
+
Position in AU
|
|
520
|
+
velocity : ndarray, shape (3,)
|
|
521
|
+
Velocity in AU/day
|
|
522
|
+
|
|
523
|
+
"""
|
|
524
|
+
return _get_default_ephemeris().barycenter_position(body, jd)
|
pytcl/astronomical/lambert.py
CHANGED
|
@@ -15,7 +15,8 @@ References
|
|
|
15
15
|
orbital boundary-value problem," Celestial Mechanics, 1990.
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
from typing import NamedTuple
|
|
18
|
+
from typing import NamedTuple
|
|
19
|
+
from typing import Tuple
|
|
19
20
|
|
|
20
21
|
import numpy as np
|
|
21
22
|
from numpy.typing import NDArray
|
|
@@ -198,9 +199,7 @@ def lambert_universal(
|
|
|
198
199
|
psi = (psi_low + psi_high) / 2
|
|
199
200
|
|
|
200
201
|
else:
|
|
201
|
-
raise ValueError(
|
|
202
|
-
f"Lambert's problem did not converge after {max_iter} iterations"
|
|
203
|
-
)
|
|
202
|
+
raise ValueError(f"Lambert's problem did not converge after {max_iter} iterations")
|
|
204
203
|
|
|
205
204
|
# Compute f, g, f_dot, g_dot
|
|
206
205
|
f = 1 - y / r1_mag
|
|
@@ -288,11 +287,7 @@ def lambert_izzo(
|
|
|
288
287
|
|
|
289
288
|
# Cross product for angular momentum direction
|
|
290
289
|
cross = np.cross(r1, r2)
|
|
291
|
-
h_hat = (
|
|
292
|
-
cross / np.linalg.norm(cross)
|
|
293
|
-
if np.linalg.norm(cross) > 1e-10
|
|
294
|
-
else np.array([0, 0, 1])
|
|
295
|
-
)
|
|
290
|
+
h_hat = cross / np.linalg.norm(cross) if np.linalg.norm(cross) > 1e-10 else np.array([0, 0, 1])
|
|
296
291
|
|
|
297
292
|
# Transfer angle
|
|
298
293
|
cos_dnu = np.dot(r1_hat, r2_hat)
|
|
@@ -335,13 +330,9 @@ def lambert_izzo(
|
|
|
335
330
|
|
|
336
331
|
# Time of flight equation
|
|
337
332
|
if x < 1:
|
|
338
|
-
psi = np.arccos(
|
|
339
|
-
x * lambda_param + y * np.sqrt(1 - lambda_param * lambda_param)
|
|
340
|
-
)
|
|
333
|
+
psi = np.arccos(x * lambda_param + y * np.sqrt(1 - lambda_param * lambda_param))
|
|
341
334
|
else:
|
|
342
|
-
psi = np.arccosh(
|
|
343
|
-
x * lambda_param + y * np.sqrt(lambda_param * lambda_param - 1)
|
|
344
|
-
)
|
|
335
|
+
psi = np.arccosh(x * lambda_param + y * np.sqrt(lambda_param * lambda_param - 1))
|
|
345
336
|
|
|
346
337
|
T_x = (
|
|
347
338
|
psi + multi_rev * np.pi - (x - lambda_param * y) * np.sqrt(abs(1 - x * x))
|
|
@@ -176,9 +176,7 @@ def mean_to_hyperbolic_anomaly(
|
|
|
176
176
|
if abs(delta) < tol:
|
|
177
177
|
return H
|
|
178
178
|
|
|
179
|
-
raise ValueError(
|
|
180
|
-
f"Hyperbolic Kepler's equation did not converge after {max_iter} iterations"
|
|
181
|
-
)
|
|
179
|
+
raise ValueError(f"Hyperbolic Kepler's equation did not converge after {max_iter} iterations")
|
|
182
180
|
|
|
183
181
|
|
|
184
182
|
def eccentric_to_true_anomaly(E: float, e: float) -> float:
|
|
@@ -292,9 +292,7 @@ def gmst_iau82(jd_ut1: float) -> float:
|
|
|
292
292
|
T_u = (jd_0h - JD_J2000) / 36525.0
|
|
293
293
|
|
|
294
294
|
# GMST at 0h UT1 (seconds)
|
|
295
|
-
gmst_0h_sec =
|
|
296
|
-
24110.54841 + 8640184.812866 * T_u + 0.093104 * T_u**2 - 6.2e-6 * T_u**3
|
|
297
|
-
)
|
|
295
|
+
gmst_0h_sec = 24110.54841 + 8640184.812866 * T_u + 0.093104 * T_u**2 - 6.2e-6 * T_u**3
|
|
298
296
|
|
|
299
297
|
# Add UT1 fraction
|
|
300
298
|
ut1_fraction = (jd_ut1 - jd_0h) * 86400.0
|