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
@@ -165,11 +165,13 @@ def ecef2geodetic(
165
165
  N = a / np.sqrt(1 - e2 * sin_lat**2)
166
166
 
167
167
  # Altitude
168
- alt = np.where(
169
- np.abs(cos_lat) > 1e-10,
170
- p / cos_lat - N,
171
- np.abs(z) / np.abs(sin_lat) - N * (1 - e2),
172
- )
168
+ # Use cos_lat when available, otherwise use sin_lat with guard against division by zero
169
+ with np.errstate(divide="ignore", invalid="ignore"):
170
+ alt = np.where(
171
+ np.abs(cos_lat) > 1e-10,
172
+ p / cos_lat - N,
173
+ np.abs(z) / np.abs(sin_lat) - N * (1 - e2),
174
+ )
173
175
  else:
174
176
  # Direct/closed-form method (simplified Vermeille)
175
177
  zp = np.abs(z)
@@ -525,6 +527,267 @@ def ned2enu(ned: ArrayLike) -> NDArray[np.floating]:
525
527
  return np.array([ned[1], ned[0], -ned[2]], dtype=np.float64)
526
528
 
527
529
 
530
+ def geodetic2sez(
531
+ lat: ArrayLike,
532
+ lon: ArrayLike,
533
+ alt: ArrayLike,
534
+ lat_ref: float,
535
+ lon_ref: float,
536
+ alt_ref: float,
537
+ a: float = WGS84.a,
538
+ f: float = WGS84.f,
539
+ ) -> NDArray[np.floating]:
540
+ """
541
+ Convert geodetic coordinates to local SEZ (South-East-Zenith) coordinates.
542
+
543
+ SEZ is a horizon-relative coordinate frame where:
544
+ - S (South) points in the southward direction
545
+ - E (East) points in the eastward direction
546
+ - Z (Zenith) points upward (away from Earth center)
547
+
548
+ Parameters
549
+ ----------
550
+ lat : array_like
551
+ Geodetic latitude in radians.
552
+ lon : array_like
553
+ Geodetic longitude in radians.
554
+ alt : array_like
555
+ Altitude in meters.
556
+ lat_ref : float
557
+ Reference point latitude in radians.
558
+ lon_ref : float
559
+ Reference point longitude in radians.
560
+ alt_ref : float
561
+ Reference point altitude in meters.
562
+ a : float, optional
563
+ Semi-major axis of the reference ellipsoid.
564
+ f : float, optional
565
+ Flattening of the reference ellipsoid.
566
+
567
+ Returns
568
+ -------
569
+ sez : ndarray
570
+ Local SEZ coordinates [south, east, zenith] in meters.
571
+
572
+ See Also
573
+ --------
574
+ sez2geodetic : Inverse conversion.
575
+ ecef2sez : ECEF to SEZ conversion.
576
+
577
+ Notes
578
+ -----
579
+ SEZ is equivalent to NED when azimuth is measured from south.
580
+ Conversion: SEZ = [S, E, Z] = [NED[0], NED[1], -NED[2]]
581
+
582
+ Examples
583
+ --------
584
+ >>> sez = geodetic2sez(lat, lon, alt, lat_ref, lon_ref, alt_ref)
585
+ """
586
+ # Convert both to ECEF
587
+ ecef = geodetic2ecef(lat, lon, alt, a, f)
588
+ ecef_ref = geodetic2ecef(lat_ref, lon_ref, alt_ref, a, f)
589
+
590
+ # Get SEZ from ECEF difference
591
+ return ecef2sez(ecef, lat_ref, lon_ref, ecef_ref)
592
+
593
+
594
+ def ecef2sez(
595
+ ecef: ArrayLike,
596
+ lat_ref: float,
597
+ lon_ref: float,
598
+ ecef_ref: Optional[ArrayLike] = None,
599
+ ) -> NDArray[np.floating]:
600
+ """
601
+ Convert ECEF coordinates to local SEZ coordinates.
602
+
603
+ Parameters
604
+ ----------
605
+ ecef : array_like
606
+ ECEF coordinates [X, Y, Z] in meters, shape (3,) or (3, N).
607
+ lat_ref : float
608
+ Reference point latitude in radians.
609
+ lon_ref : float
610
+ Reference point longitude in radians.
611
+ ecef_ref : array_like, optional
612
+ Reference ECEF position. If None, the reference point is
613
+ at (lat_ref, lon_ref) with zero altitude.
614
+
615
+ Returns
616
+ -------
617
+ sez : ndarray
618
+ SEZ coordinates [south, east, zenith] in meters.
619
+
620
+ See Also
621
+ --------
622
+ sez2ecef : Inverse conversion.
623
+ """
624
+ ecef = np.asarray(ecef, dtype=np.float64)
625
+
626
+ if ecef_ref is None:
627
+ ecef_ref = geodetic2ecef(lat_ref, lon_ref, 0.0)
628
+ else:
629
+ ecef_ref = np.asarray(ecef_ref, dtype=np.float64)
630
+
631
+ # Relative position in ECEF
632
+ if ecef.ndim == 1:
633
+ delta_ecef = ecef - ecef_ref
634
+ else:
635
+ if ecef.shape[0] != 3:
636
+ ecef = ecef.T
637
+ if ecef_ref.ndim == 1:
638
+ delta_ecef = ecef - ecef_ref[:, np.newaxis]
639
+ else:
640
+ delta_ecef = ecef - ecef_ref[:, np.newaxis]
641
+
642
+ # Rotation matrix from ECEF to SEZ
643
+ sin_lat = np.sin(lat_ref)
644
+ cos_lat = np.cos(lat_ref)
645
+ sin_lon = np.sin(lon_ref)
646
+ cos_lon = np.cos(lon_ref)
647
+
648
+ # SEZ rotation matrix (transforms ECEF delta to SEZ)
649
+ # S = -sin(lat)*cos(lon)*dX - sin(lat)*sin(lon)*dY + cos(lat)*dZ
650
+ # E = -sin(lon)*dX + cos(lon)*dY
651
+ # Z = cos(lat)*cos(lon)*dX + cos(lat)*sin(lon)*dY + sin(lat)*dZ
652
+
653
+ if delta_ecef.ndim == 1:
654
+ s = (
655
+ -sin_lat * cos_lon * delta_ecef[0]
656
+ - sin_lat * sin_lon * delta_ecef[1]
657
+ + cos_lat * delta_ecef[2]
658
+ )
659
+ e = -sin_lon * delta_ecef[0] + cos_lon * delta_ecef[1]
660
+ z = (
661
+ cos_lat * cos_lon * delta_ecef[0]
662
+ + cos_lat * sin_lon * delta_ecef[1]
663
+ + sin_lat * delta_ecef[2]
664
+ )
665
+ return np.array([s, e, z], dtype=np.float64)
666
+ else:
667
+ s = (
668
+ -sin_lat * cos_lon * delta_ecef[0, :]
669
+ - sin_lat * sin_lon * delta_ecef[1, :]
670
+ + cos_lat * delta_ecef[2, :]
671
+ )
672
+ e = -sin_lon * delta_ecef[0, :] + cos_lon * delta_ecef[1, :]
673
+ z = (
674
+ cos_lat * cos_lon * delta_ecef[0, :]
675
+ + cos_lat * sin_lon * delta_ecef[1, :]
676
+ + sin_lat * delta_ecef[2, :]
677
+ )
678
+ return np.array([s, e, z], dtype=np.float64)
679
+
680
+
681
+ def sez2ecef(
682
+ sez: ArrayLike,
683
+ lat_ref: float,
684
+ lon_ref: float,
685
+ ecef_ref: Optional[ArrayLike] = None,
686
+ ) -> NDArray[np.floating]:
687
+ """
688
+ Convert local SEZ coordinates to ECEF coordinates.
689
+
690
+ Parameters
691
+ ----------
692
+ sez : array_like
693
+ SEZ coordinates [south, east, zenith] in meters, shape (3,) or (3, N).
694
+ lat_ref : float
695
+ Reference point latitude in radians.
696
+ lon_ref : float
697
+ Reference point longitude in radians.
698
+ ecef_ref : array_like, optional
699
+ Reference ECEF position. If None, the reference point is
700
+ at (lat_ref, lon_ref) with zero altitude.
701
+
702
+ Returns
703
+ -------
704
+ ecef : ndarray
705
+ ECEF coordinates [X, Y, Z] in meters.
706
+
707
+ See Also
708
+ --------
709
+ ecef2sez : Forward conversion.
710
+ """
711
+ sez = np.asarray(sez, dtype=np.float64)
712
+
713
+ if ecef_ref is None:
714
+ ecef_ref = geodetic2ecef(lat_ref, lon_ref, 0.0)
715
+ else:
716
+ ecef_ref = np.asarray(ecef_ref, dtype=np.float64)
717
+
718
+ # Rotation matrix from SEZ to ECEF (transpose of ECEF to SEZ)
719
+ sin_lat = np.sin(lat_ref)
720
+ cos_lat = np.cos(lat_ref)
721
+ sin_lon = np.sin(lon_ref)
722
+ cos_lon = np.cos(lon_ref)
723
+
724
+ # Inverse rotation: ECEF = ECEF_ref + R_inv @ SEZ
725
+ if sez.ndim == 1:
726
+ dX = -sin_lat * cos_lon * sez[0] - sin_lon * sez[1] + cos_lat * cos_lon * sez[2]
727
+ dY = -sin_lat * sin_lon * sez[0] + cos_lon * sez[1] + cos_lat * sin_lon * sez[2]
728
+ dZ = cos_lat * sez[0] + sin_lat * sez[2]
729
+ return ecef_ref + np.array([dX, dY, dZ], dtype=np.float64)
730
+ else:
731
+ if sez.shape[0] != 3:
732
+ sez = sez.T
733
+ dX = (
734
+ -sin_lat * cos_lon * sez[0, :]
735
+ - sin_lon * sez[1, :]
736
+ + cos_lat * cos_lon * sez[2, :]
737
+ )
738
+ dY = (
739
+ -sin_lat * sin_lon * sez[0, :]
740
+ + cos_lon * sez[1, :]
741
+ + cos_lat * sin_lon * sez[2, :]
742
+ )
743
+ dZ = cos_lat * sez[0, :] + sin_lat * sez[2, :]
744
+ return ecef_ref[:, np.newaxis] + np.array([dX, dY, dZ], dtype=np.float64)
745
+
746
+
747
+ def sez2geodetic(
748
+ sez: ArrayLike,
749
+ lat_ref: float,
750
+ lon_ref: float,
751
+ alt_ref: float,
752
+ a: float = WGS84.a,
753
+ f: float = WGS84.f,
754
+ ) -> Tuple[NDArray[np.floating], NDArray[np.floating], NDArray[np.floating]]:
755
+ """
756
+ Convert local SEZ coordinates to geodetic coordinates.
757
+
758
+ Parameters
759
+ ----------
760
+ sez : array_like
761
+ SEZ coordinates [south, east, zenith] in meters.
762
+ lat_ref : float
763
+ Reference point latitude in radians.
764
+ lon_ref : float
765
+ Reference point longitude in radians.
766
+ alt_ref : float
767
+ Reference point altitude in meters.
768
+ a : float, optional
769
+ Semi-major axis.
770
+ f : float, optional
771
+ Flattening.
772
+
773
+ Returns
774
+ -------
775
+ lat : ndarray
776
+ Geodetic latitude in radians.
777
+ lon : ndarray
778
+ Geodetic longitude in radians.
779
+ alt : ndarray
780
+ Altitude in meters.
781
+
782
+ See Also
783
+ --------
784
+ geodetic2sez : Forward conversion.
785
+ """
786
+ ecef_ref = geodetic2ecef(lat_ref, lon_ref, alt_ref, a, f)
787
+ ecef = sez2ecef(sez, lat_ref, lon_ref, ecef_ref)
788
+ return ecef2geodetic(ecef, a, f)
789
+
790
+
528
791
  def geocentric_radius(
529
792
  lat: ArrayLike,
530
793
  a: float = WGS84.a,
@@ -625,6 +888,10 @@ __all__ = [
625
888
  "ned2ecef",
626
889
  "enu2ned",
627
890
  "ned2enu",
891
+ "geodetic2sez",
892
+ "ecef2sez",
893
+ "sez2ecef",
894
+ "sez2geodetic",
628
895
  "geocentric_radius",
629
896
  "prime_vertical_radius",
630
897
  "meridional_radius",
@@ -6,7 +6,7 @@ coordinate transformations, essential for error propagation in tracking
6
6
  filters (e.g., converting measurement covariances between coordinate systems).
7
7
  """
8
8
 
9
- from typing import Literal
9
+ from typing import Callable, Literal
10
10
 
11
11
  import numpy as np
12
12
  from numpy.typing import ArrayLike, NDArray
@@ -431,7 +431,7 @@ def cross_covariance_transform(
431
431
 
432
432
 
433
433
  def numerical_jacobian(
434
- func,
434
+ func: Callable[[ArrayLike], ArrayLike],
435
435
  x: ArrayLike,
436
436
  dx: float = 1e-7,
437
437
  ) -> NDArray[np.floating]:
@@ -27,7 +27,7 @@ Examples
27
27
  """
28
28
 
29
29
  from pytcl.coordinate_systems.projections.projections import (
30
- WGS84_A, # Constants; Result types; Azimuthal Equidistant; UTM; Lambert Conformal Conic; Mercator; Stereographic; Transverse Mercator
30
+ WGS84_A, # Constants; Result types
31
31
  )
32
32
  from pytcl.coordinate_systems.projections.projections import (
33
33
  WGS84_B,
@@ -25,7 +25,7 @@ References
25
25
  nanometers." Journal of Geodesy 85.8 (2011): 475-485.
26
26
  """
27
27
 
28
- from typing import NamedTuple, Optional, Tuple
28
+ from typing import Any, NamedTuple, Optional, Tuple
29
29
 
30
30
  import numpy as np
31
31
  from numpy.typing import NDArray
@@ -1253,7 +1253,7 @@ def geodetic2utm_batch(
1253
1253
  lats: NDArray[np.floating],
1254
1254
  lons: NDArray[np.floating],
1255
1255
  zone: Optional[int] = None,
1256
- ) -> Tuple[NDArray[np.floating], NDArray[np.floating], NDArray[np.int_], NDArray]:
1256
+ ) -> tuple[NDArray[np.floating], NDArray[np.floating], NDArray[np.intp], NDArray[Any]]:
1257
1257
  """
1258
1258
  Batch convert geodetic coordinates to UTM.
1259
1259
 
@@ -6,7 +6,7 @@ representations including rotation matrices, quaternions, Euler angles,
6
6
  axis-angle, and Rodrigues parameters.
7
7
  """
8
8
 
9
- from typing import Tuple
9
+ from typing import Any, Tuple
10
10
 
11
11
  import numpy as np
12
12
  from numba import njit
@@ -14,7 +14,7 @@ from numpy.typing import ArrayLike, NDArray
14
14
 
15
15
 
16
16
  @njit(cache=True, fastmath=True)
17
- def _rotx_inplace(angle: float, R: np.ndarray) -> None:
17
+ def _rotx_inplace(angle: float, R: np.ndarray[Any, Any]) -> None:
18
18
  """JIT-compiled rotation about x-axis (fills existing matrix)."""
19
19
  c = np.cos(angle)
20
20
  s = np.sin(angle)
@@ -30,7 +30,7 @@ def _rotx_inplace(angle: float, R: np.ndarray) -> None:
30
30
 
31
31
 
32
32
  @njit(cache=True, fastmath=True)
33
- def _roty_inplace(angle: float, R: np.ndarray) -> None:
33
+ def _roty_inplace(angle: float, R: np.ndarray[Any, Any]) -> None:
34
34
  """JIT-compiled rotation about y-axis (fills existing matrix)."""
35
35
  c = np.cos(angle)
36
36
  s = np.sin(angle)
@@ -46,7 +46,7 @@ def _roty_inplace(angle: float, R: np.ndarray) -> None:
46
46
 
47
47
 
48
48
  @njit(cache=True, fastmath=True)
49
- def _rotz_inplace(angle: float, R: np.ndarray) -> None:
49
+ def _rotz_inplace(angle: float, R: np.ndarray[Any, Any]) -> None:
50
50
  """JIT-compiled rotation about z-axis (fills existing matrix)."""
51
51
  c = np.cos(angle)
52
52
  s = np.sin(angle)
@@ -62,7 +62,9 @@ def _rotz_inplace(angle: float, R: np.ndarray) -> None:
62
62
 
63
63
 
64
64
  @njit(cache=True, fastmath=True)
65
- def _euler_zyx_to_rotmat(yaw: float, pitch: float, roll: float, R: np.ndarray) -> None:
65
+ def _euler_zyx_to_rotmat(
66
+ yaw: float, pitch: float, roll: float, R: np.ndarray[Any, Any]
67
+ ) -> None:
66
68
  """JIT-compiled ZYX Euler angles to rotation matrix."""
67
69
  cy = np.cos(yaw)
68
70
  sy = np.sin(yaw)
@@ -84,7 +86,9 @@ def _euler_zyx_to_rotmat(yaw: float, pitch: float, roll: float, R: np.ndarray) -
84
86
 
85
87
 
86
88
  @njit(cache=True, fastmath=True)
87
- def _matmul_3x3(A: np.ndarray, B: np.ndarray, C: np.ndarray) -> None:
89
+ def _matmul_3x3(
90
+ A: np.ndarray[Any, Any], B: np.ndarray[Any, Any], C: np.ndarray[Any, Any]
91
+ ) -> None:
88
92
  """JIT-compiled 3x3 matrix multiplication C = A @ B."""
89
93
  for i in range(3):
90
94
  for j in range(3):
pytcl/core/__init__.py CHANGED
@@ -24,10 +24,19 @@ from pytcl.core.constants import (
24
24
  PhysicalConstants,
25
25
  )
26
26
  from pytcl.core.validation import (
27
+ ArraySpec,
28
+ ScalarSpec,
29
+ ValidationError,
30
+ check_compatible_shapes,
27
31
  ensure_2d,
28
32
  ensure_column_vector,
33
+ ensure_positive_definite,
29
34
  ensure_row_vector,
35
+ ensure_square_matrix,
36
+ ensure_symmetric,
30
37
  validate_array,
38
+ validate_inputs,
39
+ validate_same_shape,
31
40
  )
32
41
 
33
42
  __all__ = [
@@ -40,10 +49,19 @@ __all__ = [
40
49
  "WGS84",
41
50
  "PhysicalConstants",
42
51
  # Validation
52
+ "ValidationError",
43
53
  "validate_array",
54
+ "validate_inputs",
55
+ "validate_same_shape",
56
+ "check_compatible_shapes",
57
+ "ArraySpec",
58
+ "ScalarSpec",
44
59
  "ensure_2d",
45
60
  "ensure_column_vector",
46
61
  "ensure_row_vector",
62
+ "ensure_square_matrix",
63
+ "ensure_symmetric",
64
+ "ensure_positive_definite",
47
65
  # Array utilities
48
66
  "wrap_to_pi",
49
67
  "wrap_to_2pi",