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.
- {nrl_tracker-0.21.5.dist-info → nrl_tracker-0.22.1.dist-info}/METADATA +2 -2
- {nrl_tracker-0.21.5.dist-info → nrl_tracker-0.22.1.dist-info}/RECORD +84 -82
- pytcl/__init__.py +1 -1
- pytcl/assignment_algorithms/data_association.py +2 -7
- pytcl/assignment_algorithms/gating.py +3 -3
- pytcl/assignment_algorithms/jpda.py +4 -12
- pytcl/assignment_algorithms/two_dimensional/kbest.py +1 -3
- pytcl/astronomical/__init__.py +60 -7
- pytcl/astronomical/ephemerides.py +522 -0
- pytcl/astronomical/lambert.py +4 -14
- pytcl/astronomical/orbital_mechanics.py +1 -3
- pytcl/astronomical/reference_frames.py +1 -3
- pytcl/astronomical/relativity.py +466 -0
- pytcl/atmosphere/__init__.py +2 -2
- pytcl/atmosphere/models.py +1 -3
- pytcl/clustering/gaussian_mixture.py +4 -8
- pytcl/clustering/hierarchical.py +1 -5
- pytcl/clustering/kmeans.py +1 -3
- pytcl/containers/__init__.py +4 -21
- pytcl/containers/cluster_set.py +4 -19
- pytcl/containers/measurement_set.py +3 -15
- pytcl/containers/rtree.py +2 -5
- pytcl/coordinate_systems/conversions/geodetic.py +3 -15
- pytcl/coordinate_systems/projections/__init__.py +4 -2
- pytcl/coordinate_systems/projections/projections.py +11 -38
- pytcl/coordinate_systems/rotations/rotations.py +1 -3
- pytcl/core/array_utils.py +1 -4
- pytcl/core/constants.py +1 -3
- pytcl/core/validation.py +6 -17
- pytcl/dynamic_estimation/imm.py +4 -13
- pytcl/dynamic_estimation/kalman/extended.py +1 -4
- pytcl/dynamic_estimation/kalman/square_root.py +2 -6
- pytcl/dynamic_estimation/kalman/unscented.py +1 -4
- pytcl/dynamic_estimation/particle_filters/bootstrap.py +2 -6
- pytcl/dynamic_estimation/smoothers.py +2 -8
- pytcl/dynamic_models/discrete_time/__init__.py +1 -5
- pytcl/dynamic_models/process_noise/__init__.py +1 -5
- pytcl/dynamic_models/process_noise/polynomial.py +2 -6
- pytcl/gravity/clenshaw.py +2 -6
- pytcl/gravity/egm.py +1 -3
- pytcl/gravity/models.py +1 -3
- pytcl/gravity/spherical_harmonics.py +4 -10
- pytcl/gravity/tides.py +7 -16
- pytcl/magnetism/__init__.py +3 -14
- pytcl/magnetism/emm.py +3 -12
- pytcl/magnetism/wmm.py +2 -9
- pytcl/mathematical_functions/basic_matrix/decompositions.py +1 -3
- pytcl/mathematical_functions/combinatorics/combinatorics.py +1 -3
- pytcl/mathematical_functions/geometry/geometry.py +4 -12
- pytcl/mathematical_functions/interpolation/__init__.py +2 -2
- pytcl/mathematical_functions/interpolation/interpolation.py +1 -3
- pytcl/mathematical_functions/signal_processing/detection.py +3 -6
- pytcl/mathematical_functions/signal_processing/filters.py +2 -6
- pytcl/mathematical_functions/signal_processing/matched_filter.py +2 -4
- pytcl/mathematical_functions/special_functions/__init__.py +2 -2
- pytcl/mathematical_functions/special_functions/elliptic.py +1 -3
- pytcl/mathematical_functions/special_functions/gamma_functions.py +1 -3
- pytcl/mathematical_functions/special_functions/lambert_w.py +1 -3
- pytcl/mathematical_functions/statistics/distributions.py +12 -36
- pytcl/mathematical_functions/statistics/estimators.py +1 -3
- pytcl/mathematical_functions/transforms/stft.py +3 -9
- pytcl/mathematical_functions/transforms/wavelets.py +6 -18
- pytcl/navigation/__init__.py +14 -10
- pytcl/navigation/geodesy.py +9 -31
- pytcl/navigation/great_circle.py +4 -12
- pytcl/navigation/ins.py +3 -13
- pytcl/navigation/ins_gnss.py +7 -25
- pytcl/navigation/rhumb.py +3 -10
- pytcl/performance_evaluation/track_metrics.py +1 -3
- pytcl/plotting/coordinates.py +5 -17
- pytcl/plotting/metrics.py +3 -9
- pytcl/plotting/tracks.py +3 -11
- pytcl/static_estimation/least_squares.py +1 -2
- pytcl/static_estimation/maximum_likelihood.py +3 -3
- pytcl/terrain/dem.py +2 -8
- pytcl/terrain/loaders.py +5 -13
- pytcl/terrain/visibility.py +2 -7
- pytcl/trackers/__init__.py +3 -14
- pytcl/trackers/hypothesis.py +2 -7
- pytcl/trackers/mht.py +5 -15
- pytcl/trackers/multi_target.py +1 -4
- {nrl_tracker-0.21.5.dist-info → nrl_tracker-0.22.1.dist-info}/LICENSE +0 -0
- {nrl_tracker-0.21.5.dist-info → nrl_tracker-0.22.1.dist-info}/WHEEL +0 -0
- {nrl_tracker-0.21.5.dist-info → nrl_tracker-0.22.1.dist-info}/top_level.txt +0 -0
pytcl/navigation/great_circle.py
CHANGED
|
@@ -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
|
pytcl/navigation/ins_gnss.py
CHANGED
|
@@ -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
|
-
|
|
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(
|
pytcl/plotting/coordinates.py
CHANGED
|
@@ -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
|
-
|
|
659
|
-
)
|
|
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)
|
pytcl/terrain/visibility.py
CHANGED
|
@@ -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(
|
pytcl/trackers/__init__.py
CHANGED
|
@@ -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
|
-
|
|
21
|
-
|
|
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
|
pytcl/trackers/hypothesis.py
CHANGED
|
@@ -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
|
|
pytcl/trackers/multi_target.py
CHANGED
|
@@ -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):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|