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.
This commit is contained in:
2026-11-09 13:53:17 -05:00
parent 74e55a6859
commit cb20faeb3d
202 changed files with 35505 additions and 953 deletions

View File

@@ -0,0 +1,119 @@
#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}));
}