nrl-tracker 0.21.1__py3-none-any.whl → 0.22.5__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 (35) hide show
  1. {nrl_tracker-0.21.1.dist-info → nrl_tracker-0.22.5.dist-info}/METADATA +4 -4
  2. {nrl_tracker-0.21.1.dist-info → nrl_tracker-0.22.5.dist-info}/RECORD +35 -33
  3. pytcl/__init__.py +1 -1
  4. pytcl/assignment_algorithms/data_association.py +2 -7
  5. pytcl/assignment_algorithms/jpda.py +43 -29
  6. pytcl/assignment_algorithms/two_dimensional/assignment.py +14 -7
  7. pytcl/astronomical/__init__.py +60 -7
  8. pytcl/astronomical/ephemerides.py +530 -0
  9. pytcl/astronomical/relativity.py +472 -0
  10. pytcl/atmosphere/__init__.py +2 -2
  11. pytcl/clustering/dbscan.py +23 -5
  12. pytcl/clustering/hierarchical.py +23 -10
  13. pytcl/clustering/kmeans.py +5 -10
  14. pytcl/containers/__init__.py +4 -21
  15. pytcl/containers/cluster_set.py +1 -10
  16. pytcl/containers/measurement_set.py +1 -9
  17. pytcl/coordinate_systems/projections/__init__.py +4 -2
  18. pytcl/dynamic_estimation/imm.py +42 -36
  19. pytcl/dynamic_estimation/kalman/extended.py +1 -4
  20. pytcl/dynamic_estimation/kalman/linear.py +17 -13
  21. pytcl/dynamic_estimation/kalman/unscented.py +27 -27
  22. pytcl/dynamic_estimation/particle_filters/bootstrap.py +57 -19
  23. pytcl/dynamic_estimation/smoothers.py +1 -5
  24. pytcl/dynamic_models/discrete_time/__init__.py +1 -5
  25. pytcl/dynamic_models/process_noise/__init__.py +1 -5
  26. pytcl/magnetism/__init__.py +3 -14
  27. pytcl/mathematical_functions/interpolation/__init__.py +2 -2
  28. pytcl/mathematical_functions/special_functions/__init__.py +2 -2
  29. pytcl/navigation/__init__.py +14 -10
  30. pytcl/navigation/ins.py +1 -5
  31. pytcl/trackers/__init__.py +3 -14
  32. pytcl/trackers/multi_target.py +1 -4
  33. {nrl_tracker-0.21.1.dist-info → nrl_tracker-0.22.5.dist-info}/LICENSE +0 -0
  34. {nrl_tracker-0.21.1.dist-info → nrl_tracker-0.22.5.dist-info}/WHEEL +0 -0
  35. {nrl_tracker-0.21.1.dist-info → nrl_tracker-0.22.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,530 @@
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, Optional, Tuple
53
+
54
+ import numpy as np
55
+
56
+ # Constants for unit conversion
57
+ AU_PER_KM = 1.0 / 149597870.7 # 1 AU in km
58
+ KM_PER_DAY_TO_AU_PER_DAY = AU_PER_KM # velocity conversion factor
59
+ EPSILON_J2000 = 0.4090910179 # Mean obliquity of the ecliptic at J2000.0 (radians)
60
+
61
+ __all__ = [
62
+ "DEEphemeris",
63
+ "sun_position",
64
+ "moon_position",
65
+ "planet_position",
66
+ "barycenter_position",
67
+ ]
68
+
69
+
70
+ class DEEphemeris:
71
+ """High-precision JPL Development Ephemeris kernel wrapper.
72
+
73
+ This class manages access to JPL ephemeris files and provides methods
74
+ for querying positions and velocities of celestial bodies.
75
+
76
+ Parameters
77
+ ----------
78
+ version : {'DE405', 'DE430', 'DE432s', 'DE440'}, optional
79
+ Ephemeris version to load. Default is 'DE440' (latest).
80
+ - DE440: Latest JPL release (2020), covers 1550-2650
81
+ - DE432s: High-precision version (2013), covers 1350-3000
82
+ - DE430: Earlier release (2013), covers 1550-2650
83
+ - DE405: Older version (1998), compact, covers 1600-2200
84
+
85
+ Attributes
86
+ ----------
87
+ version : str
88
+ Ephemeris version identifier
89
+ kernel : jplephem.SpiceKernel
90
+ Loaded ephemeris kernel object
91
+ _cache : dict
92
+ Cache for frequently accessed positions
93
+
94
+ Raises
95
+ ------
96
+ ImportError
97
+ If jplephem is not installed
98
+ ValueError
99
+ If version is not recognized
100
+
101
+ Examples
102
+ --------
103
+ >>> eph = DEEphemeris(version='DE440')
104
+ >>> r_sun, v_sun = eph.sun_position(2451545.0)
105
+
106
+ """
107
+
108
+ # Valid ephemeris versions
109
+ _VALID_VERSIONS = {"DE405", "DE430", "DE432s", "DE440"}
110
+
111
+ # Supported bodies and their DE IDs
112
+ # See: https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/req/naif_ids.html
113
+ _BODY_IDS = {
114
+ "mercury": 1,
115
+ "venus": 2,
116
+ "earth": 3,
117
+ "moon": 301,
118
+ "mars": 4,
119
+ "jupiter": 5,
120
+ "saturn": 6,
121
+ "uranus": 7,
122
+ "neptune": 8,
123
+ "pluto": 9,
124
+ "sun": 10,
125
+ "earth_moon_barycenter": 3,
126
+ "solar_system_barycenter": 0,
127
+ }
128
+
129
+ def __init__(self, version: str = "DE440") -> None:
130
+ """Initialize ephemeris kernel.
131
+
132
+ Parameters
133
+ ----------
134
+ version : str, optional
135
+ Ephemeris version (default: 'DE440')
136
+
137
+ """
138
+ if version not in self._VALID_VERSIONS:
139
+ raise ValueError(
140
+ f"Ephemeris version must be one of {self._VALID_VERSIONS}, "
141
+ f"got '{version}'"
142
+ )
143
+
144
+ try:
145
+ import jplephem
146
+ except ImportError as e:
147
+ raise ImportError(
148
+ "jplephem is required for ephemeris access. "
149
+ "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 = f"https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/planets/{kernel_name}"
176
+
177
+ # Try to download if not exists
178
+ kernel_path = os.path.expanduser(f"~/.jplephem/{kernel_name}")
179
+ os_dir = os.path.dirname(kernel_path)
180
+ if not os.path.exists(os_dir):
181
+ os.makedirs(os_dir, exist_ok=True)
182
+
183
+ if not os.path.exists(kernel_path):
184
+ try:
185
+ urllib.request.urlretrieve(kernel_url, kernel_path)
186
+ except Exception as e:
187
+ raise RuntimeError(
188
+ f"Could not download ephemeris kernel from {kernel_url}. "
189
+ f"Please download manually and place at {kernel_path}"
190
+ ) from e
191
+
192
+ # Load the kernel using DAF and SPK
193
+ daf = DAF(open(kernel_path, "rb"))
194
+ self._kernel = SPK(daf)
195
+
196
+ except Exception as e:
197
+ raise RuntimeError(
198
+ f"Failed to load ephemeris kernel for version {self.version}. "
199
+ f"Ensure jplephem is installed and kernel files are accessible. "
200
+ f"Error: {str(e)}"
201
+ ) from e
202
+
203
+ return self._kernel
204
+
205
+ def sun_position(
206
+ self, jd: float, frame: Literal["icrf", "ecliptic"] = "icrf"
207
+ ) -> Tuple[np.ndarray, np.ndarray]:
208
+ """Compute Sun position and velocity.
209
+
210
+ Parameters
211
+ ----------
212
+ jd : float
213
+ Julian Day in Terrestrial Time (TT)
214
+ frame : {'icrf', 'ecliptic'}, optional
215
+ Coordinate frame (default: 'icrf').
216
+ - 'icrf': International Celestial Reference Frame
217
+ - 'ecliptic': Ecliptic coordinate system (J2000.0)
218
+
219
+ Returns
220
+ -------
221
+ position : ndarray, shape (3,)
222
+ Sun position in AU
223
+ velocity : ndarray, shape (3,)
224
+ Sun velocity in AU/day
225
+
226
+ Notes
227
+ -----
228
+ The Sun's position is computed relative to the Solar System Barycenter
229
+ (SSB) in the ICRF frame.
230
+
231
+ Examples
232
+ --------
233
+ >>> eph = DEEphemeris()
234
+ >>> r, v = eph.sun_position(2451545.0)
235
+ >>> print(f"Distance: {np.linalg.norm(r):.6f} AU")
236
+
237
+ """
238
+ # Sun position relative to SSB (in km)
239
+ segment = self.kernel[0, 10]
240
+ position, velocity = segment.compute_and_differentiate(jd)
241
+
242
+ # Convert from km to AU
243
+ position = np.array(position) * AU_PER_KM
244
+ velocity = np.array(velocity) * KM_PER_DAY_TO_AU_PER_DAY
245
+
246
+ if frame == "ecliptic":
247
+ from . import reference_frames
248
+
249
+ position = reference_frames.equatorial_to_ecliptic(position, EPSILON_J2000)
250
+ velocity = reference_frames.equatorial_to_ecliptic(velocity, EPSILON_J2000)
251
+
252
+ return position, velocity
253
+
254
+ def moon_position(
255
+ self, jd: float, frame: Literal["icrf", "ecliptic", "earth_centered"] = "icrf"
256
+ ) -> Tuple[np.ndarray, np.ndarray]:
257
+ """Compute Moon position and velocity.
258
+
259
+ Parameters
260
+ ----------
261
+ jd : float
262
+ Julian Day in Terrestrial Time (TT)
263
+ frame : {'icrf', 'ecliptic', 'earth_centered'}, optional
264
+ Coordinate frame (default: 'icrf').
265
+ - 'icrf': Moon position relative to Solar System Barycenter
266
+ - 'ecliptic': Ecliptic coordinates
267
+ - 'earth_centered': Position relative to Earth
268
+
269
+ Returns
270
+ -------
271
+ position : ndarray, shape (3,)
272
+ Moon position in AU (or relative to Earth for 'earth_centered')
273
+ velocity : ndarray, shape (3,)
274
+ Moon velocity in AU/day
275
+
276
+ Notes
277
+ -----
278
+ By default, returns Moon position relative to the Solar System Barycenter.
279
+ Use frame='earth_centered' for geocentric coordinates.
280
+
281
+ Examples
282
+ --------
283
+ >>> eph = DEEphemeris()
284
+ >>> r, v = eph.moon_position(2451545.0, frame='earth_centered')
285
+
286
+ """
287
+ if frame == "earth_centered":
288
+ # Moon relative to Earth
289
+ segment = self.kernel[3, 301]
290
+ position, velocity = segment.compute_and_differentiate(jd)
291
+ else:
292
+ # Moon relative to SSB: need to compute Earth->Moon, then add Earth->SSB
293
+ # Get Earth barycenter position
294
+ earth_segment = self.kernel[0, 3]
295
+ earth_pos, earth_vel = earth_segment.compute_and_differentiate(jd)
296
+
297
+ # Get Moon position relative to Earth
298
+ moon_segment = self.kernel[3, 301]
299
+ moon_rel_earth_pos, moon_rel_earth_vel = (
300
+ moon_segment.compute_and_differentiate(jd)
301
+ )
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[
322
+ "mercury", "venus", "mars", "jupiter", "saturn", "uranus", "neptune"
323
+ ],
324
+ jd: float,
325
+ frame: Literal["icrf", "ecliptic"] = "icrf",
326
+ ) -> Tuple[np.ndarray, np.ndarray]:
327
+ """Compute planet position and velocity.
328
+
329
+ Parameters
330
+ ----------
331
+ planet : str
332
+ Planet name: 'mercury', 'venus', 'mars', 'jupiter', 'saturn',
333
+ 'uranus', 'neptune'
334
+ jd : float
335
+ Julian Day in Terrestrial Time (TT)
336
+ frame : {'icrf', 'ecliptic'}, optional
337
+ Coordinate frame (default: 'icrf')
338
+
339
+ Returns
340
+ -------
341
+ position : ndarray, shape (3,)
342
+ Planet position in AU
343
+ velocity : ndarray, shape (3,)
344
+ Planet velocity in AU/day
345
+
346
+ Raises
347
+ ------
348
+ ValueError
349
+ If planet name is not recognized
350
+
351
+ Examples
352
+ --------
353
+ >>> eph = DEEphemeris()
354
+ >>> r, v = eph.planet_position('mars', 2451545.0)
355
+
356
+ """
357
+ planet_lower = planet.lower()
358
+ if planet_lower not in self._BODY_IDS or planet_lower == "sun":
359
+ raise ValueError(
360
+ f"Planet must be one of {set(self._BODY_IDS.keys()) - {'sun', 'moon'}}, "
361
+ f"got '{planet}'"
362
+ )
363
+
364
+ planet_id = self._BODY_IDS[planet_lower]
365
+ segment = self.kernel[0, planet_id]
366
+ position, velocity = segment.compute_and_differentiate(jd)
367
+
368
+ # Convert from km to AU
369
+ position = np.array(position) * AU_PER_KM
370
+ velocity = np.array(velocity) * KM_PER_DAY_TO_AU_PER_DAY
371
+
372
+ if frame == "ecliptic":
373
+ from . import reference_frames
374
+
375
+ position = reference_frames.equatorial_to_ecliptic(position, EPSILON_J2000)
376
+ velocity = reference_frames.equatorial_to_ecliptic(velocity, EPSILON_J2000)
377
+
378
+ return position, velocity
379
+
380
+ def barycenter_position(
381
+ self, body: str, jd: float
382
+ ) -> Tuple[np.ndarray, np.ndarray]:
383
+ """Compute position of any body relative to Solar System Barycenter.
384
+
385
+ Parameters
386
+ ----------
387
+ body : str
388
+ Body name ('sun', 'moon', 'mercury', ..., 'neptune')
389
+ jd : float
390
+ Julian Day in Terrestrial Time (TT)
391
+
392
+ Returns
393
+ -------
394
+ position : ndarray, shape (3,)
395
+ Position in AU
396
+ velocity : ndarray, shape (3,)
397
+ Velocity in AU/day
398
+
399
+ """
400
+ if body.lower() == "sun":
401
+ return self.sun_position(jd)
402
+ elif body.lower() == "moon":
403
+ return self.moon_position(jd, frame="icrf")
404
+ else:
405
+ return self.planet_position(body, jd)
406
+
407
+ def clear_cache(self) -> None:
408
+ """Clear internal position cache."""
409
+ self._cache.clear()
410
+
411
+
412
+ # Module-level convenience functions
413
+
414
+ _default_eph: Optional[DEEphemeris] = None
415
+
416
+
417
+ def _get_default_ephemeris() -> DEEphemeris:
418
+ """Get or create default ephemeris instance."""
419
+ global _default_eph
420
+ if _default_eph is None:
421
+ _default_eph = DEEphemeris(version="DE440")
422
+ return _default_eph
423
+
424
+
425
+ def sun_position(
426
+ jd: float, frame: Literal["icrf", "ecliptic"] = "icrf"
427
+ ) -> Tuple[np.ndarray, np.ndarray]:
428
+ """Convenience function: Compute Sun position and velocity.
429
+
430
+ Parameters
431
+ ----------
432
+ jd : float
433
+ Julian Day in Terrestrial Time (TT)
434
+ frame : {'icrf', 'ecliptic'}, optional
435
+ Coordinate frame (default: 'icrf')
436
+
437
+ Returns
438
+ -------
439
+ position : ndarray, shape (3,)
440
+ Sun position in AU
441
+ velocity : ndarray, shape (3,)
442
+ Sun velocity in AU/day
443
+
444
+ See Also
445
+ --------
446
+ DEEphemeris.sun_position : Full ephemeris class with caching
447
+
448
+ """
449
+ return _get_default_ephemeris().sun_position(jd, frame=frame)
450
+
451
+
452
+ def moon_position(
453
+ jd: float, frame: Literal["icrf", "ecliptic", "earth_centered"] = "icrf"
454
+ ) -> Tuple[np.ndarray, np.ndarray]:
455
+ """Convenience function: Compute Moon position and velocity.
456
+
457
+ Parameters
458
+ ----------
459
+ jd : float
460
+ Julian Day in Terrestrial Time (TT)
461
+ frame : {'icrf', 'ecliptic', 'earth_centered'}, optional
462
+ Coordinate frame (default: 'icrf')
463
+
464
+ Returns
465
+ -------
466
+ position : ndarray, shape (3,)
467
+ Moon position in AU
468
+ velocity : ndarray, shape (3,)
469
+ Moon velocity in AU/day
470
+
471
+ See Also
472
+ --------
473
+ DEEphemeris.moon_position : Full ephemeris class with caching
474
+
475
+ """
476
+ return _get_default_ephemeris().moon_position(jd, frame=frame)
477
+
478
+
479
+ def planet_position(
480
+ planet: Literal[
481
+ "mercury", "venus", "mars", "jupiter", "saturn", "uranus", "neptune"
482
+ ],
483
+ jd: float,
484
+ frame: Literal["icrf", "ecliptic"] = "icrf",
485
+ ) -> Tuple[np.ndarray, np.ndarray]:
486
+ """Convenience function: Compute planet position and velocity.
487
+
488
+ Parameters
489
+ ----------
490
+ planet : str
491
+ Planet name
492
+ jd : float
493
+ Julian Day in Terrestrial Time (TT)
494
+ frame : {'icrf', 'ecliptic'}, optional
495
+ Coordinate frame (default: 'icrf')
496
+
497
+ Returns
498
+ -------
499
+ position : ndarray, shape (3,)
500
+ Planet position in AU
501
+ velocity : ndarray, shape (3,)
502
+ Planet velocity in AU/day
503
+
504
+ See Also
505
+ --------
506
+ DEEphemeris.planet_position : Full ephemeris class with caching
507
+
508
+ """
509
+ return _get_default_ephemeris().planet_position(planet, jd, frame=frame)
510
+
511
+
512
+ def barycenter_position(body: str, jd: float) -> Tuple[np.ndarray, np.ndarray]:
513
+ """Convenience function: Position relative to Solar System Barycenter.
514
+
515
+ Parameters
516
+ ----------
517
+ body : str
518
+ Body name ('sun', 'moon', 'mercury', ..., 'neptune')
519
+ jd : float
520
+ Julian Day in Terrestrial Time (TT)
521
+
522
+ Returns
523
+ -------
524
+ position : ndarray, shape (3,)
525
+ Position in AU
526
+ velocity : ndarray, shape (3,)
527
+ Velocity in AU/day
528
+
529
+ """
530
+ return _get_default_ephemeris().barycenter_position(body, jd)