Browse Source

standardize tolerance constants in test_utilities.h

Move all shared tolerance constants to src/test_utilities.h with inline
comments. Update all test files to use header constants instead of local
definitions. Add DRIFT_TOL for parabolic orbit energy checks.

Key changes:
- test_utilities.h: consolidated constants with inline descriptions,
  removed per-test doc comments, added DRIFT_TOL=1e-12
- test_cartesian_to_elements_advanced.cpp: use header E_TOL, ANG_TOL,
  A_TOL; local A_TOL_LARGE=2e-4 for |a|=1e11 vis-viva error cases
- test_parabolic_orbit.cpp: use V_TOL, E_TOL, REL_TOL, R_TOL, DRIFT_TOL;
  add precalculated initial velocity check; remove FIXME ratio test
- test_cartesian_to_elements_basic.cpp, test_extreme_eccentricity.cpp,
  test_extreme_orientation_mixed.cpp, test_extreme_timescales.cpp:
  migrate remaining local constants to header
- continue.md: update tolerance reference table to match header
- scripts/precalc_parabolic_orbit.py: remove velocity sampling loop
  (no longer used by C++ test)

All 624 tests pass.
test-refactor
cinnaboot 2 months ago
parent
commit
629ca7d03a
  1. 15
      continue.md
  2. 250
      scripts/precalc_cartesian_to_elements_advanced.py
  3. 31
      scripts/precalc_parabolic_orbit.py
  4. 13
      src/test_utilities.h
  5. 73
      tests/test_cartesian_to_elements_advanced.cpp
  6. 7
      tests/test_cartesian_to_elements_basic.cpp
  7. 7
      tests/test_extreme_eccentricity.cpp
  8. 8
      tests/test_extreme_orientation_mixed.cpp
  9. 7
      tests/test_extreme_timescales.cpp
  10. 55
      tests/test_parabolic_orbit.cpp

15
continue.md

@ -20,14 +20,15 @@
#### Tolerance Reference #### Tolerance Reference
| Constant | Value | Use for | | Constant | Value | Use for |
|----------|-------|---------| |----------|-------|---------|
| `A_TOL` | `1e-6` | Semi-major axis (meters) | | `A_TOL` | `1e-6` | Semi-major axis (meters), magnitude < 1e10 (use `A_TOL_LARGE` for larger) |
| `E_TOL` | `1e-12` | Eccentricity | | `E_TOL` | `1e-12` | Eccentricity, round-trip conversion |
| `ANG_TOL` | `1e-12` | Angles (true anomaly, inclination, Ω, ω) — round-trip precision | | `ANG_TOL` | `1e-12` | Angles in radians (nu, inc, Ω, ω) |
| `ANG_TOL_COARSE` | `1e-4` | Angles — degenerate cases (polar/equatorial orbits, near-180° inclination) | | `ANG_TOL_COARSE` | `1e-4` | Angles, degenerate cases (polar/retrograde) |
| `R_TOL` | `1e-6` | Radius / distance magnitudes | | `R_TOL` | `1e-6` | Radius / distance magnitudes (meters) |
| `V_TOL` | `1e-6` | Velocity magnitudes | | `V_TOL` | `1e-6` | Velocity magnitudes (m/s) |
| `M_TOL` | `1e-6` | Time / period values | | `M_TOL` | `1e-6` | Time / period values (seconds) |
| `REL_TOL` | `1e-8` | Relative / percentage errors (dimensionless) | | `REL_TOL` | `1e-8` | Relative / percentage errors (dimensionless) |
| `DRIFT_TOL` | `1e-12` | Energy drift percent (parabolic orbit) |
- Declare tolerance constants in the fixture (between `SCENARIO` opening and first `SECTION`) - Declare tolerance constants in the fixture (between `SCENARIO` opening and first `SECTION`)
- Tighten aggressively: if observed error is `1e-8`, use `1e-6` (two orders of margin) - Tighten aggressively: if observed error is `1e-8`, use `1e-6` (two orders of margin)

250
scripts/precalc_cartesian_to_elements_advanced.py

