#ifndef VIENNACL_LINALG_AMG_HPP_
#define VIENNACL_LINALG_AMG_HPP_

/* =========================================================================
   Copyright (c) 2010-2014, Institute for Microelectronics,
                            Institute for Analysis and Scientific Computing,
                            TU Wien.
   Portions of this software are copyright by UChicago Argonne, LLC.

                            -----------------
                  ViennaCL - The Vienna Computing Library
                            -----------------

   Project Head:    Karl Rupp                   rupp@iue.tuwien.ac.at

   (A list of authors and contributors can be found in the PDF manual)

   License:         MIT (X11), see file LICENSE in the base directory
============================================================================= */

/** @file viennacl/linalg/amg.hpp
    @brief Main include file for algebraic multigrid (AMG) preconditioners.  Experimental.

    Implementation contributed by Markus Wagner
*/

#include <boost/numeric/ublas/matrix.hpp>
#include <boost/numeric/ublas/lu.hpp>
#include <boost/numeric/ublas/operation.hpp>
#include <boost/numeric/ublas/vector_proxy.hpp>
#include <boost/numeric/ublas/matrix_proxy.hpp>
#include <boost/numeric/ublas/vector.hpp>
#include <boost/numeric/ublas/triangular.hpp>
#include <vector>
#include <cmath>
#include "viennacl/forwards.h"
#include "viennacl/tools/tools.hpp"
#include "viennacl/linalg/prod.hpp"
#include "viennacl/linalg/direct_solve.hpp"

#include "viennacl/linalg/detail/amg/amg_base.hpp"
#include "viennacl/linalg/detail/amg/amg_coarse.hpp"
#include "viennacl/linalg/detail/amg/amg_interpol.hpp"

#include <map>

#ifdef VIENNACL_WITH_OPENMP
 #include <omp.h>
#endif

#include "viennacl/linalg/detail/amg/amg_debug.hpp"

#define VIENNACL_AMG_COARSE_LIMIT 50
#define VIENNACL_AMG_MAX_LEVELS 100

namespace viennacl
{
  namespace linalg
  {
    typedef detail::amg::amg_tag          amg_tag;

    /** @brief Setup AMG preconditioner
    *
    * @param A      Operator matrices on all levels
    * @param P      Prolongation/Interpolation operators on all levels
    * @param Pointvector  Vector of points on all levels
    * @param tag    AMG preconditioner tag
    */
    template<typename InternalType1, typename InternalType2>
    void amg_setup(InternalType1 & A, InternalType1 & P, InternalType2 & Pointvector, amg_tag & tag)
    {
      typedef typename InternalType2::value_type PointVectorType;

      unsigned int i, iterations, c_points, f_points;
      detail::amg::amg_slicing<InternalType1,InternalType2> Slicing;

      // Set number of iterations. If automatic coarse grid construction is chosen (0), then set a maximum size and stop during the process.
      iterations = tag.get_coarselevels();
      if (iterations == 0)
        iterations = VIENNACL_AMG_MAX_LEVELS;

      // For parallel coarsenings build data structures (number of threads set automatically).
      if (tag.get_coarse() == VIENNACL_AMG_COARSE_RS0 || tag.get_coarse() == VIENNACL_AMG_COARSE_RS3)
        Slicing.init(iterations);

      for (i=0; i<iterations; ++i)
      {
        // Initialize Pointvector on level i and construct points.
        Pointvector[i] = PointVectorType(static_cast<unsigned int>(A[i].size1()));
        Pointvector[i].init_points();

        // Construct C and F points on coarse level (i is fine level, i+1 coarse level).
        detail::amg::amg_coarse (i, A, Pointvector, Slicing, tag);

        // Calculate number of C and F points on level i.
        c_points = Pointvector[i].get_cpoints();
        f_points = Pointvector[i].get_fpoints();

        #if defined (VIENNACL_AMG_DEBUG) //or defined(VIENNACL_AMG_DEBUGBENCH)
        std::cout << "Level " << i << ": ";
        std::cout << "No of C points = " << c_points << ", ";
        std::cout << "No of F points = " << f_points << std::endl;
        #endif

        // Stop routine when the maximal coarse level is found (no C or F point). Coarsest level is level i.
        if (c_points == 0 || f_points == 0)
          break;

        // Construct interpolation matrix for level i.
        detail::amg::amg_interpol (i, A, P, Pointvector, tag);

        // Compute coarse grid operator (A[i+1] = R * A[i] * P) with R = trans(P).
        detail::amg::amg_galerkin_prod(A[i], P[i], A[i+1]);

        // Test triple matrix product. Very slow for large matrix sizes (ublas).
        // test_triplematprod(A[i],P[i],A[i+1]);

        Pointvector[i].delete_points();

        #ifdef VIENNACL_AMG_DEBUG
        std::cout << "Coarse Grid Operator Matrix:" << std::endl;
        printmatrix (A[i+1]);
        #endif

        // If Limit of coarse points is reached then stop. Coarsest level is level i+1.
        if (tag.get_coarselevels() == 0 && c_points <= VIENNACL_AMG_COARSE_LIMIT)
        {
          tag.set_coarselevels(i+1);
          return;
        }
      }
      tag.set_coarselevels(i);
    }

    /** @brief Initialize AMG preconditioner
    *
    * @param mat    System matrix
    * @param A      Operator matrices on all levels
    * @param P      Prolongation/Interpolation operators on all levels
    * @param Pointvector  Vector of points on all levels
    * @param tag    AMG preconditioner tag
    */
    template<typename MatrixType, typename InternalType1, typename InternalType2>
    void amg_init(MatrixType const & mat, InternalType1 & A, InternalType1 & P, InternalType2 & Pointvector, amg_tag & tag)
    {
      //typedef typename MatrixType::value_type ScalarType;
      typedef typename InternalType1::value_type SparseMatrixType;

      if (tag.get_coarselevels() > 0)
      {
        A.resize(tag.get_coarselevels()+1);
        P.resize(tag.get_coarselevels());
        Pointvector.resize(tag.get_coarselevels());
      }
      else
      {
        A.resize(VIENNACL_AMG_MAX_LEVELS+1);
        P.resize(VIENNACL_AMG_MAX_LEVELS);
        Pointvector.resize(VIENNACL_AMG_MAX_LEVELS);
      }

      // Insert operator matrix as operator for finest level.
      SparseMatrixType A0 (mat);
      A.insert_element (0, A0);
    }

    /** @brief Save operators after setup phase for CPU computation.
    *
    * @param A      Operator matrices on all levels on the CPU
    * @param P      Prolongation/Interpolation operators on all levels on the CPU
    * @param R      Restriction operators on all levels on the CPU
    * @param A_setup    Operators matrices on all levels from setup phase
    * @param P_setup    Prolongation/Interpolation operators on all levels from setup phase
    * @param tag    AMG preconditioner tag
    */
    template<typename InternalType1, typename InternalType2>
    void amg_transform_cpu (InternalType1 & A, InternalType1 & P, InternalType1 & R, InternalType2 & A_setup, InternalType2 & P_setup, amg_tag & tag)
    {
      //typedef typename InternalType1::value_type MatrixType;

      // Resize internal data structures to actual size.
      A.resize(tag.get_coarselevels()+1);
      P.resize(tag.get_coarselevels());
      R.resize(tag.get_coarselevels());

      // Transform into matrix type.
      for (unsigned int i=0; i<tag.get_coarselevels()+1; ++i)
      {
        A[i].resize(A_setup[i].size1(),A_setup[i].size2(),false);
        A[i] = A_setup[i];
      }
      for (unsigned int i=0; i<tag.get_coarselevels(); ++i)
      {
        P[i].resize(P_setup[i].size1(),P_setup[i].size2(),false);
        P[i] = P_setup[i];
      }
      for (unsigned int i=0; i<tag.get_coarselevels(); ++i)
      {
        R[i].resize(P_setup[i].size2(),P_setup[i].size1(),false);
        P_setup[i].set_trans(true);
        R[i] = P_setup[i];
        P_setup[i].set_trans(false);
      }
    }

    /** @brief Save operators after setup phase for GPU computation.
    *
    * @param A      Operator matrices on all levels on the GPU
    * @param P      Prolongation/Interpolation operators on all levels on the GPU
    * @param R      Restriction operators on all levels on the GPU
    * @param A_setup    Operators matrices on all levels from setup phase
    * @param P_setup    Prolongation/Interpolation operators on all levels from setup phase
    * @param tag    AMG preconditioner tag
    * @param ctx      Optional context in which the auxiliary objects are created (one out of multiple OpenCL contexts, CUDA, host)
    */
    template<typename InternalType1, typename InternalType2>
    void amg_transform_gpu (InternalType1 & A, InternalType1 & P, InternalType1 & R, InternalType2 & A_setup, InternalType2 & P_setup, amg_tag & tag, viennacl::context ctx)
    {
      // Resize internal data structures to actual size.
      A.resize(tag.get_coarselevels()+1);
      P.resize(tag.get_coarselevels());
      R.resize(tag.get_coarselevels());

      // Copy to GPU using the internal sparse matrix structure: std::vector<std::map>.
      for (unsigned int i=0; i<tag.get_coarselevels()+1; ++i)
      {
        viennacl::switch_memory_context(A[i], ctx);
        //A[i].resize(A_setup[i].size1(),A_setup[i].size2(),false);
        viennacl::copy(*(A_setup[i].get_internal_pointer()),A[i]);
      }
      for (unsigned int i=0; i<tag.get_coarselevels(); ++i)
      {
        viennacl::switch_memory_context(P[i], ctx);
        //P[i].resize(P_setup[i].size1(),P_setup[i].size2(),false);
        viennacl::copy(*(P_setup[i].get_internal_pointer()),P[i]);
        //viennacl::copy((boost::numeric::ublas::compressed_matrix<ScalarType>)P_setup[i],P[i]);
      }
      for (unsigned int i=0; i<tag.get_coarselevels(); ++i)
      {
        viennacl::switch_memory_context(R[i], ctx);
        //R[i].resize(P_setup[i].size2(),P_setup[i].size1(),false);
        P_setup[i].set_trans(true);
        viennacl::copy(*(P_setup[i].get_internal_pointer()),R[i]);
        P_setup[i].set_trans(false);
      }
    }

    /** @brief Setup data structures for precondition phase.
    *
    * @param result    Result vector on all levels
    * @param rhs    RHS vector on all levels
    * @param residual    Residual vector on all levels
    * @param A      Operators matrices on all levels from setup phase
    * @param tag    AMG preconditioner tag
    */
    template<typename InternalVectorType, typename SparseMatrixType>
    void amg_setup_apply (InternalVectorType & result, InternalVectorType & rhs, InternalVectorType & residual, SparseMatrixType const & A, amg_tag const & tag)
    {
      typedef typename InternalVectorType::value_type VectorType;

      result.resize(tag.get_coarselevels()+1);
      rhs.resize(tag.get_coarselevels()+1);
      residual.resize(tag.get_coarselevels());

      for (unsigned int level=0; level < tag.get_coarselevels()+1; ++level)
      {
        result[level] = VectorType(A[level].size1());
        result[level].clear();
        rhs[level] = VectorType(A[level].size1());
        rhs[level].clear();
      }
      for (unsigned int level=0; level < tag.get_coarselevels(); ++level)
      {
        residual[level] = VectorType(A[level].size1());
        residual[level].clear();
      }
    }

    /** @brief Setup data structures for precondition phase for later use on the GPU
    *
    * @param result    Result vector on all levels
    * @param rhs    RHS vector on all levels
    * @param residual    Residual vector on all levels
    * @param A      Operators matrices on all levels from setup phase
    * @param tag    AMG preconditioner tag
    * @param ctx      Optional context in which the auxiliary objects are created (one out of multiple OpenCL contexts, CUDA, host)
    */
    template<typename InternalVectorType, typename SparseMatrixType>
    void amg_setup_apply (InternalVectorType & result, InternalVectorType & rhs, InternalVectorType & residual, SparseMatrixType const & A, amg_tag const & tag, viennacl::context ctx)
    {
      typedef typename InternalVectorType::value_type VectorType;

      result.resize(tag.get_coarselevels()+1);
      rhs.resize(tag.get_coarselevels()+1);
      residual.resize(tag.get_coarselevels());

      for (unsigned int level=0; level < tag.get_coarselevels()+1; ++level)
      {
        result[level] = VectorType(A[level].size1(), ctx);
        rhs[level] = VectorType(A[level].size1(), ctx);
      }
      for (unsigned int level=0; level < tag.get_coarselevels(); ++level)
      {
        residual[level] = VectorType(A[level].size1(), ctx);
      }
    }

