nrl-tracker 1.1.3__py3-none-any.whl → 1.3.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.1.3.dist-info → nrl_tracker-1.3.0.dist-info}/METADATA +1 -1
- {nrl_tracker-1.1.3.dist-info → nrl_tracker-1.3.0.dist-info}/RECORD +28 -24
- pytcl/__init__.py +1 -1
- pytcl/astronomical/reference_frames.py +127 -55
- pytcl/atmosphere/__init__.py +32 -1
- pytcl/atmosphere/ionosphere.py +512 -0
- pytcl/containers/__init__.py +24 -0
- pytcl/containers/base.py +219 -0
- pytcl/containers/covertree.py +21 -26
- pytcl/containers/kd_tree.py +94 -29
- pytcl/containers/rtree.py +199 -0
- pytcl/containers/vptree.py +17 -26
- pytcl/core/__init__.py +18 -0
- pytcl/core/validation.py +331 -0
- pytcl/dynamic_estimation/kalman/square_root.py +52 -571
- pytcl/dynamic_estimation/kalman/sr_ukf.py +302 -0
- pytcl/dynamic_estimation/kalman/ud_filter.py +404 -0
- pytcl/gravity/egm.py +13 -0
- pytcl/gravity/spherical_harmonics.py +97 -36
- pytcl/magnetism/__init__.py +7 -0
- pytcl/magnetism/wmm.py +260 -23
- pytcl/mathematical_functions/special_functions/debye.py +132 -26
- pytcl/mathematical_functions/special_functions/hypergeometric.py +79 -15
- pytcl/navigation/geodesy.py +245 -159
- pytcl/navigation/great_circle.py +98 -16
- {nrl_tracker-1.1.3.dist-info → nrl_tracker-1.3.0.dist-info}/LICENSE +0 -0
- {nrl_tracker-1.1.3.dist-info → nrl_tracker-1.3.0.dist-info}/WHEEL +0 -0
- {nrl_tracker-1.1.3.dist-info → nrl_tracker-1.3.0.dist-info}/top_level.txt +0 -0
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
|
+
import logging
|
|
12
|
+
from functools import lru_cache
|
|
11
13
|
from typing import 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:
|
|
719
|
+
"""Get cache statistics for geodesy computations.
|
|
720
|
+
|
|
721
|
+
Returns
|
|
722
|
+
-------
|
|
723
|
+
dict
|
|
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
|
]
|
pytcl/navigation/great_circle.py
CHANGED
|
@@ -10,11 +10,20 @@ computing the shortest path on a sphere, including:
|
|
|
10
10
|
- TDOA localization on a sphere
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
+
import logging
|
|
14
|
+
from functools import lru_cache
|
|
13
15
|
from typing import NamedTuple, Optional, Tuple
|
|
14
16
|
|
|
15
17
|
import numpy as np
|
|
16
18
|
from numpy.typing import NDArray
|
|
17
19
|
|
|
20
|
+
# Module logger
|
|
21
|
+
_logger = logging.getLogger("pytcl.navigation.great_circle")
|
|
22
|
+
|
|
23
|
+
# Cache configuration for great circle calculations
|
|
24
|
+
_GC_CACHE_DECIMALS = 10 # ~0.01mm precision at Earth's surface
|
|
25
|
+
_GC_CACHE_MAXSIZE = 256 # Max cached coordinate pairs
|
|
26
|
+
|
|
18
27
|
|
|
19
28
|
class GreatCircleResult(NamedTuple):
|
|
20
29
|
"""
|
|
@@ -96,6 +105,50 @@ class CrossTrackResult(NamedTuple):
|
|
|
96
105
|
EARTH_RADIUS = 6371000.0
|
|
97
106
|
|
|
98
107
|
|
|
108
|
+
def _quantize_coord(val: float) -> float:
|
|
109
|
+
"""Quantize coordinate value for cache key compatibility."""
|
|
110
|
+
return round(val, _GC_CACHE_DECIMALS)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@lru_cache(maxsize=_GC_CACHE_MAXSIZE)
|
|
114
|
+
def _gc_distance_cached(
|
|
115
|
+
lat1_q: float,
|
|
116
|
+
lon1_q: float,
|
|
117
|
+
lat2_q: float,
|
|
118
|
+
lon2_q: float,
|
|
119
|
+
) -> float:
|
|
120
|
+
"""Cached great circle distance computation (internal).
|
|
121
|
+
|
|
122
|
+
Uses haversine formula for numerical stability.
|
|
123
|
+
Returns angular distance in radians.
|
|
124
|
+
"""
|
|
125
|
+
dlat = lat2_q - lat1_q
|
|
126
|
+
dlon = lon2_q - lon1_q
|
|
127
|
+
|
|
128
|
+
a = np.sin(dlat / 2) ** 2 + np.cos(lat1_q) * np.cos(lat2_q) * np.sin(dlon / 2) ** 2
|
|
129
|
+
return 2 * np.arctan2(np.sqrt(a), np.sqrt(1 - a))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@lru_cache(maxsize=_GC_CACHE_MAXSIZE)
|
|
133
|
+
def _gc_azimuth_cached(
|
|
134
|
+
lat1_q: float,
|
|
135
|
+
lon1_q: float,
|
|
136
|
+
lat2_q: float,
|
|
137
|
+
lon2_q: float,
|
|
138
|
+
) -> float:
|
|
139
|
+
"""Cached great circle azimuth computation (internal).
|
|
140
|
+
|
|
141
|
+
Returns azimuth in radians [0, 2π).
|
|
142
|
+
"""
|
|
143
|
+
dlon = lon2_q - lon1_q
|
|
144
|
+
|
|
145
|
+
x = np.sin(dlon) * np.cos(lat2_q)
|
|
146
|
+
y = np.cos(lat1_q) * np.sin(lat2_q) - np.sin(lat1_q) * np.cos(lat2_q) * np.cos(dlon)
|
|
147
|
+
|
|
148
|
+
azimuth = np.arctan2(x, y)
|
|
149
|
+
return azimuth % (2 * np.pi)
|
|
150
|
+
|
|
151
|
+
|
|
99
152
|
def great_circle_distance(
|
|
100
153
|
lat1: float,
|
|
101
154
|
lon1: float,
|
|
@@ -107,6 +160,7 @@ def great_circle_distance(
|
|
|
107
160
|
Compute great circle distance between two points.
|
|
108
161
|
|
|
109
162
|
Uses the haversine formula for numerical stability at small distances.
|
|
163
|
+
Results are cached for repeated queries with the same coordinates.
|
|
110
164
|
|
|
111
165
|
Parameters
|
|
112
166
|
----------
|
|
@@ -131,13 +185,14 @@ def great_circle_distance(
|
|
|
131
185
|
>>> dist = great_circle_distance(lat1, lon1, lat2, lon2)
|
|
132
186
|
>>> print(f"Distance: {dist/1000:.0f} km")
|
|
133
187
|
"""
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
188
|
+
# Use cached angular distance computation
|
|
189
|
+
angular_dist = _gc_distance_cached(
|
|
190
|
+
_quantize_coord(lat1),
|
|
191
|
+
_quantize_coord(lon1),
|
|
192
|
+
_quantize_coord(lat2),
|
|
193
|
+
_quantize_coord(lon2),
|
|
194
|
+
)
|
|
195
|
+
return radius * angular_dist
|
|
141
196
|
|
|
142
197
|
|
|
143
198
|
def great_circle_azimuth(
|
|
@@ -149,6 +204,8 @@ def great_circle_azimuth(
|
|
|
149
204
|
"""
|
|
150
205
|
Compute initial azimuth (bearing) from point 1 to point 2.
|
|
151
206
|
|
|
207
|
+
Results are cached for repeated queries with the same coordinates.
|
|
208
|
+
|
|
152
209
|
Parameters
|
|
153
210
|
----------
|
|
154
211
|
lat1, lon1 : float
|
|
@@ -170,15 +227,12 @@ def great_circle_azimuth(
|
|
|
170
227
|
>>> az = great_circle_azimuth(lat1, lon1, lat2, lon2)
|
|
171
228
|
>>> print(f"Initial bearing: {np.degrees(az):.1f}°")
|
|
172
229
|
"""
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
# Normalize to [0, 2π)
|
|
181
|
-
return azimuth % (2 * np.pi)
|
|
230
|
+
return _gc_azimuth_cached(
|
|
231
|
+
_quantize_coord(lat1),
|
|
232
|
+
_quantize_coord(lon1),
|
|
233
|
+
_quantize_coord(lat2),
|
|
234
|
+
_quantize_coord(lon2),
|
|
235
|
+
)
|
|
182
236
|
|
|
183
237
|
|
|
184
238
|
def great_circle_inverse(
|
|
@@ -771,6 +825,31 @@ def destination_point(
|
|
|
771
825
|
return WaypointResult(float(lat2), float(lon2))
|
|
772
826
|
|
|
773
827
|
|
|
828
|
+
def clear_great_circle_cache() -> None:
|
|
829
|
+
"""Clear all great circle computation caches.
|
|
830
|
+
|
|
831
|
+
This can be useful to free memory after processing large datasets
|
|
832
|
+
or when cache statistics are being monitored.
|
|
833
|
+
"""
|
|
834
|
+
_gc_distance_cached.cache_clear()
|
|
835
|
+
_gc_azimuth_cached.cache_clear()
|
|
836
|
+
_logger.debug("Great circle caches cleared")
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
def get_cache_info() -> dict:
|
|
840
|
+
"""Get cache statistics for great circle computations.
|
|
841
|
+
|
|
842
|
+
Returns
|
|
843
|
+
-------
|
|
844
|
+
dict
|
|
845
|
+
Dictionary with cache statistics for distance and azimuth caches.
|
|
846
|
+
"""
|
|
847
|
+
return {
|
|
848
|
+
"distance": _gc_distance_cached.cache_info()._asdict(),
|
|
849
|
+
"azimuth": _gc_azimuth_cached.cache_info()._asdict(),
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
|
|
774
853
|
__all__ = [
|
|
775
854
|
# Constants
|
|
776
855
|
"EARTH_RADIUS",
|
|
@@ -796,4 +875,7 @@ __all__ = [
|
|
|
796
875
|
"great_circle_path_intersect",
|
|
797
876
|
# TDOA
|
|
798
877
|
"great_circle_tdoa_loc",
|
|
878
|
+
# Cache management
|
|
879
|
+
"clear_great_circle_cache",
|
|
880
|
+
"get_cache_info",
|
|
799
881
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|