nrl-tracker 1.9.1__py3-none-any.whl → 1.10.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.
- {nrl_tracker-1.9.1.dist-info → nrl_tracker-1.10.0.dist-info}/METADATA +49 -4
- {nrl_tracker-1.9.1.dist-info → nrl_tracker-1.10.0.dist-info}/RECORD +68 -60
- pytcl/__init__.py +2 -2
- pytcl/assignment_algorithms/gating.py +18 -0
- pytcl/assignment_algorithms/jpda.py +56 -0
- pytcl/assignment_algorithms/nd_assignment.py +65 -0
- pytcl/assignment_algorithms/network_flow.py +40 -0
- pytcl/astronomical/ephemerides.py +18 -0
- pytcl/astronomical/orbital_mechanics.py +131 -0
- pytcl/atmosphere/ionosphere.py +44 -0
- pytcl/atmosphere/models.py +29 -0
- pytcl/clustering/dbscan.py +9 -0
- pytcl/clustering/gaussian_mixture.py +20 -0
- pytcl/clustering/hierarchical.py +29 -0
- pytcl/clustering/kmeans.py +9 -0
- pytcl/coordinate_systems/conversions/geodetic.py +46 -0
- pytcl/coordinate_systems/conversions/spherical.py +35 -0
- pytcl/coordinate_systems/rotations/rotations.py +147 -0
- pytcl/core/__init__.py +16 -0
- pytcl/core/maturity.py +346 -0
- pytcl/core/optional_deps.py +20 -0
- pytcl/dynamic_estimation/gaussian_sum_filter.py +55 -0
- pytcl/dynamic_estimation/imm.py +29 -0
- pytcl/dynamic_estimation/information_filter.py +64 -0
- pytcl/dynamic_estimation/kalman/extended.py +56 -0
- pytcl/dynamic_estimation/kalman/linear.py +69 -0
- pytcl/dynamic_estimation/kalman/unscented.py +81 -0
- pytcl/dynamic_estimation/particle_filters/bootstrap.py +146 -0
- pytcl/dynamic_estimation/rbpf.py +51 -0
- pytcl/dynamic_estimation/smoothers.py +58 -0
- pytcl/dynamic_models/continuous_time/dynamics.py +104 -0
- pytcl/dynamic_models/discrete_time/coordinated_turn.py +6 -0
- pytcl/dynamic_models/discrete_time/singer.py +12 -0
- pytcl/dynamic_models/process_noise/coordinated_turn.py +46 -0
- pytcl/dynamic_models/process_noise/polynomial.py +6 -0
- pytcl/dynamic_models/process_noise/singer.py +52 -0
- pytcl/gpu/__init__.py +153 -0
- pytcl/gpu/ekf.py +425 -0
- pytcl/gpu/kalman.py +543 -0
- pytcl/gpu/matrix_utils.py +486 -0
- pytcl/gpu/particle_filter.py +568 -0
- pytcl/gpu/ukf.py +476 -0
- pytcl/gpu/utils.py +582 -0
- pytcl/gravity/clenshaw.py +60 -0
- pytcl/gravity/egm.py +47 -0
- pytcl/gravity/models.py +34 -0
- pytcl/gravity/spherical_harmonics.py +73 -0
- pytcl/gravity/tides.py +34 -0
- pytcl/mathematical_functions/numerical_integration/quadrature.py +85 -0
- pytcl/mathematical_functions/special_functions/bessel.py +55 -0
- pytcl/mathematical_functions/special_functions/elliptic.py +42 -0
- pytcl/mathematical_functions/special_functions/error_functions.py +49 -0
- pytcl/mathematical_functions/special_functions/gamma_functions.py +43 -0
- pytcl/mathematical_functions/special_functions/lambert_w.py +5 -0
- pytcl/mathematical_functions/special_functions/marcum_q.py +16 -0
- pytcl/navigation/geodesy.py +101 -2
- pytcl/navigation/great_circle.py +71 -0
- pytcl/navigation/rhumb.py +74 -0
- pytcl/performance_evaluation/estimation_metrics.py +70 -0
- pytcl/performance_evaluation/track_metrics.py +30 -0
- pytcl/static_estimation/maximum_likelihood.py +54 -0
- pytcl/static_estimation/robust.py +57 -0
- pytcl/terrain/dem.py +69 -0
- pytcl/terrain/visibility.py +65 -0
- pytcl/trackers/hypothesis.py +65 -0
- {nrl_tracker-1.9.1.dist-info → nrl_tracker-1.10.0.dist-info}/LICENSE +0 -0
- {nrl_tracker-1.9.1.dist-info → nrl_tracker-1.10.0.dist-info}/WHEEL +0 -0
- {nrl_tracker-1.9.1.dist-info → nrl_tracker-1.10.0.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
|
+
]
|
pytcl/core/optional_deps.py
CHANGED
|
@@ -69,6 +69,10 @@ PACKAGE_EXTRAS: dict[str, tuple[str, str]] = {
|
|
|
69
69
|
"pywavelets": ("signal", "pywavelets"),
|
|
70
70
|
# Terrain data
|
|
71
71
|
"netCDF4": ("terrain", "netCDF4"),
|
|
72
|
+
# GPU acceleration
|
|
73
|
+
"cupy": ("gpu", "cupy-cuda12x"),
|
|
74
|
+
# Apple Silicon GPU acceleration
|
|
75
|
+
"mlx": ("gpu-apple", "mlx"),
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
# Friendly names for features provided by each package
|
|
@@ -82,6 +86,8 @@ PACKAGE_FEATURES: dict[str, str] = {
|
|
|
82
86
|
"pywt": "wavelet transforms",
|
|
83
87
|
"pywavelets": "wavelet transforms",
|
|
84
88
|
"netCDF4": "NetCDF file reading",
|
|
89
|
+
"cupy": "GPU acceleration",
|
|
90
|
+
"mlx": "Apple Silicon GPU acceleration",
|
|
85
91
|
}
|
|
86
92
|
|
|
87
93
|
|
|
@@ -374,6 +380,16 @@ class _AvailabilityFlags:
|
|
|
374
380
|
"""True if netCDF4 is available."""
|
|
375
381
|
return is_available("netCDF4")
|
|
376
382
|
|
|
383
|
+
@property
|
|
384
|
+
def HAS_CUPY(self) -> bool:
|
|
385
|
+
"""True if cupy is available."""
|
|
386
|
+
return is_available("cupy")
|
|
387
|
+
|
|
388
|
+
@property
|
|
389
|
+
def HAS_MLX(self) -> bool:
|
|
390
|
+
"""True if mlx is available (Apple Silicon)."""
|
|
391
|
+
return is_available("mlx")
|
|
392
|
+
|
|
377
393
|
|
|
378
394
|
# Create singleton instance
|
|
379
395
|
_flags = _AvailabilityFlags()
|
|
@@ -387,6 +403,8 @@ HAS_ASTROPY = property(lambda self: _flags.HAS_ASTROPY)
|
|
|
387
403
|
HAS_PYPROJ = property(lambda self: _flags.HAS_PYPROJ)
|
|
388
404
|
HAS_CVXPY = property(lambda self: _flags.HAS_CVXPY)
|
|
389
405
|
HAS_NETCDF4 = property(lambda self: _flags.HAS_NETCDF4)
|
|
406
|
+
HAS_CUPY = property(lambda self: _flags.HAS_CUPY)
|
|
407
|
+
HAS_MLX = property(lambda self: _flags.HAS_MLX)
|
|
390
408
|
|
|
391
409
|
|
|
392
410
|
# =============================================================================
|
|
@@ -525,6 +543,8 @@ __all__ = [
|
|
|
525
543
|
"HAS_PYPROJ",
|
|
526
544
|
"HAS_CVXPY",
|
|
527
545
|
"HAS_NETCDF4",
|
|
546
|
+
"HAS_CUPY",
|
|
547
|
+
"HAS_MLX",
|
|
528
548
|
# Internal (for testing)
|
|
529
549
|
"_clear_cache",
|
|
530
550
|
"_flags",
|