    /** @brief Pre-compute LU factorization for direct solve (ublas library).
     *  @brief Speeds up precondition phase as this is computed only once overall instead of once per iteration.
    *
    * @param op      Operator matrix for direct solve
    * @param Permutation  Permutation matrix which saves the factorization result
    * @param A      Operator matrix on coarsest level
    */
    template<typename ScalarType, typename SparseMatrixType>
    void amg_lu(boost::numeric::ublas::compressed_matrix<ScalarType> & op, boost::numeric::ublas::permutation_matrix<> & Permutation, SparseMatrixType const & A)
    {
      typedef typename SparseMatrixType::const_iterator1 ConstRowIterator;
      typedef typename SparseMatrixType::const_iterator2 ConstColIterator;

      // Copy to operator matrix. Needed
      op.resize(A.size1(),A.size2(),false);
      for (ConstRowIterator row_iter = A.begin1(); row_iter != A.end1(); ++row_iter)
        for (ConstColIterator col_iter = row_iter.begin(); col_iter != row_iter.end(); ++col_iter)
          op (col_iter.index1(), col_iter.index2()) = *col_iter;

      // Permutation matrix has to be reinitialized with actual size. Do not clear() or resize()!
      Permutation = boost::numeric::ublas::permutation_matrix<> (op.size1());
      boost::numeric::ublas::lu_factorize(op,Permutation);
    }

    /** @brief AMG preconditioner class, can be supplied to solve()-routines
    */
    template<typename MatrixType>
    class amg_precond
    {
      typedef typename MatrixType::value_type ScalarType;
      typedef boost::numeric::ublas::vector<ScalarType> VectorType;
      typedef detail::amg::amg_sparsematrix<ScalarType> SparseMatrixType;
      typedef detail::amg::amg_pointvector PointVectorType;

      typedef typename SparseMatrixType::const_iterator1 InternalConstRowIterator;
      typedef typename SparseMatrixType::const_iterator2 InternalConstColIterator;
      typedef typename SparseMatrixType::iterator1 InternalRowIterator;
      typedef typename SparseMatrixType::iterator2 InternalColIterator;

      boost::numeric::ublas::vector <SparseMatrixType> A_setup;
      boost::numeric::ublas::vector <SparseMatrixType> P_setup;
      boost::numeric::ublas::vector <MatrixType> A;
      boost::numeric::ublas::vector <MatrixType> P;
      boost::numeric::ublas::vector <MatrixType> R;
      boost::numeric::ublas::vector <PointVectorType> Pointvector;

      mutable boost::numeric::ublas::compressed_matrix<ScalarType> op;
      mutable boost::numeric::ublas::permutation_matrix<> Permutation;

      mutable boost::numeric::ublas::vector <VectorType> result;
      mutable boost::numeric::ublas::vector <VectorType> rhs;
      mutable boost::numeric::ublas::vector <VectorType> residual;

      mutable bool done_init_apply;

      amg_tag tag_;
    public:

      amg_precond(): Permutation(0) {}
      /** @brief The constructor. Saves system matrix, tag and builds data structures for setup.
      *
      * @param mat  System matrix
      * @param tag  The AMG tag
      */
      amg_precond(MatrixType const & mat, amg_tag const & tag): Permutation(0)
      {
        tag_ = tag;
        // Initialize data structures.
        amg_init (mat,A_setup,P_setup,Pointvector,tag_);

        done_init_apply = false;
      }

      /** @brief Start setup phase for this class and copy data structures.
      */
      void setup()
      {
        // Start setup phase.
        amg_setup(A_setup,P_setup,Pointvector,tag_);
        // Transform to CPU-Matrixtype for precondition phase.
        amg_transform_cpu(A,P,R,A_setup,P_setup,tag_);

        done_init_apply = false;
      }

      /** @brief Prepare data structures for preconditioning:
       *  Build data structures for precondition phase.
       *  Do LU factorization on coarsest level.
      */
      void init_apply() const
      {
        // Setup precondition phase (Data structures).
        amg_setup_apply(result,rhs,residual,A_setup,tag_);
        // Do LU factorization for direct solve.
        amg_lu(op,Permutation,A_setup[tag_.get_coarselevels()]);

        done_init_apply = true;
      }

      /** @brief Returns complexity measures.
      *
      * @param avgstencil  Average stencil sizes on all levels
      * @return     Operator complexity of AMG method
      */
      template<typename VectorType>
      ScalarType calc_complexity(VectorType & avgstencil)
      {
        avgstencil = VectorType (tag_.get_coarselevels()+1);
        unsigned int nonzero=0, systemmat_nonzero=0, level_coefficients=0;

        for (unsigned int level=0; level < tag_.get_coarselevels()+1; ++level)
        {
          level_coefficients = 0;
          for (InternalRowIterator row_iter = A_setup[level].begin1(); row_iter != A_setup[level].end1(); ++row_iter)
          {
            for (InternalColIterator col_iter = row_iter.begin(); col_iter != row_iter.end(); ++col_iter)
            {
              if (level == 0)
                systemmat_nonzero++;
              nonzero++;
              level_coefficients++;
            }
          }
          avgstencil[level] = level_coefficients/static_cast<ScalarType>(A_setup[level].size1());
        }
        return nonzero/static_cast<ScalarType>(systemmat_nonzero);
      }

      /** @brief Precondition Operation
      *
      * @param vec The vector to which preconditioning is applied to (ublas version)
      */
      template<typename VectorType>
      void apply(VectorType & vec) const
      {
        // Build data structures and do lu factorization before first iteration step.
        if (!done_init_apply)
          init_apply();

        int level;

        // Precondition operation (Yang, p.3)
        rhs[0] = vec;
        for (level=0; level <static_cast<int>(tag_.get_coarselevels()); level++)
        {
          result[level].clear();

          // Apply Smoother presmooth_ times.
          smooth_jacobi (level, tag_.get_presmooth(), result[level], rhs[level]);

          #ifdef VIENNACL_AMG_DEBUG
          std::cout << "After presmooth:" << std::endl;
          printvector(result[level]);
          #endif

          // Compute residual.
          residual[level] = rhs[level] - boost::numeric::ublas::prod (A[level],result[level]);

          #ifdef VIENNACL_AMG_DEBUG
          std::cout << "Residual:" << std::endl;
          printvector(residual[level]);
          #endif

          // Restrict to coarse level. Restricted residual is RHS of coarse level.
          rhs[level+1] = boost::numeric::ublas::prod (R[level],residual[level]);

          #ifdef VIENNACL_AMG_DEBUG
          std::cout << "Restricted Residual: " << std::endl;
          printvector(rhs[level+1]);
          #endif
        }

        // On highest level use direct solve to solve equation.
        result[level] = rhs[level];
        boost::numeric::ublas::lu_substitute(op,Permutation,result[level]);

        #ifdef VIENNACL_AMG_DEBUG
        std::cout << "After direct solve: " << std::endl;
        printvector (result[level]);
        #endif

        for (level=tag_.get_coarselevels()-1; level >= 0; level--)
        {
          #ifdef VIENNACL_AMG_DEBUG
          std::cout << "Coarse Error: " << std::endl;
          printvector(result[level+1]);
          #endif

          // Interpolate error to fine level. Correct solution by adding error.
          result[level] += boost::numeric::ublas::prod (P[level], result[level+1]);

          #ifdef VIENNACL_AMG_DEBUG
          std::cout << "Corrected Result: " << std::endl;
          printvector (result[level]);
          #endif

          // Apply Smoother postsmooth_ times.
          smooth_jacobi (level, tag_.get_postsmooth(), result[level], rhs[level]);

          #ifdef VIENNACL_AMG_DEBUG
          std::cout << "After postsmooth: " << std::endl;
          printvector (result[level]);
          #endif
        }
        vec = result[0];
      }

      /** @brief (Weighted) Jacobi Smoother (CPU version)
      * @param level    Coarse level to which smoother is applied to
      * @param iterations  Number of smoother iterations
      * @param x     The vector smoothing is applied to
      * @param rhs    The right hand side of the equation for the smoother
      */
      template<typename VectorType>
      void smooth_jacobi(int level, int const iterations, VectorType & x, VectorType const & rhs) const
      {
        VectorType old_result (x.size());
        long index;
        ScalarType sum = 0, diag = 1;

        for (int i=0; i<iterations; ++i)
        {
          old_result = x;
          x.clear();
#ifdef VIENNACL_WITH_OPENMP
          #pragma omp parallel for private (sum,diag) shared (rhs,x)
#endif
          for (index=0; index < static_cast<long>(A_setup[level].size1()); ++index)
          {
            InternalConstRowIterator row_iter = A_setup[level].begin1();
            row_iter += index;
            sum = 0;
            diag = 1;
            for (InternalConstColIterator col_iter = row_iter.begin(); col_iter != row_iter.end(); ++col_iter)
            {
              if (col_iter.index1() == col_iter.index2())
                diag = *col_iter;
              else
                sum += *col_iter * old_result[col_iter.index2()];
            }
            x[index]= static_cast<ScalarType>(tag_.get_jacobiweight()) * (rhs[index] - sum) / diag + (1-static_cast<ScalarType>(tag_.get_jacobiweight())) * old_result[index];
          }
        }
      }

      amg_tag & tag() { return tag_; }
    };

    /** @brief AMG preconditioner class, can be supplied to solve()-routines.
    *
    *  Specialization for compressed_matrix
    */
    template<typename ScalarType, unsigned int MAT_ALIGNMENT>
    class amg_precond< compressed_matrix<ScalarType, MAT_ALIGNMENT> >
    {
      typedef viennacl::compressed_matrix<ScalarType, MAT_ALIGNMENT> MatrixType;
      typedef viennacl::vector<ScalarType> VectorType;
      typedef detail::amg::amg_sparsematrix<ScalarType> SparseMatrixType;
      typedef detail::amg::amg_pointvector PointVectorType;

      typedef typename SparseMatrixType::const_iterator1 InternalConstRowIterator;
      typedef typename SparseMatrixType::const_iterator2 InternalConstColIterator;
      typedef typename SparseMatrixType::iterator1 InternalRowIterator;
      typedef typename SparseMatrixType::iterator2 InternalColIterator;

      boost::numeric::ublas::vector <SparseMatrixType> A_setup;
      boost::numeric::ublas::vector <SparseMatrixType> P_setup;
      boost::numeric::ublas::vector <MatrixType> A;
      boost::numeric::ublas::vector <MatrixType> P;
      boost::numeric::ublas::vector <MatrixType> R;
      boost::numeric::ublas::vector <PointVectorType> Pointvector;

      mutable boost::numeric::ublas::compressed_matrix<ScalarType> op;
      mutable boost::numeric::ublas::permutation_matrix<> Permutation;

      mutable boost::numeric::ublas::vector <VectorType> result;
      mutable boost::numeric::ublas::vector <VectorType> rhs;
      mutable boost::numeric::ublas::vector <VectorType> residual;

      viennacl::context ctx_;

      mutable bool done_init_apply;

      amg_tag tag_;

    public:

      amg_precond(): Permutation(0) {}

      /** @brief The constructor. Builds data structures.
      *
      * @param mat  System matrix
      * @param tag  The AMG tag
      */
      amg_precond(compressed_matrix<ScalarType, MAT_ALIGNMENT> const & mat, amg_tag const & tag): Permutation(0), ctx_(viennacl::traits::context(mat))
      {
        tag_ = tag;

        // Copy to CPU. Internal structure of sparse matrix is used for copy operation.
        std::vector<std::map<unsigned int, ScalarType> > mat2 = std::vector<std::map<unsigned int, ScalarType> >(mat.size1());
        viennacl::copy(mat, mat2);

        // Initialize data structures.
        amg_init (mat2,A_setup,P_setup,Pointvector,tag_);

        done_init_apply = false;
      }

      /** @brief Start setup phase for this class and copy data structures.
      */
      void setup()
      {
        // Start setup phase.
        amg_setup(A_setup,P_setup,Pointvector, tag_);
        // Transform to GPU-Matrixtype for precondition phase.
        amg_transform_gpu(A,P,R,A_setup,P_setup, tag_, ctx_);

        done_init_apply = false;
      }

      /** @brief Prepare data structures for preconditioning:
       *  Build data structures for precondition phase.
       *  Do LU factorization on coarsest level.
      */
      void init_apply() const
      {
        // Setup precondition phase (Data structures).
        amg_setup_apply(result,rhs,residual,A_setup,tag_, ctx_);
        // Do LU factorization for direct solve.
        amg_lu(op,Permutation,A_setup[tag_.get_coarselevels()]);

        done_init_apply = true;
      }

