nrl-tracker 1.5.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.
@@ -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,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",