Browse Source

feat: rewrite moon orbit config as TOML 1.0 dotted keys and refactor tests

- Rewrite precalc_moon_orbits.py to output orbit params as dotted keys
  (orbit.semi_major_axis = ...) instead of nested tables, conforming
  to TOML 1.0 spec for Python compatibility
- Add print_cpp_expected_values() helper to script output
- Add new test_moon_orbits.cpp replacing old_tests version:
  * MoonData struct now carries int parent_index instead of pointer
  * Eliminate opaque pointer arithmetic for parent index comparison
  * Collect failures in vector during sim loops, assert once at end
    (reduces assertions from 763,230 to 43 per test)
- Add test_moon_orbits.toml generated from precalc script
test-refactor
cinnaboot 2 months ago
parent
commit
d892aa8c41
  1. 57
      scripts/precalc_moon_orbits.py
  2. 273
      tests/test_moon_orbits.cpp
  3. 184
      tests/test_moon_orbits.toml

57
scripts/precalc_moon_orbits.py

@ -225,7 +225,9 @@ def print_toml():
print(f"radius = {SUN_RADIUS}")
print("parent_index = -1")
print('color = { r = 1.0, g = 1.0, b = 0.0 }')
print("orbit = { semi_major_axis = 0.0, eccentricity = 0.0, true_anomaly = 0.0 }")
print("orbit.semi_major_axis = 0.0")
print("orbit.eccentricity = 0.0")
print("orbit.true_anomaly = 0.0")
print()
# Planets
@ -242,14 +244,12 @@ def print_toml():
print(f'radius = {p["radius"]}')
print(f'parent_index = {p["parent"]}')
print('color = { r = 0.5, g = 0.5, b = 0.5 }')
print("orbit = {")
print(f" semi_major_axis = {a_m:.6e},")
print(f" eccentricity = {p['e']},")
print(f" inclination = {inc:.15f},")
print(f" longitude_of_ascending_node = {Omega:.15f},")
print(f" argument_of_periapsis = {omega:.15f},")
print(f" true_anomaly = {nu:.15f}")
print("}")
print(f"orbit.semi_major_axis = {a_m:.6e}")
print(f"orbit.eccentricity = {p['e']}")
print(f"orbit.inclination = {inc:.15f}")
print(f"orbit.longitude_of_ascending_node = {Omega:.15f}")
print(f"orbit.argument_of_periapsis = {omega:.15f}")
print(f"orbit.true_anomaly = {nu:.15f}")
print()
# Moons
@ -267,14 +267,12 @@ def print_toml():
parent_idx = {"Earth": 2, "Jupiter": 4, "Saturn": 5}[m["parent"]]
print(f'parent_index = {parent_idx}')
print('color = { r = 0.7, g = 0.7, b = 0.7 }')
print("orbit = {")
print(f" semi_major_axis = {a_m:.6e},")
print(f" eccentricity = {m['e']},")
print(f" inclination = {inc:.15f},")
print(f" longitude_of_ascending_node = {Omega:.15f},")
print(f" argument_of_periapsis = {omega:.15f},")
print(f" true_anomaly = {nu:.15f}")
print("}")
print(f"orbit.semi_major_axis = {a_m:.6e}")
print(f"orbit.eccentricity = {m['e']}")
print(f"orbit.inclination = {inc:.15f}")
print(f"orbit.longitude_of_ascending_node = {Omega:.15f}")
print(f"orbit.argument_of_periapsis = {omega:.15f}")
print(f"orbit.true_anomaly = {nu:.15f}")
print()
@ -327,6 +325,25 @@ def print_summary():
print(f'{m["name"]:10s}: r={r:.3f} m, nu={nu*180/math.pi:.2f}deg')
def print_cpp_expected_values():
"""Print C++-style comments with expected values for embedding in test."""
print("# === Expected period values (from precalc) ===")
print("# Format: name -> period_seconds, tolerance_seconds")
parent_masses = {
"Earth": 5.97e24, "Jupiter": 1.898e27, "Saturn": 5.683e26,
}
for m in MOONS:
a_m = m["a_km"] * 1000.0
pm = parent_masses[m["parent"]]
mu = G * pm
T = 2.0 * math.pi * math.sqrt(a_m**3 / mu)
T_days = T / 86400.0
# Tolerance: ~0.5% of period (simulation drift over multiple orbits)
tol = 0.005 * T
print(f'// {m["name"]:10s} T={T:>14.2f}s tol={tol:>8.1f}s ({T_days:.3f}d)')
print()
if __name__ == "__main__":
print_summary()
print()
@ -335,3 +352,9 @@ if __name__ == "__main__":
print("=" * 60)
print()
print_toml()
print()
print("=" * 60)
print("# CPP EXPECTED VALUES (copy into test fixture)")
print("=" * 60)
print()
print_cpp_expected_values()

