Files
MeanField_SERiF/tests/utils/laneemden/solution.cpp
Emily Boudreaux cb20faeb3d 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.
2026-11-09 13:53:17 -05:00

156 lines
7.0 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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));
}