nrl-tracker 1.9.0__py3-none-any.whl → 1.9.2__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-1.9.0.dist-info → nrl_tracker-1.9.2.dist-info}/METADATA +4 -4
- {nrl_tracker-1.9.0.dist-info → nrl_tracker-1.9.2.dist-info}/RECORD +61 -60
- pytcl/__init__.py +2 -2
- pytcl/assignment_algorithms/gating.py +18 -0
- pytcl/assignment_algorithms/jpda.py +56 -0
- pytcl/assignment_algorithms/nd_assignment.py +65 -0
- pytcl/assignment_algorithms/network_flow.py +40 -0
- pytcl/astronomical/ephemerides.py +18 -0
- pytcl/astronomical/orbital_mechanics.py +131 -0
- pytcl/atmosphere/ionosphere.py +44 -0
- pytcl/atmosphere/models.py +29 -0
- pytcl/clustering/dbscan.py +9 -0
- pytcl/clustering/gaussian_mixture.py +20 -0
- pytcl/clustering/hierarchical.py +29 -0
- pytcl/clustering/kmeans.py +9 -0
- pytcl/coordinate_systems/conversions/geodetic.py +46 -0
- pytcl/coordinate_systems/conversions/spherical.py +35 -0
- pytcl/coordinate_systems/rotations/rotations.py +147 -0
- pytcl/core/__init__.py +16 -0
- pytcl/core/maturity.py +346 -0
- pytcl/core/optional_deps.py +1 -1
- pytcl/dynamic_estimation/gaussian_sum_filter.py +55 -0
- pytcl/dynamic_estimation/imm.py +29 -0
- pytcl/dynamic_estimation/information_filter.py +64 -0
- pytcl/dynamic_estimation/kalman/extended.py +56 -0
- pytcl/dynamic_estimation/kalman/linear.py +69 -0
- pytcl/dynamic_estimation/kalman/unscented.py +81 -0
- pytcl/dynamic_estimation/particle_filters/bootstrap.py +146 -0
- pytcl/dynamic_estimation/rbpf.py +51 -0
- pytcl/dynamic_estimation/smoothers.py +58 -0
- pytcl/dynamic_models/continuous_time/dynamics.py +104 -0
- pytcl/dynamic_models/discrete_time/coordinated_turn.py +6 -0
- pytcl/dynamic_models/discrete_time/singer.py +12 -0
- pytcl/dynamic_models/process_noise/coordinated_turn.py +46 -0
- pytcl/dynamic_models/process_noise/polynomial.py +6 -0
- pytcl/dynamic_models/process_noise/singer.py +52 -0
- pytcl/gravity/clenshaw.py +60 -0
- pytcl/gravity/egm.py +47 -0
- pytcl/gravity/models.py +34 -0
- pytcl/gravity/spherical_harmonics.py +73 -0
- pytcl/gravity/tides.py +34 -0
- pytcl/mathematical_functions/numerical_integration/quadrature.py +85 -0
- pytcl/mathematical_functions/special_functions/bessel.py +55 -0
- pytcl/mathematical_functions/special_functions/elliptic.py +42 -0
- pytcl/mathematical_functions/special_functions/error_functions.py +49 -0
- pytcl/mathematical_functions/special_functions/gamma_functions.py +43 -0
- pytcl/mathematical_functions/special_functions/lambert_w.py +5 -0
- pytcl/mathematical_functions/special_functions/marcum_q.py +16 -0
- pytcl/navigation/geodesy.py +101 -2
- pytcl/navigation/great_circle.py +71 -0
- pytcl/navigation/rhumb.py +74 -0
- pytcl/performance_evaluation/estimation_metrics.py +70 -0
- pytcl/performance_evaluation/track_metrics.py +30 -0
- pytcl/static_estimation/maximum_likelihood.py +54 -0
- pytcl/static_estimation/robust.py +57 -0
- pytcl/terrain/dem.py +69 -0
- pytcl/terrain/visibility.py +65 -0
- pytcl/trackers/hypothesis.py +65 -0
- {nrl_tracker-1.9.0.dist-info → nrl_tracker-1.9.2.dist-info}/LICENSE +0 -0
- {nrl_tracker-1.9.0.dist-info → nrl_tracker-1.9.2.dist-info}/WHEEL +0 -0
- {nrl_tracker-1.9.0.dist-info → nrl_tracker-1.9.2.dist-info}/top_level.txt +0 -0
|
@@ -48,6 +48,15 @@ def resample_multinomial(
|
|
|
48
48
|
-------
|
|
49
49
|
resampled : ndarray
|
|
50
50
|
Resampled particles with uniform weights.
|
|
51
|
+
|
|
52
|
+
Examples
|
|
53
|
+
--------
|
|
54
|
+
>>> rng = np.random.default_rng(42)
|
|
55
|
+
>>> particles = np.array([[1.0], [2.0], [3.0], [4.0]])
|
|
56
|
+
>>> weights = np.array([0.1, 0.1, 0.1, 0.7]) # particle 4 dominant
|
|
57
|
+
>>> resampled = resample_multinomial(particles, weights, rng)
|
|
58
|
+
>>> resampled.shape
|
|
59
|
+
(4, 1)
|
|
51
60
|
"""
|
|
52
61
|
if rng is None:
|
|
53
62
|
rng = np.random.default_rng()
|
|
@@ -80,6 +89,15 @@ def resample_systematic(
|
|
|
80
89
|
-------
|
|
81
90
|
resampled : ndarray
|
|
82
91
|
Resampled particles.
|
|
92
|
+
|
|
93
|
+
Examples
|
|
94
|
+
--------
|
|
95
|
+
>>> rng = np.random.default_rng(42)
|
|
96
|
+
>>> particles = np.array([[0.0], [1.0], [2.0], [3.0]])
|
|
97
|
+
>>> weights = np.array([0.25, 0.25, 0.25, 0.25])
|
|
98
|
+
>>> resampled = resample_systematic(particles, weights, rng)
|
|
99
|
+
>>> resampled.shape
|
|
100
|
+
(4, 1)
|
|
83
101
|
"""
|
|
84
102
|
if rng is None:
|
|
85
103
|
rng = np.random.default_rng()
|
|
@@ -145,6 +163,18 @@ def resample_residual(
|
|
|
145
163
|
-------
|
|
146
164
|
resampled : ndarray
|
|
147
165
|
Resampled particles.
|
|
166
|
+
|
|
167
|
+
Examples
|
|
168
|
+
--------
|
|
169
|
+
>>> rng = np.random.default_rng(42)
|
|
170
|
+
>>> particles = np.array([[1.0], [2.0], [3.0], [4.0]])
|
|
171
|
+
>>> weights = np.array([0.1, 0.2, 0.3, 0.4])
|
|
172
|
+
>>> resampled = resample_residual(particles, weights, rng)
|
|
173
|
+
>>> resampled.shape
|
|
174
|
+
(4, 1)
|
|
175
|
+
>>> # High-weight particles are copied deterministically
|
|
176
|
+
>>> np.sum(resampled == 4.0) >= 1
|
|
177
|
+
True
|
|
148
178
|
"""
|
|
149
179
|
if rng is None:
|
|
150
180
|
rng = np.random.default_rng()
|
|
@@ -184,6 +214,17 @@ def effective_sample_size(weights: NDArray[np.floating]) -> float:
|
|
|
184
214
|
ess : float
|
|
185
215
|
Effective sample size, in range [1, N].
|
|
186
216
|
|
|
217
|
+
Examples
|
|
218
|
+
--------
|
|
219
|
+
>>> # Uniform weights -> ESS = N
|
|
220
|
+
>>> weights = np.array([0.25, 0.25, 0.25, 0.25])
|
|
221
|
+
>>> effective_sample_size(weights)
|
|
222
|
+
4.0
|
|
223
|
+
>>> # Degenerate weights -> ESS approaches 1
|
|
224
|
+
>>> weights = np.array([0.97, 0.01, 0.01, 0.01])
|
|
225
|
+
>>> effective_sample_size(weights)
|
|
226
|
+
1.06...
|
|
227
|
+
|
|
187
228
|
Notes
|
|
188
229
|
-----
|
|
189
230
|
ESS = 1 / sum(w_i^2)
|
|
@@ -220,6 +261,29 @@ def bootstrap_pf_predict(
|
|
|
220
261
|
-------
|
|
221
262
|
particles_pred : ndarray
|
|
222
263
|
Predicted particles.
|
|
264
|
+
|
|
265
|
+
Examples
|
|
266
|
+
--------
|
|
267
|
+
Predict particles through constant velocity dynamics:
|
|
268
|
+
|
|
269
|
+
>>> import numpy as np
|
|
270
|
+
>>> rng = np.random.default_rng(42)
|
|
271
|
+
>>> # 50 particles for 2D state [position, velocity]
|
|
272
|
+
>>> particles = np.column_stack([
|
|
273
|
+
... np.zeros(50), # position
|
|
274
|
+
... np.ones(50) # velocity = 1
|
|
275
|
+
... ])
|
|
276
|
+
>>> dt = 0.1
|
|
277
|
+
>>> # x_next = [pos + vel*dt, vel]
|
|
278
|
+
>>> f = lambda x: np.array([x[0] + x[1] * dt, x[1]])
|
|
279
|
+
>>> # Process noise sampler
|
|
280
|
+
>>> Q_sample = lambda n, r: r.normal(0, 0.01, (n, 2))
|
|
281
|
+
>>> pred = bootstrap_pf_predict(particles, f, Q_sample, rng)
|
|
282
|
+
>>> pred.shape
|
|
283
|
+
(50, 2)
|
|
284
|
+
>>> # Positions moved forward by ~0.1
|
|
285
|
+
>>> np.mean(pred[:, 0]) # doctest: +ELLIPSIS
|
|
286
|
+
0.1...
|
|
223
287
|
"""
|
|
224
288
|
if rng is None:
|
|
225
289
|
rng = np.random.default_rng()
|
|
@@ -266,6 +330,25 @@ def bootstrap_pf_update(
|
|
|
266
330
|
Updated normalized weights.
|
|
267
331
|
log_likelihood : float
|
|
268
332
|
Log marginal likelihood of measurement.
|
|
333
|
+
|
|
334
|
+
Examples
|
|
335
|
+
--------
|
|
336
|
+
Update particle weights with a position measurement:
|
|
337
|
+
|
|
338
|
+
>>> import numpy as np
|
|
339
|
+
>>> # 4 particles at different positions
|
|
340
|
+
>>> particles = np.array([[0.0], [1.0], [2.0], [3.0]])
|
|
341
|
+
>>> weights = np.ones(4) / 4 # uniform initial weights
|
|
342
|
+
>>> z = np.array([2.0]) # measured position
|
|
343
|
+
>>> # Gaussian likelihood centered on measurement
|
|
344
|
+
>>> def likelihood(z, x):
|
|
345
|
+
... return np.exp(-0.5 * (z[0] - x[0])**2)
|
|
346
|
+
>>> new_weights, log_lik = bootstrap_pf_update(particles, weights, z, likelihood)
|
|
347
|
+
>>> # Particle at x=2 should have highest weight
|
|
348
|
+
>>> np.argmax(new_weights)
|
|
349
|
+
2
|
|
350
|
+
>>> np.sum(new_weights) # weights sum to 1
|
|
351
|
+
1.0
|
|
269
352
|
"""
|
|
270
353
|
z = np.asarray(z, dtype=np.float64)
|
|
271
354
|
N = len(particles)
|
|
@@ -312,6 +395,19 @@ def gaussian_likelihood(
|
|
|
312
395
|
-------
|
|
313
396
|
likelihood : float
|
|
314
397
|
p(z|x) = N(z; z_pred, R).
|
|
398
|
+
|
|
399
|
+
Examples
|
|
400
|
+
--------
|
|
401
|
+
>>> z = np.array([1.0, 2.0]) # actual measurement
|
|
402
|
+
>>> z_pred = np.array([1.0, 2.0]) # perfect prediction
|
|
403
|
+
>>> R = np.eye(2) * 0.1 # measurement noise covariance
|
|
404
|
+
>>> lik = gaussian_likelihood(z, z_pred, R)
|
|
405
|
+
>>> lik > 0
|
|
406
|
+
True
|
|
407
|
+
>>> # Zero innovation -> maximum likelihood
|
|
408
|
+
>>> z_off = np.array([10.0, 10.0])
|
|
409
|
+
>>> gaussian_likelihood(z_off, z_pred, R) < lik
|
|
410
|
+
True
|
|
315
411
|
"""
|
|
316
412
|
m = len(z)
|
|
317
413
|
innovation = z - z_pred
|
|
@@ -366,6 +462,30 @@ def bootstrap_pf_step(
|
|
|
366
462
|
-------
|
|
367
463
|
state : ParticleState
|
|
368
464
|
Updated particles and weights.
|
|
465
|
+
|
|
466
|
+
Examples
|
|
467
|
+
--------
|
|
468
|
+
Track a 1D random walk with position measurements:
|
|
469
|
+
|
|
470
|
+
>>> import numpy as np
|
|
471
|
+
>>> rng = np.random.default_rng(42)
|
|
472
|
+
>>> # 100 particles for 1D state
|
|
473
|
+
>>> particles = rng.normal(0, 1, (100, 1))
|
|
474
|
+
>>> weights = np.ones(100) / 100
|
|
475
|
+
>>> # Simple dynamics: x_k+1 = x_k + w
|
|
476
|
+
>>> f = lambda x: x
|
|
477
|
+
>>> h = lambda x: x # measure position directly
|
|
478
|
+
>>> Q_sample = lambda n, r: r.normal(0, 0.1, (n, 1))
|
|
479
|
+
>>> R = np.array([[0.5]]) # measurement noise
|
|
480
|
+
>>> z = np.array([0.5]) # measurement
|
|
481
|
+
>>> state = bootstrap_pf_step(particles, weights, z, f, h, Q_sample, R, rng=rng)
|
|
482
|
+
>>> state.particles.shape
|
|
483
|
+
(100, 1)
|
|
484
|
+
|
|
485
|
+
See Also
|
|
486
|
+
--------
|
|
487
|
+
bootstrap_pf_predict : Prediction step only.
|
|
488
|
+
bootstrap_pf_update : Update step only.
|
|
369
489
|
"""
|
|
370
490
|
if rng is None:
|
|
371
491
|
rng = np.random.default_rng()
|
|
@@ -420,6 +540,13 @@ def particle_mean(
|
|
|
420
540
|
-------
|
|
421
541
|
mean : ndarray
|
|
422
542
|
Weighted mean estimate.
|
|
543
|
+
|
|
544
|
+
Examples
|
|
545
|
+
--------
|
|
546
|
+
>>> particles = np.array([[0.0], [1.0], [2.0], [3.0]])
|
|
547
|
+
>>> weights = np.array([0.1, 0.2, 0.3, 0.4])
|
|
548
|
+
>>> particle_mean(particles, weights)
|
|
549
|
+
array([2.])
|
|
423
550
|
"""
|
|
424
551
|
return np.sum(weights[:, np.newaxis] * particles, axis=0)
|
|
425
552
|
|
|
@@ -470,6 +597,14 @@ def particle_covariance(
|
|
|
470
597
|
-------
|
|
471
598
|
cov : ndarray
|
|
472
599
|
Weighted covariance.
|
|
600
|
+
|
|
601
|
+
Examples
|
|
602
|
+
--------
|
|
603
|
+
>>> particles = np.array([[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]])
|
|
604
|
+
>>> weights = np.array([0.25, 0.25, 0.25, 0.25])
|
|
605
|
+
>>> cov = particle_covariance(particles, weights)
|
|
606
|
+
>>> cov.shape
|
|
607
|
+
(2, 2)
|
|
473
608
|
"""
|
|
474
609
|
if mean is None:
|
|
475
610
|
mean = particle_mean(particles, weights)
|
|
@@ -505,6 +640,17 @@ def initialize_particles(
|
|
|
505
640
|
-------
|
|
506
641
|
state : ParticleState
|
|
507
642
|
Initial particles with uniform weights.
|
|
643
|
+
|
|
644
|
+
Examples
|
|
645
|
+
--------
|
|
646
|
+
>>> rng = np.random.default_rng(42)
|
|
647
|
+
>>> x0 = np.array([0.0, 0.0])
|
|
648
|
+
>>> P0 = np.eye(2) * 0.1
|
|
649
|
+
>>> state = initialize_particles(x0, P0, N=100, rng=rng)
|
|
650
|
+
>>> state.particles.shape
|
|
651
|
+
(100, 2)
|
|
652
|
+
>>> np.allclose(state.weights, 0.01) # uniform 1/N
|
|
653
|
+
True
|
|
508
654
|
"""
|
|
509
655
|
if rng is None:
|
|
510
656
|
rng = np.random.default_rng()
|
pytcl/dynamic_estimation/rbpf.py
CHANGED
|
@@ -501,6 +501,32 @@ def rbpf_predict(
|
|
|
501
501
|
-------
|
|
502
502
|
list[RBPFParticle]
|
|
503
503
|
Predicted particles
|
|
504
|
+
|
|
505
|
+
Examples
|
|
506
|
+
--------
|
|
507
|
+
>>> import numpy as np
|
|
508
|
+
>>> from pytcl.dynamic_estimation.rbpf import RBPFParticle
|
|
509
|
+
>>> np.random.seed(42)
|
|
510
|
+
>>> # 3 particles with nonlinear bearing and linear position
|
|
511
|
+
>>> particles = [
|
|
512
|
+
... RBPFParticle(y=np.array([0.1]), x=np.array([0.0, 1.0]),
|
|
513
|
+
... P=np.eye(2) * 0.5, w=1/3),
|
|
514
|
+
... RBPFParticle(y=np.array([0.0]), x=np.array([0.0, 1.0]),
|
|
515
|
+
... P=np.eye(2) * 0.5, w=1/3),
|
|
516
|
+
... RBPFParticle(y=np.array([-0.1]), x=np.array([0.0, 1.0]),
|
|
517
|
+
... P=np.eye(2) * 0.5, w=1/3),
|
|
518
|
+
... ]
|
|
519
|
+
>>> # Nonlinear dynamics for bearing
|
|
520
|
+
>>> g = lambda y: y # bearing stays constant
|
|
521
|
+
>>> G = np.eye(1)
|
|
522
|
+
>>> Qy = np.eye(1) * 0.01
|
|
523
|
+
>>> # Linear dynamics for position
|
|
524
|
+
>>> f = lambda x, y: np.array([x[0] + x[1] * 0.1, x[1]])
|
|
525
|
+
>>> F = np.array([[1, 0.1], [0, 1]])
|
|
526
|
+
>>> Qx = np.eye(2) * 0.01
|
|
527
|
+
>>> predicted = rbpf_predict(particles, g, G, Qy, f, F, Qx)
|
|
528
|
+
>>> len(predicted)
|
|
529
|
+
3
|
|
504
530
|
"""
|
|
505
531
|
new_particles = []
|
|
506
532
|
|
|
@@ -553,6 +579,31 @@ def rbpf_update(
|
|
|
553
579
|
-------
|
|
554
580
|
list[RBPFParticle]
|
|
555
581
|
Updated particles with adapted weights
|
|
582
|
+
|
|
583
|
+
Examples
|
|
584
|
+
--------
|
|
585
|
+
>>> import numpy as np
|
|
586
|
+
>>> from pytcl.dynamic_estimation.rbpf import RBPFParticle
|
|
587
|
+
>>> # 3 particles at different bearings
|
|
588
|
+
>>> particles = [
|
|
589
|
+
... RBPFParticle(y=np.array([0.5]), x=np.array([1.0, 0.0]),
|
|
590
|
+
... P=np.eye(2) * 0.5, w=1/3),
|
|
591
|
+
... RBPFParticle(y=np.array([0.0]), x=np.array([1.0, 0.0]),
|
|
592
|
+
... P=np.eye(2) * 0.5, w=1/3),
|
|
593
|
+
... RBPFParticle(y=np.array([-0.5]), x=np.array([1.0, 0.0]),
|
|
594
|
+
... P=np.eye(2) * 0.5, w=1/3),
|
|
595
|
+
... ]
|
|
596
|
+
>>> # Position measurement
|
|
597
|
+
>>> z = np.array([1.1])
|
|
598
|
+
>>> h = lambda x, y: np.array([x[0]]) # measure position
|
|
599
|
+
>>> H = np.array([[1, 0]])
|
|
600
|
+
>>> R = np.array([[0.1]])
|
|
601
|
+
>>> updated = rbpf_update(particles, z, h, H, R)
|
|
602
|
+
>>> len(updated)
|
|
603
|
+
3
|
|
604
|
+
>>> # Weights should sum to 1
|
|
605
|
+
>>> abs(sum(p.w for p in updated) - 1.0) < 1e-10
|
|
606
|
+
True
|
|
556
607
|
"""
|
|
557
608
|
weights = np.zeros(len(particles))
|
|
558
609
|
new_particles = []
|
|
@@ -423,6 +423,25 @@ def fixed_interval_smoother(
|
|
|
423
423
|
result : RTSResult
|
|
424
424
|
Smoothed estimates over the interval.
|
|
425
425
|
|
|
426
|
+
Examples
|
|
427
|
+
--------
|
|
428
|
+
>>> import numpy as np
|
|
429
|
+
>>> # 1D constant velocity model
|
|
430
|
+
>>> x0 = np.array([0.0, 1.0]) # [position, velocity]
|
|
431
|
+
>>> P0 = np.eye(2) * 5.0
|
|
432
|
+
>>> F = np.array([[1, 1], [0, 1]])
|
|
433
|
+
>>> Q = np.array([[0.25, 0.5], [0.5, 1.0]]) * 0.01
|
|
434
|
+
>>> H = np.array([[1, 0]])
|
|
435
|
+
>>> R = np.array([[0.5]])
|
|
436
|
+
>>> measurements = [np.array([0.9]), np.array([2.1]), np.array([3.0]),
|
|
437
|
+
... np.array([4.2]), np.array([4.9])]
|
|
438
|
+
>>> result = fixed_interval_smoother(x0, P0, measurements, F, Q, H, R)
|
|
439
|
+
>>> len(result.x_smooth)
|
|
440
|
+
5
|
|
441
|
+
>>> # Smoothed estimates have lower uncertainty
|
|
442
|
+
>>> np.trace(result.P_smooth[2]) < np.trace(result.P_filt[2])
|
|
443
|
+
True
|
|
444
|
+
|
|
426
445
|
See Also
|
|
427
446
|
--------
|
|
428
447
|
rts_smoother : Full RTS smoother with time-varying parameters.
|
|
@@ -478,6 +497,26 @@ def two_filter_smoother(
|
|
|
478
497
|
result : RTSResult
|
|
479
498
|
Smoothed estimates.
|
|
480
499
|
|
|
500
|
+
Examples
|
|
501
|
+
--------
|
|
502
|
+
>>> import numpy as np
|
|
503
|
+
>>> # 1D position-velocity system
|
|
504
|
+
>>> x0_fwd = np.array([0.0, 1.0])
|
|
505
|
+
>>> P0_fwd = np.eye(2) * 5.0
|
|
506
|
+
>>> # Backward filter starts with diffuse (high uncertainty) prior
|
|
507
|
+
>>> x0_bwd = np.array([5.0, 1.0]) # approximate final state
|
|
508
|
+
>>> P0_bwd = np.eye(2) * 100.0 # diffuse prior
|
|
509
|
+
>>> F = np.array([[1, 1], [0, 1]])
|
|
510
|
+
>>> Q = np.eye(2) * 0.1
|
|
511
|
+
>>> H = np.array([[1, 0]])
|
|
512
|
+
>>> R = np.array([[1.0]])
|
|
513
|
+
>>> measurements = [np.array([0.8]), np.array([1.9]), np.array([3.1]),
|
|
514
|
+
... np.array([4.0]), np.array([5.2])]
|
|
515
|
+
>>> result = two_filter_smoother(x0_fwd, P0_fwd, x0_bwd, P0_bwd,
|
|
516
|
+
... measurements, F, Q, H, R)
|
|
517
|
+
>>> len(result.x_smooth)
|
|
518
|
+
5
|
|
519
|
+
|
|
481
520
|
Notes
|
|
482
521
|
-----
|
|
483
522
|
The two-filter smoother runs two independent Kalman filters:
|
|
@@ -645,6 +684,25 @@ def rts_smoother_single_step(
|
|
|
645
684
|
-------
|
|
646
685
|
result : SmoothedState
|
|
647
686
|
Smoothed state and covariance at current time.
|
|
687
|
+
|
|
688
|
+
Examples
|
|
689
|
+
--------
|
|
690
|
+
>>> import numpy as np
|
|
691
|
+
>>> # After running forward filter and getting smoothed estimate at k+1
|
|
692
|
+
>>> x_filt = np.array([1.0, 0.5]) # filtered state at k
|
|
693
|
+
>>> P_filt = np.eye(2) * 0.5 # filtered covariance at k
|
|
694
|
+
>>> x_pred_next = np.array([1.5, 0.5]) # predicted state at k+1
|
|
695
|
+
>>> P_pred_next = np.eye(2) * 0.6 # predicted covariance at k+1
|
|
696
|
+
>>> x_smooth_next = np.array([1.4, 0.6]) # smoothed state at k+1
|
|
697
|
+
>>> P_smooth_next = np.eye(2) * 0.3 # smoothed covariance at k+1
|
|
698
|
+
>>> F = np.array([[1, 1], [0, 1]])
|
|
699
|
+
>>> result = rts_smoother_single_step(x_filt, P_filt, x_pred_next,
|
|
700
|
+
... P_pred_next, x_smooth_next,
|
|
701
|
+
... P_smooth_next, F)
|
|
702
|
+
>>> result.x.shape
|
|
703
|
+
(2,)
|
|
704
|
+
>>> result.P.shape
|
|
705
|
+
(2, 2)
|
|
648
706
|
"""
|
|
649
707
|
x_s, P_s = kf_smooth(
|
|
650
708
|
x_filt, P_filt, x_pred_next, P_pred_next, x_smooth_next, P_smooth_next, F
|
|
@@ -79,6 +79,13 @@ def drift_constant_acceleration(
|
|
|
79
79
|
-------
|
|
80
80
|
a : ndarray
|
|
81
81
|
Drift vector.
|
|
82
|
+
|
|
83
|
+
Examples
|
|
84
|
+
--------
|
|
85
|
+
>>> x = np.array([0, 10, 1]) # 1D: pos=0, vel=10, acc=1
|
|
86
|
+
>>> a = drift_constant_acceleration(x, num_dims=1)
|
|
87
|
+
>>> a # [vel, acc, 0]
|
|
88
|
+
array([10., 1., 0.])
|
|
82
89
|
"""
|
|
83
90
|
x = np.asarray(x, dtype=np.float64)
|
|
84
91
|
n = 3 # states per dimension
|
|
@@ -124,6 +131,13 @@ def drift_singer(
|
|
|
124
131
|
-----
|
|
125
132
|
The Singer model has acceleration following a first-order Markov process:
|
|
126
133
|
da/dt = -a/tau + w(t)
|
|
134
|
+
|
|
135
|
+
Examples
|
|
136
|
+
--------
|
|
137
|
+
>>> x = np.array([0, 10, 2]) # 1D: pos=0, vel=10, acc=2
|
|
138
|
+
>>> a = drift_singer(x, tau=5.0, num_dims=1)
|
|
139
|
+
>>> a # [vel, acc, -acc/tau]
|
|
140
|
+
array([10. , 2. , -0.4])
|
|
127
141
|
"""
|
|
128
142
|
x = np.asarray(x, dtype=np.float64)
|
|
129
143
|
n = 3 # states per dimension
|
|
@@ -167,6 +181,14 @@ def drift_coordinated_turn_2d(
|
|
|
167
181
|
dy/dt = vy
|
|
168
182
|
dvy/dt = omega * vx
|
|
169
183
|
domega/dt = 0
|
|
184
|
+
|
|
185
|
+
Examples
|
|
186
|
+
--------
|
|
187
|
+
>>> # Aircraft at origin with velocity [100, 0] and turn rate 0.1 rad/s
|
|
188
|
+
>>> x = np.array([0, 100, 0, 0, 0.1])
|
|
189
|
+
>>> a = drift_coordinated_turn_2d(x)
|
|
190
|
+
>>> a # [vx, -omega*vy, vy, omega*vx, 0]
|
|
191
|
+
array([ 100., -0., 0., 10., 0.])
|
|
170
192
|
"""
|
|
171
193
|
x = np.asarray(x, dtype=np.float64)
|
|
172
194
|
pos_x, vel_x, pos_y, vel_y, omega = x
|
|
@@ -209,6 +231,18 @@ def diffusion_constant_velocity(
|
|
|
209
231
|
-------
|
|
210
232
|
D : ndarray
|
|
211
233
|
Diffusion matrix (noise enters through velocity derivative).
|
|
234
|
+
|
|
235
|
+
Examples
|
|
236
|
+
--------
|
|
237
|
+
>>> x = np.array([0, 1, 0, 2]) # 2D state (not used)
|
|
238
|
+
>>> D = diffusion_constant_velocity(x, sigma_a=0.5, num_dims=2)
|
|
239
|
+
>>> D.shape
|
|
240
|
+
(4, 2)
|
|
241
|
+
>>> D # Noise enters velocity states only
|
|
242
|
+
array([[0. , 0. ],
|
|
243
|
+
[0.5, 0. ],
|
|
244
|
+
[0. , 0. ],
|
|
245
|
+
[0. , 0.5]])
|
|
212
246
|
"""
|
|
213
247
|
n = 2 * num_dims
|
|
214
248
|
D = np.zeros((n, num_dims), dtype=np.float64)
|
|
@@ -244,6 +278,17 @@ def diffusion_constant_acceleration(
|
|
|
244
278
|
-------
|
|
245
279
|
D : ndarray
|
|
246
280
|
Diffusion matrix.
|
|
281
|
+
|
|
282
|
+
Examples
|
|
283
|
+
--------
|
|
284
|
+
>>> x = np.array([0, 1, 0.5]) # 1D state (not used)
|
|
285
|
+
>>> D = diffusion_constant_acceleration(x, sigma_j=0.1, num_dims=1)
|
|
286
|
+
>>> D.shape
|
|
287
|
+
(3, 1)
|
|
288
|
+
>>> D # Noise enters acceleration state only
|
|
289
|
+
array([[0. ],
|
|
290
|
+
[0. ],
|
|
291
|
+
[0.1]])
|
|
247
292
|
"""
|
|
248
293
|
n = 3 * num_dims
|
|
249
294
|
D = np.zeros((n, num_dims), dtype=np.float64)
|
|
@@ -286,6 +331,15 @@ def diffusion_singer(
|
|
|
286
331
|
Notes
|
|
287
332
|
-----
|
|
288
333
|
The diffusion coefficient for Singer is sqrt(2*sigma_m^2/tau).
|
|
334
|
+
|
|
335
|
+
Examples
|
|
336
|
+
--------
|
|
337
|
+
>>> x = np.array([0, 1, 0.5]) # 1D state (not used)
|
|
338
|
+
>>> D = diffusion_singer(x, sigma_m=1.0, tau=10.0, num_dims=1)
|
|
339
|
+
>>> D.shape
|
|
340
|
+
(3, 1)
|
|
341
|
+
>>> D[2, 0] # sqrt(2*1^2/10) = sqrt(0.2)
|
|
342
|
+
0.4472135954999579
|
|
289
343
|
"""
|
|
290
344
|
n = 3 * num_dims
|
|
291
345
|
D = np.zeros((n, num_dims), dtype=np.float64)
|
|
@@ -355,6 +409,17 @@ def continuous_to_discrete(
|
|
|
355
409
|
----------
|
|
356
410
|
.. [1] Van Loan, C.F., "Computing Integrals Involving the Matrix
|
|
357
411
|
Exponential", IEEE Trans. Automatic Control, 1978.
|
|
412
|
+
|
|
413
|
+
Examples
|
|
414
|
+
--------
|
|
415
|
+
>>> # 1D constant velocity model
|
|
416
|
+
>>> A = np.array([[0, 1], [0, 0]])
|
|
417
|
+
>>> G = np.array([[0], [1]])
|
|
418
|
+
>>> Q_c = np.array([[1.0]]) # acceleration variance
|
|
419
|
+
>>> F, Q_d = continuous_to_discrete(A, G, Q_c, T=0.1)
|
|
420
|
+
>>> F # State transition matrix
|
|
421
|
+
array([[1. , 0.1],
|
|
422
|
+
[0. , 1. ]])
|
|
358
423
|
"""
|
|
359
424
|
A = np.asarray(A, dtype=np.float64)
|
|
360
425
|
G = np.asarray(G, dtype=np.float64)
|
|
@@ -411,6 +476,19 @@ def discretize_lti(
|
|
|
411
476
|
Discrete-time state transition matrix.
|
|
412
477
|
G : ndarray or None
|
|
413
478
|
Discrete-time input matrix (None if B is None).
|
|
479
|
+
|
|
480
|
+
Examples
|
|
481
|
+
--------
|
|
482
|
+
>>> # 1D constant velocity: dx/dt = v, dv/dt = u (control input)
|
|
483
|
+
>>> A = np.array([[0, 1], [0, 0]])
|
|
484
|
+
>>> B = np.array([[0], [1]])
|
|
485
|
+
>>> F, G = discretize_lti(A, B, T=0.1)
|
|
486
|
+
>>> F # State transition
|
|
487
|
+
array([[1. , 0.1],
|
|
488
|
+
[0. , 1. ]])
|
|
489
|
+
>>> G # Input matrix
|
|
490
|
+
array([[0.005],
|
|
491
|
+
[0.1 ]])
|
|
414
492
|
"""
|
|
415
493
|
A = np.asarray(A, dtype=np.float64)
|
|
416
494
|
n = A.shape[0]
|
|
@@ -454,6 +532,14 @@ def state_jacobian_cv(
|
|
|
454
532
|
-------
|
|
455
533
|
A : ndarray
|
|
456
534
|
Continuous-time state matrix.
|
|
535
|
+
|
|
536
|
+
Examples
|
|
537
|
+
--------
|
|
538
|
+
>>> x = np.array([0, 1]) # 1D state (not used)
|
|
539
|
+
>>> A = state_jacobian_cv(x, num_dims=1)
|
|
540
|
+
>>> A
|
|
541
|
+
array([[0., 1.],
|
|
542
|
+
[0., 0.]])
|
|
457
543
|
"""
|
|
458
544
|
n = 2 # states per dimension
|
|
459
545
|
A_1d = np.array(
|
|
@@ -494,6 +580,15 @@ def state_jacobian_ca(
|
|
|
494
580
|
-------
|
|
495
581
|
A : ndarray
|
|
496
582
|
Continuous-time state matrix.
|
|
583
|
+
|
|
584
|
+
Examples
|
|
585
|
+
--------
|
|
586
|
+
>>> x = np.array([0, 1, 0.5]) # 1D state (not used)
|
|
587
|
+
>>> A = state_jacobian_ca(x, num_dims=1)
|
|
588
|
+
>>> A
|
|
589
|
+
array([[0., 1., 0.],
|
|
590
|
+
[0., 0., 1.],
|
|
591
|
+
[0., 0., 0.]])
|
|
497
592
|
"""
|
|
498
593
|
n = 3 # states per dimension
|
|
499
594
|
A_1d = np.array(
|
|
@@ -538,6 +633,15 @@ def state_jacobian_singer(
|
|
|
538
633
|
-------
|
|
539
634
|
A : ndarray
|
|
540
635
|
Continuous-time state matrix.
|
|
636
|
+
|
|
637
|
+
Examples
|
|
638
|
+
--------
|
|
639
|
+
>>> x = np.array([0, 1, 0.5]) # 1D state (not used)
|
|
640
|
+
>>> A = state_jacobian_singer(x, tau=5.0, num_dims=1)
|
|
641
|
+
>>> A
|
|
642
|
+
array([[ 0. , 1. , 0. ],
|
|
643
|
+
[ 0. , 0. , 1. ],
|
|
644
|
+
[ 0. , 0. , -0.2]])
|
|
541
645
|
"""
|
|
542
646
|
n = 3 # states per dimension
|
|
543
647
|
A_1d = np.array(
|
|
@@ -232,6 +232,12 @@ def f_coord_turn_polar(
|
|
|
232
232
|
- psi is heading angle
|
|
233
233
|
- v is speed magnitude
|
|
234
234
|
- omega is turn rate
|
|
235
|
+
|
|
236
|
+
Examples
|
|
237
|
+
--------
|
|
238
|
+
>>> F = f_coord_turn_polar(T=1.0, omega=0.1, speed=100.0)
|
|
239
|
+
>>> F.shape
|
|
240
|
+
(5, 5)
|
|
235
241
|
"""
|
|
236
242
|
# Handle near-zero turn rate
|
|
237
243
|
if np.abs(omega) < 1e-10:
|
|
@@ -123,6 +123,12 @@ def f_singer_2d(T: float, tau: float) -> NDArray[np.floating]:
|
|
|
123
123
|
F : ndarray
|
|
124
124
|
State transition matrix of shape (6, 6).
|
|
125
125
|
|
|
126
|
+
Examples
|
|
127
|
+
--------
|
|
128
|
+
>>> F = f_singer_2d(T=1.0, tau=10.0)
|
|
129
|
+
>>> F.shape
|
|
130
|
+
(6, 6)
|
|
131
|
+
|
|
126
132
|
See Also
|
|
127
133
|
--------
|
|
128
134
|
f_singer : General Singer model.
|
|
@@ -148,6 +154,12 @@ def f_singer_3d(T: float, tau: float) -> NDArray[np.floating]:
|
|
|
148
154
|
F : ndarray
|
|
149
155
|
State transition matrix of shape (9, 9).
|
|
150
156
|
|
|
157
|
+
Examples
|
|
158
|
+
--------
|
|
159
|
+
>>> F = f_singer_3d(T=1.0, tau=10.0)
|
|
160
|
+
>>> F.shape
|
|
161
|
+
(9, 9)
|
|
162
|
+
|
|
151
163
|
See Also
|
|
152
164
|
--------
|
|
153
165
|
f_singer : General Singer model.
|
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Process noise covariance matrices for coordinated turn models.
|
|
3
|
+
|
|
4
|
+
This module provides functions to construct process noise covariance matrices
|
|
5
|
+
(Q matrices) for coordinated turn motion models used in target tracking.
|
|
6
|
+
Coordinated turn models describe targets that maintain a constant turn rate,
|
|
7
|
+
such as aircraft executing banking maneuvers.
|
|
8
|
+
|
|
9
|
+
The process noise captures uncertainty in the target's motion, including:
|
|
10
|
+
- Linear acceleration uncertainty along the velocity direction
|
|
11
|
+
- Turn rate (angular velocity) uncertainty
|
|
12
|
+
|
|
13
|
+
Available functions:
|
|
14
|
+
- ``q_coord_turn_2d``: 2D coordinated turn (planar motion)
|
|
15
|
+
- ``q_coord_turn_3d``: 3D coordinated turn (spatial motion)
|
|
16
|
+
- ``q_coord_turn_polar``: Polar/heading-speed representation
|
|
17
|
+
|
|
18
|
+
These Q matrices are designed to work with the corresponding state transition
|
|
19
|
+
matrices in :mod:`pytcl.dynamic_models.coordinated_turn`.
|
|
20
|
+
|
|
21
|
+
Examples
|
|
22
|
+
--------
|
|
23
|
+
Create process noise for a 2D coordinated turn tracker:
|
|
24
|
+
|
|
25
|
+
>>> from pytcl.dynamic_models.process_noise import q_coord_turn_2d
|
|
26
|
+
>>> Q = q_coord_turn_2d(T=0.1, sigma_a=2.0) # 0.1s step, 2 m/s² accel noise
|
|
27
|
+
>>> Q.shape
|
|
28
|
+
(4, 4)
|
|
29
|
+
|
|
30
|
+
With turn rate state estimation:
|
|
31
|
+
|
|
32
|
+
>>> Q = q_coord_turn_2d(T=0.1, sigma_a=2.0, sigma_omega=0.01,
|
|
33
|
+
... state_type='position_velocity_omega')
|
|
34
|
+
>>> Q.shape
|
|
35
|
+
(5, 5)
|
|
36
|
+
|
|
37
|
+
See Also
|
|
38
|
+
--------
|
|
39
|
+
pytcl.dynamic_models.coordinated_turn : State transition matrices
|
|
40
|
+
pytcl.dynamic_models.process_noise.constant_velocity : CV process noise
|
|
41
|
+
pytcl.dynamic_models.process_noise.constant_acceleration : CA process noise
|
|
42
|
+
|
|
43
|
+
References
|
|
44
|
+
----------
|
|
45
|
+
.. [1] Bar-Shalom, Y., Li, X.R., and Kirubarajan, T., "Estimation with
|
|
46
|
+
Applications to Tracking and Navigation", Wiley, 2001.
|
|
47
|
+
.. [2] Blackman, S. and Popoli, R., "Design and Analysis of Modern
|
|
48
|
+
Tracking Systems", Artech House, 1999.
|
|
3
49
|
"""
|
|
4
50
|
|
|
5
51
|
import numpy as np
|
|
@@ -229,6 +229,12 @@ def q_constant_acceleration(
|
|
|
229
229
|
Q : ndarray
|
|
230
230
|
Process noise covariance matrix.
|
|
231
231
|
|
|
232
|
+
Examples
|
|
233
|
+
--------
|
|
234
|
+
>>> Q = q_constant_acceleration(T=1.0, sigma_j=0.1, num_dims=2)
|
|
235
|
+
>>> Q.shape
|
|
236
|
+
(6, 6)
|
|
237
|
+
|
|
232
238
|
See Also
|
|
233
239
|
--------
|
|
234
240
|
f_constant_acceleration : State transition matrix for CA model.
|
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Process noise covariance matrices for Singer acceleration model.
|
|
3
|
+
|
|
4
|
+
This module provides functions to construct process noise covariance matrices
|
|
5
|
+
(Q matrices) for the Singer acceleration motion model. The Singer model
|
|
6
|
+
treats target acceleration as a first-order Gauss-Markov process, making it
|
|
7
|
+
well-suited for tracking maneuvering targets with random accelerations.
|
|
8
|
+
|
|
9
|
+
The Singer model is characterized by:
|
|
10
|
+
- A maneuver time constant (tau) that controls how quickly acceleration
|
|
11
|
+
correlations decay
|
|
12
|
+
- An RMS maneuver level (sigma_m) that sets the expected acceleration magnitude
|
|
13
|
+
- State vector [position, velocity, acceleration] per dimension
|
|
14
|
+
|
|
15
|
+
The model dynamics are:
|
|
16
|
+
da/dt = -a/tau + w(t)
|
|
17
|
+
|
|
18
|
+
where w(t) is white noise with spectral density 2*sigma_m²/tau.
|
|
19
|
+
|
|
20
|
+
Available functions:
|
|
21
|
+
- ``q_singer``: Generic N-dimensional Singer process noise
|
|
22
|
+
- ``q_singer_2d``: 2D Singer model (6x6 state)
|
|
23
|
+
- ``q_singer_3d``: 3D Singer model (9x9 state)
|
|
24
|
+
|
|
25
|
+
These Q matrices are designed to work with the corresponding state transition
|
|
26
|
+
matrices in :mod:`pytcl.dynamic_models.singer`.
|
|
27
|
+
|
|
28
|
+
Examples
|
|
29
|
+
--------
|
|
30
|
+
Create process noise for tracking a maneuvering aircraft:
|
|
31
|
+
|
|
32
|
+
>>> from pytcl.dynamic_models.process_noise import q_singer
|
|
33
|
+
>>> Q = q_singer(T=1.0, tau=20.0, sigma_m=3.0) # tau=20s, 3g maneuvers
|
|
34
|
+
>>> Q.shape
|
|
35
|
+
(3, 3)
|
|
36
|
+
|
|
37
|
+
For 3D tracking:
|
|
38
|
+
|
|
39
|
+
>>> Q = q_singer_3d(T=0.1, tau=10.0, sigma_m=2.0)
|
|
40
|
+
>>> Q.shape
|
|
41
|
+
(9, 9)
|
|
42
|
+
|
|
43
|
+
See Also
|
|
44
|
+
--------
|
|
45
|
+
pytcl.dynamic_models.singer : State transition matrices
|
|
46
|
+
pytcl.dynamic_models.process_noise.constant_acceleration : CA process noise
|
|
47
|
+
|
|
48
|
+
References
|
|
49
|
+
----------
|
|
50
|
+
.. [1] Singer, R.A., "Estimating Optimal Tracking Filter Performance for
|
|
51
|
+
Manned Maneuvering Targets", IEEE Trans. Aerospace and Electronic
|
|
52
|
+
Systems, Vol. AES-6, No. 4, July 1970, pp. 473-483.
|
|
53
|
+
.. [2] Bar-Shalom, Y., Li, X.R., and Kirubarajan, T., "Estimation with
|
|
54
|
+
Applications to Tracking and Navigation", Wiley, 2001, Chapter 6.
|
|
3
55
|
"""
|
|
4
56
|
|
|
5
57
|
import numpy as np
|