#include #include #include "../src/physics.h" #include "../src/simulation.h" #include "../src/config_loader.h" #include "../src/test_utilities.h" #include #include #include using Catch::Matchers::WithinAbs; // === Helper: run sim loop, collect failures, assert once at end === static std::string fmt_time(double t) { char buf[32]; snprintf(buf, sizeof(buf), "t=%.0fs", t); return std::string(buf); } static std::string fmt_drift(double d) { char buf[32]; snprintf(buf, sizeof(buf), "drift=%.4f", d); return std::string(buf); } // === Per-moon period tolerances (0.5% of analytical period, from precalc) === static const double MOON_PERIOD_TOL = 11861.4; // Moon static const double IO_PERIOD_TOL = 764.6; // Io static const double EUROPA_PERIOD_TOL = 1534.5; // Europa static const double GANYMEDE_PERIOD_TOL = 3091.1; // Ganymede static const double CALLISTO_PERIOD_TOL = 7210.6; // Callisto static const double TITAN_PERIOD_TOL = 6889.9; // Titan // === Drift tolerance for distance checks (10% bound) === static const double DRIFT_REL_TOL = 0.1; SCENARIO("Multi-body moon orbital stability and period measurements", "[moon][earth][jupiter][saturn][galilean][titan]" "[stability][period][integration][inclined][geometry]") { // === Fixture === const double TIME_STEP = 60.0; const double SECONDS_PER_DAY = 86400.0; SimulationState* sim = create_simulation(20, 0, 0, TIME_STEP); REQUIRE(load_system_config(sim, "tests/test_moon_orbits.toml")); CelestialBody* earth = &sim->bodies[2]; CelestialBody* moon = &sim->bodies[8]; struct MoonData { int index; const char* name; double expected_seconds; double max_days; double min_time_days; double period_tol; int parent_index; }; const MoonData galilean[] = { {9, "Io", 152928.62, 5.0, 1.0, IO_PERIOD_TOL, 4}, {10, "Europa", 306909.10, 10.0, 2.0, EUROPA_PERIOD_TOL, 4}, {11, "Ganymede", 618227.12, 15.0, 5.0, GANYMEDE_PERIOD_TOL, 4}, {12, "Callisto", 1442117.30, 25.0, 10.0, CALLISTO_PERIOD_TOL, 4}, }; const MoonData all_moons[] = { {8, "Moon", 2372274.33, 35.0, 5.0, MOON_PERIOD_TOL, 2}, {9, "Io", 152928.62, 5.0, 1.0, IO_PERIOD_TOL, 4}, {10, "Europa", 306909.10, 10.0, 2.0, EUROPA_PERIOD_TOL, 4}, {11, "Ganymede", 618227.12, 15.0, 5.0, GANYMEDE_PERIOD_TOL, 4}, {12, "Callisto", 1442117.30, 25.0, 10.0, CALLISTO_PERIOD_TOL, 4}, {13, "Titan", 1377976.07, 25.0, 10.0, TITAN_PERIOD_TOL, 5}, }; // === Helper lambdas === // Run sim loop without assertions; collect failures; assert once at end. // This tests every timestep but produces only 1 assertion per call. auto check_parent_stability = [&](CelestialBody* body, int expected_parent, double max_days) { double max_time = max_days * SECONDS_PER_DAY; std::vector failures; while (sim->time < max_time) { update_simulation(sim); if (body->parent_index != expected_parent) { failures.push_back(fmt_time(sim->time) + ": " + std::string(body->name) + " parent=" + std::to_string(body->parent_index) + " (expected " + std::to_string(expected_parent) + ")"); } } REQUIRE(failures.empty()); }; auto measure_period = [&](int body_index, int parent_index, double max_days, double min_time_days) { sim->time = 0.0; OrbitTracker* tracker = create_orbit_tracker_with_min_time( body_index, min_time_days * SECONDS_PER_DAY); double max_time = max_days * SECONDS_PER_DAY; while (sim->time < max_time && !tracker->orbit_completed) { update_simulation(sim); update_orbit_tracker(tracker, &sim->bodies[body_index], &sim->bodies[parent_index], sim->time); } double period = tracker->orbit_completed ? tracker->time_at_completion : -1.0; destroy_orbit_tracker(tracker); return period; }; auto check_distance_drift = [&](CelestialBody* body, int parent_index, double max_days) { CelestialBody* parent = &sim->bodies[parent_index]; Vec3 initial_rel = vec3_sub(body->global_position, parent->global_position); double initial_r = vec3_magnitude(initial_rel); double max_time = max_days * SECONDS_PER_DAY; std::vector failures; while (sim->time < max_time) { update_simulation(sim); Vec3 rel = vec3_sub(body->global_position, parent->global_position); double r = vec3_magnitude(rel); double drift = std::fabs(r - initial_r) / initial_r; if (drift > DRIFT_REL_TOL) { failures.push_back(fmt_time(sim->time) + ": " + std::string(body->name) + " " + fmt_drift(drift)); } } REQUIRE(failures.empty()); }; auto wait_for_all_orbits = [&](int count) { OrbitTracker* trackers[6] = {}; for (int i = 0; i < count; i++) { trackers[i] = create_orbit_tracker_with_min_time( all_moons[i].index, all_moons[i].min_time_days * SECONDS_PER_DAY); } double max_time = 60.0 * SECONDS_PER_DAY; int completed_count = 0; while (sim->time < max_time && completed_count < count) { update_simulation(sim); for (int i = 0; i < count; i++) { if (!trackers[i]->orbit_completed) { update_orbit_tracker(trackers[i], &sim->bodies[all_moons[i].index], &sim->bodies[all_moons[i].parent_index], sim->time); if (trackers[i]->orbit_completed) { completed_count++; double period = trackers[i]->time_at_completion / SECONDS_PER_DAY; INFO(all_moons[i].name << " completed orbit at " << period << " days"); } } } } for (int i = 0; i < count; i++) { REQUIRE(trackers[i]->orbit_completed); destroy_orbit_tracker(trackers[i]); } }; // === Sections === SECTION("Moon maintains Earth as parent throughout simulation") { check_parent_stability(moon, 2, 35.0); } SECTION("Moon distance from Earth stays within 10% of initial") { check_distance_drift(moon, 2, 35.0); } SECTION("Moon completes orbit in ~27.3 days") { double period = measure_period(8, 2, 35.0, 5.0); INFO("Measured Moon period: " << period / SECONDS_PER_DAY << " days (expected: ~27.3)"); REQUIRE_THAT(period, WithinAbs(2372274.33, MOON_PERIOD_TOL)); } SECTION("Galilean moons maintain Jupiter as parent") { for (const auto& m : galilean) { check_parent_stability(&sim->bodies[m.index], 4, 25.0); } } SECTION("Galilean moons complete orbits in expected periods") { for (const auto& m : galilean) { double period = measure_period(m.index, m.parent_index, m.max_days, m.min_time_days); INFO(m.name << " period: " << period / SECONDS_PER_DAY << " days (expected: " << m.expected_seconds / SECONDS_PER_DAY << ")"); REQUIRE_THAT(period, WithinAbs(m.expected_seconds, m.period_tol)); } } SECTION("Galilean moons stay within 10% of initial distance") { for (const auto& m : galilean) { check_distance_drift(&sim->bodies[m.index], m.parent_index, 25.0); } } SECTION("Titan maintains Saturn as parent") { check_parent_stability(&sim->bodies[13], 5, 25.0); } SECTION("Titan completes orbit in ~15.95 days") { double period = measure_period(13, 5, 25.0, 10.0); INFO("Measured Titan period: " << period / SECONDS_PER_DAY << " days (expected: ~15.95)"); REQUIRE_THAT(period, WithinAbs(1377976.07, TITAN_PERIOD_TOL)); } SECTION("Titan distance from Saturn stays within 10%") { check_distance_drift(&sim->bodies[13], 5, 25.0); } SECTION("All moons maintain correct parents over 60-day simulation") { double max_time = 60.0 * SECONDS_PER_DAY; std::vector failures; while (sim->time < max_time) { update_simulation(sim); for (int i = 0; i < 6; i++) { int idx = all_moons[i].index; int expected = all_moons[i].parent_index; if (sim->bodies[idx].parent_index != expected) { failures.push_back(fmt_time(sim->time) + ": " + std::string(all_moons[i].name) + " parent=" + std::to_string(sim->bodies[idx].parent_index) + " (expected " + std::to_string(expected) + ")"); } } } REQUIRE(failures.empty()); } SECTION("All moons complete at least one orbit") { wait_for_all_orbits(6); } SECTION("Moon inclined orbit produces non-zero z-coordinate") { double max_time = 10.0 * SECONDS_PER_DAY; while (sim->time < max_time) { update_simulation(sim); } Vec3 rel = vec3_sub(moon->global_position, earth->global_position); double z = rel.z; INFO("Moon z-coordinate relative to Earth: " << z << " m"); INFO("Moon orbital inclination: " << (moon->orbit.inclination * 180.0 / M_PI) << " degrees"); REQUIRE_THAT(z, !WithinAbs(0.0, 10000000.0)); } SECTION("Moon orbital elements loaded correctly from config") { REQUIRE_THAT(moon->orbit.eccentricity, WithinAbs(0.055, E_TOL)); REQUIRE_THAT(moon->orbit.inclination, WithinAbs(5.16 * M_PI / 180.0, ANG_TOL)); REQUIRE_THAT(moon->orbit.longitude_of_ascending_node, WithinAbs(125.08 * M_PI / 180.0, ANG_TOL)); REQUIRE_THAT(moon->orbit.argument_of_periapsis, WithinAbs(318.15 * M_PI / 180.0, ANG_TOL)); } destroy_simulation(sim); }