nrl-tracker 1.1.2__py3-none-any.whl → 1.2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nrl-tracker
3
- Version: 1.1.2
3
+ Version: 1.2.0
4
4
  Summary: Python port of the U.S. Naval Research Laboratory's Tracker Component Library for target tracking algorithms
5
5
  Author: Original: David F. Crouse, Naval Research Laboratory
6
6
  Maintainer: Python Port Contributors
@@ -1,4 +1,4 @@
1
- pytcl/__init__.py,sha256=40U6PSzLszzIyPQGdJbW3g2kGOAoAW4qzHukjQEcv78,1893
1
+ pytcl/__init__.py,sha256=LFQ_O4Uxr3PayIyh__HjIycG7ZwPGmfGELKpDmx4lfc,1893
2
2
  pytcl/logging_config.py,sha256=j7Zrkal5LwUIos-_Dm3cGKUR-jMkFdSZZikJTtzTeoE,8883
3
3
  pytcl/assignment_algorithms/__init__.py,sha256=f9V-TkEVmiKYYyth4PTpDfJvA7yYV_ys6Zix-QwWIYY,2136
4
4
  pytcl/assignment_algorithms/data_association.py,sha256=tsRxWJZk9aAPmE99BKXGouEpFfZrjPjb4HXvgxFUHhU,11405
@@ -13,7 +13,7 @@ pytcl/astronomical/__init__.py,sha256=Dtf6hqXyKyFL5VP-sqI7m2QGK6l-rqRGxVIhgDuYHO
13
13
  pytcl/astronomical/ephemerides.py,sha256=x2500S0rF1D2h0dMR_2BnZaChbBZTooHLdrevttxlAc,16471
14
14
  pytcl/astronomical/lambert.py,sha256=Lc8FT1JmpI9WSXsG2s5vIRkSoBSV7r5hd3o2bGh2Ojo,15607
15
15
  pytcl/astronomical/orbital_mechanics.py,sha256=8GssRanwTowCl6PJYqmB_SDnNznLUq5gkPa3j6iEo3U,19965
16
- pytcl/astronomical/reference_frames.py,sha256=GDak7af6BqOwGnCUxkvFoeqd_H2TMubdjG9lGPCoUB4,15799
16
+ pytcl/astronomical/reference_frames.py,sha256=gkbQrhHY_EN0xt8i7m0dIJp67GgpEcDArY7J3YH-Mb8,17981
17
17
  pytcl/astronomical/relativity.py,sha256=YPsXLD-VRh-nqs1laC-wKpRO00fflm4GkyLhojPydbo,15441
18
18
  pytcl/astronomical/time_systems.py,sha256=Jg0Zaq60hc4Ts1aQtb5bK4KSZhz-uQse8gYC89Y0-TA,15243
19
19
  pytcl/atmosphere/__init__.py,sha256=TTVz4hAM48Xd3jr6GKrR2GAABpx2z0aWvtzb9uIQiHk,737
@@ -23,14 +23,15 @@ pytcl/clustering/dbscan.py,sha256=PS6QlOwHFerbZNEb3zcNhN4oNQpgOOw5y0WskQzyKIo,73
23
23
  pytcl/clustering/gaussian_mixture.py,sha256=U5U0Z46tZWdTLNdNNNJenoeviwZRAOvexVFYVLt4QMc,22865
24
24
  pytcl/clustering/hierarchical.py,sha256=Hw9BFCn5df_ATpJX63R3B31MHz27ztCw9ihMDIlI688,14202
25
25
  pytcl/clustering/kmeans.py,sha256=250FQyDol5S_Y4TznNn9cEuE96UDp7wvEkPZJ1DLul8,10697
26
- pytcl/containers/__init__.py,sha256=-hnqSMKlMugj2RRssx3p_48HWnfqLSrF6BCChsinCOg,1627
26
+ pytcl/containers/__init__.py,sha256=jZAZb0VUft5gjQghfg2S9PD-LsA5xgtXkc0mAS_Gnmk,2428
27
+ pytcl/containers/base.py,sha256=h3h5mJfSn8yTy1waFZRUxJQ9eHQ5npbimadFbFZuuV4,5520
27
28
  pytcl/containers/cluster_set.py,sha256=y36D5TNzvCN6xjg6taP2SD_MC-O5iLq9ncBlHsQ5IBs,22723
