nrl-tracker 1.11.1__py3-none-any.whl → 1.12.1__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.11.1.dist-info → nrl_tracker-1.12.1.dist-info}/METADATA +5 -4
- {nrl_tracker-1.11.1.dist-info → nrl_tracker-1.12.1.dist-info}/RECORD +30 -30
- pytcl/__init__.py +2 -2
- pytcl/assignment_algorithms/network_flow.py +172 -60
- pytcl/astronomical/time_systems.py +21 -0
- pytcl/containers/cluster_set.py +36 -0
- pytcl/coordinate_systems/conversions/geodetic.py +58 -0
- pytcl/core/array_utils.py +52 -0
- pytcl/gpu/ekf.py +46 -0
- pytcl/gpu/kalman.py +16 -0
- pytcl/gpu/matrix_utils.py +44 -1
- pytcl/gpu/particle_filter.py +33 -0
- pytcl/gpu/ukf.py +31 -0
- pytcl/gpu/utils.py +15 -0
- pytcl/magnetism/igrf.py +72 -0
- pytcl/magnetism/wmm.py +52 -0
- pytcl/mathematical_functions/basic_matrix/decompositions.py +7 -0
- pytcl/mathematical_functions/basic_matrix/special_matrices.py +31 -0
- pytcl/mathematical_functions/geometry/geometry.py +33 -0
- pytcl/mathematical_functions/interpolation/interpolation.py +83 -0
- pytcl/mathematical_functions/signal_processing/detection.py +31 -0
- pytcl/mathematical_functions/signal_processing/filters.py +56 -0
- pytcl/mathematical_functions/signal_processing/matched_filter.py +32 -1
- pytcl/mathematical_functions/special_functions/hypergeometric.py +17 -0
- pytcl/mathematical_functions/statistics/estimators.py +71 -0
- pytcl/mathematical_functions/transforms/wavelets.py +25 -0
- pytcl/navigation/great_circle.py +33 -0
- {nrl_tracker-1.11.1.dist-info → nrl_tracker-1.12.1.dist-info}/LICENSE +0 -0
- {nrl_tracker-1.11.1.dist-info → nrl_tracker-1.12.1.dist-info}/WHEEL +0 -0
- {nrl_tracker-1.11.1.dist-info → nrl_tracker-1.12.1.dist-info}/top_level.txt +0 -0
|
@@ -257,6 +257,27 @@ def geodetic2enu(
|
|
|
257
257
|
enu : ndarray
|
|
258
258
|
Local ENU coordinates [east, north, up] in meters.
|
|
259
259
|
|
|
260
|
+
Examples
|
|
261
|
+
--------
|
|
262
|
+
>>> import numpy as np
|
|
263
|
+
>>> from pytcl.coordinate_systems import geodetic2enu
|
|
264
|
+
>>> # Reference point: San Francisco Airport (-122.375°, 37.615°)
|
|
265
|
+
>>> lat_ref = np.radians(37.615)
|
|
266
|
+
>>> lon_ref = np.radians(-122.375)
|
|
267
|
+
>>> alt_ref = 0
|
|
268
|
+
>>> # Target point: 2 km north, 1 km east of reference
|
|
269
|
+
>>> # Approximate location using small offsets
|
|
270
|
+
>>> lat_target = lat_ref + np.radians(0.01)
|
|
271
|
+
>>> lon_target = lon_ref + np.radians(0.01)
|
|
272
|
+
>>> alt_target = 100 # 100 m elevation
|
|
273
|
+
>>> enu = geodetic2enu(lat_target, lon_target, alt_target,
|
|
274
|
+
... lat_ref, lon_ref, alt_ref)
|
|
275
|
+
>>> # ENU coordinates should show positive north and east offsets
|
|
276
|
+
>>> enu[0] > 0 and enu[1] > 0 # East > 0, North > 0
|
|
277
|
+
True
|
|
278
|
+
>>> enu[2] > 100 # Up should be approximately the altitude difference
|
|
279
|
+
True
|
|
280
|
+
|
|
260
281
|
See Also
|
|
261
282
|
--------
|
|
262
283
|
enu2geodetic : Inverse conversion.
|
|
@@ -509,6 +530,25 @@ def ned2ecef(
|
|
|
509
530
|
ecef : ndarray
|
|
510
531
|
ECEF coordinates [x, y, z] in meters.
|
|
511
532
|
|
|
533
|
+
Examples
|
|
534
|
+
--------
|
|
535
|
+
>>> import numpy as np
|
|
536
|
+
>>> from pytcl.coordinate_systems import ned2ecef, geodetic2ecef
|
|
537
|
+
>>> # Reference point: Kennedy Space Center (28.5°N, 80.65°W)
|
|
538
|
+
>>> lat_ref = np.radians(28.5)
|
|
539
|
+
>>> lon_ref = np.radians(-80.65)
|
|
540
|
+
>>> # NED offset from reference: 5 km north, 3 km east, 1 km down
|
|
541
|
+
>>> ned = np.array([5000.0, 3000.0, 1000.0])
|
|
542
|
+
>>> # Convert to ECEF coordinates
|
|
543
|
+
>>> ecef = ned2ecef(ned, lat_ref, lon_ref)
|
|
544
|
+
>>> ecef.shape
|
|
545
|
+
(3,)
|
|
546
|
+
>>> # Verify roundtrip conversion
|
|
547
|
+
>>> from pytcl.coordinate_systems import ecef2ned
|
|
548
|
+
>>> ned_back = ecef2ned(ecef, lat_ref, lon_ref)
|
|
549
|
+
>>> np.allclose(ned, ned_back, atol=0.1) # Allow small numerical error
|
|
550
|
+
True
|
|
551
|
+
|
|
512
552
|
See Also
|
|
513
553
|
--------
|
|
514
554
|
ecef2ned : Inverse conversion.
|
|
@@ -825,6 +865,24 @@ def sez2geodetic(
|
|
|
825
865
|
alt : ndarray
|
|
826
866
|
Altitude in meters.
|
|
827
867
|
|
|
868
|
+
Examples
|
|
869
|
+
--------
|
|
870
|
+
>>> import numpy as np
|
|
871
|
+
>>> from pytcl.coordinate_systems import sez2geodetic, geodetic2sez
|
|
872
|
+
>>> # Observer location: Arecibo Observatory (18.3°N, 66.75°W)
|
|
873
|
+
>>> lat_ref = np.radians(18.3)
|
|
874
|
+
>>> lon_ref = np.radians(-66.75)
|
|
875
|
+
>>> alt_ref = 500 # m
|
|
876
|
+
>>> # Observed satellite: 30 km south, 20 km east, 35 km zenith
|
|
877
|
+
>>> sez = np.array([-30000.0, 20000.0, 35000.0])
|
|
878
|
+
>>> # Convert to geodetic coordinates
|
|
879
|
+
>>> lat, lon, alt = sez2geodetic(sez, lat_ref, lon_ref, alt_ref)
|
|
880
|
+
>>> # Verify roundtrip conversion
|
|
881
|
+
>>> from pytcl.coordinate_systems import geodetic2sez
|
|
882
|
+
>>> sez_back = geodetic2sez(lat, lon, alt, lat_ref, lon_ref, alt_ref)
|
|
883
|
+
>>> np.allclose(sez, sez_back, atol=1.0) # Allow ~1m numerical error
|
|
884
|
+
True
|
|
885
|
+
|
|
828
886
|
See Also
|
|
829
887
|
--------
|
|
830
888
|
geodetic2sez : Forward conversion.
|
pytcl/core/array_utils.py
CHANGED
|
@@ -269,6 +269,16 @@ def unvec(
|
|
|
269
269
|
-------
|
|
270
270
|
NDArray
|
|
271
271
|
Matrix with specified shape.
|
|
272
|
+
|
|
273
|
+
Examples
|
|
274
|
+
--------
|
|
275
|
+
>>> import numpy as np
|
|
276
|
+
>>> from pytcl.core.array_utils import unvec
|
|
277
|
+
>>> v = np.array([1, 2, 3, 4, 5, 6])
|
|
278
|
+
>>> M = unvec(v, (2, 3))
|
|
279
|
+
>>> M
|
|
280
|
+
array([[1, 3, 5],
|
|
281
|
+
[2, 4, 6]])
|
|
272
282
|
"""
|
|
273
283
|
v = np.asarray(v).flatten()
|
|
274
284
|
return v.reshape(shape, order=order)
|
|
@@ -362,6 +372,16 @@ def unskew(S: ArrayLike) -> NDArray[np.floating[Any]]:
|
|
|
362
372
|
-------
|
|
363
373
|
NDArray
|
|
364
374
|
3-element vector.
|
|
375
|
+
|
|
376
|
+
Examples
|
|
377
|
+
--------
|
|
378
|
+
>>> import numpy as np
|
|
379
|
+
>>> from pytcl.core.array_utils import unskew, skew_symmetric
|
|
380
|
+
>>> v = np.array([1, 2, 3])
|
|
381
|
+
>>> S = skew_symmetric(v)
|
|
382
|
+
>>> v_recovered = unskew(S)
|
|
383
|
+
>>> np.allclose(v, v_recovered)
|
|
384
|
+
True
|
|
365
385
|
"""
|
|
366
386
|
S = np.asarray(S, dtype=np.float64)
|
|
367
387
|
if S.shape != (3, 3):
|
|
@@ -503,6 +523,20 @@ def meshgrid_ij(
|
|
|
503
523
|
-------
|
|
504
524
|
tuple of NDArray
|
|
505
525
|
Coordinate matrices.
|
|
526
|
+
|
|
527
|
+
Examples
|
|
528
|
+
--------
|
|
529
|
+
>>> import numpy as np
|
|
530
|
+
>>> from pytcl.core.array_utils import meshgrid_ij
|
|
531
|
+
>>> x = np.array([1, 2, 3])
|
|
532
|
+
>>> y = np.array([4, 5])
|
|
533
|
+
>>> X, Y = meshgrid_ij(x, y)
|
|
534
|
+
>>> X
|
|
535
|
+
array([[1, 2, 3],
|
|
536
|
+
[1, 2, 3]])
|
|
537
|
+
>>> Y
|
|
538
|
+
array([[4, 4, 4],
|
|
539
|
+
[5, 5, 5]])
|
|
506
540
|
"""
|
|
507
541
|
return np.meshgrid(*xi, indexing=indexing)
|
|
508
542
|
|
|
@@ -563,6 +597,15 @@ def nearest_positive_definite(A: ArrayLike) -> NDArray[np.floating[Any]]:
|
|
|
563
597
|
-------
|
|
564
598
|
NDArray
|
|
565
599
|
Nearest positive definite matrix.
|
|
600
|
+
|
|
601
|
+
Examples
|
|
602
|
+
--------
|
|
603
|
+
>>> import numpy as np
|
|
604
|
+
>>> from pytcl.core.array_utils import nearest_positive_definite
|
|
605
|
+
>>> A = np.array([[1, -2], [-2, 1]]) # Not PD
|
|
606
|
+
>>> A_pd = nearest_positive_definite(A)
|
|
607
|
+
>>> np.all(np.linalg.eigvalsh(A_pd) > 0)
|
|
608
|
+
True
|
|
566
609
|
"""
|
|
567
610
|
A = np.asarray(A, dtype=np.float64)
|
|
568
611
|
|
|
@@ -614,6 +657,15 @@ def safe_cholesky(
|
|
|
614
657
|
------
|
|
615
658
|
np.linalg.LinAlgError
|
|
616
659
|
If Cholesky decomposition fails after all attempts.
|
|
660
|
+
|
|
661
|
+
Examples
|
|
662
|
+
--------
|
|
663
|
+
>>> import numpy as np
|
|
664
|
+
>>> from pytcl.core.array_utils import safe_cholesky
|
|
665
|
+
>>> A = np.array([[4, 2], [2, 3]]) # PD matrix
|
|
666
|
+
>>> L = safe_cholesky(A)
|
|
667
|
+
>>> np.allclose(L @ L.T, A)
|
|
668
|
+
True
|
|
617
669
|
"""
|
|
618
670
|
A = np.asarray(A, dtype=np.float64)
|
|
619
671
|
|
pytcl/gpu/ekf.py
CHANGED
|
@@ -155,6 +155,30 @@ def batch_ekf_predict(
|
|
|
155
155
|
result : BatchEKFPrediction
|
|
156
156
|
Predicted states and covariances.
|
|
157
157
|
|
|
158
|
+
Examples
|
|
159
|
+
--------
|
|
160
|
+
>>> import numpy as np
|
|
161
|
+
>>> from pytcl.gpu.ekf import batch_ekf_predict
|
|
162
|
+
>>> # Nonlinear dynamics: coordinated turn
|
|
163
|
+
>>> def f_turn(x):
|
|
164
|
+
... w = 0.01 # Turn rate
|
|
165
|
+
... return np.array([x[0] + np.cos(w)*x[2],
|
|
166
|
+
... x[1] + np.sin(w)*x[3],
|
|
167
|
+
... x[2], x[3]])
|
|
168
|
+
>>> def F_jacobian(x):
|
|
169
|
+
... w = 0.01
|
|
170
|
+
... return np.array([[1, 0, np.cos(w), 0],
|
|
171
|
+
... [0, 1, np.sin(w), 0],
|
|
172
|
+
... [0, 0, 1, 0],
|
|
173
|
+
... [0, 0, 0, 1]])
|
|
174
|
+
>>> n_tracks = 30
|
|
175
|
+
>>> x = np.random.randn(n_tracks, 4) * 0.1
|
|
176
|
+
>>> P = np.tile(np.eye(4) * 0.01, (n_tracks, 1, 1))
|
|
177
|
+
>>> Q = np.eye(4) * 0.001
|
|
178
|
+
>>> result = batch_ekf_predict(x, P, f_turn, F_jacobian, Q)
|
|
179
|
+
>>> result.x.shape
|
|
180
|
+
(30, 4)
|
|
181
|
+
|
|
158
182
|
Notes
|
|
159
183
|
-----
|
|
160
184
|
The nonlinear dynamics are applied on CPU (Python function), then
|
|
@@ -238,6 +262,28 @@ def batch_ekf_update(
|
|
|
238
262
|
-------
|
|
239
263
|
result : BatchEKFUpdate
|
|
240
264
|
Update results including states, covariances, and statistics.
|
|
265
|
+
|
|
266
|
+
Examples
|
|
267
|
+
--------
|
|
268
|
+
>>> import numpy as np
|
|
269
|
+
>>> from pytcl.gpu.ekf import batch_ekf_update
|
|
270
|
+
>>> # Polar measurement from Cartesian state
|
|
271
|
+
>>> def h_polar(x):
|
|
272
|
+
... r = np.sqrt(x[0]**2 + x[1]**2)
|
|
273
|
+
... theta = np.arctan2(x[1], x[0])
|
|
274
|
+
... return np.array([r, theta])
|
|
275
|
+
>>> def H_jacobian(x):
|
|
276
|
+
... r = np.sqrt(x[0]**2 + x[1]**2)
|
|
277
|
+
... return np.array([[x[0]/r, x[1]/r],
|
|
278
|
+
... [-x[1]/r**2, x[0]/r**2]])
|
|
279
|
+
>>> n_tracks = 20
|
|
280
|
+
>>> x = np.random.randn(n_tracks, 2)
|
|
281
|
+
>>> P = np.tile(np.eye(2), (n_tracks, 1, 1))
|
|
282
|
+
>>> z = np.random.randn(n_tracks, 2) * [100, 0.1] # r, theta
|
|
283
|
+
>>> R = np.diag([10.0, 0.01])
|
|
284
|
+
>>> result = batch_ekf_update(x, P, z, h_polar, H_jacobian, R)
|
|
285
|
+
>>> result.x.shape
|
|
286
|
+
(20, 2)
|
|
241
287
|
"""
|
|
242
288
|
cp = import_optional("cupy", extra="gpu", feature="GPU Extended Kalman filter")
|
|
243
289
|
|
pytcl/gpu/kalman.py
CHANGED
|
@@ -353,6 +353,22 @@ def batch_kf_predict_update(
|
|
|
353
353
|
-------
|
|
354
354
|
result : BatchKalmanUpdate
|
|
355
355
|
Named tuple with updated states, covariances, and statistics.
|
|
356
|
+
|
|
357
|
+
Examples
|
|
358
|
+
--------
|
|
359
|
+
>>> import numpy as np
|
|
360
|
+
>>> from pytcl.gpu.kalman import batch_kf_predict_update
|
|
361
|
+
>>> n_tracks = 50
|
|
362
|
+
>>> x = np.random.randn(n_tracks, 2)
|
|
363
|
+
>>> P = np.tile(np.eye(2), (n_tracks, 1, 1))
|
|
364
|
+
>>> z = np.random.randn(n_tracks, 2) # Measurements
|
|
365
|
+
>>> F = np.array([[1, 0.1], [0, 1]]) # Constant velocity
|
|
366
|
+
>>> Q = np.eye(2) * 0.01
|
|
367
|
+
>>> H = np.eye(2)
|
|
368
|
+
>>> R = np.eye(2) * 0.1
|
|
369
|
+
>>> result = batch_kf_predict_update(x, P, z, F, Q, H, R)
|
|
370
|
+
>>> result.x.shape
|
|
371
|
+
(50, 2)
|
|
356
372
|
"""
|
|
357
373
|
pred = batch_kf_predict(x, P, F, Q, B, u)
|
|
358
374
|
return batch_kf_update(pred.x, pred.P, z, H, R)
|
pytcl/gpu/matrix_utils.py
CHANGED
|
@@ -393,6 +393,16 @@ class MemoryPool:
|
|
|
393
393
|
-------
|
|
394
394
|
stats : dict
|
|
395
395
|
Dictionary with 'used', 'total', and 'free' bytes.
|
|
396
|
+
|
|
397
|
+
Examples
|
|
398
|
+
--------
|
|
399
|
+
>>> from pytcl.gpu.matrix_utils import get_memory_pool
|
|
400
|
+
>>> pool = get_memory_pool()
|
|
401
|
+
>>> stats = pool.get_stats()
|
|
402
|
+
>>> stats.keys()
|
|
403
|
+
dict_keys(['used', 'total', 'free', 'device_total'])
|
|
404
|
+
>>> stats['used'] >= 0
|
|
405
|
+
True
|
|
396
406
|
"""
|
|
397
407
|
if self._pool is None:
|
|
398
408
|
return {"used": 0, "total": 0, "free": 0}
|
|
@@ -409,7 +419,19 @@ class MemoryPool:
|
|
|
409
419
|
}
|
|
410
420
|
|
|
411
421
|
def free_all(self) -> None:
|
|
412
|
-
"""
|
|
422
|
+
"""
|
|
423
|
+
Free all cached memory blocks.
|
|
424
|
+
|
|
425
|
+
Clears the memory pool cache, which can help free up GPU memory
|
|
426
|
+
when operations are complete.
|
|
427
|
+
|
|
428
|
+
Examples
|
|
429
|
+
--------
|
|
430
|
+
>>> from pytcl.gpu.matrix_utils import get_memory_pool
|
|
431
|
+
>>> pool = get_memory_pool()
|
|
432
|
+
>>> # After allocations
|
|
433
|
+
>>> pool.free_all() # Clear cached blocks
|
|
434
|
+
"""
|
|
413
435
|
if self._pool is not None:
|
|
414
436
|
self._pool.free_all_blocks()
|
|
415
437
|
if self._pinned_pool is not None:
|
|
@@ -423,6 +445,15 @@ class MemoryPool:
|
|
|
423
445
|
----------
|
|
424
446
|
limit : int or None
|
|
425
447
|
Maximum bytes to allocate. None for no limit.
|
|
448
|
+
|
|
449
|
+
Examples
|
|
450
|
+
--------
|
|
451
|
+
>>> from pytcl.gpu.matrix_utils import get_memory_pool
|
|
452
|
+
>>> pool = get_memory_pool()
|
|
453
|
+
>>> # Limit to 2 GB
|
|
454
|
+
>>> pool.set_limit(2 * 1024**3)
|
|
455
|
+
>>> # Reset to unlimited
|
|
456
|
+
>>> pool.set_limit(None)
|
|
426
457
|
"""
|
|
427
458
|
if self._pool is not None:
|
|
428
459
|
if limit is None:
|
|
@@ -471,6 +502,18 @@ def get_memory_pool() -> MemoryPool:
|
|
|
471
502
|
-------
|
|
472
503
|
pool : MemoryPool
|
|
473
504
|
Global memory pool instance.
|
|
505
|
+
|
|
506
|
+
Examples
|
|
507
|
+
--------
|
|
508
|
+
>>> from pytcl.gpu.matrix_utils import get_memory_pool
|
|
509
|
+
>>> pool = get_memory_pool()
|
|
510
|
+
>>> # Get current memory stats
|
|
511
|
+
>>> stats = pool.get_stats()
|
|
512
|
+
>>> print(f"Used: {stats['used']} bytes")
|
|
513
|
+
>>> # Set memory limit
|
|
514
|
+
>>> pool.set_limit(1024**3) # 1 GB limit
|
|
515
|
+
>>> # Free cached blocks
|
|
516
|
+
>>> pool.free_all()
|
|
474
517
|
"""
|
|
475
518
|
global _memory_pool
|
|
476
519
|
if _memory_pool is None:
|
pytcl/gpu/particle_filter.py
CHANGED
|
@@ -79,6 +79,17 @@ def gpu_effective_sample_size(weights: ArrayLike) -> float:
|
|
|
79
79
|
-------
|
|
80
80
|
ess : float
|
|
81
81
|
Effective sample size.
|
|
82
|
+
|
|
83
|
+
Examples
|
|
84
|
+
--------
|
|
85
|
+
>>> import numpy as np
|
|
86
|
+
>>> from pytcl.gpu.particle_filter import gpu_effective_sample_size
|
|
87
|
+
>>> weights = np.array([0.1, 0.2, 0.3, 0.4])
|
|
88
|
+
>>> ess = gpu_effective_sample_size(weights)
|
|
89
|
+
>>> ess > 0
|
|
90
|
+
True
|
|
91
|
+
>>> ess <= len(weights)
|
|
92
|
+
True
|
|
82
93
|
"""
|
|
83
94
|
cp = import_optional("cupy", extra="gpu", feature="GPU particle filter")
|
|
84
95
|
w = ensure_gpu_array(weights, dtype=cp.float64)
|
|
@@ -151,6 +162,17 @@ def gpu_resample_multinomial(weights: ArrayLike) -> NDArray[np.intp]:
|
|
|
151
162
|
indices : ndarray
|
|
152
163
|
Resampled particle indices, shape (n_particles,).
|
|
153
164
|
|
|
165
|
+
Examples
|
|
166
|
+
--------
|
|
167
|
+
>>> import numpy as np
|
|
168
|
+
>>> from pytcl.gpu.particle_filter import gpu_resample_multinomial
|
|
169
|
+
>>> weights = np.array([0.1, 0.4, 0.5])
|
|
170
|
+
>>> indices = gpu_resample_multinomial(weights)
|
|
171
|
+
>>> indices.shape
|
|
172
|
+
(3,)
|
|
173
|
+
>>> np.all(indices < 3)
|
|
174
|
+
True
|
|
175
|
+
|
|
154
176
|
Notes
|
|
155
177
|
-----
|
|
156
178
|
Multinomial resampling has higher variance than systematic resampling
|
|
@@ -230,6 +252,17 @@ def gpu_normalize_weights(
|
|
|
230
252
|
Normalized weights, shape (n_particles,).
|
|
231
253
|
log_likelihood : float
|
|
232
254
|
Log of the normalization constant.
|
|
255
|
+
|
|
256
|
+
Examples
|
|
257
|
+
--------
|
|
258
|
+
>>> import numpy as np
|
|
259
|
+
>>> from pytcl.gpu.particle_filter import gpu_normalize_weights
|
|
260
|
+
>>> log_w = np.array([-1.0, -0.5, -2.0])
|
|
261
|
+
>>> weights, log_likelihood = gpu_normalize_weights(log_w)
|
|
262
|
+
>>> np.allclose(weights.sum(), 1.0)
|
|
263
|
+
True
|
|
264
|
+
>>> log_likelihood < 0
|
|
265
|
+
True
|
|
233
266
|
"""
|
|
234
267
|
cp = import_optional("cupy", extra="gpu", feature="GPU particle filter")
|
|
235
268
|
|
pytcl/gpu/ukf.py
CHANGED
|
@@ -209,6 +209,21 @@ def batch_ukf_predict(
|
|
|
209
209
|
-------
|
|
210
210
|
result : BatchUKFPrediction
|
|
211
211
|
Predicted states and covariances.
|
|
212
|
+
|
|
213
|
+
Examples
|
|
214
|
+
--------
|
|
215
|
+
>>> import numpy as np
|
|
216
|
+
>>> from pytcl.gpu.ukf import batch_ukf_predict
|
|
217
|
+
>>> # Nonlinear dynamics example
|
|
218
|
+
>>> def f_dynamics(x):
|
|
219
|
+
... return np.array([x[0] + 0.1*x[1], x[1] * 0.99])
|
|
220
|
+
>>> n_tracks = 50
|
|
221
|
+
>>> x = np.random.randn(n_tracks, 2)
|
|
222
|
+
>>> P = np.tile(np.eye(2) * 0.01, (n_tracks, 1, 1))
|
|
223
|
+
>>> Q = np.eye(2) * 0.001
|
|
224
|
+
>>> result = batch_ukf_predict(x, P, f_dynamics, Q)
|
|
225
|
+
>>> result.x.shape
|
|
226
|
+
(50, 2)
|
|
212
227
|
"""
|
|
213
228
|
cp = import_optional("cupy", extra="gpu", feature="GPU Unscented Kalman filter")
|
|
214
229
|
|
|
@@ -290,6 +305,22 @@ def batch_ukf_update(
|
|
|
290
305
|
-------
|
|
291
306
|
result : BatchUKFUpdate
|
|
292
307
|
Update results.
|
|
308
|
+
|
|
309
|
+
Examples
|
|
310
|
+
--------
|
|
311
|
+
>>> import numpy as np
|
|
312
|
+
>>> from pytcl.gpu.ukf import batch_ukf_update
|
|
313
|
+
>>> # Nonlinear measurement example
|
|
314
|
+
>>> def h_measurement(x):
|
|
315
|
+
... return np.sqrt(x[0]**2 + x[1]**2) # Range-only
|
|
316
|
+
>>> n_tracks = 40
|
|
317
|
+
>>> x = np.random.randn(n_tracks, 2)
|
|
318
|
+
>>> P = np.tile(np.eye(2), (n_tracks, 1, 1))
|
|
319
|
+
>>> z = np.random.randn(n_tracks, 1) * 10 + 100
|
|
320
|
+
>>> R = np.array([[1.0]])
|
|
321
|
+
>>> result = batch_ukf_update(x, P, z, h_measurement, R)
|
|
322
|
+
>>> result.x.shape
|
|
323
|
+
(40, 2)
|
|
293
324
|
"""
|
|
294
325
|
cp = import_optional("cupy", extra="gpu", feature="GPU Unscented Kalman filter")
|
|
295
326
|
|
pytcl/gpu/utils.py
CHANGED
|
@@ -115,6 +115,12 @@ def is_cupy_available() -> bool:
|
|
|
115
115
|
-------
|
|
116
116
|
bool
|
|
117
117
|
True if CuPy acceleration is available.
|
|
118
|
+
|
|
119
|
+
Examples
|
|
120
|
+
--------
|
|
121
|
+
>>> from pytcl.gpu.utils import is_cupy_available
|
|
122
|
+
>>> if is_cupy_available():
|
|
123
|
+
... print("CUDA GPU available")
|
|
118
124
|
"""
|
|
119
125
|
if not is_available("cupy"):
|
|
120
126
|
_logger.debug("CuPy not installed")
|
|
@@ -430,6 +436,15 @@ def ensure_gpu_array(
|
|
|
430
436
|
-------
|
|
431
437
|
GPUArray
|
|
432
438
|
Array on GPU with specified dtype (cupy.ndarray or mlx.array).
|
|
439
|
+
|
|
440
|
+
Examples
|
|
441
|
+
--------
|
|
442
|
+
>>> import numpy as np
|
|
443
|
+
>>> from pytcl.gpu.utils import ensure_gpu_array, is_gpu_available
|
|
444
|
+
>>> x = np.array([1, 2, 3])
|
|
445
|
+
>>> if is_gpu_available():
|
|
446
|
+
... x_gpu = ensure_gpu_array(x, dtype=np.float32)
|
|
447
|
+
... print(x_gpu.dtype)
|
|
433
448
|
"""
|
|
434
449
|
gpu_arr = to_gpu(arr, backend=backend)
|
|
435
450
|
|
pytcl/magnetism/igrf.py
CHANGED
|
@@ -53,6 +53,14 @@ def create_igrf13_coefficients() -> MagneticCoefficients:
|
|
|
53
53
|
coeffs : MagneticCoefficients
|
|
54
54
|
IGRF-13 spherical harmonic coefficients.
|
|
55
55
|
|
|
56
|
+
Examples
|
|
57
|
+
--------
|
|
58
|
+
>>> coeffs = create_igrf13_coefficients()
|
|
59
|
+
>>> coeffs.epoch
|
|
60
|
+
2020.0
|
|
61
|
+
>>> coeffs.n_max
|
|
62
|
+
13
|
|
63
|
+
|
|
56
64
|
Notes
|
|
57
65
|
-----
|
|
58
66
|
IGRF-13 is valid from 1900.0 to 2025.0. This function returns
|
|
@@ -483,6 +491,18 @@ def igrf_declination(
|
|
|
483
491
|
-------
|
|
484
492
|
D : float
|
|
485
493
|
Declination in radians.
|
|
494
|
+
|
|
495
|
+
Examples
|
|
496
|
+
--------
|
|
497
|
+
>>> import numpy as np
|
|
498
|
+
>>> from pytcl.magnetism import igrf_declination
|
|
499
|
+
>>> # Declination at Denver (40°N, 105°W)
|
|
500
|
+
>>> lat = np.radians(40)
|
|
501
|
+
>>> lon = np.radians(-105)
|
|
502
|
+
>>> D = igrf_declination(lat, lon, 1.6, 2023.0)
|
|
503
|
+
>>> # Eastern US has westerly declination (~10-20° W)
|
|
504
|
+
>>> -0.35 < D < 0 # West is negative
|
|
505
|
+
True
|
|
486
506
|
"""
|
|
487
507
|
return igrf(lat, lon, h, year).D
|
|
488
508
|
|
|
@@ -511,6 +531,19 @@ def igrf_inclination(
|
|
|
511
531
|
-------
|
|
512
532
|
I : float
|
|
513
533
|
Inclination in radians.
|
|
534
|
+
|
|
535
|
+
Examples
|
|
536
|
+
--------
|
|
537
|
+
>>> import numpy as np
|
|
538
|
+
>>> from pytcl.magnetism import igrf_inclination
|
|
539
|
+
>>> # Inclination comparison: equator vs pole
|
|
540
|
+
>>> I_eq = igrf_inclination(0, 0, 0, 2023.0) # Equator
|
|
541
|
+
>>> I_pole = igrf_inclination(np.radians(85), 0, 0, 2023.0) # Near pole
|
|
542
|
+
>>> # At equator, inclination is ~0; at poles it's ~90 degrees
|
|
543
|
+
>>> abs(I_eq) < 0.2 # ~11 degrees
|
|
544
|
+
True
|
|
545
|
+
>>> abs(I_pole) > 1.4 # ~80 degrees
|
|
546
|
+
True
|
|
514
547
|
"""
|
|
515
548
|
return igrf(lat, lon, h, year).I
|
|
516
549
|
|
|
@@ -533,6 +566,16 @@ def dipole_moment(coeffs: MagneticCoefficients = IGRF13) -> float:
|
|
|
533
566
|
-----
|
|
534
567
|
The dipole moment is computed from the n=1 Gauss coefficients:
|
|
535
568
|
M = a^3 * sqrt(g10^2 + g11^2 + h11^2)
|
|
569
|
+
|
|
570
|
+
Examples
|
|
571
|
+
--------
|
|
572
|
+
>>> from pytcl.magnetism import dipole_moment, IGRF13
|
|
573
|
+
>>> # Compute Earth's dipole moment from IGRF-13
|
|
574
|
+
>>> M = dipole_moment(IGRF13)
|
|
575
|
+
>>> # Earth's dipole moment is approximately 7.9 × 10^22 A·m²
|
|
576
|
+
>>> # In nT·km³ units, this is about 7.9 × 10^15
|
|
577
|
+
>>> 7e15 < M < 8.5e15
|
|
578
|
+
True
|
|
536
579
|
"""
|
|
537
580
|
a = 6371.2 # Reference radius in km
|
|
538
581
|
g10 = coeffs.g[1, 0]
|
|
@@ -565,6 +608,18 @@ def dipole_axis(
|
|
|
565
608
|
-----
|
|
566
609
|
The geomagnetic pole is where the centered dipole axis
|
|
567
610
|
intersects the Earth's surface.
|
|
611
|
+
|
|
612
|
+
Examples
|
|
613
|
+
--------
|
|
614
|
+
>>> import numpy as np
|
|
615
|
+
>>> from pytcl.magnetism import dipole_axis, IGRF13
|
|
616
|
+
>>> # Compute geomagnetic pole location
|
|
617
|
+
>>> lat, lon = dipole_axis(IGRF13)
|
|
618
|
+
>>> # Geomagnetic north pole is around 80°N, 72°W
|
|
619
|
+
>>> 70 < np.degrees(lat) < 85
|
|
620
|
+
True
|
|
621
|
+
>>> -100 < np.degrees(lon) < -60
|
|
622
|
+
True
|
|
568
623
|
"""
|
|
569
624
|
g10 = coeffs.g[1, 0]
|
|
570
625
|
g11 = coeffs.g[1, 1]
|
|
@@ -611,6 +666,23 @@ def magnetic_north_pole(
|
|
|
611
666
|
-----
|
|
612
667
|
This uses an iterative search starting from the dipole pole.
|
|
613
668
|
The magnetic pole moves over time.
|
|
669
|
+
|
|
670
|
+
Examples
|
|
671
|
+
--------
|
|
672
|
+
>>> import numpy as np
|
|
673
|
+
>>> from pytcl.magnetism import magnetic_north_pole, dipole_axis
|
|
674
|
+
>>> # Magnetic north pole location (2023)
|
|
675
|
+
>>> lat, lon = magnetic_north_pole(2023.0)
|
|
676
|
+
>>> # Should be in Canadian Arctic, around 80-85°N
|
|
677
|
+
>>> 75 < np.degrees(lat) < 90
|
|
678
|
+
True
|
|
679
|
+
>>> -150 < np.degrees(lon) < -60
|
|
680
|
+
True
|
|
681
|
+
>>> # Compare with geomagnetic pole
|
|
682
|
+
>>> geo_lat, geo_lon = dipole_axis()
|
|
683
|
+
>>> # Magnetic pole differs from geomagnetic pole
|
|
684
|
+
>>> abs(lat - geo_lat) > 0.01 # Different locations
|
|
685
|
+
True
|
|
614
686
|
"""
|
|
615
687
|
# Start from dipole pole
|
|
616
688
|
lat, lon = dipole_axis(coeffs)
|
pytcl/magnetism/wmm.py
CHANGED
|
@@ -103,6 +103,14 @@ def create_wmm2020_coefficients() -> MagneticCoefficients:
|
|
|
103
103
|
coeffs : MagneticCoefficients
|
|
104
104
|
WMM2020 spherical harmonic coefficients.
|
|
105
105
|
|
|
106
|
+
Examples
|
|
107
|
+
--------
|
|
108
|
+
>>> coeffs = create_wmm2020_coefficients()
|
|
109
|
+
>>> coeffs.epoch
|
|
110
|
+
2020.0
|
|
111
|
+
>>> coeffs.n_max
|
|
112
|
+
12
|
|
113
|
+
|
|
106
114
|
Notes
|
|
107
115
|
-----
|
|
108
116
|
These are the official WMM2020 coefficients valid from 2020.0 to 2025.0.
|
|
@@ -727,6 +735,19 @@ def magnetic_field_spherical(
|
|
|
727
735
|
to a configurable precision before caching to improve hit rates for
|
|
728
736
|
nearby queries. Use `get_magnetic_cache_info()` to check cache
|
|
729
737
|
statistics and `clear_magnetic_cache()` to free memory.
|
|
738
|
+
|
|
739
|
+
Examples
|
|
740
|
+
--------
|
|
741
|
+
>>> import numpy as np
|
|
742
|
+
>>> from pytcl.magnetism import magnetic_field_spherical
|
|
743
|
+
>>> # Compute at Earth's surface (40°N, 105°W, sea level)
|
|
744
|
+
>>> lat = np.radians(40)
|
|
745
|
+
>>> lon = np.radians(-105)
|
|
746
|
+
>>> r = 6371.2 # Earth mean radius in km
|
|
747
|
+
>>> B_r, B_theta, B_phi = magnetic_field_spherical(lat, lon, r, 2023.0)
|
|
748
|
+
>>> # All components should be on order of tens to tens of thousands of nT
|
|
749
|
+
>>> 20000 < (B_r**2 + B_theta**2 + B_phi**2)**0.5 < 70000
|
|
750
|
+
True
|
|
730
751
|
"""
|
|
731
752
|
if use_cache:
|
|
732
753
|
# Quantize inputs for cache key
|
|
@@ -892,6 +913,21 @@ def magnetic_inclination(
|
|
|
892
913
|
Magnetic inclination in radians.
|
|
893
914
|
Positive = field points into Earth (Northern hemisphere).
|
|
894
915
|
Negative = field points out of Earth (Southern hemisphere).
|
|
916
|
+
|
|
917
|
+
Examples
|
|
918
|
+
--------
|
|
919
|
+
>>> import numpy as np
|
|
920
|
+
>>> from pytcl.magnetism import magnetic_inclination
|
|
921
|
+
>>> # Inclination at 40°N, 105°W (Denver)
|
|
922
|
+
>>> lat = np.radians(40)
|
|
923
|
+
>>> lon = np.radians(-105)
|
|
924
|
+
>>> I = magnetic_inclination(lat, lon, 1.6, 2023.0)
|
|
925
|
+
>>> # Northern hemisphere: inclination should be positive
|
|
926
|
+
>>> I > 0
|
|
927
|
+
True
|
|
928
|
+
>>> # Typical values in US are 50-70 degrees
|
|
929
|
+
>>> 0.8 < I < 1.3 # ~46-74 degrees
|
|
930
|
+
True
|
|
895
931
|
"""
|
|
896
932
|
result = wmm(lat, lon, h, year, coeffs)
|
|
897
933
|
return result.I
|
|
@@ -924,6 +960,22 @@ def magnetic_field_intensity(
|
|
|
924
960
|
-------
|
|
925
961
|
F : float
|
|
926
962
|
Total magnetic field intensity in nT.
|
|
963
|
+
|
|
964
|
+
Examples
|
|
965
|
+
--------
|
|
966
|
+
>>> import numpy as np
|
|
967
|
+
>>> from pytcl.magnetism import magnetic_field_intensity
|
|
968
|
+
>>> # Field intensity at magnetic equator vs pole
|
|
969
|
+
>>> F_eq = magnetic_field_intensity(0, 0, 0, 2023.0) # Equator
|
|
970
|
+
>>> F_pole = magnetic_field_intensity(np.radians(80), 0, 0, 2023.0) # Near pole
|
|
971
|
+
>>> # Field is stronger at poles
|
|
972
|
+
>>> F_pole > F_eq
|
|
973
|
+
True
|
|
974
|
+
>>> # Typical Earth field is 25,000 to 65,000 nT
|
|
975
|
+
>>> 25000 < F_eq < 35000 # Equatorial field is weaker
|
|
976
|
+
True
|
|
977
|
+
>>> 55000 < F_pole < 65000 # Polar field is stronger
|
|
978
|
+
True
|
|
927
979
|
"""
|
|
928
980
|
result = wmm(lat, lon, h, year, coeffs)
|
|
929
981
|
return result.F
|
|
@@ -210,6 +210,13 @@ def pinv_truncated(
|
|
|
210
210
|
A_pinv : ndarray
|
|
211
211
|
Pseudo-inverse of A with shape (n, m).
|
|
212
212
|
|
|
213
|
+
Examples
|
|
214
|
+
--------
|
|
215
|
+
>>> A = np.array([[1, 2], [3, 4], [5, 6]])
|
|
216
|
+
>>> A_pinv = pinv_truncated(A)
|
|
217
|
+
>>> np.allclose(A @ A_pinv @ A, A)
|
|
218
|
+
True
|
|
219
|
+
|
|
213
220
|
See Also
|
|
214
221
|
--------
|
|
215
222
|
numpy.linalg.pinv : Standard pseudo-inverse.
|