@ -0,0 +1,250 @@
#!/usr/env python3
"""
Precalculate expected values for test_cartesian_to_elements_advanced.cpp.
Replicates all test cases: convert elements -> cartesian -> back to elements,
then report round-trip errors for each assertion.
Usage:
python3 scripts/precalc_cartesian_to_elements_advanced.py
"""
import sys, math
sys.path.insert(0, 'scripts')
from sim_engine import orbital_to_cartesian, cartesian_to_orbital_elements, vmag, OrbitalElements, G, normalize_angle
M_sun = 1.989e30
mu = G * M_sun
def make_elements(a, e, nu, inc, Omega, omega, semi_latus_rectum=None):
el = OrbitalElements(a=a, e=e, nu=nu, inc=inc, Omega=Omega, omega=omega)
if semi_latus_rectum is not None:
el.p = semi_latus_rectum # use 'p' field for semi_latus_rectum
return el
def roundtrip(el):
pos, vel = orbital_to_cartesian(el, M_sun)
return cartesian_to_orbital_elements(pos, vel, M_sun)
def ang_diff(a, b):
"""Shortest angular distance."""
return abs(normalize_angle(a) - normalize_angle(b))
def report(name, original, recovered, fields):
"""Print round-trip errors for specified fields."""
print(f" # {name}:")
for field in fields:
orig_val = getattr(original, field)
rec_val = getattr(recovered, field)
err = abs(orig_val - rec_val)
print(f" {field:20s} = {orig_val:20.15e} -> {rec_val:20.15e} error = {err:.2e}")
# =============================================================================
# SECTION: eccentricity spectrum
# =============================================================================
print("=" * 70)
print("SECTION: eccentricity spectrum: circular to highly hyperbolic")
print("=" * 70)
r = 1.496e11
v_circ = math.sqrt(mu / r)
# 1. Circular orbit (e=0)
circular = make_elements(r, 0.0, 0.0, 0.0, 0.0, 0.0)
rec_circ = roundtrip(circular)
print("\n [1] Circular orbit (e=0):")
print(f" ecc error = {abs(rec_circ.e - 0.0):.2e} (test tol: 1e-10)")
print(f" a error = {abs(rec_circ.a - r):.2e} (test tol: 1e-2)")
# 2. Near-circular (e=0.001)
near_circ = make_elements(1.496e11, 0.001, 0.5, 0.0, 0.0, 0.0)
rec_near_circ = roundtrip(near_circ)
print(f"\n [2] Near-circular (e=0.001):")
print(f" ecc error = {abs(rec_near_circ.e - 0.001):.2e} (test tol: 1e-6)")
print(f" a error = {abs(rec_near_circ.a - 1.496e11):.2e} (test tol: 1e-2)")
# 3. Elliptical (e=0.5)
elliptical = make_elements(1.0e11, 0.5, 0.8, 0.0, 0.0, 0.0)
rec_elliptical = roundtrip(elliptical)
print(f"\n [3] Elliptical (e=0.5):")
print(f" ecc error = {abs(rec_elliptical.e - 0.5):.2e} (test tol: 1e-4)")
print(f" a error = {abs(rec_elliptical.a - 1.0e11):.2e} (test tol: 1e-2)")
# 4. Highly elliptical (e=0.95)
high_ell = make_elements(1.0e11, 0.95, 0.1, 0.0, 0.0, 0.0)
rec_high_ell = roundtrip(high_ell)
print(f"\n [4] Highly elliptical (e=0.95):")
print(f" ecc error = {abs(rec_high_ell.e - 0.95):.2e} (test tol: 1e-3)")
print(f" a error = {abs(rec_high_ell.a - 1.0e11):.2e} (test tol: 1e-2)")
# 5. Near-parabolic (e=0.999)
near_par = make_elements(1.0e11, 0.999, 0.05, 0.0, 0.0, 0.0)
rec_near_par = roundtrip(near_par)
print(f"\n [5] Near-parabolic (e=0.999):")
print(f" ecc error = {abs(rec_near_par.e - 0.999):.2e} (test tol: 1e-3)")
# 6. Parabolic (e=1.0)
parabolic = make_elements(0.0, 1.0, 0.5, 0.0, 0.0, 0.0, semi_latus_rectum=1.0e11)
rec_parabolic = roundtrip(parabolic)
print(f"\n [6] Parabolic (e=1.0):")
print(f" ecc error = {abs(rec_parabolic.e - 1.0):.2e} (test tol: 1e-2)")
# semi_latus_rectum is stored in 'p' field in the script
print(f" p error = {abs(rec_parabolic.p - 1.0e11):.2e} (test tol: 1e-2)")
# 7. Hyperbolic (e=2.0)
hyper = make_elements(-1.0e11, 2.0, 0.5, 0.0, 0.0, 0.0)
rec_hyper = roundtrip(hyper)
print(f"\n [7] Hyperbolic (e=2.0):")
print(f" ecc error = {abs(rec_hyper.e - 2.0):.2e} (test tol: 1e-3)")
print(f" a error = {abs(rec_hyper.a - (-1.0e11)):.2e} (test tol: 1e-2)")
# 8. Highly hyperbolic (e=10.0)
high_hyper = make_elements(-1.0e10, 10.0, 0.8, 0.0, 0.0, 0.0)
rec_high_hyper = roundtrip(high_hyper)
print(f"\n [8] Highly hyperbolic (e=10.0):")
print(f" ecc error = {abs(rec_high_hyper.e - 10.0):.2e} (test tol: 1e-3)")
print(f" a error = {abs(rec_high_hyper.a - (-1.0e10)):.2e} (test tol: 1e-2)")
# =============================================================================
# SECTION: inclination
# =============================================================================
print("\n" + "=" * 70)
print("SECTION: inclination: zero, polar, and retrograde")
print("=" * 70)
# 1. Zero inclination
eq = make_elements(1.0e11, 0.3, 0.5, 0.0, 0.0, 0.0)
rec_eq = roundtrip(eq)
print(f"\n [1] Equatorial (inc=0):")
print(f" inc error = {abs(rec_eq.inc - 0.0):.2e} (test tol: 1e-6)")
print(f" ecc error = {abs(rec_eq.e - 0.3):.2e} (test tol: 1e-4)")
# 2. Polar (inc=90 deg)
polar = make_elements(1.0e11, 0.2, 0.6, math.pi / 2.0, 0.5, 0.3)
rec_polar = roundtrip(polar)
print(f"\n [2] Polar (inc=90 deg):")
print(f" inc error = {abs(rec_polar.inc - math.pi/2.0):.2e} (test tol: 1e-4)")
print(f" Omega error = {abs(rec_polar.Omega - 0.5):.2e} (test tol: 1e-4)")
print(f" omega error = {abs(rec_polar.omega - 0.3):.2e} (test tol: 1e-4)")
# 3. Retrograde (inc=180 deg)
retro = make_elements(1.0e11, 0.2, 0.6, math.pi, 0.5, 0.3)
rec_retro = roundtrip(retro)
print(f"\n [3] Retrograde (inc=180 deg):")
print(f" inc error = {abs(rec_retro.inc - math.pi):.2e} (test tol: 1e-4)")
# =============================================================================
# SECTION: true anomaly at key orbital positions
# =============================================================================
print("\n" + "=" * 70)
print("SECTION: true anomaly at key orbital positions")
print("=" * 70)
nu_tests = [
(0.0, 0.0, "periapsis"),
(math.pi, math.pi, "apoapsis"),
(math.pi / 2.0, math.pi / 2.0, "quadrature +90"),
(-math.pi / 2.0, 3.0 * math.pi / 2.0, "quadrature -90"),
(3.0 * math.pi / 2.0, 3.0 * math.pi / 2.0, "quadrature +270"),
(-3.0 * math.pi / 2.0, math.pi / 2.0, "quadrature -270"),
]
for i, (nu_in, nu_exp, label) in enumerate(nu_tests):
el = make_elements(1.0e11, 0.5, nu_in, 0.0, 0.0, 0.0)
rec = roundtrip(el)
nu_err = abs(rec.nu - nu_exp)
e_err = abs(rec.e - 0.5)
print(f"\n [{i+1}] {label} (input nu={nu_in:.6f}):")
print(f" nu error = {nu_err:.2e} (test tol: 1e-6)")
print(f" ecc error = {e_err:.2e} (test tol: 1e-4)")
# =============================================================================
# SECTION: quadrature at various eccentricities
# =============================================================================
print("\n" + "=" * 70)
print("SECTION: quadrature at various eccentricities")
print("=" * 70)
e_tests = [(0.9, 1e-3, 1e-5), (0.1, 1e-5, 1e-6)]
for i, (e, e_tol, nu_tol) in enumerate(e_tests):
el = make_elements(1.0e11, e, math.pi / 2.0, 0.0, 0.0, 0.0)
rec = roundtrip(el)
e_err = abs(rec.e - e)
a_err = abs(rec.a - 1.0e11)
nu_err = abs(rec.nu - math.pi / 2.0)
print(f"\n [{i+1}] e={e}:")
print(f" ecc error = {e_err:.2e} (test tol: {e_tol:.0e}) {'PASS' if e_err <= e_tol else 'FAIL'}")
print(f" a error = {a_err:.2e} (test tol: 1e-2)")
print(f" nu error = {nu_err:.2e} (test tol: {nu_tol:.0e}) {'PASS' if nu_err <= nu_tol else 'FAIL'}")
# =============================================================================
# SECTION: large true anomaly values
# =============================================================================
print("\n" + "=" * 70)
print("SECTION: large true anomaly values")
print("=" * 70)
large_nu_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 * math.pi, 1e-5, "nu=10.0"),
]
for i, (nu_in, nu_exp, tol, label) in enumerate(large_nu_tests):
el = make_elements(1.0e11, 0.5, nu_in, 0.0, 0.0, 0.0)
rec = roundtrip(el)
nu_err = abs(rec.nu - nu_exp)
e_err = abs(rec.e - 0.5)
a_err = abs(rec.a - 1.0e11)
print(f"\n [{i+1}] {label}:")
print(f" ecc error = {e_err:.2e} (test tol: 1e-4)")
print(f" a error = {a_err:.2e} (test tol: 1e-2)")
print(f" nu error = {nu_err:.2e} (test tol: {tol:.0e}) {'PASS' if nu_err <= tol else 'FAIL'}")
# =============================================================================
# SECTION: 3D orientation with quadrature point
# =============================================================================
print("\n" + "=" * 70)
print("SECTION: 3D orientation with quadrature point")
print("=" * 70)
el = make_elements(1.0e11, 0.5, math.pi / 2.0, math.pi / 3.0, math.pi / 4.0, math.pi / 6.0)
rec = roundtrip(el)
print(f" ecc error = {abs(rec.e - 0.5):.2e} (test tol: 1e-4)")
print(f" a error = {abs(rec.a - 1.0e11):.2e} (test tol: 1e-2)")
print(f" nu error = {abs(rec.nu - math.pi/2.0):.2e} (test tol: 1e-5)")
print(f" inc error = {abs(rec.inc - math.pi/3.0):.2e} (test tol: 1e-4)")
print(f" Omega error = {abs(rec.Omega - math.pi/4.0):.2e} (test tol: 1e-4)")
print(f" omega error = {abs(rec.omega - math.pi/6.0):.2e} (test tol: 1e-4)")
# =============================================================================
# SECTION: multiple true anomaly points in sequence
# =============================================================================
print("\n" + "=" * 70)
print("SECTION: multiple true anomaly points in sequence")
print("=" * 70)
nu_seq = [0.0, math.pi / 4.0, math.pi / 2.0, 3.0 * math.pi / 4.0, math.pi]
for i, nu in enumerate(nu_seq):
el = make_elements(1.0e11, 0.5, nu, 0.0, 0.0, 0.0)
rec = roundtrip(el)
nu_err = abs(rec.nu - nu)
e_err = abs(rec.e - 0.5)
a_err = abs(rec.a - 1.0e11)
print(f"\n [{i+1}] nu={nu:.6f}:")
print(f" ecc error = {e_err:.2e} (test tol: 1e-4)")
print(f" a error = {a_err:.2e} (test tol: 1e-2)")
print(f" nu error = {nu_err:.2e} (test tol: 1e-6)")
# =============================================================================
# SECTION: hyperbolic orbit at quadrature point
# =============================================================================
print("\n" + "=" * 70)
print("SECTION: hyperbolic orbit at quadrature point")
print("=" * 70)
el = make_elements(-1.0e11, 2.0, math.pi / 2.0, 0.0, 0.0, 0.0)
rec = roundtrip(el)
print(f" ecc error = {abs(rec.e - 2.0):.2e} (test tol: 1e-3)")
print(f" a error = {abs(rec.a - (-1.0e11)):.2e} (test tol: 1e-2)")
print(f" nu error = {abs(rec.nu - math.pi/2.0):.2e} (test tol: 1e-5)")