28
- pytcl/containers/covertree.py,sha256=1JWqXxoUFLxuMnjwj2qf0iz2uPzdujQYdwJW3l5qsOs,13282
29
- pytcl/containers/kd_tree.py,sha256=pxRC62RYkqz9zXPz6c1fubmtPPBDLYA5I9AXMAoGanw,16348
29
+ pytcl/containers/covertree.py,sha256=ePIqH1-0CxSFqCwmQ_G6MXPlXs4xH0gsmoZXF8QxhDk,13271
30
+ pytcl/containers/kd_tree.py,sha256=9CKHAzid0DZ879hut8M4dyW_976pIWNLX3uWzELPIu4,18563
30
31
  pytcl/containers/measurement_set.py,sha256=87AbdoZIUspn1yJsiMpyQ5LoEVcerUnXefXGGPtFTJg,12654
31
32
  pytcl/containers/rtree.py,sha256=gv2EztvPnaAXEa6OoFnOYBY1MfTwjNMYh_BCiIomHJk,15450
32
33
  pytcl/containers/track_list.py,sha256=6q9Qgcwm-8H_JqtOCsMssF27av4XaSkhfDl-MWb1ABc,12520
33
- pytcl/containers/vptree.py,sha256=6fBNHrezkmj7L2nH0-2bONRN92f5cZAhS-5vaI1JZnA,8814
34
+ pytcl/containers/vptree.py,sha256=4tUq0ktafusU1PILZkQxi27CZryKlsHtFbym-vZYQWk,8747
34
35
  pytcl/coordinate_systems/__init__.py,sha256=jwYhu_-9AvOeP9WLG9PYtyDwfe0GjxNZ9-xCqiLymW4,3909
35
36
  pytcl/coordinate_systems/conversions/__init__.py,sha256=PkNevB78vBw0BkalydJBbQO91AyiMJxKRrgJNt4HsYc,1100
36
37
  pytcl/coordinate_systems/conversions/geodetic.py,sha256=qQSnJRt3jg5KiostvzyslPIbfn-1xBluo1r12oavWTQ,15737
@@ -41,10 +42,10 @@ pytcl/coordinate_systems/projections/__init__.py,sha256=eWNtezPO62IUWxv7jymenIXs
41
42
  pytcl/coordinate_systems/projections/projections.py,sha256=yODS7n1gA4jsCJcU8EaeclHrbUBsZI9O2M_XJs2HOXs,33169
42
43
  pytcl/coordinate_systems/rotations/__init__.py,sha256=nqAz4iJd2hEOX_r7Tz4cE524sShyxdbtcQ5m56RrDLg,1047
43
44
  pytcl/coordinate_systems/rotations/rotations.py,sha256=FAYHkShQcpOlWJjtvLfNvtCx-a56pr-cbpo0QjC5W9U,18227
44
- pytcl/core/__init__.py,sha256=H5JJPS-43DfF1UG7fSgV-VMTcZFBO8GuzDW1lM_1sm4,1152
45
+ pytcl/core/__init__.py,sha256=3GFQX_Q9f7fhmWlA6OQiS6OpM7HWhyT9iQhB8Mhi_kk,1580
45
46
  pytcl/core/array_utils.py,sha256=SsgEiAoRCWxAVKq1aa5-nPdOi-2AB6XNObu0IaGClUk,13983
46
47
  pytcl/core/constants.py,sha256=lZVDK5zsSR02_4b2Nqx9KDtZT9QaYhkZ9wuoODbifd4,8693
47
- pytcl/core/validation.py,sha256=WRlzMlUihtqc3XZoWOTFK0sBAZVDIwTMGCiWcX5OZVY,13093
48
+ pytcl/core/validation.py,sha256=nUmG8UmUk85dQ4CxJRipACb8zjsrAJPiyK8ADZN_KvU,23426
48
49
  pytcl/dynamic_estimation/__init__.py,sha256=jA5FF6kHYklY5LMOfZaKcCeiPTpVe8vHIMp3ECDOmsc,4582
49
50
  pytcl/dynamic_estimation/imm.py,sha256=IbKmouUiyzaYJbhWty63r3n_xV8thD-wd0qgZP1SxOI,22067
50
51
  pytcl/dynamic_estimation/information_filter.py,sha256=x7iQwO_iJT1dCSvDws5LqD3yAtjw9QVGUfMPcXn1IA4,17349
@@ -71,9 +72,9 @@ pytcl/dynamic_models/process_noise/polynomial.py,sha256=natfpsdN3qM9VzPeXF_nBpsb
71
72
  pytcl/dynamic_models/process_noise/singer.py,sha256=lsJDT6xOvcS_qQKFtgHX0L7Ukpy4D7HgvPT8Q3I0ibU,3901
