// 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 "mfem.hpp" #include "../fem/dfem/doperator.hpp" #include "../fem/dfem/backends/local_qf/prelude.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 vectordivergence(const char *filename, int p) { CAPTURE(filename, DIM, p); Mesh smesh(filename); ParMesh pmesh(MPI_COMM_WORLD, smesh); MFEM_VERIFY(pmesh.Dimension() == DIM, "Mesh dimension mismatch"); 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 fec(p, DIM); ParFiniteElementSpace psfes(&pmesh, &fec); ParFiniteElementSpace pvfes(&pmesh, &fec, DIM); const int q = 3 * p + 1; const auto *ir = &IntRules.Get(pmesh.GetTypicalElementGeometry(), q); ParGridFunction xv(&pvfes); ParGridFunction ys(&psfes), sz(&psfes); Vector Xv(pvfes.GetTrueVSize()); Vector Ys(psfes.GetTrueVSize()), Zs(psfes.GetTrueVSize()); Xv.Randomize(1), xv.SetFromTrueDofs(Xv); MixedBilinearForm mblf_fa(&pvfes, &psfes); mblf_fa.AddDomainIntegrator(new VectorDivergenceIntegrator); mblf_fa.Assemble(), mblf_fa.Finalize(); mblf_fa.Mult(xv, ys); { static constexpr int P = 0, V = 1, Coords = 2; ParFiniteElementSpace *mfes = nodes->ParFESpace(); const auto inputs = std::vector { FieldDescriptor{V, &pvfes}, FieldDescriptor{Coords, mfes} }; const auto outputs = std::vector { FieldDescriptor{P, &psfes} }; DifferentiableOperator dop_mf(inputs, outputs, pmesh); const auto mf_vector_divergence_qf = [] MFEM_HOST_DEVICE(const tensor &dudxi, const tensor &J, const real_t &w, real_t &v) { const auto invJ = inv(J); const auto dudx = dudxi * invJ; v = tr(dudx) * det(J) * w; }; const auto derivatives = std::integer_sequence {}; dop_mf.AddDomainIntegrator( mf_vector_divergence_qf, tuple{Gradient{}, Gradient{}, Weight{}}, tuple{Value

{}}, *ir, all_domain_attr, derivatives); SECTION("Action") { Vector nodestv; nodes->GetTrueDofs(nodestv); MultiVector X{Xv, nodestv}; MultiVector Z{Zs}; dop_mf.Mult(X, Z); mblf_fa.Mult(xv, ys); psfes.GetProlongationMatrix()->MultTranspose(ys, Ys); Ys -= Zs; real_t norm_global = 0.0; real_t norm_local = Ys.Normlinf(); MPI_Allreduce(&norm_local, &norm_global, 1, MPI_DOUBLE, MPI_MAX, pmesh.GetComm()); REQUIRE(norm_global == MFEM_Approx(0.0)); MPI_Barrier(MPI_COMM_WORLD); } SECTION("Derivative Action") { Vector nodestv; nodes->GetTrueDofs(nodestv); MultiVector X{Xv, nodestv}; MultiVector Z{Zs}; auto dRdV = dop_mf.GetDerivative(V, X); dRdV->Mult(X[0], Z); mblf_fa.Mult(xv, ys); psfes.GetProlongationMatrix()->MultTranspose(ys, Ys); Ys -= Zs; real_t norm_global = 0.0; real_t norm_local = Ys.Normlinf(); MPI_Allreduce(&norm_local, &norm_global, 1, MPI_DOUBLE, MPI_MAX, pmesh.GetComm()); REQUIRE(norm_global == MFEM_Approx(0.0)); MPI_Barrier(MPI_COMM_WORLD); } SECTION("Derivative Transpose Action") { Vector nodestv; nodes->GetTrueDofs(nodestv); // Build cache with full primal state MultiVector state{Xv, nodestv}; auto dRdV = dop_mf.GetDerivative(V, state); // Direction in output (test) T-space: use Ys computed from mblf_fa. psfes.GetProlongationMatrix()->MultTranspose(ys, Ys); MultiVector direction{Ys}; // Result in derivative (trial) T-space. Vector result_v(pvfes.GetTrueVSize()); result_v = 0.0; MultiVector result{result_v}; dRdV->MultTranspose(direction, result); // Reference: mblf_fa.MultTranspose(ys, xv) -> restrict to T-dofs. mblf_fa.MultTranspose(ys, xv); Vector ref_v(pvfes.GetTrueVSize()); pvfes.GetProlongationMatrix()->MultTranspose(xv, ref_v); result_v -= ref_v; real_t norm_global = 0.0; real_t norm_local = result_v.Normlinf(); MPI_Allreduce(&norm_local, &norm_global, 1, MPI_DOUBLE, MPI_MAX, pmesh.GetComm()); REQUIRE(norm_global == MFEM_Approx(0.0)); MPI_Barrier(MPI_COMM_WORLD); } } } TEST_CASE("dFEM VectorDivergence", "[Parallel][dFEM]") { const bool all_tests = launch_all_non_regression_tests; const auto p = !all_tests ? 2 : GENERATE(1, 2, 3); SECTION("2D p=" + std::to_string(p)) { const auto filename = GENERATE("../../data/star.mesh", "../../data/star-q3.mesh", "../../data/rt-2d-q3.mesh", "../../data/inline-quad.mesh", "../../data/periodic-square.mesh"); vectordivergence<2>(filename, p); } SECTION("3D p=" + std::to_string(p)) { const auto filename = GENERATE("../../data/fichera.mesh", "../../data/fichera-q3.mesh", "../../data/inline-hex.mesh", "../../data/toroid-hex.mesh", "../../data/periodic-cube.mesh"); vectordivergence<3>(filename, p); } } #endif // MFEM_USE_MPI