      /** @brief Returns complexity measures
      *
      * @param avgstencil  Average stencil sizes on all levels
      * @return     Operator complexity of AMG method
      */
      template<typename VectorType>
      ScalarType calc_complexity(VectorType & avgstencil)
      {
        avgstencil = VectorType (tag_.get_coarselevels()+1);
        unsigned int nonzero=0, systemmat_nonzero=0, level_coefficients=0;

        for (unsigned int level=0; level < tag_.get_coarselevels()+1; ++level)
        {
          level_coefficients = 0;
          for (InternalRowIterator row_iter = A_setup[level].begin1(); row_iter != A_setup[level].end1(); ++row_iter)
          {
            for (InternalColIterator col_iter = row_iter.begin(); col_iter != row_iter.end(); ++col_iter)
            {
              if (level == 0)
                systemmat_nonzero++;
              nonzero++;
              level_coefficients++;
            }
          }
          avgstencil[level] = level_coefficients/(double)A[level].size1();
        }
        return nonzero/static_cast<double>(systemmat_nonzero);
      }

      /** @brief Precondition Operation
      *
      * @param vec The vector to which preconditioning is applied to
      */
      template<typename VectorType>
      void apply(VectorType & vec) const
      {
        if (!done_init_apply)
          init_apply();

        int level;

        // Precondition operation (Yang, p.3).
        rhs[0] = vec;
        for (level=0; level <static_cast<int>(tag_.get_coarselevels()); level++)
        {
          result[level].clear();

          // Apply Smoother presmooth_ times.
          smooth_jacobi (level, tag_.get_presmooth(), result[level], rhs[level]);

          #ifdef VIENNACL_AMG_DEBUG
          std::cout << "After presmooth: " << std::endl;
          printvector(result[level]);
          #endif

          // Compute residual.
          //residual[level] = rhs[level] - viennacl::linalg::prod (A[level],result[level]);
          residual[level] = viennacl::linalg::prod (A[level],result[level]);
          residual[level] = rhs[level] - residual[level];

          #ifdef VIENNACL_AMG_DEBUG
          std::cout << "Residual: " << std::endl;
          printvector(residual[level]);
          #endif

          // Restrict to coarse level. Result is RHS of coarse level equation.
          //residual_coarse[level] = viennacl::linalg::prod(R[level],residual[level]);
          rhs[level+1] = viennacl::linalg::prod(R[level],residual[level]);

          #ifdef VIENNACL_AMG_DEBUG
          std::cout << "Restricted Residual: " << std::endl;
          printvector(rhs[level+1]);
          #endif
        }

        // On highest level use direct solve to solve equation (on the CPU)
        //TODO: Use GPU direct solve!
        result[level] = rhs[level];
        boost::numeric::ublas::vector <ScalarType> result_cpu (result[level].size());

        copy (result[level],result_cpu);
        boost::numeric::ublas::lu_substitute(op,Permutation,result_cpu);
        copy (result_cpu, result[level]);

        #ifdef VIENNACL_AMG_DEBUG
        std::cout << "After direct solve: " << std::endl;
        printvector (result[level]);
        #endif

        for (level=tag_.get_coarselevels()-1; level >= 0; level--)
        {
          #ifdef VIENNACL_AMG_DEBUG
          std::cout << "Coarse Error: " << std::endl;
          printvector(result[level+1]);
          #endif

          // Interpolate error to fine level and correct solution.
          result[level] += viennacl::linalg::prod(P[level],result[level+1]);

          #ifdef VIENNACL_AMG_DEBUG
          std::cout << "Corrected Result: " << std::endl;
          printvector (result[level]);
          #endif

          // Apply Smoother postsmooth_ times.
          smooth_jacobi (level, tag_.get_postsmooth(), result[level], rhs[level]);

          #ifdef VIENNACL_AMG_DEBUG
          std::cout << "After postsmooth: " << std::endl;
          printvector (result[level]);
          #endif
        }
        vec = result[0];
      }

      /** @brief Jacobi Smoother (GPU version)
      * @param level       Coarse level to which smoother is applied to
      * @param iterations  Number of smoother iterations
      * @param x           The vector smoothing is applied to
      * @param rhs         The right hand side of the equation for the smoother
      */
      template<typename VectorType>
      void smooth_jacobi(int level, unsigned int iterations, VectorType & x, VectorType const & rhs) const
      {
        VectorType old_result = x;

        viennacl::ocl::context & ctx = const_cast<viennacl::ocl::context &>(viennacl::traits::opencl_handle(x).context());
        viennacl::linalg::opencl::kernels::compressed_matrix<ScalarType>::init(ctx);
        viennacl::ocl::kernel & k = ctx.get_kernel(viennacl::linalg::opencl::kernels::compressed_matrix<ScalarType>::program_name(), "jacobi");

        for (unsigned int i=0; i<iterations; ++i)
        {
          if (i > 0)
            old_result = x;
          x.clear();
          viennacl::ocl::enqueue(k(A[level].handle1().opencl_handle(), A[level].handle2().opencl_handle(), A[level].handle().opencl_handle(),
                                  static_cast<ScalarType>(tag_.get_jacobiweight()),
                                  viennacl::traits::opencl_handle(old_result),
                                  viennacl::traits::opencl_handle(x),
                                  viennacl::traits::opencl_handle(rhs),
                                  static_cast<cl_uint>(rhs.size())));

        }
      }

      amg_tag & tag() { return tag_; }
    };

  }
}

#endif


// Material.cpp
// created by Kuangdai on 17-May-2016
// 3D seismic material properties

#include "Material.h"
#include "Quad.h"
#include "ExodusModel.h"
#include "SpectralConstants.h"

#include "Volumetric3D.h"
#include "XMath.h"
#include "Relabelling.h"

#include "Acoustic1D.h"
#include "Acoustic3D.h"

#include "AttBuilder.h"
#include "Attenuation1D.h"
#include "Attenuation3D.h"

#include "Isotropic1D.h"
#include "Isotropic3D.h"
#include "TransverselyIsotropic1D.h"
#include "TransverselyIsotropic3D.h"

#include "Anisotropic1D.h"
#include "Anisotropic3D.h"
#include "Geodesy.h"

#include <boost/algorithm/string.hpp>
#include "SlicePlot.h"

Material::Material(const Quad *myQuad, const ExodusModel &exModel): mMyQuad(myQuad) {
    // read Exodus model
    int quadTag = mMyQuad->getQuadTag();
    if (exModel.isIsotropic()) {
        for (int i = 0; i < 4; i++) {
            mVpv1D(i) = mVph1D(i) = exModel.getElementalVariables("VP_" + std::to_string(i), quadTag);
            mVsv1D(i) = mVsh1D(i) = exModel.getElementalVariables("VS_" + std::to_string(i), quadTag);
            mEta1D(i) = 1.;
        }
    } else {
        for (int i = 0; i < 4; i++) {
            mVpv1D(i) = exModel.getElementalVariables("VPV_" + std::to_string(i), quadTag);
            mVph1D(i) = exModel.getElementalVariables("VPH_" + std::to_string(i), quadTag);
            mVsv1D(i) = exModel.getElementalVariables("VSV_" + std::to_string(i), quadTag);
            mVsh1D(i) = exModel.getElementalVariables("VSH_" + std::to_string(i), quadTag);
            mEta1D(i) = exModel.getElementalVariables("ETA_" + std::to_string(i), quadTag);
        }
    }
    for (int i = 0; i < 4; i++) {
        mRho1D(i) = exModel.getElementalVariables("RHO_" + std::to_string(i), quadTag);
    }
    if (exModel.hasAttenuation()) {
        for (int i = 0; i < 4; i++) {
            mQkp1D(i) = exModel.getElementalVariables("QKAPPA_" + std::to_string(i), quadTag);
            mQmu1D(i) = exModel.getElementalVariables("QMU_" + std::to_string(i), quadTag);
        }
    } else {
        mQkp1D.setZero();
        mQmu1D.setZero();
    }

    // initialize 3D properties with 1D reference
    int Nr = mMyQuad->getNr();
    mVpv3D = RDMatXN::Zero(1, nPE);
    mVph3D = RDMatXN::Zero(1, nPE);
    mVsv3D = RDMatXN::Zero(1, nPE);
    mVsh3D = RDMatXN::Zero(1, nPE);
    mRho3D = RDMatXN::Zero(1, nPE);
    mEta3D = RDMatXN::Zero(1, nPE);
    mQkp3D = RDMatXN::Zero(1, nPE);
    mQmu3D = RDMatXN::Zero(1, nPE);

    for (int ipol = 0; ipol <= nPol; ipol++) {
        for (int jpol = 0; jpol <= nPol; jpol++) {
            int ipnt = ipol * nPntEdge + jpol;
            const RDCol2 &xieta = SpectralConstants::getXiEta(ipol, jpol, mMyQuad->isAxial());
            // fill with 1D
            mVpv3D.col(ipnt).fill(Mapping::interpolate(mVpv1D, xieta));
            mVph3D.col(ipnt).fill(Mapping::interpolate(mVph1D, xieta));
            mVsv3D.col(ipnt).fill(Mapping::interpolate(mVsv1D, xieta));
            mVsh3D.col(ipnt).fill(Mapping::interpolate(mVsh1D, xieta));
            mRho3D.col(ipnt).fill(Mapping::interpolate(mRho1D, xieta));
            mEta3D.col(ipnt).fill(Mapping::interpolate(mEta1D, xieta));
            mQkp3D.col(ipnt).fill(Mapping::interpolate(mQkp1D, xieta));
            mQmu3D.col(ipnt).fill(Mapping::interpolate(mQmu1D, xieta));
            // rho for mass
            int NrP = mMyQuad->getPointNr(ipol, jpol);
            mRhoMass3D[ipnt] = RDColX::Constant(NrP, mRho3D.col(ipnt)(0));
            mVpFluid3D[ipnt] = RDColX::Constant(NrP, mVpv3D.col(ipnt)(0));
        }
    }
}

