nrl-tracker 0.21.5__py3-none-any.whl → 0.22.1__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 (84) hide show
  1. {nrl_tracker-0.21.5.dist-info → nrl_tracker-0.22.1.dist-info}/METADATA +2 -2
  2. {nrl_tracker-0.21.5.dist-info → nrl_tracker-0.22.1.dist-info}/RECORD +84 -82
  3. pytcl/__init__.py +1 -1
  4. pytcl/assignment_algorithms/data_association.py +2 -7
  5. pytcl/assignment_algorithms/gating.py +3 -3
  6. pytcl/assignment_algorithms/jpda.py +4 -12
  7. pytcl/assignment_algorithms/two_dimensional/kbest.py +1 -3
  8. pytcl/astronomical/__init__.py +60 -7
  9. pytcl/astronomical/ephemerides.py +522 -0
  10. pytcl/astronomical/lambert.py +4 -14
  11. pytcl/astronomical/orbital_mechanics.py +1 -3
  12. pytcl/astronomical/reference_frames.py +1 -3
  13. pytcl/astronomical/relativity.py +466 -0
  14. pytcl/atmosphere/__init__.py +2 -2
  15. pytcl/atmosphere/models.py +1 -3
  16. pytcl/clustering/gaussian_mixture.py +4 -8
  17. pytcl/clustering/hierarchical.py +1 -5
  18. pytcl/clustering/kmeans.py +1 -3
  19. pytcl/containers/__init__.py +4 -21
  20. pytcl/containers/cluster_set.py +4 -19
  21. pytcl/containers/measurement_set.py +3 -15
  22. pytcl/containers/rtree.py +2 -5
  23. pytcl/coordinate_systems/conversions/geodetic.py +3 -15
  24. pytcl/coordinate_systems/projections/__init__.py +4 -2
  25. pytcl/coordinate_systems/projections/projections.py +11 -38
  26. pytcl/coordinate_systems/rotations/rotations.py +1 -3
  27. pytcl/core/array_utils.py +1 -4
  28. pytcl/core/constants.py +1 -3
  29. pytcl/core/validation.py +6 -17
  30. pytcl/dynamic_estimation/imm.py +4 -13
  31. pytcl/dynamic_estimation/kalman/extended.py +1 -4
  32. pytcl/dynamic_estimation/kalman/square_root.py +2 -6
  33. pytcl/dynamic_estimation/kalman/unscented.py +1 -4
  34. pytcl/dynamic_estimation/particle_filters/bootstrap.py +2 -6
  35. pytcl/dynamic_estimation/smoothers.py +2 -8
  36. pytcl/dynamic_models/discrete_time/__init__.py +1 -5
  37. pytcl/dynamic_models/process_noise/__init__.py +1 -5
  38. pytcl/dynamic_models/process_noise/polynomial.py +2 -6
  39. pytcl/gravity/clenshaw.py +2 -6
  40. pytcl/gravity/egm.py +1 -3
  41. pytcl/gravity/models.py +1 -3
  42. pytcl/gravity/spherical_harmonics.py +4 -10
  43. pytcl/gravity/tides.py +7 -16
  44. pytcl/magnetism/__init__.py +3 -14
  45. pytcl/magnetism/emm.py +3 -12
  46. pytcl/magnetism/wmm.py +2 -9
  47. pytcl/mathematical_functions/basic_matrix/decompositions.py +1 -3
  48. pytcl/mathematical_functions/combinatorics/combinatorics.py +1 -3
  49. pytcl/mathematical_functions/geometry/geometry.py +4 -12
  50. pytcl/mathematical_functions/interpolation/__init__.py +2 -2
  51. pytcl/mathematical_functions/interpolation/interpolation.py +1 -3
  52. pytcl/mathematical_functions/signal_processing/detection.py +3 -6
  53. pytcl/mathematical_functions/signal_processing/filters.py +2 -6
  54. pytcl/mathematical_functions/signal_processing/matched_filter.py +2 -4
  55. pytcl/mathematical_functions/special_functions/__init__.py +2 -2
  56. pytcl/mathematical_functions/special_functions/elliptic.py +1 -3
  57. pytcl/mathematical_functions/special_functions/gamma_functions.py +1 -3
  58. pytcl/mathematical_functions/special_functions/lambert_w.py +1 -3
  59. pytcl/mathematical_functions/statistics/distributions.py +12 -36
  60. pytcl/mathematical_functions/statistics/estimators.py +1 -3
  61. pytcl/mathematical_functions/transforms/stft.py +3 -9
  62. pytcl/mathematical_functions/transforms/wavelets.py +6 -18
  63. pytcl/navigation/__init__.py +14 -10
  64. pytcl/navigation/geodesy.py +9 -31
  65. pytcl/navigation/great_circle.py +4 -12
  66. pytcl/navigation/ins.py +3 -13
  67. pytcl/navigation/ins_gnss.py +7 -25
  68. pytcl/navigation/rhumb.py +3 -10
  69. pytcl/performance_evaluation/track_metrics.py +1 -3
  70. pytcl/plotting/coordinates.py +5 -17
  71. pytcl/plotting/metrics.py +3 -9
  72. pytcl/plotting/tracks.py +3 -11
  73. pytcl/static_estimation/least_squares.py +1 -2
  74. pytcl/static_estimation/maximum_likelihood.py +3 -3
  75. pytcl/terrain/dem.py +2 -8
  76. pytcl/terrain/loaders.py +5 -13
  77. pytcl/terrain/visibility.py +2 -7
  78. pytcl/trackers/__init__.py +3 -14
  79. pytcl/trackers/hypothesis.py +2 -7
  80. pytcl/trackers/mht.py +5 -15
  81. pytcl/trackers/multi_target.py +1 -4
  82. {nrl_tracker-0.21.5.dist-info → nrl_tracker-0.22.1.dist-info}/LICENSE +0 -0
  83. {nrl_tracker-0.21.5.dist-info → nrl_tracker-0.22.1.dist-info}/WHEEL +0 -0
  84. {nrl_tracker-0.21.5.dist-info → nrl_tracker-0.22.1.dist-info}/top_level.txt +0 -0