273
tests/test_moon_orbits.cpp

@ -0,0 +1,273 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "../src/physics.h"
#include "../src/simulation.h"
#include "../src/config_loader.h"
#include "../src/test_utilities.h"
#include <cmath>
#include <vector>
#include <string>
using Catch::Matchers::WithinAbs;
// === Helper: run sim loop, collect failures, assert once at end ===
static std::string fmt_time(double t) {
char buf[32];
snprintf(buf, sizeof(buf), "t=%.0fs", t);
return std::string(buf);
}
static std::string fmt_drift(double d) {
char buf[32];
snprintf(buf, sizeof(buf), "drift=%.4f", d);
return std::string(buf);
}
// === Per-moon period tolerances (0.5% of analytical period, from precalc) ===
static const double MOON_PERIOD_TOL = 11861.4; // Moon
static const double IO_PERIOD_TOL = 764.6; // Io
static const double EUROPA_PERIOD_TOL = 1534.5; // Europa
static const double GANYMEDE_PERIOD_TOL = 3091.1; // Ganymede
static const double CALLISTO_PERIOD_TOL = 7210.6; // Callisto
static const double TITAN_PERIOD_TOL = 6889.9; // Titan
// === Drift tolerance for distance checks (10% bound) ===
static const double DRIFT_REL_TOL = 0.1;
SCENARIO("Multi-body moon orbital stability and period measurements",
"[moon][earth][jupiter][saturn][galilean][titan]"
"[stability][period][integration][inclined][geometry]") {
// === Fixture ===
const double TIME_STEP = 60.0;
const double SECONDS_PER_DAY = 86400.0;
SimulationState* sim = create_simulation(20, 0, 0, TIME_STEP);
REQUIRE(load_system_config(sim, "tests/test_moon_orbits.toml"));
CelestialBody* earth = &sim->bodies[2];
CelestialBody* moon = &sim->bodies[8];
struct MoonData {
int index;
const char* name;
double expected_seconds;
double max_days;
double min_time_days;
double period_tol;
int parent_index;
};
const MoonData galilean[] = {
{9, "Io", 152928.62, 5.0, 1.0, IO_PERIOD_TOL, 4},
{10, "Europa", 306909.10, 10.0, 2.0, EUROPA_PERIOD_TOL, 4},
{11, "Ganymede", 618227.12, 15.0, 5.0, GANYMEDE_PERIOD_TOL, 4},
{12, "Callisto", 1442117.30, 25.0, 10.0, CALLISTO_PERIOD_TOL, 4},
};
const MoonData all_moons[] = {
{8, "Moon", 2372274.33, 35.0, 5.0, MOON_PERIOD_TOL, 2},
{9, "Io", 152928.62, 5.0, 1.0, IO_PERIOD_TOL, 4},
{10, "Europa", 306909.10, 10.0, 2.0, EUROPA_PERIOD_TOL, 4},
{11, "Ganymede", 618227.12, 15.0, 5.0, GANYMEDE_PERIOD_TOL, 4},
{12, "Callisto", 1442117.30, 25.0, 10.0, CALLISTO_PERIOD_TOL, 4},
{13, "Titan", 1377976.07, 25.0, 10.0, TITAN_PERIOD_TOL, 5},
};
// === Helper lambdas ===
// Run sim loop without assertions; collect failures; assert once at end.
// This tests every timestep but produces only 1 assertion per call.
auto check_parent_stability = [&](CelestialBody* body, int expected_parent,
double max_days) {
double max_time = max_days * SECONDS_PER_DAY;
std::vector<std::string> failures;
while (sim->time < max_time) {
update_simulation(sim);
if (body->parent_index != expected_parent) {
failures.push_back(fmt_time(sim->time) + ": " +
std::string(body->name) +
" parent=" + std::to_string(body->parent_index) +
" (expected " + std::to_string(expected_parent) + ")");
}
}
REQUIRE(failures.empty());
};
auto measure_period = [&](int body_index, int parent_index,
double max_days, double min_time_days) {
sim->time = 0.0;
OrbitTracker* tracker = create_orbit_tracker_with_min_time(
body_index, min_time_days * SECONDS_PER_DAY);
double max_time = max_days * SECONDS_PER_DAY;
while (sim->time < max_time && !tracker->orbit_completed) {
update_simulation(sim);
update_orbit_tracker(tracker, &sim->bodies[body_index],
&sim->bodies[parent_index], sim->time);
}
double period = tracker->orbit_completed
? tracker->time_at_completion : -1.0;
destroy_orbit_tracker(tracker);
return period;
};
auto check_distance_drift = [&](CelestialBody* body, int parent_index,
double max_days) {
CelestialBody* parent = &sim->bodies[parent_index];
Vec3 initial_rel = vec3_sub(body->global_position, parent->global_position);
double initial_r = vec3_magnitude(initial_rel);
double max_time = max_days * SECONDS_PER_DAY;
std::vector<std::string> failures;
while (sim->time < max_time) {
update_simulation(sim);
Vec3 rel = vec3_sub(body->global_position, parent->global_position);
double r = vec3_magnitude(rel);
double drift = std::fabs(r - initial_r) / initial_r;
if (drift > DRIFT_REL_TOL) {
failures.push_back(fmt_time(sim->time) + ": " +
std::string(body->name) + " " +
fmt_drift(drift));
}
}
REQUIRE(failures.empty());
};
auto wait_for_all_orbits = [&](int count) {
OrbitTracker* trackers[6] = {};
for (int i = 0; i < count; i++) {
trackers[i] = create_orbit_tracker_with_min_time(
all_moons[i].index, all_moons[i].min_time_days * SECONDS_PER_DAY);
}
double max_time = 60.0 * SECONDS_PER_DAY;
int completed_count = 0;
while (sim->time < max_time && completed_count < count) {
update_simulation(sim);
for (int i = 0; i < count; i++) {
if (!trackers[i]->orbit_completed) {
update_orbit_tracker(trackers[i], &sim->bodies[all_moons[i].index],
&sim->bodies[all_moons[i].parent_index], sim->time);
if (trackers[i]->orbit_completed) {
completed_count++;
double period = trackers[i]->time_at_completion / SECONDS_PER_DAY;
INFO(all_moons[i].name << " completed orbit at "
<< period << " days");
}
}
}
}
for (int i = 0; i < count; i++) {
REQUIRE(trackers[i]->orbit_completed);
destroy_orbit_tracker(trackers[i]);
}
};
// === Sections ===
SECTION("Moon maintains Earth as parent throughout simulation") {
check_parent_stability(moon, 2, 35.0);
}
SECTION("Moon distance from Earth stays within 10% of initial") {
check_distance_drift(moon, 2, 35.0);
}
SECTION("Moon completes orbit in ~27.3 days") {
double period = measure_period(8, 2, 35.0, 5.0);
INFO("Measured Moon period: " << period / SECONDS_PER_DAY
<< " days (expected: ~27.3)");
REQUIRE_THAT(period, WithinAbs(2372274.33, MOON_PERIOD_TOL));
}
SECTION("Galilean moons maintain Jupiter as parent") {
for (const auto& m : galilean) {
check_parent_stability(&sim->bodies[m.index], 4, 25.0);
}
}
SECTION("Galilean moons complete orbits in expected periods") {
for (const auto& m : galilean) {
double period = measure_period(m.index, m.parent_index,
m.max_days, m.min_time_days);
INFO(m.name << " period: " << period / SECONDS_PER_DAY
<< " days (expected: " << m.expected_seconds / SECONDS_PER_DAY
<< ")");
REQUIRE_THAT(period, WithinAbs(m.expected_seconds, m.period_tol));
}
}
SECTION("Galilean moons stay within 10% of initial distance") {
for (const auto& m : galilean) {
check_distance_drift(&sim->bodies[m.index], m.parent_index, 25.0);
}
}
SECTION("Titan maintains Saturn as parent") {
check_parent_stability(&sim->bodies[13], 5, 25.0);
}
SECTION("Titan completes orbit in ~15.95 days") {
double period = measure_period(13, 5, 25.0, 10.0);
INFO("Measured Titan period: " << period / SECONDS_PER_DAY
<< " days (expected: ~15.95)");
REQUIRE_THAT(period, WithinAbs(1377976.07, TITAN_PERIOD_TOL));
}
SECTION("Titan distance from Saturn stays within 10%") {
check_distance_drift(&sim->bodies[13], 5, 25.0);
}
SECTION("All moons maintain correct parents over 60-day simulation") {
double max_time = 60.0 * SECONDS_PER_DAY;
std::vector<std::string> failures;
while (sim->time < max_time) {
update_simulation(sim);
for (int i = 0; i < 6; i++) {
int idx = all_moons[i].index;
int expected = all_moons[i].parent_index;
if (sim->bodies[idx].parent_index != expected) {
failures.push_back(fmt_time(sim->time) + ": " +
std::string(all_moons[i].name) +
" parent=" +
std::to_string(sim->bodies[idx].parent_index) +
" (expected " + std::to_string(expected) + ")");
}
}
}
REQUIRE(failures.empty());
}
SECTION("All moons complete at least one orbit") {
wait_for_all_orbits(6);
}
SECTION("Moon inclined orbit produces non-zero z-coordinate") {
double max_time = 10.0 * SECONDS_PER_DAY;
while (sim->time < max_time) {
update_simulation(sim);
}
Vec3 rel = vec3_sub(moon->global_position, earth->global_position);
double z = rel.z;
INFO("Moon z-coordinate relative to Earth: " << z << " m");
INFO("Moon orbital inclination: "
<< (moon->orbit.inclination * 180.0 / M_PI) << " degrees");
REQUIRE_THAT(z, !WithinAbs(0.0, 10000000.0));
}
SECTION("Moon orbital elements loaded correctly from config") {
REQUIRE_THAT(moon->orbit.eccentricity, WithinAbs(0.055, E_TOL));
REQUIRE_THAT(moon->orbit.inclination,
WithinAbs(5.16 * M_PI / 180.0, ANG_TOL));
REQUIRE_THAT(moon->orbit.longitude_of_ascending_node,
WithinAbs(125.08 * M_PI / 180.0, ANG_TOL));
REQUIRE_THAT(moon->orbit.argument_of_periapsis,
WithinAbs(318.15 * M_PI / 180.0, ANG_TOL));
}
destroy_simulation(sim);
}

