#include #include #include #include #include #include #include #include #include #include #include #include import mean_field; import test_helpers; namespace field_mfem_test_utils { namespace field = mean_field::field; namespace domain = mean_field::utils::domain; namespace quadrature = mean_field::quadrature; using Schema = domain::CoreEnvelopeVacuumDomainSchema; struct VectorL2Field { static constexpr std::string_view name = "test_vector_l2"; using Support = field::DomainSupport; struct Vector final : field::VectorQ> { }; using Quantities = field::TypeList; using Constraints = field::TypeList<>; using FormList = field::TypeList<>; static constexpr bool constraintsAreValid = field::validate_constraints(Constraints{}); static_assert(constraintsAreValid); }; struct NdField { static constexpr std::string_view name = "test_nd"; using Support = field::DomainSupport; struct Vector final : field::VectorQ> { }; using Quantities = field::TypeList; using Constraints = field::TypeList<>; using FormList = field::TypeList<>; static constexpr bool constraintsAreValid = field::validate_constraints(Constraints{}); static_assert(constraintsAreValid); }; using AlternateSchema = domain::DomainSchema< domain::MaterialList< domain::Material, domain::Material, domain::Material>, domain::BoundaryList<>, domain::RelationList<>>; template concept CanResolveLocalSupport = requires(const mfem::FiniteElementSpace &space) { field::resolve_field_local_dof_support(space); }; [[nodiscard]] mfem::Mesh make_two_domain_mesh( const int leftAttribute = 2, const int rightAttribute = 3 ) { mfem::Mesh mesh = mfem::Mesh::MakeCartesian2D(2, 1, mfem::Element::QUADRILATERAL, true, 2.0, 1.0); mesh.GetElement(0)->SetAttribute(leftAttribute); mesh.GetElement(1)->SetAttribute(rightAttribute); mesh.SetAttributes(); return mesh; } [[nodiscard]] mfem::Mesh make_parallel_split_mesh() { constexpr int xElementCount = 4; constexpr int yElementCount = 2; mfem::Mesh mesh = mfem::Mesh::MakeCartesian2D(xElementCount, yElementCount, mfem::Element::QUADRILATERAL, true, 4.0, 2.0); for (int elementId = 0; elementId < mesh.GetNE(); ++elementId) { const int xIndex = elementId % xElementCount; const int attribute = xIndex < 2 ? 2 : 3; mesh.GetElement(elementId)->SetAttribute(attribute); } mesh.SetAttributes(); return mesh; } [[nodiscard]] std::vector decoded_element_vdofs( const mfem::FiniteElementSpace &space, const int elementId ) { mfem::Array signedVDofs; space.GetElementVDofs(elementId, signedVDofs); std::vector result; result.reserve(static_cast(signedVDofs.Size())); for (int index = 0; index < signedVDofs.Size(); ++index) { result.push_back(mfem::FiniteElementSpace::DecodeDof(signedVDofs[index])); } std::ranges::sort(result); result.erase(std::unique(result.begin(), result.end()), result.end()); return result; } [[nodiscard]] bool contains( const mfem::Array &values, const int value ) { for (int index = 0; index < values.Size(); ++index) { if (values[index] == value) { return true; } } return false; } [[nodiscard]] std::vector intersection( const std::vector &first, const std::vector &second ) { std::vector result; std::set_intersection(first.begin(), first.end(), second.begin(), second.end(), std::back_inserter(result)); return result; } [[nodiscard]] std::vector difference( const std::vector &first, const std::vector &second ) { std::vector result; std::set_difference(first.begin(), first.end(), second.begin(), second.end(), std::back_inserter(result)); return result; } [[nodiscard]] long long global_sum(const int localValue) { const long long local = static_cast(localValue); long long global = 0; MPI_Allreduce(&local, &global, 1, MPI_LONG_LONG, MPI_SUM, MPI_COMM_WORLD); return global; } } // namespace field_mfem_test_utils TEST_CASE( "Field MFEM Support Resolution Is Available Only For Domain Supported Fields", tags::unit &tags::field ) { namespace field = mean_field::field; STATIC_REQUIRE(field::MfemDomainField); STATIC_REQUIRE(field::MfemDomainField); STATIC_REQUIRE(field::MfemDomainField); STATIC_REQUIRE(field::MfemDomainField); STATIC_REQUIRE_FALSE(field::MfemDomainField); STATIC_REQUIRE(field_mfem_test_utils::CanResolveLocalSupport); STATIC_REQUIRE_FALSE(field_mfem_test_utils::CanResolveLocalSupport); CHECK(true); } TEST_CASE( "Field MFEM Creates The Registered Finite Element Collection Families", tags::unit &tags::field ) { namespace field = mean_field::field; using DensityField = field::Field; using GravityField = field::Field; using DisplacementField = field::Field; using EnthalpyField = field::Field; auto densityCollection = DensityField::make_fec(3); auto potentialCollection = GravityField::make_fec(3); auto fluxCollection = GravityField::make_fec(3); auto displacementCollection = DisplacementField::make_fec(3); auto enthalpyCollection = EnthalpyField::make_fec(3); auto vectorL2Collection = field::Field::make_fec(3); auto ndCollection = field::Field::make_fec(3); REQUIRE(densityCollection != nullptr); REQUIRE(potentialCollection != nullptr); REQUIRE(fluxCollection != nullptr); REQUIRE(displacementCollection != nullptr); REQUIRE(enthalpyCollection != nullptr); REQUIRE(vectorL2Collection != nullptr); REQUIRE(ndCollection != nullptr); CHECK(dynamic_cast(densityCollection.get()) != nullptr); CHECK(dynamic_cast(potentialCollection.get()) != nullptr); CHECK(dynamic_cast(fluxCollection.get()) != nullptr); CHECK(dynamic_cast(displacementCollection.get()) != nullptr); CHECK(dynamic_cast(enthalpyCollection.get()) != nullptr); CHECK(dynamic_cast(vectorL2Collection.get()) != nullptr); CHECK(dynamic_cast(ndCollection.get()) != nullptr); CHECK_THROWS_AS((DensityField::make_fec(0)), std::invalid_argument); CHECK_THROWS_AS((GravityField::make_fec(-1)), std::invalid_argument); } TEST_CASE( "Field MFEM Creates Parallel Spaces With Registered Dimensions Orders And Ordering", tags::integration &tags::field ) { namespace field = mean_field::field; mfem::Mesh serialMesh = mfem::Mesh::MakeCartesian2D(4, 2, mfem::Element::QUADRILATERAL, true, 4.0, 2.0); mfem::ParMesh mesh(MPI_COMM_WORLD, serialMesh); auto densityFec = field::Field::make_fec(2); auto potentialFec = field::Field::make_fec(2); auto fluxFec = field::Field::make_fec(2); auto displacementFec = field::Field::make_fec(2); auto enthalpyFec = field::Field::make_fec(2); auto vectorL2Fec = field::Field::make_fec(2); auto ndFec = field::Field::make_fec(2); auto densitySpace = field::Field::make_fespace(mesh, *densityFec); auto potentialSpace = field::Field::make_fespace(mesh, *potentialFec); auto fluxSpace = field::Field::make_fespace(mesh, *fluxFec); auto displacementSpace = field::Field::make_fespace(mesh, *displacementFec); auto enthalpySpace = field::Field::make_fespace(mesh, *enthalpyFec); auto vectorL2Space = field::Field::make_fespace( mesh, *vectorL2Fec ); auto ndSpace = field::Field::make_fespace( mesh, *ndFec ); REQUIRE(densitySpace != nullptr); REQUIRE(potentialSpace != nullptr); REQUIRE(fluxSpace != nullptr); REQUIRE(displacementSpace != nullptr); REQUIRE(enthalpySpace != nullptr); REQUIRE(vectorL2Space != nullptr); REQUIRE(ndSpace != nullptr); CHECK(densitySpace->GetVDim() == 1); CHECK(potentialSpace->GetVDim() == 1); CHECK(fluxSpace->GetVDim() == 1); CHECK(enthalpySpace->GetVDim() == 1); CHECK(displacementSpace->GetVDim() == mesh.SpaceDimension()); CHECK(vectorL2Space->GetVDim() == mesh.SpaceDimension()); CHECK(ndSpace->GetVDim() == 1); CHECK(densitySpace->GetOrdering() == mfem::Ordering::byNODES); CHECK(potentialSpace->GetOrdering() == mfem::Ordering::byNODES); CHECK(fluxSpace->GetOrdering() == mfem::Ordering::byNODES); CHECK(enthalpySpace->GetOrdering() == mfem::Ordering::byNODES); /* * Displacement deliberately overrides the generic * vector-H1 rule and is part of the project's block/indexing * contract. */ CHECK(displacementSpace->GetOrdering() == mfem::Ordering::byNODES); /* * A generic vector L2 quantity retains the ordinary backend * realization, demonstrating that the displacement behavior is * an intentional specialization rather than a global accident. */ CHECK(vectorL2Space->GetOrdering() == mfem::Ordering::byVDIM); CHECK(ndSpace->GetOrdering() == mfem::Ordering::byNODES); CHECK(densitySpace->GetMaxElementOrder() == field::Density::Scalar::familyOrder); CHECK(potentialSpace->GetMaxElementOrder() == field::Gravity::Potential::familyOrder); CHECK(fluxSpace->GetMaxElementOrder() == field::Gravity::Flux::familyOrder + 1); CHECK(displacementSpace->GetMaxElementOrder() == field::Displacement::Vector::familyOrder); CHECK(enthalpySpace->GetMaxElementOrder() == field::Enthalpy::Scalar::familyOrder); } TEST_CASE( "Field MFEM Typed Queries Preserve Backend Polynomial Order Semantics", tags::unit &tags::field ) { namespace field = mean_field::field; namespace quadrature = mean_field::quadrature; namespace utils = mean_field::utils; using DensityField = field::Field; using GravityField = field::Field; using EnthalpyField = field::Field; const quadrature::Query densitySource = DensityField::make_query( quadrature::QuadratureRole::projection, 3, std::array{4}, utils::DOMAINS::STELLAR, quadrature::MappingKind::general ); REQUIRE(densitySource.base_order.has_value()); /* * L2_2 value order 2 * + geometry order 3 * + dynamic coefficient order 4. */ CHECK(*densitySource.base_order == 9); CHECK(densitySource.term == quadrature::Term::density_projection); CHECK(densitySource.role == quadrature::QuadratureRole::projection); CHECK(densitySource.domain == utils::DOMAINS::STELLAR); CHECK(densitySource.mapping == quadrature::MappingKind::general); CHECK(densitySource.geometry_weight_order == 3); const quadrature::Query hdivMass = GravityField::make_query(quadrature::QuadratureRole::discretization, 2); REQUIRE(hdivMass.base_order.has_value()); /* * RT_2 value order is 3, hence * 3 + 3 + geometry 2 = 8. */ CHECK(*hdivMass.base_order == 8); const quadrature::Query divergence = GravityField::make_query( quadrature::QuadratureRole::discretization, 2 ); REQUIRE(divergence.base_order.has_value()); /* * div(RT_2) order 2 * + L2_2 order 2 * + geometry 2. */ CHECK(*divergence.base_order == 6); const quadrature::Query pressureForce = EnthalpyField::make_query( quadrature::QuadratureRole::discretization, 2, std::array{9}, utils::DOMAINS::STELLAR, quadrature::MappingKind::general ); REQUIRE(pressureForce.base_order.has_value()); /* * h value order 3 * + grad(d) order 2 * + geometry 2 * + n=3 pressure extra order 9 * = 16. */ CHECK(*pressureForce.base_order == 16); const quadrature::Query equilibriumConstant = EnthalpyField::make_query( quadrature::QuadratureRole::discretization, 2 ); REQUIRE(equilibriumConstant.base_order.has_value()); /* * Global scalar C contributes zero polynomial order, * h contributes 3, and geometry contributes 2. */ CHECK(*equilibriumConstant.base_order == 5); CHECK_THROWS_AS( (DensityField::make_query(quadrature::QuadratureRole::projection, -1)), std::invalid_argument ); const std::array negativeDynamicOrder{-1}; CHECK_THROWS_AS( (EnthalpyField::make_query( quadrature::QuadratureRole::discretization, 2, negativeDynamicOrder )), std::invalid_argument ); } TEST_CASE( "Field MFEM Element Support Resolves Semantic Domains Through The Schema", tags::unit &tags::field ) { namespace field = mean_field::field; const mfem::Mesh mesh = field_mfem_test_utils::make_two_domain_mesh(); CHECK((field::element_is_in_field_support(mesh, 0))); CHECK_FALSE((field::element_is_in_field_support(mesh, 1))); CHECK((field::element_is_in_field_support(mesh, 0))); CHECK_FALSE((field::element_is_in_field_support(mesh, 1))); CHECK((field::element_is_in_field_support(mesh, 0))); CHECK((field::element_is_in_field_support(mesh, 1))); CHECK((field::element_is_in_field_support(mesh, 0))); CHECK((field::element_is_in_field_support(mesh, 1))); } TEST_CASE( "Field MFEM L2 Stellar Support Selects Exactly Stellar Element DOFs", tags::unit &tags::field ) { namespace field = mean_field::field; mfem::Mesh mesh = field_mfem_test_utils::make_two_domain_mesh(); auto fec = field::Field::make_fec(2); mfem::FiniteElementSpace space(&mesh, fec.get()); const auto support = field::resolve_field_local_dof_support(space); const std::vector stellarVDofs = field_mfem_test_utils::decoded_element_vdofs(space, 0); const std::vector vacuumVDofs = field_mfem_test_utils::decoded_element_vdofs(space, 1); REQUIRE_FALSE(stellarVDofs.empty()); REQUIRE_FALSE(vacuumVDofs.empty()); CHECK(support.activeVDofMarker.Size() == space.GetVSize()); CHECK(support.activeVDofs.Size() + support.inactiveVDofs.Size() == space.GetVSize()); for (const int vdof : stellarVDofs) { CAPTURE(vdof); CHECK(support.activeVDofMarker[vdof] == 1); CHECK(field_mfem_test_utils::contains(support.activeVDofs, vdof)); CHECK_FALSE(field_mfem_test_utils::contains(support.inactiveVDofs, vdof)); } for (const int vdof : vacuumVDofs) { CAPTURE(vdof); CHECK(support.activeVDofMarker[vdof] == 0); CHECK_FALSE(field_mfem_test_utils::contains(support.activeVDofs, vdof)); CHECK(field_mfem_test_utils::contains(support.inactiveVDofs, vdof)); } CHECK(support.activeVDofs.Size() == static_cast(stellarVDofs.size())); CHECK(support.inactiveVDofs.Size() == static_cast(vacuumVDofs.size())); } TEST_CASE( "Field MFEM H1 Stellar Support Keeps Shared Stellar Vacuum Trace DOFs Active", tags::unit &tags::field ) { namespace field = mean_field::field; mfem::Mesh mesh = field_mfem_test_utils::make_two_domain_mesh(); auto fec = field::Field::make_fec(2); mfem::FiniteElementSpace space(&mesh, fec.get()); const auto support = field::resolve_field_local_dof_support(space); const std::vector stellarVDofs = field_mfem_test_utils::decoded_element_vdofs(space, 0); const std::vector vacuumVDofs = field_mfem_test_utils::decoded_element_vdofs(space, 1); const std::vector interfaceVDofs = field_mfem_test_utils::intersection(stellarVDofs, vacuumVDofs); const std::vector vacuumOnlyVDofs = field_mfem_test_utils::difference(vacuumVDofs, stellarVDofs); REQUIRE_FALSE(interfaceVDofs.empty()); REQUIRE_FALSE(vacuumOnlyVDofs.empty()); for (const int vdof : stellarVDofs) { CAPTURE(vdof); CHECK(support.activeVDofMarker[vdof] == 1); } /* * This is the central support invariant: * * shared interface DOFs are active because they are touched * by a supported stellar element, even though they are also * touched by a vacuum element. */ for (const int vdof : interfaceVDofs) { CAPTURE(vdof); CHECK(support.activeVDofMarker[vdof] == 1); CHECK(field_mfem_test_utils::contains(support.activeVDofs, vdof)); } for (const int vdof : vacuumOnlyVDofs) { CAPTURE(vdof); CHECK(support.activeVDofMarker[vdof] == 0); CHECK(field_mfem_test_utils::contains(support.inactiveVDofs, vdof)); } CHECK(support.activeVDofs.Size() + support.inactiveVDofs.Size() == space.GetVSize()); } TEST_CASE( "Field MFEM All Domain Support Activates Every L2 And RT DOF", tags::unit &tags::field ) { namespace field = mean_field::field; mfem::Mesh mesh = field_mfem_test_utils::make_two_domain_mesh(); auto potentialFec = field::Field::make_fec(2); mfem::FiniteElementSpace potentialSpace(&mesh, potentialFec.get()); const auto potentialSupport = field::resolve_field_local_dof_support(potentialSpace); CHECK(potentialSupport.activeVDofs.Size() == potentialSpace.GetVSize()); CHECK(potentialSupport.inactiveVDofs.Size() == 0); for (int vdof = 0; vdof < potentialSupport.activeVDofMarker.Size(); ++vdof) { CHECK(potentialSupport.activeVDofMarker[vdof] == 1); } /* * Exercise signed/oriented MFEM element VDofs through RT as * well. The support resolver must DecodeDof() correctly. */ auto fluxFec = field::Field::make_fec(2); mfem::FiniteElementSpace fluxSpace(&mesh, fluxFec.get()); const auto fluxSupport = field::resolve_field_local_dof_support(fluxSpace); CHECK(fluxSupport.activeVDofs.Size() == fluxSpace.GetVSize()); CHECK(fluxSupport.inactiveVDofs.Size() == 0); for (int vdof = 0; vdof < fluxSupport.activeVDofMarker.Size(); ++vdof) { CHECK(fluxSupport.activeVDofMarker[vdof] == 1); } } TEST_CASE( "Field MFEM Support Resolution Uses Schema Material Bindings Rather Than Hard Coded IDs", tags::unit &tags::field ) { namespace field = mean_field::field; mfem::Mesh mesh = field_mfem_test_utils::make_two_domain_mesh(17, 29); auto fec = field::Field::make_fec(2); mfem::FiniteElementSpace space(&mesh, fec.get()); const auto support = field::resolve_field_local_dof_support(space); const std::vector stellarVDofs = field_mfem_test_utils::decoded_element_vdofs(space, 0); const std::vector vacuumVDofs = field_mfem_test_utils::decoded_element_vdofs(space, 1); for (const int vdof : stellarVDofs) { CHECK(support.activeVDofMarker[vdof] == 1); } for (const int vdof : vacuumVDofs) { CHECK(support.activeVDofMarker[vdof] == 0); } } TEST_CASE( "Field MFEM Parallel Stellar Support Produces Consistent Local And True DOF Partitions", tags::integration &tags::field ) { namespace field = mean_field::field; mfem::Mesh serialMesh = field_mfem_test_utils::make_parallel_split_mesh(); mfem::ParMesh mesh(MPI_COMM_WORLD, serialMesh); auto fec = field::Field::make_fec(2); auto space = field::Field::make_fespace(mesh, *fec); REQUIRE(space != nullptr); const auto support = field::resolve_field_dof_support(*space); CHECK(support.activeVDofMarker.Size() == space->GetVSize()); CHECK(support.activeVDofs.Size() + support.inactiveVDofs.Size() == space->GetVSize()); CHECK(support.activeTrueDofMarker.Size() == space->GetTrueVSize()); CHECK(support.activeTrueDofs.Size() + support.inactiveTrueDofs.Size() == space->GetTrueVSize()); /* * Every local DOF touched by a supported element must be active * after shared-DOF synchronization. */ for (int elementId = 0; elementId < mesh.GetNE(); ++elementId) { const int materialId = mesh.GetAttribute(elementId); const bool stellar = field_mfem_test_utils::Schema::template attribute_belongs_to( materialId ); if (!stellar) { continue; } const std::vector vdofs = field_mfem_test_utils::decoded_element_vdofs(*space, elementId); for (const int vdof : vdofs) { CAPTURE(elementId, vdof); CHECK(support.activeVDofMarker[vdof] == 1); } } const long long globalActiveTrueDofs = field_mfem_test_utils::global_sum(support.activeTrueDofs.Size()); const long long globalInactiveTrueDofs = field_mfem_test_utils::global_sum(support.inactiveTrueDofs.Size()); /* * The split mesh contains a finite stellar region and a finite * vacuum region with order-three H1 structure, so both categories * must genuinely exist globally. */ CHECK(globalActiveTrueDofs > 0); CHECK(globalInactiveTrueDofs > 0); } TEST_CASE( "Field MFEM Parallel All Support Activates Every True Displacement DOF", tags::integration &tags::field ) { namespace field = mean_field::field; mfem::Mesh serialMesh = field_mfem_test_utils::make_parallel_split_mesh(); mfem::ParMesh mesh(MPI_COMM_WORLD, serialMesh); auto fec = field::Field::make_fec(2); auto space = field::Field::make_fespace(mesh, *fec); REQUIRE(space != nullptr); const auto support = field::resolve_field_dof_support(*space); CHECK(support.inactiveVDofs.Size() == 0); CHECK(support.activeVDofs.Size() == space->GetVSize()); CHECK(support.inactiveTrueDofs.Size() == 0); CHECK(support.activeTrueDofs.Size() == space->GetTrueVSize()); for (int index = 0; index < support.activeTrueDofMarker.Size(); ++index) { CHECK(support.activeTrueDofMarker[index] == 1); } }