// 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. // // ------------------------------------- // Minimal Surface 2D Problem with dFEM // ------------------------------------- // // Compile with: make dfem-minimal-surface // // Sample runs: mpirun -np 4 dfem-minimal-surface -der 0 // mpirun -np 4 dfem-minimal-surface -der 0 -o 2 // mpirun -np 4 dfem-minimal-surface -der 0 -r 1 // mpirun -np 4 dfem-minimal-surface -der 0 -o 2 -r 4 -pcamg // mpirun -np 4 dfem-minimal-surface -der 1 // mpirun -np 4 dfem-minimal-surface -der 2 // // Device sample runs: // mpirun -np 4 dfem-minimal-surface -der 0 -r 1 -o 2 -d cuda // mpirun -np 4 dfem-minimal-surface -der 1 -r 1 -o 2 -d cuda // * mpirun -np 4 dfem-minimal-surface -der 0 -r 1 -o 2 -d hip // * mpirun -np 4 dfem-minimal-surface -der 1 -r 1 -o 2 -d hip // // Description: This example code demonstrates the use of MFEM to solve the // minimal surface problem in 2D: // // $ \min \left( -\nabla \cdot (1 / \sqrt(1 + |\nabla u|^2) \nabla u) \right) $ // // with Dirichlet boundary conditions. The nonlinear problem is // solved using Newton's method, where the necessary derivatives // are computed in one of three ways (controlled by -der command // line parameter): // // -der 0 = Automatic differentiation using Enzyme or dual type // (default) // -der 1 = Hand-coded derivatives // -der 2 = Finite differences // // The example demonstrates the use of MFEM's nonlinear solvers, // automatic differentiation capabilities, and GLVis/ParaView // visualization. #include "mfem.hpp" using namespace mfem; // This example code demonstrates the use of new features in MFEM that are in // development but exposed through the mfem::future namespace. All features // under this namespace might change their interface or behavior in upcoming // releases until they have stabilized. using namespace mfem::future; using mfem::future::tensor; // Derivative type enum // This enum is used to specify the type of derivative computation. // Possibilities are: // - AUTODIFF, which uses automatic differentiation (Enzyme or dual type), // - HANDCODED, which uses a manually implemented derivative, and // - FD, finite difference. enum DerivativeType { AUTODIFF, HANDCODED, FD }; // Minimal surface operator. // // This class implements the minimal surface equation, which is a nonlinear // operator that provides the residual. template class MinimalSurface : public Operator { private: static constexpr int SOLUTION_U = 1; static constexpr int MESH_NODES = 2; template MFEM_HOST_DEVICE inline static auto coeff(const tensor &a) { return 1_r / sqrt(1_r + sqnorm(a)); } // The 'nvcc' compiler needs MFApply and ManualDerivativeApply to be public. public: // Matrix-Free version of the pointwise residual form for the minimal // surface equation. struct MFApply { // Using DifferentiableOperator, we can define the residual form as a // matrix-free operation. This allows us to compute the residual without // explicitly forming any matrices or other large, temporary data // structures. // // The inputs are the gradient of the solution in *reference coordinates*, // the Jacobian of the coordinates, and the integration rule weights. // // The output is the residual in *physical coordinates* which also // includes the necessary transformation from reference to physical // coordinates for the gradient of the test function. // // Due to the description of how this pointwise operation is used in // DifferentiableOperator, we know it is applied to the gradient of the // test function in reference coordinates e.g. // $ \int coeff(\nabla_x u) (\nabla_x u) J^{-T} \det(J) w // (\nabla_{\xi} v) d\xi $ MFEM_HOST_DEVICE inline auto operator()( const tensor &dudxi, const tensor &J, const real_t &w) const { const auto invJ = inv(J); const auto dudx = dudxi * invJ; return tuple{coeff(dudx) * dudx * transpose(invJ) * det(J) * w}; } }; // This is the derivative of the residual form with respect to the // solution $u$. // // The inputs and outputs follow the same rules as the MFApply operator. struct ManualDerivativeApply { MFEM_HOST_DEVICE inline auto operator()( const tensor &ddelta_udxi, const tensor &dudxi, const tensor &J, const real_t &w) const { const auto invJ = inv(J); const auto dudx = dudxi * invJ; const auto ddelta_udx = ddelta_udxi * invJ; const auto c = coeff(dudx); const auto term1 = c * ddelta_udx; const auto term2 = c * c * c * dot(dudx, ddelta_udx) * dudx; return tuple{(term1 - term2) * transpose(invJ) * det(J) * w}; } }; // This class implements the Jacobian of the minimal surface operator. It // mostly acts as a wrapper to retrieve the Jacobian and apply essential // boundary conditions appropriately. class MinimalSurfaceJacobian : public Operator { public: MinimalSurfaceJacobian(const MinimalSurface *minsurface, const Vector &x) : Operator(minsurface->Height()), minsurface(minsurface), z(minsurface->Height()) { minsurface->u.SetFromTrueDofs(x); auto mesh_nodes = static_cast (minsurface->H1.GetParMesh()->GetNodes()); // One can retrieve the derivative of a DifferentiableOperator wrt a // field variable if the derivative has been requested during the // DifferentiableOperator::AddDomainIntegrator call. dres_du = minsurface->res->GetDerivative( SOLUTION_U, {&minsurface->u}, {mesh_nodes}); } void Mult(const Vector &x, Vector &y) const override { z = x; z.SetSubVector(minsurface->ess_tdofs, 0.0); dres_du->Mult(z, y); auto d_y = y.ReadWrite(); const auto d_x = x.Read(); const auto d_dofs = minsurface->ess_tdofs.Read(); mfem::forall(minsurface->ess_tdofs.Size(), [=] MFEM_HOST_DEVICE (int i) { d_y[d_dofs[i]] = d_x[d_dofs[i]]; }); } // Pointer to the wrapped MinimalSurface operator const MinimalSurface *minsurface = nullptr; // Pointer to the DifferentiableOperator that computes the Jacobian std::shared_ptr dres_du; // Temporary vector mutable Vector z; }; // This class implements the Jacobian of the minimal surface operator using // manually computed derivatives. class MinimalSurfaceHandcodedJacobian : public Operator { // For the Jacobian action we need another field ID for the direction // of u, called du, in dR/du = J * du. static constexpr int DIRECTION_U = 3; public: MinimalSurfaceHandcodedJacobian(const MinimalSurface *minsurface, const Vector &x) : Operator(minsurface->Height()), minsurface(minsurface), z(minsurface->Height()) { Array all_domain_attr(minsurface->H1.GetMesh()->attributes.Max()); all_domain_attr = 1; auto &mesh_nodes = *static_cast (minsurface->H1.GetParMesh()->GetNodes()); auto &mesh_nodes_fes = *mesh_nodes.ParFESpace(); std::vector solutions = { {DIRECTION_U, &minsurface->H1} }; std::vector parameters = { {SOLUTION_U, &minsurface->H1}, {MESH_NODES, &mesh_nodes_fes} }; dres_du = std::make_shared( solutions, parameters, *minsurface->H1.GetParMesh()); auto input_operators = tuple { Gradient{}, Gradient{}, Gradient{}, Weight{} }; auto output_operators = tuple { Gradient{} }; ManualDerivativeApply manual_derivative_apply; dres_du->AddDomainIntegrator(manual_derivative_apply, input_operators, output_operators, minsurface->ir, all_domain_attr); minsurface->u.SetFromTrueDofs(x); dres_du->SetParameters({&minsurface->u, &mesh_nodes}); } void Mult(const Vector &x, Vector &y) const override { z = x; z.SetSubVector(minsurface->ess_tdofs, 0.0); dres_du->Mult(z, y); auto d_y = y.HostReadWrite(); const auto d_x = x.HostRead(); for (int i = 0; i < minsurface->ess_tdofs.Size(); i++) { d_y[minsurface->ess_tdofs[i]] = d_x[minsurface->ess_tdofs[i]]; } } const MinimalSurface *minsurface = nullptr; std::shared_ptr dres_du; mutable Vector z; }; public: MinimalSurface(ParFiniteElementSpace &H1, const IntegrationRule &ir, int deriv_type = AUTODIFF) : Operator(H1.GetTrueVSize(), H1.GetTrueVSize()), H1(H1), ir(ir), u(&H1), derivative_type(deriv_type) { Array all_domain_attr(H1.GetMesh()->attributes.Max()); all_domain_attr = 1; auto &mesh_nodes = *static_cast(H1.GetParMesh()->GetNodes()); auto &mesh_nodes_fes = *mesh_nodes.ParFESpace(); // The following section is the heart of this example. It shows how to // create and interact with the DifferentialOperator class. // The constructor of DifferentiableOperator takes two vectors of // FieldDescriptors. A FieldDescriptor can be viewed as a a pair of an // identifier (the field ID) and it's accompanying space. std::vector solutions; solutions.push_back(FieldDescriptor(SOLUTION_U, &H1)); std::vector parameters; parameters.push_back(FieldDescriptor(MESH_NODES, &mesh_nodes_fes)); // Create the DifferentiableOperator on the desired mesh. res = std::make_shared( solutions, parameters, *H1.GetParMesh()); // DifferentiableOperator::AddIntegrator consists mainly of multiple // components. The input and output operators and the pointwise // "quadrature function" form a description of how the inputs and outputs // to the pointwise function have to be treated. // The input operators tuple consists of derived FieldOperator types. // Here, we use Gradient to signal that we request the gradient // on the reference coordinates of the FIELD_ID field to be interpolated // and translated to the pointwise function as the first and second input. // Other choices are possible, e.g. Value to interpolate the // pointwise funciton. `Weight` is a special field that translates the // integration rule weights to the input of the pointwise function. auto input_operators = tuple { Gradient{}, Gradient{}, Weight{} }; // The output operators tuple also consists of derived FieldOperator // types. Currently, only _one_ output operator is allowed. One should // think of this as an operator on the output of a pointwise function. For // example with the above input operators and the output operator below we // create the following operator sequence: // // $ B^T D(B u, B x, w) $ // // where B is the gradient interpolation operator, D is the pointwise // function and u and x are solution and coordinate functions, // respectively. The output operator is the gradient of the basis of the // solution, which completes the "diffusion" like weak form. auto output_operators = tuple { Gradient{} }; // The pointwise function is defined as a lambda function. Here we just // instantiate an object for it which is passed to // DifferentiableOperator::AddDomainIntegrator. MFApply mf_apply_qf; // The integeger sequence is used to specify which derivatives of the // formed integrator should be formed. This is necessary to specify at // compile time in order to instantiate the correct functions. auto derivatives = std::integer_sequence {}; res->AddDomainIntegrator(mf_apply_qf, input_operators, output_operators, ir, all_domain_attr, derivatives); // Before we are able to use DifferentiableOperator::Mult, we need to call // DifferentiableOperator::SetParameters to set the parameters of the // operator. Here, only the mesh node function is required. We do this // here once, because we know that the nodes won't change. If they do, // we'd have to call SetParameters before each call to Mult. This is done // to be mathematically consistent with fixing paramaters. res->SetParameters({&mesh_nodes}); Array ess_bdr(H1.GetParMesh()->bdr_attributes.Max()); ess_bdr = 1; H1.GetEssentialTrueDofs(ess_bdr, ess_tdofs); } void Mult(const Vector &x, Vector &y) const override { res->Mult(x, y); y.SetSubVector(ess_tdofs, 0.0); } Operator& GetGradient(const Vector &x) const override { switch (derivative_type) { case FD: fd_jac = std::make_shared(*this, x); return *fd_jac; case HANDCODED: { man_dres_du = std::make_shared( this, x); return *man_dres_du; } case AUTODIFF: default: dres_du = std::make_shared(this, x); return *dres_du; } } std::shared_ptr GetJacobian() { return dres_du; } const Array& GetEssentialTrueDofs() const { return ess_tdofs; } private: ParFiniteElementSpace &H1; const IntegrationRule &ir; mutable ParGridFunction u; Array ess_tdofs; std::shared_ptr res; mutable std::shared_ptr dres_du; mutable std::shared_ptr man_dres_du; mutable std::shared_ptr fd_jac; int derivative_type; }; template class AMGPC : public Solver { public: AMGPC(Operator &op) : Solver(op.Height()), op(op) {} void SetOperator(const Operator &) override { auto minsurface = static_cast&>(op); // We leverage dFEM to assemble the Jacobian of the minimal surface // operator into a HypreParMatrix. delete A; A = nullptr; minsurface.GetJacobian()->dres_du->Assemble(A); auto Ae = A->EliminateRowsCols(minsurface.GetEssentialTrueDofs()); delete Ae; amg.SetPrintLevel(0); amg.SetOperator(*A); } void Mult(const Vector &x, Vector &y) const override { amg.Mult(x, y); } ~AMGPC() { delete A; } private: Operator &op; HypreParMatrix *A = nullptr; HypreBoomerAMG amg; }; // Boundary function for the minimal surface problem described by the Scherk // surface. // See https://en.wikipedia.org/wiki/Scherk_surface for more details. real_t boundary_func(const Vector &coords) { const real_t x = coords(0); const real_t y = coords(1); if (coords.Size() == 3) { MFEM_ABORT("internal error"); } const real_t a = 1.0e-2; return log(cos(a * x) / cos(a * y)) / a; } int main(int argc, char *argv[]) { // 1. Initialize MPI and HYPRE Mpi::Init(); Hypre::Init(); // 2. Parse command-line options int order = 1; const char *device_config = "cpu"; bool visualization = true; int refinements = 0; int derivative_type = AUTODIFF; bool enable_pcamg = false; OptionsParser args(argc, argv); args.AddOption(&order, "-o", "--order", "Finite element order (polynomial degree)."); args.AddOption(&device_config, "-d", "--device", "Device configuration string, see Device::Configure()."); args.AddOption(&visualization, "-vis", "--visualization", "-no-vis", "--no-visualization", "Enable or disable GLVis visualization."); args.AddOption(&refinements, "-r", "--refinements", ""); args.AddOption(&derivative_type, "-der", "--derivative-type", "Derivative computation type: 0=AutomaticDifferentiation," " 1=HandCoded, 2=FiniteDifference"); args.AddOption(&enable_pcamg, "-pcamg", "--pcamg", "-no-pcamg", "--no-pcamg", "Enable AMG as a preconditioner when using automatic differentiation."); args.ParseCheck(); // 3. Enable hardware devices such as GPUs, and programming models such as // CUDA Device device(device_config); if (Mpi::Root()) { device.Print(); } // 4. Create a 2D mesh on the square domain [-π/2,π/2]^2 Mesh mesh = Mesh::MakeCartesian2D(4, 4, Element::QUADRILATERAL); mesh.SetCurvature(order); auto transform_mesh = [](const Vector &cold, Vector &cnew) { cnew = cold; cnew -= 0.5; cnew *= M_PI; }; mesh.Transform(transform_mesh); // 5. Refine the mesh to increase the resolution for (int i = 0; i < refinements; i++) { mesh.UniformRefinement(); } int dim = mesh.Dimension(); // 6. Define a parallel mesh ParMesh pmesh(MPI_COMM_WORLD, mesh); mesh.Clear(); // 7. Define a parallel finite element space on the parallel mesh H1_FECollection fec(order, dim); ParFiniteElementSpace H1(&pmesh, &fec); // 8. Set up the integration rule const auto *ir = &IntRules.Get(pmesh.GetTypicalElementGeometry(), 2 * order + 1); ParGridFunction u(&H1); Vector X(H1.GetTrueVSize()); // 9. Create the nonlinear operator for the minimal surface equation std::unique_ptr minsurface; #ifdef MFEM_USE_ENZYME // When Enzyme is available, use it for automatic differentiation minsurface = std::make_unique>(H1, *ir, derivative_type); #else // When Enzyme is not available, use the dual type for automatic // differentiation using mfem::future::dual; using dual_t = dual; minsurface = std::make_unique>(H1, *ir, derivative_type); #endif // 10. Set up and apply the boundary conditions Array ess_bdr(H1.GetParMesh()->bdr_attributes.Max()); ess_bdr = 1; // 11. Set up the essential boundary conditions and initial condition FunctionCoefficient boundary_coeff(boundary_func); u.ProjectCoefficient(boundary_coeff); u *= 1e-2; u.ProjectBdrCoefficient(boundary_coeff, ess_bdr); // 12. Set up the linear solver to be used within Newton's method CGSolver krylov(MPI_COMM_WORLD); krylov.SetAbsTol(0.0); krylov.SetRelTol(1e-4); krylov.SetMaxIter(500); krylov.SetPrintLevel(2); std::shared_ptr> amgpc; if (enable_pcamg) { if (derivative_type == AUTODIFF) { amgpc.reset(new AMGPC(*minsurface)); krylov.SetPreconditioner(*amgpc); } else { MFEM_ABORT("AMG only available for the AUTODIFF derivative type"); } } // 13. Set up the nonlinear solver (Newton) for the minimal surface equation NewtonSolver newton(MPI_COMM_WORLD); newton.SetOperator(*minsurface); newton.SetAbsTol(0.0); newton.SetRelTol(1e-6); newton.SetMaxIter(10); newton.SetSolver(krylov); newton.SetPrintLevel(1); // 14. Solve the nonlinear system using Newton's method H1.GetRestrictionMatrix()->Mult(u, X); Vector zero; newton.Mult(zero, X); H1.GetProlongationMatrix()->Mult(X, u); // 15. Send the solution by socket to a GLVis server. if (visualization) { char vishost[] = "localhost"; int visport = 19916; socketstream sol_sock(vishost, visport); sol_sock << "parallel " << Mpi::WorldSize() << " " << Mpi::WorldRank() << "\n"; sol_sock.precision(8); sol_sock << "solution\n" << pmesh << u << std::flush; } // 16. Save the solution in parallel using ParaView format ParaViewDataCollection dc("dfem-minimal-surface-output", &pmesh); dc.SetHighOrderOutput(true); dc.SetLevelsOfDetail(order); dc.RegisterField("solution", &u); dc.SetCycle(0); dc.Save(); return 0; }