nrl-tracker 1.5.0__py3-none-any.whl → 1.7.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.
@@ -0,0 +1,536 @@
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(
307
+ np.sqrt((e + 1.0) / (e - 1.0)) * np.tanh(H / 2.0)
308
+ )
309
+
310
+ return nu
311
+
312
+
313
+ def true_anomaly_to_hyperbolic_anomaly(nu: float, e: float) -> float:
314
+ """
315
+ Convert true anomaly to hyperbolic anomaly.
316
+
317
+ Parameters
318
+ ----------
319
+ nu : float
320
+ True anomaly (radians).
321
+ e : float
322
+ Eccentricity (e > 1 for hyperbolic).
323
+
324
+ Returns
325
+ -------
326
+ H : float
327
+ Hyperbolic anomaly (radians).
328
+
329
+ Raises
330
+ ------
331
+ ValueError
332
+ If eccentricity is not hyperbolic.
333
+ """
334
+ if e <= 1:
335
+ raise ValueError(f"Eccentricity must be > 1 for hyperbolic orbits, got {e}")
336
+
337
+ H = 2.0 * np.arctanh(
338
+ np.sqrt((e - 1.0) / (e + 1.0)) * np.tan(nu / 2.0)
339
+ )
340
+
341
+ return H
342
+
343
+
344
+ def escape_velocity_at_radius(mu: float, r: float) -> float:
345
+ """
346
+ Compute escape velocity at a given radius.
347
+
348
+ Escape velocity is the minimum velocity needed to reach infinity
349
+ with zero velocity, corresponding to a parabolic orbit:
350
+ v_esc = sqrt(2*mu/r)
351
+
352
+ Parameters
353
+ ----------
354
+ mu : float
355
+ Standard gravitational parameter (km^3/s^2).
356
+ r : float
357
+ Orbital radius (km).
358
+
359
+ Returns
360
+ -------
361
+ v_esc : float
362
+ Escape velocity (km/s).
363
+ """
364
+ return np.sqrt(2.0 * mu / r)
365
+
366
+
367
+ def hyperbolic_excess_velocity(mu: float, a: float) -> float:
368
+ """
369
+ Compute hyperbolic excess velocity.
370
+
371
+ For a hyperbolic orbit with semi-major axis a (negative for hyperbolic),
372
+ the excess velocity at infinity is:
373
+ v_inf = sqrt(-mu/a)
374
+
375
+ Parameters
376
+ ----------
377
+ mu : float
378
+ Standard gravitational parameter (km^3/s^2).
379
+ a : float
380
+ Semi-major axis (km). Must be negative for hyperbolic orbits.
381
+
382
+ Returns
383
+ -------
384
+ v_inf : float
385
+ Hyperbolic excess velocity (km/s).
386
+
387
+ Raises
388
+ ------
389
+ ValueError
390
+ If semi-major axis is not negative.
391
+ """
392
+ if a >= 0:
393
+ raise ValueError(
394
+ f"Semi-major axis must be negative for hyperbolic orbits, got {a}"
395
+ )
396
+
397
+ v_inf = np.sqrt(-mu / a)
398
+ return v_inf
399
+
400
+
401
+ def hyperbolic_asymptote_angle(e: float) -> float:
402
+ """
403
+ Compute the asymptote angle for a hyperbolic orbit.
404
+
405
+ For a hyperbolic orbit with eccentricity e, the true anomaly
406
+ asymptotically approaches ±nu_inf where:
407
+ cos(nu_inf) = -1/e
408
+
409
+ The asymptote angle is nu_inf.
410
+
411
+ Parameters
412
+ ----------
413
+ e : float
414
+ Eccentricity (e > 1 for hyperbolic).
415
+
416
+ Returns
417
+ -------
418
+ nu_inf : float
419
+ Asymptote angle (radians), in (0, pi).
420
+
421
+ Raises
422
+ ------
423
+ ValueError
424
+ If eccentricity is not hyperbolic.
425
+ """
426
+ if e <= 1:
427
+ raise ValueError(f"Eccentricity must be > 1 for hyperbolic orbits, got {e}")
428
+
429
+ nu_inf = np.arccos(-1.0 / e)
430
+ return nu_inf
431
+
432
+
433
+ def hyperbolic_deflection_angle(e: float) -> float:
434
+ """
435
+ Compute the deflection angle for a hyperbolic orbit.
436
+
437
+ The deflection angle is the angle through which the velocity vector
438
+ is deflected from its asymptotic direction:
439
+ delta = pi - 2*nu_inf = pi - 2*arccos(-1/e)
440
+
441
+ Parameters
442
+ ----------
443
+ e : float
444
+ Eccentricity (e > 1 for hyperbolic).
445
+
446
+ Returns
447
+ -------
448
+ delta : float
449
+ Deflection angle (radians), in (0, pi).
450
+
451
+ Raises
452
+ ------
453
+ ValueError
454
+ If eccentricity is not hyperbolic.
455
+ """
456
+ if e <= 1:
457
+ raise ValueError(f"Eccentricity must be > 1 for hyperbolic orbits, got {e}")
458
+
459
+ nu_inf = hyperbolic_asymptote_angle(e)
460
+ delta = np.pi - 2.0 * nu_inf
461
+
462
+ return delta
463
+
464
+
465
+ def semi_major_axis_from_energy(mu: float, specific_energy: float) -> float:
466
+ """
467
+ Compute semi-major axis from specific orbital energy.
468
+
469
+ The specific orbital energy relates to semi-major axis by:
470
+ epsilon = -mu / (2*a)
471
+
472
+ Rearranging: a = -mu / (2*epsilon)
473
+
474
+ Parameters
475
+ ----------
476
+ mu : float
477
+ Standard gravitational parameter (km^3/s^2).
478
+ specific_energy : float
479
+ Specific orbital energy (km^2/s^2).
480
+
481
+ Returns
482
+ -------
483
+ a : float
484
+ Semi-major axis (km).
485
+ - a > 0 for elliptical orbits (epsilon < 0)
486
+ - a < 0 for hyperbolic orbits (epsilon > 0)
487
+ - a → ∞ for parabolic orbits (epsilon = 0)
488
+
489
+ Raises
490
+ ------
491
+ ValueError
492
+ If specific energy is exactly zero (parabolic case).
493
+ """
494
+ if abs(specific_energy) < 1e-15:
495
+ raise ValueError(
496
+ "Specific energy is zero (parabolic orbit); use alternative methods"
497
+ )
498
+
499
+ a = -mu / (2.0 * specific_energy)
500
+ return a
501
+
502
+
503
+ def eccentricity_vector(
504
+ r: NDArray,
505
+ v: NDArray,
506
+ mu: float,
507
+ ) -> NDArray:
508
+ """
509
+ Compute eccentricity vector from position and velocity.
510
+
511
+ The eccentricity vector e is defined as:
512
+ e = (v^2/mu - 1/r) * r - (r·v/mu) * v
513
+
514
+ This works for all orbit types: elliptical, parabolic, and hyperbolic.
515
+
516
+ Parameters
517
+ ----------
518
+ r : ndarray
519
+ Position vector (km), shape (3,).
520
+ v : ndarray
521
+ Velocity vector (km/s), shape (3,).
522
+ mu : float
523
+ Standard gravitational parameter (km^3/s^2).
524
+
525
+ Returns
526
+ -------
527
+ e : ndarray
528
+ Eccentricity vector, shape (3,).
529
+ """
530
+ r_mag = np.linalg.norm(r)
531
+ v_mag = np.linalg.norm(v)
532
+ rv_dot = np.dot(r, v)
533
+
534
+ e_vec = (v_mag**2 / mu - 1.0 / r_mag) * r - (rv_dot / mu) * v
535
+
536
+ return e_vec