/**************************************************************************
 * Copyright (c) 2017-2019 by the mfmg authors                            *
 * All rights reserved.                                                   *
 *                                                                        *
 * This file is part of the mfmg library. mfmg is distributed under a BSD *
 * 3-clause license. For the licensing terms see the LICENSE file in the  *
 * top-level directory                                                    *
 *                                                                        *
 * SPDX-License-Identifier: BSD-3-Clause                                  *
 **************************************************************************/

#define BOOST_TEST_MODULE hierarchy

#include <mfmg/common/hierarchy.hpp>
#include <mfmg/dealii/dealii_trilinos_matrix_operator.hpp>

#include <deal.II/base/conditional_ostream.h>
#include <deal.II/dofs/dof_accessor.h>
#include <deal.II/fe/fe_q.h>
#include <deal.II/lac/la_parallel_vector.h>
#include <deal.II/lac/precondition.h>
#include <deal.II/lac/solver_cg.h>
#include <deal.II/lac/trilinos_linear_operator.h>
#include <deal.II/lac/trilinos_vector.h>

#include <EpetraExt_MatrixMatrix.h>

#include <boost/property_tree/info_parser.hpp>
#include <boost/test/data/monomorphic.hpp>
#include <boost/test/data/test_case.hpp>

#include <random>

#include "laplace.hpp"
#include "laplace_matrix_free.hpp"
#include "main.cc"
#include "test_hierarchy_helpers.hpp"

// In a lot of places in this file, we explicitly set the number of threads to
// use since some of the tests cases don't work with multiple threads yet. This
// works as follow: dealii::MultithreadInfo::set_thread_limit sets the number of
// threads to the minimum its the argument and the environment variable
// DEAL_II_NUM_THREADS. TBB is asked for the maximum nuber of cores if the
// minimum is dealii::numbers::invalid_unsigned_int. Consequently,
// MultithreadInfo::set_thread_limit(numbers::invalid_unsigned int) sets the
// number of threads to the ones specified in the environment variable.

namespace bdata = boost::unit_test::data;
namespace tt = boost::test_tools;

template <typename MeshEvaluator>
double test_mf(std::shared_ptr<boost::property_tree::ptree> params)
{
  using DVector = dealii::LinearAlgebra::distributed::Vector<double>;
  int constexpr dim = MeshEvaluator::_dim;

  MPI_Comm comm = MPI_COMM_WORLD;

  dealii::ConditionalOStream pcout(
      std::cout, dealii::Utilities::MPI::this_mpi_process(comm) == 0);

  auto material_property =
      MaterialPropertyFactory<dim>::create_material_property(
          params->get<std::string>("material_property.type"));
  Source<dim> source;

  auto laplace_ptree = params->get_child("laplace");
  int constexpr fe_degree = 1;
  LaplaceMatrixFree<dim, fe_degree, double> mf_laplace(comm);
  mf_laplace.setup_system(laplace_ptree, *material_property);

  auto const locally_owned_dofs = mf_laplace._locally_owned_dofs;
  DVector solution(locally_owned_dofs, comm);
  DVector rhs(mf_laplace._system_rhs);

  std::default_random_engine generator;
  std::uniform_real_distribution<typename DVector::value_type> distribution(0.,
                                                                            1.);
  for (auto const index : locally_owned_dofs)
  {
    // Make the solution satisfy the Dirichlet conditions because these should
    // be treated outside the preconditioner
    if (mf_laplace._constraints.is_constrained(index))
      solution[index] = 0.;
    else
      solution[index] = distribution(generator);
  }

  auto evaluator =
      std::make_shared<TestMFMeshEvaluator<dim, fe_degree, double>>(
          mf_laplace._dof_handler, mf_laplace._constraints,
          mf_laplace._laplace_operator, material_property);
  mfmg::Hierarchy<DVector> hierarchy(comm, evaluator, params);

  // We want to do 20 V-cycle iterations. The rhs of is zero.
  // Use D(istributed)Vector because deal has its own Vector class
  DVector residual(rhs);
  unsigned int const n_cycles = 20;
  std::vector<double> res(n_cycles + 1);

  mf_laplace._laplace_operator.vmult(residual, solution);
  residual.sadd(-1., 1., rhs);
  auto const residual0_norm = residual.l2_norm();

  std::cout << std::scientific;
  // pcout << "#0: " << 1.0 << std::endl;
  res[0] = 1.0;
  for (unsigned int i = 0; i < n_cycles; ++i)
  {
    hierarchy.apply(rhs, solution);

    mf_laplace._laplace_operator.vmult(residual, solution);
    residual.sadd(-1., 1., rhs);
    double rel_residual = residual.l2_norm() / residual0_norm;
    // pcout << "#" << i + 1 << ": " << rel_residual << std::endl;
    res[i + 1] = rel_residual;
  }

  double const conv_rate = res[n_cycles] / res[n_cycles - 1];
  pcout << "Convergence rate: " << std::fixed << std::setprecision(2)
        << conv_rate << std::endl;

  return conv_rate;
}

template <typename MeshEvaluator>
double test(std::shared_ptr<boost::property_tree::ptree> params)
{
  using DVector = dealii::LinearAlgebra::distributed::Vector<double>;
  int constexpr dim = MeshEvaluator::_dim;

  MPI_Comm comm = MPI_COMM_WORLD;

  dealii::ConditionalOStream pcout(
      std::cout, dealii::Utilities::MPI::this_mpi_process(comm) == 0);

  auto material_property =
      MaterialPropertyFactory<dim>::create_material_property(
          params->get<std::string>("material_property.type"));
  Source<dim> source;

  auto laplace_ptree = params->get_child("laplace");
  auto fe_degree = laplace_ptree.get<unsigned>("fe_degree", 1);
  std::cout << "FE degree: " << fe_degree << std::endl;
  Laplace<dim, DVector> laplace(comm, fe_degree);
  laplace.setup_system(laplace_ptree);
  laplace.assemble_system(source, *material_property);

  auto const &a = laplace._system_matrix;
  auto const locally_owned_dofs = laplace._locally_owned_dofs;
  DVector solution(locally_owned_dofs, comm);
  DVector rhs(laplace._system_rhs);

  std::default_random_engine generator;
  std::uniform_real_distribution<typename DVector::value_type> distribution(0.,
                                                                            1.);
  for (auto const index : locally_owned_dofs)
  {
    // Make the solution satisfy the Dirichlet conditions because these should
    // be treated outside the preconditioner
    if (laplace._constraints.is_constrained(index))
      solution[index] = 0.;
    else
      solution[index] = distribution(generator);
  }

  auto evaluator = std::make_shared<TestMeshEvaluator<MeshEvaluator>>(
      laplace._dof_handler, laplace._constraints, fe_degree, a,
      material_property);
  mfmg::Hierarchy<DVector> hierarchy(comm, evaluator, params);

  // We want to do 20 V-cycle iterations. The rhs of is zero.
  // Use D(istributed)Vector because deal has its own Vector class
  DVector residual(rhs);
  unsigned int const n_cycles = 20;
  std::vector<double> res(n_cycles + 1);

  a.vmult(residual, solution);
  residual.sadd(-1., 1., rhs);
  auto const residual0_norm = residual.l2_norm();

  std::cout << std::scientific;
  // pcout << "#0: " << 1.0 << std::endl;
  res[0] = 1.0;
  for (unsigned int i = 0; i < n_cycles; ++i)
  {
    hierarchy.apply(rhs, solution);

    a.vmult(residual, solution);
    residual.sadd(-1., 1., rhs);
    double rel_residual = residual.l2_norm() / residual0_norm;
    // pcout << "#" << i + 1 << ": " << rel_residual << std::endl;
    res[i + 1] = rel_residual;
  }

  double const conv_rate = res[n_cycles] / res[n_cycles - 1];
  pcout << "Convergence rate: " << std::fixed << std::setprecision(2)
        << conv_rate << std::endl;

  return conv_rate;
}

BOOST_AUTO_TEST_CASE(benchmark)
{
  dealii::MultithreadInfo::set_thread_limit(1);

  auto params = std::make_shared<boost::property_tree::ptree>();
  boost::property_tree::info_parser::read_info("hierarchy_input.info", *params);

  test<mfmg::DealIIMeshEvaluator<2>>(params);
}

BOOST_AUTO_TEST_CASE(benchmark_mf)
{
  dealii::MultithreadInfo::set_thread_limit(
      dealii::numbers::invalid_unsigned_int);

  auto params = std::make_shared<boost::property_tree::ptree>();
  boost::property_tree::info_parser::read_info("hierarchy_input.info", *params);
  params->put("smoother.type", "Chebyshev");

  test_mf<mfmg::DealIIMatrixFreeMeshEvaluator<2>>(params);
}

typedef std::tuple<mfmg::DealIIMeshEvaluator<2>,
                   mfmg::DealIIMatrixFreeMeshEvaluator<2>>
    mesh_evaluator_types;
BOOST_AUTO_TEST_CASE_TEMPLATE(ml, MeshEvaluator, mesh_evaluator_types)
{
  dealii::MultithreadInfo::set_thread_limit(1);

  auto params = std::make_shared<boost::property_tree::ptree>();
  boost::property_tree::info_parser::read_info("hierarchy_input.info", *params);
  bool is_matrix_free = false;
  if (mfmg::is_matrix_free<MeshEvaluator>::value)
  {
    params->put("smoother.type", "Chebyshev");
    is_matrix_free = true;
  }

  double gold_rate = is_matrix_free ? test_mf<MeshEvaluator>(params)
                                    : test<MeshEvaluator>(params);

  params->put("coarse.type", "ml");
  params->put("coarse.params.smoother: type", "symmetric Gauss-Seidel");
  params->put("coarse.params.max levels", 1);
  params->put("coarse.params.coarse: type", "Amesos-KLU");

  double ml_rate = is_matrix_free ? test_mf<MeshEvaluator>(params)
                                  : test<MeshEvaluator>(params);

  BOOST_TEST(ml_rate == gold_rate, tt::tolerance(1e-9));

  params->put("coarse.params.max levels", 2);

  ml_rate = is_matrix_free ? test_mf<MeshEvaluator>(params)
                           : test<MeshEvaluator>(params);

  BOOST_TEST(ml_rate > gold_rate);
}

BOOST_AUTO_TEST_CASE(raw_ml)
{
  auto params = std::make_shared<boost::property_tree::ptree>();
  boost::property_tree::info_parser::read_info("hierarchy_input.info", *params);
  // "Uncover" params for running raw ML
  for (auto const &x : params->get_child("hidden"))
  {
    params->put_child(x.first, x.second);
  }
  BOOST_TEST(test<mfmg::DealIIMeshEvaluator<2>>(params) < 0.2);
}

