#include <rct_optimizations/validation/camera_intrinsic_calibration_validation.h>
#include <boost/accumulators/accumulators.hpp>
#include <boost/accumulators/statistics.hpp>

namespace rct_optimizations
{
VirtualCorrespondenceResult measureVirtualTargetDiff(const Correspondence2D3D::Set &correspondences,
                                                     const CameraIntrinsics &intr,
                                                     const Eigen::Isometry3d &camera_to_target_guess,
                                                     const double pnp_sq_error_threshold)
{
  // Create a lambda for doing the PnP optimization
  auto solve_pnp = [&intr, &camera_to_target_guess, &pnp_sq_error_threshold](
                     const Correspondence2D3D::Set &corr) -> Eigen::Isometry3d {
    // Create the first virtual target PnP problem
    PnPProblem problem;
    problem.intr = intr;
    problem.correspondences = corr;
    problem.camera_to_target_guess = camera_to_target_guess;

    PnPResult result = optimize(problem);
    if (!result.converged || result.final_cost_per_obs > pnp_sq_error_threshold)
    {
      std::stringstream ss;
      ss << "PnP optimization " << (result.converged ? "converged" : "did not converge")
         << " with residual error of " << result.final_cost_per_obs << " ("
         << pnp_sq_error_threshold << " max)";
      throw std::runtime_error(ss.str());
    }

    return result.camera_to_target;
  };

  // Calculate the size of half of the correspondence set
  std::size_t half_size = correspondences.size() / 2;

  // Create two half sets of correspondences
  Correspondence2D3D::Set set_1(correspondences.begin(), correspondences.begin() + half_size);
  Correspondence2D3D::Set set_2(correspondences.begin() + half_size, correspondences.end());

  /* Get the camera to target transformation for each half set
   * Note: these transforms are from the camera to the origin of each virtual target.
   *   The origin of the second virtual target is still the same as the first virtual target,
   *   so the two transforms should be the same, given perfect camera intrinsics */
  Eigen::Isometry3d camera_to_target_1 = solve_pnp(set_1);
  Eigen::Isometry3d camera_to_target_2 = solve_pnp(set_2);

  VirtualCorrespondenceResult res;

  Eigen::Quaterniond q1(camera_to_target_1.rotation());
  Eigen::Quaterniond q2(camera_to_target_2.rotation());
  res.angular_error = q1.angularDistance(q2);
  res.positional_error = (camera_to_target_1.inverse() * camera_to_target_2).translation().norm();

  // Return the transformation from target 1 to target 2
  res.t1_to_t2 = camera_to_target_1.inverse() * camera_to_target_2;
  return res;
}

IntrinsicCalibrationAccuracyResult measureIntrinsicCalibrationAccuracy(
  const Observation2D3D::Set &observations,
  const CameraIntrinsics &intr,
  const Eigen::Isometry3d &camera_mount_to_camera,
  const Eigen::Isometry3d &target_mount_to_target,
  const Eigen::Isometry3d &camera_base_to_target_base,
  const double pnp_sq_error_threshold)
{
  // Check that the observations are all the same size
  // Assuming that each observation's correspondences are ordered the same
  for (std::size_t i = 0; i < observations.size() - 1; ++i)
  {
    const Observation2D3D &obs_1 = observations[i];
    const Observation2D3D &obs_2 = observations[i + 1];

    // Check that the correspondences in all observations are the same size
    if (obs_1.correspondence_set.size() != obs_2.correspondence_set.size())
    {
      std::stringstream ss;
      ss << "Correspondence sizes do not match between observations " << i << " and " << i + 1;
      throw OptimizationException(ss.str());
    }
  }

  // Create accumulators for mean and variance
  namespace ba = boost::accumulators;
  ba::accumulator_set<double, ba::features<ba::stats<ba::tag::mean>, ba::stats<ba::tag::variance>>>
    pos_acc;
  ba::accumulator_set<double, ba::features<ba::stats<ba::tag::mean>, ba::stats<ba::tag::variance>>>
    ang_acc;

  // Accumulate the position vector of the transformation
  for (const auto &obs : observations)
  {
    Eigen::Isometry3d camera_base_to_camera = obs.to_camera_mount * camera_mount_to_camera;
    Eigen::Isometry3d camera_base_to_target = camera_base_to_target_base * obs.to_target_mount
                                              * target_mount_to_target;

    Eigen::Isometry3d camera_to_target = camera_base_to_camera.inverse() * camera_base_to_target;

    VirtualCorrespondenceResult res = measureVirtualTargetDiff(obs.correspondence_set,
                                                               intr,
                                                               camera_to_target,
                                                               pnp_sq_error_threshold);
    pos_acc(res.positional_error);
    ang_acc(res.angular_error);
  }

  // Calculate the mean and variance of the measurements
  // Theoretically each transform from virtual target 1 to virtual target 2 should be zero; thus the mean should be zero
  // In practice the mean represents bias from the ideal zero state and the variance is the amount of change around the mean
  IntrinsicCalibrationAccuracyResult res;
  // Positional
  res.pos_error.first = ba::mean(pos_acc);
  res.pos_error.second = std::sqrt(ba::variance(pos_acc));
  // Angular
  res.ang_error.first = ba::mean(ang_acc);
  res.ang_error.second = std::sqrt(ba::variance(ang_acc));

  return res;
}

} // namespace rct_optimizations


//==============================================================================
//         Copyright 2003 - 2011 LASMEA UMR 6602 CNRS/Univ. Clermont II
//         Copyright 2009 - 2011 LRI    UMR 8623 CNRS/Univ Paris Sud XI
//
//          Distributed under the Boost Software License, Version 1.0.
//                 See accompanying file LICENSE.txt or copy at
//                     http://www.boost.org/LICENSE_1_0.txt
//==============================================================================
#ifndef BOOST_SIMD_IEEE_FUNCTIONS_SCALAR_FAST_FREXP_HPP_INCLUDED
#define BOOST_SIMD_IEEE_FUNCTIONS_SCALAR_FAST_FREXP_HPP_INCLUDED
#include <boost/simd/ieee/functions/fast_frexp.hpp>
#include <boost/dispatch/meta/adapted_traits.hpp>
#include <boost/dispatch/meta/as_integer.hpp>
#include <boost/fusion/include/std_pair.hpp>
#include <boost/type_traits/is_same.hpp>
#include <math.h>

namespace boost { namespace simd { namespace ext
{
  /// INTERNAL ONLY - map to ::frexp
  BOOST_SIMD_FUNCTOR_IMPLEMENTATION ( boost::simd::tag::fast_frexp_, tag::cpu_
                                    , (A0)(A2)
                                    , (scalar_< double_<A0> >)
                                      (scalar_< double_<A0> >)
                                      (scalar_< int64_<A2> >)
                                    )
  {
    typedef void result_type;
    BOOST_FORCEINLINE result_type operator()(A0 const& a0,A0 & a1,A2 & a2) const
    {
      boost::simd::int32_t r = 0;
      a1 = ::frexp(a0, &r);
      a2 = r;
    }
  };

  BOOST_SIMD_FUNCTOR_IMPLEMENTATION ( boost::simd::tag::fast_frexp_, tag::cpu_
                                    , (A0)(A2)
                                    , (scalar_< single_<A0> >)
                                      (scalar_< single_<A0> >)
                                      (scalar_< int32_<A2> >)
                                    )
  {
    typedef void result_type;
    BOOST_FORCEINLINE result_type operator()(A0 const& a0,A0 & a1,A2 & a2) const
    {
      a2 = 0;
      a1 = ::frexpf(a0, &a2);
    }
  };

  BOOST_SIMD_FUNCTOR_IMPLEMENTATION_IF( boost::simd::tag::fast_frexp_, tag::cpu_
                                      , (A0)(A2)
                                      , ( boost::is_same
                                          < typename dispatch::meta::
                                                     as_integer<A0,signed>::type
                                          , A2
                                          >
                                        )
                                      , (scalar_< floating_<A0> >)
                                        (scalar_< integer_<A2> >)
                                      )
  {
    typedef A0 result_type;
    BOOST_FORCEINLINE result_type operator()(A0 const& a0,A2 & a2) const
    {
      A0 a1;
      boost::simd::fast_frexp(a0, a1, a2);
      return a1;
    }
  };

  BOOST_SIMD_FUNCTOR_IMPLEMENTATION ( boost::simd::tag::fast_frexp_, tag::cpu_
                                    , (A0)
                                    , (scalar_ < floating_<A0> > )
                                    )
  {
    typedef typename dispatch::meta::as_integer<A0,signed>::type  exponent;
    typedef std::pair<A0,exponent>                          result_type;

    BOOST_FORCEINLINE result_type operator()(A0 const& a0) const
    {
      result_type res;
      boost::simd::fast_frexp(a0, res.first, res.second);
      return res;
    }
  };
} } }

#endif


#include <vector>
#include <map>
#include <boost/pending/disjoint_sets.hpp>

using namespace std;

template <class T>
class AffinityGraphCompare{
    private:
        const T * mEdgeWeightArray;
    public:
        AffinityGraphCompare(const T * EdgeWeightArray){
            mEdgeWeightArray = EdgeWeightArray;
        }
        bool operator() (const int& ind1, const int& ind2) const {
            return (mEdgeWeightArray[ind1] > mEdgeWeightArray[ind2]);
        }
};

void connected_components_cpp(const int nVert,
               const int nEdge, const uint64_t* node1, const uint64_t* node2, const int* edgeWeight,
               uint64_t* seg){
    /* Make disjoint sets */
    vector<uint64_t> rank(nVert);
    vector<uint64_t> parent(nVert);
    boost::disjoint_sets<uint64_t*, uint64_t*> dsets(&rank[0],&parent[0]);
    for (int i=0; i<nVert; ++i)
        dsets.make_set(i);

    /* union */
    for (int i = 0; i < nEdge; ++i )
         // check bounds to make sure the nodes are valid
        if ((edgeWeight[i]!=0) && (node1[i]>=0) && (node1[i]<nVert) && (node2[i]>=0) && (node2[i]<nVert))
            dsets.union_set(node1[i],node2[i]);

    /* find */
    for (int i = 0; i < nVert; ++i)
        seg[i] = dsets.find_set(i);
}

void marker_watershed_cpp(const int nVert, const uint64_t* marker,
               const int nEdge, const uint64_t* node1, const uint64_t* node2, const float* edgeWeight,
               uint64_t* seg){

    /* Make disjoint sets */
    vector<uint64_t> rank(nVert);
    vector<uint64_t> parent(nVert);
    boost::disjoint_sets<uint64_t*, uint64_t*> dsets(&rank[0],&parent[0]);
    for (uint64_t i=0; i<nVert; ++i)
        dsets.make_set(i);

    /* initialize output array and find representatives of each class */
    std::map<uint64_t,uint64_t> components;
    for (uint64_t i=0; i<nVert; ++i){
        seg[i] = marker[i];
        if (seg[i] > 0)
            components[seg[i]] = i;
    }

    // merge vertices labeled with the same marker
    for (uint64_t i=0; i<nVert; ++i)
        if (seg[i] > 0)
            dsets.union_set(components[seg[i]],i);

    /* Sort all the edges in decreasing order of weight */
    std::vector<int> pqueue( nEdge );
    int j = 0;
    for (int i = 0; i < nEdge; ++i)
        if ((edgeWeight[i]!=0) &&
            (node1[i]>=0) && (node1[i]<nVert) &&
            (node2[i]>=0) && (node2[i]<nVert) &&
            (marker[node1[i]]>=0) && (marker[node2[i]]>=0))
                pqueue[ j++ ] = i;
    unsigned long nValidEdge = j;
    pqueue.resize(nValidEdge);
    sort( pqueue.begin(), pqueue.end(), AffinityGraphCompare<float>( edgeWeight ) );

    /* Start MST */
	int e;
    int set1, set2, label_of_set1, label_of_set2;
    for (unsigned int i = 0; i < pqueue.size(); ++i ) {
		e = pqueue[i];
        set1=dsets.find_set(node1[e]);
        set2=dsets.find_set(node2[e]);
        label_of_set1 = seg[set1];
        label_of_set2 = seg[set2];

        if ((set1!=set2) &&
            ( ((label_of_set1==0) && (marker[set1]==0)) ||
             ((label_of_set2==0) && (marker[set1]==0))) ){

            dsets.link(set1, set2);
            // either label_of_set1 is 0 or label_of_set2 is 0.
            seg[dsets.find_set(set1)] = std::max(label_of_set1,label_of_set2);

        }

    }

    // write out the final coloring
    for (int i=0; i<nVert; i++)
        seg[i] = seg[dsets.find_set(i)];

}


