nrl-tracker 1.9.1__py3-none-any.whl → 1.9.2__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 (60) hide show
  1. {nrl_tracker-1.9.1.dist-info → nrl_tracker-1.9.2.dist-info}/METADATA +4 -4
  2. {nrl_tracker-1.9.1.dist-info → nrl_tracker-1.9.2.dist-info}/RECORD +60 -59
  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/dynamic_estimation/gaussian_sum_filter.py +55 -0
  22. pytcl/dynamic_estimation/imm.py +29 -0
  23. pytcl/dynamic_estimation/information_filter.py +64 -0
  24. pytcl/dynamic_estimation/kalman/extended.py +56 -0
  25. pytcl/dynamic_estimation/kalman/linear.py +69 -0
  26. pytcl/dynamic_estimation/kalman/unscented.py +81 -0
  27. pytcl/dynamic_estimation/particle_filters/bootstrap.py +146 -0
  28. pytcl/dynamic_estimation/rbpf.py +51 -0
  29. pytcl/dynamic_estimation/smoothers.py +58 -0
  30. pytcl/dynamic_models/continuous_time/dynamics.py +104 -0
  31. pytcl/dynamic_models/discrete_time/coordinated_turn.py +6 -0
  32. pytcl/dynamic_models/discrete_time/singer.py +12 -0
  33. pytcl/dynamic_models/process_noise/coordinated_turn.py +46 -0
  34. pytcl/dynamic_models/process_noise/polynomial.py +6 -0
  35. pytcl/dynamic_models/process_noise/singer.py +52 -0
  36. pytcl/gravity/clenshaw.py +60 -0
  37. pytcl/gravity/egm.py +47 -0
  38. pytcl/gravity/models.py +34 -0
  39. pytcl/gravity/spherical_harmonics.py +73 -0
  40. pytcl/gravity/tides.py +34 -0
  41. pytcl/mathematical_functions/numerical_integration/quadrature.py +85 -0
  42. pytcl/mathematical_functions/special_functions/bessel.py +55 -0
  43. pytcl/mathematical_functions/special_functions/elliptic.py +42 -0
  44. pytcl/mathematical_functions/special_functions/error_functions.py +49 -0
  45. pytcl/mathematical_functions/special_functions/gamma_functions.py +43 -0
  46. pytcl/mathematical_functions/special_functions/lambert_w.py +5 -0
  47. pytcl/mathematical_functions/special_functions/marcum_q.py +16 -0
  48. pytcl/navigation/geodesy.py +101 -2
  49. pytcl/navigation/great_circle.py +71 -0
  50. pytcl/navigation/rhumb.py +74 -0
  51. pytcl/performance_evaluation/estimation_metrics.py +70 -0
  52. pytcl/performance_evaluation/track_metrics.py +30 -0
  53. pytcl/static_estimation/maximum_likelihood.py +54 -0
  54. pytcl/static_estimation/robust.py +57 -0
  55. pytcl/terrain/dem.py +69 -0
  56. pytcl/terrain/visibility.py +65 -0
  57. pytcl/trackers/hypothesis.py +65 -0
  58. {nrl_tracker-1.9.1.dist-info → nrl_tracker-1.9.2.dist-info}/LICENSE +0 -0
  59. {nrl_tracker-1.9.1.dist-info → nrl_tracker-1.9.2.dist-info}/WHEEL +0 -0
  60. {nrl_tracker-1.9.1.dist-info → nrl_tracker-1.9.2.dist-info}/top_level.txt +0 -0
