// Port the exact reference mass-diagonal comparisons from MeanField normalization/stellar_equilibrium.cpp. #include #include #include #include "mfem/mesh/ncmesh.hpp" #include #include "mfem/fem/bilinearform.hpp" #include "mfem/fem/bilininteg.hpp" #include "mfem/fem/gridfunc.hpp" #include "mfem/general/forall.hpp" #include "serif/tests/equations/test_equations.hpp" #include "serif/backend/mfem/discretization/mass_diagonal.hpp" #include "serif/discretization/domain/schema/schemas.hpp" namespace { namespace discretization = serif::discretization; namespace backend = discretization::backend::mfem; namespace blocks = discretization::blocks; namespace domain = discretization::domain; namespace functional_space = discretization::functional_space; using Schema = domain::schema::CoreEnvelopeVacuumDomainSchema; /** @brief Exercise the same mass construction for stellar support and independent vector components. */ struct TestField final : blocks::field { template struct values final : blocks::term { using FunctionalSpace = functional_space::DiscreteFunctionalSpace; using Support = Domain; }; }; /** @brief Keep shared interface coefficients while excluding the second material's volume. */ [[nodiscard]] mfem::Mesh reference_mesh(const mfem::Element::Type geometry) { mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D(2, 1, 1, geometry, 2.0, 1.5, 0.75); for (int element = 0; element < mesh.GetNE(); ++element) { mesh.GetElement(element)->SetAttribute(element < mesh.GetNE() / 2 ? 2 : 3); } mesh.SetAttributes(); return mesh; } /** @brief Compare the production tensor shortcut or fallback against an independently assembled sparse matrix. */ template void check_reference_diagonal(mfem::FiniteElementSpace& space) { const backend::TermBinding binding{space}; const auto actual = backend::reference_mass_diagonal(binding); REQUIRE(actual); REQUIRE(actual->size() == static_cast(binding.size())); mfem::Array marker(space.GetMesh()->attributes.Max()); for (int attribute = 1; attribute <= marker.Size(); ++attribute) { marker[attribute - 1] = Schema::template domain_id_belongs_to(attribute); } mfem::BilinearForm reference(&space); if constexpr (blocks::SpatialTermTraits::rank == 1) { reference.AddDomainIntegrator(new mfem::VectorMassIntegrator(), marker); } else { reference.AddDomainIntegrator(new mfem::MassIntegrator(), marker); } reference.Assemble(); reference.Finalize(); mfem::Vector full_diagonal; reference.SpMat().GetDiag(full_diagonal); const mfem::Vector expected = binding.gather(full_diagonal); double error_squared = 0.0; for (int index = 0; index < expected.Size(); ++index) { CAPTURE(index, (*actual)[index], expected[index]); CHECK(std::isfinite((*actual)[index])); CHECK((*actual)[index] > 0.0); error_squared += std::pow((*actual)[index] - expected[index], 2); } CHECK(std::sqrt(error_squared) <= 2.0e-11 * expected.Norml2()); } /** @brief Similar reference bodies change scalar volume metrics by length cubed and flux metrics by inverse length. */ template void check_geometry_scaling(mfem::FiniteElementSpace& space, const double exponent) { const backend::TermBinding binding{space}; const auto original = backend::reference_mass_diagonal(binding); REQUIRE(original); constexpr double length_scale = 2.5; mfem::VectorFunctionCoefficient dilation(3, [](const mfem::Vector& point, mfem::Vector& image) { image = point; image *= length_scale; }); space.GetMesh()->Transform(dilation); const auto scaled = backend::reference_mass_diagonal(binding); REQUIRE(scaled); REQUIRE(scaled->size() == original->size()); for (std::size_t index = 0; index < original->size(); ++index) { const double expected = (*original)[index] * std::pow(length_scale, exponent); CHECK(std::abs((*scaled)[index] - expected) <= 2.0e-12 * expected); } } } TEST_CASE("Reference mass diagonals match exact scalar assembly", "[unit][normalization][meanfield]") { for (const mfem::Element::Type geometry : {mfem::Element::HEXAHEDRON, mfem::Element::TETRAHEDRON}) { CAPTURE(geometry); mfem::Mesh mesh = reference_mesh(geometry); mfem::H1_FECollection h1(3, 3); mfem::FiniteElementSpace enthalpy(&mesh, &h1), density(&mesh, &h1); check_reference_diagonal<::serif::tests::equations::Enthalpy>(enthalpy); check_reference_diagonal(density); mfem::H1_FECollection potential_collection(blocks::gravity::potential::FunctionalSpace::order, 3); mfem::FiniteElementSpace potential(&mesh, &potential_collection); check_reference_diagonal(potential); } } TEST_CASE("Reference mass diagonals preserve vector component ordering", "[unit][normalization]") { for (const mfem::Element::Type geometry : {mfem::Element::HEXAHEDRON, mfem::Element::TETRAHEDRON}) { mfem::Mesh mesh = reference_mesh(geometry); mfem::H1_FECollection h1(3, 3); mfem::L2_FECollection l2(2, 3); for (const mfem::Ordering::Type ordering : {mfem::Ordering::byNODES, mfem::Ordering::byVDIM}) { CAPTURE(geometry, ordering); mfem::FiniteElementSpace displacement(&mesh, &h1, 3, ordering), velocity(&mesh, &l2, 3, ordering); check_reference_diagonal(displacement); check_reference_diagonal>(velocity); } } } TEST_CASE("Reference mass diagonals use curved geometry in both assembly paths", "[unit][normalization]") { for (const mfem::Element::Type geometry : {mfem::Element::HEXAHEDRON, mfem::Element::TETRAHEDRON}) { CAPTURE(geometry); mfem::Mesh mesh = reference_mesh(geometry); mesh.SetCurvature(2); mfem::VectorFunctionCoefficient curved(3, [](const mfem::Vector& point, mfem::Vector& image) { image = point; image[1] += 0.06 * point[0] * point[0]; image[2] *= 1.0 + 0.04 * point[0]; }); mesh.Transform(curved); mfem::H1_FECollection h1(3, 3); mfem::FiniteElementSpace enthalpy(&mesh, &h1), density(&mesh, &h1), displacement(&mesh, &h1, 3); check_reference_diagonal<::serif::tests::equations::Enthalpy>(enthalpy); check_reference_diagonal(density); check_reference_diagonal(displacement); } } TEST_CASE("Reference metrics retain scalar length scaling", "[unit][normalization]") { SECTION("Scalar volume coefficients") { mfem::Mesh mesh = reference_mesh(mfem::Element::HEXAHEDRON); mfem::H1_FECollection h1(3, 3); mfem::FiniteElementSpace enthalpy(&mesh, &h1); check_geometry_scaling<::serif::tests::equations::Enthalpy>(enthalpy, 3.0); } } TEST_CASE("Reference metrics retain supported ordering and reject stale material bindings", "[unit][normalization]") { mfem::Mesh mesh = reference_mesh(mfem::Element::HEXAHEDRON); mfem::H1_FECollection h1(3, 3); mfem::FiniteElementSpace space(&mesh, &h1); const backend::TermBinding<::serif::tests::equations::Enthalpy, Schema> binding{space}; const auto diagonal = backend::reference_mass_diagonal(binding); REQUIRE(diagonal); CHECK(diagonal->size() < static_cast(space.GetVSize())); mesh.GetElement(0)->SetAttribute(3); CHECK_THROWS(backend::reference_mass_diagonal(binding)); mesh.SetAttributes(); const backend::TermBinding<::serif::tests::equations::Enthalpy, Schema> empty_binding{space}; const auto empty = backend::reference_mass_diagonal(empty_binding); REQUIRE(empty); CHECK(empty->empty()); } TEST_CASE("Reference metrics reject zero volume rather than constructing singular normalization", "[unit][normalization]") { mfem::Mesh mesh = reference_mesh(mfem::Element::HEXAHEDRON); mfem::VectorFunctionCoefficient collapsed(3, [](const mfem::Vector& point, mfem::Vector& image) { image = point; image[2] = 0.0; }); mesh.Transform(collapsed); mfem::H1_FECollection h1(3, 3); mfem::FiniteElementSpace space(&mesh, &h1); const backend::TermBinding<::serif::tests::equations::Enthalpy, Schema> binding{space}; const auto diagonal = backend::reference_mass_diagonal(binding); REQUIRE_FALSE(diagonal); CHECK(diagonal.error().find("positive finite") != std::string::npos); } namespace { /** @brief Assemble an independent dense element oracle before MFEM applies hanging-node constraints. */ class ReconstructedMass final : public mfem::BilinearFormIntegrator { public: /** @brief Borrow the weighted binding and select a verification quadrature increment. */ ReconstructedMass(const backend::DofMap& binding, int increment) : m_binding(binding), m_increment(increment) {} /** @brief Preserve every local cross term, so the oracle tests constrained diagonal reduction. */ void AssembleElementMatrix(const mfem::FiniteElement& element, mfem::ElementTransformation& transformation, mfem::DenseMatrix& matrix) override { const auto* reconstruction = m_binding.reconstruction_element(transformation.ElementNo); const int order = mfem::MassIntegrator::GetRule(element, element, transformation).GetOrder() + m_increment; const auto& rule = mfem::IntRules.Get(element.GetGeomType(), order); matrix.SetSize(element.GetDof()); matrix = 0.0; mfem::Vector shapes(element.GetDof()); mfem::DenseMatrix derivatives; for (int index = 0; index < rule.GetNPoints(); ++index) { const auto& point = rule.IntPoint(index); if (reconstruction != nullptr) { reconstruction->sample(element, point, shapes, derivatives); } else { element.CalcShape(point, shapes); } transformation.SetIntPoint(&point); const double weight = point.weight * transformation.Weight(); for (int row = 0; row < element.GetDof(); ++row) { for (int column = 0; column < element.GetDof(); ++column) { matrix(row, column) += weight * shapes[row] * shapes[column]; } } } } private: const backend::DofMap& m_binding; int m_increment; }; /** @brief Form the actual sparse mass matrix, then reduce all couplings before reading its diagonal. */ [[nodiscard]] mfem::Vector reconstructed_diagonal_oracle(const backend::DofMap& binding, int increment) { mfem::BilinearForm mass(&binding.space()); mass.AddDomainIntegrator(new ReconstructedMass(binding, increment)); mass.Assemble(); mass.Finalize(); mfem::Vector diagonal; if (binding.constrained()) { const auto& prolongation = *binding.space().GetConformingProlongation(); const std::unique_ptr reduced(mfem::RAP(prolongation, mass.SpMat(), prolongation)); reduced->GetDiag(diagonal); return binding.gather_true(diagonal); } mass.SpMat().GetDiag(diagonal); return binding.gather(diagonal); } } TEST_CASE("Surface-power reference metrics retain constrained cross terms and converge under quadrature", "[unit][normalization][reconstruction]") { for (bool constrained : {false, true}) { mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D(2, 1, 1, mfem::Element::HEXAHEDRON); mfem::Array vertices; for (int boundary = 0; boundary < mesh.GetNBE(); ++boundary) { mesh.GetBdrElementVertices(boundary, vertices); bool surface = true; for (int vertex : vertices) { surface = surface && mesh.GetVertex(vertex)[2] == 0.0; } mesh.GetBdrElement(boundary)->SetAttribute(surface ? 1 : 2); } mesh.SetAttributes(); if (constrained) { mfem::Array refinement; refinement.Append(mfem::Refinement(0, mfem::Refinement::Y)); mesh.GeneralRefinement(refinement, 1); } mfem::H1_FECollection collection(3, 3); mfem::FiniteElementSpace space(&mesh, &collection); const backend::TermBinding ordinary(space); const auto plan = std::make_shared(ordinary, 1, std::make_shared(5.0 / 3.0)); const backend::TermBinding binding(space, plan); REQUIRE(binding.constrained() == constrained); const auto actual = backend::reference_mass_diagonal(binding); REQUIRE(actual); const mfem::Vector oracle = reconstructed_diagonal_oracle(binding, plan->integration_order_increment()); const mfem::Vector refined = reconstructed_diagonal_oracle(binding, plan->integration_order_increment() + 16); for (int index = 0; index < binding.size(); ++index) { CAPTURE(constrained, index); CHECK((*actual)[index] > 0.0); CHECK(std::abs((*actual)[index] - oracle[index]) <= 2e-11 * oracle[index]); // Fractional powers are not integrated exactly by a finite Gaussian rule. CHECK(std::abs((*actual)[index] - refined[index]) <= 3e-4 * refined[index]); } } }