#include #include #include #include #include #include "serif/utils/laneemden/laneemden.hpp" namespace { namespace laneemden = serif::utils::laneemden; namespace dimensions = serif::dimensions; using Mass = dimensions::QuantityValue; using Density = dimensions::QuantityValue; using Length = dimensions::QuantityValue; using Enthalpy = dimensions::QuantityValue; using GravityConstant = dimensions::QuantityValue; } 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::infinity(), std::numeric_limits::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::infinity(), std::numeric_limits::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})); }