@ -36,27 +36,20 @@ git stash pop # Apply debug changes
### Step 1.1: Add spacecraft to `tests/configs/earth_mars_simple.toml`
Append to config file:
```toml
[[bodies]]
name = "Spacecraft"
mass = 1.0
radius = 1000.0
# Position and velocity will be initialized at runtime for LEO orbit
position = { x = 0.0, y = 0.0, z = 0.0 }
velocity = { x = 0.0, y = 0.0, z = 0.0 }
parent_index = 1 # Earth
color = { r = 1.0, g = 0.0, b = 0.5 }
eccentricity = 0.0
# Semi-major axis will be: Earth radius + 200km
semi_major_axis = 6.571e6 # Placeholder, will be set during initialization
```
Add Spacecraft body to config with placeholder position/velocity (set at runtime by `initialize_spacecraft_leo()` ).
**Implementation:** See `tests/configs/earth_mars_simple.toml` for full config
**Note**: Position/velocity are placeholders; will be calculated by `initialize_spacecraft_leo()` at runtime.
**Key parameters:**
- mass = 1.0 kg (test particle)
- radius = 1000.0 m
- parent_index = 1 (Earth)
- color = magenta (r=1.0, g=0.0, b=0.5)
- position/velocity: Placeholders (0,0,0)
**TODO**: Future config file format should support:
- Earth-relative position (e.g., `{ altitude_km = 200.0 }` )
- Earth-relative velocity (e.g., `{ orbit_type = "circular" }` )
**TODO**: Future config format should support:
- Earth-relative position: `{ altitude_km = 200.0 }`
- Earth-relative orbit: `{ orbit_type = "circular" }`
- More intuitive spacecraft mission parameters
---
@ -65,334 +58,86 @@ semi_major_axis = 6.571e6 # Placeholder, will be set during initialization
### Step 2.1: Add function declarations to `src/mission_planning.h`
```cpp
// Initialize spacecraft in circular LEO around parent body
void initialize_spacecraft_leo(CelestialBody* spacecraft, CelestialBody* parent,
double altitude_m);
**Implementation:** See `src/mission_planning.h`
// Apply patched conics impulse burn for Hohmann transfer
void apply_transfer_burn(SimulationState* sim, int spacecraft_idx,
int departure_idx, TransferParameters* params);
// Helper: Calculate current phase angle between two bodies (in degrees)
double calculate_phase_angle(SimulationState* sim, int departure_idx, int arrival_idx);
```
**Functions:**
- `initialize_spacecraft_leo()` - Initialize spacecraft in circular LEO around parent body
- `apply_transfer_burn()` - Apply patched conics impulse burn for Hohmann transfer
- `calculate_phase_angle()` - Calculate current phase angle between two bodies (in degrees)
### Step 2.2: Implement `initialize_spacecraft_leo()` in `src/mission_planning.cpp`
**Algorithm**:
```cpp
void initialize_spacecraft_leo(CelestialBody* spacecraft, CelestialBody* parent,
double altitude_m) {
// Calculate orbital radius (distance from Earth center)
double orbital_radius = parent->radius + altitude_m;
// Position spacecraft radially outward from Earth-Sun line
// Get vector from Sun to Earth
Vec3 sun_to_earth = vec3_sub(parent->position,
(Vec3){0.0, 0.0, 0.0}); // Sun at origin
Vec3 direction = vec3_normalize(sun_to_earth);
**Implementation:** `src/mission_planning.cpp:20-56`
// Position: Earth position + offset radially outward
Vec3 offset = vec3_scale(direction, orbital_radius);
spacecraft->position = vec3_add(parent->position, offset);
**Algorithm:**
- Calculate orbital radius = parent radius + altitude
- Position spacecraft radially outward from Sun (any angular position acceptable)
- Calculate circular LEO velocity: v = sqrt(G * M_parent / r)
- Set prograde orientation (tangential to Earth-Sun line)
- Set both local and global coordinates correctly
// Initialize local coordinates (relative to parent)
spacecraft->local_position = offset;
spacecraft->local_velocity = (Vec3){0.0, 0.0, 0.0}; // Will be set below
// Calculate circular LEO velocity magnitude
double v_leo = sqrt(G * parent->mass / orbital_radius);
// Direction: tangential to Earth-Sun line (prograde)
// If sun_to_earth = (x, y, 0), then tangent = (-y, x, 0)
Vec3 leo_tangent = (Vec3){-direction.y, direction.x, 0.0};
Vec3 leo_velocity = vec3_scale(leo_tangent, v_leo);
// Spacecraft velocity = Earth velocity + LEO velocity
spacecraft->velocity = vec3_add(parent->velocity, leo_velocity);
// Local velocity relative to Earth = LEO velocity only
spacecraft->local_velocity = leo_velocity;
// Update semi-major axis for reference
spacecraft->semi_major_axis = orbital_radius;
// SOI will be calculated by config loader
}
```
**Key Points**:
- Spacecraft positioned radially outward from Sun (any position is acceptable)
- LEO orbit is circular at 200km altitude
- Prograde orientation (same direction as Earth's orbital velocity)
- Both local and global coordinates set correctly
**Key Points:**
- LEO orbit is circular at 200km altitude (~7,788 m/s)
- Spacecraft velocity = Earth velocity + LEO velocity
- Local velocity = LEO velocity only (relative to Earth)
### Step 2.3: Implement `calculate_phase_angle()` in `src/mission_planning.cpp`
**Algorithm**:
```cpp
double calculate_phase_angle(SimulationState* sim, int departure_idx, int arrival_idx) {
CelestialBody* departure = &sim->bodies[departure_idx];
CelestialBody* arrival = &sim->bodies[arrival_idx];
CelestialBody* sun = &sim->bodies[0]; // Assume Sun at index 0
// Calculate angular positions relative to Sun
double theta_depart = calculate_angular_position(departure, sun);
double theta_arrival = calculate_angular_position(arrival, sun);
// Calculate phase difference
double phase_rad = theta_arrival - theta_depart;
// Normalize to [0, 2π)
while (phase_rad < 0.0 ) {
phase_rad += 2.0 * M_PI;
}
while (phase_rad >= 2.0 * M_PI) {
phase_rad -= 2.0 * M_PI;
}
// Convert to degrees
return phase_rad * 180.0 / M_PI;
}
```
**Implementation:** `src/mission_planning.cpp:58-78`
**Algorithm:**
- Calculate angular positions of departure and arrival bodies relative to Sun
- Compute phase difference: θ_arrival - θ_departure
- Normalize to [0°, 360°) range
- Return phase angle in degrees
### Step 2.4: Implement `apply_transfer_burn()` in `src/mission_planning.cpp`
**Algorithm (Patched Conics Approach)**:
```cpp
void apply_transfer_burn(SimulationState* sim, int spacecraft_idx,
int departure_idx, TransferParameters* params) {
CelestialBody* spacecraft = &sim->bodies[spacecraft_idx];
CelestialBody* departure = &sim->bodies[departure_idx];
CelestialBody* sun = &sim->bodies[0]; // Assume Sun at index 0
// Calculate required heliocentric transfer velocity
// v_transfer = params->departure_velocity
// Direction: prograde (tangential to Earth-Sun line)
Vec3 sun_to_earth = vec3_sub(departure->position, sun->position);
Vec3 sun_to_earth_norm = vec3_normalize(sun_to_earth);
// Tangent direction (prograde): (-y, x, 0)
Vec3 transfer_dir = (Vec3){-sun_to_earth_norm.y, sun_to_earth_norm.x, 0.0};
Vec3 v_transfer_helio = vec3_scale(transfer_dir, params->departure_velocity);
// Current heliocentric velocity
Vec3 current_helio = spacecraft->velocity;
// Calculate total Δv to apply
Vec3 delta_v = vec3_sub(v_transfer_helio, current_helio);
// Apply impulse burn
spacecraft->velocity = vec3_add(spacecraft->velocity, delta_v);
// Update local velocity
spacecraft->local_velocity = vec3_sub(spacecraft->velocity, departure->velocity);
// Print burn information
printf("Transfer burn applied:\n");
printf(" Current heliocentric velocity: (%.2f, %.2f, %.2f) m/s\n",
current_helio.x, current_helio.y, current_helio.z);
printf(" Target heliocentric velocity: (%.2f, %.2f, %.2f) m/s\n",
v_transfer_helio.x, v_transfer_helio.y, v_transfer_helio.z);
printf(" Delta-v: (%.2f, %.2f, %.2f) m/s\n",
delta_v.x, delta_v.y, delta_v.z);
printf(" Delta-v magnitude: %.2f m/s (%.3f km/s)\n",
vec3_magnitude(delta_v), vec3_magnitude(delta_v) / 1000.0);
}
```
**Implementation:** `src/mission_planning.cpp:80-116`
**Algorithm (Patched Conics Approach):**
- Calculate required heliocentric transfer velocity magnitude from params
- Determine prograde direction (tangential to departure-Sun line)
- Compute delta-v: Δv = v_transfer - v_current (vector subtraction)
- Apply impulse to spacecraft velocity
- Update local velocity relative to departure body
- Print burn information for debugging
**Note**: This is a simplified single-impulse approach. A true patched conics calculation would:
1. Calculate Δv to reach SOI boundary (escape trajectory)
**Note:** Simplified single-impulse approximation. True patched conics would:
1. Calculate Δv to reach SOI boundary
2. Calculate velocity at SOI boundary
3. Add transfer Δv at SOI boundary
4. Combine into equivalent single impulse
For initial implementation, we'll use single impulse as approximation.
---
## Phase 3: Comprehensive Test Case
### Step 3.1: Create new test in `tests/test_hohmann_transfer.cpp`
```cpp
TEST_CASE("Earth → Mars Hohmann Transfer with LEO Spacecraft", "[mission][hohmann][config][integration]") {
const double TIME_STEP = 60.0;
const double SECONDS_PER_DAY = 86400.0;
const double LEO_ALTITUDE_M = 200000.0; // 200 km
// 1. Load config with LEO spacecraft
SimulationState* sim = create_simulation(4, TIME_STEP);
REQUIRE(load_system_config(sim, "tests/configs/earth_mars_simple.toml"));
const int SUN_IDX = 0;
const int EARTH_IDX = 1;
const int MARS_IDX = 2;
const int CRAFT_IDX = 3;
// Verify spacecraft loaded
REQUIRE(sim->body_count == 4);
REQUIRE(strcmp(sim->bodies[CRAFT_IDX].name, "Spacecraft") == 0);
// 2. Initialize spacecraft LEO orbit
initialize_spacecraft_leo(& sim->bodies[CRAFT_IDX], & sim->bodies[EARTH_IDX],
LEO_ALTITUDE_M);
INFO("Spacecraft initialized at %.2f km altitude", LEO_ALTITUDE_M / 1000.0);
INFO("Spacecraft parent: %d (Earth)", sim->bodies[CRAFT_IDX].parent_index);
// 3. Verify initial LEO orbit is stable
REQUIRE(sim->bodies[CRAFT_IDX].parent_index == EARTH_IDX);
double dist_to_earth = vec3_distance(sim->bodies[CRAFT_IDX].position,
sim->bodies[EARTH_IDX].position);
double expected_radius = sim->bodies[EARTH_IDX].radius + LEO_ALTITUDE_M;
REQUIRE(fabs(dist_to_earth - expected_radius) < 1000.0 ) ; / / Within 1 km
// Verify LEO velocity magnitude
double leo_velocity_mag = sqrt(G * sim->bodies[EARTH_IDX].mass / dist_to_earth);
double v_leo_relative = vec3_magnitude(sim->bodies[CRAFT_IDX].local_velocity);
INFO("Expected LEO velocity: %.2f m/s", leo_velocity_mag);
INFO("Actual LEO velocity: %.2f m/s", v_leo_relative);
REQUIRE(fabs(v_leo_relative - leo_velocity_mag) < 10.0 ) ; / / Within 10 m / s
// Verify negative total energy (bound to Earth)
OrbitalMetrics leo_metrics = calculate_orbital_metrics(& sim->bodies[CRAFT_IDX],
&sim->bodies[EARTH_IDX]);
INFO("LEO total energy: %.2e J", leo_metrics.total_energy);
REQUIRE(leo_metrics.total_energy < 0.0 ) ;
// 4. Calculate Hohmann transfer parameters
double r_earth = vec3_distance(sim->bodies[EARTH_IDX].position,
sim->bodies[SUN_IDX].position);
double r_mars = vec3_distance(sim->bodies[MARS_IDX].position,
sim->bodies[SUN_IDX].position);
TransferParameters params = calculate_hohmann_transfer(r_earth, r_mars,
sim->bodies[SUN_IDX].mass);
INFO("Transfer time: %.2f days", params.transfer_time / SECONDS_PER_DAY);
INFO("Required phase angle: %.3f degrees", params.phase_angle_deg);
INFO("Delta-v injection: %.3f km/s", params.delta_v_injection / 1000.0);
// 5. Wait for Earth-Mars launch window
double wait_start_time = sim->time;
wait_for_launch_window(sim, EARTH_IDX, MARS_IDX, params.phase_angle_deg, 1.0);
double wait_duration = sim->time - wait_start_time;
INFO("Launch window opened after %.2f days", wait_duration / SECONDS_PER_DAY);
// 6. Verify launch window accuracy (within 1°)
double current_phase = calculate_phase_angle(sim, EARTH_IDX, MARS_IDX);
double phase_error = fabs(current_phase - params.phase_angle_deg);
if (phase_error > 180.0) phase_error = fabs(phase_error - 360.0);
INFO("Current phase angle: %.3f degrees", current_phase);
INFO("Required phase angle: %.3f degrees", params.phase_angle_deg);
INFO("Phase angle error: %.3f degrees", phase_error);
REQUIRE(phase_error < 1.0 ) ;
// 7. Apply impulse burn for transfer
double pre_burn_time = sim->time;
OrbitalMetrics pre_burn_metrics = calculate_orbital_metrics(& sim->bodies[CRAFT_IDX],
&sim->bodies[SUN_IDX]);
apply_transfer_burn(sim, CRAFT_IDX, EARTH_IDX, ¶ms);
OrbitalMetrics post_burn_metrics = calculate_orbital_metrics(& sim->bodies[CRAFT_IDX],
&sim->bodies[SUN_IDX]);
INFO("Pre-burn heliocentric energy: %.2e J", pre_burn_metrics.total_energy);
INFO("Post-burn heliocentric energy: %.2e J", post_burn_metrics.total_energy);
INFO("Energy added: %.2e J",
post_burn_metrics.total_energy - pre_burn_metrics.total_energy);
// Verify spacecraft is now in escape trajectory (positive or zero energy)
REQUIRE(post_burn_metrics.total_energy >= 0.0);
// 8. Track SOI transitions during transfer
int earth_soi_exit_step = 0;
int sun_soi_enter_step = 0;
int mars_soi_enter_step = 0;
double transfer_duration = params.transfer_time * 1.1;
int max_steps = (int)(transfer_duration / sim->dt);
INFO("Simulating for %.2f days (%d steps)",
transfer_duration / SECONDS_PER_DAY, max_steps);
for (int step = 0; step < max_steps ; step + + ) {
update_simulation(sim);
// Track Earth SOI exit
if (earth_soi_exit_step == 0 & &
sim->bodies[CRAFT_IDX].parent_index != EARTH_IDX) {
earth_soi_exit_step = step;
INFO("Earth SOI exit at step %d (t = %.2f days)",
step, sim->time / SECONDS_PER_DAY);
}
// Track Sun SOI entry (after leaving Earth)
if (earth_soi_exit_step > 0 & & sun_soi_enter_step == 0 & &
sim->bodies[CRAFT_IDX].parent_index == SUN_IDX) {
sun_soi_enter_step = step;
INFO("Sun SOI entry at step %d (t = %.2f days)",
step, sim->time / SECONDS_PER_DAY);
}
// Track Mars SOI entry
if (mars_soi_enter_step == 0 & &
sim->bodies[CRAFT_IDX].parent_index == MARS_IDX) {
mars_soi_enter_step = step;
INFO("Mars SOI entry at step %d (t = %.2f days)",
step, sim->time / SECONDS_PER_DAY);
}
}
// 9. Verify Earth → Sun transition occurred
INFO("Earth SOI exit step: %d", earth_soi_exit_step);
INFO("Sun SOI entry step: %d", sun_soi_enter_step);
REQUIRE(earth_soi_exit_step > 0);
REQUIRE(sun_soi_enter_step > 0);
// Final parent should be Sun or Mars
int final_parent = sim->bodies[CRAFT_IDX].parent_index;
REQUIRE(final_parent == SUN_IDX || final_parent == MARS_IDX);
INFO("Final parent: %d (%s)", final_parent,
final_parent == SUN_IDX ? "Sun" : "Mars");
// 10. Verify spacecraft followed transfer orbit (energy conservation)
OrbitalMetrics final_metrics = calculate_orbital_metrics(& sim->bodies[CRAFT_IDX],
&sim->bodies[SUN_IDX]);
double energy_drift = fabs(final_metrics.total_energy - post_burn_metrics.total_energy);
if (post_burn_metrics.total_energy != 0.0) {
energy_drift /= fabs(post_burn_metrics.total_energy);
}
INFO("Final orbital radius: %.2f AU",
final_metrics.orbital_radius / 1.496e11);
INFO("Final energy: %.2e J", final_metrics.total_energy);
INFO("Expected energy: %.2e J", post_burn_metrics.total_energy);
INFO("Energy drift: %.2f%%", energy_drift * 100.0);
REQUIRE(energy_drift < 0.05 ) ; / / < 5 % energy conservation
// 11. If Mars SOI entry occurred, verify distance
if (mars_soi_enter_step > 0) {
double dist_to_mars = vec3_distance(sim->bodies[CRAFT_IDX].position,
sim->bodies[MARS_IDX].position);
INFO("Distance to Mars: %.2f km", dist_to_mars / 1000.0);
INFO("Mars SOI radius: %.2f km", sim->bodies[MARS_IDX].soi_radius / 1000.0);
REQUIRE(dist_to_mars < 2.0 * sim- > bodies[MARS_IDX].soi_radius);
} else {
INFO("Spacecraft did not enter Mars SOI within simulation time");
INFO("This may be due to phase angle or timing inaccuracies");
}
destroy_simulation(sim);
}
```
**Implementation:** `tests/test_hohmann_transfer.cpp` - See file for full test
**Test:** "Earth → Mars Hohmann Transfer with LEO Spacecraft"
**Test Structure:**
1. Load config with 4 bodies (Sun, Earth, Mars, Spacecraft)
2. Initialize spacecraft in 200km LEO around Earth
3. Verify LEO orbit stability (parent, position, velocity, energy)
4. Calculate Hohmann transfer parameters
5. Wait for Earth-Mars launch window (within 1° tolerance)
6. Verify phase angle accuracy
7. Apply impulse burn for transfer
8. Verify post-burn energy >= 0 (escape trajectory)
9. Simulate transfer for 110% of expected duration
10. Track SOI transitions (Earth→Sun→Mars)
11. Verify final parent and energy conservation (< 5 % drift )
12. If Mars SOI entry, verify distance (< 2 × SOI )
**Key Assertions:**
- Config loading: 4 bodies loaded, spacecraft present
- LEO stability: parent=Earth, position < 1km error , velocity < 10m / s error , energy < 0
- Launch window: opens in ~94 days, phase error < 1 °
- Transfer: post-burn energy >= 0, Earth→Sun SOI transition, energy conservation
---
@ -578,50 +323,20 @@ Spacecraft may not enter Mars SOI due to:
## Test Configuration Reference
### earth_mars_simple.toml
```toml
[[bodies]]
name = "Sun"
mass = 1.989e30
radius = 6.96e8
position = { x = 0.0, y = 0.0, z = 0.0 }
parent_index = -1
color = { r = 1.0, g = 1.0, b = 0.0 }
eccentricity = 0.0
semi_major_axis = 0.0
[[bodies]]
name = "Earth"
mass = 5.972e24
radius = 6.371e6
position = { x = 1.496e11, y = 0.0, z = 0.0 }
parent_index = 0
color = { r = 0.0, g = 0.5, b = 1.0 }
eccentricity = 0.0
semi_major_axis = 1.496e11
[[bodies]]
name = "Mars"
mass = 6.39e23
radius = 3.3895e6
position = { x = 2.279e11, y = 0.0, z = 0.0 }
parent_index = 0
color = { r = 0.8, g = 0.3, b = 0.1 }
eccentricity = 0.0
semi_major_axis = 2.279e11
[[bodies]]
name = "Spacecraft"
mass = 1.0
radius = 1000.0
# Position and velocity will be initialized at runtime for LEO orbit
position = { x = 0.0, y = 0.0, z = 0.0 }
velocity = { x = 0.0, y = 0.0, z = 0.0 }
parent_index = 1 # Earth
color = { r = 1.0, g = 0.0, b = 0.5 }
eccentricity = 0.0
# Semi-major axis will be: Earth radius + 200km
semi_major_axis = 6.571e6 # Placeholder, will be set during initialization
```
**Implementation:** `tests/configs/earth_mars_simple.toml`
**Bodies:**
- Sun (index 0): Root body, 1.989e30 kg
- Earth (index 1): 5.972e24 kg, 1.496e11 m from Sun
- Mars (index 2): 6.39e23 kg, 2.279e11 m from Sun
- Spacecraft (index 3): 1.0 kg, parent=Earth (position/velocity set at runtime)
**Spacecraft parameters:**
- mass = 1.0 kg
- radius = 1000.0 m
- parent_index = 1 (Earth)
- color = magenta (r=1.0, g=0.0, b=0.5)
- position/velocity: Placeholders (0,0,0) - set by `initialize_spacecraft_leo()`
---