// Copyright (c) 2010-2025, Lawrence Livermore National Security, LLC. Produced // at the Lawrence Livermore National Laboratory. All Rights reserved. See files // LICENSE and NOTICE for details. LLNL-CODE-806117. // // This file is part of the MFEM library. For more information and source code // availability visit https://mfem.org. // // MFEM is free software; you can redistribute it and/or modify it under the // terms of the BSD-3 license. We welcome feedback and contributions, see file // CONTRIBUTING.md for details. #include "../unit_tests.hpp" #include "../linalg/test_same_matrices.hpp" #include "mfem.hpp" #ifdef MFEM_USE_MPI using namespace mfem; using namespace mfem::future; using mfem::future::tensor; #ifdef MFEM_USE_ENZYME using dscalar_t = real_t; #else using mfem::future::dual; using dscalar_t = dual; #endif template void mass_action(const char *filename, int p) { constexpr int BDIM = DIM - 1; CAPTURE(filename, DIM, p); Mesh smesh(filename); ParMesh pmesh(MPI_COMM_WORLD, smesh); pmesh.EnsureNodes(); auto* nodes = static_cast(pmesh.GetNodes()); p = std::max(p, pmesh.GetNodalFESpace()->GetMaxElementOrder()); smesh.Clear(); H1_FECollection fec(p, DIM); ParFiniteElementSpace fes(&pmesh, &fec); ParGridFunction x(&fes), y(&fes), z(&fes); Vector X(fes.GetTrueVSize()), Y(fes.GetTrueVSize()), Z(fes.GetTrueVSize()); X.Randomize(1); x.SetFromTrueDofs(X); ConstantCoefficient one(1.0); SECTION("domain") { const auto *ir = &IntRules.Get(pmesh.GetTypicalElementGeometry(), 2 * p); Array all_domain_attr; if (pmesh.attributes.Size() > 0) { all_domain_attr.SetSize(pmesh.attributes.Max()); all_domain_attr = 1; } ParBilinearForm blf(&fes); blf.AddDomainIntegrator(new MassIntegrator(one, ir)); blf.SetAssemblyLevel(AssemblyLevel::PARTIAL); blf.Assemble(); blf.Mult(x, y); fes.GetProlongationMatrix()->MultTranspose(y, Y); static constexpr int U = 0, Coords = 1; const auto sol = std::vector{ FieldDescriptor{ U, &fes } }; DifferentiableOperator dop(sol, {{Coords, nodes->ParFESpace()}}, pmesh); const auto mf_mass_qf = [] MFEM_HOST_DEVICE(const real_t &u, const tensor &J, const real_t &w) { return tuple{u * w * det(J)}; }; dop.AddDomainIntegrator(mf_mass_qf, tuple{ Value{}, Gradient{}, Weight{} }, tuple{ Value{} }, *ir, all_domain_attr); dop.SetParameters({ nodes }); fes.GetRestrictionMatrix()->Mult(x, X); dop.Mult(X, Z); Y -= Z; real_t norm_g, norm_l = Y.Normlinf(); MPI_Allreduce(&norm_l, &norm_g, 1, MPI_DOUBLE, MPI_MAX, pmesh.GetComm()); REQUIRE(norm_g == MFEM_Approx(0.0)); MPI_Barrier(MPI_COMM_WORLD); } // Test boundary // This ensures that we're not trying to test on fully periodic meshes if (!((std::string("../../data/periodic-square.mesh").compare(filename) == 0) || (std::string("../../data/periodic-cube.mesh").compare(filename) == 0))) { SECTION("boundary") { const auto *ir = &IntRules.Get(pmesh.GetTypicalFaceGeometry(), 2 * p); Array all_bdr_attr; if (pmesh.bdr_attributes.Size() > 0) { all_bdr_attr.SetSize(pmesh.bdr_attributes.Max()); all_bdr_attr = 1; } ParBilinearForm blf(&fes); blf.AddBoundaryIntegrator(new MassIntegrator(one, ir)); blf.SetAssemblyLevel(AssemblyLevel::PARTIAL); blf.Assemble(); blf.Mult(x, y); fes.GetProlongationMatrix()->MultTranspose(y, Y); static constexpr int U = 0, Coords = 1; const auto sol = std::vector{FieldDescriptor{U, &fes}}; DifferentiableOperator dop(sol, {{Coords, nodes->ParFESpace()}}, pmesh); const auto mf_mass_qf = [] MFEM_HOST_DEVICE(const dscalar_t &u, const tensor &J, const real_t &w) { return tuple{u * weight(J) * w}; }; auto derivatives = std::integer_sequence {}; dop.AddBoundaryIntegrator(mf_mass_qf, tuple{ Value{}, Gradient{}, Weight{} }, tuple{ Value{} }, *ir, all_bdr_attr, derivatives); dop.SetParameters({nodes}); fes.GetRestrictionMatrix()->Mult(x, X); dop.Mult(X, Z); Y -= Z; real_t norm_g, norm_l = Y.Normlinf(); MPI_Allreduce(&norm_l, &norm_g, 1, MPI_DOUBLE, MPI_MAX, pmesh.GetComm()); REQUIRE(norm_g == MFEM_Approx(0.0)); auto dRdU = dop.GetDerivative(U, {&x}, {nodes}); dRdU->Mult(X, Z); fes.GetProlongationMatrix()->MultTranspose(y, Y); Y -= Z; norm_l = Y.Normlinf(); MPI_Allreduce(&norm_l, &norm_g, 1, MPI_DOUBLE, MPI_MAX, pmesh.GetComm()); REQUIRE(norm_g == MFEM_Approx(0.0)); MPI_Barrier(MPI_COMM_WORLD); } } } template void mass_mat_mixed(const char* filename, int p) { CAPTURE(filename, DIM, p); Mesh smesh(filename); ParMesh pmesh(MPI_COMM_WORLD, smesh); pmesh.EnsureNodes(); auto* nodes = static_cast(pmesh.GetNodes()); p = std::max(p, pmesh.GetNodalFESpace()->GetMaxElementOrder()); smesh.Clear(); Array all_domain_attr; if (pmesh.attributes.Size() > 0) { all_domain_attr.SetSize(pmesh.attributes.Max()); all_domain_attr = 1; } H1_FECollection fec0(p, DIM); H1_FECollection fec1(p + 1, DIM); ParFiniteElementSpace fes0(&pmesh, &fec0); ParFiniteElementSpace fes1(&pmesh, &fec1); const auto* ir = &IntRules.Get(pmesh.GetTypicalElementGeometry(), 2 * p); ConstantCoefficient one(1.0); ParMixedBilinearForm blf(&fes1, &fes0); blf.AddDomainIntegrator(new MassIntegrator(one, ir)); blf.SetAssemblyLevel(AssemblyLevel::FULL); blf.Assemble(); blf.Finalize(); blf.SpMat().Finalize(); static constexpr int U = 0, P = 1, Coords = 2; const auto sol = std::vector{FieldDescriptor{U, &fes1}}; DifferentiableOperator dop(sol, {{P, &fes0}, {Coords, nodes->ParFESpace()}}, pmesh); const auto mf_mass_qf = [] MFEM_HOST_DEVICE( const dscalar_t& u, const tensor& J, const real_t& w) { return tuple{u * w * det(J)}; }; auto derivatives = std::integer_sequence {}; dop.AddDomainIntegrator(mf_mass_qf, tuple{Value{}, Gradient{}, Weight{}}, tuple{Value