BOOST_DATA_TEST_CASE(
    hierarchy_3d,
    bdata::make({"hyper_cube", "hyper_ball"}) * bdata::make({false, true}) *
        bdata::make({"None", "Reverse Cuthill_McKee"}) *
        bdata::make<std::string>({"DealIIMeshEvaluator",
                                  "DealIIMatrixFreeMeshEvaluator"}) *
        bdata::make<std::string>({"arpack", "lanczos", "anasazi"}),
    mesh, distort_random, reordering, mesh_evaluator_type, eigensolver)
{
  dealii::MultithreadInfo::set_thread_limit(1);

  // TODO investigate why there is large difference in convergence rate when
  // running in parallel.
  if (dealii::Utilities::MPI::n_mpi_processes(MPI_COMM_WORLD) == 1)
  {
    int constexpr dim = 3;
    auto params = std::make_shared<boost::property_tree::ptree>();
    boost::property_tree::info_parser::read_info("hierarchy_input.info",
                                                 *params);

    bool const is_matrix_free =
        mesh_evaluator_type == "DealIIMatrixFreeMeshEvaluator";
    if (is_matrix_free)
    {
      params->put("smoother.type", "Chebyshev");
    }

    params->put("eigensolver.type", eigensolver);
    params->put("agglomeration.nz", 2);
    params->put("laplace.n_refinements", 2);
    params->put("laplace.mesh", mesh);
    params->put("laplace.distort_random", distort_random);
    params->put("laplace.reordering", reordering);

    if (is_matrix_free && eigensolver == "arpack")
    {
      // skipping since ARPACK eigensolver is not available in matrix-free mode
      std::cout << "skip\n";
      return;
    }

    double const conv_rate =
        is_matrix_free
            ? test_mf<mfmg::DealIIMatrixFreeMeshEvaluator<dim>>(params)
            : test<mfmg::DealIIMeshEvaluator<dim>>(params);

    std::string distort_random_str =
        (distort_random ? "distort" : "no_distort");
    std::string matrix_free_str =
        (is_matrix_free ? "matrix-free" : "matrix-full");

    // This is a gold standard test. Not the greatest but it makes sure we don't
    // break the code
    //   (geometry, distortion, reordering, eigensolver, matrix-free)
    std::map<std::tuple<std::string, std::string, std::string, std::string,
                        std::string>,
             double>
        ref_solution;
    // clang-format off
    ref_solution[std::make_tuple("hyper_cube" , "no_distort" , "None"                  , "arpack"  , "matrix-full")] = 0.0235237332;
    ref_solution[std::make_tuple("hyper_cube" , "no_distort" , "Reverse Cuthill_McKee" , "arpack"  , "matrix-full")] = 0.0235237332;
    ref_solution[std::make_tuple("hyper_cube" , "distort"    , "None"                  , "arpack"  , "matrix-full")] = 0.0220847464;
    ref_solution[std::make_tuple("hyper_cube" , "distort"    , "Reverse Cuthill_McKee" , "arpack"  , "matrix-full")] = 0.0220847464;
    ref_solution[std::make_tuple("hyper_ball" , "no_distort" , "None"                  , "arpack"  , "matrix-full")] = 0.1149021369;
    ref_solution[std::make_tuple("hyper_ball" , "no_distort" , "Reverse Cuthill_McKee" , "arpack"  , "matrix-full")] = 0.1149021369;
    ref_solution[std::make_tuple("hyper_ball" , "distort"    , "None"                  , "arpack"  , "matrix-full")] = 0.1023844058;
    ref_solution[std::make_tuple("hyper_ball" , "distort"    , "Reverse Cuthill_McKee" , "arpack"  , "matrix-full")] = 0.1023844058;

    ref_solution[std::make_tuple("hyper_cube" , "no_distort" , "None"                  , "lanczos" , "matrix-full")] = 0.0235237332;
    ref_solution[std::make_tuple("hyper_cube" , "no_distort" , "None"                  , "lanczos" , "matrix-free")] = 0.0880045475;
    ref_solution[std::make_tuple("hyper_cube" , "no_distort" , "Reverse Cuthill_McKee" , "lanczos" , "matrix-full")] = 0.0235237332;
    ref_solution[std::make_tuple("hyper_cube" , "no_distort" , "Reverse Cuthill_McKee" , "lanczos" , "matrix-free")] = 0.0880045475;
    ref_solution[std::make_tuple("hyper_cube" , "distort"    , "None"                  , "lanczos" , "matrix-full")] = 0.0220847463;
    ref_solution[std::make_tuple("hyper_cube" , "distort"    , "None"                  , "lanczos" , "matrix-free")] = 0.0859227356;
    ref_solution[std::make_tuple("hyper_cube" , "distort"    , "Reverse Cuthill_McKee" , "lanczos" , "matrix-full")] = 0.0220847463;
    ref_solution[std::make_tuple("hyper_cube" , "distort"    , "Reverse Cuthill_McKee" , "lanczos" , "matrix-free")] = 0.0859227356;
    ref_solution[std::make_tuple("hyper_ball" , "no_distort" , "None"                  , "lanczos" , "matrix-full")] = 0.1148148381;
    ref_solution[std::make_tuple("hyper_ball" , "no_distort" , "None"                  , "lanczos" , "matrix-free")] = 0.2981146185;
    ref_solution[std::make_tuple("hyper_ball" , "no_distort" , "Reverse Cuthill_McKee" , "lanczos" , "matrix-full")] = 0.1148148381;
    ref_solution[std::make_tuple("hyper_ball" , "no_distort" , "Reverse Cuthill_McKee" , "lanczos" , "matrix-free")] = 0.2981146185;
    ref_solution[std::make_tuple("hyper_ball" , "distort"    , "None"                  , "lanczos" , "matrix-full")] = 0.1024523994;
    ref_solution[std::make_tuple("hyper_ball" , "distort"    , "None"                  , "lanczos" , "matrix-free")] = 0.2990825376;
    ref_solution[std::make_tuple("hyper_ball" , "distort"    , "Reverse Cuthill_McKee" , "lanczos" , "matrix-full")] = 0.1024523994;
    ref_solution[std::make_tuple("hyper_ball" , "distort"    , "Reverse Cuthill_McKee" , "lanczos" , "matrix-free")] = 0.2990825376;

    ref_solution[std::make_tuple("hyper_cube" , "no_distort" , "None"                  , "anasazi" , "matrix-full")] = 0.0219765243;
    ref_solution[std::make_tuple("hyper_cube" , "no_distort" , "None"                  , "anasazi" , "matrix-free")] = 0.0868251131;
    ref_solution[std::make_tuple("hyper_cube" , "no_distort" , "Reverse Cuthill_McKee" , "anasazi" , "matrix-full")] = 0.0219765243;
    ref_solution[std::make_tuple("hyper_cube" , "no_distort" , "Reverse Cuthill_McKee" , "anasazi" , "matrix-free")] = 0.0868251131;
    ref_solution[std::make_tuple("hyper_cube" , "distort"    , "None"                  , "anasazi" , "matrix-full")] = 0.0224382767;
    ref_solution[std::make_tuple("hyper_cube" , "distort"    , "None"                  , "anasazi" , "matrix-free")] = 0.0864777651;
    ref_solution[std::make_tuple("hyper_cube" , "distort"    , "Reverse Cuthill_McKee" , "anasazi" , "matrix-full")] = 0.0224382767;
    ref_solution[std::make_tuple("hyper_cube" , "distort"    , "Reverse Cuthill_McKee" , "anasazi" , "matrix-free")] = 0.0864777651;
    ref_solution[std::make_tuple("hyper_ball" , "no_distort" , "None"                  , "anasazi" , "matrix-full")] = 0.1060819598;
    ref_solution[std::make_tuple("hyper_ball" , "no_distort" , "None"                  , "anasazi" , "matrix-free")] = 0.3062882615;
    ref_solution[std::make_tuple("hyper_ball" , "no_distort" , "Reverse Cuthill_McKee" , "anasazi" , "matrix-full")] = 0.1060819598;
    ref_solution[std::make_tuple("hyper_ball" , "no_distort" , "Reverse Cuthill_McKee" , "anasazi" , "matrix-free")] = 0.3062882615;
    ref_solution[std::make_tuple("hyper_ball" , "distort"    , "None"                  , "anasazi" , "matrix-full")] = 0.0970940159;
    ref_solution[std::make_tuple("hyper_ball" , "distort"    , "None"                  , "anasazi" , "matrix-free")] = 0.3029292535;
    ref_solution[std::make_tuple("hyper_ball" , "distort"    , "Reverse Cuthill_McKee" , "anasazi" , "matrix-full")] = 0.0970940159;
    ref_solution[std::make_tuple("hyper_ball" , "distort"    , "Reverse Cuthill_McKee" , "anasazi" , "matrix-free")] = 0.3029292535;
    // clang-format on

    BOOST_TEST(
        conv_rate ==
            ref_solution[std::make_tuple(mesh, distort_random_str, reordering,
                                         eigensolver, matrix_free_str)],
        tt::tolerance(1e-2));
  }
}

BOOST_AUTO_TEST_CASE_TEMPLATE(zoltan, MeshEvaluator, mesh_evaluator_types)
{
  dealii::MultithreadInfo::set_thread_limit(
      dealii::numbers::invalid_unsigned_int);

  if (dealii::Utilities::MPI::n_mpi_processes(MPI_COMM_WORLD) == 1)
  {
    auto params = std::make_shared<boost::property_tree::ptree>();
    boost::property_tree::info_parser::read_info("hierarchy_input.info",
                                                 *params);

    bool constexpr is_matrix_free = mfmg::is_matrix_free<MeshEvaluator>::value;
    if (is_matrix_free)
    {
      params->put("smoother.type", "Chebyshev");
    }

    params->put("agglomeration.partitioner", "zoltan");
    params->put("agglomeration.n_agglomerates", 4);

    // This is a gold standard test. Not the greatest but it makes sure we don't
    // break the code
    double const ref_solution = is_matrix_free ? 0.895602663 : 0.836618927;
    double const conv_rate = is_matrix_free ? test_mf<MeshEvaluator>(params)
                                            : test<MeshEvaluator>(params);
    BOOST_TEST(conv_rate == ref_solution, tt::tolerance(1e-6));
  }
}

// n_local_rows passed to gimme_a_matrix() must be the same on all processes
dealii::TrilinosWrappers::SparseMatrix
gimme_a_matrix(unsigned int n_local_rows, unsigned int n_entries_per_row)
{
  unsigned int const comm_size =
      dealii::Utilities::MPI::n_mpi_processes(MPI_COMM_WORLD);
  unsigned int const comm_rank =
      dealii::Utilities::MPI::this_mpi_process(MPI_COMM_WORLD);
  unsigned int const size = comm_size * n_local_rows;
  dealii::IndexSet parallel_partitioning(size);
  unsigned int const local_offset = comm_rank * n_local_rows;
  for (unsigned int i = 0; i < n_local_rows; ++i)
    parallel_partitioning.add_index(i + local_offset);
  parallel_partitioning.compress();
  dealii::TrilinosWrappers::SparseMatrix sparse_matrix(
      parallel_partitioning, parallel_partitioning, MPI_COMM_WORLD);

  unsigned int nnz = 0;
  std::default_random_engine generator;
  for (unsigned int i = 0; i < n_local_rows; ++i)
  {
    std::uniform_int_distribution<int> distribution(0, size - 1);
    std::set<int> column_indices;
    for (unsigned int j = 0; j < n_entries_per_row; ++j)
    {
      int column_index = distribution(generator);
      int row_index = i + local_offset;
      sparse_matrix.set(row_index, column_index,
                        static_cast<double>(row_index + column_index));
      column_indices.insert(column_index);
    }
    nnz += column_indices.size();
  }
  sparse_matrix.compress(dealii::VectorOperation::insert);
  return sparse_matrix;
}

