//==================================================================================================
/**
  Copyright 2016 NumScale SAS

  Distributed under the Boost Software License, Version 1.0.
  (See accompanying file LICENSE.md or copy at http://boost.org/LICENSE_1_0.txt)
**/
//==================================================================================================
#include <boost/simd/function/shuffle.hpp>
#include <boost/simd/pack.hpp>
#include <simd_test.hpp>

using namespace boost::simd;

STF_CASE_TPL("Check unary shuffle behavior with direct permutation", STF_NUMERIC_TYPES)
{
  using p_t = pack<T,1>;
  STF_EQUAL( shuffle<-1>(p_t(4)), Zero<p_t>() );
  STF_EQUAL( shuffle<0>(p_t(4)) , p_t(4) );
}

STF_CASE_TPL("Check binary shuffle behavior with direct permutation", STF_NUMERIC_TYPES)
{
  using p_t = pack<T,1>;
  STF_EQUAL( shuffle<-1>(p_t(4),p_t(7)) , Zero<p_t>() );
  STF_EQUAL( shuffle< 0>(p_t(4),p_t(7)) , p_t(4) );
  STF_EQUAL( shuffle< 1>(p_t(4),p_t(7)) , p_t(7) );
}

template<int N> struct grab_
{
  template<typename I, typename C> struct apply : std::integral_constant<int, N> {};
};

STF_CASE_TPL("Check unary shuffle behavior with direct meta-permutation", STF_NUMERIC_TYPES)
{
  using p_t = pack<T,1>;
  STF_EQUAL( shuffle<grab_<0>>(p_t(4)) , p_t(4) );
}

STF_CASE_TPL("Check binary shuffle behavior with direct meta-permutation", STF_NUMERIC_TYPES)
{
  using p_t = pack<T,1>;
  STF_EQUAL( shuffle<grab_<0>>(p_t(4),p_t(7)) , p_t(4) );
  STF_EQUAL( shuffle<grab_<1>>(p_t(4),p_t(7)) , p_t(7) );
}

#if !defined(BOOST_NO_CONSTEXPR)
// -------------------------------------------------------------------------------------------------
// Not much variation when cardinal is equal to 1, so we just make two "permutations"

constexpr int grab0(int /*i*/, int /*c*/) { return 0; }
constexpr int grab1(int /*i*/, int /*c*/) { return 1; }

STF_CASE_TPL("Check unary shuffle behavior with constexpr-permutation", STF_NUMERIC_TYPES)
{
  using p_t = pack<T,1>;
  STF_EQUAL( shuffle<pattern<grab0>>(p_t(4),p_t(7)) , p_t(4) );
  STF_EQUAL( shuffle<pattern<grab1>>(p_t(4),p_t(7)) , p_t(7) );
}
#endif


#include "scalapack_wrapper.hpp"
#include <boost/lexical_cast.hpp>
#define ITOS boost::lexical_cast<std::string>