/*
 *            Copyright 2009-2020 The VOTCA Development Team
 *                       (http://www.votca.org)
 *
 *      Licensed under the Apache License, Version 2.0 (the "License")
 *
 * You may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *              http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

// Third party includes
#include <boost/format.hpp>

// VOTCA includes
#include <votca/tools/tokenizer.h>

// Local VOTCA includes
#include "votca/xtp/vxc_functionals.h"
#include "votca/xtp/vxc_grid.h"
#include "votca/xtp/vxc_potential.h"

namespace votca {
namespace xtp {
template <class Grid>
Vxc_Potential<Grid>::~Vxc_Potential() {
  if (_setXC) {
    xc_func_end(&xfunc);
    if (_use_separate) {
      xc_func_end(&cfunc);
    }
  }
}
template <class Grid>
double Vxc_Potential<Grid>::getExactExchange(const std::string& functional) {

  double exactexchange = 0.0;
  Vxc_Functionals map;
  tools::Tokenizer tok(functional, " ");
  std::vector<std::string> functional_names = tok.ToVector();

  if (functional_names.size() > 2) {
    throw std::runtime_error("Too many functional names");
  } else if (functional_names.size() < 1) {
    throw std::runtime_error("Specify at least one functional");
  }

  for (const std::string& functional_name : functional_names) {

    int func_id = map.getID(functional_name);
    if (func_id < 0) {
      exactexchange = 0.0;
      break;
    }
    xc_func_type func;
    if (xc_func_init(&func, func_id, XC_UNPOLARIZED) != 0) {
      throw std::runtime_error(
          (boost::format("Functional %s not found\n") % functional_name).str());
    }
    if (exactexchange > 0 && func.cam_alpha > 0) {
      throw std::runtime_error(
          "You have specified two functionals with exact exchange");
    }
    exactexchange += func.cam_alpha;
    xc_func_end(&func);
  }

  return exactexchange;
}
template <class Grid>
void Vxc_Potential<Grid>::setXCfunctional(const std::string& functional) {

  Vxc_Functionals map;
  std::vector<std::string> strs;
  tools::Tokenizer tok(functional, " ,\n\t");
  tok.ToVector(strs);
  xfunc_id = 0;
  _use_separate = false;
  cfunc_id = 0;
  if (strs.size() == 1) {
    xfunc_id = map.getID(strs[0]);
  } else if (strs.size() == 2) {
    xfunc_id = map.getID(strs[0]);
    cfunc_id = map.getID(strs[1]);
    _use_separate = true;
  } else {
    throw std::runtime_error(
        "LIBXC. Please specify one combined or an exchange and a correlation "
        "functionals");
  }

  if (xc_func_init(&xfunc, xfunc_id, XC_UNPOLARIZED) != 0) {
    throw std::runtime_error(
        (boost::format("Functional %s not found\n") % strs[0]).str());
  }
  if (xfunc.info->kind != 2 && !_use_separate) {
    throw std::runtime_error(
        "Your functional misses either correlation or exchange, please specify "
        "another functional, separated by whitespace");
  }
  if (_use_separate) {
    if (xc_func_init(&cfunc, cfunc_id, XC_UNPOLARIZED) != 0) {
      throw std::runtime_error(
          (boost::format("Functional %s not found\n") % strs[1]).str());
    }
    if ((xfunc.info->kind + cfunc.info->kind) != 1) {
      throw std::runtime_error(
          "Your functionals are not one exchange and one correlation");
    }
  }
  _setXC = true;
  return;
}
template <class Grid>
typename Vxc_Potential<Grid>::XC_entry Vxc_Potential<Grid>::EvaluateXC(
    double rho, double sigma) const {

  Vxc_Potential<Grid>::XC_entry result;
  switch (xfunc.info->family) {
    case XC_FAMILY_LDA:
      xc_lda_exc_vxc(&xfunc, 1, &rho, &result.f_xc, &result.df_drho);
      break;
    case XC_FAMILY_GGA:
    case XC_FAMILY_HYB_GGA:
      xc_gga_exc_vxc(&xfunc, 1, &rho, &sigma, &result.f_xc, &result.df_drho,
                     &result.df_dsigma);
      break;
  }
  if (_use_separate) {
    typename Vxc_Potential<Grid>::XC_entry temp;
    // via libxc correlation part only
    switch (cfunc.info->family) {
      case XC_FAMILY_LDA:
        xc_lda_exc_vxc(&cfunc, 1, &rho, &temp.f_xc, &temp.df_drho);
        break;
      case XC_FAMILY_GGA:
      case XC_FAMILY_HYB_GGA:
        xc_gga_exc_vxc(&cfunc, 1, &rho, &sigma, &temp.f_xc, &temp.df_drho,
                       &temp.df_dsigma);
        break;
    }

    result.f_xc += temp.f_xc;
    result.df_drho += temp.df_drho;
    result.df_dsigma += temp.df_dsigma;
  }

  return result;
}
template <class Grid>
Mat_p_Energy Vxc_Potential<Grid>::IntegrateVXC(
    const Eigen::MatrixXd& density_matrix) const {

  Mat_p_Energy vxc = Mat_p_Energy(density_matrix.rows(), density_matrix.cols());

#pragma omp parallel for schedule(guided) reduction(+ : vxc)
  for (Index i = 0; i < _grid.getBoxesSize(); ++i) {
    const GridBox& box = _grid[i];
    if (!box.Matrixsize()) {
      continue;
    }
    double EXC_box = 0.0;
    const Eigen::MatrixXd DMAT_here = box.ReadFromBigMatrix(density_matrix);
    const Eigen::MatrixXd DMAT_symm = DMAT_here + DMAT_here.transpose();
    double cutoff =
        1.e-40 / double(density_matrix.rows()) / double(density_matrix.rows());
    if (DMAT_here.cwiseAbs2().maxCoeff() < cutoff) {
      continue;
    }
    Eigen::MatrixXd Vxc_here =
        Eigen::MatrixXd::Zero(DMAT_here.rows(), DMAT_here.cols());
    const std::vector<Eigen::Vector3d>& points = box.getGridPoints();
    const std::vector<double>& weights = box.getGridWeights();

    // iterate over gridpoints
    for (Index p = 0; p < box.size(); p++) {
      Eigen::MatrixX3d ao_grad = Eigen::MatrixX3d::Zero(box.Matrixsize(), 3);
      Eigen::VectorXd ao = box.CalcAOValue_and_Grad(ao_grad, points[p]);
      const double rho = 0.5 * (ao.transpose() * DMAT_symm * ao).value();
      const double weight = weights[p];
      if (rho * weight < 1.e-20) {
        continue;  // skip the rest, if density is very small
      }
      const Eigen::Vector3d rho_grad = ao.transpose() * DMAT_symm * ao_grad;
      const double sigma = (rho_grad.transpose() * rho_grad).value();
      const Eigen::VectorXd grad = ao_grad * rho_grad;
      typename Vxc_Potential<Grid>::XC_entry xc = EvaluateXC(rho, sigma);
      EXC_box += weight * rho * xc.f_xc;
      auto addXC = weight * (0.5 * xc.df_drho * ao + 2.0 * xc.df_dsigma * grad);
      Vxc_here.noalias() += addXC * ao.transpose();
    }
    box.AddtoBigMatrix(vxc.matrix(), Vxc_here);
    vxc.energy() += EXC_box;
  }

  return Mat_p_Energy(vxc.energy(), vxc.matrix() + vxc.matrix().transpose());
}

template class Vxc_Potential<Vxc_Grid>;

}  // namespace xtp
}  // namespace votca


#include <boost/numeric/odeint.hpp>


/* ************************************************************************
 * Copyright 2016 Advanced Micro Devices, Inc.
 * ************************************************************************ */

#include <boost/program_options.hpp>
#include "testing_gebd2_gebrd.hpp"
#include "testing_getf2_getrf.hpp"
#include "testing_getf2_getrf_npvt.hpp"
#include "testing_geqr2_geqrf.hpp"
#include "testing_gelq2_gelqf.hpp"
#include "testing_getri.hpp"
#include "testing_getrs.hpp"
#include "testing_potf2_potrf.hpp"
#include "testing_larfg.hpp"
#include "testing_larf.hpp"
#include "testing_larft.hpp"
#include "testing_larfb.hpp"
#include "testing_lacgv.hpp"
#include "testing_laswp.hpp"
#include "testing_labrd.hpp"
#include "testing_bdsqr.hpp"
#include "testing_orgxr_ungxr.hpp"
#include "testing_ormxr_unmxr.hpp"
#include "testing_orglx_unglx.hpp"
#include "testing_ormlx_unmlx.hpp"
#include "testing_orgbr_ungbr.hpp"
#include "testing_ormbr_unmbr.hpp"

namespace po = boost::program_options;

