// 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 "transfer.hpp"
#include "bilinearform.hpp"
#include "pbilinearform.hpp"
#include "../general/forall.hpp"

namespace mfem
{

GridTransfer::GridTransfer(FiniteElementSpace &dom_fes_,
                           FiniteElementSpace &ran_fes_)
   : dom_fes(dom_fes_), ran_fes(ran_fes_),
     oper_type(Operator::ANY_TYPE),
     fw_t_oper(), bw_t_oper(), use_ea(false), d_mt(Device::GetHostMemoryType())
{
#ifdef MFEM_USE_MPI
   const bool par_dom = dynamic_cast<ParFiniteElementSpace*>(&dom_fes);
   const bool par_ran = dynamic_cast<ParFiniteElementSpace*>(&ran_fes);
   MFEM_VERIFY(par_dom == par_ran, "the domain and range FE spaces must both"
               " be either serial or parallel");
   parallel = par_dom;
#endif
}

const Operator &GridTransfer::MakeTrueOperator(
   FiniteElementSpace &fes_in, FiniteElementSpace &fes_out,
   const Operator &oper, OperatorHandle &t_oper)
{
   if (t_oper.Ptr())
   {
      return *t_oper.Ptr();
   }

   if (!Parallel())
   {
      const SparseMatrix *in_cP = fes_in.GetConformingProlongation();
      const SparseMatrix *out_cR = fes_out.GetConformingRestriction();
      if (oper_type == Operator::MFEM_SPARSEMAT)
      {
         const SparseMatrix *mat = dynamic_cast<const SparseMatrix *>(&oper);
         MFEM_VERIFY(mat != NULL, "Operator is not a SparseMatrix");
         if (!out_cR)
         {
            t_oper.Reset(const_cast<SparseMatrix*>(mat), false);
         }
         else
         {
            t_oper.Reset(mfem::Mult(*out_cR, *mat));
         }
         if (in_cP)
         {
            t_oper.Reset(mfem::Mult(*t_oper.As<SparseMatrix>(), *in_cP));
         }
      }
      else if (oper_type == Operator::ANY_TYPE)
      {
         const int RP_case = bool(out_cR) + 2*bool(in_cP);
         switch (RP_case)
         {
            case 0:
               t_oper.Reset(const_cast<Operator*>(&oper), false);
               break;
            case 1:
               t_oper.Reset(
                  new ProductOperator(out_cR, &oper, false, false));
               break;
            case 2:
               t_oper.Reset(
                  new ProductOperator(&oper, in_cP, false, false));
               break;
            case 3:
               t_oper.Reset(
                  new TripleProductOperator(
                     out_cR, &oper, in_cP, false, false, false));
               break;
         }
      }
      else
      {
         MFEM_ABORT("Operator::Type is not supported: " << oper_type);
      }
   }
   else // Parallel() == true
   {
#ifdef MFEM_USE_MPI
      if (oper_type == Operator::Hypre_ParCSR)
      {
         const SparseMatrix *out_R = fes_out.GetRestrictionMatrix();
         const ParFiniteElementSpace *pfes_in =
            dynamic_cast<const ParFiniteElementSpace *>(&fes_in);
         const ParFiniteElementSpace *pfes_out =
            dynamic_cast<const ParFiniteElementSpace *>(&fes_out);
         const SparseMatrix *sp_mat = dynamic_cast<const SparseMatrix *>(&oper);
         const HypreParMatrix *hy_mat;
         if (sp_mat)
         {
            SparseMatrix *RA = mfem::Mult(*out_R, *sp_mat);
            t_oper.Reset(pfes_in->Dof_TrueDof_Matrix()->
                         LeftDiagMult(*RA, pfes_out->GetTrueDofOffsets()));
            delete RA;
         }
         else if ((hy_mat = dynamic_cast<const HypreParMatrix *>(&oper)))
         {
            HypreParMatrix *RA =
               hy_mat->LeftDiagMult(*out_R, pfes_out->GetTrueDofOffsets());
            t_oper.Reset(mfem::ParMult(RA, pfes_in->Dof_TrueDof_Matrix()));
            delete RA;
         }
         else
         {
            MFEM_ABORT("unknown Operator type");
         }
      }
      else if (oper_type == Operator::ANY_TYPE)
      {
         const Operator *out_R = fes_out.GetRestrictionOperator();
         t_oper.Reset(new TripleProductOperator(
                         out_R, &oper, fes_in.GetProlongationMatrix(),
                         false, false, false));
      }
      else
      {
         MFEM_ABORT("Operator::Type is not supported: " << oper_type);
      }
#endif
   }

   return *t_oper.Ptr();
}


InterpolationGridTransfer::~InterpolationGridTransfer()
{
   if (own_mass_integ) { delete mass_integ; }
}

void InterpolationGridTransfer::SetMassIntegrator(
   BilinearFormIntegrator *mass_integ_, bool own_mass_integ_)
{
   if (own_mass_integ) { delete mass_integ; }

   mass_integ = mass_integ_;
   own_mass_integ = own_mass_integ_;
}

const Operator &InterpolationGridTransfer::ForwardOperator()
{
   if (F.Ptr())
   {
      return *F.Ptr();
   }

   // Construct F
   if (oper_type == Operator::ANY_TYPE)
   {
      F.Reset(new FiniteElementSpace::RefinementOperator(&ran_fes, &dom_fes));
   }
   else if (oper_type == Operator::MFEM_SPARSEMAT)
   {
      Mesh::GeometryList elem_geoms(*ran_fes.GetMesh());

      DenseTensor localP[Geometry::NumGeom];
      for (int i = 0; i < elem_geoms.Size(); i++)
      {
         ran_fes.GetLocalRefinementMatrices(dom_fes, elem_geoms[i],
                                            localP[elem_geoms[i]]);
      }
      F.Reset(ran_fes.RefinementMatrix_main(
                 dom_fes.GetNDofs(), dom_fes.GetElementToDofTable(),
                 dom_fes.GetElementToFaceOrientationTable(), localP));
   }
   else
   {
      MFEM_ABORT("Operator::Type is not supported: " << oper_type);
   }

   return *F.Ptr();
}

const Operator &InterpolationGridTransfer::BackwardOperator()
{
   if (B.Ptr())
   {
      return *B.Ptr();
   }

   // Construct B, if not set, define a suitable mass_integ
   if (!mass_integ)
   {
      const FiniteElement *f_fe_0 = ran_fes.GetTypicalFE();
      const int map_type = f_fe_0->GetMapType();
      if (map_type == FiniteElement::VALUE ||
          map_type == FiniteElement::INTEGRAL)
      {
         mass_integ = new MassIntegrator;
      }
      else if (map_type == FiniteElement::H_DIV ||
               map_type == FiniteElement::H_CURL)
      {
         mass_integ = new VectorFEMassIntegrator;
      }
      else
      {
         MFEM_ABORT("unknown type of FE space");
      }
      own_mass_integ = true;
   }
   if (oper_type == Operator::ANY_TYPE)
   {
      B.Reset(new FiniteElementSpace::DerefinementOperator(
                 &ran_fes, &dom_fes, mass_integ));
   }
   else
   {
      MFEM_ABORT("Operator::Type is not supported: " << oper_type);
   }

   return *B.Ptr();
}


L2ProjectionGridTransfer::L2Projection::L2Projection(
   const FiniteElementSpace &fes_ho_, const FiniteElementSpace &fes_lor_,
   MemoryType d_mt_)
   : Operator(fes_lor_.GetVSize(), fes_ho_.GetVSize()),
     fes_ho(fes_ho_), fes_lor(fes_lor_), d_mt(d_mt_)
{ }

void L2ProjectionGridTransfer::L2Projection::BuildHo2Lor(
   int nel_ho, int nel_lor, const CoarseFineTransformations& cf_tr)
{
   // Construct the mapping from HO to LOR
   // ho2lor.GetRow(iho) will give all the LOR elements contained in iho
   ho2lor.MakeI(nel_ho);
   for (int ilor = 0; ilor < nel_lor; ++ilor)
   {
      int iho = cf_tr.embeddings[ilor].parent;
      ho2lor.AddAColumnInRow(iho);
   }
   ho2lor.MakeJ();
   for (int ilor = 0; ilor < nel_lor; ++ilor)
   {
      int iho = cf_tr.embeddings[ilor].parent;
      ho2lor.AddConnection(iho, ilor);
   }
   ho2lor.ShiftUpI();
}

void L2ProjectionGridTransfer::L2Projection::ElemMixedMass(
   Geometry::Type geom, const FiniteElement& fe_ho,
   const FiniteElement& fe_lor, ElementTransformation* tr_ho,
   ElementTransformation* tr_lor,
   IntegrationPointTransformation& ip_tr,
   DenseMatrix& M_mixed_el) const
{
   int order = fe_lor.GetOrder() + fe_ho.GetOrder() + tr_lor->OrderW();
   const IntegrationRule* ir = &IntRules.Get(geom, order);
   M_mixed_el = 0.0;
   for (int i = 0; i < ir->GetNPoints(); i++)
   {
      const IntegrationPoint& ip_lor = ir->IntPoint(i);
      IntegrationPoint ip_ho;
      ip_tr.Transform(ip_lor, ip_ho);
      Vector shape_lor(fe_lor.GetDof());
      fe_lor.CalcShape(ip_lor, shape_lor);
      Vector shape_ho(fe_ho.GetDof());
      tr_ho->SetIntPoint(&ip_ho);
      fe_ho.CalcPhysShape(*tr_ho, shape_ho);
      tr_lor->SetIntPoint(&ip_lor);
      // For now we use the geometry information from the LOR space, which means
      // we won't be mass conservative if the mesh is curved
      real_t w = ip_lor.weight;
      if (fe_lor.GetMapType() == FiniteElement::VALUE)
      {
         w *= tr_lor->Weight();
      }
      shape_lor *= w;
      AddMultVWt(shape_lor, shape_ho, M_mixed_el);
   }
}

void L2ProjectionGridTransfer::L2Projection::ElemMixedMass(
   Geometry::Type geom, const FiniteElement& fe_ho,
   const FiniteElement& fe_lor, ElementTransformation* el_tr,
   IntegrationPointTransformation& ip_tr,
   DenseMatrix& B_L, DenseMatrix& B_H) const
{
   int order = fe_lor.GetOrder() + fe_ho.GetOrder() + el_tr->OrderW();
   const IntegrationRule* ir = &IntRules.Get(geom, order);

   for (int i = 0; i < ir->GetNPoints(); i++)
   {
      const IntegrationPoint& ip_lor = ir->IntPoint(i);
      IntegrationPoint ip_ho;

      // maps integration point ip_lor -> ip_ho
      ip_tr.Transform(ip_lor, ip_ho);
      Vector shape_lor(fe_lor.GetDof());
      fe_lor.CalcShape(ip_lor, shape_lor);
      Vector shape_ho(fe_ho.GetDof());
      fe_ho.CalcShape(ip_ho, shape_ho);

      for (int j=0; j<shape_lor.Size(); ++j)
      {
         B_L(i, j) = shape_lor(j);
      }

      for (int j=0; j<shape_ho.Size(); ++j)
      {
         B_H(i, j) = shape_ho(j);
      }
   }

}

void L2ProjectionGridTransfer::L2Projection::MixedMassEA(
   const FiniteElementSpace& fes_ho_ea,
   const FiniteElementSpace& fes_lor_ea,
   Vector &M_LH, MemoryType d_mt_)
{
   Mesh* mesh_ho = fes_ho_ea.GetMesh();
   Mesh* mesh_lor = fes_lor_ea.GetMesh();
   int nel_ho = mesh_ho->GetNE();
   int nel_lor = mesh_lor->GetNE();

   const CoarseFineTransformations& cf_tr = mesh_lor->GetRefinementTransforms();

   int nref_max = 0;
   Array<Geometry::Type> geoms;
   mesh_ho->GetGeometries(mesh_ho->Dimension(), geoms);
   for (int ig = 0; ig < geoms.Size(); ++ig)
   {
      Geometry::Type geom = geoms[ig];
      nref_max = std::max(nref_max, cf_tr.point_matrices[geom].SizeK());
   }

   BuildHo2Lor(nel_ho, nel_lor, cf_tr);

   IntegrationPointTransformation ip_tr;
   IsoparametricTransformation &emb_tr = ip_tr.Transf;

   // Gather basis functions (B_L, B_HO) and data at quadrature points
   DenseTensor B_L, B_H, D;
   {
      // Assume all HO elements are LOR in the same way
      const int iho = 0;
      {
         Array<int> lor_els;
         ho2lor.GetRow(iho, lor_els);
         int nref = ho2lor.RowSize(iho);

         Geometry::Type geom = mesh_ho->GetElementBaseGeometry(iho);
         const FiniteElement &fe_ho = *fes_ho_ea.GetFE(iho);
         const FiniteElement &fe_lor = *fes_lor_ea.GetFE(lor_els[0]);

         // Allocate space for DenseTensors
         ElementTransformation *el_tr = fes_lor_ea.GetElementTransformation(0);
         int order = fe_lor.GetOrder() + fe_ho.GetOrder() + el_tr->OrderW();
         const IntegrationRule* ir_ea = &IntRules.Get(geom, order);
         int qPts = ir_ea->GetNPoints();

         // Containers for the basis functions sampled at quadrature points
         B_L.SetSize(qPts, fe_lor.GetDof(), nref, d_mt);
         B_H.SetSize(qPts, fe_ho.GetDof(), nref, d_mt);
         D.SetSize(qPts, nref, nel_ho, d_mt);

         const GeometricFactors *geo_facts =
            mesh_lor->GetGeometricFactors(*ir_ea, GeometricFactors::DETERMINANTS);

         MFEM_ASSERT(nel_ho*nref == nel_lor, "we expect nel_ho*nref == nel_lor");

         // Setup data at quadrature points
         // TODO add support for user coefficient
         const auto W = Reshape(ir_ea->GetWeights().Read(), qPts);
         const auto J = Reshape(geo_facts->detJ.Read(), qPts, nel_lor);
         const auto d_D = Reshape(D.Write(), qPts, nref, nel_ho);

         mfem::forall(qPts * nref * nel_ho, [=] MFEM_HOST_DEVICE (int tid)
         {
            const int q    = tid % qPts;
            const int iref = (tid / qPts) % nref;
            const int iho  = (tid / (qPts * nref)) % nel_ho;

            const int lo_el_id = iref + nref*iho;
            const real_t detJ = J(q, lo_el_id);

            d_D(q, iref, iho) = W(q) * detJ;

         });

         emb_tr.SetIdentityTransformation(geom);
         const DenseTensor &pmats = cf_tr.point_matrices[geom];

         // Collect the basis functions
         for (int iref = 0; iref < nref; ++iref)
         {
            int ilor = lor_els[iref];
            // Now assemble the block-row of the mixed mass matrix associated
            // with integrating HO functions against LOR functions on the LOR
            // sub-element.

            // Create the transformation that embeds the fine low-order element
            // within the coarse high-order element in reference space
            emb_tr.SetPointMat(pmats(cf_tr.embeddings[ilor].matrix));

            DenseMatrix &b_lo = B_L(ilor);
            DenseMatrix &b_ho = B_H(ilor);

            ElemMixedMass(geom, fe_ho, fe_lor, el_tr, ip_tr, b_lo, b_ho);

         } // loop over subcells of ho element
         // end of quadrature point setup
      }

   } // completed setup of basis function and quadrature point

   // Assemble mixed mass matrix
   {
      int iho = 0;
      Array<int> lor_els;
      ho2lor.GetRow(iho, lor_els);
      int nref = ho2lor.RowSize(iho);

      const FiniteElement &fe_ho = *fes_ho_ea.GetFE(iho);
      const FiniteElement &fe_lor = *fes_lor_ea.GetFE(lor_els[0]);
      const int ndof_ho = fe_ho.GetDof();
      const int ndof_lor = fe_lor.GetDof();

      const int qPts = D.SizeI();

      M_LH.SetSize(ndof_lor*ndof_ho*nref*nel_ho, d_mt);

      // Rows x columns
      // Recall MFEM is column major
      // rows x columns is inverted  - matrix is ndof_lor x ndof_ho
      auto v_M_LH = Reshape(M_LH.Write(), ndof_lor, ndof_ho, nref,
                            nel_ho);

      const int fe_ho_ndof = fe_ho.GetDof();
      const int fe_lor_ndof = fe_lor.GetDof();

      auto d_B_L = Reshape(B_L.Read(), qPts, fe_lor_ndof, nref);
      auto d_B_H = Reshape(B_H.Read(), qPts, fe_ho_ndof, nref);
      auto d_D   = Reshape(D.Read(), qPts, nref, nel_ho);

      mfem::forall(fe_ho_ndof*nref*nel_ho, [=] MFEM_HOST_DEVICE (int idx)
      {
         const int bh = idx % fe_ho_ndof;
         const int iref = (idx / fe_ho_ndof) % nref;
         const int iho = idx / fe_ho_ndof / nref;
         // (B_lo_dofs x Q) x (Q x B_ho_dofs)
         for (int bl = 0; bl < fe_lor_ndof; ++bl)
         {
            real_t dot = 0.0;
            for (int qi=0; qi<qPts; ++qi)
            {
               dot += d_B_L(qi, bl, iref) *  d_D(qi, iref, iho) * d_B_H(qi, bh, iref);
            }
            // column major storage
            v_M_LH(bl, bh, iref, iho) = dot;
         }
      });
   } // end of mixed assembly mass matrix
}

L2ProjectionGridTransfer::L2ProjectionL2Space::L2ProjectionL2Space
(const FiniteElementSpace &fes_ho_, const FiniteElementSpace &fes_lor_,
 const bool use_ea_, MemoryType d_mt_)
   : L2Projection(fes_ho_, fes_lor_, d_mt_),
     use_ea(use_ea_)
{
   if (use_ea)
   {
      EAL2ProjectionL2Space();
      return;
   }

   Mesh *mesh_ho = fes_ho.GetMesh();
   Mesh *mesh_lor = fes_lor.GetMesh();
   int nel_ho = mesh_ho->GetNE();
   int nel_lor = mesh_lor->GetNE();

   // The prolongation operation is only well-defined when the LOR space has at
   // least as many DOFs as the high-order space.
   const bool build_P = fes_lor.GetTrueVSize() >= fes_ho.GetTrueVSize();

   // If the local mesh is empty, skip all computations
   if (nel_ho == 0) { return; }

   const CoarseFineTransformations &cf_tr = mesh_lor->GetRefinementTransforms();

   int nref_max = 0;
   Array<Geometry::Type> geoms;
   mesh_ho->GetGeometries(mesh_ho->Dimension(), geoms);
   for (int ig = 0; ig < geoms.Size(); ++ig)
   {
      Geometry::Type geom = geoms[ig];
      nref_max = std::max(nref_max, cf_tr.point_matrices[geom].SizeK());
   }

   BuildHo2Lor(nel_ho, nel_lor, cf_tr);

   offsets.SetSize(nel_ho+1);
   offsets[0] = 0;
   for (int iho = 0; iho < nel_ho; ++iho)
   {
      int nref = ho2lor.RowSize(iho);
      const FiniteElement &fe_ho = *fes_ho.GetFE(iho);
      const FiniteElement &fe_lor = *fes_lor.GetFE(ho2lor.GetRow(iho)[0]);
      offsets[iho+1] = offsets[iho] + fe_ho.GetDof()*fe_lor.GetDof()*nref;
   }
   // R will contain the restriction (L^2 projection operator) defined on each
   // coarse HO element (and corresponding patch of LOR elements)
   R.SetSize(offsets[nel_ho]);
   if (build_P)
   {
      // P will contain the corresponding prolongation operator
      P.SetSize(offsets[nel_ho]);
   }

   IntegrationPointTransformation ip_tr;
   IsoparametricTransformation &emb_tr = ip_tr.Transf;

   for (int iho = 0; iho < nel_ho; ++iho)
   {
      Array<int> lor_els;
      ho2lor.GetRow(iho, lor_els);
      int nref = ho2lor.RowSize(iho);

      Geometry::Type geom = mesh_ho->GetElementBaseGeometry(iho);
      const FiniteElement &fe_ho = *fes_ho.GetFE(iho);
      const FiniteElement &fe_lor = *fes_lor.GetFE(lor_els[0]);
      int ndof_ho = fe_ho.GetDof();
      int ndof_lor = fe_lor.GetDof();

      ElementTransformation *tr_ho = fes_ho.GetElementTransformation(iho);

      emb_tr.SetIdentityTransformation(geom);
      const DenseTensor &pmats = cf_tr.point_matrices[geom];

      DenseMatrix R_iho(&R[offsets[iho]], ndof_lor*nref, ndof_ho);

      DenseMatrix Minv_lor(ndof_lor*nref, ndof_lor*nref);
      DenseMatrix M_mixed(ndof_lor*nref, ndof_ho);

      MassIntegrator mi;
      DenseMatrix M_lor_el(ndof_lor, ndof_lor);
      DenseMatrixInverse Minv_lor_el(&M_lor_el);
      DenseMatrix M_lor(ndof_lor*nref, ndof_lor*nref);
      DenseMatrix M_mixed_el(ndof_lor, ndof_ho);

      Minv_lor = 0.0;
      M_lor = 0.0;

      DenseMatrix RtMlor(ndof_ho, ndof_lor*nref);
      DenseMatrix RtMlorR(ndof_ho, ndof_ho);
      DenseMatrixInverse RtMlorR_inv(&RtMlorR);

      for (int iref = 0; iref < nref; ++iref)
      {
         // Assemble the low-order refined mass matrix and invert locally
         int ilor = lor_els[iref];
         ElementTransformation *tr_lor = fes_lor.GetElementTransformation(ilor);
         mi.AssembleElementMatrix(fe_lor, *tr_lor, M_lor_el);
         M_lor.CopyMN(M_lor_el, iref*ndof_lor, iref*ndof_lor);
         Minv_lor_el.Factor();
         Minv_lor_el.GetInverseMatrix(M_lor_el);
         // Insert into the diagonal of the patch LOR mass matrix
         Minv_lor.CopyMN(M_lor_el, iref*ndof_lor, iref*ndof_lor);

         // Now assemble the block-row of the mixed mass matrix associated
         // with integrating HO functions against LOR functions on the LOR
         // sub-element.

         // Create the transformation that embeds the fine low-order element
         // within the coarse high-order element in reference space
         emb_tr.SetPointMat(pmats(cf_tr.embeddings[ilor].matrix));

         ElemMixedMass(geom, fe_ho, fe_lor, tr_ho, tr_lor, ip_tr, M_mixed_el);

         M_mixed.CopyMN(M_mixed_el, iref*ndof_lor, 0);
      }
      mfem::Mult(Minv_lor, M_mixed, R_iho);

      if (build_P)
      {
         DenseMatrix P_iho(&P[offsets[iho]], ndof_ho, ndof_lor*nref);

         mfem::MultAtB(R_iho, M_lor, RtMlor);
         mfem::Mult(RtMlor, R_iho, RtMlorR);
         RtMlorR_inv.Factor();
         RtMlorR_inv.Mult(RtMlor, P_iho);
      }
   }

}


void L2ProjectionGridTransfer::L2ProjectionL2Space::EAL2ProjectionL2Space()
{
   Mesh *mesh_ho = fes_ho.GetMesh();
   Mesh *mesh_lor = fes_lor.GetMesh();
   int nel_ho = mesh_ho->GetNE();
   int nel_lor = mesh_lor->GetNE();

   // The prolongation operation is only well-defined when the LOR space has at
   // least as many DOFs as the high-order space.
   const bool build_P = fes_lor.GetTrueVSize() >= fes_ho.GetTrueVSize();

   // If the local mesh is empty, skip all computations
   if (nel_ho == 0) { return; }

   const CoarseFineTransformations &cf_tr = mesh_lor->GetRefinementTransforms();

   int nref_max = 0;
   Array<Geometry::Type> geoms;
   mesh_ho->GetGeometries(mesh_ho->Dimension(), geoms);
   for (int ig = 0; ig < geoms.Size(); ++ig)
   {
      Geometry::Type geom = geoms[ig];
      nref_max = std::max(nref_max, cf_tr.point_matrices[geom].SizeK());
   }

   BuildHo2Lor(nel_ho, nel_lor, cf_tr);

   offsets.SetSize(nel_ho+1);
   offsets[0] = 0;
   for (int iho = 0; iho < nel_ho; ++iho)
   {
      int nref = ho2lor.RowSize(iho);
      const FiniteElement &fe_ho = *fes_ho.GetFE(iho);
      const FiniteElement &fe_lor = *fes_lor.GetFE(ho2lor.GetRow(iho)[0]);
      offsets[iho+1] = offsets[iho] + fe_ho.GetDof()*fe_lor.GetDof()*nref;
   }

   // R will contain the restriction (L^2 projection operator) defined on each
   // coarse HO element (and corresponding patch of LOR elements)
   R.SetSize(offsets[nel_ho]);

   if (build_P)
   {
      // P will contain the corresponding prolongation operator
      P.SetSize(offsets[nel_ho]);
   }

   // Assemble mixed mass matrix
   Vector M_mixed_all;
   MixedMassEA(fes_ho, fes_lor, M_mixed_all, d_mt);


   // R = inv(M_L) * M_mixed
   // Need to compute M_L
   // Note: Using user-inputted M_LH IntegrationRule ir
   // (higher order than needed) in order to re-use coeff
   MassIntegrator mi;

   Vector M_ea_lor;
   int ndof_lor;
   int ndof_ho;
   int nref;
   {
      int iho = 0;
      Array<int> lor_els;
      ho2lor.GetRow(iho, lor_els);
      nref = ho2lor.RowSize(iho);

      const FiniteElement &fe_ho = *fes_ho.GetFE(iho);
      const FiniteElement &fe_lor = *fes_lor.GetFE(lor_els[0]);
      ndof_ho = fe_ho.GetDof();
      ndof_lor = fe_lor.GetDof();

      M_ea_lor.SetSize(ndof_lor*ndof_lor*nel_lor, d_mt);
   }

   const bool add = false;
   mi.AssembleEA(fes_lor, M_ea_lor, add);

   DenseTensor Minv_ear_lor;
   Minv_ear_lor.SetSize(ndof_lor, ndof_lor, nel_lor, d_mt);
   Minv_ear_lor.GetMemory().CopyFrom(M_ea_lor.GetMemory(), M_ea_lor.Size());

   BatchedLinAlg::Invert(Minv_ear_lor);
   {
      // Recall mfem is column major
      // ndof_lor x ndof_ho
      auto v_M_mixed_all = Reshape(M_mixed_all.Read(), ndof_lor, ndof_ho, nref,
                                   nel_ho);

      // matrix is symmetric
      auto v_Minv_ear_lor = Reshape(Minv_ear_lor.Read(), ndof_lor, ndof_lor,
                                    nel_lor);

      // ndof_lor x ndof_ho
      auto v_R = Reshape(R.Write(), ndof_lor, nref, ndof_ho, nel_ho);

      MFEM_VERIFY(nel_lor==nel_ho*nref, "nel_lor != nel_ho*nref");

      // (ndofs_lor x ndofs_lor) x (ndofs_lor x ndof_ho)
      mfem::forall(ndof_lor * nref * ndof_ho * nel_ho, [=] MFEM_HOST_DEVICE (int tid)
      {

         const int i = tid % ndof_lor;
         const int iref = (tid / ndof_lor) % nref;
         const int j    = (tid / (ndof_lor * nref) ) % ndof_ho;
         const int iho  = (tid / (ndof_lor * nref * ndof_ho)) % nel_ho;

         const int lor_idx = iref + iho * nref;

         //matrices are stored in the transpose position
         real_t dot = 0.0;
         for (int k=0; k<ndof_lor; ++k)
         {
            dot += v_Minv_ear_lor(i, k, lor_idx) * v_M_mixed_all(k, j, iref, iho);
         }
         v_R(i, iref, j, iho) = dot;

      });
   }

   if (build_P)
   {
      // P = inv(R^T M_L R) * R^T M_L

      // M_lor is size of ndof_lor x ndof_lor
      // R is size of (ndof_lor x nref x ndof_ho)
      auto v_M_ea_lor = Reshape(M_ea_lor.Read(), ndof_lor, ndof_lor, nel_lor);
      auto v_R = Reshape(R.Read(), ndof_lor, nref, ndof_ho, nel_ho);
      // R^T M_LO is of size nref x ndof_lor

      // Compute R^T M_L
      Vector RtM_L(ndof_ho*nref*ndof_lor*nel_ho, d_mt);
      auto v_RtM_L = Reshape(RtM_L.Write(), ndof_ho, ndof_lor, nref, nel_ho);

      mfem::forall(ndof_lor * nref * ndof_ho * nel_ho, [=] MFEM_HOST_DEVICE (int tid)
      {

         const int jlo = tid % ndof_lor;
         const int iref = (tid / ndof_lor) % nref;
         const int iho  = (tid / (ndof_lor * nref)) % ndof_ho;
         const int e    = (tid / (ndof_lor * nref * ndof_ho)) % nel_ho;

         const int lor_idx = iref + e * nref;

         real_t dot = 0.0;
         for (int t=0; t<ndof_lor; ++t)
         {
            dot += v_R(t, iref, iho, e) * v_M_ea_lor(t, jlo, lor_idx);
         }

         v_RtM_L(iho, jlo, iref, e) = dot;

      });

      // Resulting matrix should be: ndof_ho x ndof_ho
      // R^T M_L x R
      DenseTensor RtM_L_dt;
      RtM_L_dt.NewMemoryAndSize(RtM_L.GetMemory(), ndof_ho, ndof_lor*nref,
                                nel_ho, false);
      Vector R_vec;
      R_vec.NewMemoryAndSize(R.GetMemory(), R.Size(), false);
      Vector RtM_LR(ndof_ho * ndof_ho * nel_ho, d_mt);
      BatchedLinAlg::Mult(RtM_L_dt, R_vec, RtM_LR);
      // Ensure that changes to the alias R_vec are propagated to the base, R
      R_vec.GetMemory().SyncAlias(R.GetMemory(), P.Size());

      // Compute the inverse of InvRtM_LR
      DenseTensor InvRtM_LR;
      InvRtM_LR.NewMemoryAndSize(RtM_LR.GetMemory(), ndof_ho, ndof_ho, nel_ho, false);
      BatchedLinAlg::Invert(InvRtM_LR);

      // Form P
      // P should be of dimension (ndof_ho x ndof_ho) x (ndof_ho x nref*ndof_lor)
      // P ndof_ho x nref*ndof_lor
      Vector P_vec;
      P_vec.NewMemoryAndSize(P.GetMemory(), P.Size(), false);
      BatchedLinAlg::Mult(InvRtM_LR, RtM_L, P_vec);
      // Ensure that changes to the alias P_vec are propagated to the base, P
      P_vec.GetMemory().SyncAlias(P.GetMemory(), P.Size());
   }
}

void L2ProjectionGridTransfer::L2ProjectionL2Space::Mult(
   const Vector &x, Vector &y) const
{

   if (use_ea)
   {
      return EAMult(x,y);
   }


   int vdim = fes_ho.GetVDim();
   Array<int> vdofs;
   DenseMatrix xel_mat, yel_mat;
   for (int iho = 0; iho < fes_ho.GetNE(); ++iho)
   {
      int nref = ho2lor.RowSize(iho);
      int ndof_ho = fes_ho.GetFE(iho)->GetDof();
      int ndof_lor = fes_lor.GetFE(ho2lor.GetRow(iho)[0])->GetDof();
      xel_mat.SetSize(ndof_ho, vdim);
      yel_mat.SetSize(ndof_lor*nref, vdim);
      DenseMatrix R_iho(&R[offsets[iho]], ndof_lor*nref, ndof_ho);

      fes_ho.GetElementVDofs(iho, vdofs);
      x.GetSubVector(vdofs, xel_mat.GetData());
      mfem::Mult(R_iho, xel_mat, yel_mat);
      // Place result correctly into the low-order vector
      for (int iref = 0; iref < nref; ++iref)
      {
         int ilor = ho2lor.GetRow(iho)[iref];
         for (int vd=0; vd<vdim; ++vd)
         {
            fes_lor.GetElementDofs(ilor, vdofs);
            fes_lor.DofsToVDofs(vd, vdofs);
            y.SetSubVector(vdofs, &yel_mat(iref*ndof_lor,vd));
         }
      }
   }
}

void L2ProjectionGridTransfer::L2ProjectionL2Space::EAMult(
   const Vector &x, Vector &y) const
{
   const int iho = 0;
   const int nref = ho2lor.RowSize(iho);
   const int ndof_ho = fes_ho.GetFE(iho)->GetDof();
   const int ndof_lor = fes_lor.GetFE(ho2lor.GetRow(iho)[0])->GetDof();
   const int nel_ho = fes_ho.GetMesh()->GetNE();

   DenseTensor R_dt;
   R_dt.NewMemoryAndSize(R.GetMemory(), ndof_lor*nref, ndof_ho, nel_ho, false);
   BatchedLinAlg::Mult(R_dt, x, y);
}

void L2ProjectionGridTransfer::L2ProjectionL2Space::MultTranspose(
   const Vector &x, Vector &y) const
{

   if (use_ea)
   {
      return EAMultTranspose(x,y);
   }

   int vdim = fes_ho.GetVDim();
   Array<int> vdofs;
   DenseMatrix xel_mat, yel_mat;
   y = 0.0;
   for (int iho = 0; iho < fes_ho.GetNE(); ++iho)
   {
      int nref = ho2lor.RowSize(iho);
      int ndof_ho = fes_ho.GetFE(iho)->GetDof();
      int ndof_lor = fes_lor.GetFE(ho2lor.GetRow(iho)[0])->GetDof();
      xel_mat.SetSize(ndof_lor*nref, vdim);
      yel_mat.SetSize(ndof_ho, vdim);
      DenseMatrix R_iho(&R[offsets[iho]], ndof_lor*nref, ndof_ho);

      // Extract the LOR DOFs
      for (int iref=0; iref<nref; ++iref)
      {
         int ilor = ho2lor.GetRow(iho)[iref];
         for (int vd=0; vd<vdim; ++vd)
         {
            fes_lor.GetElementDofs(ilor, vdofs);
            fes_lor.DofsToVDofs(vd, vdofs);
            x.GetSubVector(vdofs, &xel_mat(iref*ndof_lor, vd));
         }
      }
      // Multiply locally by the transpose
      mfem::MultAtB(R_iho, xel_mat, yel_mat);
      // Place the result in the HO vector
      fes_ho.GetElementVDofs(iho, vdofs);
      y.AddElementVector(vdofs, yel_mat.GetData());
   }

}

void L2ProjectionGridTransfer::L2ProjectionL2Space::EAMultTranspose(
   const Vector &x, Vector &y) const
{
   const int iho = 0;
   const int nref = ho2lor.RowSize(iho);
   const int ndof_ho = fes_ho.GetFE(iho)->GetDof();
   const int ndof_lor = fes_lor.GetFE(ho2lor.GetRow(iho)[0])->GetDof();
   const int nel_ho = fes_ho.GetMesh()->GetNE();

   DenseTensor R_dt;
   R_dt.NewMemoryAndSize(R.GetMemory(), ndof_lor*nref, ndof_ho, nel_ho, false);
   BatchedLinAlg::MultTranspose(R_dt, x, y);
}

void L2ProjectionGridTransfer::L2ProjectionL2Space::Prolongate(
   const Vector &x, Vector &y) const
{

   if (fes_ho.GetNE() == 0) { return; }

   if (use_ea)
   {
      return EAProlongate(x,y);
   }

   MFEM_VERIFY(P.Size() > 0, "Prolongation not supported for these spaces.")
   int vdim = fes_ho.GetVDim();
   Array<int> vdofs;
   DenseMatrix xel_mat,yel_mat;
   y = 0.0;
   for (int iho = 0; iho < fes_ho.GetNE(); ++iho)
   {
      int nref = ho2lor.RowSize(iho);
      int ndof_ho = fes_ho.GetFE(iho)->GetDof();
      int ndof_lor = fes_lor.GetFE(ho2lor.GetRow(iho)[0])->GetDof();
      xel_mat.SetSize(ndof_lor*nref, vdim);
      yel_mat.SetSize(ndof_ho, vdim);
      DenseMatrix P_iho(&P[offsets[iho]], ndof_ho, ndof_lor*nref);

      // Extract the LOR DOFs
      for (int iref = 0; iref < nref; ++iref)
      {
         int ilor = ho2lor.GetRow(iho)[iref];
         for (int vd = 0; vd < vdim; ++vd)
         {
            fes_lor.GetElementDofs(ilor, vdofs);
            fes_lor.DofsToVDofs(vd, vdofs);
            x.GetSubVector(vdofs, &xel_mat(iref*ndof_lor, vd));
         }
      }
      // Locally prolongate
      mfem::Mult(P_iho, xel_mat, yel_mat);
      // Place the result in the HO vector
      fes_ho.GetElementVDofs(iho, vdofs);
      y.AddElementVector(vdofs, yel_mat.GetData());
   }

}

void L2ProjectionGridTransfer::L2ProjectionL2Space::EAProlongate(
   const Vector &x, Vector &y) const
{
   const int iho = 0;
   const int nref = ho2lor.RowSize(iho);
   const int ndof_ho = fes_ho.GetFE(iho)->GetDof();
   const int ndof_lor = fes_lor.GetFE(ho2lor.GetRow(iho)[0])->GetDof();
   const int nel_ho = fes_ho.GetMesh()->GetNE();

   DenseTensor P_dt;
   P_dt.NewMemoryAndSize(P.GetMemory(), ndof_ho, ndof_lor * nref, nel_ho, false);
   BatchedLinAlg::Mult(P_dt, x, y);
}

void L2ProjectionGridTransfer::L2ProjectionL2Space::ProlongateTranspose(
   const Vector &x, Vector &y) const
{

   if (use_ea)
   {
      return EAProlongateTranspose(x,y);
   }


   if (fes_ho.GetNE() == 0) { return; }
   MFEM_VERIFY(P.Size() > 0, "Prolongation not supported for these spaces.")
   int vdim = fes_ho.GetVDim();
   Array<int> vdofs;
   DenseMatrix xel_mat,yel_mat;
   for (int iho = 0; iho < fes_ho.GetNE(); ++iho)
   {
      int nref = ho2lor.RowSize(iho);
      int ndof_ho = fes_ho.GetFE(iho)->GetDof();
      int ndof_lor = fes_lor.GetFE(ho2lor.GetRow(iho)[0])->GetDof();
      xel_mat.SetSize(ndof_ho, vdim);
      yel_mat.SetSize(ndof_lor*nref, vdim);
      DenseMatrix P_iho(&P[offsets[iho]], ndof_ho, ndof_lor*nref);

      fes_ho.GetElementVDofs(iho, vdofs);
      x.GetSubVector(vdofs, xel_mat.GetData());
      mfem::MultAtB(P_iho, xel_mat, yel_mat);

      // Place result correctly into the low-order vector
      for (int iref = 0; iref < nref; ++iref)
      {
         int ilor = ho2lor.GetRow(iho)[iref];
         for (int vd=0; vd<vdim; ++vd)
         {
            fes_lor.GetElementDofs(ilor, vdofs);
            fes_lor.DofsToVDofs(vd, vdofs);
            y.SetSubVector(vdofs, &yel_mat(iref*ndof_lor,vd));
         }
      }
   }

}

void L2ProjectionGridTransfer::L2ProjectionL2Space::EAProlongateTranspose(
   const Vector &x, Vector &y) const
{
   const int iho = 0;
   const int nref = ho2lor.RowSize(iho);
   const int ndof_ho = fes_ho.GetFE(iho)->GetDof();
   const int ndof_lor = fes_lor.GetFE(ho2lor.GetRow(iho)[0])->GetDof();
   const int nel_ho = fes_ho.GetMesh()->GetNE();

   DenseTensor P_dt;
   P_dt.NewMemoryAndSize(P.GetMemory(), ndof_ho, ndof_lor * nref, nel_ho, false);
   BatchedLinAlg::MultTranspose(P_dt, x, y);
}

L2ProjectionGridTransfer::L2ProjectionH1Space::L2ProjectionH1Space(
   const FiniteElementSpace& fes_ho_, const FiniteElementSpace& fes_lor_,
   const bool use_ea_, MemoryType d_mt_)
   : L2Projection(fes_ho_, fes_lor_, d_mt_),
     use_ea(use_ea_)
{

   // need scalar to keep dimensions matching (operators are built to apply
   // individually on each vdim)
   // needed in both matrix and element based versions
   fes_ho_scalar.reset(new FiniteElementSpace(fes_ho.GetMesh(),
                                              fes_ho.FEColl(), 1));
   fes_lor_scalar.reset(new FiniteElementSpace(fes_lor.GetMesh(),
                                               fes_lor.FEColl(), 1));

   if (use_ea)
   {
      EAL2ProjectionH1Space();
      return;
   }

   std::unique_ptr<SparseMatrix> R_mat, M_LH_mat;

   std::tie(R_mat, M_LH_mat) = ComputeSparseRAndM_LH();

   const SparseMatrix *P_ho = fes_ho_scalar->GetConformingProlongation();
   const SparseMatrix *P_lor = fes_lor_scalar->GetConformingProlongation();

   if (P_ho || P_lor)
   {
      if (P_ho && P_lor)
      {
         R_mat.reset(RAP(*P_lor, *R_mat, *P_ho));
         M_LH_mat.reset(RAP(*P_lor, *M_LH_mat, *P_ho));
      }
      else if (P_ho)
      {
         R_mat.reset(mfem::Mult(*R_mat, *P_ho));
         M_LH_mat.reset(mfem::Mult(*M_LH_mat, *P_ho));
      }
      else // P_lor != nullptr
      {
         R_mat.reset(mfem::Mult(*P_lor, *R_mat));
         M_LH_mat.reset(mfem::Mult(*P_lor, *M_LH_mat));
      }
   }

   SparseMatrix *RTxM_LH_mat = TransposeMult(*R_mat, *M_LH_mat);
   precon.reset(new DSmoother(*RTxM_LH_mat));

   // Set ownership
   RTxM_LH.reset(RTxM_LH_mat);
   R = std::move(R_mat);
   M_LH = std::move(M_LH_mat);

   SetupPCG();
}

#ifdef MFEM_USE_MPI

L2ProjectionGridTransfer::L2ProjectionH1Space::L2ProjectionH1Space(
   const ParFiniteElementSpace& pfes_ho, const ParFiniteElementSpace& pfes_lor,
   const bool use_ea_, MemoryType d_mt_)
   : L2Projection(pfes_ho, pfes_lor, d_mt_),
     use_ea(use_ea_), pcg(pfes_ho.GetComm())
{

   // need scalar to keep dimensions matching (operators are built to apply
   // individually on each vdim)
   // needed in both matrix and element based versions
   pfes_ho_scalar.reset(new ParFiniteElementSpace(pfes_ho.GetParMesh(),
                                                  pfes_ho.FEColl(), 1));
   pfes_lor_scalar.reset(new ParFiniteElementSpace(pfes_lor.GetParMesh(),
                                                   pfes_lor.FEColl(), 1));

   if (use_ea)
   {
      EAL2ProjectionH1Space(pfes_ho, pfes_lor);
      return;
   }

   std::tie(R, M_LH) = ComputeSparseRAndM_LH();


   HypreParMatrix R_local = HypreParMatrix(pfes_ho.GetComm(),
                                           pfes_lor_scalar->GlobalVSize(),
                                           pfes_ho_scalar->GlobalVSize(),
                                           pfes_lor_scalar->GetDofOffsets(),
                                           pfes_ho_scalar->GetDofOffsets(),
                                           static_cast<SparseMatrix*>(R.get()));
   HypreParMatrix M_LH_local = HypreParMatrix(pfes_ho.GetComm(),
                                              pfes_lor_scalar->GlobalVSize(),
                                              pfes_ho_scalar->GlobalVSize(),
                                              pfes_lor_scalar->GetDofOffsets(),
                                              pfes_ho_scalar->GetDofOffsets(),
                                              static_cast<SparseMatrix*>(M_LH.get()));

   HypreParMatrix *R_mat = RAP(pfes_lor_scalar->Dof_TrueDof_Matrix(),
                               &R_local, pfes_ho_scalar->Dof_TrueDof_Matrix());
   HypreParMatrix *M_LH_mat = RAP(pfes_lor_scalar->Dof_TrueDof_Matrix(),
                                  &M_LH_local, pfes_ho_scalar->Dof_TrueDof_Matrix());

   std::unique_ptr<HypreParMatrix> R_T(R_mat->Transpose());
   HypreParMatrix *RTxM_LH_mat = ParMult(R_T.get(), M_LH_mat, true);

   HypreBoomerAMG *amg = new HypreBoomerAMG(*RTxM_LH_mat);
   amg->SetPrintLevel(0);

   R.reset(R_mat);
   M_LH.reset(M_LH_mat);
   RTxM_LH.reset(RTxM_LH_mat);
   precon.reset(amg);

   SetupPCG();
   pcg.SetPreconditioner(*precon);
   pcg.SetOperator(*RTxM_LH);
}

#endif

void L2ProjectionGridTransfer::L2ProjectionH1Space::SetupPCG()
{
   // Basic PCG solver setup
   pcg.SetPrintLevel(0);
   // pcg.SetPrintLevel(IterativeSolver::PrintLevel().Summary());
   pcg.SetMaxIter(1000);
   // initial values for relative and absolute tolerance
   pcg.SetRelTol(1e-13);
   pcg.SetAbsTol(1e-13);
   pcg.SetPreconditioner(*precon);
   pcg.SetOperator(*RTxM_LH);
}

void L2ProjectionGridTransfer::L2ProjectionH1Space::EAL2ProjectionH1Space()
{
   Mesh* mesh_ho = fes_ho.GetMesh();
   Mesh* mesh_lor = fes_lor.GetMesh();
   int nel_ho = mesh_ho->GetNE();
   int nel_lor = mesh_lor->GetNE();
   int ndof_ho = fes_ho.GetNDofs();
   int ndof_lor = fes_lor.GetNDofs();

   // If the local mesh is empty, skip all computations
   if (nel_ho == 0)
   {
      return;
   }

   const CoarseFineTransformations& cf_tr = mesh_lor->GetRefinementTransforms();

   int nref_max = 0;
   Array<Geometry::Type> geoms;
   mesh_ho->GetGeometries(mesh_ho->Dimension(), geoms);
   for (int ig = 0; ig < geoms.Size(); ++ig)
   {
      Geometry::Type geom = geoms[ig];
      nref_max = std::max(nref_max, cf_tr.point_matrices[geom].SizeK());
   }

   BuildHo2Lor(nel_ho, nel_lor, cf_tr);

   // lumped M_H and inv lumped M_L

   // M_H contains the lumped (row sum) high order mass matrix. This is built for
   // preconditioning the inverse needed to build the prolongation operator P
   Vector M_H(ndof_ho);
   M_H = 0.0;
   // ML_inv_ea contains the inverse lumped (row sum) mass matrix. Note that the
   // method will also work with a full (consistent) mass matrix, though this is
   // not implemented here. L refers to the low-order refined mesh
   ML_inv_ea.SetSize(ndof_lor);
   ML_inv_ea = 0.0;

   BilinearForm Mho(fes_ho_scalar.get());
   Mho.SetAssemblyLevel(AssemblyLevel::PARTIAL);
   Mho.AddDomainIntegrator(new MassIntegrator);
   Mho.Assemble();

   // Processor local lumped Mass
   Vector ones_ho(Mho.Width()); ones_ho = 1.0;
   M_H = 0.0;
   Mho.Mult(ones_ho, M_H);

   BilinearForm Mlor(fes_lor_scalar.get());
   Mlor.SetAssemblyLevel(AssemblyLevel::PARTIAL);
   Mlor.AddDomainIntegrator(new MassIntegrator);
   Mlor.Assemble();

   Vector ones_lor(Mlor.Width()); ones_lor = 1.0;
   Mlor.Mult(ones_lor, ML_inv_ea);

   // DOF by DOF inverse of non-zero entries
   LumpedMassInverse(ML_inv_ea);

   // mixed mass M_LH
   MixedMassEA(fes_ho, fes_lor, M_LH_ea, d_mt);

   // Set ownership
   M_LH_local_op = new H1SpaceMixedMassOperator(fes_ho_scalar.get(),
                                                fes_lor_scalar.get(),
                                                &ho2lor,
                                                &M_LH_ea);

   ML_inv_vea.reset(new H1SpaceLumpedMassOperator(fes_ho_scalar.get(),
                                                  fes_lor_scalar.get(),
                                                  ML_inv_ea));
   M_LH.reset(M_LH_local_op);
   R.reset(new ProductOperator(ML_inv_vea.get(), M_LH.get(), false,
                               false));

   Array<int> ess_tdof_list;  // leave empty
   precon.reset(new OperatorJacobiSmoother(M_H, ess_tdof_list));

   TransposeOperator* RT = new TransposeOperator(R.get());
   RTxM_LH.reset(new ProductOperator(RT, M_LH.get(), true, false));

   SetupPCG();
}

#ifdef MFEM_USE_MPI
void L2ProjectionGridTransfer::L2ProjectionH1Space::EAL2ProjectionH1Space
(const ParFiniteElementSpace& pfes_ho, const ParFiniteElementSpace& pfes_lor)
{
   Mesh* mesh_ho = pfes_ho.GetParMesh();
   Mesh* mesh_lor = pfes_lor.GetParMesh();
   int nel_ho = mesh_ho->GetNE();
   int nel_lor = mesh_lor->GetNE();
   int ndof_ho = pfes_ho.GetNDofs();
   int ndof_lor = pfes_lor.GetNDofs();


   // If the local mesh is empty, skip all computations
   if (nel_ho == 0)
   {
      return;
   }

   const CoarseFineTransformations& cf_tr = mesh_lor->GetRefinementTransforms();

   int nref_max = 0;
   Array<Geometry::Type> geoms;
   mesh_ho->GetGeometries(mesh_ho->Dimension(), geoms);
   for (int ig = 0; ig < geoms.Size(); ++ig)
   {
      Geometry::Type geom = geoms[ig];
      nref_max = std::max(nref_max, cf_tr.point_matrices[geom].SizeK());
   }

   BuildHo2Lor(nel_ho, nel_lor, cf_tr);

   // lumped M_H and inv lumped M_L

   // M_H contains the lumped (row sum) high order mass matrix. This is built for
   // preconditioning the inverse needed to build the prolongation operator P
   Vector M_H(ndof_ho);
   M_H = 0.0;
   // ML_inv_ea contains the inverse lumped (row sum) mass matrix. Note that the
   // method will also work with a full (consistent) mass matrix, though this is
   // not implemented here. L refers to the low-order refined mesh
   ML_inv_ea.SetSize(ndof_lor);
   ML_inv_ea = 0.0;

   ParBilinearForm pMho(pfes_ho_scalar.get());
   pMho.SetAssemblyLevel(AssemblyLevel::PARTIAL);
   pMho.AddDomainIntegrator(new MassIntegrator);
   pMho.Assemble();

   // Processor local lumped Mass
   Vector ones_ho(pMho.Width()); ones_ho = 1.0;
   M_H = 0.0;
   pMho.Mult(ones_ho, M_H);

   ParBilinearForm pMlor(pfes_lor_scalar.get());
   pMlor.SetAssemblyLevel(AssemblyLevel::PARTIAL);
   pMlor.AddDomainIntegrator(new MassIntegrator);
   pMlor.Assemble();

   Vector ones_lor(pMlor.Width()); ones_lor = 1.0;
   pMlor.Mult(ones_lor, ML_inv_ea);


   // DOF by DOF inverse of non-zero entries
   LumpedMassInverse(ML_inv_ea);

   // mixed mass M_LH
   MixedMassEA(*pfes_ho_scalar.get(), *pfes_lor_scalar.get(), M_LH_ea, d_mt);

   // Set ownership
   M_LH_local_op = new H1SpaceMixedMassOperator(pfes_ho_scalar.get(),
                                                pfes_lor_scalar.get(),
                                                &ho2lor, &M_LH_ea);

   const Operator *P_ho = pfes_ho_scalar->GetProlongationMatrix();
   const Operator *P_lor = pfes_lor_scalar->GetProlongationMatrix();

   Array<int> ess_tdof_list;  // leave empty

   if (P_ho || P_lor)
   {
      if (P_ho && P_lor)
      {
         Operator *Pt_lor = new TransposeOperator(P_lor);
         RML_inv.SetSize(pfes_lor_scalar->GetTrueVSize());
         GetTDofs(*pfes_lor_scalar, ML_inv_ea, RML_inv);
         ML_inv_vea.reset(new H1SpaceLumpedMassOperator(pfes_ho_scalar.get(),
                                                        pfes_lor_scalar.get(),
                                                        RML_inv));
         M_LH.reset(new TripleProductOperator(Pt_lor, M_LH_local_op, P_ho, true,
                                              true, false));

         Vector RM_H(pfes_ho_scalar->GetTrueVSize());
         GetTDofsTranspose(*pfes_ho_scalar, M_H, RM_H);
         precon.reset(new OperatorJacobiSmoother(RM_H, ess_tdof_list));
      }
      else if (P_ho)
      {
         ML_inv_vea.reset(new H1SpaceLumpedMassOperator(pfes_ho_scalar.get(),
                                                        pfes_lor_scalar.get(),
                                                        ML_inv_ea));
         M_LH.reset(new ProductOperator(M_LH_local_op, P_ho, true, false));

         Vector RM_H(pfes_ho_scalar->GetTrueVSize());
         GetTDofsTranspose(*pfes_ho_scalar.get(), M_H, RM_H);
         precon.reset(new OperatorJacobiSmoother(RM_H, ess_tdof_list));
      }
      else if (P_lor)
      {
         Operator *Pt_lor = new TransposeOperator(P_lor);
         RML_inv.SetSize(pfes_lor_scalar->GetTrueVSize());
         GetTDofsTranspose(*pfes_lor_scalar, ML_inv_ea, RML_inv);
         ML_inv_vea.reset(new H1SpaceLumpedMassOperator(pfes_ho_scalar.get(),
                                                        pfes_lor_scalar.get(),
                                                        RML_inv));
         M_LH.reset(new ProductOperator(Pt_lor, M_LH_local_op, true, true));
         R.reset(new ProductOperator(ML_inv_vea.get(), M_LH.get(), false,
                                     false));

         precon.reset(new OperatorJacobiSmoother(M_H, ess_tdof_list));
      }
      else
      {
         ML_inv_vea.reset(new H1SpaceLumpedMassOperator(pfes_ho_scalar.get(),
                                                        pfes_lor_scalar.get(),
                                                        ML_inv_ea));
         M_LH.reset(M_LH_local_op);

         precon.reset(new OperatorJacobiSmoother(M_H, ess_tdof_list));
      }
   }
   R.reset(new ProductOperator(ML_inv_vea.get(), M_LH.get(), false,
                               false));

   TransposeOperator* RT = new TransposeOperator(R.get());
   RTxM_LH.reset(new ProductOperator(RT, M_LH.get(), true, false));

   SetupPCG();
}

#endif

void L2ProjectionGridTransfer::L2ProjectionH1Space::Mult(
   const Vector& x, Vector& y) const
{
   Vector X(fes_ho.GetTrueVSize());
   Vector X_dim(R->Width());

   Vector Y_dim(R->Height());
   Vector Y(fes_lor.GetTrueVSize());

   Array<int> vdofs_list;

   GetTDofs(fes_ho, x, X);

   for (int d = 0; d < fes_ho.GetVDim(); ++d)
   {
      TDofsListByVDim(fes_ho, d, vdofs_list);
      X.GetSubVector(vdofs_list, X_dim);
      R->Mult(X_dim, Y_dim);
      TDofsListByVDim(fes_lor, d, vdofs_list);
      Y.SetSubVector(vdofs_list, Y_dim);
   }

   SetFromTDofs(fes_lor, Y, y);

}

void L2ProjectionGridTransfer::L2ProjectionH1Space::MultTranspose(
   const Vector& x, Vector& y) const
{
   Vector X(fes_lor.GetTrueVSize());
   Vector X_dim(R->Height());

   Vector Y_dim(R->Width());
   Vector Y(fes_ho.GetTrueVSize());

   Array<int> vdofs_list;

   GetTDofsTranspose(fes_lor, x, X);

   for (int d = 0; d < fes_ho.GetVDim(); ++d)
   {
      TDofsListByVDim(fes_lor, d, vdofs_list);
      X.GetSubVector(vdofs_list, X_dim);
      R->MultTranspose(X_dim, Y_dim);
      TDofsListByVDim(fes_ho, d, vdofs_list);
      Y.SetSubVector(vdofs_list, Y_dim);
   }

   SetFromTDofsTranspose(fes_ho, Y, y);

}

void L2ProjectionGridTransfer::L2ProjectionH1Space::Prolongate(
   const Vector& x, Vector& y) const
{

   Vector X(fes_lor.GetTrueVSize());
   Vector X_dim(M_LH->Height());
   Vector Xbar(pcg.Width());

   Vector Y_dim(pcg.Height());
   Y_dim = 0.0;
   Vector Y(fes_ho.GetTrueVSize());

   Array<int> vdofs_list;

   GetTDofs(fes_lor, x, X);

   for (int d = 0; d < fes_ho.GetVDim(); ++d)
   {
      TDofsListByVDim(fes_lor, d, vdofs_list);
      X.GetSubVector(vdofs_list, X_dim);
      // Compute y = P x = (R^T M_LH)^(-1) M_LH^T X = (R^T M_LH)^(-1) Xbar
      M_LH->MultTranspose(X_dim, Xbar);
      pcg.Mult(Xbar, Y_dim);
      TDofsListByVDim(fes_ho, d, vdofs_list);
      Y.SetSubVector(vdofs_list, Y_dim);
   }

   SetFromTDofs(fes_ho, Y, y);

}

void L2ProjectionGridTransfer::L2ProjectionH1Space::ProlongateTranspose(
   const Vector& x, Vector& y) const
{
   Vector X(fes_ho.GetTrueVSize());
   Vector X_dim(pcg.Width());
   Vector Xbar(pcg.Height());

   Vector Y_dim(M_LH->Height());
   Vector Y(fes_lor.GetTrueVSize());

   Array<int> vdofs_list;

   GetTDofsTranspose(fes_ho, x, X);

   for (int d = 0; d < fes_ho.GetVDim(); ++d)
   {
      TDofsListByVDim(fes_ho, d, vdofs_list);
      X.GetSubVector(vdofs_list, X_dim);
      // Compute y = P^T x = M_LH (R^T M_LH)^(-1) X = M_LH Xbar
      Xbar = 0.0;
      pcg.Mult(X_dim, Xbar);
      M_LH->Mult(Xbar, Y_dim);
      TDofsListByVDim(fes_lor, d, vdofs_list);
      Y.SetSubVector(vdofs_list, Y_dim);
   }

   SetFromTDofsTranspose(fes_lor, Y, y);

}

void L2ProjectionGridTransfer::L2ProjectionH1Space::SetRelTol(real_t p_rtol_)
{
   pcg.SetRelTol(p_rtol_);
}

void L2ProjectionGridTransfer::L2ProjectionH1Space::SetAbsTol(real_t p_atol_)
{
   pcg.SetAbsTol(p_atol_);
}

std::pair<
std::unique_ptr<SparseMatrix>,
std::unique_ptr<SparseMatrix>>
                            L2ProjectionGridTransfer::L2ProjectionH1Space::ComputeSparseRAndM_LH()
{
   std::pair<std::unique_ptr<SparseMatrix>,
       std::unique_ptr<SparseMatrix>> r_and_mlh;

   Mesh* mesh_ho = fes_ho.GetMesh();
   Mesh* mesh_lor = fes_lor.GetMesh();
   int nel_ho = mesh_ho->GetNE();
   int nel_lor = mesh_lor->GetNE();
   int ndof_lor = fes_lor.GetNDofs();

   // If the local mesh is empty, skip all computations
   if (nel_ho == 0)
   {
      return std::make_pair(
                std::unique_ptr<SparseMatrix>(new SparseMatrix),
                std::unique_ptr<SparseMatrix>(new SparseMatrix)
             );
   }

   const CoarseFineTransformations& cf_tr = mesh_lor->GetRefinementTransforms();

   int nref_max = 0;
   Array<Geometry::Type> geoms;
   mesh_ho->GetGeometries(mesh_ho->Dimension(), geoms);
   for (int ig = 0; ig < geoms.Size(); ++ig)
   {
      Geometry::Type geom = geoms[ig];
      nref_max = std::max(nref_max, cf_tr.point_matrices[geom].SizeK());
   }

   BuildHo2Lor(nel_ho, nel_lor, cf_tr);

   // ML_inv contains the inverse lumped (row sum) mass matrix. Note that the
   // method will also work with a full (consistent) mass matrix, though this is
   // not implemented here. L refers to the low-order refined mesh
   Vector ML_inv(ndof_lor);
   ML_inv = 0.0;

   // Compute ML_inv
   for (int iho = 0; iho < nel_ho; ++iho)
   {
      Array<int> lor_els;
      ho2lor.GetRow(iho, lor_els);
      int nref = ho2lor.RowSize(iho);

      Geometry::Type geom = mesh_ho->GetElementBaseGeometry(iho);
      const FiniteElement& fe_lor = *fes_lor.GetFE(lor_els[0]);
      int nedof_lor = fe_lor.GetDof();

      // Instead of using a MassIntegrator, manually loop over integration
      // points so we can row sum and store the diagonal as a Vector.
      Vector ML_el(nedof_lor);
      Vector shape_lor(nedof_lor);
      Array<int> dofs_lor(nedof_lor);

      for (int iref = 0; iref < nref; ++iref)
      {
         int ilor = lor_els[iref];
         ElementTransformation* el_tr = fes_lor.GetElementTransformation(ilor);

         int order = 2 * fe_lor.GetOrder() + el_tr->OrderW();
         const IntegrationRule* ir = &IntRules.Get(geom, order);
         ML_el = 0.0;
         for (int i = 0; i < ir->GetNPoints(); ++i)
         {
            const IntegrationPoint& ip_lor = ir->IntPoint(i);
            fe_lor.CalcShape(ip_lor, shape_lor);
            el_tr->SetIntPoint(&ip_lor);
            ML_el += (shape_lor *= (el_tr->Weight() * ip_lor.weight));
         }
         fes_lor.GetElementDofs(ilor, dofs_lor);
         ML_inv.AddElementVector(dofs_lor, ML_el);
      }
   }
   // DOF by DOF inverse of non-zero entries
   LumpedMassInverse(ML_inv);

   // Compute sparsity pattern for R = M_L^(-1) M_LH and allocate
   r_and_mlh.first = AllocR();
   // Allocate M_LH (same sparsity pattern as R)
   // L refers to the low-order refined mesh (DOFs correspond to rows)
   // H refers to the higher-order mesh (DOFs correspond to columns)
   Memory<int> I(r_and_mlh.first->Height() + 1);
   for (int icol = 0; icol < r_and_mlh.first->Height() + 1; ++icol)
   {
      I[icol] = r_and_mlh.first->GetI()[icol];
   }
   Memory<int> J(r_and_mlh.first->NumNonZeroElems());
   for (int jcol = 0; jcol < r_and_mlh.first->NumNonZeroElems(); ++jcol)
   {
      J[jcol] = r_and_mlh.first->GetJ()[jcol];
   }
   r_and_mlh.second = std::unique_ptr<SparseMatrix>(
                         new SparseMatrix(I, J, NULL, r_and_mlh.first->Height(),
                                          r_and_mlh.first->Width(), true, true, true));

   IntegrationPointTransformation ip_tr;
   IsoparametricTransformation& emb_tr = ip_tr.Transf;

   // Compute M_LH and R
   offsets.SetSize(nel_ho+1);
   offsets[0] = 0;
   for (int iho = 0; iho < nel_ho; ++iho)
   {
      Array<int> lor_els;
      ho2lor.GetRow(iho, lor_els);
      int nref = ho2lor.RowSize(iho);

      Geometry::Type geom = mesh_ho->GetElementBaseGeometry(iho);
      const FiniteElement& fe_ho = *fes_ho.GetFE(iho);
      const FiniteElement& fe_lor = *fes_lor.GetFE(lor_els[0]);
      offsets[iho+1] = offsets[iho] + fe_ho.GetDof()*fe_lor.GetDof()*nref;

      ElementTransformation *tr_ho = fes_ho.GetElementTransformation(iho);

      emb_tr.SetIdentityTransformation(geom);
      const DenseTensor& pmats = cf_tr.point_matrices[geom];

      int nedof_ho = fe_ho.GetDof();
      int nedof_lor = fe_lor.GetDof();
      DenseMatrix M_LH_el(nedof_lor, nedof_ho);
      DenseMatrix R_el(nedof_lor, nedof_ho);

      for (int iref = 0; iref < nref; ++iref)
      {
         int ilor = lor_els[iref];
         ElementTransformation* tr_lor = fes_lor.GetElementTransformation(ilor);

         // Create the transformation that embeds the fine low-order element
         // within the coarse high-order element in reference space
         emb_tr.SetPointMat(pmats(cf_tr.embeddings[ilor].matrix));

         ElemMixedMass(geom, fe_ho, fe_lor, tr_ho, tr_lor, ip_tr, M_LH_el);

         Array<int> dofs_lor(nedof_lor);
         fes_lor.GetElementDofs(ilor, dofs_lor);
         Vector R_row;
         for (int i = 0; i < nedof_lor; ++i)
         {
            M_LH_el.GetRow(i, R_row);
            R_el.SetRow(i, R_row.Set(ML_inv[dofs_lor[i]], R_row));
         }
         Array<int> dofs_ho(nedof_ho);
         fes_ho.GetElementDofs(iho, dofs_ho);
         r_and_mlh.second->AddSubMatrix(dofs_lor, dofs_ho, M_LH_el);
         r_and_mlh.first->AddSubMatrix(dofs_lor, dofs_ho, R_el);

      }
   }

   return r_and_mlh;
}

void L2ProjectionGridTransfer::L2ProjectionH1Space::GetTDofs(
   const FiniteElementSpace& fes, const Vector& x, Vector& X) const
{
   const Operator* res = fes.GetRestrictionOperator();
   if (res)
   {
      res->Mult(x, X);
   }
   else
   {
      X = x;
   }
}

void L2ProjectionGridTransfer::L2ProjectionH1Space::SetFromTDofs(
   const FiniteElementSpace& fes, const Vector &X, Vector& x) const
{
   const Operator* P = fes.GetProlongationMatrix();
   if (P)
   {
      P->Mult(X, x);
   }
   else
   {
      x = X;
   }
}

void L2ProjectionGridTransfer::L2ProjectionH1Space::GetTDofsTranspose(
   const FiniteElementSpace& fes, const Vector& x, Vector& X) const
{
   const Operator* P = fes.GetProlongationMatrix();
   if (P)
   {
      P->MultTranspose(x, X);
   }
   else
   {
      X = x;
   }
}

void L2ProjectionGridTransfer::L2ProjectionH1Space::SetFromTDofsTranspose(
   const FiniteElementSpace& fes, const Vector &X, Vector& x) const
{
   const Operator *R_op = fes.GetRestrictionOperator();
   if (R_op)
   {
      R_op->MultTranspose(X, x);
   }
   else
   {
      x = X;
   }
}

void L2ProjectionGridTransfer::L2ProjectionH1Space::TDofsListByVDim(
   const FiniteElementSpace& fes, int vdim, Array<int>& vdofs_list) const
{
   const SparseMatrix *R_mat = fes.GetRestrictionMatrix();
   if (R_mat)
   {
      Array<int> x_vdofs_list(fes.GetNDofs());
      Array<int> x_vdofs_marker(fes.GetVSize());
      Array<int> X_vdofs_marker(fes.GetTrueVSize());
      fes.GetVDofs(vdim, x_vdofs_list);
      FiniteElementSpace::ListToMarker(x_vdofs_list, fes.GetVSize(), x_vdofs_marker);
      R_mat->BooleanMult(x_vdofs_marker, X_vdofs_marker);
      FiniteElementSpace::MarkerToList(X_vdofs_marker, vdofs_list);
   }
   else
   {
      vdofs_list.SetSize(fes.GetNDofs());
      fes.GetVDofs(vdim, vdofs_list);
   }
}

void L2ProjectionGridTransfer::L2ProjectionH1Space::LumpedMassInverse(
   Vector& ML_inv) const
{
#ifdef MFEM_USE_MPI
   // LumpedMassInverse may get called from serial and MPI parallel routines
   // since we do not know which code path is calling it we must check if
   // the pfes pointer is null when MPI is available.
   auto * fes = pfes_lor_scalar == nullptr ? fes_lor_scalar.get() :
                pfes_lor_scalar.get();
#else
   auto * fes = fes_lor_scalar.get();
#endif
   MFEM_ASSERT(fes != nullptr, "[p]fes_lor_scalar is nullptr");

   Vector ML_inv_true(fes->GetTrueVSize());
   const Operator *P = fes->GetProlongationMatrix();
   if (P) { P->MultTranspose(ML_inv, ML_inv_true); }
   else { ML_inv_true = ML_inv; }

   ML_inv_true.Reciprocal();

   if (P) { P->Mult(ML_inv_true, ML_inv); }
   else { ML_inv = ML_inv_true; }

}

std::unique_ptr<SparseMatrix>
L2ProjectionGridTransfer::L2ProjectionH1Space::AllocR()
{
   const Table& elem_dof_ho = fes_ho.GetElementToDofTable();
   const Table& elem_dof_lor = fes_lor.GetElementToDofTable();
   const int ndof_ho = fes_ho.GetNDofs();
   const int ndof_lor = fes_lor.GetNDofs();

   Table dof_elem_lor;
   Transpose(elem_dof_lor, dof_elem_lor, ndof_lor);

   Mesh* mesh_lor = fes_lor.GetMesh();
   const CoarseFineTransformations& cf_tr = mesh_lor->GetRefinementTransforms();

   // mfem::Mult but uses ho2lor to map HO elements to LOR elements
   const int* elem_dof_hoI = elem_dof_ho.GetI();
   const int* elem_dof_hoJ = elem_dof_ho.GetJ();
   const int* dof_elem_lorI = dof_elem_lor.GetI();
   const int* dof_elem_lorJ = dof_elem_lor.GetJ();

   Array<int> I(ndof_lor + 1);

   // figure out the size of J
   Array<int> dof_used_ho;
   dof_used_ho.SetSize(ndof_ho, -1);

   int sizeJ = 0;
   for (int ilor = 0; ilor < ndof_lor; ++ilor)
   {
      for (int jlor = dof_elem_lorI[ilor]; jlor < dof_elem_lorI[ilor + 1]; ++jlor)
      {
         int el_lor = dof_elem_lorJ[jlor];
         int iho = cf_tr.embeddings[el_lor].parent;
         for (int jho = elem_dof_hoI[iho]; jho < elem_dof_hoI[iho + 1]; ++jho)
         {
            int dof_ho = elem_dof_hoJ[jho];
            if (dof_used_ho[dof_ho] != ilor)
            {
               dof_used_ho[dof_ho] = ilor;
               ++sizeJ;
            }
         }
      }
   }

   // initialize dof_ho_dof_lor
   Table dof_lor_dof_ho;
   dof_lor_dof_ho.SetDims(ndof_lor, sizeJ);

   for (int i = 0; i < ndof_ho; ++i)
   {
      dof_used_ho[i] = -1;
   }

   // set values of J
   int* dof_dofI = dof_lor_dof_ho.GetI();
   int* dof_dofJ = dof_lor_dof_ho.GetJ();
   sizeJ = 0;
   for (int ilor = 0; ilor < ndof_lor; ++ilor)
   {
      dof_dofI[ilor] = sizeJ;
      for (int jlor = dof_elem_lorI[ilor]; jlor < dof_elem_lorI[ilor + 1]; ++jlor)
      {
         int el_lor = dof_elem_lorJ[jlor];
         int iho = cf_tr.embeddings[el_lor].parent;
         for (int jho = elem_dof_hoI[iho]; jho < elem_dof_hoI[iho + 1]; ++jho)
         {
            int dof_ho = elem_dof_hoJ[jho];
            if (dof_used_ho[dof_ho] != ilor)
            {
               dof_used_ho[dof_ho] = ilor;
               dof_dofJ[sizeJ] = dof_ho;
               ++sizeJ;
            }
         }
      }
   }

   dof_lor_dof_ho.SortRows();
   real_t* data = Memory<real_t>(dof_dofI[ndof_lor]);

   std::unique_ptr<SparseMatrix> R_local(new SparseMatrix(
                                            dof_dofI, dof_dofJ, data, ndof_lor,
                                            ndof_ho, true, true, true));
   (*R_local) = 0.0;

   dof_lor_dof_ho.LoseData();

   return R_local;
}

L2ProjectionGridTransfer::H1SpaceMixedMassOperator::H1SpaceMixedMassOperator(
   const FiniteElementSpace* fes_ho_, const FiniteElementSpace* fes_lor_,
   Table* ho2lor_, Vector* M_LH_ea_) :
   Operator(fes_lor_->GetElementRestriction(ElementDofOrdering::NATIVE)->Width(),
            fes_ho_->GetElementRestriction(ElementDofOrdering::NATIVE)->Width()),
   fes_ho(fes_ho_), fes_lor(fes_lor_), ho2lor(ho2lor_),
   M_LH_ea(M_LH_ea_)
{ }

void L2ProjectionGridTransfer::H1SpaceMixedMassOperator::Mult(const Vector &x,
                                                              Vector &y) const
{
   const Operator* elem_restrict_ho = fes_ho->GetElementRestriction(
                                         ElementDofOrdering::NATIVE);
   const Operator* elem_restrict_lor = fes_lor->GetElementRestriction(
                                          ElementDofOrdering::NATIVE);

   const int vdim = fes_ho->GetVDim();
   const int iho = 0;
   const int nref = ho2lor->RowSize(iho);
   const int ndof_ho = fes_ho->GetFE(iho)->GetDof();
   const int ndof_lor = fes_lor->GetFE(ho2lor->GetRow(iho)[0])->GetDof();
   const Mesh *mesh_ho = fes_ho->GetMesh();
   const int nel_ho = mesh_ho->GetNE();

   Vector tempx(elem_restrict_ho->Height());
   elem_restrict_ho->Mult(x, tempx);

   Vector tempy(ndof_lor*nref*vdim*nel_ho);

   auto v_M_mixed_ea = Reshape(M_LH_ea->Read(), ndof_lor, ndof_ho, nref,
                               nel_ho);
   auto v_tempx    = Reshape(tempx.Read(), ndof_ho, vdim, nel_ho);
   auto v_tempy    = Reshape(tempy.Write(), ndof_lor, nref, vdim, nel_ho);


   mfem::forall(ndof_lor * nref * vdim * nel_ho, [=] MFEM_HOST_DEVICE (int tid)
   {
      const int j   = tid % ndof_lor;
      const int i   = (tid / ndof_lor) % nref;
      const int v   = (tid / (ndof_lor * nref)) % vdim;
      const int iho = (tid / (ndof_lor * nref * vdim)) % nel_ho;

      real_t dot = 0.0;
      for (int k=0; k<ndof_ho; ++k)
      {
         dot += v_M_mixed_ea(j, k, i, iho) * v_tempx(k, v, iho);
      }

      v_tempy(j, i, v, iho) = dot;
   });

   elem_restrict_lor->MultTranspose(tempy, y);
}

void L2ProjectionGridTransfer::H1SpaceMixedMassOperator::MultTranspose(
   const Vector &x, Vector &y) const
{
   const Operator* elem_restrict_ho = fes_ho->GetElementRestriction(
                                         ElementDofOrdering::NATIVE);
   const Operator* elem_restrict_lor = fes_lor->GetElementRestriction(
                                          ElementDofOrdering::NATIVE);

   const int vdim = fes_ho->GetVDim();
   const int iho = 0;
   const int nref = ho2lor->RowSize(iho);
   const int ndof_ho = fes_ho->GetFE(iho)->GetDof();
   const int ndof_lor = fes_lor->GetFE(ho2lor->GetRow(iho)[0])->GetDof();
   const Mesh *mesh_ho = fes_ho->GetMesh();
   const int nel_ho = mesh_ho->GetNE();

   Vector tempx(elem_restrict_lor->Height());
   elem_restrict_lor->Mult(x, tempx);

   Vector tempy(ndof_ho*vdim*nel_ho);

   auto v_M_mixed_ea = Reshape(M_LH_ea->Read(), ndof_lor, ndof_ho, nref,
                               nel_ho);
   auto v_tempx    = Reshape(tempx.Read(), ndof_lor, nref, vdim, nel_ho);
   auto v_tempy    = Reshape(tempy.Write(), ndof_ho, vdim, nel_ho);

   mfem::forall(ndof_ho * vdim * nel_ho, [=] MFEM_HOST_DEVICE (int tid)
   {
      const int k = tid % ndof_ho;
      const int v = (tid / ndof_ho) % vdim;
      const int iho = (tid / (ndof_ho * vdim)) % nel_ho;

      real_t dot = 0.0;
      for (int i=0; i<nref; ++i)
      {
         for (int j=0; j<ndof_lor; ++j)
         {
            dot += v_M_mixed_ea(j, k, i, iho) * v_tempx(j, i, v, iho);
         }
         v_tempy(k, v, iho) = dot;
      }
   });

   elem_restrict_ho->MultTranspose(tempy, y);
}

L2ProjectionGridTransfer::H1SpaceLumpedMassOperator::H1SpaceLumpedMassOperator(
   const FiniteElementSpace* fes_ho_,
   const FiniteElementSpace* fes_lor_,
   Vector& ML_inv_) :
   Operator(ML_inv_.Size(), ML_inv_.Size()),
   fes_ho(fes_ho_), fes_lor(fes_lor_),
   ML_inv(&ML_inv_)
{ }

void L2ProjectionGridTransfer::H1SpaceLumpedMassOperator::Mult(const Vector &x,
                                                               Vector &y) const
{
   MFEM_ASSERT(ML_inv->Size() == x.Size(), "sizes not the same");
   auto v_ML_inv = Reshape(ML_inv->Read(), ML_inv->Size());
   auto v_x = Reshape(x.Read(), x.Size());
   auto v_y = Reshape(y.Write(), y.Size());

   mfem::forall(ML_inv->Size(), [=] MFEM_HOST_DEVICE(int i)
   { v_y(i) = v_ML_inv(i) * v_x(i); });
}

void L2ProjectionGridTransfer::H1SpaceLumpedMassOperator::MultTranspose(
   const Vector &x, Vector &y) const
{
   this->Mult(x,y); // lumped diagonal has the same Mult and MultTranspose behavior
}

L2ProjectionGridTransfer::~L2ProjectionGridTransfer()
{
   delete F;
   delete B;
}

const Operator &L2ProjectionGridTransfer::ForwardOperator()
{
   if (!F) { BuildF(); }
   return *F;
}

const Operator &L2ProjectionGridTransfer::BackwardOperator()
{
   if (!B)
   {
      if (!F) { BuildF(); }
      B = new L2Prolongation(*F);
   }
   return *B;
}

void L2ProjectionGridTransfer::BuildF()
{
   if (!force_l2_space &&
       dom_fes.FEColl()->GetContType() == FiniteElementCollection::CONTINUOUS)
   {
      if (!Parallel())
      {
         F = new L2ProjectionH1Space(dom_fes, ran_fes,
                                     use_ea, d_mt);
      }
      else
      {
#ifdef MFEM_USE_MPI
         const mfem::ParFiniteElementSpace& dom_pfes =
            static_cast<mfem::ParFiniteElementSpace&>(dom_fes);
         const mfem::ParFiniteElementSpace& ran_pfes =
            static_cast<mfem::ParFiniteElementSpace&>(ran_fes);
         F = new L2ProjectionH1Space(dom_pfes, ran_pfes,
                                     use_ea, d_mt);
#endif
      }
   }
   else
   {
      F = new L2ProjectionL2Space(dom_fes, ran_fes,
                                  use_ea, d_mt);
   }
}

bool L2ProjectionGridTransfer::SupportsBackwardsOperator() const
{
   return ran_fes.GetTrueVSize() >= dom_fes.GetTrueVSize();
}


TransferOperator::TransferOperator(const FiniteElementSpace& lFESpace_,
                                   const FiniteElementSpace& hFESpace_)
   : Operator(hFESpace_.GetVSize(), lFESpace_.GetVSize())
{
   bool isvar_order = lFESpace_.IsVariableOrder() || hFESpace_.IsVariableOrder();
   if (lFESpace_.FEColl() == hFESpace_.FEColl() && !isvar_order)
   {
      OperatorPtr P(Operator::ANY_TYPE);
      hFESpace_.GetTransferOperator(lFESpace_, P);
      P.SetOperatorOwner(false);
      opr = P.Ptr();
   }
   else if (lFESpace_.GetVDim() == 1
            && hFESpace_.GetVDim() == 1
            && dynamic_cast<const TensorBasisElement*>(lFESpace_.GetTypicalFE())
            && dynamic_cast<const TensorBasisElement*>(hFESpace_.GetTypicalFE())
            && !isvar_order
            && (hFESpace_.FEColl()->GetContType() ==
                mfem::FiniteElementCollection::CONTINUOUS ||
                hFESpace_.FEColl()->GetContType() ==
                mfem::FiniteElementCollection::DISCONTINUOUS))
   {
      opr = new TensorProductPRefinementTransferOperator(lFESpace_, hFESpace_);
   }
   else
   {
      opr = new PRefinementTransferOperator(lFESpace_, hFESpace_);
   }
}

TransferOperator::~TransferOperator() { delete opr; }

void TransferOperator::Mult(const Vector& x, Vector& y) const
{
   opr->Mult(x, y);
}

void TransferOperator::MultTranspose(const Vector& x, Vector& y) const
{
   opr->MultTranspose(x, y);
}


PRefinementTransferOperator::PRefinementTransferOperator(
   const FiniteElementSpace& lFESpace_, const FiniteElementSpace& hFESpace_)
   : Operator(hFESpace_.GetVSize(), lFESpace_.GetVSize()), lFESpace(lFESpace_),
     hFESpace(hFESpace_)
{
   isvar_order = lFESpace_.IsVariableOrder() || hFESpace_.IsVariableOrder();
}

void PRefinementTransferOperator::Mult(const Vector& x, Vector& y) const
{
   Mesh* mesh = hFESpace.GetMesh();
   Array<int> l_dofs, h_dofs, l_vdofs, h_vdofs;
   DenseMatrix loc_prol;
   Vector subY, subX;

   Geometry::Type cached_geom = Geometry::INVALID;
   const FiniteElement* h_fe = NULL;
   const FiniteElement* l_fe = NULL;
   IsoparametricTransformation T;

   int vdim = lFESpace.GetVDim();

   y = 0.0;

   DofTransformation doftrans_h, doftrans_l;
   for (int i = 0; i < mesh->GetNE(); i++)
   {
      hFESpace.GetElementDofs(i, h_dofs, doftrans_h);
      lFESpace.GetElementDofs(i, l_dofs, doftrans_l);

      const Geometry::Type geom = mesh->GetElementBaseGeometry(i);
      if (geom != cached_geom || isvar_order)
      {
         h_fe = hFESpace.GetFE(i);
         l_fe = lFESpace.GetFE(i);
         T.SetIdentityTransformation(h_fe->GetGeomType());
         h_fe->GetTransferMatrix(*l_fe, T, loc_prol);
         subY.SetSize(loc_prol.Height());
         cached_geom = geom;
      }

      for (int vd = 0; vd < vdim; vd++)
      {
         l_dofs.Copy(l_vdofs);
         lFESpace.DofsToVDofs(vd, l_vdofs);
         h_dofs.Copy(h_vdofs);
         hFESpace.DofsToVDofs(vd, h_vdofs);
         x.GetSubVector(l_vdofs, subX);
         doftrans_l.InvTransformPrimal(subX);
         loc_prol.Mult(subX, subY);
         doftrans_h.TransformPrimal(subY);
         y.SetSubVector(h_vdofs, subY);
      }
   }
}

void PRefinementTransferOperator::MultTranspose(const Vector& x,
                                                Vector& y) const
{
   y = 0.0;

   Mesh* mesh = hFESpace.GetMesh();
   Array<int> l_dofs, h_dofs, l_vdofs, h_vdofs;
   DenseMatrix loc_prol;
   Vector subY, subX;

   Array<char> processed(hFESpace.GetVSize());
   processed = 0;

   Geometry::Type cached_geom = Geometry::INVALID;
   const FiniteElement* h_fe = NULL;
   const FiniteElement* l_fe = NULL;
   IsoparametricTransformation T;

   int vdim = lFESpace.GetVDim();

   DofTransformation doftrans_h, doftrans_l;

   for (int i = 0; i < mesh->GetNE(); i++)
   {
      hFESpace.GetElementDofs(i, h_dofs, doftrans_h);
      lFESpace.GetElementDofs(i, l_dofs, doftrans_l);

      const Geometry::Type geom = mesh->GetElementBaseGeometry(i);
      if (geom != cached_geom || isvar_order)
      {
         h_fe = hFESpace.GetFE(i);
         l_fe = lFESpace.GetFE(i);
         T.SetIdentityTransformation(h_fe->GetGeomType());
         h_fe->GetTransferMatrix(*l_fe, T, loc_prol);
         loc_prol.Transpose();
         subY.SetSize(loc_prol.Height());
         cached_geom = geom;
      }

      for (int vd = 0; vd < vdim; vd++)
      {
         l_dofs.Copy(l_vdofs);
         lFESpace.DofsToVDofs(vd, l_vdofs);
         h_dofs.Copy(h_vdofs);
         hFESpace.DofsToVDofs(vd, h_vdofs);

         x.GetSubVector(h_vdofs, subX);
         doftrans_h.InvTransformDual(subX);
         for (int p = 0; p < h_dofs.Size(); ++p)
         {
            if (processed[lFESpace.DecodeDof(h_dofs[p])])
            {
               subX[p] = 0.0;
            }
         }

         loc_prol.Mult(subX, subY);
         doftrans_l.TransformDual(subY);
         y.AddElementVector(l_vdofs, subY);
      }

      for (int p = 0; p < h_dofs.Size(); ++p)
      {
         processed[lFESpace.DecodeDof(h_dofs[p])] = 1;
      }
   }
}


TensorProductPRefinementTransferOperator::
TensorProductPRefinementTransferOperator(
   const FiniteElementSpace& lFESpace_,
   const FiniteElementSpace& hFESpace_)
   : Operator(hFESpace_.GetVSize(), lFESpace_.GetVSize()), lFESpace(lFESpace_),
     hFESpace(hFESpace_)
{
   // Assuming the same element type
   Mesh* mesh = lFESpace.GetMesh();
   dim = mesh->Dimension();
   const FiniteElement& el = *lFESpace.GetTypicalFE();

   const TensorBasisElement* ltel =
      dynamic_cast<const TensorBasisElement*>(&el);
   MFEM_VERIFY(ltel, "Low order FE space must be tensor product space");

   const TensorBasisElement* htel =
      dynamic_cast<const TensorBasisElement*>(hFESpace.GetTypicalFE());
   MFEM_VERIFY(htel, "High order FE space must be tensor product space");
   const Array<int>& hdofmap = htel->GetDofMap();

   const IntegrationRule& ir = hFESpace.GetTypicalFE()->GetNodes();
   IntegrationRule irLex = ir;

   // The quadrature points, or equivalently, the dofs of the high order space
   // must be sorted in lexicographical order
   for (int i = 0; i < ir.GetNPoints(); ++i)
   {
      int j = hdofmap[i] >=0 ? hdofmap[i] : -1 - hdofmap[i];
      irLex.IntPoint(i) = ir.IntPoint(j);
   }

   NE = lFESpace.GetNE();
   const DofToQuad& maps = el.GetDofToQuad(irLex, DofToQuad::TENSOR);

   D1D = maps.ndof;
   Q1D = maps.nqpt;
   B = maps.B;
   Bt = maps.Bt;

   elem_restrict_lex_l =
      lFESpace.GetElementRestriction(ElementDofOrdering::LEXICOGRAPHIC);

   MFEM_VERIFY(elem_restrict_lex_l,
               "Low order ElementRestriction not available");

   elem_restrict_lex_h =
      hFESpace.GetElementRestriction(ElementDofOrdering::LEXICOGRAPHIC);

   MFEM_VERIFY(elem_restrict_lex_h,
               "High order ElementRestriction not available");

   localL.SetSize(elem_restrict_lex_l->Height(), Device::GetMemoryType());
   localH.SetSize(elem_restrict_lex_h->Height(), Device::GetMemoryType());
   localL.UseDevice(true);
   localH.UseDevice(true);

   MFEM_VERIFY(dynamic_cast<const ElementRestriction*>(elem_restrict_lex_h),
               "High order element restriction is of unsupported type");

   mask.SetSize(localH.Size(), Device::GetMemoryType());
   static_cast<const ElementRestriction*>(elem_restrict_lex_h)
   ->BooleanMask(mask);
   mask.UseDevice(true);
}

namespace TransferKernels
{
void Prolongation2D(const int NE, const int D1D, const int Q1D,
                    const Vector& localL, Vector& localH,
                    const Array<real_t>& B, const Vector& mask)
{
   auto x_ = Reshape(localL.Read(), D1D, D1D, NE);
   auto y_ = Reshape(localH.Write(), Q1D, Q1D, NE);
   auto B_ = Reshape(B.Read(), Q1D, D1D);
   auto m_ = Reshape(mask.Read(), Q1D, Q1D, NE);

   mfem::forall(NE, [=] MFEM_HOST_DEVICE (int e)
   {
      for (int qy = 0; qy < Q1D; ++qy)
      {
         for (int qx = 0; qx < Q1D; ++qx)
         {
            y_(qx, qy, e) = 0.0;
         }
      }

      for (int dy = 0; dy < D1D; ++dy)
      {
         real_t sol_x[DofQuadLimits::MAX_Q1D];
         for (int qy = 0; qy < Q1D; ++qy)
         {
            sol_x[qy] = 0.0;
         }
         for (int dx = 0; dx < D1D; ++dx)
         {
            const real_t s = x_(dx, dy, e);
            for (int qx = 0; qx < Q1D; ++qx)
            {
               sol_x[qx] += B_(qx, dx) * s;
            }
         }
         for (int qy = 0; qy < Q1D; ++qy)
         {
            const real_t d2q = B_(qy, dy);
            for (int qx = 0; qx < Q1D; ++qx)
            {
               y_(qx, qy, e) += d2q * sol_x[qx];
            }
         }
      }
      for (int qy = 0; qy < Q1D; ++qy)
      {
         for (int qx = 0; qx < Q1D; ++qx)
         {
            y_(qx, qy, e) *= m_(qx, qy, e);
         }
      }
   });
}

void Prolongation3D(const int NE, const int D1D, const int Q1D,
                    const Vector& localL, Vector& localH,
                    const Array<real_t>& B, const Vector& mask)
{
   auto x_ = Reshape(localL.Read(), D1D, D1D, D1D, NE);
   auto y_ = Reshape(localH.Write(), Q1D, Q1D, Q1D, NE);
   auto B_ = Reshape(B.Read(), Q1D, D1D);
   auto m_ = Reshape(mask.Read(), Q1D, Q1D, Q1D, NE);

   mfem::forall(NE, [=] MFEM_HOST_DEVICE (int e)
   {
      for (int qz = 0; qz < Q1D; ++qz)
      {
         for (int qy = 0; qy < Q1D; ++qy)
         {
            for (int qx = 0; qx < Q1D; ++qx)
            {
               y_(qx, qy, qz, e) = 0.0;
            }
         }
      }

      for (int dz = 0; dz < D1D; ++dz)
      {
         real_t sol_xy[DofQuadLimits::MAX_Q1D][DofQuadLimits::MAX_Q1D];
         for (int qy = 0; qy < Q1D; ++qy)
         {
            for (int qx = 0; qx < Q1D; ++qx)
            {
               sol_xy[qy][qx] = 0.0;
            }
         }
         for (int dy = 0; dy < D1D; ++dy)
         {
            real_t sol_x[DofQuadLimits::MAX_Q1D];
            for (int qx = 0; qx < Q1D; ++qx)
            {
               sol_x[qx] = 0;
            }
            for (int dx = 0; dx < D1D; ++dx)
            {
               const real_t s = x_(dx, dy, dz, e);
               for (int qx = 0; qx < Q1D; ++qx)
               {
                  sol_x[qx] += B_(qx, dx) * s;
               }
            }
            for (int qy = 0; qy < Q1D; ++qy)
            {
               const real_t wy = B_(qy, dy);
               for (int qx = 0; qx < Q1D; ++qx)
               {
                  sol_xy[qy][qx] += wy * sol_x[qx];
               }
            }
         }
         for (int qz = 0; qz < Q1D; ++qz)
         {
            const real_t wz = B_(qz, dz);
            for (int qy = 0; qy < Q1D; ++qy)
            {
               for (int qx = 0; qx < Q1D; ++qx)
               {
                  y_(qx, qy, qz, e) += wz * sol_xy[qy][qx];
               }
            }
         }
      }
      for (int qz = 0; qz < Q1D; ++qz)
      {
         for (int qy = 0; qy < Q1D; ++qy)
         {
            for (int qx = 0; qx < Q1D; ++qx)
            {
               y_(qx, qy, qz, e) *= m_(qx, qy, qz, e);
            }
         }
      }
   });
}

void Restriction2D(const int NE, const int D1D, const int Q1D,
                   const Vector& localH, Vector& localL,
                   const Array<real_t>& Bt, const Vector& mask)
{
   auto x_ = Reshape(localH.Read(), Q1D, Q1D, NE);
   auto y_ = Reshape(localL.Write(), D1D, D1D, NE);
   auto Bt_ = Reshape(Bt.Read(), D1D, Q1D);
   auto m_ = Reshape(mask.Read(), Q1D, Q1D, NE);

   mfem::forall(NE, [=] MFEM_HOST_DEVICE (int e)
   {
      for (int dy = 0; dy < D1D; ++dy)
      {
         for (int dx = 0; dx < D1D; ++dx)
         {
            y_(dx, dy, e) = 0.0;
         }
      }

      for (int qy = 0; qy < Q1D; ++qy)
      {
         real_t sol_x[DofQuadLimits::MAX_D1D];
         for (int dx = 0; dx < D1D; ++dx)
         {
            sol_x[dx] = 0.0;
         }
         for (int qx = 0; qx < Q1D; ++qx)
         {
            const real_t s = m_(qx, qy, e) * x_(qx, qy, e);
            for (int dx = 0; dx < D1D; ++dx)
            {
               sol_x[dx] += Bt_(dx, qx) * s;
            }
         }
         for (int dy = 0; dy < D1D; ++dy)
         {
            const real_t q2d = Bt_(dy, qy);
            for (int dx = 0; dx < D1D; ++dx)
            {
               y_(dx, dy, e) += q2d * sol_x[dx];
            }
         }
      }
   });
}
void Restriction3D(const int NE, const int D1D, const int Q1D,
                   const Vector& localH, Vector& localL,
                   const Array<real_t>& Bt, const Vector& mask)
{
   auto x_ = Reshape(localH.Read(), Q1D, Q1D, Q1D, NE);
   auto y_ = Reshape(localL.Write(), D1D, D1D, D1D, NE);
   auto Bt_ = Reshape(Bt.Read(), D1D, Q1D);
   auto m_ = Reshape(mask.Read(), Q1D, Q1D, Q1D, NE);

   mfem::forall(NE, [=] MFEM_HOST_DEVICE (int e)
   {
      for (int dz = 0; dz < D1D; ++dz)
      {
         for (int dy = 0; dy < D1D; ++dy)
         {
            for (int dx = 0; dx < D1D; ++dx)
            {
               y_(dx, dy, dz, e) = 0.0;
            }
         }
      }

      for (int qz = 0; qz < Q1D; ++qz)
      {
         real_t sol_xy[DofQuadLimits::MAX_D1D][DofQuadLimits::MAX_D1D];
         for (int dy = 0; dy < D1D; ++dy)
         {
            for (int dx = 0; dx < D1D; ++dx)
            {
               sol_xy[dy][dx] = 0;
            }
         }
         for (int qy = 0; qy < Q1D; ++qy)
         {
            real_t sol_x[DofQuadLimits::MAX_D1D];
            for (int dx = 0; dx < D1D; ++dx)
            {
               sol_x[dx] = 0;
            }
            for (int qx = 0; qx < Q1D; ++qx)
            {
               const real_t s = m_(qx, qy, qz, e) * x_(qx, qy, qz, e);
               for (int dx = 0; dx < D1D; ++dx)
               {
                  sol_x[dx] += Bt_(dx, qx) * s;
               }
            }
            for (int dy = 0; dy < D1D; ++dy)
            {
               const real_t wy = Bt_(dy, qy);
               for (int dx = 0; dx < D1D; ++dx)
               {
                  sol_xy[dy][dx] += wy * sol_x[dx];
               }
            }
         }
         for (int dz = 0; dz < D1D; ++dz)
         {
            const real_t wz = Bt_(dz, qz);
            for (int dy = 0; dy < D1D; ++dy)
            {
               for (int dx = 0; dx < D1D; ++dx)
               {
                  y_(dx, dy, dz, e) += wz * sol_xy[dy][dx];
               }
            }
         }
      }
   });
}
} // namespace TransferKernels

void TensorProductPRefinementTransferOperator::Mult(const Vector& x,
                                                    Vector& y) const
{
   if (lFESpace.GetMesh()->GetNE() == 0)
   {
      return;
   }

   elem_restrict_lex_l->Mult(x, localL);
   if (dim == 2)
   {
      TransferKernels::Prolongation2D(NE, D1D, Q1D, localL, localH, B, mask);
   }
   else if (dim == 3)
   {
      TransferKernels::Prolongation3D(NE, D1D, Q1D, localL, localH, B, mask);
   }
   else
   {
      MFEM_ABORT("TensorProductPRefinementTransferOperator::Mult not "
                 "implemented for dim = "
                 << dim);
   }
   elem_restrict_lex_h->MultTranspose(localH, y);
}

void TensorProductPRefinementTransferOperator::MultTranspose(const Vector& x,
                                                             Vector& y) const
{
   if (lFESpace.GetMesh()->GetNE() == 0)
   {
      return;
   }

   elem_restrict_lex_h->Mult(x, localH);
   if (dim == 2)
   {
      TransferKernels::Restriction2D(NE, D1D, Q1D, localH, localL, Bt, mask);
   }
   else if (dim == 3)
   {
      TransferKernels::Restriction3D(NE, D1D, Q1D, localH, localL, Bt, mask);
   }
   else
   {
      MFEM_ABORT("TensorProductPRefinementTransferOperator::MultTranspose not "
                 "implemented for dim = "
                 << dim);
   }
   elem_restrict_lex_l->MultTranspose(localL, y);
}


TrueTransferOperator::TrueTransferOperator(const FiniteElementSpace& lFESpace_,
                                           const FiniteElementSpace& hFESpace_)
   : Operator(hFESpace_.GetTrueVSize(), lFESpace_.GetTrueVSize()),
     lFESpace(lFESpace_),
     hFESpace(hFESpace_)
{
   localTransferOperator = new TransferOperator(lFESpace_, hFESpace_);

   P = lFESpace.GetProlongationMatrix();
   R = hFESpace.IsVariableOrder() ? hFESpace.GetHpRestrictionMatrix() :
       hFESpace.GetRestrictionMatrix();

   // P and R can be both null
   // P can be null and R not null
   // If P is not null it is assumed that R is not null as well
   if (P) { MFEM_VERIFY(R, "Both P and R have to be not NULL") }

   if (P)
   {
      tmpL.SetSize(lFESpace_.GetVSize());
      tmpH.SetSize(hFESpace_.GetVSize());
   }
   // P can be null and R not null
   else if (R)
   {
      tmpH.SetSize(hFESpace_.GetVSize());
   }
}

TrueTransferOperator::~TrueTransferOperator()
{
   delete localTransferOperator;
}

void TrueTransferOperator::Mult(const Vector& x, Vector& y) const
{
   if (P)
   {
      P->Mult(x, tmpL);
      localTransferOperator->Mult(tmpL, tmpH);
      R->Mult(tmpH, y);
   }
   else if (R)
   {
      localTransferOperator->Mult(x, tmpH);
      R->Mult(tmpH, y);
   }
   else
   {
      localTransferOperator->Mult(x, y);
   }
}

void TrueTransferOperator::MultTranspose(const Vector& x, Vector& y) const
{
   if (P)
   {
      R->MultTranspose(x, tmpH);
      localTransferOperator->MultTranspose(tmpH, tmpL);
      P->MultTranspose(tmpL, y);
   }
   else if (R)
   {
      R->MultTranspose(x, tmpH);
      localTransferOperator->MultTranspose(tmpH, y);
   }
   else
   {
      localTransferOperator->MultTranspose(x, y);
   }
}

} // namespace mfem