72
73
  pytcl/gravity/__init__.py,sha256=5xNdQSrrkt7-1-JPOYqR38CqvNJ7qKlPyMK36DGm6-I,3693
73
74
  pytcl/gravity/clenshaw.py,sha256=1BdxzU8IfGGd68H_U35soIJkiOHphY35e9mLElhPTOg,15364
74
- pytcl/gravity/egm.py,sha256=QTRuvCiMjuNQdZF163OGwjxuivpGu2dB6E0zQLbKPP8,18083
75
+ pytcl/gravity/egm.py,sha256=47I8nyXNhXUKPkufXahs4JGsBcqhM-9z2xGz0X4JPmU,18422
75
76
  pytcl/gravity/models.py,sha256=rdY3Do4M1eRFO74gu3xy-bBn7tox3zM49wYbfnsIQWw,11159
76
- pytcl/gravity/spherical_harmonics.py,sha256=uZasz-w2K16sWT6xrNIPyTEP6MSlMQSe_BCWpXhRkWY,14722
77
+ pytcl/gravity/spherical_harmonics.py,sha256=IpBh0LW4BQMzJck9Li6yveGlvYigCuXaoApRWDPsWtc,16498
77
78
  pytcl/gravity/tides.py,sha256=hef_BGewFGD7dJwg0t09Z6tfWLco_avATLuu66rnTpI,27733
78
79
  pytcl/magnetism/__init__.py,sha256=hE2BvberFSmimYuuwCYJ0g7ByxJAdj844vZJNkEotws,2502
79
80
  pytcl/magnetism/emm.py,sha256=5Jwl99wvdKYtx1-3LBB7x-w5KT-fqLiRg7uBW0Ai_Gw,22292
@@ -103,7 +104,7 @@ pytcl/mathematical_functions/special_functions/debye.py,sha256=Nchjwkl1vzSL1L7nQ
103
104
  pytcl/mathematical_functions/special_functions/elliptic.py,sha256=WyzBkrfZufIR5dUmCKGcxp6KNpVDrU89NGLDyRrZOqQ,7418
104
105
  pytcl/mathematical_functions/special_functions/error_functions.py,sha256=a3SS8FYAMRv1KdCmebOZL95yjvVt9gZRF2XOjHvQ9M8,6253
105
106
  pytcl/mathematical_functions/special_functions/gamma_functions.py,sha256=xXN_9SCokH10HjE8PpaPKHYVK_RZRHRAbZgR2mZYIAA,10191
106
- pytcl/mathematical_functions/special_functions/hypergeometric.py,sha256=gKn_tXboEst7pVDiW15IbKFAANM4XVqKtDc1dmWL-2A,9768
107
+ pytcl/mathematical_functions/special_functions/hypergeometric.py,sha256=5C4dXv3XxjyeGGVH-0i22NSLbAqkcPTR3kZ2J_GTles,11364
107
108
  pytcl/mathematical_functions/special_functions/lambert_w.py,sha256=ivRc4KH5Lwoxb_yijrJEwG0ITa0hhcYF7_gCfVBBNW4,6855
108
109
  pytcl/mathematical_functions/special_functions/marcum_q.py,sha256=OZ5QjIB1e_XvRG8A-3dbZ13YXHtdk2EYVEPaqtgVr14,9580
109
110
  pytcl/mathematical_functions/statistics/__init__.py,sha256=dfypStgmnFmOrnWcm-3CEvLinONHraFgx9O66_37bqw,1278
@@ -115,8 +116,8 @@ pytcl/mathematical_functions/transforms/stft.py,sha256=zQapXl-v69_RDPwMqci83jah1
115
116
  pytcl/mathematical_functions/transforms/wavelets.py,sha256=dm273Z_t13BlEVSlHTaGE7jR1ocugL7lEkcO499U7bY,21656
116
117
  pytcl/misc/__init__.py,sha256=SCHf_lQVfdl2gwUluHBiIloTF8HRH8EkgYfbNr7zOug,33
117
118
  pytcl/navigation/__init__.py,sha256=k1_x_FnnPrIzGeNu7zejPtPubIhweBgCfwqlZJEMw0I,6042
