From 49c748f4feb74769b515b2562abd50d1b0fb0584 Mon Sep 17 00:00:00 2001 From: cinnaboot Date: Mon, 9 Feb 2026 12:14:53 -0500 Subject: [PATCH] Add test cases capturing periapsis burn bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added test cases that reveal the bug where periapsis burns execute at apoapsis instead of periapsis after the first burn. Changes: - Modified tests/test_periapsis_burn.toml to include second spacecraft (TestSatelliteCrossing) starting at true_anomaly = 1.57 - Replaced test "Prograde burn at periapsis raises apoapsis" with "Two periapsis burns execute at same location" to verify sequential periapsis burns one orbit apart both fire at correct location - Added test "Periapsis burn fires when crossing periapsis" to verify burn triggers when spacecraft crosses periapsis from 90 degrees Both new tests fail as expected, confirming the bug exists in cartesian_to_orbital_elements() where argument_of_periapsis is calculated as π instead of 0 after a burn. Test status: 4 periapsis tests fail (capturing bug) --- tests/test_periapsis_burn.cpp | 156 ++++++++++++++++++++++++++++++--- tests/test_periapsis_burn.toml | 29 +++++- 2 files changed, 174 insertions(+), 11 deletions(-) diff --git a/tests/test_periapsis_burn.cpp b/tests/test_periapsis_burn.cpp index 5131c2b..62a37ee 100644 --- a/tests/test_periapsis_burn.cpp +++ b/tests/test_periapsis_burn.cpp @@ -61,33 +61,169 @@ TEST_CASE("Prograde burn at periapsis preserves periapsis distance", "[maneuver] destroy_simulation(sim); } -TEST_CASE("Prograde burn at periapsis raises apoapsis not periapsis", "[maneuver][periapsis][apoapsis]") { +TEST_CASE("Two periapsis burns execute at same location", "[maneuver][periapsis][sequential]") { const double TIME_STEP = 60.0; + const int ORBIT_STEPS = 175; SimulationState* sim = create_simulation(10, 10, 100, TIME_STEP); REQUIRE(load_system_config(sim, "tests/test_periapsis_burn.toml")); Spacecraft* craft = &sim->spacecraft[0]; + CelestialBody* parent = &sim->bodies[craft->parent_index]; + + int maneuver_indices[2]; + int maneuver_count_for_craft = 0; + for (int i = 0; i < sim->maneuver_count; i++) { + if (sim->maneuvers[i].craft_index == 0) { + maneuver_indices[maneuver_count_for_craft++] = i; + } + } + REQUIRE(maneuver_count_for_craft == 2); double initial_periapsis = craft->orbit.semi_major_axis * (1.0 - craft->orbit.eccentricity); double initial_apoapsis = craft->orbit.semi_major_axis * (1.0 + craft->orbit.eccentricity); - update_simulation(sim); + INFO("Initial orbit:"); + INFO(" Periapsis: " << initial_periapsis); + INFO(" Apoapsis: " << initial_apoapsis); + INFO(" Eccentricity: " << craft->orbit.eccentricity); + + double burn1_time = -1.0; + double burn1_radius = -1.0; + double burn1_true_anomaly = -10.0; + double burn2_time = -1.0; + double burn2_radius = -1.0; + double burn2_true_anomaly = -10.0; + + for (int i = 0; i < ORBIT_STEPS; i++) { + update_simulation(sim); + + if (sim->maneuvers[maneuver_indices[0]].executed && burn1_time < 0) { + burn1_time = sim->time; + burn1_radius = vec3_magnitude(craft->local_position); + + Vec3 r = craft->local_position; + Vec3 v = craft->local_velocity; + Vec3 h = vec3_cross(r, v); + Vec3 e_vec = calculate_eccentricity_vector(r, v, h, G * parent->mass); + double e_mag = vec3_magnitude(e_vec); + burn1_true_anomaly = calculate_true_anomaly(r, v, e_vec, e_mag, burn1_radius); + burn1_true_anomaly = normalize_angle(burn1_true_anomaly); + + INFO("First burn executed at step " << i); + INFO(" Time: " << burn1_time); + INFO(" Radius: " << burn1_radius); + INFO(" True anomaly: " << burn1_true_anomaly << " rad (" << burn1_true_anomaly * 180.0 / M_PI << "°)"); + INFO(" Periapsis: " << initial_periapsis); + INFO(" Apoapsis: " << initial_apoapsis); + } + + if (sim->maneuvers[maneuver_indices[1]].executed && burn2_time < 0) { + burn2_time = sim->time; + burn2_radius = vec3_magnitude(craft->local_position); + + Vec3 r = craft->local_position; + Vec3 v = craft->local_velocity; + Vec3 h = vec3_cross(r, v); + Vec3 e_vec = calculate_eccentricity_vector(r, v, h, G * parent->mass); + double e_mag = vec3_magnitude(e_vec); + burn2_true_anomaly = calculate_true_anomaly(r, v, e_vec, e_mag, burn2_radius); + burn2_true_anomaly = normalize_angle(burn2_true_anomaly); + + INFO("Second burn executed at step " << i); + INFO(" Time: " << burn2_time); + INFO(" Radius: " << burn2_radius); + INFO(" True anomaly: " << burn2_true_anomaly << " rad (" << burn2_true_anomaly * 180.0 / M_PI << "°)"); + } + } + + REQUIRE(sim->maneuvers[maneuver_indices[0]].executed); + REQUIRE(sim->maneuvers[maneuver_indices[1]].executed); + + INFO("Burn comparison:"); + INFO(" Burn 1: time=" << burn1_time << ", radius=" << burn1_radius << ", true_anomaly=" << burn1_true_anomaly); + INFO(" Burn 2: time=" << burn2_time << ", radius=" << burn2_radius << ", true_anomaly=" << burn2_true_anomaly); + + REQUIRE_THAT(burn1_radius, Catch::Matchers::WithinAbs(initial_periapsis, 1000.0)); + REQUIRE_THAT(burn2_radius, Catch::Matchers::WithinAbs(initial_periapsis, 1000.0)); + + REQUIRE(fabs(burn1_true_anomaly) < 0.5); + REQUIRE(fabs(burn2_true_anomaly) < 0.5); + + double period = 2.0 * M_PI * sqrt(pow(craft->orbit.semi_major_axis, 3.0) / (G * parent->mass)); + INFO("Expected orbital period: " << period << " seconds"); + INFO("Actual time between burns: " << (burn2_time - burn1_time) << " seconds"); + + REQUIRE_THAT(burn2_time - burn1_time, Catch::Matchers::WithinAbs(period, TIME_STEP * 2.0)); - double final_sma = craft->orbit.semi_major_axis; - double final_ecc = craft->orbit.eccentricity; - double final_periapsis = final_sma * (1.0 - final_ecc); - double final_apoapsis = final_sma * (1.0 + final_ecc); + destroy_simulation(sim); +} - INFO("Initial: peri=" << initial_periapsis << " apo=" << initial_apoapsis); - INFO("Final: peri=" << final_periapsis << " apo=" << final_apoapsis); +TEST_CASE("Periapsis burn fires when crossing periapsis", "[maneuver][periapsis][crossing]") { + const double TIME_STEP = 60.0; + SimulationState* sim = create_simulation(10, 10, 100, TIME_STEP); - REQUIRE(final_apoapsis > initial_apoapsis); - REQUIRE_THAT(final_periapsis, Catch::Matchers::WithinAbs(initial_periapsis, 1.0)); + REQUIRE(load_system_config(sim, "tests/test_periapsis_burn.toml")); + + Spacecraft* craft = &sim->spacecraft[1]; // TestSatelliteCrossing + CelestialBody* parent = &sim->bodies[craft->parent_index]; + + int maneuver_index = -1; + for (int i = 0; i < sim->maneuver_count; i++) { + if (sim->maneuvers[i].craft_index == 1) { + maneuver_index = i; + break; + } + } + REQUIRE(maneuver_index >= 0); + + double initial_periapsis = craft->orbit.semi_major_axis * (1.0 - craft->orbit.eccentricity); + double initial_apoapsis = craft->orbit.semi_major_axis * (1.0 + craft->orbit.eccentricity); + + INFO("Initial orbit:"); + INFO(" Periapsis: " << initial_periapsis); + INFO(" Apoapsis: " << initial_apoapsis); + INFO(" Initial true anomaly: " << craft->orbit.true_anomaly << " rad"); + + double burn_time = -1.0; + double burn_radius = -1.0; + double burn_true_anomaly = -10.0; + + int max_steps = 1000; + for (int i = 0; i < max_steps && !sim->maneuvers[maneuver_index].executed; i++) { + update_simulation(sim); + + if (sim->maneuvers[maneuver_index].executed) { + burn_time = sim->time; + burn_radius = vec3_magnitude(craft->local_position); + + Vec3 r = craft->local_position; + Vec3 v = craft->local_velocity; + Vec3 h = vec3_cross(r, v); + Vec3 e_vec = calculate_eccentricity_vector(r, v, h, G * parent->mass); + double e_mag = vec3_magnitude(e_vec); + burn_true_anomaly = calculate_true_anomaly(r, v, e_vec, e_mag, burn_radius); + burn_true_anomaly = normalize_angle(burn_true_anomaly); + + INFO("Burn executed at step " << i); + INFO(" Time: " << burn_time); + INFO(" Radius: " << burn_radius); + INFO(" True anomaly: " << burn_true_anomaly << " rad (" << burn_true_anomaly * 180.0 / M_PI << "°)"); + INFO(" Periapsis: " << initial_periapsis); + INFO(" Apoapsis: " << initial_apoapsis); + } + } + + REQUIRE(sim->maneuvers[maneuver_index].executed); + + REQUIRE_THAT(burn_radius, Catch::Matchers::WithinAbs(initial_periapsis, 1000.0)); + + REQUIRE(fabs(burn_true_anomaly) < 0.5); destroy_simulation(sim); } + TEST_CASE("Burn location equals new periapsis after prograde burn", "[maneuver][periapsis][location]") { const double TIME_STEP = 60.0; SimulationState* sim = create_simulation(10, 10, 100, TIME_STEP); diff --git a/tests/test_periapsis_burn.toml b/tests/test_periapsis_burn.toml index 45663df..23ba305 100644 --- a/tests/test_periapsis_burn.toml +++ b/tests/test_periapsis_burn.toml @@ -35,7 +35,18 @@ parent_index = 1 orbit = { semi_major_axis = 1.0371e7, eccentricity = 0.3, - true_anomaly = 0.0 + true_anomaly = 0.1 # Start slightly past periapsis so first burn doesn't trigger immediately +} + +[[spacecraft]] +name = "TestSatelliteCrossing" +mass = 1000.0 +parent_index = 1 +# Start at 90 degrees from periapsis for crossing test +orbit = { + semi_major_axis = 1.0371e7, + eccentricity = 0.3, + true_anomaly = 1.57 # 90 degrees (pi/2) } [[maneuvers]] @@ -45,3 +56,19 @@ trigger_type = "true_anomaly" trigger_value = 0.0 direction = "prograde" delta_v = 500.0 + +[[maneuvers]] +name = "periapsis_prograde_burn_2" +spacecraft_name = "TestSatellite" +trigger_type = "true_anomaly" +trigger_value = 0.0 +direction = "prograde" +delta_v = 500.0 + +[[maneuvers]] +name = "periapsis_prograde_burn_crossing" +spacecraft_name = "TestSatelliteCrossing" +trigger_type = "true_anomaly" +trigger_value = 0.0 +direction = "prograde" +delta_v = 500.0