nrl-tracker 1.11.0__py3-none-any.whl → 1.12.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 (30) hide show
  1. {nrl_tracker-1.11.0.dist-info → nrl_tracker-1.12.0.dist-info}/METADATA +4 -4
  2. {nrl_tracker-1.11.0.dist-info → nrl_tracker-1.12.0.dist-info}/RECORD +30 -30
  3. pytcl/__init__.py +2 -2
  4. pytcl/assignment_algorithms/network_flow.py +172 -60
  5. pytcl/astronomical/time_systems.py +21 -0
  6. pytcl/containers/cluster_set.py +36 -0
  7. pytcl/coordinate_systems/conversions/geodetic.py +58 -0
  8. pytcl/core/array_utils.py +52 -0
  9. pytcl/gpu/ekf.py +46 -0
  10. pytcl/gpu/kalman.py +16 -0
  11. pytcl/gpu/matrix_utils.py +44 -1
  12. pytcl/gpu/particle_filter.py +33 -0
  13. pytcl/gpu/ukf.py +31 -0
  14. pytcl/gpu/utils.py +15 -0
  15. pytcl/magnetism/igrf.py +72 -0
  16. pytcl/magnetism/wmm.py +52 -0
  17. pytcl/mathematical_functions/basic_matrix/decompositions.py +7 -0
  18. pytcl/mathematical_functions/basic_matrix/special_matrices.py +31 -0
  19. pytcl/mathematical_functions/geometry/geometry.py +33 -0
  20. pytcl/mathematical_functions/interpolation/interpolation.py +83 -0
  21. pytcl/mathematical_functions/signal_processing/detection.py +31 -0
  22. pytcl/mathematical_functions/signal_processing/filters.py +56 -0
  23. pytcl/mathematical_functions/signal_processing/matched_filter.py +32 -1
  24. pytcl/mathematical_functions/special_functions/hypergeometric.py +17 -0
  25. pytcl/mathematical_functions/statistics/estimators.py +71 -0
  26. pytcl/mathematical_functions/transforms/wavelets.py +25 -0
  27. pytcl/navigation/great_circle.py +33 -0
  28. {nrl_tracker-1.11.0.dist-info → nrl_tracker-1.12.0.dist-info}/LICENSE +0 -0
  29. {nrl_tracker-1.11.0.dist-info → nrl_tracker-1.12.0.dist-info}/WHEEL +0 -0
  30. {nrl_tracker-1.11.0.dist-info → nrl_tracker-1.12.0.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
- """Free all cached memory blocks."""
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:
@@ -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.