// 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 <typename dscalar_t, int dim = 2>
class MinimalSurface : public Operator
{
private:
   static constexpr int SOLUTION_U = 1;
   static constexpr int MESH_NODES = 2;

   template <typename T>
   MFEM_HOST_DEVICE inline
   static auto coeff(const tensor<T, dim> &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<dscalar_t, dim> &dudxi,
         const tensor<real_t, dim, dim> &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<real_t, dim> &ddelta_udxi,
         const tensor<real_t, dim> &dudxi,
         const tensor<real_t, dim, dim> &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<ParGridFunction*>
                           (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<DerivativeOperator> 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<int> all_domain_attr(minsurface->H1.GetMesh()->attributes.Max());
         all_domain_attr = 1;

         auto &mesh_nodes = *static_cast<ParGridFunction *>
                            (minsurface->H1.GetParMesh()->GetNodes());
         auto &mesh_nodes_fes = *mesh_nodes.ParFESpace();

         std::vector<FieldDescriptor> solutions =
         {
            {DIRECTION_U, &minsurface->H1}
         };
         std::vector<FieldDescriptor> parameters =
         {
            {SOLUTION_U, &minsurface->H1},
            {MESH_NODES, &mesh_nodes_fes}
         };

         dres_du = std::make_shared<DifferentiableOperator>(
                      solutions, parameters, *minsurface->H1.GetParMesh());

         auto input_operators = tuple
         {
            Gradient<DIRECTION_U>{},
            Gradient<SOLUTION_U>{},
            Gradient<MESH_NODES>{},
            Weight{}
         };

         auto output_operators = tuple
         {
            Gradient<SOLUTION_U>{}
         };

         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<DifferentiableOperator> 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<int> all_domain_attr(H1.GetMesh()->attributes.Max());
      all_domain_attr = 1;

      auto &mesh_nodes =
         *static_cast<ParGridFunction *>(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<FieldDescriptor> solutions;
      solutions.push_back(FieldDescriptor(SOLUTION_U, &H1));
      std::vector<FieldDescriptor> parameters;
      parameters.push_back(FieldDescriptor(MESH_NODES, &mesh_nodes_fes));

      // Create the DifferentiableOperator on the desired mesh.
      res = std::make_shared<DifferentiableOperator>(
               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<FIELD_ID> 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<FIELD_ID> 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<SOLUTION_U>{},
         Gradient<MESH_NODES>{},
         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<SOLUTION_U>{}
      };

      // 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<size_t, SOLUTION_U> {};
      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<int> 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<FDJacobian>(*this, x);
            return *fd_jac;

         case HANDCODED:
         {
            man_dres_du = std::make_shared<MinimalSurfaceHandcodedJacobian>(
                             this, x);
            return *man_dres_du;
         }

         case AUTODIFF:
         default:
            dres_du = std::make_shared<MinimalSurfaceJacobian>(this, x);
            return *dres_du;
      }
   }

   std::shared_ptr<MinimalSurfaceJacobian> GetJacobian()
   {
      return dres_du;
   }

   const Array<int>& GetEssentialTrueDofs() const
   {
      return ess_tdofs;
   }

private:
   ParFiniteElementSpace &H1;
   const IntegrationRule &ir;

   mutable ParGridFunction u;

   Array<int> ess_tdofs;

   std::shared_ptr<DifferentiableOperator> res;
   mutable std::shared_ptr<MinimalSurfaceJacobian> dres_du;
   mutable std::shared_ptr<MinimalSurfaceHandcodedJacobian> man_dres_du;
   mutable std::shared_ptr<FDJacobian> fd_jac;
   int derivative_type;
};

template <typename dscalar_t, int dim = 2>
class AMGPC : public Solver
{
public:
   AMGPC(Operator &op) :
      Solver(op.Height()),
      op(op)
   {}

   void SetOperator(const Operator &) override
   {
      auto minsurface = static_cast<MinimalSurface<dscalar_t, dim>&>(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<Operator> minsurface;
#ifdef MFEM_USE_ENZYME
   // When Enzyme is available, use it for automatic differentiation
   minsurface = std::make_unique<MinimalSurface<real_t>>(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<real_t, real_t>;
   minsurface = std::make_unique<MinimalSurface<dual_t>>(H1, *ir,
                                                         derivative_type);
#endif

   // 10. Set up and apply the boundary conditions
   Array<int> 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<real_t>> amgpc;
   if (enable_pcamg)
   {
      if (derivative_type == AUTODIFF)
      {
         amgpc.reset(new AMGPC<real_t>(*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;
}