dealii::TrilinosWrappers::SparseMatrix gimme_identity(unsigned int n_local_rows)
{
  unsigned int const comm_size =
      dealii::Utilities::MPI::n_mpi_processes(MPI_COMM_WORLD);
  unsigned int const comm_rank =
      dealii::Utilities::MPI::this_mpi_process(MPI_COMM_WORLD);
  unsigned int const size = comm_size * n_local_rows;
  dealii::IndexSet parallel_partitioning(size);
  unsigned int const local_offset = comm_rank * n_local_rows;
  for (unsigned int i = 0; i < n_local_rows; ++i)
    parallel_partitioning.add_index(i + local_offset);
  parallel_partitioning.compress();

  dealii::TrilinosWrappers::SparseMatrix identity_matrix(
      parallel_partitioning, parallel_partitioning, MPI_COMM_WORLD);

  for (unsigned int i = 0; i < n_local_rows; ++i)
  {
    unsigned int const global_i = i + local_offset;
    identity_matrix.set(global_i, global_i, 1.);
  }
  identity_matrix.compress(dealii::VectorOperation::insert);
  return identity_matrix;
}

BOOST_AUTO_TEST_CASE(fast_multiply_transpose)
{
  dealii::MultithreadInfo::set_thread_limit(
      dealii::numbers::invalid_unsigned_int);

  MPI_Comm comm = MPI_COMM_WORLD;

  using DVector = dealii::LinearAlgebra::distributed::Vector<double>;
  int constexpr dim = mfmg::DealIIMeshEvaluator<2>::_dim;

  auto params = std::make_shared<boost::property_tree::ptree>();
  boost::property_tree::info_parser::read_info("hierarchy_input.info", *params);
  params->put("eigensolver.type", "lapack");
  auto material_property =
      MaterialPropertyFactory<dim>::create_material_property(
          params->get<std::string>("material_property.type"));
  Source<dim> source;
  auto laplace_ptree = params->get_child("laplace");

  // Compute the ref AP
  Laplace<dim, DVector> ref_laplace(comm, 1);
  ref_laplace.setup_system(laplace_ptree);
  ref_laplace.assemble_system(source, *material_property);

  auto ref_evaluator =
      std::make_shared<TestMeshEvaluator<mfmg::DealIIMeshEvaluator<2>>>(
          ref_laplace._dof_handler, ref_laplace._constraints, 1,
          ref_laplace._system_matrix, material_property);
  std::unique_ptr<mfmg::HierarchyHelpers<DVector>> ref_hierarchy_helpers(
      new mfmg::DealIIHierarchyHelpers<dim, DVector>());
  auto ref_a = ref_hierarchy_helpers->get_global_operator(ref_evaluator);
  auto ref_restrictor =
      ref_hierarchy_helpers->build_restrictor(comm, ref_evaluator, params);
  auto ref_ap = ref_a->multiply_transpose(ref_restrictor);
  auto ref_matrix =
      std::dynamic_pointer_cast<mfmg::DealIITrilinosMatrixOperator<DVector>>(
          ref_ap)
          ->get_matrix();

  // Compute the fast AP
  params->put("fast_ap", true);
  Laplace<dim, DVector> fast_laplace(comm, 1);
  fast_laplace.setup_system(laplace_ptree);
  fast_laplace.assemble_system(source, *material_property);

  auto fast_evaluator =
      std::make_shared<TestMeshEvaluator<mfmg::DealIIMeshEvaluator<2>>>(
          fast_laplace._dof_handler, fast_laplace._constraints, 1,
          fast_laplace._system_matrix, material_property);
  std::unique_ptr<mfmg::HierarchyHelpers<DVector>> fast_hierarchy_helpers(
      new mfmg::DealIIHierarchyHelpers<dim, DVector>());

  auto fast_restrictor =
      fast_hierarchy_helpers->build_restrictor(comm, fast_evaluator, params);

  auto fast_ap = fast_hierarchy_helpers->fast_multiply_transpose();
  auto fast_matrix =
      std::dynamic_pointer_cast<mfmg::DealIITrilinosMatrixOperator<DVector>>(
          fast_ap)
          ->get_matrix();

  // Compare the two matrices obtained
  for (unsigned int i = 0; i < ref_matrix->m(); ++i)
    for (unsigned int j = 0; j < ref_matrix->n(); ++j)
      if (ref_matrix->el(i, j) > 1e-10)
        BOOST_TEST(fast_matrix->el(i, j) == ref_matrix->el(i, j),
                   tt::tolerance(1e-9));
}

BOOST_AUTO_TEST_CASE(fast_multiply_transpose_mf)
{
  dealii::MultithreadInfo::set_thread_limit(1);

  MPI_Comm comm = MPI_COMM_WORLD;

  using DVector = dealii::LinearAlgebra::distributed::Vector<double>;
  int constexpr dim = mfmg::DealIIMatrixFreeMeshEvaluator<2>::_dim;

  auto params = std::make_shared<boost::property_tree::ptree>();
  boost::property_tree::info_parser::read_info("hierarchy_input.info", *params);
  params->put("eigensolver.type", "lanczos");
  auto material_property =
      MaterialPropertyFactory<dim>::create_material_property(
          params->get<std::string>("material_property.type"));
  Source<dim> source;
  auto laplace_ptree = params->get_child("laplace");

  // Compute the ref AP
  Laplace<dim, DVector> ref_laplace(comm, 1);
  ref_laplace.setup_system(laplace_ptree);
  ref_laplace.assemble_system(source, *material_property);

  auto ref_evaluator =
      std::make_shared<TestMeshEvaluator<mfmg::DealIIMeshEvaluator<2>>>(
          ref_laplace._dof_handler, ref_laplace._constraints, 1,
          ref_laplace._system_matrix, material_property);
  std::unique_ptr<mfmg::HierarchyHelpers<DVector>> ref_hierarchy_helpers(
      new mfmg::DealIIHierarchyHelpers<dim, DVector>());
  auto ref_a = ref_hierarchy_helpers->get_global_operator(ref_evaluator);
  auto ref_restrictor =
      ref_hierarchy_helpers->build_restrictor(comm, ref_evaluator, params);
  auto ref_ap = ref_a->multiply_transpose(ref_restrictor);
  auto ref_matrix =
      std::dynamic_pointer_cast<mfmg::DealIITrilinosMatrixOperator<DVector>>(
          ref_ap)
          ->get_matrix();

  // Compute the fast AP
  params->put("fast_ap", true);
  int constexpr fe_degree = 1;
  LaplaceMatrixFree<dim, fe_degree, double> fast_laplace(comm);
  fast_laplace.setup_system(laplace_ptree, *material_property);

  auto fast_evaluator =
      std::make_shared<TestMFMeshEvaluator<2, fe_degree, double>>(
          fast_laplace._dof_handler, fast_laplace._constraints,
          fast_laplace._laplace_operator, material_property);
  std::unique_ptr<mfmg::HierarchyHelpers<DVector>> fast_hierarchy_helpers(
      new mfmg::DealIIMatrixFreeHierarchyHelpers<dim, DVector>());

  auto fast_restrictor =
      fast_hierarchy_helpers->build_restrictor(comm, fast_evaluator, params);

  auto fast_ap = fast_hierarchy_helpers->fast_multiply_transpose();
  auto fast_matrix =
      std::dynamic_pointer_cast<mfmg::DealIITrilinosMatrixOperator<DVector>>(
          fast_ap)
          ->get_matrix();

  // Compare the two matrices obtained
  for (unsigned int i = 0; i < ref_matrix->m(); ++i)
    for (unsigned int j = 0; j < ref_matrix->n(); ++j)
      if (ref_matrix->el(i, j) > 1e-10)
        BOOST_TEST(fast_matrix->el(i, j) == ref_matrix->el(i, j),
                   tt::tolerance(1e-9));
}


#ifndef SM_AMA_MATRIX_IO_HPP
#define SM_AMA_MATRIX_IO_HPP

#include <string>
#include <map>
#include <set>
#include <boost/static_assert.hpp>
#include <boost/filesystem.hpp>
#include <sm/assert_macros.hpp>
#include <Eigen/Core>

// This code only works if a double is 8 bytes.
BOOST_STATIC_ASSERT(sizeof(double) == 8);
BOOST_STATIC_ASSERT(sizeof(std::string::value_type) == 1);

namespace sm {

    SM_DEFINE_EXCEPTION(MatrixArchiveException,std::runtime_error);

    class MatrixArchive{
    public:
      typedef std::map< std::string, Eigen::MatrixXd > matrix_map_t;
      typedef std::map< std::string, std::string > string_map_t;

      MatrixArchive();
      ~MatrixArchive();

      // clears the matrix and strings archive.
      void clear();
      // clears a specific value from the archive.
      void clear(std::string const & entryName);
      // gets the number of matrices or strings in the archive.
      size_t size() const;
      // gets the number of matrices in the archive.
      size_t sizeMatrices() const;
      // gets the number of strings in the archive.
      size_t sizeStrings() const;

      matrix_map_t::const_iterator begin() const;
      matrix_map_t::const_iterator end() const;
      matrix_map_t::const_iterator find(std::string const & name) const;

      // Loads matrices from a file into the archive.
      void load(const std::string & amaFilePath);
      void load(boost::filesystem::path const & amaFilePath);
      void load(boost::filesystem::path const & amaFilePath, std::set<std::string> const & validNames);

      // Saves matrices from a file into the archive.
      void save(const std::string & amaFilePath) const;
      void save(boost::filesystem::path const & amaFilePath) const;
      void save(boost::filesystem::path const & amaFilePath, std::set<std::string> const & validNames) const;
      void save(std::ostream & fout, std::set<std::string> const & validNames) const;

      // Appends matrices to a file.
      void append(std::string const & amaFilePath) const;
      void append(boost::filesystem::path const & amaFilePath) const;
      void append(boost::filesystem::path const & amaFilePath, std::set<std::string> const & validNames) const;

      template<typename Derived>
      void setMatrix(std::string const & matrixName, Eigen::MatrixBase<Derived> const & matrix);
      template<typename Derived>
      void setVector(std::string const & matrixName, Eigen::MatrixBase<Derived> const & matrix);

      void setMatrixXd(std::string const & matrixName, Eigen::MatrixXd const & matrix);
      void setVectorXd(std::string const & matrixName, Eigen::VectorXd const & matrix);
      void setScalar(std::string const & scalarName, double scalar);

      void setString(std::string const & stringName, const std::string & value);

      void getMatrix(std::string const & matrixName, Eigen::MatrixXd & outMatrix) const;
      const Eigen::MatrixXd & getMatrix(std::string const & matrixName) const;
      Eigen::MatrixXd & getMatrix(std::string const & matrixName);
      Eigen::MatrixXd & createMatrix(std::string const & matrixName, int rows, int cols, bool overwriteExisting = false);
      void getVector(std::string const & vectorName, Eigen::VectorXd & outVector) const;
      void getScalar(std::string const & scalarName, double & outScalar) const;
      double getScalar(std::string const & scalarName) const;

      void getString(std::string const & stringName, std::string & stringValue) const;
      const std::string & getString(std::string const & stringName) const;
      std::string & getString(std::string const & stringName);

      const string_map_t & getStrings() const;

      bool isSystemLittleEndian() const;

      size_t maxNameSize();
    private:
      static const size_t s_fixedNameSize;
      static const char s_magicCharStartAMatrixBlock;
      static const char s_magicCharStartAStringBlock;
      static const char s_magicCharEnd;

      void writeMatrixBlock(std::ostream & fout, std::string const & name, Eigen::MatrixXd const & matrix) const;
      void writeMatrixBlockSwapBytes(std::ostream & fout, std::string const & name, Eigen::MatrixXd const & matrix) const;
      void writeStringBlock(std::ostream & fout, std::string const & name, std::string const & stringValue) const;

      void readMatrix(std::istream & fin, Eigen::MatrixXd & matrix) const;
      void readMatrixSwapBytes(std::istream & fin, std::string & name, Eigen::MatrixXd & matrix) const;
      void readString(std::istream & fin, std::string & stringValue) const;