118
- pytcl/navigation/geodesy.py,sha256=M9XXfBTMCRdaWMV2-ViDSTEt94WZnMtxMeJQ1FAgQHY,17227
119
- pytcl/navigation/great_circle.py,sha256=TtlkWZbzr-HzSt4ultG_h137ZnX0pJZx_87kr3uvpjI,20923
119
+ pytcl/navigation/geodesy.py,sha256=KXRQqOrJGbxWQ4yrZI9jfMDvaoUx_4RL8mTWUHhBJX0,19694
120
+ pytcl/navigation/great_circle.py,sha256=y5YHBie21j1a1ac7OziEgg_jgUT1FpltgkTA5AGFGsk,23251
120
121
  pytcl/navigation/ins.py,sha256=OIi8_RjrgEYl0MFpJEFMjIlpgX8DYGTEhdLEvqG-ABU,31151
121
122
  pytcl/navigation/ins_gnss.py,sha256=euKF5JGgwmVBsw3jBf7_wa2z1BpZeVbSNmBuwzhGS6c,30157
122
123
  pytcl/navigation/rhumb.py,sha256=lr1c3iEXfoOSfIyyXSRWv6He5TlaxEHbJy-dhqM1gRw,18224
@@ -144,8 +145,8 @@ pytcl/trackers/mht.py,sha256=7mwhMmja3ri2wnx7W1wueDGn2r3ArwAxJDPUJ7IZAkQ,20617
144
145
  pytcl/trackers/multi_target.py,sha256=hvt89ERhMwpcHcIJeKHnkQSKdE3_LoRiX-gbaGoo300,10516
145
146
  pytcl/trackers/single_target.py,sha256=Yy3FwaNTArMWcaod-0HVeiioNV4xLWxNDn_7ZPVqQYs,6562
146
147
  pytcl/transponders/__init__.py,sha256=5fL4u3lKCYgPLo5uFeuZbtRZkJPABntuKYGUvVgMMEI,41
147
- nrl_tracker-1.1.2.dist-info/LICENSE,sha256=rB5G4WppIIUzMOYr2N6uyYlNJ00hRJqE5tie6BMvYuE,1612
148
- nrl_tracker-1.1.2.dist-info/METADATA,sha256=ILwTHebbvAlAhKGX-O-cWS7rjLiRCaDrkd2fRbU22BE,10145
149
- nrl_tracker-1.1.2.dist-info/WHEEL,sha256=pL8R0wFFS65tNSRnaOVrsw9EOkOqxLrlUPenUYnJKNo,91
150
- nrl_tracker-1.1.2.dist-info/top_level.txt,sha256=17megxcrTPBWwPZTh6jTkwTKxX7No-ZqRpyvElnnO-s,6
151
- nrl_tracker-1.1.2.dist-info/RECORD,,
148
+ nrl_tracker-1.2.0.dist-info/LICENSE,sha256=rB5G4WppIIUzMOYr2N6uyYlNJ00hRJqE5tie6BMvYuE,1612
149
+ nrl_tracker-1.2.0.dist-info/METADATA,sha256=TV1ddsQ8eyXE0y4Y6O5AuUR6glbTc8pkBZ7g2J10uCA,10145
150
+ nrl_tracker-1.2.0.dist-info/WHEEL,sha256=pL8R0wFFS65tNSRnaOVrsw9EOkOqxLrlUPenUYnJKNo,91
151
+ nrl_tracker-1.2.0.dist-info/top_level.txt,sha256=17megxcrTPBWwPZTh6jTkwTKxX7No-ZqRpyvElnnO-s,6
152
+ nrl_tracker-1.2.0.dist-info/RECORD,,
pytcl/__init__.py CHANGED
@@ -20,7 +20,7 @@ References
20
20
  no. 5, pp. 18-27, May 2017.
21
21
  """
22
22
 
23
- __version__ = "1.1.2"
23
+ __version__ = "1.2.0"
24
24
  __author__ = "Python Port Contributors"
25
25
  __original_author__ = "David F. Crouse, Naval Research Laboratory"
26
26
 
@@ -21,6 +21,8 @@ References
21
21
  A&A, 2003.
22
22
  """
23
23
 
24
+ import logging
25
+ from functools import lru_cache
24
26
  from typing import Tuple
25
27
 
26
28
  import numpy as np
@@ -28,6 +30,22 @@ from numpy.typing import NDArray
28
30
 
29
31
  from pytcl.astronomical.time_systems import JD_J2000
30
32
 