namespace frovedis {

template <>
int pgetrf (sliced_blockcyclic_matrix_local<float>& inMat,
            std::vector<int>& IPIV) {

  int INFO = 0;

  if(is_active(inMat)) {
#ifdef ERROR_CHK
    ASSERT_PTR(inMat.data);

    if(!inMat.is_valid())
      REPORT_ERROR(USER_ERROR,"Invalid input matrix!!\n");
#endif

    int M = static_cast<int>(inMat.sliced_num_row);
    int N = static_cast<int>(inMat.sliced_num_col);

    float* aptr = inMat.data;
    int IA = static_cast<int>(inMat.IA);
    int JA = static_cast<int>(inMat.JA);
    int* DESCA = inMat.descA;

    // --- checking for special conditions ---
    int MB_A, NB_A;
    MB_A = DESCA[4];
    NB_A = DESCA[5];

    int row_offsetA, col_offsetA;
    row_offsetA = (IA-1) % MB_A;
    col_offsetA = (JA-1) % NB_A;

    if (row_offsetA != 0 || col_offsetA != 0 || MB_A != NB_A) {
      std::string input_set = "\nIA: "   + ITOS(IA) +
                              ", JA: "   + ITOS(JA) +
                              ", MB_A: " + ITOS(MB_A) +
                              ", NB_A: " + ITOS(NB_A) + "\n";
      input_set += "IROFFA: "   + ITOS(row_offsetA) +
                   ", ICOFFA: " + ITOS(col_offsetA) + "\n";

      std::string msg = input_set;
      msg += "Input matrix internal distribution is incompatible for the ";
      msg += "requested operation!!\nPlease consider different number of ";
      msg += "parallel processes.\nOr you may change the internal distribution ";
      msg += "by specifying suitable blocksize while loading the global input matrix.\n";
      REPORT_ERROR(INTERNAL_ERROR, msg);
    }

    int ipivSize = DESCA[4] + DESCA[8];
    checkAssumption(ipivSize > 0);
    IPIV.resize(ipivSize, 0);

    psgetrf_(&M, &N,
             aptr, &IA, &JA, DESCA,
	     &IPIV[0], &INFO);
  }

  return INFO;
}

template <>
int pgetrf (sliced_blockcyclic_matrix_local<double>& inMat,
            std::vector<int>& IPIV) {

  int INFO = 0;

  if(is_active(inMat)) {
#ifdef ERROR_CHK
    ASSERT_PTR(inMat.data);

    if(!inMat.is_valid())
      REPORT_ERROR(USER_ERROR,"Invalid input matrix!!\n");
#endif

    int M = static_cast<int>(inMat.sliced_num_row);
    int N = static_cast<int>(inMat.sliced_num_col);

    double* aptr = inMat.data;
    int IA = static_cast<int>(inMat.IA);
    int JA = static_cast<int>(inMat.JA);
    int* DESCA = inMat.descA;

    // --- checking for special conditions ---
    int MB_A, NB_A;
    MB_A = DESCA[4];
    NB_A = DESCA[5];

    int row_offsetA, col_offsetA;
    row_offsetA = (IA-1) % MB_A;
    col_offsetA = (JA-1) % NB_A;

    if (row_offsetA != 0 || col_offsetA != 0 || MB_A != NB_A) {
      std::string input_set = "\nIA: "   + ITOS(IA) +
                              ", JA: "   + ITOS(JA) +
                              ", MB_A: " + ITOS(MB_A) +
                              ", NB_A: " + ITOS(NB_A) + "\n";
      input_set += "IROFFA: "   + ITOS(row_offsetA) +
                   ", ICOFFA: " + ITOS(col_offsetA) + "\n";

      std::string msg = input_set;
      msg += "Input matrix internal distribution is incompatible for the ";
      msg += "requested operation!!\nPlease consider different number of ";
      msg += "parallel processes.\nOr you may change the internal distribution ";
      msg += "by specifying suitable blocksize while loading the global input matrix.\n";
      REPORT_ERROR(INTERNAL_ERROR, msg);
    }

    int ipivSize = DESCA[4] + DESCA[8];
    checkAssumption(ipivSize > 0);
    IPIV.resize(ipivSize, 0);

    pdgetrf_(&M, &N,
             aptr, &IA, &JA, DESCA,
	     &IPIV[0], &INFO);
  }

  return INFO;
}

template <>
int pgetri (sliced_blockcyclic_matrix_local<float>& inMat,
            std::vector<int>& IPIV) {

  int INFO = 0;

  if(is_active(inMat)) {
#ifdef ERROR_CHK
    ASSERT_PTR(inMat.data);

    if(!inMat.is_valid())
      REPORT_ERROR(USER_ERROR,"Invalid input matrix!!\n");

    if(inMat.sliced_num_row != inMat.sliced_num_col)
      REPORT_ERROR(USER_ERROR,
         "Matrix-inverse not possible: Input matrix is not a square matrix!!\n");
#endif

    int N = static_cast<int>(inMat.sliced_num_row); // inMat.sliced_num_col;

    float* aptr = inMat.data;
    int IA = static_cast<int>(inMat.IA);
    int JA = static_cast<int>(inMat.JA);
    int* DESCA = inMat.descA;

    // --- checking for special conditions ---
    int MB_A, NB_A;
    MB_A = DESCA[4];
    NB_A = DESCA[5];

    int row_offsetA, col_offsetA;
    row_offsetA = (IA-1) % MB_A;
    col_offsetA = (JA-1) % NB_A;

    if (row_offsetA != 0 || col_offsetA != 0 || MB_A != NB_A) {
      std::string input_set = "\nIA: "   + ITOS(IA) +
                              ", JA: "   + ITOS(JA) +
                              ", MB_A: " + ITOS(MB_A) +
                              ", NB_A: " + ITOS(NB_A) + "\n";
      input_set += "IROFFA: "   + ITOS(row_offsetA) +
                   ", ICOFFA: " + ITOS(col_offsetA) + "\n";

      std::string msg = input_set;
      msg += "Input matrix internal distribution is incompatible for the ";
      msg += "requested operation!!\nPlease consider different number of ";
      msg += "parallel processes.\nOr you may change the internal distribution ";
      msg += "by specifying suitable blocksize while loading the global input matrix.\n";
      REPORT_ERROR(INTERNAL_ERROR, msg);
    }

    int ipivSize = DESCA[4] + DESCA[8];
    checkAssumption(ipivSize > 0 && IPIV.size() >= ipivSize);

    int LWORK = -1, LIWORK = -1;
    std::vector<float> WORK(1);
    std::vector<int> IWORK(1);

    // LWORK and LIWORK query...
    psgetri_(&N,
             aptr, &IA, &JA, DESCA,
	     &IPIV[0],
	     &WORK[0], &LWORK,
	     &IWORK[0], &LIWORK,
	     &INFO);

    LWORK  = WORK[0];
    LIWORK = IWORK[0];
    WORK.resize(LWORK, 0);
    IWORK.resize(LIWORK, 0);

    // Actual call...
    psgetri_(&N,
             aptr, &IA, &JA, DESCA,
	     &IPIV[0],
	     &WORK[0], &LWORK,
	     &IWORK[0], &LIWORK,
	     &INFO);
  }

  return INFO;
}

template <>
int pgetri (sliced_blockcyclic_matrix_local<double>& inMat,
            std::vector<int>& IPIV) {

  int INFO = 0;

  if(is_active(inMat)) {
#ifdef ERROR_CHK
    ASSERT_PTR(inMat.data);

    if(!inMat.is_valid())
      REPORT_ERROR(USER_ERROR,"Invalid input matrix!!\n");

    if(inMat.sliced_num_row != inMat.sliced_num_col)
      REPORT_ERROR(USER_ERROR,
         "Matrix-inverse not possible: Input matrix is not a square matrix!!\n");
#endif

    int N = static_cast<int>(inMat.sliced_num_row); // inMat.sliced_num_col;

    double* aptr = inMat.data;
    int IA = static_cast<int>(inMat.IA);
    int JA = static_cast<int>(inMat.JA);
    int* DESCA = inMat.descA;

    // --- checking for special conditions ---
    int MB_A, NB_A;
    MB_A = DESCA[4];
    NB_A = DESCA[5];

    int row_offsetA, col_offsetA;
    row_offsetA = (IA-1) % MB_A;
    col_offsetA = (JA-1) % NB_A;

    if (row_offsetA != 0 || col_offsetA != 0 || MB_A != NB_A) {
      std::string input_set = "\nIA: "   + ITOS(IA) +
                              ", JA: "   + ITOS(JA) +
                              ", MB_A: " + ITOS(MB_A) +
                              ", NB_A: " + ITOS(NB_A) + "\n";
      input_set += "IROFFA: "   + ITOS(row_offsetA) +
                   ", ICOFFA: " + ITOS(col_offsetA) + "\n";

      std::string msg = input_set;
      msg += "Input matrix internal distribution is incompatible for the ";
      msg += "requested operation!!\nPlease consider different number of ";
      msg += "parallel processes.\nOr you may change the internal distribution ";
      msg += "by specifying suitable blocksize while loading the global input matrix.\n";
      REPORT_ERROR(INTERNAL_ERROR, msg);
    }

    int ipivSize = DESCA[4] + DESCA[8];
    checkAssumption(ipivSize > 0 && IPIV.size() >= ipivSize);

    int LWORK = -1, LIWORK = -1;
    std::vector<double> WORK(1);
    std::vector<int> IWORK(1);

    // LWORK and LIWORK query...
    pdgetri_(&N,
             aptr, &IA, &JA, DESCA,
	     &IPIV[0],
	     &WORK[0], &LWORK,
	     &IWORK[0], &LIWORK,
	     &INFO);

    LWORK  = WORK[0];
    LIWORK = IWORK[0];
    WORK.resize(LWORK, 0);
    IWORK.resize(LIWORK, 0);

    // Actual call...
    pdgetri_(&N,
             aptr, &IA, &JA, DESCA,
	     &IPIV[0],
	     &WORK[0], &LWORK,
	     &IWORK[0], &LIWORK,
	     &INFO);
  }

  return INFO;
}

template <>
int pgetrs (sliced_blockcyclic_matrix_local<float>& inMat,
            sliced_blockcyclic_matrix_local<float>& outMat,
            std::vector<int>& IPIV,
	    char TRANS ){

  int INFO = 0;

  if(!is_same_distribution(inMat,outMat))
    REPORT_FATAL(USER_ERROR,"Incompatible distribution of the inputs\n");
  // since inputs are of same distribution, activeness check on one input is ok
  if(is_active(inMat)) {
#ifdef ERROR_CHK
    ASSERT_PTR(inMat.data && outMat.data);

    if(!inMat.is_valid() || !outMat.is_valid())
      REPORT_ERROR(USER_ERROR,"Invalid input matrix!!\n");

    if(inMat.sliced_num_row != inMat.sliced_num_col)
      REPORT_ERROR(USER_ERROR,"The co-efficient matrix is not a square matrix!!\n");

    if(outMat.sliced_num_row < inMat.sliced_num_row ||
       outMat.sliced_num_col < 1)
      REPORT_ERROR(USER_ERROR,"Invalid right hand side matrix!!\n");

    if(TRANS != 'N' && TRANS != 'T')
      REPORT_ERROR(USER_ERROR,"Illegal value for the TRANS parameter!!\n");
#endif

    int N = static_cast<int>(inMat.sliced_num_row); // inMat.sliced_num_col;
    int NRHS = static_cast<int>(outMat.sliced_num_col);

    float* aptr = inMat.data;
    int IA = static_cast<int>(inMat.IA);
    int JA = static_cast<int>(inMat.JA);
    int* DESCA = inMat.descA;

    float* bptr = outMat.data;
    int IB = static_cast<int>(outMat.IA);
    int JB = static_cast<int>(outMat.JA);
    int* DESCB = outMat.descA;

    // --- checking for special conditions ---
    int MB_A, NB_A, MB_B;
    MB_A = DESCA[4];
    NB_A = DESCA[5];
    MB_B = DESCB[4];

    int row_offsetA, col_offsetA, row_offsetB;
    row_offsetA = (IA-1) % MB_A;
    col_offsetA = (JA-1) % NB_A;
    row_offsetB = (IB-1) % MB_B;

    if (row_offsetA != 0 || col_offsetA != 0 || col_offsetA != row_offsetB
        || MB_A != NB_A  || NB_A != MB_B) {
      std::string input_set = "\nIA: "   + ITOS(IA) +
                              ", JA: "   + ITOS(JA) +
                              ", MB_A: " + ITOS(MB_A) +
                              ", NB_A: " + ITOS(NB_A) +
                              ", IB: "   + ITOS(IB) +
                              ", MB_B: " + ITOS(MB_B) + "\n";
      input_set += "IROFFA: "   + ITOS(row_offsetA) +
                   ", ICOFFA: " + ITOS(col_offsetA) +
                   ", IROFFB: " + ITOS(row_offsetB) + "\n";

      std::string msg = input_set;
      msg += "Input matrix internal distribution is incompatible for the ";
      msg += "requested operation!!\nPlease consider different number of ";
      msg += "parallel processes.\nOr you may change the internal distribution ";
      msg += "by specifying suitable blocksize while loading the global input matrix.\n";
      REPORT_ERROR(INTERNAL_ERROR, msg);
    }

    int ipivSize = DESCA[4] + DESCA[8];
    checkAssumption(ipivSize > 0 && IPIV.size() >= ipivSize);

    psgetrs_(&TRANS,
             &N, &NRHS,
	     aptr, &IA, &JA, DESCA,
	     &IPIV[0],
	     bptr, &IB, &JB, DESCB,
	     &INFO);
  }

  return INFO;
}

template <>
int pgetrs (sliced_blockcyclic_matrix_local<double>& inMat,
            sliced_blockcyclic_matrix_local<double>& outMat,
            std::vector<int>& IPIV,
            char TRANS) {

  int INFO = 0;

  if(!is_same_distribution(inMat,outMat))
    REPORT_FATAL(USER_ERROR,"Incompatible distribution of the inputs\n");
  // since inputs are of same distribution, activeness check on one input is ok
  if(is_active(inMat)) {
#ifdef ERROR_CHK
    ASSERT_PTR(inMat.data && outMat.data);

    if(!inMat.is_valid() || !outMat.is_valid())
      REPORT_ERROR(USER_ERROR,"Invalid input matrix!!\n");

    if(inMat.sliced_num_row != inMat.sliced_num_col)
      REPORT_ERROR(USER_ERROR,"The co-efficient matrix is not a square matrix!!\n");

    if(outMat.sliced_num_row < inMat.sliced_num_row ||
       outMat.sliced_num_col < 1)
      REPORT_ERROR(USER_ERROR,"Invalid right hand side matrix!!\n");

    if(TRANS != 'N' && TRANS != 'T')
      REPORT_ERROR(USER_ERROR,"Illegal value for the TRANS parameter!!\n");
#endif

    int N = static_cast<int>(inMat.sliced_num_row); // inMat.sliced_num_col;
    int NRHS = static_cast<int>(outMat.sliced_num_col);

    double* aptr = inMat.data;
    int IA = static_cast<int>(inMat.IA);
    int JA = static_cast<int>(inMat.JA);
    int* DESCA = inMat.descA;

    double* bptr = outMat.data;
    int IB = static_cast<int>(outMat.IA);
    int JB = static_cast<int>(outMat.JA);
    int* DESCB = outMat.descA;

    // --- checking for special conditions ---
    int MB_A, NB_A, MB_B;
    MB_A = DESCA[4];
    NB_A = DESCA[5];
    MB_B = DESCB[4];

    int row_offsetA, col_offsetA, row_offsetB;
    row_offsetA = (IA-1) % MB_A;
    col_offsetA = (JA-1) % NB_A;
    row_offsetB = (IB-1) % MB_B;

    if (row_offsetA != 0 || col_offsetA != 0 || col_offsetA != row_offsetB
        || MB_A != NB_A  || NB_A != MB_B) {
      std::string input_set = "\nIA: "   + ITOS(IA) +
                              ", JA: "   + ITOS(JA) +
                              ", MB_A: " + ITOS(MB_A) +
                              ", NB_A: " + ITOS(NB_A) +
                              ", IB: "   + ITOS(IB) +
                              ", MB_B: " + ITOS(MB_B) + "\n";
      input_set += "IROFFA: "   + ITOS(row_offsetA) +
                   ", ICOFFA: " + ITOS(col_offsetA) +
                   ", IROFFB: " + ITOS(row_offsetB) + "\n";

      std::string msg = input_set;
      msg += "Input matrix internal distribution is incompatible for the ";
      msg += "requested operation!!\nPlease consider different number of ";
      msg += "parallel processes.\nOr you may change the internal distribution ";
      msg += "by specifying suitable blocksize while loading the global input matrix.\n";
      REPORT_ERROR(INTERNAL_ERROR, msg);
    }

    int ipivSize = DESCA[4] + DESCA[8];
    checkAssumption(ipivSize > 0 && IPIV.size() >= ipivSize);

    pdgetrs_(&TRANS,
             &N, &NRHS,
	     aptr, &IA, &JA, DESCA,
	     &IPIV[0],
	     bptr, &IB, &JB, DESCB,
	     &INFO);
  }

  return INFO;
}

template <>
int pgesv2 (sliced_blockcyclic_matrix_local<float>& inMat,
            sliced_blockcyclic_matrix_local<float>& outMat,
            std::vector<int>& IPIV) {

  int INFO = 0;

  if(!is_same_distribution(inMat,outMat))
    REPORT_FATAL(USER_ERROR,"Incompatible distribution of the inputs\n");
  // since inputs are of same distribution, activeness check on one input is ok
  if(is_active(inMat)) {
#ifdef ERROR_CHK
    ASSERT_PTR(inMat.data && outMat.data);

    if(!inMat.is_valid() || !outMat.is_valid())
      REPORT_ERROR(USER_ERROR,"Invalid input matrix!!\n");

    if(inMat.sliced_num_row != inMat.sliced_num_col)
      REPORT_ERROR(USER_ERROR,"The co-efficient matrix is not a square matrix!!\n");

    if(outMat.sliced_num_row < inMat.sliced_num_row ||
       outMat.sliced_num_col < 1)
      REPORT_ERROR(USER_ERROR,"Invalid right hand side matrix!!\n");
#endif

    int N = static_cast<int>(inMat.sliced_num_row); // inMat.sliced_num_col;
    int NRHS = static_cast<int>(outMat.sliced_num_col);

    float* aptr = inMat.data;
    int IA = static_cast<int>(inMat.IA);
    int JA = static_cast<int>(inMat.JA);
    int* DESCA = inMat.descA;

    int ipivSize = DESCA[4] + DESCA[8];
    checkAssumption(ipivSize > 0);
    IPIV.resize(ipivSize, 0);

    float* bptr = outMat.data;
    int IB = static_cast<int>(outMat.IA);
    int JB = static_cast<int>(outMat.JA);
    int* DESCB = outMat.descA;

    // --- checking for special conditions ---
    int MB_A, NB_A, MB_B;
    MB_A = DESCA[4];
    NB_A = DESCA[5];
    MB_B = DESCB[4];

    int row_offsetA, col_offsetA, row_offsetB;
    row_offsetA = (IA-1) % MB_A;
    col_offsetA = (JA-1) % NB_A;
    row_offsetB = (IB-1) % MB_B;

    if (row_offsetA != 0 || col_offsetA != 0 || col_offsetA != row_offsetB
        || MB_A != NB_A  || NB_A != MB_B) {
      std::string input_set = "\nIA: "   + ITOS(IA) +
                              ", JA: "   + ITOS(JA) +
                              ", MB_A: " + ITOS(MB_A) +
                              ", NB_A: " + ITOS(NB_A) +
                              ", IB: "   + ITOS(IB) +
                              ", MB_B: " + ITOS(MB_B) + "\n";
      input_set += "IROFFA: "   + ITOS(row_offsetA) +
                   ", ICOFFA: " + ITOS(col_offsetA) +
                   ", IROFFB: " + ITOS(row_offsetB) + "\n";

      std::string msg = input_set;
      msg += "Input matrix internal distribution is incompatible for the ";
      msg += "requested operation!!\nPlease consider different number of ";
      msg += "parallel processes.\nOr you may change the internal distribution ";
      msg += "by specifying suitable blocksize while loading the global input matrix.\n";
      REPORT_ERROR(INTERNAL_ERROR, msg);
    }

    psgesv_(&N, &NRHS,
	    aptr, &IA, &JA, DESCA,
	    &IPIV[0],
	    bptr, &IB, &JB, DESCB,
	    &INFO);
  }

  return INFO;
}

template <>
int pgesv2 (sliced_blockcyclic_matrix_local<double>& inMat,
            sliced_blockcyclic_matrix_local<double>& outMat,
            std::vector<int>& IPIV) {

  int INFO = 0;

  if(!is_same_distribution(inMat,outMat))
    REPORT_FATAL(USER_ERROR,"Incompatible distribution of the inputs\n");
  // since inputs are of same distribution, activeness check on one input is ok
  if(is_active(inMat)) {
#ifdef ERROR_CHK
    ASSERT_PTR(inMat.data && outMat.data);

    if(!inMat.is_valid() || !outMat.is_valid())
      REPORT_ERROR(USER_ERROR,"Invalid input matrix!!\n");

    if(inMat.sliced_num_row != inMat.sliced_num_col)
      REPORT_ERROR(USER_ERROR,"The co-efficient matrix is not a square matrix!!\n");

    if(outMat.sliced_num_row < inMat.sliced_num_row ||
       outMat.sliced_num_col < 1)
      REPORT_ERROR(USER_ERROR,"Invalid right hand side matrix!!\n");
#endif

    int N = static_cast<int>(inMat.sliced_num_row); // inMat.sliced_num_col;
    int NRHS = static_cast<int>(outMat.sliced_num_col);

    double* aptr = inMat.data;
    int IA = static_cast<int>(inMat.IA);
    int JA = static_cast<int>(inMat.JA);
    int* DESCA = inMat.descA;

    int ipivSize = DESCA[4] + DESCA[8];
    checkAssumption(ipivSize > 0);
    IPIV.resize(ipivSize, 0);

    double* bptr = outMat.data;
    int IB = static_cast<int>(outMat.IA);
    int JB = static_cast<int>(outMat.JA);
    int* DESCB = outMat.descA;

    // --- checking for special conditions ---
    int MB_A, NB_A, MB_B;
    MB_A = DESCA[4];
    NB_A = DESCA[5];
    MB_B = DESCB[4];

    int row_offsetA, col_offsetA, row_offsetB;
    row_offsetA = (IA-1) % MB_A;
    col_offsetA = (JA-1) % NB_A;
    row_offsetB = (IB-1) % MB_B;

    if (row_offsetA != 0 || col_offsetA != 0 || col_offsetA != row_offsetB
        || MB_A != NB_A  || NB_A != MB_B) {
      std::string input_set = "\nIA: "   + ITOS(IA) +
                              ", JA: "   + ITOS(JA) +
                              ", MB_A: " + ITOS(MB_A) +
                              ", NB_A: " + ITOS(NB_A) +
                              ", IB: "   + ITOS(IB) +
                              ", MB_B: " + ITOS(MB_B) + "\n";
      input_set += "IROFFA: "   + ITOS(row_offsetA) +
                   ", ICOFFA: " + ITOS(col_offsetA) +
                   ", IROFFB: " + ITOS(row_offsetB) + "\n";

      std::string msg = input_set;
      msg += "Input matrix internal distribution is incompatible for the ";
      msg += "requested operation!!\nPlease consider different number of ";
      msg += "parallel processes.\nOr you may change the internal distribution ";
      msg += "by specifying suitable blocksize while loading the global input matrix.\n";
      REPORT_ERROR(INTERNAL_ERROR, msg);
    }

    pdgesv_(&N, &NRHS,
	    aptr, &IA, &JA, DESCA,
	    &IPIV[0],
	    bptr, &IB, &JB, DESCB,
	    &INFO);
  }

  return INFO;
}

template <>
int pgesv1 (sliced_blockcyclic_matrix_local<float>& inMat,
            sliced_blockcyclic_matrix_local<float>& outMat) {

  std::vector<int> IPIV;
  return pgesv2<float>(inMat, outMat, IPIV);
}

template <>
int pgesv1 (sliced_blockcyclic_matrix_local<double>& inMat,
            sliced_blockcyclic_matrix_local<double>& outMat) {

  std::vector<int> IPIV;
  return pgesv2<double>(inMat, outMat, IPIV);
}

template <>
int pgels (sliced_blockcyclic_matrix_local<float>& inMat,
           sliced_blockcyclic_matrix_local<float>& outMat,
           char TRANS) {

  int INFO = 0;

  if(!is_same_distribution(inMat,outMat))
    REPORT_FATAL(USER_ERROR,"Incompatible distribution of the inputs\n");
  // since inputs are of same distribution, activeness check on one input is ok
  if(is_active(inMat)) {
#ifdef ERROR_CHK
    ASSERT_PTR(inMat.data && outMat.data);

    if(!inMat.is_valid() || !outMat.is_valid())
      REPORT_ERROR(USER_ERROR,"Invalid input matrix!!\n");

    if((TRANS != 'N') && (TRANS != 'T'))
      REPORT_ERROR(USER_ERROR, "Invalid value for TRANS parameter!!\n");

    size_t maxSize = std::max(inMat.sliced_num_row,inMat.sliced_num_col);
    if(outMat.sliced_num_row < maxSize || outMat.sliced_num_col < 1)
      REPORT_ERROR(USER_ERROR, "Invalid right hand side matrix!!\n");
#endif

    int M = static_cast<int>(inMat.sliced_num_row);
    int N = static_cast<int>(inMat.sliced_num_col);
    int NRHS = static_cast<int>(outMat.sliced_num_col);

    float* aptr = inMat.data;
    int IA = static_cast<int>(inMat.IA);
    int JA = static_cast<int>(inMat.JA);
    int* DESCA = inMat.descA;

    float* bptr = outMat.data;
    int IB = static_cast<int>(outMat.IA);
    int JB = static_cast<int>(outMat.JA);
    int* DESCB = outMat.descA;

    // --- checking for special conditions ---
    int MB_A, NB_A, MB_B;
    MB_A = DESCA[4];
    NB_A = DESCA[5];
    MB_B = DESCB[4];

    int row_offsetA, col_offsetA, row_offsetB;
    row_offsetA = (IA-1) % MB_A;
    col_offsetA = (JA-1) % NB_A;
    row_offsetB = (IB-1) % MB_B;

    if ( (M >= N && row_offsetA != row_offsetB) ||
         (M < N  && col_offsetA != row_offsetB) ||
         (M >= N && MB_A != MB_B) ||
         (M < N  && NB_A != MB_B) ) {
      std::string input_set = "\nM: "    + ITOS(M) +
                              ", N: "    + ITOS(N) +
                              ", IA: "   + ITOS(IA) +
                              ", JA: "   + ITOS(JA) +
                              ", MB_A: " + ITOS(MB_A) +
                              ", NB_A: " + ITOS(NB_A) +
                              ", IB: "   + ITOS(IB) +
                              ", MB_B: " + ITOS(MB_B) + "\n";
      input_set += "IROFFA: "   + ITOS(row_offsetA) +
                   ", ICOFFA: " + ITOS(col_offsetA) +
                   ", IROFFB: " + ITOS(row_offsetB) + "\n";

      std::string msg = input_set;
      msg += "Input matrix internal distribution is incompatible for the ";
      msg += "requested operation!!\nPlease consider different number of ";
      msg += "parallel processes.\nOr you may change the internal distribution ";
      msg += "by specifying suitable blocksize while loading the global input matrix.\n";
      REPORT_ERROR(INTERNAL_ERROR, msg);
    }

    int LWORK = -1;
    std::vector<float> WORK(1);

    // LWORK query...
    psgels_(&TRANS,
            &M, &N, &NRHS,
	    aptr, &IA, &JA, DESCA,
	    bptr, &IB, &JB, DESCB,
	    &WORK[0], &LWORK, &INFO);

    LWORK = WORK[0];
    WORK.resize(LWORK, 0);

    // Actual call...
    psgels_(&TRANS,
            &M, &N, &NRHS,
	    aptr, &IA, &JA, DESCA,
	    bptr, &IB, &JB, DESCB,
	    &WORK[0], &LWORK, &INFO);
  }

  return INFO;
}

template <>
int pgels (sliced_blockcyclic_matrix_local<double>& inMat,
           sliced_blockcyclic_matrix_local<double>& outMat,
           char TRANS) {

  int INFO = 0;

  if(!is_same_distribution(inMat,outMat))
    REPORT_FATAL(USER_ERROR,"Incompatible distribution of the inputs\n");
  // since inputs are of same distribution, activeness check on one input is ok
  if(is_active(inMat)) {
#ifdef ERROR_CHK
    ASSERT_PTR(inMat.data && outMat.data);

    if(!inMat.is_valid() || !outMat.is_valid())
      REPORT_ERROR(USER_ERROR,"Invalid input matrix!!\n");

    if((TRANS != 'N') && (TRANS != 'T'))
      REPORT_ERROR(USER_ERROR, "Invalid value for TRANS parameter!!\n");

    size_t maxSize = std::max(inMat.sliced_num_row,inMat.sliced_num_col);
    if(outMat.sliced_num_row < maxSize || outMat.sliced_num_col < 1)
      REPORT_ERROR(USER_ERROR, "Invalid right hand side matrix!!\n");
#endif

    int M = static_cast<int>(inMat.sliced_num_row);
    int N = static_cast<int>(inMat.sliced_num_col);
    int NRHS = static_cast<int>(outMat.sliced_num_col);

    double* aptr = inMat.data;
    int IA = static_cast<int>(inMat.IA);
    int JA = static_cast<int>(inMat.JA);
    int* DESCA = inMat.descA;

    double* bptr = outMat.data;
    int IB = static_cast<int>(outMat.IA);
    int JB = static_cast<int>(outMat.JA);
    int* DESCB = outMat.descA;

    // --- checking for special conditions ---
    int MB_A, NB_A, MB_B;
    MB_A = DESCA[4];
    NB_A = DESCA[5];
    MB_B = DESCB[4];

    int row_offsetA, col_offsetA, row_offsetB;
    row_offsetA = (IA-1) % MB_A;
    col_offsetA = (JA-1) % NB_A;
    row_offsetB = (IB-1) % MB_B;

    if ( (M >= N && row_offsetA != row_offsetB) ||
         (M < N  && col_offsetA != row_offsetB) ||
         (M >= N && MB_A != MB_B) ||
         (M < N  && NB_A != MB_B) ) {
      std::string input_set = "\nM: "    + ITOS(M) +
                              ", N: "    + ITOS(N) +
                              ", IA: "   + ITOS(IA) +
                              ", JA: "   + ITOS(JA) +
                              ", MB_A: " + ITOS(MB_A) +
                              ", NB_A: " + ITOS(NB_A) +
                              ", IB: "   + ITOS(IB) +
                              ", MB_B: " + ITOS(MB_B) + "\n";
      input_set += "IROFFA: "   + ITOS(row_offsetA) +
                   ", ICOFFA: " + ITOS(col_offsetA) +
                   ", IROFFB: " + ITOS(row_offsetB) + "\n";

      std::string msg = input_set;
      msg += "Input matrix internal distribution is incompatible for the ";
      msg += "requested operation!!\nPlease consider different number of ";
      msg += "parallel processes.\nOr you may change the internal distribution ";
      msg += "by specifying suitable blocksize while loading the global input matrix.\n";
      REPORT_ERROR(INTERNAL_ERROR, msg);
    }

    int LWORK = -1;
    std::vector<double> WORK(1);

    // LWORK query...
    pdgels_(&TRANS,
            &M, &N, &NRHS,
	    aptr, &IA, &JA, DESCA,
	    bptr, &IB, &JB, DESCB,
	    &WORK[0], &LWORK, &INFO);

    LWORK = WORK[0];
    WORK.resize(LWORK, 0);

    // Actual call...
    pdgels_(&TRANS,
            &M, &N, &NRHS,
	    aptr, &IA, &JA, DESCA,
	    bptr, &IB, &JB, DESCB,
	    &WORK[0], &LWORK, &INFO);
  }

  return INFO;
}

template <>
int pgesvd1 (sliced_blockcyclic_matrix_local<float>& inMat,
             std::vector<float>& S_VAL) {

  int INFO = 0;

  if(is_active(inMat)) {
#ifdef ERROR_CHK
    ASSERT_PTR(inMat.data);

    if(!inMat.is_valid())
      REPORT_ERROR(USER_ERROR,"Invalid input matrix!!\n");
#endif

    char JOBZ = 'N';

    int M = static_cast<int>(inMat.sliced_num_row);
    int N = static_cast<int>(inMat.sliced_num_col);

    float* aptr = inMat.data;
    int IA = static_cast<int>(inMat.IA);
    int JA = static_cast<int>(inMat.JA);
    int* DESCA = inMat.descA;

    // checking for special conditions in order to avoid error msg from PSGEBRD
    int MB_A, NB_A;
    MB_A = DESCA[4];
    NB_A = DESCA[5];

    int row_offsetA, col_offsetA;
    row_offsetA = (IA-1) % MB_A;
    col_offsetA = (JA-1) % NB_A;

    if (row_offsetA != col_offsetA || MB_A != NB_A) {
      std::string input_set = "\nIA: "   + ITOS(IA) +
                              ", JA: "   + ITOS(JA) +
                              ", MB_A: " + ITOS(MB_A) +
                              ", NB_A: " + ITOS(NB_A) + "\n";
      input_set += "IROFFA: "   + ITOS(row_offsetA) +
                   ", ICOFFA: " + ITOS(col_offsetA) + "\n";

      std::string msg = input_set;
      msg += "Input matrix internal distribution is incompatible for the ";
      msg += "requested operation!!\nPlease consider different number of ";
      msg += "parallel processes.\nOr you may change the internal distribution ";
      msg += "by specifying suitable blocksize while loading the global input matrix.\n";
      REPORT_ERROR(INTERNAL_ERROR, msg);
    }

    S_VAL.resize(std::min(M,N), 0);

    float* uptr = NULL;
    int IU = 1, JU = 1;
    int* DESCU = NULL;

    float* vtptr = NULL;
    int IVT = 1, JVT = 1;
    int* DESCVT = NULL;

    int LWORK = -1;
    std::vector<float> WORK(1);

    // LWORK query...
    psgesvd_(&JOBZ, &JOBZ,
             &M, &N,
	     aptr, &IA, &JA, DESCA,
	     &S_VAL[0],
	     uptr, &IU, &JU, DESCU,
	     vtptr, &IVT, &JVT, DESCVT,
	     &WORK[0], &LWORK, &INFO);

    LWORK = WORK[0];
    WORK.resize(LWORK, 0);

    // Actual call...
    psgesvd_(&JOBZ, &JOBZ,
             &M, &N,
	     aptr, &IA, &JA, DESCA,
	     &S_VAL[0],
	     uptr, &IU, &JU, DESCU,
	     vtptr, &IVT, &JVT, DESCVT,
	     &WORK[0], &LWORK, &INFO);
  }

  return INFO;
}

template <>
int pgesvd1 (sliced_blockcyclic_matrix_local<double>& inMat,
             std::vector<double>& S_VAL) {

  int INFO = 0;

  if(is_active(inMat)) {
#ifdef ERROR_CHK
    ASSERT_PTR(inMat.data);

    if(!inMat.is_valid())
      REPORT_ERROR(USER_ERROR,"Invalid input matrix!!\n");
#endif

    char JOBZ = 'N';

    int M = static_cast<int>(inMat.sliced_num_row);
    int N = static_cast<int>(inMat.sliced_num_col);

    double* aptr = inMat.data;
    int IA = static_cast<int>(inMat.IA);
    int JA = static_cast<int>(inMat.JA);
    int* DESCA = inMat.descA;

    // checking for special conditions in order to avoid error msg from PDGEBRD
    int MB_A, NB_A;
    MB_A = DESCA[4];
    NB_A = DESCA[5];

    int row_offsetA, col_offsetA;
    row_offsetA = (IA-1) % MB_A;
    col_offsetA = (JA-1) % NB_A;

    if (row_offsetA != col_offsetA || MB_A != NB_A) {
      std::string input_set = "\nIA: "   + ITOS(IA) +
                              ", JA: "   + ITOS(JA) +
                              ", MB_A: " + ITOS(MB_A) +
                              ", NB_A: " + ITOS(NB_A) + "\n";
      input_set += "IROFFA: "   + ITOS(row_offsetA) +
                   ", ICOFFA: " + ITOS(col_offsetA) + "\n";

      std::string msg = input_set;
      msg += "Input matrix internal distribution is incompatible for the ";
      msg += "requested operation!!\nPlease consider different number of ";
      msg += "parallel processes.\nOr you may change the internal distribution ";
      msg += "by specifying suitable blocksize while loading the global input matrix.\n";
      REPORT_ERROR(INTERNAL_ERROR, msg);
    }

    S_VAL.resize(std::min(M,N), 0);

    double* uptr = NULL;
    int IU = 1, JU = 1;
    int* DESCU = NULL;

    double* vtptr = NULL;
    int IVT = 1, JVT = 1;
    int* DESCVT = NULL;

    int LWORK = -1;
    std::vector<double> WORK(1);

    // LWORK query...
    pdgesvd_(&JOBZ, &JOBZ,
             &M, &N,
	     aptr, &IA, &JA, DESCA,
	     &S_VAL[0],
	     uptr, &IU, &JU, DESCU,
	     vtptr, &IVT, &JVT, DESCVT,
	     &WORK[0], &LWORK, &INFO);

    LWORK = WORK[0];
    WORK.resize(LWORK, 0);

    // Actual call...
    pdgesvd_(&JOBZ, &JOBZ,
             &M, &N,
	     aptr, &IA, &JA, DESCA,
	     &S_VAL[0],
	     uptr, &IU, &JU, DESCU,
	     vtptr, &IVT, &JVT, DESCVT,
	     &WORK[0], &LWORK, &INFO);
  }

  return INFO;
}

template <>
int pgesvd2 (sliced_blockcyclic_matrix_local<float>& inMat,
             std::vector<float>& S_VAL,
	     sliced_blockcyclic_matrix_local<float>& S_VEC,
	     char VEC_TYPE) {

  int INFO = 0;

  if(!is_same_distribution(inMat,S_VEC))
    REPORT_FATAL(USER_ERROR,"Incompatible distribution of the inputs\n");
  // since inputs are of same distribution, activeness check on one input is ok
  if(is_active(inMat)) {
#ifdef ERROR_CHK
    ASSERT_PTR(inMat.data);

    if(!inMat.is_valid())
      REPORT_ERROR(USER_ERROR,"Invalid input matrix!!\n");
#endif

    char JOBU = 'N', JOBVT = 'N';

    int M = static_cast<int>(inMat.sliced_num_row);
    int N = static_cast<int>(inMat.sliced_num_col);
    int MIN_MN = std::min(M,N);

    float* uptr = NULL;
    int IU = 1, JU = 1;
    int* DESCU = NULL;

    float* vtptr = NULL;
    int IVT = 1, JVT = 1;
    int* DESCVT = NULL;

    if(VEC_TYPE == 'L') {
      if(!S_VEC.data)
        REPORT_ERROR(USER_ERROR,
          "Outmat matrix for Left Singular Vectors is not allocated!!\n");

      if(!S_VEC.is_valid())
        REPORT_ERROR(USER_ERROR,
	  "Invalid output matrix (left singular vector)!!\n");

      if (S_VEC.sliced_num_row < M || S_VEC.sliced_num_col < MIN_MN)
        REPORT_ERROR(USER_ERROR,
                     "Incompatible size for the 3rd argument matrix!!\n");

      JOBU = 'V';
      uptr = S_VEC.data;
      IU = static_cast<int>(S_VEC.IA);
      JU = static_cast<int>(S_VEC.JA);
      DESCU = S_VEC.descA;
    }

    else if (VEC_TYPE == 'R') {
      if(!S_VEC.data)
        REPORT_ERROR(USER_ERROR,
          "Outmat matrix for Right Singular Vectors is not allocated!!\n");

      if(!S_VEC.is_valid())
        REPORT_ERROR(USER_ERROR,
	  "Invalid output matrix (right singular vector)!!\n");

      if (S_VEC.sliced_num_row < MIN_MN || S_VEC.sliced_num_col < N)
        REPORT_ERROR(USER_ERROR,
                     "Incompatible size for the 3rd argument matrix!!\n");

      JOBVT = 'V';
      vtptr = S_VEC.data;
      IVT = static_cast<int>(S_VEC.IA);
      JVT = static_cast<int>(S_VEC.JA);
      DESCVT = S_VEC.descA;
    }

    else
      REPORT_ERROR(USER_ERROR,"Invalid vector type is specified!!\n");

    float* aptr = inMat.data;
    int IA = static_cast<int>(inMat.IA);
    int JA = static_cast<int>(inMat.JA);
    int* DESCA = inMat.descA;

    // checking for special conditions to avoid err msg from PSGEBRD & PSORMBR
    int MB_A, NB_A;
    MB_A = DESCA[4];
    NB_A = DESCA[5];

    int row_offsetA, col_offsetA;
    row_offsetA = (IA-1) % MB_A;
    col_offsetA = (JA-1) % NB_A;

    bool WANTU = (VEC_TYPE == 'L') ? true : false;
    int x_offsetC, IJ_C, xB_C;
    if(WANTU) {
      xB_C = DESCU[4];  // MB_U
      IJ_C = IU;
      x_offsetC =  (IJ_C-1) % xB_C; // row_offsetU
    }
    else {
      xB_C = DESCVT[5]; // NB_VT
      IJ_C = JVT;
      x_offsetC = (IJ_C-1) % xB_C; // col_offsetVT
    }

    if ( row_offsetA != col_offsetA || MB_A != NB_A ||
         (WANTU  && row_offsetA != x_offsetC) ||
         (WANTU  && MB_A != xB_C) ||
         (!WANTU && col_offsetA != x_offsetC) ||
         (!WANTU && NB_A != xB_C) ){
      std::string input_set = "\nIA: "   + ITOS(IA) +
                              ", JA: "   + ITOS(JA) +
                              ", MB_A: " + ITOS(MB_A) +
                              ", NB_A: " + ITOS(NB_A) + "\n";
      input_set += "IROFFA: "   + ITOS(row_offsetA) +
                   ", ICOFFA: " + ITOS(col_offsetA) + "\n";
      input_set += "VEC_TYPE: ";
      input_set += VEC_TYPE ;
      input_set += ", IXOFFC: " + ITOS(x_offsetC) +
                   ", IJ_C: "   + ITOS(IJ_C) +
                   ", XB_C: "   + ITOS(xB_C) + "\n";

      std::string msg = input_set;
      msg += "Input matrix internal distribution is incompatible for the ";
      msg += "requested operation!!\nPlease consider different number of ";
      msg += "parallel processes.\nOr you may change the internal distribution ";
      msg += "by specifying suitable blocksize while loading the global input matrix.\n";
      REPORT_ERROR(INTERNAL_ERROR, msg);
    }

    S_VAL.resize(MIN_MN, 0);

    int LWORK = -1;
    std::vector<float> WORK(1);

    // LWORK query...
    psgesvd_(&JOBU, &JOBVT,
             &M, &N,
	     aptr, &IA, &JA, DESCA,
	     &S_VAL[0],
	     uptr, &IU, &JU, DESCU,
	     vtptr, &IVT, &JVT, DESCVT,
	     &WORK[0], &LWORK, &INFO);

    LWORK = WORK[0];
    WORK.resize(LWORK, 0);

    // Actual call...
    psgesvd_(&JOBU, &JOBVT,
             &M, &N,
	     aptr, &IA, &JA, DESCA,
	     &S_VAL[0],
	     uptr, &IU, &JU, DESCU,
	     vtptr, &IVT, &JVT, DESCVT,
	     &WORK[0], &LWORK, &INFO);
  }

  return INFO;
}

template <>
int pgesvd2 (sliced_blockcyclic_matrix_local<double>& inMat,
             std::vector<double>& S_VAL,
	     sliced_blockcyclic_matrix_local<double>& S_VEC,
	     char VEC_TYPE) {

  int INFO = 0;

  if(!is_same_distribution(inMat,S_VEC))
    REPORT_FATAL(USER_ERROR,"Incompatible distribution of the inputs\n");
  // since inputs are of same distribution, activeness check on one input is ok
  if(is_active(inMat)) {
#ifdef ERROR_CHK
    ASSERT_PTR(inMat.data);

    if(!inMat.is_valid())
      REPORT_ERROR(USER_ERROR,"Invalid input matrix!!\n");
#endif

    char JOBU = 'N', JOBVT = 'N';

    int M = static_cast<int>(inMat.sliced_num_row);
    int N = static_cast<int>(inMat.sliced_num_col);
    int MIN_MN = std::min(M,N);

    double* uptr = NULL;
    int IU = 1, JU = 1;
    int* DESCU = NULL;

    double* vtptr = NULL;
    int IVT = 1, JVT = 1;
    int* DESCVT = NULL;

    if(VEC_TYPE == 'L') {
      if(!S_VEC.data)
        REPORT_ERROR(USER_ERROR,
          "Outmat matrix for Left Singular Vectors is not allocated!!\n");

      if(!S_VEC.is_valid())
        REPORT_ERROR(USER_ERROR,
	  "Invalid output matrix (left singular vector)!!\n");

      if (S_VEC.sliced_num_row < M || S_VEC.sliced_num_col < MIN_MN)
        REPORT_ERROR(USER_ERROR,
                     "Incompatible size for the 3rd argument matrix!!\n");

      JOBU = 'V';
      uptr = S_VEC.data;
      IU = static_cast<int>(S_VEC.IA);
      JU = static_cast<int>(S_VEC.JA);
      DESCU = S_VEC.descA;
    }

    else if (VEC_TYPE == 'R') {
      if(!S_VEC.data)
        REPORT_ERROR(USER_ERROR,
          "Outmat matrix for Right Singular Vectors is not allocated!!\n");

      if(!S_VEC.is_valid())
        REPORT_ERROR(USER_ERROR,
	  "Invalid output matrix (right singular vector)!!\n");

      if (S_VEC.sliced_num_row < MIN_MN || S_VEC.sliced_num_col < N)
        REPORT_ERROR(USER_ERROR,
                     "Incompatible size for the 3rd argument matrix!!\n");

      JOBVT = 'V';
      vtptr = S_VEC.data;
      IVT = static_cast<int>(S_VEC.IA);
      JVT = static_cast<int>(S_VEC.JA);
      DESCVT = S_VEC.descA;
    }

    else
      REPORT_ERROR(USER_ERROR,"Invalid vector type is specified!!\n");

    double* aptr = inMat.data;
    int IA = static_cast<int>(inMat.IA);
    int JA = static_cast<int>(inMat.JA);
    int* DESCA = inMat.descA;

    // checking for special conditions to avoid err msg from PDGEBRD & PDORMBR
    int MB_A, NB_A;
    MB_A = DESCA[4];
    NB_A = DESCA[5];

    int row_offsetA, col_offsetA;
    row_offsetA = (IA-1) % MB_A;
    col_offsetA = (JA-1) % NB_A;

    bool WANTU = (VEC_TYPE == 'L') ? true : false;
    int x_offsetC, IJ_C, xB_C;
    if(WANTU) {
      xB_C = DESCU[4];  // MB_U
      IJ_C = IU;
      x_offsetC =  (IJ_C-1) % xB_C; // row_offsetU
    }
    else {
      xB_C = DESCVT[5]; // NB_VT
      IJ_C = JVT;
      x_offsetC = (IJ_C-1) % xB_C; // col_offsetVT
    }

    if ( row_offsetA != col_offsetA || MB_A != NB_A ||
         (WANTU  && row_offsetA != x_offsetC) ||
         (WANTU  && MB_A != xB_C) ||
         (!WANTU && col_offsetA != x_offsetC) ||
         (!WANTU && NB_A != xB_C) ){
      std::string input_set = "\nIA: "   + ITOS(IA) +
                              ", JA: "   + ITOS(JA) +
                              ", MB_A: " + ITOS(MB_A) +
                              ", NB_A: " + ITOS(NB_A) + "\n";
      input_set += "IROFFA: "   + ITOS(row_offsetA) +
                   ", ICOFFA: " + ITOS(col_offsetA) + "\n";
      input_set += "VEC_TYPE: ";
      input_set += VEC_TYPE ;
      input_set += ", IXOFFC: " + ITOS(x_offsetC) +
                   ", IJ_C: "   + ITOS(IJ_C) +
                   ", XB_C: "   + ITOS(xB_C) + "\n";

      std::string msg = input_set;
      msg += "Input matrix internal distribution is incompatible for the ";
      msg += "requested operation!!\nPlease consider different number of ";
      msg += "parallel processes.\nOr you may change the internal distribution ";
      msg += "by specifying suitable blocksize while loading the global input matrix.\n";
      REPORT_ERROR(INTERNAL_ERROR, msg);
    }

    S_VAL.resize(MIN_MN, 0);

    int LWORK = -1;
    std::vector<double> WORK(1);

    // LWORK query...
    pdgesvd_(&JOBU, &JOBVT,
             &M, &N,
	     aptr, &IA, &JA, DESCA,
	     &S_VAL[0],
	     uptr, &IU, &JU, DESCU,
	     vtptr, &IVT, &JVT, DESCVT,
	     &WORK[0], &LWORK, &INFO);

    LWORK = WORK[0];
    WORK.resize(LWORK, 0);

    // Actual call...
    pdgesvd_(&JOBU, &JOBVT,
             &M, &N,
	     aptr, &IA, &JA, DESCA,
	     &S_VAL[0],
	     uptr, &IU, &JU, DESCU,
	     vtptr, &IVT, &JVT, DESCVT,
	     &WORK[0], &LWORK, &INFO);
  }

  return INFO;
}

template <>
int pgesvd3 (sliced_blockcyclic_matrix_local<float>& inMat,
             std::vector<float>& S_VAL,
	     sliced_blockcyclic_matrix_local<float>& L_SVEC,
	     sliced_blockcyclic_matrix_local<float>& R_SVEC) {

  int INFO = 0;

  if(!is_same_distribution(inMat,L_SVEC,R_SVEC))
    REPORT_FATAL(USER_ERROR,"Incompatible distribution of the inputs\n");
  // since inputs are of same distribution, activeness check on one input is ok
  if(is_active(inMat)) {
#ifdef ERROR_CHK
    ASSERT_PTR(inMat.data);

    if(!inMat.is_valid())
      REPORT_ERROR(USER_ERROR,"Invalid input matrix!!\n");

    if(!L_SVEC.data || !R_SVEC.data)
      REPORT_ERROR(USER_ERROR,
            "Output matrix for singular vectors is not allocated!!\n");

    if(!L_SVEC.is_valid() || !R_SVEC.is_valid())
      REPORT_ERROR(USER_ERROR,"Invalid output matrix for singular vectors!!\n");

    int MIN_MN = std::min(inMat.sliced_num_row,inMat.sliced_num_col);

    if (L_SVEC.sliced_num_row < inMat.sliced_num_row ||
        L_SVEC.sliced_num_col < MIN_MN)
      REPORT_ERROR(USER_ERROR,
                   "Incompatible size for the 3rd argument matrix!!\n");

    if (R_SVEC.sliced_num_row < MIN_MN ||
        R_SVEC.sliced_num_col < inMat.sliced_num_col)
      REPORT_ERROR(USER_ERROR,
                   "Incompatible size for the 4th argument matrix!!\n");
#endif

    char JOBU = 'V', JOBVT = 'V';
    int M = static_cast<int>(inMat.sliced_num_row);
    int N = static_cast<int>(inMat.sliced_num_col);

    float* aptr = inMat.data;
    int IA = static_cast<int>(inMat.IA);
    int JA = static_cast<int>(inMat.JA);
    int* DESCA = inMat.descA;

    S_VAL.resize(std::min(M,N), 0);

    float* uptr = L_SVEC.data;
    int IU = static_cast<int>(L_SVEC.IA);
    int JU = static_cast<int>(L_SVEC.JA);
    int* DESCU = L_SVEC.descA;

    float* vtptr = R_SVEC.data;
    int IVT = static_cast<int>(R_SVEC.IA);
    int JVT = static_cast<int>(R_SVEC.JA);
    int* DESCVT = R_SVEC.descA;

    // checking for special conditions to avoid err msg from PSGEBRD & PSORMBR
    int MB_A, NB_A;
    MB_A = DESCA[4];
    NB_A = DESCA[5];

    int row_offsetA, col_offsetA;
    row_offsetA = (IA-1) % MB_A;
    col_offsetA = (JA-1) % NB_A;

    int row_offsetU, MB_U;
    MB_U = DESCU[4];
    row_offsetU = (IU-1) % MB_U;

    int col_offsetVT, NB_VT;
    NB_VT = DESCVT[5];
    col_offsetVT = (JVT-1) % NB_VT;

    if ( row_offsetA != col_offsetA  || MB_A != NB_A ||
         row_offsetA != row_offsetU  || MB_A != MB_U ||
         col_offsetA != col_offsetVT || NB_A != NB_VT ) {
      std::string input_set = "\nIA: "   + ITOS(IA) +
                              ", JA: "   + ITOS(JA) +
                              ", MB_A: " + ITOS(MB_A) +
                              ", NB_A: " + ITOS(NB_A) + "\n";
      input_set += "IROFFA: "   + ITOS(row_offsetA) +
                   ", ICOFFA: " + ITOS(col_offsetA) + "\n";
      input_set += "IROFFU: " + ITOS(row_offsetU) +
                   ", IA_U: " + ITOS(IU) +
                   ", MB_U: " + ITOS(MB_U)  + "\n";
      input_set += "ICOFFVT: " + ITOS(col_offsetVT) +
                   ", JA_VT: " + ITOS(JVT) +
                   ", NB_VT: " + ITOS(NB_VT)  + "\n";

      std::string msg = input_set;
      msg += "Input matrix internal distribution is incompatible for the ";
      msg += "requested operation!!\nPlease consider different number of ";
      msg += "parallel processes.\nOr you may change the internal distribution ";
      msg += "by specifying suitable blocksize while loading the global input matrix.\n";
      REPORT_ERROR(INTERNAL_ERROR, msg);
    }

    int LWORK = -1;
    std::vector<float> WORK(1);

    // LWORK query...
    psgesvd_(&JOBU, &JOBVT,
             &M, &N,
	     aptr, &IA, &JA, DESCA,
	     &S_VAL[0],
	     uptr, &IU, &JU, DESCU,
	     vtptr, &IVT, &JVT, DESCVT,
	     &WORK[0], &LWORK, &INFO);

    LWORK = WORK[0];
    WORK.resize(LWORK, 0);

    // Actual call...
    psgesvd_(&JOBU, &JOBVT,
             &M, &N,
	     aptr, &IA, &JA, DESCA,
	     &S_VAL[0],
	     uptr, &IU, &JU, DESCU,
	     vtptr, &IVT, &JVT, DESCVT,
	     &WORK[0], &LWORK, &INFO);

  }

  return INFO;
}

template <>
int pgesvd3 (sliced_blockcyclic_matrix_local<double>& inMat,
             std::vector<double>& S_VAL,
	     sliced_blockcyclic_matrix_local<double>& L_SVEC,
	     sliced_blockcyclic_matrix_local<double>& R_SVEC) {

  int INFO = 0;

  if(!is_same_distribution(inMat,L_SVEC,R_SVEC))
    REPORT_FATAL(USER_ERROR,"Incompatible distribution of the inputs\n");
  // since inputs are of same distribution, activeness check on one input is ok
  if(is_active(inMat)) {
#ifdef ERROR_CHK
    ASSERT_PTR(inMat.data);

    if(!inMat.is_valid())
      REPORT_ERROR(USER_ERROR,"Invalid input matrix!!\n");

    if(!L_SVEC.data || !R_SVEC.data)
      REPORT_ERROR(USER_ERROR,
            "Output matrix for singular vectors is not allocated!!\n");

    if(!L_SVEC.is_valid() || !R_SVEC.is_valid())
      REPORT_ERROR(USER_ERROR,"Invalid output matrix for singular vectors!!\n");

    int MIN_MN = std::min(inMat.sliced_num_row,inMat.sliced_num_col);

    if (L_SVEC.sliced_num_row < inMat.sliced_num_row ||
        L_SVEC.sliced_num_col < MIN_MN)
      REPORT_ERROR(USER_ERROR,
                   "Incompatible size for the 3rd argument matrix!!\n");

    if (R_SVEC.sliced_num_row < MIN_MN ||
        R_SVEC.sliced_num_col < inMat.sliced_num_col)
      REPORT_ERROR(USER_ERROR,
                   "Incompatible size for the 4th argument matrix!!\n");
#endif

    char JOBU = 'V', JOBVT = 'V';
    int M = static_cast<int>(inMat.sliced_num_row);
    int N = static_cast<int>(inMat.sliced_num_col);

    double* aptr = inMat.data;
    int IA = static_cast<int>(inMat.IA);
    int JA = static_cast<int>(inMat.JA);
    int* DESCA = inMat.descA;

    S_VAL.resize(std::min(M,N), 0);

    double* uptr = L_SVEC.data;
    int IU = static_cast<int>(L_SVEC.IA);
    int JU = static_cast<int>(L_SVEC.JA);
    int* DESCU = L_SVEC.descA;

    double* vtptr = R_SVEC.data;
    int IVT = static_cast<int>(R_SVEC.IA);
    int JVT = static_cast<int>(R_SVEC.JA);
    int* DESCVT = R_SVEC.descA;

    // checking for special conditions to avoid err msg from PDGEBRD & PDORMBR
    int MB_A, NB_A;
    MB_A = DESCA[4];
    NB_A = DESCA[5];

    int row_offsetA, col_offsetA;
    row_offsetA = (IA-1) % MB_A;
    col_offsetA = (JA-1) % NB_A;

    int row_offsetU, MB_U;
    MB_U = DESCU[4];
    row_offsetU = (IU-1) % MB_U;

    int col_offsetVT, NB_VT;
    NB_VT = DESCVT[5];
    col_offsetVT = (JVT-1) % NB_VT;

    if ( row_offsetA != col_offsetA  || MB_A != NB_A ||
         row_offsetA != row_offsetU  || MB_A != MB_U ||
         col_offsetA != col_offsetVT || NB_A != NB_VT ) {
      std::string input_set = "\nIA: "   + ITOS(IA) +
                              ", JA: "   + ITOS(JA) +
                              ", MB_A: " + ITOS(MB_A) +
                              ", NB_A: " + ITOS(NB_A) + "\n";
      input_set += "IROFFA: "   + ITOS(row_offsetA) +
                   ", ICOFFA: " + ITOS(col_offsetA) + "\n";
      input_set += "IROFFU: " + ITOS(row_offsetU) +
                   ", IA_U: " + ITOS(IU) +
                   ", MB_U: " + ITOS(MB_U)  + "\n";
      input_set += "ICOFFVT: " + ITOS(col_offsetVT) +
                   ", JA_VT: " + ITOS(JVT) +
                   ", NB_VT: " + ITOS(NB_VT)  + "\n";

      std::string msg = input_set;
      msg += "Input matrix internal distribution is incompatible for the ";
      msg += "requested operation!!\nPlease consider different number of ";
      msg += "parallel processes.\nOr you may change the internal distribution ";
      msg += "by specifying suitable blocksize while loading the global input matrix.\n";
      REPORT_ERROR(INTERNAL_ERROR, msg);
    }

    int LWORK = -1;
    std::vector<double> WORK(1);

    // LWORK query...
    pdgesvd_(&JOBU, &JOBVT,
             &M, &N,
	     aptr, &IA, &JA, DESCA,
	     &S_VAL[0],
	     uptr, &IU, &JU, DESCU,
	     vtptr, &IVT, &JVT, DESCVT,
	     &WORK[0], &LWORK, &INFO);

    LWORK = WORK[0];
    WORK.resize(LWORK, 0);

    // Actual call...
    pdgesvd_(&JOBU, &JOBVT,
             &M, &N,
	     aptr, &IA, &JA, DESCA,
	     &S_VAL[0],
	     uptr, &IU, &JU, DESCU,
	     vtptr, &IVT, &JVT, DESCVT,
	     &WORK[0], &LWORK, &INFO);
  }

  return INFO;
}

template <>
void placpy(sliced_blockcyclic_matrix_local<float>& inMat,
            sliced_blockcyclic_matrix_local<float>& outMat,
            char UPLO) {

  if(!is_same_distribution(inMat,outMat))
    REPORT_FATAL(USER_ERROR,"Incompatible distribution of the inputs\n");
  // since inputs are of same distribution, activeness check on one input is ok
  if(is_active(inMat)) {
#ifdef ERROR_CHK
    ASSERT_PTR(inMat.data);

    if(!inMat.is_valid())
      REPORT_ERROR(USER_ERROR,"Invalid input matrix!!\n");

    if(!outMat.data)
      REPORT_ERROR(USER_ERROR,"Output matrix is not allocated!!\n");

    if(!outMat.is_valid())
      REPORT_ERROR(USER_ERROR,"Invalid output matrix!!\n");

    if(inMat.sliced_num_row != outMat.sliced_num_row ||
       inMat.sliced_num_col != outMat.sliced_num_col)
      REPORT_ERROR(USER_ERROR,"Incompatible sizes of the input matrices!!\n");

    if(UPLO != 'U' && UPLO != 'L' && UPLO != 'A')
      REPORT_ERROR(USER_ERROR,"Illegal value provided for 3rd argument!!\n");
#endif

    int M = static_cast<int>(inMat.sliced_num_row);
    int N = static_cast<int>(inMat.sliced_num_col);

    float* aptr = inMat.data;
    int IA = static_cast<int>(inMat.IA);
    int JA = static_cast<int>(inMat.JA);
    int* DESCA = inMat.descA;

    float* bptr = outMat.data;
    int IB = static_cast<int>(outMat.IA);
    int JB = static_cast<int>(outMat.JA);
    int* DESCB = outMat.descA;

    pslacpy_(&UPLO,
             &M, &N,
             aptr, &IA, &JA, DESCA,
             bptr, &IB, &JB, DESCB);
  }
}

template <>
void placpy(sliced_blockcyclic_matrix_local<double>& inMat,
            sliced_blockcyclic_matrix_local<double>& outMat,
            char UPLO) {

  if(!is_same_distribution(inMat,outMat))
    REPORT_FATAL(USER_ERROR,"Incompatible distribution of the inputs\n");
  // since inputs are of same distribution, activeness check on one input is ok
  if(is_active(inMat)) {
#ifdef ERROR_CHK
    ASSERT_PTR(inMat.data);

    if(!inMat.is_valid())
      REPORT_ERROR(USER_ERROR,"Invalid input matrix!!\n");

    if(!outMat.data)
      REPORT_ERROR(USER_ERROR,"Output matrix is not allocated!!\n");

    if(!outMat.is_valid())
      REPORT_ERROR(USER_ERROR,"Invalid output matrix!!\n");

    if(inMat.sliced_num_row != outMat.sliced_num_row ||
       inMat.sliced_num_col != outMat.sliced_num_col)
      REPORT_ERROR(USER_ERROR,"Incompatible sizes of the input matrices!!\n");

    if(UPLO != 'U' && UPLO != 'L' && UPLO != 'A')
      REPORT_ERROR(USER_ERROR,"Illegal value provided for 3rd argument!!\n");
#endif

    int M = static_cast<int>(inMat.sliced_num_row);
    int N = static_cast<int>(inMat.sliced_num_col);

    double* aptr = inMat.data;
    int IA = static_cast<int>(inMat.IA);
    int JA = static_cast<int>(inMat.JA);
    int* DESCA = inMat.descA;

    double* bptr = outMat.data;
    int IB = static_cast<int>(outMat.IA);
    int JB = static_cast<int>(outMat.JA);
    int* DESCB = outMat.descA;

    pdlacpy_(&UPLO,
             &M, &N,
             aptr, &IA, &JA, DESCA,
             bptr, &IB, &JB, DESCB);
  }
}

}