      enum BlockType {
        MATRIX,
        STRING
      };
      BlockType readBlock(std::istream & fin, std::string & name, Eigen::MatrixXd & matrix, std::string & stringValue) const;

      void validateName(std::string const & name, sm::source_file_pos const & sfp) const;
      void writeName(std::ostream & fout, std::string const & name) const;

      void saveMatrices(std::ostream & fout, std::set<std::string> const & validNames) const;
      void saveStrings(std::ostream & fout, std::set<std::string> const & validNames) const;

      matrix_map_t m_values;
      string_map_t m_strings;

    }; // end class MatrixArchive

    template<typename Derived>
    void MatrixArchive::setMatrix(std::string const & matrixName, Eigen::MatrixBase<Derived> const & matrix)
    {
      if(m_strings.count(matrixName) > 0){
        m_strings.erase(matrixName);
      }
      validateName(matrixName, SM_SOURCE_FILE_POS);
      m_values[matrixName] = matrix;
    }

    template<typename Derived>
    void MatrixArchive::setVector(std::string const & vectorName, Eigen::MatrixBase<Derived> const & vector)
    {
      SM_ASSERT_EQ(MatrixArchiveException, vector.cols(),1, "The input must be a column vector");
      validateName(vectorName, SM_SOURCE_FILE_POS);
      m_values[vectorName] = vector;
    }

  } // end namespace sm

#endif


// Software License for MTL
//
// Copyright (c) 2007 The Trustees of Indiana University.
//               2008 Dresden University of Technology and the Trustees of Indiana University.
//               2010 SimuNova UG (haftungsbeschränkt), www.simunova.com.
// All rights reserved.
// Authors: Peter Gottschling and Andrew Lumsdaine
//
// This file is part of the Matrix Template Library
//
// See also license.mtl.txt in the distribution.

#ifndef MATH_ACCUMULATE_INCLUDE
#define MATH_ACCUMULATE_INCLUDE

#include <concepts>

#include <boost/numeric/linear_algebra/identity.hpp>
#include <boost/numeric/linear_algebra/new_concepts.hpp>

namespace math {

// Dispatching between simple and unrolled version
template <std::InputIterator Iter, std::CopyConstructible Value, typename Op>
     requires std::Callable2<Op, Value, Value>
           && std::CopyAssignable<Value, std::Callable2<Op, Value, Value>::result_type>
Value inline accumulate(Iter first, Iter last, Value init, Op op)
{
    // std::cout << "Simple accumulate\n";
    for (; first != last; ++first)
	init= op(init, Value(*first));
    return init;
}

template <std::RandomAccessIterator Iter, std::CopyConstructible Value, typename Op>
    requires std::Callable2<Op, Value, Value>
          && std::CopyAssignable<Value, std::Callable2<Op, Value, Value>::result_type>
          && Commutative<Op, Value>
          && Monoid<Op, Value>
          && std::Convertible<Monoid<Op, Value>::identity_result_type, Value>
Value inline accumulate(Iter first, Iter last, Value init, Op op)
{
    // std::cout << "Unrolled accumulate\n";
    typedef typename std::RandomAccessIterator<Iter>::difference_type difference_type;
    Value            t0= identity(op, init), t1= identity(op, init),
	             t2= identity(op, init), t3= init;
    difference_type  size= last - first, bsize= size >> 2 << 2, i;

    for (i= 0; i < bsize; i+= 4) {
	t0= op(t0, Value(first[i]));
	t1= op(t1, Value(first[i+1]));
	t2= op(t2, Value(first[i+2]));
	t3= op(t3, Value(first[i+3]));
    }
    for (; i < size; i++)
	t0= op(t0, Value(first[i]));

    t0= op(t0, t1), t2= op(t2, t3), t0= op(t0, t2);
    return t0;
}

} // namespace math

#endif // MATH_ACCUMULATE_INCLUDE


//---------------------------------------------------------------------------//
//!
//! \file   tstMaxwellFissionDistribution.cpp
//! \author Aaron Tumulak
//! \brief  Maxwell Fission distribution unit tests.
//!
//---------------------------------------------------------------------------//

// Std Lib Includes
#include <iostream>

// Boost Includes
#include <boost/units/systems/si.hpp>
#include <boost/units/systems/cgs.hpp>
#include <boost/units/io.hpp>

// FRENSIE Includes
#include "Utility_MaxwellFissionDistribution.hpp"
#include "Utility_PhysicalConstants.hpp"
#include "Utility_RandomNumberGenerator.hpp"
#include "Utility_UnitTraits.hpp"
#include "Utility_QuantityTraits.hpp"
#include "Utility_ElectronVoltUnit.hpp"
#include "Utility_UnitTestHarnessWithMain.hpp"
#include "ArchiveTestHelpers.hpp"

//---------------------------------------------------------------------------//
// Testing Types
//---------------------------------------------------------------------------//

using boost::units::quantity;
using namespace Utility::Units;
namespace si = boost::units::si;
namespace cgs = boost::units::cgs;

typedef TestArchiveHelper::TestArchives TestArchives;

typedef std::tuple<
  std::tuple<si::energy,si::amount,cgs::energy,si::amount>,
  std::tuple<cgs::energy,si::amount,si::energy,si::amount>,
  std::tuple<si::energy,si::length,cgs::energy,cgs::length>,
  std::tuple<cgs::energy,cgs::length,si::energy,si::length>,
  std::tuple<si::energy,si::mass,cgs::energy,cgs::mass>,
  std::tuple<cgs::energy,cgs::mass,si::energy,si::mass>,
  std::tuple<si::energy,si::dimensionless,cgs::energy,cgs::dimensionless>,
  std::tuple<cgs::energy,cgs::dimensionless,si::energy,si::dimensionless>,
  std::tuple<si::energy,void*,cgs::energy,void*>,
  std::tuple<cgs::energy,void*,si::energy,void*>,
  std::tuple<ElectronVolt,si::amount,si::energy,si::amount>,
  std::tuple<ElectronVolt,si::amount,cgs::energy,si::amount>,
  std::tuple<ElectronVolt,si::amount,KiloElectronVolt,si::amount>,
  std::tuple<ElectronVolt,si::amount,MegaElectronVolt,si::amount>,
  std::tuple<KiloElectronVolt,si::amount,si::energy,si::amount>,
  std::tuple<KiloElectronVolt,si::amount,cgs::energy,si::amount>,
  std::tuple<KiloElectronVolt,si::amount,ElectronVolt,si::amount>,
  std::tuple<KiloElectronVolt,si::amount,MegaElectronVolt,si::amount>,
  std::tuple<MegaElectronVolt,si::amount,si::energy,si::amount>,
  std::tuple<MegaElectronVolt,si::amount,cgs::energy,si::amount>,
  std::tuple<MegaElectronVolt,si::amount,ElectronVolt,si::amount>,
  std::tuple<MegaElectronVolt,si::amount,KiloElectronVolt,si::amount>,
  std::tuple<void*,MegaElectronVolt,void*,KiloElectronVolt>
 > TestUnitTypeQuads;

//---------------------------------------------------------------------------//
// Testing Variables
//---------------------------------------------------------------------------//

std::shared_ptr<Utility::UnivariateDistribution> distribution(
				 new Utility::MaxwellFissionDistribution( 1.0, 1.0, 0.1 ) );

std::shared_ptr<Utility::UnitAwareUnivariateDistribution<MegaElectronVolt,si::amount> >
  unit_aware_distribution( new Utility::UnitAwareMaxwellFissionDistribution<MegaElectronVolt,si::amount>( 1e6*eV, 1e3*keV, 0.1*MeV ) );

//---------------------------------------------------------------------------//
// Tests.
//---------------------------------------------------------------------------//
// Check that the distribution can be evaluated
FRENSIE_UNIT_TEST( MaxwellFissionDistribution, evaluate )
{
  double test_value_1 = 0.0 ;
  double test_value_2 = sqrt( 0.9 )*exp( -0.9 );

  FRENSIE_CHECK_EQUAL( distribution->evaluate( 0.0 ), test_value_1 );
  FRENSIE_CHECK_EQUAL( distribution->evaluate( 0.9 ), test_value_2 );
  FRENSIE_CHECK_EQUAL( distribution->evaluate( 1.0 ), 0.0 );
}

//---------------------------------------------------------------------------//
// Check that the unit-aware distribution can be evaluated
FRENSIE_UNIT_TEST( UnitAwareMaxwellFissionDistribution, evaluate )
{
  FRENSIE_CHECK_EQUAL( unit_aware_distribution->evaluate( 0.0*MeV ),
		       0.0*si::mole );

  double scale_factor = sqrt( 0.9 )*exp( -0.9 );

  FRENSIE_CHECK_EQUAL( unit_aware_distribution->evaluate( 0.9*MeV ),
                       scale_factor*si::mole );

  FRENSIE_CHECK_EQUAL( unit_aware_distribution->evaluate( 1.0*MeV ),
		       0.0*si::mole );
}

//---------------------------------------------------------------------------//
// Check that the PDF can be evaluated
FRENSIE_UNIT_TEST( MaxwellFissionDistribution, evaluatePDF )
{
  double test_value_1 = 0.0;
  double test_value_2 = sqrt( 0.9 )*exp( -0.9 )/
    (sqrt(Utility::PhysicalConstants::pi)*0.5*erf(sqrt(0.9)) -
     sqrt(0.9)*exp(-0.9));

  FRENSIE_CHECK_EQUAL( distribution->evaluatePDF( 0.0 ), test_value_1 );
  FRENSIE_CHECK_EQUAL( distribution->evaluatePDF( 0.9 ), test_value_2 );
  FRENSIE_CHECK_EQUAL( distribution->evaluatePDF( 1.0 ), 0.0 );
}

//---------------------------------------------------------------------------//
// Check that the unit-aware PDF can be evaluated
FRENSIE_UNIT_TEST( UnitAwareMaxwellFissionDistribution, evaluatePDF )
{
  double scale_factor = sqrt( 0.9 )*exp( -0.9 )/
    (sqrt(Utility::PhysicalConstants::pi)*0.5*erf(sqrt(0.9)) -
     sqrt(0.9)*exp(-0.9));

  FRENSIE_CHECK_EQUAL( unit_aware_distribution->evaluatePDF( 0.0*MeV ),
		       0.0/MeV );
  FRENSIE_CHECK_EQUAL( unit_aware_distribution->evaluatePDF( 0.9*MeV ),
                       scale_factor/MeV );
  FRENSIE_CHECK_EQUAL( unit_aware_distribution->evaluatePDF( 1.0*MeV ),
		       0.0/MeV );
}

//---------------------------------------------------------------------------//
// Check that the distribution can be sampled using OpenMC method
FRENSIE_UNIT_TEST( MaxwellFissionDistribution, sample )
{
  std::vector<double> fake_stream( 9 );
  fake_stream[0] = 0.9526660807223237;
  fake_stream[1] = 0.2628340201570147;
  fake_stream[2] = 0.4684607693058543;
  fake_stream[3] = 0.2948581100330813;
  fake_stream[4] = 0.4810986077957187;
  fake_stream[5] = 0.8453449553051804;
  fake_stream[6] = 0.4656289304981871;
  fake_stream[7] = 0.7470315175721101;
  fake_stream[8] = 0.5418708973800604;

  Utility::RandomNumberGenerator::setFakeStream( fake_stream );

  double sample = distribution->sample();
  FRENSIE_CHECK_FLOATING_EQUALITY( sample, 0.78269807500829, 1e-14 );

  sample = distribution->sample();
  FRENSIE_CHECK_FLOATING_EQUALITY( sample, 0.89106359701991, 1e-14 );

  Utility::RandomNumberGenerator::unsetFakeStream();
}

