Browse Source

Add local frame spacecraft maneuvering interface

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
cinnaboot 6 months ago
parent
commit
2f7ae72bf0
  1. 9
      Makefile
  2. 100
      src/config_loader.cpp
  3. 2
      src/config_loader.h
  4. 69
      src/maneuver.cpp
  5. 42
      src/maneuver.h
  6. 0
      src/mission_planning_old.cpp
  7. 0
      src/mission_planning_old.h
  8. 5
      src/physics.cpp
  9. 1
      src/physics.h
  10. 71
      src/spacecraft.cpp
  11. 19
      src/spacecraft.h
  12. 30
      tests/configs/spacecraft_test.toml
  13. 183
      tests/test_maneuvers.cpp

9
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

100
src/config_loader.cpp

@ -3,6 +3,7 @@
#include <cmath>
#include <cstring>
#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;
}

2
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

69
src/maneuver.cpp

@ -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);
}

42
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

0
src/mission_planning.cpp → src/mission_planning_old.cpp

0
src/mission_planning.h → src/mission_planning_old.h

5
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) {

1
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);

71
src/spacecraft.cpp

@ -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);
}

19
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

30
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

183
tests/test_maneuvers.cpp

@ -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…
Cancel
Save