/* Copyright (c) 2016 - 2020, the adamantine authors.
 *
 * This file is subject to the Modified BSD License and may not be distributed
 * without copyright and license information. Please refer to the file LICENSE
 * for the text and further information on this license.
 */

#define BOOST_TEST_MODULE MaterialProperty

#include <Geometry.hh>
#include <MaterialProperty.hh>

#include <deal.II/fe/fe_q.h>
#include <deal.II/grid/grid_generator.h>
#include <deal.II/grid/tria.h>
#include <deal.II/lac/la_parallel_vector.h>

#include <boost/property_tree/ptree.hpp>

#include "main.cc"

BOOST_AUTO_TEST_CASE(material_property)
{
  MPI_Comm communicator = MPI_COMM_WORLD;

  // Create the Geometry
  boost::property_tree::ptree geometry_database;
  geometry_database.put("length", 12);
  geometry_database.put("length_divisions", 4);
  geometry_database.put("height", 6);
  geometry_database.put("height_divisions", 5);
  adamantine::Geometry<2> geometry(communicator, geometry_database);
  auto const &triangulation = geometry.get_triangulation();

  for (auto cell : triangulation.cell_iterators())
  {
    cell->set_material_id(0);
    cell->set_user_index(static_cast<int>(adamantine::MaterialState::solid));
  }

  // Create the MaterialProperty
  boost::property_tree::ptree database;
  database.put("property_format", "polynomial");
  database.put("n_materials", 1);
  database.put("material_0.solid.density", 1.);
  database.put("material_0.solid.thermal_conductivity", 10.);
  database.put("material_0.powder.conductivity", 10.);
  database.put("material_0.liquid", "");
  database.put("material_0.liquidus", "100");
  adamantine::MaterialProperty<2> mat_prop(communicator, triangulation,
                                           database);
  // Evaluate the material property at the given temperature
  dealii::FE_Q<2> fe(4);
  dealii::DoFHandler<2> dof_handler(triangulation);
  dof_handler.distribute_dofs(fe);
  dealii::LinearAlgebra::distributed::Vector<double, dealii::MemorySpace::Host>
      temperature(dof_handler.locally_owned_dofs(), communicator);
  mat_prop.update(dof_handler, temperature);

  // This check only one cell
  for (auto cell : triangulation.active_cell_iterators())
  {
    double const density =
        mat_prop.get(cell, adamantine::StateProperty::density);
    BOOST_CHECK(density == 1.);
    double const th_conduc =
        mat_prop.get(cell, adamantine::StateProperty::thermal_conductivity);
    BOOST_CHECK(th_conduc == 10.);
    double const liquidus = mat_prop.get(cell, adamantine::Property::liquidus);
    BOOST_CHECK(liquidus == 100.);
  }
}

