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.
@@ -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
- a = ellipsoid.a
404
- f = ellipsoid.f
405
- b = ellipsoid.b
406
-
407
- sin_alpha1 = np.sin(azimuth)
408
- cos_alpha1 = np.cos(azimuth)
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
- a = ellipsoid.a
530
- f = ellipsoid.f
531
- b = ellipsoid.b
532
-
533
- # Reduced latitudes
534
- U1 = np.arctan((1 - f) * np.tan(lat1))
535
- U2 = np.arctan((1 - f) * np.tan(lat2))
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
  ]
@@ -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
- dlat = lat2 - lat1
135
- dlon = lon2 - lon1
136
-
137
- a = np.sin(dlat / 2) ** 2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon / 2) ** 2
138
- c = 2 * np.arctan2(np.sqrt(a), np.sqrt(1 - a))
139
-
140
- return radius * c
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
- dlon = lon2 - lon1
174
-
175
- x = np.sin(dlon) * np.cos(lat2)
176
- y = np.cos(lat1) * np.sin(lat2) - np.sin(lat1) * np.cos(lat2) * np.cos(dlon)
177
-
178
- azimuth = np.arctan2(x, y)
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
  ]