nrl-tracker 0.21.5__py3-none-any.whl → 0.22.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 (138) hide show
  1. {nrl_tracker-0.21.5.dist-info → nrl_tracker-0.22.0.dist-info}/METADATA +2 -2
  2. nrl_tracker-0.22.0.dist-info/RECORD +150 -0
  3. pytcl/__init__.py +9 -11
  4. pytcl/assignment_algorithms/__init__.py +32 -42
  5. pytcl/assignment_algorithms/data_association.py +9 -10
  6. pytcl/assignment_algorithms/gating.py +7 -5
  7. pytcl/assignment_algorithms/jpda.py +10 -14
  8. pytcl/assignment_algorithms/three_dimensional/__init__.py +6 -8
  9. pytcl/assignment_algorithms/three_dimensional/assignment.py +6 -2
  10. pytcl/assignment_algorithms/two_dimensional/__init__.py +9 -13
  11. pytcl/assignment_algorithms/two_dimensional/assignment.py +5 -2
  12. pytcl/assignment_algorithms/two_dimensional/kbest.py +9 -9
  13. pytcl/astronomical/__init__.py +130 -89
  14. pytcl/astronomical/ephemerides.py +524 -0
  15. pytcl/astronomical/lambert.py +6 -15
  16. pytcl/astronomical/orbital_mechanics.py +1 -3
  17. pytcl/astronomical/reference_frames.py +1 -3
  18. pytcl/astronomical/relativity.py +466 -0
  19. pytcl/astronomical/time_systems.py +2 -1
  20. pytcl/atmosphere/__init__.py +12 -14
  21. pytcl/atmosphere/models.py +5 -5
  22. pytcl/clustering/__init__.py +28 -36
  23. pytcl/clustering/dbscan.py +5 -2
  24. pytcl/clustering/gaussian_mixture.py +10 -10
  25. pytcl/clustering/hierarchical.py +7 -7
  26. pytcl/clustering/kmeans.py +7 -5
  27. pytcl/containers/__init__.py +29 -43
  28. pytcl/containers/cluster_set.py +13 -20
  29. pytcl/containers/covertree.py +8 -2
  30. pytcl/containers/kd_tree.py +6 -2
  31. pytcl/containers/measurement_set.py +11 -16
  32. pytcl/containers/rtree.py +8 -7
  33. pytcl/containers/track_list.py +13 -13
  34. pytcl/containers/vptree.py +7 -2
  35. pytcl/coordinate_systems/__init__.py +69 -74
  36. pytcl/coordinate_systems/conversions/__init__.py +20 -24
  37. pytcl/coordinate_systems/conversions/geodetic.py +7 -17
  38. pytcl/coordinate_systems/conversions/spherical.py +4 -2
  39. pytcl/coordinate_systems/jacobians/__init__.py +10 -12
  40. pytcl/coordinate_systems/jacobians/jacobians.py +2 -1
  41. pytcl/coordinate_systems/projections/__init__.py +27 -23
  42. pytcl/coordinate_systems/projections/projections.py +14 -39
  43. pytcl/coordinate_systems/rotations/__init__.py +20 -22
  44. pytcl/coordinate_systems/rotations/rotations.py +3 -4
  45. pytcl/core/__init__.py +16 -22
  46. pytcl/core/array_utils.py +7 -7
  47. pytcl/core/constants.py +1 -3
  48. pytcl/core/validation.py +13 -19
  49. pytcl/dynamic_estimation/__init__.py +77 -86
  50. pytcl/dynamic_estimation/imm.py +10 -15
  51. pytcl/dynamic_estimation/information_filter.py +8 -6
  52. pytcl/dynamic_estimation/kalman/__init__.py +40 -48
  53. pytcl/dynamic_estimation/kalman/extended.py +4 -5
  54. pytcl/dynamic_estimation/kalman/linear.py +7 -3
  55. pytcl/dynamic_estimation/kalman/square_root.py +7 -8
  56. pytcl/dynamic_estimation/kalman/unscented.py +8 -6
  57. pytcl/dynamic_estimation/particle_filters/__init__.py +12 -14
  58. pytcl/dynamic_estimation/particle_filters/bootstrap.py +8 -8
  59. pytcl/dynamic_estimation/smoothers.py +9 -10
  60. pytcl/dynamic_models/__init__.py +37 -41
  61. pytcl/dynamic_models/continuous_time/__init__.py +11 -11
  62. pytcl/dynamic_models/continuous_time/dynamics.py +4 -2
  63. pytcl/dynamic_models/discrete_time/__init__.py +11 -17
  64. pytcl/dynamic_models/process_noise/__init__.py +11 -17
  65. pytcl/dynamic_models/process_noise/polynomial.py +2 -6
  66. pytcl/gravity/__init__.py +55 -65
  67. pytcl/gravity/clenshaw.py +4 -7
  68. pytcl/gravity/egm.py +9 -6
  69. pytcl/gravity/models.py +1 -3
  70. pytcl/gravity/spherical_harmonics.py +6 -11
  71. pytcl/gravity/tides.py +9 -17
  72. pytcl/magnetism/__init__.py +26 -36
  73. pytcl/magnetism/emm.py +7 -13
  74. pytcl/magnetism/igrf.py +5 -6
  75. pytcl/magnetism/wmm.py +4 -10
  76. pytcl/mathematical_functions/__init__.py +69 -87
  77. pytcl/mathematical_functions/basic_matrix/__init__.py +25 -19
  78. pytcl/mathematical_functions/basic_matrix/decompositions.py +6 -5
  79. pytcl/mathematical_functions/basic_matrix/special_matrices.py +2 -1
  80. pytcl/mathematical_functions/combinatorics/__init__.py +18 -14
  81. pytcl/mathematical_functions/combinatorics/combinatorics.py +5 -4
  82. pytcl/mathematical_functions/geometry/__init__.py +15 -15
  83. pytcl/mathematical_functions/geometry/geometry.py +10 -15
  84. pytcl/mathematical_functions/interpolation/__init__.py +11 -13
  85. pytcl/mathematical_functions/interpolation/interpolation.py +8 -5
  86. pytcl/mathematical_functions/numerical_integration/__init__.py +16 -10
  87. pytcl/mathematical_functions/numerical_integration/quadrature.py +6 -2
  88. pytcl/mathematical_functions/signal_processing/__init__.py +42 -30
  89. pytcl/mathematical_functions/signal_processing/detection.py +9 -9
  90. pytcl/mathematical_functions/signal_processing/filters.py +7 -8
  91. pytcl/mathematical_functions/signal_processing/matched_filter.py +8 -7
  92. pytcl/mathematical_functions/special_functions/__init__.py +75 -77
  93. pytcl/mathematical_functions/special_functions/bessel.py +2 -1
  94. pytcl/mathematical_functions/special_functions/debye.py +4 -2
  95. pytcl/mathematical_functions/special_functions/elliptic.py +3 -4
  96. pytcl/mathematical_functions/special_functions/error_functions.py +2 -1
  97. pytcl/mathematical_functions/special_functions/gamma_functions.py +3 -4
  98. pytcl/mathematical_functions/special_functions/hypergeometric.py +2 -1
  99. pytcl/mathematical_functions/special_functions/lambert_w.py +3 -4
  100. pytcl/mathematical_functions/special_functions/marcum_q.py +2 -1
  101. pytcl/mathematical_functions/statistics/__init__.py +27 -31
  102. pytcl/mathematical_functions/statistics/distributions.py +21 -40
  103. pytcl/mathematical_functions/statistics/estimators.py +3 -4
  104. pytcl/mathematical_functions/transforms/__init__.py +45 -51
  105. pytcl/mathematical_functions/transforms/fourier.py +5 -2
  106. pytcl/mathematical_functions/transforms/stft.py +8 -11
  107. pytcl/mathematical_functions/transforms/wavelets.py +13 -20
  108. pytcl/navigation/__init__.py +96 -102
  109. pytcl/navigation/geodesy.py +13 -33
  110. pytcl/navigation/great_circle.py +7 -13
  111. pytcl/navigation/ins.py +12 -16
  112. pytcl/navigation/ins_gnss.py +24 -37
  113. pytcl/navigation/rhumb.py +7 -12
  114. pytcl/performance_evaluation/__init__.py +21 -25
  115. pytcl/performance_evaluation/estimation_metrics.py +3 -1
  116. pytcl/performance_evaluation/track_metrics.py +4 -4
  117. pytcl/plotting/__init__.py +30 -38
  118. pytcl/plotting/coordinates.py +8 -18
  119. pytcl/plotting/ellipses.py +5 -2
  120. pytcl/plotting/metrics.py +5 -10
  121. pytcl/plotting/tracks.py +7 -12
  122. pytcl/static_estimation/__init__.py +37 -41
  123. pytcl/static_estimation/least_squares.py +5 -4
  124. pytcl/static_estimation/maximum_likelihood.py +8 -5
  125. pytcl/static_estimation/robust.py +5 -2
  126. pytcl/terrain/__init__.py +28 -34
  127. pytcl/terrain/dem.py +6 -9
  128. pytcl/terrain/loaders.py +9 -14
  129. pytcl/terrain/visibility.py +4 -8
  130. pytcl/trackers/__init__.py +17 -25
  131. pytcl/trackers/hypothesis.py +8 -8
  132. pytcl/trackers/mht.py +18 -24
  133. pytcl/trackers/multi_target.py +8 -6
  134. pytcl/trackers/single_target.py +5 -2
  135. nrl_tracker-0.21.5.dist-info/RECORD +0 -148
  136. {nrl_tracker-0.21.5.dist-info → nrl_tracker-0.22.0.dist-info}/LICENSE +0 -0
  137. {nrl_tracker-0.21.5.dist-info → nrl_tracker-0.22.0.dist-info}/WHEEL +0 -0
  138. {nrl_tracker-0.21.5.dist-info → nrl_tracker-0.22.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,466 @@
1
+ """Relativistic corrections for precision astronomy and satellite positioning.
2
+
3
+ This module provides utilities for computing relativistic effects in orbital mechanics,
4
+ including gravitational time dilation, Shapiro delay, and coordinate transformations
5
+ in the Schwarzschild metric. These effects are critical for high-precision applications
6
+ such as GPS, pulsar timing, and celestial mechanics.
7
+
8
+ Key Physical Constants:
9
+ - Schwarzschild radius: r_s = 2GM/c^2
10
+ - Gravitational parameter for Earth: μ = GM = 3.986004418e14 m^3/s^2
11
+ - Speed of light: c = 299792458 m/s
12
+ - Gravitational constant: G = 6.67430e-11 m^3/(kg·s^2)
13
+
14
+ References:
15
+ - Soffel et al. (2003): The IAU 2000 Resolutions for Astrometry, Celestial Mechanics,
16
+ and Reference Frames
17
+ - Will, C. M. (2014): The Confrontation between General Relativity and Experiment
18
+ - Ries et al. (1992): Preliminary Analysis of LAGEOS II Observations for the
19
+ Determination of Relativistic Effects
20
+ """
21
+
22
+ import numpy as np
23
+
24
+ # Physical constants (CODATA 2018 values)
25
+ C_LIGHT = 299792458.0 # Speed of light (m/s)
26
+ G_GRAV = 6.67430e-11 # Gravitational constant (m^3/(kg·s^2))
27
+ GM_EARTH = 3.986004418e14 # Gravitational parameter for Earth (m^3/s^2)
28
+ GM_SUN = 1.32712440018e20 # Gravitational parameter for Sun (m^3/s^2)
29
+ AU = 1.495978707e11 # Astronomical unit (m)
30
+
31
+
32
+ def schwarzschild_radius(mass: float) -> float:
33
+ """Compute Schwarzschild radius for a given mass.
34
+
35
+ The Schwarzschild radius is the radius at which an object becomes a black hole.
36
+ It is given by r_s = 2GM/c^2.
37
+
38
+ Parameters
39
+ ----------
40
+ mass : float
41
+ Mass of the object (kg)
42
+
43
+ Returns
44
+ -------
45
+ float
46
+ Schwarzschild radius (m)
47
+
48
+ Examples
49
+ --------
50
+ >>> r_s_earth = schwarzschild_radius(5.972e24) # Earth's mass
51
+ >>> print(f"Earth's Schwarzschild radius: {r_s_earth:.3e} m")
52
+ Earth's Schwarzschild radius: 8.870e-03 m
53
+
54
+ >>> r_s_sun = schwarzschild_radius(1.989e30) # Sun's mass
55
+ >>> print(f"Sun's Schwarzschild radius: {r_s_sun:.3e} m")
56
+ Sun's Schwarzschild radius: 2.952e+03 m
57
+ """
58
+ return 2.0 * G_GRAV * mass / (C_LIGHT**2)
59
+
60
+
61
+ def gravitational_time_dilation(r: float, gm: float = GM_EARTH) -> float:
62
+ """Compute gravitational time dilation factor sqrt(1 - 2GM/(rc^2)).
63
+
64
+ In general relativity, time passes slower in stronger gravitational fields.
65
+ This function computes the metric coefficient g_00 for the Schwarzschild metric,
66
+ which determines proper time relative to coordinate time.
67
+
68
+ Parameters
69
+ ----------
70
+ r : float
71
+ Distance from the gravitational body (m)
72
+ gm : float, optional
73
+ Gravitational parameter GM of the body (m^3/s^2).
74
+ Default is GM_EARTH.
75
+
76
+ Returns
77
+ -------
78
+ float
79
+ Time dilation factor in [0, 1]. A value less than 1 indicates time
80
+ passes slower at radius r compared to infinity.
81
+
82
+ Raises
83
+ ------
84
+ ValueError
85
+ If r is less than or equal to Schwarzschild radius
86
+
87
+ Examples
88
+ --------
89
+ Compute time dilation at Earth's surface (6371 km):
90
+
91
+ >>> r_earth = 6.371e6 # meters
92
+ >>> dilation = gravitational_time_dilation(r_earth)
93
+ >>> print(f"Time dilation at surface: {dilation:.15f}")
94
+ Time dilation at surface: 0.999999999300693
95
+
96
+ At GPS orbital altitude (~20,200 km):
97
+
98
+ >>> r_gps = 26.56e6 # meters
99
+ >>> dilation_gps = gravitational_time_dilation(r_gps)
100
+ >>> time_shift = (1 - dilation_gps) * 86400 * 1e9 # nanoseconds per day
101
+ >>> print(f"Time shift: {time_shift:.1f} ns/day")
102
+ """
103
+ r_s = schwarzschild_radius(gm / G_GRAV)
104
+ if r <= r_s:
105
+ raise ValueError(f"Radius {r} m is at or within Schwarzschild radius {r_s} m")
106
+
107
+ dilation_squared = 1.0 - 2.0 * gm / (C_LIGHT**2 * r)
108
+ return np.sqrt(dilation_squared)
109
+
110
+
111
+ def proper_time_rate(v: float, r: float, gm: float = GM_EARTH) -> float:
112
+ """Compute proper time rate accounting for both velocity and gravity.
113
+
114
+ The proper time rate combines special relativistic time dilation from velocity
115
+ and general relativistic time dilation from the gravitational potential.
116
+
117
+ d(tau)/d(t) = sqrt(1 - v^2/c^2) * sqrt(1 - 2GM/(rc^2))
118
+
119
+ For small velocities and weak fields: 1 - v^2/(2c^2) - GM/(rc^2)
120
+
121
+ Parameters
122
+ ----------
123
+ v : float
124
+ Velocity magnitude (m/s)
125
+ r : float
126
+ Distance from gravitational body (m)
127
+ gm : float, optional
128
+ Gravitational parameter GM (m^3/s^2). Default is GM_EARTH.
129
+
130
+ Returns
131
+ -------
132
+ float
133
+ Proper time rate. A value less than 1 indicates proper time passes
134
+ slower than coordinate time.
135
+
136
+ Examples
137
+ --------
138
+ Proper time rate for a GPS satellite at ~3.87 km/s and 26.56 Mm altitude:
139
+
140
+ >>> v_gps = 3870.0 # m/s
141
+ >>> r_gps = 26.56e6 # m
142
+ >>> rate = proper_time_rate(v_gps, r_gps)
143
+ >>> print(f"Proper time rate: {rate:.15f}")
144
+ >>> time_shift = (1 - rate) * 86400 # seconds per day
145
+ >>> print(f"Daily time shift: {time_shift:.3f} s/day")
146
+ """
147
+ # Special relativistic effect
148
+ special_rel = 1.0 - (v**2) / (2.0 * C_LIGHT**2)
149
+
150
+ # General relativistic effect
151
+ general_rel = -gm / (C_LIGHT**2 * r)
152
+
153
+ return special_rel + general_rel
154
+
155
+
156
+ def shapiro_delay(
157
+ observer_pos: np.ndarray,
158
+ light_source_pos: np.ndarray,
159
+ gravitating_body_pos: np.ndarray,
160
+ gm: float = GM_SUN,
161
+ ) -> float:
162
+ """Compute Shapiro time delay for light propagation through gravitational field.
163
+
164
+ The Shapiro delay is the additional propagation time experienced by light
165
+ traveling through a gravitational field, compared to flat spacetime.
166
+
167
+ delay = (2GM/c^3) * ln((r_o + r_s + r_os) / (r_o + r_s - r_os))
168
+
169
+ where r_o is distance from body to observer, r_s is distance from body to
170
+ source, and r_os is distance from observer to source.
171
+
172
+ Parameters
173
+ ----------
174
+ observer_pos : np.ndarray
175
+ Position of observer (m), shape (3,)
176
+ light_source_pos : np.ndarray
177
+ Position of light source (m), shape (3,)
178
+ gravitating_body_pos : np.ndarray
179
+ Position of gravitating body (m), shape (3,)
180
+ gm : float, optional
181
+ Gravitational parameter GM (m^3/s^2). Default is GM_SUN.
182
+
183
+ Returns
184
+ -------
185
+ float
186
+ Shapiro delay (seconds)
187
+
188
+ Examples
189
+ --------
190
+ Earth-Sun-Spacecraft signal at superior conjunction (worst case):
191
+
192
+ >>> # Simplified geometry: Sun at origin, Earth at 1 AU, spacecraft beyond at distance
193
+ >>> sun_pos = np.array([0.0, 0.0, 0.0])
194
+ >>> earth_pos = np.array([1.496e11, 0.0, 0.0]) # 1 AU
195
+ >>> spacecraft_pos = np.array([1.496e11, 1.0e11, 0.0]) # Far from sun
196
+ >>> delay = shapiro_delay(earth_pos, spacecraft_pos, sun_pos, GM_SUN)
197
+ >>> print(f"Shapiro delay: {delay:.3e} seconds")
198
+ >>> print(f"Shapiro delay: {delay*1e6:.1f} microseconds")
199
+ """
200
+ # Compute distances
201
+ r_observer = np.linalg.norm(observer_pos - gravitating_body_pos)
202
+ r_source = np.linalg.norm(light_source_pos - gravitating_body_pos)
203
+ r_os = np.linalg.norm(observer_pos - light_source_pos)
204
+
205
+ # Shapiro delay formula (second-order PN)
206
+ # Check for valid geometry (gravitating body should affect path)
207
+ # The formula is valid when the impact parameter is close to the body
208
+ numerator = r_observer + r_source + r_os
209
+ denominator = r_observer + r_source - r_os
210
+
211
+ # If denominator <= 0, it means the path doesn't pass near the gravitating body
212
+ if denominator <= 0.0:
213
+ # Return zero delay if geometry is invalid (light path doesn't bend)
214
+ return 0.0
215
+
216
+ delay = (2.0 * gm / (C_LIGHT**3)) * np.log(numerator / denominator)
217
+ return delay
218
+
219
+
220
+ def schwarzschild_precession_per_orbit(a: float, e: float, gm: float = GM_SUN) -> float:
221
+ """Compute perihelion precession per orbit due to general relativity.
222
+
223
+ The advance of perihelion for an orbit around a central mass M is:
224
+
225
+ Δφ = (6π * GM) / (c^2 * a * (1 - e^2))
226
+
227
+ This effect is a key test of general relativity. For Mercury,
228
+ the predicted precession is ~43 arcseconds per century.
229
+
230
+ Parameters
231
+ ----------
232
+ a : float
233
+ Semi-major axis (m)
234
+ e : float
235
+ Eccentricity (dimensionless), must be in [0, 1)
236
+ gm : float, optional
237
+ Gravitational parameter GM (m^3/s^2). Default is GM_SUN.
238
+
239
+ Returns
240
+ -------
241
+ float
242
+ Perihelion precession per orbit (radians)
243
+
244
+ Examples
245
+ --------
246
+ Mercury's perihelion precession (GR contribution):
247
+
248
+ >>> a_mercury = 0.38709927 * AU # Semi-major axis in meters
249
+ >>> e_mercury = 0.20563593 # Eccentricity
250
+ >>> precession_rad = schwarzschild_precession_per_orbit(a_mercury, e_mercury, GM_SUN)
251
+ >>> precession_arcsec = precession_rad * 206265 # Convert to arcseconds
252
+ >>> orbital_period = 87.969 # days
253
+ >>> centuries = 36525 / orbital_period # Orbits per century
254
+ >>> precession_per_century = precession_arcsec * centuries
255
+ >>> print(f"GR perihelion precession: {precession_per_century:.1f} arcsec/century")
256
+ GR perihelion precession: 42.98 arcsec/century
257
+ """
258
+ if e < 0 or e >= 1:
259
+ raise ValueError(f"Eccentricity {e} must be in [0, 1)")
260
+
261
+ precession = (6.0 * np.pi * gm) / (C_LIGHT**2 * a * (1.0 - e**2))
262
+ return precession
263
+
264
+
265
+ def post_newtonian_acceleration(
266
+ r_vec: np.ndarray, v_vec: np.ndarray, gm: float = GM_EARTH
267
+ ) -> np.ndarray:
268
+ """Compute post-Newtonian acceleration corrections (1PN order).
269
+
270
+ Extends Newtonian gravity with first-order post-Newtonian corrections.
271
+
272
+ a_PN = -GM/r^2 * u_r + a_1PN
273
+
274
+ where a_1PN includes velocity-dependent and metric perturbation terms.
275
+
276
+ Parameters
277
+ ----------
278
+ r_vec : np.ndarray
279
+ Position vector (m), shape (3,)
280
+ v_vec : np.ndarray
281
+ Velocity vector (m/s), shape (3,)
282
+ gm : float, optional
283
+ Gravitational parameter GM (m^3/s^2). Default is GM_EARTH.
284
+
285
+ Returns
286
+ -------
287
+ np.ndarray
288
+ Total acceleration including 1PN corrections (m/s^2), shape (3,)
289
+
290
+ Examples
291
+ --------
292
+ Compare Newtonian and PN acceleration for LEO satellite:
293
+
294
+ >>> r = np.array([6.678e6, 0.0, 0.0]) # ~300 km altitude
295
+ >>> v = np.array([0.0, 7.7e3, 0.0]) # Circular orbit velocity
296
+ >>> a_total = post_newtonian_acceleration(r, v)
297
+ >>> a_newt = -GM_EARTH / np.linalg.norm(r)**3 * r
298
+ >>> correction_ratio = np.linalg.norm(a_total - a_newt) / np.linalg.norm(a_newt)
299
+ >>> print(f"PN correction: {correction_ratio*1e6:.1f} ppm")
300
+ """
301
+ r = np.linalg.norm(r_vec)
302
+ v_squared = np.sum(v_vec**2)
303
+
304
+ # Unit vector
305
+ u_r = r_vec / r
306
+
307
+ # Newtonian acceleration
308
+ a_newt = -gm / (r**2) * u_r
309
+
310
+ # 1PN corrections (in m/s^2)
311
+ c2 = C_LIGHT**2
312
+
313
+ # Term 1: Velocity squared effect on metric
314
+ term1 = (gm / c2) * (2.0 * v_squared / r - 4.0 * gm / r) * u_r / r
315
+
316
+ # Term 2: Radial velocity coupling
317
+ v_dot_r = np.dot(v_vec, u_r)
318
+ term2 = (4.0 * gm / c2) * v_dot_r * v_vec / r
319
+
320
+ # Combine corrections (these are small corrections to Newtonian acceleration)
321
+ a_1pn = term1 + term2
322
+
323
+ return a_newt + a_1pn
324
+
325
+
326
+ def geodetic_precession(a: float, e: float, inclination: float, gm: float = GM_EARTH) -> float:
327
+ """Compute geodetic (de Sitter) precession rate of orbital plane.
328
+
329
+ The orbital plane of a satellite precesses due to frame-dragging effects
330
+ and spacetime curvature. The geodetic precession rate is:
331
+
332
+ Ω_geodetic = -GM/(c^2 * a^3 * (1 - e^2)^2) * cos(i)
333
+
334
+ Parameters
335
+ ----------
336
+ a : float
337
+ Semi-major axis (m)
338
+ e : float
339
+ Eccentricity (dimensionless)
340
+ inclination : float
341
+ Orbital inclination (radians)
342
+ gm : float, optional
343
+ Gravitational parameter (m^3/s^2). Default is GM_EARTH.
344
+
345
+ Returns
346
+ -------
347
+ float
348
+ Geodetic precession rate (radians per orbit)
349
+
350
+ Examples
351
+ --------
352
+ Geodetic precession for a typical Earth satellite:
353
+
354
+ >>> a = 6.678e6 # ~300 km altitude
355
+ >>> e = 0.0 # Circular
356
+ >>> i = np.radians(51.6) # ISS-like inclination
357
+ >>> rate = geodetic_precession(a, e, i)
358
+ >>> print(f"Precession per orbit: {rate*206265:.3f} arcsec")
359
+ """
360
+ p = a * (1.0 - e**2)
361
+ precession = -(gm / (C_LIGHT**2 * p**2)) * np.cos(inclination)
362
+ return precession
363
+
364
+
365
+ def lense_thirring_precession(
366
+ a: float, e: float, inclination: float, angular_momentum: float, gm: float = GM_EARTH
367
+ ) -> float:
368
+ """Compute Lense-Thirring (frame-dragging) precession of orbital node.
369
+
370
+ A rotating central body drags the orbital plane of nearby objects.
371
+ The nodal precession rate due to this effect is:
372
+
373
+ Ω_LT = (2GM * J_2 * a * ω) / (c^2 * p^2) * f(e, i)
374
+
375
+ where J_2 is the quadrupole moment, ω is angular velocity, and f depends
376
+ on eccentricity and inclination.
377
+
378
+ Parameters
379
+ ----------
380
+ a : float
381
+ Semi-major axis (m)
382
+ e : float
383
+ Eccentricity (dimensionless)
384
+ inclination : float
385
+ Orbital inclination (radians)
386
+ angular_momentum : float
387
+ Angular momentum of central body (kg·m^2/s)
388
+ gm : float, optional
389
+ Gravitational parameter (m^3/s^2). Default is GM_EARTH.
390
+
391
+ Returns
392
+ -------
393
+ float
394
+ Lense-Thirring precession rate (radians per orbit)
395
+
396
+ Notes
397
+ -----
398
+ This is a simplified version. For Earth, J_2 effects typically dominate
399
+ classical nodal precession, while Lense-Thirring is a small correction
400
+ (~1 arcsec per year for typical satellites).
401
+
402
+ Examples
403
+ --------
404
+ Lense-Thirring effect for LAGEOS satellite:
405
+
406
+ >>> # LAGEOS parameters
407
+ >>> a = 12.27e6 # Semi-major axis
408
+ >>> e = 0.0045
409
+ >>> i = np.radians(109.9)
410
+ >>> L_earth = 7.05e33 # Earth's angular momentum
411
+ >>> rate = lense_thirring_precession(a, e, i, L_earth)
412
+ >>> print(f"LT precession per orbit: {rate*206265*1e3:.1f} milliarcsec")
413
+ """
414
+ p = a * (1.0 - e**2)
415
+
416
+ # Simplified Lense-Thirring term (second-order PN effect)
417
+ # For a sphere: Lense-Thirring parameter = 2GM*L/(c^2*M*r^3)
418
+ precession = (2.0 * angular_momentum * gm) / (C_LIGHT**2 * p**3)
419
+
420
+ return precession
421
+
422
+
423
+ def relativistic_range_correction(
424
+ distance: float, relative_velocity: float, gm: float = GM_EARTH
425
+ ) -> float:
426
+ """Compute relativistic range correction for ranging measurements.
427
+
428
+ When measuring distance to a satellite or spacecraft using ranging
429
+ (e.g., laser ranging), relativistic effects introduce corrections to
430
+ the measured range.
431
+
432
+ The main contributions are:
433
+ - Gravitational time dilation
434
+ - Relativistic Doppler effect
435
+
436
+ Parameters
437
+ ----------
438
+ distance : float
439
+ Distance to object (m)
440
+ relative_velocity : float
441
+ Radial velocity component (m/s, positive = receding)
442
+ gm : float, optional
443
+ Gravitational parameter (m^3/s^2). Default is GM_EARTH.
444
+
445
+ Returns
446
+ -------
447
+ float
448
+ Range correction (m)
449
+
450
+ Examples
451
+ --------
452
+ Range correction for lunar laser ranging:
453
+
454
+ >>> distance_to_moon = 3.84e8 # meters
455
+ >>> radial_velocity = 0.0 # Average over orbit
456
+ >>> correction = relativistic_range_correction(distance_to_moon, radial_velocity, GM_EARTH)
457
+ >>> print(f"Range correction: {correction:.1f} m")
458
+ """
459
+ # Gravitational correction (positive because the signal is delayed)
460
+ # Uses weak-field approximation
461
+ grav_correction = gm / (C_LIGHT**2)
462
+
463
+ # Doppler correction (second order effect, small)
464
+ doppler_correction = (relative_velocity**2) / (3.0 * C_LIGHT**2)
465
+
466
+ return grav_correction + doppler_correction
@@ -11,7 +11,8 @@ This module provides conversions between various time systems:
11
11
  - Sidereal time (GMST, GAST)
12
12
  """
13
13
 
14
- from typing import List, Tuple
14
+ from typing import List
15
+ from typing import Tuple
15
16
 
16
17
  import numpy as np
17
18
 
@@ -5,20 +5,18 @@ This module provides standard atmosphere models used for computing
5
5
  temperature, pressure, density, and other properties at various altitudes.
6
6
  """
7
7
 
8
- from pytcl.atmosphere.models import ( # Constants
9
- G0,
10
- GAMMA,
11
- P0,
12
- RHO0,
13
- T0,
14
- AtmosphereState,
15
- R,
16
- altitude_from_pressure,
17
- isa_atmosphere,
18
- mach_number,
19
- true_airspeed_from_mach,
20
- us_standard_atmosphere_1976,
21
- )
8
+ from pytcl.atmosphere.models import G0 # Constants
9
+ from pytcl.atmosphere.models import GAMMA
10
+ from pytcl.atmosphere.models import P0
11
+ from pytcl.atmosphere.models import RHO0
12
+ from pytcl.atmosphere.models import T0
13
+ from pytcl.atmosphere.models import AtmosphereState
14
+ from pytcl.atmosphere.models import R
15
+ from pytcl.atmosphere.models import altitude_from_pressure
16
+ from pytcl.atmosphere.models import isa_atmosphere
17
+ from pytcl.atmosphere.models import mach_number
18
+ from pytcl.atmosphere.models import true_airspeed_from_mach
19
+ from pytcl.atmosphere.models import us_standard_atmosphere_1976
22
20
 
23
21
  __all__ = [
24
22
  "AtmosphereState",
@@ -5,10 +5,12 @@ This module provides standard atmosphere models used for computing
5
5
  temperature, pressure, and density at various altitudes.
6
6
  """
7
7
 
8
- from typing import NamedTuple, Tuple
8
+ from typing import NamedTuple
9
+ from typing import Tuple
9
10
 
10
11
  import numpy as np
11
- from numpy.typing import ArrayLike, NDArray
12
+ from numpy.typing import ArrayLike
13
+ from numpy.typing import NDArray
12
14
 
13
15
 
14
16
  class AtmosphereState(NamedTuple):
@@ -218,9 +220,7 @@ def isa_atmosphere(
218
220
  strat_mask = altitude > h_trop
219
221
  temperature[strat_mask] = T_trop + temperature_offset
220
222
  # Pressure at tropopause
221
- P_trop = P0 * ((T0 + temperature_offset) / (T_trop + temperature_offset)) ** (
222
- G0 / (R * L)
223
- )
223
+ P_trop = P0 * ((T0 + temperature_offset) / (T_trop + temperature_offset)) ** (G0 / (R * L))
224
224
  pressure[strat_mask] = P_trop * np.exp(
225
225
  -G0 * (altitude[strat_mask] - h_trop) / (R * (T_trop + temperature_offset))
226
226
  )
@@ -6,42 +6,34 @@ commonly used in multi-target tracking for hypothesis management and
6
6
  track clustering.
7
7
  """
8
8
 
9
- from pytcl.clustering.dbscan import (
10
- DBSCANResult,
11
- compute_neighbors,
12
- dbscan,
13
- dbscan_predict,
14
- )
15
- from pytcl.clustering.gaussian_mixture import (
16
- GaussianComponent,
17
- GaussianMixture,
18
- MergeResult,
19
- ReductionResult,
20
- merge_gaussians,
21
- moment_match,
22
- prune_mixture,
23
- reduce_mixture_runnalls,
24
- reduce_mixture_west,
25
- runnalls_merge_cost,
26
- west_merge_cost,
27
- )
28
- from pytcl.clustering.hierarchical import (
29
- DendrogramNode,
30
- HierarchicalResult,
31
- LinkageType,
32
- agglomerative_clustering,
33
- compute_distance_matrix,
34
- cut_dendrogram,
35
- fcluster,
36
- )
37
- from pytcl.clustering.kmeans import (
38
- KMeansResult,
39
- assign_clusters,
40
- kmeans,
41
- kmeans_elbow,
42
- kmeans_plusplus_init,
43
- update_centers,
44
- )
9
+ from pytcl.clustering.dbscan import DBSCANResult
10
+ from pytcl.clustering.dbscan import compute_neighbors
11
+ from pytcl.clustering.dbscan import dbscan
12
+ from pytcl.clustering.dbscan import dbscan_predict
13
+ from pytcl.clustering.gaussian_mixture import GaussianComponent
14
+ from pytcl.clustering.gaussian_mixture import GaussianMixture
15
+ from pytcl.clustering.gaussian_mixture import MergeResult
16
+ from pytcl.clustering.gaussian_mixture import ReductionResult
17
+ from pytcl.clustering.gaussian_mixture import merge_gaussians
18
+ from pytcl.clustering.gaussian_mixture import moment_match
19
+ from pytcl.clustering.gaussian_mixture import prune_mixture
20
+ from pytcl.clustering.gaussian_mixture import reduce_mixture_runnalls
21
+ from pytcl.clustering.gaussian_mixture import reduce_mixture_west
22
+ from pytcl.clustering.gaussian_mixture import runnalls_merge_cost
23
+ from pytcl.clustering.gaussian_mixture import west_merge_cost
24
+ from pytcl.clustering.hierarchical import DendrogramNode
25
+ from pytcl.clustering.hierarchical import HierarchicalResult
26
+ from pytcl.clustering.hierarchical import LinkageType
27
+ from pytcl.clustering.hierarchical import agglomerative_clustering
28
+ from pytcl.clustering.hierarchical import compute_distance_matrix
29
+ from pytcl.clustering.hierarchical import cut_dendrogram
30
+ from pytcl.clustering.hierarchical import fcluster
31
+ from pytcl.clustering.kmeans import KMeansResult
32
+ from pytcl.clustering.kmeans import assign_clusters
33
+ from pytcl.clustering.kmeans import kmeans
34
+ from pytcl.clustering.kmeans import kmeans_elbow
35
+ from pytcl.clustering.kmeans import kmeans_plusplus_init
36
+ from pytcl.clustering.kmeans import update_centers
45
37
 
46
38
  __all__ = [
47
39
  # Gaussian mixture
@@ -12,11 +12,14 @@ References
12
12
  with Noise," KDD 1996.
13
13
  """
14
14
 
15
- from typing import List, NamedTuple, Set
15
+ from typing import List
16
+ from typing import NamedTuple
17
+ from typing import Set
16
18
 
17
19
  import numpy as np
18
20
  from numba import njit
19
- from numpy.typing import ArrayLike, NDArray
21
+ from numpy.typing import ArrayLike
22
+ from numpy.typing import NDArray
20
23
 
21
24
 
22
25
  class DBSCANResult(NamedTuple):
@@ -13,10 +13,14 @@ References
13
13
  Journal of the Royal Statistical Society, Series B, vol. 55, no. 2, 1993.
14
14
  """
15
15
 
16
- from typing import List, NamedTuple, Optional, Tuple
16
+ from typing import List
17
+ from typing import NamedTuple
18
+ from typing import Optional
19
+ from typing import Tuple
17
20
 
18
21
  import numpy as np
19
- from numpy.typing import ArrayLike, NDArray
22
+ from numpy.typing import ArrayLike
23
+ from numpy.typing import NDArray
20
24
 
21
25
 
22
26
  class GaussianComponent(NamedTuple):
@@ -297,8 +301,7 @@ def prune_mixture(
297
301
  total_weight = sum(c.weight for c in surviving)
298
302
  if total_weight > 0:
299
303
  surviving = [
300
- GaussianComponent(c.weight / total_weight, c.mean, c.covariance)
301
- for c in surviving
304
+ GaussianComponent(c.weight / total_weight, c.mean, c.covariance) for c in surviving
302
305
  ]
303
306
 
304
307
  return surviving
@@ -392,8 +395,7 @@ def reduce_mixture_runnalls(
392
395
  total_weight = sum(c.weight for c in working)
393
396
  if total_weight > 0:
394
397
  working = [
395
- GaussianComponent(c.weight / total_weight, c.mean, c.covariance)
396
- for c in working
398
+ GaussianComponent(c.weight / total_weight, c.mean, c.covariance) for c in working
397
399
  ]
398
400
 
399
401
  return ReductionResult(working, n_original, len(working), total_cost)
@@ -533,8 +535,7 @@ def reduce_mixture_west(
533
535
  total_weight = sum(c.weight for c in working)
534
536
  if total_weight > 0:
535
537
  working = [
536
- GaussianComponent(c.weight / total_weight, c.mean, c.covariance)
537
- for c in working
538
+ GaussianComponent(c.weight / total_weight, c.mean, c.covariance) for c in working
538
539
  ]
539
540
 
540
541
  return ReductionResult(working, n_original, len(working), total_cost)
@@ -605,8 +606,7 @@ class GaussianMixture:
605
606
  total = sum(c.weight for c in self.components)
606
607
  if total > 0:
607
608
  self.components = [
608
- GaussianComponent(c.weight / total, c.mean, c.covariance)
609
- for c in self.components
609
+ GaussianComponent(c.weight / total, c.mean, c.covariance) for c in self.components
610
610
  ]
611
611
 
612
612
  @property