nrl-tracker 1.9.1__py3-none-any.whl → 1.10.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.
Files changed (68) hide show
  1. {nrl_tracker-1.9.1.dist-info → nrl_tracker-1.10.0.dist-info}/METADATA +49 -4
  2. {nrl_tracker-1.9.1.dist-info → nrl_tracker-1.10.0.dist-info}/RECORD +68 -60
  3. pytcl/__init__.py +2 -2
  4. pytcl/assignment_algorithms/gating.py +18 -0
  5. pytcl/assignment_algorithms/jpda.py +56 -0
  6. pytcl/assignment_algorithms/nd_assignment.py +65 -0
  7. pytcl/assignment_algorithms/network_flow.py +40 -0
  8. pytcl/astronomical/ephemerides.py +18 -0
  9. pytcl/astronomical/orbital_mechanics.py +131 -0
  10. pytcl/atmosphere/ionosphere.py +44 -0
  11. pytcl/atmosphere/models.py +29 -0
  12. pytcl/clustering/dbscan.py +9 -0
  13. pytcl/clustering/gaussian_mixture.py +20 -0
  14. pytcl/clustering/hierarchical.py +29 -0
  15. pytcl/clustering/kmeans.py +9 -0
  16. pytcl/coordinate_systems/conversions/geodetic.py +46 -0
  17. pytcl/coordinate_systems/conversions/spherical.py +35 -0
  18. pytcl/coordinate_systems/rotations/rotations.py +147 -0
  19. pytcl/core/__init__.py +16 -0
  20. pytcl/core/maturity.py +346 -0
  21. pytcl/core/optional_deps.py +20 -0
  22. pytcl/dynamic_estimation/gaussian_sum_filter.py +55 -0
  23. pytcl/dynamic_estimation/imm.py +29 -0
  24. pytcl/dynamic_estimation/information_filter.py +64 -0
  25. pytcl/dynamic_estimation/kalman/extended.py +56 -0
  26. pytcl/dynamic_estimation/kalman/linear.py +69 -0
  27. pytcl/dynamic_estimation/kalman/unscented.py +81 -0
  28. pytcl/dynamic_estimation/particle_filters/bootstrap.py +146 -0
  29. pytcl/dynamic_estimation/rbpf.py +51 -0
  30. pytcl/dynamic_estimation/smoothers.py +58 -0
  31. pytcl/dynamic_models/continuous_time/dynamics.py +104 -0
  32. pytcl/dynamic_models/discrete_time/coordinated_turn.py +6 -0
  33. pytcl/dynamic_models/discrete_time/singer.py +12 -0
  34. pytcl/dynamic_models/process_noise/coordinated_turn.py +46 -0
  35. pytcl/dynamic_models/process_noise/polynomial.py +6 -0
  36. pytcl/dynamic_models/process_noise/singer.py +52 -0
  37. pytcl/gpu/__init__.py +153 -0
  38. pytcl/gpu/ekf.py +425 -0
  39. pytcl/gpu/kalman.py +543 -0
  40. pytcl/gpu/matrix_utils.py +486 -0
  41. pytcl/gpu/particle_filter.py +568 -0
  42. pytcl/gpu/ukf.py +476 -0
  43. pytcl/gpu/utils.py +582 -0
  44. pytcl/gravity/clenshaw.py +60 -0
  45. pytcl/gravity/egm.py +47 -0
  46. pytcl/gravity/models.py +34 -0
  47. pytcl/gravity/spherical_harmonics.py +73 -0
  48. pytcl/gravity/tides.py +34 -0
  49. pytcl/mathematical_functions/numerical_integration/quadrature.py +85 -0
  50. pytcl/mathematical_functions/special_functions/bessel.py +55 -0
  51. pytcl/mathematical_functions/special_functions/elliptic.py +42 -0
  52. pytcl/mathematical_functions/special_functions/error_functions.py +49 -0
  53. pytcl/mathematical_functions/special_functions/gamma_functions.py +43 -0
  54. pytcl/mathematical_functions/special_functions/lambert_w.py +5 -0
  55. pytcl/mathematical_functions/special_functions/marcum_q.py +16 -0
  56. pytcl/navigation/geodesy.py +101 -2
  57. pytcl/navigation/great_circle.py +71 -0
  58. pytcl/navigation/rhumb.py +74 -0
  59. pytcl/performance_evaluation/estimation_metrics.py +70 -0
  60. pytcl/performance_evaluation/track_metrics.py +30 -0
  61. pytcl/static_estimation/maximum_likelihood.py +54 -0
  62. pytcl/static_estimation/robust.py +57 -0
  63. pytcl/terrain/dem.py +69 -0
  64. pytcl/terrain/visibility.py +65 -0
  65. pytcl/trackers/hypothesis.py +65 -0
  66. {nrl_tracker-1.9.1.dist-info → nrl_tracker-1.10.0.dist-info}/LICENSE +0 -0
  67. {nrl_tracker-1.9.1.dist-info → nrl_tracker-1.10.0.dist-info}/WHEEL +0 -0
  68. {nrl_tracker-1.9.1.dist-info → nrl_tracker-1.10.0.dist-info}/top_level.txt +0 -0
