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.
 
 
 
 
 

324 lines
14 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 <array>
using Catch::Matchers::WithinAbs;
SCENARIO("Extreme orientation conversion accuracy and rotation matrix properties",
"[extreme][orientation][mixed]") {
const double TIME_STEP = 60.0;
const double parent_mass = 5.972e24;
const double mu = G * parent_mass;
SimulationState* sim = create_simulation(10, 5, 0, TIME_STEP);
REQUIRE(load_system_config(sim, "tests/test_extreme_orientation_mixed.toml"));
Spacecraft* sc0 = &sim->spacecraft[0];
Spacecraft* sc1 = &sim->spacecraft[1];
Spacecraft* sc2 = &sim->spacecraft[2];
Spacecraft* sc3 = &sim->spacecraft[3];
Spacecraft* sc4 = &sim->spacecraft[4];
// Unique tolerances for this test
const double VDOT_TOL = 1e-3;
const double MAT_TOL = 1e-10;
// Precomputed periapsis radii
const double r_peri0 = sc0->orbit.semi_major_axis * (1.0 - sc0->orbit.eccentricity); // 7.5e6
const double r_peri1 = sc1->orbit.semi_major_axis * (1.0 - sc1->orbit.eccentricity); // 1.0e7
const double r_peri2 = sc2->orbit.semi_major_axis * (1.0 - sc2->orbit.eccentricity); // 7.0e6
const double r_peri3 = sc3->orbit.semi_major_axis * (1.0 - sc3->orbit.eccentricity); // 8.0e6
const double r_peri4 = sc4->orbit.semi_major_axis * (1.0 - sc4->orbit.eccentricity); // 8.0e6
// Precomputed apoapsis radii (elliptical only)
const double r_apo0 = sc0->orbit.semi_major_axis * (1.0 + sc0->orbit.eccentricity); // 9.25e7
const double r_apo1 = sc1->orbit.semi_major_axis * (1.0 + sc1->orbit.eccentricity); // 3.0e7
const double r_apo2 = sc2->orbit.semi_major_axis * (1.0 + sc2->orbit.eccentricity); // 1.393e9
// Helper: convert elements to cartesian at given true anomaly
auto convert_at_nu = [&](Spacecraft* craft, double nu) {
craft->orbit.true_anomaly = nu;
orbital_elements_to_cartesian(craft->orbit, parent_mass, &craft->local_position, &craft->local_velocity);
};
// Helper: vis-viva check
auto check_visviva = [&](double r, double v, double a) {
const double expected_v_sq = mu * (2.0 / r - 1.0 / a);
// Safety: expected_v_sq must be positive for sqrt (guaranteed for all elliptical orbits)
REQUIRE(expected_v_sq > 0.0);
const double expected_v = sqrt(expected_v_sq);
const double rel_err = fabs(v - expected_v) / expected_v;
INFO("v=" << v << " m/s, v_exp=" << expected_v << " m/s, rel_err=" << rel_err);
REQUIRE_THAT(rel_err, WithinAbs(0.0, REL_TOL));
};
// Helper: round-trip check
auto roundtrip = [&](double a, double e, double nu, double inc, double O, double w) {
OrbitalElements elements = {};
elements.semi_major_axis = a;
elements.eccentricity = e;
elements.true_anomaly = nu;
elements.inclination = inc;
elements.longitude_of_ascending_node = O;
elements.argument_of_periapsis = w;
Vec3 pos, vel;
orbital_elements_to_cartesian(elements, parent_mass, &pos, &vel);
OrbitalElements recovered = cartesian_to_orbital_elements(pos, vel, parent_mass);
return recovered;
};
SECTION("periapsis position for extreme orbits") {
convert_at_nu(sc0, 0.0);
const double r0 = vec3_magnitude(sc0->local_position);
const double v0 = vec3_magnitude(sc0->local_velocity);
INFO("sc0 (i=1.2, e=0.85): r=" << r0 << " m, v=" << v0 << " m/s");
REQUIRE_THAT(r0, WithinAbs(r_peri0, R_TOL));
check_visviva(r0, v0, sc0->orbit.semi_major_axis);
convert_at_nu(sc1, 0.0);
const double r1 = vec3_magnitude(sc1->local_position);
const double v1 = vec3_magnitude(sc1->local_velocity);
INFO("sc1 (i=1.4, e=0.5): r=" << r1 << " m, v=" << v1 << " m/s");
REQUIRE_THAT(r1, WithinAbs(r_peri1, R_TOL));
check_visviva(r1, v1, sc1->orbit.semi_major_axis);
convert_at_nu(sc2, 0.0);
const double r2 = vec3_magnitude(sc2->local_position);
const double v2 = vec3_magnitude(sc2->local_velocity);
INFO("sc2 (i=0.5, e=0.99): r=" << r2 << " m, v=" << v2 << " m/s");
REQUIRE_THAT(r2, WithinAbs(r_peri2, R_TOL));
check_visviva(r2, v2, sc2->orbit.semi_major_axis);
}
SECTION("velocity at apsides for extreme orbits") {
// sc0 at periapsis
convert_at_nu(sc0, 0.0);
const double v0p = vec3_magnitude(sc0->local_velocity);
convert_at_nu(sc0, M_PI);
const double v0a = vec3_magnitude(sc0->local_velocity);
INFO("sc0: v_peri=" << v0p << " m/s, v_apo=" << v0a << " m/s");
REQUIRE_THAT(v0p, WithinAbs(9915.577056, V_TOL));
REQUIRE_THAT(v0a, WithinAbs(803.965707, V_TOL));
// sc1 at periapsis
convert_at_nu(sc1, 0.0);
const double v1p = vec3_magnitude(sc1->local_velocity);
convert_at_nu(sc1, M_PI);
const double v1a = vec3_magnitude(sc1->local_velocity);
INFO("sc1: v_peri=" << v1p << " m/s, v_apo=" << v1a << " m/s");
REQUIRE_THAT(v1p, WithinAbs(7732.294575, V_TOL));
REQUIRE_THAT(v1a, WithinAbs(2577.431525, V_TOL));
// sc2 at periapsis
convert_at_nu(sc2, 0.0);
const double v2p = vec3_magnitude(sc2->local_velocity);
convert_at_nu(sc2, M_PI);
const double v2a = vec3_magnitude(sc2->local_velocity);
INFO("sc2: v_peri=" << v2p << " m/s, v_apo=" << v2a << " m/s");
REQUIRE_THAT(v2p, WithinAbs(10644.867979, V_TOL));
REQUIRE_THAT(v2a, WithinAbs(53.491799, V_TOL));
}
SECTION("apoapsis position for extreme orbits") {
convert_at_nu(sc0, M_PI);
const double r0 = vec3_magnitude(sc0->local_position);
INFO("sc0: r_apo=" << r0 << " m, expected=" << r_apo0 << " m");
REQUIRE_THAT(r0, WithinAbs(r_apo0, R_TOL));
convert_at_nu(sc1, M_PI);
const double r1 = vec3_magnitude(sc1->local_position);
INFO("sc1: r_apo=" << r1 << " m, expected=" << r_apo1 << " m");
REQUIRE_THAT(r1, WithinAbs(r_apo1, R_TOL));
convert_at_nu(sc2, M_PI);
const double r2 = vec3_magnitude(sc2->local_position);
INFO("sc2: r_apo=" << r2 << " m, expected=" << r_apo2 << " m");
REQUIRE_THAT(r2, WithinAbs(r_apo2, R_TOL));
}
SECTION("vis-viva accuracy at multiple true anomalies") {
const std::array<double, 4> true_anomalies = {0.0, M_PI / 2.0, M_PI, 3.0 * M_PI / 2.0};
for (int i = 0; i < 5; i++) {
Spacecraft* craft = &sim->spacecraft[i];
const double a = craft->orbit.semi_major_axis;
const double e = craft->orbit.eccentricity;
INFO("Spacecraft " << i << ": e=" << e << ", a=" << a);
for (int j = 0; j < 4; j++) {
double nu = true_anomalies[j];
craft->orbit.true_anomaly = nu;
Vec3 pos, vel;
orbital_elements_to_cartesian(craft->orbit, parent_mass, &pos, &vel);
const double r = vec3_magnitude(pos);
const double v = vec3_magnitude(vel);
const double expected_v_sq = mu * (2.0 / r - 1.0 / a);
// All spacecraft have elliptical orbits (e < 1), so vis-viva always yields v² > 0
const double expected_v = sqrt(expected_v_sq);
const double rel_err = fabs(v - expected_v) / expected_v;
INFO(" nu=" << nu << " rad: v=" << v << " m/s, rel_err=" << rel_err);
REQUIRE_THAT(rel_err, WithinAbs(0.0, REL_TOL));
}
}
}
SECTION("apsidal velocity orthogonality") {
const std::array<double, 2> apsides = {0.0, M_PI};
for (int i = 0; i < 5; i++) {
Spacecraft* craft = &sim->spacecraft[i];
INFO("Spacecraft " << i << ": e=" << craft->orbit.eccentricity
<< ", i=" << craft->orbit.inclination << " rad");
for (int j = 0; j < 2; j++) {
craft->orbit.true_anomaly = apsides[j];
Vec3 pos, vel;
orbital_elements_to_cartesian(craft->orbit, parent_mass, &pos, &vel);
const double pos_dot_vel = vec3_dot(pos, vel);
const double h = vec3_magnitude(vec3_cross(pos, vel));
INFO(" nu=" << apsides[j] << " rad: pos·vel=" << pos_dot_vel << ", |h|=" << h);
REQUIRE_THAT(pos_dot_vel, WithinAbs(0.0, VDOT_TOL));
// Angular momentum must be non-zero for a valid orbit
REQUIRE(h > 0.0);
}
}
}
SECTION("Omega=0 singularity handling") {
convert_at_nu(sc3, 0.0);
const double r = vec3_magnitude(sc3->local_position);
INFO("sc3 (Omega=0): r=" << r << " m");
INFO(" pos=(" << sc3->local_position.x << ", " << sc3->local_position.y << ", " << sc3->local_position.z << ")");
Vec3 pos, vel;
orbital_elements_to_cartesian(sc3->orbit, parent_mass, &pos, &vel);
// Rotation matrix at Omega=0 must produce a valid position in the x-y plane
REQUIRE(pos.x > 0.0);
REQUIRE_THAT(r, WithinAbs(r_peri3, R_TOL));
const double h = vec3_magnitude(vec3_cross(pos, vel));
INFO(" |h|=" << h);
// Angular momentum must be non-zero for a valid orbit
REQUIRE(h > 0.0);
}
SECTION("Arg_peri=0 singularity handling") {
convert_at_nu(sc4, 0.0);
const double r = vec3_magnitude(sc4->local_position);
INFO("sc4 (w=0): r=" << r << " m, expected=" << r_peri4 << " m");
REQUIRE_THAT(r, WithinAbs(r_peri4, R_TOL));
const std::array<double, 4> true_anomalies = {0.0, M_PI / 2.0, M_PI, 3.0 * M_PI / 2.0};
const std::array<double, 4> expected_r = {8.0e6, 1.44e7, 7.2e7, 1.44e7};
const std::array<double, 4> expected_v = {9470.088125, 6737.572312, 1052.232014, 6737.572312};
for (int j = 0; j < 4; j++) {
sc4->orbit.true_anomaly = true_anomalies[j];
Vec3 pos, vel;
orbital_elements_to_cartesian(sc4->orbit, parent_mass, &pos, &vel);
const double r_j = vec3_magnitude(pos);
const double v_j = vec3_magnitude(vel);
INFO(" nu=" << true_anomalies[j] << " rad: r=" << r_j << " m, v=" << v_j << " m/s");
REQUIRE_THAT(r_j, WithinAbs(expected_r[j], R_TOL));
REQUIRE_THAT(v_j, WithinAbs(expected_v[j], V_TOL));
}
}
SECTION("round-trip conversion accuracy") {
const std::array<Spacecraft*, 5> crafts = {sc0, sc1, sc2, sc3, sc4};
for (int i = 0; i < 5; i++) {
const Spacecraft* craft = crafts[i];
const double a = craft->orbit.semi_major_axis;
const double e = craft->orbit.eccentricity;
const double inc = craft->orbit.inclination;
const double O = craft->orbit.longitude_of_ascending_node;
const double w = craft->orbit.argument_of_periapsis;
INFO("Spacecraft " << i << ": " << craft->name);
const OrbitalElements recovered = roundtrip(a, e, 0.0, inc, O, w);
INFO(" e: " << e << " -> " << recovered.eccentricity);
INFO(" i: " << inc << " -> " << recovered.inclination);
INFO(" a: " << a << " -> " << recovered.semi_major_axis);
REQUIRE_THAT(recovered.semi_major_axis, WithinAbs(a, fabs(a) * 0.01));
REQUIRE_THAT(recovered.eccentricity, WithinAbs(e, E_TOL));
REQUIRE_THAT(recovered.inclination, WithinAbs(inc, ANG_TOL));
if (O > 1e-6 || O < -1e-6) {
REQUIRE_THAT(recovered.longitude_of_ascending_node, WithinAbs(O, ANG_TOL));
}
if (w > 1e-6 || w < -1e-6) {
REQUIRE_THAT(recovered.argument_of_periapsis, WithinAbs(w, ANG_TOL));
}
// Round-trip preserves position and velocity
Vec3 pos, vel;
orbital_elements_to_cartesian(recovered, parent_mass, &pos, &vel);
Vec3 pos2, vel2;
orbital_elements_to_cartesian(recovered, parent_mass, &pos2, &vel2);
const double pos_err = vec3_magnitude(vec3_sub(pos, pos2));
const double vel_err = vec3_magnitude(vec3_sub(vel, vel2));
INFO(" pos_err=" << pos_err << " m, vel_err=" << vel_err << " m/s");
REQUIRE_THAT(pos_err, WithinAbs(0.0, R_TOL));
REQUIRE_THAT(vel_err, WithinAbs(0.0, V_TOL));
}
}
SECTION("rotation matrix orthogonality") {
for (int i = 0; i < 5; i++) {
Spacecraft* craft = &sim->spacecraft[i];
const double omega = craft->orbit.argument_of_periapsis;
const double inc = craft->orbit.inclination;
const double Omega = craft->orbit.longitude_of_ascending_node;
Mat3 R = mat3_rotation_orbital(omega, inc, Omega);
const Vec3 unit_x = {1.0, 0.0, 0.0};
const Vec3 unit_y = {0.0, 1.0, 0.0};
const Vec3 unit_z = {0.0, 0.0, 1.0};
Vec3 rot_x = mat3_multiply_vec3(R, unit_x);
Vec3 rot_y = mat3_multiply_vec3(R, unit_y);
Vec3 rot_z = mat3_multiply_vec3(R, unit_z);
const double mag_x = vec3_magnitude(rot_x);
const double mag_y = vec3_magnitude(rot_y);
const double mag_z = vec3_magnitude(rot_z);
const double xy_dot = vec3_dot(rot_x, rot_y);
const double yz_dot = vec3_dot(rot_y, rot_z);
const double xz_dot = vec3_dot(rot_x, rot_z);
INFO("Spacecraft " << i << ": " << craft->name);
INFO(" |R·x|=" << mag_x << ", |R·y|=" << mag_y << ", |R·z|=" << mag_z);
INFO(" (R·x)·(R·y)=" << xy_dot << ", (R·y)·(R·z)=" << yz_dot << ", (R·x)·(R·z)=" << xz_dot);
REQUIRE_THAT(mag_x, WithinAbs(1.0, MAT_TOL));
REQUIRE_THAT(mag_y, WithinAbs(1.0, MAT_TOL));
REQUIRE_THAT(mag_z, WithinAbs(1.0, MAT_TOL));
REQUIRE_THAT(xy_dot, WithinAbs(0.0, MAT_TOL));
REQUIRE_THAT(yz_dot, WithinAbs(0.0, MAT_TOL));
REQUIRE_THAT(xz_dot, WithinAbs(0.0, MAT_TOL));
}
}
destroy_simulation(sim);
}