You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
273 lines
10 KiB
273 lines
10 KiB
#include <catch2/catch_test_macros.hpp> |
|
#include <catch2/matchers/catch_matchers_floating_point.hpp> |
|
#include "../src/physics.h" |
|
#include "../src/simulation.h" |
|
#include "../src/config_loader.h" |
|
#include "../src/test_utilities.h" |
|
#include <cmath> |
|
#include <vector> |
|
#include <string> |
|
|
|
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<std::string> 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<std::string> 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<std::string> 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); |
|
}
|
|
|