BOOST_AUTO_TEST_CASE(ratios)
{
  MPI_Comm communicator = MPI_COMM_WORLD;

  // Create the Geometry
  boost::property_tree::ptree geometry_database;
  geometry_database.put("length", 12);
  geometry_database.put("length_divisions", 4);
  geometry_database.put("height", 6);
  geometry_database.put("height_divisions", 5);
  adamantine::Geometry<2> geometry(communicator, geometry_database);
  auto const &triangulation = geometry.get_triangulation();

  for (auto cell : triangulation.active_cell_iterators())
  {
    cell->set_material_id(0);
    cell->set_user_index(static_cast<int>(adamantine::MaterialState::powder));
  }
  dealii::FE_Q<2> fe(1);
  dealii::DoFHandler<2> dof_handler(triangulation);
  dof_handler.distribute_dofs(fe);

  // Create the MaterialProperty
  boost::property_tree::ptree database;
  database.put("property_format", "polynomial");
  database.put("n_materials", 1);
  database.put("material_0.solid.density", 10.);
  database.put("material_0.solid.thermal_conductivity", 10.);
  database.put("material_0.solid.specific_heat", 10.);
  database.put("material_0.powder.density", 1.);
  database.put("material_0.powder.thermal_conductivity", 1.);
  database.put("material_0.powder.specific_heat", 1.);
  database.put("material_0.liquid.density", 1.);
  database.put("material_0.liquid.thermal_conductivity", 20.);
  database.put("material_0.liquid.specific_heat", 20.);
  database.put("material_0.solidus", "50");
  database.put("material_0.liquidus", "100");
  database.put("material_0.latent_heat", "1000");
  adamantine::MaterialProperty<2> mat_prop(communicator, triangulation,
                                           database);
  dealii::LinearAlgebra::distributed::Vector<double, dealii::MemorySpace::Host>
      temperature(dof_handler.locally_owned_dofs(), communicator);
  mat_prop.update(dof_handler, temperature);

  // Check the material properties of the powder
  for (auto cell : triangulation.active_cell_iterators())
  {
    double powder_ratio =
        mat_prop.get_state_ratio(cell, adamantine::MaterialState::powder);
    BOOST_CHECK(powder_ratio == 1.);
    double solid_ratio =
        mat_prop.get_state_ratio(cell, adamantine::MaterialState::solid);
    BOOST_CHECK(solid_ratio == 0.);
    double liquid_ratio =
        mat_prop.get_state_ratio(cell, adamantine::MaterialState::liquid);
    BOOST_CHECK(liquid_ratio == 0.);

    double const density =
        mat_prop.get(cell, adamantine::StateProperty::density);
    BOOST_CHECK(density == 1.);
    double const th_conduc =
        mat_prop.get(cell, adamantine::StateProperty::thermal_conductivity);
    BOOST_CHECK(th_conduc == 1.);
    double const liquidus = mat_prop.get(cell, adamantine::Property::liquidus);
    BOOST_CHECK(liquidus == 100.);
    double const solidus = mat_prop.get(cell, adamantine::Property::solidus);
    BOOST_CHECK(solidus == 50.);
    double const latent_heat =
        mat_prop.get(cell, adamantine::Property::latent_heat);
    BOOST_CHECK(latent_heat == 1000.);
    double const specific_heat =
        mat_prop.get(cell, adamantine::StateProperty::specific_heat);
    BOOST_CHECK(specific_heat == 1.);
  }

  // Check the material properties of the liquid
  dealii::types::global_dof_index const n_dofs = dof_handler.n_dofs();
  dealii::LA::distributed::Vector<double> avg_temperature(n_dofs);
  for (unsigned int i = 0; i < n_dofs; ++i)
    avg_temperature[i] = 100000.;
  mat_prop.update(dof_handler, avg_temperature);
  for (auto cell : triangulation.active_cell_iterators())
  {
    double powder_ratio =
        mat_prop.get_state_ratio(cell, adamantine::MaterialState::powder);
    BOOST_CHECK(powder_ratio == 0.);
    double solid_ratio =
        mat_prop.get_state_ratio(cell, adamantine::MaterialState::solid);
    BOOST_CHECK(solid_ratio == 0.);
    double liquid_ratio =
        mat_prop.get_state_ratio(cell, adamantine::MaterialState::liquid);
    BOOST_CHECK(liquid_ratio == 1.);

    double const density =
        mat_prop.get(cell, adamantine::StateProperty::density);
    BOOST_CHECK(density == 1.);
    double const th_conduc =
        mat_prop.get(cell, adamantine::StateProperty::thermal_conductivity);
    BOOST_CHECK(th_conduc == 20.);
    double const liquidus = mat_prop.get(cell, adamantine::Property::liquidus);
    BOOST_CHECK(liquidus == 100.);
    double const solidus = mat_prop.get(cell, adamantine::Property::solidus);
    BOOST_CHECK(solidus == 50.);
    double const latent_heat =
        mat_prop.get(cell, adamantine::Property::latent_heat);
    BOOST_CHECK(latent_heat == 1000.);
    double const specific_heat =
        mat_prop.get(cell, adamantine::StateProperty::specific_heat);
    BOOST_CHECK(specific_heat == 20.);
  }

  // Check the material properties of the solid
  avg_temperature = 0.;
  mat_prop.update(dof_handler, avg_temperature);
  for (auto cell : triangulation.active_cell_iterators())
  {
    double powder_ratio =
        mat_prop.get_state_ratio(cell, adamantine::MaterialState::powder);
    BOOST_CHECK(powder_ratio == 0.);
    double solid_ratio =
        mat_prop.get_state_ratio(cell, adamantine::MaterialState::solid);
    BOOST_CHECK(solid_ratio == 1.);
    double liquid_ratio =
        mat_prop.get_state_ratio(cell, adamantine::MaterialState::liquid);
    BOOST_CHECK(liquid_ratio == 0.);

    double const density =
        mat_prop.get(cell, adamantine::StateProperty::density);
    BOOST_CHECK(density == 10.);
    double const th_conduc =
        mat_prop.get(cell, adamantine::StateProperty::thermal_conductivity);
    BOOST_CHECK(th_conduc == 10.);
    double const liquidus = mat_prop.get(cell, adamantine::Property::liquidus);
    BOOST_CHECK(liquidus == 100.);
    double const solidus = mat_prop.get(cell, adamantine::Property::solidus);
    BOOST_CHECK(solidus == 50.);
    double const latent_heat =
        mat_prop.get(cell, adamantine::Property::latent_heat);
    BOOST_CHECK(latent_heat == 1000.);
    double const specific_heat =
        mat_prop.get(cell, adamantine::StateProperty::specific_heat);
    BOOST_CHECK(specific_heat == 10.);
  }
}

