nrl-tracker 1.6.0__py3-none-any.whl → 1.7.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.6.0.dist-info → nrl_tracker-1.7.0.dist-info}/METADATA +13 -10
- {nrl_tracker-1.6.0.dist-info → nrl_tracker-1.7.0.dist-info}/RECORD +20 -13
- pytcl/assignment_algorithms/__init__.py +28 -0
- pytcl/assignment_algorithms/nd_assignment.py +378 -0
- pytcl/assignment_algorithms/network_flow.py +361 -0
- pytcl/astronomical/__init__.py +35 -0
- pytcl/astronomical/reference_frames.py +102 -0
- pytcl/astronomical/special_orbits.py +536 -0
- pytcl/atmosphere/__init__.py +11 -0
- pytcl/atmosphere/nrlmsise00.py +808 -0
- pytcl/coordinate_systems/conversions/geodetic.py +248 -5
- pytcl/dynamic_estimation/__init__.py +26 -0
- pytcl/dynamic_estimation/gaussian_sum_filter.py +452 -0
- pytcl/dynamic_estimation/kalman/__init__.py +12 -0
- pytcl/dynamic_estimation/kalman/constrained.py +370 -0
- pytcl/dynamic_estimation/kalman/h_infinity.py +2 -2
- pytcl/dynamic_estimation/rbpf.py +593 -0
- {nrl_tracker-1.6.0.dist-info → nrl_tracker-1.7.0.dist-info}/LICENSE +0 -0
- {nrl_tracker-1.6.0.dist-info → nrl_tracker-1.7.0.dist-info}/WHEEL +0 -0
- {nrl_tracker-1.6.0.dist-info → nrl_tracker-1.7.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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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,243 @@ 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 = -sin_lat * cos_lon * delta_ecef[0] - sin_lat * sin_lon * delta_ecef[1] + cos_lat * delta_ecef[2]
|
|
655
|
+
e = -sin_lon * delta_ecef[0] + cos_lon * delta_ecef[1]
|
|
656
|
+
z = cos_lat * cos_lon * delta_ecef[0] + cos_lat * sin_lon * delta_ecef[1] + sin_lat * delta_ecef[2]
|
|
657
|
+
return np.array([s, e, z], dtype=np.float64)
|
|
658
|
+
else:
|
|
659
|
+
s = -sin_lat * cos_lon * delta_ecef[0, :] - sin_lat * sin_lon * delta_ecef[1, :] + cos_lat * delta_ecef[2, :]
|
|
660
|
+
e = -sin_lon * delta_ecef[0, :] + cos_lon * delta_ecef[1, :]
|
|
661
|
+
z = cos_lat * cos_lon * delta_ecef[0, :] + cos_lat * sin_lon * delta_ecef[1, :] + sin_lat * delta_ecef[2, :]
|
|
662
|
+
return np.array([s, e, z], dtype=np.float64)
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def sez2ecef(
|
|
666
|
+
sez: ArrayLike,
|
|
667
|
+
lat_ref: float,
|
|
668
|
+
lon_ref: float,
|
|
669
|
+
ecef_ref: Optional[ArrayLike] = None,
|
|
670
|
+
) -> NDArray[np.floating]:
|
|
671
|
+
"""
|
|
672
|
+
Convert local SEZ coordinates to ECEF coordinates.
|
|
673
|
+
|
|
674
|
+
Parameters
|
|
675
|
+
----------
|
|
676
|
+
sez : array_like
|
|
677
|
+
SEZ coordinates [south, east, zenith] in meters, shape (3,) or (3, N).
|
|
678
|
+
lat_ref : float
|
|
679
|
+
Reference point latitude in radians.
|
|
680
|
+
lon_ref : float
|
|
681
|
+
Reference point longitude in radians.
|
|
682
|
+
ecef_ref : array_like, optional
|
|
683
|
+
Reference ECEF position. If None, the reference point is
|
|
684
|
+
at (lat_ref, lon_ref) with zero altitude.
|
|
685
|
+
|
|
686
|
+
Returns
|
|
687
|
+
-------
|
|
688
|
+
ecef : ndarray
|
|
689
|
+
ECEF coordinates [X, Y, Z] in meters.
|
|
690
|
+
|
|
691
|
+
See Also
|
|
692
|
+
--------
|
|
693
|
+
ecef2sez : Forward conversion.
|
|
694
|
+
"""
|
|
695
|
+
sez = np.asarray(sez, dtype=np.float64)
|
|
696
|
+
|
|
697
|
+
if ecef_ref is None:
|
|
698
|
+
ecef_ref = geodetic2ecef(lat_ref, lon_ref, 0.0)
|
|
699
|
+
else:
|
|
700
|
+
ecef_ref = np.asarray(ecef_ref, dtype=np.float64)
|
|
701
|
+
|
|
702
|
+
# Rotation matrix from SEZ to ECEF (transpose of ECEF to SEZ)
|
|
703
|
+
sin_lat = np.sin(lat_ref)
|
|
704
|
+
cos_lat = np.cos(lat_ref)
|
|
705
|
+
sin_lon = np.sin(lon_ref)
|
|
706
|
+
cos_lon = np.cos(lon_ref)
|
|
707
|
+
|
|
708
|
+
# Inverse rotation: ECEF = ECEF_ref + R_inv @ SEZ
|
|
709
|
+
if sez.ndim == 1:
|
|
710
|
+
dX = -sin_lat * cos_lon * sez[0] - sin_lon * sez[1] + cos_lat * cos_lon * sez[2]
|
|
711
|
+
dY = -sin_lat * sin_lon * sez[0] + cos_lon * sez[1] + cos_lat * sin_lon * sez[2]
|
|
712
|
+
dZ = cos_lat * sez[0] + sin_lat * sez[2]
|
|
713
|
+
return ecef_ref + np.array([dX, dY, dZ], dtype=np.float64)
|
|
714
|
+
else:
|
|
715
|
+
if sez.shape[0] != 3:
|
|
716
|
+
sez = sez.T
|
|
717
|
+
dX = -sin_lat * cos_lon * sez[0, :] - sin_lon * sez[1, :] + cos_lat * cos_lon * sez[2, :]
|
|
718
|
+
dY = -sin_lat * sin_lon * sez[0, :] + cos_lon * sez[1, :] + cos_lat * sin_lon * sez[2, :]
|
|
719
|
+
dZ = cos_lat * sez[0, :] + sin_lat * sez[2, :]
|
|
720
|
+
return ecef_ref[:, np.newaxis] + np.array([dX, dY, dZ], dtype=np.float64)
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def sez2geodetic(
|
|
724
|
+
sez: ArrayLike,
|
|
725
|
+
lat_ref: float,
|
|
726
|
+
lon_ref: float,
|
|
727
|
+
alt_ref: float,
|
|
728
|
+
a: float = WGS84.a,
|
|
729
|
+
f: float = WGS84.f,
|
|
730
|
+
) -> Tuple[NDArray[np.floating], NDArray[np.floating], NDArray[np.floating]]:
|
|
731
|
+
"""
|
|
732
|
+
Convert local SEZ coordinates to geodetic coordinates.
|
|
733
|
+
|
|
734
|
+
Parameters
|
|
735
|
+
----------
|
|
736
|
+
sez : array_like
|
|
737
|
+
SEZ coordinates [south, east, zenith] in meters.
|
|
738
|
+
lat_ref : float
|
|
739
|
+
Reference point latitude in radians.
|
|
740
|
+
lon_ref : float
|
|
741
|
+
Reference point longitude in radians.
|
|
742
|
+
alt_ref : float
|
|
743
|
+
Reference point altitude in meters.
|
|
744
|
+
a : float, optional
|
|
745
|
+
Semi-major axis.
|
|
746
|
+
f : float, optional
|
|
747
|
+
Flattening.
|
|
748
|
+
|
|
749
|
+
Returns
|
|
750
|
+
-------
|
|
751
|
+
lat : ndarray
|
|
752
|
+
Geodetic latitude in radians.
|
|
753
|
+
lon : ndarray
|
|
754
|
+
Geodetic longitude in radians.
|
|
755
|
+
alt : ndarray
|
|
756
|
+
Altitude in meters.
|
|
757
|
+
|
|
758
|
+
See Also
|
|
759
|
+
--------
|
|
760
|
+
geodetic2sez : Forward conversion.
|
|
761
|
+
"""
|
|
762
|
+
ecef_ref = geodetic2ecef(lat_ref, lon_ref, alt_ref, a, f)
|
|
763
|
+
ecef = sez2ecef(sez, lat_ref, lon_ref, ecef_ref)
|
|
764
|
+
return ecef2geodetic(ecef, a, f)
|
|
765
|
+
|
|
766
|
+
|
|
528
767
|
def geocentric_radius(
|
|
529
768
|
lat: ArrayLike,
|
|
530
769
|
a: float = WGS84.a,
|
|
@@ -625,6 +864,10 @@ __all__ = [
|
|
|
625
864
|
"ned2ecef",
|
|
626
865
|
"enu2ned",
|
|
627
866
|
"ned2enu",
|
|
867
|
+
"geodetic2sez",
|
|
868
|
+
"ecef2sez",
|
|
869
|
+
"sez2ecef",
|
|
870
|
+
"sez2geodetic",
|
|
628
871
|
"geocentric_radius",
|
|
629
872
|
"prime_vertical_radius",
|
|
630
873
|
"meridional_radius",
|
|
@@ -25,6 +25,14 @@ from pytcl.dynamic_estimation.imm import (
|
|
|
25
25
|
imm_update,
|
|
26
26
|
)
|
|
27
27
|
|
|
28
|
+
# Gaussian Sum Filter
|
|
29
|
+
from pytcl.dynamic_estimation.gaussian_sum_filter import (
|
|
30
|
+
GaussianComponent,
|
|
31
|
+
GaussianSumFilter,
|
|
32
|
+
gaussian_sum_filter_predict,
|
|
33
|
+
gaussian_sum_filter_update,
|
|
34
|
+
)
|
|
35
|
+
|
|
28
36
|
# Information filter
|
|
29
37
|
from pytcl.dynamic_estimation.information_filter import (
|
|
30
38
|
InformationFilterResult,
|
|
@@ -85,6 +93,14 @@ from pytcl.dynamic_estimation.kalman import (
|
|
|
85
93
|
unscented_transform,
|
|
86
94
|
)
|
|
87
95
|
|
|
96
|
+
# Rao-Blackwellized Particle Filter
|
|
97
|
+
from pytcl.dynamic_estimation.rbpf import (
|
|
98
|
+
RBPFFilter,
|
|
99
|
+
RBPFParticle,
|
|
100
|
+
rbpf_predict,
|
|
101
|
+
rbpf_update,
|
|
102
|
+
)
|
|
103
|
+
|
|
88
104
|
# Particle filters
|
|
89
105
|
from pytcl.dynamic_estimation.particle_filters import (
|
|
90
106
|
ParticleState,
|
|
@@ -192,6 +208,16 @@ __all__ = [
|
|
|
192
208
|
"imm_predict_update",
|
|
193
209
|
"IMMEstimator",
|
|
194
210
|
# Particle filters
|
|
211
|
+
# Gaussian Sum Filter
|
|
212
|
+
"GaussianComponent",
|
|
213
|
+
"GaussianSumFilter",
|
|
214
|
+
"gaussian_sum_filter_predict",
|
|
215
|
+
"gaussian_sum_filter_update",
|
|
216
|
+
# Rao-Blackwellized Particle Filter
|
|
217
|
+
"RBPFParticle",
|
|
218
|
+
"RBPFFilter",
|
|
219
|
+
"rbpf_predict",
|
|
220
|
+
"rbpf_update",
|
|
195
221
|
"ParticleState",
|
|
196
222
|
"resample_multinomial",
|
|
197
223
|
"resample_systematic",
|