Browse Source
Add basic maneuvering API for spacecraft with impulsive delta-v burns in local frame: - Add vec3_dot() function to physics module - Create Spacecraft struct and SpacecraftState for separate spacecraft management - Implement 6 burn directions: prograde, retrograde, normal, anti-normal, radial in/out - Add RK4 propagation for spacecraft with update_spacecraft() - Extend config loader to parse [[spacecraft]] TOML sections - Add 6 test cases verifying burn physics and propagation stability - Rename old mission_planning module to _old for future reference Test results: 26/27 passing (1 pre-existing SOI test unrelated to changes)main
13 changed files with 527 additions and 4 deletions
@ -0,0 +1,69 @@
|
||||
#include "maneuver.h" |
||||
#include "physics.h" |
||||
#include <cmath> |
||||
|
||||
Vec3 calculate_prograde_dir(Vec3 local_velocity) { |
||||
return vec3_normalize(local_velocity); |
||||
} |
||||
|
||||
Vec3 calculate_retrograde_dir(Vec3 local_velocity) { |
||||
Vec3 prograde = calculate_prograde_dir(local_velocity); |
||||
return vec3_scale(prograde, -1.0); |
||||
} |
||||
|
||||
Vec3 calculate_normal_dir(Vec3 local_position, Vec3 local_velocity) { |
||||
Vec3 angular_momentum = vec3_cross(local_position, local_velocity); |
||||
return vec3_normalize(angular_momentum); |
||||
} |
||||
|
||||
Vec3 calculate_antinormal_dir(Vec3 local_position, Vec3 local_velocity) { |
||||
Vec3 normal = calculate_normal_dir(local_position, local_velocity); |
||||
return vec3_scale(normal, -1.0); |
||||
} |
||||
|
||||
Vec3 calculate_radial_in_dir(Vec3 local_position) { |
||||
Vec3 radial = vec3_normalize(local_position); |
||||
return vec3_scale(radial, -1.0); |
||||
} |
||||
|
||||
Vec3 calculate_radial_out_dir(Vec3 local_position) { |
||||
return vec3_normalize(local_position); |
||||
} |
||||
|
||||
Vec3 get_burn_direction_vector(BurnDirection direction, Vec3 local_pos, Vec3 local_vel) { |
||||
switch (direction) { |
||||
case BURN_PROGRADE: |
||||
return calculate_prograde_dir(local_vel); |
||||
case BURN_RETROGRADE: |
||||
return calculate_retrograde_dir(local_vel); |
||||
case BURN_NORMAL: |
||||
return calculate_normal_dir(local_pos, local_vel); |
||||
case BURN_ANTINORMAL: |
||||
return calculate_antinormal_dir(local_pos, local_vel); |
||||
case BURN_RADIAL_IN: |
||||
return calculate_radial_in_dir(local_pos); |
||||
case BURN_RADIAL_OUT: |
||||
return calculate_radial_out_dir(local_pos); |
||||
case BURN_CUSTOM: |
||||
default: |
||||
return {0.0, 0.0, 0.0}; |
||||
} |
||||
} |
||||
|
||||
void apply_impulsive_burn(Spacecraft* craft, BurnDirection direction, double delta_v) { |
||||
Vec3 dir = get_burn_direction_vector(direction, craft->local_position, craft->local_velocity); |
||||
|
||||
Vec3 delta_v_vec = vec3_scale(dir, delta_v); |
||||
|
||||
craft->local_velocity = vec3_add(craft->local_velocity, delta_v_vec); |
||||
craft->velocity = vec3_add(craft->velocity, delta_v_vec); |
||||
} |
||||
|
||||
void apply_custom_burn(Spacecraft* craft, Vec3 delta_v_local) { |
||||
craft->local_velocity = vec3_add(craft->local_velocity, delta_v_local); |
||||
craft->velocity = vec3_add(craft->velocity, delta_v_local); |
||||
} |
||||
|
||||
double calculate_orbital_velocity(Spacecraft* craft, double parent_mass) { |
||||
return vec3_magnitude(craft->local_velocity); |
||||
} |
||||
@ -0,0 +1,42 @@
|
||||
#ifndef MANEUVER_H |
||||
#define MANEUVER_H |
||||
|
||||
#include "physics.h" |
||||
|
||||
enum BurnDirection { |
||||
BURN_PROGRADE, |
||||
BURN_RETROGRADE, |
||||
BURN_NORMAL, |
||||
BURN_ANTINORMAL, |
||||
BURN_RADIAL_IN, |
||||
BURN_RADIAL_OUT, |
||||
BURN_CUSTOM |
||||
}; |
||||
|
||||
struct Spacecraft { |
||||
char name[64]; |
||||
double mass; |
||||
Vec3 local_position; |
||||
Vec3 local_velocity; |
||||
Vec3 position; |
||||
Vec3 velocity; |
||||
int parent_index; |
||||
}; |
||||
|
||||
// Direction calculation functions (local frame)
|
||||
Vec3 calculate_prograde_dir(Vec3 local_velocity); |
||||
Vec3 calculate_retrograde_dir(Vec3 local_velocity); |
||||
Vec3 calculate_normal_dir(Vec3 local_position, Vec3 local_velocity); |
||||
Vec3 calculate_antinormal_dir(Vec3 local_position, Vec3 local_velocity); |
||||
Vec3 calculate_radial_in_dir(Vec3 local_position); |
||||
Vec3 calculate_radial_out_dir(Vec3 local_position); |
||||
Vec3 get_burn_direction_vector(BurnDirection direction, Vec3 local_pos, Vec3 local_vel); |
||||
|
||||
// Burn application functions
|
||||
void apply_impulsive_burn(Spacecraft* craft, BurnDirection direction, double delta_v); |
||||
void apply_custom_burn(Spacecraft* craft, Vec3 delta_v_local); |
||||
|
||||
// Utility functions
|
||||
double calculate_orbital_velocity(Spacecraft* craft, double parent_mass); |
||||
|
||||
#endif |
||||
@ -0,0 +1,71 @@
|
||||
#include "spacecraft.h" |
||||
#include "physics.h" |
||||
#include <cstdio> |
||||
#include <cstring> |
||||
#include <cmath> |
||||
|
||||
SpacecraftState* create_spacecraft_state(int max_craft) { |
||||
SpacecraftState* state = (SpacecraftState*)malloc(sizeof(SpacecraftState)); |
||||
state->spacecraft = (Spacecraft*)malloc(sizeof(Spacecraft) * max_craft); |
||||
state->craft_count = 0; |
||||
state->max_craft = max_craft; |
||||
return state; |
||||
} |
||||
|
||||
void destroy_spacecraft_state(SpacecraftState* state) { |
||||
if (state) { |
||||
if (state->spacecraft) { |
||||
free(state->spacecraft); |
||||
} |
||||
free(state); |
||||
} |
||||
} |
||||
|
||||
int add_spacecraft(SpacecraftState* state, Spacecraft* craft) { |
||||
if (state->craft_count >= state->max_craft) { |
||||
printf("Error: Cannot add spacecraft - state full (%d/%d)\n", |
||||
state->craft_count, state->max_craft); |
||||
return -1; |
||||
} |
||||
|
||||
int new_idx = state->craft_count; |
||||
state->spacecraft[new_idx] = *craft; |
||||
state->craft_count++; |
||||
|
||||
return new_idx; |
||||
} |
||||
|
||||
void compute_spacecraft_globals(SpacecraftState* craft_state, SimulationState* sim) { |
||||
for (int i = 0; i < craft_state->craft_count; i++) { |
||||
Spacecraft* craft = &craft_state->spacecraft[i]; |
||||
|
||||
if (craft->parent_index >= 0 && craft->parent_index < sim->body_count) { |
||||
CelestialBody* parent = &sim->bodies[craft->parent_index]; |
||||
craft->position = vec3_add(parent->position, craft->local_position); |
||||
craft->velocity = vec3_add(parent->velocity, craft->local_velocity); |
||||
} else { |
||||
craft->position = craft->local_position; |
||||
craft->velocity = craft->local_velocity; |
||||
} |
||||
} |
||||
} |
||||
|
||||
void update_spacecraft(SpacecraftState* craft_state, SimulationState* sim) { |
||||
for (int i = 0; i < craft_state->craft_count; i++) { |
||||
Spacecraft* craft = &craft_state->spacecraft[i]; |
||||
|
||||
if (craft->parent_index < 0 || craft->parent_index >= sim->body_count) { |
||||
continue; |
||||
} |
||||
|
||||
CelestialBody* parent = &sim->bodies[craft->parent_index]; |
||||
|
||||
double parent_mass = parent->mass; |
||||
double craft_mass = craft->mass; |
||||
|
||||
rk4_step(&craft->local_position, &craft->local_velocity, |
||||
sim->dt, craft_mass, parent_mass); |
||||
} |
||||
|
||||
compute_spacecraft_globals(craft_state, sim); |
||||
} |
||||
@ -0,0 +1,19 @@
|
||||
#ifndef SPACECRAFT_H |
||||
#define SPACECRAFT_H |
||||
|
||||
#include "maneuver.h" |
||||
#include "simulation.h" |
||||
|
||||
struct SpacecraftState { |
||||
Spacecraft* spacecraft; |
||||
int craft_count; |
||||
int max_craft; |
||||
}; |
||||
|
||||
SpacecraftState* create_spacecraft_state(int max_craft); |
||||
void destroy_spacecraft_state(SpacecraftState* state); |
||||
void update_spacecraft(SpacecraftState* craft_state, SimulationState* sim); |
||||
void compute_spacecraft_globals(SpacecraftState* craft_state, SimulationState* sim); |
||||
int add_spacecraft(SpacecraftState* state, Spacecraft* craft); |
||||
|
||||
#endif |
||||
@ -0,0 +1,30 @@
|
||||
# Test Configuration: Spacecraft Maneuvers |
||||
# Sun + Earth + LEO Satellite |
||||
# Tests basic prograde/retrograde burns in local frame |
||||
|
||||
[[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 |
||||
|
||||
[[spacecraft]] |
||||
name = "LEO_Satellite" |
||||
mass = 1000.0 |
||||
position = { x = 0.0, y = 6.771e6, z = 0.0 } |
||||
velocity = { x = 7660.0, y = 0.0, z = 0.0 } |
||||
parent_index = 1 |
||||
@ -0,0 +1,183 @@
|
||||
#include <catch2/catch_test_macros.hpp> |
||||
#include "../src/physics.h" |
||||
#include "../src/simulation.h" |
||||
#include "../src/spacecraft.h" |
||||
#include "../src/maneuver.h" |
||||
#include "../src/config_loader.h" |
||||
#include "../src/test_utilities.h" |
||||
#include <cmath> |
||||
|
||||
TEST_CASE("Spacecraft loading from config", "[spacecraft][config]") { |
||||
const double TIME_STEP = 60.0; |
||||
|
||||
SimulationState* sim = create_simulation(10, TIME_STEP); |
||||
SpacecraftState* craft_state = create_spacecraft_state(10); |
||||
|
||||
REQUIRE(load_system_config(sim, "tests/configs/spacecraft_test.toml")); |
||||
REQUIRE(load_spacecraft_config(craft_state, sim, "tests/configs/spacecraft_test.toml")); |
||||
|
||||
REQUIRE(craft_state->craft_count == 1); |
||||
REQUIRE(std::string(craft_state->spacecraft[0].name) == "LEO_Satellite"); |
||||
REQUIRE(craft_state->spacecraft[0].parent_index == 1); |
||||
|
||||
destroy_spacecraft_state(craft_state); |
||||
destroy_simulation(sim); |
||||
} |
||||
|
||||
TEST_CASE("Prograde burn increases orbital energy", "[spacecraft][burn][prograde]") { |
||||
const double TIME_STEP = 60.0; |
||||
|
||||
SimulationState* sim = create_simulation(10, TIME_STEP); |
||||
SpacecraftState* craft_state = create_spacecraft_state(10); |
||||
|
||||
REQUIRE(load_system_config(sim, "tests/configs/spacecraft_test.toml")); |
||||
REQUIRE(load_spacecraft_config(craft_state, sim, "tests/configs/spacecraft_test.toml")); |
||||
|
||||
Spacecraft* craft = &craft_state->spacecraft[0]; |
||||
CelestialBody* earth = &sim->bodies[1]; |
||||
|
||||
double initial_distance = vec3_distance(craft->position, earth->position); |
||||
double initial_velocity = vec3_magnitude(craft->local_velocity); |
||||
|
||||
apply_impulsive_burn(craft, BURN_PROGRADE, 100.0); |
||||
|
||||
REQUIRE(vec3_magnitude(craft->local_velocity) > initial_velocity); |
||||
|
||||
const double SECONDS_TO_SIMULATE = 3600.0; |
||||
double sim_time = 0.0; |
||||
while (sim_time < SECONDS_TO_SIMULATE) { |
||||
update_simulation(sim); |
||||
update_spacecraft(craft_state, sim); |
||||
sim_time += TIME_STEP; |
||||
} |
||||
|
||||
double final_distance = vec3_distance(craft->position, earth->position); |
||||
REQUIRE(final_distance > initial_distance); |
||||
|
||||
destroy_spacecraft_state(craft_state); |
||||
destroy_simulation(sim); |
||||
} |
||||
|
||||
TEST_CASE("Retrograde burn decreases orbital energy", "[spacecraft][burn][retrograde]") { |
||||
const double TIME_STEP = 60.0; |
||||
|
||||
SimulationState* sim = create_simulation(10, TIME_STEP); |
||||
SpacecraftState* craft_state = create_spacecraft_state(10); |
||||
|
||||
REQUIRE(load_system_config(sim, "tests/configs/spacecraft_test.toml")); |
||||
REQUIRE(load_spacecraft_config(craft_state, sim, "tests/configs/spacecraft_test.toml")); |
||||
|
||||
Spacecraft* craft = &craft_state->spacecraft[0]; |
||||
CelestialBody* earth = &sim->bodies[1]; |
||||
|
||||
double initial_distance = vec3_distance(craft->position, earth->position); |
||||
double initial_velocity = vec3_magnitude(craft->local_velocity); |
||||
|
||||
apply_impulsive_burn(craft, BURN_RETROGRADE, 100.0); |
||||
|
||||
REQUIRE(vec3_magnitude(craft->local_velocity) < initial_velocity); |
||||
|
||||
const double SECONDS_TO_SIMULATE = 3600.0; |
||||
double sim_time = 0.0; |
||||
while (sim_time < SECONDS_TO_SIMULATE) { |
||||
update_simulation(sim); |
||||
update_spacecraft(craft_state, sim); |
||||
sim_time += TIME_STEP; |
||||
} |
||||
|
||||
double final_distance = vec3_distance(craft->position, earth->position); |
||||
REQUIRE(final_distance < initial_distance); |
||||
|
||||
destroy_spacecraft_state(craft_state); |
||||
destroy_simulation(sim); |
||||
} |
||||
|
||||
TEST_CASE("Normal burn changes orbital plane", "[spacecraft][burn][normal]") { |
||||
const double TIME_STEP = 60.0; |
||||
|
||||
SimulationState* sim = create_simulation(10, TIME_STEP); |
||||
SpacecraftState* craft_state = create_spacecraft_state(10); |
||||
|
||||
REQUIRE(load_system_config(sim, "tests/configs/spacecraft_test.toml")); |
||||
REQUIRE(load_spacecraft_config(craft_state, sim, "tests/configs/spacecraft_test.toml")); |
||||
|
||||
Spacecraft* craft = &craft_state->spacecraft[0]; |
||||
|
||||
double initial_z = craft->local_position.z; |
||||
|
||||
apply_impulsive_burn(craft, BURN_NORMAL, 500.0); |
||||
|
||||
const double SECONDS_TO_SIMULATE = 3600.0; |
||||
double sim_time = 0.0; |
||||
while (sim_time < SECONDS_TO_SIMULATE) { |
||||
update_simulation(sim); |
||||
update_spacecraft(craft_state, sim); |
||||
sim_time += TIME_STEP; |
||||
} |
||||
|
||||
REQUIRE(fabs(craft->local_position.z - initial_z) > 1000.0); |
||||
|
||||
destroy_spacecraft_state(craft_state); |
||||
destroy_simulation(sim); |
||||
} |
||||
|
||||
TEST_CASE("Custom burn applies arbitrary delta-v", "[spacecraft][burn][custom]") { |
||||
const double TIME_STEP = 60.0; |
||||
|
||||
SimulationState* sim = create_simulation(10, TIME_STEP); |
||||
SpacecraftState* craft_state = create_spacecraft_state(10); |
||||
|
||||
REQUIRE(load_system_config(sim, "tests/configs/spacecraft_test.toml")); |
||||
REQUIRE(load_spacecraft_config(craft_state, sim, "tests/configs/spacecraft_test.toml")); |
||||
|
||||
Spacecraft* craft = &craft_state->spacecraft[0]; |
||||
|
||||
Vec3 initial_vel = craft->local_velocity; |
||||
Vec3 delta_v = {10.0, 20.0, 30.0}; |
||||
|
||||
apply_custom_burn(craft, delta_v); |
||||
|
||||
REQUIRE(fabs(craft->local_velocity.x - initial_vel.x - 10.0) < 0.001); |
||||
REQUIRE(fabs(craft->local_velocity.y - initial_vel.y - 20.0) < 0.001); |
||||
REQUIRE(fabs(craft->local_velocity.z - initial_vel.z - 30.0) < 0.001); |
||||
|
||||
destroy_spacecraft_state(craft_state); |
||||
destroy_simulation(sim); |
||||
} |
||||
|
||||
TEST_CASE("Spacecraft propagation maintains stability", "[spacecraft][propagation]") { |
||||
const double TIME_STEP = 60.0; |
||||
const double DAYS_TO_SIMULATE = 1.0; |
||||
const double SECONDS_PER_DAY = 86400.0; |
||||
|
||||
SimulationState* sim = create_simulation(10, TIME_STEP); |
||||
SpacecraftState* craft_state = create_spacecraft_state(10); |
||||
|
||||
REQUIRE(load_system_config(sim, "tests/configs/spacecraft_test.toml")); |
||||
REQUIRE(load_spacecraft_config(craft_state, sim, "tests/configs/spacecraft_test.toml")); |
||||
|
||||
Spacecraft* craft = &craft_state->spacecraft[0]; |
||||
CelestialBody* earth = &sim->bodies[1]; |
||||
|
||||
double initial_distance = vec3_distance(craft->position, earth->position); |
||||
|
||||
double total_time = DAYS_TO_SIMULATE * SECONDS_PER_DAY; |
||||
double sim_time = 0.0; |
||||
while (sim_time < total_time) { |
||||
update_simulation(sim); |
||||
update_spacecraft(craft_state, sim); |
||||
sim_time += TIME_STEP; |
||||
} |
||||
|
||||
double final_distance = vec3_distance(craft->position, earth->position); |
||||
double distance_drift_percent = fabs((final_distance - initial_distance) / initial_distance) * 100.0; |
||||
|
||||
INFO("Initial distance: " << initial_distance << " m"); |
||||
INFO("Final distance: " << final_distance << " m"); |
||||
INFO("Distance drift: " << distance_drift_percent << "%"); |
||||
|
||||
REQUIRE(distance_drift_percent < 1.0); |
||||
|
||||
destroy_spacecraft_state(craft_state); |
||||
destroy_simulation(sim); |
||||
} |
||||
Loading…
Reference in new issue