You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
221 lines
10 KiB
221 lines
10 KiB
#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; |
|
|
|
// NOTE: Semi-major axis tolerance for |a|=1e11 m cases. The vis-viva equation |
|
// a = -mu/(2*epsilon) amplifies floating-point error in specific energy |
|
// (~1e-15 rel) to absolute errors of ~1e-4 m at this scale. Header A_TOL=1e-6 |
|
// would fail; 2e-4 provides comfortable margin over observed ~1.4e-4 m error. |
|
const double A_TOL_LARGE = 2e-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, E_TOL)); |
|
REQUIRE_THAT(recovered_circ.semi_major_axis, WithinAbs(r, A_TOL_LARGE)); |
|
REQUIRE(compare_vec3(pos_circ, converted_pos, A_TOL_LARGE)); |
|
REQUIRE(compare_vec3(vel_circ, converted_vel, V_TOL)); |
|
|
|
// 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, E_TOL)); |
|
REQUIRE_THAT(rec_near_circ.semi_major_axis, WithinAbs(1.496e11, A_TOL_LARGE)); |
|
|
|
// 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_LARGE)); |
|
|
|
// 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, E_TOL)); |
|
REQUIRE_THAT(rec_high_ell.semi_major_axis, WithinAbs(1.0e11, A_TOL_LARGE)); |
|
|
|
// 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, E_TOL)); |
|
|
|
// 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, E_TOL)); |
|
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, E_TOL)); |
|
REQUIRE_THAT(rec_hyper.semi_major_axis, WithinAbs(-1.0e11, A_TOL_LARGE)); |
|
|
|
// 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, E_TOL)); |
|
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; |
|
}; |
|
std::vector<e_test> tests = { |
|
{0.9}, |
|
{0.1}, |
|
}; |
|
|
|
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, E_TOL)); |
|
REQUIRE_THAT(recovered.semi_major_axis, WithinAbs(1.0e11, A_TOL_LARGE)); |
|
REQUIRE_THAT(recovered.true_anomaly, WithinAbs(M_PI / 2.0, ANG_TOL)); |
|
} |
|
} |
|
|
|
SECTION("large true anomaly values") { |
|
struct large_nu_test { |
|
double nu; |
|
double expected_nu; |
|
const char* label; |
|
}; |
|
std::vector<large_nu_test> tests = { |
|
{5.0, 5.0, "nu=5.0"}, |
|
{-5.0, 1.28318530717958623, "nu=-5.0"}, |
|
{10.0, 10.0 - 2.0 * M_PI, "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_LARGE)); |
|
REQUIRE_THAT(recovered.true_anomaly, WithinAbs(t.expected_nu, ANG_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_LARGE)); |
|
REQUIRE_THAT(recovered.true_anomaly, WithinAbs(M_PI / 2.0, ANG_TOL)); |
|
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_LARGE)); |
|
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, E_TOL)); |
|
REQUIRE_THAT(recovered.semi_major_axis, WithinAbs(-1.0e11, A_TOL_LARGE)); |
|
REQUIRE_THAT(recovered.true_anomaly, WithinAbs(M_PI / 2.0, ANG_TOL)); |
|
} |
|
}
|
|
|