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