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.
Files changed (61) hide show
  1. {nrl_tracker-1.9.0.dist-info → nrl_tracker-1.9.2.dist-info}/METADATA +4 -4
  2. {nrl_tracker-1.9.0.dist-info → nrl_tracker-1.9.2.dist-info}/RECORD +61 -60
  3. pytcl/__init__.py +2 -2
  4. pytcl/assignment_algorithms/gating.py +18 -0
  5. pytcl/assignment_algorithms/jpda.py +56 -0
  6. pytcl/assignment_algorithms/nd_assignment.py +65 -0
  7. pytcl/assignment_algorithms/network_flow.py +40 -0
  8. pytcl/astronomical/ephemerides.py +18 -0
  9. pytcl/astronomical/orbital_mechanics.py +131 -0
  10. pytcl/atmosphere/ionosphere.py +44 -0
  11. pytcl/atmosphere/models.py +29 -0
  12. pytcl/clustering/dbscan.py +9 -0
  13. pytcl/clustering/gaussian_mixture.py +20 -0
  14. pytcl/clustering/hierarchical.py +29 -0
  15. pytcl/clustering/kmeans.py +9 -0
  16. pytcl/coordinate_systems/conversions/geodetic.py +46 -0
  17. pytcl/coordinate_systems/conversions/spherical.py +35 -0
  18. pytcl/coordinate_systems/rotations/rotations.py +147 -0
  19. pytcl/core/__init__.py +16 -0
  20. pytcl/core/maturity.py +346 -0
  21. pytcl/core/optional_deps.py +1 -1
  22. pytcl/dynamic_estimation/gaussian_sum_filter.py +55 -0
  23. pytcl/dynamic_estimation/imm.py +29 -0
  24. pytcl/dynamic_estimation/information_filter.py +64 -0
  25. pytcl/dynamic_estimation/kalman/extended.py +56 -0
  26. pytcl/dynamic_estimation/kalman/linear.py +69 -0
  27. pytcl/dynamic_estimation/kalman/unscented.py +81 -0
  28. pytcl/dynamic_estimation/particle_filters/bootstrap.py +146 -0
  29. pytcl/dynamic_estimation/rbpf.py +51 -0
  30. pytcl/dynamic_estimation/smoothers.py +58 -0
  31. pytcl/dynamic_models/continuous_time/dynamics.py +104 -0
  32. pytcl/dynamic_models/discrete_time/coordinated_turn.py +6 -0
  33. pytcl/dynamic_models/discrete_time/singer.py +12 -0
  34. pytcl/dynamic_models/process_noise/coordinated_turn.py +46 -0
  35. pytcl/dynamic_models/process_noise/polynomial.py +6 -0
  36. pytcl/dynamic_models/process_noise/singer.py +52 -0
  37. pytcl/gravity/clenshaw.py +60 -0
  38. pytcl/gravity/egm.py +47 -0
  39. pytcl/gravity/models.py +34 -0
  40. pytcl/gravity/spherical_harmonics.py +73 -0
  41. pytcl/gravity/tides.py +34 -0
  42. pytcl/mathematical_functions/numerical_integration/quadrature.py +85 -0
  43. pytcl/mathematical_functions/special_functions/bessel.py +55 -0
  44. pytcl/mathematical_functions/special_functions/elliptic.py +42 -0
  45. pytcl/mathematical_functions/special_functions/error_functions.py +49 -0
  46. pytcl/mathematical_functions/special_functions/gamma_functions.py +43 -0
  47. pytcl/mathematical_functions/special_functions/lambert_w.py +5 -0
  48. pytcl/mathematical_functions/special_functions/marcum_q.py +16 -0
  49. pytcl/navigation/geodesy.py +101 -2
  50. pytcl/navigation/great_circle.py +71 -0
  51. pytcl/navigation/rhumb.py +74 -0
  52. pytcl/performance_evaluation/estimation_metrics.py +70 -0
  53. pytcl/performance_evaluation/track_metrics.py +30 -0
  54. pytcl/static_estimation/maximum_likelihood.py +54 -0
  55. pytcl/static_estimation/robust.py +57 -0
  56. pytcl/terrain/dem.py +69 -0
  57. pytcl/terrain/visibility.py +65 -0
  58. pytcl/trackers/hypothesis.py +65 -0
  59. {nrl_tracker-1.9.0.dist-info → nrl_tracker-1.9.2.dist-info}/LICENSE +0 -0
  60. {nrl_tracker-1.9.0.dist-info → nrl_tracker-1.9.2.dist-info}/WHEEL +0 -0
  61. {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()
@@ -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