diff --git a/continue.md b/continue.md index e15eef2..2819488 100644 --- a/continue.md +++ b/continue.md @@ -68,6 +68,7 @@ All constants defined in `src/test_utilities.h` — use those, do not redefine l - `test_energy` — Energy calculations and conservation tests - `test_inclined_orbits` — 3D inclined orbit conversions, Molniya orbits, rotation matrices - `test_maneuvers` — Impulsive burn tests with precalculated values +- `test_maneuver_planning` — Maneuver trigger system (TIME + elliptical TRUE_ANOMALY triggers) - `test_orbital_period` — Orbital period calculations, SCENARIO/SECTION pattern - `test_true_anomaly_roundtrip` — True anomaly conversion round-trips, tight tolerances - `test_physics_utilities` — Vector math, acceleration, matrix ops, rotation matrices, compare_vec3 @@ -75,7 +76,6 @@ All constants defined in `src/test_utilities.h` — use those, do not redefine l - `test_hybrid_burns` — 14 TEST_CASEs → 1 SCENARIO with 22 SECTIONs, impulse + continuous burns, precalculated values; converted 17 qualitative checks to quantitative, replaced hardcoded tolerances with named constants (A_TOL, E_TOL, D_TOL, M_TOL, ANG_TOL, R_TOL) ### Can Refactor Now (sim_engine.py supports all features needed) -- `test_maneuver_planning` — maneuver trigger system (TIME + elliptical TRUE_ANOMALY triggers) - `test_orbit_rendering` — rendering tests (check if sim_engine needed) - `test_precision_boundaries` — boundary condition tests - `test_invalid_parent_assignment` — validation/error handling tests diff --git a/scripts/precalc_maneuver_planning.py b/scripts/precalc_maneuver_planning.py new file mode 100644 index 0000000..78e5ff7 --- /dev/null +++ b/scripts/precalc_maneuver_planning.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +""" +Precalculate expected values for test_maneuver_planning.cpp refactoring. +Computes velocities and energy after time-based and true anomaly-based +maneuver triggers. +""" + +import math +import sys +sys.path.insert(0, "/home/agent/dev/claudes_game") +from scripts.sim_engine import * + + +def main(): + G_const = G + earth_mass = 5.972e24 + mu = G_const * earth_mass + + # Initial circular orbit at r = 6.771e6 m + r0 = 6.771e6 + v_circular = math.sqrt(mu / r0) + + print("// Initial circular orbit") + print(f"// r = {r0} m") + print(f"// v_circular = {v_circular:.15e} m/s") + print() + + # Run the full simulation with the TOML config + sim = Simulator("tests/test_maneuver_planning.toml", dt=60.0) + + # Get initial craft velocity + craft = sim.get_craft("LEO_Satellite") + v_initial = vmag(craft.local_vel) + print(f"// Initial craft velocity from config: {v_initial:.15e} m/s") + print() + + # Run to just before first burn (t=3600) + steps_to_first_burn = int(3600.0 / 60.0) # 60 steps + sim.run(steps_to_first_burn) + + craft = sim.get_craft("LEO_Satellite") + v_before_burn1 = vmag(craft.local_vel) + t_before = sim.time + + # Check if maneuver[0] executed + man = sim.maneuvers[0] + print("// First burn (time trigger at 3600.0 s)") + print(f"// Time at step end: {t_before:.1f} s") + print(f"// Executed: {man.executed}") + print(f"// Executed time: {man.executed_time:.15e} s") + print(f"// Velocity before burn: {v_before_burn1:.15e} m/s") + print() + + # Continue a bit more to ensure burn fires + sim.run(1) + man = sim.maneuvers[0] + craft = sim.get_craft("LEO_Satellite") + v_after_burn1 = vmag(craft.local_vel) + a_after_burn1 = craft.orbit.a + e_after_burn1 = craft.orbit.e + r_after_burn1 = vmag(craft.local_pos) + + print(f"// After stepping past burn:") + print(f"// Executed: {man.executed}") + print(f"// Executed time: {man.executed_time:.15e} s") + print(f"// Velocity after burn: {v_after_burn1:.15e} m/s") + print(f"// Semi-major axis: {a_after_burn1:.15e} m") + print(f"// Eccentricity: {e_after_burn1:.15e}") + print(f"// Radius: {r_after_burn1:.15e} m") + print(f"// KE after first burn: {0.5 * 1000.0 * v_after_burn1 * v_after_burn1:.15e} J") + print() + + # Continue until second burn fires (true anomaly 0.0) + max_additional_steps = 2000 # should be enough + second_burn_fired = False + for step in range(max_additional_steps): + sim.run(1) + if sim.maneuvers[1].executed: + second_burn_fired = True + break + + craft = sim.get_craft("LEO_Satellite") + v_after_burn2 = vmag(craft.local_vel) + a_after_burn2 = craft.orbit.a + e_after_burn2 = craft.orbit.e + + man2 = sim.maneuvers[1] + print("// Second burn (true anomaly trigger at 0.0)") + print(f"// Fired: {second_burn_fired}") + print(f"// Executed time: {man2.executed_time:.15e} s") + print(f"// Sim time: {sim.time:.1f} s") + print(f"// Velocity after second burn: {v_after_burn2:.15e} m/s") + print(f"// Semi-major axis: {a_after_burn2:.15e} m") + print(f"// Eccentricity: {e_after_burn2:.15e}") + print(f"// KE after second burn: {0.5 * 1000.0 * v_after_burn2 * v_after_burn2:.15e} J") + print() + + # Also get the exact burn result for second burn + br = man2.burn_result + print(f"// Pre-burn state at second burn:") + print(f"// pos = {br.position}") + print(f"// vel = {br.velocity}") + print(f"// true_anomaly = {br.true_anomaly:.15e} rad") + print() + + # Run beyond well past to verify no extra executions + sim.run(500) + exec_count = sum(1 for m in sim.maneuvers if m.executed) + print("// After extra simulation") + print(f"// Total executed: {exec_count}") + for i, m in enumerate(sim.maneuvers): + print(f"// Maneuver[{i}] '{m.name}': executed={m.executed}, time={m.executed_time:.1f} s") + print() + + print("// For WithinAbs assertions:") + print(f"// v_initial := {v_circular:.15e}") + print(f"// v_after_burn1 := {v_after_burn1:.15e}") + print(f"// a_after_burn1 := {a_after_burn1:.15e}") + print(f"// e_after_burn1 := {e_after_burn1:.15e}") + print(f"// v_after_burn2 := {v_after_burn2:.15e}") + print(f"// a_after_burn2 := {a_after_burn2:.15e}") + print(f"// e_after_burn2 := {e_after_burn2:.15e}") + print(f"// executed_time_1 := {man.executed_time:.15e}") + print(f"// executed_time_2 := {man2.executed_time:.15e}") + + +if __name__ == "__main__": + main() diff --git a/tests/test_maneuver_planning.cpp b/tests/test_maneuver_planning.cpp new file mode 100644 index 0000000..fb39830 --- /dev/null +++ b/tests/test_maneuver_planning.cpp @@ -0,0 +1,81 @@ +#include +#include +#include "../src/physics.h" +#include "../src/simulation.h" +#include "../src/orbital_objects.h" +#include "../src/maneuver.h" +#include "../src/config_loader.h" +#include "../src/test_utilities.h" +using Catch::Matchers::WithinAbs; + +SCENARIO("Maneuver planning and execution", "[maneuver][planning][trigger][config]") { + const double TIME_STEP = 60.0; + const double BURN_TIME = 3600.0; + SimulationState* sim = create_simulation(10, 10, 100, TIME_STEP); + + REQUIRE(load_system_config(sim, "tests/test_maneuver_planning.toml")); + + auto run_until = [&](double target_time) { + while (sim->time < target_time) { + update_simulation(sim); + } + }; + + SECTION("maneuvers load from config with correct properties") { + REQUIRE(sim->maneuver_count == 2); + REQUIRE(std::string(sim->maneuvers[0].name) == "orbit_raise_1"); + REQUIRE(std::string(sim->maneuvers[1].name) == "orbit_raise_2"); + REQUIRE(sim->maneuvers[0].trigger_type == TRIGGER_TIME); + REQUIRE(sim->maneuvers[1].trigger_type == TRIGGER_TRUE_ANOMALY); + REQUIRE_THAT(sim->maneuvers[0].delta_v, WithinAbs(500.0, D_TOL)); + } + + SECTION("time-based trigger executes at 3600 s") { + REQUIRE(!sim->maneuvers[0].executed); + REQUIRE(!sim->maneuvers[1].executed); + + run_until(BURN_TIME + TIME_STEP); + + REQUIRE(sim->maneuvers[0].executed); + REQUIRE_THAT(sim->maneuvers[0].executed_time, WithinAbs(BURN_TIME, TIME_STEP)); + + const double after_velocity = vec3_magnitude(sim->spacecraft[0].local_velocity); + REQUIRE_THAT(after_velocity, WithinAbs(8.170251503359999e3, V_TOL)); + } + + SECTION("true anomaly trigger executes after first burn") { + REQUIRE(!sim->maneuvers[1].executed); + + run_until(BURN_TIME + TIME_STEP); + REQUIRE(sim->maneuvers[0].executed); + + const double max_sim_time = BURN_TIME + 72000.0; + while (sim->time < max_sim_time) { + update_simulation(sim); + if (sim->maneuvers[1].executed) { + break; + } + } + + REQUIRE(sim->maneuvers[1].executed); + REQUIRE_THAT(vec3_magnitude(sim->spacecraft[0].local_velocity), + WithinAbs(8.672299586435140e3, V_TOL)); + } + + SECTION("maneuvers execute only once") { + run_until(20000.0); + + REQUIRE(sim->maneuvers[0].executed); + REQUIRE(sim->maneuvers[1].executed); + + int execution_count = 0; + for (int i = 0; i < sim->maneuver_count; i++) { + if (sim->maneuvers[i].executed) { + execution_count++; + } + } + REQUIRE(execution_count == 2); + } + + destroy_simulation(sim); +} diff --git a/tests/test_maneuver_planning.toml b/tests/test_maneuver_planning.toml new file mode 100644 index 0000000..4e08a4e --- /dev/null +++ b/tests/test_maneuver_planning.toml @@ -0,0 +1,41 @@ +# Test Configuration: Maneuver Sequence +# Sun + Earth + LEO Satellite with scheduled burns +# Tests time-based and true anomaly-based maneuver triggers + +[[bodies]] +name = "Sun" +mass = 1.989e30 +radius = 6.96e8 +parent_index = -1 +color = {r = 1.0, g = 1.0, b = 0.0} +orbit = {semi_major_axis = 0.0, eccentricity = 0.0, true_anomaly = 0.0} + +[[bodies]] +name = "Earth" +mass = 5.972e24 +radius = 6.371e6 +parent_index = 0 +color = {r = 0.0, g = 0.5, b = 1.0} +orbit = {semi_major_axis = 1.496e11, eccentricity = 0.0, true_anomaly = 0.0} + +[[spacecraft]] +name = "LEO_Satellite" +mass = 1000.0 +parent_index = 1 +orbit = {semi_major_axis = 6.771e6, eccentricity = 0.0, true_anomaly = 1.57} + +[[maneuvers]] +name = "orbit_raise_1" +spacecraft_name = "LEO_Satellite" +trigger_type = "time" +trigger_value = 3600.0 +direction = "prograde" +delta_v = 500.0 + +[[maneuvers]] +name = "orbit_raise_2" +spacecraft_name = "LEO_Satellite" +trigger_type = "true_anomaly" +trigger_value = 0.0 +direction = "prograde" +delta_v = 500.0