int main(int argc, char *argv[])
try
{
    rocblas_initialize();

    Arguments argus;

    //disable unit_check in client benchmark, it is only
    // used in gtest unit test
    argus.unit_check = 0;

    // enable timing check,otherwise no performance data collected
    argus.timing = 1;

    std::string function;
    char precision;
    rocblas_int device_id;

    // take arguments and set default values
    // (TODO) IMPROVE WORDING/INFORMATION. CHANGE ARGUMENT NAMES FOR
    // MORE RELATED NAMES (THESE ARE BLAS-BASED NAMES)

    po::options_description desc("rocsolver client command line options");
    desc.add_options()("help,h", "produces this help message")

        ("sizem,m",
         po::value<rocblas_int>(&argus.M)->default_value(1024),
         "Specific matrix size testing: the number of rows of a matrix.")

        ("sizen,n",
         po::value<rocblas_int>(&argus.N)->default_value(1024),
         "Specific matrix/vector/order size testing: the number of columns of a matrix,"
         "or the order of a system or transformation.")

        ("sizek,k",
         po::value<rocblas_int>(&argus.K)->default_value(1024),
         "Specific...  the number of columns in "
         "A & C  and rows in B.")

        ("size4,S4",
         po::value<rocblas_int>(&argus.S4)->default_value(1024),
         "Extra size value.")

        ("k1",
         po::value<rocblas_int>(&argus.k1)->default_value(1),
         "First index for row interchange, used with laswp. ")

        ("k2",
         po::value<rocblas_int>(&argus.k2)->default_value(2),
         "Last index for row interchange, used with laswp. ")

        ("lda",
         po::value<rocblas_int>(&argus.lda)->default_value(1024),
         "Specific leading dimension of matrix A, is only applicable to "
         "BLAS-2 & BLAS-3: the number of rows.")

        ("ldb",
         po::value<rocblas_int>(&argus.ldb)->default_value(1024),
         "Specific leading dimension of matrix B, is only applicable to BLAS-2 & BLAS-3: the number "
         "of rows.")

        ("ldc",
         po::value<rocblas_int>(&argus.ldc)->default_value(1024),
         "Specific leading dimension of matrix C, is only applicable to BLAS-2 & "
         "BLAS-3: the number of rows.")

        ("ldv",
         po::value<rocblas_int>(&argus.ldv)->default_value(1024),
         "Specific leading dimension.")

        ("ldt",
         po::value<rocblas_int>(&argus.ldt)->default_value(1024),
         "Specific leading dimension.")

        ("bsa",
         po::value<rocblas_int>(&argus.bsa)->default_value(1024*1024),
         "Specific stride of strided_batched matrix A, is only applicable to strided batched"
         "BLAS-2 and BLAS-3: second dimension * leading dimension.")

        ("bsb",
         po::value<rocblas_int>(&argus.bsb)->default_value(1024*1024),
         "Specific stride of strided_batched matrix B, is only applicable to strided batched"
         "BLAS-2 and BLAS-3: second dimension * leading dimension.")

        ("bsc",
         po::value<rocblas_int>(&argus.bsc)->default_value(1024*1024),
         "Specific stride of strided_batched matrix B, is only applicable to strided batched"
         "BLAS-2 and BLAS-3: second dimension * leading dimension.")

        ("bsp",
         po::value<rocblas_int>(&argus.bsp)->default_value(1024),
         "Specific stride of batched pivots vector Ipiv, is only applicable to batched and strided_batched"
         "factorizations: min(first dimension, second dimension).")

        ("incx",
         po::value<rocblas_int>(&argus.incx)->default_value(1),
         "increment between values in x vector")

        ("incy",
         po::value<rocblas_int>(&argus.incy)->default_value(1),
         "increment between values in y vector")

        ("alpha",
          po::value<double>(&argus.alpha)->default_value(1.0), "specifies the scalar alpha")

        ("beta",
         po::value<double>(&argus.beta)->default_value(0.0), "specifies the scalar beta")

        ("function,f",
         po::value<std::string>(&function)->default_value("potf2"),
         "LAPACK function to test. Options: potf2, getf2, getrf, getrs")

        ("precision,r",
         po::value<char>(&precision)->default_value('s'), "Options: h,s,d,c,z")

        ("transposeA",
         po::value<char>(&argus.transA_option)->default_value('N'),
         "N = no transpose, T = transpose, C = conjugate transpose")

        ("transposeB",
         po::value<char>(&argus.transB_option)->default_value('N'),
         "N = no transpose, T = transpose, C = conjugate transpose")

        ("transposeH",
         po::value<char>(&argus.transH_option)->default_value('N'),
         "N = no transpose, T = transpose, C = conjugate transpose")

        ("side",
         po::value<char>(&argus.side_option)->default_value('L'),
         "L = left, R = right. Only applicable to certain routines")

        ("uplo",
         po::value<char>(&argus.uplo_option)->default_value('U'),
         "U = upper, L = lower. Only applicable to certain routines")

        ("direct",
         po::value<char>(&argus.direct_option)->default_value('F'),
         "F = forward, B = backward. Only applicable to certain routines")

        ("storev",
         po::value<char>(&argus.storev)->default_value('C'),
         "C = column_wise, R = row_wise. Only applicable to certain routines")

        ("batch",
         po::value<rocblas_int>(&argus.batch_count)->default_value(1),
         "Number of matrices. Only applicable to batched routines")

        ("verify,v",
         po::value<rocblas_int>(&argus.norm_check)->default_value(0),
         "Validate GPU results with CPU? 0 = No, 1 = Yes (default: No)")

        ("iters,i",
         po::value<rocblas_int>(&argus.iters)->default_value(10),
         "Iterations to run inside timing loop")

        ("perf",
         po::value<rocblas_int>(&argus.perf)->default_value(0),
         "If equal 1, only GPU timing results are collected and printed (default is 0)")

        ("device",
         po::value<rocblas_int>(&device_id)->default_value(0),
         "Set default device to be used for subsequent program runs");

    po::variables_map vm;
    po::store(po::parse_command_line(argc, argv, desc), vm);
    po::notify(vm);

    // print help message
    if (vm.count("help")) {
        rocblas_cout << desc << std::endl;
        return 0;
    }

    // catch invalid arguments for:

    // precision
    if (precision != 's' && precision != 'd' && precision != 'c' && precision != 'z')
        throw std::invalid_argument("Invalid value for --precision ");

    // deviceID
    if (!argus.perf) {
        rocblas_int device_count = query_device_property();
        if (device_count <= device_id)
            throw std::invalid_argument("Invalid Device ID");
    }
    set_device(device_id);

    // operation transA
    if (argus.transA_option != 'N' &&
        argus.transA_option != 'T' &&
        argus.transA_option != 'C')
        throw std::invalid_argument("Invalid value for --transposeA");

    // operation transB
    if (argus.transB_option != 'N' &&
        argus.transB_option != 'T' &&
        argus.transB_option != 'C')
        throw std::invalid_argument("Invalid value for --transposeB");

    // operation transH
    if (argus.transH_option != 'N' &&
        argus.transH_option != 'T' &&
        argus.transH_option != 'C')
        throw std::invalid_argument("Invalid value for --transposeH");

    // side
    if (argus.side_option != 'L' &&
        argus.side_option != 'R' &&
        argus.side_option != 'B')
        throw std::invalid_argument("Invalid value for --side");

    // uplo
    if (argus.uplo_option != 'U' &&
        argus.uplo_option != 'L' &&
        argus.uplo_option != 'F')
        throw std::invalid_argument("Invalid value for --uplo");

    // direct
    if (argus.direct_option != 'F' &&
        argus.direct_option != 'B')
        throw std::invalid_argument("Invalid value for --direct");

    // storev
    if (argus.storev != 'R' &&
        argus.storev != 'C')
        throw std::invalid_argument("Invalid value for --storev");

    // select and dispatch function test/benchmark
    // (TODO) MOVE THIS TO A SEPARATE IMPROVED DISPATCH FUNCTION
    if (function == "potf2") {
        if (precision == 's')
            testing_potf2_potrf<false,false,0,float>(argus);
        else if (precision == 'd')
            testing_potf2_potrf<false,false,0,double>(argus);
        else if (precision == 'c')
            testing_potf2_potrf<false,false,0,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_potf2_potrf<false,false,0,rocblas_double_complex>(argus);
    }
    else if (function == "potf2_batched") {
        if (precision == 's')
            testing_potf2_potrf<true,true,0,float>(argus);
        else if (precision == 'd')
            testing_potf2_potrf<true,true,0,double>(argus);
        else if (precision == 'c')
            testing_potf2_potrf<true,true,0,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_potf2_potrf<true,true,0,rocblas_double_complex>(argus);
    }
    else if (function == "potf2_strided_batched") {
        if (precision == 's')
            testing_potf2_potrf<false,true,0,float>(argus);
        else if (precision == 'd')
            testing_potf2_potrf<false,true,0,double>(argus);
        else if (precision == 'c')
            testing_potf2_potrf<false,true,0,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_potf2_potrf<false,true,0,rocblas_double_complex>(argus);
    }
    else if (function == "potrf") {
        if (precision == 's')
            testing_potf2_potrf<false,false,1,float>(argus);
        else if (precision == 'd')
            testing_potf2_potrf<false,false,1,double>(argus);
        else if (precision == 'c')
            testing_potf2_potrf<false,false,1,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_potf2_potrf<false,false,1,rocblas_double_complex>(argus);
    }
    else if (function == "potrf_batched") {
        if (precision == 's')
            testing_potf2_potrf<true,true,1,float>(argus);
        else if (precision == 'd')
            testing_potf2_potrf<true,true,1,double>(argus);
        else if (precision == 'c')
            testing_potf2_potrf<true,true,1,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_potf2_potrf<true,true,1,rocblas_double_complex>(argus);
    }
    else if (function == "potrf_strided_batched") {
        if (precision == 's')
            testing_potf2_potrf<false,true,1,float>(argus);
        else if (precision == 'd')
            testing_potf2_potrf<false,true,1,double>(argus);
        else if (precision == 'c')
            testing_potf2_potrf<false,true,1,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_potf2_potrf<false,true,1,rocblas_double_complex>(argus);
    }
    else if (function == "getf2_npvt") {
        if (precision == 's')
            testing_getf2_getrf_npvt<false,false,0,float>(argus);
        else if (precision == 'd')
            testing_getf2_getrf_npvt<false,false,0,double>(argus);
        else if (precision == 'c')
            testing_getf2_getrf_npvt<false,false,0,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_getf2_getrf_npvt<false,false,0,rocblas_double_complex>(argus);
    }
    else if (function == "getf2_npvt_batched") {
        if (precision == 's')
            testing_getf2_getrf_npvt<true,true,0,float>(argus);
        else if (precision == 'd')
            testing_getf2_getrf_npvt<true,true,0,double>(argus);
        else if (precision == 'c')
            testing_getf2_getrf_npvt<true,true,0,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_getf2_getrf_npvt<true,true,0,rocblas_double_complex>(argus);
    }
    else if (function == "getf2_npvt_strided_batched") {
        if (precision == 's')
            testing_getf2_getrf_npvt<false,true,0,float>(argus);
        else if (precision == 'd')
            testing_getf2_getrf_npvt<false,true,0,double>(argus);
        else if (precision == 'c')
            testing_getf2_getrf_npvt<false,true,0,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_getf2_getrf_npvt<false,true,0,rocblas_double_complex>(argus);
    }
    else if (function == "getrf_npvt") {
        if (precision == 's')
            testing_getf2_getrf_npvt<false,false,1,float>(argus);
        else if (precision == 'd')
            testing_getf2_getrf_npvt<false,false,1,double>(argus);
        else if (precision == 'c')
            testing_getf2_getrf_npvt<false,false,1,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_getf2_getrf_npvt<false,false,1,rocblas_double_complex>(argus);
    }
    else if (function == "getrf_npvt_batched") {
        if (precision == 's')
            testing_getf2_getrf_npvt<true,true,1,float>(argus);
        else if (precision == 'd')
            testing_getf2_getrf_npvt<true,true,1,double>(argus);
        else if (precision == 'c')
            testing_getf2_getrf_npvt<true,true,1,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_getf2_getrf_npvt<true,true,1,rocblas_double_complex>(argus);
    }
    else if (function == "getrf_npvt_strided_batched") {
        if (precision == 's')
            testing_getf2_getrf_npvt<false,true,1,float>(argus);
        else if (precision == 'd')
            testing_getf2_getrf_npvt<false,true,1,double>(argus);
        else if (precision == 'c')
            testing_getf2_getrf_npvt<false,true,1,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_getf2_getrf_npvt<false,true,1,rocblas_double_complex>(argus);
    }
    else if (function == "getf2") {
        if (precision == 's')
            testing_getf2_getrf<false,false,0,float>(argus);
        else if (precision == 'd')
            testing_getf2_getrf<false,false,0,double>(argus);
        else if (precision == 'c')
            testing_getf2_getrf<false,false,0,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_getf2_getrf<false,false,0,rocblas_double_complex>(argus);
    }
    else if (function == "getf2_batched") {
        if (precision == 's')
            testing_getf2_getrf<true,true,0,float>(argus);
        else if (precision == 'd')
            testing_getf2_getrf<true,true,0,double>(argus);
        else if (precision == 'c')
            testing_getf2_getrf<true,true,0,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_getf2_getrf<true,true,0,rocblas_double_complex>(argus);
    }
    else if (function == "getf2_strided_batched") {
        if (precision == 's')
            testing_getf2_getrf<false,true,0,float>(argus);
        else if (precision == 'd')
            testing_getf2_getrf<false,true,0,double>(argus);
        else if (precision == 'c')
            testing_getf2_getrf<false,true,0,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_getf2_getrf<false,true,0,rocblas_double_complex>(argus);
    }
    else if (function == "getrf") {
        if (precision == 's')
            testing_getf2_getrf<false,false,1,float>(argus);
        else if (precision == 'd')
            testing_getf2_getrf<false,false,1,double>(argus);
        else if (precision == 'c')
            testing_getf2_getrf<false,false,1,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_getf2_getrf<false,false,1,rocblas_double_complex>(argus);
    }
    else if (function == "getrf_batched") {
        if (precision == 's')
            testing_getf2_getrf<true,true,1,float>(argus);
        else if (precision == 'd')
            testing_getf2_getrf<true,true,1,double>(argus);
        else if (precision == 'c')
            testing_getf2_getrf<true,true,1,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_getf2_getrf<true,true,1,rocblas_double_complex>(argus);
    }
    else if (function == "getrf_strided_batched") {
        if (precision == 's')
            testing_getf2_getrf<false,true,1,float>(argus);
        else if (precision == 'd')
            testing_getf2_getrf<false,true,1,double>(argus);
        else if (precision == 'c')
            testing_getf2_getrf<false,true,1,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_getf2_getrf<false,true,1,rocblas_double_complex>(argus);
    }
    else if (function == "geqr2") {
        if (precision == 's')
            testing_geqr2_geqrf<false,false,0,float>(argus);
        else if (precision == 'd')
            testing_geqr2_geqrf<false,false,0,double>(argus);
        else if (precision == 'c')
            testing_geqr2_geqrf<false,false,0,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_geqr2_geqrf<false,false,0,rocblas_double_complex>(argus);
    }
    else if (function == "geqr2_batched") {
        if (precision == 's')
            testing_geqr2_geqrf<true,true,0,float>(argus);
        else if (precision == 'd')
            testing_geqr2_geqrf<true,true,0,double>(argus);
        else if (precision == 'c')
            testing_geqr2_geqrf<true,true,0,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_geqr2_geqrf<true,true,0,rocblas_double_complex>(argus);
    }
    else if (function == "geqr2_strided_batched") {
        if (precision == 's')
            testing_geqr2_geqrf<false,true,0,float>(argus);
        else if (precision == 'd')
            testing_geqr2_geqrf<false,true,0,double>(argus);
        else if (precision == 'c')
            testing_geqr2_geqrf<false,true,0,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_geqr2_geqrf<false,true,0,rocblas_double_complex>(argus);
    }
    else if (function == "geqrf") {
        if (precision == 's')
            testing_geqr2_geqrf<false,false,1,float>(argus);
        else if (precision == 'd')
            testing_geqr2_geqrf<false,false,1,double>(argus);
        else if (precision == 'c')
            testing_geqr2_geqrf<false,false,1,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_geqr2_geqrf<false,false,1,rocblas_double_complex>(argus);
    }
    else if (function == "geqrf_batched") {
        if (precision == 's')
            testing_geqr2_geqrf<true,true,1,float>(argus);
        else if (precision == 'd')
            testing_geqr2_geqrf<true,true,1,double>(argus);
        else if (precision == 'c')
            testing_geqr2_geqrf<true,true,1,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_geqr2_geqrf<true,true,1,rocblas_double_complex>(argus);
    }
    else if (function == "geqrf_strided_batched") {
        if (precision == 's')
            testing_geqr2_geqrf<false,true,1,float>(argus);
        else if (precision == 'd')
            testing_geqr2_geqrf<false,true,1,double>(argus);
        else if (precision == 'c')
            testing_geqr2_geqrf<false,true,1,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_geqr2_geqrf<false,true,1,rocblas_double_complex>(argus);
    }
    else if (function == "geqrf_ptr_batched") {
        if (precision == 's')
            testing_geqr2_geqrf<true,false,1,float>(argus);
        else if (precision == 'd')
            testing_geqr2_geqrf<true,false,1,double>(argus);
        else if (precision == 'c')
            testing_geqr2_geqrf<true,false,1,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_geqr2_geqrf<true,false,1,rocblas_double_complex>(argus);
    }
    else if (function == "gelq2") {
        if (precision == 's')
            testing_gelq2_gelqf<false,false,0,float>(argus);
        else if (precision == 'd')
            testing_gelq2_gelqf<false,false,0,double>(argus);
        else if (precision == 'c')
            testing_gelq2_gelqf<false,false,0,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_gelq2_gelqf<false,false,0,rocblas_double_complex>(argus);
    }
    else if (function == "gelq2_batched") {
        if (precision == 's')
            testing_gelq2_gelqf<true,true,0,float>(argus);
        else if (precision == 'd')
            testing_gelq2_gelqf<true,true,0,double>(argus);
        else if (precision == 'c')
            testing_gelq2_gelqf<true,true,0,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_gelq2_gelqf<true,true,0,rocblas_double_complex>(argus);
    }
    else if (function == "gelq2_strided_batched") {
        if (precision == 's')
            testing_gelq2_gelqf<false,true,0,float>(argus);
        else if (precision == 'd')
            testing_gelq2_gelqf<false,true,0,double>(argus);
        else if (precision == 'c')
            testing_gelq2_gelqf<false,true,0,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_gelq2_gelqf<false,true,0,rocblas_double_complex>(argus);
    }
    else if (function == "gelqf") {
        if (precision == 's')
            testing_gelq2_gelqf<false,false,1,float>(argus);
        else if (precision == 'd')
            testing_gelq2_gelqf<false,false,1,double>(argus);
        else if (precision == 'c')
            testing_gelq2_gelqf<false,false,1,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_gelq2_gelqf<false,false,1,rocblas_double_complex>(argus);
    }
    else if (function == "gelqf_batched") {
        if (precision == 's')
            testing_gelq2_gelqf<true,true,1,float>(argus);
        else if (precision == 'd')
            testing_gelq2_gelqf<true,true,1,double>(argus);
        else if (precision == 'c')
            testing_gelq2_gelqf<true,true,1,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_gelq2_gelqf<true,true,1,rocblas_double_complex>(argus);
    }
    else if (function == "gelqf_strided_batched") {
        if (precision == 's')
            testing_gelq2_gelqf<false,true,1,float>(argus);
        else if (precision == 'd')
            testing_gelq2_gelqf<false,true,1,double>(argus);
        else if (precision == 'c')
            testing_gelq2_gelqf<false,true,1,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_gelq2_gelqf<false,true,1,rocblas_double_complex>(argus);
    }
    else if (function == "getrs") {
        if (precision == 's')
            testing_getrs<false,false,float>(argus);
        else if (precision == 'd')
            testing_getrs<false,false,double>(argus);
        else if (precision == 'c')
            testing_getrs<false,false,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_getrs<false,false,rocblas_double_complex>(argus);
    }
    else if (function == "getrs_batched") {
        if (precision == 's')
            testing_getrs<true,true,float>(argus);
        else if (precision == 'd')
            testing_getrs<true,true,double>(argus);
        else if (precision == 'c')
            testing_getrs<true,true,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_getrs<true,true,rocblas_double_complex>(argus);
    }
    else if (function == "getrs_strided_batched") {
        if (precision == 's')
            testing_getrs<false,true,float>(argus);
        else if (precision == 'd')
            testing_getrs<false,true,double>(argus);
        else if (precision == 'c')
            testing_getrs<false,true,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_getrs<false,true,rocblas_double_complex>(argus);
    }
    else if (function == "getri") {
        if (precision == 's')
            testing_getri<false,false,float>(argus);
        else if (precision == 'd')
            testing_getri<false,false,double>(argus);
        else if (precision == 'c')
            testing_getri<false,false,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_getri<false,false,rocblas_double_complex>(argus);
    }
    else if (function == "getri_batched") {
        if (precision == 's')
            testing_getri<true,true,float>(argus);
        else if (precision == 'd')
            testing_getri<true,true,double>(argus);
        else if (precision == 'c')
            testing_getri<true,true,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_getri<true,true,rocblas_double_complex>(argus);
    }
    else if (function == "getri_strided_batched") {
        if (precision == 's')
            testing_getri<false,true,float>(argus);
        else if (precision == 'd')
            testing_getri<false,true,double>(argus);
        else if (precision == 'c')
            testing_getri<false,true,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_getri<false,true,rocblas_double_complex>(argus);
    }
    else if (function == "getri_outofplace_batched") {
        if (precision == 's')
            testing_getri<true,false,float>(argus);
        else if (precision == 'd')
            testing_getri<true,false,double>(argus);
        else if (precision == 'c')
            testing_getri<true,false,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_getri<true,false,rocblas_double_complex>(argus);
    }
    else if (function == "gebd2") {
        if (precision == 's')
            testing_gebd2_gebrd<false,false,0,float>(argus);
        else if (precision == 'd')
            testing_gebd2_gebrd<false,false,0,double>(argus);
        else if (precision == 'c')
            testing_gebd2_gebrd<false,false,0,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_gebd2_gebrd<false,false,0,rocblas_double_complex>(argus);
    }
    else if (function == "gebd2_batched") {
        if (precision == 's')
            testing_gebd2_gebrd<true,true,0,float>(argus);
        else if (precision == 'd')
            testing_gebd2_gebrd<true,true,0,double>(argus);
        else if (precision == 'c')
            testing_gebd2_gebrd<true,true,0,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_gebd2_gebrd<true,true,0,rocblas_double_complex>(argus);
    }
    else if (function == "gebd2_strided_batched") {
        if (precision == 's')
            testing_gebd2_gebrd<false,true,0,float>(argus);
        else if (precision == 'd')
            testing_gebd2_gebrd<false,true,0,double>(argus);
        else if (precision == 'c')
            testing_gebd2_gebrd<false,true,0,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_gebd2_gebrd<false,true,0,rocblas_double_complex>(argus);
    }
    else if (function == "gebrd") {
        if (precision == 's')
            testing_gebd2_gebrd<false,false,1,float>(argus);
        else if (precision == 'd')
            testing_gebd2_gebrd<false,false,1,double>(argus);
        else if (precision == 'c')
            testing_gebd2_gebrd<false,false,1,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_gebd2_gebrd<false,false,1,rocblas_double_complex>(argus);
    }
    else if (function == "gebrd_batched") {
        if (precision == 's')
            testing_gebd2_gebrd<true,true,1,float>(argus);
        else if (precision == 'd')
            testing_gebd2_gebrd<true,true,1,double>(argus);
        else if (precision == 'c')
            testing_gebd2_gebrd<true,true,1,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_gebd2_gebrd<true,true,1,rocblas_double_complex>(argus);
    }
    else if (function == "gebrd_strided_batched") {
        if (precision == 's')
            testing_gebd2_gebrd<false,true,1,float>(argus);
        else if (precision == 'd')
            testing_gebd2_gebrd<false,true,1,double>(argus);
        else if (precision == 'c')
            testing_gebd2_gebrd<false,true,1,rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_gebd2_gebrd<false,true,1,rocblas_double_complex>(argus);
    }
    else if (function == "lacgv") {
        if (precision == 'c')
            testing_lacgv<rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_lacgv<rocblas_double_complex>(argus);
        else
            throw std::invalid_argument("This function does not support the given --precision");
    }
    else if (function == "laswp") {
        if (precision == 's')
            testing_laswp<float>(argus);
        else if (precision == 'd')
            testing_laswp<double>(argus);
        else if (precision == 'c')
            testing_laswp<rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_laswp<rocblas_double_complex>(argus);
    }
    else if (function == "larfg") {
        if (precision == 's')
            testing_larfg<float>(argus);
        else if (precision == 'd')
            testing_larfg<double>(argus);
        else if (precision == 'c')
            testing_larfg<rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_larfg<rocblas_double_complex>(argus);
    }
    else if (function == "larf") {
        if (precision == 's')
            testing_larf<float>(argus);
        else if (precision == 'd')
            testing_larf<double>(argus);
        else if (precision == 'c')
            testing_larf<rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_larf<rocblas_double_complex>(argus);
    }
    else if (function == "larft") {
        if (precision == 's')
            testing_larft<float>(argus);
        else if (precision == 'd')
            testing_larft<double>(argus);
        else if (precision == 'c')
            testing_larft<rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_larft<rocblas_double_complex>(argus);
    }
    else if (function == "larfb") {
        if (precision == 's')
            testing_larfb<float>(argus);
        else if (precision == 'd')
            testing_larfb<double>(argus);
        else if (precision == 'c')
            testing_larfb<rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_larfb<rocblas_double_complex>(argus);
    }
    else if (function == "labrd") {
        if (precision == 's')
            testing_labrd<float>(argus);
        else if (precision == 'd')
            testing_labrd<double>(argus);
        else if (precision == 'c')
            testing_labrd<rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_labrd<rocblas_double_complex>(argus);
    }
    else if (function == "org2r") {
        if (precision == 's')
            testing_orgxr_ungxr<float,0>(argus);
        else if (precision == 'd')
            testing_orgxr_ungxr<double,0>(argus);
        else
            throw std::invalid_argument("This function does not support the given --precision");
    }
    else if (function == "ung2r") {
        if (precision == 'c')
            testing_orgxr_ungxr<rocblas_float_complex,0>(argus);
        else if (precision == 'z')
            testing_orgxr_ungxr<rocblas_double_complex,0>(argus);
        else
            throw std::invalid_argument("This function does not support the given --precision");
    }
    else if (function == "orgqr") {
        if (precision == 's')
            testing_orgxr_ungxr<float,1>(argus);
        else if (precision == 'd')
            testing_orgxr_ungxr<double,1>(argus);
        else
            throw std::invalid_argument("This function does not support the given --precision");
    }
    else if (function == "ungqr") {
        if (precision == 'c')
            testing_orgxr_ungxr<rocblas_float_complex,1>(argus);
        else if (precision == 'z')
            testing_orgxr_ungxr<rocblas_double_complex,1>(argus);
        else
            throw std::invalid_argument("This function does not support the given --precision");
    }
    else if (function == "orm2r") {
        if (precision == 's')
            testing_ormxr_unmxr<float,0>(argus);
        else if (precision == 'd')
            testing_ormxr_unmxr<double,0>(argus);
        else
            throw std::invalid_argument("This function does not support the given --precision");
    }
    else if (function == "unm2r") {
        if (precision == 'c')
            testing_ormxr_unmxr<rocblas_float_complex,0>(argus);
        else if (precision == 'z')
            testing_ormxr_unmxr<rocblas_double_complex,0>(argus);
        else
            throw std::invalid_argument("This function does not support the given --precision");
    }
    else if (function == "ormqr") {
        if (precision == 's')
            testing_ormxr_unmxr<float,1>(argus);
        else if (precision == 'd')
            testing_ormxr_unmxr<double,1>(argus);
        else
            throw std::invalid_argument("This function does not support the given --precision");
    }
    else if (function == "unmqr") {
        if (precision == 'c')
            testing_ormxr_unmxr<rocblas_float_complex,1>(argus);
        else if (precision == 'z')
            testing_ormxr_unmxr<rocblas_double_complex,1>(argus);
        else
            throw std::invalid_argument("This function does not support the given --precision");
    }
    else if (function == "orml2") {
        if (precision == 's')
            testing_ormlx_unmlx<float,0>(argus);
        else if (precision == 'd')
            testing_ormlx_unmlx<double,0>(argus);
        else
            throw std::invalid_argument("This function does not support the given --precision");
    }
    else if (function == "unml2") {
        if (precision == 'c')
            testing_ormlx_unmlx<rocblas_float_complex,0>(argus);
        else if (precision == 'z')
            testing_ormlx_unmlx<rocblas_double_complex,0>(argus);
        else
            throw std::invalid_argument("This function does not support the given --precision");
    }
    else if (function == "ormlq") {
        if (precision == 's')
            testing_ormlx_unmlx<float,1>(argus);
        else if (precision == 'd')
            testing_ormlx_unmlx<double,1>(argus);
        else
            throw std::invalid_argument("This function does not support the given --precision");
    }
    else if (function == "unmlq") {
        if (precision == 'c')
            testing_ormlx_unmlx<rocblas_float_complex,1>(argus);
        else if (precision == 'z')
            testing_ormlx_unmlx<rocblas_double_complex,1>(argus);
        else
            throw std::invalid_argument("This function does not support the given --precision");
    }
    else if (function == "orgl2") {
        if (precision == 's')
            testing_orglx_unglx<float,0>(argus);
        else if (precision == 'd')
            testing_orglx_unglx<double,0>(argus);
        else
            throw std::invalid_argument("This function does not support the given --precision");
    }
    else if (function == "ungl2") {
        if (precision == 'c')
            testing_orglx_unglx<rocblas_float_complex,0>(argus);
        else if (precision == 'z')
            testing_orglx_unglx<rocblas_double_complex,0>(argus);
        else
            throw std::invalid_argument("This function does not support the given --precision");
    }
    else if (function == "orglq") {
        if (precision == 's')
            testing_orglx_unglx<float,1>(argus);
        else if (precision == 'd')
            testing_orglx_unglx<double,1>(argus);
        else
            throw std::invalid_argument("This function does not support the given --precision");
    }
    else if (function == "unglq") {
        if (precision == 'c')
            testing_orglx_unglx<rocblas_float_complex,1>(argus);
        else if (precision == 'z')
            testing_orglx_unglx<rocblas_double_complex,1>(argus);
        else
            throw std::invalid_argument("This function does not support the given --precision");
    }
    else if (function == "orgbr") {
        if (precision == 's')
            testing_orgbr_ungbr<float>(argus);
        else if (precision == 'd')
            testing_orgbr_ungbr<double>(argus);
        else
            throw std::invalid_argument("This function does not support the given --precision");
    }
    else if (function == "ungbr") {
        if (precision == 'c')
            testing_orgbr_ungbr<rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_orgbr_ungbr<rocblas_double_complex>(argus);
        else
            throw std::invalid_argument("This function does not support the given --precision");
    }
    else if (function == "ormbr") {
        if (precision == 's')
            testing_ormbr_unmbr<float>(argus);
        else if (precision == 'd')
            testing_ormbr_unmbr<double>(argus);
        else
            throw std::invalid_argument("This function does not support the given --precision");
    }
    else if (function == "unmbr") {
        if (precision == 'c')
            testing_ormbr_unmbr<rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_ormbr_unmbr<rocblas_double_complex>(argus);
        else
            throw std::invalid_argument("This function does not support the given --precision");
    }
    else if (function == "bdsqr") {
        if (precision == 's')
            testing_bdsqr<float>(argus);
        else if (precision == 'd')
            testing_bdsqr<double>(argus);
        else if (precision == 'c')
            testing_bdsqr<rocblas_float_complex>(argus);
        else if (precision == 'z')
            testing_bdsqr<rocblas_double_complex>(argus);
    }
    else
        throw std::invalid_argument("Invalid value for --function");

    return 0;
}

