@ -25,35 +25,11 @@ SCENARIO("Periapsis-triggered prograde burn behavior", "[maneuver][periapsis]")
const double initial_periapsis = 7259700.0 ;
const double burn1_expected_sma = 13404876.6810 ;
const double burn1_expected_v = 8943.1448 ;
const double burn1_expected_radius = 7265936.0570 ;
const double burn2_expected_radius = 7262462.4116 ;
const double cross_expected_radius = 7259786.1864 ;
// Propagation-level tolerance constants (coarser than conversion tolerances).
//
// NOTE: These tolerances exist because the test measures spacecraft state
// AFTER update_simulation() returns — i.e. after the full 60s post-burn
// propagation in the new orbit. The burn itself fires at exact nu=0 (the
// trigger detects angular_distance(0,0) < 0.01 and sets scheduled_dt=0).
// The burn happens, the orbit changes, then the craft flies 60s in the
// new orbit before the test reads craft->local_position. So nu=0.074 rad
// is the true anomaly 60s after the burn, not the nu at burn time.
//
// Plan: add a BurnResult struct (Vec3 position, Vec3 velocity) to Maneuver.
// populate it in execute_maneuver() before the remaining_dt propagation.
// The test will then read sim->maneuvers[i].burn_result to get exact
// burn-time state vectors, eliminating these propagation-level tolerances
// and allowing assertions like "burn position == periapsis" directly.
//
// C++ vs Python state vector agreement at the same simulation step is
// ~50 microns (floating-point noise), confirming sim_engine.py matches.
const double PERIAPSIS_TOL = 1.0 ; // periapsis preserved by burn
const double PROP_RADIUS_TOL = 0.001 ; // sub-step offset at burn (~50 μm)
const double PROP_ANGLE_TOL = 0.075 ; // nu 60s after burn1 (~0.074 rad)
const double PROP_TIME_TOL = 28.0 ; // period vs time_between (~25.8 s)
const double CROSS_ANGLE_TOL = 0.009 ; // nu 60s after cross burn (~0.009 rad)
const double PROP_SMA_TOL = 1.0 ; // SMA after single burn
const double PROP_VEL_TOL = 0.1 ; // velocity after single burn
// BurnResult captures exact pre-burn state vectors, enabling tight
// tolerances (R_TOL, ANG_TOL) for periapsis assertions.
// Propagation-level tolerances (A_TOL*10, V_TOL*100, M_TOL*10) remain
// for post-burn+60s-propagation state comparisons.
SECTION ( " spacecraft loads correctly " ) {
REQUIRE ( sim - > craft_count = = 2 ) ;
@ -72,6 +48,12 @@ SCENARIO("Periapsis-triggered prograde burn behavior", "[maneuver][periapsis]")
// Execute one step — burn fires immediately (nu=0, trigger=0)
update_simulation ( sim ) ;
// Verify burn fired at exact periapsis via burn_result
const BurnResult & br = sim - > maneuvers [ 0 ] . burn_result ;
REQUIRE ( br . valid ) ;
REQUIRE_THAT ( br . true_anomaly , WithinAbs ( 0.0 , ANG_TOL ) ) ;
REQUIRE_THAT ( vec3_magnitude ( br . position ) , WithinAbs ( initial_periapsis , R_TOL ) ) ;
// Maneuver executed
REQUIRE ( sim - > maneuvers [ 0 ] . executed ) ;
@ -79,17 +61,20 @@ SCENARIO("Periapsis-triggered prograde burn behavior", "[maneuver][periapsis]")
double final_sma = craft - > orbit . semi_major_axis ;
double final_ecc = craft - > orbit . eccentricity ;
double final_periapsis = final_sma * ( 1.0 - final_ecc ) ;
REQUIRE_THAT ( final_periapsis , WithinAbs ( initial_periapsis , PE RIAPSIS _TOL) ) ;
REQUIRE_THAT ( final_periapsis , WithinAbs ( initial_periapsis , R_TOL ) ) ;
// Semi-major axis and velocity increase (from precalculated expected values)
REQUIRE_THAT ( final_sma , WithinAbs ( burn1_expected_sma , PROP_SMA_TOL ) ) ;
REQUIRE_THAT ( vec3_magnitude ( craft - > local_velocity ) , WithinAbs ( burn1_expected_v , PROP_VEL_TOL ) ) ;
// Note: these are post-burn + 60s propagation, so use propagation-level tolerance
REQUIRE_THAT ( final_sma , WithinAbs ( burn1_expected_sma , A_TOL * 10 ) ) ;
REQUIRE_THAT ( vec3_magnitude ( craft - > local_velocity ) , WithinAbs ( burn1_expected_v , V_TOL * 100 ) ) ;
INFO ( " Initial SMA: " < < a_before < < " m " ) ;
INFO ( " Final SMA: " < < final_sma < < " m " ) ;
INFO ( " Initial periapsis: " < < peri_before < < " m " ) ;
INFO ( " Final periapsis: " < < final_periapsis < < " m " ) ;
INFO ( " Velocity change: " < < ( v_before - vec3_magnitude ( craft - > local_velocity ) ) < < " m/s " ) ;
INFO ( " Burn time position: " < < br . position . x < < " , " < < br . position . y < < " , " < < br . position . z ) ;
INFO ( " Burn time velocity: " < < br . velocity . x < < " , " < < br . velocity . y < < " , " < < br . velocity . z ) ;
}
SECTION ( " two sequential periapsis burns execute at same location " ) {
@ -109,56 +94,48 @@ SCENARIO("Periapsis-triggered prograde burn behavior", "[maneuver][periapsis]")
INFO ( " Initial periapsis: " < < initial_periapsis_val < < " m " ) ;
INFO ( " Initial apoapsis: " < < initial_apoapsis_val < < " m " ) ;
double burn1_time = - 1.0 , burn1_radius = - 1.0 , burn1_nu = - 10.0 ;
double burn2_time = - 1.0 , burn2_radius = - 1.0 , burn2_nu = - 10.0 ;
double burn1_period = - 1.0 ;
Vec3 burn1_pos = { } , burn2_pos = { } ;
Vec3 burn1_vel = { } , burn2_vel = { } ;
const int max_steps = 300 ;
for ( int i = 0 ; i < max_steps ; i + + ) {
update_simulation ( sim ) ;
if ( sim - > maneuvers [ burn1_idx ] . executed & & burn1_time < 0 ) {
burn1_time = sim - > time ;
burn1_radius = vec3_magnitude ( craft - > local_position ) ;
burn1_nu = craft - > orbit . true_anomaly ;
burn1_period = 2.0 * M_PI * sqrt ( pow ( craft - > orbit . semi_major_axis , 3.0 ) / ( G * parent - > mass ) ) ;
burn1_pos = craft - > local_position ;
burn1_vel = craft - > local_velocity ;
}
if ( sim - > maneuvers [ burn2_idx ] . executed & & burn2_time < 0 ) {
burn2_time = sim - > time ;
burn2_radius = vec3_magnitude ( craft - > local_position ) ;
burn2_nu = craft - > orbit . true_anomaly ;
burn2_pos = craft - > local_position ;
burn2_vel = craft - > local_velocity ;
}
}
REQUIRE ( sim - > maneuvers [ burn1_idx ] . executed ) ;
REQUIRE ( sim - > maneuvers [ burn2_idx ] . executed ) ;
// Both burns at expected periapsis-adjacent radius
REQUIRE_THAT ( burn1_radius , WithinAbs ( burn1_expected_radius , PROP_RADIUS_TOL ) ) ;
REQUIRE_THAT ( burn2_radius , WithinAbs ( burn2_expected_radius , PROP_RADIUS_TOL ) ) ;
// Read exact burn-time state from burn_result
const BurnResult & br1 = sim - > maneuvers [ burn1_idx ] . burn_result ;
const BurnResult & br2 = sim - > maneuvers [ burn2_idx ] . burn_result ;
REQUIRE ( br1 . valid ) ;
REQUIRE ( br2 . valid ) ;
double burn1_radius = vec3_magnitude ( br1 . position ) ;
double burn2_radius = vec3_magnitude ( br2 . position ) ;
double burn1_time = sim - > maneuvers [ burn1_idx ] . executed_time ;
double burn2_time = sim - > maneuvers [ burn2_idx ] . executed_time ;
// Both at true anomaly ≈ 0 (within 0.1 rad after post-burn propagation)
REQUIRE_THAT ( burn1_nu , WithinAbs ( 0.0 , PROP_ANGLE_TOL ) ) ;
REQUIRE_THAT ( burn2_nu , WithinAbs ( 0.0 , PROP_ANGLE_TOL ) ) ;
// Both burns at exact periapsis radius
REQUIRE_THAT ( burn1_radius , WithinAbs ( initial_periapsis , R _TOL) ) ;
REQUIRE_THAT ( burn2_radius , WithinAbs ( initial_periapsis , R _TOL) ) ;
// Time between burns ≈ orbital period (within 1 timestep)
// Both at exact true anomaly = 0 (burn_result captures pre-burn state)
REQUIRE_THAT ( br1 . true_anomaly , WithinAbs ( 0.0 , ANG_TOL ) ) ;
REQUIRE_THAT ( br2 . true_anomaly , WithinAbs ( 0.0 , ANG_TOL ) ) ;
// Both burns at same radius (same periapsis location)
REQUIRE_THAT ( burn1_radius , WithinAbs ( burn2_radius , R_TOL ) ) ;
// Time between burns ≈ orbital period
double time_between = burn2_time - burn1_time ;
REQUIRE_THAT ( time_between , WithinAbs ( burn1_period , PROP_TIME_TOL ) ) ;
double burn1_period = 2.0 * M_PI * sqrt ( pow ( burn1_expected_sma , 3.0 ) / ( G * parent - > mass ) ) ;
REQUIRE_THAT ( time_between , WithinAbs ( burn1_period , M_TOL * 10 ) ) ;
// Debug info (after assertions so Catch2 captures it)
INFO ( " Burn 1: t= " < < burn1_time < < " s, r= " < < burn1_radius < < " m, nu= " < < burn1_nu < < " rad " ) ;
INFO ( " pos= " < < burn1_pos . x < < " , " < < burn1_pos . y < < " , " < < burn1_pos . z ) ;
INFO ( " vel= " < < burn1_vel . x < < " , " < < burn1_vel . y < < " , " < < burn1_vel . z ) ;
INFO ( " Burn 2: t= " < < burn2_time < < " s, r= " < < burn2_radius < < " m, nu= " < < burn2_nu < < " rad " ) ;
INFO ( " pos= " < < burn2_pos . x < < " , " < < burn2_pos . y < < " , " < < burn2_pos . z ) ;
INFO ( " vel= " < < burn2_vel . x < < " , " < < burn2_vel . y < < " , " < < burn2_vel . z ) ;
INFO ( " Burn 1: t= " < < burn1_time < < " s, r= " < < burn1_radius < < " m, nu= " < < br1 . true_anomaly < < " rad " ) ;
INFO ( " pos= " < < br1 . position . x < < " , " < < br1 . position . y < < " , " < < br1 . position . z ) ;
INFO ( " vel= " < < br1 . velocity . x < < " , " < < br1 . velocity . y < < " , " < < br1 . velocity . z ) ;
INFO ( " Burn 2: t= " < < burn2_time < < " s, r= " < < burn2_radius < < " m, nu= " < < br2 . true_anomaly < < " rad " ) ;
INFO ( " pos= " < < br2 . position . x < < " , " < < br2 . position . y < < " , " < < br2 . position . z ) ;
INFO ( " vel= " < < br2 . velocity . x < < " , " < < br2 . velocity . y < < " , " < < br2 . velocity . z ) ;
INFO ( " Time between burns: " < < time_between < < " s " ) ;
INFO ( " Expected period: " < < burn1_period < < " s " ) ;
REQUIRE ( true ) ; // dummy to capture INFO
@ -180,26 +157,29 @@ SCENARIO("Periapsis-triggered prograde burn behavior", "[maneuver][periapsis]")
INFO ( " Initial periapsis: " < < cross_initial_periapsis < < " m " ) ;
INFO ( " Initial apoapsis: " < < cross_initial_apoapsis < < " m " ) ;
double burn_time = - 1.0 , burn_radius = - 1.0 , burn_nu = - 10.0 ;
const int max_steps = 1000 ;
for ( int i = 0 ; i < max_steps & & ! sim - > maneuvers [ cross_maneuver ] . executed ; i + + ) {
update_simulation ( sim ) ;
if ( sim - > maneuvers [ cross_maneuver ] . executed ) {
burn_time = sim - > time ;
burn_radius = vec3_magnitude ( craft_cross - > local_position ) ;
burn_nu = craft_cross - > orbit . true_anomaly ;
INFO ( " Burn at step " < < i < < " , t= " < < burn_time < < " s " ) ;
INFO ( " radius= " < < burn_radius < < " , nu= " < < burn_nu < < " rad " ) ;
}
}
REQUIRE ( sim - > maneuvers [ cross_maneuver ] . executed ) ;
// Burn radius close to expected periapsis-adjacent radius
REQUIRE_THAT ( burn_radius , WithinAbs ( cross_expected_radius , PROP_RADIUS_TOL ) ) ;
// Read exact burn-time state from burn_result
const BurnResult & br = sim - > maneuvers [ cross_maneuver ] . burn_result ;
REQUIRE ( br . valid ) ;
double burn_radius = vec3_magnitude ( br . position ) ;
// True anomaly ≈ 0 at burn (within 0.01 rad after post-burn propagation)
REQUIRE_THAT ( burn_nu , WithinAbs ( 0.0 , CROSS_ANGLE_TOL ) ) ;
// Burn at exact periapsis radius
REQUIRE_THAT ( burn_radius , WithinAbs ( cross_initial_periapsis , R_TOL ) ) ;
// True anomaly = 0 at burn (burn_result captures pre-burn state)
REQUIRE_THAT ( br . true_anomaly , WithinAbs ( 0.0 , ANG_TOL ) ) ;
INFO ( " Burn at step " < < max_steps < < " , t= " < < sim - > maneuvers [ cross_maneuver ] . executed_time < < " s " ) ;
INFO ( " radius= " < < burn_radius < < " , nu= " < < br . true_anomaly < < " rad " ) ;
INFO ( " pos= " < < br . position . x < < " , " < < br . position . y < < " , " < < br . position . z ) ;
INFO ( " vel= " < < br . velocity . x < < " , " < < br . velocity . y < < " , " < < br . velocity . z ) ;
}
SECTION ( " burn location equals new periapsis after prograde burn " ) {
@ -212,17 +192,21 @@ SCENARIO("Periapsis-triggered prograde burn behavior", "[maneuver][periapsis]")
REQUIRE ( sim - > maneuvers [ 0 ] . executed ) ;
double final_periapsis = craft - > orbit . semi_major_axis * ( 1.0 - craft - > orbit . eccentricity ) ;
// Verify burn happened at periapsis via burn_result
const BurnResult & br = sim - > maneuvers [ 0 ] . burn_result ;
REQUIRE ( br . valid ) ;
REQUIRE_THAT ( vec3_magnitude ( br . position ) , WithinAbs ( peri_before , R_TOL ) ) ;
REQUIRE_THAT ( br . true_anomaly , WithinAbs ( 0.0 , ANG_TOL ) ) ;
// Initial radius equals periapsis
REQUIRE_THAT ( r_before , WithinAbs ( peri_before , PERIAPSIS_TOL ) ) ;
double final_periapsis = craft - > orbit . semi_major_axis * ( 1.0 - craft - > orbit . eccentricity ) ;
// Final periapsis equals initial periapsis (burn at periapsis preserves it)
REQUIRE_THAT ( final_periapsis , WithinAbs ( peri_before , PE RIAPSIS _TOL) ) ;
REQUIRE_THAT ( final_periapsis , WithinAbs ( peri_before , R_TOL ) ) ;
INFO ( " Initial radius: " < < r_before < < " m " ) ;
INFO ( " Initial periapsis: " < < peri_before < < " m " ) ;
INFO ( " Final periapsis: " < < final_periapsis < < " m " ) ;
INFO ( " Burn_result radius: " < < vec3_magnitude ( br . position ) < < " m " ) ;
}
destroy_simulation ( sim ) ;