@@ -157,6 +157,12 @@ def mean_to_hyperbolic_anomaly(
157
157
  -------
158
158
  H : float
159
159
  Hyperbolic anomaly (radians).
160
+
161
+ Examples
162
+ --------
163
+ >>> H = mean_to_hyperbolic_anomaly(1.0, 1.5)
164
+ >>> abs(1.5 * np.sinh(H) - H - 1.0) < 1e-10
165
+ True
160
166
  """
161
167
  if e <= 1:
162
168
  raise ValueError(f"Eccentricity must be > 1 for hyperbolic orbits, got {e}")
@@ -196,6 +202,12 @@ def eccentric_to_true_anomaly(E: float, e: float) -> float:
196
202
  -------
197
203
  nu : float
198
204
  True anomaly (radians), in [0, 2*pi).
205
+
206
+ Examples
207
+ --------
208
+ >>> nu = eccentric_to_true_anomaly(np.pi/4, 0.5)
209
+ >>> 0 <= nu < 2 * np.pi
210
+ True
199
211
  """
200
212
  # Use half-angle formula for numerical stability
201
213
  nu = 2 * np.arctan2(np.sqrt(1 + e) * np.sin(E / 2), np.sqrt(1 - e) * np.cos(E / 2))
@@ -217,6 +229,12 @@ def true_to_eccentric_anomaly(nu: float, e: float) -> float:
217
229
  -------
218
230
  E : float
219
231
  Eccentric anomaly (radians), in [0, 2*pi).
232
+
233
+ Examples
234
+ --------
235
+ >>> E = true_to_eccentric_anomaly(np.pi/3, 0.5)
236
+ >>> 0 <= E < 2 * np.pi
237
+ True
220
238
  """
221
239
  E = 2 * np.arctan2(np.sqrt(1 - e) * np.sin(nu / 2), np.sqrt(1 + e) * np.cos(nu / 2))
222
240
  return E % (2 * np.pi)
@@ -237,6 +255,12 @@ def hyperbolic_to_true_anomaly(H: float, e: float) -> float:
237
255
  -------
238
256
  nu : float
239
257
  True anomaly (radians).
258
+
259
+ Examples
260
+ --------
261
+ >>> nu = hyperbolic_to_true_anomaly(0.5, 1.5)
262
+ >>> isinstance(nu, float)
263
+ True
240
264
  """
241
265
  nu = 2 * np.arctan(np.sqrt((e + 1) / (e - 1)) * np.tanh(H / 2))
242
266
  return nu
@@ -277,6 +301,12 @@ def eccentric_to_mean_anomaly(E: float, e: float) -> float:
277
301
  -------
278
302
  M : float
279
303
  Mean anomaly (radians).
304
+
305
+ Examples
306
+ --------
307
+ >>> M = eccentric_to_mean_anomaly(np.pi/4, 0.5)
308
+ >>> 0 <= M < 2 * np.pi
309
+ True
280
310
  """
281
311
  M = E - e * np.sin(E)
282
312
  return M % (2 * np.pi)
@@ -297,6 +327,12 @@ def mean_to_true_anomaly(M: float, e: float) -> float:
297
327
  -------
298
328
  nu : float
299
329
  True anomaly (radians).
330
+
331
+ Examples
332
+ --------
333
+ >>> nu = mean_to_true_anomaly(np.pi/4, 0.1)
334
+ >>> 0 <= nu < 2 * np.pi
335
+ True
300
336
  """
301
337
  if e < 1:
302
338
  E = mean_to_eccentric_anomaly(M, e)
@@ -514,6 +550,12 @@ def orbital_period(a: float, mu: float = GM_EARTH) -> float:
514
550
  -------
515
551
  T : float
516
552
  Orbital period (seconds).
553
+
554
+ Examples
555
+ --------
556
+ >>> T = orbital_period(7000) # LEO satellite
557
+ >>> T / 60 # Convert to minutes # doctest: +SKIP
558
+ 97.8...
517
559
  """
518
560
  if a <= 0:
519
561
  raise ValueError("Semi-major axis must be positive for elliptic orbits")
@@ -535,6 +577,13 @@ def mean_motion(a: float, mu: float = GM_EARTH) -> float:
535
577
  -------
536
578
  n : float
537
579
  Mean motion (radians/second).
580
+
581
+ Examples
582
+ --------
583
+ >>> n = mean_motion(42164) # GEO orbit
584
+ >>> revs_per_day = n * 86400 / (2 * np.pi)
585
+ >>> abs(revs_per_day - 1.0) < 0.01 # Approximately 1 rev/day
586
+ True
538
587
  """
539
588
  return np.sqrt(mu / abs(a) ** 3)
540
589
 
@@ -607,6 +656,15 @@ def kepler_propagate_state(
607
656
  -------
608
657
  new_state : StateVector
609
658
  Propagated state vector.
659
+
660
+ Examples
661
+ --------
662
+ >>> r = np.array([7000.0, 0.0, 0.0])
663
+ >>> v = np.array([0.0, 7.5, 0.0])
664
+ >>> state = StateVector(r=r, v=v)
665
+ >>> new_state = kepler_propagate_state(state, 3600)
666
+ >>> np.linalg.norm(new_state.r) > 0
667
+ True
610
668
  """
611
669
  elements = state_to_orbital_elements(state, mu)
612
670
  new_elements = kepler_propagate(elements, dt, mu)
@@ -630,6 +688,12 @@ def vis_viva(r: float, a: float, mu: float = GM_EARTH) -> float:
630
688
  -------
631
689
  v : float
632
690
  Orbital velocity (km/s).
691
+
692
+ Examples
693
+ --------
694
+ >>> v = vis_viva(7000, 7000) # Circular orbit
695
+ >>> abs(v - circular_velocity(7000)) < 0.01
696
+ True
633
697
  """
634
698
  return np.sqrt(mu * (2 / r - 1 / a))
635
699
 
@@ -649,6 +713,15 @@ def specific_angular_momentum(
649
713
  -------
650
714
  h : ndarray
651
715
  Specific angular momentum vector (km^2/s).
716
+
717
+ Examples
718
+ --------
719
+ >>> r = np.array([7000.0, 0.0, 0.0])
720
+ >>> v = np.array([0.0, 7.5, 0.0])
721
+ >>> state = StateVector(r=r, v=v)
722
+ >>> h = specific_angular_momentum(state)
723
+ >>> h[2] # Angular momentum in z-direction
724
+ 52500.0
652
725
  """
653
726
  return np.cross(state.r, state.v)
654
727
 
@@ -672,6 +745,15 @@ def specific_orbital_energy(
672
745
  energy : float
673
746
  Specific orbital energy (km^2/s^2).
674
747
  Negative for bound orbits, positive for escape trajectories.
748
+
749
+ Examples
750
+ --------
751
+ >>> r = np.array([7000.0, 0.0, 0.0])
752
+ >>> v = np.array([0.0, 7.5, 0.0])
753
+ >>> state = StateVector(r=r, v=v)
754
+ >>> energy = specific_orbital_energy(state)
755
+ >>> energy < 0 # Bound orbit
756
+ True
675
757
  """
676
758
  r_mag = np.linalg.norm(state.r)
677
759
  v_mag = np.linalg.norm(state.v)
@@ -692,6 +774,15 @@ def flight_path_angle(state: StateVector) -> float:
692
774
  gamma : float
693
775
  Flight path angle (radians).
694
776
  Positive when climbing, negative when descending.
777
+
778
+ Examples
779
+ --------
780
+ >>> r = np.array([7000.0, 0.0, 0.0])
781
+ >>> v = np.array([0.0, 7.5, 0.0]) # Tangential velocity
782
+ >>> state = StateVector(r=r, v=v)
783
+ >>> gamma = flight_path_angle(state)
784
+ >>> abs(gamma) < 0.01 # Nearly zero for circular motion
785
+ True
695
786
  """
696
787
  r = np.asarray(state.r)
697
788
  v = np.asarray(state.v)
@@ -720,6 +811,12 @@ def periapsis_radius(a: float, e: float) -> float:
720
811
  -------
721
812
  r_p : float
722
813
  Periapsis radius (km).
814
+
815
+ Examples
816
+ --------
817
+ >>> r_p = periapsis_radius(10000, 0.3)
818
+ >>> r_p
819
+ 7000.0
723
820
  """
724
821
  return a * (1 - e)
725
822
 
@@ -739,6 +836,12 @@ def apoapsis_radius(a: float, e: float) -> float:
739
836
  -------
740
837
  r_a : float
741
838
  Apoapsis radius (km). Infinite for parabolic/hyperbolic orbits.
839
+
840
+ Examples
841
+ --------
842
+ >>> r_a = apoapsis_radius(10000, 0.3)
843
+ >>> r_a
844
+ 13000.0
742
845
  """
743
846
  if e >= 1:
744
847
  return np.inf
@@ -769,6 +872,13 @@ def time_since_periapsis(
769
872
  -------
770
873
  t : float
771
874
  Time since periapsis (seconds).
875
+
876
+ Examples
877
+ --------
878
+ >>> t = time_since_periapsis(np.pi, 7000, 0.1) # At apoapsis
879
+ >>> T = orbital_period(7000)
880
+ >>> abs(t - T/2) < 1 # Approximately half the period
881
+ True
772
882
  """
773
883
  M = true_to_mean_anomaly(nu, e)
774
884
  n = mean_motion(a, mu)
@@ -792,6 +902,15 @@ def orbit_radius(nu: float, a: float, e: float) -> float:
792
902
  -------
793
903
  r : float
794
904
  Orbital radius (km).
905
+
906
+ Examples
907
+ --------
908
+ >>> r = orbit_radius(0, 10000, 0.3) # At periapsis
909
+ >>> r
910
+ 7000.0
911
+ >>> r = orbit_radius(np.pi, 10000, 0.3) # At apoapsis
912
+ >>> r
913
+ 13000.0
795
914
  """
796
915
  p = a * (1 - e * e)
797
916
  return p / (1 + e * np.cos(nu))
@@ -812,6 +931,12 @@ def escape_velocity(r: float, mu: float = GM_EARTH) -> float:
812
931
  -------
813
932
  v_esc : float
814
933
  Escape velocity (km/s).
934
+
935
+ Examples
936
+ --------
937
+ >>> v_esc = escape_velocity(6378 + 400) # At ISS altitude
938
+ >>> 10 < v_esc < 12 # About 11 km/s
939
+ True
815
940
  """
816
941
  return np.sqrt(2 * mu / r)
817
942
 
@@ -831,6 +956,12 @@ def circular_velocity(r: float, mu: float = GM_EARTH) -> float:
831
956
  -------
832
957
  v_circ : float
833
958
  Circular velocity (km/s).
959
+
960
+ Examples
961
+ --------
962
+ >>> v_circ = circular_velocity(6378 + 400) # At ISS altitude
963
+ >>> 7 < v_circ < 8 # About 7.7 km/s
964
+ True
834
965
  """
835
966
  return np.sqrt(mu / r)
836
967
 
@@ -222,6 +222,15 @@ def dual_frequency_tec(
222
222
  tec : ndarray
223
223
  Total Electron Content in TECU (10^16 electrons/m²).
224
224
 
225
+ Examples
226
+ --------
227
+ >>> # Pseudorange measurements from dual-frequency receiver
228
+ >>> p_l1 = 22000000.0 # L1 pseudorange in meters
229
+ >>> p_l2 = 22000002.5 # L2 pseudorange (slightly delayed)
230
+ >>> tec = dual_frequency_tec(p_l1, p_l2)
231
+ >>> tec > 0 # TEC should be positive
232
+ True
233
+
225
234
  Notes
226
235
  -----
227
236
  The ionospheric delay is proportional to TEC and inversely
@@ -271,6 +280,17 @@ def ionospheric_delay_from_tec(
271
280
  delay : ndarray
272
281
  Ionospheric delay in meters.
273
282
 
283
+ Examples
284
+ --------
285
+ >>> # Typical mid-latitude TEC of 20 TECU
286
+ >>> delay = ionospheric_delay_from_tec(20.0)
287
+ >>> delay > 0
288
+ True
289
+ >>> # Delay at L2 is larger than at L1 (lower frequency)
290
+ >>> delay_l2 = ionospheric_delay_from_tec(20.0, frequency=F_L2)
291
+ >>> delay_l2 > delay
292
+ True
293
+
274
294
  Notes
275
295
  -----
276
296
  The ionospheric delay for a signal is:
@@ -410,6 +430,18 @@ def magnetic_latitude(
410
430
  -------
411
431
  mag_lat : ndarray
412
432
  Geomagnetic latitude in radians.
433
+
434
+ Examples
435
+ --------
436
+ >>> import numpy as np
437
+ >>> # New York City (40.7°N, 74°W)
438
+ >>> mag_lat = magnetic_latitude(np.radians(40.7), np.radians(-74))
439
+ >>> np.degrees(mag_lat) # doctest: +ELLIPSIS
440
+ 51.4...
441
+ >>> # Equator at 0° longitude
442
+ >>> mag_lat_eq = magnetic_latitude(0.0, 0.0)
443
+ >>> np.abs(np.degrees(mag_lat_eq)) < 15 # Near magnetic equator
444
+ True
413
445
  """
414
446
  latitude = np.asarray(latitude, dtype=np.float64)
415
447
  longitude = np.asarray(longitude, dtype=np.float64)
@@ -457,6 +489,18 @@ def scintillation_index(
457
489
  s4 : ndarray
458
490
  S4 amplitude scintillation index (0-1).
459
491
 
492
+ Examples
493
+ --------
494
+ >>> import numpy as np
495
+ >>> # Equatorial region at night (high scintillation risk)
496
+ >>> s4 = scintillation_index(np.radians(10), 21, kp_index=5.0)
497
+ >>> s4 > 0.3 # Moderate to strong scintillation
498
+ True
499
+ >>> # Mid-latitude during daytime (low scintillation)
500
+ >>> s4_low = scintillation_index(np.radians(45), 12, kp_index=1.0)
501
+ >>> s4_low < 0.2
502
+ True
503
+
460
504
  Notes
461
505
  -----
462
506
  S4 > 0.3 indicates moderate scintillation.
@@ -260,6 +260,16 @@ def altitude_from_pressure(
260
260
  altitude : ndarray
261
261
  Geometric altitude in meters.
262
262
 
263
+ Examples
264
+ --------
265
+ >>> # Sea level pressure
266
+ >>> altitude_from_pressure(101325)
267
+ 0.0
268
+ >>> # Pressure at approximately 5000m
269
+ >>> alt = altitude_from_pressure(54000)
270
+ >>> 4800 < alt < 5200
271
+ True
272
+
263
273
  Notes
264
274
  -----
265
275
  This is an approximate inversion of the ISA model, valid primarily
@@ -292,6 +302,15 @@ def mach_number(
292
302
  -------
293
303
  mach : ndarray
294
304
  Mach number.
305
+
306
+ Examples
307
+ --------
308
+ >>> # Aircraft at 300 m/s at sea level
309
+ >>> mach_number(300, 0) # doctest: +ELLIPSIS
310
+ 0.88...
311
+ >>> # Same speed at 10 km altitude (lower speed of sound)
312
+ >>> mach_number(300, 10000) # doctest: +ELLIPSIS
313
+ 1.00...
295
314
  """
296
315
  velocity = np.asarray(velocity, dtype=np.float64)
297
316
  altitude = np.asarray(altitude, dtype=np.float64)
@@ -318,6 +337,16 @@ def true_airspeed_from_mach(
318
337
  -------
319
338
  velocity : ndarray
320
339
  True airspeed in m/s.
340
+
341
+ Examples
342
+ --------
343
+ >>> # Mach 0.8 at cruise altitude (10 km)
344
+ >>> tas = true_airspeed_from_mach(0.8, 10000)
345
+ >>> 230 < tas < 250 # approximately 240 m/s
346
+ True
347
+ >>> # Supersonic at sea level
348
+ >>> true_airspeed_from_mach(1.0, 0) # doctest: +ELLIPSIS
349
+ 340.2...
321
350
  """
322
351
  mach = np.asarray(mach, dtype=np.float64)
323
352
  altitude = np.asarray(altitude, dtype=np.float64)
@@ -76,6 +76,15 @@ def compute_neighbors(
76
76
  -------
77
77
  neighbors : list of ndarray
78
78
  neighbors[i] contains indices of points within eps of point i.
79
+
80
+ Examples
81
+ --------
82
+ >>> X = np.array([[0.0, 0.0], [0.5, 0.0], [3.0, 0.0]])
83
+ >>> neighbors = compute_neighbors(X, eps=1.0)
84
+ >>> 0 in neighbors[1] and 1 in neighbors[0] # Points 0 and 1 are neighbors
85
+ True
86
+ >>> 2 in neighbors[0] # Point 2 is far from point 0
87
+ False
79
88
  """
80
89
  n_samples = X.shape[0]
81
90
 
@@ -168,6 +168,16 @@ def runnalls_merge_cost(
168
168
  cost = 0.5 * [w_m * log|P_m| - w_1 * log|P_1| - w_2 * log|P_2|]
169
169
 
170
170
  where w_m = w_1 + w_2, P_m is the moment-matched covariance.
171
+
172
+ Examples
173
+ --------
174
+ >>> c1 = GaussianComponent(0.3, np.array([0., 0.]), np.eye(2) * 0.1)
175
+ >>> c2 = GaussianComponent(0.2, np.array([0.1, 0.]), np.eye(2) * 0.1) # Close
176
+ >>> c3 = GaussianComponent(0.2, np.array([5., 5.]), np.eye(2) * 0.1) # Far
177
+ >>> cost_close = runnalls_merge_cost(c1, c2)
178
+ >>> cost_far = runnalls_merge_cost(c1, c3)
179
+ >>> cost_close < cost_far # Closer components have lower merge cost
180
+ True
171
181
  """
172
182
  w1, w2 = c1.weight, c2.weight
173
183
  w_merged = w1 + w2
@@ -420,6 +430,16 @@ def west_merge_cost(
420
430
  -------
421
431
  cost : float
422
432
  Merge cost based on weighted mean separation.
433
+
434
+ Examples
435
+ --------
436
+ >>> c1 = GaussianComponent(0.3, np.array([0., 0.]), np.eye(2) * 0.1)
437
+ >>> c2 = GaussianComponent(0.2, np.array([0.1, 0.]), np.eye(2) * 0.1) # Close
438
+ >>> c3 = GaussianComponent(0.2, np.array([5., 5.]), np.eye(2) * 0.1) # Far
439
+ >>> cost_close = west_merge_cost(c1, c2)
440
+ >>> cost_far = west_merge_cost(c1, c3)
441
+ >>> cost_close < cost_far # Closer components have lower merge cost
442
+ True
423
443
  """
424
444
  w1, w2 = c1.weight, c2.weight
425
445
  w_merged = w1 + w2
@@ -106,6 +106,15 @@ def compute_distance_matrix(
106
106
  -------
107
107
  distances : ndarray
108
108
  Distance matrix, shape (n_samples, n_samples).
109
+
110
+ Examples
111
+ --------
112
+ >>> X = np.array([[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]])
113
+ >>> D = compute_distance_matrix(X)
114
+ >>> D.shape
115
+ (3, 3)
116
+ >>> D[0, 1] # Distance between points 0 and 1
117
+ 1.0
109
118
  """
110
119
  X = np.asarray(X, dtype=np.float64)
111
120
  return _compute_distance_matrix_jit(X)
@@ -402,6 +411,16 @@ def cut_dendrogram(
402
411
  -------
403
412
  labels : ndarray
404
413
  Cluster labels, shape (n_samples,).
414
+
415
+ Examples
416
+ --------
417
+ >>> import numpy as np
418
+ >>> rng = np.random.default_rng(42)
419
+ >>> X = np.vstack([rng.normal(0, 0.5, (10, 2)), rng.normal(3, 0.5, (10, 2))])
420
+ >>> result = agglomerative_clustering(X)
421
+ >>> labels = cut_dendrogram(result.linkage_matrix, n_samples=20, n_clusters=2)
422
+ >>> len(np.unique(labels))
423
+ 2
405
424
  """
406
425
  linkage_matrix = np.asarray(linkage_matrix)
407
426
 
@@ -472,6 +491,16 @@ def fcluster(
472
491
  -------
473
492
  labels : ndarray
474
493
  Cluster labels (1-indexed for scipy compatibility).
494
+
495
+ Examples
496
+ --------
497
+ >>> import numpy as np
498
+ >>> rng = np.random.default_rng(42)
499
+ >>> X = np.vstack([rng.normal(0, 0.5, (10, 2)), rng.normal(3, 0.5, (10, 2))])
500
+ >>> result = agglomerative_clustering(X)
501
+ >>> labels = fcluster(result.linkage_matrix, n_samples=20, t=2, criterion='maxclust')
502
+ >>> labels.min() # 1-indexed
503
+ 1
475
504
  """
476
505
  if criterion == "distance":
477
506
  labels = cut_dendrogram(linkage_matrix, n_samples, distance_threshold=t)
@@ -171,6 +171,15 @@ def update_centers(
171
171
  centers : ndarray
172
172
  Updated cluster centers, shape (n_clusters, n_features).
173
173
  Empty clusters retain their previous position (zeros).
174
+
175
+ Examples
176
+ --------
177
+ >>> X = np.array([[0, 0], [1, 0], [10, 10], [11, 10]])
178
+ >>> labels = np.array([0, 0, 1, 1])
179
+ >>> centers = update_centers(X, labels, n_clusters=2)
180
+ >>> centers
181
+ array([[ 0.5, 0. ],
182
+ [10.5, 10. ]])
174
183
  """
175
184
  X = np.asarray(X, dtype=np.float64)
176
185
  labels = np.asarray(labels, dtype=np.intp)
@@ -295,9 +295,25 @@ def ecef2enu(
295
295
  enu : ndarray
296
296
  Local ENU coordinates [east, north, up] in meters.
297
297
 
298
+ Examples
299
+ --------
300
+ Convert an aircraft position from ECEF to local ENU frame:
301
+
302
+ >>> import numpy as np
303
+ >>> from pytcl.coordinate_systems.conversions import ecef2enu, geodetic2ecef
304
+ >>> # Reference point (airport at 38.9°N, 77.0°W)
305
+ >>> lat_ref = np.radians(38.9)
306
+ >>> lon_ref = np.radians(-77.0)
307
+ >>> # Aircraft ECEF position
308
+ >>> ecef_aircraft = np.array([1130000.0, -4830000.0, 3990000.0])
309
+ >>> enu = ecef2enu(ecef_aircraft, lat_ref, lon_ref)
310
+ >>> enu.shape
311
+ (3,)
312
+
298
313
  See Also
299
314
  --------
300
315
  enu2ecef : Inverse conversion.
316
+ ecef2ned : Convert to NED (North-East-Down) frame.
301
317
  """
302
318
  ecef = np.asarray(ecef, dtype=np.float64)
303
319
 
@@ -355,6 +371,21 @@ def enu2ecef(
355
371
  ecef : ndarray
356
372
  ECEF coordinates [x, y, z] in meters.
357
373
 
374
+ Examples
375
+ --------
376
+ Convert local ENU offset to ECEF coordinates:
377
+
378
+ >>> import numpy as np
379
+ >>> from pytcl.coordinate_systems.conversions import enu2ecef
380
+ >>> # Reference point (airport at 38.9°N, 77.0°W)
381
+ >>> lat_ref = np.radians(38.9)
382
+ >>> lon_ref = np.radians(-77.0)
383
+ >>> # Aircraft 1km east, 2km north, 500m up
384
+ >>> enu = np.array([1000.0, 2000.0, 500.0])
385
+ >>> ecef = enu2ecef(enu, lat_ref, lon_ref)
386
+ >>> ecef.shape
387
+ (3,)
388
+
358
389
  See Also
359
390
  --------
360
391
  ecef2enu : Inverse conversion.
@@ -425,6 +456,21 @@ def ecef2ned(
425
456
  ned : ndarray
426
457
  Local NED coordinates [north, east, down] in meters.
427
458
 
459
+ Examples
460
+ --------
461
+ Convert aircraft ECEF position to local NED frame:
462
+
463
+ >>> import numpy as np
464
+ >>> from pytcl.coordinate_systems.conversions import ecef2ned, geodetic2ecef
465
+ >>> # Reference point (airport)
466
+ >>> lat_ref = np.radians(38.9)
467
+ >>> lon_ref = np.radians(-77.0)
468
+ >>> # Aircraft ECEF position
469
+ >>> ecef_aircraft = np.array([1130000.0, -4830000.0, 3990000.0])
470
+ >>> ned = ecef2ned(ecef_aircraft, lat_ref, lon_ref)
471
+ >>> ned.shape # [north, east, down]
472
+ (3,)
473
+
428
474
  See Also
429
475
  --------
430
476
  ned2ecef : Inverse conversion.
@@ -264,6 +264,16 @@ def cart2cyl(
264
264
  z : ndarray
265
265
  Height (same as Cartesian z).
266
266
 
267
+ Examples
268
+ --------
269
+ >>> rho, phi, z = cart2cyl([1, 1, 5])
270
+ >>> rho
271
+ 1.4142135623730951
272
+ >>> np.degrees(phi)
273
+ 45.0
274
+ >>> z
275
+ 5.0
276
+
267
277
  See Also
268
278
  --------
269
279
  cyl2cart : Inverse conversion.
@@ -310,6 +320,12 @@ def cyl2cart(
310
320
  cart_points : ndarray
311
321
  Cartesian coordinates of shape (3,) or (3, n).
312
322
 
323
+ Examples
324
+ --------
325
+ >>> cart = cyl2cart(1.414, np.radians(45), 5.0)
326
+ >>> cart
327
+ array([1.00..., 1.00..., 5. ])
328
+
313
329
  See Also
314
330
  --------
315
331
  cart2cyl : Inverse conversion.
@@ -354,6 +370,16 @@ def ruv2cart(
354
370
  cart_points : ndarray
355
371
  Cartesian coordinates.
356
372
 
373
+ Examples
374
+ --------
375
+ >>> # Target at 45 deg azimuth, 30 deg elevation, range 100
376
+ >>> az, el = np.radians(45), np.radians(30)
377
+ >>> u = np.cos(az) * np.cos(el)
378
+ >>> v = np.sin(az) * np.cos(el)
379
+ >>> cart = ruv2cart(100, u, v)
380
+ >>> cart
381
+ array([61.23..., 61.23..., 50. ])
382
+
357
383
  Notes
358
384
  -----
359
385
  This representation is common in radar tracking systems.
@@ -396,6 +422,15 @@ def cart2ruv(
396
422
  v : ndarray
397
423
  Direction cosine along y-axis (y/r).
398
424
 
425
+ Examples
426
+ --------
427
+ >>> r, u, v = cart2ruv([100, 0, 0])
428
+ >>> r, u, v
429
+ (100.0, 1.0, 0.0)
430
+ >>> r, u, v = cart2ruv([50, 50, 50])
431
+ >>> r
432
+ 86.602...
433
+
399
434
  See Also
400
435
  --------
401
436
  ruv2cart : Inverse conversion.