catch(const std::invalid_argument& exp)
{
    rocblas_cerr << exp.what() << std::endl;
    return -1;
}


#include <boost/graph/bandwidth.hpp>


#include <NTL/mat_ZZ_p.h>
#include <NTL/vec_ZZVec.h>
#include <NTL/vec_long.h>
#include <NTL/BasicThreadPool.h>

// FIXME: only needed if we use multi-modular MM
#include <NTL/MatPrime.h>
#include <NTL/mat_lzz_p.h>

NTL_START_IMPL

// ******************** Matrix Multiplication ************************

#ifdef NTL_HAVE_LL_TYPE
#define NTL_USE_MM_MATMUL (1)
#else
#define NTL_USE_MM_MATMUL (0)
#endif

#define PAR_THRESH (40000.0)

// *********************** Plain Matrix Multiplication ***************

void plain_mul_aux(mat_ZZ_p& X, const mat_ZZ_p& A, const mat_ZZ_p& B)
{
   long n = A.NumRows();
   long l = A.NumCols();
   long m = B.NumCols();

   if (l != B.NumRows())
      LogicError("matrix mul: dimension mismatch");

   X.SetDims(n, m);

   ZZ_pContext context;
   context.save();

   long sz = ZZ_p::ModulusSize();
   bool seq = (double(n)*double(l)*double(m)*double(sz)*double(sz) < PAR_THRESH);

   NTL_GEXEC_RANGE(seq, m, first, last)
   NTL_IMPORT(n)
   NTL_IMPORT(l)
   NTL_IMPORT(m)

   context.restore();

   long i, j, k;
   ZZ acc, tmp;

   vec_ZZ_p B_col;
   B_col.SetLength(l);

   for (j = first; j < last; j++) {
      for (k = 0; k < l; k++) B_col[k] = B[k][j];

      for (i = 0; i < n; i++) {
         clear(acc);
         for (k = 0; k < l; k++) {
            mul(tmp, rep(A[i][k]), rep(B_col[k]));
            add(acc, acc, tmp);
         }
         conv(X[i][j], acc);
      }
   }

   NTL_GEXEC_RANGE_END
}

