Files
MeanField_SERiF/tests/utils/laneemden/scaling.cpp

153 lines
9.6 KiB
C++
Raw Normal View History

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