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.
Files changed (138) hide show
  1. {nrl_tracker-0.21.5.dist-info → nrl_tracker-0.22.0.dist-info}/METADATA +2 -2
  2. nrl_tracker-0.22.0.dist-info/RECORD +150 -0
  3. pytcl/__init__.py +9 -11
  4. pytcl/assignment_algorithms/__init__.py +32 -42
  5. pytcl/assignment_algorithms/data_association.py +9 -10
  6. pytcl/assignment_algorithms/gating.py +7 -5
  7. pytcl/assignment_algorithms/jpda.py +10 -14
  8. pytcl/assignment_algorithms/three_dimensional/__init__.py +6 -8
  9. pytcl/assignment_algorithms/three_dimensional/assignment.py +6 -2
  10. pytcl/assignment_algorithms/two_dimensional/__init__.py +9 -13
  11. pytcl/assignment_algorithms/two_dimensional/assignment.py +5 -2
  12. pytcl/assignment_algorithms/two_dimensional/kbest.py +9 -9
  13. pytcl/astronomical/__init__.py +130 -89
  14. pytcl/astronomical/ephemerides.py +524 -0
  15. pytcl/astronomical/lambert.py +6 -15
  16. pytcl/astronomical/orbital_mechanics.py +1 -3
  17. pytcl/astronomical/reference_frames.py +1 -3
  18. pytcl/astronomical/relativity.py +466 -0
  19. pytcl/astronomical/time_systems.py +2 -1
  20. pytcl/atmosphere/__init__.py +12 -14
  21. pytcl/atmosphere/models.py +5 -5
  22. pytcl/clustering/__init__.py +28 -36
  23. pytcl/clustering/dbscan.py +5 -2
  24. pytcl/clustering/gaussian_mixture.py +10 -10
  25. pytcl/clustering/hierarchical.py +7 -7
  26. pytcl/clustering/kmeans.py +7 -5
  27. pytcl/containers/__init__.py +29 -43
  28. pytcl/containers/cluster_set.py +13 -20
  29. pytcl/containers/covertree.py +8 -2
  30. pytcl/containers/kd_tree.py +6 -2
  31. pytcl/containers/measurement_set.py +11 -16
  32. pytcl/containers/rtree.py +8 -7
  33. pytcl/containers/track_list.py +13 -13
  34. pytcl/containers/vptree.py +7 -2
  35. pytcl/coordinate_systems/__init__.py +69 -74
  36. pytcl/coordinate_systems/conversions/__init__.py +20 -24
  37. pytcl/coordinate_systems/conversions/geodetic.py +7 -17
  38. pytcl/coordinate_systems/conversions/spherical.py +4 -2
  39. pytcl/coordinate_systems/jacobians/__init__.py +10 -12
  40. pytcl/coordinate_systems/jacobians/jacobians.py +2 -1
  41. pytcl/coordinate_systems/projections/__init__.py +27 -23
  42. pytcl/coordinate_systems/projections/projections.py +14 -39
  43. pytcl/coordinate_systems/rotations/__init__.py +20 -22
  44. pytcl/coordinate_systems/rotations/rotations.py +3 -4
  45. pytcl/core/__init__.py +16 -22
  46. pytcl/core/array_utils.py +7 -7
  47. pytcl/core/constants.py +1 -3
  48. pytcl/core/validation.py +13 -19
  49. pytcl/dynamic_estimation/__init__.py +77 -86
  50. pytcl/dynamic_estimation/imm.py +10 -15
  51. pytcl/dynamic_estimation/information_filter.py +8 -6
  52. pytcl/dynamic_estimation/kalman/__init__.py +40 -48
  53. pytcl/dynamic_estimation/kalman/extended.py +4 -5
  54. pytcl/dynamic_estimation/kalman/linear.py +7 -3
  55. pytcl/dynamic_estimation/kalman/square_root.py +7 -8
  56. pytcl/dynamic_estimation/kalman/unscented.py +8 -6
  57. pytcl/dynamic_estimation/particle_filters/__init__.py +12 -14
  58. pytcl/dynamic_estimation/particle_filters/bootstrap.py +8 -8
  59. pytcl/dynamic_estimation/smoothers.py +9 -10
  60. pytcl/dynamic_models/__init__.py +37 -41
  61. pytcl/dynamic_models/continuous_time/__init__.py +11 -11
  62. pytcl/dynamic_models/continuous_time/dynamics.py +4 -2
  63. pytcl/dynamic_models/discrete_time/__init__.py +11 -17
  64. pytcl/dynamic_models/process_noise/__init__.py +11 -17
  65. pytcl/dynamic_models/process_noise/polynomial.py +2 -6
  66. pytcl/gravity/__init__.py +55 -65
  67. pytcl/gravity/clenshaw.py +4 -7
  68. pytcl/gravity/egm.py +9 -6
  69. pytcl/gravity/models.py +1 -3
  70. pytcl/gravity/spherical_harmonics.py +6 -11
  71. pytcl/gravity/tides.py +9 -17
  72. pytcl/magnetism/__init__.py +26 -36
  73. pytcl/magnetism/emm.py +7 -13
  74. pytcl/magnetism/igrf.py +5 -6
  75. pytcl/magnetism/wmm.py +4 -10
  76. pytcl/mathematical_functions/__init__.py +69 -87
  77. pytcl/mathematical_functions/basic_matrix/__init__.py +25 -19
  78. pytcl/mathematical_functions/basic_matrix/decompositions.py +6 -5
  79. pytcl/mathematical_functions/basic_matrix/special_matrices.py +2 -1
  80. pytcl/mathematical_functions/combinatorics/__init__.py +18 -14
  81. pytcl/mathematical_functions/combinatorics/combinatorics.py +5 -4
  82. pytcl/mathematical_functions/geometry/__init__.py +15 -15
  83. pytcl/mathematical_functions/geometry/geometry.py +10 -15
  84. pytcl/mathematical_functions/interpolation/__init__.py +11 -13
  85. pytcl/mathematical_functions/interpolation/interpolation.py +8 -5
  86. pytcl/mathematical_functions/numerical_integration/__init__.py +16 -10
  87. pytcl/mathematical_functions/numerical_integration/quadrature.py +6 -2
  88. pytcl/mathematical_functions/signal_processing/__init__.py +42 -30
  89. pytcl/mathematical_functions/signal_processing/detection.py +9 -9
  90. pytcl/mathematical_functions/signal_processing/filters.py +7 -8
  91. pytcl/mathematical_functions/signal_processing/matched_filter.py +8 -7
  92. pytcl/mathematical_functions/special_functions/__init__.py +75 -77
  93. pytcl/mathematical_functions/special_functions/bessel.py +2 -1
  94. pytcl/mathematical_functions/special_functions/debye.py +4 -2
  95. pytcl/mathematical_functions/special_functions/elliptic.py +3 -4
  96. pytcl/mathematical_functions/special_functions/error_functions.py +2 -1
  97. pytcl/mathematical_functions/special_functions/gamma_functions.py +3 -4
  98. pytcl/mathematical_functions/special_functions/hypergeometric.py +2 -1
  99. pytcl/mathematical_functions/special_functions/lambert_w.py +3 -4
  100. pytcl/mathematical_functions/special_functions/marcum_q.py +2 -1
  101. pytcl/mathematical_functions/statistics/__init__.py +27 -31
  102. pytcl/mathematical_functions/statistics/distributions.py +21 -40
  103. pytcl/mathematical_functions/statistics/estimators.py +3 -4
  104. pytcl/mathematical_functions/transforms/__init__.py +45 -51
  105. pytcl/mathematical_functions/transforms/fourier.py +5 -2
  106. pytcl/mathematical_functions/transforms/stft.py +8 -11
  107. pytcl/mathematical_functions/transforms/wavelets.py +13 -20
  108. pytcl/navigation/__init__.py +96 -102
  109. pytcl/navigation/geodesy.py +13 -33
  110. pytcl/navigation/great_circle.py +7 -13
  111. pytcl/navigation/ins.py +12 -16
  112. pytcl/navigation/ins_gnss.py +24 -37
  113. pytcl/navigation/rhumb.py +7 -12
  114. pytcl/performance_evaluation/__init__.py +21 -25
  115. pytcl/performance_evaluation/estimation_metrics.py +3 -1
  116. pytcl/performance_evaluation/track_metrics.py +4 -4
  117. pytcl/plotting/__init__.py +30 -38
  118. pytcl/plotting/coordinates.py +8 -18
  119. pytcl/plotting/ellipses.py +5 -2
  120. pytcl/plotting/metrics.py +5 -10
  121. pytcl/plotting/tracks.py +7 -12
  122. pytcl/static_estimation/__init__.py +37 -41
  123. pytcl/static_estimation/least_squares.py +5 -4
  124. pytcl/static_estimation/maximum_likelihood.py +8 -5
  125. pytcl/static_estimation/robust.py +5 -2
  126. pytcl/terrain/__init__.py +28 -34
  127. pytcl/terrain/dem.py +6 -9
  128. pytcl/terrain/loaders.py +9 -14
  129. pytcl/terrain/visibility.py +4 -8
  130. pytcl/trackers/__init__.py +17 -25
  131. pytcl/trackers/hypothesis.py +8 -8
  132. pytcl/trackers/mht.py +18 -24
  133. pytcl/trackers/multi_target.py +8 -6
  134. pytcl/trackers/single_target.py +5 -2
  135. nrl_tracker-0.21.5.dist-info/RECORD +0 -148
  136. {nrl_tracker-0.21.5.dist-info → nrl_tracker-0.22.0.dist-info}/LICENSE +0 -0
  137. {nrl_tracker-0.21.5.dist-info → nrl_tracker-0.22.0.dist-info}/WHEEL +0 -0
  138. {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)
@@ -15,7 +15,8 @@ References
15
15
  orbital boundary-value problem," Celestial Mechanics, 1990.
16
16
  """
17
17
 
18
- from typing import NamedTuple, Tuple
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