Files
MeanField_SERiF/tests/utils/laneemden/scaling.cpp
Emily Boudreaux cb20faeb3d 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.
2026-11-09 13:53:17 -05:00

153 lines
9.6 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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}));
}