// 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" #include "linalg/tensor_arrays.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 constexpr int DIM = 2; class DummyParameterSpace : public ParameterSpace { public: class Bimpl : public Operator { virtual void Mult(const Vector &x, Vector &y) const { for (int i = 0; i < y.Size(); i++) { y(i) = x(0); } } }; class Btimpl : public Operator { virtual void Mult(const Vector &x, Vector &y) const { y(0) = x(0); } }; DummyParameterSpace() : ParameterSpace(1) {} virtual int GetTrueVSize() const override { return 1; } virtual int GetVSize() const override { return 1; } virtual const Operator* GetB() const override { if (!B) { B.reset(new Bimpl()); } return B.get(); } virtual const Operator* GetBt() const override { if (!Bt) { Bt.reset(new Btimpl()); } return Bt.get(); } }; struct massqf { inline MFEM_HOST_DEVICE void operator()( tensor_array &u, tensor_array &J, tensor_array &w, tensor_array &out1, tensor_array &out2) const { for (size_t q = 0; q < u.size(); q++) { const auto v = u(q) * det(J(q)) * w(q); out1(q) = v; out2(q) = v; } } }; struct mass_diffusion_qdata_qf { inline MFEM_HOST_DEVICE void operator()( tensor_array &u, tensor_array &dudxi, tensor_array &J, tensor_array &qdata, tensor_array &w, tensor_array &dummy_parameter, tensor_array &out1, tensor_array &out2, tensor_array &out3) const { for (size_t q = 0; q < u.size(); q++) { const auto invJq = inv(J(q)); const auto detJq = det(J(q)); out1(q) = u(q) * detJq * w(q); // out2(q) = (dudxi(q) * invJq) * transpose(invJq) * (detJq * w(q)); out3(q) = J(q); } jit_bounds(dudxi, J, w, out2, u.size()); } // XXX: Attribute instrumentation does not work due to ABI differences that // change the argument number. //__attribute__((annotate("jit", 5))) void jit_bounds( tensor_array &dudxi, tensor_array &J, tensor_array &w, tensor_array &out, size_t NQ) const { for (size_t q = 0; q < NQ; q++) { const auto invJq = inv(J(q)); const auto detJq = det(J(q)); out(q) = (dudxi(q) * invJq) * transpose(invJq) * (detJq * w(q)); } } }; struct massqflocal { inline MFEM_HOST_DEVICE void operator()( const tensor &u, const tensor &J, const tensor &w, tensor &out1, tensor &out2) const { const auto v = u * det(J) * w; out1 = v; out2 = v; } }; TEST_CASE("dFEM Multiple Outputs", "[Parallel][dFEM]") { const bool all_tests = launch_all_non_regression_tests; const auto p = !all_tests ? 2 : GENERATE(1, 2, 3); const char *filename = "../../data/inline-quad.mesh"; CAPTURE(filename, DIM, p); Mesh smesh(filename); MFEM_ASSERT(smesh.Dimension() == DIM, "DIM and mesh dimension have to match"); ParMesh pmesh(MPI_COMM_WORLD, smesh); pmesh.EnsureNodes(); auto* nodes = static_cast(pmesh.GetNodes()); smesh.Clear(); H1_FECollection fec(p, DIM); ParFiniteElementSpace fes(&pmesh, &fec); const auto *ir = &IntRules.Get(pmesh.GetTypicalElementGeometry(), 2 * p); ParGridFunction x(&fes), y(&fes), z(&fes); ConstantCoefficient one(1.0); Array all_domain_attr; if (pmesh.attributes.Size() > 0) { all_domain_attr.SetSize(pmesh.attributes.Max()); all_domain_attr = 1; } // { // Array inoffsets(3); // inoffsets[0] = 0; // inoffsets[1] = fes.GetTrueVSize(); // inoffsets[2] = nodes->ParFESpace()->GetTrueVSize(); // inoffsets.PartialSum(); // BlockVector X(inoffsets); // X.GetBlock(0).Randomize(1); // X.GetBlock(1) = *nodes; // x.SetFromTrueDofs(X.GetBlock(0)); // Array outoffsets(2); // outoffsets[0] = 0; // outoffsets[1] = fes.GetTrueVSize(); // outoffsets.PartialSum(); // BlockVector Z(outoffsets); // ParBilinearForm blf(&fes); // blf.AddDomainIntegrator(new MassIntegrator(one, ir)); // blf.SetAssemblyLevel(AssemblyLevel::PARTIAL); // blf.Assemble(); // blf.Mult(x, y); // Vector Y(fes.GetTrueVSize()); // fes.GetProlongationMatrix()->MultTranspose(y, Y); // static constexpr int U = 0, COORDINATES = 1, V = 2; // const std::vector in // { // {U, &fes}, // {COORDINATES, nodes->ParFESpace()} // }; // const std::vector out // test spaces? // { // {V, &fes}, // }; // DifferentiableOperator dop(in, out, pmesh); // auto derivatives = std::integer_sequence {}; // auto mass_qfunc = massqf{}; // dop.AddDomainIntegrator(mass_qfunc, // tuple{ Value{}, Gradient{}, Weight{} }, // tuple{ Value{}, Value{} }, // *ir, all_domain_attr, derivatives); // fes.GetRestrictionMatrix()->Mult(x, X.GetBlock(0)); // dop.Mult(X, Z); // Vector Y0(Y); // Y0 *= 2.0; // Y0 -= Z.GetBlock(0); // real_t norm_g, norm_l = Y0.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); // auto ddop = dop.GetDerivative(U, X); // ddop->Mult(X.GetBlock(0), Z); // Y0 = Y; // Y0 *= 2.0; // Y0 -= Z.GetBlock(0); // norm_l = Y0.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); // } { QuadratureSpace qs(pmesh, *ir); QuadratureFunction qdata(qs, DIM*DIM); DummyParameterSpace dps; ParameterFunction dpf(dps); dpf = 9.12345; auto coef_func = [](const Vector &coords) { return coords[0] * coords[1] * (DIM == 3 ? coords[2] : 1.0); }; FunctionCoefficient coef(coef_func); x.ProjectCoefficient(coef); Vector xtvec, ytvec, ytvecmfem; x.GetTrueDofs(xtvec); ytvec.SetSize(xtvec.Size()); ytvecmfem.SetSize(xtvec.Size()); Vector nodestvec; nodes->GetTrueDofs(nodestvec); qdata = 123.0; Vector yqdata(qdata.Size()); MultiVector X{xtvec, nodestvec, qdata, dpf}; MultiVector Z{ytvec, yqdata}; ParBilinearForm blf(&fes); blf.AddDomainIntegrator(new MassIntegrator(ir)); blf.AddDomainIntegrator(new DiffusionIntegrator(ir)); blf.SetAssemblyLevel(AssemblyLevel::PARTIAL); blf.Assemble(); blf.Mult(x, y); fes.GetProlongationMatrix()->MultTranspose(y, ytvecmfem); static constexpr int U = 0, COORDINATES = 1, V = 2, S = 3, L = 4; const std::vector in { {U, &fes}, {COORDINATES, nodes->ParFESpace()}, {S, &qdata}, {L, &dps} }; const std::vector out { {V, &fes}, {S, &qdata} }; { DifferentiableOperator dop(in, out, pmesh); dop.SetQLayouts({{Value{}, {1, 0}}}, {}); auto derivatives = std::integer_sequence {}; auto mass_diffusion_qfunc = mass_diffusion_qdata_qf{}; dop.AddDomainIntegrator( mass_diffusion_qfunc, tuple{Value{}, Gradient{}, Gradient{}, Identity{}, Weight{}, Value{}}, tuple{Value{}, Gradient{}, Identity{}}, *ir, all_domain_attr, derivatives); fes.GetRestrictionMatrix()->Mult(x, xtvec); dop.Mult(X, Z); Vector Y0(ytvecmfem); Y0 -= Z[0]; real_t norm_l = Y0.Normlinf(); real_t norm_g = norm_l; 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); auto ddop = dop.GetDerivative(U, X); ddop->Mult(X[0], Z); Y0 = ytvecmfem; Y0 -= Z[0]; norm_l = Y0.Normlinf(); norm_g = norm_l; 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); } { static constexpr int W = 0; ParBilinearForm blf(&fes); blf.AddDomainIntegrator(new MassIntegrator(ir)); blf.SetAssemblyLevel(AssemblyLevel::PARTIAL); blf.Assemble(); blf.Mult(x, y); fes.GetProlongationMatrix()->MultTranspose(y, ytvecmfem); const std::vector in { {U, &fes}, {COORDINATES, nodes->ParFESpace()}, }; const std::vector out { {V, &fes}, {W, &fes}, }; DifferentiableOperator dop(in, out, pmesh); auto mass_qfunclocal = massqflocal{}; dop.AddDomainIntegrator( mass_qfunclocal, tuple{Value{}, Gradient{}, Weight{}}, tuple{Value{}, Value{}}, *ir, all_domain_attr); Vector nodestv; nodes->GetTrueDofs(nodestv); fes.GetRestrictionMatrix()->Mult(x, xtvec); Vector ztvec(xtvec.Size()); Vector zztvec(xtvec.Size()); MultiVector X{xtvec, nodestv}; MultiVector Z{ztvec, zztvec}; dop.Mult(X, Z); Vector Y0(ytvecmfem); Y0 -= Z[0]; real_t norm_l = Y0.Normlinf(); real_t norm_g = norm_l; MPI_Allreduce(&norm_l, &norm_g, 1, MPI_DOUBLE, MPI_MAX, pmesh.GetComm()); REQUIRE(norm_g == MFEM_Approx(0.0)); Vector Y1(ytvecmfem); Y1 -= Z[1]; norm_l = Y1.Normlinf(); norm_g = norm_l; 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); } } } #endif // MFEM_USE_MPI