nrl-tracker 0.21.4__py3-none-any.whl → 1.7.5__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 (95) hide show
  1. {nrl_tracker-0.21.4.dist-info → nrl_tracker-1.7.5.dist-info}/METADATA +57 -10
  2. nrl_tracker-1.7.5.dist-info/RECORD +165 -0
  3. pytcl/__init__.py +4 -3
  4. pytcl/assignment_algorithms/__init__.py +28 -0
  5. pytcl/assignment_algorithms/data_association.py +2 -7
  6. pytcl/assignment_algorithms/gating.py +10 -10
  7. pytcl/assignment_algorithms/jpda.py +40 -40
  8. pytcl/assignment_algorithms/nd_assignment.py +379 -0
  9. pytcl/assignment_algorithms/network_flow.py +371 -0
  10. pytcl/assignment_algorithms/three_dimensional/assignment.py +3 -3
  11. pytcl/astronomical/__init__.py +162 -8
  12. pytcl/astronomical/ephemerides.py +533 -0
  13. pytcl/astronomical/reference_frames.py +865 -56
  14. pytcl/astronomical/relativity.py +473 -0
  15. pytcl/astronomical/sgp4.py +710 -0
  16. pytcl/astronomical/special_orbits.py +532 -0
  17. pytcl/astronomical/tle.py +558 -0
  18. pytcl/atmosphere/__init__.py +45 -3
  19. pytcl/atmosphere/ionosphere.py +512 -0
  20. pytcl/atmosphere/nrlmsise00.py +809 -0
  21. pytcl/clustering/dbscan.py +2 -2
  22. pytcl/clustering/gaussian_mixture.py +3 -3
  23. pytcl/clustering/hierarchical.py +15 -15
  24. pytcl/clustering/kmeans.py +4 -4
  25. pytcl/containers/__init__.py +28 -21
  26. pytcl/containers/base.py +219 -0
  27. pytcl/containers/cluster_set.py +2 -1
  28. pytcl/containers/covertree.py +26 -29
  29. pytcl/containers/kd_tree.py +94 -29
  30. pytcl/containers/measurement_set.py +1 -9
  31. pytcl/containers/rtree.py +200 -1
  32. pytcl/containers/vptree.py +21 -28
  33. pytcl/coordinate_systems/conversions/geodetic.py +272 -5
  34. pytcl/coordinate_systems/jacobians/jacobians.py +2 -2
  35. pytcl/coordinate_systems/projections/__init__.py +4 -2
  36. pytcl/coordinate_systems/projections/projections.py +2 -2
  37. pytcl/coordinate_systems/rotations/rotations.py +10 -6
  38. pytcl/core/__init__.py +18 -0
  39. pytcl/core/validation.py +333 -2
  40. pytcl/dynamic_estimation/__init__.py +26 -0
  41. pytcl/dynamic_estimation/gaussian_sum_filter.py +434 -0
  42. pytcl/dynamic_estimation/imm.py +15 -18
  43. pytcl/dynamic_estimation/kalman/__init__.py +30 -0
  44. pytcl/dynamic_estimation/kalman/constrained.py +382 -0
  45. pytcl/dynamic_estimation/kalman/extended.py +9 -12
  46. pytcl/dynamic_estimation/kalman/h_infinity.py +613 -0
  47. pytcl/dynamic_estimation/kalman/square_root.py +60 -573
  48. pytcl/dynamic_estimation/kalman/sr_ukf.py +302 -0
  49. pytcl/dynamic_estimation/kalman/ud_filter.py +410 -0
  50. pytcl/dynamic_estimation/kalman/unscented.py +9 -10
  51. pytcl/dynamic_estimation/particle_filters/bootstrap.py +15 -15
  52. pytcl/dynamic_estimation/rbpf.py +589 -0
  53. pytcl/dynamic_estimation/smoothers.py +1 -5
  54. pytcl/dynamic_models/discrete_time/__init__.py +1 -5
  55. pytcl/dynamic_models/process_noise/__init__.py +1 -5
  56. pytcl/gravity/egm.py +13 -0
  57. pytcl/gravity/spherical_harmonics.py +98 -37
  58. pytcl/gravity/tides.py +6 -6
  59. pytcl/logging_config.py +328 -0
  60. pytcl/magnetism/__init__.py +10 -14
  61. pytcl/magnetism/emm.py +10 -3
  62. pytcl/magnetism/wmm.py +260 -23
  63. pytcl/mathematical_functions/combinatorics/combinatorics.py +5 -5
  64. pytcl/mathematical_functions/geometry/geometry.py +5 -5
  65. pytcl/mathematical_functions/interpolation/__init__.py +2 -2
  66. pytcl/mathematical_functions/numerical_integration/quadrature.py +6 -6
  67. pytcl/mathematical_functions/signal_processing/detection.py +24 -24
  68. pytcl/mathematical_functions/signal_processing/filters.py +14 -14
  69. pytcl/mathematical_functions/signal_processing/matched_filter.py +12 -12
  70. pytcl/mathematical_functions/special_functions/__init__.py +2 -2
  71. pytcl/mathematical_functions/special_functions/bessel.py +15 -3
  72. pytcl/mathematical_functions/special_functions/debye.py +136 -26
  73. pytcl/mathematical_functions/special_functions/error_functions.py +3 -1
  74. pytcl/mathematical_functions/special_functions/gamma_functions.py +4 -4
  75. pytcl/mathematical_functions/special_functions/hypergeometric.py +81 -15
  76. pytcl/mathematical_functions/transforms/fourier.py +8 -8
  77. pytcl/mathematical_functions/transforms/stft.py +12 -12
  78. pytcl/mathematical_functions/transforms/wavelets.py +9 -9
  79. pytcl/navigation/__init__.py +14 -10
  80. pytcl/navigation/geodesy.py +246 -160
  81. pytcl/navigation/great_circle.py +101 -19
  82. pytcl/navigation/ins.py +1 -5
  83. pytcl/plotting/coordinates.py +7 -7
  84. pytcl/plotting/tracks.py +2 -2
  85. pytcl/static_estimation/maximum_likelihood.py +16 -14
  86. pytcl/static_estimation/robust.py +5 -5
  87. pytcl/terrain/loaders.py +5 -5
  88. pytcl/trackers/__init__.py +3 -14
  89. pytcl/trackers/hypothesis.py +1 -1
  90. pytcl/trackers/mht.py +9 -9
  91. pytcl/trackers/multi_target.py +2 -5
  92. nrl_tracker-0.21.4.dist-info/RECORD +0 -148
  93. {nrl_tracker-0.21.4.dist-info → nrl_tracker-1.7.5.dist-info}/LICENSE +0 -0
  94. {nrl_tracker-0.21.4.dist-info → nrl_tracker-1.7.5.dist-info}/WHEEL +0 -0
  95. {nrl_tracker-0.21.4.dist-info → nrl_tracker-1.7.5.dist-info}/top_level.txt +0 -0
