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.
 
 
 
 
 

417 lines
17 KiB

#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "../src/physics.h"
#include "../src/orbital_mechanics.h"
#include "../src/simulation.h"
#include "../src/config_loader.h"
#include "../src/test_utilities.h"
#include <cmath>
#include <limits>
const double CONVERGENCE_TOLERANCE = 1.0e-10;
const int MAX_ITERATIONS = 50;
double calculate_orbital_period(double semi_major_axis, double parent_mass) {
double mu = G * parent_mass;
return 2.0 * M_PI * sqrt(pow(semi_major_axis, 3.0) / mu);
}
double calculate_orbital_energy(const Vec3& position, const Vec3& velocity, double parent_mass, double craft_mass) {
double r = vec3_magnitude(position);
double v_squared = velocity.x * velocity.x + velocity.y * velocity.y + velocity.z * velocity.z;
double kinetic = 0.5 * craft_mass * v_squared;
double potential = -G * craft_mass * parent_mass / r;
return kinetic + potential;
}
TEST_CASE("Fast orbit - LEO (period ~92 minutes)", "[extreme][timescales][fast]") {
const double TIME_STEP = 10.0;
const int NUM_ORBITS = 10;
SimulationState* sim = create_simulation(10, 10, 0, TIME_STEP);
REQUIRE(load_system_config(sim, "tests/test_extreme_timescales.toml"));
const int CRAFT_INDEX = 0;
const int PARENT_INDEX = 0;
Spacecraft* craft = &sim->spacecraft[CRAFT_INDEX];
CelestialBody* parent = &sim->bodies[PARENT_INDEX];
double expected_period = calculate_orbital_period(craft->orbit.semi_major_axis, parent->mass);
INFO("Expected LEO period: " << expected_period << " s (" << (expected_period / 60.0) << " minutes)");
Vec3 initial_pos, initial_vel;
orbital_elements_to_cartesian(craft->orbit, parent->mass, &initial_pos, &initial_vel);
double initial_energy = calculate_orbital_energy(initial_pos, initial_vel, parent->mass, craft->mass);
for (int orbit = 0; orbit < NUM_ORBITS; orbit++) {
double orbit_start_time = sim->time;
OrbitalElements propagated = craft->orbit;
while (sim->time < orbit_start_time + expected_period) {
propagated = propagate_orbital_elements(propagated, TIME_STEP, parent->mass);
sim->time += TIME_STEP;
}
Vec3 final_pos, final_vel;
orbital_elements_to_cartesian(propagated, parent->mass, &final_pos, &final_vel);
double final_energy = calculate_orbital_energy(final_pos, final_vel, parent->mass, craft->mass);
double energy_error = fabs(final_energy - initial_energy) / fabs(initial_energy);
double pos_error = vec3_magnitude(vec3_sub(final_pos, initial_pos));
INFO("Orbit " << orbit << " energy error: " << energy_error);
INFO("Orbit " << orbit << " position error: " << pos_error << " m");
REQUIRE_THAT(energy_error, Catch::Matchers::WithinAbs(0.0, 1e-9));
}
destroy_simulation(sim);
}
TEST_CASE("Fast orbit - Mercury-like (period ~88 days)", "[extreme][timescales][fast]") {
const double TIME_STEP = 3600.0;
SimulationState* sim = create_simulation(10, 10, 0, TIME_STEP);
REQUIRE(load_system_config(sim, "tests/test_extreme_timescales.toml"));
const int CRAFT_INDEX = 1;
const int PARENT_INDEX = 1;
Spacecraft* craft = &sim->spacecraft[CRAFT_INDEX];
CelestialBody* parent = &sim->bodies[PARENT_INDEX];
double expected_period = calculate_orbital_period(craft->orbit.semi_major_axis, parent->mass);
INFO("Expected Mercury-like period: " << expected_period << " s (" << (expected_period / 86400.0) << " days)");
Vec3 initial_pos, initial_vel;
orbital_elements_to_cartesian(craft->orbit, parent->mass, &initial_pos, &initial_vel);
double initial_energy = calculate_orbital_energy(initial_pos, initial_vel, parent->mass, craft->mass);
const int NUM_ORBITS = 5;
for (int orbit = 0; orbit < NUM_ORBITS; orbit++) {
OrbitalElements propagated = craft->orbit;
for (int step = 0; step < (int)(expected_period / TIME_STEP); step++) {
propagated = propagate_orbital_elements(propagated, TIME_STEP, parent->mass);
}
Vec3 final_pos, final_vel;
orbital_elements_to_cartesian(propagated, parent->mass, &final_pos, &final_vel);
double final_energy = calculate_orbital_energy(final_pos, final_vel, parent->mass, craft->mass);
double energy_error = fabs(final_energy - initial_energy) / fabs(initial_energy);
double pos_error = vec3_magnitude(vec3_sub(final_pos, initial_pos));
INFO("Orbit " << orbit << " energy error: " << energy_error);
INFO("Orbit " << orbit << " position error: " << pos_error << " m");
REQUIRE_THAT(energy_error, Catch::Matchers::WithinAbs(0.0, 1e-9));
}
destroy_simulation(sim);
}
TEST_CASE("Long period orbit - Jupiter-like (period ~12 years)", "[extreme][timescales][long]") {
const double TIME_STEP = 86400.0;
SimulationState* sim = create_simulation(10, 10, 0, TIME_STEP);
REQUIRE(load_system_config(sim, "tests/test_extreme_timescales.toml"));
const int CRAFT_INDEX = 2;
const int PARENT_INDEX = 1;
Spacecraft* craft = &sim->spacecraft[CRAFT_INDEX];
CelestialBody* parent = &sim->bodies[PARENT_INDEX];
double expected_period = calculate_orbital_period(craft->orbit.semi_major_axis, parent->mass);
INFO("Expected long period: " << expected_period << " s (" << (expected_period / (86400.0 * 365.0)) << " years)");
Vec3 initial_pos, initial_vel;
orbital_elements_to_cartesian(craft->orbit, parent->mass, &initial_pos, &initial_vel);
double initial_energy = calculate_orbital_energy(initial_pos, initial_vel, parent->mass, craft->mass);
const double PROPAGATION_TIME = 2.0 * 365.0 * 86400.0;
OrbitalElements propagated = craft->orbit;
int num_steps = (int)(PROPAGATION_TIME / TIME_STEP);
for (int step = 0; step < num_steps; step++) {
propagated = propagate_orbital_elements(propagated, TIME_STEP, parent->mass);
}
Vec3 final_pos, final_vel;
orbital_elements_to_cartesian(propagated, parent->mass, &final_pos, &final_vel);
double final_energy = calculate_orbital_energy(final_pos, final_vel, parent->mass, craft->mass);
double energy_error = fabs(final_energy - initial_energy) / fabs(initial_energy);
INFO("After " << (PROPAGATION_TIME / (86400.0 * 365.0)) << " years:");
INFO("Energy error: " << energy_error);
REQUIRE_THAT(energy_error, Catch::Matchers::WithinAbs(0.0, 1e-9));
destroy_simulation(sim);
}
TEST_CASE("Low altitude orbit (~100 km)", "[extreme][timescales][low]") {
const double TIME_STEP = 10.0;
SimulationState* sim = create_simulation(10, 10, 0, TIME_STEP);
REQUIRE(load_system_config(sim, "tests/test_extreme_timescales.toml"));
const int CRAFT_INDEX = 3;
const int PARENT_INDEX = 0;
Spacecraft* craft = &sim->spacecraft[CRAFT_INDEX];
CelestialBody* parent = &sim->bodies[PARENT_INDEX];
double expected_period = calculate_orbital_period(craft->orbit.semi_major_axis, parent->mass);
INFO("Expected low altitude period: " << expected_period << " s (" << (expected_period / 60.0) << " minutes)");
const int NUM_ORBITS = 10;
for (int orbit = 0; orbit < NUM_ORBITS; orbit++) {
OrbitalElements propagated = craft->orbit;
for (int step = 0; step < (int)(expected_period / TIME_STEP); step++) {
propagated = propagate_orbital_elements(propagated, TIME_STEP, parent->mass);
}
Vec3 pos, vel;
orbital_elements_to_cartesian(propagated, parent->mass, &pos, &vel);
double r = vec3_magnitude(pos);
INFO("Orbit " << orbit << " radius: " << r << " m");
INFO("Parent radius: " << parent->radius << " m");
INFO("Altitude: " << (r - parent->radius) << " m");
REQUIRE(r > parent->radius);
}
destroy_simulation(sim);
}
TEST_CASE("Super-synchronous orbit (period > 24 hours)", "[extreme][timescales][super_sync]") {
const double TIME_STEP = 3600.0;
const double TARGET_PERIOD = 24.0 * 3600.0;
SimulationState* sim = create_simulation(10, 10, 0, TIME_STEP);
REQUIRE(load_system_config(sim, "tests/test_extreme_timescales.toml"));
const int CRAFT_INDEX = 4;
const int PARENT_INDEX = 0;
Spacecraft* craft = &sim->spacecraft[CRAFT_INDEX];
CelestialBody* parent = &sim->bodies[PARENT_INDEX];
double period = calculate_orbital_period(craft->orbit.semi_major_axis, parent->mass);
INFO("Super-synchronous period: " << period << " s (" << (period / 3600.0) << " hours)");
INFO("One Earth day: " << TARGET_PERIOD << " s (" << (TARGET_PERIOD / 3600.0) << " hours)");
REQUIRE(period > TARGET_PERIOD);
Vec3 initial_pos, initial_vel;
orbital_elements_to_cartesian(craft->orbit, parent->mass, &initial_pos, &initial_vel);
double initial_energy = calculate_orbital_energy(initial_pos, initial_vel, parent->mass, craft->mass);
const double PROPAGATION_TIME = 3.0 * TARGET_PERIOD;
OrbitalElements propagated = craft->orbit;
int num_steps = (int)(PROPAGATION_TIME / TIME_STEP);
for (int step = 0; step < num_steps; step++) {
propagated = propagate_orbital_elements(propagated, TIME_STEP, parent->mass);
}
Vec3 final_pos, final_vel;
orbital_elements_to_cartesian(propagated, parent->mass, &final_pos, &final_vel);
double final_energy = calculate_orbital_energy(final_pos, final_vel, parent->mass, craft->mass);
double energy_error = fabs(final_energy - initial_energy) / fabs(initial_energy);
INFO("After 3 Earth days, energy error: " << energy_error);
REQUIRE_THAT(energy_error, Catch::Matchers::WithinAbs(0.0, 1e-9));
destroy_simulation(sim);
}
TEST_CASE("Geosynchronous orbit (period = sidereal day)", "[extreme][timescales][geosync]") {
const double SIDEREAL_DAY_HOURS = 23.93447;
SimulationState* sim = create_simulation(10, 10, 0, 60.0);
REQUIRE(load_system_config(sim, "tests/test_extreme_timescales.toml"));
const int CRAFT_INDEX = 5;
const int PARENT_INDEX = 0;
Spacecraft* craft = &sim->spacecraft[CRAFT_INDEX];
CelestialBody* parent = &sim->bodies[PARENT_INDEX];
double period = calculate_orbital_period(craft->orbit.semi_major_axis, parent->mass);
double period_hours = period / 3600.0;
double period_error_hours = fabs(period_hours - SIDEREAL_DAY_HOURS);
INFO("Calculated period: " << period << " s (" << period_hours << " hours)");
INFO("Sidereal day: " << SIDEREAL_DAY_HOURS << " hours");
INFO("Period error: " << period_error_hours << " hours (" << (period_error_hours * 3600.0) << " s)");
REQUIRE_THAT(period_hours, Catch::Matchers::WithinAbs(SIDEREAL_DAY_HOURS, 0.0002));
Vec3 initial_pos, initial_vel;
orbital_elements_to_cartesian(craft->orbit, parent->mass, &initial_pos, &initial_vel);
OrbitalElements propagated = craft->orbit;
propagated = propagate_orbital_elements(propagated, period, parent->mass);
Vec3 final_pos, final_vel;
orbital_elements_to_cartesian(propagated, parent->mass, &final_pos, &final_vel);
double pos_error = vec3_magnitude(vec3_sub(final_pos, initial_pos));
INFO("Position error after one period: " << pos_error << " m");
REQUIRE_THAT(pos_error, Catch::Matchers::WithinAbs(0.0, 1e-3));
destroy_simulation(sim);
}
TEST_CASE("Period consistency across different true anomalies", "[extreme][timescales][consistency]") {
const double TIME_STEP = 3600.0;
SimulationState* sim = create_simulation(10, 10, 0, TIME_STEP);
REQUIRE(load_system_config(sim, "tests/test_extreme_timescales.toml"));
const int CRAFT_INDEX = 1;
const int PARENT_INDEX = 1;
Spacecraft* craft = &sim->spacecraft[CRAFT_INDEX];
CelestialBody* parent = &sim->bodies[PARENT_INDEX];
double period = calculate_orbital_period(craft->orbit.semi_major_axis, parent->mass);
const double test_anomalies[] = {0.0, M_PI / 2.0, M_PI, 3.0 * M_PI / 2.0};
for (int i = 0; i < 4; i++) {
OrbitalElements test_orbit = craft->orbit;
test_orbit.true_anomaly = test_anomalies[i];
OrbitalElements propagated = test_orbit;
propagated = propagate_orbital_elements(propagated, period, parent->mass);
Vec3 initial_pos, initial_vel;
Vec3 final_pos, final_vel;
orbital_elements_to_cartesian(test_orbit, parent->mass, &initial_pos, &initial_vel);
orbital_elements_to_cartesian(propagated, parent->mass, &final_pos, &final_vel);
double pos_error = vec3_magnitude(vec3_sub(final_pos, initial_pos));
double vel_error = vec3_magnitude(vec3_sub(final_vel, initial_vel));
INFO("True anomaly: " << test_anomalies[i] << " rad");
INFO("Position error: " << pos_error << " m");
INFO("Velocity error: " << vel_error << " m/s");
REQUIRE_THAT(pos_error, Catch::Matchers::WithinAbs(0.0, 1e-3));
REQUIRE_THAT(vel_error, Catch::Matchers::WithinAbs(0.0, 1e-6));
}
destroy_simulation(sim);
}
TEST_CASE("Energy conservation across all timescales", "[extreme][timescales][energy]") {
const double TIME_STEP = 3600.0;
SimulationState* sim = create_simulation(10, 10, 0, TIME_STEP);
REQUIRE(load_system_config(sim, "tests/test_extreme_timescales.toml"));
struct EnergyTest {
int craft_index;
int parent_index;
const char* name;
};
EnergyTest tests[] = {
{0, 0, "LEO (fast)"},
{1, 1, "Mercury-like (fast)"},
{2, 1, "Jupiter-like (long)"},
{3, 0, "Low altitude (low)"},
{4, 0, "Super-synchronous"},
{5, 0, "Geosynchronous"}
};
for (int t = 0; t < 6; t++) {
EnergyTest test = tests[t];
Spacecraft* craft = &sim->spacecraft[test.craft_index];
CelestialBody* parent = &sim->bodies[test.parent_index];
Vec3 initial_pos, initial_vel;
orbital_elements_to_cartesian(craft->orbit, parent->mass, &initial_pos, &initial_vel);
double initial_energy = calculate_orbital_energy(initial_pos, initial_vel, parent->mass, craft->mass);
double period = calculate_orbital_period(craft->orbit.semi_major_axis, parent->mass);
double propagation_time = period * 2.0;
OrbitalElements propagated = craft->orbit;
int num_steps = (int)(propagation_time / TIME_STEP);
for (int step = 0; step < num_steps; step++) {
propagated = propagate_orbital_elements(propagated, TIME_STEP, parent->mass);
}
Vec3 final_pos, final_vel;
orbital_elements_to_cartesian(propagated, parent->mass, &final_pos, &final_vel);
double final_energy = calculate_orbital_energy(final_pos, final_vel, parent->mass, craft->mass);
double energy_error = fabs(final_energy - initial_energy) / fabs(initial_energy);
INFO(test.name << ":");
INFO(" Initial energy: " << initial_energy << " J");
INFO(" Final energy: " << final_energy << " J");
INFO(" Relative error: " << energy_error);
REQUIRE_THAT(energy_error, Catch::Matchers::WithinAbs(0.0, 1e-9));
}
destroy_simulation(sim);
}
TEST_CASE("Mean anomaly accumulation for very long periods", "[extreme][timescales][mean_anomaly]") {
const double TIME_STEP = 86400.0;
SimulationState* sim = create_simulation(10, 10, 0, TIME_STEP);
REQUIRE(load_system_config(sim, "tests/test_extreme_timescales.toml"));
const int CRAFT_INDEX = 2;
const int PARENT_INDEX = 1;
Spacecraft* craft = &sim->spacecraft[CRAFT_INDEX];
CelestialBody* parent = &sim->bodies[PARENT_INDEX];
double mu = G * parent->mass;
double a = craft->orbit.semi_major_axis;
double e = craft->orbit.eccentricity;
double n = sqrt(mu / pow(a, 3.0));
const double PROPAGATION_TIME = 10.0 * 365.0 * 86400.0;
double expected_mean_anomaly = n * PROPAGATION_TIME;
double expected_orbits = expected_mean_anomaly / (2.0 * M_PI);
INFO("Expected mean anomaly after 10 years: " << expected_mean_anomaly << " rad");
INFO("Expected number of orbits: " << expected_orbits);
OrbitalElements propagated = craft->orbit;
int num_steps = (int)(PROPAGATION_TIME / TIME_STEP);
for (int step = 0; step < num_steps; step++) {
propagated = propagate_orbital_elements(propagated, TIME_STEP, parent->mass);
}
Vec3 final_pos, final_vel;
orbital_elements_to_cartesian(propagated, parent->mass, &final_pos, &final_vel);
double true_anomaly_change = propagated.true_anomaly - craft->orbit.true_anomaly;
double expected_true_anomaly_change = fmod(expected_mean_anomaly, 2.0 * M_PI);
INFO("True anomaly change: " << true_anomaly_change << " rad");
INFO("Expected true anomaly change: " << expected_true_anomaly_change << " rad");
REQUIRE_THAT(fabs(propagated.eccentricity - e), Catch::Matchers::WithinAbs(0.0, 1e-10));
REQUIRE_THAT(fabs(propagated.semi_major_axis - a), Catch::Matchers::WithinAbs(0.0, 1e-6));
destroy_simulation(sim);
}