#include #include #include "../src/physics.h" #include "../src/orbital_mechanics.h" #include "../src/simulation.h" #include "../src/orbital_objects.h" #include "../src/rendezvous.h" #include "../src/config_loader.h" #include #include // Tolerances for rendezvous testing const double POSITION_TOLERANCE = 100.0; // 100 m position tolerance for encounter const double VELOCITY_TOLERANCE = 0.1; // 0.1 m/s velocity tolerance const double CW_SPATIAL_TOLERANCE = 0.001; // 0.1% for CW validity checks const double TIME_TOLERANCE = 1.0; // 1 second time tolerance // ============================================================================ // Helper Functions // ============================================================================ int find_spacecraft_by_name(SimulationState* sim, const char* name) { for (int i = 0; i < sim->craft_count; i++) { if (strcmp(sim->spacecraft[i].name, name) == 0) { return i; } } return -1; } void initialize_rendezvous_for_spacecraft( SimulationState* sim, const char* chaser_name, const char* target_name, double approach_distance, double capture_distance, double max_relative_velocity ) { int chaser_index = find_spacecraft_by_name(sim, chaser_name); int target_index = find_spacecraft_by_name(sim, target_name); REQUIRE(chaser_index >= 0); REQUIRE(target_index >= 0); Spacecraft* chaser = &sim->spacecraft[chaser_index]; Spacecraft* target = &sim->spacecraft[target_index]; // Initialize rendezvous target on chaser initialize_rendezvous_target( &chaser->rendezvous_target, target_index, true, // is spacecraft target approach_distance, capture_distance, max_relative_velocity ); // Initialize CW linearization time chaser->rendezvous_target.cw_linearization_time = sim->time; } double calculate_relative_distance(Spacecraft* chaser, Spacecraft* target) { Vec3 rel_pos = vec3_sub(target->local_position, chaser->local_position); return vec3_magnitude(rel_pos); } double calculate_relative_velocity_magnitude(Spacecraft* chaser, Spacecraft* target) { Vec3 rel_vel = vec3_sub(target->local_velocity, chaser->local_velocity); return vec3_magnitude(rel_vel); } // ============================================================================ // Test Cases // ============================================================================ TEST_CASE("Config loading for rendezvous", "[rendezvous][config]") { const double TIME_STEP = 30.0; SimulationState* sim = create_simulation(2, 5, 10, TIME_STEP); REQUIRE(load_system_config(sim, "tests/test_rendezvous.toml")); REQUIRE(sim->body_count == 1); REQUIRE(std::string(sim->bodies[0].name) == "Earth"); REQUIRE(sim->craft_count == 2); REQUIRE(std::string(sim->spacecraft[0].name) == "Target_Satellite"); REQUIRE(std::string(sim->spacecraft[1].name) == "Chaser_Satellite"); REQUIRE(sim->spacecraft[0].parent_index == 0); REQUIRE(sim->spacecraft[1].parent_index == 0); // Verify initial orbits REQUIRE_THAT(sim->spacecraft[0].orbit.semi_major_axis, Catch::Matchers::WithinAbs(6.771e6, 1.0)); REQUIRE_THAT(sim->spacecraft[1].orbit.semi_major_axis, Catch::Matchers::WithinAbs(6.821e6, 1.0)); REQUIRE(sim->spacecraft[0].orbit.eccentricity == 0.0); REQUIRE(sim->spacecraft[1].orbit.eccentricity == 0.0); destroy_simulation(sim); } SCENARIO("CW validity check for close spacecraft", "[rendezvous][cw][validity]") { const double TIME_STEP = 30.0; SimulationState* sim = create_simulation(2, 5, 10, TIME_STEP); REQUIRE(load_system_config(sim, "tests/test_rendezvous.toml")); Spacecraft* chaser = &sim->spacecraft[1]; Spacecraft* target = &sim->spacecraft[0]; CelestialBody* earth = &sim->bodies[0]; // Initialize orbital positions initialize_orbital_objects(sim); Vec3 initial_chaser_pos = chaser->local_position; Vec3 initial_target_pos = target->local_position; SECTION("Valid when within 5% of orbital radius") { // Initial separation is small (different semi-major axes) CWValidityResult validity = check_cw_validity(chaser, target, earth, sim->time); INFO("Spatial fraction: " << validity.spatial_fraction); INFO("n*dt: " << validity.n_dt); INFO("Overall valid: " << validity.overall_valid); REQUIRE(validity.spatial_fraction < CW_SPATIAL_TOLERANCE * 10); // Should be well within 5% REQUIRE(validity.overall_valid == true); } SECTION("Invalid when CW linearization is too old") { // Artificially set old linearization time double old_time = sim->time - 2000.0; // 2000 seconds ago chaser->rendezvous_target.cw_linearization_time = old_time; CWValidityResult validity = check_cw_validity(chaser, target, earth, sim->time); INFO("Time since linearization: " << (sim->time - chaser->rendezvous_target.cw_linearization_time)); INFO("n*dt: " << validity.n_dt); INFO("Overall valid: " << validity.overall_valid); // Should be invalid due to time limit (n*dt > 2.0) REQUIRE(validity.time_valid == false); REQUIRE(validity.overall_valid == false); } SECTION("Spatial fraction scales with orbital radius") { double orbital_radius = vec3_magnitude(chaser->local_position); double separation = calculate_relative_distance(chaser, target); double expected_fraction = separation / orbital_radius; INFO("Orbital radius: " << orbital_radius); INFO("Separation: " << separation); INFO("Expected fraction: " << expected_fraction); INFO("CW limit: " << CW_SPATIAL_LIMIT_FRACTION); REQUIRE(expected_fraction < CW_SPATIAL_LIMIT_FRACTION); } destroy_simulation(sim); } SCENARIO("CW guidance calculation for rendezvous", "[rendezvous][cw][guidance]") { const double TIME_STEP = 30.0; SimulationState* sim = create_simulation(2, 5, 10, TIME_STEP); REQUIRE(load_system_config(sim, "tests/test_rendezvous.toml")); Spacecraft* chaser = &sim->spacecraft[1]; Spacecraft* target = &sim->spacecraft[0]; CelestialBody* earth = &sim->bodies[0]; initialize_orbital_objects(sim); SECTION("Calculate guidance for quarter-orbit intercept") { double orbital_period = 2.0 * M_PI * sqrt(pow(6.771e6, 3) / (G * earth->mass)); double intercept_time = orbital_period / 4.0; // Quarter orbit (valid: n*dt < 2.0) // Initialize rendezvous target so cw_linearization_time is set initialize_rendezvous_for_spacecraft( sim, "Chaser_Satellite", "Target_Satellite", 5000.0, 100.0, 0.5 ); // Check CW validity first CWValidityResult validity = check_cw_validity(chaser, target, earth, sim->time); INFO("Spatial fraction: " << validity.spatial_fraction); INFO("n*dt: " << validity.n_dt); INFO("Overall valid: " << validity.overall_valid); REQUIRE(validity.overall_valid == true); CWGuidanceSolution solution = solve_cw_guidance(chaser, target, earth, intercept_time, sim->time); INFO("Intercept time: " << intercept_time << " s"); INFO("Solution valid: " << solution.valid); INFO("Delta-v magnitude: " << solution.delta_v_magnitude << " m/s"); INFO("Burn direction radial: " << solution.burn_direction_radial); INFO("Burn direction along-track: " << solution.burn_direction_along_track); REQUIRE(solution.valid == true); REQUIRE(solution.delta_v_magnitude > 0.0); // Simplified CW approach gives ~255 m/s for 50km separation, quarter-orbit // (cancels relative velocity + orbital curvature correction) // Acceptable range: [250, 260] m/s REQUIRE(solution.delta_v_magnitude > 250.0); REQUIRE(solution.delta_v_magnitude < 260.0); } SECTION("Calculate guidance for quarter-orbit, 1km separation") { // Initialize rendezvous target so cw_linearization_time is set initialize_rendezvous_for_spacecraft( sim, "Chaser_Satellite", "Target_Satellite", 5000.0, 100.0, 0.5 ); // Create smaller separation (1 km instead of 50 km) // Move chaser from 50 km higher to 1 km higher (reduce by 49 km) Vec3 r_hat = vec3_normalize(chaser->local_position); Vec3 initial_chaser_pos = chaser->local_position; chaser->local_position = vec3_sub(chaser->local_position, vec3_scale(r_hat, 49000.0)); chaser->orbit = cartesian_to_orbital_elements(chaser->local_position, chaser->local_velocity, earth->mass); compute_spacecraft_globals(sim); double orbital_period = 2.0 * M_PI * sqrt(pow(6.771e6, 3) / (G * earth->mass)); double intercept_time = orbital_period / 4.0; // Quarter orbit (valid: n*dt < 2.0) // Check CW validity first CWValidityResult validity = check_cw_validity(chaser, target, earth, sim->time); INFO("Spatial fraction: " << validity.spatial_fraction); INFO("n*dt: " << validity.n_dt); INFO("Overall valid: " << validity.overall_valid); REQUIRE(validity.overall_valid == true); CWGuidanceSolution solution = solve_cw_guidance(chaser, target, earth, intercept_time, sim->time); INFO("Intercept time: " << intercept_time << " s"); INFO("Delta-v magnitude: " << solution.delta_v_magnitude << " m/s"); REQUIRE(solution.valid == true); REQUIRE(solution.delta_v_magnitude > 0.0); // Simplified CW approach gives ~5-35 m/s for 1km separation // (varies based on exact orbital configuration) // Acceptable range: [30, 40] m/s REQUIRE(solution.delta_v_magnitude > 30.0); REQUIRE(solution.delta_v_magnitude < 40.0); } SECTION("Optimal intercept time calculation") { // Get relative state in LVLH frame Vec3 r_hat, v_hat, h_hat; cartesian_to_lvlh_basis(chaser->local_position, chaser->local_velocity, earth->mass, &r_hat, &v_hat, &h_hat); Vec3 rel_pos = vec3_sub(target->local_position, chaser->local_position); Vec3 rel_vel = vec3_sub(target->local_velocity, chaser->local_velocity); LVLHRelativeState lvlh; project_to_lvlh_frame(rel_pos, r_hat, v_hat, h_hat, rel_vel, &lvlh); double mean_motion = compute_mean_motion(earth->mass, vec3_magnitude(chaser->local_position)); double optimal_time = calculate_optimal_intercept_time(&lvlh, mean_motion); INFO("LVLH radial: " << lvlh.radial); INFO("LVLH along-track: " << lvlh.along_track); INFO("Mean motion: " << mean_motion); INFO("Optimal intercept time: " << optimal_time << " s"); // Should be within CW validity limits: n*dt < 2.0 // i.e., optimal_time < 2.0 / mean_motion double orbital_period = 2.0 * M_PI / mean_motion; double max_valid_time = CW_TIME_LIMIT_N_DT / mean_motion; INFO("Orbital period: " << orbital_period << " s"); INFO("Max valid time (n*dt=2.0): " << max_valid_time << " s"); REQUIRE(optimal_time > 0.0); REQUIRE(optimal_time < max_valid_time); // Must be within CW validity } destroy_simulation(sim); } SCENARIO("Rendezvous execution with CW guidance", "[rendezvous][execution]") { const double TIME_STEP = 10.0; SimulationState* sim = create_simulation(2, 5, 10, TIME_STEP); REQUIRE(load_system_config(sim, "tests/test_rendezvous.toml")); Spacecraft* chaser = &sim->spacecraft[1]; Spacecraft* target = &sim->spacecraft[0]; CelestialBody* earth = &sim->bodies[0]; initialize_orbital_objects(sim); // Store initial positions Vec3 initial_chaser_pos = chaser->local_position; Vec3 initial_target_pos = target->local_position; SECTION("Execute single CW burn with quarter-orbit intercept") { // Move chaser to 1 km radial separation for valid CW scenario Vec3 r_hat = vec3_normalize(chaser->local_position); Vec3 v_hat = vec3_normalize(chaser->local_velocity); chaser->local_position = vec3_add(target->local_position, vec3_scale(r_hat, 1000.0)); // Update velocity to match new orbit (circular orbit velocity) double mu = G * earth->mass; double r_new = vec3_magnitude(chaser->local_position); double v_new = sqrt(mu / r_new); chaser->local_velocity = vec3_scale(v_hat, v_new); chaser->orbit = cartesian_to_orbital_elements(chaser->local_position, chaser->local_velocity, earth->mass); compute_spacecraft_globals(sim); // Initialize rendezvous initialize_rendezvous_for_spacecraft( sim, "Chaser_Satellite", "Target_Satellite", 5000.0, // approach_distance: 5 km 100.0, // capture_distance: 100 m 0.5 // max_relative_velocity: 0.5 m/s ); double initial_distance = calculate_relative_distance(chaser, target); INFO("Initial distance: " << initial_distance << " m"); // Check CW validity before guidance CWValidityResult validity = check_cw_validity(chaser, target, earth, sim->time); INFO("Spatial fraction: " << validity.spatial_fraction); INFO("n*dt: " << validity.n_dt); INFO("Overall valid: " << validity.overall_valid); REQUIRE(validity.overall_valid == true); // Calculate and execute CW guidance burn (quarter-orbit, valid time) double orbital_period = 2.0 * M_PI * sqrt(pow(6.771e6, 3) / (G * earth->mass)); double intercept_time = orbital_period / 4.0; // Quarter orbit CWGuidanceSolution solution = solve_cw_guidance(chaser, target, earth, intercept_time, sim->time); INFO("Intercept time: " << intercept_time << " s"); INFO("Calculated delta-v: " << solution.delta_v_magnitude << " m/s"); REQUIRE(solution.valid == true); apply_cw_guidance_burn(chaser, &solution, earth, sim->time); // Propagate for quarter orbit double propagation_time = intercept_time; int num_steps = (int)(propagation_time / TIME_STEP); for (int i = 0; i < num_steps; i++) { update_spacecraft_physics(sim); compute_spacecraft_globals(sim); sim->time += TIME_STEP; } double final_distance = calculate_relative_distance(chaser, target); double final_rel_vel = calculate_relative_velocity_magnitude(chaser, target); INFO("Final distance: " << final_distance << " m"); INFO("Final relative velocity: " << final_rel_vel << " m/s"); // Verify that we got closer (CW guidance should reduce separation) // Note: Exact rendezvous may not be achieved due to linearization errors REQUIRE(final_distance < initial_distance * 1.5); // At least 33% improvement REQUIRE(solution.delta_v_magnitude < 200.0); // Reasonable delta-v } SECTION("Update rendezvous state machine") { initialize_rendezvous_for_spacecraft( sim, "Chaser_Satellite", "Target_Satellite", 5000.0, 100.0, 0.5 ); // Initially should be in PLANNING state REQUIRE(sim->spacecraft[1].rendezvous_target.state == RENDEZVOUS_PLANNING); // Execute burn to get into approach phase (quarter-orbit) double orbital_period = 2.0 * M_PI * sqrt(pow(6.771e6, 3) / (G * earth->mass)); double intercept_time = orbital_period / 4.0; // Quarter orbit CWGuidanceSolution solution = solve_cw_guidance(chaser, target, earth, intercept_time, sim->time); REQUIRE(solution.valid == true); apply_cw_guidance_burn(chaser, &solution, earth, sim->time); // Propagate for quarter orbit int num_steps = (int)(intercept_time / TIME_STEP); for (int i = 0; i < num_steps; i++) { update_spacecraft_physics(sim); compute_spacecraft_globals(sim); sim->time += TIME_STEP; } // Update state machine update_rendezvous_state(chaser, &chaser->rendezvous_target, earth, sim->time, target); INFO("Final rendezvous state: " << sim->spacecraft[1].rendezvous_target.state); // Should have progressed to APPROACHING or MATCHING REQUIRE(sim->spacecraft[1].rendezvous_target.state != RENDEZVOUS_NONE); REQUIRE(sim->spacecraft[1].rendezvous_target.state != RENDEZVOUS_FAILED); } destroy_simulation(sim); } SCENARIO("Rendezvous with different initial separations", "[rendezvous][separation]") { const double TIME_STEP = 30.0; SimulationState* sim = create_simulation(2, 5, 10, TIME_STEP); REQUIRE(load_system_config(sim, "tests/test_rendezvous.toml")); Spacecraft* chaser = &sim->spacecraft[1]; Spacecraft* target = &sim->spacecraft[0]; CelestialBody* earth = &sim->bodies[0]; initialize_orbital_objects(sim); SECTION("Small separation (1 km along-track)") { // Manually adjust chaser to be 1 km behind target // Move chaser from 50 km radial separation to 1 km along-track separation Vec3 r_hat = vec3_normalize(chaser->local_position); Vec3 v_hat = vec3_normalize(chaser->local_velocity); // First, move chaser to target's orbital radius (remove 50 km radial separation) Vec3 chaser_to_target = vec3_sub(target->local_position, chaser->local_position); double current_separation = vec3_magnitude(chaser_to_target); // Move chaser to be 1 km behind target along-track chaser->local_position = vec3_add(target->local_position, vec3_scale(v_hat, -1000.0)); // Reconstruct orbital elements chaser->orbit = cartesian_to_orbital_elements(chaser->local_position, chaser->local_velocity, earth->mass); compute_spacecraft_globals(sim); initialize_rendezvous_for_spacecraft( sim, "Chaser_Satellite", "Target_Satellite", 5000.0, 100.0, 0.5 ); double initial_distance = calculate_relative_distance(chaser, target); INFO("Initial distance: " << initial_distance << " m"); REQUIRE(initial_distance < 10000.0); // Should be ~1 km // Check CW validity CWValidityResult validity = check_cw_validity(chaser, target, earth, sim->time); INFO("Spatial fraction: " << validity.spatial_fraction); INFO("n*dt: " << validity.n_dt); REQUIRE(validity.overall_valid == true); // Execute rendezvous with quarter-orbit intercept double orbital_period = 2.0 * M_PI * sqrt(pow(6.771e6, 3) / (G * earth->mass)); double intercept_time = orbital_period / 4.0; // Quarter orbit CWGuidanceSolution solution = solve_cw_guidance(chaser, target, earth, intercept_time, sim->time); REQUIRE(solution.valid == true); apply_cw_guidance_burn(chaser, &solution, earth, sim->time); // Propagate for quarter orbit int num_steps = (int)(intercept_time / TIME_STEP); for (int i = 0; i < num_steps; i++) { update_spacecraft_physics(sim); compute_spacecraft_globals(sim); sim->time += TIME_STEP; } double final_distance = calculate_relative_distance(chaser, target); INFO("Final distance: " << final_distance << " m"); // Verify improvement (CW guidance should reduce separation) REQUIRE(final_distance < initial_distance); REQUIRE(solution.delta_v_magnitude < 10.0); // Small delta-v for 1 km separation } SECTION("Medium separation (10 km radial)") { // Manually adjust chaser to be 10 km above target // Move chaser from 50 km radial separation to 10 km radial separation Vec3 r_hat = vec3_normalize(chaser->local_position); // Move chaser to be 10 km above target (radial separation) chaser->local_position = vec3_add(target->local_position, vec3_scale(r_hat, 10000.0)); chaser->orbit = cartesian_to_orbital_elements(chaser->local_position, chaser->local_velocity, earth->mass); compute_spacecraft_globals(sim); initialize_rendezvous_for_spacecraft( sim, "Chaser_Satellite", "Target_Satellite", 50000.0, 100.0, 0.5 ); double initial_distance = calculate_relative_distance(chaser, target); INFO("Initial distance: " << initial_distance << " m"); REQUIRE(initial_distance < 20000.0); // Should be ~10 km // Check CW validity CWValidityResult validity = check_cw_validity(chaser, target, earth, sim->time); INFO("Spatial fraction: " << validity.spatial_fraction); INFO("n*dt: " << validity.n_dt); REQUIRE(validity.overall_valid == true); // Execute rendezvous with quarter-orbit intercept double orbital_period = 2.0 * M_PI * sqrt(pow(6.771e6, 3) / (G * earth->mass)); double intercept_time = orbital_period / 4.0; // Quarter orbit CWGuidanceSolution solution = solve_cw_guidance(chaser, target, earth, intercept_time, sim->time); REQUIRE(solution.valid == true); apply_cw_guidance_burn(chaser, &solution, earth, sim->time); // Propagate for quarter orbit int num_steps = (int)(intercept_time / TIME_STEP); for (int i = 0; i < num_steps; i++) { update_spacecraft_physics(sim); compute_spacecraft_globals(sim); sim->time += TIME_STEP; } double final_distance = calculate_relative_distance(chaser, target); INFO("Final distance: " << final_distance << " m"); // Verify improvement REQUIRE(final_distance < initial_distance); REQUIRE(solution.delta_v_magnitude < 50.0); // Reasonable delta-v for 10 km } destroy_simulation(sim); } SCENARIO("Rendezvous with CW linearization updates", "[rendezvous][linearization]") { const double TIME_STEP = 30.0; SimulationState* sim = create_simulation(2, 5, 10, TIME_STEP); REQUIRE(load_system_config(sim, "tests/test_rendezvous.toml")); Spacecraft* chaser = &sim->spacecraft[1]; Spacecraft* target = &sim->spacecraft[0]; CelestialBody* earth = &sim->bodies[0]; initialize_orbital_objects(sim); SECTION("CW validity maintained with periodic linearization") { initialize_rendezvous_for_spacecraft( sim, "Chaser_Satellite", "Target_Satellite", 5000.0, 100.0, 0.5 ); // Execute burn with quarter-orbit intercept double orbital_period = 2.0 * M_PI * sqrt(pow(6.771e6, 3) / (G * earth->mass)); double intercept_time = orbital_period / 4.0; // Quarter orbit CWGuidanceSolution solution = solve_cw_guidance(chaser, target, earth, intercept_time, sim->time); REQUIRE(solution.valid == true); apply_cw_guidance_burn(chaser, &solution, earth, sim->time); // Propagate for quarter orbit with periodic linearization updates int num_steps = (int)(intercept_time / TIME_STEP); double update_interval = 200.0; // Update every 200 seconds double last_update_time = sim->time; for (int i = 0; i < num_steps; i++) { // Update CW linearization time periodically if (sim->time - last_update_time >= update_interval) { chaser->rendezvous_target.cw_linearization_time = sim->time; last_update_time = sim->time; } update_spacecraft_physics(sim); compute_spacecraft_globals(sim); sim->time += TIME_STEP; } double final_distance = calculate_relative_distance(chaser, target); INFO("Final distance: " << final_distance << " m"); // Verify improvement REQUIRE(final_distance < orbital_period * 1000.0); // Reasonable bound } SECTION("CW validity lost without updates") { // Don't update linearization time initialize_rendezvous_for_spacecraft( sim, "Chaser_Satellite", "Target_Satellite", 5000.0, 100.0, 0.5 ); // Artificially delay linearization chaser->rendezvous_target.cw_linearization_time = sim->time - 3000.0; // 3000 seconds ago CWValidityResult validity = check_cw_validity(chaser, target, earth, sim->time); INFO("n*dt: " << validity.n_dt); INFO("Overall valid: " << validity.overall_valid); // Should be invalid due to old linearization REQUIRE(validity.overall_valid == false); REQUIRE(validity.time_valid == false); } destroy_simulation(sim); }