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,245 @@
#include <cmath>
#include <limits>
#include "serif/equations/mass_normalization.hpp"
#include "serif/tests/equations/gravity_port.hpp"
namespace {
using namespace serif;
using namespace serif::tests::equations;
using namespace serif::tests::equations::gravity;
using Equation = serif::equations::MassNormalization;
using Prepared = VolumeEquationFixture<Equation, Schema>;
using Vector = ::mfem::Vector;
auto state(GravityFixture& f, const Vector& density) {
return backend::make_term_coefficients(backend::bind(f.density_binding, density));
}
auto make_operator(GravityFixture& f, double target) {
return Prepared{Equation{target}, f.constant_binding, f.displacement_binding, f.compactification, f.mapper, reference_policy()};
}
double residual(GravityFixture& f, Prepared& op, const Vector& density, const Vector& displacement) {
op.prepare(state(f, density), displacement, f.workspace);
Vector result;
op.residual(f.workspace, result);
REQUIRE(result.Size() == 1);
return result[0];
}
double action(GravityFixture& f, Prepared& op, const Vector& density, const Vector& displacement) {
Vector result;
op.apply_jacobian(state(f, density), displacement, f.workspace, result);
REQUIRE(result.Size() == 1);
return result[0];
}
// MeanField prepared_mass_normalization.cpp:66,98,132. Preserve the original profiles.
Vector density(GravityFixture& f, double phase, bool direction = false) {
return f.project(f.density_binding, [=](const Vector& x) {
if (direction) {
return 0.19 * std::sin(0.71 * x[0] + phase) - 0.13 * std::cos(0.89 * x[1] - phase) + 0.08 * x[2];
}
return 0.91 + 0.07 * std::sin(0.83 * x[0] + phase) + 0.05 * std::cos(0.61 * x[1] - phase) + 0.03 * x[2] * x[2];
});
}
Vector displacement(GravityFixture& f, double scale) {
return gravity_port::project_vector(f.displacement_binding, [=](const Vector& x, Vector& value) {
value.SetSize(3);
value[0] = scale * (0.07 * x[0] + 0.018 * x[1] * x[2]);
value[1] = scale * (-0.05 * x[1] + 0.013 * x[0] * x[2]);
value[2] = scale * (0.04 * x[2] - 0.011 * x[0] * x[1]);
});
}
double relative(double actual, double expected) {
return std::abs(actual - expected) / std::max(std::abs(expected), 100.0 * std::numeric_limits<double>::epsilon());
}
// Independent MFEM quadrature of rho det(A) and delta-rho det(A) + rho cof(A):delta-A.
double reference(GravityFixture& f, const Vector& rho, const Vector& u, const Vector* drho = nullptr, const Vector* du = nullptr) {
::mfem::GridFunction density(&f.density_space), displacement(&f.displacement_space);
::mfem::GridFunction delta_density(&f.density_space), delta_displacement(&f.displacement_space);
density = f.density_binding.scatter(rho);
displacement = f.displacement_binding.scatter(u);
if (drho) { delta_density = f.density_binding.scatter(*drho); }
if (du) { delta_displacement = f.displacement_binding.scatter(*du); }
double result = 0.0;
for (int element = 0; element < f.mesh.GetNE(); ++element) {
if (!f.density_binding.supports(element)) { continue; }
auto& transform = *f.mesh.GetElementTransformation(element);
const auto& rule = ::mfem::IntRules.Get(f.mesh.GetElementGeometry(element), reference_order);
for (int point = 0; point < rule.GetNPoints(); ++point) {
const auto& ip = rule.IntPoint(point);
transform.SetIntPoint(&ip);
::mfem::DenseMatrix A(3), dA(3), inverse(3);
displacement.GetVectorGradient(transform, A);
for (int axis = 0; axis < 3; ++axis) { A(axis, axis) += 1.0; }
double integrand = density.GetValue(transform, ip);
if (drho || du) {
integrand = drho ? delta_density.GetValue(transform, ip) : 0.0;
if (du) {
delta_displacement.GetVectorGradient(transform, dA);
::mfem::CalcInverse(A, inverse);
double trace = 0.0;
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) { trace += inverse(i, j) * dA(j, i); }
}
integrand += density.GetValue(transform, ip) * trace;
}
}
result += ip.weight * transform.Weight() * A.Det() * integrand;
}
}
return result;
}
void check_columns(GravityFixture& f) {
CAPTURE(f.mesh.GetNE(), f.mesh.GetElementGeometry(0), f.displacement_space.GetOrdering());
f.set_compactification();
const auto zero_rho = constant(f.density_binding.size(), 0.0), zero_u = constant(f.displacement_binding.size(), 0.0);
// Original density and geometry finite differences, with their original steps and tolerances.
for (int column = 0; column < 2; ++column) {
const auto rho = density(f, column == 0 ? 0.31 : 0.37);
const auto u = displacement(f, column == 0 ? 0.43 : 0.51);
const auto drho = column == 0 ? density(f, 0.67, true) : zero_rho;
const auto du = column == 0 ? zero_u : displacement(f, -0.79);
auto op = make_operator(f, column == 0 ? 1.23 : 1.11);
(void)residual(f, op, rho, u);
const double actual = action(f, op, drho, du);
const double step = column == 0 ? 1.0e-3 : 1.0e-6;
auto plus_rho = rho, minus_rho = rho, plus_u = u, minus_u = u;
plus_rho.Add(step, drho); minus_rho.Add(-step, drho);
plus_u.Add(step, du); minus_u.Add(-step, du);
const double difference = (residual(f, op, plus_rho, plus_u) - residual(f, op, minus_rho, minus_u)) / (2.0 * step);
CAPTURE(column, actual, difference);
CHECK(relative(actual, difference) < (column == 0 ? 3.0e-8 : 2.0e-7));
}
// Original complete action, independent reference, column sum, and supported local adapter routing.
const auto rho = density(f, 0.47), u = displacement(f, 0.57);
const auto drho = density(f, 0.71, true), du = displacement(f, -0.63);
auto op = make_operator(f, 1.19);
const auto initial = residual(f, op, rho, u);
CHECK(relative(initial, reference(f, rho, u) - 1.19) < 1.0e-12);
const double complete = action(f, op, drho, du);
CHECK(relative(complete, reference(f, rho, u, &drho, &du)) < 2.0e-11);
CHECK(relative(complete, action(f, op, drho, zero_u) + action(f, op, zero_rho, du)) < 2.0e-15);
backend::JacobianOperator adapter(op.cache());
CHECK(adapter.Height() == 1);
CHECK(adapter.Width() == drho.Size() + du.Size());
Vector joined(adapter.Width()), result;
std::copy_n(drho.GetData(), drho.Size(), joined.GetData());
std::copy_n(du.GetData(), du.Size(), joined.GetData() + drho.Size());
adapter.Mult(joined, result);
CHECK(result[0] == complete);
}
}
TEST_CASE("Mass normalization retains affine volume scaling and target subtraction", "[equations][mass_normalization][integration]") {
auto fixture = gravity_port::stellar_fixture();
auto& f = *fixture;
const auto rho = constant(f.density_binding.size(), 1.37), zero = constant(f.displacement_binding.size(), 0.0);
auto op = make_operator(f, 0.73);
const double undeformed = residual(f, op, rho, zero) + 0.73;
CHECK(relative(undeformed, reference(f, rho, zero)) < 1.0e-12);
CHECK(relative(residual(f, op, rho, zero), undeformed - 0.73) < 2.0e-15);
const double deformed = residual(f, op, rho, gravity_port::affine_displacement(f, 0.086)) + 0.73;
CHECK(relative(deformed / undeformed, std::pow(1.0 + 0.086, 3)) < 5.0e-7);
}
TEST_CASE("Mass normalization retains density geometry and complete action accuracy", "[equations][mass_normalization][integration]") {
for (const auto geometry : {::mfem::Element::TETRAHEDRON, ::mfem::Element::HEXAHEDRON}) {
for (const auto ordering : {::mfem::Ordering::byNODES, ::mfem::Ordering::byVDIM}) {
GravityFixture f(geometry, ordering);
check_columns(f);
}
}
auto fixture = gravity_port::stellar_fixture();
check_columns(*fixture);
}
TEST_CASE("Global mass preparations own their density state", "[equations][mass_normalization][unit]") {
GravityFixture f;
auto rho = density(f, 0.47);
const auto u = displacement(f, 0.57);
VolumeEquationFixture op{Equation{0.73}, f.constant_binding, f.displacement_binding, f.compactification, f.mapper, reference_policy()};
const double expected = reference(f, rho, u) - 0.73;
op.prepare(state(f, rho), u, f.workspace);
Vector result;
op.residual(f.workspace, result);
CHECK(relative(result[0], expected) < 2.0e-15);
rho *= 1.2;
op.residual(f.workspace, result);
CHECK(relative(result[0], expected) < 2.0e-15);
op.prepare(state(f, rho), u, f.workspace);
op.residual(f.workspace, result);
CHECK(relative(result[0], reference(f, rho, u) - 0.73) < 2.0e-15);
const auto drho = density(f, 0.71, true), du = displacement(f, -0.63);
op.apply_jacobian(state(f, drho), du, f.workspace, result);
CHECK(relative(result[0], reference(f, rho, u, &drho, &du)) < 2.0e-11);
}
TEST_CASE("Mass normalization rejects invalid targets density and geometry then recovers", "[equations][mass_normalization][unit]") {
for (double target : {0.0, -1.0, std::numeric_limits<double>::infinity(), std::numeric_limits<double>::quiet_NaN()}) {
CHECK_THROWS_AS(Equation{target}, std::invalid_argument);
}
GravityFixture f;
auto op = make_operator(f, 1.0);
auto rho = constant(f.density_binding.size(), 1.0);
auto u = constant(f.displacement_binding.size(), 0.0);
const double expected = residual(f, op, rho, u);
rho = std::numeric_limits<double>::quiet_NaN();
auto rejected = op.try_prepare(state(f, rho), u, f.workspace);
REQUIRE_FALSE(rejected.has_value());
CHECK(rejected.error().reason == backend::PreparationRejectionReason::NON_FINITE);
CHECK_FALSE(op.is_prepared());
CHECK_THROWS_AS(op.prepare(state(f, rho), u, f.workspace), std::domain_error);
rho = 1.0;
u = gravity_port::affine_displacement(f, -2.0);
rejected = op.try_prepare(state(f, rho), u, f.workspace);
REQUIRE_FALSE(rejected.has_value());
CHECK(rejected.error().reason == backend::PreparationRejectionReason::MAPPING);
u = 0.0;
CHECK(residual(f, op, rho, u) == expected);
}
// MeanField prepared_mass_normalization.cpp:282: finite coefficients can overflow during interpolation.
TEST_CASE("Mass normalization rejects nonfinite density interpolation and recovers", "[equations][mass_normalization][unit]") {
auto fixture = gravity_port::stellar_fixture();
auto& f = *fixture;
auto rho = constant(f.density_binding.size(), -0.5 * std::numeric_limits<double>::max());
const auto u = constant(f.displacement_binding.size(), 0.0);
auto op = make_operator(f, std::numeric_limits<double>::max());
const auto rejected = op.try_prepare(state(f, rho), u, f.workspace);
REQUIRE_FALSE(rejected.has_value());
CHECK(rejected.error().reason == backend::PreparationRejectionReason::NON_FINITE);
CHECK(rejected.error().element >= 0);
CHECK_FALSE(op.is_prepared());
CHECK_THROWS_AS(op.prepare(state(f, rho), u, f.workspace), std::domain_error);
rho = 1.0;
auto recovered = make_operator(f, 1.0);
REQUIRE(recovered.try_prepare(state(f, rho), u, f.workspace).has_value());
CHECK(recovered.is_prepared());
}
TEST_CASE("Global scalar bindings retain one coefficient and reject stale meshes", "[equations][mass_normalization][unit]") {
GravityFixture f;
auto& binding = f.constant_binding;
const auto coefficient = constant(1, 0.39);
CHECK(binding.size() == 1);
CHECK(binding.full_size() == 1);
CHECK(binding.gather(binding.scatter(coefficient))[0] == coefficient[0]);
CHECK_THROWS_AS(binding.space(), std::logic_error);
CHECK_THROWS_AS(binding.validate_values(constant(2, 0.0)), std::invalid_argument);
for (int element = 0; element < f.mesh.GetNE(); ++element) {
Vector local;
binding.element_values(element, coefficient, local);
REQUIRE(local.Size() == 1);
CHECK(local[0] == (binding.supports(element) ? coefficient[0] : 0.0));
}
f.mesh.GetElement(0)->SetAttribute(3);
CHECK_THROWS(binding.validate());
}