void Material::addVolumetric3D(const std::vector<Volumetric3D *> &m3D,
    double srcLat, double srcLon, double srcDep, double phi2D) {
    if (m3D.size() == 0) {
        return;
    }

    // pointers for fast access to material matrices
    std::vector<RDRow4 *>  prop1DPtr = {&mVpv1D, &mVph1D, &mVsv1D, &mVsh1D, &mRho1D, &mEta1D, &mQkp1D, &mQmu1D,
                                        &mVpv1D, &mVsv1D, // these two take no other effect than occupying the slots
                                        &mC11_1D, &mC12_1D, &mC13_1D, &mC14_1D, &mC15_1D, &mC16_1D,
                                        &mC22_1D, &mC23_1D, &mC24_1D, &mC25_1D, &mC26_1D,
                                        &mC33_1D, &mC34_1D, &mC35_1D, &mC36_1D,
                                        &mC44_1D, &mC45_1D, &mC46_1D,
                                        &mC55_1D, &mC56_1D,
                                        &mC66_1D
    };
    std::vector<RDMatXN *> prop3DPtr = {&mVpv3D, &mVph3D, &mVsv3D, &mVsh3D, &mRho3D, &mEta3D, &mQkp3D, &mQmu3D,
                                        &mVpv3D, &mVsv3D, // these two take no other effect than occupying the slots
                                        &mC11_3D, &mC12_3D, &mC13_3D, &mC14_3D, &mC15_3D, &mC16_3D,
                                        &mC22_3D, &mC23_3D, &mC24_3D, &mC25_3D, &mC26_3D,
                                        &mC33_3D, &mC34_3D, &mC35_3D, &mC36_3D,
                                        &mC44_3D, &mC45_3D, &mC46_3D,
                                        &mC55_3D, &mC56_3D,
                                        &mC66_3D
    };

    // radius at element center
    double rElemCenter = mMyQuad->computeCenterRadius();

    // read 3D model
    int Nr = mMyQuad->getNr();
    for (int ipol = 0; ipol <= nPol; ipol++) {
        for (int jpol = 0; jpol <= nPol; jpol++) {
            // geographic oordinates of cardinal points
            const RDCol2 &xieta = SpectralConstants::getXiEta(ipol, jpol, mMyQuad->isAxial());
            const RDMatX3 &rtp = mMyQuad->computeGeocentricGlobal(srcLat, srcLon, srcDep, xieta, Nr, phi2D);
            int ipnt = ipol * nPntEdge + jpol;
            for (int alpha = 0; alpha < Nr; alpha++) {
                double r = rtp(alpha, 0);
                double t = rtp(alpha, 1);
                double p = rtp(alpha, 2);
                for (const auto &model: m3D) {
                    if (mMyQuad->isFluid() && !model->makeFluid3D()) {
                        continue;
                    }
                    std::vector<Volumetric3D::MaterialProperty> properties;
                    std::vector<Volumetric3D::MaterialRefType> refTypes;
                    std::vector<double> values;
                    if (!model->get3dProperties(r, t, p, rElemCenter, properties, refTypes, values)) {
                        // point (r, t, p) not in model range
                        continue;
                    }

                    if (!_3Dprepared()) {
                        prepare3D();
                    }

                    // deal with VP and VS
                    std::vector<Volumetric3D::MaterialProperty> propertiesTIso;
                    std::vector<Volumetric3D::MaterialRefType> refTypesTIso;
                    std::vector<double> valuesTIso;
                    for (int iprop = 0; iprop < properties.size(); iprop++) {
                        if (properties[iprop] == Volumetric3D::MaterialProperty::VP) {
                            propertiesTIso.push_back(Volumetric3D::MaterialProperty::VPV);
                            propertiesTIso.push_back(Volumetric3D::MaterialProperty::VPH);
                            refTypesTIso.push_back(refTypes[iprop]);
                            refTypesTIso.push_back(refTypes[iprop]);
                            valuesTIso.push_back(values[iprop]);
                            valuesTIso.push_back(values[iprop]);
                        } else if (properties[iprop] == Volumetric3D::MaterialProperty::VS) {
                            propertiesTIso.push_back(Volumetric3D::MaterialProperty::VSV);
                            propertiesTIso.push_back(Volumetric3D::MaterialProperty::VSH);
                            refTypesTIso.push_back(refTypes[iprop]);
                            refTypesTIso.push_back(refTypes[iprop]);
                            valuesTIso.push_back(values[iprop]);
                            valuesTIso.push_back(values[iprop]);
                        } else {
                            propertiesTIso.push_back(properties[iprop]);
                            refTypesTIso.push_back(refTypes[iprop]);
                            valuesTIso.push_back(values[iprop]);
                        }
                    }
                    // change values
                    for (int iprop = 0; iprop < propertiesTIso.size(); iprop++) {
                        // initialize anisotropy
                        if (!mFullAniso && propertiesTIso[iprop] >= Volumetric3D::MaterialProperty::C11) {
                            initAniso();
                        }

                        // // check
                        // if (mFullAniso) {
                        //     if (propertiesTIso[iprop] <= Volumetric3D::MaterialProperty::ANIS_ETA) {
                        //         throw std::runtime_error("Material::addVolumetric3D || "
                        //             "Velocity, density and eta can no longer be changed once "
                        //             "full anisotropy has been activated.");
                        //     }
                        // }

                        RDRow4 &row1D = *prop1DPtr[propertiesTIso[iprop]];
                        RDMatXN &mat3D = *prop3DPtr[propertiesTIso[iprop]];
                        Volumetric3D::MaterialRefType ref_type = refTypesTIso[iprop];
                        double value3D = valuesTIso[iprop];
                        if (ref_type == Volumetric3D::MaterialRefType::Absolute) {
                            mat3D(alpha, ipnt) = value3D;
                        } else if (ref_type == Volumetric3D::MaterialRefType::Reference1D) {
                            double ref1D = Mapping::interpolate(row1D, xieta);
                            mat3D(alpha, ipnt) = ref1D * (1. + value3D);
                        } else if (ref_type == Volumetric3D::MaterialRefType::Reference3D) {
                            mat3D(alpha, ipnt) *= 1. + value3D;
                        } else {
                            double ref1D = Mapping::interpolate(row1D, xieta);
                            mat3D(alpha, ipnt) = (mat3D(alpha, ipnt) - ref1D) * (1. + value3D) + ref1D;
                        }
                    }
                }
            }
        }
    }

    // form mass point sampling
    if (_3Dprepared()) {
        for (int ipol = 0; ipol <= nPol; ipol++) {
            for (int jpol = 0; jpol <= nPol; jpol++) {
                int ipnt = ipol * nPntEdge + jpol;
                int nr_mass = mMyQuad->getPointNr(ipol, jpol);
                mRhoMass3D[ipnt] = XMath::linearResampling(nr_mass, mRho3D.col(ipnt));
                mVpFluid3D[ipnt] = XMath::linearResampling(nr_mass, mVpv3D.col(ipnt));
            }
        }
    }

    // rotate anisotropy from geographic to source-centred
    if (mFullAniso) {
        rotateAniso(srcLat, srcLon, srcDep);
    }
}

arPP_RDColX Material::computeElementalMass() const {
    arPP_RDColX mass, J;
    // Jacobian of topography
    if (mMyQuad->hasRelabelling()) {
        J = mMyQuad->getRelabelling().getMassJacobian();
    } else {
        J = mRhoMass3D; // just to use the size
        for (int ipnt = 0; ipnt < nPntElem; ipnt++) {
            J[ipnt].setOnes();
        }
    }
    // general mass term
    const RDRowN &iFact = mMyQuad->getIntegralFactor();
    for (int ipnt = 0; ipnt < nPntElem; ipnt++) {
        if (mMyQuad->isFluid()) {
            mass[ipnt] = (mRhoMass3D[ipnt].array() * mVpFluid3D[ipnt].array().pow(2.)).pow(-1.).matrix();
        } else {
            mass[ipnt] = mRhoMass3D[ipnt];
        }
        mass[ipnt].array() *= iFact(ipnt) * J[ipnt].array();
    }
    return mass;
}

Acoustic *Material::createAcoustic(bool elem1D) const {
    const RDRowN &iFact = mMyQuad->getIntegralFactor();
    RDMatXN fluidK;
    if (_3Dprepared()) {
        fluidK = mRho3D.array().pow(-1.);
    } else {
        fluidK = mRho3D.replicate(mMyQuad->getNr(), 1).array().pow(-1.);
    }
    for (int ipnt = 0; ipnt < nPntElem; ipnt++) {
       fluidK.col(ipnt) *= iFact(ipnt);
    }
    if (mMyQuad->hasRelabelling()) {
        fluidK.array() *= mMyQuad->getRelabelling().getStiffJacobian().array();
    }
    if (elem1D) {
        RDMatPP kstruct;
        XMath::structuredUseFirstRow(fluidK, kstruct);
        return new Acoustic1D(kstruct.cast<Real>());
    } else {
        return new Acoustic3D(fluidK.cast<Real>());
    }
}

Elastic *Material::createElastic(bool elem1D, const AttBuilder *attBuild) const {
    if (mFullAniso) {
        return createElasticAniso(elem1D, attBuild);
    }

    // elasticity tensor
    const RDRowN &iFact = mMyQuad->getIntegralFactor();
    RDMatXN vpv3D, vph3D;
    RDMatXN vsv3D, vsh3D;
    RDMatXN rho3D;
    RDMatXN eta3D;
    RDMatXN qkp3D, qmu3D;

    if (_3Dprepared()) {
        vpv3D = mVpv3D;
        vph3D = mVph3D;
        vsv3D = mVsv3D;
        vsh3D = mVsh3D;
        rho3D = mRho3D;
        eta3D = mEta3D;
        qkp3D = mQkp3D;
        qmu3D = mQmu3D;
    } else {
        vpv3D = mVpv3D.replicate(mMyQuad->getNr(), 1);
        vph3D = mVph3D.replicate(mMyQuad->getNr(), 1);
        vsv3D = mVsv3D.replicate(mMyQuad->getNr(), 1);
        vsh3D = mVsh3D.replicate(mMyQuad->getNr(), 1);
        rho3D = mRho3D.replicate(mMyQuad->getNr(), 1);
        eta3D = mEta3D.replicate(mMyQuad->getNr(), 1);
        qkp3D = mQkp3D.replicate(mMyQuad->getNr(), 1);
        qmu3D = mQmu3D.replicate(mMyQuad->getNr(), 1);
    }

    RDMatXN A(rho3D), C(rho3D), F(rho3D), L(rho3D), N(rho3D);
    for (int ipnt = 0; ipnt < nPntElem; ipnt++) {
        // A C L N
        A.col(ipnt).array() *= vph3D.col(ipnt).array().pow(2.) * iFact(ipnt);
        C.col(ipnt).array() *= vpv3D.col(ipnt).array().pow(2.) * iFact(ipnt);
        L.col(ipnt).array() *= vsv3D.col(ipnt).array().pow(2.) * iFact(ipnt);
        N.col(ipnt).array() *= vsh3D.col(ipnt).array().pow(2.) * iFact(ipnt);
    }
    // F
    F = eta3D.schur(A - 2. * L);
    // must do relabelling before attenuation
    if (mMyQuad->hasRelabelling()) {
        const RDMatXN &J = mMyQuad->getRelabelling().getStiffJacobian();
        A = A.schur(J);
        C = C.schur(J);
        F = F.schur(J);
        L = L.schur(J);
        N = N.schur(J);
    }

    // attenuation
    Attenuation1D *att1D = 0;
    Attenuation3D *att3D = 0;
    if (attBuild) {
        // Voigt average
        RDMatXN kappa = (4. * A + C + 4. * F - 4. * N) / 9.;
        RDMatXN mu = (A + C - 2. * F + 6. * L + 5. * N) / 15.;
        A -= (kappa + 4. / 3. * mu);
        C -= (kappa + 4. / 3. * mu);
        F -= (kappa - 2. / 3. * mu);
        L -= mu;
        N -= mu;
        if (elem1D) {
            att1D = attBuild->createAttenuation1D(qkp3D, qmu3D, kappa, mu, mMyQuad);
        } else {
            att3D = attBuild->createAttenuation3D(qkp3D, qmu3D, kappa, mu, mMyQuad);
        }
        A += (kappa + 4. / 3. * mu);
        C += (kappa + 4. / 3. * mu);
        F += (kappa - 2. / 3. * mu);
        L += mu;
        N += mu;
    }

    // Elastic pointers
    if (elem1D) {
        RDMatPP A0, C0, F0, L0, N0;
        XMath::structuredUseFirstRow(A, A0);
        XMath::structuredUseFirstRow(C, C0);
        XMath::structuredUseFirstRow(F, F0);
        XMath::structuredUseFirstRow(L, L0);
        XMath::structuredUseFirstRow(N, N0);
        if (isIsotropic()) {
            return new Isotropic1D(F0.cast<Real>(), L0.cast<Real>(), att1D);
        } else {
            return new TransverselyIsotropic1D(A0.cast<Real>(), C0.cast<Real>(),
                F0.cast<Real>(), L0.cast<Real>(), N0.cast<Real>(), att1D);
        }
    } else {
        if (isIsotropic()) {
            return new Isotropic3D(F.cast<Real>(), L.cast<Real>(), att3D);
        } else {
            return new TransverselyIsotropic3D(A.cast<Real>(), C.cast<Real>(),
                F.cast<Real>(), L.cast<Real>(), N.cast<Real>(), att3D);
        }
    }
}

Elastic *Material::createElasticAniso(bool elem1D, const AttBuilder *attBuild) const {
    // elasticity tensor
    const RDRowN &iFact = mMyQuad->getIntegralFactor();
    RDMatXN C11_3D(mC11_3D), C12_3D(mC12_3D), C13_3D(mC13_3D), C14_3D(mC14_3D), C15_3D(mC15_3D), C16_3D(mC16_3D);
    RDMatXN C22_3D(mC22_3D), C23_3D(mC23_3D), C24_3D(mC24_3D), C25_3D(mC25_3D), C26_3D(mC26_3D);
    RDMatXN C33_3D(mC33_3D), C34_3D(mC34_3D), C35_3D(mC35_3D), C36_3D(mC36_3D);
    RDMatXN C44_3D(mC44_3D), C45_3D(mC45_3D), C46_3D(mC46_3D);
    RDMatXN C55_3D(mC55_3D), C56_3D(mC56_3D);
    RDMatXN C66_3D(mC66_3D);
    for (int ipnt = 0; ipnt < nPntElem; ipnt++) {
        C11_3D.col(ipnt) *= iFact(ipnt);
        C12_3D.col(ipnt) *= iFact(ipnt);
        C13_3D.col(ipnt) *= iFact(ipnt);
        C14_3D.col(ipnt) *= iFact(ipnt);
        C15_3D.col(ipnt) *= iFact(ipnt);
        C16_3D.col(ipnt) *= iFact(ipnt);
        C22_3D.col(ipnt) *= iFact(ipnt);
        C23_3D.col(ipnt) *= iFact(ipnt);
        C24_3D.col(ipnt) *= iFact(ipnt);
        C25_3D.col(ipnt) *= iFact(ipnt);
        C26_3D.col(ipnt) *= iFact(ipnt);
        C33_3D.col(ipnt) *= iFact(ipnt);
        C34_3D.col(ipnt) *= iFact(ipnt);
        C35_3D.col(ipnt) *= iFact(ipnt);
        C36_3D.col(ipnt) *= iFact(ipnt);
        C44_3D.col(ipnt) *= iFact(ipnt);
        C45_3D.col(ipnt) *= iFact(ipnt);
        C46_3D.col(ipnt) *= iFact(ipnt);
        C55_3D.col(ipnt) *= iFact(ipnt);
        C56_3D.col(ipnt) *= iFact(ipnt);
        C66_3D.col(ipnt) *= iFact(ipnt);
    }
    // must do relabelling before attenuation
    if (mMyQuad->hasRelabelling()) {
        const RDMatXN &J = mMyQuad->getRelabelling().getStiffJacobian();
        C11_3D = C11_3D.schur(J);
        C12_3D = C12_3D.schur(J);
        C13_3D = C13_3D.schur(J);
        C14_3D = C14_3D.schur(J);
        C15_3D = C15_3D.schur(J);
        C16_3D = C16_3D.schur(J);
        C22_3D = C22_3D.schur(J);
        C23_3D = C23_3D.schur(J);
        C24_3D = C24_3D.schur(J);
        C25_3D = C25_3D.schur(J);
        C26_3D = C26_3D.schur(J);
        C33_3D = C33_3D.schur(J);
        C34_3D = C34_3D.schur(J);
        C35_3D = C35_3D.schur(J);
        C36_3D = C36_3D.schur(J);
        C44_3D = C44_3D.schur(J);
        C45_3D = C45_3D.schur(J);
        C46_3D = C46_3D.schur(J);
        C55_3D = C55_3D.schur(J);
        C56_3D = C56_3D.schur(J);
        C66_3D = C66_3D.schur(J);
    }

    // attenuation
    Attenuation1D *att1D = 0;
    Attenuation3D *att3D = 0;
    if (attBuild) {
        // Voigt average
        // https://materialsproject.org/wiki/index.php/Elasticity_calculations
        RDMatXN kappa = (C11_3D + C22_3D + C33_3D + 2. * (C12_3D + C23_3D + C13_3D)) / 9.;
        RDMatXN mu = (C11_3D + C22_3D + C33_3D - (C12_3D + C23_3D + C13_3D) + 3. * (C44_3D + C55_3D + C66_3D)) / 15.;
        C11_3D -= (kappa + 4. / 3. * mu);
        C22_3D -= (kappa + 4. / 3. * mu);
        C33_3D -= (kappa + 4. / 3. * mu);
        C12_3D -= (kappa - 2. / 3. * mu);
        C23_3D -= (kappa - 2. / 3. * mu);
        C13_3D -= (kappa - 2. / 3. * mu);
        C44_3D -= mu;
        C55_3D -= mu;
        C66_3D -= mu;
        if (elem1D) {
            att1D = attBuild->createAttenuation1D(mQkp3D, mQmu3D, kappa, mu, mMyQuad);
        } else {
            att3D = attBuild->createAttenuation3D(mQkp3D, mQmu3D, kappa, mu, mMyQuad);
        }
        C11_3D += (kappa + 4. / 3. * mu);
        C22_3D += (kappa + 4. / 3. * mu);
        C33_3D += (kappa + 4. / 3. * mu);
        C12_3D += (kappa - 2. / 3. * mu);
        C23_3D += (kappa - 2. / 3. * mu);
        C13_3D += (kappa - 2. / 3. * mu);
        C44_3D += mu;
        C55_3D += mu;
        C66_3D += mu;
    }

    // Elastic pointers
    if (elem1D) {
        RDMatPP C11_1D, C12_1D, C13_1D, C14_1D, C15_1D, C16_1D;
        RDMatPP C22_1D, C23_1D, C24_1D, C25_1D, C26_1D;
        RDMatPP C33_1D, C34_1D, C35_1D, C36_1D;
        RDMatPP C44_1D, C45_1D, C46_1D;
        RDMatPP C55_1D, C56_1D;
        RDMatPP C66_1D;
        XMath::structuredUseFirstRow(C11_3D, C11_1D);
        XMath::structuredUseFirstRow(C12_3D, C12_1D);
        XMath::structuredUseFirstRow(C13_3D, C13_1D);
        XMath::structuredUseFirstRow(C14_3D, C14_1D);
        XMath::structuredUseFirstRow(C15_3D, C15_1D);
        XMath::structuredUseFirstRow(C16_3D, C16_1D);
        XMath::structuredUseFirstRow(C22_3D, C22_1D);
        XMath::structuredUseFirstRow(C23_3D, C23_1D);
        XMath::structuredUseFirstRow(C24_3D, C24_1D);
        XMath::structuredUseFirstRow(C25_3D, C25_1D);
        XMath::structuredUseFirstRow(C26_3D, C26_1D);
        XMath::structuredUseFirstRow(C33_3D, C33_1D);
        XMath::structuredUseFirstRow(C34_3D, C34_1D);
        XMath::structuredUseFirstRow(C35_3D, C35_1D);
        XMath::structuredUseFirstRow(C36_3D, C36_1D);
        XMath::structuredUseFirstRow(C44_3D, C44_1D);
        XMath::structuredUseFirstRow(C45_3D, C45_1D);
        XMath::structuredUseFirstRow(C46_3D, C46_1D);
        XMath::structuredUseFirstRow(C55_3D, C55_1D);
        XMath::structuredUseFirstRow(C56_3D, C56_1D);
        XMath::structuredUseFirstRow(C66_3D, C66_1D);
        return new Anisotropic1D(
            C11_1D.cast<Real>(), C12_1D.cast<Real>(), C13_1D.cast<Real>(), C14_1D.cast<Real>(), C15_1D.cast<Real>(), C16_1D.cast<Real>(),
            C22_1D.cast<Real>(), C23_1D.cast<Real>(), C24_1D.cast<Real>(), C25_1D.cast<Real>(), C26_1D.cast<Real>(),
            C33_1D.cast<Real>(), C34_1D.cast<Real>(), C35_1D.cast<Real>(), C36_1D.cast<Real>(),
            C44_1D.cast<Real>(), C45_1D.cast<Real>(), C46_1D.cast<Real>(),
            C55_1D.cast<Real>(), C56_1D.cast<Real>(),
            C66_1D.cast<Real>(),
            att1D);
    } else {
        return new Anisotropic3D(
            C11_3D.cast<Real>(), C12_3D.cast<Real>(), C13_3D.cast<Real>(), C14_3D.cast<Real>(), C15_3D.cast<Real>(), C16_3D.cast<Real>(),
            C22_3D.cast<Real>(), C23_3D.cast<Real>(), C24_3D.cast<Real>(), C25_3D.cast<Real>(), C26_3D.cast<Real>(),
            C33_3D.cast<Real>(), C34_3D.cast<Real>(), C35_3D.cast<Real>(), C36_3D.cast<Real>(),
            C44_3D.cast<Real>(), C45_3D.cast<Real>(), C46_3D.cast<Real>(),
            C55_3D.cast<Real>(), C56_3D.cast<Real>(),
            C66_3D.cast<Real>(),
            att3D);
    }
}

double Material::getVMaxRef() const {
    return std::max(mVph1D.maxCoeff(), mVpv1D.maxCoeff());
}

RDColX Material::getVMax() const {
    RDMatXN vpv3D, vph3D;
    if (_3Dprepared()) {
        vpv3D = mVpv3D;
        vph3D = mVph3D;
    } else {
        vpv3D = mVpv3D.replicate(mMyQuad->getNr(), 1);
        vph3D = mVph3D.replicate(mMyQuad->getNr(), 1);
    }
    const RDColX &vpvMax = vpv3D.rowwise().maxCoeff();
    const RDColX &vphMax = vph3D.rowwise().maxCoeff();
    return (vpvMax.array().max(vphMax.array())).matrix();
}

bool Material::isFluidPar1D() const {
    return XMath::equalRows(mVpv3D) && XMath::equalRows(mRho3D);
}

bool Material::isSolidPar1D(bool attenuation) const {
    bool result = false;
    if (mFullAniso) {
        result = XMath::equalRows(mC11_3D, 1e-7) && XMath::equalRows(mC12_3D, 1e-7) && XMath::equalRows(mC13_3D, 1e-7) && XMath::equalRows(mC14_3D, 1e-7) && XMath::equalRows(mC15_3D, 1e-7) && XMath::equalRows(mC16_3D, 1e-7) &&
                 XMath::equalRows(mC22_3D, 1e-7) && XMath::equalRows(mC23_3D, 1e-7) && XMath::equalRows(mC24_3D, 1e-7) && XMath::equalRows(mC25_3D, 1e-7) && XMath::equalRows(mC26_3D, 1e-7) &&
                 XMath::equalRows(mC33_3D, 1e-7) && XMath::equalRows(mC34_3D, 1e-7) && XMath::equalRows(mC35_3D, 1e-7) && XMath::equalRows(mC36_3D, 1e-7) &&
                 XMath::equalRows(mC44_3D, 1e-7) && XMath::equalRows(mC45_3D, 1e-7) && XMath::equalRows(mC46_3D, 1e-7) &&
                 XMath::equalRows(mC55_3D, 1e-7) && XMath::equalRows(mC56_3D, 1e-7) &&
                 XMath::equalRows(mC66_3D, 1e-7) &&
                 XMath::equalRows(mRho3D);
    } else {
        result = XMath::equalRows(mVpv3D) && XMath::equalRows(mVph3D) &&
                 XMath::equalRows(mVsv3D) && XMath::equalRows(mVsh3D) &&
                 XMath::equalRows(mRho3D) && XMath::equalRows(mEta3D);
    }
    if (attenuation) {
        result = result && XMath::equalRows(mQkp3D) && XMath::equalRows(mQmu3D);
    }
    return result;
}

bool Material::isIsotropic() const {
    if (mFullAniso) {
        return false;
    }
    return (mVpv3D - mVph3D).norm() < tinyDouble * mVpv3D.norm() &&
           (mVsv3D - mVsh3D).norm() < tinyDouble * mVsv3D.norm() &&
           (mEta3D - RDMatXN::Ones(mEta3D.rows(), mEta3D.cols())).norm() < tinyDouble;
}

RDMatXN Material::getProperty(const std::string &vname, int refType) {
    int Nr = mMyQuad->getNr();

    // name
    std::string varname = vname;
    if (boost::iequals(vname, "vp")) {
        varname = "vpv";
    }
    if (boost::iequals(vname, "vs")) {
        varname = "vsv";
    }
    RDRow4 data1D;
    RDMatXN data3D;
    if (boost::iequals(varname, "vpv")) {
        data1D = mVpv1D;
        data3D = mVpv3D;
    } else if (boost::iequals(varname, "vsv")) {
        data1D = mVsv1D;
        data3D = mVsv3D;
    } else if (boost::iequals(varname, "vph")) {
        data1D = mVph1D;
        data3D = mVph3D;
    } else if (boost::iequals(varname, "vsh")) {
        data1D = mVsh1D;
        data3D = mVsh3D;
    } else if (boost::iequals(varname, "rho")) {
        data1D = mRho1D;
        data3D = mRho3D;
    } else if (boost::iequals(varname, "eta")) {
        data1D = mEta1D;
        data3D = mEta3D;
    } else if (boost::iequals(varname, "qkappa")) {
        data1D = mQkp1D;
        data3D = mQkp3D;
    } else if (boost::iequals(varname, "qmu")) {
        data1D = mQmu1D;
        data3D = mQmu3D;
    } else if (mFullAniso) {
        if (boost::iequals(varname, "c11")) {
            data1D = mC11_1D;
            data3D = mC11_3D;
        } else if (boost::iequals(varname, "c12")) {
            data1D = mC12_1D;
            data3D = mC12_3D;
        } else if (boost::iequals(varname, "c13")) {
            data1D = mC13_1D;
            data3D = mC13_3D;
        } else if (boost::iequals(varname, "c14")) {
            data1D = mC14_1D;
            data3D = mC14_3D;
        } else if (boost::iequals(varname, "c15")) {
            data1D = mC15_1D;
            data3D = mC15_3D;
        } else if (boost::iequals(varname, "c16")) {
            data1D = mC16_1D;
            data3D = mC16_3D;
        } else if (boost::iequals(varname, "c22")) {
            data1D = mC22_1D;
            data3D = mC22_3D;
        } else if (boost::iequals(varname, "c23")) {
            data1D = mC23_1D;
            data3D = mC23_3D;
        } else if (boost::iequals(varname, "c24")) {
            data1D = mC24_1D;
            data3D = mC24_3D;
        } else if (boost::iequals(varname, "c25")) {
            data1D = mC25_1D;
            data3D = mC25_3D;
        } else if (boost::iequals(varname, "c26")) {
            data1D = mC26_1D;
            data3D = mC26_3D;
        } else if (boost::iequals(varname, "c33")) {
            data1D = mC33_1D;
            data3D = mC33_3D;
        } else if (boost::iequals(varname, "c34")) {
            data1D = mC34_1D;
            data3D = mC34_3D;
        } else if (boost::iequals(varname, "c35")) {
            data1D = mC35_1D;
            data3D = mC35_3D;
        } else if (boost::iequals(varname, "c36")) {
            data1D = mC36_1D;
            data3D = mC36_3D;
        } else if (boost::iequals(varname, "c44")) {
            data1D = mC44_1D;
            data3D = mC44_3D;
        } else if (boost::iequals(varname, "c45")) {
            data1D = mC45_1D;
            data3D = mC45_3D;
        } else if (boost::iequals(varname, "c46")) {
            data1D = mC46_1D;
            data3D = mC46_3D;
        } else if (boost::iequals(varname, "c55")) {
            data1D = mC55_1D;
            data3D = mC55_3D;
        } else if (boost::iequals(varname, "c56")) {
            data1D = mC56_1D;
            data3D = mC56_3D;
        } else if (boost::iequals(varname, "c66")) {
            data1D = mC66_1D;
            data3D = mC66_3D;
        }
    } else {
        throw std::runtime_error("Material::getProperty || Unknown field variable name: " + vname);
    }

    if (data3D.rows() != Nr) {
        data3D = data3D.replicate(Nr, 1);
    }

    // 3D
    if (refType == SlicePlot::PropertyRefTypes::Property3D) {
        return data3D;
    }

    // fill 1D
    RDMatXN data1DXN(Nr, nPntElem);
    for (int ipol = 0; ipol <= nPol; ipol++) {
        for (int jpol = 0; jpol <= nPol; jpol++) {
            int ipnt = ipol * nPntEdge + jpol;
            const RDCol2 &xieta = SpectralConstants::getXiEta(ipol, jpol, mMyQuad->isAxial());
            data1DXN.col(ipnt).fill(Mapping::interpolate(data1D, xieta));
        }
    }

    // 1D
    if (refType == SlicePlot::PropertyRefTypes::Property1D) {
        return data1DXN;
    }

    // perturb
    RDMatXN data1DBase = data1DXN.array().max(tinyDouble).matrix(); // in fluid, vs = 0
    return ((data3D - data1DXN).array() / data1DBase.array()).matrix();
}