//---------------------------------------------------------------------------//
// Check that the unit-aware distribution can be sampled using OpenMC method
FRENSIE_UNIT_TEST( UnitAwareMaxwellFissionDistribution, sample )
{
  std::vector<double> fake_stream( 9 );
  fake_stream[0] = 0.9526660807223237;
  fake_stream[1] = 0.2628340201570147;
  fake_stream[2] = 0.4684607693058543;
  fake_stream[3] = 0.2948581100330813;
  fake_stream[4] = 0.4810986077957187;
  fake_stream[5] = 0.8453449553051804;
  fake_stream[6] = 0.4656289304981871;
  fake_stream[7] = 0.7470315175721101;
  fake_stream[8] = 0.5418708973800604;

  Utility::RandomNumberGenerator::setFakeStream( fake_stream );

  quantity<MegaElectronVolt> sample = unit_aware_distribution->sample();
  FRENSIE_CHECK_FLOATING_EQUALITY( sample, 0.78269807500829*MeV, 1e-14 );

  sample = unit_aware_distribution->sample();
  FRENSIE_CHECK_FLOATING_EQUALITY( sample, 0.89106359701991*MeV, 1e-14 );

  Utility::RandomNumberGenerator::unsetFakeStream();
}

//---------------------------------------------------------------------------//
// Check that the distribution can be sampled using OpenMC method and the
// trials can be recorded
FRENSIE_UNIT_TEST( MaxwellFissionDistribution, sampleAndRecordTrials )
{
  std::vector<double> fake_stream( 9 );
  fake_stream[0] = 0.9526660807223237;
  fake_stream[1] = 0.2628340201570147;
  fake_stream[2] = 0.4684607693058543;
  fake_stream[3] = 0.2948581100330813;
  fake_stream[4] = 0.4810986077957187;
  fake_stream[5] = 0.8453449553051804;
  fake_stream[6] = 0.4656289304981871;
  fake_stream[7] = 0.7470315175721101;
  fake_stream[8] = 0.5418708973800604;

  Utility::RandomNumberGenerator::setFakeStream( fake_stream );

  Utility::DistributionTraits::Counter trials = 0;

  double sample = distribution->sampleAndRecordTrials( trials );
  FRENSIE_CHECK_FLOATING_EQUALITY( sample, 0.78269807500829, 1e-14 );
  FRENSIE_CHECK_EQUAL( trials, 1.0 );

  sample = distribution->sampleAndRecordTrials( trials );
  FRENSIE_CHECK_FLOATING_EQUALITY( sample, 0.89106359701991, 1e-14 );
  FRENSIE_CHECK_EQUAL( trials, 3.0 );

  Utility::RandomNumberGenerator::unsetFakeStream();
}

//---------------------------------------------------------------------------//
// Check that the unit-aware distribution can be sampled using OpenMC method
// and the trials can be recorded
FRENSIE_UNIT_TEST( UnitAwareMaxwellFissionDistribution, sampleAndRecordTrials )
{
  std::vector<double> fake_stream( 9 );
  fake_stream[0] = 0.9526660807223237;
  fake_stream[1] = 0.2628340201570147;
  fake_stream[2] = 0.4684607693058543;
  fake_stream[3] = 0.2948581100330813;
  fake_stream[4] = 0.4810986077957187;
  fake_stream[5] = 0.8453449553051804;
  fake_stream[6] = 0.4656289304981871;
  fake_stream[7] = 0.7470315175721101;
  fake_stream[8] = 0.5418708973800604;

  Utility::RandomNumberGenerator::setFakeStream( fake_stream );

  Utility::DistributionTraits::Counter trials = 0;

  quantity<MegaElectronVolt> sample =
    unit_aware_distribution->sampleAndRecordTrials( trials );
  FRENSIE_CHECK_FLOATING_EQUALITY( sample, 0.78269807500829*MeV, 1e-14 );
  FRENSIE_CHECK_EQUAL( trials, 1.0 );

  sample = unit_aware_distribution->sampleAndRecordTrials( trials );
  FRENSIE_CHECK_FLOATING_EQUALITY( sample, 0.89106359701991*MeV, 1e-14 );
  FRENSIE_CHECK_EQUAL( trials, 3.0 );

  Utility::RandomNumberGenerator::unsetFakeStream();
}

//---------------------------------------------------------------------------//
// Check that the distribution can be sampled using OpenMC method, passing in
// parameters
FRENSIE_UNIT_TEST( MaxwellFissionDistribution,
		   sample_pass_parameters )
{
  std::vector<double> fake_stream( 9 );
  fake_stream[0] = 0.1324998063336137;
  fake_stream[1] = 0.9247996934252033;
  fake_stream[2] = 0.1918514900811418;
  fake_stream[3] = 0.3062640867105594;
  fake_stream[4] = 0.8754839687423933;
  fake_stream[5] = 0.2274300298185482;
  fake_stream[6] = 0.9336967385663441;
  fake_stream[7] = 0.2459018944829507;
  fake_stream[8] = 0.4770873202414019;

  Utility::RandomNumberGenerator::setFakeStream( fake_stream );

  double incident_energy, nuclear_temperature, restriction_energy, sample;

  incident_energy = 0.5;
  nuclear_temperature = 0.1;
  restriction_energy = 0.01;

  sample = Utility::MaxwellFissionDistribution::sample( incident_energy, nuclear_temperature, restriction_energy );
  FRENSIE_CHECK_FLOATING_EQUALITY( sample, 0.20924646054839, 1e-13 );

  incident_energy = 0.75;
  nuclear_temperature = 0.5;
  restriction_energy = 0.25;

  sample = Utility::MaxwellFissionDistribution::sample( incident_energy, nuclear_temperature, restriction_energy );
  FRENSIE_CHECK_FLOATING_EQUALITY( sample, 0.41023025568120, 1e-13 );

  Utility::RandomNumberGenerator::unsetFakeStream();
}

//---------------------------------------------------------------------------//
// Check that the unit-aware distribution can be sampled using OpenMC method,
// passing in parameters
FRENSIE_UNIT_TEST( UnitAwareMaxwellFissionDistribution,
		   sample_pass_parameters )
{
  std::vector<double> fake_stream( 9 );
  fake_stream[0] = 0.1324998063336137;
  fake_stream[1] = 0.9247996934252033;
  fake_stream[2] = 0.1918514900811418;
  fake_stream[3] = 0.3062640867105594;
  fake_stream[4] = 0.8754839687423933;
  fake_stream[5] = 0.2274300298185482;
  fake_stream[6] = 0.9336967385663441;
  fake_stream[7] = 0.2459018944829507;
  fake_stream[8] = 0.4770873202414019;

  Utility::RandomNumberGenerator::setFakeStream( fake_stream );

  quantity<MegaElectronVolt> incident_energy, nuclear_temperature,
    restriction_energy, sample;

  incident_energy = 0.5*MeV;
  nuclear_temperature = 0.1*MeV;
  restriction_energy = 0.01*MeV;

  sample = Utility::UnitAwareMaxwellFissionDistribution<MegaElectronVolt>::sample( incident_energy, nuclear_temperature, restriction_energy );
  FRENSIE_CHECK_FLOATING_EQUALITY( sample, 0.20924646054839*MeV, 1e-13 );

  incident_energy = 0.75*MeV;
  nuclear_temperature = 0.5*MeV;
  restriction_energy = 0.25*MeV;

  sample = Utility::UnitAwareMaxwellFissionDistribution<MegaElectronVolt>::sample( incident_energy, nuclear_temperature, restriction_energy );
  FRENSIE_CHECK_FLOATING_EQUALITY( sample, 0.41023025568120*MeV, 1e-13 );

  Utility::RandomNumberGenerator::unsetFakeStream();
}

//---------------------------------------------------------------------------//
// Check that the distribution can be sampled using OpenMC method, passing in
// parameters
FRENSIE_UNIT_TEST( MaxwellFissionDistribution,
		   sampleAndRecordTrials_pass_parameters )
{
  std::vector<double> fake_stream( 9 );
  fake_stream[0] = 0.1324998063336137;
  fake_stream[1] = 0.9247996934252033;
  fake_stream[2] = 0.1918514900811418;
  fake_stream[3] = 0.3062640867105594;
  fake_stream[4] = 0.8754839687423933;
  fake_stream[5] = 0.2274300298185482;
  fake_stream[6] = 0.9336967385663441;
  fake_stream[7] = 0.2459018944829507;
  fake_stream[8] = 0.4770873202414019;

  Utility::RandomNumberGenerator::setFakeStream( fake_stream );

  double incident_energy, nuclear_temperature, restriction_energy, sample;
  Utility::DistributionTraits::Counter trials = 0;

  incident_energy = 0.5;
  nuclear_temperature = 0.1;
  restriction_energy = 0.01;

  sample = Utility::MaxwellFissionDistribution::sampleAndRecordTrials(incident_energy, nuclear_temperature, restriction_energy, trials);
  FRENSIE_CHECK_FLOATING_EQUALITY( sample, 0.20924646054839, 1e-13 );

  incident_energy = 0.75;
  nuclear_temperature = 0.5;
  restriction_energy = 0.25;

  sample = Utility::MaxwellFissionDistribution::sampleAndRecordTrials(incident_energy, nuclear_temperature, restriction_energy, trials);
  FRENSIE_CHECK_FLOATING_EQUALITY( sample, 0.41023025568120, 1e-13 );

  Utility::RandomNumberGenerator::unsetFakeStream();
}

//---------------------------------------------------------------------------//
// Check that the unit-aware distribution can be sampled using OpenMC method,
// passing in parameters
FRENSIE_UNIT_TEST( UnitAwareMaxwellFissionDistribution,
		   sampleAndRecordTrials_pass_parameters )
{
  std::vector<double> fake_stream( 9 );
  fake_stream[0] = 0.1324998063336137;
  fake_stream[1] = 0.9247996934252033;
  fake_stream[2] = 0.1918514900811418;
  fake_stream[3] = 0.3062640867105594;
  fake_stream[4] = 0.8754839687423933;
  fake_stream[5] = 0.2274300298185482;
  fake_stream[6] = 0.9336967385663441;
  fake_stream[7] = 0.2459018944829507;
  fake_stream[8] = 0.4770873202414019;

  Utility::RandomNumberGenerator::setFakeStream( fake_stream );

  quantity<MegaElectronVolt> incident_energy, nuclear_temperature,
    restriction_energy, sample;
  Utility::DistributionTraits::Counter trials = 0;

  incident_energy = 0.5*MeV;
  nuclear_temperature = 0.1*MeV;
  restriction_energy = 0.01*MeV;

  sample = Utility::UnitAwareMaxwellFissionDistribution<MegaElectronVolt,void>::sampleAndRecordTrials(incident_energy, nuclear_temperature, restriction_energy, trials);
  FRENSIE_CHECK_FLOATING_EQUALITY( sample, 0.20924646054839*MeV, 1e-13 );

  incident_energy = 0.75*MeV;
  nuclear_temperature = 0.5*MeV;
  restriction_energy = 0.25*MeV;

  sample = Utility::UnitAwareMaxwellFissionDistribution<MegaElectronVolt,si::amount>::sampleAndRecordTrials(incident_energy, nuclear_temperature, restriction_energy, trials);
  FRENSIE_CHECK_FLOATING_EQUALITY( sample, 0.41023025568120*MeV, 1e-13 );

  Utility::RandomNumberGenerator::unsetFakeStream();
}