void plain_mul(mat_ZZ_p& X, const mat_ZZ_p& A, const mat_ZZ_p& B)
{
   if (&X == &A || &X == &B) {
      mat_ZZ_p tmp;
      plain_mul_aux(tmp, A, B);
      X = tmp;
   }
   else
      plain_mul_aux(X, A, B);
}

// X = A*transpose(B)

void plain_mul_transpose_aux(mat_ZZ_p& X, const mat_ZZ_p& A, const mat_ZZ_p& B)
{
   long n = A.NumRows();
   long l = A.NumCols();
   long m = B.NumRows();

   if (l != B.NumCols())
      LogicError("matrix mul: dimension mismatch");

   X.SetDims(n, m);

   ZZ_pContext context;
   context.save();

   long sz = ZZ_p::ModulusSize();
   bool seq = (double(n)*double(l)*double(m)*double(sz)*double(sz) < PAR_THRESH);

   NTL_GEXEC_RANGE(seq, m, first, last)
   NTL_IMPORT(n)
   NTL_IMPORT(l)
   NTL_IMPORT(m)

   context.restore();

   long i, j, k;
   ZZ acc, tmp;

   for (j = first; j < last; j++) {
      const ZZ_p *B_col = B[j].elts();

      for (i = 0; i < n; i++) {
         clear(acc);
         for (k = 0; k < l; k++) {
            mul(tmp, rep(A[i][k]), rep(B_col[k]));
            add(acc, acc, tmp);
         }
         conv(X[i][j], acc);
      }
   }

   NTL_GEXEC_RANGE_END
}

void plain_mul_transpose(mat_ZZ_p& X, const mat_ZZ_p& A, const mat_ZZ_p& B)
{
   if (&X == &A || &X == &B) {
      mat_ZZ_p tmp;
      plain_mul_transpose_aux(tmp, A, B);
      X = tmp;
   }
   else
      plain_mul_transpose_aux(X, A, B);
}

// ***************** Multi-modular Matrix Multiplication *************

struct mat_ZZ_p_crt_rep {

   Vec< Mat<MatPrime_residue_t> > rep;

};

static
const MatPrime_crt_helper& get_MatPrime_crt_helper_info()
{
   do {
      Lazy<MatPrime_crt_helper,ZZ_pInfoT::MatPrime_crt_helper_deleter_policy>::Builder
         builder(ZZ_pInfo->MatPrime_crt_helper_info);
      if (!builder()) break;

      UniquePtr<MatPrime_crt_helper,ZZ_pInfoT::MatPrime_crt_helper_deleter_policy> p;
      p.make();
      build(*p, ZZ_pInfo->p);
      builder.move(p);
   } while (0);

   return *ZZ_pInfo->MatPrime_crt_helper_info;
}

static
void RawConvert(Mat<zz_p>& X, const Mat<MatPrime_residue_t>& A)
{
   long n = A.NumRows();
   long m = A.NumCols();

   X.SetDims(n, m);
   for (long i = 0; i < n; i++) {
      const MatPrime_residue_t *Ai = A[i].elts();
      zz_p *Xi = X[i].elts();
      for (long j = 0; j < m; j++)
         Xi[j].LoopHole() = Ai[j];
   }
}

static
void RawConvertTranspose(Mat<zz_p>& X, const Mat<MatPrime_residue_t>& A)
{
   long n = A.NumRows();
   long m = A.NumCols();

   X.SetDims(m, n);
   for (long i = 0; i < n; i++) {
      const MatPrime_residue_t *Ai = A[i].elts();
      for (long j = 0; j < m; j++)
         X[j][i] = Ai[j];
   }
}

static
void RawConvert(Mat<MatPrime_residue_t>& X, const Mat<zz_p>& A)
{
   long n = A.NumRows();
   long m = A.NumCols();

   X.SetDims(n, m);
   for (long i = 0; i < n; i++) {
      const zz_p *Ai = A[i].elts();
      MatPrime_residue_t *Xi = X[i].elts();
      for (long j = 0; j < m; j++)
         Xi[j] = rep(Ai[j]);
   }
}

#define CRT_BLK (8)

void to_mat_ZZ_p_crt_rep(mat_ZZ_p_crt_rep& X, const mat_ZZ_p& A)
{
   long n = A.NumRows();
   long m = A.NumCols();

   const MatPrime_crt_helper& H = get_MatPrime_crt_helper_info();
   long nprimes = H.GetNumPrimes();

   if (NTL_OVERFLOW(nprimes, CRT_BLK, 0))
      ResourceError("overflow"); // this is pretty academic

   X.rep.SetLength(nprimes);
   for (long k = 0; k < nprimes; k++) X.rep[k].SetDims(n, m);

   ZZ_pContext context;
   context.save();

   bool seq = (double(n)*double(m)*H.GetCost() < PAR_THRESH);

   // FIXME: right now, we just partition the rows, but if
   // #cols > #rows, we should perhaps partition the cols
   NTL_GEXEC_RANGE(seq, n, first, last)
   NTL_IMPORT(n)
   NTL_IMPORT(m)
   NTL_IMPORT(nprimes)

   context.restore();

   MatPrime_crt_helper_scratch scratch;
   Vec<MatPrime_residue_t> remainders_store;
   remainders_store.SetLength(nprimes*CRT_BLK);
   MatPrime_residue_t *remainders = remainders_store.elts();

   for (long i = first; i < last; i++) {
      const ZZ_p *a = A[i].elts();

      long jj = 0;
      for (; jj <= m-CRT_BLK; jj += CRT_BLK) {
         for (long j = 0; j < CRT_BLK; j++)
            reduce(H, rep(a[jj+j]), remainders + j*nprimes, scratch);
         for (long k = 0; k < nprimes; k++) {
            MatPrime_residue_t *x = X.rep[k][i].elts();
            for (long j = 0; j < CRT_BLK; j++)
               x[jj+j] = remainders[j*nprimes+k];
         }
      }
      if (jj < m) {
         for (long j = 0; j < m-jj; j++)
            reduce(H, rep(a[jj+j]), remainders + j*nprimes, scratch);
         for (long k = 0; k < nprimes; k++) {
            MatPrime_residue_t *x = X.rep[k][i].elts();
            for (long j = 0; j < m-jj; j++)
               x[jj+j] = remainders[j*nprimes+k];
         }
      }
   }

   NTL_GEXEC_RANGE_END
}

void from_mat_ZZ_p_crt_rep(const mat_ZZ_p_crt_rep& X, mat_ZZ_p& A)
{
   long n = X.rep[0].NumRows();
   long m = X.rep[0].NumCols();

   const MatPrime_crt_helper& H = get_MatPrime_crt_helper_info();
   long nprimes = H.GetNumPrimes();

   if (NTL_OVERFLOW(nprimes, CRT_BLK, 0))
      ResourceError("overflow"); // this is pretty academic

   A.SetDims(n, m);

   ZZ_pContext context;
   context.save();

   bool seq = (double(n)*double(m)*H.GetCost() < PAR_THRESH);

   // FIXME: right now, we just partition the rows, but if
   // #cols > #rows, we should perhaps partition the cols
   NTL_GEXEC_RANGE(seq, n, first, last)
   NTL_IMPORT(n)
   NTL_IMPORT(m)
   NTL_IMPORT(nprimes)

   context.restore();

   MatPrime_crt_helper_scratch scratch;
   Vec<MatPrime_residue_t> remainders_store;
   remainders_store.SetLength(nprimes*CRT_BLK);
   MatPrime_residue_t *remainders = remainders_store.elts();

   for (long i = first; i < last; i++) {
      ZZ_p *a = A[i].elts();

      long jj = 0;
      for (; jj <= m-CRT_BLK; jj += CRT_BLK) {
         for (long k = 0; k < nprimes; k++) {
            const MatPrime_residue_t *x = X.rep[k][i].elts();
            for (long j = 0; j < CRT_BLK; j++)
               remainders[j*nprimes+k] = x[jj+j];
         }
         for (long j = 0; j < CRT_BLK; j++)
            reconstruct(H, a[jj+j].LoopHole(), remainders + j*nprimes, scratch);
      }
      if (jj < m) {
         for (long k = 0; k < nprimes; k++) {
            const MatPrime_residue_t *x = X.rep[k][i].elts();
            for (long j = 0; j < m-jj; j++)
               remainders[j*nprimes+k] = x[jj+j];
         }
         for (long j = 0; j < m-jj; j++)
            reconstruct(H, a[jj+j].LoopHole(), remainders + j*nprimes, scratch);
      }
   }

   NTL_GEXEC_RANGE_END
}

