nrl-tracker 1.9.1__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 (60) hide show
  1. {nrl_tracker-1.9.1.dist-info → nrl_tracker-1.9.2.dist-info}/METADATA +4 -4
  2. {nrl_tracker-1.9.1.dist-info → nrl_tracker-1.9.2.dist-info}/RECORD +60 -59
  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/dynamic_estimation/gaussian_sum_filter.py +55 -0
  22. pytcl/dynamic_estimation/imm.py +29 -0
  23. pytcl/dynamic_estimation/information_filter.py +64 -0
  24. pytcl/dynamic_estimation/kalman/extended.py +56 -0
  25. pytcl/dynamic_estimation/kalman/linear.py +69 -0
  26. pytcl/dynamic_estimation/kalman/unscented.py +81 -0
  27. pytcl/dynamic_estimation/particle_filters/bootstrap.py +146 -0
  28. pytcl/dynamic_estimation/rbpf.py +51 -0
  29. pytcl/dynamic_estimation/smoothers.py +58 -0
  30. pytcl/dynamic_models/continuous_time/dynamics.py +104 -0
  31. pytcl/dynamic_models/discrete_time/coordinated_turn.py +6 -0
  32. pytcl/dynamic_models/discrete_time/singer.py +12 -0
  33. pytcl/dynamic_models/process_noise/coordinated_turn.py +46 -0
  34. pytcl/dynamic_models/process_noise/polynomial.py +6 -0
  35. pytcl/dynamic_models/process_noise/singer.py +52 -0
  36. pytcl/gravity/clenshaw.py +60 -0
  37. pytcl/gravity/egm.py +47 -0
  38. pytcl/gravity/models.py +34 -0
  39. pytcl/gravity/spherical_harmonics.py +73 -0
  40. pytcl/gravity/tides.py +34 -0
  41. pytcl/mathematical_functions/numerical_integration/quadrature.py +85 -0
  42. pytcl/mathematical_functions/special_functions/bessel.py +55 -0
  43. pytcl/mathematical_functions/special_functions/elliptic.py +42 -0
  44. pytcl/mathematical_functions/special_functions/error_functions.py +49 -0
  45. pytcl/mathematical_functions/special_functions/gamma_functions.py +43 -0
  46. pytcl/mathematical_functions/special_functions/lambert_w.py +5 -0
  47. pytcl/mathematical_functions/special_functions/marcum_q.py +16 -0
  48. pytcl/navigation/geodesy.py +101 -2
  49. pytcl/navigation/great_circle.py +71 -0
  50. pytcl/navigation/rhumb.py +74 -0
  51. pytcl/performance_evaluation/estimation_metrics.py +70 -0
  52. pytcl/performance_evaluation/track_metrics.py +30 -0
  53. pytcl/static_estimation/maximum_likelihood.py +54 -0
  54. pytcl/static_estimation/robust.py +57 -0
  55. pytcl/terrain/dem.py +69 -0
  56. pytcl/terrain/visibility.py +65 -0
  57. pytcl/trackers/hypothesis.py +65 -0
  58. {nrl_tracker-1.9.1.dist-info → nrl_tracker-1.9.2.dist-info}/LICENSE +0 -0
  59. {nrl_tracker-1.9.1.dist-info → nrl_tracker-1.9.2.dist-info}/WHEEL +0 -0
  60. {nrl_tracker-1.9.1.dist-info → nrl_tracker-1.9.2.dist-info}/top_level.txt +0 -0
@@ -135,6 +135,12 @@ def roty(angle: float) -> NDArray[np.floating]:
135
135
  -------
136
136
  R : ndarray
137
137
  3x3 rotation matrix.
138
+
139
+ Examples
140
+ --------
141
+ >>> R = roty(np.pi/2) # 90 degree rotation about y
142
+ >>> R @ [1, 0, 0] # x-axis maps to -z-axis
143
+ array([ 0., 0., -1.])
138
144
  """
139
145
  c = np.cos(angle)
140
146
  s = np.sin(angle)
@@ -154,6 +160,12 @@ def rotz(angle: float) -> NDArray[np.floating]:
154
160
  -------
155
161
  R : ndarray
156
162
  3x3 rotation matrix.
163
+
164
+ Examples
165
+ --------
166
+ >>> R = rotz(np.pi/2) # 90 degree rotation about z
167
+ >>> R @ [1, 0, 0] # x-axis maps to y-axis
168
+ array([0., 1., 0.])
157
169
  """