@@ -107,6 +107,13 @@ def erfcx(x: ArrayLike) -> NDArray[np.floating]:
107
107
  This function is useful when erfc(x) underflows but the scaled
108
108
  version remains representable.
109
109
 
110
+ Examples
111
+ --------
112
+ >>> erfcx(0)
113
+ 1.0
114
+ >>> erfcx(10) # Remains finite even when erfc(10) underflows
115
+ 0.056...
116
+
110
117
  See Also
111
118
  --------
112
119
  scipy.special.erfcx : Scaled complementary error function.
@@ -130,6 +137,13 @@ def erfi(x: ArrayLike) -> NDArray[np.floating]:
130
137
  y : ndarray
131
138
  Values of erfi(x).
132
139
 
140
+ Examples
141
+ --------
142
+ >>> erfi(0)
143
+ 0.0
144
+ >>> erfi(1)
145
+ 1.650...
146
+
133
147
  See Also
134
148
  --------
135
149
  scipy.special.erfi : Imaginary error function.
@@ -183,6 +197,13 @@ def erfcinv(y: ArrayLike) -> NDArray[np.floating]:
183
197
  x : ndarray
184
198
  Inverse complementary error function values.
185
199
 
200
+ Examples
201
+ --------
202
+ >>> erfcinv(1)
203
+ 0.0
204
+ >>> erfc(erfcinv(0.5))
205
+ 0.5
206
+
186
207
  See Also
187
208
  --------
188
209
  scipy.special.erfcinv : Inverse complementary error function.
@@ -211,6 +232,13 @@ def dawsn(x: ArrayLike) -> NDArray[np.floating]:
211
232
  Dawson's integral is related to the imaginary error function by:
212
233
  F(x) = (√π/2) * exp(-x²) * erfi(x)
213
234
 
235
+ Examples
236
+ --------
237
+ >>> dawsn(0)
238
+ 0.0
239
+ >>> dawsn(1)
240
+ 0.538...
241
+
214
242
  See Also
215
243
  --------
216
244
  scipy.special.dawsn : Dawson's integral.
@@ -238,6 +266,14 @@ def fresnel(x: ArrayLike) -> tuple[np.ndarray[Any, Any], np.ndarray[Any, Any]]:
238
266
  C : ndarray
239
267
  Fresnel cosine integral.
240
268
 
269
+ Examples
270
+ --------
271
+ >>> S, C = fresnel(1)
272
+ >>> S
273
+ 0.438...
274
+ >>> C
275
+ 0.779...
276
+
241
277
  See Also
242
278
  --------
243
279
  scipy.special.fresnel : Fresnel integrals.
@@ -266,6 +302,14 @@ def wofz(z: ArrayLike) -> NDArray[np.complexfloating]:
266
302
  -----
267
303
  This function is useful in spectral line modeling and plasma physics.
268
304
 
305
+ Examples
306
+ --------
307
+ >>> w = wofz(0)
308
+ >>> w.real
309
+ 1.0
310
+ >>> w.imag
311
+ 0.0
312
+
269
313
  See Also
270
314
  --------
271
315
  scipy.special.wofz : Faddeeva function.
