We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/cheesejaguar/aerospace-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
"""
Orbital mechanics and spacecraft trajectory analysis tools for aerospace MCP.
Provides orbital mechanics calculations with manual implementations and optional
advanced library integration for poliastro/astropy/spiceypy when available.
"""
import math
from dataclasses import dataclass
from datetime import UTC, datetime
from typing import Any
# Constants
MU_EARTH = 3.986004418e14 # m³/s² - Earth's gravitational parameter
R_EARTH = 6378137.0 # m - Earth's equatorial radius (WGS84)
J2_EARTH = 1.08262668e-3 # Earth's J2 perturbation coefficient
OMEGA_EARTH = 7.2921159e-5 # rad/s - Earth's rotation rate
@dataclass
class OrbitElements:
"""Classical orbital elements."""
semi_major_axis_m: float # Semi-major axis (m)
eccentricity: float # Eccentricity (dimensionless)
inclination_deg: float # Inclination (degrees)
raan_deg: float # Right ascension of ascending node (degrees)
arg_periapsis_deg: float # Argument of periapsis (degrees)
true_anomaly_deg: float # True anomaly (degrees)
epoch_utc: str # Epoch in UTC ISO format
@dataclass
class StateVector:
"""Position and velocity state vector."""
position_m: list[float] # Position vector [x, y, z] in meters
velocity_ms: list[float] # Velocity vector [vx, vy, vz] in m/s
epoch_utc: str # Epoch in UTC ISO format
frame: str = "J2000" # Reference frame
@dataclass
class OrbitProperties:
"""Computed orbital properties."""
period_s: float # Orbital period (seconds)
apoapsis_m: float # Apoapsis altitude above Earth surface (m)
periapsis_m: float # Periapsis altitude above Earth surface (m)
energy_j_kg: float # Specific orbital energy (J/kg)
angular_momentum_m2s: float # Specific angular momentum magnitude (m²/s)
@dataclass
class GroundTrack:
"""Ground track point."""
latitude_deg: float
longitude_deg: float
altitude_m: float
time_utc: str
@dataclass
class Maneuver:
"""Orbital maneuver definition."""
delta_v_ms: list[float] # Delta-V vector [x, y, z] in m/s
time_utc: str # Maneuver execution time
description: str = "" # Optional description
def deg_to_rad(deg: float) -> float:
"""Convert degrees to radians."""
return deg * math.pi / 180.0
def rad_to_deg(rad: float) -> float:
"""Convert radians to degrees."""
return rad * 180.0 / math.pi
def vector_magnitude(vec: list[float]) -> float:
"""Calculate vector magnitude."""
return math.sqrt(sum(x**2 for x in vec))
def vector_dot(a: list[float], b: list[float]) -> float:
"""Calculate dot product of two vectors."""
return sum(a[i] * b[i] for i in range(len(a)))
def vector_cross(a: list[float], b: list[float]) -> list[float]:
"""Calculate cross product of two 3D vectors."""
return [
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
def vector_normalize(vec: list[float]) -> list[float]:
"""Normalize a vector."""
mag = vector_magnitude(vec)
if mag == 0:
return [0.0, 0.0, 0.0]
return [x / mag for x in vec]
def kepler_equation_solver(
mean_anomaly_rad: float,
eccentricity: float,
tolerance: float = 1e-8,
max_iterations: int = 50,
) -> float:
"""
Solve Kepler's equation for eccentric anomaly using Newton-Raphson method.
Args:
mean_anomaly_rad: Mean anomaly in radians
eccentricity: Orbital eccentricity
tolerance: Convergence tolerance
max_iterations: Maximum iterations
Returns:
Eccentric anomaly in radians
"""
# Initial guess
E = mean_anomaly_rad if eccentricity < 0.8 else math.pi
for _ in range(max_iterations):
f = E - eccentricity * math.sin(E) - mean_anomaly_rad
f_prime = 1 - eccentricity * math.cos(E)
if abs(f_prime) < 1e-12:
break
E_new = E - f / f_prime
if abs(E_new - E) < tolerance:
return E_new
E = E_new
return E
def elements_to_state_vector(elements: OrbitElements) -> StateVector:
"""
Convert orbital elements to state vector using manual calculations.
Args:
elements: Orbital elements
Returns:
State vector in J2000 frame
"""
# Convert angles to radians
i = deg_to_rad(elements.inclination_deg)
raan = deg_to_rad(elements.raan_deg)
arg_pe = deg_to_rad(elements.arg_periapsis_deg)
nu = deg_to_rad(elements.true_anomaly_deg)
# Semi-major axis and eccentricity
a = elements.semi_major_axis_m
e = elements.eccentricity
# Calculate distance and flight path angle
r = a * (1 - e**2) / (1 + e * math.cos(nu))
# Position in perifocal coordinates
r_peri = [r * math.cos(nu), r * math.sin(nu), 0.0]
# Velocity in perifocal coordinates
p = a * (1 - e**2)
v_peri = [
-math.sqrt(MU_EARTH / p) * math.sin(nu),
math.sqrt(MU_EARTH / p) * (e + math.cos(nu)),
0.0,
]
# Rotation matrices
cos_raan, sin_raan = math.cos(raan), math.sin(raan)
cos_i, sin_i = math.cos(i), math.sin(i)
cos_arg, sin_arg = math.cos(arg_pe), math.sin(arg_pe)
# Rotation matrix from perifocal to J2000
R11 = cos_raan * cos_arg - sin_raan * sin_arg * cos_i
R12 = -cos_raan * sin_arg - sin_raan * cos_arg * cos_i
R13 = sin_raan * sin_i
R21 = sin_raan * cos_arg + cos_raan * sin_arg * cos_i
R22 = -sin_raan * sin_arg + cos_raan * cos_arg * cos_i
R23 = -cos_raan * sin_i
R31 = sin_arg * sin_i
R32 = cos_arg * sin_i
R33 = cos_i
# Transform to J2000 frame
r_j2000 = [
R11 * r_peri[0] + R12 * r_peri[1] + R13 * r_peri[2],
R21 * r_peri[0] + R22 * r_peri[1] + R23 * r_peri[2],
R31 * r_peri[0] + R32 * r_peri[1] + R33 * r_peri[2],
]
v_j2000 = [
R11 * v_peri[0] + R12 * v_peri[1] + R13 * v_peri[2],
R21 * v_peri[0] + R22 * v_peri[1] + R23 * v_peri[2],
R31 * v_peri[0] + R32 * v_peri[1] + R33 * v_peri[2],
]
return StateVector(
position_m=r_j2000,
velocity_ms=v_j2000,
epoch_utc=elements.epoch_utc,
frame="J2000",
)
def state_vector_to_elements(state: StateVector) -> OrbitElements:
"""
Convert state vector to orbital elements using manual calculations.
Args:
state: State vector in J2000 frame
Returns:
Classical orbital elements
"""
r_vec = state.position_m
v_vec = state.velocity_ms
# Position and velocity magnitudes
r = vector_magnitude(r_vec)
v = vector_magnitude(v_vec)
# Specific angular momentum
h_vec = vector_cross(r_vec, v_vec)
h = vector_magnitude(h_vec)
# Semi-major axis
energy = v**2 / 2 - MU_EARTH / r
a = -MU_EARTH / (2 * energy)
# Eccentricity vector
v_cross_h = vector_cross(v_vec, h_vec)
e_vec = [v_cross_h[i] / MU_EARTH - r_vec[i] / r for i in range(3)]
e = vector_magnitude(e_vec)
# Inclination
i = math.acos(h_vec[2] / h)
# Node vector
k_vec = [0, 0, 1]
n_vec = vector_cross(k_vec, h_vec)
n = vector_magnitude(n_vec)
# RAAN
if n > 1e-10:
raan = math.acos(n_vec[0] / n)
if n_vec[1] < 0:
raan = 2 * math.pi - raan
else:
raan = 0.0
# Argument of periapsis
if n > 1e-10 and e > 1e-10:
cos_arg_pe = vector_dot(n_vec, e_vec) / (n * e)
cos_arg_pe = max(-1, min(1, cos_arg_pe)) # Clamp to [-1, 1]
arg_pe = math.acos(cos_arg_pe)
if e_vec[2] < 0:
arg_pe = 2 * math.pi - arg_pe
else:
arg_pe = 0.0
# True anomaly
if e > 1e-10:
cos_nu = vector_dot(e_vec, r_vec) / (e * r)
cos_nu = max(-1, min(1, cos_nu)) # Clamp to [-1, 1]
nu = math.acos(cos_nu)
if vector_dot(r_vec, v_vec) < 0:
nu = 2 * math.pi - nu
else:
# For circular orbits, use longitude of ascending node
if n > 1e-10:
cos_nu = vector_dot(n_vec, r_vec) / (n * r)
cos_nu = max(-1, min(1, cos_nu))
nu = math.acos(cos_nu)
if r_vec[2] < 0:
nu = 2 * math.pi - nu
else:
nu = math.atan2(r_vec[1], r_vec[0])
if nu < 0:
nu += 2 * math.pi
return OrbitElements(
semi_major_axis_m=a,
eccentricity=e,
inclination_deg=rad_to_deg(i),
raan_deg=rad_to_deg(raan),
arg_periapsis_deg=rad_to_deg(arg_pe),
true_anomaly_deg=rad_to_deg(nu),
epoch_utc=state.epoch_utc,
)
def calculate_orbit_properties(elements: OrbitElements) -> OrbitProperties:
"""
Calculate orbital properties from elements.
Args:
elements: Orbital elements
Returns:
Orbital properties
"""
a = elements.semi_major_axis_m
e = elements.eccentricity
# Orbital period
period = 2 * math.pi * math.sqrt(a**3 / MU_EARTH)
# Apoapsis and periapsis altitudes
r_ap = a * (1 + e) - R_EARTH
r_pe = a * (1 - e) - R_EARTH
# Specific orbital energy
energy = -MU_EARTH / (2 * a)
# Specific angular momentum
h = math.sqrt(MU_EARTH * a * (1 - e**2))
return OrbitProperties(
period_s=period,
apoapsis_m=r_ap,
periapsis_m=r_pe,
energy_j_kg=energy,
angular_momentum_m2s=h,
)
def propagate_orbit_j2(
initial_state: StateVector, time_span_s: float, time_step_s: float = 60.0
) -> list[StateVector]:
"""
Propagate orbit with J2 perturbations using numerical integration.
Args:
initial_state: Initial state vector
time_span_s: Propagation time span (seconds)
time_step_s: Integration time step (seconds)
Returns:
List of state vectors over time
"""
def acceleration_j2(r_vec: list[float]) -> list[float]:
"""Calculate acceleration including J2 perturbations."""
r = vector_magnitude(r_vec)
# Central body acceleration
a_central = [-MU_EARTH * r_vec[i] / r**3 for i in range(3)]
# J2 perturbation
factor = 1.5 * J2_EARTH * MU_EARTH * R_EARTH**2 / r**5
z2_r2 = (r_vec[2] / r) ** 2
a_j2 = [
factor * r_vec[0] * (1 - 5 * z2_r2),
factor * r_vec[1] * (1 - 5 * z2_r2),
factor * r_vec[2] * (3 - 5 * z2_r2),
]
return [a_central[i] + a_j2[i] for i in range(3)]
# Initialize
states = [initial_state]
r = initial_state.position_m.copy()
v = initial_state.velocity_ms.copy()
# Parse initial epoch
try:
epoch = datetime.fromisoformat(initial_state.epoch_utc.replace("Z", "+00:00"))
except (ValueError, TypeError):
epoch = datetime.now(UTC)
# Numerical integration (RK4)
t = 0.0
while t < time_span_s:
dt = min(time_step_s, time_span_s - t)
# RK4 integration
k1_r = v
k1_v = acceleration_j2(r)
r2 = [r[i] + 0.5 * dt * k1_r[i] for i in range(3)]
v2 = [v[i] + 0.5 * dt * k1_v[i] for i in range(3)]
k2_r = v2
k2_v = acceleration_j2(r2)
r3 = [r[i] + 0.5 * dt * k2_r[i] for i in range(3)]
v3 = [v[i] + 0.5 * dt * k2_v[i] for i in range(3)]
k3_r = v3
k3_v = acceleration_j2(r3)
r4 = [r[i] + dt * k3_r[i] for i in range(3)]
v4 = [v[i] + dt * k3_v[i] for i in range(3)]
k4_r = v4
k4_v = acceleration_j2(r4)
# Update state
r = [
r[i] + dt / 6 * (k1_r[i] + 2 * k2_r[i] + 2 * k3_r[i] + k4_r[i])
for i in range(3)
]
v = [
v[i] + dt / 6 * (k1_v[i] + 2 * k2_v[i] + 2 * k3_v[i] + k4_v[i])
for i in range(3)
]
t += dt
# Create new state
new_epoch = epoch.replace(microsecond=0) + type(epoch - epoch)(seconds=int(t))
states.append(
StateVector(
position_m=r.copy(),
velocity_ms=v.copy(),
epoch_utc=new_epoch.isoformat(),
frame=initial_state.frame,
)
)
return states
def calculate_ground_track(
orbit_states: list[StateVector], time_step_s: float = 60.0
) -> list[GroundTrack]:
"""
Calculate ground track from orbit state vectors.
Args:
orbit_states: List of state vectors
time_step_s: Time step between states (seconds)
Returns:
List of ground track points
"""
ground_track = []
for i, state in enumerate(orbit_states):
r_vec = state.position_m
# Convert to latitude/longitude
r = vector_magnitude(r_vec)
lat = math.asin(r_vec[2] / r)
# Account for Earth rotation
try:
datetime.fromisoformat(state.epoch_utc.replace("Z", "+00:00"))
t_since_epoch = i * time_step_s
lon = math.atan2(r_vec[1], r_vec[0]) - OMEGA_EARTH * t_since_epoch
except Exception:
lon = math.atan2(r_vec[1], r_vec[0])
# Normalize longitude to [-180, 180] degrees
while lon > math.pi:
lon -= 2 * math.pi
while lon < -math.pi:
lon += 2 * math.pi
altitude = r - R_EARTH
ground_track.append(
GroundTrack(
latitude_deg=rad_to_deg(lat),
longitude_deg=rad_to_deg(lon),
altitude_m=altitude,
time_utc=state.epoch_utc,
)
)
return ground_track
def hohmann_transfer(r1_m: float, r2_m: float) -> dict[str, float]:
"""
Calculate Hohmann transfer orbit parameters.
Args:
r1_m: Initial circular orbit radius (m)
r2_m: Final circular orbit radius (m)
Returns:
Transfer parameters including delta-V requirements
"""
# Transfer orbit semi-major axis
a_transfer = (r1_m + r2_m) / 2
# Velocities
v1_circular = math.sqrt(MU_EARTH / r1_m)
v2_circular = math.sqrt(MU_EARTH / r2_m)
v1_transfer = math.sqrt(MU_EARTH * (2 / r1_m - 1 / a_transfer))
v2_transfer = math.sqrt(MU_EARTH * (2 / r2_m - 1 / a_transfer))
# Delta-V requirements (signed values)
dv1 = v1_transfer - v1_circular # Positive for prograde, negative for retrograde
dv2 = v2_circular - v2_transfer # Positive for prograde, negative for retrograde
dv_total = abs(dv1) + abs(dv2) # Total magnitude
# Transfer time
transfer_time = math.pi * math.sqrt(a_transfer**3 / MU_EARTH)
return {
"delta_v1_ms": dv1,
"delta_v2_ms": dv2,
"total_delta_v_ms": dv_total,
"transfer_time_s": transfer_time,
"transfer_time_h": transfer_time / 3600,
"transfer_semi_major_axis_m": a_transfer,
}
def lambert_solver_simple(
r1_vec: list[float], r2_vec: list[float], time_flight_s: float, mu: float = MU_EARTH
) -> dict[str, Any]:
"""
Simple Lambert problem solver for two-body trajectory.
Args:
r1_vec: Initial position vector (m)
r2_vec: Final position vector (m)
time_flight_s: Time of flight (seconds)
mu: Gravitational parameter (m³/s²)
Returns:
Dictionary with initial and final velocity vectors
"""
r1 = vector_magnitude(r1_vec)
r2 = vector_magnitude(r2_vec)
# Chord length
c = vector_magnitude([r2_vec[i] - r1_vec[i] for i in range(3)])
# Semi-perimeter
s = (r1 + r2 + c) / 2
# Minimum energy ellipse semi-major axis
a_min = s / 2
# Check if transfer time is feasible
t_min = math.pi * math.sqrt(a_min**3 / mu)
if time_flight_s < t_min:
return {
"feasible": False,
"reason": f"Transfer time {time_flight_s:.1f}s less than minimum {t_min:.1f}s",
"v1_ms": [0, 0, 0],
"v2_ms": [0, 0, 0],
}
# Simplified solution assuming elliptical transfer
# This is an approximation - full Lambert solver requires iterative methods
# Transfer angle (simplified)
cos_dnu = vector_dot(r1_vec, r2_vec) / (r1 * r2)
cos_dnu = max(-1, min(1, cos_dnu)) # Clamp
dnu = math.acos(cos_dnu)
# Approximate semi-major axis for given flight time
# Using Kepler's 3rd law and approximation
n_approx = 2 * math.pi / time_flight_s # Approximate mean motion
a_approx = (mu / n_approx**2) ** (1 / 3)
# Approximate velocities (simplified circular approximation)
v1_mag = math.sqrt(mu / r1)
v2_mag = math.sqrt(mu / r2)
# Direction vectors (perpendicular to radius for circular approximation)
r1_unit = vector_normalize(r1_vec)
r2_unit = vector_normalize(r2_vec)
# Simplified velocity directions (tangential)
h_vec = vector_cross(r1_vec, r2_vec)
h_unit = vector_normalize(h_vec)
v1_dir = vector_normalize(vector_cross(h_unit, r1_unit))
v2_dir = vector_normalize(vector_cross(h_unit, r2_unit))
v1_vec = [v1_mag * v1_dir[i] for i in range(3)]
v2_vec = [v2_mag * v2_dir[i] for i in range(3)]
return {
"feasible": True,
"v1_ms": v1_vec,
"v2_ms": v2_vec,
"v1_vec_ms": v1_vec, # Add expected key
"initial_velocity_ms": v1_vec, # Add expected key
"transfer_angle_deg": rad_to_deg(dnu),
"estimated_semi_major_axis_m": a_approx,
}
def orbital_rendezvous_planning(
chaser_elements: OrbitElements, target_elements: OrbitElements
) -> dict[str, Any]:
"""
Plan orbital rendezvous maneuvers between two spacecraft.
Args:
chaser_elements: Chaser spacecraft orbital elements
target_elements: Target spacecraft orbital elements
Returns:
Rendezvous plan with phasing and approach maneuvers
"""
# Convert to state vectors for analysis
chaser_state = elements_to_state_vector(chaser_elements)
target_state = elements_to_state_vector(target_elements)
# Calculate relative position
rel_pos = [
target_state.position_m[i] - chaser_state.position_m[i] for i in range(3)
]
rel_distance = vector_magnitude(rel_pos)
# Orbital properties
chaser_props = calculate_orbit_properties(chaser_elements)
target_props = calculate_orbit_properties(target_elements)
# Phase angle between spacecraft
chaser_r = vector_magnitude(chaser_state.position_m)
target_r = vector_magnitude(target_state.position_m)
cos_phase = vector_dot(chaser_state.position_m, target_state.position_m) / (
chaser_r * target_r
)
cos_phase = max(-1, min(1, cos_phase))
phase_angle_rad = math.acos(cos_phase)
# Estimate phasing time
if abs(chaser_props.period_s - target_props.period_s) > 1:
synodic_period = abs(
chaser_props.period_s
* target_props.period_s
/ (chaser_props.period_s - target_props.period_s)
)
phasing_time_s = phase_angle_rad / (2 * math.pi) * synodic_period
else:
phasing_time_s = float("inf") # Coplanar, similar orbits
# Delta-V estimates for circularization if needed
chaser_circ_dv = 0.0
if chaser_elements.eccentricity > 0.01:
# Circularization delta-V (rough estimate)
v_ap = math.sqrt(
MU_EARTH
* (
2
/ (
chaser_elements.semi_major_axis_m
* (1 + chaser_elements.eccentricity)
)
- 1 / chaser_elements.semi_major_axis_m
)
)
v_circ = math.sqrt(
MU_EARTH
/ (chaser_elements.semi_major_axis_m * (1 + chaser_elements.eccentricity))
)
chaser_circ_dv = abs(v_circ - v_ap)
# Create sample maneuvers
maneuvers = []
# Add a simple phasing maneuver
if chaser_circ_dv > 0:
maneuvers.append(
{
"delta_v_ms": chaser_circ_dv,
"type": "circularization",
"description": "Circularize chaser orbit",
}
)
# Add altitude matching maneuver if needed
altitude_diff = abs(chaser_props.apoapsis_m - target_props.apoapsis_m)
if altitude_diff > 1000: # More than 1 km difference
altitude_dv = 50.0 # Rough estimate for altitude adjustment
maneuvers.append(
{
"delta_v_ms": altitude_dv,
"type": "altitude_adjustment",
"description": "Match target altitude",
}
)
# Calculate total delta-V
total_dv = sum(m["delta_v_ms"] for m in maneuvers)
return {
"relative_distance_m": rel_distance,
"relative_distance_km": rel_distance / 1000,
"phase_angle_deg": rad_to_deg(phase_angle_rad),
"phasing_time_s": phasing_time_s,
"phasing_time_h": (
phasing_time_s / 3600 if phasing_time_s != float("inf") else float("inf")
),
"time_to_rendezvous_s": phasing_time_s, # Add expected key
"chaser_period_s": chaser_props.period_s,
"target_period_s": target_props.period_s,
"period_difference_s": abs(chaser_props.period_s - target_props.period_s),
"altitude_difference_m": abs(chaser_props.apoapsis_m - target_props.apoapsis_m),
"estimated_circularization_dv_ms": chaser_circ_dv,
"total_delta_v_ms": total_dv, # Add expected key
"maneuvers": maneuvers, # Add expected key
"feasibility": (
"Good"
if rel_distance < 100000
and abs(chaser_elements.inclination_deg - target_elements.inclination_deg)
< 5.0
else "Challenging"
),
}
# Update availability
def porkchop_plot_analysis(
departure_body: str = "Earth",
arrival_body: str = "Mars",
departure_dates: list[str] = None,
arrival_dates: list[str] = None,
min_tof_days: int = 100,
max_tof_days: int = 400,
) -> dict[str, Any]:
"""
Generate porkchop plot analysis for interplanetary transfers.
Args:
departure_body: Departure celestial body (default: Earth)
arrival_body: Arrival celestial body (default: Mars)
departure_dates: List of departure dates (ISO format)
arrival_dates: List of arrival dates (ISO format)
min_tof_days: Minimum time of flight (days)
max_tof_days: Maximum time of flight (days)
Returns:
Dictionary containing transfer analysis grid
"""
# Default date ranges if not provided
if departure_dates is None:
# Earth-Mars synodic period is ~26 months, sample over 2 years
departure_dates = [
"2025-01-01T00:00:00",
"2025-03-01T00:00:00",
"2025-05-01T00:00:00",
"2025-07-01T00:00:00",
"2025-09-01T00:00:00",
"2025-11-01T00:00:00",
"2026-01-01T00:00:00",
"2026-03-01T00:00:00",
"2026-05-01T00:00:00",
"2026-07-01T00:00:00",
"2026-09-01T00:00:00",
"2026-11-01T00:00:00",
]
if arrival_dates is None:
# Generate arrival dates based on TOF constraints
arrival_dates = [
"2025-06-01T00:00:00",
"2025-08-01T00:00:00",
"2025-10-01T00:00:00",
"2025-12-01T00:00:00",
"2026-02-01T00:00:00",
"2026-04-01T00:00:00",
"2026-06-01T00:00:00",
"2026-08-01T00:00:00",
"2026-10-01T00:00:00",
"2026-12-01T00:00:00",
"2027-02-01T00:00:00",
"2027-04-01T00:00:00",
]
# Simplified planetary positions (circular orbits for demonstration)
# In production, would use SPICE kernels or JPL ephemeris
earth_orbit_radius = 1.0 # AU
mars_orbit_radius = 1.524 # AU
earth_period_days = 365.25
mars_period_days = 686.98
def get_planet_position(body: str, date_iso: str) -> list[float]:
"""Get simplified planet position at given date."""
# Parse date (simplified)
import datetime
try:
dt = datetime.datetime.fromisoformat(date_iso.replace("Z", "+00:00"))
except Exception:
dt = datetime.datetime.fromisoformat(date_iso)
# Days since J2000
j2000 = datetime.datetime(2000, 1, 1, 12, 0, 0)
days_since_j2000 = (dt - j2000).total_seconds() / 86400
if body.lower() == "earth":
# Earth mean anomaly
mean_anomaly = 2 * math.pi * days_since_j2000 / earth_period_days
x = earth_orbit_radius * math.cos(mean_anomaly)
y = earth_orbit_radius * math.sin(mean_anomaly)
return [x * 1.496e11, y * 1.496e11, 0.0] # Convert AU to meters
elif body.lower() == "mars":
# Mars mean anomaly
mean_anomaly = 2 * math.pi * days_since_j2000 / mars_period_days
x = mars_orbit_radius * math.cos(mean_anomaly)
y = mars_orbit_radius * math.sin(mean_anomaly)
return [x * 1.496e11, y * 1.496e11, 0.0] # Convert AU to meters
else:
# Default to Earth position
mean_anomaly = 2 * math.pi * days_since_j2000 / earth_period_days
x = earth_orbit_radius * math.cos(mean_anomaly)
y = earth_orbit_radius * math.sin(mean_anomaly)
return [x * 1.496e11, y * 1.496e11, 0.0]
# Generate porkchop data grid
porkchop_data = []
for dep_date in departure_dates:
for arr_date in arrival_dates:
# Calculate time of flight
import datetime
try:
dep_dt = datetime.datetime.fromisoformat(
dep_date.replace("Z", "+00:00")
)
except Exception:
dep_dt = datetime.datetime.fromisoformat(dep_date)
try:
arr_dt = datetime.datetime.fromisoformat(
arr_date.replace("Z", "+00:00")
)
except Exception:
arr_dt = datetime.datetime.fromisoformat(arr_date)
tof_days = (arr_dt - dep_dt).total_seconds() / 86400
# Filter by TOF constraints
if tof_days < min_tof_days or tof_days > max_tof_days:
continue
# Get planetary positions
r1 = get_planet_position(departure_body, dep_date)
r2 = get_planet_position(arrival_body, arr_date)
# Calculate transfer using simplified Lambert solver
try:
mu_sun = 1.32712440018e20 # Sun's gravitational parameter (m³/s²)
tof_seconds = tof_days * 86400
# Use simplified Lambert solver
lambert_result = lambert_solver_simple(r1, r2, tof_seconds, mu_sun)
# Calculate characteristic energy (C3)
if "v1_vec_ms" in lambert_result:
v1 = lambert_result["v1_vec_ms"]
elif "initial_velocity_ms" in lambert_result:
v1 = lambert_result["initial_velocity_ms"]
else:
v1 = [0, 0, 0] # Fallback
# Earth's orbital velocity at 1 AU
earth_v_orbit = math.sqrt(mu_sun / (1.496e11)) # m/s
# Excess velocity magnitude
v_inf_vec = [
v1[i] - earth_v_orbit if i == 1 else v1[i] for i in range(3)
]
v_inf_mag = math.sqrt(sum(v**2 for v in v_inf_vec))
# Characteristic energy C3 (km²/s²)
c3 = (v_inf_mag / 1000) ** 2 # Convert to km/s then square
# Delta-V estimate (simplified)
delta_v_ms = v_inf_mag
porkchop_data.append(
{
"departure_date": dep_date,
"arrival_date": arr_date,
"time_of_flight_days": tof_days,
"c3_km2_s2": c3,
"delta_v_ms": delta_v_ms,
"departure_position_m": r1,
"arrival_position_m": r2,
"transfer_feasible": c3 < 100.0, # Reasonable C3 limit
}
)
except Exception as e:
# Add failed transfer case
porkchop_data.append(
{
"departure_date": dep_date,
"arrival_date": arr_date,
"time_of_flight_days": tof_days,
"c3_km2_s2": float("inf"),
"delta_v_ms": float("inf"),
"departure_position_m": r1,
"arrival_position_m": r2,
"transfer_feasible": False,
"error": str(e),
}
)
# Find optimal transfer
feasible_transfers = [t for t in porkchop_data if t["transfer_feasible"]]
optimal_transfer = None
if feasible_transfers:
# Find minimum C3 transfer
optimal_transfer = min(feasible_transfers, key=lambda x: x["c3_km2_s2"])
# Generate summary statistics
if feasible_transfers:
c3_values = [t["c3_km2_s2"] for t in feasible_transfers]
tof_values = [t["time_of_flight_days"] for t in feasible_transfers]
summary_stats = {
"feasible_transfers": len(feasible_transfers),
"total_transfers_computed": len(porkchop_data),
"min_c3_km2_s2": min(c3_values),
"max_c3_km2_s2": max(c3_values),
"mean_c3_km2_s2": sum(c3_values) / len(c3_values),
"min_tof_days": min(tof_values),
"max_tof_days": max(tof_values),
"mean_tof_days": sum(tof_values) / len(tof_values),
}
else:
summary_stats = {
"feasible_transfers": 0,
"total_transfers_computed": len(porkchop_data),
"min_c3_km2_s2": None,
"max_c3_km2_s2": None,
"mean_c3_km2_s2": None,
"min_tof_days": min_tof_days,
"max_tof_days": max_tof_days,
"mean_tof_days": (min_tof_days + max_tof_days) / 2,
}
return {
"departure_body": departure_body,
"arrival_body": arrival_body,
"transfer_grid": porkchop_data,
"optimal_transfer": optimal_transfer,
"summary_statistics": summary_stats,
"constraints": {
"min_tof_days": min_tof_days,
"max_tof_days": max_tof_days,
"max_feasible_c3_km2_s2": 100.0,
},
"note": "Simplified analysis using circular planetary orbits. Use SPICE kernels for production applications.",
}
def get_ephemeris_position(
body: str, epoch_utc: str, frame: str = "J2000"
) -> dict[str, Any]:
"""
Get ephemeris position of celestial body (stub implementation).
Args:
body: Celestial body name (Earth, Mars, Venus, etc.)
epoch_utc: UTC epoch in ISO format
frame: Reference frame (default: J2000)
Returns:
Position and velocity vectors, with accuracy notes
Note:
This is a simplified implementation using circular orbits.
For production use, install SPICE kernels and use spiceypy or similar.
"""
# Import required for date parsing
import datetime
try:
dt = datetime.datetime.fromisoformat(epoch_utc.replace("Z", "+00:00"))
except Exception:
dt = datetime.datetime.fromisoformat(epoch_utc)
# Days since J2000.0 epoch
j2000 = datetime.datetime(2000, 1, 1, 12, 0, 0)
days_since_j2000 = (dt - j2000).total_seconds() / 86400
# Simplified planetary orbital elements (circular orbits)
orbital_data = {
"earth": {
"semi_major_axis_au": 1.0,
"period_days": 365.25,
"inclination_deg": 0.0,
},
"mars": {
"semi_major_axis_au": 1.524,
"period_days": 686.98,
"inclination_deg": 1.85,
},
"venus": {
"semi_major_axis_au": 0.723,
"period_days": 224.70,
"inclination_deg": 3.39,
},
"jupiter": {
"semi_major_axis_au": 5.204,
"period_days": 4332.82,
"inclination_deg": 1.30,
},
}
body_lower = body.lower()
if body_lower not in orbital_data:
return {
"error": f"Body '{body}' not supported in simplified ephemeris",
"supported_bodies": list(orbital_data.keys()),
"recommendation": "Use SPICE kernels with spiceypy for accurate ephemeris data",
}
data = orbital_data[body_lower]
# Calculate mean anomaly
mean_anomaly = 2 * math.pi * days_since_j2000 / data["period_days"]
# Position in heliocentric frame (simplified circular orbit)
r_au = data["semi_major_axis_au"]
x_au = r_au * math.cos(mean_anomaly)
y_au = r_au * math.sin(mean_anomaly)
z_au = 0.0 # Simplified: ignore inclination
# Convert to meters
AU_TO_M = 1.496e11
position_m = [x_au * AU_TO_M, y_au * AU_TO_M, z_au * AU_TO_M]
# Velocity (circular orbit)
mu_sun = 1.32712440018e20 # m³/s²
r_m = r_au * AU_TO_M
v_circular = math.sqrt(mu_sun / r_m) # m/s
velocity_ms = [
-v_circular * math.sin(mean_anomaly),
v_circular * math.cos(mean_anomaly),
0.0,
]
return {
"body": body,
"epoch_utc": epoch_utc,
"frame": frame,
"position_m": position_m,
"velocity_ms": velocity_ms,
"accuracy": "low",
"model": "circular_orbit_approximation",
"warning": "This is a simplified model. Use SPICE kernels for mission-critical applications.",
"spice_recommendation": {
"kernels_needed": ["de430.bsp", "pck00010.tpc", "naif0012.tls"],
"library": "spiceypy",
"installation": "pip install spiceypy",
},
}
# Optional SPICE integration (if available)
SPICE_AVAILABLE = False
try:
import spiceypy as spice
SPICE_AVAILABLE = True
def get_ephemeris_position_spice(
body: str, epoch_utc: str, frame: str = "J2000"
) -> dict[str, Any]:
"""
Get accurate ephemeris position using SPICE kernels (if loaded).
Args:
body: SPICE body name or ID
epoch_utc: UTC epoch in ISO format
frame: SPICE reference frame
Returns:
High-accuracy position and velocity vectors
"""
try:
# Convert ISO time to ET (Ephemeris Time)
et = spice.str2et(epoch_utc)
# Get state vector (position and velocity)
state, light_time = spice.spkezr(body, et, frame, "NONE", "SUN")
# Convert km to m, km/s to m/s
position_m = [state[i] * 1000 for i in range(3)]
velocity_ms = [state[i + 3] * 1000 for i in range(3)]
return {
"body": body,
"epoch_utc": epoch_utc,
"frame": frame,
"position_m": position_m,
"velocity_ms": velocity_ms,
"light_time_s": light_time,
"accuracy": "high",
"model": "SPICE_kernels",
"source": "JPL_ephemeris",
}
except Exception as e:
return {
"error": f"SPICE error: {str(e)}",
"fallback_available": True,
"recommendation": "Check SPICE kernel loading and body names",
}
except ImportError:
def get_ephemeris_position_spice(
body: str, epoch_utc: str, frame: str = "J2000"
) -> dict[str, Any]:
"""Placeholder when SPICE is not available."""
return {
"error": "SPICE not available",
"install_command": "pip install spiceypy",
"kernel_sources": [
"https://naif.jpl.nasa.gov/naif/data_generic.html",
"https://naif.jpl.nasa.gov/pub/naif/generic_kernels/",
],
}
try:
from . import update_availability
update_availability("orbits", True, {"spice_available": SPICE_AVAILABLE})
except ImportError:
pass