//---------------------------------------------------------------------------//
// Check that the upper bound of the distribution independent variable can be
// returned
FRENSIE_UNIT_TEST( MaxwellFissionDistribution, getUpperBoundOfIndepVar )
{
  FRENSIE_CHECK_EQUAL( distribution->getUpperBoundOfIndepVar(), 0.9 );
}

//---------------------------------------------------------------------------//
// Check that the upper bound of the unit-aware distribution independent
// variable can be returned
FRENSIE_UNIT_TEST( UnitAwareMaxwellFissionDistribution,
		   getUpperBoundOfIndepVar )
{
  FRENSIE_CHECK_EQUAL( unit_aware_distribution->getUpperBoundOfIndepVar(),
		       0.9*MeV );
}

//---------------------------------------------------------------------------//
// Check that the lower bound of the distribution independent variable can be
// returned
FRENSIE_UNIT_TEST( MaxwellFissionDistribution, getLowerBoundOfIndepVar )
{
  FRENSIE_CHECK_EQUAL( distribution->getLowerBoundOfIndepVar(), 0.0 );
}

//---------------------------------------------------------------------------//
// Check that the lower bound of the unit-aware distribution independent
// variable can be returned
FRENSIE_UNIT_TEST( UnitAwareMaxwellFissionDistribution,
		   getLowerBoundOfIndepVar )
{
  FRENSIE_CHECK_EQUAL( unit_aware_distribution->getLowerBoundOfIndepVar(),
		       0.0*MeV );
}

//---------------------------------------------------------------------------//
// Check that the distribution type can be returned
FRENSIE_UNIT_TEST( MaxwellFissionDistribution, getDistributionType )
{
  FRENSIE_CHECK_EQUAL( distribution->getDistributionType(),
		       Utility::MAXWELL_FISSION_DISTRIBUTION );
}

//---------------------------------------------------------------------------//
// Check that the unit-aware distribution type can be returned
FRENSIE_UNIT_TEST( UnitAwareMaxwellFissionDistribution, getDistributionType )
{
  FRENSIE_CHECK_EQUAL( unit_aware_distribution->getDistributionType(),
		       Utility::MAXWELL_FISSION_DISTRIBUTION );
}

//---------------------------------------------------------------------------//
// Check if the distribution is tabular
FRENSIE_UNIT_TEST( MaxwellFissionDistribution, isTabular )
{
  FRENSIE_CHECK( !distribution->isTabular() );
}

//---------------------------------------------------------------------------//
// Check if the unit-aware distribution is tabular
FRENSIE_UNIT_TEST( UnitAwareMaxwellFissionDistribution, isTabular )
{
  FRENSIE_CHECK( !unit_aware_distribution->isTabular() );
}

//---------------------------------------------------------------------------//
// Check if the distribution is continuous
FRENSIE_UNIT_TEST( MaxwellFissionDistribution, isContinuous )
{
  FRENSIE_CHECK( distribution->isContinuous() );
}

//---------------------------------------------------------------------------//
// Check if the unit-aware distribution is continuous
FRENSIE_UNIT_TEST( UnitAwareMaxwellFissionDistribution, isContinuous )
{
  FRENSIE_CHECK( unit_aware_distribution->isContinuous() );
}

//---------------------------------------------------------------------------//
// Check if the distribution is compatible with the interpolation type
FRENSIE_UNIT_TEST( MaxwellFissionDistribution, isCompatibleWithInterpType )
{
  FRENSIE_CHECK( distribution->isCompatibleWithInterpType<Utility::LinLin>() );
  FRENSIE_CHECK( distribution->isCompatibleWithInterpType<Utility::LinLog>() );
  FRENSIE_CHECK( !distribution->isCompatibleWithInterpType<Utility::LogLin>() );
  FRENSIE_CHECK( !distribution->isCompatibleWithInterpType<Utility::LogLog>() );
}

//---------------------------------------------------------------------------//
// Check if the unit-aware distribution is compatible with the interp type
FRENSIE_UNIT_TEST( UnitAwareMaxwellFissionDistribution,
                   isCompatibleWithInterpType )
{
  FRENSIE_CHECK( unit_aware_distribution->isCompatibleWithInterpType<Utility::LinLin>() );
  FRENSIE_CHECK( unit_aware_distribution->isCompatibleWithInterpType<Utility::LinLog>() );
  FRENSIE_CHECK( !unit_aware_distribution->isCompatibleWithInterpType<Utility::LogLin>() );
  FRENSIE_CHECK( !unit_aware_distribution->isCompatibleWithInterpType<Utility::LogLog>() );
}

//---------------------------------------------------------------------------//
// Check that the distribution can be placed in a stream
FRENSIE_UNIT_TEST( MaxwellFissionDistribution, ostream_operator )
{
  std::ostringstream oss;

  oss << Utility::MaxwellFissionDistribution();

  Utility::VariantMap dist_data =
    Utility::fromString<Utility::VariantMap>( oss.str() );

  FRENSIE_CHECK_EQUAL( dist_data["type"].toString(),
                       "Maxwell Fission Distribution" );
  FRENSIE_CHECK_EQUAL( dist_data["independent unit"].toString(), "void", SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["dependent unit"].toString(), "void", SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["incident energy"].toDouble(), 1.0, SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["nuclear temp"].toDouble(), 1.0, SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["restriction energy"].toDouble(), 0.0, SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["multiplier"].toDouble(), 1.0, SHOW_LHS );

  oss.str( "" );
  oss.clear();

  oss << Utility::MaxwellFissionDistribution( 2.0 );

  dist_data = Utility::fromString<Utility::VariantMap>( oss.str() );

  FRENSIE_CHECK_EQUAL( dist_data["type"].toString(),
                       "Maxwell Fission Distribution" );
  FRENSIE_CHECK_EQUAL( dist_data["independent unit"].toString(), "void", SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["dependent unit"].toString(), "void", SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["incident energy"].toDouble(), 2.0, SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["nuclear temp"].toDouble(), 1.0, SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["restriction energy"].toDouble(), 0.0, SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["multiplier"].toDouble(), 1.0, SHOW_LHS );

  oss.str( "" );
  oss.clear();

  oss << Utility::MaxwellFissionDistribution( 3.0, 2.0 );

  dist_data = Utility::fromString<Utility::VariantMap>( oss.str() );

  FRENSIE_CHECK_EQUAL( dist_data["type"].toString(),
                       "Maxwell Fission Distribution" );
  FRENSIE_CHECK_EQUAL( dist_data["independent unit"].toString(), "void", SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["dependent unit"].toString(), "void", SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["incident energy"].toDouble(), 3.0, SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["nuclear temp"].toDouble(), 2.0, SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["restriction energy"].toDouble(), 0.0, SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["multiplier"].toDouble(), 1.0, SHOW_LHS );

  oss.str( "" );
  oss.clear();

  oss << Utility::MaxwellFissionDistribution( 3.0, 2.0, 1.0 );

  dist_data = Utility::fromString<Utility::VariantMap>( oss.str() );

  FRENSIE_CHECK_EQUAL( dist_data["type"].toString(),
                       "Maxwell Fission Distribution" );
  FRENSIE_CHECK_EQUAL( dist_data["independent unit"].toString(), "void", SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["dependent unit"].toString(), "void", SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["incident energy"].toDouble(), 3.0, SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["nuclear temp"].toDouble(), 2.0, SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["restriction energy"].toDouble(), 1.0, SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["multiplier"].toDouble(), 1.0, SHOW_LHS );

  oss.str( "" );
  oss.clear();

  oss << Utility::MaxwellFissionDistribution( 3.0, 2.0, 1.0, 10.0 );

  dist_data = Utility::fromString<Utility::VariantMap>( oss.str() );

  FRENSIE_CHECK_EQUAL( dist_data["type"].toString(),
                       "Maxwell Fission Distribution" );
  FRENSIE_CHECK_EQUAL( dist_data["independent unit"].toString(), "void", SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["dependent unit"].toString(), "void", SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["incident energy"].toDouble(), 3.0, SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["nuclear temp"].toDouble(), 2.0, SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["restriction energy"].toDouble(), 1.0, SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["multiplier"].toDouble(), 10.0, SHOW_LHS );

  oss.str( "" );
  oss.clear();

  oss << *distribution;

  dist_data = Utility::fromString<Utility::VariantMap>( oss.str() );

  FRENSIE_CHECK_EQUAL( dist_data["type"].toString(),
                       "Maxwell Fission Distribution" );
  FRENSIE_CHECK_EQUAL( dist_data["independent unit"].toString(), "void", SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["dependent unit"].toString(), "void", SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["incident energy"].toDouble(), 1.0, SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["nuclear temp"].toDouble(), 1.0, SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["restriction energy"].toDouble(), 0.1, SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["multiplier"].toDouble(), 1.0, SHOW_LHS );
}

