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,472 @@
1
+ """Relativistic corrections for precision astronomy and satellite positioning.
2
+
3
+ This module provides utilities for computing relativistic effects in orbital mechanics,
4
+ including gravitational time dilation, Shapiro delay, and coordinate transformations
5
+ in the Schwarzschild metric. These effects are critical for high-precision applications
6
+ such as GPS, pulsar timing, and celestial mechanics.
7
+
8
+ Key Physical Constants:
9
+ - Schwarzschild radius: r_s = 2GM/c^2
10
+ - Gravitational parameter for Earth: μ = GM = 3.986004418e14 m^3/s^2
11
+ - Speed of light: c = 299792458 m/s
12
+ - Gravitational constant: G = 6.67430e-11 m^3/(kg·s^2)
13
+
14
+ References:
15
+ - Soffel et al. (2003): The IAU 2000 Resolutions for Astrometry, Celestial Mechanics,
16
+ and Reference Frames
17
+ - Will, C. M. (2014): The Confrontation between General Relativity and Experiment
18
+ - Ries et al. (1992): Preliminary Analysis of LAGEOS II Observations for the
19
+ Determination of Relativistic Effects
20
+ """
21
+
22
+ import numpy as np
23
+
24
+ # Physical constants (CODATA 2018 values)
25
+ C_LIGHT = 299792458.0 # Speed of light (m/s)
26
+ G_GRAV = 6.67430e-11 # Gravitational constant (m^3/(kg·s^2))
27
+ GM_EARTH = 3.986004418e14 # Gravitational parameter for Earth (m^3/s^2)
28
+ GM_SUN = 1.32712440018e20 # Gravitational parameter for Sun (m^3/s^2)
29
+ AU = 1.495978707e11 # Astronomical unit (m)
30
+
31
+
32
+ def schwarzschild_radius(mass: float) -> float:
33
+ """Compute Schwarzschild radius for a given mass.
34
+
35
+ The Schwarzschild radius is the radius at which an object becomes a black hole.
36
+ It is given by r_s = 2GM/c^2.
37
+
38
+ Parameters
39
+ ----------
40
+ mass : float
41
+ Mass of the object (kg)
42
+
43
+ Returns
44
+ -------
45
+ float
46
+ Schwarzschild radius (m)
47
+
48
+ Examples
49
+ --------
50
+ >>> r_s_earth = schwarzschild_radius(5.972e24) # Earth's mass
51
+ >>> print(f"Earth's Schwarzschild radius: {r_s_earth:.3e} m")
52
+ Earth's Schwarzschild radius: 8.870e-03 m
53
+
54
+ >>> r_s_sun = schwarzschild_radius(1.989e30) # Sun's mass
55
+ >>> print(f"Sun's Schwarzschild radius: {r_s_sun:.3e} m")
56
+ Sun's Schwarzschild radius: 2.952e+03 m
57
+ """
58
+ return 2.0 * G_GRAV * mass / (C_LIGHT**2)
59
+
60
+
61
+ def gravitational_time_dilation(r: float, gm: float = GM_EARTH) -> float:
62
+ """Compute gravitational time dilation factor sqrt(1 - 2GM/(rc^2)).
63
+
64
+ In general relativity, time passes slower in stronger gravitational fields.
65
+ This function computes the metric coefficient g_00 for the Schwarzschild metric,
66
+ which determines proper time relative to coordinate time.
67
+
68
+ Parameters
69
+ ----------
70
+ r : float
71
+ Distance from the gravitational body (m)
72
+ gm : float, optional
73
+ Gravitational parameter GM of the body (m^3/s^2).
74
+ Default is GM_EARTH.
75
+
76
+ Returns
77
+ -------
78
+ float
79
+ Time dilation factor in [0, 1]. A value less than 1 indicates time
80
+ passes slower at radius r compared to infinity.
81
+
82
+ Raises
83
+ ------
84
+ ValueError
85
+ If r is less than or equal to Schwarzschild radius
86
+
87
+ Examples
88
+ --------
89
+ Compute time dilation at Earth's surface (6371 km):
90
+
91
+ >>> r_earth = 6.371e6 # meters
92
+ >>> dilation = gravitational_time_dilation(r_earth)
93
+ >>> print(f"Time dilation at surface: {dilation:.15f}")
94
+ Time dilation at surface: 0.999999999300693
95
+
96
+ At GPS orbital altitude (~20,200 km):
97
+
98
+ >>> r_gps = 26.56e6 # meters
99
+ >>> dilation_gps = gravitational_time_dilation(r_gps)
100
+ >>> time_shift = (1 - dilation_gps) * 86400 * 1e9 # nanoseconds per day
101
+ >>> print(f"Time shift: {time_shift:.1f} ns/day")
102
+ """
103
+ r_s = schwarzschild_radius(gm / G_GRAV)
104
+ if r <= r_s:
105
+ raise ValueError(f"Radius {r} m is at or within Schwarzschild radius {r_s} m")
106
+
107
+ dilation_squared = 1.0 - 2.0 * gm / (C_LIGHT**2 * r)
108
+ return np.sqrt(dilation_squared)
109
+
110
+
111
+ def proper_time_rate(v: float, r: float, gm: float = GM_EARTH) -> float:
112
+ """Compute proper time rate accounting for both velocity and gravity.
113
+
114
+ The proper time rate combines special relativistic time dilation from velocity
115
+ and general relativistic time dilation from the gravitational potential.
116
+
117
+ d(tau)/d(t) = sqrt(1 - v^2/c^2) * sqrt(1 - 2GM/(rc^2))
118
+
119
+ For small velocities and weak fields: 1 - v^2/(2c^2) - GM/(rc^2)
120
+
121
+ Parameters
122
+ ----------
123
+ v : float
124
+ Velocity magnitude (m/s)
125
+ r : float
126
+ Distance from gravitational body (m)
127
+ gm : float, optional
128
+ Gravitational parameter GM (m^3/s^2). Default is GM_EARTH.
129
+
130
+ Returns
131
+ -------
132
+ float
133
+ Proper time rate. A value less than 1 indicates proper time passes
134
+ slower than coordinate time.
135
+
136
+ Examples
137
+ --------
138
+ Proper time rate for a GPS satellite at ~3.87 km/s and 26.56 Mm altitude:
139
+
140
+ >>> v_gps = 3870.0 # m/s
141
+ >>> r_gps = 26.56e6 # m
142
+ >>> rate = proper_time_rate(v_gps, r_gps)
143
+ >>> print(f"Proper time rate: {rate:.15f}")
144
+ >>> time_shift = (1 - rate) * 86400 # seconds per day
145
+ >>> print(f"Daily time shift: {time_shift:.3f} s/day")
146
+ """
147
+ # Special relativistic effect
148
+ special_rel = 1.0 - (v**2) / (2.0 * C_LIGHT**2)
149
+
150
+ # General relativistic effect
151
+ general_rel = -gm / (C_LIGHT**2 * r)
152
+
153
+ return special_rel + general_rel
154
+
155
+
156
+ def shapiro_delay(
157
+ observer_pos: np.ndarray,
158
+ light_source_pos: np.ndarray,
159
+ gravitating_body_pos: np.ndarray,
160
+ gm: float = GM_SUN,
161
+ ) -> float:
162
+ """Compute Shapiro time delay for light propagation through gravitational field.
163
+
164
+ The Shapiro delay is the additional propagation time experienced by light
165
+ traveling through a gravitational field, compared to flat spacetime.
166
+
167
+ delay = (2GM/c^3) * ln((r_o + r_s + r_os) / (r_o + r_s - r_os))
168
+
169
+ where r_o is distance from body to observer, r_s is distance from body to
170
+ source, and r_os is distance from observer to source.
171
+
172
+ Parameters
173
+ ----------
174
+ observer_pos : np.ndarray
175
+ Position of observer (m), shape (3,)
176
+ light_source_pos : np.ndarray
177
+ Position of light source (m), shape (3,)
178
+ gravitating_body_pos : np.ndarray
179
+ Position of gravitating body (m), shape (3,)
180
+ gm : float, optional
181
+ Gravitational parameter GM (m^3/s^2). Default is GM_SUN.
182
+
183
+ Returns
184
+ -------
185
+ float
186
+ Shapiro delay (seconds)
187
+
188
+ Examples
189
+ --------
190
+ Earth-Sun-Spacecraft signal at superior conjunction (worst case):
191
+
192
+ >>> # Simplified geometry: Sun at origin, Earth at 1 AU, spacecraft beyond at distance
193
+ >>> sun_pos = np.array([0.0, 0.0, 0.0])
194
+ >>> earth_pos = np.array([1.496e11, 0.0, 0.0]) # 1 AU
195
+ >>> spacecraft_pos = np.array([1.496e11, 1.0e11, 0.0]) # Far from sun
196
+ >>> delay = shapiro_delay(earth_pos, spacecraft_pos, sun_pos, GM_SUN)
197
+ >>> print(f"Shapiro delay: {delay:.3e} seconds")
198
+ >>> print(f"Shapiro delay: {delay*1e6:.1f} microseconds")
199
+ """
200
+ # Compute distances
201
+ r_observer = np.linalg.norm(observer_pos - gravitating_body_pos)
202
+ r_source = np.linalg.norm(light_source_pos - gravitating_body_pos)
203
+ r_os = np.linalg.norm(observer_pos - light_source_pos)
204
+
205
+ # Shapiro delay formula (second-order PN)
206
+ # Check for valid geometry (gravitating body should affect path)
207
+ # The formula is valid when the impact parameter is close to the body
208
+ numerator = r_observer + r_source + r_os
209
+ denominator = r_observer + r_source - r_os
210
+
211
+ # If denominator <= 0, it means the path doesn't pass near the gravitating body
212
+ if denominator <= 0.0:
213
+ # Return zero delay if geometry is invalid (light path doesn't bend)
214
+ return 0.0
215
+
216
+ delay = (2.0 * gm / (C_LIGHT**3)) * np.log(numerator / denominator)
217
+ return delay
218
+
219
+
220
+ def schwarzschild_precession_per_orbit(a: float, e: float, gm: float = GM_SUN) -> float:
221
+ """Compute perihelion precession per orbit due to general relativity.
222
+
223
+ The advance of perihelion for an orbit around a central mass M is:
224
+
225
+ Δφ = (6π * GM) / (c^2 * a * (1 - e^2))
226
+
227
+ This effect is a key test of general relativity. For Mercury,
228
+ the predicted precession is ~43 arcseconds per century.
229
+
230
+ Parameters
231
+ ----------
232
+ a : float
233
+ Semi-major axis (m)
234
+ e : float
235
+ Eccentricity (dimensionless), must be in [0, 1)
236
+ gm : float, optional
237
+ Gravitational parameter GM (m^3/s^2). Default is GM_SUN.
238
+
239
+ Returns
240
+ -------
241
+ float
242
+ Perihelion precession per orbit (radians)
243
+
244
+ Examples
245
+ --------
246
+ Mercury's perihelion precession (GR contribution):
247
+
248
+ >>> a_mercury = 0.38709927 * AU # Semi-major axis in meters
249
+ >>> e_mercury = 0.20563593 # Eccentricity
250
+ >>> precession_rad = schwarzschild_precession_per_orbit(a_mercury, e_mercury, GM_SUN)
251
+ >>> precession_arcsec = precession_rad * 206265 # Convert to arcseconds
252
+ >>> orbital_period = 87.969 # days
253
+ >>> centuries = 36525 / orbital_period # Orbits per century
254
+ >>> precession_per_century = precession_arcsec * centuries
255
+ >>> print(f"GR perihelion precession: {precession_per_century:.1f} arcsec/century")
256
+ GR perihelion precession: 42.98 arcsec/century
257
+ """
258
+ if e < 0 or e >= 1:
259
+ raise ValueError(f"Eccentricity {e} must be in [0, 1)")
260
+
261
+ precession = (6.0 * np.pi * gm) / (C_LIGHT**2 * a * (1.0 - e**2))
262
+ return precession
263
+
264
+
265
+ def post_newtonian_acceleration(
266
+ r_vec: np.ndarray, v_vec: np.ndarray, gm: float = GM_EARTH
267
+ ) -> np.ndarray:
268
+ """Compute post-Newtonian acceleration corrections (1PN order).
269
+
270
+ Extends Newtonian gravity with first-order post-Newtonian corrections.
271
+
272
+ a_PN = -GM/r^2 * u_r + a_1PN
273
+
274
+ where a_1PN includes velocity-dependent and metric perturbation terms.
275
+
276
+ Parameters
277
+ ----------
278
+ r_vec : np.ndarray
279
+ Position vector (m), shape (3,)
280
+ v_vec : np.ndarray
281
+ Velocity vector (m/s), shape (3,)
282
+ gm : float, optional
283
+ Gravitational parameter GM (m^3/s^2). Default is GM_EARTH.
284
+
285
+ Returns
286
+ -------
287
+ np.ndarray
288
+ Total acceleration including 1PN corrections (m/s^2), shape (3,)
289
+
290
+ Examples
291
+ --------
292
+ Compare Newtonian and PN acceleration for LEO satellite:
293
+
294
+ >>> r = np.array([6.678e6, 0.0, 0.0]) # ~300 km altitude
295
+ >>> v = np.array([0.0, 7.7e3, 0.0]) # Circular orbit velocity
296
+ >>> a_total = post_newtonian_acceleration(r, v)
297
+ >>> a_newt = -GM_EARTH / np.linalg.norm(r)**3 * r
298
+ >>> correction_ratio = np.linalg.norm(a_total - a_newt) / np.linalg.norm(a_newt)
299
+ >>> print(f"PN correction: {correction_ratio*1e6:.1f} ppm")
300
+ """
301
+ r = np.linalg.norm(r_vec)
302
+ v_squared = np.sum(v_vec**2)
303
+
304
+ # Unit vector
305
+ u_r = r_vec / r
306
+
307
+ # Newtonian acceleration
308
+ a_newt = -gm / (r**2) * u_r
309
+
310
+ # 1PN corrections (in m/s^2)
311
+ c2 = C_LIGHT**2
312
+
313
+ # Term 1: Velocity squared effect on metric
314
+ term1 = (gm / c2) * (2.0 * v_squared / r - 4.0 * gm / r) * u_r / r
315
+
316
+ # Term 2: Radial velocity coupling
317
+ v_dot_r = np.dot(v_vec, u_r)
318
+ term2 = (4.0 * gm / c2) * v_dot_r * v_vec / r
319
+
320
+ # Combine corrections (these are small corrections to Newtonian acceleration)
321
+ a_1pn = term1 + term2
322
+
323
+ return a_newt + a_1pn
324
+
325
+
326
+ def geodetic_precession(
327
+ a: float, e: float, inclination: float, gm: float = GM_EARTH
328
+ ) -> float:
329
+ """Compute geodetic (de Sitter) precession rate of orbital plane.
330
+
331
+ The orbital plane of a satellite precesses due to frame-dragging effects
332
+ and spacetime curvature. The geodetic precession rate is:
333
+
334
+ Ω_geodetic = -GM/(c^2 * a^3 * (1 - e^2)^2) * cos(i)
335
+
336
+ Parameters
337
+ ----------
338
+ a : float
339
+ Semi-major axis (m)
340
+ e : float
341
+ Eccentricity (dimensionless)
342
+ inclination : float
343
+ Orbital inclination (radians)
344
+ gm : float, optional
345
+ Gravitational parameter (m^3/s^2). Default is GM_EARTH.
346
+
347
+ Returns
348
+ -------
349
+ float
350
+ Geodetic precession rate (radians per orbit)
351
+
352
+ Examples
353
+ --------
354
+ Geodetic precession for a typical Earth satellite:
355
+
356
+ >>> a = 6.678e6 # ~300 km altitude
357
+ >>> e = 0.0 # Circular
358
+ >>> i = np.radians(51.6) # ISS-like inclination
359
+ >>> rate = geodetic_precession(a, e, i)
360
+ >>> print(f"Precession per orbit: {rate*206265:.3f} arcsec")
361
+ """
362
+ p = a * (1.0 - e**2)
363
+ precession = -(gm / (C_LIGHT**2 * p**2)) * np.cos(inclination)
364
+ return precession
365
+
366
+
367
+ def lense_thirring_precession(
368
+ a: float,
369
+ e: float,
370
+ inclination: float,
371
+ angular_momentum: float,
372
+ gm: float = GM_EARTH,
373
+ ) -> float:
374
+ """Compute Lense-Thirring (frame-dragging) precession of orbital node.
375
+
376
+ A rotating central body drags the orbital plane of nearby objects.
377
+ The nodal precession rate due to this effect is:
378
+
379
+ Ω_LT = (2GM * J_2 * a * ω) / (c^2 * p^2) * f(e, i)
380
+
381
+ where J_2 is the quadrupole moment, ω is angular velocity, and f depends
382
+ on eccentricity and inclination.
383
+
384
+ Parameters
385
+ ----------
386
+ a : float
387
+ Semi-major axis (m)
388
+ e : float
389
+ Eccentricity (dimensionless)
390
+ inclination : float
391
+ Orbital inclination (radians)
392
+ angular_momentum : float
393
+ Angular momentum of central body (kg·m^2/s)
394
+ gm : float, optional
395
+ Gravitational parameter (m^3/s^2). Default is GM_EARTH.
396
+
397
+ Returns
398
+ -------
399
+ float
400
+ Lense-Thirring precession rate (radians per orbit)
401
+
402
+ Notes
403
+ -----
404
+ This is a simplified version. For Earth, J_2 effects typically dominate
405
+ classical nodal precession, while Lense-Thirring is a small correction
406
+ (~1 arcsec per year for typical satellites).
407
+
408
+ Examples
409
+ --------
410
+ Lense-Thirring effect for LAGEOS satellite:
411
+
412
+ >>> # LAGEOS parameters
413
+ >>> a = 12.27e6 # Semi-major axis
414
+ >>> e = 0.0045
415
+ >>> i = np.radians(109.9)
416
+ >>> L_earth = 7.05e33 # Earth's angular momentum
417
+ >>> rate = lense_thirring_precession(a, e, i, L_earth)
418
+ >>> print(f"LT precession per orbit: {rate*206265*1e3:.1f} milliarcsec")
419
+ """
420
+ p = a * (1.0 - e**2)
421
+
422
+ # Simplified Lense-Thirring term (second-order PN effect)
423
+ # For a sphere: Lense-Thirring parameter = 2GM*L/(c^2*M*r^3)
424
+ precession = (2.0 * angular_momentum * gm) / (C_LIGHT**2 * p**3)
425
+
426
+ return precession
427
+
428
+
429
+ def relativistic_range_correction(
430
+ distance: float, relative_velocity: float, gm: float = GM_EARTH
431
+ ) -> float:
432
+ """Compute relativistic range correction for ranging measurements.
433
+
434
+ When measuring distance to a satellite or spacecraft using ranging
435
+ (e.g., laser ranging), relativistic effects introduce corrections to
436
+ the measured range.
437
+
438
+ The main contributions are:
439
+ - Gravitational time dilation
440
+ - Relativistic Doppler effect
441
+
442
+ Parameters
443
+ ----------
444
+ distance : float
445
+ Distance to object (m)
446
+ relative_velocity : float
447
+ Radial velocity component (m/s, positive = receding)
448
+ gm : float, optional
449
+ Gravitational parameter (m^3/s^2). Default is GM_EARTH.
450
+
451
+ Returns
452
+ -------
453
+ float
454
+ Range correction (m)
455
+
456
+ Examples
457
+ --------
458
+ Range correction for lunar laser ranging:
459
+
460
+ >>> distance_to_moon = 3.84e8 # meters
461
+ >>> radial_velocity = 0.0 # Average over orbit
462
+ >>> correction = relativistic_range_correction(distance_to_moon, radial_velocity, GM_EARTH)
463
+ >>> print(f"Range correction: {correction:.1f} m")
464
+ """
465
+ # Gravitational correction (positive because the signal is delayed)
466
+ # Uses weak-field approximation
467
+ grav_correction = gm / (C_LIGHT**2)
468
+
469
+ # Doppler correction (second order effect, small)
470
+ doppler_correction = (relative_velocity**2) / (3.0 * C_LIGHT**2)
471
+
472
+ return grav_correction + doppler_correction
@@ -5,8 +5,8 @@ This module provides standard atmosphere models used for computing
5
5
  temperature, pressure, density, and other properties at various altitudes.
