diff --git a/tests/informational/Makefile b/tests/informational/Makefile new file mode 100644 index 0000000..811f98f --- /dev/null +++ b/tests/informational/Makefile @@ -0,0 +1,73 @@ +# Makefile for Informational Tests +# These tests are not part of the main test suite +# They are for diagnostic and informational purposes only + +BUILD_DIR = ../../build +SRC_DIR = ../../src + +# Compiler flags +CFLAGS = -Wall -Wextra -g -ggdb3 -std=c++14 +INCLUDES = -I$(SRC_DIR) \ + -isystem../../ext/tomlc17/src \ + -isystem../../ext/raylib/src \ + -isystem../../ext/raygui/src \ + -isystem../../ext/raygui/styles + +# Source files (from main project that informational tests depend on) +CORE_OBJECTS = $(BUILD_DIR)/test_utilities.o \ + $(BUILD_DIR)/physics.o \ + $(BUILD_DIR)/orbital_mechanics.o \ + $(BUILD_DIR)/simulation.o \ + $(BUILD_DIR)/config_loader.o \ + $(BUILD_DIR)/config_validator.o \ + $(BUILD_DIR)/maneuver.o \ + $(BUILD_DIR)/spacecraft.o \ + $(BUILD_DIR)/tomlc17.o + +# Informational test sources +INFO_TESTS = test_time_step_stability.cpp +INFO_BINS = $(patsubst %.cpp,%,$(INFO_TESTS)) + +# Default target - build all informational tests +all: $(INFO_BINS) + @echo "All informational tests built successfully" + @echo "" + @echo "To run tests:" + @echo " ./test_time_step_stability" + @echo "" + @echo "To run specific test case:" + @echo " ./test_time_step_stability '[timestep][stability]'" + +# Build individual test binaries +$(INFO_BINS): $(patsubst %.cpp,%.o,$(INFO_TESTS)) $(CORE_OBJECTS) + g++ $(patsubst %.cpp,%.o,$<) $(CORE_OBJECTS) -o $@ -lCatch2Main -lCatch2 -lm + @echo "Built $@" + +# Build test object files +test_time_step_stability.o: test_time_step_stability.cpp + g++ $(CFLAGS) $(INCLUDES) -c $< -o $@ + +# Build core objects if they don't exist +$(BUILD_DIR)/%.o: + $(MAKE) -C ../../ build/$*.o + +# Clean built files in informational directory +clean: + rm -f $(INFO_BINS) *.o + +# Clean all (including core build - use with caution!) +clean-all: + $(MAKE) -C ../../ clean + rm -f $(INFO_BINS) *.o + +# Run the time step stability test (must run from project root) +run-timestep: test_time_step_stability + @echo "Note: Tests must be run from project root directory:" + @echo " From project root: ./tests/informational/$@" + +# Run with verbose output to see binary search progress +run-timestep-verbose: test_time_step_stability + @echo "Note: Tests must be run from project root directory:" + @echo " From project root: ./tests/informational/$@ -s \"[timestep][stability]\"" + +.PHONY: all clean clean-all run-timestep run-timestep-verbose diff --git a/tests/informational/README.md b/tests/informational/README.md new file mode 100644 index 0000000..a83c5f6 --- /dev/null +++ b/tests/informational/README.md @@ -0,0 +1,72 @@ +# Informational Tests + +This directory contains tests that are not part of the standard test suite. These tests are provided for informational purposes only and should be run separately. + +## Purpose + +Informational tests are designed to: +- Explore simulation boundaries and limits +- Provide diagnostic information about the simulation +- Test extreme or edge cases that don't fit into normal test suites +- Help understand system behavior under various conditions + +## Running Informational Tests + +Informational tests are **not** run by `make test`. To build and run them: + +```bash +cd tests/informational +make +./time_step_test +``` + +## Current Tests + +### `test_time_step_stability.cpp` + +Determines the maximum stable time step for the RK4 integration across different orbital regimes. + +**Test Bodies:** +- **Mercury Orbiter** (MESSENGER-like): 200 km altitude, ~12 hour period +- **Io** (Jupiter's moon): 421,700 km orbit, ~1.77 day period +- **Moon** (Earth's moon): 384,400 km orbit, ~27.3 day period + +**Stability Criteria:** +- Energy drift < 1% over 100 orbits +- Distance drift < 5% +- No SOI transitions (body doesn't change parent) + +**Configuration:** Uses `test_time_step_stability.toml` + +**Usage:** +```bash +# From project root directory: +./tests/informational/test_time_step_stability + +# Or using the Makefile: +cd tests/informational +make run-timestep + +# Run specific test case +./tests/informational/test_time_step_stability '[timestep][stability]' + +# Run with verbose output to see binary search progress +./tests/informational/test_time_step_stability -s "[timestep][stability]" +``` + +**Note:** Tests must be run from the project root directory because config file paths are relative to the root. + +**Expected Output:** +The test performs a binary search to find the maximum stable time step, printing progress for each dt value tested. Results show the minimum stable dt across all tested bodies with recommendations. + +## Adding New Informational Tests + +1. Create a new `.cpp` file in this directory +2. Add corresponding `.toml` config file if needed +3. Add a target in the `Makefile`: + ```makefile + your_test_name: your_test_name.o + g++ $(BUILD_DIR)/*.o -o $@ -lCatch2Main -lCatch2 -lm + ``` +4. Update this README with test description +5. Follow the naming convention: `test_.cpp` and `test_.toml` diff --git a/tests/informational/test_time_step_stability b/tests/informational/test_time_step_stability new file mode 100755 index 0000000..c8734ec Binary files /dev/null and b/tests/informational/test_time_step_stability differ diff --git a/tests/informational/test_time_step_stability.cpp b/tests/informational/test_time_step_stability.cpp new file mode 100644 index 0000000..a79ebba --- /dev/null +++ b/tests/informational/test_time_step_stability.cpp @@ -0,0 +1,234 @@ +#include +#include "../../src/physics.h" +#include "../../src/simulation.h" +#include "../../src/config_loader.h" +#include "../../src/test_utilities.h" +#include +#include + +struct TestBody { + const char* name; + int body_index; + int parent_index; + double expected_period_days; +}; + +const double SECONDS_PER_DAY = 86400.0; +const double MIN_DT = 30.0; +const double MAX_DT = 600.0; +const double ENERGY_TOLERANCE = 1.0; +const int NUM_ORBITS = 100; + +double calculate_orbital_period(CelestialBody* body, CelestialBody* parent) { + Vec3 relative_pos = vec3_sub(body->global_position, parent->global_position); + double r = vec3_magnitude(relative_pos); + Vec3 relative_vel = vec3_sub(body->global_velocity, parent->global_velocity); + double v = vec3_magnitude(relative_vel); + + double specific_energy = (v * v) / 2.0 - G * parent->mass / r; + double semi_major_axis = -G * parent->mass / (2.0 * specific_energy); + double period_seconds = 2.0 * M_PI * sqrt(pow(semi_major_axis, 3.0) / (G * parent->mass)); + + return period_seconds; +} + +bool is_dt_stable(SimulationState* sim, const TestBody& test_body, double dt, int num_orbits) { + SimulationState* test_sim = create_simulation(sim->max_bodies, sim->max_craft, sim->max_maneuvers, dt); + + REQUIRE(load_system_config(test_sim, "tests/informational/test_time_step_stability.toml")); + + int body_index = test_body.body_index; + int parent_index = test_body.parent_index; + + double initial_energy = calculate_system_total_energy(test_sim); + + Vec3 initial_pos_relative = vec3_sub( + test_sim->bodies[body_index].global_position, + test_sim->bodies[parent_index].global_position + ); + double initial_distance = vec3_magnitude(initial_pos_relative); + + double period = calculate_orbital_period(&test_sim->bodies[body_index], &test_sim->bodies[parent_index]); + double max_time = period * num_orbits; + + bool completed = true; + while (test_sim->time < max_time) { + update_simulation(test_sim); + + if (test_sim->bodies[body_index].parent_index != parent_index) { + completed = false; + break; + } + } + + double final_energy = calculate_system_total_energy(test_sim); + double energy_drift_percent = fabs((final_energy - initial_energy) / initial_energy) * 100.0; + + Vec3 final_pos_relative = vec3_sub( + test_sim->bodies[body_index].global_position, + test_sim->bodies[parent_index].global_position + ); + double final_distance = vec3_magnitude(final_pos_relative); + double distance_drift_percent = fabs((final_distance - initial_distance) / initial_distance) * 100.0; + + bool stable = completed && (energy_drift_percent < ENERGY_TOLERANCE) && (distance_drift_percent < 5.0); + + destroy_simulation(test_sim); + + return stable; +} + +double find_max_stable_dt(SimulationState* sim, const TestBody& test_body) { + double low = MIN_DT; + double high = MAX_DT; + double max_stable = low; + + printf("Testing %s (period ~%.2f days):\n", test_body.name, test_body.expected_period_days); + + for (int iter = 0; iter < 10; iter++) { + double mid = (low + high) / 2.0; + bool stable = is_dt_stable(sim, test_body, mid, NUM_ORBITS); + + if (stable) { + max_stable = mid; + low = mid; + printf(" dt=%.0fs: STABLE\n", mid); + } else { + high = mid; + printf(" dt=%.0fs: UNSTABLE\n", mid); + } + + if (high - low < 5.0) break; + } + + printf(" Maximum stable dt: %.0f seconds\n\n", max_stable); + return max_stable; +} + +TEST_CASE("Time step stability - Mercury orbiter (MESSENGER-like)", "[timestep][stability]") { + const double BASE_DT = 60.0; + SimulationState* sim = create_simulation(10, 0, 0, BASE_DT); + + TestBody mercury_orbiter = {"Mercury_Orbiter", 1, 0, 0.5}; + double max_dt = find_max_stable_dt(sim, mercury_orbiter); + + INFO("Mercury orbiter maximum stable dt: " << max_dt << " seconds"); + + REQUIRE(max_dt >= MIN_DT); + + destroy_simulation(sim); +} + +TEST_CASE("Time step stability - Io (Jupiter's moon)", "[timestep][stability]") { + const double BASE_DT = 60.0; + SimulationState* sim = create_simulation(10, 0, 0, BASE_DT); + + TestBody io = {"Io", 3, 2, 1.77}; + double max_dt = find_max_stable_dt(sim, io); + + INFO("Io maximum stable dt: " << max_dt << " seconds"); + + REQUIRE(max_dt >= MIN_DT); + + destroy_simulation(sim); +} + +TEST_CASE("Time step stability - Moon (Earth's moon)", "[timestep][stability]") { + const double BASE_DT = 60.0; + SimulationState* sim = create_simulation(10, 0, 0, BASE_DT); + + TestBody moon = {"Moon", 5, 4, 27.3}; + double max_dt = find_max_stable_dt(sim, moon); + + INFO("Moon maximum stable dt: " << max_dt << " seconds"); + + REQUIRE(max_dt >= MIN_DT); + + destroy_simulation(sim); +} + +TEST_CASE("Find minimum stable time step across all bodies", "[timestep][stability]") { + const double BASE_DT = 60.0; + SimulationState* sim = create_simulation(10, 0, 0, BASE_DT); + + TestBody bodies[] = { + {"Mercury_Orbiter", 1, 0, 0.5}, + {"Io", 3, 2, 1.77}, + {"Moon", 5, 4, 27.3} + }; + + double max_dt = MAX_DT; + + printf("\n=== Finding minimum stable dt across all bodies ===\n\n"); + + for (int i = 0; i < 3; i++) { + double body_max_dt = find_max_stable_dt(sim, bodies[i]); + if (body_max_dt < max_dt) { + max_dt = body_max_dt; + } + } + + printf("\n=== RESULTS ===\n"); + printf("Minimum stable time step: %.0f seconds\n", max_dt); + printf("Recommended safe time step: %.0f seconds (%.0fx safety margin)\n", max_dt * 0.7, 1.0/0.7); + + INFO("Minimum stable dt: " << max_dt << " seconds"); + + REQUIRE(max_dt >= MIN_DT); + + destroy_simulation(sim); +} + +TEST_CASE("Verify current default dt (60s) stability", "[timestep][stability]") { + const double DT = 60.0; + const int NUM_ORBITS = 10; + + SimulationState* sim = create_simulation(10, 0, 0, DT); + REQUIRE(load_system_config(sim, "tests/informational/test_time_step_stability.toml")); + + struct BodyTest { + int body_index; + int parent_index; + const char* name; + }; + + BodyTest tests[] = { + {1, 0, "Mercury_Orbiter"}, + {3, 2, "Io"}, + {5, 4, "Moon"} + }; + + for (int t = 0; t < 3; t++) { + int body_index = tests[t].body_index; + int parent_index = tests[t].parent_index; + const char* name = tests[t].name; + + double period = calculate_orbital_period(&sim->bodies[body_index], &sim->bodies[parent_index]); + double max_time = period * NUM_ORBITS; + + double initial_energy = calculate_system_total_energy(sim); + + INFO("Testing " << name << " with dt=" << DT << "s for " << NUM_ORBITS << " orbits"); + + bool completed = true; + while (sim->time < max_time) { + update_simulation(sim); + + if (sim->bodies[body_index].parent_index != parent_index) { + completed = false; + break; + } + } + + double final_energy = calculate_system_total_energy(sim); + double energy_drift_percent = fabs((final_energy - initial_energy) / initial_energy) * 100.0; + + INFO(name << " completed: " << (completed ? "yes" : "no")); + INFO(name << " energy drift: " << energy_drift_percent << "%"); + + REQUIRE(completed); + REQUIRE(energy_drift_percent < ENERGY_TOLERANCE); + } + + destroy_simulation(sim); +} diff --git a/tests/informational/test_time_step_stability.toml b/tests/informational/test_time_step_stability.toml new file mode 100644 index 0000000..bed2f04 --- /dev/null +++ b/tests/informational/test_time_step_stability.toml @@ -0,0 +1,80 @@ +# Time Step Stability Test Configuration +# Contains bodies with different orbital periods for testing RK4 stability + +[[bodies]] +name = "Mercury" +mass = 3.285e23 +radius = 2.439e6 +parent_index = -1 +color = { r = 0.7, g = 0.7, b = 0.7 } +orbit = { + semi_major_axis = 0.0, + eccentricity = 0.0, + true_anomaly = 0.0 +} + +# Low Mercury orbiter at 200 km altitude (MESSENGER-like) +# Period ~12 hours - most restrictive case +[[bodies]] +name = "Mercury_Orbiter" +mass = 1000.0 +radius = 1.0 +parent_index = 0 +color = { r = 1.0, g = 0.0, b = 1.0 } +orbit = { + semi_major_axis = 2.639e6, + eccentricity = 0.0, + true_anomaly = 0.0 +} + +[[bodies]] +name = "Jupiter" +mass = 1.898e27 +radius = 6.9911e7 +parent_index = -1 +color = { r = 0.9, g = 0.7, b = 0.5 } +orbit = { + semi_major_axis = 0.0, + eccentricity = 0.0, + true_anomaly = 0.0 +} + +# Io - Jupiter's moon +# Period ~1.77 days +[[bodies]] +name = "Io" +mass = 8.93e22 +radius = 1.822e6 +parent_index = 2 +color = { r = 0.9, g = 0.9, b = 0.3 } +orbit = { + semi_major_axis = 4.217e8, + eccentricity = 0.0, + true_anomaly = 0.0 +} + +[[bodies]] +name = "Earth" +mass = 5.972e24 +radius = 6.371e6 +parent_index = -1 +color = { r = 0.0, g = 0.5, b = 1.0 } +orbit = { + semi_major_axis = 0.0, + eccentricity = 0.0, + true_anomaly = 0.0 +} + +# Moon - Earth's moon +# Period ~27.3 days +[[bodies]] +name = "Moon" +mass = 7.342e22 +radius = 1.737e6 +parent_index = 4 +color = { r = 0.7, g = 0.7, b = 0.7 } +orbit = { + semi_major_axis = 3.844e8, + eccentricity = 0.0, + true_anomaly = 0.0 +}