Browse Source
- Rewrite test with SCENARIO/SECTION, helper lambdas, std::array - Tighten tolerances: A_TOL=1e-2, E_TOL=1e-4, ANG_TOL=1e-6 - Restore compare_vec3() to test_utilities for cleaner assertions - Document 3-step refactoring procedure in continue.md - Fix duplicate entry in status trackingtest-refactor
4 changed files with 260 additions and 15 deletions
@ -0,0 +1,222 @@
|
||||
#include <catch2/catch_test_macros.hpp> |
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp> |
||||
#include <cmath> |
||||
#include <array> |
||||
#include <vector> |
||||
#include "../src/orbital_mechanics.h" |
||||
#include "../src/test_utilities.h" |
||||
|
||||
using Catch::Matchers::WithinAbs; |
||||
|
||||
SCENARIO("Cartesian to Elements - Advanced conversion tests", |
||||
"[orbital_mechanics][cartesian][elements]") { |
||||
const double M_sun = 1.989e30; |
||||
const double A_TOL = 1e-2; |
||||
const double E_TOL = 1e-4; |
||||
const double ANG_TOL = 1e-6; |
||||
const double ANG_TOL_COARSE = 1e-4; |
||||
|
||||
auto convert_and_recover = [&](const OrbitalElements& elements) { |
||||
Vec3 pos, vel; |
||||
orbital_elements_to_cartesian(elements, M_sun, &pos, &vel); |
||||
return cartesian_to_orbital_elements(pos, vel, M_sun); |
||||
}; |
||||
|
||||
auto make_elements = [&](double a, double e, double nu, double inc, |
||||
double lon_anode, double arg_peri) { |
||||
OrbitalElements el = {}; |
||||
el.semi_major_axis = a; |
||||
el.eccentricity = e; |
||||
el.true_anomaly = nu; |
||||
el.inclination = inc; |
||||
el.longitude_of_ascending_node = lon_anode; |
||||
el.argument_of_periapsis = arg_peri; |
||||
return el; |
||||
}; |
||||
|
||||
SECTION("eccentricity spectrum: circular to highly hyperbolic") { |
||||
const double r = 1.496e11; |
||||
const double v_circular = sqrt(G * M_sun / r); |
||||
const Vec3 pos_circ = {r, 0.0, 0.0}; |
||||
const Vec3 vel_circ = {0.0, v_circular, 0.0}; |
||||
|
||||
const OrbitalElements circular = make_elements(r, 0.0, 0.0, 0.0, 0.0, 0.0); |
||||
|
||||
Vec3 converted_pos, converted_vel; |
||||
orbital_elements_to_cartesian(circular, M_sun, &converted_pos, &converted_vel); |
||||
|
||||
const OrbitalElements recovered_circ = |
||||
cartesian_to_orbital_elements(converted_pos, converted_vel, M_sun); |
||||
REQUIRE_THAT(recovered_circ.eccentricity, WithinAbs(0.0, 1e-10)); |
||||
REQUIRE_THAT(recovered_circ.semi_major_axis, WithinAbs(r, A_TOL)); |
||||
REQUIRE(compare_vec3(pos_circ, converted_pos, A_TOL)); |
||||
REQUIRE(compare_vec3(vel_circ, converted_vel, 1e-6)); |
||||
|
||||
// Near-circular (e=0.001)
|
||||
const OrbitalElements near_circ = make_elements(1.496e11, 0.001, 0.5, 0.0, 0.0, 0.0); |
||||
const OrbitalElements rec_near_circ = convert_and_recover(near_circ); |
||||
REQUIRE_THAT(rec_near_circ.eccentricity, WithinAbs(0.001, 1e-6)); |
||||
REQUIRE_THAT(rec_near_circ.semi_major_axis, WithinAbs(1.496e11, A_TOL)); |
||||
|
||||
// Elliptical (e=0.5)
|
||||
const OrbitalElements elliptical = make_elements(1.0e11, 0.5, 0.8, 0.0, 0.0, 0.0); |
||||
const OrbitalElements rec_elliptical = convert_and_recover(elliptical); |
||||
REQUIRE_THAT(rec_elliptical.eccentricity, WithinAbs(0.5, E_TOL)); |
||||
REQUIRE_THAT(rec_elliptical.semi_major_axis, WithinAbs(1.0e11, A_TOL)); |
||||
|
||||
// Highly elliptical (e=0.95)
|
||||
const OrbitalElements high_ell = make_elements(1.0e11, 0.95, 0.1, 0.0, 0.0, 0.0); |
||||
const OrbitalElements rec_high_ell = convert_and_recover(high_ell); |
||||
REQUIRE_THAT(rec_high_ell.eccentricity, WithinAbs(0.95, 1e-3)); |
||||
REQUIRE_THAT(rec_high_ell.semi_major_axis, WithinAbs(1.0e11, A_TOL)); |
||||
|
||||
// Near-parabolic (e=0.999)
|
||||
const OrbitalElements near_par = make_elements(1.0e11, 0.999, 0.05, 0.0, 0.0, 0.0); |
||||
const OrbitalElements rec_near_par = convert_and_recover(near_par); |
||||
REQUIRE_THAT(rec_near_par.eccentricity, WithinAbs(0.999, 1e-3)); |
||||
|
||||
// Parabolic (e=1.0)
|
||||
OrbitalElements parabolic = {}; |
||||
parabolic.semi_latus_rectum = 1.0e11; |
||||
parabolic.eccentricity = 1.0; |
||||
parabolic.true_anomaly = 0.5; |
||||
parabolic.inclination = 0.0; |
||||
parabolic.longitude_of_ascending_node = 0.0; |
||||
parabolic.argument_of_periapsis = 0.0; |
||||
const OrbitalElements rec_parabolic = convert_and_recover(parabolic); |
||||
REQUIRE_THAT(rec_parabolic.eccentricity, WithinAbs(1.0, 1e-2)); |
||||
REQUIRE_THAT(rec_parabolic.semi_latus_rectum, WithinAbs(1.0e11, A_TOL)); |
||||
|
||||
// Hyperbolic (e=2.0)
|
||||
const OrbitalElements hyper = make_elements(-1.0e11, 2.0, 0.5, 0.0, 0.0, 0.0); |
||||
const OrbitalElements rec_hyper = convert_and_recover(hyper); |
||||
REQUIRE_THAT(rec_hyper.eccentricity, WithinAbs(2.0, 1e-3)); |
||||
REQUIRE_THAT(rec_hyper.semi_major_axis, WithinAbs(-1.0e11, A_TOL)); |
||||
|
||||
// Highly hyperbolic (e=10.0)
|
||||
const OrbitalElements high_hyper = make_elements(-1.0e10, 10.0, 0.8, 0.0, 0.0, 0.0); |
||||
const OrbitalElements rec_high_hyper = convert_and_recover(high_hyper); |
||||
REQUIRE_THAT(rec_high_hyper.eccentricity, WithinAbs(10.0, 1e-3)); |
||||
REQUIRE_THAT(rec_high_hyper.semi_major_axis, WithinAbs(-1.0e10, A_TOL)); |
||||
} |
||||
|
||||
SECTION("inclination: zero, polar, and retrograde") { |
||||
// Zero inclination (equatorial)
|
||||
const OrbitalElements eq = make_elements(1.0e11, 0.3, 0.5, 0.0, 0.0, 0.0); |
||||
const OrbitalElements rec_eq = convert_and_recover(eq); |
||||
REQUIRE_THAT(rec_eq.inclination, WithinAbs(0.0, ANG_TOL)); |
||||
REQUIRE_THAT(rec_eq.eccentricity, WithinAbs(0.3, E_TOL)); |
||||
|
||||
// 90-degree inclination (polar)
|
||||
const OrbitalElements polar = make_elements(1.0e11, 0.2, 0.6, M_PI / 2.0, 0.5, 0.3); |
||||
const OrbitalElements rec_polar = convert_and_recover(polar); |
||||
REQUIRE_THAT(rec_polar.inclination, WithinAbs(M_PI / 2.0, ANG_TOL_COARSE)); |
||||
REQUIRE_THAT(rec_polar.longitude_of_ascending_node, WithinAbs(0.5, ANG_TOL_COARSE)); |
||||
REQUIRE_THAT(rec_polar.argument_of_periapsis, WithinAbs(0.3, ANG_TOL_COARSE)); |
||||
|
||||
// 180-degree inclination (retrograde)
|
||||
const OrbitalElements retro = make_elements(1.0e11, 0.2, 0.6, M_PI, 0.5, 0.3); |
||||
const OrbitalElements rec_retro = convert_and_recover(retro); |
||||
REQUIRE_THAT(rec_retro.inclination, WithinAbs(M_PI, ANG_TOL_COARSE)); |
||||
} |
||||
|
||||
SECTION("true anomaly at key orbital positions") { |
||||
struct nu_test { |
||||
double nu; |
||||
double expected_nu; |
||||
const char* label; |
||||
}; |
||||
std::vector<nu_test> tests = { |
||||
{0.0, 0.0, "periapsis"}, |
||||
{M_PI, M_PI, "apoapsis"}, |
||||
{M_PI / 2.0, M_PI / 2.0, "quadrature +90"}, |
||||
{-M_PI / 2.0, 3.0 * M_PI / 2.0, "quadrature -90"}, |
||||
{3.0 * M_PI / 2.0, 3.0 * M_PI / 2.0, "quadrature +270"}, |
||||
{-3.0 * M_PI / 2.0, M_PI / 2.0, "quadrature -270"}, |
||||
}; |
||||
|
||||
for (const auto& t : tests) { |
||||
const OrbitalElements elements = make_elements(1.0e11, 0.5, t.nu, 0.0, 0.0, 0.0); |
||||
const OrbitalElements recovered = convert_and_recover(elements); |
||||
INFO("Test: " << t.label << " (input nu=" << t.nu << ")"); |
||||
REQUIRE_THAT(recovered.true_anomaly, WithinAbs(t.expected_nu, ANG_TOL)); |
||||
REQUIRE_THAT(recovered.eccentricity, WithinAbs(0.5, E_TOL)); |
||||
} |
||||
} |
||||
|
||||
SECTION("quadrature at various eccentricities") { |
||||
struct e_test { |
||||
double e; |
||||
double e_tol; |
||||
double nu_tol; |
||||
}; |
||||
std::vector<e_test> tests = { |
||||
{0.9, 1e-3, 1e-5}, |
||||
{0.1, 1e-5, 1e-6}, |
||||
}; |
||||
|
||||
for (const auto& t : tests) { |
||||
const OrbitalElements elements = make_elements(1.0e11, t.e, M_PI / 2.0, 0.0, 0.0, 0.0); |
||||
const OrbitalElements recovered = convert_and_recover(elements); |
||||
REQUIRE_THAT(recovered.eccentricity, WithinAbs(t.e, t.e_tol)); |
||||
REQUIRE_THAT(recovered.semi_major_axis, WithinAbs(1.0e11, A_TOL)); |
||||
REQUIRE_THAT(recovered.true_anomaly, WithinAbs(M_PI / 2.0, t.nu_tol)); |
||||
} |
||||
} |
||||
|
||||
SECTION("large true anomaly values") { |
||||
struct large_nu_test { |
||||
double nu; |
||||
double expected_nu; |
||||
double tol; |
||||
const char* label; |
||||
}; |
||||
std::vector<large_nu_test> tests = { |
||||
{5.0, 5.0, 1e-6, "nu=5.0"}, |
||||
{-5.0, 1.28318530717958623, 1e-6, "nu=-5.0"}, |
||||
{10.0, 10.0 - 2.0 * M_PI, 1e-5, "nu=10.0"}, |
||||
}; |
||||
|
||||
for (const auto& t : tests) { |
||||
const OrbitalElements elements = make_elements(1.0e11, 0.5, t.nu, 0.0, 0.0, 0.0); |
||||
const OrbitalElements recovered = convert_and_recover(elements); |
||||
INFO("Test: " << t.label); |
||||
REQUIRE_THAT(recovered.eccentricity, WithinAbs(0.5, E_TOL)); |
||||
REQUIRE_THAT(recovered.semi_major_axis, WithinAbs(1.0e11, A_TOL)); |
||||
REQUIRE_THAT(recovered.true_anomaly, WithinAbs(t.expected_nu, t.tol)); |
||||
} |
||||
} |
||||
|
||||
SECTION("3D orientation with quadrature point") { |
||||
const OrbitalElements elements = make_elements(1.0e11, 0.5, M_PI / 2.0, |
||||
M_PI / 3.0, M_PI / 4.0, M_PI / 6.0); |
||||
const OrbitalElements recovered = convert_and_recover(elements); |
||||
REQUIRE_THAT(recovered.eccentricity, WithinAbs(0.5, E_TOL)); |
||||
REQUIRE_THAT(recovered.semi_major_axis, WithinAbs(1.0e11, A_TOL)); |
||||
REQUIRE_THAT(recovered.true_anomaly, WithinAbs(M_PI / 2.0, 1e-5)); |
||||
REQUIRE_THAT(recovered.inclination, WithinAbs(M_PI / 3.0, ANG_TOL_COARSE)); |
||||
REQUIRE_THAT(recovered.longitude_of_ascending_node, WithinAbs(M_PI / 4.0, ANG_TOL_COARSE)); |
||||
REQUIRE_THAT(recovered.argument_of_periapsis, WithinAbs(M_PI / 6.0, ANG_TOL_COARSE)); |
||||
} |
||||
|
||||
SECTION("multiple true anomaly points in sequence") { |
||||
std::array<double, 5> true_anomalies = {0.0, M_PI / 4.0, M_PI / 2.0, |
||||
3.0 * M_PI / 4.0, M_PI}; |
||||
|
||||
for (double nu : true_anomalies) { |
||||
const OrbitalElements elements = make_elements(1.0e11, 0.5, nu, 0.0, 0.0, 0.0); |
||||
const OrbitalElements recovered = convert_and_recover(elements); |
||||
REQUIRE_THAT(recovered.eccentricity, WithinAbs(0.5, E_TOL)); |
||||
REQUIRE_THAT(recovered.semi_major_axis, WithinAbs(1.0e11, A_TOL)); |
||||
REQUIRE_THAT(recovered.true_anomaly, WithinAbs(nu, ANG_TOL)); |
||||
} |
||||
} |
||||
|
||||
SECTION("hyperbolic orbit at quadrature point") { |
||||
const OrbitalElements elements = make_elements(-1.0e11, 2.0, M_PI / 2.0, 0.0, 0.0, 0.0); |
||||
const OrbitalElements recovered = convert_and_recover(elements); |
||||
REQUIRE_THAT(recovered.eccentricity, WithinAbs(2.0, 1e-3)); |
||||
REQUIRE_THAT(recovered.semi_major_axis, WithinAbs(-1.0e11, A_TOL)); |
||||
REQUIRE_THAT(recovered.true_anomaly, WithinAbs(M_PI / 2.0, 1e-5)); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue