Files
MeanField_SERiF/tests/equations/state_validation.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

253 lines
13 KiB
C++

#include <algorithm>
#include <limits>
#include <vector>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include "serif/tests/equations/fixture.hpp"
#include "serif/backend/mfem/discretization/element_coordinates.hpp"
#include "serif/tests/test_tags.hpp"
namespace serif::tests::equations {
TEST_CASE("Scalar Operand Evaluation Rejects Incompatible MFEM Elements", tags::unit & tags::equations) {
Fixture f;
::mfem::ND_FECollection collection(1, 3);
::mfem::FiniteElementSpace space(&f.mesh, &collection);
const auto& element = *space.GetFE(0);
const auto& rule = ::mfem::IntRules.Get(element.GetGeomType(), 2);
backend::BasisEvaluation basis;
backend::TensorBasisCache cache;
backend::BasisWorkspace workspace;
auto& transformation = *f.mesh.GetElementTransformation(0);
using Operand = discretization::forms::Operand<blocks::density::mass>;
// Direct sampling must enforce the scalar representation even without a TermBinding.
CHECK_THROWS_AS(
backend::OperandEvaluator<Operand>::sample_basis(element, rule, basis, cache, backend::ContractionMode::automatic, transformation, workspace),
std::invalid_argument
);
}
TEST_CASE("Equation State Guards Remain Active For Invalid Sizes And Nonfinite Values", tags::unit & tags::equations) {
Fixture f;
auto rho = f.density(), h = f.enthalpy(), u = f.displacement(0.0);
PreparedClosure prepared(
Closure{eos::models::Polytrope{3.0, 1.5}}, f.density_binding, f.displacement_binding, f.compactification,
f.mapper
);
::mfem::Vector result;
CHECK_THROWS_AS(prepared.residual(f.workspace, result), std::logic_error);
CHECK_THROWS_AS(prepared.width(), std::logic_error);
const auto wrong = constant(rho.Size() + 1, 0.0);
CHECK_THROWS_AS(prepared.prepare(f.state(wrong, h), u, f.workspace), std::invalid_argument);
CHECK_FALSE(prepared.is_prepared());
rho[0] = std::numeric_limits<double>::quiet_NaN();
const auto invalid = prepared.try_prepare(f.state(rho, h), u, f.workspace);
REQUIRE_FALSE(invalid.has_value());
CHECK(invalid.error().reason == backend::PreparationRejectionReason::NON_FINITE);
rho = f.density();
prepared.prepare(f.state(rho, h), u, f.workspace);
CHECK_THROWS_AS(prepared.apply_jacobian(f.state(wrong, h), u, f.workspace, result), std::invalid_argument);
CHECK_THROWS_AS(prepared.apply_jacobian(wrong, f.workspace, result), std::invalid_argument);
h[0] = std::numeric_limits<double>::infinity();
CHECK_THROWS_AS(prepared.apply_jacobian(f.state(rho, h), u, f.workspace, result), std::domain_error);
}
TEST_CASE("Equation Bindings Reject Wrong Spaces And Different Meshes", tags::unit & tags::equations) {
Fixture f, other;
::mfem::H1_FECollection wrong_collection(2, 3);
::mfem::FiniteElementSpace wrong_order(&f.mesh, &wrong_collection);
CHECK_THROWS_AS(
(discretization::backend::mfem::TermBinding<::serif::tests::equations::Enthalpy, Schema>{wrong_order}), std::invalid_argument
);
CHECK_THROWS_AS(
(discretization::backend::mfem::TermBinding<blocks::density::mass, Schema>{wrong_order}), std::invalid_argument
);
const auto rho = f.density(), h = other.enthalpy(), u = f.displacement(0.0);
PreparedClosure prepared(
Closure{eos::models::Polytrope{3.0, 1.5}}, f.density_binding, f.displacement_binding, f.compactification,
f.mapper
);
const auto wrong_state =
backend::make_term_coefficients(backend::bind(f.density_binding, rho), backend::bind(other.enthalpy_binding, h));
CHECK_THROWS_AS(prepared.prepare(wrong_state, u, f.workspace), std::invalid_argument);
const auto local_h = f.enthalpy();
prepared.prepare(f.state(rho, local_h), u, f.workspace);
::mfem::Vector result;
CHECK_THROWS_AS(prepared.apply_jacobian(wrong_state, u, f.workspace, result), std::invalid_argument);
}
TEST_CASE("Prepared Equations Reject Changed Discretization And Domain Support", tags::unit & tags::equations) {
Fixture f;
const auto rho = f.density(), h = f.enthalpy(), u = f.displacement(0.0);
PreparedClosure prepared(
Closure{eos::models::Polytrope{3.0, 1.5}}, f.density_binding, f.displacement_binding, f.compactification,
f.mapper
);
prepared.prepare(f.state(rho, h), u, f.workspace);
::mfem::Vector result;
SECTION("Changed domain attributes") {
f.mesh.GetElement(0)->SetAttribute(3);
CHECK_THROWS_AS(prepared.residual(f.workspace, result), std::logic_error);
}
SECTION("Mesh refinement") {
f.mesh.UniformRefinement();
CHECK_THROWS_AS(prepared.residual(f.workspace, result), std::logic_error);
}
SECTION("Updated node geometry") {
f.mesh.NodesUpdated();
CHECK_THROWS_AS(prepared.residual(f.workspace, result), std::logic_error);
prepared.prepare(f.state(rho, h), u, f.workspace);
CHECK_NOTHROW(prepared.residual(f.workspace, result));
}
SECTION("Rebound compactification") {
Fixture other;
f.compactification.SetSpace(&other.enthalpy_space);
CHECK_THROWS_AS(prepared.residual(f.workspace, result), std::logic_error);
}
}
TEST_CASE("Equation Field Restriction Excludes Only Vacuum-Only Coefficients", tags::unit & tags::equations) {
Fixture f;
auto full = constant(f.density_space.GetVSize(), 0.0);
::mfem::Array<int> dofs;
std::vector<bool> stellar_dofs(full.Size(), false);
for (int element = 0; element < f.mesh.GetNE(); ++element) {
if (f.mesh.GetAttribute(element) == 3) {
continue;
}
f.density_space.GetElementDofs(element, dofs);
for (const int dof : dofs) {
stellar_dofs[dof] = true;
}
}
for (int dof = 0; dof < full.Size(); ++dof) {
full[dof] = stellar_dofs[dof] ? 0.5 : std::numeric_limits<double>::quiet_NaN();
}
const auto selected = std::count(stellar_dofs.begin(), stellar_dofs.end(), true);
REQUIRE(selected > 0);
REQUIRE(selected < full.Size());
CHECK(f.density_binding.size() == selected);
const auto reduced = f.density_binding.gather(full);
CHECK_NOTHROW(f.density_binding.validate_values(reduced));
const auto expanded = f.density_binding.scatter(reduced);
CHECK(expanded.CheckFinite() == 0);
CHECK(reduced.Min() == 0.5);
}
TEST_CASE("Equation Bindings Reject Unsupported Meshes And Element Indices", tags::unit & tags::equations) {
Fixture f;
CHECK_THROWS_AS(f.density_binding.supports(-1), std::out_of_range);
CHECK_THROWS_AS(f.density_binding.supports(f.mesh.GetNE()), std::out_of_range);
::mfem::Vector empty, output;
CHECK_THROWS_AS(f.density_binding.element_values(0, empty, output), std::invalid_argument);
auto flat_mesh = ::mfem::Mesh::MakeCartesian2D(2, 2, ::mfem::Element::QUADRILATERAL);
::mfem::L2_FECollection flat_collection(2, 2);
::mfem::FiniteElementSpace flat_space(&flat_mesh, &flat_collection);
CHECK_THROWS_AS((discretization::backend::mfem::TermBinding<blocks::density::mass, Schema>{flat_space}), std::invalid_argument);
auto nonconforming_mesh = make_mesh(::mfem::Element::HEXAHEDRON);
nonconforming_mesh.EnsureNCMesh();
::mfem::FiniteElementSpace nonconforming_space(&nonconforming_mesh, &f.density_collection);
CHECK_NOTHROW((discretization::backend::mfem::TermBinding<blocks::density::mass, Schema>{nonconforming_space}));
}
namespace {
/** @brief Compare primal/dual coordinate access with independent MFEM extraction and assembly. */
void check_element_coordinates(const discretization::backend::mfem::DofMap& binding) {
const auto reduced = deterministic(binding.size(), 0.31);
const auto full = binding.scatter(reduced);
const auto elements = binding.validated_elements();
for (int element = 0; element < binding.mesh().GetNE(); ++element) {
::mfem::Array<int> dofs;
::mfem::DofTransformation transformation;
binding.space().GetElementVDofs(element, dofs, transformation);
::mfem::Vector expected, actual;
full.GetSubVector(dofs, expected);
if (transformation.GetDofTransformation() != nullptr) {
transformation.InvTransformPrimal(expected);
}
elements.element_values(element, reduced, actual);
CHECK(relative_error(actual, expected) < 1.0e-13);
const auto coordinates = elements.coordinates(element);
REQUIRE(coordinates.size() == dofs.Size());
for (int local = 0; local < dofs.Size(); ++local) {
const int full_index = dofs[local] < 0 ? -1 - dofs[local] : dofs[local];
const int index = binding.reduced_dof(full_index).value_or(-1);
CHECK(coordinates.reduced_dof(local) == index);
if (index >= 0) {
CHECK(coordinates.local_dof(index) == local);
::mfem::Vector unit = constant(binding.size(), 0.0), direction;
unit[index] = 1.0;
coordinates.element_values(unit, expected);
coordinates.unit_direction(local, direction);
CHECK(relative_error(direction, expected) < 1.0e-13);
}
}
const auto dual = deterministic(dofs.Size(), 0.78);
::mfem::Vector expected_dual(dual), actual_dual(dual);
auto expected_full = constant(binding.full_size(), 0.0);
auto assembled = constant(binding.full_size(), 0.0);
if (transformation.GetDofTransformation() != nullptr) {
transformation.TransformDual(expected_dual);
}
expected_full.AddElementVector(dofs, expected_dual);
elements.add_element(element, actual_dual, assembled);
CHECK(relative_error(assembled, expected_full) < 1.0e-13);
CHECK(full * assembled == Catch::Approx(actual * dual).margin(1.0e-12));
actual_dual = dual;
coordinates.orient_dual(actual_dual);
for (int local = 0; local < dofs.Size(); ++local) {
const double expected_sign = dofs[local] < 0 ? -1.0 : 1.0;
CHECK(actual_dual[local] == Catch::Approx(expected_sign * expected_dual[local]).margin(1.0e-14));
}
}
}
}
TEST_CASE("Validated Element Traversals Preserve Native MFEM Coordinates", tags::unit & tags::equations) {
for (const auto geometry : {::mfem::Element::HEXAHEDRON, ::mfem::Element::TETRAHEDRON}) {
for (const auto ordering : {::mfem::Ordering::byNODES, ::mfem::Ordering::byVDIM}) {
Fixture fixture(geometry, ordering);
check_element_coordinates(fixture.density_binding);
check_element_coordinates(fixture.enthalpy_binding);
check_element_coordinates(fixture.displacement_binding);
}
}
}
TEST_CASE("Validated Element Traversals Preserve Global Scalar Support And Guards", tags::unit & tags::equations) {
Fixture fixture;
const auto& binding = fixture.constant_binding;
const auto elements = binding.validated_elements();
const auto reduced = constant(1, 2.0);
for (int element = 0; element < fixture.mesh.GetNE(); ++element) {
::mfem::Vector local;
elements.element_values(element, reduced, local);
CHECK(local[0] == (binding.supports(element) ? 2.0 : 0.0));
auto assembled = constant(1, 0.0);
auto dual = constant(1, 3.0);
elements.add_element(element, dual, assembled);
CHECK(assembled[0] == (binding.supports(element) ? 3.0 : 0.0));
}
::mfem::Vector output, empty;
CHECK_THROWS_AS(elements.element_values(-1, reduced, output), std::out_of_range);
CHECK_THROWS_AS(elements.element_values(0, empty, output), std::invalid_argument);
CHECK_THROWS_AS(elements.add_element(0, empty, output), std::invalid_argument);
fixture.mesh.GetElement(0)->SetAttribute(3);
CHECK_THROWS_AS(binding.validated_elements(), std::logic_error);
CHECK_THROWS_AS(binding.element_values(1, reduced, output), std::logic_error);
CHECK_THROWS_AS(elements.element_values(0, reduced, output), std::logic_error);
}
TEST_CASE("Validated Element Traversals Reject Changed Mesh Revisions", tags::unit & tags::equations) {
Fixture fixture;
const auto elements = fixture.enthalpy_binding.validated_elements();
const auto values = fixture.enthalpy();
fixture.mesh.UniformRefinement();
::mfem::Vector output;
CHECK_THROWS_AS(elements.element_values(0, values, output), std::logic_error);
CHECK_THROWS_AS(fixture.enthalpy_binding.validated_elements(), std::logic_error);
}
} // namespace serif::tests::equations