Files
MeanField_SERiF/tests/normalization/configuration.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

120 lines
7.2 KiB
C++

#include <cmath>
#include <limits>
#include <numbers>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "serif/solver/normalization/configuration.hpp"
namespace {
namespace normalization = serif::solver::normalization;
namespace dimensions = serif::dimensions;
using Gravity = dimensions::QuantityValue<dimensions::GravitationalConstant>;
using Catch::Matchers::WithinRel;
/** @brief Keep dimensional reference inputs visible in each numerical test. */
[[nodiscard]] auto reference_scheme(const double n, const double mass, const double density) {
return normalization::LaneEmdenApproximation{
.n = n,
.mass = dimensions::QuantityValue<dimensions::Mass>{mass},
.central_density = dimensions::QuantityValue<dimensions::MassDensity>{density}
};
}
}
TEST_CASE("Normalization configuration preserves explicit choices without numerical preparation", "[unit][normalization][configuration]") {
const auto defaults = normalization::make_normalization();
CHECK(defaults.scheme.n == 3.0);
CHECK(defaults.gravitational_constant.value() == 1.0);
// An invalid numerical input can still be described; preparation supplies the recoverable failure.
const auto options = normalization::make_normalization(normalization::NormalizationOptions{
.scheme = reference_scheme(-1.0, 7.0, 2.0),
.coordinates = normalization::Dimensionless{},
.gravitational_constant = Gravity{0.25}
});
CHECK(options.scheme.n == -1.0);
CHECK(options.scheme.mass.value() == 7.0);
CHECK(options.scheme.central_density.value() == 2.0);
CHECK(options.gravitational_constant.value() == 0.25);
CHECK_FALSE(normalization::prepare_reference_scales(options.scheme, options.gravitational_constant));
}
TEST_CASE("Lane Emden normalization reference radii match analytic polytropes", "[unit][normalization][configuration]") {
constexpr double mass = 7.0;
constexpr double central_density = 2.0;
constexpr double gravity = 0.25;
for (const double n : {0.0, 1.0}) {
const auto reference = normalization::prepare_reference_scales(reference_scheme(n, mass, central_density), Gravity{gravity});
REQUIRE(reference);
double radius_cubed = 3.0 * mass / (4.0 * std::numbers::pi * central_density);
if (n == 1.0) {
radius_cubed = std::numbers::pi * mass / (4.0 * central_density);
}
const double radius = std::cbrt(radius_cubed);
CHECK_THAT(reference->radius.value(), WithinRel(radius, 2.0e-8));
CHECK_THAT(reference->mass.value(), WithinRel(mass, 2.0e-14));
CHECK(reference->gravitational_constant.value() == gravity);
CHECK_THAT(reference->density.value(), WithinRel(mass / radius_cubed, 6.0e-8));
CHECK_THAT(reference->specific_energy.value(), WithinRel(gravity * mass / radius, 2.0e-8));
CHECK_THAT(reference->pressure.value(), WithinRel(gravity * mass * mass / std::pow(radius, 4), 8.0e-8));
CHECK_THAT(reference->angular_velocity.value(), WithinRel(std::sqrt(gravity * mass / radius_cubed), 4.0e-8));
}
}
TEST_CASE("Lane Emden reference preparation respects consistent physical-unit conversions", "[unit][normalization][configuration]") {
constexpr double mass_conversion = 1000.0;
constexpr double length_conversion = 100.0;
constexpr double time_conversion = 10000.0;
constexpr double original_mass = 2.0;
constexpr double original_density = 3.0;
constexpr double original_gravity = 0.8;
const auto original = normalization::prepare_reference_scales(reference_scheme(3.0, original_mass, original_density), Gravity{original_gravity});
const auto converted = normalization::prepare_reference_scales(reference_scheme(
3.0,
original_mass * mass_conversion,
original_density * mass_conversion / std::pow(length_conversion, 3)
), Gravity{original_gravity * std::pow(length_conversion, 3) / (mass_conversion * time_conversion * time_conversion)});
REQUIRE(original);
REQUIRE(converted);
CHECK_THAT(converted->radius.value(), WithinRel(original->radius.value() * length_conversion, 2.0e-13));
CHECK_THAT(converted->density.value(), WithinRel(original->density.value() * mass_conversion / std::pow(length_conversion, 3), 2.0e-13));
CHECK_THAT(converted->acceleration.value(), WithinRel(original->acceleration.value() * length_conversion / std::pow(time_conversion, 2), 2.0e-13));
CHECK_THAT(converted->specific_energy.value(), WithinRel(original->specific_energy.value() * std::pow(length_conversion / time_conversion, 2), 2.0e-13));
CHECK_THAT(converted->pressure.value(), WithinRel(original->pressure.value() * mass_conversion / (length_conversion * std::pow(time_conversion, 2)), 2.0e-13));
CHECK_THAT(converted->angular_velocity.value(), WithinRel(original->angular_velocity.value() / time_conversion, 2.0e-13));
CHECK_THAT(converted->angular_momentum.value(), WithinRel(original->angular_momentum.value() * mass_conversion * length_conversion * length_conversion / time_conversion, 2.0e-13));
CHECK_THAT(converted->force.value(), WithinRel(original->force.value() * mass_conversion * length_conversion / std::pow(time_conversion, 2), 2.0e-13));
}
TEST_CASE("Normalization reference failures stay explicit for unusable inputs and missing surfaces", "[unit][normalization][configuration]") {
for (const double invalid : {0.0, -1.0, std::numeric_limits<double>::quiet_NaN(), std::numeric_limits<double>::infinity()}) {
CHECK_FALSE(normalization::prepare_reference_scales(reference_scheme(1.0, invalid, 1.0), Gravity{1.0}));
CHECK_FALSE(normalization::prepare_reference_scales(reference_scheme(1.0, 1.0, invalid), Gravity{1.0}));
CHECK_FALSE(normalization::prepare_reference_scales(reference_scheme(1.0, 1.0, 1.0), Gravity{invalid}));
}
const auto infinite_polytrope = normalization::prepare_reference_scales(reference_scheme(5.0, 1.0, 1.0), Gravity{1.0});
REQUIRE_FALSE(infinite_polytrope);
CHECK_FALSE(infinite_polytrope.error().empty());
auto unresolved = reference_scheme(3.0, 1.0, 1.0);
unresolved.integration.maximum_radius = 1.0;
CHECK_FALSE(normalization::prepare_reference_scales(unresolved, Gravity{1.0}));
}
TEST_CASE("Explicit normalization scales share validation and require a consistent G", "[unit][normalization][configuration]") {
const auto reference = normalization::prepare_stellar_reference_scales(dimensions::QuantityValue<dimensions::Mass>{2.0}, dimensions::QuantityValue<dimensions::Length>{3.0}, Gravity{0.5});
REQUIRE(reference);
const auto options = normalization::make_normalization(normalization::NormalizationOptions{
.scheme = *reference,
.gravitational_constant = Gravity{0.5}
});
const auto prepared = normalization::prepare_reference_scales(options.scheme, options.gravitational_constant);
REQUIRE(prepared);
CHECK(*prepared == *reference);
CHECK_FALSE(normalization::prepare_reference_scales(options.scheme, Gravity{1.0}));
auto invalid = *reference;
invalid.radius = dimensions::QuantityValue<dimensions::Length>{0.0};
CHECK_FALSE(normalization::prepare_reference_scales(invalid, Gravity{0.5}));
}