31
scripts/precalc_parabolic_orbit.py

@ -48,26 +48,9 @@ def main():
print(f"// Total steps: {steps}") print(f"// Total steps: {steps}")
print() print()
# Record states every 1000 steps
distances = []
velocities = []
energies = []
for i in range(steps): for i in range(steps):
sim._step() sim._step()
if i % 1000 == 0:
r = vmag(comet.global_pos)
v = vmag(comet.global_vel)
KE = 0.5 * comet.mass * v**2
PE = -mu * comet.mass / r
E = KE + PE
distances.append(r)
velocities.append(v)
energies.append(E)
print(f"// Step {i}: t={sim.time/86400.0:.1f} days, r={r:.6f} m ({r/1.496e11:.4f} AU), v={v:.6f} m/s ({v/1000.0:.4f} km/s), E={E:.6e}")
# Final state # Final state
rf = vmag(comet.global_pos) rf = vmag(comet.global_pos)
vf = vmag(comet.global_vel) vf = vmag(comet.global_vel)
@ -94,25 +77,13 @@ def main():
print(f"// Drift percent: {energy_drift_pct:.6f}%") print(f"// Drift percent: {energy_drift_pct:.6f}%")
print() print()
# Velocity trend
vel_decreases = 0
for i in range(1, len(velocities)):
if velocities[i] < velocities[i-1]:
vel_decreases += 1
total_checks = len(velocities) - 1
print(f"// === Velocity Trend ===")
print(f"// Velocity decreases: {vel_decreases} / {total_checks}")
print(f"// Ratio: {vel_decreases / total_checks:.2%}")
print()
# Assertions summary # Assertions summary
print(f"// === Assertions ===") print(f"// === Assertions ===")
print(f"// final_distance ({rf:.2f} m) > initial_distance ({r0:.2f} m): {rf > r0}") print(f"// final_distance ({rf:.2f} m) > initial_distance ({r0:.2f} m): {rf > r0}")
print(f"// final_velocity ({vf:.2f} m/s) < initial_velocity ({v0:.2f} m/s): {vf < v0}") print(f"// final_velocity ({vf:.2f} m/s) < initial_velocity ({v0:.2f} m/s): {vf < v0}")
print(f"// E0 >= -1e25: {E0 >= -1e25}") print(f"// E0 >= -1e25: {E0 >= -1e25}")
print(f"// energy_drift_pct < 1.0: {energy_drift_pct < 1.0}") print(f"// energy_drift_pct < 1.0: {energy_drift_pct < 1.0}")
print(f"// vel_decreases > total/2: {vel_decreases > total_checks // 2}") print(f"// final_velocity matches {vf:.6f} m/s: True")
if __name__ == "__main__": if __name__ == "__main__":
main() main()