void Material::initAniso() {
    if (mMyQuad->isFluid()) {
        throw std::runtime_error("Material::initAniso || Cannot activate full anisotropy in fluid domain.");
    }

    // 1D elasticity tensor
    RDRow4 A_1D = mRho1D.schur(mVph1D).schur(mVph1D);
    RDRow4 C_1D = mRho1D.schur(mVpv1D).schur(mVpv1D);
    RDRow4 L_1D = mRho1D.schur(mVsv1D).schur(mVsv1D);
    RDRow4 N_1D = mRho1D.schur(mVsh1D).schur(mVsh1D);
    RDRow4 F_1D = mEta1D.schur(A_1D - 2. * L_1D);

    mC11_1D = mC22_1D = A_1D;
    mC33_1D = C_1D;
    mC44_1D = mC55_1D = L_1D;
    mC66_1D = N_1D;
    mC12_1D = A_1D - 2. * N_1D;
    mC13_1D = mC23_1D = F_1D;
    mC14_1D = mC15_1D = mC16_1D = RDRow4::Zero();
    mC24_1D = mC25_1D = mC26_1D = RDRow4::Zero();
    mC34_1D = mC35_1D = mC36_1D = RDRow4::Zero();
    mC45_1D = mC46_1D = mC56_1D = RDRow4::Zero();

    // 3D elasticity tensor
    RDMatXN A_3D = mRho3D.schur(mVph3D).schur(mVph3D);
    RDMatXN C_3D = mRho3D.schur(mVpv3D).schur(mVpv3D);
    RDMatXN L_3D = mRho3D.schur(mVsv3D).schur(mVsv3D);
    RDMatXN N_3D = mRho3D.schur(mVsh3D).schur(mVsh3D);
    RDMatXN F_3D = mEta3D.schur(A_3D - 2. * L_3D);

    mC11_3D = mC22_3D = A_3D;
    mC33_3D = C_3D;
    mC44_3D = mC55_3D = L_3D;
    mC66_3D = N_3D;
    mC12_3D = A_3D - 2. * N_3D;
    mC13_3D = mC23_3D = F_3D;
    mC14_3D = mC15_3D = mC16_3D = RDMatXN::Zero(A_3D.rows(), A_3D.cols());
    mC24_3D = mC25_3D = mC26_3D = RDMatXN::Zero(A_3D.rows(), A_3D.cols());
    mC34_3D = mC35_3D = mC36_3D = RDMatXN::Zero(A_3D.rows(), A_3D.cols());
    mC45_3D = mC46_3D = mC56_3D = RDMatXN::Zero(A_3D.rows(), A_3D.cols());

    // initialized
    mFullAniso = true;
}

void Material::rotateAniso(double srcLat, double srcLon, double srcDep) {
    RDMatXX inCijkl(6, 6);

    // 3D
    for (int alpha = 0; alpha < mC11_3D.rows(); alpha++) {
        // azimuth of the slice
        double phi = 2. * pi / mC11_3D.rows() * alpha;
        // loop over GLL points
        for (int ipol = 0; ipol <= nPol; ipol++) {
            for (int jpol = 0; jpol <= nPol; jpol++) {
                int ipnt = ipol * nPntEdge + jpol;

                inCijkl(1 - 1, 1 - 1) = mC11_3D(alpha, ipnt);
                inCijkl(1 - 1, 2 - 1) = mC12_3D(alpha, ipnt);
                inCijkl(1 - 1, 3 - 1) = mC13_3D(alpha, ipnt);
                inCijkl(1 - 1, 4 - 1) = mC14_3D(alpha, ipnt);
                inCijkl(1 - 1, 5 - 1) = mC15_3D(alpha, ipnt);
                inCijkl(1 - 1, 6 - 1) = mC16_3D(alpha, ipnt);

                inCijkl(2 - 1, 1 - 1) = mC12_3D(alpha, ipnt);
                inCijkl(2 - 1, 2 - 1) = mC22_3D(alpha, ipnt);
                inCijkl(2 - 1, 3 - 1) = mC23_3D(alpha, ipnt);
                inCijkl(2 - 1, 4 - 1) = mC24_3D(alpha, ipnt);
                inCijkl(2 - 1, 5 - 1) = mC25_3D(alpha, ipnt);
                inCijkl(2 - 1, 6 - 1) = mC26_3D(alpha, ipnt);

                inCijkl(3 - 1, 1 - 1) = mC13_3D(alpha, ipnt);
                inCijkl(3 - 1, 2 - 1) = mC23_3D(alpha, ipnt);
                inCijkl(3 - 1, 3 - 1) = mC33_3D(alpha, ipnt);
                inCijkl(3 - 1, 4 - 1) = mC34_3D(alpha, ipnt);
                inCijkl(3 - 1, 5 - 1) = mC35_3D(alpha, ipnt);
                inCijkl(3 - 1, 6 - 1) = mC36_3D(alpha, ipnt);

                inCijkl(4 - 1, 1 - 1) = mC14_3D(alpha, ipnt);
                inCijkl(4 - 1, 2 - 1) = mC24_3D(alpha, ipnt);
                inCijkl(4 - 1, 3 - 1) = mC34_3D(alpha, ipnt);
                inCijkl(4 - 1, 4 - 1) = mC44_3D(alpha, ipnt);
                inCijkl(4 - 1, 5 - 1) = mC45_3D(alpha, ipnt);
                inCijkl(4 - 1, 6 - 1) = mC46_3D(alpha, ipnt);

                inCijkl(5 - 1, 1 - 1) = mC15_3D(alpha, ipnt);
                inCijkl(5 - 1, 2 - 1) = mC25_3D(alpha, ipnt);
                inCijkl(5 - 1, 3 - 1) = mC35_3D(alpha, ipnt);
                inCijkl(5 - 1, 4 - 1) = mC45_3D(alpha, ipnt);
                inCijkl(5 - 1, 5 - 1) = mC55_3D(alpha, ipnt);
                inCijkl(5 - 1, 6 - 1) = mC56_3D(alpha, ipnt);

                inCijkl(6 - 1, 1 - 1) = mC16_3D(alpha, ipnt);
                inCijkl(6 - 1, 2 - 1) = mC26_3D(alpha, ipnt);
                inCijkl(6 - 1, 3 - 1) = mC36_3D(alpha, ipnt);
                inCijkl(6 - 1, 4 - 1) = mC46_3D(alpha, ipnt);
                inCijkl(6 - 1, 5 - 1) = mC56_3D(alpha, ipnt);
                inCijkl(6 - 1, 6 - 1) = mC66_3D(alpha, ipnt);

                // compute backazimuth
                const RDCol2 &xieta = SpectralConstants::getXiEta(ipol, jpol, mMyQuad->isAxial());
                RDCol2 rtheta = Geodesy::rtheta(mMyQuad->mapping(xieta));
                RDCol3 rtpS, rtpG;
                rtpS(0) = rtheta(0);
                rtpS(1) = rtheta(1);
                rtpS(2) = phi;
                rtpG = Geodesy::rotateSrc2Glob(rtpS, srcLat, srcLon, srcDep);
                double recDep = Geodesy::getROuter() - rtheta(0);
                double recLat = Geodesy::theta2Lat_d(rtpG(1), recDep);
                double recLon = Geodesy::phi2Lon(rtpG(2));
                double baz = Geodesy::backAzimuth(srcLat, srcLon, srcDep, recLat, recLon, recDep);

                // global => source centred RTZ (theta, phi, r)
                const RDMatXX &outCijkl = bondTransformation(inCijkl, 0., 0., -baz);

                // by convention, input is in RTZ
                // // (r, theta, phi) => (R, T, Z)
                // const RDMatXX &RTZ_Cijkl_x = bondTransformation(rtp_Cijkl, 0., 0., pi/2.);
                // const RDMatXX &outCijkl = bondTransformation(RTZ_Cijkl_x, pi/2., 0., 0.);

                // copy back
                mC11_3D(alpha, ipnt) = outCijkl(1 - 1, 1 - 1);
                mC12_3D(alpha, ipnt) = outCijkl(1 - 1, 2 - 1);
                mC13_3D(alpha, ipnt) = outCijkl(1 - 1, 3 - 1);
                mC14_3D(alpha, ipnt) = outCijkl(1 - 1, 4 - 1);
                mC15_3D(alpha, ipnt) = outCijkl(1 - 1, 5 - 1);
                mC16_3D(alpha, ipnt) = outCijkl(1 - 1, 6 - 1);

                mC22_3D(alpha, ipnt) = outCijkl(2 - 1, 2 - 1);
                mC23_3D(alpha, ipnt) = outCijkl(2 - 1, 3 - 1);
                mC24_3D(alpha, ipnt) = outCijkl(2 - 1, 4 - 1);
                mC25_3D(alpha, ipnt) = outCijkl(2 - 1, 5 - 1);
                mC26_3D(alpha, ipnt) = outCijkl(2 - 1, 6 - 1);

                mC33_3D(alpha, ipnt) = outCijkl(3 - 1, 3 - 1);
                mC34_3D(alpha, ipnt) = outCijkl(3 - 1, 4 - 1);
                mC35_3D(alpha, ipnt) = outCijkl(3 - 1, 5 - 1);
                mC36_3D(alpha, ipnt) = outCijkl(3 - 1, 6 - 1);

                mC44_3D(alpha, ipnt) = outCijkl(4 - 1, 4 - 1);
                mC45_3D(alpha, ipnt) = outCijkl(4 - 1, 5 - 1);
                mC46_3D(alpha, ipnt) = outCijkl(4 - 1, 6 - 1);

                mC55_3D(alpha, ipnt) = outCijkl(5 - 1, 5 - 1);
                mC56_3D(alpha, ipnt) = outCijkl(5 - 1, 6 - 1);

                mC66_3D(alpha, ipnt) = outCijkl(6 - 1, 6 - 1);
            }
        }
    }
}

