nrl-tracker 0.22.5__py3-none-any.whl → 1.7.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.22.5.dist-info → nrl_tracker-1.7.5.dist-info}/METADATA +57 -10
- {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.7.5.dist-info}/RECORD +84 -69
- pytcl/__init__.py +4 -3
- pytcl/assignment_algorithms/__init__.py +28 -0
- pytcl/assignment_algorithms/gating.py +10 -10
- pytcl/assignment_algorithms/jpda.py +40 -40
- pytcl/assignment_algorithms/nd_assignment.py +379 -0
- pytcl/assignment_algorithms/network_flow.py +371 -0
- pytcl/assignment_algorithms/three_dimensional/assignment.py +3 -3
- pytcl/astronomical/__init__.py +104 -3
- pytcl/astronomical/ephemerides.py +14 -11
- pytcl/astronomical/reference_frames.py +865 -56
- pytcl/astronomical/relativity.py +6 -5
- pytcl/astronomical/sgp4.py +710 -0
- pytcl/astronomical/special_orbits.py +532 -0
- pytcl/astronomical/tle.py +558 -0
- pytcl/atmosphere/__init__.py +43 -1
- pytcl/atmosphere/ionosphere.py +512 -0
- pytcl/atmosphere/nrlmsise00.py +809 -0
- pytcl/clustering/dbscan.py +2 -2
- pytcl/clustering/gaussian_mixture.py +3 -3
- pytcl/clustering/hierarchical.py +15 -15
- pytcl/clustering/kmeans.py +4 -4
- pytcl/containers/__init__.py +24 -0
- pytcl/containers/base.py +219 -0
- pytcl/containers/cluster_set.py +12 -2
- pytcl/containers/covertree.py +26 -29
- pytcl/containers/kd_tree.py +94 -29
- pytcl/containers/rtree.py +200 -1
- pytcl/containers/vptree.py +21 -28
- pytcl/coordinate_systems/conversions/geodetic.py +272 -5
- pytcl/coordinate_systems/jacobians/jacobians.py +2 -2
- pytcl/coordinate_systems/projections/__init__.py +1 -1
- pytcl/coordinate_systems/projections/projections.py +2 -2
- pytcl/coordinate_systems/rotations/rotations.py +10 -6
- pytcl/core/__init__.py +18 -0
- pytcl/core/validation.py +333 -2
- pytcl/dynamic_estimation/__init__.py +26 -0
- pytcl/dynamic_estimation/gaussian_sum_filter.py +434 -0
- pytcl/dynamic_estimation/imm.py +14 -14
- pytcl/dynamic_estimation/kalman/__init__.py +30 -0
- pytcl/dynamic_estimation/kalman/constrained.py +382 -0
- pytcl/dynamic_estimation/kalman/extended.py +8 -8
- pytcl/dynamic_estimation/kalman/h_infinity.py +613 -0
- pytcl/dynamic_estimation/kalman/square_root.py +60 -573
- pytcl/dynamic_estimation/kalman/sr_ukf.py +302 -0
- pytcl/dynamic_estimation/kalman/ud_filter.py +410 -0
- pytcl/dynamic_estimation/kalman/unscented.py +8 -6
- pytcl/dynamic_estimation/particle_filters/bootstrap.py +15 -15
- pytcl/dynamic_estimation/rbpf.py +589 -0
- pytcl/gravity/egm.py +13 -0
- pytcl/gravity/spherical_harmonics.py +98 -37
- pytcl/gravity/tides.py +6 -6
- pytcl/logging_config.py +328 -0
- pytcl/magnetism/__init__.py +7 -0
- pytcl/magnetism/emm.py +10 -3
- pytcl/magnetism/wmm.py +260 -23
- pytcl/mathematical_functions/combinatorics/combinatorics.py +5 -5
- pytcl/mathematical_functions/geometry/geometry.py +5 -5
- pytcl/mathematical_functions/numerical_integration/quadrature.py +6 -6
- pytcl/mathematical_functions/signal_processing/detection.py +24 -24
- pytcl/mathematical_functions/signal_processing/filters.py +14 -14
- pytcl/mathematical_functions/signal_processing/matched_filter.py +12 -12
- pytcl/mathematical_functions/special_functions/bessel.py +15 -3
- pytcl/mathematical_functions/special_functions/debye.py +136 -26
- pytcl/mathematical_functions/special_functions/error_functions.py +3 -1
- pytcl/mathematical_functions/special_functions/gamma_functions.py +4 -4
- pytcl/mathematical_functions/special_functions/hypergeometric.py +81 -15
- pytcl/mathematical_functions/transforms/fourier.py +8 -8
- pytcl/mathematical_functions/transforms/stft.py +12 -12
- pytcl/mathematical_functions/transforms/wavelets.py +9 -9
- pytcl/navigation/geodesy.py +246 -160
- pytcl/navigation/great_circle.py +101 -19
- pytcl/plotting/coordinates.py +7 -7
- pytcl/plotting/tracks.py +2 -2
- pytcl/static_estimation/maximum_likelihood.py +16 -14
- pytcl/static_estimation/robust.py +5 -5
- pytcl/terrain/loaders.py +5 -5
- pytcl/trackers/hypothesis.py +1 -1
- pytcl/trackers/mht.py +9 -9
- pytcl/trackers/multi_target.py +1 -1
- {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.7.5.dist-info}/LICENSE +0 -0
- {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.7.5.dist-info}/WHEEL +0 -0
- {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.7.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Special orbit cases: parabolic and advanced hyperbolic orbits.
|
|
3
|
+
|
|
4
|
+
This module extends orbital_mechanics.py with handling for edge cases:
|
|
5
|
+
- Parabolic orbits (e = 1, unbounded trajectory with zero energy)
|
|
6
|
+
- Advanced hyperbolic orbit calculations (escape trajectories)
|
|
7
|
+
- Unified orbit type detection and handling
|
|
8
|
+
|
|
9
|
+
References
|
|
10
|
+
----------
|
|
11
|
+
.. [1] Vallado, D. A., "Fundamentals of Astrodynamics and Applications,"
|
|
12
|
+
4th ed., Microcosm Press, 2013.
|
|
13
|
+
.. [2] Curtis, H. D., "Orbital Mechanics for Engineering Students,"
|
|
14
|
+
3rd ed., Butterworth-Heinemann, 2014.
|
|
15
|
+
.. [3] Battin, R. H., "An Introduction to the Mathematics and Methods
|
|
16
|
+
of Astrodynamics," 2nd ed., AIAA, 1999.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from enum import Enum
|
|
20
|
+
from typing import NamedTuple
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
from numpy.typing import NDArray
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class OrbitType(Enum):
|
|
27
|
+
"""Classification of orbit types based on eccentricity."""
|
|
28
|
+
|
|
29
|
+
CIRCULAR = 0 # e = 0
|
|
30
|
+
ELLIPTICAL = 1 # 0 < e < 1
|
|
31
|
+
PARABOLIC = 2 # e = 1 (boundary case)
|
|
32
|
+
HYPERBOLIC = 3 # e > 1
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ParabolicElements(NamedTuple):
|
|
36
|
+
"""Parabolic (escape) orbit elements.
|
|
37
|
+
|
|
38
|
+
For a parabolic orbit (e=1), the semi-major axis is infinite.
|
|
39
|
+
Instead, we use the periapsis distance and orientation parameters.
|
|
40
|
+
|
|
41
|
+
Attributes
|
|
42
|
+
----------
|
|
43
|
+
rp : float
|
|
44
|
+
Periapsis distance (km). Also called pericenter or closest approach.
|
|
45
|
+
i : float
|
|
46
|
+
Inclination (radians), 0 to pi.
|
|
47
|
+
raan : float
|
|
48
|
+
Right ascension of ascending node (radians), 0 to 2*pi.
|
|
49
|
+
omega : float
|
|
50
|
+
Argument of periapsis (radians), 0 to 2*pi.
|
|
51
|
+
nu : float
|
|
52
|
+
True anomaly (radians), typically in [-pi, pi] for parabolic orbits.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
rp: float
|
|
56
|
+
i: float
|
|
57
|
+
raan: float
|
|
58
|
+
omega: float
|
|
59
|
+
nu: float
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def classify_orbit(e: float, tol: float = 1e-9) -> OrbitType:
|
|
63
|
+
"""
|
|
64
|
+
Classify orbit type based on eccentricity.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
e : float
|
|
69
|
+
Eccentricity value.
|
|
70
|
+
tol : float, optional
|
|
71
|
+
Tolerance for parabolic classification (default 1e-9).
|
|
72
|
+
Orbits with abs(e - 1) < tol are classified as parabolic.
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
OrbitType
|
|
77
|
+
Classified orbit type.
|
|
78
|
+
|
|
79
|
+
Raises
|
|
80
|
+
------
|
|
81
|
+
ValueError
|
|
82
|
+
If eccentricity is negative or NaN.
|
|
83
|
+
"""
|
|
84
|
+
if np.isnan(e) or e < 0:
|
|
85
|
+
raise ValueError(f"Eccentricity must be non-negative, got {e}")
|
|
86
|
+
|
|
87
|
+
if e < tol:
|
|
88
|
+
return OrbitType.CIRCULAR
|
|
89
|
+
elif abs(e - 1.0) < tol:
|
|
90
|
+
return OrbitType.PARABOLIC
|
|
91
|
+
elif e < 1 - tol:
|
|
92
|
+
return OrbitType.ELLIPTICAL
|
|
93
|
+
else:
|
|
94
|
+
return OrbitType.HYPERBOLIC
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def mean_to_parabolic_anomaly(
|
|
98
|
+
M: float,
|
|
99
|
+
tol: float = 1e-12,
|
|
100
|
+
max_iter: int = 100,
|
|
101
|
+
) -> float:
|
|
102
|
+
"""
|
|
103
|
+
Solve parabolic Kepler's equation: M = D + (1/3)*D^3.
|
|
104
|
+
|
|
105
|
+
For parabolic orbits (e=1), the "anomaly" is the parabolic anomaly D,
|
|
106
|
+
related to true anomaly by: D = tan(nu/2).
|
|
107
|
+
|
|
108
|
+
The equation relates mean anomaly to parabolic anomaly:
|
|
109
|
+
M = D + (1/3)*D^3
|
|
110
|
+
|
|
111
|
+
This is solved numerically using Newton-Raphson iteration.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
M : float
|
|
116
|
+
Mean anomaly (radians).
|
|
117
|
+
tol : float, optional
|
|
118
|
+
Convergence tolerance (default 1e-12).
|
|
119
|
+
max_iter : int, optional
|
|
120
|
+
Maximum iterations (default 100).
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
D : float
|
|
125
|
+
Parabolic anomaly (the parameter D such that tan(nu/2) = D).
|
|
126
|
+
|
|
127
|
+
Notes
|
|
128
|
+
-----
|
|
129
|
+
For parabolic orbits, mean anomaly relates to time as:
|
|
130
|
+
M = sqrt(mu/rp^3) * t where rp is periapsis distance and mu is GM.
|
|
131
|
+
|
|
132
|
+
The solution D satisfies: D + (1/3)*D^3 = M
|
|
133
|
+
"""
|
|
134
|
+
# Newton-Raphson for parabolic anomaly
|
|
135
|
+
# f(D) = D + (1/3)*D^3 - M = 0
|
|
136
|
+
# f'(D) = 1 + D^2
|
|
137
|
+
|
|
138
|
+
D = M # Initial guess
|
|
139
|
+
|
|
140
|
+
for _ in range(max_iter):
|
|
141
|
+
f = D + (1.0 / 3.0) * D**3 - M
|
|
142
|
+
f_prime = 1.0 + D**2
|
|
143
|
+
delta = f / f_prime
|
|
144
|
+
D = D - delta
|
|
145
|
+
|
|
146
|
+
if abs(delta) < tol:
|
|
147
|
+
return D
|
|
148
|
+
|
|
149
|
+
raise ValueError(
|
|
150
|
+
f"Parabolic Kepler's equation did not converge after {max_iter} iterations"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def parabolic_anomaly_to_true_anomaly(D: float) -> float:
|
|
155
|
+
"""
|
|
156
|
+
Convert parabolic anomaly to true anomaly.
|
|
157
|
+
|
|
158
|
+
For parabolic orbits, the parabolic anomaly D relates to true anomaly by:
|
|
159
|
+
tan(nu/2) = D
|
|
160
|
+
|
|
161
|
+
Parameters
|
|
162
|
+
----------
|
|
163
|
+
D : float
|
|
164
|
+
Parabolic anomaly (the parameter such that tan(nu/2) = D).
|
|
165
|
+
|
|
166
|
+
Returns
|
|
167
|
+
-------
|
|
168
|
+
nu : float
|
|
169
|
+
True anomaly (radians), in [-pi, pi].
|
|
170
|
+
"""
|
|
171
|
+
return 2.0 * np.arctan(D)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def true_anomaly_to_parabolic_anomaly(nu: float) -> float:
|
|
175
|
+
"""
|
|
176
|
+
Convert true anomaly to parabolic anomaly.
|
|
177
|
+
|
|
178
|
+
Parameters
|
|
179
|
+
----------
|
|
180
|
+
nu : float
|
|
181
|
+
True anomaly (radians).
|
|
182
|
+
|
|
183
|
+
Returns
|
|
184
|
+
-------
|
|
185
|
+
D : float
|
|
186
|
+
Parabolic anomaly.
|
|
187
|
+
"""
|
|
188
|
+
return np.tan(nu / 2.0)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def mean_to_true_anomaly_parabolic(M: float, tol: float = 1e-12) -> float:
|
|
192
|
+
"""
|
|
193
|
+
Direct conversion from mean to true anomaly for parabolic orbits.
|
|
194
|
+
|
|
195
|
+
Parameters
|
|
196
|
+
----------
|
|
197
|
+
M : float
|
|
198
|
+
Mean anomaly (radians).
|
|
199
|
+
tol : float, optional
|
|
200
|
+
Convergence tolerance (default 1e-12).
|
|
201
|
+
|
|
202
|
+
Returns
|
|
203
|
+
-------
|
|
204
|
+
nu : float
|
|
205
|
+
True anomaly (radians).
|
|
206
|
+
"""
|
|
207
|
+
D = mean_to_parabolic_anomaly(M, tol=tol)
|
|
208
|
+
return parabolic_anomaly_to_true_anomaly(D)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def radius_parabolic(rp: float, nu: float) -> float:
|
|
212
|
+
"""
|
|
213
|
+
Compute radius for parabolic orbit.
|
|
214
|
+
|
|
215
|
+
For a parabolic orbit with periapsis distance rp and true anomaly nu:
|
|
216
|
+
r = 2*rp / (1 + cos(nu))
|
|
217
|
+
|
|
218
|
+
This formula is consistent with the general conic section equation
|
|
219
|
+
with e=1: r = p/(1 + e*cos(nu)) where p = 2*rp (semi-latus rectum).
|
|
220
|
+
|
|
221
|
+
Parameters
|
|
222
|
+
----------
|
|
223
|
+
rp : float
|
|
224
|
+
Periapsis distance (km).
|
|
225
|
+
nu : float
|
|
226
|
+
True anomaly (radians).
|
|
227
|
+
|
|
228
|
+
Returns
|
|
229
|
+
-------
|
|
230
|
+
r : float
|
|
231
|
+
Orbital radius (km).
|
|
232
|
+
|
|
233
|
+
Raises
|
|
234
|
+
------
|
|
235
|
+
ValueError
|
|
236
|
+
If radius would be negative (nu near +pi for parabolic orbit).
|
|
237
|
+
"""
|
|
238
|
+
denom = 1.0 + np.cos(nu)
|
|
239
|
+
|
|
240
|
+
if denom <= 0:
|
|
241
|
+
raise ValueError(
|
|
242
|
+
f"Parabolic orbit undefined at true anomaly nu={np.degrees(nu):.2f}°"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
r = 2.0 * rp / denom
|
|
246
|
+
|
|
247
|
+
if r < 0:
|
|
248
|
+
raise ValueError(f"Computed radius is negative: r={r}")
|
|
249
|
+
|
|
250
|
+
return r
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def velocity_parabolic(mu: float, rp: float, nu: float) -> float:
|
|
254
|
+
"""
|
|
255
|
+
Compute velocity magnitude for parabolic orbit.
|
|
256
|
+
|
|
257
|
+
For a parabolic orbit (e=1), the specific orbital energy is zero,
|
|
258
|
+
and the velocity relates to radius by:
|
|
259
|
+
v = sqrt(2*mu/r)
|
|
260
|
+
|
|
261
|
+
Parameters
|
|
262
|
+
----------
|
|
263
|
+
mu : float
|
|
264
|
+
Standard gravitational parameter (km^3/s^2).
|
|
265
|
+
rp : float
|
|
266
|
+
Periapsis distance (km).
|
|
267
|
+
nu : float
|
|
268
|
+
True anomaly (radians).
|
|
269
|
+
|
|
270
|
+
Returns
|
|
271
|
+
-------
|
|
272
|
+
v : float
|
|
273
|
+
Velocity magnitude (km/s).
|
|
274
|
+
"""
|
|
275
|
+
r = radius_parabolic(rp, nu)
|
|
276
|
+
return np.sqrt(2.0 * mu / r)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def hyperbolic_anomaly_to_true_anomaly(H: float, e: float) -> float:
|
|
280
|
+
"""
|
|
281
|
+
Convert hyperbolic anomaly to true anomaly.
|
|
282
|
+
|
|
283
|
+
For hyperbolic orbits (e > 1), hyperbolic anomaly H relates to true anomaly by:
|
|
284
|
+
tan(nu/2) = sqrt((e+1)/(e-1)) * tanh(H/2)
|
|
285
|
+
|
|
286
|
+
Parameters
|
|
287
|
+
----------
|
|
288
|
+
H : float
|
|
289
|
+
Hyperbolic anomaly (radians).
|
|
290
|
+
e : float
|
|
291
|
+
Eccentricity (e > 1 for hyperbolic).
|
|
292
|
+
|
|
293
|
+
Returns
|
|
294
|
+
-------
|
|
295
|
+
nu : float
|
|
296
|
+
True anomaly (radians).
|
|
297
|
+
|
|
298
|
+
Raises
|
|
299
|
+
------
|
|
300
|
+
ValueError
|
|
301
|
+
If eccentricity is not hyperbolic (e <= 1).
|
|
302
|
+
"""
|
|
303
|
+
if e <= 1:
|
|
304
|
+
raise ValueError(f"Eccentricity must be > 1 for hyperbolic orbits, got {e}")
|
|
305
|
+
|
|
306
|
+
nu = 2.0 * np.arctan(np.sqrt((e + 1.0) / (e - 1.0)) * np.tanh(H / 2.0))
|
|
307
|
+
|
|
308
|
+
return nu
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def true_anomaly_to_hyperbolic_anomaly(nu: float, e: float) -> float:
|
|
312
|
+
"""
|
|
313
|
+
Convert true anomaly to hyperbolic anomaly.
|
|
314
|
+
|
|
315
|
+
Parameters
|
|
316
|
+
----------
|
|
317
|
+
nu : float
|
|
318
|
+
True anomaly (radians).
|
|
319
|
+
e : float
|
|
320
|
+
Eccentricity (e > 1 for hyperbolic).
|
|
321
|
+
|
|
322
|
+
Returns
|
|
323
|
+
-------
|
|
324
|
+
H : float
|
|
325
|
+
Hyperbolic anomaly (radians).
|
|
326
|
+
|
|
327
|
+
Raises
|
|
328
|
+
------
|
|
329
|
+
ValueError
|
|
330
|
+
If eccentricity is not hyperbolic.
|
|
331
|
+
"""
|
|
332
|
+
if e <= 1:
|
|
333
|
+
raise ValueError(f"Eccentricity must be > 1 for hyperbolic orbits, got {e}")
|
|
334
|
+
|
|
335
|
+
H = 2.0 * np.arctanh(np.sqrt((e - 1.0) / (e + 1.0)) * np.tan(nu / 2.0))
|
|
336
|
+
|
|
337
|
+
return H
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def escape_velocity_at_radius(mu: float, r: float) -> float:
|
|
341
|
+
"""
|
|
342
|
+
Compute escape velocity at a given radius.
|
|
343
|
+
|
|
344
|
+
Escape velocity is the minimum velocity needed to reach infinity
|
|
345
|
+
with zero velocity, corresponding to a parabolic orbit:
|
|
346
|
+
v_esc = sqrt(2*mu/r)
|
|
347
|
+
|
|
348
|
+
Parameters
|
|
349
|
+
----------
|
|
350
|
+
mu : float
|
|
351
|
+
Standard gravitational parameter (km^3/s^2).
|
|
352
|
+
r : float
|
|
353
|
+
Orbital radius (km).
|
|
354
|
+
|
|
355
|
+
Returns
|
|
356
|
+
-------
|
|
357
|
+
v_esc : float
|
|
358
|
+
Escape velocity (km/s).
|
|
359
|
+
"""
|
|
360
|
+
return np.sqrt(2.0 * mu / r)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def hyperbolic_excess_velocity(mu: float, a: float) -> float:
|
|
364
|
+
"""
|
|
365
|
+
Compute hyperbolic excess velocity.
|
|
366
|
+
|
|
367
|
+
For a hyperbolic orbit with semi-major axis a (negative for hyperbolic),
|
|
368
|
+
the excess velocity at infinity is:
|
|
369
|
+
v_inf = sqrt(-mu/a)
|
|
370
|
+
|
|
371
|
+
Parameters
|
|
372
|
+
----------
|
|
373
|
+
mu : float
|
|
374
|
+
Standard gravitational parameter (km^3/s^2).
|
|
375
|
+
a : float
|
|
376
|
+
Semi-major axis (km). Must be negative for hyperbolic orbits.
|
|
377
|
+
|
|
378
|
+
Returns
|
|
379
|
+
-------
|
|
380
|
+
v_inf : float
|
|
381
|
+
Hyperbolic excess velocity (km/s).
|
|
382
|
+
|
|
383
|
+
Raises
|
|
384
|
+
------
|
|
385
|
+
ValueError
|
|
386
|
+
If semi-major axis is not negative.
|
|
387
|
+
"""
|
|
388
|
+
if a >= 0:
|
|
389
|
+
raise ValueError(
|
|
390
|
+
f"Semi-major axis must be negative for hyperbolic orbits, got {a}"
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
v_inf = np.sqrt(-mu / a)
|
|
394
|
+
return v_inf
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def hyperbolic_asymptote_angle(e: float) -> float:
|
|
398
|
+
"""
|
|
399
|
+
Compute the asymptote angle for a hyperbolic orbit.
|
|
400
|
+
|
|
401
|
+
For a hyperbolic orbit with eccentricity e, the true anomaly
|
|
402
|
+
asymptotically approaches ±nu_inf where:
|
|
403
|
+
cos(nu_inf) = -1/e
|
|
404
|
+
|
|
405
|
+
The asymptote angle is nu_inf.
|
|
406
|
+
|
|
407
|
+
Parameters
|
|
408
|
+
----------
|
|
409
|
+
e : float
|
|
410
|
+
Eccentricity (e > 1 for hyperbolic).
|
|
411
|
+
|
|
412
|
+
Returns
|
|
413
|
+
-------
|
|
414
|
+
nu_inf : float
|
|
415
|
+
Asymptote angle (radians), in (0, pi).
|
|
416
|
+
|
|
417
|
+
Raises
|
|
418
|
+
------
|
|
419
|
+
ValueError
|
|
420
|
+
If eccentricity is not hyperbolic.
|
|
421
|
+
"""
|
|
422
|
+
if e <= 1:
|
|
423
|
+
raise ValueError(f"Eccentricity must be > 1 for hyperbolic orbits, got {e}")
|
|
424
|
+
|
|
425
|
+
nu_inf = np.arccos(-1.0 / e)
|
|
426
|
+
return nu_inf
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def hyperbolic_deflection_angle(e: float) -> float:
|
|
430
|
+
"""
|
|
431
|
+
Compute the deflection angle for a hyperbolic orbit.
|
|
432
|
+
|
|
433
|
+
The deflection angle is the angle through which the velocity vector
|
|
434
|
+
is deflected from its asymptotic direction:
|
|
435
|
+
delta = pi - 2*nu_inf = pi - 2*arccos(-1/e)
|
|
436
|
+
|
|
437
|
+
Parameters
|
|
438
|
+
----------
|
|
439
|
+
e : float
|
|
440
|
+
Eccentricity (e > 1 for hyperbolic).
|
|
441
|
+
|
|
442
|
+
Returns
|
|
443
|
+
-------
|
|
444
|
+
delta : float
|
|
445
|
+
Deflection angle (radians), in (0, pi).
|
|
446
|
+
|
|
447
|
+
Raises
|
|
448
|
+
------
|
|
449
|
+
ValueError
|
|
450
|
+
If eccentricity is not hyperbolic.
|
|
451
|
+
"""
|
|
452
|
+
if e <= 1:
|
|
453
|
+
raise ValueError(f"Eccentricity must be > 1 for hyperbolic orbits, got {e}")
|
|
454
|
+
|
|
455
|
+
nu_inf = hyperbolic_asymptote_angle(e)
|
|
456
|
+
delta = np.pi - 2.0 * nu_inf
|
|
457
|
+
|
|
458
|
+
return delta
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def semi_major_axis_from_energy(mu: float, specific_energy: float) -> float:
|
|
462
|
+
"""
|
|
463
|
+
Compute semi-major axis from specific orbital energy.
|
|
464
|
+
|
|
465
|
+
The specific orbital energy relates to semi-major axis by:
|
|
466
|
+
epsilon = -mu / (2*a)
|
|
467
|
+
|
|
468
|
+
Rearranging: a = -mu / (2*epsilon)
|
|
469
|
+
|
|
470
|
+
Parameters
|
|
471
|
+
----------
|
|
472
|
+
mu : float
|
|
473
|
+
Standard gravitational parameter (km^3/s^2).
|
|
474
|
+
specific_energy : float
|
|
475
|
+
Specific orbital energy (km^2/s^2).
|
|
476
|
+
|
|
477
|
+
Returns
|
|
478
|
+
-------
|
|
479
|
+
a : float
|
|
480
|
+
Semi-major axis (km).
|
|
481
|
+
- a > 0 for elliptical orbits (epsilon < 0)
|
|
482
|
+
- a < 0 for hyperbolic orbits (epsilon > 0)
|
|
483
|
+
- a → ∞ for parabolic orbits (epsilon = 0)
|
|
484
|
+
|
|
485
|
+
Raises
|
|
486
|
+
------
|
|
487
|
+
ValueError
|
|
488
|
+
If specific energy is exactly zero (parabolic case).
|
|
489
|
+
"""
|
|
490
|
+
if abs(specific_energy) < 1e-15:
|
|
491
|
+
raise ValueError(
|
|
492
|
+
"Specific energy is zero (parabolic orbit); use alternative methods"
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
a = -mu / (2.0 * specific_energy)
|
|
496
|
+
return a
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def eccentricity_vector(
|
|
500
|
+
r: NDArray[np.floating],
|
|
501
|
+
v: NDArray[np.floating],
|
|
502
|
+
mu: float,
|
|
503
|
+
) -> NDArray[np.floating]:
|
|
504
|
+
"""
|
|
505
|
+
Compute eccentricity vector from position and velocity.
|
|
506
|
+
|
|
507
|
+
The eccentricity vector e is defined as:
|
|
508
|
+
e = (v^2/mu - 1/r) * r - (r·v/mu) * v
|
|
509
|
+
|
|
510
|
+
This works for all orbit types: elliptical, parabolic, and hyperbolic.
|
|
511
|
+
|
|
512
|
+
Parameters
|
|
513
|
+
----------
|
|
514
|
+
r : ndarray
|
|
515
|
+
Position vector (km), shape (3,).
|
|
516
|
+
v : ndarray
|
|
517
|
+
Velocity vector (km/s), shape (3,).
|
|
518
|
+
mu : float
|
|
519
|
+
Standard gravitational parameter (km^3/s^2).
|
|
520
|
+
|
|
521
|
+
Returns
|
|
522
|
+
-------
|
|
523
|
+
e : ndarray
|
|
524
|
+
Eccentricity vector, shape (3,).
|
|
525
|
+
"""
|
|
526
|
+
r_mag = np.linalg.norm(r)
|
|
527
|
+
v_mag = np.linalg.norm(v)
|
|
528
|
+
rv_dot = np.dot(r, v)
|
|
529
|
+
|
|
530
|
+
e_vec = (v_mag**2 / mu - 1.0 / r_mag) * r - (rv_dot / mu) * v
|
|
531
|
+
|
|
532
|
+
return e_vec
|