BOOST_AUTO_TEST_CASE(material_property_table)
{
  MPI_Comm communicator = MPI_COMM_WORLD;

  // Create the Geometry
  boost::property_tree::ptree geometry_database;
  geometry_database.put("length", 12);
  geometry_database.put("length_divisions", 4);
  geometry_database.put("height", 6);
  geometry_database.put("height_divisions", 5);
  adamantine::Geometry<2> geometry(communicator, geometry_database);
  auto const &triangulation = geometry.get_triangulation();

  unsigned int n = 0;
  for (auto cell : triangulation.active_cell_iterators())
  {
    if (n < 10)
      cell->set_material_id(0);
    else
      cell->set_material_id(1);

    if (n < 15)
      cell->set_user_index(static_cast<int>(adamantine::MaterialState::solid));
    else
      cell->set_user_index(static_cast<int>(adamantine::MaterialState::powder));

    ++n;
  }

  // Create the MaterialProperty
  boost::property_tree::ptree database;
  database.put("property_format", "table");
  database.put("n_materials", 2);
  database.put("material_0.solid.density", "0., 1.");
  database.put("material_0.solid.thermal_conductivity", "0., 10.; 10., 100.");
  database.put("material_1.solid.density", "0., 1.; 20., 2.; 30., 3.");
  database.put("material_1.solid.thermal_conductivity",
               "0., 10.; 10., 100.; 20., 200.");
  database.put("material_1.powder.density", "0., 1.; 15., 2.; 30., 3.");
  database.put("material_1.powder.thermal_conductivity",
               "0., 10.; 10., 100.; 18., 200.");
  adamantine::MaterialProperty<2> mat_prop(communicator, triangulation,
                                           database);
  // Evaluate the material property at the given temperature
  dealii::FE_Q<2> fe(4);
  dealii::DoFHandler<2> dof_handler(triangulation);
  dof_handler.distribute_dofs(fe);
  dealii::LinearAlgebra::distributed::Vector<double, dealii::MemorySpace::Host>
      temperature(dof_handler.locally_owned_dofs(), communicator);
  for (unsigned int i = 0; i < temperature.local_size(); ++i)
    temperature.local_element(i) = 15;
  mat_prop.update(dof_handler, temperature);

  n = 0;
  double constexpr tolerance = 1e-10;
  for (auto cell : triangulation.active_cell_iterators())
  {
    if (n < 10)
    {
      BOOST_CHECK_CLOSE(mat_prop.get(cell, adamantine::StateProperty::density),
                        1., tolerance);
      BOOST_CHECK_CLOSE(
          mat_prop.get(cell, adamantine::StateProperty::thermal_conductivity),
          100., tolerance);
    }
    else if (n < 15)
    {
      BOOST_CHECK_CLOSE(mat_prop.get(cell, adamantine::StateProperty::density),
                        1.75, tolerance);
      BOOST_CHECK_CLOSE(
          mat_prop.get(cell, adamantine::StateProperty::thermal_conductivity),
          150., tolerance);
    }
    else
    {
      BOOST_CHECK_CLOSE(mat_prop.get(cell, adamantine::StateProperty::density),
                        2., tolerance);
      BOOST_CHECK_CLOSE(
          mat_prop.get(cell, adamantine::StateProperty::thermal_conductivity),
          162.5, tolerance);
    }
    ++n;
  }
}

