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.
- {nrl_tracker-1.9.1.dist-info → nrl_tracker-1.9.2.dist-info}/METADATA +4 -4
- {nrl_tracker-1.9.1.dist-info → nrl_tracker-1.9.2.dist-info}/RECORD +60 -59
- pytcl/__init__.py +2 -2
- pytcl/assignment_algorithms/gating.py +18 -0
- pytcl/assignment_algorithms/jpda.py +56 -0
- pytcl/assignment_algorithms/nd_assignment.py +65 -0
- pytcl/assignment_algorithms/network_flow.py +40 -0
- pytcl/astronomical/ephemerides.py +18 -0
- pytcl/astronomical/orbital_mechanics.py +131 -0
- pytcl/atmosphere/ionosphere.py +44 -0
- pytcl/atmosphere/models.py +29 -0
- pytcl/clustering/dbscan.py +9 -0
- pytcl/clustering/gaussian_mixture.py +20 -0
- pytcl/clustering/hierarchical.py +29 -0
- pytcl/clustering/kmeans.py +9 -0
- pytcl/coordinate_systems/conversions/geodetic.py +46 -0
- pytcl/coordinate_systems/conversions/spherical.py +35 -0
- pytcl/coordinate_systems/rotations/rotations.py +147 -0
- pytcl/core/__init__.py +16 -0
- pytcl/core/maturity.py +346 -0
- pytcl/dynamic_estimation/gaussian_sum_filter.py +55 -0
- pytcl/dynamic_estimation/imm.py +29 -0
- pytcl/dynamic_estimation/information_filter.py +64 -0
- pytcl/dynamic_estimation/kalman/extended.py +56 -0
- pytcl/dynamic_estimation/kalman/linear.py +69 -0
- pytcl/dynamic_estimation/kalman/unscented.py +81 -0
- pytcl/dynamic_estimation/particle_filters/bootstrap.py +146 -0
- pytcl/dynamic_estimation/rbpf.py +51 -0
- pytcl/dynamic_estimation/smoothers.py +58 -0
- pytcl/dynamic_models/continuous_time/dynamics.py +104 -0
- pytcl/dynamic_models/discrete_time/coordinated_turn.py +6 -0
- pytcl/dynamic_models/discrete_time/singer.py +12 -0
- pytcl/dynamic_models/process_noise/coordinated_turn.py +46 -0
- pytcl/dynamic_models/process_noise/polynomial.py +6 -0
- pytcl/dynamic_models/process_noise/singer.py +52 -0
- pytcl/gravity/clenshaw.py +60 -0
- pytcl/gravity/egm.py +47 -0
- pytcl/gravity/models.py +34 -0
- pytcl/gravity/spherical_harmonics.py +73 -0
- pytcl/gravity/tides.py +34 -0
- pytcl/mathematical_functions/numerical_integration/quadrature.py +85 -0
- pytcl/mathematical_functions/special_functions/bessel.py +55 -0
- pytcl/mathematical_functions/special_functions/elliptic.py +42 -0
- pytcl/mathematical_functions/special_functions/error_functions.py +49 -0
- pytcl/mathematical_functions/special_functions/gamma_functions.py +43 -0
- pytcl/mathematical_functions/special_functions/lambert_w.py +5 -0
- pytcl/mathematical_functions/special_functions/marcum_q.py +16 -0
- pytcl/navigation/geodesy.py +101 -2
- pytcl/navigation/great_circle.py +71 -0
- pytcl/navigation/rhumb.py +74 -0
- pytcl/performance_evaluation/estimation_metrics.py +70 -0
- pytcl/performance_evaluation/track_metrics.py +30 -0
- pytcl/static_estimation/maximum_likelihood.py +54 -0
- pytcl/static_estimation/robust.py +57 -0
- pytcl/terrain/dem.py +69 -0
- pytcl/terrain/visibility.py +65 -0
- pytcl/trackers/hypothesis.py +65 -0
- {nrl_tracker-1.9.1.dist-info → nrl_tracker-1.9.2.dist-info}/LICENSE +0 -0
- {nrl_tracker-1.9.1.dist-info → nrl_tracker-1.9.2.dist-info}/WHEEL +0 -0
- {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
|
|
pytcl/atmosphere/ionosphere.py
CHANGED
|
@@ -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.
|
pytcl/atmosphere/models.py
CHANGED
|
@@ -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)
|
pytcl/clustering/dbscan.py
CHANGED
|
@@ -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
|
pytcl/clustering/hierarchical.py
CHANGED
|
@@ -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)
|
pytcl/clustering/kmeans.py
CHANGED
|
@@ -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.
|