Compare commits
7 Commits
926dbb20cc
...
6c28eb1560
| Author | SHA1 | Date |
|---|---|---|
|
|
6c28eb1560 | 3 weeks ago |
|
|
2d14dbfbcf | 3 weeks ago |
|
|
3bbac86ca5 | 3 weeks ago |
|
|
804a82babb | 3 weeks ago |
|
|
2575457ade | 2 months ago |
|
|
5d8ee97610 | 2 months ago |
|
|
3e388d178b | 2 months ago |
9 changed files with 412 additions and 240 deletions
@ -1,152 +0,0 @@
|
||||
#include <catch2/catch_test_macros.hpp> |
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp> |
||||
#include "../src/physics.h" |
||||
#include "../src/simulation.h" |
||||
#include "../src/orbital_objects.h" |
||||
#include "../src/maneuver.h" |
||||
#include "../src/config_loader.h" |
||||
#include "../src/rendezvous.h" |
||||
#include "../src/test_utilities.h" |
||||
#include <cmath> |
||||
#include <cstring> |
||||
|
||||
using Catch::Matchers::WithinAbs; |
||||
|
||||
TEST_CASE("Maneuver loading from config", "[maneuver][config]") { |
||||
const double TIME_STEP = 60.0; |
||||
|
||||
SimulationState* sim = create_simulation(10, 10, 100, TIME_STEP); |
||||
|
||||
REQUIRE(load_system_config(sim, "tests/test_maneuver_planning.toml")); |
||||
|
||||
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(sim->maneuvers[0].delta_v == 500.0); |
||||
|
||||
destroy_simulation(sim); |
||||
} |
||||
|
||||
TEST_CASE("Time-based trigger executes at correct time", "[maneuver][trigger][time]") { |
||||
const double TIME_STEP = 60.0; |
||||
|
||||
SimulationState* sim = create_simulation(10, 10, 100, TIME_STEP); |
||||
|
||||
REQUIRE(load_system_config(sim, "tests/test_maneuver_planning.toml")); |
||||
|
||||
REQUIRE(sim->maneuver_count == 2); |
||||
REQUIRE(!sim->maneuvers[0].executed); |
||||
REQUIRE(!sim->maneuvers[1].executed); |
||||
|
||||
double initial_velocity = vec3_magnitude(sim->spacecraft[0].local_velocity); |
||||
|
||||
const double BURN_TIME = 3600.0; |
||||
while (sim->time < BURN_TIME + sim->dt) { |
||||
update_simulation(sim); |
||||
} |
||||
|
||||
REQUIRE(sim->maneuvers[0].executed); |
||||
REQUIRE(fabs(sim->maneuvers[0].executed_time - BURN_TIME) < TIME_STEP); |
||||
|
||||
double after_velocity = vec3_magnitude(sim->spacecraft[0].local_velocity); |
||||
REQUIRE(after_velocity > initial_velocity); |
||||
|
||||
destroy_simulation(sim); |
||||
} |
||||
|
||||
TEST_CASE("True anomaly trigger executes at correct angle", "[maneuver][trigger][true_anomaly]") { |
||||
const double TIME_STEP = 60.0; |
||||
|
||||
SimulationState* sim = create_simulation(10, 10, 100, TIME_STEP); |
||||
|
||||
REQUIRE(load_system_config(sim, "tests/test_maneuver_planning.toml")); |
||||
|
||||
REQUIRE(sim->maneuver_count == 2); |
||||
REQUIRE(!sim->maneuvers[1].executed); |
||||
|
||||
double first_burn_velocity = 0.0; |
||||
|
||||
const double FIRST_BURN_TIME = 3600.0; |
||||
while (sim->time < FIRST_BURN_TIME) { |
||||
update_simulation(sim); |
||||
} |
||||
|
||||
first_burn_velocity = vec3_magnitude(sim->spacecraft[0].local_velocity); |
||||
|
||||
const double TARGET_ANOMALY = 3.14159; |
||||
(void)TARGET_ANOMALY; |
||||
double max_sim_time = FIRST_BURN_TIME + 72000.0; |
||||
|
||||
while (sim->time < max_sim_time) { |
||||
update_simulation(sim); |
||||
if (sim->maneuvers[1].executed) { |
||||
break; |
||||
} |
||||
} |
||||
|
||||
REQUIRE(sim->maneuvers[1].executed); |
||||
|
||||
double second_burn_velocity = vec3_magnitude(sim->spacecraft[0].local_velocity); |
||||
double second_burn_kinetic_energy = 0.5 * sim->spacecraft[0].mass * |
||||
second_burn_velocity * second_burn_velocity; |
||||
double first_burn_kinetic_energy = 0.5 * sim->spacecraft[0].mass * |
||||
first_burn_velocity * first_burn_velocity; |
||||
|
||||
REQUIRE(second_burn_kinetic_energy > first_burn_kinetic_energy); |
||||
|
||||
destroy_simulation(sim); |
||||
} |
||||
|
||||
TEST_CASE("Maneuvers only execute once", "[maneuver][execution]") { |
||||
const double TIME_STEP = 60.0; |
||||
|
||||
SimulationState* sim = create_simulation(10, 10, 100, TIME_STEP); |
||||
|
||||
REQUIRE(load_system_config(sim, "tests/test_maneuver_planning.toml")); |
||||
|
||||
const double MAX_TIME = 20000.0; |
||||
while (sim->time < MAX_TIME) { |
||||
update_simulation(sim); |
||||
} |
||||
|
||||
REQUIRE(sim->maneuvers[0].executed); |
||||
REQUIRE(sim->maneuvers[1].executed); |
||||
|
||||
double execution_count = 0.0; |
||||
for (int i = 0; i < sim->maneuver_count; i++) { |
||||
if (sim->maneuvers[i].executed) { |
||||
execution_count += 1.0; |
||||
} |
||||
} |
||||
|
||||
REQUIRE(execution_count == 2.0); |
||||
|
||||
destroy_simulation(sim); |
||||
} |
||||
|
||||
TEST_CASE("Duplicate maneuver names fail config load", "[maneuver][config][error]") { |
||||
const double TIME_STEP = 60.0; |
||||
|
||||
SimulationState* sim = create_simulation(10, 10, 100, TIME_STEP); |
||||
|
||||
bool result = load_system_config(sim, "tests/test_maneuver_planning.toml"); |
||||
REQUIRE(result); |
||||
|
||||
REQUIRE(sim->maneuver_count == 2); |
||||
|
||||
Maneuver duplicate_maneuver = sim->maneuvers[0]; |
||||
sim->maneuvers[sim->maneuver_count] = duplicate_maneuver; |
||||
|
||||
(void)sim->maneuver_count; |
||||
sim->maneuver_count++; |
||||
|
||||
bool is_duplicate = (std::string(sim->maneuvers[0].name) == |
||||
std::string(sim->maneuvers[2].name)); |
||||
|
||||
REQUIRE(is_duplicate); |
||||
|
||||
destroy_simulation(sim); |
||||
} |
||||
|
||||
@ -1,65 +0,0 @@
|
||||
#include <catch2/catch_test_macros.hpp> |
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp> |
||||
#include "../src/physics.h" |
||||
#include "../src/simulation.h" |
||||
#include "../src/orbital_objects.h" |
||||
#include "../src/maneuver.h" |
||||
#include "../src/config_loader.h" |
||||
#include "../src/orbital_mechanics.h" |
||||
#include <cmath> |
||||
|
||||
// Test: Check omega after prograde burn that flips eccentricity vector direction
|
||||
TEST_CASE("Omega calculation after prograde burn", "[omega][debug]") { |
||||
double parent_mass = 5.972e24; // Earth mass
|
||||
|
||||
// Initial orbit: zero inclination, omega = 0
|
||||
// Start at apoapsis where eccentricity vector points opposite to position
|
||||
OrbitalElements elements = {0}; |
||||
elements.semi_major_axis = 1.0e7; |
||||
elements.eccentricity = 0.3; |
||||
elements.true_anomaly = M_PI; // Start at apoapsis
|
||||
elements.inclination = 1e-12; // Tiny inclination to trigger atan2 path
|
||||
elements.longitude_of_ascending_node = 0.0; |
||||
elements.argument_of_periapsis = 0.0; |
||||
|
||||
// Get initial position and velocity
|
||||
Vec3 pos, vel; |
||||
orbital_elements_to_cartesian(elements, parent_mass, &pos, &vel); |
||||
|
||||
// Get initial eccentricity vector
|
||||
Vec3 v = vel; |
||||
double r = vec3_magnitude(pos); |
||||
double mu = G * parent_mass; |
||||
Vec3 e_vec_initial = vec3_scale(vec3_sub(vec3_scale(vel, r), vec3_scale(pos, vec3_magnitude(vel) / mu)), 1.0 / mu); |
||||
double e_initial = vec3_magnitude(e_vec_initial); |
||||
|
||||
INFO("Initial state:"); |
||||
INFO(" e = " << e_initial); |
||||
INFO(" e_vec = (" << e_vec_initial.x << ", " << e_vec_initial.y << ", " << e_vec_initial.z << ")"); |
||||
INFO(" pos = (" << pos.x << ", " << pos.y << ", " << pos.z << ")"); |
||||
INFO(" vel = (" << vel.x << ", " << vel.y << ", " << vel.z << ")"); |
||||
|
||||
// Apply a prograde burn at periapsis
|
||||
Vec3 vel_dir = vec3_normalize(vel); |
||||
Vec3 delta_v = vec3_scale(vel_dir, 1000.0); // Large burn to flip e_vec
|
||||
vel = vec3_add(vel, delta_v); |
||||
|
||||
// Reconstruct orbital elements
|
||||
OrbitalElements new_elements = cartesian_to_orbital_elements(pos, vel, parent_mass); |
||||
|
||||
INFO("After prograde burn:"); |
||||
INFO(" new omega = " << new_elements.argument_of_periapsis << " rad (" << new_elements.argument_of_periapsis * 180.0 / M_PI << " deg)"); |
||||
INFO(" new e = " << new_elements.eccentricity); |
||||
|
||||
// Get new eccentricity vector
|
||||
Vec3 e_vec_new = vec3_scale(vec3_sub(vec3_scale(vel, r), vec3_scale(pos, vec3_magnitude(vel) / mu)), 1.0 / mu); |
||||
INFO(" new e_vec = (" << e_vec_new.x << ", " << e_vec_new.y << ", " << e_vec_new.z << ")"); |
||||
|
||||
// For zero-inclination orbit, omega is computed from the eccentricity vector
|
||||
// (longitude of periapsis since ascending node is undefined)
|
||||
// The key constraint is that omega should be in [0, 2π)
|
||||
bool omega_in_range = (new_elements.argument_of_periapsis >= 0.0) && |
||||
(new_elements.argument_of_periapsis < 2.0 * M_PI); |
||||
|
||||
REQUIRE(omega_in_range); |
||||
} |
||||
@ -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() |
||||
@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env python3 |
||||
""" |
||||
Precalculate expected values for test_omega_debug.cpp refactoring. |
||||
Computes expected orbital elements after a prograde burn at apoapsis. |
||||
""" |
||||
|
||||
import math |
||||
import sys |
||||
sys.path.insert(0, "/home/agent/dev/claudes_game") |
||||
from scripts.sim_engine import * |
||||
|
||||
|
||||
def main(): |
||||
earth_mass = 5.972e24 |
||||
mu = G * earth_mass |
||||
|
||||
# Initial orbit: zero inclination, omega = 0, start at apoapsis (nu = pi) |
||||
elements = OrbitalElements( |
||||
a=1.0e7, |
||||
e=0.3, |
||||
nu=math.pi, |
||||
inc=1e-12, |
||||
Omega=0.0, |
||||
omega=0.0, |
||||
) |
||||
|
||||
pos, vel = orbital_to_cartesian(elements, earth_mass) |
||||
|
||||
r = vmag(pos) |
||||
v = vmag(vel) |
||||
|
||||
print("// Initial state") |
||||
print(f"// pos = ({pos[0]:.15e}, {pos[1]:.15e}, {pos[2]:.15e}) m") |
||||
print(f"// vel = ({vel[0]:.15e}, {vel[1]:.15e}, {vel[2]:.15e}) m/s") |
||||
print(f"// r = {r:.15e} m") |
||||
print(f"// v = {v:.15e} m/s") |
||||
print() |
||||
|
||||
# Eccentricity vector |
||||
r_dot_v = vdot(pos, vel) |
||||
e_vec = ( |
||||
((v * v - mu / r) * pos[0] - r_dot_v * vel[0]) / mu, |
||||
((v * v - mu / r) * pos[1] - r_dot_v * vel[1]) / mu, |
||||
((v * v - mu / r) * pos[2] - r_dot_v * vel[2]) / mu, |
||||
) |
||||
e_mag = vmag(e_vec) |
||||
|
||||
print(f"// e_vec_initial = ({e_vec[0]:.15e}, {e_vec[1]:.15e}, {e_vec[2]:.15e})") |
||||
print(f"// e_initial = {e_mag:.15e}") |
||||
print() |
||||
|
||||
# Apply prograde burn (1000 m/s) |
||||
burn_dir = get_burn_direction(BurnDirection.PROGRADE, pos, vel) |
||||
dv = 1000.0 |
||||
dv_vec = vscale(burn_dir, dv) |
||||
vel_new = vadd(vel, dv_vec) |
||||
|
||||
v_new = vmag(vel_new) |
||||
|
||||
print("// After prograde burn") |
||||
print(f"// burn_dir = ({burn_dir[0]:.15e}, {burn_dir[1]:.15e}, {burn_dir[2]:.15e})") |
||||
print(f"// vel_new = ({vel_new[0]:.15e}, {vel_new[1]:.15e}, {vel_new[2]:.15e}) m/s") |
||||
print(f"// v_new = {v_new:.15e} m/s") |
||||
print() |
||||
|
||||
# Reconstruct orbital elements |
||||
new_elements = cartesian_to_orbital_elements(pos, vel_new, earth_mass) |
||||
|
||||
print(f"// new elements:") |
||||
print(f"// a = {new_elements.a:.15e} m") |
||||
print(f"// e = {new_elements.e:.15e}") |
||||
print(f"// nu = {new_elements.nu:.15e} rad ({math.degrees(new_elements.nu):.6f} deg)") |
||||
print(f"// inc = {new_elements.inc:.15e} rad") |
||||
print(f"// Omega = {new_elements.Omega:.15e} rad") |
||||
print(f"// omega = {new_elements.omega:.15e} rad ({math.degrees(new_elements.omega):.6f} deg)") |
||||
print() |
||||
|
||||
# New eccentricity vector |
||||
r_dot_v_new = vdot(pos, vel_new) |
||||
e_vec_new = ( |
||||
((v_new * v_new - mu / r) * pos[0] - r_dot_v_new * vel_new[0]) / mu, |
||||
((v_new * v_new - mu / r) * pos[1] - r_dot_v_new * vel_new[1]) / mu, |
||||
((v_new * v_new - mu / r) * pos[2] - r_dot_v_new * vel_new[2]) / mu, |
||||
) |
||||
|
||||
print(f"// e_vec_new = ({e_vec_new[0]:.15e}, {e_vec_new[1]:.15e}, {e_vec_new[2]:.15e})") |
||||
print(f"// e_new = {vmag(e_vec_new):.15e}") |
||||
print() |
||||
|
||||
# Verify omega is in [0, 2*pi) |
||||
print("// Omega range check") |
||||
print(f"// omega = {new_elements.omega:.15e} rad") |
||||
print(f"// omega in [0, 2*pi)? {0.0 <= new_elements.omega < 2.0 * math.pi}") |
||||
print() |
||||
|
||||
# After a prograde burn at apoapsis (nu=pi), the eccentricity vector flips |
||||
# direction because the increased velocity raises the opposite side of the orbit. |
||||
# This means omega should change from 0 to approximately pi. |
||||
# |
||||
# Rationale: at apoapsis, position and velocity are perpendicular. |
||||
# A prograde burn adds velocity along the velocity direction, increasing energy. |
||||
# The eccentricity vector formula: e_vec = (v^2 - mu/r)*r/μ - (r·v)*v/μ |
||||
# At apoapsis: r·v = 0, so e_vec = (v^2 - mu/r) * r / mu |
||||
# After prograde burn, v increases, so (v^2 - mu/r) becomes more positive, |
||||
# making e_vec more aligned with r direction. |
||||
# Since at apoapsis, r points opposite to periapsis direction (for ω=0), |
||||
# the eccentricity vector flips, meaning periapsis moves to the opposite side, |
||||
# so ω → π. |
||||
|
||||
print("// Expected test values") |
||||
print(f"// a_expected = {new_elements.a:.15e}") |
||||
print(f"// e_expected = {new_elements.e:.15e}") |
||||
print(f"// omega_expected = {new_elements.omega:.15e} rad ({math.degrees(new_elements.omega):.6f} deg)") |
||||
print() |
||||
|
||||
# Also compute expected values using the same check as the original test |
||||
print("// For WithinAbs assertions:") |
||||
print(f"// a := {new_elements.a:.15e}") |
||||
print(f"// e := {new_elements.e:.15e}") |
||||
print(f"// omega := {new_elements.omega:.15e}") |
||||
print(f"// inc := {new_elements.inc:.15e}") |
||||
print(f"// Omega := {new_elements.Omega:.15e}") |
||||
print(f"// nu := {new_elements.nu:.15e}") |
||||
print(f"// r := {r:.15e}") |
||||
print(f"// v_new := {v_new:.15e}") |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
main() |
||||
@ -0,0 +1,81 @@
|
||||
#include <catch2/catch_test_macros.hpp> |
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp> |
||||
#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); |
||||
} |
||||
@ -0,0 +1,60 @@
|
||||
#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/test_utilities.h" |
||||
#include <cmath> |
||||
|
||||
using Catch::Matchers::WithinAbs; |
||||
|
||||
SCENARIO("Omega reconstruction after prograde burn at apoapsis", "[omega][debug]") { |
||||
const double parent_mass = 5.972e24; |
||||
const double mu = G * parent_mass; |
||||
|
||||
OrbitalElements elements = {}; |
||||
elements.semi_major_axis = 1.0e7; |
||||
elements.eccentricity = 0.3; |
||||
elements.true_anomaly = M_PI; |
||||
elements.inclination = 1e-12; |
||||
elements.longitude_of_ascending_node = 0.0; |
||||
elements.argument_of_periapsis = 0.0; |
||||
|
||||
Vec3 pos = {}, vel = {}; |
||||
orbital_elements_to_cartesian(elements, parent_mass, &pos, &vel); |
||||
|
||||
const double r = vec3_magnitude(pos); |
||||
const double v = vec3_magnitude(vel); |
||||
|
||||
const double r_dot_v = vec3_dot(pos, vel); |
||||
const Vec3 e_vec = { |
||||
((v * v - mu / r) * pos.x - r_dot_v * vel.x) / mu, |
||||
((v * v - mu / r) * pos.y - r_dot_v * vel.y) / mu, |
||||
((v * v - mu / r) * pos.z - r_dot_v * vel.z) / mu, |
||||
}; |
||||
const double e_initial = vec3_magnitude(e_vec); |
||||
|
||||
SECTION("initial apoapsis state is correct") { |
||||
REQUIRE_THAT(r, WithinAbs(1.3e7, R_TOL)); |
||||
REQUIRE_THAT(v, WithinAbs(4632.763232589246, V_TOL)); |
||||
REQUIRE_THAT(e_initial, WithinAbs(0.3, E_TOL)); |
||||
REQUIRE_THAT(e_vec.x / e_initial, WithinAbs(1.0, E_TOL)); |
||||
REQUIRE_THAT(e_vec.y, WithinAbs(0.0, E_TOL)); |
||||
REQUIRE_THAT(e_vec.z, WithinAbs(0.0, E_TOL)); |
||||
} |
||||
|
||||
SECTION("prograde burn at apoapsis flips periapsis") { |
||||
const Vec3 burn_dir = vec3_normalize(vel); |
||||
const Vec3 delta_v = vec3_scale(burn_dir, 1000.0); |
||||
const Vec3 vel_new = vec3_add(vel, delta_v); |
||||
const double v_new = vec3_magnitude(vel_new); |
||||
|
||||
const OrbitalElements new_elements = cartesian_to_orbital_elements(pos, vel_new, parent_mass); |
||||
|
||||
REQUIRE_THAT(new_elements.semi_major_axis, WithinAbs(1.346885753127762e7, A_TOL)); |
||||
REQUIRE_THAT(new_elements.eccentricity, WithinAbs(3.481049006486453e-2, E_TOL)); |
||||
REQUIRE_THAT(new_elements.argument_of_periapsis, WithinAbs(M_PI, ANG_TOL)); |
||||
REQUIRE_THAT(new_elements.true_anomaly, WithinAbs(0.0, ANG_TOL)); |
||||
REQUIRE_THAT(new_elements.inclination, WithinAbs(0.0, ANG_TOL)); |
||||
REQUIRE_THAT(v_new, WithinAbs(5632.763232589246, V_TOL)); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue