nrl-tracker 0.22.5__py3-none-any.whl → 1.8.0__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 (86) hide show
  1. {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.8.0.dist-info}/METADATA +57 -10
  2. {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.8.0.dist-info}/RECORD +86 -69
  3. pytcl/__init__.py +4 -3
  4. pytcl/assignment_algorithms/__init__.py +28 -0
  5. pytcl/assignment_algorithms/dijkstra_min_cost.py +184 -0
  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 +464 -0
  10. pytcl/assignment_algorithms/network_simplex.py +167 -0
  11. pytcl/assignment_algorithms/three_dimensional/assignment.py +3 -3
  12. pytcl/astronomical/__init__.py +104 -3
  13. pytcl/astronomical/ephemerides.py +14 -11
  14. pytcl/astronomical/reference_frames.py +865 -56
  15. pytcl/astronomical/relativity.py +6 -5
  16. pytcl/astronomical/sgp4.py +710 -0
  17. pytcl/astronomical/special_orbits.py +532 -0
  18. pytcl/astronomical/tle.py +558 -0
  19. pytcl/atmosphere/__init__.py +43 -1
  20. pytcl/atmosphere/ionosphere.py +512 -0
  21. pytcl/atmosphere/nrlmsise00.py +809 -0
  22. pytcl/clustering/dbscan.py +2 -2
  23. pytcl/clustering/gaussian_mixture.py +3 -3
  24. pytcl/clustering/hierarchical.py +15 -15
  25. pytcl/clustering/kmeans.py +4 -4
  26. pytcl/containers/__init__.py +24 -0
  27. pytcl/containers/base.py +219 -0
  28. pytcl/containers/cluster_set.py +12 -2
  29. pytcl/containers/covertree.py +26 -29
  30. pytcl/containers/kd_tree.py +94 -29
  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 +1 -1
  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 +14 -14
  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 +8 -8
  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 +8 -6
  51. pytcl/dynamic_estimation/particle_filters/bootstrap.py +15 -15
  52. pytcl/dynamic_estimation/rbpf.py +589 -0
  53. pytcl/gravity/egm.py +13 -0
  54. pytcl/gravity/spherical_harmonics.py +98 -37
  55. pytcl/gravity/tides.py +6 -6
  56. pytcl/logging_config.py +328 -0
  57. pytcl/magnetism/__init__.py +7 -0
  58. pytcl/magnetism/emm.py +10 -3
  59. pytcl/magnetism/wmm.py +260 -23
  60. pytcl/mathematical_functions/combinatorics/combinatorics.py +5 -5
  61. pytcl/mathematical_functions/geometry/geometry.py +5 -5
  62. pytcl/mathematical_functions/numerical_integration/quadrature.py +6 -6
  63. pytcl/mathematical_functions/signal_processing/detection.py +24 -24
  64. pytcl/mathematical_functions/signal_processing/filters.py +14 -14
  65. pytcl/mathematical_functions/signal_processing/matched_filter.py +12 -12
  66. pytcl/mathematical_functions/special_functions/bessel.py +15 -3
  67. pytcl/mathematical_functions/special_functions/debye.py +136 -26
  68. pytcl/mathematical_functions/special_functions/error_functions.py +3 -1
  69. pytcl/mathematical_functions/special_functions/gamma_functions.py +4 -4
  70. pytcl/mathematical_functions/special_functions/hypergeometric.py +81 -15
  71. pytcl/mathematical_functions/transforms/fourier.py +8 -8
  72. pytcl/mathematical_functions/transforms/stft.py +12 -12
  73. pytcl/mathematical_functions/transforms/wavelets.py +9 -9
  74. pytcl/navigation/geodesy.py +246 -160
  75. pytcl/navigation/great_circle.py +101 -19
  76. pytcl/plotting/coordinates.py +7 -7
  77. pytcl/plotting/tracks.py +2 -2
  78. pytcl/static_estimation/maximum_likelihood.py +16 -14
  79. pytcl/static_estimation/robust.py +5 -5
  80. pytcl/terrain/loaders.py +5 -5
  81. pytcl/trackers/hypothesis.py +1 -1
  82. pytcl/trackers/mht.py +9 -9
  83. pytcl/trackers/multi_target.py +1 -1
  84. {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.8.0.dist-info}/LICENSE +0 -0
  85. {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.8.0.dist-info}/WHEEL +0 -0
  86. {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.8.0.dist-info}/top_level.txt +0 -0