{}}, *ir, all_domain_attr, derivatives); ParGridFunction ugf(&fes1); ugf = 0.0; ParGridFunction pgf(&fes0); pgf = 0.0; dop.SetParameters({&pgf, nodes}); auto ddopdu = dop.GetDerivative(U, {&ugf}, {&pgf, nodes}); SECTION("spmat") { SparseMatrix *A; ddopdu->Assemble(A); TestSameMatrices(*A, blf.SpMat()); delete A; } SECTION("hypre parallel mat") { HypreParMatrix *Amfem = blf.ParallelAssemble(); HypreParMatrix *Adfem; ddopdu->Assemble(Adfem); TestSameMatrices(*Adfem, *Amfem); delete Amfem; delete Adfem; } } // no GPU tag to avoid failing 'hypre parallel mat' section TEST_CASE("dFEM Mass", "[Parallel][dFEM]") { const bool all_tests = launch_all_non_regression_tests; const auto p = !all_tests ? 2 : GENERATE(1, 2, 3); SECTION("2d") { const auto filename2d = GENERATE( "../../data/star.mesh", "../../data/star-q3.mesh", "../../data/rt-2d-q3.mesh", "../../data/inline-quad.mesh", "../../data/periodic-square.mesh" ); mass_action<2>(filename2d, p); mass_mat_mixed<2>(filename2d, p); } SECTION("3d") { const auto filename3d = GENERATE( "../../data/fichera.mesh", "../../data/fichera-q3.mesh", "../../data/inline-hex.mesh", "../../data/toroid-hex.mesh", "../../data/periodic-cube.mesh" ); mass_action<3>(filename3d, p); mass_mat_mixed<3>(filename3d, p); } } #endif // MFEM_USE_MPI