pytcl/core/validation.py CHANGED
@@ -28,7 +28,7 @@ def validate_array(
28
28
  arr: ArrayLike,
29
29
  name: str = "array",
30
30
  *,
31
- dtype: type | np.dtype | None = None,
31
+ dtype: type | np.dtype[Any] | None = None,
32
32
  ndim: int | tuple[int, ...] | None = None,
33
33
  shape: tuple[int | None, ...] | None = None,
34
34
  min_ndim: int | None = None,
@@ -415,7 +415,7 @@ def validate_same_shape(*arrays: ArrayLike, names: Sequence[str] | None = None)
415
415
  def validated_array_input(
416
416
  param_name: str,
417
417
  *,
418
- dtype: type | np.dtype | None = None,
418
+ dtype: type | np.dtype[Any] | None = None,
419
419
  ndim: int | tuple[int, ...] | None = None,
420
420
  shape: tuple[int | None, ...] | None = None,
421
421
  finite: bool = False,
@@ -472,3 +472,334 @@ def validated_array_input(
472
472
  return wrapper
473
473
 
474
474
  return decorator
475
+
476
+
477
+ class ArraySpec:
478
+ """
479
+ Specification for array validation in @validate_inputs decorator.
480
+
481
+ Parameters
482
+ ----------
483
+ dtype : type or np.dtype, optional
484
+ Required dtype.
485
+ ndim : int or tuple of int, optional
486
+ Required dimensionality.
487
+ shape : tuple, optional
488
+ Required shape (None for any size).
489
+ min_ndim : int, optional
490
+ Minimum dimensions required.
491
+ max_ndim : int, optional
492
+ Maximum dimensions allowed.
493
+ finite : bool, optional
494
+ Require all finite values.
495
+ non_negative : bool, optional
496
+ Require all values >= 0.
497
+ positive : bool, optional
498
+ Require all values > 0.
499
+ allow_empty : bool, optional
500
+ Allow empty arrays. Default True.
501
+ square : bool, optional
502
+ Require square matrix.
503
+ symmetric : bool, optional
504
+ Require symmetric matrix.
505
+ positive_definite : bool, optional
506
+ Require positive definite matrix.
507
+
508
+ Examples
509
+ --------
510
+ >>> spec = ArraySpec(ndim=2, finite=True, square=True)
511
+ >>> @validate_inputs(matrix=spec)
512
+ ... def process_matrix(matrix):
513
+ ... return np.linalg.inv(matrix)
514
+ """
515
+
516
+ def __init__(
517
+ self,
518
+ *,
519
+ dtype: type | np.dtype[Any] | None = None,
520
+ ndim: int | tuple[int, ...] | None = None,
521
+ shape: tuple[int | None, ...] | None = None,
522
+ min_ndim: int | None = None,
523
+ max_ndim: int | None = None,
524
+ finite: bool = False,
525
+ non_negative: bool = False,
526
+ positive: bool = False,
527
+ allow_empty: bool = True,
528
+ square: bool = False,
529
+ symmetric: bool = False,
530
+ positive_definite: bool = False,
531
+ ):
532
+ self.dtype = dtype
533
+ self.ndim = ndim
534
+ self.shape = shape
535
+ self.min_ndim = min_ndim
536
+ self.max_ndim = max_ndim
537
+ self.finite = finite
538
+ self.non_negative = non_negative
539
+ self.positive = positive
540
+ self.allow_empty = allow_empty
541
+ self.square = square
542
+ self.symmetric = symmetric
543
+ self.positive_definite = positive_definite
544
+
545
+ def validate(self, arr: ArrayLike, name: str) -> NDArray[Any]:
546
+ """Validate an array against this specification."""
547
+ result = validate_array(
548
+ arr,
549
+ name,
550
+ dtype=self.dtype,
551
+ ndim=self.ndim,
552
+ shape=self.shape,
553
+ min_ndim=self.min_ndim,
554
+ max_ndim=self.max_ndim,
555
+ finite=self.finite,
556
+ non_negative=self.non_negative,
557
+ positive=self.positive,
558
+ allow_empty=self.allow_empty,
559
+ )
560
+
561
+ if self.positive_definite:
562
+ result = ensure_positive_definite(result, name)
563
+ elif self.symmetric:
564
+ result = ensure_symmetric(result, name)
565
+ elif self.square:
566
+ result = ensure_square_matrix(result, name)
567
+
568
+ return result
569
+
570
+
571
+ class ScalarSpec:
572
+ """
573
+ Specification for scalar validation in @validate_inputs decorator.
574
+
575
+ Parameters
576
+ ----------
577
+ dtype : type, optional
578
+ Required type (int, float, etc.).
579
+ min_value : float, optional
580
+ Minimum allowed value (inclusive).
581
+ max_value : float, optional
582
+ Maximum allowed value (inclusive).
583
+ finite : bool, optional
584
+ Require finite value.
585
+ positive : bool, optional
586
+ Require value > 0.
587
+ non_negative : bool, optional
588
+ Require value >= 0.
589
+
590
+ Examples
591
+ --------
592
+ >>> spec = ScalarSpec(dtype=int, min_value=1, max_value=10)
593
+ >>> @validate_inputs(k=spec)
594
+ ... def get_k_nearest(k, data):
595
+ ... return data[:k]
596
+ """
597
+
598
+ def __init__(
599
+ self,
600
+ *,
601
+ dtype: type | None = None,
602
+ min_value: float | None = None,
603
+ max_value: float | None = None,
604
+ finite: bool = False,
605
+ positive: bool = False,
606
+ non_negative: bool = False,
607
+ ):
608
+ self.dtype = dtype
609
+ self.min_value = min_value
610
+ self.max_value = max_value
611
+ self.finite = finite
612
+ self.positive = positive
613
+ self.non_negative = non_negative
614
+
615
+ def validate(self, value: Any, name: str) -> Any:
616
+ """Validate a scalar value against this specification."""
617
+ # Type check
618
+ if self.dtype is not None:
619
+ if not isinstance(value, self.dtype):
620
+ try:
621
+ value = self.dtype(value)
622
+ except (ValueError, TypeError) as e:
623
+ raise ValidationError(
624
+ f"{name} must be {self.dtype.__name__}, got {type(value).__name__}"
625
+ ) from e
626
+
627
+ # Convert to float for numeric checks
628
+ try:
629
+ num_value = float(value)
630
+ except (ValueError, TypeError):
631
+ if any(
632
+ [
633
+ self.finite,
634
+ self.positive,
635
+ self.non_negative,
636
+ self.min_value is not None,
637
+ self.max_value is not None,
638
+ ]
639
+ ):
640
+ raise ValidationError(
641
+ f"{name} must be numeric for range validation"
642
+ ) from None
643
+ return value
644
+
645
+ # Finite check
646
+ if self.finite and not np.isfinite(num_value):
647
+ raise ValidationError(f"{name} must be finite, got {value}")
648
+
649
+ # Positive check
650
+ if self.positive and num_value <= 0:
651
+ raise ValidationError(f"{name} must be positive, got {value}")
652
+
653
+ # Non-negative check
654
+ if self.non_negative and num_value < 0:
655
+ raise ValidationError(f"{name} must be non-negative, got {value}")
656
+
657
+ # Range checks
658
+ if self.min_value is not None and num_value < self.min_value:
659
+ raise ValidationError(f"{name} must be >= {self.min_value}, got {value}")
660
+
661
+ if self.max_value is not None and num_value > self.max_value:
662
+ raise ValidationError(f"{name} must be <= {self.max_value}, got {value}")
663
+
664
+ return value
665
+
666
+
667
+ def validate_inputs(
668
+ **param_specs: ArraySpec | ScalarSpec | dict[str, Any],
669
+ ) -> Callable[[F], F]:
670
+ """
671
+ Decorator for validating multiple function parameters.
672
+
673
+ This decorator enables declarative input validation using specification
674
+ objects (ArraySpec, ScalarSpec) or dictionaries of validation options.
675
+
676
+ Parameters
677
+ ----------
678
+ **param_specs : ArraySpec | ScalarSpec | dict
679
+ Keyword arguments mapping parameter names to validation specs.
680
+ Each spec can be:
681
+ - ArraySpec: For array validation
682
+ - ScalarSpec: For scalar validation
683
+ - dict: Options passed to ArraySpec (for convenience)
684
+
685
+ Returns
686
+ -------
687
+ Callable
688
+ Decorated function with input validation.
689
+
690
+ Examples
691
+ --------
692
+ >>> @validate_inputs(
693
+ ... x=ArraySpec(ndim=2, finite=True),
694
+ ... P=ArraySpec(ndim=2, positive_definite=True),
695
+ ... k=ScalarSpec(dtype=int, min_value=1),
696
+ ... )
697
+ ... def kalman_update(x, P, z, H, R, k=1):
698
+ ... # x and P are guaranteed valid here
699
+ ... pass
700
+
701
+ Using dict shorthand:
702
+
703
+ >>> @validate_inputs(
704
+ ... state={"ndim": 1, "finite": True},
705
+ ... covariance={"ndim": 2, "positive_definite": True},
706
+ ... )
707
+ ... def predict(state, covariance, dt):
708
+ ... pass
709
+
710
+ Notes
711
+ -----
712
+ Validation happens in the order parameters are defined in the decorator.
713
+ If any validation fails, a ValidationError is raised with a descriptive
714
+ message identifying the parameter and the constraint violated.
715
+
716
+ See Also
717
+ --------
718
+ ArraySpec : Specification class for array validation.
719
+ ScalarSpec : Specification class for scalar validation.
720
+ validate_array : Lower-level array validation function.
721
+ """
722
+
723
+ def decorator(func: F) -> F:
724
+ import inspect
725
+
726
+ # Pre-fetch signature for efficiency
727
+ sig = inspect.signature(func)
728
+
729
+ @wraps(func)
730
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
731
+ bound = sig.bind(*args, **kwargs)
732
+ bound.apply_defaults()
733
+
734
+ for param_name, spec in param_specs.items():
735
+ if param_name not in bound.arguments:
736
+ continue
737
+
738
+ value = bound.arguments[param_name]
739
+
740
+ # Convert dict to ArraySpec
741
+ if isinstance(spec, dict):
742
+ spec = ArraySpec(**spec)
743
+
744
+ # Validate using spec
745
+ if isinstance(spec, (ArraySpec, ScalarSpec)):
746
+ bound.arguments[param_name] = spec.validate(value, param_name)
747
+ else:
748
+ raise TypeError(
749
+ f"Invalid spec type for {param_name}: {type(spec)}. "
750
+ "Use ArraySpec, ScalarSpec, or dict."
751
+ )
752
+
753
+ return func(*bound.args, **bound.kwargs)
754
+
755
+ return wrapper
756
+
757
+ return decorator
758
+
759
+
760
+ def check_compatible_shapes(
761
+ *shapes: tuple[int, ...],
762
+ names: Sequence[str] | None = None,
763
+ dimension: int | None = None,
764
+ ) -> None:
765
+ """
766
+ Check that array shapes are compatible for operations.
767
+
768
+ Parameters
769
+ ----------
770
+ *shapes : tuple of int
771
+ Shapes to check for compatibility.
772
+ names : sequence of str, optional
773
+ Names for error messages.
774
+ dimension : int, optional
775
+ If provided, only check compatibility along this dimension.
776
+
777
+ Raises
778
+ ------
779
+ ValidationError
780
+ If shapes are not compatible.
781
+
782
+ Examples
783
+ --------
784
+ >>> check_compatible_shapes((3, 4), (4, 5), names=["A", "B"], dimension=0)
785
+ # Raises: A has 3 rows but B has 4 rows
786
+
787
+ >>> check_compatible_shapes((3, 4), (4, 5), names=["A", "B"])
788
+ # Passes (inner dimensions compatible for matrix multiply)
789
+ """
790
+ if len(shapes) < 2:
791
+ return
792
+
793
+ if names is None:
794
+ names = [f"array_{i}" for i in range(len(shapes))]
795
+
796
+ if dimension is not None:
797
+ # Check specific dimension
798
+ dims = [s[dimension] if len(s) > dimension else None for s in shapes]
799
+ valid_dims = [d for d in dims if d is not None]
800
+ if valid_dims and not all(d == valid_dims[0] for d in valid_dims):
801
+ dim_strs = [f"{n}={d}" for n, d in zip(names, dims) if d is not None]
802
+ raise ValidationError(
803
+ f"Arrays have incompatible sizes along dimension {dimension}: "
804
+ f"{', '.join(dim_strs)}"
805
+ )
@@ -14,6 +14,14 @@ This module provides filtering and smoothing algorithms for state estimation:
14
14
  # Import submodules for easy access
15
15
  from pytcl.dynamic_estimation import kalman, particle_filters
16
16
 
17
+ # Gaussian Sum Filter
18
+ from pytcl.dynamic_estimation.gaussian_sum_filter import (
19
+ GaussianComponent,
20
+ GaussianSumFilter,
21
+ gaussian_sum_filter_predict,
22
+ gaussian_sum_filter_update,
23
+ )
24
+
17
25
  # IMM estimator
18
26
  from pytcl.dynamic_estimation.imm import (
19
27
  IMMEstimator,
@@ -101,6 +109,14 @@ from pytcl.dynamic_estimation.particle_filters import (
101
109
  resample_systematic,
102
110
  )
103
111
 
112
+ # Rao-Blackwellized Particle Filter
113
+ from pytcl.dynamic_estimation.rbpf import (
114
+ RBPFFilter,
115
+ RBPFParticle,
116
+ rbpf_predict,
117
+ rbpf_update,
118
+ )
119
+
104
120
  # Smoothers
105
121
  from pytcl.dynamic_estimation.smoothers import (
106
122
  FixedLagResult,
@@ -192,6 +208,16 @@ __all__ = [
192
208
  "imm_predict_update",
193
209
  "IMMEstimator",
194
210
  # Particle filters
211
+ # Gaussian Sum Filter
212
+ "GaussianComponent",
213
+ "GaussianSumFilter",
214
+ "gaussian_sum_filter_predict",
215
+ "gaussian_sum_filter_update",
216
+ # Rao-Blackwellized Particle Filter
217
+ "RBPFParticle",
218
+ "RBPFFilter",
219
+ "rbpf_predict",
220
+ "rbpf_update",
195
221
  "ParticleState",
196
222
  "resample_multinomial",
197
223
  "resample_systematic",