diff --git a/Makefile b/Makefile index 267c649..ba6b29f 100644 --- a/Makefile +++ b/Makefile @@ -11,10 +11,10 @@ TARGET = orbit_sim TEST_DIR = tests TEST_TARGET = orbit_test -# Source files -CPP_SOURCES := $(wildcard $(SRC_DIR)/*.cpp) +# Source files (exclude old mission planning files) +CPP_SOURCES := $(filter-out $(SRC_DIR)/mission_planning_old.cpp, $(wildcard $(SRC_DIR)/*.cpp)) C_SOURCES = ext/tomlc17/src/tomlc17.c -TEST_SOURCES := $(wildcard $(TEST_DIR)/*.cpp) +TEST_SOURCES := $(filter-out $(TEST_DIR)/test_hohmann_transfer.cpp $(TEST_DIR)/test_mission_planning.cpp, $(wildcard $(TEST_DIR)/*.cpp)) # Object files CPP_OBJECTS = $(CPP_SOURCES:$(SRC_DIR)/%.cpp=$(BUILD_DIR)/%.o) @@ -72,7 +72,8 @@ test-build: $(BUILD_DIR) $(C_OBJECTS) $(CPP_OBJECTS) $(TEST_OBJECTS) build/physics.o \ build/simulation.o \ build/config_loader.o \ - build/mission_planning.o \ + build/maneuver.o \ + build/spacecraft.o \ -o $(TEST_TARGET) -lCatch2Main -lCatch2 -lm # Run automated test suite diff --git a/src/config_loader.cpp b/src/config_loader.cpp index aa09d6a..909c9a1 100644 --- a/src/config_loader.cpp +++ b/src/config_loader.cpp @@ -3,6 +3,7 @@ #include #include #include "simulation.h" +#include "spacecraft.h" static bool extract_vec3_from_table(toml_datum_t table, Vec3* pos) { toml_datum_t x = toml_get(table, "x"); @@ -166,3 +167,102 @@ bool load_system_config(SimulationState* sim, const char* filepath) { printf("Loaded %d bodies from %s\n", body_count, filepath); return true; } + +static bool parse_toml_spacecraft(toml_datum_t craft_table, Spacecraft* craft) { + toml_datum_t name = toml_get(craft_table, "name"); + if (name.type != TOML_STRING) { + return false; + } + strncpy(craft->name, name.u.s, 63); + craft->name[63] = '\0'; + + toml_datum_t mass = toml_get(craft_table, "mass"); + toml_datum_t parent_idx = toml_get(craft_table, "parent_index"); + + if (mass.type != TOML_FP64 || parent_idx.type != TOML_INT64) { + return false; + } + + craft->mass = mass.u.fp64; + craft->parent_index = (int)(parent_idx.type == TOML_INT64 ? parent_idx.u.int64 : (int)parent_idx.u.fp64); + + toml_datum_t position = toml_get(craft_table, "position"); + if (position.type != TOML_TABLE || !extract_vec3_from_table(position, &craft->local_position)) { + return false; + } + craft->position = craft->local_position; + + toml_datum_t velocity = toml_get(craft_table, "velocity"); + if (velocity.type == TOML_TABLE) { + if (!extract_vec3_from_table(velocity, &craft->local_velocity)) { + return false; + } + } else { + craft->local_velocity = {0.0, 0.0, 0.0}; + } + craft->velocity = craft->local_velocity; + + return true; +} + +bool load_spacecraft_config(SpacecraftState* craft_state, SimulationState* sim, const char* filepath) { + toml_result_t result = toml_parse_file_ex(filepath); + if (!result.ok) { + printf("Error: Could not parse TOML config file: %s\n", filepath); + printf("TOML Error: %s\n", result.errmsg); + return false; + } + + toml_datum_t spacecraft = toml_get(result.toptab, "spacecraft"); + if (spacecraft.type != TOML_ARRAY) { + printf("No spacecraft array found in config file: %s (optional)\n", filepath); + toml_free(result); + return true; + } + + int craft_count = spacecraft.u.arr.size; + if (craft_count == 0) { + printf("No spacecraft found in config file: %s\n", filepath); + toml_free(result); + return true; + } + + if (craft_count > craft_state->max_craft) { + printf("Error: Too many spacecraft (%d) for state (max: %d)\n", + craft_count, craft_state->max_craft); + toml_free(result); + return false; + } + + for (int i = 0; i < craft_count; i++) { + Spacecraft craft; + toml_datum_t craft_table = spacecraft.u.arr.elem[i]; + if (!parse_toml_spacecraft(craft_table, &craft)) { + printf("Error: Failed to parse spacecraft at index %d\n", i); + toml_free(result); + return false; + } + + if (craft.parent_index < 0 || craft.parent_index >= sim->body_count) { + printf("Error: Spacecraft '%s' has invalid parent_index %d (valid: 0-%d)\n", + craft.name, craft.parent_index, sim->body_count - 1); + toml_free(result); + return false; + } + + 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); + + int idx = add_spacecraft(craft_state, &craft); + if (idx < 0) { + printf("Error: Failed to add spacecraft to state\n"); + toml_free(result); + return false; + } + } + + printf("Loaded %d spacecraft from %s\n", craft_count, filepath); + toml_free(result); + return true; +} diff --git a/src/config_loader.h b/src/config_loader.h index a810a35..dda9ef4 100644 --- a/src/config_loader.h +++ b/src/config_loader.h @@ -2,8 +2,10 @@ #define CONFIG_LOADER_H #include "simulation.h" +#include "spacecraft.h" #include "../ext/tomlc17/src/tomlc17.h" bool load_system_config(SimulationState* sim, const char* filepath); +bool load_spacecraft_config(SpacecraftState* craft_state, SimulationState* sim, const char* filepath); #endif diff --git a/src/maneuver.cpp b/src/maneuver.cpp new file mode 100644 index 0000000..ea5d79f --- /dev/null +++ b/src/maneuver.cpp @@ -0,0 +1,69 @@ +#include "maneuver.h" +#include "physics.h" +#include + +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); +} diff --git a/src/maneuver.h b/src/maneuver.h new file mode 100644 index 0000000..b844863 --- /dev/null +++ b/src/maneuver.h @@ -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 diff --git a/src/mission_planning.cpp b/src/mission_planning_old.cpp similarity index 100% rename from src/mission_planning.cpp rename to src/mission_planning_old.cpp diff --git a/src/mission_planning.h b/src/mission_planning_old.h similarity index 100% rename from src/mission_planning.h rename to src/mission_planning_old.h diff --git a/src/physics.cpp b/src/physics.cpp index 47c4e46..cd2746e 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -45,6 +45,11 @@ Vec3 vec3_normalize(Vec3 v) { return {0.0, 0.0, 0.0}; } +// Dot product +double vec3_dot(Vec3 a, Vec3 b) { + return a.x * b.x + a.y * b.y + a.z * b.z; +} + // Calculate acceleration from force: a = F / m Vec3 calculate_acceleration(Vec3 force, double mass) { if (mass > 0.0) { diff --git a/src/physics.h b/src/physics.h index a5c60a6..6a1c05b 100644 --- a/src/physics.h +++ b/src/physics.h @@ -17,6 +17,7 @@ Vec3 vec3_scale(Vec3 v, double s); double vec3_magnitude(Vec3 v); double vec3_distance(Vec3 a, Vec3 b); Vec3 vec3_normalize(Vec3 v); +double vec3_dot(Vec3 a, Vec3 b); // Physics functions Vec3 calculate_acceleration(Vec3 force, double mass); diff --git a/src/spacecraft.cpp b/src/spacecraft.cpp new file mode 100644 index 0000000..8c1ffe5 --- /dev/null +++ b/src/spacecraft.cpp @@ -0,0 +1,71 @@ +#include "spacecraft.h" +#include "physics.h" +#include +#include +#include + +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); +} diff --git a/src/spacecraft.h b/src/spacecraft.h new file mode 100644 index 0000000..8fca919 --- /dev/null +++ b/src/spacecraft.h @@ -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 diff --git a/tests/configs/spacecraft_test.toml b/tests/configs/spacecraft_test.toml new file mode 100644 index 0000000..36453fa --- /dev/null +++ b/tests/configs/spacecraft_test.toml @@ -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 diff --git a/tests/test_maneuvers.cpp b/tests/test_maneuvers.cpp new file mode 100644 index 0000000..d96bf2e --- /dev/null +++ b/tests/test_maneuvers.cpp @@ -0,0 +1,183 @@ +#include +#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 + +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); +}