6
6
  """
7
7
 
8
- from pytcl.atmosphere.models import ( # Constants
9
- G0,
8
+ from pytcl.atmosphere.models import G0 # Constants
9
+ from pytcl.atmosphere.models import (
10
10
  GAMMA,
11
11
  P0,
12
12
  RHO0,
@@ -15,6 +15,7 @@ References
15
15
  from typing import List, NamedTuple, Set
16
16
 
17
17
  import numpy as np
18
+ from numba import njit
18
19
  from numpy.typing import ArrayLike, NDArray
19
20
 
20
21
 
@@ -40,6 +41,23 @@ class DBSCANResult(NamedTuple):
40
41
  n_noise: int
41
42
 
42
43
 
44
+ @njit(cache=True)
45
+ def _compute_distance_matrix(X: np.ndarray) -> np.ndarray:
46
+ """Compute pairwise Euclidean distance matrix (JIT-compiled)."""
47
+ n = X.shape[0]
48
+ dist = np.zeros((n, n), dtype=np.float64)
49
+ for i in range(n):
50
+ for j in range(i + 1, n):
51
+ d = 0.0
52
+ for k in range(X.shape[1]):
53
+ diff = X[i, k] - X[j, k]
54
+ d += diff * diff
55
+ d = np.sqrt(d)
56
+ dist[i, j] = d
57
+ dist[j, i] = d
58
+ return dist
59
+
60
+
43
61
  def compute_neighbors(
44
62
  X: NDArray[np.floating],
45
63
  eps: float,
@@ -60,13 +78,13 @@ def compute_neighbors(
60
78
  neighbors[i] contains indices of points within eps of point i.
61
79
  """