BOOST_AUTO_TEST_CASE(material_property_polynomials)
{
  MPI_Comm communicator = MPI_COMM_WORLD;

  // Create the Geometry
  boost::property_tree::ptree geometry_database;
  geometry_database.put("length", 12);
  geometry_database.put("length_divisions", 4);
  geometry_database.put("height", 6);
  geometry_database.put("height_divisions", 5);
  adamantine::Geometry<2> geometry(communicator, geometry_database);
  auto const &triangulation = geometry.get_triangulation();

  unsigned int n = 0;
  for (auto cell : triangulation.active_cell_iterators())
  {
    if (n < 10)
      cell->set_material_id(0);
    else
      cell->set_material_id(1);

    if (n < 15)
      cell->set_user_index(static_cast<int>(adamantine::MaterialState::solid));
    else
      cell->set_user_index(static_cast<int>(adamantine::MaterialState::powder));

    ++n;
  }

  // Create the MaterialProperty
  boost::property_tree::ptree database;
  database.put("property_format", "polynomial");
  database.put("n_materials", 2);
  database.put("material_0.solid.density", "0., 1.");
  database.put("material_0.solid.thermal_conductivity", "0., 1., 2.");
  database.put("material_1.solid.density", " 1., 2., 3.");
  database.put("material_1.solid.thermal_conductivity", "1.,  100., 20., 200.");
  database.put("material_1.powder.density", "15., 2., 3.");
  database.put("material_1.powder.thermal_conductivity", " 10., 18., 200.");
  adamantine::MaterialProperty<2> mat_prop(communicator, triangulation,
                                           database);
  // Evaluate the material property at the given temperature
  dealii::FE_Q<2> fe(4);
  dealii::DoFHandler<2> dof_handler(triangulation);
  dof_handler.distribute_dofs(fe);
  dealii::LinearAlgebra::distributed::Vector<double, dealii::MemorySpace::Host>
      temperature(dof_handler.locally_owned_dofs(), communicator);
  for (unsigned int i = 0; i < temperature.local_size(); ++i)
    temperature.local_element(i) = 15;
  mat_prop.update(dof_handler, temperature);

  n = 0;
  double constexpr tolerance = 1e-10;
  for (auto cell : triangulation.active_cell_iterators())
  {
    if (n < 10)
    {
      BOOST_CHECK_CLOSE(mat_prop.get(cell, adamantine::StateProperty::density),
                        15., tolerance);
      BOOST_CHECK_CLOSE(
          mat_prop.get(cell, adamantine::StateProperty::thermal_conductivity),
          465., tolerance);
    }
    else if (n < 15)
    {
      BOOST_CHECK_CLOSE(mat_prop.get(cell, adamantine::StateProperty::density),
                        706., tolerance);
      BOOST_CHECK_CLOSE(
          mat_prop.get(cell, adamantine::StateProperty::thermal_conductivity),
          681001., tolerance);
    }
    else
    {
      BOOST_CHECK_CLOSE(mat_prop.get(cell, adamantine::StateProperty::density),
                        720., tolerance);
      BOOST_CHECK_CLOSE(
          mat_prop.get(cell, adamantine::StateProperty::thermal_conductivity),
          45280., tolerance);
    }
    ++n;
  }
}


#include "distances.h"
#include <EMD_wrapper.h>

#define EIGEN_NO_DEBUG
#define EIGEN_DONT_PARALLELIZE
#include <Eigen/Dense>

#include <cmath> // for std::isnormal
#include <limits>
#include <vector>

#if defined(WITH_ARRAYFIRE)
#include <cuda_runtime.h>
#if WITH_GPU_PROFILING
#include <nvtx3/nvToolsExt.h>
#endif
#define AF_DEFINE_CUDA_TYPES
#include <af/cuda.h>
#include <arrayfire.h>

#include "lp_distance.cuh"

using af::array;
#endif

using MatrixXd = Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>;

std::vector<Metric> expand_metrics(const ManifoldGenerator& generator, int E, Distance distance,
                                   const std::vector<Metric>& metrics)
{
  // Expand the 'metrics' vector now that we know the value of E.
  std::vector<Metric> expandedMetrics;

  // For the Wasserstein distance, it's more convenient to have one 'metric' for each variable (before taking lags).
  // However, for the L^1 / L^2 distances, it's more convenient to have one 'metric' for each individual
  // point of each observations, so metrics.size() == M.E_actual().
  if (distance == Distance::Wasserstein) {
    // Add a metric for the main variable and for the dt variable.
    // These are always treated as a continuous values (though perhaps in the future this will change).
    expandedMetrics.push_back(Metric::Diff);
    if (generator.E_dt(E) > 0) {
      expandedMetrics.push_back(Metric::Diff);
    }

    // Add in the metrics for the 'extra' variables as they were supplied to us.
    for (int k = 0; k < generator.numExtras(); k++) {
      expandedMetrics.push_back(metrics[k]);
    }
  } else {
    // Add metrics for the main variable and the dt variable and their lags.
    // These are always treated as a continuous values (though perhaps in the future this will change).
    for (int lagNum = 0; lagNum < E + generator.E_dt(E); lagNum++) {
      expandedMetrics.push_back(Metric::Diff);
    }

    // The user specified how to treat the extra variables.
    for (int k = 0; k < generator.numExtras(); k++) {
      int numLags = (k < generator.numExtrasLagged()) ? E : 1;
      for (int lagNum = 0; lagNum < numLags; lagNum++) {
        expandedMetrics.push_back(metrics[k]);
      }
    }
  }

  return expandedMetrics;
}

