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,152 @@
#include <cmath>
#include <limits>
#include <numbers>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include "serif/utils/laneemden/laneemden.hpp"
namespace {
namespace laneemden = serif::utils::laneemden;
namespace dimensions = serif::dimensions;
using Mass = dimensions::QuantityValue<dimensions::Mass>;
using Density = dimensions::QuantityValue<dimensions::MassDensity>;
using Length = dimensions::QuantityValue<dimensions::Length>;
using Enthalpy = dimensions::QuantityValue<dimensions::SpecificEnthalpy>;
using GravityConstant = dimensions::QuantityValue<dimensions::GravitationalConstant>;
}
TEST_CASE("Lane–Emden scaling prescriptions describe the same physical polytrope", "[laneemden][scaling]") {
for (double n : {0.5, 1.0, 1.5, 3.0}) {
const auto solution = laneemden::solve(n);
REQUIRE(solution);
const auto mass_scales = laneemden::scale_from_mass(*solution, Mass{2.0}, Density{3.0}, GravityConstant{0.7});
REQUIRE(mass_scales);
CHECK(mass_scales->mass.value() == Catch::Approx(2.0).epsilon(1e-14));
REQUIRE(mass_scales->polytropic_constant);
const auto enthalpy_scales = laneemden::scale_from_enthalpy(*solution, mass_scales->central_enthalpy, Density{3.0}, GravityConstant{0.7});
const auto eos_scales = laneemden::scale_from_polytropic_constant(*solution, *mass_scales->polytropic_constant, Density{3.0}, GravityConstant{0.7});
REQUIRE(enthalpy_scales);
REQUIRE(eos_scales);
CHECK(enthalpy_scales->radius.value() == Catch::Approx(mass_scales->radius.value()).epsilon(1e-14));
CHECK(eos_scales->radius.value() == Catch::Approx(mass_scales->radius.value()).epsilon(1e-14));
CHECK(eos_scales->mass.value() == Catch::Approx(2.0).epsilon(1e-14));
for (double fraction : {0.0, 0.1, 0.5, 0.9}) {
const auto fields = laneemden::evaluate_physical(*solution, *mass_scales, Length{fraction * mass_scales->radius.value()});
REQUIRE(fields);
CHECK(fields->pressure.value() == Catch::Approx(*mass_scales->polytropic_constant * std::pow(fields->density.value(), 1.0 + 1.0 / n)).epsilon(2e-13));
CHECK(fields->enthalpy.value() == Catch::Approx((n + 1.0) * fields->pressure.value() / fields->density.value()).epsilon(2e-13));
CHECK(fields->potential.value() + fields->enthalpy.value() == Catch::Approx(-0.7 * 2.0 / mass_scales->radius.value()).epsilon(2e-13));
}
}
}
TEST_CASE("The n equals three EOS mass is independent of central density", "[laneemden][scaling]") {
const auto solution = laneemden::solve(3.0);
REQUIRE(solution);
const auto first = laneemden::scale_from_polytropic_constant(*solution, 2.0, Density{1.0}, GravityConstant{0.7});
const auto second = laneemden::scale_from_polytropic_constant(*solution, 2.0, Density{8.0}, GravityConstant{0.7});
REQUIRE(first);
REQUIRE(second);
CHECK(second->mass.value() == Catch::Approx(first->mass.value()).epsilon(1e-14));
CHECK(second->radius.value() == Catch::Approx(0.5 * first->radius.value()).epsilon(1e-14));
CHECK(second->polytropic_constant == first->polytropic_constant);
}
TEST_CASE("Physical incompressible polytropes recover uniform sphere fields", "[laneemden][scaling][analytic]") {
const auto solution = laneemden::solve(0.0);
REQUIRE(solution);
const auto scales = laneemden::scale_from_mass(*solution, Mass{2.0}, Density{3.0}, GravityConstant{0.7});
REQUIRE(scales);
CHECK_FALSE(scales->polytropic_constant);
const double surface_radius = std::cbrt(3.0 * 2.0 / (4.0 * std::numbers::pi * 3.0));
CHECK(scales->radius.value() == Catch::Approx(surface_radius).epsilon(2e-11));
for (double fraction : {0.0, 0.01, 0.2, 0.6, 0.99}) {
const double radius = fraction * scales->radius.value();
const auto fields = laneemden::evaluate_physical(*solution, *scales, Length{radius});
REQUIRE(fields);
CHECK(fields->density.value() == 3.0);
CHECK(fields->enclosed_mass.value() == Catch::Approx(2.0 * fraction * fraction * fraction).margin(1e-10));
CHECK(fields->gravity.value() == Catch::Approx(4.0 * std::numbers::pi * 0.7 * radius).margin(1e-10));
const double enthalpy = 2.0 * std::numbers::pi * 0.7 * (surface_radius * surface_radius - radius * radius);
CHECK(fields->enthalpy.value() == Catch::Approx(enthalpy).margin(1e-10));
CHECK(fields->pressure.value() == Catch::Approx(3.0 * enthalpy).margin(3e-10));
const double potential = -0.7 * 2.0 * (3.0 - fraction * fraction) / (2.0 * surface_radius);
CHECK(fields->potential.value() == Catch::Approx(potential).margin(1e-10));
}
}
TEST_CASE("Physical profiles match the vacuum exterior continuously", "[laneemden][scaling]") {
const auto solution = laneemden::solve(1.5);
REQUIRE(solution);
const auto scales = laneemden::scale_from_mass(*solution, Mass{2.0}, Density{3.0}, GravityConstant{0.7});
REQUIRE(scales);
for (double fraction : {1.0, 1.5, 100.0}) {
const double radius = fraction * scales->radius.value();
const auto fields = laneemden::evaluate_physical(*solution, *scales, Length{radius});
REQUIRE(fields);
CHECK(fields->density.value() == 0.0);
CHECK(fields->pressure.value() == 0.0);
CHECK(fields->enthalpy.value() == 0.0);
CHECK(fields->enclosed_mass.value() == Catch::Approx(2.0));
CHECK(fields->potential.value() == Catch::Approx(-1.4 / radius));
CHECK(fields->gravity.value() == Catch::Approx(1.4 / (radius * radius)));
}
const auto inside = laneemden::evaluate_physical(*solution, *scales, Length{scales->radius.value() * (1.0 - 1e-8)});
const auto surface = laneemden::evaluate_physical(*solution, *scales, scales->radius);
REQUIRE(inside);
REQUIRE(surface);
CHECK(inside->potential.value() == Catch::Approx(surface->potential.value()).epsilon(3e-8));
CHECK(inside->gravity.value() == Catch::Approx(surface->gravity.value()).epsilon(3e-8));
CHECK(inside->enclosed_mass.value() == Catch::Approx(surface->enclosed_mass.value()).epsilon(3e-8));
}
TEST_CASE("Physical scaling is covariant between metre kilogram and centimetre gram units", "[laneemden][scaling][units]") {
const auto solution = laneemden::solve(3.0);
REQUIRE(solution);
const auto metres = laneemden::scale_from_mass(*solution, Mass{2e30}, Density{1e5}, GravityConstant{6.6743e-11});
const auto centimetres = laneemden::scale_from_mass(*solution, Mass{2e33}, Density{1e2}, GravityConstant{6.6743e-8});
REQUIRE(metres);
REQUIRE(centimetres);
CHECK(centimetres->radius.value() == Catch::Approx(100.0 * metres->radius.value()).epsilon(1e-14));
CHECK(centimetres->central_enthalpy.value() == Catch::Approx(1e4 * metres->central_enthalpy.value()).epsilon(1e-14));
for (double fraction : {0.0, 0.3, 0.9, 1.0, 3.0}) {
const auto first = laneemden::evaluate_physical(*solution, *metres, metres->radius * fraction);
const auto second = laneemden::evaluate_physical(*solution, *centimetres, centimetres->radius * fraction);
REQUIRE(first);
REQUIRE(second);
CHECK(second->density.value() == Catch::Approx(1e-3 * first->density.value()).margin(1e-12));
CHECK(second->pressure.value() == Catch::Approx(10.0 * first->pressure.value()).epsilon(1e-13));
CHECK(second->enthalpy.value() == Catch::Approx(1e4 * first->enthalpy.value()).epsilon(1e-13));
CHECK(second->potential.value() == Catch::Approx(1e4 * first->potential.value()).epsilon(1e-13));
CHECK(second->enclosed_mass.value() == Catch::Approx(1e3 * first->enclosed_mass.value()).epsilon(1e-13));
CHECK(second->gravity.value() == Catch::Approx(100.0 * first->gravity.value()).epsilon(1e-13));
}
}
TEST_CASE("Physical scaling rejects invalid data and finite interval profiles without surfaces", "[laneemden][scaling][rejection]") {
const auto solution = laneemden::solve(1.0);
const auto infinite = laneemden::solve(5.0, {.maximum_radius = 2.0});
REQUIRE(solution);
REQUIRE(infinite);
CHECK_FALSE(laneemden::scale_from_mass(*infinite, Mass{1.0}, Density{1.0}, GravityConstant{1.0}));
CHECK_FALSE(laneemden::scale_from_enthalpy(*infinite, Enthalpy{1.0}, Density{1.0}, GravityConstant{1.0}));
for (double invalid : {0.0, -1.0, std::numeric_limits<double>::infinity(), std::numeric_limits<double>::quiet_NaN()}) {
CHECK_FALSE(laneemden::scale_from_mass(*solution, Mass{invalid}, Density{1.0}, GravityConstant{1.0}));
CHECK_FALSE(laneemden::scale_from_mass(*solution, Mass{1.0}, Density{invalid}, GravityConstant{1.0}));
CHECK_FALSE(laneemden::scale_from_mass(*solution, Mass{1.0}, Density{1.0}, GravityConstant{invalid}));
CHECK_FALSE(laneemden::scale_from_enthalpy(*solution, Enthalpy{invalid}, Density{1.0}, GravityConstant{1.0}));
CHECK_FALSE(laneemden::scale_from_polytropic_constant(*solution, invalid, Density{1.0}, GravityConstant{1.0}));
}
const auto incompressible = laneemden::solve(0.0);
REQUIRE(incompressible);
CHECK_FALSE(laneemden::scale_from_polytropic_constant(*incompressible, 1.0, Density{1.0}, GravityConstant{1.0}));
const auto scales = laneemden::scale_from_mass(*solution, Mass{1.0}, Density{1.0}, GravityConstant{1.0});
REQUIRE(scales);
for (double invalid : {-1.0, std::numeric_limits<double>::infinity(), std::numeric_limits<double>::quiet_NaN()}) {
CHECK_FALSE(laneemden::evaluate_physical(*solution, *scales, Length{invalid}));
}
CHECK_FALSE(laneemden::evaluate_physical(*incompressible, *scales, Length{0.0}));
CHECK_FALSE(laneemden::scale_from_enthalpy(*solution, Enthalpy{1e308}, Density{1e-300}, GravityConstant{1e-300}));
}