Initial commit: Keplerian orbital mechanics with patched-conics SOI transitions

- orbital_elements.py: elliptical + hyperbolic orbit support
- orbit_drawer.py: orbit point generation with SOI truncation
- soi_calculator.py: SOI crossing time calculator
- frame_transition.py: reference frame switching
- test_orbital.py: 147 assertions, all passing
- visual_test.py: pygame flyby visualization
This commit is contained in:
2026-05-21 20:31:17 +02:00
commit 655f1c9af6
6 changed files with 1376 additions and 0 deletions

77
orbit_drawer.py Normal file
View File

@@ -0,0 +1,77 @@
"""
Orbit trajectory point generation for rendering.
generate_orbit_points() — full orbit (elliptical) or truncated
hyperbolic path within a given cut-off radius.
"""
import math
from typing import List, Optional, Tuple
from orbital_elements import OrbitalElements, TAU
def generate_orbit_points(
orbit: OrbitalElements,
num_points: int = 200,
max_radius: Optional[float] = None,
) -> List[Tuple[float, float]]:
"""
Generate *num_points* world-space positions along the orbit.
Elliptical orbits → full closed loop, equally spaced in E.
Hyperbolic orbits → samples H ∈ [H_max, +H_max], truncated
at *max_radius* (e.g. the planet's SOI edge).
Parameters
----------
max_radius : float or None
Only used for hyperbolic orbits. Points with r > max_radius
are omitted. If None, samples out to r ≈ 5·|a| from focus.
"""
if orbit.is_hyperbolic:
return _hyperbolic_points(orbit, num_points, max_radius)
return _elliptical_points(orbit, num_points)
def _elliptical_points(
orbit: OrbitalElements, num_points: int,
) -> List[Tuple[float, float]]:
points = []
for i in range(num_points):
E = TAU * i / num_points
points.append(orbit.position_from_E(E))
return points
def _hyperbolic_points(
orbit: OrbitalElements,
num_points: int,
max_radius: Optional[float],
) -> List[Tuple[float, float]]:
"""
Sample the hyperbolic branch that passes through periapsis.
Truncates at *max_radius* by solving for the H values where
r = max_radius, then sampling uniformly between those limits.
"""
r_limit = max_radius if max_radius is not None else 5.0 * abs(orbit.a)
# Find H where r = r_limit.
# r = |a|(e·cosh(H) 1) → cosh(H) = (r/|a| + 1) / e
abs_a = abs(orbit.a)
cosh_Hmax = (r_limit / abs_a + 1.0) / orbit.e
if cosh_Hmax < 1.0:
return [] # entire orbit is outside the radius
H_max = math.acosh(cosh_Hmax)
points = []
for i in range(num_points):
frac = -1.0 + 2.0 * i / (num_points - 1) if num_points > 1 else 0.0
H = frac * H_max
x, y = orbit.position_from_H(H)
r = math.hypot(x, y)
if r <= r_limit + 1e-9:
points.append((x, y))
return points