Files

156 lines
7.0 KiB
C++
Raw Permalink Normal View History

#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));
}