158
170
  c = np.cos(angle)
159
171
  s = np.sin(angle)
@@ -225,6 +237,13 @@ def rotmat2euler(
225
237
  angles : ndarray
226
238
  Three Euler angles in radians.
227
239
 
240
+ Examples
241
+ --------
242
+ >>> R = rotz(np.radians(45)) @ roty(np.radians(30)) @ rotx(np.radians(15))
243
+ >>> angles = rotmat2euler(R, 'ZYX')
244
+ >>> np.degrees(angles)
245
+ array([45., 30., 15.])
246
+
228
247
  Notes
229
248
  -----
230
249
  May have singularities (gimbal lock) at certain angles.
@@ -298,6 +317,13 @@ def axisangle2rotmat(
298
317
  R : ndarray
299
318
  3x3 rotation matrix.
300
319
 
320
+ Examples
321
+ --------
322
+ >>> axis = [0, 0, 1] # Z-axis
323
+ >>> R = axisangle2rotmat(axis, np.pi/2) # 90 deg about Z
324
+ >>> R @ [1, 0, 0] # x-axis maps to y-axis
325
+ array([0., 1., 0.])
326
+
301
327
  Notes
302
328
  -----
303
329
  Uses Rodrigues' rotation formula.
@@ -340,6 +366,15 @@ def rotmat2axisangle(
340
366
  Unit vector rotation axis.
341
367
  angle : float
342
368
  Rotation angle in radians [0, π].
369
+
370
+ Examples
371
+ --------
372
+ >>> R = rotz(np.pi/2) # 90 deg about Z
373
+ >>> axis, angle = rotmat2axisangle(R)
374
+ >>> axis # Z-axis
375
+ array([0., 0., 1.])
376
+ >>> np.degrees(angle)
377
+ 90.0
343
378
  """
344
379
  R = np.asarray(R, dtype=np.float64)
345
380
 
@@ -417,6 +452,12 @@ def rotmat2quat(R: ArrayLike) -> NDArray[np.floating]:
417
452
  q : ndarray
418
453
  Quaternion [qw, qx, qy, qz] (scalar-first, positive qw).
419
454
 
455
+ Examples
456
+ --------
457
+ >>> R = np.eye(3) # Identity rotation
458
+ >>> rotmat2quat(R)
459
+ array([1., 0., 0., 0.])
460
+
420
461
  Notes
421
462
  -----
422
463
  Uses Shepperd's method for numerical stability.
@@ -477,6 +518,25 @@ def euler2quat(
477
518
  -------
478
519
  q : ndarray
479
520
  Quaternion [qw, qx, qy, qz].
521
+
522
+ Examples
523
+ --------
524
+ Convert yaw-pitch-roll angles to quaternion:
525
+
526
+ >>> import numpy as np
527
+ >>> from pytcl.coordinate_systems.rotations import euler2quat
528
+ >>> # 45° yaw, 30° pitch, 0° roll
529
+ >>> angles = np.radians([45, 30, 0])
530
+ >>> q = euler2quat(angles, sequence='ZYX')
531
+ >>> q.shape
532
+ (4,)
533
+ >>> np.abs(q[0]) > 0.5 # scalar part should be significant
534
+ True
535
+
536
+ See Also
537
+ --------
538
+ quat2euler : Inverse conversion.
539
+ euler2rotmat : Convert to rotation matrix instead.
480
540
  """
481
541
  R = euler2rotmat(angles, sequence)
482
542
  return rotmat2quat(R)
@@ -500,6 +560,13 @@ def quat2euler(
500
560
  -------
501
561
  angles : ndarray
502
562
  Three Euler angles in radians.
563
+
564
+ Examples
565
+ --------
566
+ >>> q = [1, 0, 0, 0] # Identity quaternion
567
+ >>> angles = quat2euler(q, 'ZYX')
568
+ >>> np.allclose(angles, [0, 0, 0])
569
+ True
503
570
  """
504
571
  R = quat2rotmat(q)
505
572
  return rotmat2euler(R, sequence)
@@ -525,6 +592,26 @@ def quat_multiply(q1: ArrayLike, q2: ArrayLike) -> NDArray[np.floating]:
525
592
  -----
526
593
  Quaternion multiplication represents composition of rotations.
527
594
  q1 * q2 applies q2 first, then q1.
595
+
596
+ Examples
597
+ --------
598
+ Combine two rotations using quaternion multiplication:
599
+
600
+ >>> import numpy as np
601
+ >>> from pytcl.coordinate_systems.rotations import quat_multiply, euler2quat
602
+ >>> # 90° rotation about Z, then 45° about X
603
+ >>> q_z90 = euler2quat(np.radians([90, 0, 0]), 'ZYX')
604
+ >>> q_x45 = euler2quat(np.radians([0, 0, 45]), 'ZYX')
605
+ >>> q_combined = quat_multiply(q_z90, q_x45)
606
+ >>> q_combined.shape
607
+ (4,)
608
+ >>> np.isclose(np.linalg.norm(q_combined), 1.0) # unit quaternion
609
+ True
610
+
611
+ See Also
612
+ --------
613
+ quat_inverse : Compute quaternion inverse.
614
+ quat_rotate : Rotate a vector by a quaternion.
528
615
  """
529
616
  q1 = np.asarray(q1, dtype=np.float64)
530
617
  q2 = np.asarray(q2, dtype=np.float64)
@@ -556,6 +643,11 @@ def quat_conjugate(q: ArrayLike) -> NDArray[np.floating]:
556
643
  -------
557
644
  q_conj : ndarray
558
645
  Conjugate quaternion [qw, -qx, -qy, -qz].
646
+
647
+ Examples
648
+ --------
649
+ >>> quat_conjugate([0.707, 0.707, 0, 0])
650
+ array([ 0.707, -0.707, -0. , -0. ])
559
651
  """
560
652
  q = np.asarray(q, dtype=np.float64)
561
653
  return np.array([q[0], -q[1], -q[2], -q[3]], dtype=np.float64)
@@ -575,6 +667,13 @@ def quat_inverse(q: ArrayLike) -> NDArray[np.floating]:
575
667
  q_inv : ndarray
576
668
  Inverse quaternion.
577
669
 
670
+ Examples
671
+ --------
672
+ >>> q = euler2quat(np.radians([45, 0, 0]), 'ZYX')
673
+ >>> q_inv = quat_inverse(q)
674
+ >>> quat_multiply(q, q_inv) # Should be identity
675
+ array([1., 0., 0., 0.])
676
+
578
677
  Notes
579
678
  -----
580
679
  For unit quaternions, inverse equals conjugate.
@@ -599,6 +698,15 @@ def quat_rotate(q: ArrayLike, v: ArrayLike) -> NDArray[np.floating]:
599
698
  v_rot : ndarray
600
699
  Rotated vector.
601
700
 
701
+ Examples
702
+ --------
703
+ >>> # 90 degree rotation about z-axis
704
+ >>> q = euler2quat(np.radians([90, 0, 0]), 'ZYX')
705
+ >>> v = np.array([1.0, 0.0, 0.0])
706
+ >>> v_rot = quat_rotate(q, v)
707
+ >>> v_rot # x-axis becomes y-axis
708
+ array([0., 1., 0.])
709
+
602
710
  Notes
603
711
  -----
604
712
  Computes q * v * q^(-1) where v is treated as a pure quaternion.
@@ -632,6 +740,15 @@ def slerp(
632
740
  -------
633
741
  q : ndarray
634
742
  Interpolated quaternion.
743
+
744
+ Examples
745
+ --------
746
+ >>> q1 = np.array([1, 0, 0, 0]) # identity
747
+ >>> q2 = euler2quat(np.radians([90, 0, 0]), 'ZYX') # 90 deg about z
748
+ >>> q_mid = slerp(q1, q2, 0.5) # halfway = 45 deg
749
+ >>> angles = quat2euler(q_mid, 'ZYX')
750
+ >>> np.degrees(angles[0]) # yaw should be ~45
751
+ 45.0...
635
752
  """
636
753
  q1 = np.asarray(q1, dtype=np.float64)
637
754
  q2 = np.asarray(q2, dtype=np.float64)
@@ -674,6 +791,13 @@ def rodrigues2rotmat(rvec: ArrayLike) -> NDArray[np.floating]:
674
791
  R : ndarray
675
792
  3x3 rotation matrix.
676
793
 
794
+ Examples
795
+ --------
796
+ >>> rvec = [0, 0, np.pi/2] # 90 deg about Z
797
+ >>> R = rodrigues2rotmat(rvec)
798
+ >>> R @ [1, 0, 0] # x-axis maps to y-axis
799
+ array([0., 1., 0.])
800
+
677
801
  Notes
678
802
  -----
679
803
  The Rodrigues vector encodes both the rotation axis and angle:
@@ -702,6 +826,13 @@ def rotmat2rodrigues(R: ArrayLike) -> NDArray[np.floating]:
702
826
  -------
703
827
  rvec : ndarray
704
828
  Rodrigues vector (axis * angle).
829
+
830
+ Examples
831
+ --------
832
+ >>> R = rotz(np.pi/2) # 90 deg about Z
833
+ >>> rvec = rotmat2rodrigues(R)
834
+ >>> np.linalg.norm(rvec) # magnitude is the angle
835
+ 1.5707...
705
836
  """
706
837
  axis, angle = rotmat2axisangle(R)
707
838
  return axis * angle
@@ -726,6 +857,14 @@ def dcm_rate(
726
857
  R_dot : ndarray
727
858
  Time derivative of R.
728
859
 
860
+ Examples
861
+ --------
862
+ >>> R = np.eye(3)
863
+ >>> omega = [0, 0, 1] # 1 rad/s about Z
864
+ >>> R_dot = dcm_rate(R, omega)
865
+ >>> R_dot[0, 1] # Off-diagonal elements show rotation
866
+ -1.0
867
+
729
868
  Notes
730
869
  -----
731
870
  R_dot = R @ skew(omega)
@@ -756,6 +895,14 @@ def is_rotation_matrix(R: ArrayLike, tol: float = 1e-6) -> bool:
756
895
  -------
757
896
  valid : bool
758
897
  True if R is a valid rotation matrix.
898
+
899
+ Examples
900
+ --------
901
+ >>> R = rotx(np.pi/4)
902
+ >>> is_rotation_matrix(R)
903
+ True
904
+ >>> is_rotation_matrix(np.eye(3) * 2) # not orthonormal
905
+ False
759
906
  """
760
907
  R = np.asarray(R, dtype=np.float64)
761
908
 
pytcl/core/__init__.py CHANGED
@@ -7,6 +7,7 @@ This module provides foundational functionality used throughout the library:
7
7
  - Array manipulation helpers compatible with MATLAB conventions
8
8
  - Custom exception hierarchy for consistent error handling
9
9
  - Optional dependency management
10
+ - Module maturity classification system
10
11
  """
11
12
 
12
13
  from pytcl.core.array_utils import (
@@ -45,6 +46,14 @@ from pytcl.core.exceptions import (
45
46
  UninitializedError,
46
47
  ValidationError,
47
48
  )
49
+ from pytcl.core.maturity import (
50
+ MaturityLevel,
51
+ get_maturity,
52
+ get_maturity_summary,
53
+ get_modules_by_maturity,
54
+ is_production_ready,
55
+ is_stable,
56
+ )
48
57
  from pytcl.core.optional_deps import (
49
58
  LazyModule,
50
59
  check_dependencies,
@@ -125,4 +134,11 @@ __all__ = [
125
134
  "requires",
126
135
  "check_dependencies",
127
136
  "LazyModule",
137
+ # Maturity classification
138
+ "MaturityLevel",
139
+ "get_maturity",
140
+ "get_modules_by_maturity",
141
+ "get_maturity_summary",
142
+ "is_stable",
143
+ "is_production_ready",
128
144
  ]
pytcl/core/maturity.py ADDED
@@ -0,0 +1,346 @@
1
+ """
2
+ Module maturity classification system for the Tracker Component Library.
3
+
4
+ This module provides a standardized way to indicate the production-readiness
5
+ and stability of different modules within pyTCL. The maturity levels help
6
+ users understand which APIs are stable and which may change.
7
+
8
+ Maturity Levels
9
+ ---------------
10
+ STABLE (3)
11
+ Production-ready. Thoroughly tested, well-documented, and API is frozen.
12
+ Breaking changes only in major version bumps.
13
+
14
+ MATURE (2)
15
+ Ready for production use. Good test coverage and documentation.
16
+ Minor API adjustments possible in minor versions.
17
+
18
+ EXPERIMENTAL (1)
19
+ Functional but may change. Limited testing or documentation.
20
+ API may change in any release.
21
+
22
+ DEPRECATED (0)
23
+ Scheduled for removal. Use the recommended replacement.
24
+
25
+ Examples
26
+ --------
27
+ Check the maturity level of a module:
28
+
29
+ >>> from pytcl.core.maturity import get_maturity, MaturityLevel
30
+ >>> level = get_maturity("pytcl.dynamic_estimation.kalman.linear")
31
+ >>> level == MaturityLevel.STABLE
32
+ True
33
+
34
+ List all stable modules:
35
+
36
+ >>> from pytcl.core.maturity import get_modules_by_maturity, MaturityLevel
37
+ >>> stable_modules = get_modules_by_maturity(MaturityLevel.STABLE)
38
+
39
+ See Also
40
+ --------
41
+ pytcl.core.optional_deps : Optional dependency management.
42
+ """
43
+
44
+ from enum import IntEnum
45
+ from typing import Dict, List
46
+
47
+
48
+ class MaturityLevel(IntEnum):
49
+ """Maturity level classification for modules.
50
+
51
+ Attributes
52
+ ----------
53
+ DEPRECATED : int
54
+ Level 0. Scheduled for removal.
55
+ EXPERIMENTAL : int
56
+ Level 1. Functional but unstable API.
57
+ MATURE : int
58
+ Level 2. Production-ready with possible minor changes.
59
+ STABLE : int
60
+ Level 3. Production-ready with frozen API.
61
+ """
62
+
63
+ DEPRECATED = 0
64
+ EXPERIMENTAL = 1
65
+ MATURE = 2
66
+ STABLE = 3
67
+
68
+
69
+ # Module maturity classifications
70
+ # Keys are module paths relative to pytcl (e.g., "dynamic_estimation.kalman.linear")
71
+ MODULE_MATURITY: Dict[str, MaturityLevel] = {
72
+ # =========================================================================
73
+ # STABLE (3) - Production-ready, frozen API
74
+ # =========================================================================
75
+ # Core
76
+ "core.constants": MaturityLevel.STABLE,
77
+ "core.exceptions": MaturityLevel.STABLE,
78
+ "core.validation": MaturityLevel.STABLE,
79
+ "core.array_utils": MaturityLevel.STABLE,
80
+ "core.optional_deps": MaturityLevel.STABLE,
81
+ # Kalman Filters
82
+ "dynamic_estimation.kalman.linear": MaturityLevel.STABLE,
83
+ "dynamic_estimation.kalman.extended": MaturityLevel.STABLE,
84
+ "dynamic_estimation.kalman.unscented": MaturityLevel.STABLE,
85
+ "dynamic_estimation.kalman.types": MaturityLevel.STABLE,
86
+ "dynamic_estimation.kalman.matrix_utils": MaturityLevel.STABLE,
87
+ # Motion Models
88
+ "dynamic_models.constant_velocity": MaturityLevel.STABLE,
89
+ "dynamic_models.constant_acceleration": MaturityLevel.STABLE,
90
+ "dynamic_models.coordinated_turn": MaturityLevel.STABLE,
91
+ "dynamic_models.singer": MaturityLevel.STABLE,
92
+ "dynamic_models.process_noise.constant_velocity": MaturityLevel.STABLE,
93
+ "dynamic_models.process_noise.constant_acceleration": MaturityLevel.STABLE,
94
+ # Coordinate Systems
95
+ "coordinate_systems.conversions.geodetic": MaturityLevel.STABLE,
96
+ "coordinate_systems.conversions.spherical": MaturityLevel.STABLE,
97
+ "coordinate_systems.rotations.rotations": MaturityLevel.STABLE,
98
+ "coordinate_systems.rotations.quaternions": MaturityLevel.STABLE,
99
+ # Assignment Algorithms
100
+ "assignment_algorithms.hungarian": MaturityLevel.STABLE,
101
+ "assignment_algorithms.auction": MaturityLevel.STABLE,
102
+ "assignment_algorithms.gating": MaturityLevel.STABLE,
103
+ # Containers
104
+ "containers.kd_tree": MaturityLevel.STABLE,
105
+ "containers.base": MaturityLevel.STABLE,
106
+ # Mathematical Functions
107
+ "mathematical_functions.special": MaturityLevel.STABLE,
108
+ # =========================================================================
109
+ # MATURE (2) - Production-ready, minor changes possible
110
+ # =========================================================================
111
+ # Kalman Filters
112
+ "dynamic_estimation.kalman.square_root": MaturityLevel.MATURE,
113
+ "dynamic_estimation.kalman.ud_filter": MaturityLevel.MATURE,
114
+ "dynamic_estimation.kalman.sr_ukf": MaturityLevel.MATURE,
115
+ "dynamic_estimation.kalman.cubature": MaturityLevel.MATURE,
116
+ "dynamic_estimation.kalman.constrained": MaturityLevel.MATURE,
117
+ "dynamic_estimation.information_filter": MaturityLevel.MATURE,
118
+ "dynamic_estimation.imm": MaturityLevel.MATURE,
119
+ "dynamic_estimation.h_infinity": MaturityLevel.MATURE,
120
+ # Particle Filters
121
+ "dynamic_estimation.particle_filters.bootstrap": MaturityLevel.MATURE,
122
+ "dynamic_estimation.particle_filters.resampling": MaturityLevel.MATURE,
123
+ # Smoothers
124
+ "dynamic_estimation.smoothers.rts": MaturityLevel.MATURE,
125
+ "dynamic_estimation.smoothers.fixed_lag": MaturityLevel.MATURE,
126
+ # Motion Models
127
+ "dynamic_models.process_noise.coordinated_turn": MaturityLevel.MATURE,
128
+ "dynamic_models.process_noise.singer": MaturityLevel.MATURE,
129
+ # Assignment Algorithms
130
+ "assignment_algorithms.jpda": MaturityLevel.MATURE,
131
+ "assignment_algorithms.mht": MaturityLevel.MATURE,
132
+ "assignment_algorithms.murty": MaturityLevel.MATURE,
133
+ "assignment_algorithms.assignment_3d": MaturityLevel.MATURE,
134
+ # Containers
135
+ "containers.ball_tree": MaturityLevel.MATURE,
136
+ "containers.rtree": MaturityLevel.MATURE,
137
+ "containers.vptree": MaturityLevel.MATURE,
138
+ "containers.covertree": MaturityLevel.MATURE,
139
+ "containers.track_list": MaturityLevel.MATURE,
140
+ "containers.measurement_set": MaturityLevel.MATURE,
141
+ "containers.cluster_set": MaturityLevel.MATURE,
142
+ # Navigation
143
+ "navigation.ins.strapdown": MaturityLevel.MATURE,
144
+ "navigation.ins.error_model": MaturityLevel.MATURE,
145
+ "navigation.gnss.positioning": MaturityLevel.MATURE,
146
+ "navigation.geodesy": MaturityLevel.MATURE,
147
+ "navigation.great_circle": MaturityLevel.MATURE,
148
+ # Coordinate Systems
149
+ "coordinate_systems.jacobians.jacobians": MaturityLevel.MATURE,
150
+ "coordinate_systems.projections.utm": MaturityLevel.MATURE,
151
+ "coordinate_systems.projections.mercator": MaturityLevel.MATURE,
152
+ # Mathematical Functions
153
+ "mathematical_functions.signal_processing.filters": MaturityLevel.MATURE,
154
+ "mathematical_functions.signal_processing.detection": MaturityLevel.MATURE,
155
+ "mathematical_functions.transforms.fft": MaturityLevel.MATURE,
156
+ "mathematical_functions.transforms.wavelets": MaturityLevel.MATURE,
157
+ # Static Estimation
158
+ "static_estimation.least_squares": MaturityLevel.MATURE,
159
+ "static_estimation.robust": MaturityLevel.MATURE,
160
+ "static_estimation.ransac": MaturityLevel.MATURE,
161
+ # Astronomical
162
+ "astronomical.orbital_mechanics": MaturityLevel.MATURE,
163
+ "astronomical.ephemerides": MaturityLevel.MATURE,
164
+ "astronomical.reference_frames": MaturityLevel.MATURE,
165
+ # =========================================================================
166
+ # EXPERIMENTAL (1) - Functional but API may change
167
+ # =========================================================================
168
+ # Advanced Filters
169
+ "dynamic_estimation.kalman.gaussian_sum": MaturityLevel.EXPERIMENTAL,
170
+ "dynamic_estimation.kalman.rao_blackwellized": MaturityLevel.EXPERIMENTAL,
171
+ # Geophysical Models
172
+ "geophysical.gravity.egm": MaturityLevel.EXPERIMENTAL,
173
+ "geophysical.magnetism.wmm": MaturityLevel.EXPERIMENTAL,
174
+ "geophysical.tides": MaturityLevel.EXPERIMENTAL,
175
+ # Terrain
176
+ "terrain.dem": MaturityLevel.EXPERIMENTAL,
177
+ "terrain.loaders": MaturityLevel.EXPERIMENTAL,
178
+ "terrain.analysis": MaturityLevel.EXPERIMENTAL,
179
+ # Relativity
180
+ "astronomical.relativity": MaturityLevel.EXPERIMENTAL,
181
+ "astronomical.satellite.sgp4": MaturityLevel.EXPERIMENTAL,
182
+ }
183
+
184
+
185
+ def get_maturity(module_path: str) -> MaturityLevel:
186
+ """
187
+ Get the maturity level of a module.
188
+
189
+ Parameters
190
+ ----------
191
+ module_path : str
192
+ Module path relative to pytcl (e.g., "dynamic_estimation.kalman.linear")
193
+ or full path (e.g., "pytcl.dynamic_estimation.kalman.linear").
194
+
195
+ Returns
196
+ -------
197
+ MaturityLevel
198
+ The module's maturity level. Returns EXPERIMENTAL if not classified.
199
+
200
+ Examples
201
+ --------
202
+ >>> get_maturity("dynamic_estimation.kalman.linear")
203
+ <MaturityLevel.STABLE: 3>
204
+ >>> get_maturity("pytcl.core.constants")
205
+ <MaturityLevel.STABLE: 3>
206
+ """
207
+ # Strip pytcl. prefix if present
208
+ if module_path.startswith("pytcl."):
209
+ module_path = module_path[6:]
210
+
211
+ return MODULE_MATURITY.get(module_path, MaturityLevel.EXPERIMENTAL)
212
+
213
+
214
+ def get_modules_by_maturity(level: MaturityLevel) -> List[str]:
215
+ """
216
+ Get all modules at a specific maturity level.
217
+
218
+ Parameters
219
+ ----------
220
+ level : MaturityLevel
221
+ The maturity level to filter by.
222
+
223
+ Returns
224
+ -------
225
+ list of str
226
+ Module paths at the specified maturity level.
227
+
228
+ Examples
229
+ --------
230
+ >>> stable = get_modules_by_maturity(MaturityLevel.STABLE)
231
+ >>> "core.constants" in stable
232
+ True
233
+ """
234
+ return [path for path, mat in MODULE_MATURITY.items() if mat == level]
235
+
236
+
237
+ def get_maturity_summary() -> Dict[MaturityLevel, int]:
238
+ """
239
+ Get a summary count of modules at each maturity level.
240
+
241
+ Returns
242
+ -------
243
+ dict
244
+ Mapping from MaturityLevel to count of modules.
245
+
246
+ Examples
247
+ --------
248
+ >>> summary = get_maturity_summary()
249
+ >>> summary[MaturityLevel.STABLE] > 0
250
+ True
251
+ """
252
+ summary = {level: 0 for level in MaturityLevel}
253
+ for level in MODULE_MATURITY.values():
254
+ summary[level] += 1
255
+ return summary
256
+
257
+
258
+ def is_stable(module_path: str) -> bool:
259
+ """
260
+ Check if a module is stable (production-ready with frozen API).
261
+
262
+ Parameters
263
+ ----------
264
+ module_path : str
265
+ Module path to check.
266
+
267
+ Returns
268
+ -------
269
+ bool
270
+ True if the module is stable.
271
+
272
+ Examples
273
+ --------
274
+ >>> is_stable("dynamic_estimation.kalman.linear")
275
+ True
276
+ >>> is_stable("terrain.dem")
277
+ False
278
+ """
279
+ return get_maturity(module_path) == MaturityLevel.STABLE
280
+
281
+
282
+ def is_production_ready(module_path: str) -> bool:
283
+ """
284
+ Check if a module is production-ready (STABLE or MATURE).
285
+
286
+ Parameters
287
+ ----------
288
+ module_path : str
289
+ Module path to check.
290
+
291
+ Returns
292
+ -------
293
+ bool
294
+ True if the module is STABLE or MATURE.
295
+
296
+ Examples
297
+ --------
298
+ >>> is_production_ready("dynamic_estimation.kalman.linear")
299
+ True
300
+ >>> is_production_ready("dynamic_estimation.imm")
301
+ True
302
+ >>> is_production_ready("terrain.dem")
303
+ False
304
+ """
305
+ level = get_maturity(module_path)
306
+ return level >= MaturityLevel.MATURE
307
+
308
+
309
+ def format_maturity_badge(level: MaturityLevel) -> str:
310
+ """
311
+ Get a formatted badge string for a maturity level.
312
+
313
+ Parameters
314
+ ----------
315
+ level : MaturityLevel
316
+ The maturity level.
317
+
318
+ Returns
319
+ -------
320
+ str
321
+ A badge string suitable for documentation.
322
+
323
+ Examples
324
+ --------
325
+ >>> format_maturity_badge(MaturityLevel.STABLE)
326
+ '|stable|'
327
+ """
328
+ badges = {
329
+ MaturityLevel.STABLE: "|stable|",
330
+ MaturityLevel.MATURE: "|mature|",
331
+ MaturityLevel.EXPERIMENTAL: "|experimental|",
332
+ MaturityLevel.DEPRECATED: "|deprecated|",
333
+ }
334
+ return badges.get(level, "|unknown|")
335
+
336
+
337
+ __all__ = [
338
+ "MaturityLevel",
339
+ "MODULE_MATURITY",
340
+ "get_maturity",
341
+ "get_modules_by_maturity",
342
+ "get_maturity_summary",
343
+ "is_stable",
344
+ "is_production_ready",
345
+ "format_maturity_badge",
346
+ ]
@@ -363,6 +363,33 @@ def gaussian_sum_filter_predict(
363
363
  -------
364
364
  components_new : list[GaussianComponent]
365
365
  Predicted components.
366
+
367
+ Examples
368
+ --------
369
+ >>> import numpy as np
370
+ >>> from pytcl.dynamic_estimation.gaussian_sum_filter import GaussianComponent
371
+ >>> # Two-component mixture for position-velocity state
372
+ >>> comp1 = GaussianComponent(
373
+ ... x=np.array([0.0, 1.0]), # moving right
374
+ ... P=np.eye(2) * 0.5,
375
+ ... w=0.5
376
+ ... )
377
+ >>> comp2 = GaussianComponent(
378
+ ... x=np.array([0.0, -1.0]), # moving left
379
+ ... P=np.eye(2) * 0.5,
380
+ ... w=0.5
381
+ ... )
382
+ >>> components = [comp1, comp2]
383
+ >>> # Constant velocity dynamics
384
+ >>> dt = 0.1
385
+ >>> f = lambda x: np.array([x[0] + x[1] * dt, x[1]])
386
+ >>> F = np.array([[1, dt], [0, 1]])
387
+ >>> Q = np.eye(2) * 0.01
388
+ >>> predicted = gaussian_sum_filter_predict(components, f, F, Q)
389
+ >>> len(predicted)
390
+ 2
391
+ >>> predicted[0].w # weights unchanged in prediction
392
+ 0.5
366
393
  """
367
394
  F = np.asarray(F, dtype=np.float64)
368
395
  Q = np.asarray(Q, dtype=np.float64)
@@ -401,6 +428,34 @@ def gaussian_sum_filter_update(
401
428
  -------
402
429
  components_new : list[GaussianComponent]
403
430
  Updated components with adapted weights.
431
+
432
+ Examples
433
+ --------
434
+ >>> import numpy as np
435
+ >>> from pytcl.dynamic_estimation.gaussian_sum_filter import GaussianComponent
436
+ >>> # Two-component mixture
437
+ >>> comp1 = GaussianComponent(
438
+ ... x=np.array([1.0, 0.5]),
439
+ ... P=np.eye(2) * 0.5,
440
+ ... w=0.5
441
+ ... )
442
+ >>> comp2 = GaussianComponent(
443
+ ... x=np.array([3.0, 0.5]),
444
+ ... P=np.eye(2) * 0.5,
445
+ ... w=0.5
446
+ ... )
447
+ >>> components = [comp1, comp2]
448
+ >>> # Position measurement near component 1
449
+ >>> z = np.array([1.1])
450
+ >>> h = lambda x: np.array([x[0]])
451
+ >>> H = np.array([[1, 0]])
452
+ >>> R = np.array([[0.1]])
453
+ >>> updated = gaussian_sum_filter_update(components, z, h, H, R)
454
+ >>> len(updated)
455
+ 2
456
+ >>> # Component 1 should have higher weight (closer to measurement)
457
+ >>> updated[0].w > updated[1].w
458
+ True
404
459
  """
405
460
  z = np.asarray(z, dtype=np.float64)
406
461
  H = np.asarray(H, dtype=np.float64)