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.
- {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.8.0.dist-info}/METADATA +57 -10
- {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.8.0.dist-info}/RECORD +86 -69
- pytcl/__init__.py +4 -3
- pytcl/assignment_algorithms/__init__.py +28 -0
- pytcl/assignment_algorithms/dijkstra_min_cost.py +184 -0
- pytcl/assignment_algorithms/gating.py +10 -10
- pytcl/assignment_algorithms/jpda.py +40 -40
- pytcl/assignment_algorithms/nd_assignment.py +379 -0
- pytcl/assignment_algorithms/network_flow.py +464 -0
- pytcl/assignment_algorithms/network_simplex.py +167 -0
- pytcl/assignment_algorithms/three_dimensional/assignment.py +3 -3
- pytcl/astronomical/__init__.py +104 -3
- pytcl/astronomical/ephemerides.py +14 -11
- pytcl/astronomical/reference_frames.py +865 -56
- pytcl/astronomical/relativity.py +6 -5
- pytcl/astronomical/sgp4.py +710 -0
- pytcl/astronomical/special_orbits.py +532 -0
- pytcl/astronomical/tle.py +558 -0
- pytcl/atmosphere/__init__.py +43 -1
- pytcl/atmosphere/ionosphere.py +512 -0
- pytcl/atmosphere/nrlmsise00.py +809 -0
- pytcl/clustering/dbscan.py +2 -2
- pytcl/clustering/gaussian_mixture.py +3 -3
- pytcl/clustering/hierarchical.py +15 -15
- pytcl/clustering/kmeans.py +4 -4
- pytcl/containers/__init__.py +24 -0
- pytcl/containers/base.py +219 -0
- pytcl/containers/cluster_set.py +12 -2
- pytcl/containers/covertree.py +26 -29
- pytcl/containers/kd_tree.py +94 -29
- pytcl/containers/rtree.py +200 -1
- pytcl/containers/vptree.py +21 -28
- pytcl/coordinate_systems/conversions/geodetic.py +272 -5
- pytcl/coordinate_systems/jacobians/jacobians.py +2 -2
- pytcl/coordinate_systems/projections/__init__.py +1 -1
- pytcl/coordinate_systems/projections/projections.py +2 -2
- pytcl/coordinate_systems/rotations/rotations.py +10 -6
- pytcl/core/__init__.py +18 -0
- pytcl/core/validation.py +333 -2
- pytcl/dynamic_estimation/__init__.py +26 -0
- pytcl/dynamic_estimation/gaussian_sum_filter.py +434 -0
- pytcl/dynamic_estimation/imm.py +14 -14
- pytcl/dynamic_estimation/kalman/__init__.py +30 -0
- pytcl/dynamic_estimation/kalman/constrained.py +382 -0
- pytcl/dynamic_estimation/kalman/extended.py +8 -8
- pytcl/dynamic_estimation/kalman/h_infinity.py +613 -0
- pytcl/dynamic_estimation/kalman/square_root.py +60 -573
- pytcl/dynamic_estimation/kalman/sr_ukf.py +302 -0
- pytcl/dynamic_estimation/kalman/ud_filter.py +410 -0
- pytcl/dynamic_estimation/kalman/unscented.py +8 -6
- pytcl/dynamic_estimation/particle_filters/bootstrap.py +15 -15
- pytcl/dynamic_estimation/rbpf.py +589 -0
- pytcl/gravity/egm.py +13 -0
- pytcl/gravity/spherical_harmonics.py +98 -37
- pytcl/gravity/tides.py +6 -6
- pytcl/logging_config.py +328 -0
- pytcl/magnetism/__init__.py +7 -0
- pytcl/magnetism/emm.py +10 -3
- pytcl/magnetism/wmm.py +260 -23
- pytcl/mathematical_functions/combinatorics/combinatorics.py +5 -5
- pytcl/mathematical_functions/geometry/geometry.py +5 -5
- pytcl/mathematical_functions/numerical_integration/quadrature.py +6 -6
- pytcl/mathematical_functions/signal_processing/detection.py +24 -24
- pytcl/mathematical_functions/signal_processing/filters.py +14 -14
- pytcl/mathematical_functions/signal_processing/matched_filter.py +12 -12
- pytcl/mathematical_functions/special_functions/bessel.py +15 -3
- pytcl/mathematical_functions/special_functions/debye.py +136 -26
- pytcl/mathematical_functions/special_functions/error_functions.py +3 -1
- pytcl/mathematical_functions/special_functions/gamma_functions.py +4 -4
- pytcl/mathematical_functions/special_functions/hypergeometric.py +81 -15
- pytcl/mathematical_functions/transforms/fourier.py +8 -8
- pytcl/mathematical_functions/transforms/stft.py +12 -12
- pytcl/mathematical_functions/transforms/wavelets.py +9 -9
- pytcl/navigation/geodesy.py +246 -160
- pytcl/navigation/great_circle.py +101 -19
- pytcl/plotting/coordinates.py +7 -7
- pytcl/plotting/tracks.py +2 -2
- pytcl/static_estimation/maximum_likelihood.py +16 -14
- pytcl/static_estimation/robust.py +5 -5
- pytcl/terrain/loaders.py +5 -5
- pytcl/trackers/hypothesis.py +1 -1
- pytcl/trackers/mht.py +9 -9
- pytcl/trackers/multi_target.py +1 -1
- {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.8.0.dist-info}/LICENSE +0 -0
- {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.8.0.dist-info}/WHEEL +0 -0
- {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.8.0.dist-info}/top_level.txt +0 -0
|
@@ -22,7 +22,7 @@ References
|
|
|
22
22
|
Speech, and Signal Processing, 32(2), 236-243.
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
|
-
from typing import NamedTuple, Optional, Union
|
|
25
|
+
from typing import Any, NamedTuple, Optional, Union
|
|
26
26
|
|
|
27
27
|
import numpy as np
|
|
28
28
|
from numpy.typing import ArrayLike, NDArray
|
|
@@ -77,7 +77,7 @@ class Spectrogram(NamedTuple):
|
|
|
77
77
|
|
|
78
78
|
|
|
79
79
|
def get_window(
|
|
80
|
-
window: Union[str, tuple, ArrayLike],
|
|
80
|
+
window: Union[str, tuple[str, Any], ArrayLike],
|
|
81
81
|
length: int,
|
|
82
82
|
fftbins: bool = True,
|
|
83
83
|
) -> NDArray[np.floating]:
|
|
@@ -174,7 +174,7 @@ def window_bandwidth(
|
|
|
174
174
|
def stft(
|
|
175
175
|
x: ArrayLike,
|
|
176
176
|
fs: float = 1.0,
|
|
177
|
-
window: Union[str, tuple, ArrayLike] = "hann",
|
|
177
|
+
window: Union[str, tuple[str, Any], ArrayLike] = "hann",
|
|
178
178
|
nperseg: int = 256,
|
|
179
179
|
noverlap: Optional[int] = None,
|
|
180
180
|
nfft: Optional[int] = None,
|
|
@@ -264,13 +264,13 @@ def stft(
|
|
|
264
264
|
def istft(
|
|
265
265
|
Zxx: ArrayLike,
|
|
266
266
|
fs: float = 1.0,
|
|
267
|
-
window: Union[str, tuple, ArrayLike] = "hann",
|
|
267
|
+
window: Union[str, tuple[str, Any], ArrayLike] = "hann",
|
|
268
268
|
nperseg: Optional[int] = None,
|
|
269
269
|
noverlap: Optional[int] = None,
|
|
270
270
|
nfft: Optional[int] = None,
|
|
271
271
|
input_onesided: bool = True,
|
|
272
272
|
boundary: bool = True,
|
|
273
|
-
) -> tuple:
|
|
273
|
+
) -> tuple[NDArray[np.floating], NDArray[np.floating]]:
|
|
274
274
|
"""
|
|
275
275
|
Compute the inverse Short-Time Fourier Transform.
|
|
276
276
|
|
|
@@ -351,7 +351,7 @@ def istft(
|
|
|
351
351
|
def spectrogram(
|
|
352
352
|
x: ArrayLike,
|
|
353
353
|
fs: float = 1.0,
|
|
354
|
-
window: Union[str, tuple, ArrayLike] = "hann",
|
|
354
|
+
window: Union[str, tuple[str, Any], ArrayLike] = "hann",
|
|
355
355
|
nperseg: int = 256,
|
|
356
356
|
noverlap: Optional[int] = None,
|
|
357
357
|
nfft: Optional[int] = None,
|
|
@@ -436,11 +436,11 @@ def spectrogram(
|
|
|
436
436
|
def reassigned_spectrogram(
|
|
437
437
|
x: ArrayLike,
|
|
438
438
|
fs: float = 1.0,
|
|
439
|
-
window: Union[str, tuple, ArrayLike] = "hann",
|
|
439
|
+
window: Union[str, tuple[str, Any], ArrayLike] = "hann",
|
|
440
440
|
nperseg: int = 256,
|
|
441
441
|
noverlap: Optional[int] = None,
|
|
442
442
|
nfft: Optional[int] = None,
|
|
443
|
-
) -> tuple:
|
|
443
|
+
) -> tuple[NDArray[np.floating], NDArray[np.floating], NDArray[np.floating]]:
|
|
444
444
|
"""
|
|
445
445
|
Compute reassigned spectrogram for improved time-frequency resolution.
|
|
446
446
|
|
|
@@ -538,7 +538,7 @@ def mel_spectrogram(
|
|
|
538
538
|
window: str = "hann",
|
|
539
539
|
nperseg: int = 2048,
|
|
540
540
|
noverlap: Optional[int] = None,
|
|
541
|
-
) -> tuple:
|
|
541
|
+
) -> tuple[NDArray[np.floating], NDArray[np.floating], NDArray[np.floating]]:
|
|
542
542
|
"""
|
|
543
543
|
Compute mel-scaled spectrogram.
|
|
544
544
|
|
|
@@ -611,15 +611,15 @@ def mel_spectrogram(
|
|
|
611
611
|
# Mel frequency centers
|
|
612
612
|
mel_freqs = _mel_frequencies(n_mels, fmin, fmax)
|
|
613
613
|
|
|
614
|
-
return mel_freqs, spec_result.times, mel_spec
|
|
614
|
+
return (mel_freqs, spec_result.times, mel_spec)
|
|
615
615
|
|
|
616
616
|
|
|
617
|
-
def _hz_to_mel(hz: Union[float, ArrayLike]) -> Union[float, NDArray]:
|
|
617
|
+
def _hz_to_mel(hz: Union[float, ArrayLike]) -> Union[float, NDArray[np.floating]]:
|
|
618
618
|
"""Convert frequency in Hz to mel scale."""
|
|
619
619
|
return 2595.0 * np.log10(1.0 + np.asarray(hz) / 700.0)
|
|
620
620
|
|
|
621
621
|
|
|
622
|
-
def _mel_to_hz(mel: Union[float, ArrayLike]) -> Union[float, NDArray]:
|
|
622
|
+
def _mel_to_hz(mel: Union[float, ArrayLike]) -> Union[float, NDArray[np.floating]]:
|
|
623
623
|
"""Convert mel scale to frequency in Hz."""
|
|
624
624
|
return 700.0 * (10.0 ** (np.asarray(mel) / 2595.0) - 1.0)
|
|
625
625
|
|
|
@@ -20,7 +20,7 @@ References
|
|
|
20
20
|
.. [2] Daubechies, I. (1992). Ten Lectures on Wavelets. SIAM.
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
|
-
from typing import Callable, List, NamedTuple, Optional, Union
|
|
23
|
+
from typing import Any, Callable, List, NamedTuple, Optional, Union
|
|
24
24
|
|
|
25
25
|
import numpy as np
|
|
26
26
|
from numpy.typing import ArrayLike, NDArray
|
|
@@ -262,7 +262,7 @@ def gaussian_wavelet(
|
|
|
262
262
|
def cwt(
|
|
263
263
|
signal: ArrayLike,
|
|
264
264
|
scales: ArrayLike,
|
|
265
|
-
wavelet: Union[str, Callable[[int], NDArray]] = "morlet",
|
|
265
|
+
wavelet: Union[str, Callable[[int], NDArray[np.floating]]] = "morlet",
|
|
266
266
|
fs: float = 1.0,
|
|
267
267
|
method: str = "fft",
|
|
268
268
|
) -> CWTResult:
|
|
@@ -312,16 +312,16 @@ def cwt(
|
|
|
312
312
|
n = len(signal)
|
|
313
313
|
|
|
314
314
|
# Determine wavelet function
|
|
315
|
-
def _morlet_default(M: int) -> NDArray:
|
|
315
|
+
def _morlet_default(M: int) -> NDArray[np.floating]:
|
|
316
316
|
return morlet_wavelet(M, w=5.0)
|
|
317
317
|
|
|
318
|
-
def _ricker_default(M: int) -> NDArray:
|
|
318
|
+
def _ricker_default(M: int) -> NDArray[np.floating]:
|
|
319
319
|
return ricker_wavelet(M, a=1.0)
|
|
320
320
|
|
|
321
|
-
def _gaussian1_default(M: int) -> NDArray:
|
|
321
|
+
def _gaussian1_default(M: int) -> NDArray[np.floating]:
|
|
322
322
|
return gaussian_wavelet(M, order=1)
|
|
323
323
|
|
|
324
|
-
def _gaussian2_default(M: int) -> NDArray:
|
|
324
|
+
def _gaussian2_default(M: int) -> NDArray[np.floating]:
|
|
325
325
|
return gaussian_wavelet(M, order=2)
|
|
326
326
|
|
|
327
327
|
if callable(wavelet):
|
|
@@ -596,7 +596,7 @@ def dwt_single_level(
|
|
|
596
596
|
signal: ArrayLike,
|
|
597
597
|
wavelet: str = "db4",
|
|
598
598
|
mode: str = "symmetric",
|
|
599
|
-
) -> tuple:
|
|
599
|
+
) -> tuple[NDArray[np.floating], NDArray[np.floating]]:
|
|
600
600
|
"""
|
|
601
601
|
Compute single-level DWT decomposition.
|
|
602
602
|
|
|
@@ -673,7 +673,7 @@ def wpt(
|
|
|
673
673
|
wavelet: str = "db4",
|
|
674
674
|
level: Optional[int] = None,
|
|
675
675
|
mode: str = "symmetric",
|
|
676
|
-
) -> dict:
|
|
676
|
+
) -> dict[str, NDArray[np.floating]]:
|
|
677
677
|
"""
|
|
678
678
|
Compute the Wavelet Packet Transform.
|
|
679
679
|
|
|
@@ -748,7 +748,7 @@ def available_wavelets() -> List[str]:
|
|
|
748
748
|
return pywt.wavelist()
|
|
749
749
|
|
|
750
750
|
|
|
751
|
-
def wavelet_info(wavelet: str) -> dict:
|
|
751
|
+
def wavelet_info(wavelet: str) -> dict[str, Any]:
|
|
752
752
|
"""
|
|
753
753
|
Get information about a wavelet.
|
|
754
754
|
|
pytcl/navigation/geodesy.py
CHANGED
|
@@ -8,11 +8,20 @@ This module provides geodetic utilities including:
|
|
|
8
8
|
- Earth ellipsoid parameters
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
import logging
|
|
12
|
+
from functools import lru_cache
|
|
13
|
+
from typing import Any, NamedTuple, Tuple
|
|
12
14
|
|
|
13
15
|
import numpy as np
|
|
14
16
|
from numpy.typing import ArrayLike, NDArray
|
|
15
17
|
|
|
18
|
+
# Module logger
|
|
19
|
+
_logger = logging.getLogger("pytcl.navigation.geodesy")
|
|
20
|
+
|
|
21
|
+
# Cache configuration for Vincenty geodetic calculations
|
|
22
|
+
_VINCENTY_CACHE_DECIMALS = 10 # ~0.01mm precision
|
|
23
|
+
_VINCENTY_CACHE_MAXSIZE = 128 # Max cached coordinate pairs
|
|
24
|
+
|
|
16
25
|
|
|
17
26
|
class Ellipsoid(NamedTuple):
|
|
18
27
|
"""
|
|
@@ -51,6 +60,198 @@ GRS80 = Ellipsoid(a=6378137.0, f=1.0 / 298.257222101)
|
|
|
51
60
|
SPHERE = Ellipsoid(a=6371000.0, f=0.0)
|
|
52
61
|
|
|
53
62
|
|
|
63
|
+
def _quantize_geodetic(val: float) -> float:
|
|
64
|
+
"""Quantize geodetic coordinate for cache key compatibility."""
|
|
65
|
+
return round(val, _VINCENTY_CACHE_DECIMALS)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@lru_cache(maxsize=_VINCENTY_CACHE_MAXSIZE)
|
|
69
|
+
def _inverse_geodetic_cached(
|
|
70
|
+
lat1_q: float,
|
|
71
|
+
lon1_q: float,
|
|
72
|
+
lat2_q: float,
|
|
73
|
+
lon2_q: float,
|
|
74
|
+
a: float,
|
|
75
|
+
f: float,
|
|
76
|
+
) -> Tuple[float, float, float]:
|
|
77
|
+
"""Cached Vincenty inverse geodetic computation (internal).
|
|
78
|
+
|
|
79
|
+
Returns (distance, azimuth1, azimuth2).
|
|
80
|
+
"""
|
|
81
|
+
b = a * (1 - f)
|
|
82
|
+
|
|
83
|
+
# Reduced latitudes
|
|
84
|
+
U1 = np.arctan((1 - f) * np.tan(lat1_q))
|
|
85
|
+
U2 = np.arctan((1 - f) * np.tan(lat2_q))
|
|
86
|
+
sin_U1, cos_U1 = np.sin(U1), np.cos(U1)
|
|
87
|
+
sin_U2, cos_U2 = np.sin(U2), np.cos(U2)
|
|
88
|
+
|
|
89
|
+
L = lon2_q - lon1_q
|
|
90
|
+
lam = L
|
|
91
|
+
|
|
92
|
+
for _ in range(100):
|
|
93
|
+
sin_lam = np.sin(lam)
|
|
94
|
+
cos_lam = np.cos(lam)
|
|
95
|
+
|
|
96
|
+
sin_sigma = np.sqrt(
|
|
97
|
+
(cos_U2 * sin_lam) ** 2 + (cos_U1 * sin_U2 - sin_U1 * cos_U2 * cos_lam) ** 2
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if sin_sigma == 0:
|
|
101
|
+
# Coincident points
|
|
102
|
+
return 0.0, 0.0, 0.0
|
|
103
|
+
|
|
104
|
+
cos_sigma = sin_U1 * sin_U2 + cos_U1 * cos_U2 * cos_lam
|
|
105
|
+
sigma = np.arctan2(sin_sigma, cos_sigma)
|
|
106
|
+
|
|
107
|
+
sin_alpha = cos_U1 * cos_U2 * sin_lam / sin_sigma
|
|
108
|
+
cos2_alpha = 1 - sin_alpha**2
|
|
109
|
+
|
|
110
|
+
if cos2_alpha == 0:
|
|
111
|
+
cos_2sigma_m = 0
|
|
112
|
+
else:
|
|
113
|
+
cos_2sigma_m = cos_sigma - 2 * sin_U1 * sin_U2 / cos2_alpha
|
|
114
|
+
|
|
115
|
+
C = f / 16 * cos2_alpha * (4 + f * (4 - 3 * cos2_alpha))
|
|
116
|
+
|
|
117
|
+
lam_new = L + (1 - C) * f * sin_alpha * (
|
|
118
|
+
sigma
|
|
119
|
+
+ C
|
|
120
|
+
* sin_sigma
|
|
121
|
+
* (cos_2sigma_m + C * cos_sigma * (-1 + 2 * cos_2sigma_m**2))
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if abs(lam_new - lam) < 1e-12:
|
|
125
|
+
break
|
|
126
|
+
lam = lam_new
|
|
127
|
+
|
|
128
|
+
u2 = cos2_alpha * (a**2 - b**2) / b**2
|
|
129
|
+
A = 1 + u2 / 16384 * (4096 + u2 * (-768 + u2 * (320 - 175 * u2)))
|
|
130
|
+
B = u2 / 1024 * (256 + u2 * (-128 + u2 * (74 - 47 * u2)))
|
|
131
|
+
|
|
132
|
+
delta_sigma = (
|
|
133
|
+
B
|
|
134
|
+
* sin_sigma
|
|
135
|
+
* (
|
|
136
|
+
cos_2sigma_m
|
|
137
|
+
+ B
|
|
138
|
+
/ 4
|
|
139
|
+
* (
|
|
140
|
+
cos_sigma * (-1 + 2 * cos_2sigma_m**2)
|
|
141
|
+
- B
|
|
142
|
+
/ 6
|
|
143
|
+
* cos_2sigma_m
|
|
144
|
+
* (-3 + 4 * sin_sigma**2)
|
|
145
|
+
* (-3 + 4 * cos_2sigma_m**2)
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
distance = b * A * (sigma - delta_sigma)
|
|
151
|
+
|
|
152
|
+
# Azimuths
|
|
153
|
+
azimuth1 = np.arctan2(cos_U2 * sin_lam, cos_U1 * sin_U2 - sin_U1 * cos_U2 * cos_lam)
|
|
154
|
+
azimuth2 = np.arctan2(
|
|
155
|
+
cos_U1 * sin_lam, -sin_U1 * cos_U2 + cos_U1 * sin_U2 * cos_lam
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return float(distance), float(azimuth1), float(azimuth2)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@lru_cache(maxsize=_VINCENTY_CACHE_MAXSIZE)
|
|
162
|
+
def _direct_geodetic_cached(
|
|
163
|
+
lat1_q: float,
|
|
164
|
+
lon1_q: float,
|
|
165
|
+
azimuth_q: float,
|
|
166
|
+
distance_q: float,
|
|
167
|
+
a: float,
|
|
168
|
+
f: float,
|
|
169
|
+
) -> Tuple[float, float, float]:
|
|
170
|
+
"""Cached Vincenty direct geodetic computation (internal).
|
|
171
|
+
|
|
172
|
+
Returns (lat2, lon2, azimuth2).
|
|
173
|
+
"""
|
|
174
|
+
b = a * (1 - f)
|
|
175
|
+
|
|
176
|
+
sin_alpha1 = np.sin(azimuth_q)
|
|
177
|
+
cos_alpha1 = np.cos(azimuth_q)
|
|
178
|
+
|
|
179
|
+
# Reduced latitude
|
|
180
|
+
tan_U1 = (1 - f) * np.tan(lat1_q)
|
|
181
|
+
cos_U1 = 1.0 / np.sqrt(1 + tan_U1**2)
|
|
182
|
+
sin_U1 = tan_U1 * cos_U1
|
|
183
|
+
|
|
184
|
+
sigma1 = np.arctan2(tan_U1, cos_alpha1)
|
|
185
|
+
sin_alpha = cos_U1 * sin_alpha1
|
|
186
|
+
cos2_alpha = 1 - sin_alpha**2
|
|
187
|
+
|
|
188
|
+
u2 = cos2_alpha * (a**2 - b**2) / b**2
|
|
189
|
+
A = 1 + u2 / 16384 * (4096 + u2 * (-768 + u2 * (320 - 175 * u2)))
|
|
190
|
+
B = u2 / 1024 * (256 + u2 * (-128 + u2 * (74 - 47 * u2)))
|
|
191
|
+
|
|
192
|
+
sigma = distance_q / (b * A)
|
|
193
|
+
|
|
194
|
+
for _ in range(100):
|
|
195
|
+
cos_2sigma_m = np.cos(2 * sigma1 + sigma)
|
|
196
|
+
sin_sigma = np.sin(sigma)
|
|
197
|
+
cos_sigma = np.cos(sigma)
|
|
198
|
+
|
|
199
|
+
delta_sigma = (
|
|
200
|
+
B
|
|
201
|
+
* sin_sigma
|
|
202
|
+
* (
|
|
203
|
+
cos_2sigma_m
|
|
204
|
+
+ B
|
|
205
|
+
/ 4
|
|
206
|
+
* (
|
|
207
|
+
cos_sigma * (-1 + 2 * cos_2sigma_m**2)
|
|
208
|
+
- B
|
|
209
|
+
/ 6
|
|
210
|
+
* cos_2sigma_m
|
|
211
|
+
* (-3 + 4 * sin_sigma**2)
|
|
212
|
+
* (-3 + 4 * cos_2sigma_m**2)
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
sigma_new = distance_q / (b * A) + delta_sigma
|
|
218
|
+
if abs(sigma_new - sigma) < 1e-12:
|
|
219
|
+
break
|
|
220
|
+
sigma = sigma_new
|
|
221
|
+
|
|
222
|
+
cos_2sigma_m = np.cos(2 * sigma1 + sigma)
|
|
223
|
+
sin_sigma = np.sin(sigma)
|
|
224
|
+
cos_sigma = np.cos(sigma)
|
|
225
|
+
|
|
226
|
+
sin_U2 = sin_U1 * cos_sigma + cos_U1 * sin_sigma * cos_alpha1
|
|
227
|
+
lat2 = np.arctan2(
|
|
228
|
+
sin_U2,
|
|
229
|
+
(1 - f)
|
|
230
|
+
* np.sqrt(
|
|
231
|
+
sin_alpha**2 + (sin_U1 * sin_sigma - cos_U1 * cos_sigma * cos_alpha1) ** 2
|
|
232
|
+
),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
lam = np.arctan2(
|
|
236
|
+
sin_sigma * sin_alpha1, cos_U1 * cos_sigma - sin_U1 * sin_sigma * cos_alpha1
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
C = f / 16 * cos2_alpha * (4 + f * (4 - 3 * cos2_alpha))
|
|
240
|
+
L = lam - (1 - C) * f * sin_alpha * (
|
|
241
|
+
sigma
|
|
242
|
+
+ C * sin_sigma * (cos_2sigma_m + C * cos_sigma * (-1 + 2 * cos_2sigma_m**2))
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
lon2 = lon1_q + L
|
|
246
|
+
|
|
247
|
+
# Back azimuth
|
|
248
|
+
azimuth2 = np.arctan2(
|
|
249
|
+
sin_alpha, -sin_U1 * sin_sigma + cos_U1 * cos_sigma * cos_alpha1
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
return float(lat2), float(lon2), float(azimuth2)
|
|
253
|
+
|
|
254
|
+
|
|
54
255
|
def geodetic_to_ecef(
|
|
55
256
|
lat: ArrayLike,
|
|
56
257
|
lon: ArrayLike,
|
|
@@ -372,6 +573,7 @@ def direct_geodetic(
|
|
|
372
573
|
Solve the direct geodetic problem (Vincenty).
|
|
373
574
|
|
|
374
575
|
Given a starting point, azimuth, and distance, find the destination point.
|
|
576
|
+
Results are cached for repeated queries with the same parameters.
|
|
375
577
|
|
|
376
578
|
Parameters
|
|
377
579
|
----------
|
|
@@ -400,88 +602,15 @@ def direct_geodetic(
|
|
|
400
602
|
.. [1] Vincenty, T., "Direct and Inverse Solutions of Geodesics on the
|
|
401
603
|
Ellipsoid with Application of Nested Equations", Survey Review, 1975.
|
|
402
604
|
"""
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
# Reduced latitude
|
|
411
|
-
tan_U1 = (1 - f) * np.tan(lat1)
|
|
412
|
-
cos_U1 = 1.0 / np.sqrt(1 + tan_U1**2)
|
|
413
|
-
sin_U1 = tan_U1 * cos_U1
|
|
414
|
-
|
|
415
|
-
sigma1 = np.arctan2(tan_U1, cos_alpha1)
|
|
416
|
-
sin_alpha = cos_U1 * sin_alpha1
|
|
417
|
-
cos2_alpha = 1 - sin_alpha**2
|
|
418
|
-
|
|
419
|
-
u2 = cos2_alpha * (a**2 - b**2) / b**2
|
|
420
|
-
A = 1 + u2 / 16384 * (4096 + u2 * (-768 + u2 * (320 - 175 * u2)))
|
|
421
|
-
B = u2 / 1024 * (256 + u2 * (-128 + u2 * (74 - 47 * u2)))
|
|
422
|
-
|
|
423
|
-
sigma = distance / (b * A)
|
|
424
|
-
|
|
425
|
-
for _ in range(100):
|
|
426
|
-
cos_2sigma_m = np.cos(2 * sigma1 + sigma)
|
|
427
|
-
sin_sigma = np.sin(sigma)
|
|
428
|
-
cos_sigma = np.cos(sigma)
|
|
429
|
-
|
|
430
|
-
delta_sigma = (
|
|
431
|
-
B
|
|
432
|
-
* sin_sigma
|
|
433
|
-
* (
|
|
434
|
-
cos_2sigma_m
|
|
435
|
-
+ B
|
|
436
|
-
/ 4
|
|
437
|
-
* (
|
|
438
|
-
cos_sigma * (-1 + 2 * cos_2sigma_m**2)
|
|
439
|
-
- B
|
|
440
|
-
/ 6
|
|
441
|
-
* cos_2sigma_m
|
|
442
|
-
* (-3 + 4 * sin_sigma**2)
|
|
443
|
-
* (-3 + 4 * cos_2sigma_m**2)
|
|
444
|
-
)
|
|
445
|
-
)
|
|
446
|
-
)
|
|
447
|
-
|
|
448
|
-
sigma_new = distance / (b * A) + delta_sigma
|
|
449
|
-
if abs(sigma_new - sigma) < 1e-12:
|
|
450
|
-
break
|
|
451
|
-
sigma = sigma_new
|
|
452
|
-
|
|
453
|
-
cos_2sigma_m = np.cos(2 * sigma1 + sigma)
|
|
454
|
-
sin_sigma = np.sin(sigma)
|
|
455
|
-
cos_sigma = np.cos(sigma)
|
|
456
|
-
|
|
457
|
-
sin_U2 = sin_U1 * cos_sigma + cos_U1 * sin_sigma * cos_alpha1
|
|
458
|
-
lat2 = np.arctan2(
|
|
459
|
-
sin_U2,
|
|
460
|
-
(1 - f)
|
|
461
|
-
* np.sqrt(
|
|
462
|
-
sin_alpha**2 + (sin_U1 * sin_sigma - cos_U1 * cos_sigma * cos_alpha1) ** 2
|
|
463
|
-
),
|
|
605
|
+
return _direct_geodetic_cached(
|
|
606
|
+
_quantize_geodetic(lat1),
|
|
607
|
+
_quantize_geodetic(lon1),
|
|
608
|
+
_quantize_geodetic(azimuth),
|
|
609
|
+
round(distance, 3), # 1mm precision for distance
|
|
610
|
+
ellipsoid.a,
|
|
611
|
+
ellipsoid.f,
|
|
464
612
|
)
|
|
465
613
|
|
|
466
|
-
lam = np.arctan2(
|
|
467
|
-
sin_sigma * sin_alpha1, cos_U1 * cos_sigma - sin_U1 * sin_sigma * cos_alpha1
|
|
468
|
-
)
|
|
469
|
-
|
|
470
|
-
C = f / 16 * cos2_alpha * (4 + f * (4 - 3 * cos2_alpha))
|
|
471
|
-
L = lam - (1 - C) * f * sin_alpha * (
|
|
472
|
-
sigma
|
|
473
|
-
+ C * sin_sigma * (cos_2sigma_m + C * cos_sigma * (-1 + 2 * cos_2sigma_m**2))
|
|
474
|
-
)
|
|
475
|
-
|
|
476
|
-
lon2 = lon1 + L
|
|
477
|
-
|
|
478
|
-
# Back azimuth
|
|
479
|
-
azimuth2 = np.arctan2(
|
|
480
|
-
sin_alpha, -sin_U1 * sin_sigma + cos_U1 * cos_sigma * cos_alpha1
|
|
481
|
-
)
|
|
482
|
-
|
|
483
|
-
return float(lat2), float(lon2), float(azimuth2)
|
|
484
|
-
|
|
485
614
|
|
|
486
615
|
def inverse_geodetic(
|
|
487
616
|
lat1: float,
|
|
@@ -494,6 +623,7 @@ def inverse_geodetic(
|
|
|
494
623
|
Solve the inverse geodetic problem (Vincenty).
|
|
495
624
|
|
|
496
625
|
Given two points, find the distance and azimuths between them.
|
|
626
|
+
Results are cached for repeated queries with the same coordinates.
|
|
497
627
|
|
|
498
628
|
Parameters
|
|
499
629
|
----------
|
|
@@ -526,87 +656,15 @@ def inverse_geodetic(
|
|
|
526
656
|
.. [1] Vincenty, T., "Direct and Inverse Solutions of Geodesics on the
|
|
527
657
|
Ellipsoid with Application of Nested Equations", Survey Review, 1975.
|
|
528
658
|
"""
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
sin_U1, cos_U1 = np.sin(U1), np.cos(U1)
|
|
537
|
-
sin_U2, cos_U2 = np.sin(U2), np.cos(U2)
|
|
538
|
-
|
|
539
|
-
L = lon2 - lon1
|
|
540
|
-
lam = L
|
|
541
|
-
|
|
542
|
-
for _ in range(100):
|
|
543
|
-
sin_lam = np.sin(lam)
|
|
544
|
-
cos_lam = np.cos(lam)
|
|
545
|
-
|
|
546
|
-
sin_sigma = np.sqrt(
|
|
547
|
-
(cos_U2 * sin_lam) ** 2 + (cos_U1 * sin_U2 - sin_U1 * cos_U2 * cos_lam) ** 2
|
|
548
|
-
)
|
|
549
|
-
|
|
550
|
-
if sin_sigma == 0:
|
|
551
|
-
# Coincident points
|
|
552
|
-
return 0.0, 0.0, 0.0
|
|
553
|
-
|
|
554
|
-
cos_sigma = sin_U1 * sin_U2 + cos_U1 * cos_U2 * cos_lam
|
|
555
|
-
sigma = np.arctan2(sin_sigma, cos_sigma)
|
|
556
|
-
|
|
557
|
-
sin_alpha = cos_U1 * cos_U2 * sin_lam / sin_sigma
|
|
558
|
-
cos2_alpha = 1 - sin_alpha**2
|
|
559
|
-
|
|
560
|
-
if cos2_alpha == 0:
|
|
561
|
-
cos_2sigma_m = 0
|
|
562
|
-
else:
|
|
563
|
-
cos_2sigma_m = cos_sigma - 2 * sin_U1 * sin_U2 / cos2_alpha
|
|
564
|
-
|
|
565
|
-
C = f / 16 * cos2_alpha * (4 + f * (4 - 3 * cos2_alpha))
|
|
566
|
-
|
|
567
|
-
lam_new = L + (1 - C) * f * sin_alpha * (
|
|
568
|
-
sigma
|
|
569
|
-
+ C
|
|
570
|
-
* sin_sigma
|
|
571
|
-
* (cos_2sigma_m + C * cos_sigma * (-1 + 2 * cos_2sigma_m**2))
|
|
572
|
-
)
|
|
573
|
-
|
|
574
|
-
if abs(lam_new - lam) < 1e-12:
|
|
575
|
-
break
|
|
576
|
-
lam = lam_new
|
|
577
|
-
|
|
578
|
-
u2 = cos2_alpha * (a**2 - b**2) / b**2
|
|
579
|
-
A = 1 + u2 / 16384 * (4096 + u2 * (-768 + u2 * (320 - 175 * u2)))
|
|
580
|
-
B = u2 / 1024 * (256 + u2 * (-128 + u2 * (74 - 47 * u2)))
|
|
581
|
-
|
|
582
|
-
delta_sigma = (
|
|
583
|
-
B
|
|
584
|
-
* sin_sigma
|
|
585
|
-
* (
|
|
586
|
-
cos_2sigma_m
|
|
587
|
-
+ B
|
|
588
|
-
/ 4
|
|
589
|
-
* (
|
|
590
|
-
cos_sigma * (-1 + 2 * cos_2sigma_m**2)
|
|
591
|
-
- B
|
|
592
|
-
/ 6
|
|
593
|
-
* cos_2sigma_m
|
|
594
|
-
* (-3 + 4 * sin_sigma**2)
|
|
595
|
-
* (-3 + 4 * cos_2sigma_m**2)
|
|
596
|
-
)
|
|
597
|
-
)
|
|
659
|
+
return _inverse_geodetic_cached(
|
|
660
|
+
_quantize_geodetic(lat1),
|
|
661
|
+
_quantize_geodetic(lon1),
|
|
662
|
+
_quantize_geodetic(lat2),
|
|
663
|
+
_quantize_geodetic(lon2),
|
|
664
|
+
ellipsoid.a,
|
|
665
|
+
ellipsoid.f,
|
|
598
666
|
)
|
|
599
667
|
|
|
600
|
-
distance = b * A * (sigma - delta_sigma)
|
|
601
|
-
|
|
602
|
-
# Azimuths
|
|
603
|
-
azimuth1 = np.arctan2(cos_U2 * sin_lam, cos_U1 * sin_U2 - sin_U1 * cos_U2 * cos_lam)
|
|
604
|
-
azimuth2 = np.arctan2(
|
|
605
|
-
cos_U1 * sin_lam, -sin_U1 * cos_U2 + cos_U1 * sin_U2 * cos_lam
|
|
606
|
-
)
|
|
607
|
-
|
|
608
|
-
return float(distance), float(azimuth1), float(azimuth2)
|
|
609
|
-
|
|
610
668
|
|
|
611
669
|
def haversine_distance(
|
|
612
670
|
lat1: float,
|
|
@@ -646,6 +704,31 @@ def haversine_distance(
|
|
|
646
704
|
return radius * c
|
|
647
705
|
|
|
648
706
|
|
|
707
|
+
def clear_geodesy_cache() -> None:
|
|
708
|
+
"""Clear all geodesy computation caches.
|
|
709
|
+
|
|
710
|
+
This can be useful to free memory after processing large datasets
|
|
711
|
+
or when cache statistics are being monitored.
|
|
712
|
+
"""
|
|
713
|
+
_inverse_geodetic_cached.cache_clear()
|
|
714
|
+
_direct_geodetic_cached.cache_clear()
|
|
715
|
+
_logger.debug("Geodesy caches cleared")
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def get_geodesy_cache_info() -> dict[str, Any]:
|
|
719
|
+
"""Get cache statistics for geodesy computations.
|
|
720
|
+
|
|
721
|
+
Returns
|
|
722
|
+
-------
|
|
723
|
+
dict[str, Any]
|
|
724
|
+
Dictionary with cache statistics for inverse and direct geodetic caches.
|
|
725
|
+
"""
|
|
726
|
+
return {
|
|
727
|
+
"inverse_geodetic": _inverse_geodetic_cached.cache_info()._asdict(),
|
|
728
|
+
"direct_geodetic": _direct_geodetic_cached.cache_info()._asdict(),
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
|
|
649
732
|
__all__ = [
|
|
650
733
|
# Ellipsoids
|
|
651
734
|
"Ellipsoid",
|
|
@@ -663,4 +746,7 @@ __all__ = [
|
|
|
663
746
|
"direct_geodetic",
|
|
664
747
|
"inverse_geodetic",
|
|
665
748
|
"haversine_distance",
|
|
749
|
+
# Cache management
|
|
750
|
+
"clear_geodesy_cache",
|
|
751
|
+
"get_geodesy_cache_info",
|
|
666
752
|
]
|