//---------------------------------------------------------------------------//
// Check that the unit-aware distribution can be placed in a stream
FRENSIE_UNIT_TEST( UnitAwareMaxwellFissionDistribution, ostream_operator )
{
  std::ostringstream oss;

  oss << Utility::UnitAwareMaxwellFissionDistribution<ElectronVolt,si::amount>();

  Utility::VariantMap dist_data =
    Utility::fromString<Utility::VariantMap>( oss.str() );

  FRENSIE_CHECK_EQUAL( dist_data["type"].toString(),
                       "Maxwell Fission Distribution" );
  FRENSIE_CHECK_EQUAL( dist_data["independent unit"].toString(),
                       Utility::UnitTraits<ElectronVolt>::name(),
                       SHOW_BOTH );
  FRENSIE_CHECK_EQUAL( dist_data["dependent unit"].toString(),
                       Utility::UnitTraits<si::amount>::name(),
                       SHOW_BOTH );
  FRENSIE_CHECK_EQUAL( dist_data["incident energy"].toType<quantity<ElectronVolt> >(),
                       1.0*eV,
                       SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["nuclear temp"].toType<quantity<ElectronVolt> >(),
                       1.0*eV,
                       SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["restriction energy"].toType<quantity<ElectronVolt> >(),
                       0.0*eV,
                       SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["multiplier"].toDouble(), 1.0, SHOW_LHS );

  oss.str( "" );
  oss.clear();

  oss << Utility::UnitAwareMaxwellFissionDistribution<ElectronVolt,si::amount>( 2.0*eV );

  dist_data = Utility::fromString<Utility::VariantMap>( oss.str() );

  FRENSIE_CHECK_EQUAL( dist_data["type"].toString(),
                       "Maxwell Fission Distribution" );
  FRENSIE_CHECK_EQUAL( dist_data["independent unit"].toString(),
                       Utility::UnitTraits<ElectronVolt>::name(),
                       SHOW_BOTH );
  FRENSIE_CHECK_EQUAL( dist_data["dependent unit"].toString(),
                       Utility::UnitTraits<si::amount>::name(),
                       SHOW_BOTH );
  FRENSIE_CHECK_EQUAL( dist_data["incident energy"].toType<quantity<ElectronVolt> >(),
                       2.0*eV,
                       SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["nuclear temp"].toType<quantity<ElectronVolt> >(),
                       1.0*eV,
                       SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["restriction energy"].toType<quantity<ElectronVolt> >(),
                       0.0*eV,
                       SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["multiplier"].toDouble(), 1.0, SHOW_LHS );

  oss.str( "" );
  oss.clear();

  oss << Utility::UnitAwareMaxwellFissionDistribution<ElectronVolt,si::amount>( 3.0*eV, 2.0*eV );

  dist_data = Utility::fromString<Utility::VariantMap>( oss.str() );

  FRENSIE_CHECK_EQUAL( dist_data["type"].toString(),
                       "Maxwell Fission Distribution" );
  FRENSIE_CHECK_EQUAL( dist_data["independent unit"].toString(),
                       Utility::UnitTraits<ElectronVolt>::name(),
                       SHOW_BOTH );
  FRENSIE_CHECK_EQUAL( dist_data["dependent unit"].toString(),
                       Utility::UnitTraits<si::amount>::name(),
                       SHOW_BOTH );
  FRENSIE_CHECK_EQUAL( dist_data["incident energy"].toType<quantity<ElectronVolt> >(),
                       3.0*eV,
                       SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["nuclear temp"].toType<quantity<ElectronVolt> >(),
                       2.0*eV,
                       SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["restriction energy"].toType<quantity<ElectronVolt> >(),
                       0.0*eV,
                       SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["multiplier"].toDouble(), 1.0, SHOW_LHS );

  oss.str( "" );
  oss.clear();

  oss << Utility::UnitAwareMaxwellFissionDistribution<ElectronVolt,si::amount>( 3.0*eV, 2.0*eV, 1.0*eV );

  dist_data = Utility::fromString<Utility::VariantMap>( oss.str() );

  FRENSIE_CHECK_EQUAL( dist_data["type"].toString(),
                       "Maxwell Fission Distribution" );
  FRENSIE_CHECK_EQUAL( dist_data["independent unit"].toString(),
                       Utility::UnitTraits<ElectronVolt>::name(),
                       SHOW_BOTH );
  FRENSIE_CHECK_EQUAL( dist_data["dependent unit"].toString(),
                       Utility::UnitTraits<si::amount>::name(),
                       SHOW_BOTH );
  FRENSIE_CHECK_EQUAL( dist_data["incident energy"].toType<quantity<ElectronVolt> >(),
                       3.0*eV,
                       SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["nuclear temp"].toType<quantity<ElectronVolt> >(),
                       2.0*eV,
                       SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["restriction energy"].toType<quantity<ElectronVolt> >(),
                       1.0*eV,
                       SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["multiplier"].toDouble(), 1.0, SHOW_LHS );

  oss.str( "" );
  oss.clear();

  oss << Utility::UnitAwareMaxwellFissionDistribution<ElectronVolt,si::amount>( 3.0*eV, 2.0*eV, 1.0*eV, 10.0 );

  dist_data = Utility::fromString<Utility::VariantMap>( oss.str() );

  FRENSIE_CHECK_EQUAL( dist_data["type"].toString(),
                       "Maxwell Fission Distribution" );
  FRENSIE_CHECK_EQUAL( dist_data["independent unit"].toString(),
                       Utility::UnitTraits<ElectronVolt>::name(),
                       SHOW_BOTH );
  FRENSIE_CHECK_EQUAL( dist_data["dependent unit"].toString(),
                       Utility::UnitTraits<si::amount>::name(),
                       SHOW_BOTH );
  FRENSIE_CHECK_EQUAL( dist_data["incident energy"].toType<quantity<ElectronVolt> >(),
                       3.0*eV,
                       SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["nuclear temp"].toType<quantity<ElectronVolt> >(),
                       2.0*eV,
                       SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["restriction energy"].toType<quantity<ElectronVolt> >(),
                       1.0*eV,
                       SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["multiplier"].toDouble(), 10.0, SHOW_LHS );

  oss.str( "" );
  oss.clear();

  oss << *unit_aware_distribution;

  dist_data = Utility::fromString<Utility::VariantMap>( oss.str() );

  FRENSIE_CHECK_EQUAL( dist_data["type"].toString(),
                       "Maxwell Fission Distribution" );
  FRENSIE_CHECK_EQUAL( dist_data["independent unit"].toString(),
                       Utility::UnitTraits<MegaElectronVolt>::name(),
                       SHOW_BOTH );
  FRENSIE_CHECK_EQUAL( dist_data["dependent unit"].toString(),
                       Utility::UnitTraits<si::amount>::name(),
                       SHOW_BOTH );
  FRENSIE_CHECK_EQUAL( dist_data["incident energy"].toType<quantity<MegaElectronVolt> >(),
                       1.0*MeV,
                       SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["nuclear temp"].toType<quantity<MegaElectronVolt> >(),
                       1.0*MeV,
                       SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["restriction energy"].toType<quantity<MegaElectronVolt> >(),
                       0.1*MeV,
                       SHOW_LHS );
  FRENSIE_CHECK_EQUAL( dist_data["multiplier"].toDouble(), 1.0, SHOW_LHS );
}

//---------------------------------------------------------------------------//
// Check that a distribution can be archived
FRENSIE_UNIT_TEST_TEMPLATE_EXPAND( MaxwellFissionDistribution,
                                   archive,
                                   TestArchives )
{
  FETCH_TEMPLATE_PARAM( 0, RawOArchive );
  FETCH_TEMPLATE_PARAM( 1, RawIArchive );

  typedef typename std::remove_pointer<RawOArchive>::type OArchive;
  typedef typename std::remove_pointer<RawIArchive>::type IArchive;

  std::string archive_base_name( "test_maxwell_fission_dist" );
  std::ostringstream archive_ostream;

  // Create and archive some maxwell fission distributions
  {
    std::unique_ptr<OArchive> oarchive;

    createOArchive( archive_base_name, archive_ostream, oarchive );

    Utility::MaxwellFissionDistribution dist_a;
    Utility::MaxwellFissionDistribution dist_b( 3.0 );
    Utility::MaxwellFissionDistribution dist_c( 3.0, 2.0 );
    Utility::MaxwellFissionDistribution dist_d( 3.0, 2.0, 1.0 );
    Utility::MaxwellFissionDistribution dist_e( 3.0, 2.0, 1.0, 10.0 );

    FRENSIE_REQUIRE_NO_THROW(
                             (*oarchive) << BOOST_SERIALIZATION_NVP( dist_a )
                             );
    FRENSIE_REQUIRE_NO_THROW(
                             (*oarchive) << BOOST_SERIALIZATION_NVP( dist_b )
                             );
    FRENSIE_REQUIRE_NO_THROW(
                             (*oarchive) << BOOST_SERIALIZATION_NVP( dist_c )
                             );
    FRENSIE_REQUIRE_NO_THROW(
                             (*oarchive) << BOOST_SERIALIZATION_NVP( dist_d )
                             );
    FRENSIE_REQUIRE_NO_THROW(
                             (*oarchive) << BOOST_SERIALIZATION_NVP( dist_e )
                             );
    FRENSIE_REQUIRE_NO_THROW(
                             (*oarchive) << BOOST_SERIALIZATION_NVP( distribution )
                             );
  }

  // Copy the archive ostream to an istream
  std::istringstream archive_istream( archive_ostream.str() );

  // Load the archived distributions
  std::unique_ptr<IArchive> iarchive;

  createIArchive( archive_istream, iarchive );

  Utility::MaxwellFissionDistribution dist_a;

  FRENSIE_REQUIRE_NO_THROW(
                           (*iarchive) >> BOOST_SERIALIZATION_NVP( dist_a )
                           );
  FRENSIE_CHECK_EQUAL( dist_a, Utility::MaxwellFissionDistribution() );

  Utility::MaxwellFissionDistribution dist_b;

  FRENSIE_REQUIRE_NO_THROW(
                           (*iarchive) >> BOOST_SERIALIZATION_NVP( dist_b )
                           );
  FRENSIE_CHECK_EQUAL( dist_b, Utility::MaxwellFissionDistribution( 3.0 ) );

  Utility::MaxwellFissionDistribution dist_c;

  FRENSIE_REQUIRE_NO_THROW(
                           (*iarchive) >> BOOST_SERIALIZATION_NVP( dist_c )
                           );
  FRENSIE_CHECK_EQUAL( dist_c, Utility::MaxwellFissionDistribution( 3.0, 2.0 ) );

  Utility::MaxwellFissionDistribution dist_d;

  FRENSIE_REQUIRE_NO_THROW(
                           (*iarchive) >> BOOST_SERIALIZATION_NVP( dist_d )
                           );
  FRENSIE_CHECK_EQUAL( dist_d, Utility::MaxwellFissionDistribution( 3.0, 2.0, 1.0 ) );

  Utility::MaxwellFissionDistribution dist_e;

  FRENSIE_REQUIRE_NO_THROW(
                           (*iarchive) >> BOOST_SERIALIZATION_NVP( dist_e )
                           );
  FRENSIE_CHECK_EQUAL( dist_e, Utility::MaxwellFissionDistribution( 3.0, 2.0, 1.0, 10.0 ) );

  std::shared_ptr<Utility::UnivariateDistribution> shared_dist;

  FRENSIE_REQUIRE_NO_THROW( (*iarchive) >> boost::serialization::make_nvp( "distribution", shared_dist ) );
  FRENSIE_CHECK_EQUAL( *dynamic_cast<Utility::MaxwellFissionDistribution*>( shared_dist.get() ),
                       *dynamic_cast<Utility::MaxwellFissionDistribution*>( distribution.get() ) );
}