33
+ # Module logger
34
+ _logger = logging.getLogger("pytcl.astronomical.reference_frames")
35
+
36
+ # Cache configuration
37
+ _CACHE_JD_DECIMALS = 6 # ~86ms precision for JD quantization
38
+ _CACHE_MAXSIZE = 128 # Max cached epochs
39
+
40
+
41
+ def _quantize_jd(jd: float) -> float:
42
+ """Quantize Julian date for cache key compatibility.
43
+
44
+ Rounds to _CACHE_JD_DECIMALS decimal places (~86ms precision).
45
+ This enables cache hits for nearly identical epochs.
46
+ """
47
+ return round(jd, _CACHE_JD_DECIMALS)
48
+
31
49
 
32
50
  def julian_centuries_j2000(jd: float) -> float:
33
51
  """
@@ -78,6 +96,37 @@ def precession_angles_iau76(T: float) -> Tuple[float, float, float]:
78
96
  )
79
97
 
80
98
 
99
+ @lru_cache(maxsize=_CACHE_MAXSIZE)
100
+ def _precession_matrix_cached(jd_quantized: float) -> tuple:
101
+ """Cached precession matrix computation (internal).
102
+
103
+ Returns tuple of tuples for hashability.
104
+ """
105
+ T = julian_centuries_j2000(jd_quantized)
106
+ zeta, theta, z = precession_angles_iau76(T)
107
+
108
+ cos_zeta = np.cos(zeta)
109
+ sin_zeta = np.sin(zeta)
110
+ cos_theta = np.cos(theta)
111
+ sin_theta = np.sin(theta)
112
+ cos_z = np.cos(z)
113
+ sin_z = np.sin(z)
114
+
115
+ return (
116
+ (
117
+ cos_zeta * cos_theta * cos_z - sin_zeta * sin_z,
118
+ -sin_zeta * cos_theta * cos_z - cos_zeta * sin_z,
119
+ -sin_theta * cos_z,
120
+ ),
121
+ (
122
+ cos_zeta * cos_theta * sin_z + sin_zeta * cos_z,
123
+ -sin_zeta * cos_theta * sin_z + cos_zeta * cos_z,
124
+ -sin_theta * sin_z,
125
+ ),
126
+ (cos_zeta * sin_theta, -sin_zeta * sin_theta, cos_theta),
127
+ )
128
+
129
+
81
130
  def precession_matrix_iau76(jd: float) -> NDArray[np.floating]:
82
131
  """
83
132
  Compute IAU 1976 precession matrix from J2000 to date.
@@ -92,34 +141,15 @@ def precession_matrix_iau76(jd: float) -> NDArray[np.floating]:
92
141
  P : ndarray
93
142
  Precession rotation matrix (3x3).
94
143
  Transforms from J2000 (GCRF) to mean of date.
95
- """
96
- T = julian_centuries_j2000(jd)
97
- zeta, theta, z = precession_angles_iau76(T)
98
144
 
99
- cos_zeta = np.cos(zeta)
100
- sin_zeta = np.sin(zeta)
101
- cos_theta = np.cos(theta)
102
- sin_theta = np.sin(theta)
103
- cos_z = np.cos(z)
104
- sin_z = np.sin(z)
105
-
106
- P = np.array(
107
- [
108
- [
109
- cos_zeta * cos_theta * cos_z - sin_zeta * sin_z,
110
- -sin_zeta * cos_theta * cos_z - cos_zeta * sin_z,
111
- -sin_theta * cos_z,
112
- ],
113
- [
114
- cos_zeta * cos_theta * sin_z + sin_zeta * cos_z,
115
- -sin_zeta * cos_theta * sin_z + cos_zeta * cos_z,
116
- -sin_theta * sin_z,
117
- ],
118
- [cos_zeta * sin_theta, -sin_zeta * sin_theta, cos_theta],
119
- ]
120
- )
121
-
122
- return P
145
+ Notes
146
+ -----
147
+ Results are cached for repeated queries at the same epoch.
148
+ Cache key is quantized to ~86ms precision.
149
+ """
150
+ jd_q = _quantize_jd(jd)
151
+ cached = _precession_matrix_cached(jd_q)
152
+ return np.array(cached)
123
153
 
124
154
 
125
155
  def nutation_angles_iau80(jd: float) -> Tuple[float, float]:
@@ -203,6 +233,38 @@ def mean_obliquity_iau80(jd: float) -> float:
203
233
  return eps0_arcsec * np.pi / (180 * 3600)
204
234
 
205
235
 
