#include #include #include #include #include #include #include #include "mfem/fem/coefficient.hpp" #include "mfem/fem/gridfunc.hpp" #include "serif/tests/equations/test_equations.hpp" #include "serif/backend/mfem/discretization/boundary_trace.hpp" #include "serif/discretization/blocks/fields.hpp" #include "serif/backend/mfem/geometry/deformation/inputs.hpp" #include "serif/backend/mfem/geometry/deformation/reference_geometry.hpp" #include "serif/backend/mfem/geometry/deformation/surface_metric.hpp" #include "serif/tests/deformation/mesh.hpp" namespace { namespace deformation = serif::geometry::deformation; namespace backend = deformation::backend::mfem; namespace finite_elements = serif::discretization::backend::mfem; namespace domain = serif::discretization::domain; using Schema = domain::schema::CoreEnvelopeVacuumDomainSchema; using Catch::Matchers::WithinAbs; using Catch::Matchers::WithinRel; /** @brief Retain the original eight-coordinate indexing example without imposing a higher-order physical field. */ struct LinearEnthalpy : serif::discretization::blocks::term<::serif::tests::equations::enthalpy> { using FunctionalSpace = serif::discretization::functional_space::DiscreteFunctionalSpace; using Support = domain::StellarDomains; using Quantity = serif::dimensions::SpecificEnthalpy; struct value : serif::discretization::blocks::value_block_base { }; struct residual : serif::discretization::blocks::residual_block_base { }; }; using AlternateBoundarySchema = domain::schema::DomainSchema, domain::ids::BoundaryID>, domain::relation::lists::RelationList<>>; /** @brief Resolve boundary coordinates independently using MFEM's serial essential true-DOF list. */ [[nodiscard]] std::vector essential_dofs(const mfem::FiniteElementSpace& space, const int attribute) { mfem::Array marker(space.GetMesh()->bdr_attributes.Max()), selected; marker = 0; marker[attribute - 1] = 1; space.GetEssentialTrueDofs(marker, selected); return std::vector(selected.begin(), selected.end()); } } // namespace TEST_CASE("MeanField canonical scalar coordinates retain exact gather and transpose scatter indexing", "[unit][deformation][meanfield]") { mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D(1, 1, 1, mfem::Element::HEXAHEDRON); mfem::H1_FECollection collection(1, 3); mfem::FiniteElementSpace space(&mesh, &collection); finite_elements::TermBinding binding(space); REQUIRE(binding.size() == 8); const mfem::Array selected{1, 3, 6}; const auto rows = std::make_shared(binding, selected); const auto trace = finite_elements::make_boundary_trace(rows, binding); mfem::Vector full(binding.size()), sampled(rows->size()), scattered(binding.size()); for (int dof = 0; dof < full.Size(); ++dof) { full[dof] = 10.0 + dof; } trace.interpolation().Mult(full, sampled); CHECK(sampled[0] == 11.0); CHECK(sampled[1] == 13.0); CHECK(sampled[2] == 16.0); trace.interpolation().MultTranspose(sampled, scattered); for (int dof = 0; dof < scattered.Size(); ++dof) { CHECK(scattered[dof] == (dof == 1 || dof == 3 || dof == 6 ? full[dof] : 0.0)); } CHECK_THROWS_AS((finite_elements::DofSelection{binding, mfem::Array{3, 1}}), std::invalid_argument); CHECK_THROWS_AS((finite_elements::DofSelection{binding, mfem::Array{1, 1}}), std::invalid_argument); CHECK_THROWS_AS((finite_elements::DofSelection{binding, mfem::Array{-1}}), std::invalid_argument); CHECK_THROWS_AS((finite_elements::DofSelection{binding, mfem::Array{8}}), std::invalid_argument); } TEST_CASE("MeanField surface coordinates resolve semantic boundary IDs and scalar companion nodes", "[unit][deformation][meanfield]") { serif::tests::deformation::PairedMesh fixture(0, 0.1, 3, 4); const auto primary = backend::deformation_discretization(fixture.scalar_space, fixture.vector_space, *fixture.generated.reference_mesh); const auto alternate = backend::deformation_discretization(fixture.scalar_space, fixture.vector_space, *fixture.generated.reference_mesh); const auto stellar = backend::sample_reference_surface(fixture.scalar_space, primary.surface_attribute); const auto infinity = backend::select_surface_scalar_dofs(fixture.scalar_space, alternate.surface_attribute); REQUIRE(stellar); REQUIRE(infinity); CHECK(stellar->scalar_dofs == essential_dofs(fixture.scalar_space, 1)); CHECK(*infinity == essential_dofs(fixture.scalar_space, 2)); CHECK(stellar->scalar_dofs != *infinity); CHECK(std::is_sorted(stellar->scalar_dofs.begin(), stellar->scalar_dofs.end())); REQUIRE(stellar->scalar_dofs.size() > 0); CHECK(stellar->scalar_dofs.size() < static_cast(fixture.scalar_space.GetTrueVSize())); const auto repeated = backend::select_surface_scalar_dofs(fixture.scalar_space, primary.surface_attribute); REQUIRE(repeated); CHECK(*repeated == stellar->scalar_dofs); for (const mfem::Ordering::Type ordering : {mfem::Ordering::byNODES, mfem::Ordering::byVDIM}) { mfem::FiniteElementSpace vector_space(fixture.generated.mesh.get(), &fixture.collection, 3, ordering); const std::vector vector_dofs = essential_dofs(vector_space, primary.surface_attribute); CHECK(vector_dofs.size() == 3 * stellar->scalar_dofs.size()); mfem::VectorFunctionCoefficient coordinates(3, [](const mfem::Vector& position, mfem::Vector& value) { value = position; }); mfem::GridFunction projected(&vector_space); projected.ProjectCoefficient(coordinates); for (std::size_t node = 0; node < stellar->positions.size(); ++node) { for (int component = 0; component < 3; ++component) { CHECK_THAT(stellar->positions[node][component], WithinAbs(projected[vector_space.DofToVDof(stellar->scalar_dofs[node], component)], 2.0e-13)); } } } } TEST_CASE("MeanField stellar surface metrics include tagged internal interfaces", "[unit][deformation][meanfield]") { serif::tests::deformation::PairedMesh fixture; int selected_faces = 0; for (int boundary = 0; boundary < fixture.generated.mesh->GetNBE(); ++boundary) { if (fixture.generated.mesh->GetBdrAttribute(boundary) == fixture.surface_attribute) { const int face = fixture.generated.mesh->GetBdrElementFaceIndex(boundary); REQUIRE(face >= 0); CHECK(fixture.generated.mesh->GetFaceInformation(face).IsOfFaceType(mfem::FaceType::Interior)); ++selected_faces; } } REQUIRE(selected_faces > 0); const auto reference = backend::sample_reference_surface(fixture.scalar_space, fixture.surface_attribute); REQUIRE(reference); const auto embedding = deformation::build_surface_embedding(deformation::NodalRadialSurface{}, reference->positions); REQUIRE(embedding); const auto metric = backend::surface_parameter_mass_diagonal(fixture.scalar_space, fixture.surface_attribute, *reference, *embedding); REQUIRE(metric); REQUIRE(metric->size() == reference->scalar_dofs.size()); for (const double diagonal : *metric) { CHECK(std::isfinite(diagonal)); CHECK(diagonal > 0.0); } } TEST_CASE("MeanField STROID spherical volume retains its original mesh accuracy", "[integration][deformation][geometry][meanfield]") { // Match sandbox.smesh: two refinements, quartic geometry, stellar radius 1 and reference infinity 5. auto generated = serif::tests::deformation::make_mesh(2, 0.0, 4, 5.0); long double volume = 0.0L; int stellar_elements = 0; for (int element = 0; element < generated.mesh->GetNE(); ++element) { if (!Schema::domain_id_belongs_to(generated.mesh->GetAttribute(element))) { continue; } mfem::ElementTransformation& transformation = *generated.mesh->GetElementTransformation(element); const mfem::IntegrationRule& rule = mfem::IntRules.Get(transformation.GetGeometryType(), transformation.OrderW()); for (int point = 0; point < rule.GetNPoints(); ++point) { const mfem::IntegrationPoint& sample = rule.IntPoint(point); transformation.SetIntPoint(&sample); volume += sample.weight * transformation.Weight(); } ++stellar_elements; } REQUIRE(stellar_elements > 0); CHECK_THAT(static_cast(volume), WithinRel(4.0 * std::numbers::pi / 3.0, 1.0e-6)); } TEST_CASE("Shared MFEM reference tracking distinguishes coordinate and label dependencies", "[unit][deformation][reference_tracking]") { namespace shared = serif::backend::mfem::shared; mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D(1, 1, 1, mfem::Element::HEXAHEDRON); mfem::H1_FECollection collection(1, 3); mfem::FiniteElementSpace space(&mesh, &collection); const auto topology = shared::mesh_snapshot(&mesh, false, false); const auto materials = shared::mesh_snapshot(&mesh, false, true); const auto coordinates = shared::mesh_snapshot(&mesh, true, true); const auto original_space = shared::space_snapshot(space); REQUIRE(shared::mesh_matches(topology, &mesh)); REQUIRE(shared::mesh_matches(coordinates, &mesh)); mesh.Transform([](const mfem::Vector& position, mfem::Vector& transformed) { transformed = position; transformed[0] *= 1.2; }); CHECK(shared::mesh_matches(topology, &mesh)); CHECK(shared::mesh_matches(materials, &mesh)); CHECK_FALSE(shared::mesh_matches(coordinates, &mesh)); CHECK(shared::space_snapshot(space) == original_space); mesh.GetElement(0)->SetAttribute(9); CHECK(shared::mesh_matches(topology, &mesh)); CHECK_FALSE(shared::mesh_matches(materials, &mesh)); mesh.GetBdrElement(0)->SetAttribute(9); CHECK_FALSE(shared::mesh_matches(topology, &mesh)); mfem::FiniteElementSpace other(&mesh, &collection); CHECK_FALSE(shared::space_snapshot(other) == original_space); CHECK_FALSE(shared::mesh_matches(topology, nullptr)); CHECK(shared::mesh_matches(shared::mesh_snapshot(nullptr, true, true), nullptr)); }