vibe coding an orbital mechanics simulation to try out claude code
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

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