#include #include #include #include #include #include "serif/solver/normalization/configuration.hpp" namespace { namespace normalization = serif::solver::normalization; namespace dimensions = serif::dimensions; using Gravity = dimensions::QuantityValue; 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{mass}, .central_density = dimensions::QuantityValue{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::quiet_NaN(), std::numeric_limits::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{2.0}, dimensions::QuantityValue{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{0.0}; CHECK_FALSE(normalization::prepare_reference_scales(invalid, Gravity{0.5})); }