void mul(mat_ZZ_p_crt_rep& X, const mat_ZZ_p_crt_rep& A, const mat_ZZ_p_crt_rep& B)
{
   long nprimes = A.rep.length();

   long n = A.rep[0].NumRows();
   long l = A.rep[0].NumCols();
   long m = B.rep[0].NumCols();

   X.rep.SetLength(nprimes);
   for (long k = 0; k < nprimes; k++) X.rep[k].SetDims(n, m);

   bool seq = (double(n)*double(l)*double(m)*double(nprimes) < PAR_THRESH);

   NTL_GEXEC_RANGE(seq, nprimes, first, last)
   NTL_IMPORT(n)
   NTL_IMPORT(l)
   NTL_IMPORT(m)

   zz_pPush push;

   Mat<zz_p> x, a, b;
   x.SetDims(n, m);
   a.SetDims(n, l);
   b.SetDims(l, m);

   for (long k = first; k < last; k++) {
      RestoreMatPrime(k);
      RawConvert(a, A.rep[k]);
      RawConvert(b, B.rep[k]);
      mul(x, a, b);
      RawConvert(X.rep[k], x);
   }

   NTL_GEXEC_RANGE_END
}

// X = A*transpose(B)
void mul_transpose(mat_ZZ_p_crt_rep& X, const mat_ZZ_p_crt_rep& A, const mat_ZZ_p_crt_rep& B)
{
   long nprimes = A.rep.length();

   long n = A.rep[0].NumRows();
   long l = A.rep[0].NumCols();
   long m = B.rep[0].NumRows();

   X.rep.SetLength(nprimes);
   for (long k = 0; k < nprimes; k++) X.rep[k].SetDims(n, m);

   bool seq = (double(n)*double(l)*double(m)*double(nprimes) < PAR_THRESH);

   NTL_GEXEC_RANGE(seq, nprimes, first, last)
   NTL_IMPORT(n)
   NTL_IMPORT(l)
   NTL_IMPORT(m)

   zz_pPush push;

   Mat<zz_p> x, a, b;
   x.SetDims(n, m);
   a.SetDims(n, l);
   b.SetDims(l, m);

   for (long k = first; k < last; k++) {
      RestoreMatPrime(k);
      RawConvert(a, A.rep[k]);
      RawConvertTranspose(b, B.rep[k]);
      mul(x, a, b);
      RawConvert(X.rep[k], x);
   }

   NTL_GEXEC_RANGE_END
}

void multi_modular_mul(mat_ZZ_p& X, const mat_ZZ_p& A, const mat_ZZ_p& B)
{
   long l = A.NumCols();

   if (l != B.NumRows())
      LogicError("matrix mul: dimension mismatch");

   if (l > NTL_MatPrimeLimit)
      ResourceError("matrix mul: dimension too large");

   mat_ZZ_p_crt_rep x, a, b;

   to_mat_ZZ_p_crt_rep(a, A);
   to_mat_ZZ_p_crt_rep(b, B);
   mul(x, a, b);
   from_mat_ZZ_p_crt_rep(x, X);
}

void multi_modular_mul(mat_ZZ_p& X, const mat_ZZ_p& A, const mat_ZZ_p_crt_rep& B)
{
   long l = A.NumCols();

   if (l != B.rep[0].NumRows())
      LogicError("matrix mul: dimension mismatch");

   if (l > NTL_MatPrimeLimit)
      ResourceError("matrix mul: dimension too large");

   mat_ZZ_p_crt_rep x, a;

   to_mat_ZZ_p_crt_rep(a, A);
   mul(x, a, B);
   from_mat_ZZ_p_crt_rep(x, X);
}

void multi_modular_mul_transpose(mat_ZZ_p& X, const mat_ZZ_p& A, const mat_ZZ_p_crt_rep& B)
{
   long l = A.NumCols();

   if (l != B.rep[0].NumCols())
      LogicError("matrix mul: dimension mismatch");

   if (l > NTL_MatPrimeLimit)
      ResourceError("matrix mul: dimension too large");

   mat_ZZ_p_crt_rep x, a;

   to_mat_ZZ_p_crt_rep(a, A);
   mul_transpose(x, a, B);
   from_mat_ZZ_p_crt_rep(x, X);
}

// ******************** mat_ZZ_p_opaque implementation ************

// This could (and maybe eventually will) be implemented using
// derived types, if we want run-time polymorphism...we'll see

struct mat_ZZ_p_opaque_body_crt : mat_ZZ_p_opaque_body {
   mat_ZZ_p_crt_rep body;

   mat_ZZ_p_opaque_body* clone() const
   {
      return MakeRaw<mat_ZZ_p_opaque_body_crt>(*this);
   }

   long NumRows() const
   {
      return body.rep.length() == 0 ? 0 : body.rep[0].NumRows();
   }

   long NumCols() const
   {
      return body.rep.length() == 0 ? 0 : body.rep[0].NumCols();
   }

   void mul(mat_ZZ_p& X, const mat_ZZ_p& A) const
   {
      multi_modular_mul(X, A, body);
   }

   void mul_transpose(mat_ZZ_p& X, const mat_ZZ_p& A) const
   {
      multi_modular_mul_transpose(X, A, body);
   }

};

struct mat_ZZ_p_opaque_body_plain : mat_ZZ_p_opaque_body {
   mat_ZZ_p body;

   mat_ZZ_p_opaque_body* clone() const
   {
      return MakeRaw<mat_ZZ_p_opaque_body_plain>(*this);
   }

   long NumRows() const
   {
      return body.NumRows();
   }

   long NumCols() const
   {
      return body.NumCols();
   }

   void mul(mat_ZZ_p& X, const mat_ZZ_p& A) const
   {
      plain_mul(X, A, body);
   }

   void mul_transpose(mat_ZZ_p& X, const mat_ZZ_p& A) const
   {
      plain_mul_transpose(X, A, body);
   }

};

// This is a "factory" method that makes a mat_ZZ_p_opaque_body
// from a matrix A.  The matrix A is destroyed in the process.

mat_ZZ_p_opaque_body *mat_ZZ_p_opaque_body_move(mat_ZZ_p& A)
{
   if (NTL_USE_MM_MATMUL && A.NumRows() >= 16 && A.NumCols() >= 16) {
      UniquePtr<mat_ZZ_p_opaque_body_crt> tmp;
      tmp.make();
      to_mat_ZZ_p_crt_rep(tmp->body, A);
      A.kill();
      return tmp.release();
   }
   else {
      UniquePtr<mat_ZZ_p_opaque_body_plain> tmp;
      tmp.make();
      tmp->body.move(A);
      return tmp.release();
   }
}

// *******************************************************************

void mul(mat_ZZ_p& X, const mat_ZZ_p& A, const mat_ZZ_p& B)
{
   long n = A.NumRows();
   long l = A.NumCols();
   long m = B.NumCols();

   if (l != B.NumRows()) LogicError("matrix mul: dimension mismatch");

   if (NTL_USE_MM_MATMUL && n >= 24 && l >= 24 && m >= 24)
      multi_modular_mul(X, A, B);
   else
      plain_mul(X, A, B);
}

// *******************************************************************

void add(mat_ZZ_p& X, const mat_ZZ_p& A, const mat_ZZ_p& B)
{
   long n = A.NumRows();
   long m = A.NumCols();

   if (B.NumRows() != n || B.NumCols() != m)
      LogicError("matrix add: dimension mismatch");

   X.SetDims(n, m);

   long i, j;
   for (i = 1; i <= n; i++)
      for (j = 1; j <= m; j++)
         add(X(i,j), A(i,j), B(i,j));
}

void sub(mat_ZZ_p& X, const mat_ZZ_p& A, const mat_ZZ_p& B)
{
   long n = A.NumRows();
   long m = A.NumCols();

   if (B.NumRows() != n || B.NumCols() != m)
      LogicError("matrix sub: dimension mismatch");

   X.SetDims(n, m);

   long i, j;
   for (i = 1; i <= n; i++)
      for (j = 1; j <= m; j++)
         sub(X(i,j), A(i,j), B(i,j));
}

void negate(mat_ZZ_p& X, const mat_ZZ_p& A)
{
   long n = A.NumRows();
   long m = A.NumCols();

   X.SetDims(n, m);

   long i, j;
   for (i = 1; i <= n; i++)
      for (j = 1; j <= m; j++)
         negate(X(i,j), A(i,j));
}

static
void mul_aux(vec_ZZ_p& x, const mat_ZZ_p& A, const vec_ZZ_p& b)
{
   long n = A.NumRows();
   long l = A.NumCols();

   if (l != b.length())
      LogicError("matrix mul: dimension mismatch");

   x.SetLength(n);

   long i, k;
   ZZ acc, tmp;

   for (i = 1; i <= n; i++) {
      clear(acc);
      for (k = 1; k <= l; k++) {
         mul(tmp, rep(A(i,k)), rep(b(k)));
         add(acc, acc, tmp);
      }
      conv(x(i), acc);
   }
}

void mul(vec_ZZ_p& x, const mat_ZZ_p& A, const vec_ZZ_p& b)
{
   if (&b == &x || A.position1(x) != -1) {
      vec_ZZ_p tmp;
      mul_aux(tmp, A, b);
      x = tmp;
   }
   else
      mul_aux(x, A, b);
}

static
void mul_aux(vec_ZZ_p& x, const vec_ZZ_p& a, const mat_ZZ_p& B)
{
   long n = B.NumRows();
   long l = B.NumCols();

   if (n != a.length())
      LogicError("matrix mul: dimension mismatch");

   x.SetLength(l);

   long i, k;
   ZZ acc, tmp;

   for (i = 1; i <= l; i++) {
      clear(acc);
      for (k = 1; k <= n; k++) {
         mul(tmp, rep(a(k)), rep(B(k,i)));
         add(acc, acc, tmp);
      }
      conv(x(i), acc);
   }
}

void mul(vec_ZZ_p& x, const vec_ZZ_p& a, const mat_ZZ_p& B)
{
   if (&a == &x) {
      vec_ZZ_p tmp;
      mul_aux(tmp, a, B);
      x = tmp;
   }
   else
      mul_aux(x, a, B);
}

void ident(mat_ZZ_p& X, long n)
{
   X.SetDims(n, n);
   long i, j;

   for (i = 1; i <= n; i++)
      for (j = 1; j <= n; j++)
         if (i == j)
            set(X(i, j));
         else
            clear(X(i, j));
}

void determinant(ZZ_p& d, const mat_ZZ_p& M_in)
{
   ZZ t1, t2;

   const ZZ& p = ZZ_p::modulus();

   long n = M_in.NumRows();

   if (M_in.NumCols() != n)
      LogicError("determinant: nonsquare matrix");

   if (n == 0) {
      set(d);
      return;
   }

   Vec<ZZVec> M;
   sqr(t1, p);
   mul(t1, t1, n);

   M.SetLength(n);
   for (long i = 0; i < n; i++) {
      M[i].SetSize(n, t1.size());
      for (long j = 0; j < n; j++)
         M[i][j] = rep(M_in[i][j]);
   }

   ZZ det;
   set(det);

   for (long k = 0; k < n; k++) {
      long pos = -1;
      for (long i = k; i < n; i++) {
         rem(t1, M[i][k], p);
         M[i][k] = t1;
         if (pos == -1 && !IsZero(t1))
            pos = i;
      }

      if (pos != -1) {
         if (k != pos) {
            swap(M[pos], M[k]);
            NegateMod(det, det, p);
         }

         MulMod(det, det, M[k][k], p);

         // make M[k, k] == -1 mod p, and make row k reduced

         InvMod(t1, M[k][k], p);
         NegateMod(t1, t1, p);
         for (long j = k+1; j < n; j++) {
            rem(t2, M[k][j], p);
            MulMod(M[k][j], t2, t1, p);
         }

         bool seq =
            double(n-(k+1))*(n-(k+1))*double(p.size())*double(p.size()) < PAR_THRESH;
         NTL_GEXEC_RANGE(seq, n-(k+1), first, last)
         NTL_IMPORT(n)
         NTL_IMPORT(k)

         ZZ t1, t2;

         for (long ii = first; ii < last; ii++) {
            long i = ii + k+1;

            // M[i] = M[i] + M[k]*M[i,k]

            t1 = M[i][k];   // this is already reduced

            ZZ *x = M[i].elts() + (k+1);
            ZZ *y = M[k].elts() + (k+1);

            for (long j = k+1; j < n; j++, x++, y++) {
               // *x = *x + (*y)*t1

               mul(t2, *y, t1);
               add(*x, *x, t2);
            }
         }

         NTL_GEXEC_RANGE_END

      }
      else {
         clear(d);
         return;
      }
   }

   conv(d, det);
}

long IsIdent(const mat_ZZ_p& A, long n)
{
   if (A.NumRows() != n || A.NumCols() != n)
      return 0;

   long i, j;

   for (i = 1; i <= n; i++)
      for (j = 1; j <= n; j++)
         if (i != j) {
            if (!IsZero(A(i, j))) return 0;
         }
         else {
            if (!IsOne(A(i, j))) return 0;
         }

   return 1;
}

void transpose(mat_ZZ_p& X, const mat_ZZ_p& A)
{
   long n = A.NumRows();
   long m = A.NumCols();

   long i, j;

   if (&X == & A) {
      if (n == m)
         for (i = 1; i <= n; i++)
            for (j = i+1; j <= n; j++)
               swap(X(i, j), X(j, i));
      else {
         mat_ZZ_p tmp;
         tmp.SetDims(m, n);
         for (i = 1; i <= n; i++)
            for (j = 1; j <= m; j++)
               tmp(j, i) = A(i, j);
         X.kill();
         X = tmp;
      }
   }
   else {
      X.SetDims(m, n);
      for (i = 1; i <= n; i++)
         for (j = 1; j <= m; j++)
            X(j, i) = A(i, j);
   }
}

static
void solve_impl(ZZ_p& d, vec_ZZ_p& X, const mat_ZZ_p& A, const vec_ZZ_p& b, bool trans)

{
   long n = A.NumRows();
   if (A.NumCols() != n)
      LogicError("solve: nonsquare matrix");

   if (b.length() != n)
      LogicError("solve: dimension mismatch");

   if (n == 0) {
      set(d);
      X.SetLength(0);
      return;
   }

   ZZ t1, t2;

   const ZZ& p = ZZ_p::modulus();

   Vec<ZZVec> M;
   sqr(t1, p);
   mul(t1, t1, n);

   M.SetLength(n);

   for (long i = 0; i < n; i++) {
      M[i].SetSize(n+1, t1.size());

      if (trans)
         for (long j = 0; j < n; j++) M[i][j] = rep(A[j][i]);
      else
         for (long j = 0; j < n; j++) M[i][j] = rep(A[i][j]);

      M[i][n] = rep(b[i]);
   }

   ZZ det;
   set(det);

   for (long k = 0; k < n; k++) {
      long pos = -1;
      for (long i = k; i < n; i++) {
         rem(t1, M[i][k], p);
         M[i][k] = t1;
         if (pos == -1 && !IsZero(t1)) {
            pos = i;
         }
      }

      if (pos != -1) {
         if (k != pos) {
            swap(M[pos], M[k]);
            NegateMod(det, det, p);
         }

         MulMod(det, det, M[k][k], p);

         // make M[k, k] == -1 mod p, and make row k reduced

         InvMod(t1, M[k][k], p);
         NegateMod(t1, t1, p);
         for (long j = k+1; j <= n; j++) {
            rem(t2, M[k][j], p);
            MulMod(M[k][j], t2, t1, p);
         }

         bool seq =
            double(n-(k+1))*(n-(k+1))*double(p.size())*double(p.size()) < PAR_THRESH;
         NTL_GEXEC_RANGE(seq, n-(k+1), first, last)
         NTL_IMPORT(n)
         NTL_IMPORT(k)

         ZZ t1, t2;

         for (long ii = first; ii < last; ii++) {
            long i = ii + k+1;

            // M[i] = M[i] + M[k]*M[i,k]

            t1 = M[i][k];   // this is already reduced

            ZZ *x = M[i].elts() + (k+1);
            ZZ *y = M[k].elts() + (k+1);

            for (long j = k+1; j <= n; j++, x++, y++) {
               // *x = *x + (*y)*t1

               mul(t2, *y, t1);
               add(*x, *x, t2);
            }
         }

         NTL_GEXEC_RANGE_END

      }
      else {
         clear(d);
         return;
      }
   }

   X.SetLength(n);
   for (long i = n-1; i >= 0; i--) {
      clear(t1);
      for (long j = i+1; j < n; j++) {
         mul(t2, rep(X[j]), M[i][j]);
         add(t1, t1, t2);
      }
      sub(t1, t1, M[i][n]);
      conv(X[i], t1);
   }

   conv(d, det);
}

void solve(ZZ_p& d, vec_ZZ_p& x, const mat_ZZ_p& A, const vec_ZZ_p& b)
{
   solve_impl(d, x, A, b, true);
}

void solve(ZZ_p& d, const mat_ZZ_p& A, vec_ZZ_p& x,  const vec_ZZ_p& b)
{
   solve_impl(d, x, A, b, false);
}

void inv(ZZ_p& d, mat_ZZ_p& X, const mat_ZZ_p& A)
{
   long n = A.NumRows();

   if (A.NumCols() != n)
      LogicError("inv: nonsquare matrix");

   if (n == 0) {
      set(d);
      X.SetDims(0, 0);
      return;
   }

   const ZZ& p = ZZ_p::modulus();

   ZZ t1, t2;
   ZZ pivot;
   ZZ pivot_inv;

   Vec<ZZVec> M;
   // scratch space

   sqr(t1, p);
   mul(t1, t1, n);

   M.SetLength(n);
   for (long i = 0; i < n; i++) {
      M[i].SetSize(n, t1.size());
      for (long j = 0; j < n; j++)
         M[i][j] = rep(A[i][j]);
   }

   ZZ det;
   det = 1;

   Vec<long> P;
   P.SetLength(n);
   for (long k = 0; k < n; k++) P[k] = k;
   // records swap operations

   bool seq =
      double(n)*double(n)*double(p.size())*double(p.size()) < PAR_THRESH;

   bool pivoting = false;

   for (long k = 0; k < n; k++) {

      long pos = -1;

      for (long i = k; i < n; i++) {
         rem(pivot, M[i][k], p);
         if (pivot != 0) {
            InvMod(pivot_inv, pivot, p);
            pos = i;
            break;
         }
      }

      if (pos != -1) {
         if (k != pos) {
            swap(M[pos], M[k]);
            NegateMod(det, det, p);
            P[k] = pos;
            pivoting = true;
         }

         MulMod(det, det, pivot, p);

         {
            // multiply row k by pivot_inv
            ZZ *y = &M[k][0];
            for (long j = 0; j < n; j++) {
               rem(t2, y[j], p);
               MulMod(y[j], t2, pivot_inv, p);
            }
            y[k] = pivot_inv;
         }

         NTL_GEXEC_RANGE(seq, n, first, last)
         NTL_IMPORT(n)
         NTL_IMPORT(k)

         ZZ *y = &M[k][0];
         ZZ t1, t2;

         for (long i = first; i < last; i++) {
            if (i == k) continue; // skip row k

            ZZ *x = &M[i][0];
            rem(t1, x[k], p);
            NegateMod(t1, t1, p);
            x[k] = 0;
            if (t1 == 0) continue;

            // add t1 * row k to row i
            for (long j = 0; j < n; j++) {
               mul(t2, y[j], t1);
               add(x[j], x[j], t2);
            }
         }
         NTL_GEXEC_RANGE_END
      }
      else {
         clear(d);
         return;
      }
   }

   if (pivoting) {
      // pivot colums, using reverse swap sequence

      for (long i = 0; i < n; i++) {
         ZZ *x = &M[i][0];

         for (long k = n-1; k >= 0; k--) {
            long pos = P[k];
            if (pos != k) swap(x[pos], x[k]);
         }
      }
   }

   X.SetDims(n, n);
   for (long i = 0; i < n; i++)
      for (long j = 0; j < n; j++)
         conv(X[i][j], M[i][j]);

   conv(d, det);
}

long gauss(mat_ZZ_p& M_in, long w)
{
   ZZ t1, t2;
   ZZ piv;

   long n = M_in.NumRows();
   long m = M_in.NumCols();

   if (w < 0 || w > m)
      LogicError("gauss: bad args");

   const ZZ& p = ZZ_p::modulus();

   Vec<ZZVec> M;
   sqr(t1, p);
   mul(t1, t1, n);

   M.SetLength(n);
   for (long i = 0; i < n; i++) {
      M[i].SetSize(m, t1.size());
      for (long j = 0; j < m; j++) {
         M[i][j] = rep(M_in[i][j]);
      }
   }

   long l = 0;
   for (long k = 0; k < w && l < n; k++) {

      long pos = -1;
      for (long i = l; i < n; i++) {
         rem(t1, M[i][k], p);
         M[i][k] = t1;
         if (pos == -1 && !IsZero(t1)) {
            pos = i;
         }
      }

      if (pos != -1) {
         swap(M[pos], M[l]);

         InvMod(piv, M[l][k], p);
         NegateMod(piv, piv, p);

         for (long j = k+1; j < m; j++) {
            rem(M[l][j], M[l][j], p);
         }

         bool seq =
            double(n-(l+1))*double(m-(k+1))*double(p.size())*double(p.size()) < PAR_THRESH;

         NTL_GEXEC_RANGE(seq, n-(l+1), first, last)
         NTL_IMPORT(m)
         NTL_IMPORT(k)
         NTL_IMPORT(l)

         ZZ t1, t2;

         for (long ii = first; ii < last; ii++) {
            long i = ii + l+1;

            // M[i] = M[i] + M[l]*M[i,k]*piv

            MulMod(t1, M[i][k], piv, p);

            clear(M[i][k]);

            ZZ *x = M[i].elts() + (k+1);
            ZZ *y = M[l].elts() + (k+1);

            for (long j = k+1; j < m; j++, x++, y++) {
               // *x = *x + (*y)*t1

               mul(t2, *y, t1);
               add(t2, t2, *x);
               *x = t2;
            }
         }

         NTL_GEXEC_RANGE_END

         l++;
      }
   }

   for (long i = 0; i < n; i++)
      for (long j = 0; j < m; j++)
         conv(M_in[i][j], M[i][j]);

   return l;
}

long gauss(mat_ZZ_p& M)
{
   return gauss(M, M.NumCols());
}

void image(mat_ZZ_p& X, const mat_ZZ_p& A)
{
   mat_ZZ_p M;
   M = A;
   long r = gauss(M);
   M.SetDims(r, M.NumCols());
   X = M;
}

void kernel(mat_ZZ_p& X, const mat_ZZ_p& A)
{
   long m = A.NumRows();
   long n = A.NumCols();

   const ZZ& p = ZZ_p::modulus();

   mat_ZZ_p M;

   transpose(M, A);
   long r = gauss(M);

   if (r == 0) {
      ident(X, m);
      return;
   }

   X.SetDims(m-r, m);

   if (m-r == 0 || m == 0) return;

   Vec<long> D;
   D.SetLength(m);
   for (long j = 0; j < m; j++) D[j] = -1;

   Vec<ZZ_p> inverses;
   inverses.SetLength(m);

   for (long i = 0, j = -1; i < r; i++) {
      do {
         j++;
      } while (IsZero(M[i][j]));

      D[j] = i;
      inv(inverses[j], M[i][j]);
   }

   bool seq =
      double(m-r)*double(r)*double(r)*double(p.size())*double(p.size()) < PAR_THRESH;

   NTL_GEXEC_RANGE(seq, m-r, first, last)
   NTL_IMPORT(m)
   NTL_IMPORT(r)

   ZZ t1, t2;
   ZZ_p T3;

   for (long k = first; k < last; k++) {
      vec_ZZ_p& v = X[k];
      long pos = 0;
      for (long j = m-1; j >= 0; j--) {
         if (D[j] == -1) {
            if (pos == k)
               set(v[j]);
            else
               clear(v[j]);
            pos++;
         }
         else {
            long i = D[j];

            clear(t1);

            for (long s = j+1; s < m; s++) {
               mul(t2, rep(v[s]), rep(M[i][s]));
               add(t1, t1, t2);
            }

            conv(T3, t1);
            mul(T3, T3, inverses[j]);
            negate(v[j], T3);
         }
      }
   }

   NTL_GEXEC_RANGE_END
}

