test(tests): added test suite

The test suite is extensive. Note that LLMs were used to aid in the development of the test suite. All LLM generated code was manually reviewed.
This commit is contained in:
2026-11-09 13:53:17 -05:00
parent 74e55a6859
commit cb20faeb3d
202 changed files with 35505 additions and 953 deletions

View File

@@ -0,0 +1,155 @@
#include <cmath>
#include <limits>
#include <numbers>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include "serif/utils/laneemden/solution.hpp"
namespace {
namespace laneemden = serif::utils::laneemden;
/** @brief The three analytic regular-center Lane–Emden profiles and their derivatives. */
laneemden::Sample analytic(double n, double xi) {
if (n == 0.0) {
return {.xi = xi, .theta = 1.0 - xi * xi / 6.0, .derivative = -xi / 3.0};
}
if (n == 1.0) {
if (xi == 0.0) {
return {.xi = 0.0, .theta = 1.0, .derivative = 0.0};
}
return {.xi = xi, .theta = std::sin(xi) / xi, .derivative = (xi * std::cos(xi) - std::sin(xi)) / (xi * xi)};
}
const double denominator = 1.0 + xi * xi / 3.0;
return {.xi = xi, .theta = 1.0 / std::sqrt(denominator), .derivative = -xi / (3.0 * std::pow(denominator, 1.5))};
}
}
TEST_CASE("Lane–Emden matches all regular-center analytic solutions", "[laneemden][analytic]") {
for (double n : {0.0, 1.0, 5.0}) {
CAPTURE(n);
const auto solution = laneemden::solve(n, {.maximum_radius = 20.0});
REQUIRE(solution);
REQUIRE(solution->samples.front().xi == 0.0);
REQUIRE(solution->samples.front().theta == 1.0);
REQUIRE(solution->samples.front().derivative == 0.0);
const double endpoint = solution->samples.back().xi;
for (int index = 0; index <= 400; ++index) {
const double xi = endpoint * static_cast<double>(index) / 400.0;
const auto actual = laneemden::evaluate(*solution, xi);
REQUIRE(actual);
const laneemden::Sample exact = analytic(n, xi);
CHECK(actual->theta == Catch::Approx(exact.theta).margin(3e-10));
CHECK(actual->derivative == Catch::Approx(exact.derivative).margin(3e-9));
}
if (n < 5.0) {
REQUIRE(solution->surface);
const double exact_radius = n == 0.0 ? std::sqrt(6.0) : std::numbers::pi;
CHECK(solution->surface->xi == Catch::Approx(exact_radius).margin(2e-9));
CHECK(solution->surface->derivative == Catch::Approx(analytic(n, exact_radius).derivative).margin(2e-9));
CHECK(solution->samples.back().theta == 0.0);
} else {
CHECK_FALSE(solution->surface);
CHECK(endpoint == 20.0);
}
}
}
TEST_CASE("Lane–Emden advances the coupled system at fourth order", "[laneemden][convergence]") {
double previous_error = 0.0;
for (double step : {0.08, 0.04, 0.02, 0.01}) {
const auto solution = laneemden::solve(1.0, {.step = step, .maximum_radius = 2.0});
REQUIRE(solution);
const double error = std::abs(solution->samples.back().theta - analytic(1.0, 2.0).theta);
if (previous_error > 0.0) {
CHECK(previous_error / error > 14.0);
CHECK(previous_error / error < 18.0);
}
previous_error = error;
}
}
TEST_CASE("Lane–Emden finds stellar surfaces for fractional and centrally concentrated profiles", "[laneemden][surface]") {
struct Reference final {
double n;
double radius;
double mass;
double mass_tolerance;
};
// Standard first-zero values provide an independent endpoint check beyond the analytic indices.
for (const Reference reference : {
Reference{.n = 0.5, .radius = 2.752698054065, .mass = 3.78865118488, .mass_tolerance = 2e-8},
Reference{.n = 1.5, .radius = 3.653753736219, .mass = 2.714055120109, .mass_tolerance = 2e-9},
Reference{.n = 3.0, .radius = 6.896848619377, .mass = 2.018235950966, .mass_tolerance = 2e-9}}) {
CAPTURE(reference.n);
const auto solution = laneemden::solve(reference.n, {.step = 0.0005});
REQUIRE(solution);
REQUIRE(solution->surface);
const laneemden::Sample surface = *solution->surface;
CHECK(surface.xi == Catch::Approx(reference.radius).margin(2e-9));
CHECK(-surface.xi * surface.xi * surface.derivative == Catch::Approx(reference.mass).margin(reference.mass_tolerance));
for (std::size_t index = 1; index < solution->samples.size(); ++index) {
const laneemden::Sample &previous = solution->samples[index - 1];
const laneemden::Sample &current = solution->samples[index];
REQUIRE(current.xi > previous.xi);
REQUIRE(current.theta >= 0.0);
REQUIRE(current.theta < previous.theta);
REQUIRE(std::isfinite(current.derivative));
}
}
}
TEST_CASE("Fractional-index surface values converge without taking nonreal powers", "[laneemden][convergence]") {
double previous_error = 0.0;
for (double step : {0.04, 0.02, 0.01, 0.005}) {
const auto solution = laneemden::solve(0.5, {.step = step});
REQUIRE(solution);
REQUIRE(solution->surface);
const laneemden::Sample surface = *solution->surface;
const double error = std::abs(-surface.xi * surface.xi * surface.derivative - 3.78865118488);
if (previous_error > 0.0) {
CHECK(error < 0.45 * previous_error);
}
previous_error = error;
}
}
TEST_CASE("Lane–Emden distinguishes finite interval profiles from failures", "[laneemden]") {
for (double n : {1.0, 5.0, 6.0}) {
const auto solution = laneemden::solve(n, {.maximum_radius = 1.0});
REQUIRE(solution);
CHECK_FALSE(solution->surface);
CHECK(solution->samples.back().xi == 1.0);
CHECK(solution->samples.back().theta > 0.0);
}
const auto exhausted = laneemden::solve(3.0, {.maximum_steps = 2});
CHECK_FALSE(exhausted);
const auto tiny_interval = laneemden::solve(1.0, {.maximum_radius = 1e-6});
REQUIRE(tiny_interval);
CHECK(tiny_interval->samples.size() == 2);
CHECK(tiny_interval->samples.back().derivative == Catch::Approx(-1e-6 / 3.0).margin(1e-18));
}
TEST_CASE("Lane–Emden rejects invalid controls and evaluation radii", "[laneemden][rejection]") {
const double infinity = std::numeric_limits<double>::infinity();
const double nan = std::numeric_limits<double>::quiet_NaN();
for (double invalid : {-1.0, infinity, nan}) {
CHECK_FALSE(laneemden::solve(invalid));
}
for (double invalid : {0.0, -1.0, infinity, nan}) {
CHECK_FALSE(laneemden::solve(1.0, {.step = invalid}));
CHECK_FALSE(laneemden::solve(1.0, {.maximum_radius = invalid}));
CHECK_FALSE(laneemden::solve(1.0, {.surface_tolerance = invalid}));
}
CHECK_FALSE(laneemden::solve(1.0, {.maximum_steps = 0}));
CHECK_FALSE(laneemden::solve(1.0, {.surface_tolerance = 0.01}));
CHECK_FALSE(laneemden::solve(1.0, {.central_series_terms = 5}));
CHECK_FALSE(laneemden::solve(1.0, {.central_series_terms = 65}));
const auto solution = laneemden::solve(1.0);
REQUIRE(solution);
for (double invalid : {-1.0, infinity, nan, 4.0}) {
CHECK_FALSE(laneemden::evaluate(*solution, invalid));
}
CHECK_FALSE(laneemden::evaluate(laneemden::Solution{.n = 1.0, .samples = {}, .surface = {}}, 0.0));
}