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.
153 lines
9.6 KiB
C++
153 lines
9.6 KiB
C++
#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}));
|
||
}
|