pytcl/magnetism/wmm.py CHANGED
@@ -12,13 +12,30 @@ References
12
12
  .. [2] https://www.ngdc.noaa.gov/geomag/WMM/
13
13
  """
14
14
 
15
- from typing import NamedTuple, Tuple
15
+ from functools import lru_cache
16
+ from typing import Any, NamedTuple, Optional, Tuple
16
17
 
17
18
  import numpy as np
18
19
  from numpy.typing import NDArray
19
20
 
20
21
  from pytcl.gravity.spherical_harmonics import associated_legendre
21
22
 
23
+ # =============================================================================
24
+ # Cache Configuration
25
+ # =============================================================================
26
+
27
+ # Default cache size (number of unique location/time combinations to cache)
28
+ _DEFAULT_CACHE_SIZE = 1024
29
+
30
+ # Precision for rounding inputs (radians for lat/lon, km for radius, years for time)
31
+ # These control how aggressively similar inputs are grouped
32
+ _CACHE_PRECISION = {
33
+ "lat": 6, # ~0.1 meter precision at Earth surface
34
+ "lon": 6,
35
+ "r": 3, # 1 meter precision
36
+ "year": 2, # ~4 day precision
37
+ }
38
+
22
39
 
23
40
  class MagneticResult(NamedTuple):
24
41
  """Result of magnetic field computation.
@@ -404,37 +421,90 @@ def create_wmm2020_coefficients() -> MagneticCoefficients:
404
421
  WMM2020 = create_wmm2020_coefficients()
405
422
 
406
423
 