236
+ @lru_cache(maxsize=_CACHE_MAXSIZE)
237
+ def _nutation_matrix_cached(jd_quantized: float) -> tuple:
238
+ """Cached nutation matrix computation (internal).
239
+
240
+ Returns tuple of tuples for hashability.
241
+ """
242
+ dpsi, deps = nutation_angles_iau80(jd_quantized)
243
+ eps0 = mean_obliquity_iau80(jd_quantized)
244
+ eps = eps0 + deps
245
+
246
+ cos_eps0 = np.cos(eps0)
247
+ sin_eps0 = np.sin(eps0)
248
+ cos_eps = np.cos(eps)
249
+ sin_eps = np.sin(eps)
250
+ cos_dpsi = np.cos(dpsi)
251
+ sin_dpsi = np.sin(dpsi)
252
+
253
+ return (
254
+ (cos_dpsi, -sin_dpsi * cos_eps0, -sin_dpsi * sin_eps0),
255
+ (
256
+ sin_dpsi * cos_eps,
257
+ cos_dpsi * cos_eps0 * cos_eps + sin_eps0 * sin_eps,
258
+ cos_dpsi * sin_eps0 * cos_eps - cos_eps0 * sin_eps,
259
+ ),
260
+ (
261
+ sin_dpsi * sin_eps,
262
+ cos_dpsi * cos_eps0 * sin_eps - sin_eps0 * cos_eps,
263
+ cos_dpsi * sin_eps0 * sin_eps + cos_eps0 * cos_eps,
264
+ ),
265
+ )
266
+
267
+
206
268
  def nutation_matrix(jd: float) -> NDArray[np.floating]:
207
269
  """
208
270
  Compute nutation matrix.
@@ -217,35 +279,15 @@ def nutation_matrix(jd: float) -> NDArray[np.floating]:
217
279
  N : ndarray
218
280
  Nutation rotation matrix (3x3).
219
281
  Transforms from mean of date to true of date.
220
- """
221
- dpsi, deps = nutation_angles_iau80(jd)
222
- eps0 = mean_obliquity_iau80(jd)
223
- eps = eps0 + deps
224
282
 
225
- cos_eps0 = np.cos(eps0)
226
- sin_eps0 = np.sin(eps0)
227
- cos_eps = np.cos(eps)
228
- sin_eps = np.sin(eps)
229
- cos_dpsi = np.cos(dpsi)
230
- sin_dpsi = np.sin(dpsi)
231
-
232
- N = np.array(
233
- [
234
- [cos_dpsi, -sin_dpsi * cos_eps0, -sin_dpsi * sin_eps0],
235
- [
236
- sin_dpsi * cos_eps,
237
- cos_dpsi * cos_eps0 * cos_eps + sin_eps0 * sin_eps,
238
- cos_dpsi * sin_eps0 * cos_eps - cos_eps0 * sin_eps,
239
- ],
240
- [
241
- sin_dpsi * sin_eps,
242
- cos_dpsi * cos_eps0 * sin_eps - sin_eps0 * cos_eps,
243
- cos_dpsi * sin_eps0 * sin_eps + cos_eps0 * cos_eps,
244
- ],
245
- ]
246
- )
247
-
248
- return N
283
+ Notes
284
+ -----
285
+ Results are cached for repeated queries at the same epoch.
286
+ Cache key is quantized to ~86ms precision.
287
+ """
288
+ jd_q = _quantize_jd(jd)
289
+ cached = _nutation_matrix_cached(jd_q)
290
+ return np.array(cached)
249
291
 
250
292
 
251
293
  def earth_rotation_angle(jd_ut1: float) -> float:
@@ -647,6 +689,33 @@ def equatorial_to_ecliptic(
647
689
  return R @ r_eq
648
690
 
649
691
 
692
+ def clear_transformation_cache() -> None:
693
+ """Clear cached transformation matrices.
694
+
695
+ Call this function to clear all cached precession and nutation
696
+ matrices. Useful when memory is constrained or after processing
697
+ a batch of observations at different epochs.
698
+ """
699
+ _precession_matrix_cached.cache_clear()
700
+ _nutation_matrix_cached.cache_clear()
701
+ _logger.debug("Transformation matrix cache cleared")
702
+
703
+
704
+ def get_cache_info() -> dict:
705
+ """Get cache statistics for transformation matrices.
706
+
707
+ Returns
708
+ -------
709
+ dict
710
+ Dictionary with 'precession' and 'nutation' keys, each containing
711
+ CacheInfo namedtuple with hits, misses, maxsize, currsize.
712
+ """
713
+ return {
714
+ "precession": _precession_matrix_cached.cache_info(),
715
+ "nutation": _nutation_matrix_cached.cache_info(),
716
+ }
717
+
718
+
650
719
  __all__ = [
651
720
  # Time utilities
652
721
  "julian_centuries_j2000",
@@ -674,4 +743,7 @@ __all__ = [
674
743
  # Ecliptic/equatorial
675
744
  "ecliptic_to_equatorial",
676
745
  "equatorial_to_ecliptic",
746
+ # Cache management
747
+ "clear_transformation_cache",
748
+ "get_cache_info",
677
749
  ]
@@ -3,8 +3,27 @@ Containers module.
3
3
 
4
4
  This module provides spatial data structures for efficient
5
5
  nearest neighbor queries, spatial indexing, and tracking containers.
6
+
7
+ Spatial Index Hierarchy
8
+ -----------------------
9
+ All spatial index structures inherit from BaseSpatialIndex which defines
10
+ a common interface for k-nearest neighbor and radius queries:
11
+
12
+ BaseSpatialIndex (abstract)
13
+ ├── KDTree - K-dimensional tree (Euclidean space)
14
+ ├── BallTree - Ball tree variant of KD-tree
15
+ ├── RTree - Rectangle tree for bounding boxes
16
+ └── MetricSpatialIndex (abstract)
17
+ ├── VPTree - Vantage point tree (any metric)
18
+ └── CoverTree - Cover tree (any metric)
6
19
  """