@@ -294,6 +338,11 @@ def voigt_profile(x: ArrayLike, sigma: float, gamma: float) -> NDArray[np.floati
294
338
  V : ndarray
295
339
  Voigt profile values (normalized to unit area).
296
340
 
341
+ Examples
342
+ --------
343
+ >>> voigt_profile(0, 1, 0) # Pure Gaussian at x=0
344
+ 0.398...
345
+
297
346
  See Also
298
347
  --------
299
348
  scipy.special.voigt_profile : Voigt profile.
@@ -95,6 +95,11 @@ def gammainc(a: ArrayLike, x: ArrayLike) -> NDArray[np.floating]:
95
95
  -----
96
96
  This is the CDF of the gamma distribution.
97
97
 
98
+ Examples
99
+ --------
100
+ >>> gammainc(1, 1) # 1 - exp(-1)
101
+ 0.632...
102
+
98
103
  See Also
99
104
  --------
100
105
  scipy.special.gammainc : Regularized lower incomplete gamma function.
@@ -121,6 +126,11 @@ def gammaincc(a: ArrayLike, x: ArrayLike) -> NDArray[np.floating]:
121
126
  Q : ndarray
122
127
  Values of the regularized upper incomplete gamma function.
123
128
 
129
+ Examples
130
+ --------
131
+ >>> gammaincc(1, 1) # exp(-1)
132
+ 0.367...
133
+
124
134
  See Also
125
135
  --------
126
136
  scipy.special.gammaincc : Regularized upper incomplete gamma function.
@@ -146,6 +156,11 @@ def gammaincinv(a: ArrayLike, y: ArrayLike) -> NDArray[np.floating]:
146
156
  x : ndarray
147
157
  Values where P(a, x) = y.
148
158
 
159
+ Examples
160
+ --------
161
+ >>> gammaincinv(1, 0.5) # Median of exponential distribution
162
+ 0.693...
163
+
149
164
  See Also
150
165
  --------
151
166
  scipy.special.gammaincinv : Inverse of lower incomplete gamma.
@@ -169,6 +184,11 @@ def digamma(x: ArrayLike) -> NDArray[np.floating]:
169
184
  ψ : ndarray
170
185
  Values of the digamma function.
171
186
 
187
+ Examples
188
+ --------
189
+ >>> digamma(1) # -γ (negative Euler-Mascheroni constant)
190
+ -0.577...
191
+
172
192
  See Also
173
193
  --------
174
194
  scipy.special.digamma : Digamma function.
@@ -195,6 +215,13 @@ def polygamma(n: int, x: ArrayLike) -> NDArray[np.floating]:
195
215
  ψn : ndarray
196
216
  Values of the n-th polygamma function.
197
217
 
218
+ Examples
219
+ --------
220
+ >>> polygamma(0, 1) # Digamma at 1 = -γ
221
+ -0.577...
222
+ >>> polygamma(1, 1) # Trigamma at 1 = π²/6
223
+ 1.644...
224
+
198
225
  See Also
199
226
  --------
200
227
  scipy.special.polygamma : Polygamma function.
@@ -252,6 +279,12 @@ def betaln(a: ArrayLike, b: ArrayLike) -> NDArray[np.floating]:
252
279
  lnB : ndarray
253
280
  Values of ln(B(a, b)).
254
281
 
282
+ Examples
283
+ --------
284
+ >>> import numpy as np
285
+ >>> betaln(100, 100) # More stable than log(beta(100, 100))
286
+ -137.74...
287
+
255
288
  See Also
256
289
  --------
257
290
  scipy.special.betaln : Log of beta function.
@@ -284,6 +317,11 @@ def betainc(a: ArrayLike, b: ArrayLike, x: ArrayLike) -> NDArray[np.floating]:
284
317
  -----
285
318
  This is the CDF of the beta distribution.
286
319
 
320
+ Examples
321
+ --------
322
+ >>> betainc(1, 1, 0.5) # Uniform distribution CDF at 0.5
323
+ 0.5
324
+
287
325
  See Also
288
326
  --------
289
327
  scipy.special.betainc : Regularized incomplete beta function.
@@ -311,6 +349,11 @@ def betaincinv(a: ArrayLike, b: ArrayLike, y: ArrayLike) -> NDArray[np.floating]
311
349
  x : ndarray
312
350
  Values where I_x(a, b) = y.
313
351
 
352
+ Examples
353
+ --------
354
+ >>> betaincinv(1, 1, 0.5) # Median of uniform distribution
355
+ 0.5
356
+
314
357
  See Also
315
358
  --------
316
359
  scipy.special.betaincinv : Inverse of incomplete beta function.
@@ -170,6 +170,11 @@ def wright_omega(z: ArrayLike) -> NDArray[np.complexfloating]:
170
170
 
171
171
  It is entire (analytic everywhere) unlike the Lambert W function.
172
172
 
173
+ Examples
174
+ --------
175
+ >>> wright_omega(0) # Omega constant
176
+ (0.5671...+0j)
177
+
173
178
  References
174
179
  ----------
175
180
  .. [1] Wright, E.M. (1959). "Solution of the equation z*exp(z) = a".
@@ -112,6 +112,11 @@ def marcum_q1(
112
112
  Q : ndarray
113
113
  Values of Q_1(a, b).
114
114
 
115
+ Examples
116
+ --------
117
+ >>> marcum_q1(2, 2)
118
+ 0.735...
119
+
115
120
  See Also
116
121
  --------
117
122
  marcum_q : Generalized Marcum Q function.
@@ -274,6 +279,11 @@ def nuttall_q(
274
279
  -----
275
280
  This is the probability P(X <= b^2) for X ~ chi^2(2, a^2).
276
281
 
282
+ Examples
283
+ --------
284
+ >>> nuttall_q(2, 2) # 1 - Q_1(2, 2)
285
+ 0.264...
286
+
277
287
  See Also
278
288
  --------
279
289
  marcum_q : Marcum Q function.
@@ -322,6 +332,12 @@ def swerling_detection_probability(
322
332
  For Swerling 1:
323
333
  P_d = exp(-threshold / (2 + 2*n*SNR)) * (1 + n*SNR/...)
324
334
 
335
+ Examples
336
+ --------
337
+ >>> pd = swerling_detection_probability(10, 1e-6, n_pulses=10, swerling_case=0)
338
+ >>> pd > 0.9 # High probability of detection with 10 dB SNR
339
+ True
340
+
325
341
  References
326
342
  ----------
327
343
  .. [1] Swerling, P. (1960). "Probability of Detection for Fluctuating
@@ -280,8 +280,17 @@ def geodetic_to_ecef(
280
280
  Examples
281
281
  --------
282
282
  >>> import numpy as np
283
+ >>> # Philadelphia (40°N, 75°W) at 100m altitude
283
284
  >>> lat, lon, alt = np.radians(40.0), np.radians(-75.0), 100.0
284
285
  >>> x, y, z = geodetic_to_ecef(lat, lon, alt)
286
+ >>> x / 1e6 # ~1.2 million meters
287
+ 1.24...
288
+ >>> # Equator at prime meridian
289
+ >>> x, y, z = geodetic_to_ecef(0.0, 0.0, 0.0)
290
+ >>> x # Semi-major axis (equatorial radius)
291
+ 6378137.0
292
+ >>> y, z
293
+ (0.0, 0.0)
285
294
  """
286
295
  lat = np.asarray(lat, dtype=np.float64)
287
296
  lon = np.asarray(lon, dtype=np.float64)
@@ -327,6 +336,19 @@ def ecef_to_geodetic(
327
336
  alt : ndarray
328
337
  Altitude above ellipsoid in meters.
329
338
 
339
+ Examples
340
+ --------
341
+ >>> import numpy as np
342
+ >>> # Point on equator at prime meridian
343
+ >>> lat, lon, alt = ecef_to_geodetic(6378137.0, 0.0, 0.0)
344
+ >>> np.degrees(lat), np.degrees(lon), alt
345
+ (0.0, 0.0, 0.0)
346
+ >>> # Round-trip conversion
347
+ >>> x, y, z = geodetic_to_ecef(np.radians(45.0), np.radians(90.0), 1000.0)
348
+ >>> lat2, lon2, alt2 = ecef_to_geodetic(x, y, z)
349
+ >>> np.degrees(lat2), np.degrees(lon2), alt2
350
+ (45.0..., 90.0..., 1000.0...)
351
+
330
352
  Notes
331
353
  -----
332
354
  Uses Bowring's iterative algorithm for robust conversion.
@@ -409,12 +431,19 @@ def ecef_to_enu(
409
431
 
410
432
  Examples
411
433
  --------
412
- >>> # Reference point
434
+ >>> import numpy as np
435
+ >>> # Reference point: Philadelphia
413
436
  >>> lat_ref, lon_ref, alt_ref = np.radians(40.0), np.radians(-75.0), 0.0
414
- >>> # Target point (1 km east)
437
+ >>> # Target point slightly east
415
438
  >>> lat, lon, alt = np.radians(40.0), np.radians(-74.99), 0.0
416
439
  >>> x, y, z = geodetic_to_ecef(lat, lon, alt)
417
440
  >>> e, n, u = ecef_to_enu(x, y, z, lat_ref, lon_ref, alt_ref)
441
+ >>> e # East displacement in meters
442
+ 850...
443
+ >>> abs(n) < 10 # North displacement should be ~0
444
+ True
445
+ >>> abs(u) < 10 # Up displacement should be ~0
446
+ True
418
447
  """
419
448
  x = np.asarray(x, dtype=np.float64)
420
449
  y = np.asarray(y, dtype=np.float64)
@@ -471,6 +500,18 @@ def enu_to_ecef(
471
500
  -------
472
501
  x, y, z : ndarray
473
502
  ECEF coordinates in meters.
503
+
504
+ Examples
505
+ --------
506
+ >>> import numpy as np
507
+ >>> # Reference point
508
+ >>> lat_ref, lon_ref, alt_ref = np.radians(40.0), np.radians(-75.0), 0.0
509
+ >>> # 1 km east, 500 m north, 100 m up
510
+ >>> x, y, z = enu_to_ecef(1000.0, 500.0, 100.0, lat_ref, lon_ref, alt_ref)
511
+ >>> # Convert back to verify
512
+ >>> e, n, u = ecef_to_enu(x, y, z, lat_ref, lon_ref, alt_ref)
513
+ >>> e, n, u
514
+ (1000.0..., 500.0..., 100.0...)
474
515
  """
475
516
  east = np.asarray(east, dtype=np.float64)
476
517
  north = np.asarray(north, dtype=np.float64)
@@ -522,6 +563,17 @@ def ecef_to_ned(
522
563
  -------
523
564
  north, east, down : ndarray
524
565
  NED coordinates in meters relative to reference point.
566
+
567
+ Examples
568
+ --------
569
+ >>> import numpy as np
570
+ >>> # Reference point
571
+ >>> lat_ref, lon_ref, alt_ref = np.radians(40.0), np.radians(-75.0), 0.0
572
+ >>> # Target above reference
573
+ >>> x, y, z = geodetic_to_ecef(lat_ref, lon_ref, 1000.0) # 1km above
574
+ >>> n, e, d = ecef_to_ned(x, y, z, lat_ref, lon_ref, alt_ref)
575
+ >>> abs(n) < 1, abs(e) < 1, d # Should be ~0, ~0, -1000
576
+ (True, True, -1000.0...)
525
577
  """
526
578
  east, north, up = ecef_to_enu(x, y, z, lat_ref, lon_ref, alt_ref, ellipsoid)
527
579
  return north, east, -up
@@ -556,6 +608,17 @@ def ned_to_ecef(
556
608
  -------
557
609
  x, y, z : ndarray
558
610
  ECEF coordinates in meters.
611
+
612
+ Examples
613
+ --------
614
+ >>> import numpy as np
615
+ >>> lat_ref, lon_ref, alt_ref = np.radians(40.0), np.radians(-75.0), 0.0
616
+ >>> # 100m north, 50m east, 10m down
617
+ >>> x, y, z = ned_to_ecef(100.0, 50.0, 10.0, lat_ref, lon_ref, alt_ref)
618
+ >>> # Verify round-trip
619
+ >>> n, e, d = ecef_to_ned(x, y, z, lat_ref, lon_ref, alt_ref)
620
+ >>> n, e, d
621
+ (100.0..., 50.0..., 10.0...)
559
622
  """
560
623
  return enu_to_ecef(
561
624
  east, north, -np.asarray(down), lat_ref, lon_ref, alt_ref, ellipsoid
@@ -597,6 +660,17 @@ def direct_geodetic(
597
660
  azimuth2 : float
598
661
  Back azimuth at destination in radians.
599
662
 
663
+ Examples
664
+ --------
665
+ >>> import numpy as np
666
+ >>> # From New York, travel 1000 km northeast
667
+ >>> lat1, lon1 = np.radians(40.7), np.radians(-74.0)
668
+ >>> azimuth = np.radians(45) # Northeast
669
+ >>> distance = 1_000_000 # 1000 km
670
+ >>> lat2, lon2, az2 = direct_geodetic(lat1, lon1, azimuth, distance)
671
+ >>> np.degrees(lat2), np.degrees(lon2) # Destination
672
+ (47.0..., -62.6...)
673
+
600
674
  References
601
675
  ----------
602
676
  .. [1] Vincenty, T., "Direct and Inverse Solutions of Geodesics on the
@@ -647,6 +721,18 @@ def inverse_geodetic(
647
721
  azimuth2 : float
648
722
  Back azimuth at destination in radians.
649
723
 
724
+ Examples
725
+ --------
726
+ >>> import numpy as np
727
+ >>> # Distance from New York to London
728
+ >>> lat1, lon1 = np.radians(40.7128), np.radians(-74.0060) # NYC
729
+ >>> lat2, lon2 = np.radians(51.5074), np.radians(-0.1278) # London
730
+ >>> dist, az1, az2 = inverse_geodetic(lat1, lon1, lat2, lon2)
731
+ >>> dist / 1000 # Distance in km
732
+ 5570...
733
+ >>> np.degrees(az1) # Initial heading from NYC
734
+ 51.2...
735
+
650
736
  Notes
651
737
  -----
652
738
  May fail to converge for nearly antipodal points.
@@ -690,6 +776,19 @@ def haversine_distance(
690
776
  float
691
777
  Great-circle distance in meters.
692
778
 
779
+ Examples
780
+ --------
781
+ >>> import numpy as np
782
+ >>> # Distance from equator to 45°N along prime meridian
783
+ >>> lat1, lon1 = 0.0, 0.0
784
+ >>> lat2, lon2 = np.radians(45.0), 0.0
785
+ >>> dist = haversine_distance(lat1, lon1, lat2, lon2)
786
+ >>> dist / 1000 # ~5000 km
787
+ 5003...
788
+ >>> # Same point -> 0 distance
789
+ >>> haversine_distance(0.0, 0.0, 0.0, 0.0)
790
+ 0.0
791
+
693
792
  Notes
694
793
  -----
695
794
  This is a spherical approximation. For higher accuracy on an ellipsoid,
@@ -260,6 +260,14 @@ def great_circle_inverse(
260
260
  -------
261
261
  GreatCircleResult
262
262
  Distance and azimuths.
263
+
264
+ Examples
265
+ --------
266
+ >>> lat1, lon1 = np.radians(40.7128), np.radians(-74.0060) # NYC
267
+ >>> lat2, lon2 = np.radians(51.5074), np.radians(-0.1278) # London
268
+ >>> result = great_circle_inverse(lat1, lon1, lat2, lon2)
269
+ >>> result.distance > 5000000 # Over 5000 km
270
+ True
263
271
  """
264
272
  distance = great_circle_distance(lat1, lon1, lat2, lon2, radius)
265
273
  azimuth1 = great_circle_azimuth(lat1, lon1, lat2, lon2)
@@ -346,6 +354,14 @@ def great_circle_waypoints(
346
354
  -------
347
355
  lats, lons : ndarray
348
356
  Arrays of waypoint latitudes and longitudes in radians.
357
+
358
+ Examples
359
+ --------
360
+ >>> lat1, lon1 = 0.0, 0.0
361
+ >>> lat2, lon2 = np.pi/4, np.pi/4
362
+ >>> lats, lons = great_circle_waypoints(lat1, lon1, lat2, lon2, 5)
363
+ >>> len(lats)
364
+ 5
349
365
  """
350
366
  fractions = np.linspace(0, 1, n_points)
351
367
  lats = np.zeros(n_points)
@@ -448,6 +464,16 @@ def cross_track_distance(
448
464
  -----
449
465
  Positive cross-track means the point is to the right of the path
450
466
  (when traveling from start to end).
467
+
468
+ Examples
469
+ --------
470
+ >>> # Point near a path from origin to northeast
471
+ >>> lat_pt, lon_pt = np.radians(5), np.radians(2)
472
+ >>> lat1, lon1 = 0.0, 0.0
473
+ >>> lat2, lon2 = np.radians(10), np.radians(10)
474
+ >>> result = cross_track_distance(lat_pt, lon_pt, lat1, lon1, lat2, lon2)
475
+ >>> abs(result.cross_track) < 500000 # Within 500 km
476
+ True
451
477
  """
452
478
  # Angular distance from start to point
453
479
  d13 = great_circle_distance(lat1, lon1, lat_point, lon_point, radius=1.0)
@@ -502,6 +528,16 @@ def great_circle_intersect(
502
528
  Great circles always intersect at two antipodal points (unless they
503
529
  are identical or parallel). The returned points are the intersections
504
530
  closest to the given points.
531
+
532
+ Examples
533
+ --------
534
+ >>> lat1, lon1 = 0.0, 0.0
535
+ >>> az1 = np.radians(45) # Northeast
536
+ >>> lat2, lon2 = 0.0, np.radians(10)
537
+ >>> az2 = np.radians(315) # Northwest
538
+ >>> result = great_circle_intersect(lat1, lon1, az1, lat2, lon2, az2)
539
+ >>> result.valid
540
+ True
505
541
  """
506
542
 
507
543
  # Convert to Cartesian unit vectors
@@ -595,6 +631,16 @@ def great_circle_path_intersect(
595
631
  -------
596
632
  IntersectionResult
597
633
  Intersection points and validity.
634
+
635
+ Examples
636
+ --------
637
+ >>> # Two crossing paths
638
+ >>> result = great_circle_path_intersect(
639
+ ... 0.0, 0.0, np.radians(10), np.radians(10), # Path A
640
+ ... 0.0, np.radians(10), np.radians(10), 0.0 # Path B
641
+ ... )
642
+ >>> result.valid
643
+ True
598
644
  """
599
645
  # Get bearings from start points
600
646
  az1 = great_circle_azimuth(lat1a, lon1a, lat2a, lon2a)
@@ -783,6 +829,22 @@ def angular_distance(
783
829
  -------
784
830
  float
785
831
  Angular distance in radians.
832
+
833
+ Examples
834
+ --------
835
+ Compute angular distance between New York and London:
836
+
837
+ >>> import numpy as np
838
+ >>> # NYC: 40.7°N, 74.0°W; London: 51.5°N, 0.1°W
839
+ >>> lat1, lon1 = np.radians(40.7), np.radians(-74.0)
840
+ >>> lat2, lon2 = np.radians(51.5), np.radians(-0.1)
841
+ >>> angle = angular_distance(lat1, lon1, lat2, lon2)
842
+ >>> np.degrees(angle) # about 50 degrees
843
+ 49.9...
844
+
845
+ See Also
846
+ --------
847
+ great_circle_distance : Compute distance on sphere with given radius.
786
848
  """
787
849
  return great_circle_distance(lat1, lon1, lat2, lon2, radius=1.0)
788
850
 
@@ -809,6 +871,15 @@ def destination_point(
809
871
  -------
810
872
  WaypointResult
811
873
  Destination coordinates.
874
+
875
+ Examples
876
+ --------
877
+ >>> lat, lon = 0.0, 0.0
878
+ >>> bearing = np.radians(90) # Due East
879
+ >>> ang_dist = np.radians(10) # 10 degrees
880
+ >>> dest = destination_point(lat, lon, bearing, ang_dist)
881
+ >>> np.degrees(dest.lon) # Should be ~10 degrees East
882
+ 10.0
812
883
  """
813
884
  lat2 = np.arcsin(
814
885
  np.sin(lat) * np.cos(angular_distance)
pytcl/navigation/rhumb.py CHANGED
@@ -263,6 +263,15 @@ def indirect_rhumb_spherical(
263
263
  -------
264
264
  RhumbResult
265
265
  Distance and constant bearing.
266
+
267
+ Examples
268
+ --------
269
+ >>> import numpy as np
270
+ >>> lat1, lon1 = np.radians(40), np.radians(-74) # New York
271
+ >>> lat2, lon2 = np.radians(51), np.radians(0) # London
272
+ >>> result = indirect_rhumb_spherical(lat1, lon1, lat2, lon2)
273
+ >>> result.distance > 5000000 # Over 5000 km
274
+ True
266
275
  """
267
276
  distance = rhumb_distance_spherical(lat1, lon1, lat2, lon2, radius)
268
277
  bearing = rhumb_bearing(lat1, lon1, lat2, lon2)
@@ -354,6 +363,15 @@ def rhumb_distance_ellipsoidal(
354
363
  -------
355
364
  float
356
365
  Rhumb line distance in meters.
366
+
367
+ Examples
368
+ --------
369
+ >>> import numpy as np
370
+ >>> lat1, lon1 = np.radians(40), np.radians(-74)
371
+ >>> lat2, lon2 = np.radians(51), np.radians(0)
372
+ >>> dist = rhumb_distance_ellipsoidal(lat1, lon1, lat2, lon2)
373
+ >>> dist > 5000000 # Over 5000 km
374
+ True
357
375
  """
358
376
  a = ellipsoid.a
359
377
  e2 = ellipsoid.e2
@@ -419,6 +437,15 @@ def indirect_rhumb(
419
437
  -------
420
438
  RhumbResult
421
439
  Distance and constant bearing.
440
+
441
+ Examples
442
+ --------
443
+ >>> import numpy as np
444
+ >>> lat1, lon1 = np.radians(40), np.radians(-74) # New York
445
+ >>> lat2, lon2 = np.radians(51), np.radians(0) # London
446
+ >>> result = indirect_rhumb(lat1, lon1, lat2, lon2)
447
+ >>> 0 < result.bearing < np.pi # Eastward bearing
448
+ True
422
449
  """
423
450
  distance = rhumb_distance_ellipsoidal(lat1, lon1, lat2, lon2, ellipsoid)
424
451
 
@@ -460,6 +487,15 @@ def direct_rhumb(
460
487
  -------
461
488
  RhumbDirectResult
462
489
  Destination latitude and longitude.
490
+
491
+ Examples
492
+ --------
493
+ >>> import numpy as np
494
+ >>> lat, lon = np.radians(40), np.radians(-74)
495
+ >>> bearing = np.radians(90) # Due east
496
+ >>> dest = direct_rhumb(lat, lon, bearing, 100000) # 100 km
497
+ >>> np.degrees(dest.lon) > -74 # Moved east
498
+ True
463
499
  """
464
500
  a = ellipsoid.a
465
501
  e2 = ellipsoid.e2
@@ -534,6 +570,17 @@ def rhumb_intersect(
534
570
  RhumbIntersectionResult
535
571
  Intersection point and validity flag.
536
572
 
573
+ Examples
574
+ --------
575
+ >>> import numpy as np
576
+ >>> lat1, lon1 = np.radians(40), np.radians(-74)
577
+ >>> lat2, lon2 = np.radians(51), np.radians(0)
578
+ >>> bearing1 = np.radians(45)
579
+ >>> bearing2 = np.radians(270)
580
+ >>> result = rhumb_intersect(lat1, lon1, bearing1, lat2, lon2, bearing2)
581
+ >>> result.valid # May or may not intersect
582
+ True
583
+
537
584
  Notes
538
585
  -----
539
586
  Unlike great circles, two rhumb lines may not intersect (if bearings
@@ -612,6 +659,15 @@ def rhumb_midpoint(
612
659
  -------
613
660
  RhumbDirectResult
614
661
  Midpoint latitude and longitude.
662
+
663
+ Examples
664
+ --------
665
+ >>> import numpy as np
666
+ >>> lat1, lon1 = np.radians(0), np.radians(0)
667
+ >>> lat2, lon2 = np.radians(10), np.radians(10)
668
+ >>> mid = rhumb_midpoint(lat1, lon1, lat2, lon2)
669
+ >>> np.isclose(np.degrees(mid.lat), 5, atol=0.1)
670
+ True
615
671
  """
616
672
  result = indirect_rhumb_spherical(lat1, lon1, lat2, lon2)
617
673
  return direct_rhumb_spherical(lat1, lon1, result.bearing, result.distance / 2)
@@ -643,6 +699,15 @@ def rhumb_waypoints(
643
699
  -------
644
700
  lats, lons : ndarray
645
701
  Arrays of waypoint latitudes and longitudes in radians.
702
+
703
+ Examples
704
+ --------
705
+ >>> import numpy as np
706
+ >>> lat1, lon1 = np.radians(40), np.radians(-74)
707
+ >>> lat2, lon2 = np.radians(51), np.radians(0)
708
+ >>> lats, lons = rhumb_waypoints(lat1, lon1, lat2, lon2, 5)
709
+ >>> len(lats)
710
+ 5
646
711
  """
647
712
  result = indirect_rhumb_spherical(lat1, lon1, lat2, lon2, radius)
648
713
 
@@ -685,6 +750,15 @@ def compare_great_circle_rhumb(
685
750
  Rhumb line distance in meters.
686
751
  difference_percent : float
687
752
  Percentage difference (rhumb is longer).
753
+
754
+ Examples
755
+ --------
756
+ >>> import numpy as np
757
+ >>> lat1, lon1 = np.radians(40), np.radians(-74) # NYC
758
+ >>> lat2, lon2 = np.radians(51), np.radians(0) # London
759
+ >>> gc, rhumb, diff = compare_great_circle_rhumb(lat1, lon1, lat2, lon2)
760
+ >>> rhumb > gc # Rhumb is always longer
761
+ True
688
762
  """
689
763
  from pytcl.navigation.great_circle import great_circle_distance
690
764