407
- def magnetic_field_spherical(
424
+ # =============================================================================
425
+ # Cached Computation Core
426
+ # =============================================================================
427
+
428
+
429
+ def _quantize_inputs(
430
+ lat: float, lon: float, r: float, year: float
431
+ ) -> Tuple[float, float, float, float]:
432
+ """Round inputs to cache precision for consistent cache hits."""
433
+ return (
434
+ round(lat, _CACHE_PRECISION["lat"]),
435
+ round(lon, _CACHE_PRECISION["lon"]),
436
+ round(r, _CACHE_PRECISION["r"]),
437
+ round(year, _CACHE_PRECISION["year"]),
438
+ )
439
+
440
+
441
+ @lru_cache(maxsize=_DEFAULT_CACHE_SIZE)
442
+ def _magnetic_field_spherical_cached(
408
443
  lat: float,
409
444
  lon: float,
410
445
  r: float,
411
446
  year: float,
412
- coeffs: MagneticCoefficients = WMM2020,
447
+ n_max: int,
448
+ coeff_id: int,
413
449
  ) -> Tuple[float, float, float]:
414
450
  """
415
- Compute magnetic field in spherical coordinates.
451
+ Cached core computation of magnetic field in spherical coordinates.
452
+
453
+ This is the internal cached version. The coefficient arrays are identified
454
+ by their id() since NamedTuples with numpy arrays aren't hashable.
416
455
 
417
456
  Parameters
418
457
  ----------
419
458
  lat : float
420
- Geocentric latitude in radians.
459
+ Geocentric latitude in radians (quantized).
421
460
  lon : float
422
- Longitude in radians.
461
+ Longitude in radians (quantized).
423
462
  r : float
424
- Radial distance from Earth's center in km.
463
+ Radial distance in km (quantized).
425
464
  year : float
426
- Decimal year (e.g., 2023.5 for mid-2023).
427
- coeffs : MagneticCoefficients, optional
428
- Model coefficients. Default WMM2020.
465
+ Decimal year (quantized).
466
+ n_max : int
467
+ Maximum spherical harmonic degree.
468
+ coeff_id : int
469
+ Unique identifier for the coefficient set.
429
470
 
430
471
  Returns
431
472
  -------
432
- B_r : float
433
- Radial component (positive outward) in nT.
434
- B_theta : float
435
- Colatitude component (positive southward) in nT.
436
- B_phi : float
437
- Longitude component (positive eastward) in nT.
473
+ B_r, B_theta, B_phi : tuple of float
474
+ Magnetic field components in spherical coordinates (nT).
475
+ """
476
+ # Retrieve coefficients from registry
477
+ coeffs = _coefficient_registry.get(coeff_id)
478
+ if coeffs is None:
479
+ raise ValueError(f"Coefficient set {coeff_id} not found in registry")
480
+
481
+ return _compute_magnetic_field_spherical_impl(lat, lon, r, year, coeffs)
482
+
483
+
484
+ # Registry to hold coefficient sets by id
485
+ _coefficient_registry: dict[str, Any] = {}
486
+
487
+
488
+ def _register_coefficients(coeffs: "MagneticCoefficients") -> int:
489
+ """Register a coefficient set and return its unique ID."""
490
+ coeff_id = id(coeffs)
491
+ if coeff_id not in _coefficient_registry:
492
+ _coefficient_registry[coeff_id] = coeffs
493
+ return coeff_id
494
+
495
+
496
+ def _compute_magnetic_field_spherical_impl(
497
+ lat: float,
498
+ lon: float,
499
+ r: float,
500
+ year: float,
501
+ coeffs: "MagneticCoefficients",
502
+ ) -> Tuple[float, float, float]:
503
+ """
504
+ Core implementation of magnetic field computation.
505
+
506
+ This contains the actual spherical harmonic expansion logic,
507
+ separated for clarity and to support caching.
438
508
  """
439
509
  n_max = coeffs.n_max
440
510
  a = 6371.2 # Reference radius in km (WMM convention)
@@ -452,11 +522,9 @@ def magnetic_field_spherical(
452
522
  sin_theta = np.sin(theta)
453
523
 
454
524
  # Compute associated Legendre functions (Schmidt semi-normalized)
455
- # WMM uses Schmidt semi-normalization
456
525
  P = associated_legendre(n_max, n_max, cos_theta, normalized=True)
457
526
 
458
- # Compute dP/dtheta = -sin(theta) * dP/d(cos(theta))
459
- # For efficiency, use recurrence relation
527
+ # Compute dP/dtheta
460
528
  dP = np.zeros((n_max + 1, n_max + 1))
461
529
  if abs(sin_theta) > 1e-10:
462
530
  for n in range(1, n_max + 1):
@@ -464,7 +532,6 @@ def magnetic_field_spherical(
464
532
  if m == n:
465
533
  dP[n, m] = n * cos_theta / sin_theta * P[n, m]
466
534
  elif n > m:
467
- # Recurrence relation for derivative
468
535
  factor = np.sqrt((n - m) * (n + m + 1))
469
536
  if m + 1 <= n:
470
537
  dP[n, m] = (
@@ -489,13 +556,10 @@ def magnetic_field_spherical(
489
556
  cos_m_lon = np.cos(m * lon)
490
557
  sin_m_lon = np.sin(m * lon)
491
558
 
492
- # Gauss coefficients
493
559
  gnm = g[n, m]
494
560
  hnm = h[n, m]
495
561
 
496
- # Field contributions
497
562
  B_r += (n + 1) * r_power * P[n, m] * (gnm * cos_m_lon + hnm * sin_m_lon)
498
-
499
563
  B_theta += -r_power * dP[n, m] * (gnm * cos_m_lon + hnm * sin_m_lon)
500
564
 
501
565
  if abs(sin_theta) > 1e-10:
@@ -510,6 +574,175 @@ def magnetic_field_spherical(
510
574
  return B_r, B_theta, B_phi
511
575
 
512
576
 
577
+ # =============================================================================
578
+ # Cache Management
579
+ # =============================================================================
580
+
581
+
582
+ def get_magnetic_cache_info() -> dict[str, Any]:
583
+ """
584
+ Get information about the magnetic field computation cache.
585
+
586
+ Returns
587
+ -------
588
+ info : dict
589
+ Dictionary containing cache statistics:
590
+ - hits: Number of cache hits
591
+ - misses: Number of cache misses
592
+ - maxsize: Maximum cache size
593
+ - currsize: Current number of cached entries
594
+ - hit_rate: Ratio of hits to total calls (0-1)
595
+
596
+ Examples
597
+ --------
598
+ >>> from pytcl.magnetism import get_magnetic_cache_info
599
+ >>> info = get_magnetic_cache_info()
600
+ >>> print(f"Cache hit rate: {info['hit_rate']:.1%}")
601
+ """
602
+ cache_info = _magnetic_field_spherical_cached.cache_info()
603
+ total = cache_info.hits + cache_info.misses
604
+ hit_rate = cache_info.hits / total if total > 0 else 0.0
605
+
606
+ return {
607
+ "hits": cache_info.hits,
608
+ "misses": cache_info.misses,
609
+ "maxsize": cache_info.maxsize,
610
+ "currsize": cache_info.currsize,
611
+ "hit_rate": hit_rate,
612
+ }
613
+
614
+
615
+ def clear_magnetic_cache() -> None:
616
+ """
617
+ Clear the magnetic field computation cache.
618
+
619
+ This can be useful when memory is constrained or when switching
620
+ between different coefficient sets.
621
+
622
+ Examples
623
+ --------
624
+ >>> from pytcl.magnetism import clear_magnetic_cache
625
+ >>> clear_magnetic_cache() # Free cached computations
626
+ """
627
+ _magnetic_field_spherical_cached.cache_clear()
628
+ _coefficient_registry.clear()
629
+
630
+
631
+ def configure_magnetic_cache(
632
+ maxsize: Optional[int] = None,
633
+ precision: Optional[dict[str, Any]] = None,
634
+ ) -> None:
635
+ """
636
+ Configure the magnetic field computation cache.
637
+
638
+ Parameters
639
+ ----------
640
+ maxsize : int, optional
641
+ Maximum number of entries in the cache. If None, keeps current.
642
+ Set to 0 to disable caching.
643
+ precision : dict, optional
644
+ Dictionary with keys 'lat', 'lon', 'r', 'year' specifying
645
+ decimal places for rounding. Higher values = more precision
646
+ but fewer cache hits.
647
+
648
+ Notes
649
+ -----
650
+ Changing cache configuration clears the existing cache.
651
+
652
+ Examples
653
+ --------
654
+ >>> from pytcl.magnetism import configure_magnetic_cache
655
+ >>> # Increase cache size for batch processing
656
+ >>> configure_magnetic_cache(maxsize=4096)
657
+ >>> # Reduce precision for more cache hits
658
+ >>> configure_magnetic_cache(precision={'lat': 4, 'lon': 4, 'r': 2, 'year': 1})
659
+ """
660
+ global _magnetic_field_spherical_cached
661
+
662
+ if precision is not None:
663
+ for key in ["lat", "lon", "r", "year"]:
664
+ if key in precision:
665
+ _CACHE_PRECISION[key] = precision[key]
666
+
667
+ if maxsize is not None:
668
+ # Recreate the cached function with new maxsize
669
+ clear_magnetic_cache()
670
+
671
+ @lru_cache(maxsize=maxsize)
672
+ def new_cached(
673
+ lat: float,
674
+ lon: float,
675
+ r: float,
676
+ year: float,
677
+ n_max: int,
678
+ coeff_id: int,
679
+ ) -> Tuple[float, float, float]:
680
+ coeffs = _coefficient_registry.get(coeff_id)
681
+ if coeffs is None:
682
+ raise ValueError(f"Coefficient set {coeff_id} not found")
683
+ return _compute_magnetic_field_spherical_impl(lat, lon, r, year, coeffs)
684
+
685
+ _magnetic_field_spherical_cached = new_cached
686
+
687
+
688
+ def magnetic_field_spherical(
689
+ lat: float,
690
+ lon: float,
691
+ r: float,
692
+ year: float,
693
+ coeffs: MagneticCoefficients = WMM2020,
694
+ use_cache: bool = True,
695
+ ) -> Tuple[float, float, float]:
696
+ """
697
+ Compute magnetic field in spherical coordinates.
698
+
699
+ Parameters
700
+ ----------
701
+ lat : float
702
+ Geocentric latitude in radians.
703
+ lon : float
704
+ Longitude in radians.
705
+ r : float
706
+ Radial distance from Earth's center in km.
707
+ year : float
708
+ Decimal year (e.g., 2023.5 for mid-2023).
709
+ coeffs : MagneticCoefficients, optional
710
+ Model coefficients. Default WMM2020.
711
+ use_cache : bool, optional
712
+ Whether to use LRU caching for repeated queries. Default True.
713
+ Set to False for single-use queries or when memory is constrained.
714
+
715
+ Returns
716
+ -------
717
+ B_r : float
718
+ Radial component (positive outward) in nT.
719
+ B_theta : float
720
+ Colatitude component (positive southward) in nT.
721
+ B_phi : float
722
+ Longitude component (positive eastward) in nT.
723
+
724
+ Notes
725
+ -----
726
+ Results are cached by default using LRU caching. Inputs are quantized
727
+ to a configurable precision before caching to improve hit rates for
728
+ nearby queries. Use `get_magnetic_cache_info()` to check cache
729
+ statistics and `clear_magnetic_cache()` to free memory.
730
+ """
731
+ if use_cache:
732
+ # Quantize inputs for cache key
733
+ q_lat, q_lon, q_r, q_year = _quantize_inputs(lat, lon, r, year)
734
+
735
+ # Register coefficients and get ID
736
+ coeff_id = _register_coefficients(coeffs)
737
+
738
+ return _magnetic_field_spherical_cached(
739
+ q_lat, q_lon, q_r, q_year, coeffs.n_max, coeff_id
740
+ )
741
+ else:
742
+ # Direct computation without caching
743
+ return _compute_magnetic_field_spherical_impl(lat, lon, r, year, coeffs)
744
+
745
+
513
746
  def wmm(
514
747
  lat: float,
515
748
  lon: float,
@@ -706,4 +939,8 @@ __all__ = [
706
939
  "magnetic_declination",
707
940
  "magnetic_inclination",
708
941
  "magnetic_field_intensity",
942
+ # Cache management
943
+ "get_magnetic_cache_info",
944
+ "clear_magnetic_cache",
945
+ "configure_magnetic_cache",
709
946
  ]
@@ -7,7 +7,7 @@ related operations commonly used in assignment problems and data association.
7
7
 
8
8
  import itertools
9
9
  from functools import lru_cache
10
- from typing import Iterator, List, Optional, Tuple
10
+ from typing import Any, Iterator, List, Optional, Tuple
11
11
 
12
12
  from numpy.typing import ArrayLike
13
13
 
@@ -108,7 +108,7 @@ def n_permute_k(n: int, k: int) -> int:
108
108
  def permutations(
109
109
  items: ArrayLike,
110
110
  k: Optional[int] = None,
111
- ) -> Iterator[Tuple]:
111
+ ) -> Iterator[tuple[Any, ...]]:
112
112
  """
113
113
  Generate all k-permutations of items.
114
114
 
@@ -136,7 +136,7 @@ def permutations(
136
136
  def combinations(
137
137
  items: ArrayLike,
138
138
  k: int,
139
- ) -> Iterator[Tuple]:
139
+ ) -> Iterator[tuple[Any, ...]]:
140
140
  """
141
141
  Generate all k-combinations of items.
142
142
 
@@ -164,7 +164,7 @@ def combinations(
164
164
  def combinations_with_replacement(
165
165
  items: ArrayLike,
166
166
  k: int,
167
- ) -> Iterator[Tuple]:
167
+ ) -> Iterator[tuple[Any, ...]]:
168
168
  """
169
169
  Generate all k-combinations with replacement.
170
170
 
@@ -263,7 +263,7 @@ def permutation_unrank(rank: int, n: int) -> List[int]:
263
263
  return perm
264
264
 
265
265
 
266
- def next_permutation(perm: ArrayLike) -> Optional[List]:
266
+ def next_permutation(perm: ArrayLike) -> Optional[List[Any]]:
267
267
  """
268
268
  Generate the next permutation in lexicographic order.
269
269
 
@@ -5,7 +5,7 @@ This module provides geometric functions for points, lines, planes,
5
5
  polygons, and related operations used in tracking applications.
6
6
  """
7
7
 
8
- from typing import Optional, Tuple
8
+ from typing import Any, Optional, Tuple
9
9
 
10
10
  import numpy as np
11
11
  from numpy.typing import ArrayLike, NDArray
@@ -527,12 +527,12 @@ def minimum_bounding_circle(
527
527
  """
528
528
  points = np.asarray(points, dtype=np.float64)
529
529
 
530
- def circle_from_two_points(p1, p2):
530
+ def circle_from_two_points(p1: Any, p2: Any) -> tuple[Any, Any]:
531
531
  center = (p1 + p2) / 2
532
532
  radius = np.linalg.norm(p1 - center)
533
533
  return center, radius
534
534
 
535
- def circle_from_three_points(p1, p2, p3):
535
+ def circle_from_three_points(p1: Any, p2: Any, p3: Any) -> tuple[Any, Any]:
536
536
  ax, ay = p1
537
537
  bx, by = p2
538
538
  cx, cy = p3
@@ -565,10 +565,10 @@ def minimum_bounding_circle(
565
565
  radius = np.linalg.norm(p1 - center)
566
566
  return center, radius
567
567
 
568
- def is_inside(c, r, p):
568
+ def is_inside(c: Any, r: Any, p: Any) -> Any:
569
569
  return np.linalg.norm(p - c) <= r + 1e-10
570
570
 
571
- def welzl(P, R):
571
+ def welzl(P: Any, R: Any) -> tuple[Any, Any]:
572
572
  if len(P) == 0 or len(R) == 3:
573
573
  if len(R) == 0:
574
574
  return np.array([0.0, 0.0]), 0.0
@@ -5,7 +5,7 @@ This module provides Gaussian quadrature rules and numerical integration
5
5
  functions commonly used in state estimation and filtering.
6
6
  """
7
7
 
8
- from typing import Callable, Literal, Optional, Tuple
8
+ from typing import Any, Callable, Literal, Optional, Tuple
9
9
 
10
10
  import numpy as np
11
11
  import scipy.integrate as integrate
@@ -158,7 +158,7 @@ def quad(
158
158
  f: Callable[[float], float],
159
159
  a: float,
160
160
  b: float,
161
- **kwargs,
161
+ **kwargs: Any,
162
162
  ) -> Tuple[float, float]:
163
163
  """
164
164
  Adaptive quadrature integration.
@@ -203,7 +203,7 @@ def dblquad(
203
203
  b: float,
204
204
  gfun: Callable[[float], float],
205
205
  hfun: Callable[[float], float],
206
- **kwargs,
206
+ **kwargs: Any,
207
207
  ) -> Tuple[float, float]:
208
208
  """
209
209
  Double integration.
@@ -248,7 +248,7 @@ def tplquad(
248
248
  hfun: Callable[[float], float],
249
249
  qfun: Callable[[float, float], float],
250
250
  rfun: Callable[[float, float], float],
251
- **kwargs,
251
+ **kwargs: Any,
252
252
  ) -> Tuple[float, float]:
253
253
  """
254
254
  Triple integration.
@@ -290,11 +290,11 @@ def tplquad(
290
290
 
291
291
 
292
292
  def fixed_quad(
293
- f: Callable[[NDArray], NDArray],
293
+ f: Callable[[np.ndarray[Any, Any]], np.ndarray[Any, Any]],
294
294
  a: float,
295
295
  b: float,
296
296
  n: int = 5,
297
- ) -> Tuple[float, None]:
297
+ ) -> tuple[float, None]:
298
298
  """
299
299
  Fixed-order Gaussian quadrature.
300
300
 
@@ -24,7 +24,7 @@ References
24
24
  Systems, 19(4), 608-621.
25
25
  """
26
26
 
27
- from typing import NamedTuple, Optional
27
+ from typing import Any, NamedTuple, Optional
28
28
 
29
29
  import numpy as np
30
30
  from numba import njit, prange
@@ -214,12 +214,12 @@ def detection_probability(
214
214
 
215
215
  @njit(cache=True, fastmath=True)
216
216
  def _cfar_ca_kernel(
217
- signal: np.ndarray,
217
+ signal: np.ndarray[Any, Any],
218
218
  guard_cells: int,
219
219
  ref_cells: int,
220
220
  alpha: float,
221
- noise_estimate: np.ndarray,
222
- threshold: np.ndarray,
221
+ noise_estimate: np.ndarray[Any, Any],
222
+ threshold: np.ndarray[Any, Any],
223
223
  ) -> None:
224
224
  """JIT-compiled CA-CFAR kernel."""
225
225
  n = len(signal)
@@ -252,12 +252,12 @@ def _cfar_ca_kernel(
252
252
 
253
253
  @njit(cache=True, fastmath=True)
254
254
  def _cfar_go_kernel(
255
- signal: np.ndarray,
255
+ signal: np.ndarray[Any, Any],
256
256
  guard_cells: int,
257
257
  ref_cells: int,
258
258
  alpha: float,
259
- noise_estimate: np.ndarray,
260
- threshold: np.ndarray,
259
+ noise_estimate: np.ndarray[Any, Any],
260
+ threshold: np.ndarray[Any, Any],
261
261
  ) -> None:
262
262
  """JIT-compiled GO-CFAR kernel."""
263
263
  n = len(signal)
@@ -290,12 +290,12 @@ def _cfar_go_kernel(
290
290
 
291
291
  @njit(cache=True, fastmath=True)
292
292
  def _cfar_so_kernel(
293
- signal: np.ndarray,
293
+ signal: np.ndarray[Any, Any],
294
294
  guard_cells: int,
295
295
  ref_cells: int,
296
296
  alpha: float,
297
- noise_estimate: np.ndarray,
298
- threshold: np.ndarray,
297
+ noise_estimate: np.ndarray[Any, Any],
298
+ threshold: np.ndarray[Any, Any],
299
299
  ) -> None:
300
300
  """JIT-compiled SO-CFAR kernel."""
301
301
  n = len(signal)
@@ -331,13 +331,13 @@ def _cfar_so_kernel(
331
331
 
332
332
  @njit(cache=True, fastmath=True)
333
333
  def _cfar_os_kernel(
334
- signal: np.ndarray,
334
+ signal: np.ndarray[Any, Any],
335
335
  guard_cells: int,
336
336
  ref_cells: int,
337
337
  k: int,
338
338
  alpha: float,
339
- noise_estimate: np.ndarray,
340
- threshold: np.ndarray,
339
+ noise_estimate: np.ndarray[Any, Any],
340
+ threshold: np.ndarray[Any, Any],
341
341
  ) -> None:
342
342
  """JIT-compiled OS-CFAR kernel."""
343
343
  n = len(signal)
@@ -378,14 +378,14 @@ def _cfar_os_kernel(
378
378
 
379
379
  @njit(cache=True, fastmath=True, parallel=True)
380
380
  def _cfar_2d_ca_kernel(
381
- image: np.ndarray,
381
+ image: np.ndarray[Any, Any],
382
382
  guard_rows: int,
383
383
  guard_cols: int,
384
384
  ref_rows: int,
385
385
  ref_cols: int,
386
386
  alpha: float,
387
- noise_estimate: np.ndarray,
388
- threshold: np.ndarray,
387
+ noise_estimate: np.ndarray[Any, Any],
388
+ threshold: np.ndarray[Any, Any],
389
389
  ) -> None:
390
390
  """JIT-compiled 2D CA-CFAR kernel with parallel execution."""
391
391
  n_rows, n_cols = image.shape
@@ -426,14 +426,14 @@ def _cfar_2d_ca_kernel(
426
426
 
427
427
  @njit(cache=True, fastmath=True, parallel=True)
428
428
  def _cfar_2d_go_kernel(
429
- image: np.ndarray,
429
+ image: np.ndarray[Any, Any],
430
430
  guard_rows: int,
431
431
  guard_cols: int,
432
432
  ref_rows: int,
433
433
  ref_cols: int,
434
434
  alpha: float,
435
- noise_estimate: np.ndarray,
436
- threshold: np.ndarray,
435
+ noise_estimate: np.ndarray[Any, Any],
436
+ threshold: np.ndarray[Any, Any],
437
437
  ) -> None:
438
438
  """JIT-compiled 2D GO-CFAR kernel with parallel execution."""
439
439
  n_rows, n_cols = image.shape
@@ -478,14 +478,14 @@ def _cfar_2d_go_kernel(
478
478
 
479
479
  @njit(cache=True, fastmath=True, parallel=True)
480
480
  def _cfar_2d_so_kernel(
481
- image: np.ndarray,
481
+ image: np.ndarray[Any, Any],
482
482
  guard_rows: int,
483
483
  guard_cols: int,
484
484
  ref_rows: int,
485
485
  ref_cols: int,
486
486
  alpha: float,
487
- noise_estimate: np.ndarray,
488
- threshold: np.ndarray,
487
+ noise_estimate: np.ndarray[Any, Any],
488
+ threshold: np.ndarray[Any, Any],
489
489
  ) -> None:
490
490
  """JIT-compiled 2D SO-CFAR kernel with parallel execution."""
491
491
  n_rows, n_cols = image.shape
@@ -830,8 +830,8 @@ def cfar_os(
830
830
 
831
831
  def cfar_2d(
832
832
  image: ArrayLike,
833
- guard_cells: tuple,
834
- ref_cells: tuple,
833
+ guard_cells: tuple[int, int],
834
+ ref_cells: tuple[int, int],
835
835
  pfa: float = 1e-6,
836
836
  method: str = "ca",
837
837
  alpha: Optional[float] = None,