nrl-tracker 0.21.1__py3-none-any.whl → 0.22.5__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-0.21.1.dist-info → nrl_tracker-0.22.5.dist-info}/METADATA +4 -4
- {nrl_tracker-0.21.1.dist-info → nrl_tracker-0.22.5.dist-info}/RECORD +35 -33
- pytcl/__init__.py +1 -1
- pytcl/assignment_algorithms/data_association.py +2 -7
- pytcl/assignment_algorithms/jpda.py +43 -29
- pytcl/assignment_algorithms/two_dimensional/assignment.py +14 -7
- pytcl/astronomical/__init__.py +60 -7
- pytcl/astronomical/ephemerides.py +530 -0
- pytcl/astronomical/relativity.py +472 -0
- pytcl/atmosphere/__init__.py +2 -2
- pytcl/clustering/dbscan.py +23 -5
- pytcl/clustering/hierarchical.py +23 -10
- pytcl/clustering/kmeans.py +5 -10
- pytcl/containers/__init__.py +4 -21
- pytcl/containers/cluster_set.py +1 -10
- pytcl/containers/measurement_set.py +1 -9
- pytcl/coordinate_systems/projections/__init__.py +4 -2
- pytcl/dynamic_estimation/imm.py +42 -36
- pytcl/dynamic_estimation/kalman/extended.py +1 -4
- pytcl/dynamic_estimation/kalman/linear.py +17 -13
- pytcl/dynamic_estimation/kalman/unscented.py +27 -27
- pytcl/dynamic_estimation/particle_filters/bootstrap.py +57 -19
- pytcl/dynamic_estimation/smoothers.py +1 -5
- pytcl/dynamic_models/discrete_time/__init__.py +1 -5
- pytcl/dynamic_models/process_noise/__init__.py +1 -5
- pytcl/magnetism/__init__.py +3 -14
- pytcl/mathematical_functions/interpolation/__init__.py +2 -2
- pytcl/mathematical_functions/special_functions/__init__.py +2 -2
- pytcl/navigation/__init__.py +14 -10
- pytcl/navigation/ins.py +1 -5
- pytcl/trackers/__init__.py +3 -14
- pytcl/trackers/multi_target.py +1 -4
- {nrl_tracker-0.21.1.dist-info → nrl_tracker-0.22.5.dist-info}/LICENSE +0 -0
- {nrl_tracker-0.21.1.dist-info → nrl_tracker-0.22.5.dist-info}/WHEEL +0 -0
- {nrl_tracker-0.21.1.dist-info → nrl_tracker-0.22.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,472 @@
|
|
|
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(
|
|
327
|
+
a: float, e: float, inclination: float, gm: float = GM_EARTH
|
|
328
|
+
) -> float:
|
|
329
|
+
"""Compute geodetic (de Sitter) precession rate of orbital plane.
|
|
330
|
+
|
|
331
|
+
The orbital plane of a satellite precesses due to frame-dragging effects
|
|
332
|
+
and spacetime curvature. The geodetic precession rate is:
|
|
333
|
+
|
|
334
|
+
Ω_geodetic = -GM/(c^2 * a^3 * (1 - e^2)^2) * cos(i)
|
|
335
|
+
|
|
336
|
+
Parameters
|
|
337
|
+
----------
|
|
338
|
+
a : float
|
|
339
|
+
Semi-major axis (m)
|
|
340
|
+
e : float
|
|
341
|
+
Eccentricity (dimensionless)
|
|
342
|
+
inclination : float
|
|
343
|
+
Orbital inclination (radians)
|
|
344
|
+
gm : float, optional
|
|
345
|
+
Gravitational parameter (m^3/s^2). Default is GM_EARTH.
|
|
346
|
+
|
|
347
|
+
Returns
|
|
348
|
+
-------
|
|
349
|
+
float
|
|
350
|
+
Geodetic precession rate (radians per orbit)
|
|
351
|
+
|
|
352
|
+
Examples
|
|
353
|
+
--------
|
|
354
|
+
Geodetic precession for a typical Earth satellite:
|
|
355
|
+
|
|
356
|
+
>>> a = 6.678e6 # ~300 km altitude
|
|
357
|
+
>>> e = 0.0 # Circular
|
|
358
|
+
>>> i = np.radians(51.6) # ISS-like inclination
|
|
359
|
+
>>> rate = geodetic_precession(a, e, i)
|
|
360
|
+
>>> print(f"Precession per orbit: {rate*206265:.3f} arcsec")
|
|
361
|
+
"""
|
|
362
|
+
p = a * (1.0 - e**2)
|
|
363
|
+
precession = -(gm / (C_LIGHT**2 * p**2)) * np.cos(inclination)
|
|
364
|
+
return precession
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def lense_thirring_precession(
|
|
368
|
+
a: float,
|
|
369
|
+
e: float,
|
|
370
|
+
inclination: float,
|
|
371
|
+
angular_momentum: float,
|
|
372
|
+
gm: float = GM_EARTH,
|
|
373
|
+
) -> float:
|
|
374
|
+
"""Compute Lense-Thirring (frame-dragging) precession of orbital node.
|
|
375
|
+
|
|
376
|
+
A rotating central body drags the orbital plane of nearby objects.
|
|
377
|
+
The nodal precession rate due to this effect is:
|
|
378
|
+
|
|
379
|
+
Ω_LT = (2GM * J_2 * a * ω) / (c^2 * p^2) * f(e, i)
|
|
380
|
+
|
|
381
|
+
where J_2 is the quadrupole moment, ω is angular velocity, and f depends
|
|
382
|
+
on eccentricity and inclination.
|
|
383
|
+
|
|
384
|
+
Parameters
|
|
385
|
+
----------
|
|
386
|
+
a : float
|
|
387
|
+
Semi-major axis (m)
|
|
388
|
+
e : float
|
|
389
|
+
Eccentricity (dimensionless)
|
|
390
|
+
inclination : float
|
|
391
|
+
Orbital inclination (radians)
|
|
392
|
+
angular_momentum : float
|
|
393
|
+
Angular momentum of central body (kg·m^2/s)
|
|
394
|
+
gm : float, optional
|
|
395
|
+
Gravitational parameter (m^3/s^2). Default is GM_EARTH.
|
|
396
|
+
|
|
397
|
+
Returns
|
|
398
|
+
-------
|
|
399
|
+
float
|
|
400
|
+
Lense-Thirring precession rate (radians per orbit)
|
|
401
|
+
|
|
402
|
+
Notes
|
|
403
|
+
-----
|
|
404
|
+
This is a simplified version. For Earth, J_2 effects typically dominate
|
|
405
|
+
classical nodal precession, while Lense-Thirring is a small correction
|
|
406
|
+
(~1 arcsec per year for typical satellites).
|
|
407
|
+
|
|
408
|
+
Examples
|
|
409
|
+
--------
|
|
410
|
+
Lense-Thirring effect for LAGEOS satellite:
|
|
411
|
+
|
|
412
|
+
>>> # LAGEOS parameters
|
|
413
|
+
>>> a = 12.27e6 # Semi-major axis
|
|
414
|
+
>>> e = 0.0045
|
|
415
|
+
>>> i = np.radians(109.9)
|
|
416
|
+
>>> L_earth = 7.05e33 # Earth's angular momentum
|
|
417
|
+
>>> rate = lense_thirring_precession(a, e, i, L_earth)
|
|
418
|
+
>>> print(f"LT precession per orbit: {rate*206265*1e3:.1f} milliarcsec")
|
|
419
|
+
"""
|
|
420
|
+
p = a * (1.0 - e**2)
|
|
421
|
+
|
|
422
|
+
# Simplified Lense-Thirring term (second-order PN effect)
|
|
423
|
+
# For a sphere: Lense-Thirring parameter = 2GM*L/(c^2*M*r^3)
|
|
424
|
+
precession = (2.0 * angular_momentum * gm) / (C_LIGHT**2 * p**3)
|
|
425
|
+
|
|
426
|
+
return precession
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def relativistic_range_correction(
|
|
430
|
+
distance: float, relative_velocity: float, gm: float = GM_EARTH
|
|
431
|
+
) -> float:
|
|
432
|
+
"""Compute relativistic range correction for ranging measurements.
|
|
433
|
+
|
|
434
|
+
When measuring distance to a satellite or spacecraft using ranging
|
|
435
|
+
(e.g., laser ranging), relativistic effects introduce corrections to
|
|
436
|
+
the measured range.
|
|
437
|
+
|
|
438
|
+
The main contributions are:
|
|
439
|
+
- Gravitational time dilation
|
|
440
|
+
- Relativistic Doppler effect
|
|
441
|
+
|
|
442
|
+
Parameters
|
|
443
|
+
----------
|
|
444
|
+
distance : float
|
|
445
|
+
Distance to object (m)
|
|
446
|
+
relative_velocity : float
|
|
447
|
+
Radial velocity component (m/s, positive = receding)
|
|
448
|
+
gm : float, optional
|
|
449
|
+
Gravitational parameter (m^3/s^2). Default is GM_EARTH.
|
|
450
|
+
|
|
451
|
+
Returns
|
|
452
|
+
-------
|
|
453
|
+
float
|
|
454
|
+
Range correction (m)
|
|
455
|
+
|
|
456
|
+
Examples
|
|
457
|
+
--------
|
|
458
|
+
Range correction for lunar laser ranging:
|
|
459
|
+
|
|
460
|
+
>>> distance_to_moon = 3.84e8 # meters
|
|
461
|
+
>>> radial_velocity = 0.0 # Average over orbit
|
|
462
|
+
>>> correction = relativistic_range_correction(distance_to_moon, radial_velocity, GM_EARTH)
|
|
463
|
+
>>> print(f"Range correction: {correction:.1f} m")
|
|
464
|
+
"""
|
|
465
|
+
# Gravitational correction (positive because the signal is delayed)
|
|
466
|
+
# Uses weak-field approximation
|
|
467
|
+
grav_correction = gm / (C_LIGHT**2)
|
|
468
|
+
|
|
469
|
+
# Doppler correction (second order effect, small)
|
|
470
|
+
doppler_correction = (relative_velocity**2) / (3.0 * C_LIGHT**2)
|
|
471
|
+
|
|
472
|
+
return grav_correction + doppler_correction
|
pytcl/atmosphere/__init__.py
CHANGED
|
@@ -5,8 +5,8 @@ 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
|
|
9
|
-
|
|
8
|
+
from pytcl.atmosphere.models import G0 # Constants
|
|
9
|
+
from pytcl.atmosphere.models import (
|
|
10
10
|
GAMMA,
|
|
11
11
|
P0,
|
|
12
12
|
RHO0,
|
pytcl/clustering/dbscan.py
CHANGED
|
@@ -15,6 +15,7 @@ References
|
|
|
15
15
|
from typing import List, NamedTuple, Set
|
|
16
16
|
|
|
17
17
|
import numpy as np
|
|
18
|
+
from numba import njit
|
|
18
19
|
from numpy.typing import ArrayLike, NDArray
|
|
19
20
|
|
|
20
21
|
|
|
@@ -40,6 +41,23 @@ class DBSCANResult(NamedTuple):
|
|
|
40
41
|
n_noise: int
|
|
41
42
|
|
|
42
43
|
|
|
44
|
+
@njit(cache=True)
|
|
45
|
+
def _compute_distance_matrix(X: np.ndarray) -> np.ndarray:
|
|
46
|
+
"""Compute pairwise Euclidean distance matrix (JIT-compiled)."""
|
|
47
|
+
n = X.shape[0]
|
|
48
|
+
dist = np.zeros((n, n), dtype=np.float64)
|
|
49
|
+
for i in range(n):
|
|
50
|
+
for j in range(i + 1, n):
|
|
51
|
+
d = 0.0
|
|
52
|
+
for k in range(X.shape[1]):
|
|
53
|
+
diff = X[i, k] - X[j, k]
|
|
54
|
+
d += diff * diff
|
|
55
|
+
d = np.sqrt(d)
|
|
56
|
+
dist[i, j] = d
|
|
57
|
+
dist[j, i] = d
|
|
58
|
+
return dist
|
|
59
|
+
|
|
60
|
+
|
|
43
61
|
def compute_neighbors(
|
|
44
62
|
X: NDArray[np.floating],
|
|
45
63
|
eps: float,
|
|
@@ -60,13 +78,13 @@ def compute_neighbors(
|
|
|
60
78
|
neighbors[i] contains indices of points within eps of point i.
|
|
61
79
|
"""
|
|
62
80
|
n_samples = X.shape[0]
|
|
63
|
-
neighbors = []
|
|
64
81
|
|
|
82
|
+
# Use JIT-compiled distance matrix computation
|
|
83
|
+
dist_matrix = _compute_distance_matrix(X)
|
|
84
|
+
|
|
85
|
+
neighbors = []
|
|
65
86
|
for i in range(n_samples):
|
|
66
|
-
|
|
67
|
-
distances = np.sqrt(np.sum((X - X[i]) ** 2, axis=1))
|
|
68
|
-
# Find points within eps (including self)
|
|
69
|
-
neighbor_indices = np.where(distances <= eps)[0]
|
|
87
|
+
neighbor_indices = np.where(dist_matrix[i] <= eps)[0]
|
|
70
88
|
neighbors.append(neighbor_indices)
|
|
71
89
|
|
|
72
90
|
return neighbors
|
pytcl/clustering/hierarchical.py
CHANGED
|
@@ -15,6 +15,7 @@ from enum import Enum
|
|
|
15
15
|
from typing import List, Literal, NamedTuple, Optional
|
|
16
16
|
|
|
17
17
|
import numpy as np
|
|
18
|
+
from numba import njit
|
|
18
19
|
from numpy.typing import ArrayLike, NDArray
|
|
19
20
|
|
|
20
21
|
|
|
@@ -70,6 +71,26 @@ class HierarchicalResult(NamedTuple):
|
|
|
70
71
|
dendrogram: List[DendrogramNode]
|
|
71
72
|
|
|
72
73
|
|
|
74
|
+
@njit(cache=True)
|
|
75
|
+
def _compute_distance_matrix_jit(X: np.ndarray) -> np.ndarray:
|
|
76
|
+
"""JIT-compiled pairwise Euclidean distance computation."""
|
|
77
|
+
n = X.shape[0]
|
|
78
|
+
n_features = X.shape[1]
|
|
79
|
+
distances = np.zeros((n, n), dtype=np.float64)
|
|
80
|
+
|
|
81
|
+
for i in range(n):
|
|
82
|
+
for j in range(i + 1, n):
|
|
83
|
+
d = 0.0
|
|
84
|
+
for k in range(n_features):
|
|
85
|
+
diff = X[i, k] - X[j, k]
|
|
86
|
+
d += diff * diff
|
|
87
|
+
d = np.sqrt(d)
|
|
88
|
+
distances[i, j] = d
|
|
89
|
+
distances[j, i] = d
|
|
90
|
+
|
|
91
|
+
return distances
|
|
92
|
+
|
|
93
|
+
|
|
73
94
|
def compute_distance_matrix(
|
|
74
95
|
X: NDArray[np.floating],
|
|
75
96
|
) -> NDArray[np.floating]:
|
|
@@ -86,16 +107,8 @@ def compute_distance_matrix(
|
|
|
86
107
|
distances : ndarray
|
|
87
108
|
Distance matrix, shape (n_samples, n_samples).
|
|
88
109
|
"""
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
for i in range(n):
|
|
93
|
-
for j in range(i + 1, n):
|
|
94
|
-
d = np.sqrt(np.sum((X[i] - X[j]) ** 2))
|
|
95
|
-
distances[i, j] = d
|
|
96
|
-
distances[j, i] = d
|
|
97
|
-
|
|
98
|
-
return distances
|
|
110
|
+
X = np.asarray(X, dtype=np.float64)
|
|
111
|
+
return _compute_distance_matrix_jit(X)
|
|
99
112
|
|
|
100
113
|
|
|
101
114
|
def _single_linkage(
|
pytcl/clustering/kmeans.py
CHANGED
|
@@ -14,6 +14,7 @@ from typing import Literal, NamedTuple, Optional, Union
|
|
|
14
14
|
|
|
15
15
|
import numpy as np
|
|
16
16
|
from numpy.typing import ArrayLike, NDArray
|
|
17
|
+
from scipy.spatial.distance import cdist
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class KMeansResult(NamedTuple):
|
|
@@ -91,11 +92,8 @@ def kmeans_plusplus_init(
|
|
|
91
92
|
|
|
92
93
|
# Subsequent centers: sample proportional to D^2
|
|
93
94
|
for k in range(1, n_clusters):
|
|
94
|
-
# Compute squared distances to nearest center
|
|
95
|
-
distances_sq =
|
|
96
|
-
for j in range(k):
|
|
97
|
-
d_sq = np.sum((X - centers[j]) ** 2, axis=1)
|
|
98
|
-
distances_sq = np.minimum(distances_sq, d_sq)
|
|
95
|
+
# Compute squared distances to nearest center (vectorized via cdist)
|
|
96
|
+
distances_sq = cdist(X, centers[:k], metric="sqeuclidean").min(axis=1)
|
|
99
97
|
|
|
100
98
|
# Sample proportional to D^2
|
|
101
99
|
probs = distances_sq / distances_sq.sum()
|
|
@@ -138,12 +136,9 @@ def assign_clusters(
|
|
|
138
136
|
centers = np.asarray(centers, dtype=np.float64)
|
|
139
137
|
|
|
140
138
|
n_samples = X.shape[0]
|
|
141
|
-
n_clusters = centers.shape[0]
|
|
142
139
|
|
|
143
|
-
# Compute distances to all centers
|
|
144
|
-
distances_sq =
|
|
145
|
-
for k in range(n_clusters):
|
|
146
|
-
distances_sq[:, k] = np.sum((X - centers[k]) ** 2, axis=1)
|
|
140
|
+
# Compute squared distances to all centers (vectorized via cdist)
|
|
141
|
+
distances_sq = cdist(X, centers, metric="sqeuclidean")
|
|
147
142
|
|
|
148
143
|
# Assign to nearest center
|
|
149
144
|
labels = np.argmin(distances_sq, axis=1).astype(np.intp)
|
pytcl/containers/__init__.py
CHANGED
|
@@ -13,17 +13,8 @@ from pytcl.containers.cluster_set import (
|
|
|
13
13
|
cluster_tracks_kmeans,
|
|
14
14
|
compute_cluster_centroid,
|
|
15
15
|
)
|
|
16
|
-
from pytcl.containers.covertree import
|
|
17
|
-
|
|
18
|
-
CoverTreeNode,
|
|
19
|
-
CoverTreeResult,
|
|
20
|
-
)
|
|
21
|
-
from pytcl.containers.kd_tree import (
|
|
22
|
-
BallTree,
|
|
23
|
-
KDNode,
|
|
24
|
-
KDTree,
|
|
25
|
-
NearestNeighborResult,
|
|
26
|
-
)
|
|
16
|
+
from pytcl.containers.covertree import CoverTree, CoverTreeNode, CoverTreeResult
|
|
17
|
+
from pytcl.containers.kd_tree import BallTree, KDNode, KDTree, NearestNeighborResult
|
|
27
18
|
from pytcl.containers.measurement_set import (
|
|
28
19
|
Measurement,
|
|
29
20
|
MeasurementQuery,
|
|
@@ -38,16 +29,8 @@ from pytcl.containers.rtree import (
|
|
|
38
29
|
box_from_points,
|
|
39
30
|
merge_boxes,
|
|
40
31
|
)
|
|
41
|
-
from pytcl.containers.track_list import
|
|
42
|
-
|
|
43
|
-
TrackListStats,
|
|
44
|
-
TrackQuery,
|
|
45
|
-
)
|
|
46
|
-
from pytcl.containers.vptree import (
|
|
47
|
-
VPNode,
|
|
48
|
-
VPTree,
|
|
49
|
-
VPTreeResult,
|
|
50
|
-
)
|
|
32
|
+
from pytcl.containers.track_list import TrackList, TrackListStats, TrackQuery
|
|
33
|
+
from pytcl.containers.vptree import VPNode, VPTree, VPTreeResult
|
|
51
34
|
|
|
52
35
|
__all__ = [
|
|
53
36
|
# K-D Tree
|
pytcl/containers/cluster_set.py
CHANGED
|
@@ -7,16 +7,7 @@ that move together (formations, convoys, etc.).
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
from typing import
|
|
11
|
-
Dict,
|
|
12
|
-
Iterable,
|
|
13
|
-
Iterator,
|
|
14
|
-
List,
|
|
15
|
-
NamedTuple,
|
|
16
|
-
Optional,
|
|
17
|
-
Tuple,
|
|
18
|
-
Union,
|
|
19
|
-
)
|
|
10
|
+
from typing import Dict, Iterable, Iterator, List, NamedTuple, Optional, Tuple, Union
|
|
20
11
|
|
|
21
12
|
import numpy as np
|
|
22
13
|
from numpy.typing import ArrayLike, NDArray
|
|
@@ -7,15 +7,7 @@ with spatial query support.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
from typing import
|
|
11
|
-
Iterable,
|
|
12
|
-
Iterator,
|
|
13
|
-
List,
|
|
14
|
-
NamedTuple,
|
|
15
|
-
Optional,
|
|
16
|
-
Tuple,
|
|
17
|
-
Union,
|
|
18
|
-
)
|
|
10
|
+
from typing import Iterable, Iterator, List, NamedTuple, Optional, Tuple, Union
|
|
19
11
|
|
|
20
12
|
import numpy as np
|
|
21
13
|
from numpy.typing import ArrayLike, NDArray
|