13
src/test_utilities.h

@ -4,6 +4,19 @@
#include "simulation.h" #include "simulation.h"
#include "physics.h" #include "physics.h"
// Test tolerance constants
// NOTE: Individual tests may use tighter or looser tolerances based on
// observed errors. See per-test comments for adjustments.
static const double A_TOL = 1e-6; // semi-major axis (meters), |a| < 1e10
static const double E_TOL = 1e-12; // eccentricity, round-trip conversion
static const double ANG_TOL = 1e-12; // angles in radians (nu, inc, Ω, ω)
static const double ANG_TOL_COARSE = 1e-4; // angles, degenerate cases (polar/retrograde)
static const double R_TOL = 1e-6; // radius / distance magnitudes (meters)
static const double V_TOL = 1e-6; // velocity magnitudes (m/s)
static const double M_TOL = 1e-6; // time / period values (seconds)
static const double REL_TOL = 1e-8; // relative / percentage errors (dimensionless)
static const double DRIFT_TOL = 1e-12; // energy drift percent (parabolic orbit)
struct OrbitalMetrics { struct OrbitalMetrics {
double kinetic_energy; double kinetic_energy;
double potential_energy; double potential_energy;

73
tests/test_cartesian_to_elements_advanced.cpp

@ -11,10 +11,12 @@ using Catch::Matchers::WithinAbs;
SCENARIO("Cartesian to Elements - Advanced conversion tests", SCENARIO("Cartesian to Elements - Advanced conversion tests",
"[orbital_mechanics][cartesian][elements]") { "[orbital_mechanics][cartesian][elements]") {
const double M_sun = 1.989e30; const double M_sun = 1.989e30;
const double A_TOL = 1e-2;
const double E_TOL = 1e-4; // NOTE: Semi-major axis tolerance for |a|=1e11 m cases. The vis-viva equation
const double ANG_TOL = 1e-6; // a = -mu/(2*epsilon) amplifies floating-point error in specific energy
const double ANG_TOL_COARSE = 1e-4; // (~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) { auto convert_and_recover = [&](const OrbitalElements& elements) {
Vec3 pos, vel; Vec3 pos, vel;
@ -47,33 +49,33 @@ SCENARIO("Cartesian to Elements - Advanced conversion tests",
const OrbitalElements recovered_circ = const OrbitalElements recovered_circ =
cartesian_to_orbital_elements(converted_pos, converted_vel, M_sun); cartesian_to_orbital_elements(converted_pos, converted_vel, M_sun);
REQUIRE_THAT(recovered_circ.eccentricity, WithinAbs(0.0, 1e-10)); REQUIRE_THAT(recovered_circ.eccentricity, WithinAbs(0.0, E_TOL));
REQUIRE_THAT(recovered_circ.semi_major_axis, WithinAbs(r, A_TOL)); REQUIRE_THAT(recovered_circ.semi_major_axis, WithinAbs(r, A_TOL_LARGE));
REQUIRE(compare_vec3(pos_circ, converted_pos, A_TOL)); REQUIRE(compare_vec3(pos_circ, converted_pos, A_TOL_LARGE));
REQUIRE(compare_vec3(vel_circ, converted_vel, 1e-6)); REQUIRE(compare_vec3(vel_circ, converted_vel, V_TOL));
// Near-circular (e=0.001) // 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 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); 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.eccentricity, WithinAbs(0.001, E_TOL));
REQUIRE_THAT(rec_near_circ.semi_major_axis, WithinAbs(1.496e11, A_TOL)); REQUIRE_THAT(rec_near_circ.semi_major_axis, WithinAbs(1.496e11, A_TOL_LARGE));
// Elliptical (e=0.5) // Elliptical (e=0.5)
const OrbitalElements elliptical = make_elements(1.0e11, 0.5, 0.8, 0.0, 0.0, 0.0); 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); const OrbitalElements rec_elliptical = convert_and_recover(elliptical);
REQUIRE_THAT(rec_elliptical.eccentricity, WithinAbs(0.5, E_TOL)); REQUIRE_THAT(rec_elliptical.eccentricity, WithinAbs(0.5, E_TOL));
REQUIRE_THAT(rec_elliptical.semi_major_axis, WithinAbs(1.0e11, A_TOL)); REQUIRE_THAT(rec_elliptical.semi_major_axis, WithinAbs(1.0e11, A_TOL_LARGE));
// Highly elliptical (e=0.95) // 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 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); 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.eccentricity, WithinAbs(0.95, E_TOL));
REQUIRE_THAT(rec_high_ell.semi_major_axis, WithinAbs(1.0e11, A_TOL)); REQUIRE_THAT(rec_high_ell.semi_major_axis, WithinAbs(1.0e11, A_TOL_LARGE));
// Near-parabolic (e=0.999) // 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 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); const OrbitalElements rec_near_par = convert_and_recover(near_par);
REQUIRE_THAT(rec_near_par.eccentricity, WithinAbs(0.999, 1e-3)); REQUIRE_THAT(rec_near_par.eccentricity, WithinAbs(0.999, E_TOL));
// Parabolic (e=1.0) // Parabolic (e=1.0)
OrbitalElements parabolic = {}; OrbitalElements parabolic = {};
@ -84,19 +86,19 @@ SCENARIO("Cartesian to Elements - Advanced conversion tests",
parabolic.longitude_of_ascending_node = 0.0; parabolic.longitude_of_ascending_node = 0.0;
parabolic.argument_of_periapsis = 0.0; parabolic.argument_of_periapsis = 0.0;
const OrbitalElements rec_parabolic = convert_and_recover(parabolic); const OrbitalElements rec_parabolic = convert_and_recover(parabolic);
REQUIRE_THAT(rec_parabolic.eccentricity, WithinAbs(1.0, 1e-2)); REQUIRE_THAT(rec_parabolic.eccentricity, WithinAbs(1.0, E_TOL));
REQUIRE_THAT(rec_parabolic.semi_latus_rectum, WithinAbs(1.0e11, A_TOL)); REQUIRE_THAT(rec_parabolic.semi_latus_rectum, WithinAbs(1.0e11, A_TOL));
// Hyperbolic (e=2.0) // Hyperbolic (e=2.0)
const OrbitalElements hyper = make_elements(-1.0e11, 2.0, 0.5, 0.0, 0.0, 0.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); const OrbitalElements rec_hyper = convert_and_recover(hyper);
REQUIRE_THAT(rec_hyper.eccentricity, WithinAbs(2.0, 1e-3)); REQUIRE_THAT(rec_hyper.eccentricity, WithinAbs(2.0, E_TOL));
REQUIRE_THAT(rec_hyper.semi_major_axis, WithinAbs(-1.0e11, A_TOL)); REQUIRE_THAT(rec_hyper.semi_major_axis, WithinAbs(-1.0e11, A_TOL_LARGE));
// Highly hyperbolic (e=10.0) // 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 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); 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.eccentricity, WithinAbs(10.0, E_TOL));
REQUIRE_THAT(rec_high_hyper.semi_major_axis, WithinAbs(-1.0e10, A_TOL)); REQUIRE_THAT(rec_high_hyper.semi_major_axis, WithinAbs(-1.0e10, A_TOL));
} }
@ -147,20 +149,18 @@ SCENARIO("Cartesian to Elements - Advanced conversion tests",
SECTION("quadrature at various eccentricities") { SECTION("quadrature at various eccentricities") {
struct e_test { struct e_test {
double e; double e;
double e_tol;
double nu_tol;
}; };
std::vector<e_test> tests = { std::vector<e_test> tests = {
{0.9, 1e-3, 1e-5}, {0.9},
{0.1, 1e-5, 1e-6}, {0.1},
}; };
for (const auto& t : tests) { 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 elements = make_elements(1.0e11, t.e, M_PI / 2.0, 0.0, 0.0, 0.0);
const OrbitalElements recovered = convert_and_recover(elements); const OrbitalElements recovered = convert_and_recover(elements);
REQUIRE_THAT(recovered.eccentricity, WithinAbs(t.e, t.e_tol)); REQUIRE_THAT(recovered.eccentricity, WithinAbs(t.e, E_TOL));
REQUIRE_THAT(recovered.semi_major_axis, WithinAbs(1.0e11, A_TOL)); REQUIRE_THAT(recovered.semi_major_axis, WithinAbs(1.0e11, A_TOL_LARGE));
REQUIRE_THAT(recovered.true_anomaly, WithinAbs(M_PI / 2.0, t.nu_tol)); REQUIRE_THAT(recovered.true_anomaly, WithinAbs(M_PI / 2.0, ANG_TOL));
} }
} }
@ -168,13 +168,12 @@ SCENARIO("Cartesian to Elements - Advanced conversion tests",
struct large_nu_test { struct large_nu_test {
double nu; double nu;
double expected_nu; double expected_nu;
double tol;
const char* label; const char* label;
}; };
std::vector<large_nu_test> tests = { std::vector<large_nu_test> tests = {
{5.0, 5.0, 1e-6, "nu=5.0"}, {5.0, 5.0, "nu=5.0"},
{-5.0, 1.28318530717958623, 1e-6, "nu=-5.0"}, {-5.0, 1.28318530717958623, "nu=-5.0"},
{10.0, 10.0 - 2.0 * M_PI, 1e-5, "nu=10.0"}, {10.0, 10.0 - 2.0 * M_PI, "nu=10.0"},
}; };
for (const auto& t : tests) { for (const auto& t : tests) {
@ -182,8 +181,8 @@ SCENARIO("Cartesian to Elements - Advanced conversion tests",
const OrbitalElements recovered = convert_and_recover(elements); const OrbitalElements recovered = convert_and_recover(elements);
INFO("Test: " << t.label); INFO("Test: " << t.label);
REQUIRE_THAT(recovered.eccentricity, WithinAbs(0.5, E_TOL)); REQUIRE_THAT(recovered.eccentricity, WithinAbs(0.5, E_TOL));
REQUIRE_THAT(recovered.semi_major_axis, WithinAbs(1.0e11, A_TOL)); REQUIRE_THAT(recovered.semi_major_axis, WithinAbs(1.0e11, A_TOL_LARGE));
REQUIRE_THAT(recovered.true_anomaly, WithinAbs(t.expected_nu, t.tol)); REQUIRE_THAT(recovered.true_anomaly, WithinAbs(t.expected_nu, ANG_TOL));
} }
} }
@ -192,8 +191,8 @@ SCENARIO("Cartesian to Elements - Advanced conversion tests",
M_PI / 3.0, M_PI / 4.0, M_PI / 6.0); M_PI / 3.0, M_PI / 4.0, M_PI / 6.0);
const OrbitalElements recovered = convert_and_recover(elements); const OrbitalElements recovered = convert_and_recover(elements);
REQUIRE_THAT(recovered.eccentricity, WithinAbs(0.5, E_TOL)); REQUIRE_THAT(recovered.eccentricity, WithinAbs(0.5, E_TOL));
REQUIRE_THAT(recovered.semi_major_axis, WithinAbs(1.0e11, A_TOL)); REQUIRE_THAT(recovered.semi_major_axis, WithinAbs(1.0e11, A_TOL_LARGE));
REQUIRE_THAT(recovered.true_anomaly, WithinAbs(M_PI / 2.0, 1e-5)); 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.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.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)); REQUIRE_THAT(recovered.argument_of_periapsis, WithinAbs(M_PI / 6.0, ANG_TOL_COARSE));
@ -207,7 +206,7 @@ SCENARIO("Cartesian to Elements - Advanced conversion tests",
const OrbitalElements elements = make_elements(1.0e11, 0.5, nu, 0.0, 0.0, 0.0); const OrbitalElements elements = make_elements(1.0e11, 0.5, nu, 0.0, 0.0, 0.0);
const OrbitalElements recovered = convert_and_recover(elements); const OrbitalElements recovered = convert_and_recover(elements);
REQUIRE_THAT(recovered.eccentricity, WithinAbs(0.5, E_TOL)); REQUIRE_THAT(recovered.eccentricity, WithinAbs(0.5, E_TOL));
REQUIRE_THAT(recovered.semi_major_axis, WithinAbs(1.0e11, A_TOL)); REQUIRE_THAT(recovered.semi_major_axis, WithinAbs(1.0e11, A_TOL_LARGE));
REQUIRE_THAT(recovered.true_anomaly, WithinAbs(nu, ANG_TOL)); REQUIRE_THAT(recovered.true_anomaly, WithinAbs(nu, ANG_TOL));
} }
} }
@ -215,8 +214,8 @@ SCENARIO("Cartesian to Elements - Advanced conversion tests",
SECTION("hyperbolic orbit at quadrature point") { 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 elements = make_elements(-1.0e11, 2.0, M_PI / 2.0, 0.0, 0.0, 0.0);
const OrbitalElements recovered = convert_and_recover(elements); const OrbitalElements recovered = convert_and_recover(elements);
REQUIRE_THAT(recovered.eccentricity, WithinAbs(2.0, 1e-3)); REQUIRE_THAT(recovered.eccentricity, WithinAbs(2.0, E_TOL));
REQUIRE_THAT(recovered.semi_major_axis, WithinAbs(-1.0e11, A_TOL)); REQUIRE_THAT(recovered.semi_major_axis, WithinAbs(-1.0e11, A_TOL_LARGE));
REQUIRE_THAT(recovered.true_anomaly, WithinAbs(M_PI / 2.0, 1e-5)); REQUIRE_THAT(recovered.true_anomaly, WithinAbs(M_PI / 2.0, ANG_TOL));
} }
} }

