nrl-tracker 0.22.3__py3-none-any.whl → 0.22.4__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 (69) hide show
  1. {nrl_tracker-0.22.3.dist-info → nrl_tracker-0.22.4.dist-info}/METADATA +1 -1
  2. {nrl_tracker-0.22.3.dist-info → nrl_tracker-0.22.4.dist-info}/RECORD +69 -69
  3. pytcl/__init__.py +1 -1
  4. pytcl/assignment_algorithms/gating.py +3 -3
  5. pytcl/assignment_algorithms/jpda.py +12 -4
  6. pytcl/assignment_algorithms/two_dimensional/kbest.py +3 -1
  7. pytcl/astronomical/ephemerides.py +17 -9
  8. pytcl/astronomical/lambert.py +14 -4
  9. pytcl/astronomical/orbital_mechanics.py +3 -1
  10. pytcl/astronomical/reference_frames.py +3 -1
  11. pytcl/astronomical/relativity.py +8 -2
  12. pytcl/atmosphere/models.py +3 -1
  13. pytcl/clustering/gaussian_mixture.py +8 -4
  14. pytcl/clustering/hierarchical.py +5 -1
  15. pytcl/clustering/kmeans.py +3 -1
  16. pytcl/containers/cluster_set.py +9 -3
  17. pytcl/containers/measurement_set.py +6 -2
  18. pytcl/containers/rtree.py +5 -2
  19. pytcl/coordinate_systems/conversions/geodetic.py +15 -3
  20. pytcl/coordinate_systems/projections/projections.py +38 -11
  21. pytcl/coordinate_systems/rotations/rotations.py +3 -1
  22. pytcl/core/array_utils.py +4 -1
  23. pytcl/core/constants.py +3 -1
  24. pytcl/core/validation.py +17 -6
  25. pytcl/dynamic_estimation/imm.py +9 -3
  26. pytcl/dynamic_estimation/kalman/square_root.py +6 -2
  27. pytcl/dynamic_estimation/particle_filters/bootstrap.py +6 -2
  28. pytcl/dynamic_estimation/smoothers.py +3 -1
  29. pytcl/dynamic_models/process_noise/polynomial.py +6 -2
  30. pytcl/gravity/clenshaw.py +6 -2
  31. pytcl/gravity/egm.py +3 -1
  32. pytcl/gravity/models.py +3 -1
  33. pytcl/gravity/spherical_harmonics.py +10 -4
  34. pytcl/gravity/tides.py +16 -7
  35. pytcl/magnetism/emm.py +12 -3
  36. pytcl/magnetism/wmm.py +9 -2
  37. pytcl/mathematical_functions/basic_matrix/decompositions.py +3 -1
  38. pytcl/mathematical_functions/combinatorics/combinatorics.py +3 -1
  39. pytcl/mathematical_functions/geometry/geometry.py +12 -4
  40. pytcl/mathematical_functions/interpolation/interpolation.py +3 -1
  41. pytcl/mathematical_functions/signal_processing/detection.py +6 -3
  42. pytcl/mathematical_functions/signal_processing/filters.py +6 -2
  43. pytcl/mathematical_functions/signal_processing/matched_filter.py +4 -2
  44. pytcl/mathematical_functions/special_functions/elliptic.py +3 -1
  45. pytcl/mathematical_functions/special_functions/gamma_functions.py +3 -1
  46. pytcl/mathematical_functions/special_functions/lambert_w.py +3 -1
  47. pytcl/mathematical_functions/statistics/distributions.py +36 -12
  48. pytcl/mathematical_functions/statistics/estimators.py +3 -1
  49. pytcl/mathematical_functions/transforms/stft.py +9 -3
  50. pytcl/mathematical_functions/transforms/wavelets.py +18 -6
  51. pytcl/navigation/geodesy.py +31 -9
  52. pytcl/navigation/great_circle.py +12 -4
  53. pytcl/navigation/ins.py +8 -2
  54. pytcl/navigation/ins_gnss.py +25 -7
  55. pytcl/navigation/rhumb.py +10 -3
  56. pytcl/performance_evaluation/track_metrics.py +3 -1
  57. pytcl/plotting/coordinates.py +17 -5
  58. pytcl/plotting/metrics.py +9 -3
  59. pytcl/plotting/tracks.py +11 -3
  60. pytcl/static_estimation/least_squares.py +2 -1
  61. pytcl/static_estimation/maximum_likelihood.py +3 -3
  62. pytcl/terrain/dem.py +8 -2
  63. pytcl/terrain/loaders.py +13 -5
  64. pytcl/terrain/visibility.py +7 -2
  65. pytcl/trackers/hypothesis.py +7 -2
  66. pytcl/trackers/mht.py +15 -5
  67. {nrl_tracker-0.22.3.dist-info → nrl_tracker-0.22.4.dist-info}/LICENSE +0 -0
  68. {nrl_tracker-0.22.3.dist-info → nrl_tracker-0.22.4.dist-info}/WHEEL +0 -0
  69. {nrl_tracker-0.22.3.dist-info → nrl_tracker-0.22.4.dist-info}/top_level.txt +0 -0