void mul(mat_ZZ_p& X, const mat_ZZ_p& A, const ZZ_p& b_in)
{
   NTL_ZZ_pRegister(b);
   b = b_in;
   long n = A.NumRows();
   long m = A.NumCols();

   X.SetDims(n, m);

   long i, j;
   for (i = 0; i < n; i++)
      for (j = 0; j < m; j++)
         mul(X[i][j], A[i][j], b);
}

void mul(mat_ZZ_p& X, const mat_ZZ_p& A, long b_in)
{
   NTL_ZZ_pRegister(b);
   b = b_in;
   long n = A.NumRows();
   long m = A.NumCols();

   X.SetDims(n, m);

   long i, j;
   for (i = 0; i < n; i++)
      for (j = 0; j < m; j++)
         mul(X[i][j], A[i][j], b);
}

void diag(mat_ZZ_p& X, long n, const ZZ_p& d_in)
{
   ZZ_p d = d_in;
   X.SetDims(n, n);
   long i, j;

   for (i = 1; i <= n; i++)
      for (j = 1; j <= n; j++)
         if (i == j)
            X(i, j) = d;
         else
            clear(X(i, j));
}

long IsDiag(const mat_ZZ_p& A, long n, const ZZ_p& d)
{
   if (A.NumRows() != n || A.NumCols() != n)
      return 0;

   long i, j;

   for (i = 1; i <= n; i++)
      for (j = 1; j <= n; j++)
         if (i != j) {
            if (!IsZero(A(i, j))) return 0;
         }
         else {
            if (A(i, j) != d) return 0;
         }

   return 1;
}

long IsZero(const mat_ZZ_p& a)
{
   long n = a.NumRows();
   long i;

   for (i = 0; i < n; i++)
      if (!IsZero(a[i]))
         return 0;

   return 1;
}

void clear(mat_ZZ_p& x)
{
   long n = x.NumRows();
   long i;
   for (i = 0; i < n; i++)
      clear(x[i]);
}

mat_ZZ_p operator+(const mat_ZZ_p& a, const mat_ZZ_p& b)
{
   mat_ZZ_p res;
   add(res, a, b);
   NTL_OPT_RETURN(mat_ZZ_p, res);
}

mat_ZZ_p operator*(const mat_ZZ_p& a, const mat_ZZ_p& b)
{
   mat_ZZ_p res;
   mul(res, a, b);
   NTL_OPT_RETURN(mat_ZZ_p, res);
}

mat_ZZ_p operator-(const mat_ZZ_p& a, const mat_ZZ_p& b)
{
   mat_ZZ_p res;
   sub(res, a, b);
   NTL_OPT_RETURN(mat_ZZ_p, res);
}

mat_ZZ_p operator-(const mat_ZZ_p& a)
{
   mat_ZZ_p res;
   negate(res, a);
   NTL_OPT_RETURN(mat_ZZ_p, res);
}

vec_ZZ_p operator*(const mat_ZZ_p& a, const vec_ZZ_p& b)
{
   vec_ZZ_p res;
   mul_aux(res, a, b);
   NTL_OPT_RETURN(vec_ZZ_p, res);
}

vec_ZZ_p operator*(const vec_ZZ_p& a, const mat_ZZ_p& b)
{
   vec_ZZ_p res;
   mul_aux(res, a, b);
   NTL_OPT_RETURN(vec_ZZ_p, res);
}

void inv(mat_ZZ_p& X, const mat_ZZ_p& A)
{
   ZZ_p d;
   inv(d, X, A);
   if (d == 0) ArithmeticError("inv: non-invertible matrix");
}

void power(mat_ZZ_p& X, const mat_ZZ_p& A, const ZZ& e)
{
   if (A.NumRows() != A.NumCols()) LogicError("power: non-square matrix");

   if (e == 0) {
      ident(X, A.NumRows());
      return;
   }

   mat_ZZ_p T1, T2;
   long i, k;

   k = NumBits(e);
   T1 = A;

   for (i = k-2; i >= 0; i--) {
      sqr(T2, T1);
      if (bit(e, i))
         mul(T1, T2, A);
      else
         T1 = T2;
   }

   if (e < 0)
      inv(X, T1);
   else
      X = T1;
}

void random(mat_ZZ_p& x, long n, long m)
{
   x.SetDims(n, m);
   for (long i = 0; i < n; i++) random(x[i], m);
}

NTL_END_IMPL


#include <boost/multiprecision/cpp_dec_float.hpp>


/// @file
/// @copyright The code is licensed under the BSD License
///            <http://opensource.org/licenses/BSD-2-Clause>,
///            Copyright (c) 2013-2015 Alexandre Hamez.
/// @author Alexandre Hamez

#include <algorithm>
#include <cassert>
#include <functional> // function
#include <memory>
#include <random>
#include <unordered_map>
#include <vector>

#include <iosfwd>

#include <boost/iterator/transform_iterator.hpp>

#include "mc/classic/filter_ge.hh"
#include "mc/classic/filter_lt.hh"
#include "mc/classic/path_to.hh"
#include "mc/classic/post.hh"
#include "mc/classic/pre.hh"

namespace pnmc { namespace mc { namespace classic {

/*------------------------------------------------------------------------------------------------*/

using boost::make_transform_iterator;

/*------------------------------------------------------------------------------------------------*/

// Ordered by identifier position in SDD's order.
using state_type = std::vector<pn::valuation_type>;

state_type
one_state_of(const SDD& x)
{
  assert(x != zero());
  auto res = state_type{};
  auto path_generator = x.paths();
  const auto& path = path_generator.get(); // get only first path
  for (const auto& values : path)
  {
    res.emplace_back(*values.cbegin());
  }
  return res;
}

/*------------------------------------------------------------------------------------------------*/

using fn_type = std::function<flat_set (const flat_set&)>;

using identifier_map_type =
  std::unordered_map< std::string // transition identifier
                    , std::vector<fn_type>>; // operations to apply on a flat_set

using functions_type = std::vector<identifier_map_type>;

functions_type
mk_functions(const std::deque<std::string>& identifiers, const pn::net& net)
{
  auto map = std::unordered_map< std::string /*identifier*/, identifier_map_type>{};

  // intialize map for each identifier
  for (const auto& transition : net.transitions())
  {
    for (const auto& arc : transition.pre)
    {
      map.emplace(arc.first, identifier_map_type{});
    }
    for (const auto& arc : transition.post)
    {
      map.emplace(arc.first, identifier_map_type{});
    }
  }

  const auto add_fn = [&](const auto& arc, const auto& tid, const auto& fn)
  {
    const auto search = map.find(arc.first); // search variable
    assert(search != end(map));
    auto& identifier_map = search->second;
    const auto transition_search = identifier_map.find(tid);
    if (transition_search == end(identifier_map))
    {
      identifier_map.emplace_hint(end(identifier_map), tid, std::vector<fn_type>{fn});
    }
    else
    {
      transition_search->second.emplace_back(fn);
    }
  };

  for (const auto& transition : net.transitions())
  {
    for (const auto& arc : transition.pre)
    {
      const auto fn = [&]
      {
        switch (arc.second.kind)
        {
          case pn::arc::type::normal:
            return pre{arc.second.weight};

          default:
            throw std::runtime_error(__PRETTY_FUNCTION__);
        }
      }();
      add_fn(arc, transition.name, fn);
    }

    for (const auto& arc : transition.post)
    {
      const auto fn = [&]
      {
        switch (arc.second.kind)
        {
          case pn::arc::type::normal:
            return post{arc.second.weight};

          default:
            throw std::runtime_error(__PRETTY_FUNCTION__);
        }
      }();
      add_fn(arc, transition.name, fn);
    }
  }

  auto res = functions_type{};
  for (const auto& id : identifiers)
  {
    res.emplace_back(std::move(map.at(id)));
  }
  return res;
}

/*------------------------------------------------------------------------------------------------*/

struct visitor
{
  const std::string& transition;

  SDD
  operator()( const sdd::hierarchical_node<sdd_conf>&, state_type::const_iterator
            , functions_type::const_iterator)
  const
  {
    throw std::runtime_error(__PRETTY_FUNCTION__);
  }

  SDD
  operator()( const sdd::flat_node<sdd_conf>& n, state_type::const_iterator state_it
            , functions_type::const_iterator functions_it)
  const
  {
    const auto& operations_search = functions_it->find(transition);
    if (operations_search == (*functions_it).cend())
    {
      for (const auto& arc : n)
      {
        if (arc.valuation().find(*state_it) == arc.valuation().end())
        {
          continue; // to next arc
        }
        const auto rec = visit( *this, arc.successor(), std::next(state_it)
                              , std::next(functions_it));
        if (rec != zero())
        {
          return {n.variable(), arc.valuation(), rec}; // Hit!
        }
        else
        {
          continue; // to next arc
        }
      }
    }
    else // transition is applied on the current level
    {
      for (const auto& arc : n)
      {
        for (const auto v : arc.valuation())
        {
          auto values = flat_set{v};
          for (const auto& operation : operations_search->second)
          {
            values = operation(values);
            if (values.empty()) {break;} // No need to apply more operations
          }
          if (values.empty())
          {
            continue; // to next valuation
          }
          else
          {
            if (*values.cbegin() == *state_it)
            {
              const auto rec = visit( *this, arc.successor(), std::next(state_it)
                                    , std::next(functions_it));
              if (rec != zero())
              {
                return {n.variable(), arc.valuation(), rec}; // Hit!
              }
            }
          }
        }
      }
    }
    return zero();
  }

  SDD
  operator()( const sdd::one_terminal<sdd_conf>&, state_type::const_iterator
            , functions_type::const_iterator)
  const
  {
    return one();
  }

  SDD
  operator()( const sdd::zero_terminal<sdd_conf>&, state_type::const_iterator
            , functions_type::const_iterator)
  const
  {
    throw std::runtime_error(__PRETTY_FUNCTION__);
  }
};

/*------------------------------------------------------------------------------------------------*/

std::deque<std::pair<std::string, SDD>>
shortest_path( const order& o, const SDD& initial, const SDD& targets, const pn::net& net
             , const std::multimap<homomorphism, std::string>& operands)
{
  auto identifiers = std::deque<std::string>{};
  o.flat(std::back_inserter(identifiers));

  auto top_bottom = [&]
  {
    const auto key = [](const auto& kv){return kv.first;};
    const auto firing_rule = rewrite(o, sum( o
                                           , make_transform_iterator(begin(operands), key)
                                           , make_transform_iterator(end(operands), key)));

    auto current_layer = initial;
    auto acc = current_layer; // Keep all computed states to avoid to recompute states.

    auto res = std::deque<SDD>{};
    res.push_back(initial);

    // BFS
    while (true)
    {
      const auto next_layer = firing_rule(o, current_layer) // get successors of the current layer
                            - acc; // remove all states that were computed in a previous layer;

      const auto test_targets = next_layer & targets;
      if (test_targets != zero()) // some targets found
      {
        res.emplace_back(test_targets); // add found targets
        break;
      }

      acc += next_layer;
      res.emplace_back(next_layer);
      current_layer = next_layer; // move on to the next layer
    }

    return res;
  }();
  // We now have the shortest paths to the targets that are the closest to the initial state, along
  // with some other possible paths which do not lead to the targets.

  std::random_device rd;
  std::mt19937 g(rd());

  const auto id = [](const auto& transition){return transition.name;};
  auto transitions = std::vector<std::string>{ make_transform_iterator(begin(net.transitions()), id)
                                             , make_transform_iterator(end(net.transitions()), id)};

  const auto functions = mk_functions(identifiers, net);
  auto trace = std::deque<std::pair<std::string, SDD>>{};
  trace.emplace_front("", *top_bottom.rbegin());
  for (auto rcit = std::next(top_bottom.rbegin()); rcit != top_bottom.rend(); ++rcit)
  {
    std::shuffle(begin(transitions), end(transitions), g);

    const auto next_states = *std::prev(rcit);
    const auto a_state = one_state_of(next_states);

    for (const auto transition : transitions)
    {
      const auto res = visit( visitor{transition}
                            , *rcit            // current states to filter
                            , a_state.cbegin() // next state target
                            , functions.cbegin());
      if (res != zero())
      {
        *rcit = res;
        trace.front().first = transition;
        trace.emplace_front("", *rcit);
        break;
      }
    }
  }
  return trace;
}

/*------------------------------------------------------------------------------------------------*/

}}} // namespace pnmc::mc::classic