7
20
 
21
+ from pytcl.containers.base import (
22
+ BaseSpatialIndex,
23
+ MetricSpatialIndex,
24
+ SpatialQueryResult,
25
+ validate_query_input,
26
+ )
8
27
  from pytcl.containers.cluster_set import (
9
28
  ClusterSet,
10
29
  ClusterStats,
@@ -33,6 +52,11 @@ from pytcl.containers.track_list import TrackList, TrackListStats, TrackQuery
33
52
  from pytcl.containers.vptree import VPNode, VPTree, VPTreeResult
34
53
 
35
54
  __all__ = [
55
+ # Base classes
56
+ "BaseSpatialIndex",
57
+ "MetricSpatialIndex",
58
+ "SpatialQueryResult",
59
+ "validate_query_input",
36
60
  # K-D Tree
37
61
  "KDNode",
38
62
  "NearestNeighborResult",
@@ -0,0 +1,219 @@
1
+ """
2
+ Base classes for spatial data structures.
3
+
4
+ This module provides abstract base classes that define the common interface
5
+ for spatial indexing data structures like KD-trees, VP-trees, R-trees, and
6
+ Cover trees.
7
+ """
8
+
9
+ import logging
10
+ from abc import ABC, abstractmethod
11
+ from typing import Callable, List, NamedTuple, Optional
12
+
13
+ import numpy as np
14
+ from numpy.typing import ArrayLike, NDArray
15
+
16
+ # Module logger
17
+ _logger = logging.getLogger("pytcl.containers")
18
+
19
+
20
+ class SpatialQueryResult(NamedTuple):
21
+ """Result of a spatial query.
22
+
23
+ Attributes
24
+ ----------
25
+ indices : ndarray
26
+ Indices of matching points in the original data.
27
+ distances : ndarray
28
+ Distances to matching points.
29
+ """
30
+
31
+ indices: NDArray[np.intp]
32
+ distances: NDArray[np.floating]
33
+
34
+
35
+ class BaseSpatialIndex(ABC):
36
+ """
37
+ Abstract base class for spatial indexing data structures.
38
+
39
+ All spatial index implementations (KDTree, VPTree, RTree, CoverTree)
40
+ should inherit from this class and implement the required methods.
41
+
42
+ This provides a consistent interface for:
43
+ - Building the index from point data
44
+ - k-nearest neighbor queries
45
+ - Range/radius queries
46
+ - Dimension and size introspection
47
+
48
+ Parameters
49
+ ----------
50
+ data : array_like
51
+ Data points of shape (n_samples, n_features).
52
+
53
+ Attributes
54
+ ----------
55
+ data : ndarray
56
+ The indexed data points.
57
+ n_samples : int
58
+ Number of data points.
59
+ n_features : int
60
+ Dimensionality of data points.
61
+ """
62
+
63
+ def __init__(self, data: ArrayLike):
64
+ self.data = np.asarray(data, dtype=np.float64)
65
+
66
+ if self.data.ndim != 2:
67
+ raise ValueError(
68
+ f"Data must be 2-dimensional (n_samples, n_features), "
69
+ f"got shape {self.data.shape}"
70
+ )
71
+
72
+ self.n_samples, self.n_features = self.data.shape
73
+ _logger.debug(
74
+ "%s initialized with %d points in %d dimensions",
75
+ self.__class__.__name__,
76
+ self.n_samples,
77
+ self.n_features,
78
+ )
79
+
80
+ @abstractmethod
81
+ def query(
82
+ self,
83
+ X: ArrayLike,
84
+ k: int = 1,
85
+ ) -> SpatialQueryResult:
86
+ """
87
+ Query the index for k nearest neighbors.
88
+
89
+ Parameters
90
+ ----------
91
+ X : array_like
92
+ Query points of shape (n_queries, n_features) or (n_features,).
93
+ k : int, optional
94
+ Number of nearest neighbors to return. Default is 1.
95
+
96
+ Returns
97
+ -------
98
+ result : SpatialQueryResult
99
+ Named tuple with indices and distances of k nearest neighbors
100
+ for each query point.
101
+ """
102
+ pass
103
+
104
+ @abstractmethod
105
+ def query_radius(
106
+ self,
107
+ X: ArrayLike,
108
+ r: float,
109
+ ) -> List[List[int]]:
110
+ """
111
+ Query the index for all points within radius r.
112
+
113
+ Parameters
114
+ ----------
115
+ X : array_like
116
+ Query points of shape (n_queries, n_features) or (n_features,).
117
+ r : float
118
+ Search radius.
119
+
120
+ Returns
121
+ -------
122
+ indices : list of list of int
123
+ For each query point, a list of indices of data points
124
+ within distance r.
125
+ """
126
+ pass
127
+
128
+ def __len__(self) -> int:
129
+ """Return number of indexed points."""
130
+ return self.n_samples
131
+
132
+ def __repr__(self) -> str:
133
+ return (
134
+ f"{self.__class__.__name__}("
135
+ f"n_samples={self.n_samples}, n_features={self.n_features})"
136
+ )
137
+
138
+
139
+ class MetricSpatialIndex(BaseSpatialIndex):
140
+ """
141
+ Base class for metric space spatial indices.
142
+
143
+ Extends BaseSpatialIndex with support for custom distance metrics.
144
+ Used by VP-trees and Cover trees which can work with any metric.
145
+
146
+ Parameters
147
+ ----------
148
+ data : array_like
149
+ Data points of shape (n_samples, n_features).
150
+ metric : callable, optional
151
+ Distance function with signature metric(x, y) -> float.
152
+ Default is Euclidean distance.
153
+ """
154
+
155
+ def __init__(
156
+ self,
157
+ data: ArrayLike,
158
+ metric: Optional[Callable[[NDArray, NDArray], float]] = None,
159
+ ):
160
+ super().__init__(data)
161
+
162
+ if metric is None:
163
+ self.metric = self._euclidean_distance
164
+ else:
165
+ self.metric = metric
166
+
167
+ @staticmethod
168
+ def _euclidean_distance(x: NDArray, y: NDArray) -> float:
169
+ """Default Euclidean distance metric."""
170
+ return float(np.sqrt(np.sum((x - y) ** 2)))
171
+
172
+
173
+ def validate_query_input(
174
+ X: ArrayLike,
175
+ n_features: int,
176
+ ) -> NDArray[np.floating]:
177
+ """
178
+ Validate and reshape query input.
179
+
180
+ Parameters
181
+ ----------
182
+ X : array_like
183
+ Query points.
184
+ n_features : int
185
+ Expected number of features.
186
+
187
+ Returns
188
+ -------
189
+ X : ndarray
190
+ Validated query array of shape (n_queries, n_features).
191
+
192
+ Raises
193
+ ------
194
+ ValueError
195
+ If query has wrong number of features.
196
+ """
197
+ X = np.asarray(X, dtype=np.float64)
198
+
199
+ if X.ndim == 1:
200
+ X = X.reshape(1, -1)
201
+
202
+ if X.shape[1] != n_features:
203
+ _logger.warning(
204
+ "Query feature mismatch: got %d, expected %d", X.shape[1], n_features
205
+ )
206
+ raise ValueError(f"Query has {X.shape[1]} features, expected {n_features}")
207
+
208
+ _logger.debug(
209
+ "Validated query input: %d queries, %d features", X.shape[0], X.shape[1]
210
+ )
211
+ return X
212
+
213
+
214
+ __all__ = [
215
+ "SpatialQueryResult",
216
+ "BaseSpatialIndex",
217
+ "MetricSpatialIndex",
218
+ "validate_query_input",
219
+ ]