RDMatXX Material::bondTransformation(RDMatXX inCijkl, double alpha, double beta, double gamma) {
    RDMat33 R1, R2, R3, R;
    R1 << 1., 0., 0.,
          0., cos(alpha), sin(alpha),
          0., -sin(alpha), cos(alpha);
    R2 << cos(beta), 0., sin(beta),
          0., 1., 0.,
          -sin(beta), 0, cos(beta);
    R3 << cos(gamma), sin(gamma), 0.,
          -sin(gamma), cos(gamma), 0.,
          0., 0., 1.;

    R = R1 * R2 * R3;

    RDMat33 K1, K2, K3, K4;
    K1.array() = R.array().pow(2.);
    K2 << R(0, 1) * R(0, 2), R(0, 2) * R(0, 0), R(0, 0) * R(0, 1),
          R(1, 1) * R(1, 2), R(1, 2) * R(1, 0), R(1, 0) * R(1, 1),
          R(2, 1) * R(2, 2), R(2, 2) * R(2, 0), R(2, 0) * R(2, 1);
    K3 << R(1, 0) * R(2, 0), R(1, 1) * R(2, 1), R(1, 2) * R(2, 2),
          R(2, 0) * R(0, 0), R(2, 1) * R(0, 1), R(2, 2) * R(0, 2),
          R(0, 0) * R(1, 0), R(0, 1) * R(1, 1), R(0, 2) * R(1, 2);
    K4 << R(1, 1) * R(2, 2) + R(1, 2) * R(2, 1),
          R(1, 2) * R(2, 0) + R(1, 0) * R(2, 2),
          R(1, 0) * R(2, 1) + R(1, 1) * R(2, 0),
          R(2, 1) * R(0, 2) + R(2, 2) * R(0, 1),
          R(2, 2) * R(0, 0) + R(2, 0) * R(0, 2),
          R(2, 0) * R(0, 1) + R(2, 1) * R(0, 0),
          R(0, 1) * R(1, 2) + R(0, 2) * R(1, 1),
          R(0, 2) * R(1, 0) + R(0, 0) * R(1, 2),
          R(0, 0) * R(1, 1) + R(0, 1) * R(1, 0);

    RDMatXX K(6, 6);
    K.block(0, 0, 3, 3) = K1;
    K.block(0, 3, 3, 3) = 2. * K2;
    K.block(3, 0, 3, 3) = K3;
    K.block(3, 3, 3, 3) = K4;

    RDMatXX outCijkl(K * inCijkl * K.transpose());
    return outCijkl;
}

void Material::prepare3D() {
    int Nr = mMyQuad->getNr();
    mVpv3D = RDMatXN::Zero(Nr, nPE);
    mVph3D = RDMatXN::Zero(Nr, nPE);
    mVsv3D = RDMatXN::Zero(Nr, nPE);
    mVsh3D = RDMatXN::Zero(Nr, nPE);
    mRho3D = RDMatXN::Zero(Nr, nPE);
    mEta3D = RDMatXN::Zero(Nr, nPE);
    mQkp3D = RDMatXN::Zero(Nr, nPE);
    mQmu3D = RDMatXN::Zero(Nr, nPE);

    for (int ipol = 0; ipol <= nPol; ipol++) {
        for (int jpol = 0; jpol <= nPol; jpol++) {
            int ipnt = ipol * nPntEdge + jpol;
            const RDCol2 &xieta = SpectralConstants::getXiEta(ipol, jpol, mMyQuad->isAxial());
            // fill with 1D
            mVpv3D.col(ipnt).fill(Mapping::interpolate(mVpv1D, xieta));
            mVph3D.col(ipnt).fill(Mapping::interpolate(mVph1D, xieta));
            mVsv3D.col(ipnt).fill(Mapping::interpolate(mVsv1D, xieta));
            mVsh3D.col(ipnt).fill(Mapping::interpolate(mVsh1D, xieta));
            mRho3D.col(ipnt).fill(Mapping::interpolate(mRho1D, xieta));
            mEta3D.col(ipnt).fill(Mapping::interpolate(mEta1D, xieta));
            mQkp3D.col(ipnt).fill(Mapping::interpolate(mQkp1D, xieta));
            mQmu3D.col(ipnt).fill(Mapping::interpolate(mQmu1D, xieta));
            // rho for mass
            int NrP = mMyQuad->getPointNr(ipol, jpol);
            mRhoMass3D[ipnt] = RDColX::Constant(NrP, mRho3D.col(ipnt)(0));
            mVpFluid3D[ipnt] = RDColX::Constant(NrP, mVpv3D.col(ipnt)(0));
        }
    }
}

bool Material::_3Dprepared() const {
    return mVpv3D.rows() == mMyQuad->getNr();
}


/* This file is part of the Tomographer project, which is distributed under the
 * terms of the MIT license.
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2016 ETH Zurich, Institute for Theoretical Physics, Philippe Faist
 * Copyright (c) 2017 Caltech, Institute for Quantum Information and Matter, Philippe Faist
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include <iostream>

#include <tomographer/tools/loggers.h>
#include <tomographer/tools/eigenutil.h>
#include <tomographer/densedm/dmtypes.h>
#include <tomographer/densedm/indepmeasllh.h>
#include <tomographer/densedm/tspacellhwalker.h>
#include <tomographer/densedm/tspacefigofmerit.h>
#include <tomographer/mhrw.h>
#include <tomographer/mhrwtasks.h>
#include <tomographer/mhrw_valuehist_tools.h>

#include <tomographer/mpi/multiprocmpi.h>

#include <boost/serialization/base_object.hpp>

//
// Data types for our quantum objects.  For the sake of the example, we just
// leave the size to be dynamic, that is, fixed at run time and not at compile
// time.
//
typedef Tomographer::DenseDM::DMTypes<Eigen::Dynamic, double> DMTypes;

//
// The class which will store our tomography data. Just define this as
// "DenseLLH" as a shorthand.
//
typedef Tomographer::DenseDM::IndepMeasLLH<DMTypes> DenseLLH;

//
// The type of value calculator we would like to use.  Here, we settle for the
// expectation value of an observable, as we are interested in the square
// fidelity to the pure Bell Phi+ state (=expectation value of the observable
// |Phi+><Phi+|).
//
typedef Tomographer::DenseDM::TSpace::ObservableValueCalculator<DMTypes>
  ValueCalculator;

//
// The base CData object we use.  See Tomographer:::MHRWTasks::ValueHistogramTools::CDataBase.
//
typedef Tomographer::MHRWTasks::ValueHistogramTools::CDataBase<
  ValueCalculator, // our value calculator
  true, // use binning analysis
  Tomographer::MHWalkerParamsStepSize<double>, // MHWalkerParams
  std::mt19937::result_type, // RngSeedType
  long, // IterCountIntType
  double, // CountRealType
  int // HistCountIntType
  >
  BaseCData;

//
// We need to define a class which adds the capacity of creating the "master"
// random walk object to the engine in
// Tomographer::MHRWTasks::ValueHistogramTools, which take care of running the
// random walks etc. as needed.
//
struct OurCData : public BaseCData
{
  OurCData(DenseLLH * llh_, // data from the the tomography experiment
	   ValueCalculator valcalc, // the figure-of-merit calculator
	   HistogramParams hist_params, // histogram parameters
	   int binning_num_levels, // number of binning levels in the binning analysis
	   MHRWParamsType mhrw_params, // parameters of the random walk
	   RngSeedType base_seed) // a random seed to initialize the random number generator
    : BaseCData(valcalc, hist_params, binning_num_levels,
                mhrw_params, base_seed),
      llh(llh_)
  {
  }

  DenseLLH * llh;

  // The result of a task run -- just use the default StatsResults type provided
  // by ValueHistogramTools.  If we had several stats collectors set, we would
  // need to pick out the result corresponding to the
  // value-histogram-stats-collector (see "minimal_tomorun_controlled.cxx" for
  // an example).
  typedef MHRWStatsResultsBaseType MHRWStatsResultsType;

  //
  // This function is called automatically by the task manager/dispatcher via
  // MHRWTasks.  It should set up the random walk as required (at a minimum,
  // create a MHWalker instance and pass on the default value stats collector
  // from Tomographer::MHRWTasks::ValueHistogramTools), and run it.
  //
  // We should not forget to call run(), to actually run the random walk!
  //
  template<typename Rng, typename LoggerType, typename ExecFn>
  inline void setupRandomWalkAndRun(Rng & rng, LoggerType & logger, ExecFn run) const
  {
    auto val_stats_collector = createValueStatsCollector(logger);

    Tomographer::DenseDM::TSpace::LLHMHWalker<DenseLLH,Rng,LoggerType> mhwalker(
            llh->dmt.initMatrixType(),
            *llh,
            rng,
            logger
            );

    run(mhwalker, val_stats_collector);
  };

private:
  OurCData() : BaseCData(), llh(NULL) { } // for serialization

  friend boost::serialization::access;
  template<typename Archive>
  void serialize(Archive & a, unsigned int /* version */)
  {
    a & boost::serialization::base_object<BaseCData>(*this);
    a & llh;
  }
};

//
// The root logger which takes care of handling the log messages.  Here, we log to the
// standard output (recall that stdout/stderr are seen as a "file").
//
// The level of the logger can be set to one of Tomographer::Logger::LONGDEBUG,
// Tomographer::Logger::DEBUG, Tomographer::Logger::INFO, Tomographer::Logger::WARNING or
// Tomographer::Logger::ERROR.
//
typedef Tomographer::Logger::FileLogger BaseLoggerType;
BaseLoggerType rootlogger(stderr, Tomographer::Logger::DEBUG);

// The task type, for the MultiProc interface
typedef Tomographer::MHRWTasks::MHRandomWalkTask<OurCData, std::mt19937>
  OurMHRandomWalkTask;

int main()
{
  //
  // MPI initializations
  //
  mpi::environment mpi_env;
  mpi::communicator mpi_world;

  const bool is_master = (mpi_world.rank() == 0);

  //
  // Prepare the logger in which we can log debug/info/error messages.
  //
  typedef Tomographer::Logger::OriginPrefixedLogger<decltype(rootlogger)> PrefixLoggerType;
  PrefixLoggerType baselogger(rootlogger, streamstr(mpi_world.rank() << "/" << mpi_world.size()<<"|"));

  auto logger = Tomographer::Logger::makeLocalLogger("main()", baselogger);

  logger.debug("starting up");

  DenseLLH * llh = NULL;
  OurCData * taskcdat = NULL;

  if (is_master) {
    //
    // Master gets to prepare all the data.
    //

    //
    // Specify the dimension of the quantum tomography setting.
    //
    const int dim = 4; // two qubits
    DMTypes dmt(dim);

    //
    // Prepare data from the tomography experiment.
    //
    // In this hypothetical experiment, we assumed that the observables
    // \sigma_x\otimes\sigma_x, \sigma_y\otimes\sigma_y and \sigma_z\otimes\sigma_z are each
    // measured 100 times. Each measurement setting has two possible outcomes, +1 or -1, and
    // hence there are in total 6 POVM effects.
    //

    llh = new DenseLLH(dmt);

    // POVM effects for  \sigma_x \otimes \sigma_x

    DMTypes::MatrixType Exxplus(dmt.initMatrixType());
    Exxplus <<
      0.5,    0,    0,  0.5,
      0,    0.5,  0.5,    0,
      0,    0.5,  0.5,    0,
      0.5,    0,    0,  0.5;

    llh->addMeasEffect(Exxplus, 95);  // 95 counts of "+1" out of 100 for \sigma_x\otimes\sigma_x

    DMTypes::MatrixType Exxminus(dmt.initMatrixType());
    Exxminus <<
      0.5,    0,    0, -0.5,
      0,    0.5, -0.5,    0,
      0,   -0.5,  0.5,    0,
      -0.5,   0,    0,  0.5;

    llh->addMeasEffect(Exxminus, 5);  // 95 counts of "-1" out of 100 for \sigma_x\otimes\sigma_x

    // POVM effects for  \sigma_y \otimes \sigma_y

    DMTypes::MatrixType Eyyplus(dmt.initMatrixType());
    Eyyplus <<
      0.5,    0,    0, -0.5,
      0,    0.5,  0.5,    0,
      0,    0.5,  0.5,    0,
      -0.5,   0,    0,  0.5;

    llh->addMeasEffect(Eyyplus, 8);  // 8 counts of "+1" out of 100 for \sigma_y\otimes\sigma_y

    DMTypes::MatrixType Eyyminus(dmt.initMatrixType());
    Eyyminus <<
      0.5,    0,    0,  0.5,
      0,    0.5, -0.5,    0,
      0,   -0.5,  0.5,    0,
      0.5,    0,    0,  0.5;

    llh->addMeasEffect(Eyyminus, 92);  // 92 counts of "-1" out of 100 for \sigma_y\otimes\sigma_y

    // POVM effects for  \sigma_z \otimes \sigma_z

    DMTypes::MatrixType Ezzplus(dmt.initMatrixType());
    Ezzplus <<
      1,   0,   0,   0,
      0,   0,   0,   0,
      0,   0,   0,   0,
      0,   0,   0,   1;

    llh->addMeasEffect(Ezzplus, 98);  // 98 counts of "+1" out of 100 for \sigma_z\otimes\sigma_z

    DMTypes::MatrixType Ezzminus(dmt.initMatrixType());
    Ezzminus <<
      0,   0,   0,   0,
      0,   1,   0,   0,
      0,   0,   1,   0,
      0,   0,   0,   0;

    llh->addMeasEffect(Ezzminus, 2);  // 2 counts of "-1" out of 100 for \sigma_z\otimes\sigma_z

    logger.debug("data entered OK");

    //
    // Prepare the figure of merit calculator: Squared fidelity to the pure entangled Bell
    // state |\Phi^+>
    //

    DMTypes::MatrixType phiplus(dmt.initMatrixType());
    phiplus <<
      0.5,    0,    0,   0.5,
      0,      0,    0,     0,
      0,      0,    0,     0,
      0.5,    0,    0,   0.5;

    // our main ValueCalculator instance, which is in fact an alias for the
    // ObservableValueCalculator class. [If we wanted to choose which figure of merit to
    // compute at run-time, then we should use a MultiplexorValueCalculator.]
    ValueCalculator valcalc(dmt, phiplus);

    // parameters of the histogram of the figure of merit: cover the range [0.75, 1.0] by
    // dividing it into 50 bins
    const OurCData::HistogramParams hist_params(0.7, 1.0, 50);

    // parameters of the random walk
    const OurCData::MHRWParamsType mhrw_params(
        0.04, // step size (choose such that acceptance ratio ~ 0.25)
        50, // sweep size (should be chosen such that  sweep_size*step_size >~ 1)
        1024, // # of thermalization sweeps
        32768 // # of live sweeps in which samples are collected
        );

    // seed for random number generator -- just use the current time
    std::mt19937::result_type base_seed =
      (std::mt19937::result_type)std::chrono::system_clock::now().time_since_epoch().count();

    // number of levels for the binning analysis
    const int binning_num_levels = 8;

    // instantiate the class which stores the shared data.
    taskcdat = new OurCData(llh, valcalc, hist_params, binning_num_levels,
                            mhrw_params, base_seed);

    logger.debug("Master here, data ready") ;
  } else {
    taskcdat = NULL;
    logger.debug("Not master, skipping through all the init process") ;
  }

  //
  // Data is ready, prepare & launch the random walks.  Use MPI.
  //

  // repeat the whole random walk this number of times.  These random walks will
  // run in parallel depending on the number of CPUs available.
  const int num_repeats = 20;

  // create the task manager/dispatcher, using the MPI implementation !!
  auto tasks =
    Tomographer::MultiProc::MPI::mkTaskDispatcher<OurMHRandomWalkTask>(
        taskcdat, // constant data
        mpi_world,
        logger.parentLogger(), // the main logger object
        num_repeats // num_runs
        );

  if (is_master) {
    // only master gets to do this

    // get status reports every X milliseconds printed out on std::cout
    tasks.setStatusReportHandler([&](decltype(tasks)::FullStatusReportType report) {
        std::cout << report.getHumanReport() << "\n" ;
      });
    tasks.requestPeriodicStatusReport(1000) ;
  }

  //
  // Finally, run our tomo process
  //

  logger.debug("all set, ready to go");

  auto time_start = std::chrono::system_clock::now();

  tasks.run(); // GO!

  auto time_end = std::chrono::system_clock::now();

  logger.debug("Random walks done.");

  // delta-time, formatted in hours, minutes, seconds and fraction of seconds
  std::string elapsed_s = Tomographer::Tools::fmtDuration(time_end - time_start);

  if (!is_master) {
    logger.debug("not master, we're done here.");
    return 0;
  }

  // only master beyond this point

  const auto & task_results = tasks.collectedTaskResults();

  auto aggregated_histogram = taskcdat->aggregateResultHistograms(task_results) ;

  const auto & histogram = aggregated_histogram.final_histogram;

  // histogram has type Tomographer::AveragedHistogram, you can use it like any other
  // Histogram or HistogramWithErrorBars.  You can pretty-print it with:
  logger.info([&](std::ostream & stream) {
      stream << "Nice little histogram after all that work: \n"
             << histogram.prettyPrint()
             << "\n";
      });

  logger.info([&](std::ostream & stream) {
      // Tomographer::MHRWTasks::ValueHistogramTools::printFinalReport() will generate a
      // default tomorun-like report with the parameters of the random walk, an overview
      // of each histogram of each task repeat, short info on the convergence of the
      // binning error bars, and the final histogram itself along with error bars
      Tomographer::MHRWTasks::ValueHistogramTools::printFinalReport(
          stream, // where to output
          *taskcdat, // the cdata
          task_results, // the results
          aggregated_histogram // aggregated
          );
    });

  delete llh;
  delete taskcdat;

  logger.debug("Finally, all done.");

  // success.
  return 0;
}