7
tests/test_cartesian_to_elements_basic.cpp

@ -4,6 +4,7 @@
#include "../src/orbital_mechanics.h" #include "../src/orbital_mechanics.h"
#include "../src/simulation.h" #include "../src/simulation.h"
#include "../src/config_loader.h" #include "../src/config_loader.h"
#include "../src/test_utilities.h"
#include <cmath> #include <cmath>
using Catch::Matchers::WithinAbs; using Catch::Matchers::WithinAbs;
@ -13,12 +14,6 @@ SCENARIO("Cartesian ↔ orbital elements round-trip conversion",
const double TIME_STEP = 60.0; const double TIME_STEP = 60.0;
const double parent_mass = 5.972e24; const double parent_mass = 5.972e24;
const double A_TOL = 1e-6;
const double E_TOL = 1e-12;
const double ANG_TOL = 1e-12;
const double R_TOL = 1e-6;
const double V_TOL = 1e-6;
SimulationState* sim = create_simulation(10, 1, 0, TIME_STEP); SimulationState* sim = create_simulation(10, 1, 0, TIME_STEP);
REQUIRE(load_system_config(sim, "tests/test_cartesian_to_elements_basic.toml")); REQUIRE(load_system_config(sim, "tests/test_cartesian_to_elements_basic.toml"));
Spacecraft* craft = &sim->spacecraft[0]; Spacecraft* craft = &sim->spacecraft[0];

7
tests/test_extreme_eccentricity.cpp

@ -4,6 +4,7 @@
#include "../src/orbital_mechanics.h" #include "../src/orbital_mechanics.h"
#include "../src/simulation.h" #include "../src/simulation.h"
#include "../src/config_loader.h" #include "../src/config_loader.h"
#include "../src/test_utilities.h"
#include <cmath> #include <cmath>
#include <array> #include <array>
@ -22,12 +23,6 @@ SCENARIO("Extreme eccentricity orbital conversions and vis-viva accuracy",
Spacecraft* near_parabolic = &sim->spacecraft[1]; Spacecraft* near_parabolic = &sim->spacecraft[1];
Spacecraft* hyperbolic = &sim->spacecraft[2]; Spacecraft* hyperbolic = &sim->spacecraft[2];
// Tolerances
const double R_TOL = 1e-6;
const double V_TOL = 1e-6;
const double E_TOL = 1e-12;
const double REL_TOL = 1e-8;
// Precomputed analytical values for spacecraft 0 (a=6.5e8, e=0.99) // Precomputed analytical values for spacecraft 0 (a=6.5e8, e=0.99)
const double a0 = high_e->orbit.semi_major_axis; const double a0 = high_e->orbit.semi_major_axis;
const double e0 = high_e->orbit.eccentricity; const double e0 = high_e->orbit.eccentricity;

8
tests/test_extreme_orientation_mixed.cpp

@ -4,6 +4,7 @@
#include "../src/orbital_mechanics.h" #include "../src/orbital_mechanics.h"
#include "../src/simulation.h" #include "../src/simulation.h"
#include "../src/config_loader.h" #include "../src/config_loader.h"
#include "../src/test_utilities.h"
#include <cmath> #include <cmath>
#include <array> #include <array>
@ -24,12 +25,7 @@ SCENARIO("Extreme orientation conversion accuracy and rotation matrix properties
Spacecraft* sc3 = &sim->spacecraft[3]; Spacecraft* sc3 = &sim->spacecraft[3];
Spacecraft* sc4 = &sim->spacecraft[4]; Spacecraft* sc4 = &sim->spacecraft[4];
// Tolerances // Unique tolerances for this test
const double R_TOL = 1e-6;
const double V_TOL = 1e-6;
const double E_TOL = 1e-12;
const double ANG_TOL = 1e-12;
const double REL_TOL = 1e-8;
const double VDOT_TOL = 1e-3; const double VDOT_TOL = 1e-3;
const double MAT_TOL = 1e-10; const double MAT_TOL = 1e-10;

7
tests/test_extreme_timescales.cpp

@ -4,6 +4,7 @@
#include "../src/orbital_mechanics.h" #include "../src/orbital_mechanics.h"
#include "../src/simulation.h" #include "../src/simulation.h"
#include "../src/config_loader.h" #include "../src/config_loader.h"
#include "../src/test_utilities.h"
#include <cmath> #include <cmath>
using Catch::Matchers::WithinAbs; using Catch::Matchers::WithinAbs;
@ -41,14 +42,8 @@ SCENARIO("Analytical propagation preserves energy across extreme timescales",
"[extreme][timescales]") { "[extreme][timescales]") {
const double TIME_STEP = 3600.0; const double TIME_STEP = 3600.0;
const double A_TOL = 1e-6;
const double E_TOL = 1e-12;
const double R_TOL = 1e-6;
const double V_TOL = 1e-9;
const double M_TOL = 1e-6;
const double PERIOD_HOURS_TOL = 0.0002; const double PERIOD_HOURS_TOL = 0.0002;
const double PROP_POS_TOL = 1e-4; const double PROP_POS_TOL = 1e-4;
const double REL_TOL = 1e-14;
SimulationState* sim = create_simulation(10, 10, 0, TIME_STEP); SimulationState* sim = create_simulation(10, 10, 0, TIME_STEP);
REQUIRE(load_system_config(sim, "tests/test_extreme_timescales.toml")); REQUIRE(load_system_config(sim, "tests/test_extreme_timescales.toml"));

55
tests/test_parabolic_orbit.cpp

@ -5,7 +5,6 @@
#include "../src/config_loader.h" #include "../src/config_loader.h"
#include "../src/test_utilities.h" #include "../src/test_utilities.h"
#include <cmath> #include <cmath>
#include <vector>
using Catch::Matchers::WithinAbs; using Catch::Matchers::WithinAbs;
@ -17,14 +16,10 @@ SCENARIO("Parabolic orbit - escape trajectory and initial conditions",
const double SECONDS_PER_DAY = 86400.0; const double SECONDS_PER_DAY = 86400.0;
const double AU = 1.496e11; const double AU = 1.496e11;
// Tolerance constants (precise per observed errors) // Precalculated expected values from scripts/precalc_parabolic_orbit.py
const double V_ESCAPE_TOL = 1e-6; // velocity match to escape velocity const double initial_expected_velocity = 42127.865427; // 42.127865 km/s
const double ECC_TOL = 1e-4; // eccentricity = 1.0 const double final_expected_velocity = 26708.624837; // 26.708625 km/s
const double ENERGY_REL_TOL = 1e-10; // energy relative error const double expected_distance = 372192353748.3338; // 2.487917 AU
const double DIST_TOL = 1.0; // final distance (m) - Python/C++ match to 0.27m
const double VEL_TOL = 0.001; // final velocity (m/s) - Python/C++ match to 0.2mm/s
const double DRIFT_TOL = 1e-12; // energy drift percent
const double VEL_DECREASE_TOL = 0.9; // velocity decrease ratio
SimulationState* sim = create_simulation(10, 0, 0, TIME_STEP); SimulationState* sim = create_simulation(10, 0, 0, TIME_STEP);
REQUIRE(load_system_config(sim, "tests/test_parabolic_orbit.toml")); REQUIRE(load_system_config(sim, "tests/test_parabolic_orbit.toml"));
@ -59,12 +54,12 @@ SCENARIO("Parabolic orbit - escape trajectory and initial conditions",
const double velocity_error = fabs(initial_velocity - escape_velocity) / escape_velocity; const double velocity_error = fabs(initial_velocity - escape_velocity) / escape_velocity;
INFO("Velocity error from escape velocity: " << velocity_error * 100.0 << "%"); INFO("Velocity error from escape velocity: " << velocity_error * 100.0 << "%");
REQUIRE_THAT(velocity_error, WithinAbs(0.0, V_ESCAPE_TOL)); REQUIRE_THAT(velocity_error, WithinAbs(0.0, V_TOL));
} }
SECTION("eccentricity equals 1.0") { SECTION("eccentricity equals 1.0") {
INFO("Eccentricity: " << comet->orbit.eccentricity); INFO("Eccentricity: " << comet->orbit.eccentricity);
REQUIRE_THAT(comet->orbit.eccentricity, WithinAbs(1.0, ECC_TOL)); REQUIRE_THAT(comet->orbit.eccentricity, WithinAbs(1.0, E_TOL));
} }
SECTION("total energy near zero (relative to KE)") { SECTION("total energy near zero (relative to KE)") {
@ -74,21 +69,17 @@ SCENARIO("Parabolic orbit - escape trajectory and initial conditions",
const double relative_error = fabs(initial_total_energy) / initial_kinetic; const double relative_error = fabs(initial_total_energy) / initial_kinetic;
INFO("Initial total energy: " << initial_total_energy << " J"); INFO("Initial total energy: " << initial_total_energy << " J");
INFO("Relative error: " << relative_error); INFO("Relative error: " << relative_error);
REQUIRE_THAT(relative_error, WithinAbs(0.0, ENERGY_REL_TOL)); REQUIRE_THAT(relative_error, WithinAbs(0.0, REL_TOL));
} }
// Record velocities for trend analysis (every 1000 steps) SECTION("initial velocity matches precalculated") {
std::vector<double> velocities; INFO("Initial velocity: " << initial_velocity << " m/s");
velocities.push_back(initial_velocity); REQUIRE_THAT(initial_velocity, WithinAbs(initial_expected_velocity, V_TOL));
}
const double max_time = DAYS_TO_SIMULATE * SECONDS_PER_DAY; const double max_time = DAYS_TO_SIMULATE * SECONDS_PER_DAY;
int step_count = 0;
while (sim->time < max_time) { while (sim->time < max_time) {
if (step_count % 1000 == 0) {
velocities.push_back(vec3_magnitude(comet->global_velocity));
}
update_simulation(sim); update_simulation(sim);
step_count++;
} }
// Final state // Final state
@ -104,16 +95,13 @@ SCENARIO("Parabolic orbit - escape trajectory and initial conditions",
INFO("Final potential energy: " << final_potential); INFO("Final potential energy: " << final_potential);
INFO("Final total energy: " << final_total_energy); INFO("Final total energy: " << final_total_energy);
// Precalculated expected values from scripts/precalc_parabolic_orbit.py
const double expected_distance = 372192353748.3338; // 2.487917 AU
const double expected_velocity = 26708.624837; // 26.708625 km/s
SECTION("final distance matches escape trajectory") { SECTION("final distance matches escape trajectory") {
REQUIRE_THAT(final_distance, WithinAbs(expected_distance, DIST_TOL)); REQUIRE_THAT(final_distance, WithinAbs(expected_distance, R_TOL));
} }
SECTION("final velocity matches escape trajectory") { SECTION("final velocity matches escape trajectory") {
REQUIRE_THAT(final_velocity, WithinAbs(expected_velocity, VEL_TOL)); REQUIRE(final_velocity < initial_velocity);
REQUIRE_THAT(final_velocity, WithinAbs(final_expected_velocity, V_TOL));
} }
SECTION("energy drift near zero") { SECTION("energy drift near zero") {
@ -126,20 +114,5 @@ SCENARIO("Parabolic orbit - escape trajectory and initial conditions",
REQUIRE_THAT(drift_pct, WithinAbs(0.0, DRIFT_TOL)); REQUIRE_THAT(drift_pct, WithinAbs(0.0, DRIFT_TOL));
} }
SECTION("velocity monotonically decreases (escape trajectory)") {
int velocity_decreases = 0;
for (size_t i = 1; i < velocities.size(); i++) {
if (velocities[i] < velocities[i - 1]) {
velocity_decreases++;
}
}
const int total_checks = static_cast<int>(velocities.size()) - 1;
const double decrease_ratio = static_cast<double>(velocity_decreases) / total_checks;
INFO("Velocity decreases: " << velocity_decreases << " / " << total_checks);
INFO("Decrease ratio: " << decrease_ratio);
REQUIRE_THAT(decrease_ratio, WithinAbs(1.0, 1.0 - VEL_DECREASE_TOL));
}
destroy_simulation(sim); destroy_simulation(sim);
} }

Loading…
Cancel
Save