DistanceIndexPairs lazy_lp_distances(int Mp_i, const Options& opts, const Manifold& M, const Manifold& Mp)
{
  std::vector<int> inds;
  std::vector<double> dists;

  inds.reserve(M.numPoints());
  dists.reserve(M.numPoints());

  // We'll store the points we are comparing in the following two arrays.
  auto x = std::unique_ptr<double[]>(new double[M.E_actual()], std::default_delete<double[]>());
  auto y = std::unique_ptr<double[]>(new double[M.E_actual()], std::default_delete<double[]>());

  Mp.lazy_fill_in_point(Mp_i, y.get());

  const bool skipOtherPanels = opts.panelMode && (opts.idw < 0);

  // Compare every observation in the M manifold to the
  // Mp_i'th observation in the Mp manifold.
  for (int i = 0; i < M.numPoints(); i++) {

    if (skipOtherPanels && (M.panel(i) != Mp.panel(Mp_i))) {
      continue;
    }

    // Calculate the distance between M[i] and Mp[Mp_i]
    double dist_i = 0.0;

    M.lazy_fill_in_point(i, x.get());

    // If we have panel data and the M[i] / Mp[Mp_j] observations come from different panels
    // then add the user-supplied penalty/distance for the mismatch.
    if (opts.panelMode && opts.idw > 0) {
      dist_i += opts.idw * (M.panel(i) != Mp.panel(Mp_i));
    }

    for (int j = 0; j < M.E_actual(); j++) {
      // Get the sub-distance between M[i,j] and Mp[Mp_i, j]
      double dist_ij;

      // If either of these values is missing, the distance from
      // M[i,j] to Mp[Mp_i, j] is opts.missingdistance.
      // However, if the user doesn't specify this, then the entire
      // M[i] to Mp[Mp_i] distance is set as missing.
      if ((x[j] == MISSING_D) || (y[j] == MISSING_D)) {
        if (opts.missingdistance == 0) {
          dist_i = MISSING_D;
          break;
        } else {
          dist_ij = opts.missingdistance;
        }
      } else { // Neither M[i,j] nor Mp[Mp_i, j] is missing.
        // How do we compare them? Do we treat them like continuous values and subtract them,
        // or treat them like unordered categorical variables and just check if they're the same?
        if (opts.metrics[j] == Metric::Diff) {
          dist_ij = x[j] - y[j];
        } else { // Metric::CheckSame
          dist_ij = (x[j] != y[j]);
        }
      }

      if (opts.distance == Distance::MeanAbsoluteError) {
        dist_i += abs(dist_ij) / M.E_actual();
      } else { // Distance::Euclidean
        dist_i += dist_ij * dist_ij;
      }
    }

    if (dist_i != 0 && dist_i != MISSING_D) {
      if (opts.distance == Distance::MeanAbsoluteError) {
        dists.push_back(dist_i);
      } else { // Distance::Euclidean
        dists.push_back(sqrt(dist_i));
      }
      inds.push_back(i);
    }
  }

  return { inds, dists };
}

DistanceIndexPairs eager_lp_distances(int Mp_i, const Options& opts, const Manifold& M, const Manifold& Mp)
{
  std::vector<int> inds;
  std::vector<double> dists;

  inds.reserve(M.numPoints());
  dists.reserve(M.numPoints());

  const bool skipOtherPanels = opts.panelMode && (opts.idw < 0);

  // Compare every observation in the M manifold to the
  // Mp_i'th observation in the Mp manifold.
  for (int i = 0; i < M.numPoints(); i++) {

    if (skipOtherPanels && (M.panel(i) != Mp.panel(Mp_i))) {
      continue;
    }

    // Calculate the distance between M[i] and Mp[Mp_i]
    double dist_i = 0.0;

    // If we have panel data and the M[i] / Mp[Mp_j] observations come from different panels
    // then add the user-supplied penalty/distance for the mismatch.
    if (opts.panelMode && opts.idw > 0) {
      dist_i += opts.idw * (M.panel(i) != Mp.panel(Mp_i));
    }

    for (int j = 0; j < M.E_actual(); j++) {
      // Get the sub-distance between M[i,j] and Mp[Mp_i, j]
      double dist_ij;

      // If either of these values is missing, the distance from
      // M[i,j] to Mp[Mp_i, j] is opts.missingdistance.
      // However, if the user doesn't specify this, then the entire
      // M[i] to Mp[Mp_i] distance is set as missing.
      if ((M(i, j) == MISSING_D) || (Mp(Mp_i, j) == MISSING_D)) {
        if (opts.missingdistance == 0) {
          dist_i = MISSING_D;
          break;
        } else {
          dist_ij = opts.missingdistance;
        }
      } else { // Neither M[i,j] nor Mp[Mp_i, j] is missing.
        // How do we compare them? Do we treat them like continuous values and subtract them,
        // or treat them like unordered categorical variables and just check if they're the same?
        if (opts.metrics[j] == Metric::Diff) {
          dist_ij = M(i, j) - Mp(Mp_i, j);
        } else { // Metric::CheckSame
          dist_ij = (M(i, j) != Mp(Mp_i, j));
        }
      }

      if (opts.distance == Distance::MeanAbsoluteError) {
        dist_i += abs(dist_ij) / M.E_actual();
      } else { // Distance::Euclidean
        dist_i += dist_ij * dist_ij;
      }
    }

    if (dist_i != 0 && dist_i != MISSING_D) {
      if (opts.distance == Distance::MeanAbsoluteError) {
        dists.push_back(dist_i);
      } else { // Distance::Euclidean
        dists.push_back(sqrt(dist_i));
      }
      inds.push_back(i);
    }
  }

  return { inds, dists };
}

// This function compares the M(i,.) multivariate time series to the Mp(j,.) multivariate time series.
// The M(i,.) observation has data for E consecutive time points (e.g. time(i), time(i+1), ..., time(i+E-1)) and
// the Mp(j,.) observation corresponds to E consecutive time points (e.g. time(j), time(j+1), ..., time(j+E-1)).
// At each time instant we observe n >= 1 pieces of data.
// These may either be continuous data or unordered categorical data.
//
// The Wasserstein distance (using the 'curve-matching' strategy) is equivalent to the (minimum) cost of turning
// the first time series into the second time series. In a simple example, say E = 2 and n = 1, and
//         M(i,.) = [ 1, 2 ] and Mp(j,.) = [ 2, 2 ].
// To turn M(i,.) into Mp(j,.) the first element needs to be increased by 1, so the overall cost is
//         Wasserstein( M(i,.), Mp(j,.) ) = 1.
// The distance can also reorder the points, so for example say
//         M(i,.) = [ 1, 100 ] and Mp(j,.) = [ 100, 1 ].
// If we just change the 1 to 100 and the 100 to 1 then the cost of each is 99 + 99 = 198.
// However, Wasserstein can instead reorder these points at a cost of
//         Wasserstein( M(i,.), Mp(j,.) ) = 2 * gamma * (time(1)-time(2))
// so if the observations occur on a regular grid so time(i) = i then the distance will just be 2 * gamma.
//
// The return value of this function is a matrix which shows the pairwise costs associated to each
// potential Wasserstein solution. E.g. the (n,m) element of the returned matrix shows the cost
// of turning the individual point M(i, n) into Mp(j, m).
//
// When there are missing values in one or other observation, we can either ignore this time period
// and compute the Wasserstein for the mismatched regime where M(i,.) is of size len_i and Mp(j,.) is
// of size len_j, where len_i != len_j is possible. Alternatively, we can fill in the affected elements
// of the cost matrix with some user-supplied 'missingDistance' value and then len_i == len_j is upheld.
std::unique_ptr<double[]> wasserstein_cost_matrix(const Manifold& M, const Manifold& Mp, int i, int j,
                                                  const Options& opts, int& len_i, int& len_j)
{
  // The M(i,.) observation will be stored as one flat vector of length M.E_actual():
  // - the first M.E() observations will the lagged version of the main time series
  // - the next M.E() observations will be the lagged 'dt' time series (if it is included, i.e., if M.E_dt() > 0)
  // - the next n * M.E() observations will be the n lagged extra variables,
  //   so in total that is M.E_lagged_extras() = n * M.E() observations
  // - the remaining M.E_actual() - M.E() - M.E_dt() - M.E_lagged_extras() are the unlagged extras and the distance
  //   between those two vectors forms a kind of minimum distance which is added to the time-series curve matching
  //   Wasserstein distance.

  bool skipMissing = (opts.missingdistance == 0);

  // We'll store the points we are comparing in the following two arrays.
  auto x = std::unique_ptr<double[]>(new double[M.E_actual()], std::default_delete<double[]>());
  auto y = std::unique_ptr<double[]>(new double[M.E_actual()], std::default_delete<double[]>());

  if (opts.lowMemoryMode) {
    M.lazy_fill_in_point(i, x.get());
    Mp.lazy_fill_in_point(j, y.get());
  } else {
    M.eager_fill_in_point(i, x.get());
    Mp.eager_fill_in_point(j, y.get());
  }

  int numLaggedExtras = M.E_lagged_extras() / M.E();

  auto M_i = Eigen::Map<MatrixXd>(x.get(), 1 + (M.E_dt() > 0) + numLaggedExtras, M.E());
  auto Mp_j = Eigen::Map<MatrixXd>(y.get(), 1 + (M.E_dt() > 0) + numLaggedExtras, M.E());

  auto M_i_missing = (M_i.array() == M.missing()).colwise().any();
  auto Mp_j_missing = (Mp_j.array() == Mp.missing()).colwise().any();

  if (skipMissing) {
    // N.B. Can't .sum() a vector of bools to count them in Eigen.
    len_i = M.E() - M_i_missing.count();
    len_j = Mp.E() - Mp_j_missing.count();
  } else {
    len_i = M.E();
    len_j = Mp.E();
  }

  double gamma = 1.0;
  if (M.E_dt() > 0) {
    // Imagine the M_i time series as a plot, and calculate the
    // aspect ratio of this plot, so we can rescale the time variable
    // to get the user-supplied aspect ratio.
    double minData = std::numeric_limits<double>::max();
    double maxData = std::numeric_limits<double>::min();
    double maxTime = 0.0;
    for (int t = 0; t < M_i.cols(); t++) {
      if (M_i(0, t) != MISSING_D) {
        if (M_i(0, t) < minData) {
          minData = M_i(0, t);
        }
        if (M_i(0, t) > maxData) {
          maxData = M_i(0, t);
        }
      }
      if (M_i(1, t) != MISSING_D && M_i(1, t) > maxTime) {
        maxTime = M_i(1, t);
      }
    }

    double epsilon = 1e-6; // Some small number in case the following ratio gets wildly large/small
    gamma = opts.aspectRatio * (maxData - minData + epsilon) / (maxTime + epsilon);
  }

  int timeSeriesDim = M_i.rows();

  double unlaggedDist = 0.0;
  int numUnlaggedExtras = M.E_extras() - M.E_lagged_extras();
  for (int e = 0; e < numUnlaggedExtras; e++) {
    double x_e = x[M_i.size() + e];
    double y_e = y[Mp_j.size() + e];

    bool eitherMissing = (x_e == M.missing()) || (y_e == M.missing());

    if (eitherMissing) {
      unlaggedDist += opts.missingdistance;
    } else {
      if (opts.metrics[timeSeriesDim + e] == Metric::Diff) {
        unlaggedDist += abs(x_e - y_e);
      } else {
        unlaggedDist += (x_e != y_e);
      }
    }
  }

  // If we have panel data and the M[i] / Mp[j] observations come from different panels
  // then add the user-supplied penalty/distance for the mismatch.
  if (opts.panelMode && opts.idw > 0) {
    unlaggedDist += opts.idw * (M.panel(i) != Mp.panel(j));
  }

  auto flatCostMatrix = std::make_unique<double[]>(len_i * len_j);
  std::fill_n(flatCostMatrix.get(), len_i * len_j, unlaggedDist);
  Eigen::Map<MatrixXd> costMatrix(flatCostMatrix.get(), len_i, len_j);

  for (int k = 0; k < timeSeriesDim; k++) {
    int n = 0;
    for (int nn = 0; nn < M_i.cols(); nn++) {
      if (skipMissing && M_i_missing[nn]) {
        continue;
      }

      int m = 0;

      for (int mm = 0; mm < Mp_j.cols(); mm++) {
        if (skipMissing && Mp_j_missing[mm]) {
          continue;
        }
        double dist;
        bool eitherMissing = M_i_missing[nn] || Mp_j_missing[mm];

        if (eitherMissing) {
          dist = opts.missingdistance;
        } else {
          if (opts.metrics[k] == Metric::Diff) {
            dist = abs(M_i(k, nn) - Mp_j(k, mm));
          } else {
            dist = M_i(k, nn) != Mp_j(k, mm);
          }
        }

        // For the time data, we add in the 'gamma' scaling factor calculated earlier
        if ((M.E_dt() > 0) && (k == 1)) {
          dist *= gamma;
        }

        costMatrix(n, m) += dist;

        m += 1;
      }

      n += 1;
    }
  }

  return flatCostMatrix;
}

// TODO: Subtract the D(x,x) and D(y,y) parts from this.
double approx_wasserstein(double* C, int len_i, int len_j, double eps, double stopErr)
{
  Eigen::Map<Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>> costMatrix(C, len_i, len_j);

  double r = 1.0 / len_i;
  double c = 1.0 / len_j;

  Eigen::MatrixXd K = Eigen::exp(-costMatrix.array() / eps);
  Eigen::MatrixXd Kp = len_i * K.array();

  Eigen::VectorXd u = Eigen::VectorXd::Ones(len_i) / len_i;
  Eigen::VectorXd v = Eigen::VectorXd::Ones(len_j) / len_j;

  int maxIter = 10000;
  for (int iter = 0; iter < maxIter; iter++) {

    v = c / (K.transpose() * u).array();
    u = 1.0 / (Kp * v).array();

    if (iter % 10 == 0) {
      // Compute right marginal (diag(u) K diag(v))^T1
      Eigen::VectorXd tempColSums = (u.asDiagonal() * K * v.asDiagonal()).colwise().sum();
      double LInfErr = (tempColSums.array() - c).abs().maxCoeff();
      if (LInfErr < stopErr) {
        break;
      }
    }
  }

  Eigen::MatrixXd transportPlan = u.asDiagonal() * K * v.asDiagonal();
  double dist = (transportPlan.array() * costMatrix.array()).sum();
  return dist;
}