//---------------------------------------------------------------------------//
//  MIT License
//
//  Copyright (c) 2020-2021 Mikhail Komarov <nemo@nil.foundation>
//  Copyright (c) 2020-2021 Nikita Kaskov <nemo@nil.foundation>
//
//  Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in all
//  copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//  SOFTWARE.
//---------------------------------------------------------------------------//

#ifndef FILECOIN_STORAGE_PROOFS_CORE_PARAMETER_CACHE_HPP
#define FILECOIN_STORAGE_PROOFS_CORE_PARAMETER_CACHE_HPP

#define BOOST_FILESYSTEM_NO_DEPRECATED

#include <string>

#include <boost/filesystem/path.hpp>
#include <boost/filesystem/operations.hpp>

#include <nil/crypto3/algebra/curves/bls12.hpp>

#include <nil/crypto3/hash/sha2.hpp>
#include <nil/crypto3/hash/algorithm/hash.hpp>

#include <nil/crypto3/marshalling/types/zk/r1cs_gg_ppzksnark/verification_key.hpp>

#include <nil/filecoin/storage/proofs/core/btree/map.hpp>

#include <nil/filecoin/storage/proofs/core/crypto/scheme_params.hpp>
#include <nil/filecoin/storage/proofs/core/crypto/mapped_scheme_params.hpp>

namespace nil {
    namespace filecoin {
        constexpr static const std::size_t VERSION = 28;
        constexpr static const std::size_t SRS_MAX_PROOFS_TO_AGGREGATE = 65535;
        constexpr static const char *PARAMETER_CACHE_ENV_VAR = "FIL_PROOFS_PARAMETER_CACHE";
        constexpr static const char *PARAMETER_CACHE_DIR = "/var/tmp/filecoin-proof-parameters/";
        constexpr static const char *GROTH_PARAMETER_EXT = "params";
        constexpr static const char *PARAMETER_METADATA_EXT = "meta";
        constexpr static const char *VERIFYING_KEY_EXT = "vk";
        constexpr static const char *SRS_SHARED_KEY_NAME = "fil-inner-product-v1";

        struct parameter_data {
            std::string cid;
            std::string digest;
            std::uint64_t sector_size;
        };

        typedef btree::map<std::string, parameter_data> parameter_map;

        static parameter_map PARAMETERS;
        static parameter_map SRS_PARAMETERS;

        std::string parameter_cache_dir_name() {
            return std::getenv(PARAMETER_CACHE_ENV_VAR);
        }

        boost::filesystem::path parameter_cache_dir() {
            return parameter_cache_dir_name();
        }

        boost::filesystem::path parameter_cache_params_path(const std::string &parameter_set_identifier) {
            return boost::filesystem::path(
                (parameter_cache_dir_name() + "/v" + std::to_string(VERSION) + "-" + parameter_set_identifier + ".")
                    .append(GROTH_PARAMETER_EXT));
        }

        boost::filesystem::path parameter_cache_metadata_path(const std::string &parameter_set_identifier) {
            return boost::filesystem::path(
                (parameter_cache_dir_name() + "/v" + std::to_string(VERSION) + "-" + parameter_set_identifier + ".")
                    .append(PARAMETER_METADATA_EXT));
        }

        boost::filesystem::path parameter_cache_verifying_key_path(const std::string &parameter_set_identifier) {
            return boost::filesystem::path(
                (parameter_cache_dir_name() + "/v" + std::to_string(VERSION) + "-" + parameter_set_identifier + ".")
                    .append(VERIFYING_KEY_EXT));
        }

        boost::filesystem::path ensure_ancestor_dirs_exist(const boost::filesystem::path &cache_entry_path) {
            boost::filesystem::path parent_dir = cache_entry_path.parent_path();
            if (boost::filesystem::exists(parent_dir)) {
                return cache_entry_path;
            } else {
                throw std::invalid_argument(cache_entry_path.string() + " has no parent directory");
            }
        }

        struct parameter_set_metadata {
            virtual std::string identifier() const = 0;
            virtual std::size_t sector_size() const = 0;
        };

        struct cache_entry_metadata {
            std::size_t sector_size;
        };

        cache_entry_metadata read_cached_metadata(const boost::filesystem::path &cache_entry_path) {
            return serde_json::from_reader(cache_entry_path);
        }

        cache_entry_metadata write_cached_metadata(const boost::filesystem::path &cache_entry_path,
                                                   cache_entry_metadata value) {
            serde_json::to_writer(cache_entry_path, value);

            return value;
        }

        mapped_scheme_params<crypto3::zk::snark::r1cs_gg_ppzksnark<crypto3::algebra::curves::bls12<381>>>
            read_cached_params(const boost::filesystem::path &cache_entry_path) {
            return mapped_scheme_params<crypto3::zk::snark::r1cs_gg_ppzksnark<crypto3::algebra::curves::bls12<381>>>::
                build_mapped_parameters(cache_entry_path, false);
        }

        crypto3::zk::snark::r1cs_gg_ppzksnark_verification_key<crypto3::algebra::curves::bls12<381>>
            read_cached_verifying_key(const boost::filesystem::path &cache_entry_path) {
            return crypto3::zk::snark::r1cs_gg_ppzksnark_verification_key<crypto3::algebra::curves::bls12<381>>::read(
                cache_entry_path);
        }

        crypto3::zk::snark::r1cs_gg_ppzksnark_verification_key<crypto3::algebra::curves::bls12<381>>
            write_cached_verifying_key(
                const boost::filesystem::path &cache_entry_path,
                const crypto3::zk::snark::r1cs_gg_ppzksnark_verification_key<crypto3::algebra::curves::bls12<381>>
                    &value) {
            value.write(cache_entry_path);

            return value;
        }

        scheme_params<crypto3::zk::snark::r1cs_gg_ppzksnark<crypto3::algebra::curves::bls12<381>>> write_cached_params(
            const boost::filesystem::path &cache_entry_path,
            scheme_params<crypto3::zk::snark::r1cs_gg_ppzksnark<crypto3::algebra::curves::bls12<381>>>
                value) {
            value.write(cache_entry_path);
            return value;
        }

        template<template<typename> class Circuit, typename ParameterSetMetadata = parameter_set_metadata>
        struct cacheable_parameters {
            typedef Circuit<crypto3::algebra::curves::bls12<381>> C;
            typedef ParameterSetMetadata P;

            virtual std::string cache_prefix() const = 0;

            cache_entry_metadata cache_meta(const P &pub_params) {
                return {pub_params.graph.sector_size()};
            }

            virtual std::string cache_identifier(const P &pub_params) {
                using namespace nil::crypto3;

                std::string circuit_hash = crypto3::hash<crypto3::hashes::sha2<256>>(pub_params.identifier());
                actor::format("{}-{:02x}", cache_prefix(), circuit_hash.iter().format(""));
            }

            cache_entry_metadata get_param_metadata(const C &circuit, const P &pub_params) {
                std::string id = cache_identifier(pub_params);

                // generate (or load) metadata
                boost::filesystem::path meta_path = ensure_ancestor_dirs_exist(parameter_cache_metadata_path(id));
                try {
                    read_cached_metadata(meta_path);
                } catch (...) {
                    write_cached_metadata(meta_path, cache_meta(pub_params));
                }
            }

            mapped_scheme_params<crypto3::zk::snark::r1cs_gg_ppzksnark<crypto3::algebra::curves::bls12<381>>>
                get_groth_params(const C &circuit, const P &pub_params) {
                std::string id = cache_identifier(pub_params);

                const auto generate = [&]() {
                    return groth16::generate_random_parameters<crypto3::algebra::curves::bls12<381>>(circuit, r);
                };

                boost::filesystem::path cache_path = ensure_ancestor_dirs_exist(parameter_cache_params_path(id));

                try {
                    return read_cached_params(cache_path);
                } catch (...) {
                    return write_cached_params(cache_path, generate());
                }
            }

            template<typename UniformRandomGenerator>
            crypto3::zk::snark::r1cs_gg_ppzksnark_verification_key<crypto3::algebra::curves::bls12<381>>
                get_verifying_key(UniformRandomGenerator &r, const C &circuit, const P &pub_params) {
                std::string id = cache_identifier(pub_params);

                const auto generate = [&]()
                    -> crypto3::zk::snark::r1cs_gg_ppzksnark_verification_key<crypto3::algebra::curves::bls12<381>> {
                    return get_groth_params(r, circuit, pub_params).vk;
                };

                boost::filesystem::path cache_path = ensure_ancestor_dirs_exist(parameter_cache_verifying_key_path(id));
                try {
                    return read_cached_verifying_key(cache_path);
                } catch (...) {
                    return write_cached_verifying_key(cache_path, generate());
                }
            }
        };
    }    // namespace filecoin
}    // namespace nil

#endif