nrl-tracker 0.22.2__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.
- {nrl_tracker-0.22.2.dist-info → nrl_tracker-0.22.4.dist-info}/METADATA +1 -1
- {nrl_tracker-0.22.2.dist-info → nrl_tracker-0.22.4.dist-info}/RECORD +69 -69
- pytcl/__init__.py +1 -1
- pytcl/assignment_algorithms/gating.py +3 -3
- pytcl/assignment_algorithms/jpda.py +12 -4
- pytcl/assignment_algorithms/two_dimensional/kbest.py +3 -1
- pytcl/astronomical/ephemerides.py +17 -9
- pytcl/astronomical/lambert.py +14 -4
- pytcl/astronomical/orbital_mechanics.py +3 -1
- pytcl/astronomical/reference_frames.py +3 -1
- pytcl/astronomical/relativity.py +8 -2
- pytcl/atmosphere/models.py +3 -1
- pytcl/clustering/gaussian_mixture.py +8 -4
- pytcl/clustering/hierarchical.py +5 -1
- pytcl/clustering/kmeans.py +3 -1
- pytcl/containers/cluster_set.py +9 -3
- pytcl/containers/measurement_set.py +6 -2
- pytcl/containers/rtree.py +5 -2
- pytcl/coordinate_systems/conversions/geodetic.py +15 -3
- pytcl/coordinate_systems/projections/projections.py +38 -11
- pytcl/coordinate_systems/rotations/rotations.py +3 -1
- pytcl/core/array_utils.py +4 -1
- pytcl/core/constants.py +3 -1
- pytcl/core/validation.py +17 -6
- pytcl/dynamic_estimation/imm.py +9 -3
- pytcl/dynamic_estimation/kalman/square_root.py +6 -2
- pytcl/dynamic_estimation/particle_filters/bootstrap.py +6 -2
- pytcl/dynamic_estimation/smoothers.py +3 -1
- pytcl/dynamic_models/process_noise/polynomial.py +6 -2
- pytcl/gravity/clenshaw.py +6 -2
- pytcl/gravity/egm.py +3 -1
- pytcl/gravity/models.py +3 -1
- pytcl/gravity/spherical_harmonics.py +10 -4
- pytcl/gravity/tides.py +16 -7
- pytcl/magnetism/emm.py +12 -3
- pytcl/magnetism/wmm.py +9 -2
- pytcl/mathematical_functions/basic_matrix/decompositions.py +3 -1
- pytcl/mathematical_functions/combinatorics/combinatorics.py +3 -1
- pytcl/mathematical_functions/geometry/geometry.py +12 -4
- pytcl/mathematical_functions/interpolation/interpolation.py +3 -1
- pytcl/mathematical_functions/signal_processing/detection.py +6 -3
- pytcl/mathematical_functions/signal_processing/filters.py +6 -2
- pytcl/mathematical_functions/signal_processing/matched_filter.py +4 -2
- pytcl/mathematical_functions/special_functions/elliptic.py +3 -1
- pytcl/mathematical_functions/special_functions/gamma_functions.py +3 -1
- pytcl/mathematical_functions/special_functions/lambert_w.py +3 -1
- pytcl/mathematical_functions/statistics/distributions.py +36 -12
- pytcl/mathematical_functions/statistics/estimators.py +3 -1
- pytcl/mathematical_functions/transforms/stft.py +9 -3
- pytcl/mathematical_functions/transforms/wavelets.py +18 -6
- pytcl/navigation/geodesy.py +31 -9
- pytcl/navigation/great_circle.py +12 -4
- pytcl/navigation/ins.py +8 -2
- pytcl/navigation/ins_gnss.py +25 -7
- pytcl/navigation/rhumb.py +10 -3
- pytcl/performance_evaluation/track_metrics.py +3 -1
- pytcl/plotting/coordinates.py +17 -5
- pytcl/plotting/metrics.py +9 -3
- pytcl/plotting/tracks.py +11 -3
- pytcl/static_estimation/least_squares.py +2 -1
- pytcl/static_estimation/maximum_likelihood.py +3 -3
- pytcl/terrain/dem.py +8 -2
- pytcl/terrain/loaders.py +13 -5
- pytcl/terrain/visibility.py +7 -2
- pytcl/trackers/hypothesis.py +7 -2
- pytcl/trackers/mht.py +15 -5
- {nrl_tracker-0.22.2.dist-info → nrl_tracker-0.22.4.dist-info}/LICENSE +0 -0
- {nrl_tracker-0.22.2.dist-info → nrl_tracker-0.22.4.dist-info}/WHEEL +0 -0
- {nrl_tracker-0.22.2.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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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 "
|
|
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(
|
|
658
|
-
|
|
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
|
|
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 =
|
|
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"
|
|
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(
|
|
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(
|
|
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(
|
|
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}. "
|
|
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)
|
pytcl/terrain/visibility.py
CHANGED
|
@@ -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 =
|
|
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(
|
|
253
|
+
return LOSResult(
|
|
254
|
+
visible, grazing_angle, obstacle_distance, obstacle_elevation, min_clearance
|
|
255
|
+
)
|
|
251
256
|
|
|
252
257
|
|
|
253
258
|
def viewshed(
|
pytcl/trackers/hypothesis.py
CHANGED
|
@@ -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
|
|
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(
|
|
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(
|
|
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]] =
|
|
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(
|
|
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 *= (
|
|
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(
|
|
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
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|