pytcl/plotting/metrics.py CHANGED
@@ -148,7 +148,9 @@ def plot_nees_sequence(
148
148
  fig.add_trace(
149
149
  go.Scatter(
150
150
  x=np.concatenate([time, time[::-1]]),
151
- y=np.concatenate([np.full(n_steps, upper_bound), np.full(n_steps, lower_bound)]),
151
+ y=np.concatenate(
152
+ [np.full(n_steps, upper_bound), np.full(n_steps, lower_bound)]
153
+ ),
152
154
  fill="toself",
153
155
  fillcolor="rgba(0, 255, 0, 0.1)",
154
156
  line=dict(color="rgba(0,0,0,0)"),
@@ -532,7 +534,9 @@ def plot_consistency_summary(
532
534
  fig.add_trace(
533
535
  go.Scatter(
534
536
  x=np.concatenate([time, time[::-1]]),
535
- y=np.concatenate([np.full(n_steps, nees_upper), np.full(n_steps, nees_lower)]),
537
+ y=np.concatenate(
538
+ [np.full(n_steps, nees_upper), np.full(n_steps, nees_lower)]
539
+ ),
536
540
  fill="toself",
537
541
  fillcolor="rgba(0, 255, 0, 0.1)",
538
542
  line=dict(color="rgba(0,0,0,0)"),
@@ -576,7 +580,9 @@ def plot_consistency_summary(
576
580
  fig.add_trace(
577
581
  go.Scatter(
578
582
  x=np.concatenate([time, time[::-1]]),
579
- y=np.concatenate([np.full(n_steps, nis_upper), np.full(n_steps, nis_lower)]),
583
+ y=np.concatenate(
584
+ [np.full(n_steps, nis_upper), np.full(n_steps, nis_lower)]
585
+ ),
580
586
  fill="toself",
581
587
  fillcolor="rgba(0, 255, 0, 0.1)",
582
588
  line=dict(color="rgba(0,0,0,0)"),
pytcl/plotting/tracks.py CHANGED
@@ -369,7 +369,11 @@ def plot_multi_target_tracks(
369
369
 
370
370
  for idx, (track_id, states) in enumerate(tracks.items()):
371
371
  states = np.asarray(states)
372
- color = colors.get(track_id) if colors else default_colors[idx % len(default_colors)]
372
+ color = (
373
+ colors.get(track_id)
374
+ if colors
375
+ else default_colors[idx % len(default_colors)]
376
+ )
373
377
 
374
378
  fig.add_trace(
375
379
  go.Scatter(
@@ -532,7 +536,9 @@ def plot_estimation_comparison(
532
536
 
533
537
  # Error bounds
534
538
  if covariances is not None:
535
- sigma = n_std * np.array([np.sqrt(P[state_idx, state_idx]) for P in covariances])
539
+ sigma = n_std * np.array(
540
+ [np.sqrt(P[state_idx, state_idx]) for P in covariances]
541
+ )
536
542
  upper = estimates[:, state_idx] + sigma
537
543
  lower = estimates[:, state_idx] - sigma
538
544
 
@@ -734,7 +740,9 @@ def create_animated_tracking(
734
740
  method="animate",
735
741
  args=[
736
742
  [None],
737
- dict(frame=dict(duration=0, redraw=False), mode="immediate"),
743
+ dict(
744
+ frame=dict(duration=0, redraw=False), mode="immediate"
745
+ ),
738
746
  ],
739
747
  ),
740
748
  ],
@@ -303,7 +303,8 @@ def total_least_squares(
303
303
  # The solution exists if V[n, n] != 0
304
304
  if abs(V[n, n]) < 1e-14:
305
305
  raise ValueError(
306
- "TLS solution does not exist. The smallest singular value " "has multiplicity > 1."
306
+ "TLS solution does not exist. The smallest singular value "
307
+ "has multiplicity > 1."
307
308
  )
308
309
 
309
310
  # TLS solution: x = -V[0:n, n] / V[n, n]
@@ -654,9 +654,9 @@ def mle_gaussian(
654
654
  theta = np.array(theta)
655
655
 
656
656
  # Log-likelihood
657
- log_lik = -n / 2 * np.log(2 * np.pi * var_mle) - np.sum((data - mean_mle) ** 2) / (
658
- 2 * var_mle
659
- )
657
+ log_lik = -n / 2 * np.log(2 * np.pi * var_mle) - np.sum(
658
+ (data - mean_mle) ** 2
659
+ ) / (2 * var_mle)
660
660
 
661
661
  # Fisher information
662
662
  n_params = len(theta)
pytcl/terrain/dem.py CHANGED
@@ -193,7 +193,9 @@ class DEMGrid:
193
193
 
194
194
  def _in_bounds(self, lat: float, lon: float) -> bool:
195
195
  """Check if coordinates are within DEM bounds."""
196
- return self.lat_min <= lat <= self.lat_max and self.lon_min <= lon <= self.lon_max
196
+ return (
197
+ self.lat_min <= lat <= self.lat_max and self.lon_min <= lon <= self.lon_max
198
+ )
197
199
 
198
200
  def _get_indices(self, lat: float, lon: float) -> Tuple[int, int, float, float]:
199
201
  """Get grid indices and fractional parts for interpolation.
@@ -273,7 +275,11 @@ class DEMGrid:
273
275
  z00 = self.data[i, j]
274
276
  z01 = self.data[i, j + 1] if j + 1 < self.n_lon else z00
275
277
  z10 = self.data[i + 1, j] if i + 1 < self.n_lat else z00
276
- z11 = self.data[i + 1, j + 1] if (i + 1 < self.n_lat and j + 1 < self.n_lon) else z00
278
+ z11 = (
279
+ self.data[i + 1, j + 1]
280
+ if (i + 1 < self.n_lat and j + 1 < self.n_lon)
281
+ else z00
282
+ )
277
283
 
278
284
  # Check for nodata
279
285
  values = [z00, z01, z10, z11]
pytcl/terrain/loaders.py CHANGED
@@ -308,7 +308,8 @@ def parse_gebco_netcdf(
308
308
  import netCDF4 as nc
309
309
  except ImportError:
310
310
  raise ImportError(
311
- "netCDF4 is required for loading GEBCO files.\n" "Install with: pip install netCDF4"
311
+ "netCDF4 is required for loading GEBCO files.\n"
312
+ "Install with: pip install netCDF4"
312
313
  )
313
314
 
314
315
  # Set defaults for global extent
@@ -431,9 +432,13 @@ def parse_earth2014_binary(
431
432
 
432
433
  # Compute row/column indices
433
434
  i_start = max(0, int(np.floor((np.radians(lat_min_deg) - lat_start) / d_lat)))
434
- i_end = min(EARTH2014_N_LAT, int(np.ceil((np.radians(lat_max_deg) - lat_start) / d_lat)) + 1)
435
+ i_end = min(
436
+ EARTH2014_N_LAT, int(np.ceil((np.radians(lat_max_deg) - lat_start) / d_lat)) + 1
437
+ )
435
438
  j_start = max(0, int(np.floor((np.radians(lon_min_deg) - lon_start) / d_lon)))
436
- j_end = min(EARTH2014_N_LON, int(np.ceil((np.radians(lon_max_deg) - lon_start) / d_lon)) + 1)
439
+ j_end = min(
440
+ EARTH2014_N_LON, int(np.ceil((np.radians(lon_max_deg) - lon_start) / d_lon)) + 1
441
+ )
437
442
 
438
443
  # Read binary data
439
444
  # File is stored as int16 big-endian, rows from south to north
@@ -450,7 +455,9 @@ def parse_earth2014_binary(
450
455
  f.seek(row_offset + col_offset)
451
456
 
452
457
  # Read row segment
453
- row_data = np.frombuffer(f.read(n_cols * 2), dtype=">i2") # big-endian int16
458
+ row_data = np.frombuffer(
459
+ f.read(n_cols * 2), dtype=">i2"
460
+ ) # big-endian int16
454
461
  data[i, :] = row_data.astype(np.float64)
455
462
 
456
463
  # Compute actual bounds
@@ -571,7 +578,8 @@ def load_gebco(
571
578
  """
572
579
  if version not in GEBCO_PARAMETERS:
573
580
  raise ValueError(
574
- f"Unknown GEBCO version: {version}. " f"Valid versions: {list(GEBCO_PARAMETERS.keys())}"
581
+ f"Unknown GEBCO version: {version}. "
582
+ f"Valid versions: {list(GEBCO_PARAMETERS.keys())}"
575
583
  )
576
584
 
577
585
  return _load_gebco_cached(version, lat_min, lat_max, lon_min, lon_max)
@@ -184,7 +184,10 @@ def line_of_sight(
184
184
  # Compute distances from observer
185
185
  dlat = sample_lats - obs_lat
186
186
  dlon = sample_lons - obs_lon
187
- a = np.sin(dlat / 2) ** 2 + np.cos(obs_lat) * np.cos(sample_lats) * np.sin(dlon / 2) ** 2
187
+ a = (
188
+ np.sin(dlat / 2) ** 2
189
+ + np.cos(obs_lat) * np.cos(sample_lats) * np.sin(dlon / 2) ** 2
190
+ )
188
191
  c = 2 * np.arctan2(np.sqrt(a), np.sqrt(1 - a))
189
192
  distances = earth_radius * c
190
193
 
@@ -247,7 +250,9 @@ def line_of_sight(
247
250
  else:
248
251
  grazing_angle = -np.pi / 2
249
252
 
250
- return LOSResult(visible, grazing_angle, obstacle_distance, obstacle_elevation, min_clearance)
253
+ return LOSResult(
254
+ visible, grazing_angle, obstacle_distance, obstacle_elevation, min_clearance
255
+ )
251
256
 
252
257
 
253
258
  def viewshed(
@@ -294,7 +294,10 @@ def n_scan_prune(
294
294
  hyp_tracks_at_cutoff.add(track_id)
295
295
 
296
296
  # Keep if tracks match (or if no tracks at cutoff)
297
- if hyp_tracks_at_cutoff == best_tracks_at_cutoff or len(best_tracks_at_cutoff) == 0:
297
+ if (
298
+ hyp_tracks_at_cutoff == best_tracks_at_cutoff
299
+ or len(best_tracks_at_cutoff) == 0
300
+ ):
298
301
  pruned.append(hyp)
299
302
 
300
303
  # Renormalize probabilities
@@ -512,7 +515,9 @@ class HypothesisTree:
512
515
  new_hypotheses = []
513
516
 
514
517
  for hyp in self.hypotheses:
515
- for assoc_idx, (assoc, likelihood) in enumerate(zip(associations, likelihoods)):
518
+ for assoc_idx, (assoc, likelihood) in enumerate(
519
+ zip(associations, likelihoods)
520
+ ):
516
521
  # Compute new hypothesis probability
517
522
  new_prob = hyp.probability * likelihood
518
523
 
pytcl/trackers/mht.py CHANGED
@@ -223,7 +223,9 @@ class MHTTracker:
223
223
  predicted_tracks = self._predict_tracks(current_tracks, F, Q)
224
224
 
225
225
  # Compute gating and likelihoods
226
- gated, likelihood_matrix = self._compute_gating_and_likelihoods(predicted_tracks, Z)
226
+ gated, likelihood_matrix = self._compute_gating_and_likelihoods(
227
+ predicted_tracks, Z
228
+ )
227
229
 
228
230
  # Generate associations for each hypothesis
229
231
  track_id_list = list(predicted_tracks.keys())
@@ -268,7 +270,9 @@ class MHTTracker:
268
270
 
269
271
  # Update tracks based on associations
270
272
  new_tracks_per_assoc: Dict[int, List[MHTTrack]] = {}
271
- updated_tracks: Dict[int, Dict[int, MHTTrack]] = {} # assoc_idx -> track_id -> track
273
+ updated_tracks: Dict[int, Dict[int, MHTTrack]] = (
274
+ {}
275
+ ) # assoc_idx -> track_id -> track
272
276
 
273
277
  for assoc_idx, assoc in enumerate(associations):
274
278
  updated_tracks[assoc_idx] = {}
@@ -291,7 +295,9 @@ class MHTTracker:
291
295
  updated_tracks[assoc_idx][track_id] = upd_track
292
296
 
293
297
  # Handle unassigned measurements -> new tracks
294
- assigned_meas = set(meas_idx for meas_idx in assoc.values() if meas_idx >= 0)
298
+ assigned_meas = set(
299
+ meas_idx for meas_idx in assoc.values() if meas_idx >= 0
300
+ )
295
301
  for j in range(n_meas):
296
302
  if j not in assigned_meas:
297
303
  new_track = self._initiate_track(Z[j], j)
@@ -402,7 +408,9 @@ class MHTTracker:
402
408
 
403
409
  # Clutter and new track terms for unassigned measurements
404
410
  n_unassigned = len(Z) - len(used_meas)
405
- likelihood *= (self.config.clutter_density + self.config.new_track_weight) ** n_unassigned
411
+ likelihood *= (
412
+ self.config.clutter_density + self.config.new_track_weight
413
+ ) ** n_unassigned
406
414
 
407
415
  return likelihood
408
416
 
@@ -526,7 +534,9 @@ class MHTTracker:
526
534
  new_hypotheses = []
527
535
 
528
536
  for hyp in self.hypothesis_tree.hypotheses:
529
- for assoc_idx, (assoc, likelihood) in enumerate(zip(associations, likelihoods)):
537
+ for assoc_idx, (assoc, likelihood) in enumerate(
538
+ zip(associations, likelihoods)
539
+ ):
530
540
  # Compute new hypothesis probability
531
541
  new_prob = hyp.probability * likelihood
532
542