double wasserstein(double* C, int len_i, int len_j)
{
  // Create vectors which are just 1/len_i and 1/len_j of length len_i and len_j.
  auto w_1 = std::make_unique<double[]>(len_i);
  std::fill_n(w_1.get(), len_i, 1.0 / len_i);
  auto w_2 = std::make_unique<double[]>(len_j);
  std::fill_n(w_2.get(), len_j, 1.0 / len_j);

  int maxIter = 10000;
  double cost;
  EMD_wrap(len_i, len_j, w_1.get(), w_2.get(), C, &cost, maxIter);
  return cost;
}

DistanceIndexPairs wasserstein_distances(int Mp_i, const Options& opts, const Manifold& M, const Manifold& Mp)
{
  std::vector<int> inds;
  std::vector<double> dists;

  inds.reserve(M.numPoints());
  dists.reserve(M.numPoints());

  const bool skipOtherPanels = opts.panelMode && (opts.idw < 0);

  // Compare every observation in the M manifold to the
  // Mp_i'th observation in the Mp manifold.
  for (int i = 0; i < M.numPoints(); i++) {

    if (skipOtherPanels && (M.panel(i) != Mp.panel(Mp_i))) {
      continue;
    }

    int len_i, len_j;
    auto C = wasserstein_cost_matrix(M, Mp, i, Mp_i, opts, len_i, len_j);

    if (len_i > 0 && len_j > 0) {
      double dist_i = wasserstein(C.get(), len_i, len_j);

      // Alternatively, the approximate version based on Sinkhorn's algorithm can be called with something like:
      // double dist_i = approx_wasserstein(C.get(), len_i, len_j, 0.1, 0.1)
      // In that case, the "std::isnormal" is really needed on the next line, as some
      // instability gives us some 'nan' distances using that method.

      if (dist_i != 0 && std::isnormal(dist_i)) {
        dists.push_back(dist_i);
        inds.push_back(i);
      }
    }
  }

  return { inds, dists };
}

/////////////////////////////////////////////////////////////// ArrayFire PORTED versions BEGIN HERE

#if defined(WITH_ARRAYFIRE)
DistanceIndexPairsOnGPU afLPDistances(const int numPredictions, const Options& opts, const ManifoldOnGPU& M,
                                      const ManifoldOnGPU& Mp, const af::array& metricOpts)
{
  constexpr bool useCustomKernel = true;

#if WITH_GPU_PROFILING
  auto range = nvtxRangeStartA(__FUNCTION__);
#endif

  const af_dtype cType = M.mdata.type();

  if (useCustomKernel) {
    af::array valids(M.numPoints, numPredictions, b8);
    af::array dists(M.numPoints, numPredictions, cType);
    if (cType == f64) {
      cuLPDistances(valids.device<char>(), dists.device<double>(), numPredictions,
                    opts.distance == Distance::MeanAbsoluteError, opts.panelMode, opts.idw, opts.missingdistance,
                    M.E_actual, M.numPoints, M.mdata.device<double>(), M.panel.device<int>(), Mp.mdata.device<double>(),
                    Mp.panel.device<int>(), metricOpts.device<char>(), afcu::getStream(0));
    } else if (cType == f32) {
      cuLPDistances(valids.device<char>(), dists.device<float>(), numPredictions,
                    opts.distance == Distance::MeanAbsoluteError, opts.panelMode, opts.idw, opts.missingdistance,
                    M.E_actual, M.numPoints, M.mdata.device<float>(), M.panel.device<int>(), Mp.mdata.device<float>(),
                    Mp.panel.device<int>(), metricOpts.device<char>(), afcu::getStream(0));
    }
    valids.unlock();
    dists.unlock();
    M.mdata.unlock();
    M.panel.unlock();
    Mp.mdata.unlock();
    Mp.panel.unlock();
    metricOpts.unlock();

#if WITH_GPU_PROFILING
    nvtxRangeEnd(range);
#endif
    return { valids, dists };
  } else {
    using af::array;
    using af::moddims;
    using af::select;
    using af::seq;
    using af::span;
    using af::sum;
    using af::tile;

    // Mp_i goes from 0 to numPredictions - 1
    // All coloumns of Manifold M are considered valid for batch operation

    const bool imdoZero = opts.missingdistance == 0;
    const int numLibraryPoints = M.numPoints;
    const int eacts = M.E_actual;

    array anyMCols, distsMat;
    {
      array predsM = tile(M.mdata, 1, 1, numPredictions);
      array predsMp = tile(moddims(Mp.mdata(span, seq(numPredictions)), eacts, 1, numPredictions), 1, numLibraryPoints);
      array diffMMp = predsM - predsMp;
      array compMMp = (predsM != predsMp).as(cType);
      array distMMp = select(tile(metricOpts, 1, numLibraryPoints, numPredictions), diffMMp, compMMp);
      array missing = predsM == MISSING_D || predsMp == MISSING_D;

      distsMat = (imdoZero ? distMMp : select(missing, opts.missingdistance, distMMp));
      anyMCols = anyTrue(missing, 0);

      if (opts.distance == Distance::MeanAbsoluteError) {
        distsMat = af::abs(distsMat) / double(eacts);
      } else {
        distsMat = distsMat * distsMat;
      }
    }
    if (opts.panelMode && opts.idw > 0) {
      array npPanelMp = tile(Mp.panel(seq(numPredictions)).T(), numLibraryPoints);
      array npPanelM = tile(M.panel, 1, numPredictions);
      array penalty = (opts.idw * (npPanelM != npPanelMp));
      array penalties = tile(moddims(penalty, 1, numLibraryPoints, numPredictions), eacts);

      distsMat += penalties;
    }
    array accDists = sum(distsMat, 0);
    array distances = select(anyMCols * imdoZero, double(MISSING_D), accDists);
    array valids = (distances != 0.0 && distances != double(MISSING_D));
    array dists = (opts.distance == Distance::MeanAbsoluteError ? distances : af::sqrt(distances));

    valids = moddims(valids, numLibraryPoints, numPredictions);
    dists = moddims(dists, numLibraryPoints, numPredictions);

#if WITH_GPU_PROFILING
    nvtxRangeEnd(range);
#endif
    return { valids, dists };
  }
}

array afWassersteinCostMatrix(const bool& skipMissing, const Options& opts, const array& metricOpts,
                              const ManifoldOnGPU& M, const array& M_i, const array& M_i_missing, const array& x,
                              const int& len_i, const array& Mp_j, const array& Mp_j_missing, const array& y,
                              const int& len_j, const bool arePanelIdsSame)
{
  using af::array;
  using af::constant;
  using af::dim4;
  using af::moddims;
  using af::seq;
  using af::span;
  using af::sum;
  using af::tile;
  using af::where;

  const af_dtype cType = M.mdata.type();

  double gamma = 1.0;
  if (M.E_dt > 0) {
    array firstColumn = M_i(span, 0);
    array nonMissings = firstColumn != MISSING_D;
    array validIndexs = where(nonMissings);
    array validValues = firstColumn(validIndexs);
    double minData = af::min<double>(validValues);
    double maxData = af::max<double>(validValues);
    double maxTime = af::max<double>(M_i(span, 1));

    // Some small number in case the following ratio gets wildly large/small
    constexpr double epsilon = 1e-6;

    gamma = opts.aspectRatio * (maxData - minData + epsilon) / (maxTime + epsilon);
  }

  const int timeSeriesDim = M_i.dims(1);
  double unlaggedDist = 0.0;
  {
    const int numUnlaggedExtras = M.E_extras - M.E_lagged_extras;

    array eitherMissing = (x == M.missing || y == M.missing);

    array cond = metricOpts(seq(timeSeriesDim, numUnlaggedExtras + timeSeriesDim - 1));
    array ulDists = (cond * af::abs(x - y) + (1 - cond) * (x != y).as(cType));
    array dists = (eitherMissing * opts.missingdistance + (1 - eitherMissing) * ulDists);
    unlaggedDist = sum<double>(dists);
  }

  if (opts.panelMode && opts.idw > 0) {
    unlaggedDist += opts.idw * arePanelIdsSame;
  }

  const seq timeSeries(timeSeriesDim);
  array costMatrix = af::constant(unlaggedDist, len_j, len_i, cType);

  array cmIsMetricDiff = metricOpts(timeSeries);
  cmIsMetricDiff = tile(moddims(cmIsMetricDiff, 1, 1, timeSeriesDim), len_j, len_i);

  if (skipMissing) {
    // In this case: Unless both entries are available, no need to process anything else
    array idxM_i_missing = where(!M_i_missing);
    array idxMp_j_missing = where(!Mp_j_missing);

    array Mp_j_k = moddims(Mp_j(idxMp_j_missing, timeSeries), len_j, 1, timeSeriesDim);
    array M_i_k = moddims(M_i(idxM_i_missing, timeSeries), 1, len_i, timeSeriesDim);
    array cmMp_j = tile(Mp_j_k, 1, len_i);
    array cmM_i = tile(M_i_k, len_j);
    array cmDiff = select(cmIsMetricDiff, af::abs(cmMp_j - cmM_i), (cmM_i != cmMp_j).as(cType));

    if (M.E_dt > 0) {
      // For time series k = 1, scale by gamma
      cmDiff(span, span, 1) *= gamma;
    }
    costMatrix += sum(cmDiff, 2); // Add results to unlaggedDist
  } else {
    array Mp_j_missingT = tile(Mp_j_missing, 1, M_i_missing.dims(0)); // cost matrix shape
    array M_i_missingT = tile(M_i_missing.T(), Mp_j_missing.dims(0)); // cost matrix shape
    array eitherMissing = (M_i_missingT || Mp_j_missingT);            // one of the entries missing
    array eitherMissingT = tile(eitherMissing, 1, 1, timeSeriesDim);  // [len_j len_i timeSeriesDim 1]

    array Mp_j_k = moddims(Mp_j(span, timeSeries), len_j, 1, timeSeriesDim);
    array M_i_k = moddims(M_i(span, timeSeries), 1, len_i, timeSeriesDim);
    array cmMp_j = tile(Mp_j_k, 1, len_i);
    array cmM_i = tile(M_i_k, len_j);
    array cmDiff = select(cmIsMetricDiff, af::abs(cmMp_j - cmM_i), (cmM_i != cmMp_j).as(cType));
    array cmDist = select(eitherMissingT, opts.missingdistance, cmDiff);

    if (M.E_dt > 0) {
      // For time series k = 1, scale by gamma
      cmDist(span, span, 1) *= gamma;
    }
    costMatrix += sum(cmDist, 2); // Add results to unlaggedDist
  }

  return costMatrix;
}

DistanceIndexPairs afWassersteinDistances(int Mp_i, const Options& opts, const Manifold& hostM, const Manifold& hostMp,
                                          const ManifoldOnGPU& M, const ManifoldOnGPU& Mp,
                                          const std::vector<int>& inpInds, const af::array& metricOpts)
{
  using af::anyTrue;
  using af::seq;

  const bool skipMissing = (opts.missingdistance == 0);

  // Precompute values that are iteration invariant
  const seq mpjRange0(Mp.E_x);
  const seq mpjRange1(Mp_i, Mp_i + 1 + (Mp.E_dt > 0) + Mp.E_lagged_extras / Mp.E_x);

  const array Mp_j = Mp.mdata(mpjRange0, mpjRange1);

  const array Mp_j_missing = anyTrue(Mp_j == Mp.missing, 1);

  const int numUnlaggedExtrasEnd = M.E_extras - M.E_lagged_extras;

  const seq xseq(M.E_x + M.E_dt + M.E_lagged_extras, M.E_x + M.E_dt + M.E_lagged_extras + numUnlaggedExtrasEnd - 1);
  const seq yseq(Mp.E_x + Mp.E_dt + Mp.E_lagged_extras,
                 Mp.E_x + Mp.E_dt + Mp.E_lagged_extras + numUnlaggedExtrasEnd - 1);

  const array y = Mp.mdata(yseq, Mp_i);
  const int len_j = (skipMissing ? Mp.E_x - af::sum<int>(Mp_j_missing) : Mp.E_x);

  // Since both len_i and len_j should be greater than zero
  // to collect valid indices and respective distances, just return empty handed
  if (len_j <= 0) {
    return {};
  }

  // Return Items
  std::vector<int> inds;
  std::vector<double> dists;

  // Compare every observation in the M manifold to the Mp_i'th observation in the Mp manifold.
  for (int i : inpInds) {
    const seq miRange0(M.E_x);
    const seq miRange1(i, i + 1 + (M.E_dt > 0) + M.E_lagged_extras / M.E_x);

    array M_i = M.mdata(miRange0, miRange1);

    array M_i_missing = anyTrue(M_i == M.missing, 1);

    array x = M.mdata(xseq, i);

    const int len_i = (skipMissing ? M.E_x - af::sum<int>(M_i_missing) : M.E_x);

    if (len_i > 0) { // Short-check for len_j already passed
      // TODO I think afWassersteinCostMatrix can be further vectorized to run for all i's
      array cm = afWassersteinCostMatrix(skipMissing, opts, metricOpts, M, M_i, M_i_missing, x, len_i, Mp_j,
                                         Mp_j_missing, y, len_j, (hostM.panel(i) != hostMp.panel(Mp_i)));
      std::vector<double> C(cm.elements());
      cm.host(C.data());
      double dist_i = wasserstein(C.data(), len_i, len_j);

      // Alternative: approximate version based on Sinkhorn's algorithm
      // double dist_i = approx_wasserstein(C.get(), len_i, len_j, 0.1, 0.1)
      // In that case, the "std::isnormal" is really needed on the next line, as some
      // instability gives us some 'nan' distances using that method.
      if (dist_i != 0 && std::isnormal(dist_i)) {
        dists.push_back(dist_i);
        inds.push_back(i);
      }
    }
  }

  return { inds, dists };
}
#endif