//---------------------------------------------------------------------------//
// Check that a unit-aware distribution can be archived
FRENSIE_UNIT_TEST_TEMPLATE_EXPAND( UnitAwareMaxwellFissionDistribution,
                                   archive,
                                   TestArchives )
{
  FETCH_TEMPLATE_PARAM( 0, RawOArchive );
  FETCH_TEMPLATE_PARAM( 1, RawIArchive );

  typedef typename std::remove_pointer<RawOArchive>::type OArchive;
  typedef typename std::remove_pointer<RawIArchive>::type IArchive;

  std::string archive_base_name( "test_unit_aware_maxwell_fission_dist" );
  std::ostringstream archive_ostream;

  // Create and archive some maxwell fission distributions
  {
    std::unique_ptr<OArchive> oarchive;

    createOArchive( archive_base_name, archive_ostream, oarchive );

    Utility::UnitAwareMaxwellFissionDistribution<MegaElectronVolt,si::amount> dist_a;
    Utility::UnitAwareMaxwellFissionDistribution<MegaElectronVolt,si::amount> dist_b( 3.0*MeV );
    Utility::UnitAwareMaxwellFissionDistribution<MegaElectronVolt,si::amount> dist_c( 3.0*MeV, 2.0*MeV );
    Utility::UnitAwareMaxwellFissionDistribution<MegaElectronVolt,si::amount> dist_d( 3.0*MeV, 2.0*MeV, 1.0*MeV );
    Utility::UnitAwareMaxwellFissionDistribution<MegaElectronVolt,si::amount> dist_e( 3.0*MeV, 2.0*MeV, 1.0*MeV, 10.0 );

    FRENSIE_REQUIRE_NO_THROW(
                             (*oarchive) << BOOST_SERIALIZATION_NVP( dist_a )
                             );
    FRENSIE_REQUIRE_NO_THROW(
                             (*oarchive) << BOOST_SERIALIZATION_NVP( dist_b )
                             );
    FRENSIE_REQUIRE_NO_THROW(
                             (*oarchive) << BOOST_SERIALIZATION_NVP( dist_c )
                             );
    FRENSIE_REQUIRE_NO_THROW(
                             (*oarchive) << BOOST_SERIALIZATION_NVP( dist_d )
                             );
    FRENSIE_REQUIRE_NO_THROW(
                             (*oarchive) << BOOST_SERIALIZATION_NVP( dist_e )
                             );
    FRENSIE_REQUIRE_NO_THROW(
                  (*oarchive) << BOOST_SERIALIZATION_NVP( unit_aware_distribution )
                             );
  }

  // Copy the archive ostream to an istream
  std::istringstream archive_istream( archive_ostream.str() );

  // Load the archived distributions
  std::unique_ptr<IArchive> iarchive;

  createIArchive( archive_istream, iarchive );

  Utility::UnitAwareMaxwellFissionDistribution<MegaElectronVolt,si::amount> dist_a;

  FRENSIE_REQUIRE_NO_THROW(
                           (*iarchive) >> BOOST_SERIALIZATION_NVP( dist_a )
                           );
  FRENSIE_CHECK_EQUAL( dist_a, (Utility::UnitAwareMaxwellFissionDistribution<MegaElectronVolt,si::amount>()) );

  Utility::UnitAwareMaxwellFissionDistribution<MegaElectronVolt,si::amount> dist_b;

  FRENSIE_REQUIRE_NO_THROW(
                           (*iarchive) >> BOOST_SERIALIZATION_NVP( dist_b )
                           );
  FRENSIE_CHECK_EQUAL( dist_b, (Utility::UnitAwareMaxwellFissionDistribution<MegaElectronVolt,si::amount>( 3.0*MeV )) );

  Utility::UnitAwareMaxwellFissionDistribution<MegaElectronVolt,si::amount> dist_c;

  FRENSIE_REQUIRE_NO_THROW(
                           (*iarchive) >> BOOST_SERIALIZATION_NVP( dist_c )
                           );
  FRENSIE_CHECK_EQUAL( dist_c, (Utility::UnitAwareMaxwellFissionDistribution<MegaElectronVolt,si::amount>( 3.0*MeV, 2.0*MeV )) );

  Utility::UnitAwareMaxwellFissionDistribution<MegaElectronVolt,si::amount> dist_d;

  FRENSIE_REQUIRE_NO_THROW(
                           (*iarchive) >> BOOST_SERIALIZATION_NVP( dist_d )
                           );
  FRENSIE_CHECK_EQUAL( dist_d, (Utility::UnitAwareMaxwellFissionDistribution<MegaElectronVolt,si::amount>( 3.0*MeV, 2.0*MeV, 1.0*MeV )) );

  Utility::UnitAwareMaxwellFissionDistribution<MegaElectronVolt,si::amount> dist_e;

  FRENSIE_REQUIRE_NO_THROW(
                           (*iarchive) >> BOOST_SERIALIZATION_NVP( dist_e )
                           );
  FRENSIE_CHECK_EQUAL( dist_e, (Utility::UnitAwareMaxwellFissionDistribution<MegaElectronVolt,si::amount>( 3.0*MeV, 2.0*MeV, 1.0*MeV, 10.0 )) );

  std::shared_ptr<Utility::UnitAwareUnivariateDistribution<MegaElectronVolt,si::amount> > shared_dist;

  FRENSIE_REQUIRE_NO_THROW( (*iarchive) >> boost::serialization::make_nvp( "distribution", shared_dist ) );
  FRENSIE_CHECK_EQUAL( (*dynamic_cast<Utility::UnitAwareMaxwellFissionDistribution<MegaElectronVolt,si::amount>*>( shared_dist.get() )),
                       (*dynamic_cast<Utility::UnitAwareMaxwellFissionDistribution<MegaElectronVolt,si::amount>*>( unit_aware_distribution.get() )) );

}

//---------------------------------------------------------------------------//
// Check that distributions can be scaled
FRENSIE_UNIT_TEST_TEMPLATE_EXPAND( UnitAwareMaxwellFissionDistribution,
				   explicit_conversion,
                                   TestUnitTypeQuads )
{
  FETCH_TEMPLATE_PARAM( 0, RawIndepUnitA );
  FETCH_TEMPLATE_PARAM( 1, RawDepUnitA );
  FETCH_TEMPLATE_PARAM( 2, RawIndepUnitB );
  FETCH_TEMPLATE_PARAM( 3, RawDepUnitB );

  typedef typename std::remove_pointer<RawIndepUnitA>::type IndepUnitA;
  typedef typename std::remove_pointer<RawDepUnitA>::type DepUnitA;
  typedef typename std::remove_pointer<RawIndepUnitB>::type IndepUnitB;
  typedef typename std::remove_pointer<RawDepUnitB>::type DepUnitB;

  typedef typename Utility::UnitTraits<IndepUnitA>::template GetQuantityType<double>::type IndepQuantityA;
  typedef typename Utility::UnitTraits<typename Utility::UnitTraits<IndepUnitA>::InverseUnit>::template GetQuantityType<double>::type InverseIndepQuantityA;

  typedef typename Utility::UnitTraits<IndepUnitB>::template GetQuantityType<double>::type IndepQuantityB;
  typedef typename Utility::UnitTraits<typename Utility::UnitTraits<IndepUnitB>::InverseUnit>::template GetQuantityType<double>::type InverseIndepQuantityB;

  typedef typename Utility::UnitTraits<DepUnitA>::template GetQuantityType<double>::type DepQuantityA;
  typedef typename Utility::UnitTraits<DepUnitB>::template GetQuantityType<double>::type DepQuantityB;

  // Copy from unitless distribution to distribution type A
  Utility::UnitAwareMaxwellFissionDistribution<IndepUnitA,DepUnitA>
    unit_aware_dist_a_copy = Utility::UnitAwareMaxwellFissionDistribution<IndepUnitA,DepUnitA>::fromUnitlessDistribution( *dynamic_cast<Utility::MaxwellFissionDistribution*>( distribution.get() ) );

  // Copy from distribution type A to distribution type B
  Utility::UnitAwareMaxwellFissionDistribution<IndepUnitB,DepUnitB>
    unit_aware_dist_b_copy( unit_aware_dist_a_copy );

  IndepQuantityA indep_quantity_a =
    Utility::QuantityTraits<IndepQuantityA>::initializeQuantity( 0.0 );
  InverseIndepQuantityA inv_indep_quantity_a =
    Utility::QuantityTraits<InverseIndepQuantityA>::initializeQuantity( 0.0 );
  DepQuantityA dep_quantity_a =
    Utility::QuantityTraits<DepQuantityA>::initializeQuantity( 0.0 );

  IndepQuantityB indep_quantity_b( indep_quantity_a );
  InverseIndepQuantityB inv_indep_quantity_b( inv_indep_quantity_a );
  DepQuantityB dep_quantity_b( dep_quantity_a );

  FRENSIE_CHECK_FLOATING_EQUALITY(
			   unit_aware_dist_a_copy.evaluate( indep_quantity_a ),
			   dep_quantity_a,
			   1e-15 );
  FRENSIE_CHECK_FLOATING_EQUALITY(
			unit_aware_dist_a_copy.evaluatePDF( indep_quantity_a ),
			inv_indep_quantity_a,
			1e-15 );
  FRENSIE_CHECK_FLOATING_EQUALITY(
			   unit_aware_dist_b_copy.evaluate( indep_quantity_b ),
			   dep_quantity_b,
			   1e-15 );
  FRENSIE_CHECK_FLOATING_EQUALITY(
			unit_aware_dist_b_copy.evaluatePDF( indep_quantity_b ),
			inv_indep_quantity_b,
			1e-15 );

  Utility::setQuantity( indep_quantity_a, 0.899999999999999 );
  Utility::setQuantity( inv_indep_quantity_a,
			sqrt(0.9)*exp(-0.9)/
                        (sqrt(Utility::PhysicalConstants::pi)*0.5*
                         erf(sqrt(0.9)) - sqrt(0.9)*exp(-0.9) ) );
  Utility::setQuantity( dep_quantity_a, sqrt(0.9)*exp(-0.9) );

  indep_quantity_b = IndepQuantityB( indep_quantity_a );
  inv_indep_quantity_b = InverseIndepQuantityB( inv_indep_quantity_a );
  dep_quantity_b = DepQuantityB( dep_quantity_a );

  FRENSIE_CHECK_FLOATING_EQUALITY(
			   unit_aware_dist_a_copy.evaluate( indep_quantity_a ),
			   dep_quantity_a,
			   1e-15 );
  FRENSIE_CHECK_FLOATING_EQUALITY(
			unit_aware_dist_a_copy.evaluatePDF( indep_quantity_a ),
			inv_indep_quantity_a,
			1e-15 );
  FRENSIE_CHECK_FLOATING_EQUALITY(
			   unit_aware_dist_b_copy.evaluate( indep_quantity_b ),
			   dep_quantity_b,
			   1e-15 );
  FRENSIE_CHECK_FLOATING_EQUALITY(
			unit_aware_dist_b_copy.evaluatePDF( indep_quantity_b ),
			inv_indep_quantity_b,
			1e-15 );
}

//---------------------------------------------------------------------------//
// Custom setup
//---------------------------------------------------------------------------//
FRENSIE_CUSTOM_UNIT_TEST_SETUP_BEGIN();

FRENSIE_CUSTOM_UNIT_TEST_INIT()
{
  // Initialize the random number generator
  Utility::RandomNumberGenerator::createStreams();
}

FRENSIE_CUSTOM_UNIT_TEST_SETUP_END();

//---------------------------------------------------------------------------//
// end tstMaxwellFissionDistribution.cpp
//---------------------------------------------------------------------------//


#include <iostream>
#include <boost/math/special_functions/daubechies_scaling.hpp>
#include <boost/core/demangle.hpp>
#include <boost/hana/for_each.hpp>
#include <boost/hana/ext/std/integer_sequence.hpp>

int main()
{
    boost::hana::for_each(std::make_index_sequence<18>(),
    [](auto i) {
        std::cout << std::right;
        auto daub = boost::math::daubechies_scaling<float, i+2>();
        std::cout << "The Daubechies " << std::setw(2) <<  i + 2 << " scaling function occupies "
                  << std::setw(12) << daub.bytes()/1000.0 << " kilobytes in relative accuracy mode in "
                  << boost::core::demangle(typeid(float).name()) << " precision\n";
    });

    std::cout << std::endl;
    std::cout << std::endl;
    std::cout << std::endl;

    boost::hana::for_each(std::make_index_sequence<18>(),
    [](auto i) {
        std::cout << std::right;
        auto daub = boost::math::daubechies_scaling<float, i+2>(-2);
        std::cout << "The Daubechies " << std::setw(2) <<  i + 2 << " scaling function occupies "
                  << std::setw(12) << daub.bytes()/1000.0 << " kilobytes in absolute accuracy mode in "
                  << boost::core::demangle(typeid(float).name()) << " precision\n";
    });

    std::cout << std::endl;
    std::cout << std::endl;
    std::cout << std::endl;

    boost::hana::for_each(std::make_index_sequence<18>(),
    [](auto i) {
        std::cout << std::right;
        auto daub = boost::math::daubechies_scaling<double, i+2>();
        std::cout << "The Daubechies " << std::setw(2) <<  i + 2 << " scaling function occupies "
                  << std::setw(12) << daub.bytes()/1000.0 << " kilobytes in relative accuracy mode in "
                  << boost::core::demangle(typeid(double).name()) << " precision\n";
    });

    std::cout << std::endl;
    std::cout << std::endl;
    std::cout << std::endl;

    boost::hana::for_each(std::make_index_sequence<18>(),
    [](auto i) {
        std::cout << std::right;
        auto daub = boost::math::daubechies_scaling<double, i+2>(-2);
        std::cout << "The Daubechies " << std::setw(2) <<  i + 2 << " scaling function occupies "
                  << std::setw(12) << daub.bytes()/1000.0 << " kilobytes in absolute accuracy mode in "
                  << boost::core::demangle(typeid(double).name()) << " precision\n";
    });

}