184
tests/test_moon_orbits.toml

@ -0,0 +1,184 @@
# Moon Orbits Test Configuration
# Auto-generated by scripts/precalc_moon_orbits.py
# Data source: docs/planetary_data.md (JPL planetary facts)
# Mean anomaly converted to true anomaly via Kepler's equation
[[bodies]]
name = "Sun"
mass = 1.989e+30
radius = 696000000.0
parent_index = -1
color = { r = 1.0, g = 1.0, b = 0.0 }
orbit.semi_major_axis = 0.0
orbit.eccentricity = 0.0
orbit.true_anomaly = 0.0
[[bodies]]
name = "Venus"
mass = 4.87e+24
radius = 6052000.0
parent_index = 0
color = { r = 0.5, g = 0.5, b = 0.5 }
orbit.semi_major_axis = 1.081608e+11
orbit.eccentricity = 0.007
orbit.inclination = 0.059166661642608
orbit.longitude_of_ascending_node = 1.338318470429252
orbit.argument_of_periapsis = 0.958534825195286
orbit.true_anomaly = 0.890141231198106
[[bodies]]
name = "Earth"
mass = 5.97e+24
radius = 6378000.0
parent_index = 0
color = { r = 0.5, g = 0.5, b = 0.5 }
orbit.semi_major_axis = 1.496000e+11
orbit.eccentricity = 0.017
orbit.inclination = 0.000000000000000
orbit.longitude_of_ascending_node = 0.000000000000000
orbit.argument_of_periapsis = 1.796641932002963
orbit.true_anomaly = 6.238578647164619
[[bodies]]
name = "Mars"
mass = 6.42e+23
radius = 3396000.0
parent_index = 0
color = { r = 0.5, g = 0.5, b = 0.5 }
orbit.semi_major_axis = 2.279904e+11
orbit.eccentricity = 0.093
orbit.inclination = 0.032288591161895
orbit.longitude_of_ascending_node = 0.864985177288390
orbit.argument_of_periapsis = 5.000368306963754
orbit.true_anomaly = 0.407676817724149
[[bodies]]
name = "Jupiter"
mass = 1.898e+27
radius = 71492000.0
parent_index = 0
color = { r = 0.5, g = 0.5, b = 0.5 }
orbit.semi_major_axis = 7.783688e+11
orbit.eccentricity = 0.049
orbit.inclination = 0.022863813201126
orbit.longitude_of_ascending_node = 1.753532299478703
orbit.argument_of_periapsis = 4.786565473594449
orbit.true_anomaly = 0.378299755400337
[[bodies]]
name = "Saturn"
mass = 5.683e+26
radius = 60268000.0
parent_index = 0
color = { r = 0.5, g = 0.5, b = 0.5 }
orbit.semi_major_axis = 1.426735e+12
orbit.eccentricity = 0.057
orbit.inclination = 0.043458698374659
orbit.longitude_of_ascending_node = 1.983741227816755
orbit.argument_of_periapsis = 5.915618966709580
orbit.true_anomaly = 5.457583789473037
[[bodies]]
name = "Uranus"
mass = 8.68e+25
radius = 25559000.0
parent_index = 0
color = { r = 0.5, g = 0.5, b = 0.5 }
orbit.semi_major_axis = 2.870824e+12
orbit.eccentricity = 0.046
orbit.inclination = 0.013439035240356
orbit.longitude_of_ascending_node = 1.291892712326203
orbit.argument_of_periapsis = 1.691922176883303
orbit.true_anomaly = 2.537061863932694
[[bodies]]
name = "Neptune"
mass = 1.02e+26
radius = 24764000.0
parent_index = 0
color = { r = 0.5, g = 0.5, b = 0.5 }
orbit.semi_major_axis = 4.498472e+12
orbit.eccentricity = 0.01
orbit.inclination = 0.030892327760300
orbit.longitude_of_ascending_node = 2.299994888278127
orbit.argument_of_periapsis = 4.767890450598109
orbit.true_anomaly = 4.516812758860357
[[bodies]]
name = "Moon"
mass = 7.35e+22
radius = 1738000.0
parent_index = 2
color = { r = 0.7, g = 0.7, b = 0.7 }
orbit.semi_major_axis = 3.844000e+08
orbit.eccentricity = 0.055
orbit.inclination = 0.090058989402907
orbit.longitude_of_ascending_node = 2.183057828394507
orbit.argument_of_periapsis = 5.552765015219959
orbit.true_anomaly = 2.434643529152418
[[bodies]]
name = "Io"
mass = 8.93e+23
radius = 1822000.0
parent_index = 4
color = { r = 0.7, g = 0.7, b = 0.7 }
orbit.semi_major_axis = 4.218000e+08
orbit.eccentricity = 0.004
orbit.inclination = 0.000000000000000
orbit.longitude_of_ascending_node = 0.000000000000000
orbit.argument_of_periapsis = 0.856956662729216
orbit.true_anomaly = 5.771386752330690
[[bodies]]
name = "Europa"
mass = 4.8e+23
radius = 1561000.0
parent_index = 4
color = { r = 0.7, g = 0.7, b = 0.7 }
orbit.semi_major_axis = 6.711000e+08
orbit.eccentricity = 0.009
orbit.inclination = 0.008726646259972
orbit.longitude_of_ascending_node = 3.211405823669566
orbit.argument_of_periapsis = 0.785398163397448
orbit.true_anomaly = 6.023780086902404
[[bodies]]
name = "Ganymede"
mass = 1.48e+24
radius = 2631000.0
parent_index = 4
color = { r = 0.7, g = 0.7, b = 0.7 }
orbit.semi_major_axis = 1.070400e+09
orbit.eccentricity = 0.001
orbit.inclination = 0.003490658503989
orbit.longitude_of_ascending_node = 1.021017612416683
orbit.argument_of_periapsis = 3.460987906704756
orbit.true_anomaly = 5.667675367373862
[[bodies]]
name = "Callisto"
mass = 1.08e+24
radius = 2410000.0
parent_index = 4
color = { r = 0.7, g = 0.7, b = 0.7 }
orbit.semi_major_axis = 1.882700e+09
orbit.eccentricity = 0.007
orbit.inclination = 0.005235987755983
orbit.longitude_of_ascending_node = 5.394812717914473
orbit.argument_of_periapsis = 0.764454212373516
orbit.true_anomaly = 1.539408451124606
[[bodies]]
name = "Titan"
mass = 1.35e+24
radius = 2575000.0
parent_index = 5
color = { r = 0.7, g = 0.7, b = 0.7 }
orbit.semi_major_axis = 1.221900e+09
orbit.eccentricity = 0.029
orbit.inclination = 0.005235987755983
orbit.longitude_of_ascending_node = 1.371828792067543
orbit.argument_of_periapsis = 1.366592804311560
orbit.true_anomaly = 0.216397080390910
Loading…
Cancel
Save