@@ -344,9 +344,7 @@ def great_circle_direct(
344
344
  """
345
345
  d = distance / radius # Angular distance
346
346
 
347
- lat2 = np.arcsin(
348
- np.sin(lat1) * np.cos(d) + np.cos(lat1) * np.sin(d) * np.cos(azimuth)
349
- )
347
+ lat2 = np.arcsin(np.sin(lat1) * np.cos(d) + np.cos(lat1) * np.sin(d) * np.cos(azimuth))
350
348
 
351
349
  lon2 = lon1 + np.arctan2(
352
350
  np.sin(azimuth) * np.sin(d) * np.cos(lat1),
@@ -408,9 +406,7 @@ def cross_track_distance(
408
406
  # Along-track distance
409
407
  dat = np.arccos(np.cos(d13) / np.cos(dxt))
410
408
 
411
- return CrossTrackResult(
412
- cross_track=float(dxt * radius), along_track=float(dat * radius)
413
- )
409
+ return CrossTrackResult(cross_track=float(dxt * radius), along_track=float(dat * radius))
414
410
 
415
411
 
416
412
  def great_circle_intersect(
@@ -452,9 +448,7 @@ def great_circle_intersect(
452
448
 
453
449
  # Convert to Cartesian unit vectors
454
450
  def to_cartesian(lat, lon):
455
- return np.array(
456
- [np.cos(lat) * np.cos(lon), np.cos(lat) * np.sin(lon), np.sin(lat)]
457
- )
451
+ return np.array([np.cos(lat) * np.cos(lon), np.cos(lat) * np.sin(lon), np.sin(lat)])
458
452
 
459
453
  # Normal vectors to the great circles
460
454
  p1 = to_cartesian(lat1, lon1)
@@ -508,9 +502,7 @@ def great_circle_intersect(
508
502
  lat_i2 = -lat_i1
509
503
  lon_i2 = ((lon_i1 + np.pi) % (2 * np.pi)) - np.pi
510
504
 
511
- return IntersectionResult(
512
- float(lat_i1), float(lon_i1), float(lat_i2), float(lon_i2), True
513
- )
505
+ return IntersectionResult(float(lat_i1), float(lon_i1), float(lat_i2), float(lon_i2), True)
514
506
 
515
507
 
516
508
  def great_circle_path_intersect(
pytcl/navigation/ins.py CHANGED
@@ -23,11 +23,7 @@ from typing import NamedTuple, Optional, Tuple
23
23
  import numpy as np
24
24
  from numpy.typing import ArrayLike, NDArray
25
25
 
26
- from pytcl.coordinate_systems.rotations import (
27
- quat2rotmat,
28
- quat_multiply,
29
- rotmat2quat,
30
- )
26
+ from pytcl.coordinate_systems.rotations import quat2rotmat, quat_multiply, rotmat2quat
31
27
  from pytcl.navigation.geodesy import WGS84, Ellipsoid
32
28
 
33
29
  # =============================================================================
@@ -248,11 +244,7 @@ def normal_gravity(lat: float, alt: float = 0.0) -> float:
248
244
 
249
245
  # Free-air correction (first-order)
250
246
  g = g0 * (
251
- 1
252
- - 2
253
- * alt
254
- / A_EARTH
255
- * (1 + F_EARTH + (OMEGA_EARTH**2 * A_EARTH**2 * B_EARTH) / GM_EARTH)
247
+ 1 - 2 * alt / A_EARTH * (1 + F_EARTH + (OMEGA_EARTH**2 * A_EARTH**2 * B_EARTH) / GM_EARTH)
256
248
  )
257
249
 
258
250
  return g
@@ -576,9 +568,7 @@ def update_quaternion(
576
568
 
577
569
  if angle < 1e-10:
578
570
  # Small angle approximation
579
- delta_q = np.array(
580
- [1.0, 0.5 * delta_theta[0], 0.5 * delta_theta[1], 0.5 * delta_theta[2]]
581
- )
571
+ delta_q = np.array([1.0, 0.5 * delta_theta[0], 0.5 * delta_theta[1], 0.5 * delta_theta[2]])
582
572
  else:
583
573
  # Exact quaternion for rotation
584
574
  axis = delta_theta / angle
@@ -512,9 +512,7 @@ def loose_coupled_predict(
512
512
  dt = imu.dt
513
513
 
514
514
  # Propagate INS mechanization
515
- ins_new = mechanize_ins_ned(
516
- state.ins_state, imu, accel_prev=accel_prev, gyro_prev=gyro_prev
517
- )
515
+ ins_new = mechanize_ins_ned(state.ins_state, imu, accel_prev=accel_prev, gyro_prev=gyro_prev)
518
516
 
519
517
  # Get error state transition matrix (continuous-time)
520
518
  F_cont = ins_error_state_matrix(state.ins_state)
@@ -576,9 +574,7 @@ def loose_coupled_update_position(
576
574
  if gnss.position_cov is not None:
577
575
  R = gnss.position_cov
578
576
  else:
579
- R = np.diag(
580
- [10.0**2, 10.0**2, 15.0**2]
581
- ) # Default: 10m horizontal, 15m vertical
577
+ R = np.diag([10.0**2, 10.0**2, 15.0**2]) # Default: 10m horizontal, 15m vertical
582
578
 
583
579
  # Innovation: measured position - INS predicted position
584
580
  z = gnss.position - state.ins_state.position
@@ -695,16 +691,8 @@ def loose_coupled_update(
695
691
  # Full position + velocity update
696
692
  H = position_velocity_measurement_matrix()
697
693
 
698
- R_pos = (
699
- gnss.position_cov
700
- if gnss.position_cov is not None
701
- else np.diag([10.0**2] * 3)
702
- )
703
- R_vel = (
704
- gnss.velocity_cov
705
- if gnss.velocity_cov is not None
706
- else np.diag([0.1**2] * 3)
707
- )
694
+ R_pos = gnss.position_cov if gnss.position_cov is not None else np.diag([10.0**2] * 3)
695
+ R_vel = gnss.velocity_cov if gnss.velocity_cov is not None else np.diag([0.1**2] * 3)
708
696
  R = np.block([[R_pos, np.zeros((3, 3))], [np.zeros((3, 3)), R_vel]])
709
697
 
710
698
  z = np.concatenate(
@@ -846,9 +834,7 @@ def tight_coupled_measurement_matrix(
846
834
  los, _ = compute_line_of_sight(user_ecef, sat.position)
847
835
 
848
836
  # LOS components in ECEF
849
- los_x, los_y, los_z = (
850
- -los
851
- ) # Negative because increase in user pos decreases range
837
+ los_x, los_y, los_z = -los # Negative because increase in user pos decreases range
852
838
 
853
839
  # Transform LOS to geodetic derivatives
854
840
  # d(range)/d(lat), d(range)/d(lon), d(range)/d(alt)
@@ -858,9 +844,7 @@ def tight_coupled_measurement_matrix(
858
844
  + los_z * (cos_lat * N * (1 - ellipsoid.e2))
859
845
  )
860
846
  H[i, 1] = los_x * (-cos_lat * sin_lon * N) + los_y * (cos_lat * cos_lon * N)
861
- H[i, 2] = (
862
- los_x * cos_lat * cos_lon + los_y * cos_lat * sin_lon + los_z * sin_lat
863
- )
847
+ H[i, 2] = los_x * cos_lat * cos_lon + los_y * cos_lat * sin_lon + los_z * sin_lat
864
848
 
865
849
  # Clock bias (state 15)
866
850
  H[i, 15] = 1.0
@@ -991,9 +975,7 @@ def _apply_error_correction(
991
975
 
992
976
  # Apply small angle rotation to quaternion
993
977
  q = ins_state.quaternion
994
- delta_q = np.array(
995
- [1.0, 0.5 * phi[0], 0.5 * phi[1], 0.5 * phi[2]], dtype=np.float64
996
- )
978
+ delta_q = np.array([1.0, 0.5 * phi[0], 0.5 * phi[1], 0.5 * phi[2]], dtype=np.float64)
997
979
  delta_q = delta_q / np.linalg.norm(delta_q)
998
980
 
999
981
  # Quaternion multiplication (body frame correction)
pytcl/navigation/rhumb.py CHANGED
@@ -100,9 +100,7 @@ def _isometric_latitude(lat: float, e2: float = 0.0) -> float:
100
100
  )
101
101
 
102
102
 
103
- def _inverse_isometric_latitude(
104
- psi: float, e2: float = 0.0, max_iter: int = 20
105
- ) -> float:
103
+ def _inverse_isometric_latitude(psi: float, e2: float = 0.0, max_iter: int = 20) -> float:
106
104
  """
107
105
  Compute geodetic latitude from isometric latitude.
108
106
 
@@ -131,10 +129,7 @@ def _inverse_isometric_latitude(
131
129
  for _ in range(max_iter):
132
130
  sin_lat = np.sin(lat)
133
131
  lat_new = (
134
- 2
135
- * np.arctan(
136
- ((1 + e * sin_lat) / (1 - e * sin_lat)) ** (e / 2) * np.exp(psi)
137
- )
132
+ 2 * np.arctan(((1 + e * sin_lat) / (1 - e * sin_lat)) ** (e / 2) * np.exp(psi))
138
133
  - np.pi / 2
139
134
  )
140
135
  if abs(lat_new - lat) < 1e-12:
@@ -427,9 +422,7 @@ def indirect_rhumb(
427
422
  if abs(dlon) > np.pi:
428
423
  dlon = dlon - np.sign(dlon) * 2 * np.pi
429
424
 
430
- dpsi = _isometric_latitude(lat2, ellipsoid.e2) - _isometric_latitude(
431
- lat1, ellipsoid.e2
432
- )
425
+ dpsi = _isometric_latitude(lat2, ellipsoid.e2) - _isometric_latitude(lat1, ellipsoid.e2)
433
426
  bearing = np.arctan2(dlon, dpsi) % (2 * np.pi)
434
427
 
435
428
  return RhumbResult(distance, bearing)
@@ -158,9 +158,7 @@ def ospa(
158
158
  loc_component = (localization_sum / n) ** (1.0 / p) if localization_sum > 0 else 0.0
159
159
  card_component = (cardinality_penalty / n) ** (1.0 / p) if n > m else 0.0
160
160
 
161
- return OSPAResult(
162
- ospa=ospa_val, localization=loc_component, cardinality=card_component
163
- )
161
+ return OSPAResult(ospa=ospa_val, localization=loc_component, cardinality=card_component)
164
162
 
165
163
 
166
164
  def ospa_over_time(
@@ -181,19 +181,13 @@ def plot_euler_angles(
181
181
 
182
182
  # Create rotation matrices for each axis
183
183
  def rotx(a):
184
- return np.array(
185
- [[1, 0, 0], [0, np.cos(a), -np.sin(a)], [0, np.sin(a), np.cos(a)]]
186
- )
184
+ return np.array([[1, 0, 0], [0, np.cos(a), -np.sin(a)], [0, np.sin(a), np.cos(a)]])
187
185
 
188
186
  def roty(a):
189
- return np.array(
190
- [[np.cos(a), 0, np.sin(a)], [0, 1, 0], [-np.sin(a), 0, np.cos(a)]]
191
- )
187
+ return np.array([[np.cos(a), 0, np.sin(a)], [0, 1, 0], [-np.sin(a), 0, np.cos(a)]])
192
188
 
193
189
  def rotz(a):
194
- return np.array(
195
- [[np.cos(a), -np.sin(a), 0], [np.sin(a), np.cos(a), 0], [0, 0, 1]]
196
- )
190
+ return np.array([[np.cos(a), -np.sin(a), 0], [np.sin(a), np.cos(a), 0], [0, 0, 1]])
197
191
 
198
192
  rot_funcs = {"X": rotx, "Y": roty, "Z": rotz}
199
193
 
@@ -247,11 +241,7 @@ def plot_euler_angles(
247
241
  for i in range(4):
248
242
  scene_name = f"scene{i + 1}" if i > 0 else "scene"
249
243
  fig.update_layout(
250
- **{
251
- scene_name: dict(
252
- aspectmode="cube", camera=dict(eye=dict(x=1.5, y=1.5, z=1.5))
253
- )
254
- }
244
+ **{scene_name: dict(aspectmode="cube", camera=dict(eye=dict(x=1.5, y=1.5, z=1.5)))}
255
245
  )
256
246
 
257
247
  return fig
@@ -378,9 +368,7 @@ def plot_quaternion_interpolation(
378
368
  method="animate",
379
369
  args=[
380
370
  [None],
381
- dict(
382
- frame=dict(duration=0, redraw=False), mode="immediate"
383
- ),
371
+ dict(frame=dict(duration=0, redraw=False), mode="immediate"),
384
372
  ],
385
373
  ),
386
374
  ],
pytcl/plotting/metrics.py CHANGED
@@ -148,9 +148,7 @@ 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(
152
- [np.full(n_steps, upper_bound), np.full(n_steps, lower_bound)]
153
- ),
151
+ y=np.concatenate([np.full(n_steps, upper_bound), np.full(n_steps, lower_bound)]),
154
152
  fill="toself",
155
153
  fillcolor="rgba(0, 255, 0, 0.1)",
156
154
  line=dict(color="rgba(0,0,0,0)"),
@@ -534,9 +532,7 @@ def plot_consistency_summary(
534
532
  fig.add_trace(
535
533
  go.Scatter(
536
534
  x=np.concatenate([time, time[::-1]]),
537
- y=np.concatenate(
538
- [np.full(n_steps, nees_upper), np.full(n_steps, nees_lower)]
539
- ),
535
+ y=np.concatenate([np.full(n_steps, nees_upper), np.full(n_steps, nees_lower)]),
540
536
  fill="toself",
541
537
  fillcolor="rgba(0, 255, 0, 0.1)",
542
538
  line=dict(color="rgba(0,0,0,0)"),
@@ -580,9 +576,7 @@ def plot_consistency_summary(
580
576
  fig.add_trace(
581
577
  go.Scatter(
582
578
  x=np.concatenate([time, time[::-1]]),
583
- y=np.concatenate(
584
- [np.full(n_steps, nis_upper), np.full(n_steps, nis_lower)]
585
- ),
579
+ y=np.concatenate([np.full(n_steps, nis_upper), np.full(n_steps, nis_lower)]),
586
580
  fill="toself",
587
581
  fillcolor="rgba(0, 255, 0, 0.1)",
588
582
  line=dict(color="rgba(0,0,0,0)"),
pytcl/plotting/tracks.py CHANGED
@@ -369,11 +369,7 @@ 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 = (
373
- colors.get(track_id)
374
- if colors
375
- else default_colors[idx % len(default_colors)]
376
- )
372
+ color = colors.get(track_id) if colors else default_colors[idx % len(default_colors)]
377
373
 
378
374
  fig.add_trace(
379
375
  go.Scatter(
@@ -536,9 +532,7 @@ def plot_estimation_comparison(
536
532
 
537
533
  # Error bounds
538
534
  if covariances is not None:
539
- sigma = n_std * np.array(
540
- [np.sqrt(P[state_idx, state_idx]) for P in covariances]
541
- )
535
+ sigma = n_std * np.array([np.sqrt(P[state_idx, state_idx]) for P in covariances])
542
536
  upper = estimates[:, state_idx] + sigma
543
537
  lower = estimates[:, state_idx] - sigma
544
538
 
@@ -740,9 +734,7 @@ def create_animated_tracking(
740
734
  method="animate",
741
735
  args=[
742
736
  [None],
743
- dict(
744
- frame=dict(duration=0, redraw=False), mode="immediate"
745
- ),
737
+ dict(frame=dict(duration=0, redraw=False), mode="immediate"),
746
738
  ],
747
739
  ),
748
740
  ],
@@ -303,8 +303,7 @@ 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 "
307
- "has multiplicity > 1."
306
+ "TLS solution does not exist. The smallest singular value " "has multiplicity > 1."
308
307
  )
309
308
 
310
309
  # 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(
658
- (data - mean_mle) ** 2
659
- ) / (2 * var_mle)
657
+ log_lik = -n / 2 * np.log(2 * np.pi * var_mle) - np.sum((data - mean_mle) ** 2) / (
658
+ 2 * var_mle
659
+ )
660
660
 
661
661
  # Fisher information
662
662
  n_params = len(theta)
pytcl/terrain/dem.py CHANGED
@@ -193,9 +193,7 @@ 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 (
197
- self.lat_min <= lat <= self.lat_max and self.lon_min <= lon <= self.lon_max
198
- )
196
+ return self.lat_min <= lat <= self.lat_max and self.lon_min <= lon <= self.lon_max
199
197
 
200
198
  def _get_indices(self, lat: float, lon: float) -> Tuple[int, int, float, float]:
201
199
  """Get grid indices and fractional parts for interpolation.
@@ -275,11 +273,7 @@ class DEMGrid:
275
273
  z00 = self.data[i, j]
276
274
  z01 = self.data[i, j + 1] if j + 1 < self.n_lon else z00
277
275
  z10 = self.data[i + 1, j] if i + 1 < self.n_lat 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
- )
276
+ z11 = self.data[i + 1, j + 1] if (i + 1 < self.n_lat and j + 1 < self.n_lon) else z00
283
277
 
284
278
  # Check for nodata
285
279
  values = [z00, z01, z10, z11]
pytcl/terrain/loaders.py CHANGED
@@ -308,8 +308,7 @@ 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"
312
- "Install with: pip install netCDF4"
311
+ "netCDF4 is required for loading GEBCO files.\n" "Install with: pip install netCDF4"
313
312
  )
314
313
 
315
314
  # Set defaults for global extent
@@ -432,13 +431,9 @@ def parse_earth2014_binary(
432
431
 
433
432
  # Compute row/column indices
434
433
  i_start = max(0, int(np.floor((np.radians(lat_min_deg) - lat_start) / d_lat)))
435
- i_end = min(
436
- EARTH2014_N_LAT, int(np.ceil((np.radians(lat_max_deg) - lat_start) / d_lat)) + 1
437
- )
434
+ i_end = min(EARTH2014_N_LAT, int(np.ceil((np.radians(lat_max_deg) - lat_start) / d_lat)) + 1)
438
435
  j_start = max(0, int(np.floor((np.radians(lon_min_deg) - lon_start) / d_lon)))
439
- j_end = min(
440
- EARTH2014_N_LON, int(np.ceil((np.radians(lon_max_deg) - lon_start) / d_lon)) + 1
441
- )
436
+ j_end = min(EARTH2014_N_LON, int(np.ceil((np.radians(lon_max_deg) - lon_start) / d_lon)) + 1)
442
437
 
443
438
  # Read binary data
444
439
  # File is stored as int16 big-endian, rows from south to north
@@ -455,9 +450,7 @@ def parse_earth2014_binary(
455
450
  f.seek(row_offset + col_offset)
456
451
 
457
452
  # Read row segment
458
- row_data = np.frombuffer(
459
- f.read(n_cols * 2), dtype=">i2"
460
- ) # big-endian int16
453
+ row_data = np.frombuffer(f.read(n_cols * 2), dtype=">i2") # big-endian int16
461
454
  data[i, :] = row_data.astype(np.float64)
462
455
 
463
456
  # Compute actual bounds
@@ -578,8 +571,7 @@ def load_gebco(
578
571
  """
579
572
  if version not in GEBCO_PARAMETERS:
580
573
  raise ValueError(
581
- f"Unknown GEBCO version: {version}. "
582
- f"Valid versions: {list(GEBCO_PARAMETERS.keys())}"
574
+ f"Unknown GEBCO version: {version}. " f"Valid versions: {list(GEBCO_PARAMETERS.keys())}"
583
575
  )
584
576
 
585
577
  return _load_gebco_cached(version, lat_min, lat_max, lon_min, lon_max)
@@ -184,10 +184,7 @@ 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 = (
188
- np.sin(dlat / 2) ** 2
189
- + np.cos(obs_lat) * np.cos(sample_lats) * np.sin(dlon / 2) ** 2
190
- )
187
+ a = np.sin(dlat / 2) ** 2 + np.cos(obs_lat) * np.cos(sample_lats) * np.sin(dlon / 2) ** 2
191
188
  c = 2 * np.arctan2(np.sqrt(a), np.sqrt(1 - a))
192
189
  distances = earth_radius * c
193
190
 
@@ -250,9 +247,7 @@ def line_of_sight(
250
247
  else:
251
248
  grazing_angle = -np.pi / 2
252
249
 
253
- return LOSResult(
254
- visible, grazing_angle, obstacle_distance, obstacle_elevation, min_clearance
255
- )
250
+ return LOSResult(visible, grazing_angle, obstacle_distance, obstacle_elevation, min_clearance)
256
251
 
257
252
 
258
253
  def viewshed(
@@ -16,20 +16,9 @@ from pytcl.trackers.hypothesis import (
16
16
  n_scan_prune,
17
17
  prune_hypotheses_by_probability,
18
18
  )
19
- from pytcl.trackers.mht import (
20
- MHTConfig,
21
- MHTResult,
22
- MHTTracker,
23
- )
24
- from pytcl.trackers.multi_target import (
25
- MultiTargetTracker,
26
- Track,
27
- TrackStatus,
28
- )
29
- from pytcl.trackers.single_target import (
30
- SingleTargetTracker,
31
- TrackState,
32
- )
19
+ from pytcl.trackers.mht import MHTConfig, MHTResult, MHTTracker
20
+ from pytcl.trackers.multi_target import MultiTargetTracker, Track, TrackStatus
21
+ from pytcl.trackers.single_target import SingleTargetTracker, TrackState
33
22
 
34
23
  __all__ = [
35
24
  # Single target
@@ -294,10 +294,7 @@ 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 (
298
- hyp_tracks_at_cutoff == best_tracks_at_cutoff
299
- or len(best_tracks_at_cutoff) == 0
300
- ):
297
+ if hyp_tracks_at_cutoff == best_tracks_at_cutoff or len(best_tracks_at_cutoff) == 0:
301
298
  pruned.append(hyp)
302
299
 
303
300
  # Renormalize probabilities
@@ -515,9 +512,7 @@ class HypothesisTree:
515
512
  new_hypotheses = []
516
513
 
517
514
  for hyp in self.hypotheses:
518
- for assoc_idx, (assoc, likelihood) in enumerate(
519
- zip(associations, likelihoods)
520
- ):
515
+ for assoc_idx, (assoc, likelihood) in enumerate(zip(associations, likelihoods)):
521
516
  # Compute new hypothesis probability
522
517
  new_prob = hyp.probability * likelihood
523
518
 
pytcl/trackers/mht.py CHANGED
@@ -223,9 +223,7 @@ 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(
227
- predicted_tracks, Z
228
- )
226
+ gated, likelihood_matrix = self._compute_gating_and_likelihoods(predicted_tracks, Z)
229
227
 
230
228
  # Generate associations for each hypothesis
231
229
  track_id_list = list(predicted_tracks.keys())
@@ -270,9 +268,7 @@ class MHTTracker:
270
268
 
271
269
  # Update tracks based on associations
272
270
  new_tracks_per_assoc: Dict[int, List[MHTTrack]] = {}
273
- updated_tracks: Dict[int, Dict[int, MHTTrack]] = (
274
- {}
275
- ) # assoc_idx -> track_id -> track
271
+ updated_tracks: Dict[int, Dict[int, MHTTrack]] = {} # assoc_idx -> track_id -> track
276
272
 
277
273
  for assoc_idx, assoc in enumerate(associations):
278
274
  updated_tracks[assoc_idx] = {}
@@ -295,9 +291,7 @@ class MHTTracker:
295
291
  updated_tracks[assoc_idx][track_id] = upd_track
296
292
 
297
293
  # Handle unassigned measurements -> new tracks
298
- assigned_meas = set(
299
- meas_idx for meas_idx in assoc.values() if meas_idx >= 0
300
- )
294
+ assigned_meas = set(meas_idx for meas_idx in assoc.values() if meas_idx >= 0)
301
295
  for j in range(n_meas):
302
296
  if j not in assigned_meas:
303
297
  new_track = self._initiate_track(Z[j], j)
@@ -408,9 +402,7 @@ class MHTTracker:
408
402
 
409
403
  # Clutter and new track terms for unassigned measurements
410
404
  n_unassigned = len(Z) - len(used_meas)
411
- likelihood *= (
412
- self.config.clutter_density + self.config.new_track_weight
413
- ) ** n_unassigned
405
+ likelihood *= (self.config.clutter_density + self.config.new_track_weight) ** n_unassigned
414
406
 
415
407
  return likelihood
416
408
 
@@ -534,9 +526,7 @@ class MHTTracker:
534
526
  new_hypotheses = []
535
527
 
536
528
  for hyp in self.hypothesis_tree.hypotheses:
537
- for assoc_idx, (assoc, likelihood) in enumerate(
538
- zip(associations, likelihoods)
539
- ):
529
+ for assoc_idx, (assoc, likelihood) in enumerate(zip(associations, likelihoods)):
540
530
  # Compute new hypothesis probability
541
531
  new_prob = hyp.probability * likelihood
542
532
 
@@ -11,10 +11,7 @@ from typing import Callable, List, NamedTuple, Optional
11
11
  import numpy as np
12
12
  from numpy.typing import ArrayLike, NDArray
13
13
 
14
- from pytcl.assignment_algorithms import (
15
- chi2_gate_threshold,
16
- gnn_association,
17
- )
14
+ from pytcl.assignment_algorithms import chi2_gate_threshold, gnn_association
18
15
 
19
16
 
20
17
  class TrackStatus(Enum):