62
80
  n_samples = X.shape[0]
63
- neighbors = []
64
81
 
82
+ # Use JIT-compiled distance matrix computation
83
+ dist_matrix = _compute_distance_matrix(X)
84
+
85
+ neighbors = []
65
86
  for i in range(n_samples):
66
- # Compute distances from point i to all points
67
- distances = np.sqrt(np.sum((X - X[i]) ** 2, axis=1))
68
- # Find points within eps (including self)
69
- neighbor_indices = np.where(distances <= eps)[0]
87
+ neighbor_indices = np.where(dist_matrix[i] <= eps)[0]
70
88
  neighbors.append(neighbor_indices)
71
89
 
72
90
  return neighbors
@@ -15,6 +15,7 @@ from enum import Enum
15
15
  from typing import List, Literal, NamedTuple, Optional
16
16
 
17
17
  import numpy as np
18
+ from numba import njit
18
19
  from numpy.typing import ArrayLike, NDArray
19
20
 
20
21
 
@@ -70,6 +71,26 @@ class HierarchicalResult(NamedTuple):
70
71
  dendrogram: List[DendrogramNode]
71
72
 
72
73
 
74
+ @njit(cache=True)
75
+ def _compute_distance_matrix_jit(X: np.ndarray) -> np.ndarray:
76
+ """JIT-compiled pairwise Euclidean distance computation."""
77
+ n = X.shape[0]
78
+ n_features = X.shape[1]
79
+ distances = np.zeros((n, n), dtype=np.float64)
80
+
81
+ for i in range(n):
82
+ for j in range(i + 1, n):
83
+ d = 0.0
84
+ for k in range(n_features):
85
+ diff = X[i, k] - X[j, k]
86
+ d += diff * diff
87
+ d = np.sqrt(d)
88
+ distances[i, j] = d
89
+ distances[j, i] = d
90
+
91
+ return distances
92
+
93
+
73
94
  def compute_distance_matrix(
74
95
  X: NDArray[np.floating],
75
96
  ) -> NDArray[np.floating]:
@@ -86,16 +107,8 @@ def compute_distance_matrix(
86
107
  distances : ndarray
87
108
  Distance matrix, shape (n_samples, n_samples).
88
109
  """
89
- n = X.shape[0]
90
- distances = np.zeros((n, n))
91
-
92
- for i in range(n):
93
- for j in range(i + 1, n):
94
- d = np.sqrt(np.sum((X[i] - X[j]) ** 2))
95
- distances[i, j] = d
96
- distances[j, i] = d
97
-
98
- return distances
110
+ X = np.asarray(X, dtype=np.float64)
111
+ return _compute_distance_matrix_jit(X)
99
112
 
100
113
 
101
114
  def _single_linkage(
@@ -14,6 +14,7 @@ from typing import Literal, NamedTuple, Optional, Union
14
14
 
15
15
  import numpy as np
16
16
  from numpy.typing import ArrayLike, NDArray
17
+ from scipy.spatial.distance import cdist
17
18
 
18
19
 
19
20
  class KMeansResult(NamedTuple):
@@ -91,11 +92,8 @@ def kmeans_plusplus_init(
91
92
 
92
93
  # Subsequent centers: sample proportional to D^2
93
94
  for k in range(1, n_clusters):
94
- # Compute squared distances to nearest center
95
- distances_sq = np.full(n_samples, np.inf)
96
- for j in range(k):
97
- d_sq = np.sum((X - centers[j]) ** 2, axis=1)
98
- distances_sq = np.minimum(distances_sq, d_sq)
95
+ # Compute squared distances to nearest center (vectorized via cdist)
96
+ distances_sq = cdist(X, centers[:k], metric="sqeuclidean").min(axis=1)
99
97
 
100
98
  # Sample proportional to D^2
101
99
  probs = distances_sq / distances_sq.sum()
@@ -138,12 +136,9 @@ def assign_clusters(
138
136
  centers = np.asarray(centers, dtype=np.float64)
139
137
 
140
138
  n_samples = X.shape[0]
141
- n_clusters = centers.shape[0]
142
139
 
143
- # Compute distances to all centers
144
- distances_sq = np.zeros((n_samples, n_clusters))
145
- for k in range(n_clusters):
146
- distances_sq[:, k] = np.sum((X - centers[k]) ** 2, axis=1)
140
+ # Compute squared distances to all centers (vectorized via cdist)
141
+ distances_sq = cdist(X, centers, metric="sqeuclidean")
147
142
 
148
143
  # Assign to nearest center
149
144
  labels = np.argmin(distances_sq, axis=1).astype(np.intp)
@@ -13,17 +13,8 @@ from pytcl.containers.cluster_set import (
13
13
  cluster_tracks_kmeans,
14
14
  compute_cluster_centroid,
15
15
  )
16
- from pytcl.containers.covertree import (
17
- CoverTree,
18
- CoverTreeNode,
19
- CoverTreeResult,
20
- )
21
- from pytcl.containers.kd_tree import (
22
- BallTree,
23
- KDNode,
24
- KDTree,
25
- NearestNeighborResult,
26
- )
16
+ from pytcl.containers.covertree import CoverTree, CoverTreeNode, CoverTreeResult
17
+ from pytcl.containers.kd_tree import BallTree, KDNode, KDTree, NearestNeighborResult
27
18
  from pytcl.containers.measurement_set import (
28
19
  Measurement,
29
20
  MeasurementQuery,
@@ -38,16 +29,8 @@ from pytcl.containers.rtree import (
38
29
  box_from_points,
39
30
  merge_boxes,
40
31
  )
41
- from pytcl.containers.track_list import (
42
- TrackList,
43
- TrackListStats,
44
- TrackQuery,
45
- )
46
- from pytcl.containers.vptree import (
47
- VPNode,
48
- VPTree,
49
- VPTreeResult,
50
- )
32
+ from pytcl.containers.track_list import TrackList, TrackListStats, TrackQuery
33
+ from pytcl.containers.vptree import VPNode, VPTree, VPTreeResult
51
34
 
52
35
  __all__ = [
53
36
  # K-D Tree
@@ -7,16 +7,7 @@ that move together (formations, convoys, etc.).
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- from typing import (
11
- Dict,
12
- Iterable,
13
- Iterator,
14
- List,
15
- NamedTuple,
16
- Optional,
17
- Tuple,
18
- Union,
19
- )
10
+ from typing import Dict, Iterable, Iterator, List, NamedTuple, Optional, Tuple, Union
20
11
 
21
12
  import numpy as np
22
13
  from numpy.typing import ArrayLike, NDArray
@@ -7,15 +7,7 @@ with spatial query support.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- from typing import (
11
- Iterable,
12
- Iterator,
13
- List,
14
- NamedTuple,
15
- Optional,
16
- Tuple,
17
- Union,
18
- )
10
+ from typing import Iterable, Iterator, List, NamedTuple, Optional, Tuple, Union
19
11
 
20
12
  import numpy as np
21
13
  from numpy.typing import ArrayLike, NDArray