'''''
PyMFEM
built on mfem 4.3 
'''''

PyMFEM is a python wrapper for MFEM, lightweight FEM (finite element
method) library developed by LLNL (http://mfem.org).
PyMFEM is tested with python = 3.6, 3.7, 3.8, 3.9

This wrapper is meant for a rapid-prototyping of FEM programs, and
is built using SWIG 4.0.2
With PyMFEM, a user can create C++ MFEM objects and call their
method from python. We strongly recommend visiting the MFEM web site
to find more details of the MFEM library.

-- Module structure --
  <root>/mfem/ser : wrapper for serial MFEM
             /par : wrapper for parallel MFEM
	     /common : helper modules 
             /Makefile_templates: example template
	     /examples : example scripts

-- INSTALLATION --

  Please see INSTALL.txt

-- Features --
 Following features are realized in PyMFEM using SWIG interface
 in order to use MFEM more conveniently from python. 
 Some of them may be integrated to MFEM itself in future.

  4-0) mfem::Array
     Array<T> is a template in MFEM. PyMFEM has couple of intstatiation of
     this template such as
       * Array<int>  -> intArray
       * Array<double> -> doubleArray
     (example)
        >>> x = mfem.intArray(5)
	>>> x.Assign(3)   # set all elements to 3 (replacement of = operator)
	>>> x.ToList()    # convert to list
	>>> for k in x:
	       print(k)   # iterator
	>>> print(x[0])   # access to element
	>>> x[3] = 5      # set element
        >>> x[-3] = 2     # set element counting from end

        >>> print(x[3:])   # slice returns subarray. Note memory is not copyed
	                   # subarray does not own data
	
     mfem.intArray provides an access to int *. For this, we need to
     pass (int *, length) as a list/tuple.
        >>> parts = mesh.GeneratePartitioning(3)
        >>> mfem.intArray([parts, mesh.GetNE()])   <-- int * and length 


  4-1) mfem::Vector
     Constructor using python list or NumPy array:
     
        v = mfem.Vector([1,2,3.])
        v = mfem.Vector(np.array([1,2,3,4.])

        Note that when list is passed. Contents of list is copied and data is
	owned by mfem::Vector. When a NumPy array is passed, data is not copied.
	mfem::Vector holds the passed NumPy array as _link_to_data attribute,
	so that python does not garbage collect the NumPy array until mfem::Vector
	is deleted.

        Constructor also get SWIG object to <double *>. However, memory
	management for this pointer object is left to a user.
	
     Array element access:
         reading element: print v[0]
	 element assignment v[0] = 3

         negative index is also possible; v[-1] is equivalent to v[size-1]
	 
     Assign method: operator= is replaced by Assign method 
        v.Assign(1.) (equivalent to v=1 in c++)
        v.Assign(<some mfem::Vector>) (copy data from mfem vector)
        v.Assign(np.array([1., 2., 3.])  (copy data from np.array)
	
     Data Access as NumPy:  v.GetDataArray()

     In c++, a new partial view to existing vector is acquired as
	Vector v(vec.GetData() + sc, sc);
     In Python, one can call either
        v = mfem.Vector(vec, offset, size)
     or use slice
        vec[offset:offset+size]  # more python-ish...

     Note that this slice returns mere new memory view, sharing the same vector
     object. However, be careful that, when slice requires non-contiguous memory
     view, PyMFEM returns a new vector object NOT sharing the memory.
     If one wants to get the access to a memory view with non-trivial striding,
     use GetDataArray discussed below to get a NumPy array pointing the
     memory allocated for Vector, and use slice of the newly created NumPy object.
     
     Vector::GetDataArray returns NumPy object viewing the data stored in C++ Vector
     object. Note that the returned NumPy array does not own the data.

     Vector::WriteToStream(IOString, width=8) writes data to io.IOString
     ex)
         v = mfem.Vector([1,2,3])
         o = io.StringIO()
         length = v.WriteToStream(o)
         print(o.getvalue())
     

  4-2) mfem::Coefficient
       PyCoefficient is derived from FunctionCoefficient
       PyCoefficientT is time-dependent version
       VectorPyCoefficient  is derived from VectorFunctionCoefficient
       VectorPyCoefficientT is time-dependent version

       When using these class, inherit one of them and define
       cls::EvalValue method as follows

       class Jt(mfem.VectorPyCoefficient):
           def EvalValue(self, x):
               return [0.,0.,  np.cos(np.abs(x[2]-0.03)/0.03*np.pi/2)]
       class Sigma(mfem.PyCoefficient):
           def EvalValue(self, x):
               return 0.01   // need to return 

       then use it

       Cof_Jt = Jt(3) // VectorPyCoefficient constructor need sdim 
       dd = mfem.VectorFEBoundaryTangentLFIntegrator(Cof_Jt);

       If one wants to call Eval with a transformation object and
       an integration point, and one can inherit PyCoefficientBase
       (or their Vector/Matrix version) and overwrite Eval methods.
       These classes are defined as a director classes in c++.
       Therefore, if one overwrite Eval method, SWIG reroute the
       call to the Python side.

       V/M ConstantCoefficeint supports natural python object as input
          MatrixConstantCoefficient([[1,2], [2,3]])
          VectorConstantCoefficient([1,2,3])

          Note: the argument is first tested if it can be converted using
	        np.array(x,  dtype=float)

       Function coefficient written in Python can be JITed using Numba. To use
       this feature, one can either use mfem.jit.* decorators or instantiate
       NumberFunction (or Vector/MatrixNumbaFunction) and call GenerateCoefficient
       in most case, the first one is convenient. 

       -Example-
       (Using jit decorator -- see more example in ex1 and ex24)
       >>>  @mfem.jit.scalar()
            def c12(ptx):
                return s_func0(ptx[0], ptx[1],  ptx[2])
       >>>  @mfem.jit.vector()
            def f_exact(x, out):
               out[0] = (1 + kappa**2)*sin(kappa * x[1])
               out[1] = (1 + kappa**2)*sin(kappa * x[2])
               out[2] = (1 + kappa**2)*sin(kappa * x[0])
       >>> x.ProjectCoefficient(f_exact)       

       (Using NumbaFunction object)
       For example, to use Numba compiled function, one can use
       GenerateCoefficient mehtod in NumberFunction class as follows.

       >>> from numba import cfunc, carray
       >>> from mfem.coefficient import scalar_sig
       >>> sdim = mesh.SpaceDimension()
       >>> vdim = mesh.Dimension()
       >>> @cfunc(scalar_sig)
           def s_func(ptx, sdim):
           ...    return 2*ptx[0]
       >>> c = NumbaFunction(s_func, sdim).GenerateCoefficient()

       Please look test_numba.py and ex3.py for more examples

  4-3) mfem::GridFunction
       GetNodalValues(i) will perform GetNodalValue(Vector(), i) and
       return NumPy array of Vector()
  
       = operator is renamed as Assign method.
         (python)
	     g = mfem.GridFunction(fespace)
	     g.Assign(0)
         (c++)
	     GridFunction *g = new GridFunction(&fespace);
	     g = 0;
	     
      GridFunction(FiniteElementSpace *f, double *data) is extended to
      support offsets
          v = mfem.GridFunction(fec, <pointer to double>, offset)
      so that following can be wrapped
	  GridFunction(fes, vec.GetData() + sc)
      the same is used for ParGridFunction

      GridFunction::Save(std::ostream &out) is wrapped as Save(filename, precision=8)
      QuadratureFunction::Save(std::ostream &out) is wrapped as Save(filename, precision=8)

      GridFunction::WriteToStream(IOString) writes data to io.IOString
      ex)
          mesh = mfem.Mesh(3)
          fec = mfem.H1_FECollection(1)
          fes = mfem.FiniteElementSpace(mesh, fec)
          x = mfem.GridFunction(fes)
          x.Assign(0.0)
	  
          o = io.StringIO()
          l1 = mesh.WriteToStream(o)
          l2 = x.WriteToStream(o)

          print('result: ', o.getvalue())

      Those methods which take a pointer to IntegrationRule array (
      const IntegrationRule *irs[]) accept a list of IntegrationRule
      ex)
         coeff = mfem.ConstantCoefficient(1.0)
         irs = [mfem.IntRules.Get(i, order)
                for i in range(mfem.Geometry.NumGeom)]
         err = x.ComputeL2Error(coeff, irs)
	     
  4-4) mfem::Mesh
         Simple mesh generator is wrapped as follows
	     mesh = mfem.Mesh(10, 10, "TRIANGLE")
	     mesh = mfem.Mesh(10, 10, "QUADRILATERAL")
	     mesh = mfem.Mesh(10, 10, 10, "HEXAHEDRON")
	     mesh = mfem.Mesh(10, 10, 10, "TETRAHEDRON")
	     
         Wrapper of following methods are customized to
	 return either python list object or tuple of lists or int
	  
            GetBdrElementVertices(i)
            GetElementVertices(i)
            GetElementVEdges(i)
            GetBdrElementEdges(i)
            GetFaceEdges(i)
            GetEdgeVertices(i)
            GetFaceVertices(i)
            GetElementFaces(i)
	    GetBdrElementAdjacentElement()
	    GetFaceInfos()

         These methods returns an IsoparametricTransformation object
	 
            GetElementTransformation(i)
            GetBdrElementTransformation(i)
            GetFaceTransformation(i)
            GetEdgeTransformation(i)
	    
	 pointer passing in following methods are returned
	 as tuple
            elem1, elem2 = mesh.GetFaceElements(i)
	    
         Additional constructors:
	    # this one takes file name as string
	    Mesh(const char *mesh_file, int generate_edges, int refine,
                 bool fix_orientation = True)
		 
	    # this one takes element type as string
            Mesh(int nx, int ny, int nz, const char *type, int generate_edges = 0,
              double sx = 1.0, double sy = 1.0, double sz = 1.0)
            Mesh(int nx, int ny, const char *type, int generate_edges = 0,
              double sx = 1.0, double sy = 1.0)
	      
	    (note) an issue is Element::Type (c++ enum) is treated as int in
	    python, and therefore, the wrapper can not distinguish it from the
	    following constructor.
              Mesh(int _Dim, int NVert, int NElem, int NBdrElem = 0,
    	           int _spaceDim= -1)
         
         GetVertexArray
	    GetVertexArray(i) : returns i-th vertex as numpy array
            GetVertexArray()  : returns all vertices as 2D numpy array
	    
	 GetElementCenterArray(i): returns i-th vertex element center as numpy array
	 
	 as NumPy object

         GetBdrElementFace(i) returns tuple
         GetBdrArray(int idx) returns array of boundary element whose BdrAttribute is idx
         GetBdrAttributeArray() returns array of boundary attribute
         GetAttributeArray() returns array of attribute
         GetDomainArray(int idx) returns array of element whose Attribute is idx
	 
         SwapNodes returns nodes and owns_nodes as follows.
	    nodes, owns_nodes = mesh.SwapNodes(nodes, owns_nodes)
	    nodes, owns_nodes = pmesh.SwapNodes(nodes, owns_nodes)

         Following method filename
	    virtual void PrintXG(std::ostream &out = mfem::out) const;
            virtual void Print(std::ostream &out = mfem::out) const { Printer(out); }
            void PrintVTK(std::ostream &out);	    
         Following method accept empty argument (=std:cout) and filename
            virtual void PrintInfo(std::ostream &out = mfem::out)


         FindPoints receive only one argument (points to look for) and returns
	 count, element_id, integration_points. Note points are row order. For example,

         >>> count, elem_ids, int_pts = mesh.FindPoint([1,2,3], [3,4,5], [4,5,6], [7,8,9]],
 	                                               warn=True, int_trans=None)
         will look for 4 points.

         GetScaledJacobian(i, sd) returns minimum Jacobian computed on in each element.

         CaltecianPartitioning(nxyz, return_list=False)
	    * accept tuple/list as nxyz
            * return_list = True will return a list to represent the patitioning
	      by default it returns SWIG native int * object.

         Mesh::WriteToStream(IOString) writes data to io.IOString
	 ex)
             v = mfem.Mesh(3)
             o = io.StringIO()
             length = v.WriteToStream(o)
	     print('data: ', o.getvalue())

        AddVertex accepts either numpy float array, mfem.Vector or list/tuple
	AddQuad (and other similar methods) accepts either numpy int32 array, Array<int> or list/tuple

  4-5) sparsemat
         RAP has two different implementations. One accept three references.
	 The other accept a pointer as a third argument, instead.
	 These two are not distinguishable from python. So, RAP_P and
	 RAP_R are added. P indicates that the third argument is a pointer.
	 
         Add functions are accessed as add_sparse.

         GetIArray, GetJArray, and GetDataArray. These methods give NumPy
	 array of CSR matrix data. It borrows data from SparseMatrix.
	 Therefore, be careful not to access after the matrix is freed.

         Following method accept empty argument (=std:cout) and filename
           void Print(std::ostream &out = mfem::out, int width_ = 4) const;
           void PrintMatlab(std::ostream &out = mfem::out) const;
           void PrintMM(std::ostream &out = mfem::out) const;
           void PrintCSR(std::ostream &out) const;
           void PrintCSR2(std::ostream &out) const;
           void PrintInfo(std::ostream &out) const;

        Constructor accept NumPy array
           indptr = np.array([0, 2, 3, 6], dtype=np.int32)
           indices = np.array([0, 2, 2, 0, 1, 2], dtype=np.int32)
           data = np.array([1, 2, 3, 4, 5, 6], dtype=float)
           mat = mfem.SparseMatrix([indptr, indices, data, 3,3])

  4-6) densemat
         Add functions are accessed as add_dense.
	 
	 elements are accessed similar to a regular python array
	 Assign method replaces = operator
         GetDataArray returns a memory view to the internal data as NumPy array.
	 
         (example)
              x = DenseMatrix(3, 3)	 
	      x.Assign(0)
              v = Vector([1,2,3,4,5,6,7,8,9])
  	      m = DenseMatrix(v.GetData(), 3, 3)
	      x.Assign(m)

              x[i,j] = xxx
	      print x[i,j]

              # Assigning (copy) from NumPy array
	      m = DenseMatrix(3,3)
	      m.Assign(np.arange(9.).reshape(3,3))  # <-- In this case,
	                                            #     wrapper automatically tranpose
	                                            #     input matrix so that data is
						    #     column order. The same is done
						    #     in GetDataArray()
	      
              x = DenseTensor(2, 3, 5)
	      x.Assign(1) # set all 1
	      x[5] # returns DenseMatrix with col=2 and row=3
	      print x[1,2,3]
	      x[1,2,3]=3

              # NOTE the index order is different in the NumPy array returned
	      # by DenseTensor::GetDataArray. This is because, we want 
	      # the major column access work in the same way in C and Python
	      # x[3].GetDataArray() == x.GetDataArray()[3]
	      
              x[1 2,3] = x.GetDataArray()[3,1,2]

              # tensor assignment behaves as expected. 
              t = np.arange(30).reshape(2,3,5)
	      x.Assign(t) # tensor assignment

              # because of the same reason as DenseMatrix, this operation changes
	      # internall array element order
	      t.flatten() ---> [0, 1, 2, 3, 4...]
	      print(mfem.Vector(m.Data(), 30).GetDataArray()) ---> [0, 15, 5,,,,]
	      

	      
	      
	 Note that internally MFEM DenseMatrix is column major, while NumPy
	 is row major. Therefore,
            v = Vector([1,2,3,4,5,6,7,8,9])
	    m = DenseMatrix(v.GetData(), 3, 3)
         will make
	    [1 4,7]
	    [2,5,8]
	    [3,6,9],
	 whereas
	    numpy.arange(9).reshape(3,3)
         would give you
	    [1 2,3]
	    [4,5,6]
	    [7,8,9].


	  
  4-7) estimators
        ZienkiewiczZhuEstimator and L2ZienkiewiczZhuEstimator
	have two versions of constructor. flux_fes
	can be either passed as pointer or reference. In python class, you
	need to pass 5th argument if you want to pass flux_fes as
	reference. By default,  own_flux_fes is  False, so a user
	can skip passing this keyword when passing by reference
	
	pass by reference 
          ZienkiewiczZhuEstimator(integ, sol, flux_fes, own_flux_fes = False)	  
        pass by pointer
          ZienkiewiczZhuEstimator(integ, sol, flux_fes, own_flux_fes = True)
	pass by reference 
          L2ZienkiewiczZhuEstimator(integ, sol, flux_fes, own_flux_fes = False)	  
        pass by pointer
          L2ZienkiewiczZhuEstimator(integ, sol, flux_fes, own_flux_fes = True)	  
	  

  4-8) operator
        PyTimeDependentOperatorBase and PyOperatorBase is added to
	allow for implementing those operator in python. A user
	can inherit these class and overwrite Mult in python. See
	ex9.py for example.
	
  4-9) ode
        Step uses reference passing for t and dt. To get the result
	from this method, use
	   t, dt = ode_solver.Step(u, t, dt)

  4-10) hypre
        Following methods are added through SWIG interface
        HypreParVector::GetPartitioningArray  returns Partitioning as NumPy array
        HypreParMatrix::GetColPartArray  returns ColPart as NumPy array
        HypreParMatrix::GetRowPartArray  returns RowPart as NumPy array	
        HypreParMatrix::GetCooDataArray  returns data to construct local coo matrix

        mfem.common.chypre
        This module defines CHypreVec and CHhypreMat classes. Those are classes
	supports...
	    1) create HypreParCSR and HypreParVector from scipy.sparse.csr_matrix
	    and numpy.ndarray, respectively
	    2) convert HypreMatrix/Vector to NumPy array or scipy.sparse matrix
	    3) handle complex number using a pair of Hypre object (real and imag)
        Class methods of these classes are named in a similar way to NumPy array.

   4-11) socketstream

         socketstream in PyMFEM has send_text and send_solution. These
	 method also send endl and flush. One can use them as follows
	 
         (example)
           sock = mfem.socketstream("localhost", 19916)
           sock.precision(8)
           sock.send_text("parallel " + str(num_procs) +  " " + str(myid))    
           sock.send_solution(pmesh, x)
	 
         socketstream object support << operator partially, but not works
	 perfectly. This was done by adding __lshift__ and other methods to
	 socketstream class in .i file. We don't want to wrap the whole
	 iostream. Instead, we are adding methods used in examples such as
	 precision. Also, note that sock << flush in c++ should be rephrased
	 sock.flush()
	   
   4-12) std::ostream& and std::istream&

         methods which takes std::ostream& are wrapped so that it accept
	 either
	   1) filename 
	   2) compressed filename (when the filename ends with .gz)
	   3) mfem.STDOUT
	   4) StringIO

         (example)
            mesh.PrintVTK("mesh.vtk", 1)
     	    mesh.Print(mfem.STDOUT)
	    
	    #
	    gf.SaveVTK("gf.vtk", "data", 1)
	    gf.Save("data.gf")

            # save multile gf to one StringIO....
	    from io import StringIO()
	    output = StringIO()
	    output.precision = 8
	    gf.Save(output)
	    gf2.Save(output)	    

         Some methos are wrapped so that it runs without argument. In this
	 case, data is passed to STDOUT.	 
	 Not all methods are wrapped yet. Look for OSTREAM_ADD_DEFAULT_FILE
	 (or OSTREAM_ADD_DEFAULT_STDOUT_FILE) macros in the SWIG interface
	 file to find which method is wrapped.

         (example)
	    SparseMat::PrintInfo
	    STable3D::Print

         std::istream & is also wrapped in a similar way
	   1) filename 
	   2) compressed filename (when the filename ends with .gz)
	   3) StringIO

         See test/test_gz.py to see an example
       

   4-13) mfem::table
           GetRowList returns a version of GetRow which returns Python
	   list instead of int pointer.

   4-14) mfem::ParMesh
   
         ParMesh(comm, filename)
            load parallel file
	 
         ParPrintToFile(filename)
            write parallel file
	    
   4-14) mfem::ParGridFunction

         ParGridFunction(pmesh, filename) : read parallel GridFunction

         example to read ParMesh and ParGridFunction:
   	    smyid = '{:0>6d}'.format(myid)  # myid = MPI.COMM_WORLD.rank
            mesh1 = mfem.ParMesh(comm, 'wg.mesh.'+smyid)
            mesh1.ReorientTetMesh()
            x = mfem.ParGridFunction(mesh1, 'wg.gf.'+smyid)

   4-15) mfem:BlockNonlinearForm, mfem:ParBlockNonlinearForm
        
         Constructor accept tuple/list of (Par)FiniteElementSpace
	     R_space = mfem.FiniteElementSpace(...)
             W_space = mfem.FiniteElementSpace(...)
             spaces = [R_space, W_space]
     	     mfem.BlockNonlinearForm(spaces)

             note that user must pay attention that Python does not
	     garbage collect spaces. (this will be fix in future)

         Similarly, mfem::(Par)BlockNonlinearForm::SetEssentialBC()
	 accept tuple/list of intArray (= wrapped Array<int>) and
	 mfem::Vector

   4-16) mfem:ComplexOperator
         ownReal, onwImag for Constructor is optional.
	 hermitian keyword is used to specify the type of matrix

             op_complex = ComplexOperator(M1, M2, ownReal=False, ownImag=False,
	                                  hermitian=True)
             hermitian = False results in BLOCK_SYMMETRIC format

         Returned ComplexOperator hold the passed real and imag operators
	 so that Python does not garbage collect them.
	 If thisown of real/imag operators are 1, a user has an option to
	 set ownReal, ownImag.

    4-17) strumpack
         if you have strumpack linked with MFEM, you can enable wrapper by
	 ENABLE_STRUMPACK option in make

	    make ENABLE_STRUMPACK=1
	    make ENABLE_STRUMPACK=1 STRUMPACK_INCLUDE=${HOME}/sandbox/include


         Here is sample script to use strumpack from PyMFEM (only in mfem.par)
	 
	    import mfem.par.strumpack as strmpk
            Arow = strmpk.STRUMPACKRowLocMatrix(A)
            args = ["--sp_hss_min_sep_size", "128", "--sp_enable_hss"]
            strumpack = strmpk.STRUMPACKSolver(args, MPI.COMM_WORLD)
            strumpack.SetPrintFactorStatistics(True)
            strumpack.SetPrintSolveStatistics(False)
            strumpack.SetKrylovSolver(strmpk.KrylovSolver_DIRECT);
            strumpack.SetReorderingStrategy(strmpk.ReorderingStrategy_METIS)
            strumpack.SetMC64Job(strmpk.MC64Job_NONE)
            # strumpack.SetSymmetricPattern(True)
            strumpack.SetOperator(Arow)
            strumpack.SetFromCommandLine()
            strumpack.Mult(B, X);

     4-18) OperatorHandle
           It is often required to convert Operator Handle to a specific operaotr class
           using OperatorHandle::As <T>. handle.hpp defines also Is, Get, Reset,
	   and ConvertFrom. These templates are instantiated for SparseMatrix,
	   HypreParMatrix, and PetscParMatrix. Instatiated methods have class name
	   appeded to its method name. For example, in ex1.py, it is use as
	   follows.
	   
              A = mfem.OperatorPtr()
	      <....>
              AA = A.AsSparseMatrix()
              M = mfem.GSSmoother(AA)
              mfem.PCG(A, M, B, X, 1, 200, 1e-12, 0.0)

-- Known issues --

  PyMFEM is an on-going effort. Not all MFEM functionality is properly
  wrapped yet. Presently all serial/parallel examples available in
  the top-level mfem/example directory are rewritten in python.
  This would be sufficient for certain projects, but for other projects,
  you may find various features are missing. Please inform us if you
  notice something missing.

  1) t*.hpp files are not wrapped
  
  3) Memory management.
      python uses garbage collection. Since wrapper does not know
      exactly how objects are used in c++ layer, MFEM object may be
      collected before being ready to be freed. We are addressing
      this by adjusting thisown flag or %newobject directive. This
      needs to be done method by method, and not yet completed.


  5) SuperLU and PETSc are not supported.
  
*This work is supported by US DoE contract DE-FC02-99ER54512
