/*!
  \file gpp_python_model_selection.cpp
  \rst
  This file has the logic to invoke C++ functions pertaining to model selection from Python.
  The data flow follows the basic 4 step from gpp_python_common.hpp.

  Note: several internal functions of this source file are only called from Export*() functions,
  so their description, inputs, outputs, etc. comments have been moved. These comments exist in
  Export*() as Python docstrings, so we saw no need to repeat ourselves.
\endrst*/
// This include violates the Google Style Guide by placing an "other" system header ahead of C and C++ system headers.  However,
// it needs to be at the top, otherwise compilation fails on some systems with some versions of python: OS X, python 2.7.3.
// Putting this include first prevents pyport from doing something illegal in C++; reference: http://bugs.python.org/issue10910
#include "Python.h"  // NOLINT(build/include)

#include "gpp_python_model_selection.hpp"

// NOLINT-ing the C, C++ header includes as well; otherwise cpplint gets confused
#include <algorithm>  // NOLINT(build/include_order)
#include <limits>  // NOLINT(build/include_order)
#include <string>  // NOLINT(build/include_order)
#include <vector>  // NOLINT(build/include_order)

#include <boost/python/def.hpp>  // NOLINT(build/include_order)
#include <boost/python/dict.hpp>  // NOLINT(build/include_order)
#include <boost/python/extract.hpp>  // NOLINT(build/include_order)
#include <boost/python/list.hpp>  // NOLINT(build/include_order)
#include <boost/python/object.hpp>  // NOLINT(build/include_order)

#include "gpp_common.hpp"
#include "gpp_covariance.hpp"
#include "gpp_domain.hpp"
#include "gpp_exception.hpp"
#include "gpp_geometry.hpp"
#include "gpp_model_selection.hpp"
#include "gpp_optimizer_parameters.hpp"
#include "gpp_python_common.hpp"

namespace optimal_learning {

namespace {

double ComputeLogLikelihoodWrapper(const boost::python::list& points_sampled,
                                   const boost::python::list& points_sampled_value,
                                   int dim, int num_sampled,
                                   LogLikelihoodTypes objective_type,
                                   const boost::python::list& hyperparameters,
                                   const boost::python::list& derivatives,
                                   int num_derivatives,
                                   const boost::python::list& noise_variance) {
  const int num_to_sample = 0;
  const boost::python::list points_to_sample_dummy;

  PythonInterfaceInputContainer input_container(hyperparameters, points_sampled, points_sampled_value, noise_variance,
                                                points_to_sample_dummy, derivatives, num_derivatives, dim, num_sampled, num_to_sample);

  SquareExponential sqexp(input_container.dim, input_container.alpha, input_container.lengths.data());

  switch (objective_type) {
    case LogLikelihoodTypes::kLogMarginalLikelihood: {
      LogMarginalLikelihoodEvaluator log_marginal_eval(input_container.points_sampled.data(),
                                                       input_container.points_sampled_value.data(),
                                                       input_container.derivatives.data(), input_container.num_derivatives,
                                                       input_container.dim, input_container.num_sampled);
      LogMarginalLikelihoodState log_marginal_state(log_marginal_eval, sqexp, input_container.noise_variance);

      double log_likelihood = log_marginal_eval.ComputeLogLikelihood(log_marginal_state);
      return log_likelihood;
    }  // end case LogLikelihoodTypes::kLogMarginalLikelihood
/*    case LogLikelihoodTypes::kLeaveOneOutLogLikelihood: {
      LeaveOneOutLogLikelihoodEvaluator leave_one_out_eval(input_container.points_sampled.data(),
                                                           input_container.points_sampled_value.data(),
                                                           input_container.noise_variance.data(),
                                                           input_container.dim,
                                                           input_container.num_sampled);
      LeaveOneOutLogLikelihoodState leave_one_out_state(leave_one_out_eval, matern_25);

      double loo_likelihood = leave_one_out_eval.ComputeLogLikelihood(leave_one_out_state);
      return loo_likelihood;
    }*/
    default: {
      double log_likelihood = -std::numeric_limits<double>::max();
      OL_THROW_EXCEPTION(OptimalLearningException, "ERROR: invalid objective mode choice. Setting log likelihood to -DBL_MAX.");
      return log_likelihood;
    }
  }  // end switch over objective_type
}

boost::python::list ComputeHyperparameterGradLogLikelihoodWrapper(const boost::python::list& points_sampled,
                                                                  const boost::python::list& points_sampled_value,
                                                                  int dim, int num_sampled,
                                                                  LogLikelihoodTypes objective_type,
                                                                  const boost::python::list& hyperparameters,
                                                                  const boost::python::list& derivatives,
                                                                  int num_derivatives,
                                                                  const boost::python::list& noise_variance) {
  const int num_to_sample = 0;
  const boost::python::list points_to_sample_dummy;

  PythonInterfaceInputContainer input_container(hyperparameters, points_sampled, points_sampled_value, noise_variance,
                                                points_to_sample_dummy, derivatives, num_derivatives, dim, num_sampled, num_to_sample);

  SquareExponential sqexp(input_container.dim, input_container.alpha, input_container.lengths.data());

  std::vector<double> grad_log_likelihood(sqexp.GetNumberOfHyperparameters() + 1 + num_derivatives);
  switch (objective_type) {
    case LogLikelihoodTypes::kLogMarginalLikelihood: {
      LogMarginalLikelihoodEvaluator log_marginal_eval(input_container.points_sampled.data(),
                                                       input_container.points_sampled_value.data(),
                                                       input_container.derivatives.data(), input_container.num_derivatives,
                                                       input_container.dim, input_container.num_sampled);
      LogMarginalLikelihoodState log_marginal_state(log_marginal_eval, sqexp, input_container.noise_variance);

      log_marginal_eval.ComputeGradLogLikelihood(&log_marginal_state, grad_log_likelihood.data());
      break;
    }  // end case LogLikelihoodTypes::kLogMarginalLikelihood
/*    case LogLikelihoodTypes::kLeaveOneOutLogLikelihood: {
      LeaveOneOutLogLikelihoodEvaluator leave_one_out_eval(input_container.points_sampled.data(),
                                                           input_container.points_sampled_value.data(),
                                                           input_container.noise_variance.data(),
                                                           input_container.dim,
                                                           input_container.num_sampled);
      LeaveOneOutLogLikelihoodState leave_one_out_state(leave_one_out_eval, matern_25);

      leave_one_out_eval.ComputeGradLogLikelihood(&leave_one_out_state, grad_log_likelihood.data());
      break;
    }*/
    default: {
      std::fill(grad_log_likelihood.begin(), grad_log_likelihood.end(), std::numeric_limits<double>::max());
      OL_THROW_EXCEPTION(OptimalLearningException, "ERROR: invalid objective mode choice. Setting all gradients to DBL_MAX.");
      break;
    }
  }  // end switch over objective_type
  return VectorToPylist(grad_log_likelihood);
}

/*!\rst
  Utility that dispatches log likelihood optimization (wrt hyperparameters) based on optimizer type.
  This is just used to reduce copy-pasted code.

  Let n_hyper = covariance.GetNumberOfHyperparameters();

  \param
    :optimizer_parameters: python/cpp_wrappers/optimization._CppOptimizerParameters
      Python object containing the LogLikelihoodTypes objective_type and OptimizerTypes optimzer_typ
      to use as well as appropriate parameter structs e.g., NewtonParameters for type kNewton).
      See comments on the python interface for multistart_hyperparameter_optimization_wrapper
    :log_likelihood_eval: object supporting evaluation of log likelihood
    :covariance: the CovarianceFunction object encoding assumptions about the GP's behavior on our data
    :hyperparameter_domain[2][n_hyper]: matrix specifying the boundaries of a n_hyper-dimensional tensor-product
      domain.  Specified as a list of [x_i_min, x_i_max] pairs, i = 0 .. dim-1
      Specify in LOG-10 SPACE!
    :optimizer_type: type of optimization to use (e.g., null, gradient descent)
    :max_num_threads: maximum number of threads for use by OpenMP (generally should be <= # cores)
    :randomness_source: object containing randomness sources for generating random points in the domain
    :status: pydict object; cannot be None
  \output
    :randomness_source: PRNG internal states modified
    :status: modified on exit to describe whether convergence occurred
    :new_hyperparameters[n_hyper]: new hyperparameters found by optimizer to maximize the specified log likelihood measure
\endrst*/
template <typename LogLikelihoodEvaluator>
void DispatchHyperparameterOptimization(const boost::python::object& optimizer_parameters,
                                        const LogLikelihoodEvaluator& log_likelihood_eval,
                                        const CovarianceInterface& covariance,
                                        const std::vector<double> noise_variance,
                                        ClosedInterval const * restrict hyperparameter_domain,
                                        OptimizerTypes optimizer_type, int max_num_threads,
                                        RandomnessSourceContainer& randomness_source,
                                        boost::python::dict& status,
                                        double * restrict new_hyperparameters) {
  bool found_flag = false;
  switch (optimizer_type) {
    case OptimizerTypes::kNull: {
      // optimizer_parameters must contain an int num_random_samples field, extract it
      int num_random_samples = boost::python::extract<int>(optimizer_parameters.attr("num_random_samples"));
      ThreadSchedule thread_schedule(max_num_threads, omp_sched_guided);
      LatinHypercubeSearchHyperparameterOptimization(log_likelihood_eval, covariance, noise_variance, hyperparameter_domain,
                                                     thread_schedule, num_random_samples, &found_flag,
                                                     &randomness_source.uniform_generator, new_hyperparameters);
      status[std::string(log_likelihood_eval.kName) + "_lhc_found_update"] = found_flag;
      break;
    }  // end case kNull for optimizer_type
    case OptimizerTypes::kGradientDescent: {
      // optimizer_parameters must contain a optimizer_parameters field
      // of type GradientDescentParameters. extract it
      const GradientDescentParameters& gradient_descent_parameters = boost::python::extract<GradientDescentParameters&>(optimizer_parameters.attr("optimizer_parameters"));
      ThreadSchedule thread_schedule(max_num_threads, omp_sched_dynamic);
      MultistartGradientDescentHyperparameterOptimization(log_likelihood_eval, covariance, noise_variance,
                                                          gradient_descent_parameters,
                                                          hyperparameter_domain,
                                                          thread_schedule, &found_flag,
                                                          &randomness_source.uniform_generator,
                                                          new_hyperparameters);
      status[std::string(log_likelihood_eval.kName) + "_gradient_descent_found_update"] = found_flag;
      break;
    }  // end case kGradientDescent for optimizer_type
/*    case OptimizerTypes::kNewton: {
      // optimizer_parameters must contain a optimizer_parameters field
      // of type NewtonParameters. extract it
      const NewtonParameters& newton_parameters = boost::python::extract<NewtonParameters&>(optimizer_parameters.attr("optimizer_parameters"));
      ThreadSchedule thread_schedule(max_num_threads, omp_sched_dynamic);
      MultistartNewtonHyperparameterOptimization(log_likelihood_eval, covariance,
                                                 newton_parameters, hyperparameter_domain,
                                                 thread_schedule, &found_flag,
                                                 &randomness_source.uniform_generator,
                                                 new_hyperparameters);
      status[std::string(log_likelihood_eval.kName) + "_newton_found_update"] = found_flag;
      break;
    }  // end case kNewton for optimizer_type */
    default: {
      std::fill(new_hyperparameters, new_hyperparameters + covariance.GetNumberOfHyperparameters() + noise_variance.size(), 1.0);
      OL_THROW_EXCEPTION(OptimalLearningException, "ERROR: invalid optimizer choice. Setting all hyperparameters to 1.0.");
      break;
    }
  }  // end switch over optimzer_type for LogLikelihoodTypes::kLogMarginalLikelihood
}

boost::python::list MultistartHyperparameterOptimizationWrapper(const boost::python::object& optimizer_parameters,
                                                                const boost::python::list& hyperparameter_domain,
                                                                const boost::python::list& points_sampled,
                                                                const boost::python::list& points_sampled_value,
                                                                int dim, int num_sampled,
                                                                const boost::python::list& hyperparameters,
                                                                const boost::python::list& noise_variance,
                                                                const boost::python::list& derivatives,
                                                                int num_derivatives, int max_num_threads,
                                                                RandomnessSourceContainer& randomness_source,
                                                                boost::python::dict& status) {
  // TODO(GH-131): make domain objects constructible from python; and pass them in through
  // the optimizer_parameters python object
  const int num_to_sample = 0;
  const boost::python::list points_to_sample_dummy;
  PythonInterfaceInputContainer input_container(hyperparameters, points_sampled, points_sampled_value, noise_variance,
                                                points_to_sample_dummy, derivatives, num_derivatives, dim, num_sampled, num_to_sample);

  SquareExponential sqexp(input_container.dim, input_container.alpha, input_container.lengths.data());

  int num_hyperparameters = sqexp.GetNumberOfHyperparameters() + 1 + num_derivatives;
  std::vector<double> new_hyperparameters(num_hyperparameters);

  std::vector<ClosedInterval> hyperparameter_domain_C(num_hyperparameters);
  CopyPylistToClosedIntervalVector(hyperparameter_domain, num_hyperparameters, hyperparameter_domain_C);

  OptimizerTypes optimizer_type = boost::python::extract<OptimizerTypes>(optimizer_parameters.attr("optimizer_type"));
  LogLikelihoodTypes objective_type = boost::python::extract<LogLikelihoodTypes>(optimizer_parameters.attr("objective_type"));
  switch (objective_type) {
    case LogLikelihoodTypes::kLogMarginalLikelihood: {
      LogMarginalLikelihoodEvaluator log_likelihood_eval(input_container.points_sampled.data(),
                                                         input_container.points_sampled_value.data(),
                                                         input_container.derivatives.data(), input_container.num_derivatives,
                                                         input_container.dim, input_container.num_sampled);

      DispatchHyperparameterOptimization(optimizer_parameters, log_likelihood_eval, sqexp, input_container.noise_variance,
                                         hyperparameter_domain_C.data(), optimizer_type, max_num_threads,
                                         randomness_source, status, new_hyperparameters.data());
      break;
    }  // end case LogLikelihoodTypes::kLogMarginalLikelihood
/*    case LogLikelihoodTypes::kLeaveOneOutLogLikelihood: {
      LeaveOneOutLogLikelihoodEvaluator log_likelihood_eval(input_container.points_sampled.data(),
                                                            input_container.points_sampled_value.data(),
                                                            input_container.noise_variance.data(),
                                                            input_container.dim, input_container.num_sampled);

      DispatchHyperparameterOptimization(optimizer_parameters, log_likelihood_eval, matern_25,
                                         hyperparameter_domain_C.data(), optimizer_type, max_num_threads,
                                         randomness_source, status, new_hyperparameters.data());
      break;
    }  // end case LogLikelihoodTypes::kLeaveOneOutLogLikelihood*/
    default: {
      std::fill(new_hyperparameters.begin(), new_hyperparameters.end(), 1.0);
      OL_THROW_EXCEPTION(OptimalLearningException, "ERROR: invalid objective type choice. Setting all hyperparameters to 1.0.");
      break;
    }
  }  // end switch over objective_type

  return VectorToPylist(new_hyperparameters);
}

boost::python::list EvaluateLogLikelihoodAtHyperparameterListWrapper(const boost::python::list& hyperparameter_list,
                                                                     const boost::python::list& points_sampled,
                                                                     const boost::python::list& points_sampled_value,
                                                                     int dim, int num_sampled,
                                                                     LogLikelihoodTypes objective_mode,
                                                                     const boost::python::list& hyperparameters,
                                                                     const boost::python::list& noise_variance,
                                                                     const boost::python::list& derivatives,
                                                                     int num_derivatives,
                                                                     int num_multistarts, int max_num_threads,
                                                                     boost::python::dict& status) {
  const int num_to_sample = 0;
  const boost::python::list points_to_sample_dummy;
  PythonInterfaceInputContainer input_container(hyperparameters, points_sampled, points_sampled_value, noise_variance,
                                                points_to_sample_dummy, derivatives, num_derivatives, dim, num_sampled, num_to_sample);

  SquareExponential sqexp(input_container.dim, input_container.alpha, input_container.lengths.data());

  std::vector<double> new_hyperparameters_C(sqexp.GetNumberOfHyperparameters() + 1 + num_derivatives);
  std::vector<double> result_function_values_C(num_multistarts);
  std::vector<double> initial_guesses_C((sqexp.GetNumberOfHyperparameters() + 1 + num_derivatives) * num_multistarts);

  CopyPylistToVector(hyperparameter_list, (sqexp.GetNumberOfHyperparameters() + 1 + num_derivatives) * num_multistarts, initial_guesses_C);

  TensorProductDomain dummy_domain(nullptr, 0);
  ThreadSchedule thread_schedule(max_num_threads, omp_sched_guided);

  bool found_flag = false;
  switch (objective_mode) {
    case LogLikelihoodTypes::kLogMarginalLikelihood: {
      LogMarginalLikelihoodEvaluator log_likelihood_eval(input_container.points_sampled.data(),
                                                         input_container.points_sampled_value.data(),
                                                         input_container.derivatives.data(), input_container.num_derivatives,
                                                         input_container.dim, input_container.num_sampled);
      EvaluateLogLikelihoodAtPointList(log_likelihood_eval, sqexp, input_container.noise_variance, dummy_domain, thread_schedule,
                                       initial_guesses_C.data(), num_multistarts, &found_flag,
                                       result_function_values_C.data(), new_hyperparameters_C.data());
      status[std::string("evaluate_") + log_likelihood_eval.kName + "_at_hyperparameter_list"] = found_flag;
      break;
    }
/*    case LogLikelihoodTypes::kLeaveOneOutLogLikelihood: {
      LeaveOneOutLogLikelihoodEvaluator log_likelihood_eval(input_container.points_sampled.data(),
                                                            input_container.points_sampled_value.data(),
                                                            input_container.noise_variance.data(),
                                                            input_container.dim, input_container.num_sampled);
      EvaluateLogLikelihoodAtPointList(log_likelihood_eval, matern_25, dummy_domain, thread_schedule,
                                       initial_guesses_C.data(), num_multistarts, &found_flag,
                                       result_function_values_C.data(), new_hyperparameters_C.data());
      status[std::string("evaluate_") + log_likelihood_eval.kName + "_at_hyperparameter_list"] = found_flag;
      break;
    }*/
    default: {
      std::fill(result_function_values_C.begin(), result_function_values_C.end(), -std::numeric_limits<double>::max());
      status["evaluate_invalid_log_likelihood_at_hyperparameter_list"] = found_flag;
      OL_THROW_EXCEPTION(OptimalLearningException, "ERROR: invalid objective mode choice. Setting all results to -DBL_MAX.");
      break;
    }
  }

  return VectorToPylist(result_function_values_C);
}

boost::python::list RestartedGradientDescentHyperparameterOptimizationWrapper(const boost::python::object& optimizer_parameters,
                                                                              const boost::python::list& hyperparameter_domain,
                                                                              const boost::python::list& points_sampled,
                                                                              const boost::python::list& points_sampled_value,
                                                                              int dim, int num_sampled,
                                                                              const boost::python::list& hyperparameters,
                                                                              const boost::python::list& noise_variance,
                                                                              const boost::python::list& derivatives,
                                                                              int num_derivatives,
                                                                              boost::python::dict& status){
  // the optimizer_parameters python object
  const int num_to_sample = 0;
  const boost::python::list points_to_sample_dummy;
  PythonInterfaceInputContainer input_container(hyperparameters, points_sampled, points_sampled_value, noise_variance,
                                                points_to_sample_dummy, derivatives, num_derivatives, dim, num_sampled, num_to_sample);

  SquareExponential sqexp(input_container.dim, input_container.alpha, input_container.lengths.data());

  int num_hyperparameters = sqexp.GetNumberOfHyperparameters() + 1 + num_derivatives;
  std::vector<double> new_hyperparameters(num_hyperparameters);

  std::vector<ClosedInterval> hyperparameter_domain_C(num_hyperparameters);
  CopyPylistToClosedIntervalVector(hyperparameter_domain, num_hyperparameters, hyperparameter_domain_C);

  LogMarginalLikelihoodEvaluator log_likelihood_eval(input_container.points_sampled.data(),
                                                     input_container.points_sampled_value.data(),
                                                     input_container.derivatives.data(), input_container.num_derivatives,
                                                     input_container.dim, input_container.num_sampled);
  const GradientDescentParameters& gradient_descent_parameters = boost::python::extract<GradientDescentParameters&>(optimizer_parameters.attr("optimizer_parameters"));
  RestartedGradientDescentHyperparameterOptimizationTensor(log_likelihood_eval, sqexp, input_container.noise_variance, gradient_descent_parameters,
                                                           hyperparameter_domain_C.data(), new_hyperparameters.data());
  return VectorToPylist(new_hyperparameters);
}

}  // end unnamed namespace

void ExportModelSelectionFunctions() {
  boost::python::def("compute_log_likelihood", ComputeLogLikelihoodWrapper, R"%%(
    Computes the specified log likelihood measure of model fit using the given
    hyperparameters.

    :param points_sampled: points that have already been sampled
    :type points_sampled: list of float64 with shape (num_sampled, dim)
    :param points_sampled_value: values of the already-sampled points
    :type points_sampled_value: list of float64 with shape (num_sampled, )
    :param dim: the spatial dimension of a point (i.e., number of independent params in experiment)
    :type dim: int > 0
    :param num_sampled: number of already-sampled points
    :type num_sampled: int > 0
    :param objective_mode: describes which log likelihood measure to compute (e.g., kLogMarginalLikelihood)
    :type objective_mode: GPP.LogLikelihoodTypes (enum)
    :param hyperparameters: covariance hyperparameters; see "Details on ..." section at the top of ``BOOST_PYTHON_MODULE``
    :type hyperparameters: list of len 2; index 0 is a float64 ``\alpha`` (signal variance) and index 1 is the length scales (list of floa64 of length ``dim``)
    :param noise_variance: the ``\sigma_n^2`` (noise variance) associated w/observation, points_sampled_value
    :type noise_variance: list of float64 with shape (num_sampled, )
    :return: computed log marginal likelihood of prior
    :rtype: float64
    )%%");

  boost::python::def("compute_hyperparameter_grad_log_likelihood", ComputeHyperparameterGradLogLikelihoodWrapper, R"%%(
    Computes the gradient of the specified log likelihood measure of model fit using the given
    hyperparameters. Gradient computed wrt the given hyperparameters.

    ``n_hyper`` denotes the number of hyperparameters.

    :param points_sampled: points that have already been sampled
    :type points_sampled: list of float64 with shape (num_sampled, dim)
    :param points_sampled_value: values of the already-sampled points
    :type points_sampled_value: list of float64 with shape (num_sampled, )
    :param dim: the spatial dimension of a point (i.e., number of independent params in experiment)
    :type dim: int > 0
    :param num_sampled: number of already-sampled points
    :type num_sampled: int > 0
    :param objective_mode: describes which log likelihood measure to compute (e.g., kLogMarginalLikelihood)
    :type objective_mode: GPP.LogLikelihoodTypes (enum)
    :param hyperparameters: covariance hyperparameters; see "Details on ..." section at the top of ``BOOST_PYTHON_MODULE``
    :type hyperparameters: list of len 2; index 0 is a float64 ``\alpha`` (signal variance) and index 1 is the length scales (list of floa64 of length ``dim``)
    :param noise_variance: the ``\sigma_n^2`` (noise variance) associated w/observation, points_sampled_value
    :type noise_variance: list of float64 with shape (num_sampled, )
    :return: gradients of log marginal likelihood wrt hyperparameters
    :rtype: list of float64 with shape (num_hyperparameters, )
    )%%");

  boost::python::def("multistart_hyperparameter_optimization", MultistartHyperparameterOptimizationWrapper, R"%%(
    Optimize the specified log likelihood measure over the specified domain using the specified optimization method.

    The _CppOptimizerParameters object is a python class defined in:
    ``python/cpp_wrappers/optimization._CppOptimizerParameters``
    See that class definition for more details.

    This function expects it to have the fields:

    * objective_type (LogLikelihoodTypes enum from this file)
    * optimizer_type (OptimizerTypes enum from this file)
    * num_random_samples (int, number of samples to 'dumb' search over, only used if optimizer_type == kNull)
    * optimizer_parameters (*Parameters struct (gpp_optimizer_parameters.hpp) where * matches optimizer_type
      unused if optimizer_type == kNull)

    ``n_hyper`` denotes the number of hyperparameters.

    :param optimizer_parameters: python object containing the LogLikelihoodTypes
      objective to use, OptimizerTypes optimzer_type to use as well as appropriate
      parameter structs e.g., NewtonParameters for type kNewton
    :type optimizer_parameters:  _CppOptimizerParameters
    :param hyperparameter_domain: [lower, upper] bound pairs for each hyperparameter dimension in LOG-10 SPACE
    :type hyperparameter_domain: list of float64 with shape (num_hyperparameters, 2)
    :param points_sampled: points that have already been sampled
    :type points_sampled: list of float64 with shape (num_sampled, dim)
    :param points_sampled_value: values of the already-sampled points
    :type points_sampled_value: list of float64 with shape (num_sampled, )
    :param dim: the spatial dimension of a point (i.e., number of independent params in experiment)
    :type dim: int > 0
    :param num_sampled: number of already-sampled points
    :type num_sampled: int > 0
    :param hyperparameters: covariance hyperparameters; see "Details on ..." section at the top of ``BOOST_PYTHON_MODULE``
    :type hyperparameters: list of len 2; index 0 is a float64 ``\alpha`` (signal variance) and index 1 is the length scales (list of floa64 of length ``dim``)
    :param noise_variance: the ``\sigma_n^2`` (noise variance) associated w/observation, points_sampled_value
    :type noise_variance: list of float64 with shape (num_sampled, )
    :param max_num_threads: max number of threads to use during EI optimization
    :type max_num_threads: int >= 1
    :param randomness_source: object containing randomness sources; only thread 0's source is used
    :type randomness_source: GPP.RandomnessSourceContainer
    :param status: pydict object (cannot be None!); modified on exit to describe whether convergence occurred
    :type status: dict
    :return: optimized hyperparameters
    :rtype: list of float64 with shape (num_hyperparameters, )
    )%%");

  boost::python::def("restarted_hyperparameter_optimization", RestartedGradientDescentHyperparameterOptimizationWrapper, R"%%(
    Optimize the specified log likelihood measure over the specified domain using the specified optimization method.

    The _CppOptimizerParameters object is a python class defined in:
    ``python/cpp_wrappers/optimization._CppOptimizerParameters``
    See that class definition for more details.

    This function expects it to have the fields:

    * objective_type (LogLikelihoodTypes enum from this file)
    * optimizer_type (OptimizerTypes enum from this file)
    * num_random_samples (int, number of samples to 'dumb' search over, only used if optimizer_type == kNull)
    * optimizer_parameters (*Parameters struct (gpp_optimizer_parameters.hpp) where * matches optimizer_type
      unused if optimizer_type == kNull)

    ``n_hyper`` denotes the number of hyperparameters.

    :param optimizer_parameters: python object containing the LogLikelihoodTypes
      objective to use, OptimizerTypes optimzer_type to use as well as appropriate
      parameter structs e.g., NewtonParameters for type kNewton
    :type optimizer_parameters:  _CppOptimizerParameters
    :param hyperparameter_domain: [lower, upper] bound pairs for each hyperparameter dimension in LOG-10 SPACE
    :type hyperparameter_domain: list of float64 with shape (num_hyperparameters, 2)
    :param points_sampled: points that have already been sampled
    :type points_sampled: list of float64 with shape (num_sampled, dim)
    :param points_sampled_value: values of the already-sampled points
    :type points_sampled_value: list of float64 with shape (num_sampled, )
    :param dim: the spatial dimension of a point (i.e., number of independent params in experiment)
    :type dim: int > 0
    :param num_sampled: number of already-sampled points
    :type num_sampled: int > 0
    :param hyperparameters: covariance hyperparameters; see "Details on ..." section at the top of ``BOOST_PYTHON_MODULE``
    :type hyperparameters: list of len 2; index 0 is a float64 ``\alpha`` (signal variance) and index 1 is the length scales (list of floa64 of length ``dim``)
    :param noise_variance: the ``\sigma_n^2`` (noise variance) associated w/observation, points_sampled_value
    :type noise_variance: list of float64 with shape (num_sampled, )
    :param max_num_threads: max number of threads to use during EI optimization
    :type max_num_threads: int >= 1
    :param randomness_source: object containing randomness sources; only thread 0's source is used
    :type randomness_source: GPP.RandomnessSourceContainer
    :param status: pydict object (cannot be None!); modified on exit to describe whether convergence occurred
    :type status: dict
    :return: optimized hyperparameters
    :rtype: list of float64 with shape (num_hyperparameters, )
    )%%");

  boost::python::def("evaluate_log_likelihood_at_hyperparameter_list", EvaluateLogLikelihoodAtHyperparameterListWrapper, R"%%(
    Evaluates the specified log likelihood measure of model fit at each member of
    hyperparameter_list. Useful for plotting.

    Equivalent to::

      result = []
      for hyperparameters in hyperparameter_list:
          result.append(compute_log_likelihood(hyperparameters, ...))

    But this method is substantially faster (loops in C++ and is multithreaded).

    ``n_hyper`` denotes the number of hyperparameters

    :param hyperparameter_list[num_multistarts][n_hyper]: list of hyperparameters at which to evaluate log likelihood
    :type hyperparameter_list: list of float64 with shape (num_multistarts, num_hyperparameters)
    :param points_sampled: points that have already been sampled
    :type points_sampled: list of float64 with shape (num_sampled, dim)
    :param points_sampled_value: values of the already-sampled points
    :type points_sampled_value: list of float64 with shape (num_sampled, )
    :param dim: the spatial dimension of a point (i.e., number of independent params in experiment)
    :type dim: int > 0
    :param num_sampled: number of already-sampled points
    :type num_sampled: int > 0
    :param objective_mode: describes which log likelihood measure to compute (e.g., kLogMarginalLikelihood)
    :type objective_mode: GPP.LogLikelihoodTypes (enum)
    :param hyperparameters: covariance hyperparameters; see "Details on ..." section at the top of ``BOOST_PYTHON_MODULE``
    :type hyperparameters: list of len 2; index 0 is a float64 ``\alpha`` (signal variance) and index 1 is the length scales (list of floa64 of length ``dim``)
    :param noise_variance: the ``\sigma_n^2`` (noise variance) associated w/observation, points_sampled_value
    :type noise_variance: list of float64 with shape (num_sampled, )
    :param num_multistarts: number hyperparameter sets at which to compute log likelihood
    :type num_multistarts: int > 0
    :param max_num_threads: max number of threads to use during EI optimization
    :type max_num_threads: int >= 1
    :param status: pydict object (cannot be None!); modified on exit to describe whether convergence occurred
    :type status: dict
    :return: log likelihood values at each point of the hyperparameter_list list, in the same order
    :rtype: list of float64 with shape (num_multistarts, )
    )%%");
}

}  // end namespace optimal_learning


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

#include "zdd.hpp"

#include <vector>

#include <boost/assign/std/vector.hpp>
#include <boost/format.hpp>
#include <boost/range/algorithm.hpp>

#include <core/utils/bitset_utils.hpp>
#include <core/utils/range_utils.hpp>
#include <classical/dd/zdd_to_sets.hpp>

using namespace boost::assign;

namespace cirkit
{

/******************************************************************************
 * Types                                                                      *
 ******************************************************************************/

enum class zdd_operation { diff, _union, intersection, symmetric_difference, join, meet, delta, nonsub, nonsup, minhit };

/******************************************************************************
 * Private functions                                                          *
 ******************************************************************************/

/******************************************************************************
 * Public functions                                                           *
 ******************************************************************************/

zdd_manager::zdd_manager( unsigned nvars, unsigned log_max_objs, bool verbose )
  : dd_manager( nvars, log_max_objs, verbose ) {}

zdd_manager::~zdd_manager() {}

unsigned zdd_manager::zdd_diff( unsigned z1, unsigned z2 )
{
  /* terminating cases */
  if ( z1 == 0u ) { return 0u; }
  if ( z2 == 0u ) { return z1; }
  if ( z1 == z2 ) { return 0u; }

  const auto r = cache.lookup( z1, z2, (unsigned)zdd_operation::diff );
  if ( r >= 0 ) { return r; }

  const auto& node1 = nodes.at( z1 );
  const auto& node2 = nodes.at( z2 );
  unsigned rlow, rhigh, idx;
  if ( node1.var < node2.var )
  {
    rlow = zdd_diff( node1.low, z2 );
    idx = unique_create( node1.var, node1.high, rlow );
  }
  else if ( node1.var > node2.var )
  {
    idx = zdd_diff( z1, node2.low );
  }
  else
  {
    rlow = zdd_diff( node1.low, node2.low );
    rhigh = zdd_diff( node1.high, node2.high );
    idx = unique_create( node1.var, rhigh, rlow );
  }
  return cache.insert( z1, z2, (unsigned)zdd_operation::diff, idx );
}

unsigned zdd_manager::zdd_union( unsigned z1, unsigned z2 )
{
  /* terminating cases */
  if ( z1 == 0u ) { return z2; }
  if ( z2 == 0u ) { return z1; }
  if ( z1 == z2 ) { return z1; }

  /* commutativity */
  if ( z1 > z2 ) { return zdd_union( z2, z1 ); }

  const auto r = cache.lookup( z1, z2, (unsigned)zdd_operation::_union );
  if ( r >= 0 ) { return r; }

  const auto& node1 = nodes.at( z1 );
  const auto& node2 = nodes.at( z2 );
  unsigned rlow, rhigh;
  if ( node1.var < node2.var )
  {
    rlow = zdd_union( node1.low, z2 );
    rhigh = node1.high;
  }
  else if ( node1.var > node2.var )
  {
    rlow = zdd_union( z1, node2.low );
    rhigh = node2.high;
  }
  else
  {
    rlow = zdd_union( node1.low, node2.low );
    rhigh = zdd_union( node1.high, node2.high );
  }
  const auto idx = unique_create( std::min( node1.var, node2.var ), rhigh, rlow );
  return cache.insert( z1, z2, (unsigned)zdd_operation::_union, idx );
}

unsigned zdd_manager::zdd_intersection( unsigned z1, unsigned z2 )
{
  /* terminating cases */
  if ( z1 == 0u ) { return 0u; }
  if ( z2 == 0u ) { return 0u; }
  if ( z1 == z2 ) { return z1; }

  /* commutativity */
  if ( z1 > z2 ) { return zdd_intersection( z2, z1 ); }

  const auto& node1 = nodes.at( z1 );
  const auto& node2 = nodes.at( z2 );
  if ( node1.var < node2.var )
  {
    return zdd_intersection( node1.low, z2 );
  }
  if ( node1.var > node2.var )
  {
    return zdd_intersection( z1, node2.low );
  }

  const auto r = cache.lookup( z1, z2, (unsigned)zdd_operation::intersection );
  if ( r >= 0 ) { return r; }

  auto rlow = zdd_intersection( node1.low, node2.low );
  auto rhigh = zdd_intersection( node1.high, node2.high );

  const auto idx = unique_create( node1.var, rhigh, rlow );
  return cache.insert( z1, z2, (unsigned)zdd_operation::intersection, idx );
}

unsigned zdd_manager::zdd_symmetric_difference( unsigned z1, unsigned z2 )
{
  /* terminating cases */
  if ( z1 == 0u ) { return z2; }
  if ( z2 == 0u ) { return z1; }
  if ( z1 == z2 ) { return 0u; }

  /* commutativity */
  if ( z1 > z2 ) { return zdd_symmetric_difference( z2, z1 ); }

  const auto r = cache.lookup( z1, z2, (unsigned)zdd_operation::symmetric_difference );
  if ( r >= 0 ) { return r; }

  const auto& node1 = nodes.at( z1 );
  const auto& node2 = nodes.at( z2 );
  unsigned rlow, rhigh;
  if ( node1.var < node2.var )
  {
    rlow = zdd_symmetric_difference( node1.low, z2 );
    rhigh = node1.high;
  }
  else if ( node1.var > node2.var )
  {
    rlow = zdd_symmetric_difference( z1, node2.low );
    rhigh = node2.high;
  }
  else
  {
    rlow = zdd_symmetric_difference( node1.low, node2.low );
    rhigh = zdd_symmetric_difference( node1.high, node2.high );
  }
  const auto idx = unique_create( std::min( node1.var, node2.var ), rhigh, rlow );
  return cache.insert( z1, z2, (unsigned)zdd_operation::symmetric_difference, idx );
}

unsigned zdd_manager::zdd_join( unsigned z1, unsigned z2 )
{
  /* swapping */
  const auto& node1 = nodes.at( z1 );
  const auto& node2 = nodes.at( z2 );

  /* commutativity */
  if ( node1.var < node2.var || ( ( node1.var == node2.var ) && ( z1 > z2 ) ) ) { return zdd_join( z2, z1 ); }

  /* terminating cases */
  if ( z1 == 0u ) { return 0u; }
  if ( z1 == 1u ) { return z2; }

  const auto r = cache.lookup( z1, z2, (unsigned)zdd_operation::join );
  if ( r >= 0 ) { return r; }

  unsigned rlow, rhigh;
  if ( node1.var > node2.var )
  {
    rlow = zdd_join( z1, node2.low );
    rhigh = zdd_join( z1, node2.high );
  }
  else
  {
    rlow = zdd_join( node1.low, node2.low );
    auto r1 = zdd_join( node1.low, node2.high );
    auto r2 = zdd_join( node1.high, node2.low );
    auto r3 = zdd_join( node1.high, node2.high );
    rhigh = zdd_union( zdd_union( r1, r2 ), r3 );
  }

  const auto idx = unique_create( node2.var, rhigh, rlow );
  return cache.insert( z1, z2, (unsigned)zdd_operation::join, idx );
}

unsigned zdd_manager::zdd_meet( unsigned z1, unsigned z2 )
{
  /* swapping */
  const auto& node1 = nodes.at( z1 );
  const auto& node2 = nodes.at( z2 );

  /* commutativity */
  if ( node1.var < node2.var || ( ( node1.var == node2.var ) && ( z1 > z2 ) ) ) { return zdd_join( z2, z1 ); }

  /* terminating cases */
  if ( z1 <= 1u ) { return z1; }

  const auto r = cache.lookup( z1, z2, (unsigned)zdd_operation::meet );
  if ( r >= 0 ) { return r; }

  if ( node1.var > node2.var )
  {
    auto idx = zdd_meet( z1, zdd_union( node2.low, node2.high ) );
    return cache.insert( z1, z2, (unsigned)zdd_operation::join, idx );
  }
  else
  {
    auto rhigh = zdd_join( node1.high, node2.high );
    auto r1 = zdd_join( node1.low, node2.high );
    auto r2 = zdd_join( node1.high, node2.low );
    auto r3 = zdd_join( node1.low, node2.low );
    auto rlow = zdd_union( zdd_union( r1, r2 ), r3 );

    const auto idx = unique_create( node2.var, rhigh, rlow );
    return cache.insert( z1, z2, (unsigned)zdd_operation::join, idx );
  }
}

unsigned zdd_manager::zdd_delta( unsigned z1, unsigned z2 )
{
  /* swapping */
  const auto& node1 = nodes.at( z1 );
  const auto& node2 = nodes.at( z2 );

  /* commutativity */
  if ( node1.var < node2.var || ( ( node1.var == node2.var ) && ( z1 > z2 ) ) ) { return zdd_delta( z2, z1 ); }

  /* terminating cases */
  if ( z1 == 0u ) { return 0u; }
  if ( z1 == 1u ) { return z2; }

  const auto r = cache.lookup( z1, z2, (unsigned)zdd_operation::delta );
  if ( r >= 0 ) { return r; }

  unsigned rlow, rhigh;
  if ( node1.var > node2.var )
  {
    rlow = zdd_delta( z1, node2.low );
    rhigh = zdd_delta( z1, node2.high );
  }
  else
  {
    rlow  = zdd_union( zdd_delta( node1.low, node2.low ), zdd_delta( node1.high, node2.high ) );
    rhigh = zdd_union( zdd_delta( node1.low, node2.high ), zdd_delta( node1.high, node2.low ) );
  }

  const auto idx = unique_create( node2.var, rhigh, rlow );
  return cache.insert( z1, z2, (unsigned)zdd_operation::delta, idx );
}

unsigned zdd_manager::zdd_nonsub( unsigned z1, unsigned z2 )
{
  /* terminating cases */
  if ( z1 == 0u ) { return 0u; }
  if ( z1 == 1u ) { return 0u; }
  if ( z2 == 0u ) { return z1; }
  if ( z1 == z2 ) { return 0u; }

  const auto r = cache.lookup( z1, z2, (unsigned)zdd_operation::nonsub );
  if ( r >= 0 ) { return r; }

  const auto& node1 = nodes.at( z1 );
  const auto& node2 = nodes.at( z2 );

  unsigned rlow, rhigh;

  if ( node1.var > node2.var )
  {
    rlow = zdd_nonsub( node1.low, z2 );
    rhigh = node1.high;
  }
  else
  {
    auto r1 = zdd_nonsub( node1.low, node2.low );
    auto r2 = zdd_nonsub( node1.low, node2.high );
    rlow = zdd_intersection( r1, r2 );
    rhigh = zdd_nonsub( node1.high, node2.high );
  }

  const auto idx = unique_create( node1.var, rhigh, rlow );
  return cache.insert( z1, z2, (unsigned)zdd_operation::nonsub, idx );
}

unsigned zdd_manager::zdd_nonsup( unsigned z1, unsigned z2 )
{
  /* terminating cases */
  if ( z1 == 0u ) { return 0u; }
  if ( z2 == 1u ) { return 0u; }
  if ( z2 == 0u ) { return z1; }
  if ( z1 == z2 ) { return 0u; }

  const auto& node1 = nodes.at( z1 );
  const auto& node2 = nodes.at( z2 );

  if ( node1.var > node2.var )
  {
    return zdd_nonsup( z1, node2.low );
  }

  const auto r = cache.lookup( z1, z2, (unsigned)zdd_operation::nonsup );
  if ( r >= 0 ) { return r; }

  unsigned rlow, rhigh;

  if ( node1.var < node2.var )
  {
    rlow = zdd_nonsup( node1.low, z2 );
    rhigh = zdd_nonsup( node1.high, z2 );
  }
  else
  {
    rlow = zdd_nonsup( node1.high, node2.high );
    auto rtmp = zdd_nonsup( node1.high, node2.low );
    rhigh = zdd_intersection( rtmp, rlow );
    rlow = zdd_nonsup( node1.low, node2.low );
  }
  const auto idx = unique_create( node1.var, rhigh, rlow );
  return cache.insert( z1, z2, (unsigned)zdd_operation::nonsup, idx );
}

unsigned zdd_manager::zdd_minhit( unsigned z )
{
  /* terminating cases */
  if ( z == 0u ) { return 1u; }
  if ( z == 1u ) { return 0u; }

  const auto r = cache.lookup( z, z, (unsigned)zdd_operation::minhit );
  if ( r >= 0 ) { return r; }

  const auto& node = nodes.at( z );
  auto rtmp = zdd_union( node.low, node.high );
  auto rlow = zdd_minhit( rtmp );
  rtmp = zdd_minhit( node.low );
  auto rhigh = zdd_nonsup( rtmp, rlow );

  const auto idx = unique_create( node.var, rhigh, rlow );
  return cache.insert( z, z, (unsigned)zdd_operation::minhit, idx );
}

unsigned zdd_manager::unique_create( unsigned var, unsigned high, unsigned low )
{
  if ( verbose )
  {
    std::cout << boost::format( "[i] attempt to create (%d, %d, %d)" ) % var % high % low << std::endl;
  }
  assert( var < nvars );
  assert( var < nodes[high].var );
  assert( var < nodes[low].var );

  if ( high == 0u ) { return low; }

  return unique_lookup( var, high, low );
}

std::ostream& operator<<( std::ostream& os, const zdd_manager& mgr )
{
  for ( auto node : index( mgr.nodes ) )
  {
    os << node.index << ": " << node.value << std::endl;
  }
  return os;
}

zdd& zdd::operator=( const zdd& other )
{
  if ( this == &other ) { return *this; }
  assert( !manager || manager == other.manager );
  manager = other.manager;
  index   = other.index;
  return *this;
}

unsigned zdd::var() const
{
  return manager->get_var( index );
}

zdd zdd::high() const
{
  return zdd( manager, manager->get_high( index ) );
}

zdd zdd::low() const
{
  return zdd( manager, manager->get_low( index ) );
}

zdd zdd::operator-( const zdd& other ) const
{
  assert( manager == other.manager );
  return zdd( manager, manager->zdd_diff( index, other.index ) );
}

zdd zdd::operator||( const zdd& other ) const
{
  assert( manager == other.manager );
  return zdd( manager, manager->zdd_union( index, other.index ) );
}

zdd zdd::operator&&( const zdd& other ) const
{
  assert( manager == other.manager );
  return zdd( manager, manager->zdd_intersection( index, other.index ) );
}

zdd zdd::operator^( const zdd& other ) const
{
  assert( manager == other.manager );
  return zdd( manager, manager->zdd_symmetric_difference( index, other.index ) );
}

zdd zdd::operator+( const zdd& other ) const
{
  assert( manager == other.manager );
  return zdd( manager, manager->zdd_join( index, other.index ) );
}

zdd zdd::operator*( const zdd& other ) const
{
  assert( manager == other.manager );
  return zdd( manager, manager->zdd_meet( index, other.index ) );
}

zdd zdd::delta( const zdd& other ) const
{
  assert( manager == other.manager );
  return zdd( manager, manager->zdd_delta( index, other.index ) );
}

zdd zdd::nonsub( const zdd& other ) const
{
  assert( manager == other.manager );
  return zdd( manager, manager->zdd_nonsub( index, other.index ) );
}

zdd zdd::nonsup( const zdd& other ) const
{
  assert( manager == other.manager );
  return zdd( manager, manager->zdd_nonsup( index, other.index ) );
}

zdd zdd::minhit() const
{
  return zdd( manager, manager->zdd_minhit( index ) );
}

bool zdd::equals( const zdd& other ) const
{
  assert( manager == other.manager );
  return index == other.index;
}

std::ostream& operator <<( std::ostream& os, const cirkit::zdd& z )
{
  auto s = cirkit::zdd_to_sets( z );
  for ( const auto& e : s )
  {
    cirkit::print_as_set( os, e ) << std::endl;
  }
  return os;
}

}

// Local Variables:
// c-basic-offset: 2
// eval: (c-set-offset 'substatement-open 0)
// eval: (c-set-offset 'innamespace 0)
// End:


/**
 * Copyright Soramitsu Co., Ltd. 2017 All Rights Reserved.
 * http://soramitsu.co.jp
 *
 * 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.
 */
#ifndef IROHA_AMOUNT_H
#define IROHA_AMOUNT_H

#include <boost/multiprecision/cpp_dec_float.hpp>
#include <boost/multiprecision/cpp_int.hpp>
#include <boost/optional.hpp>
#include <ciso646>
#include <cstdint>
#include <string>

namespace iroha {

  /**
   * Keeps integer and scale values allowing performing math
   * operations on them
   */
  class Amount {
   public:
    /**
     * Creates Amount with integer = 0 and scale = 0
     */
    Amount();

    /**
     * Amount with integer = amount and scale = 0
     * @param amount integer part
     */
    Amount(boost::multiprecision::uint256_t amount);

    /**
     * Amount with provided integer and scale part
     * @param amount integer part
     * @param precision scale part
     */
    Amount(boost::multiprecision::uint256_t amount, uint8_t precision);

    Amount(uint64_t first, uint64_t second, uint64_t third, uint64_t fourth);

    Amount(uint64_t first,
           uint64_t second,
           uint64_t third,
           uint64_t fourth,
           uint8_t precision);

    std::vector<uint64_t> to_uint64s();

    /**
     * Copy constructor
     */
    Amount(const Amount &);
    Amount &operator=(const Amount &);

    /**
     * Move constructor
     */
    Amount(Amount &&);
    Amount &operator=(Amount &&);

    boost::multiprecision::uint256_t getIntValue();
    uint8_t getPrecision();

    static boost::optional<Amount> createFromString(std::string str_amount);

    /**
     * Takes percentage from current amount
     * @param percents
     * @return
     */
    Amount percentage(boost::multiprecision::uint256_t percents) const;

    /**
     * Takes percentage represented as amount value
     * The current scale and scale of percents may differ
     * @param percents
     * @return
     */
    Amount percentage(const Amount &percents) const;

    /**
     * Sums up two optionals of the amounts.
     * Requires to have the same scale.
     * Otherwise nullopt is returned
     * @param a left term
     * @param b right term
     * @param optional result
     */
    friend boost::optional<Amount> operator+(boost::optional<Amount> a,
                                             boost::optional<Amount> b) {
      // check precisions
      if (a->precision_ != b->precision_) {
        return boost::none;
      }
      auto res = a->add(*b);
      // check overflow
      if (res.value_ < a->value_ or res.value_ < b->value_) {
        return boost::none;
      }
      return res;
    }

    /**
     * Subtracts right term from the left term
     * Requires to have the same scale.
     * Otherwise nullopt is returned
     * @param a left term
     * @param b right term
     * @param optional result
     */
    friend boost::optional<Amount> operator-(boost::optional<Amount> a,
                                             boost::optional<Amount> b) {
      // check precisions
      if (a->precision_ != b->precision_) {
        return boost::none;
      }
      // check if a greater than b
      if (a->value_ < b->value_) {
        return boost::none;
      }
      return a->subtract(*b);
    }

    /**
     * Comparisons are possible between amounts with different precisions.
     *
     * @return
     */
    bool operator==(const Amount &) const;
    bool operator!=(const Amount &) const;
    bool operator<(const Amount &) const;
    bool operator>(const Amount &) const;
    bool operator<=(const Amount &) const;
    bool operator>=(const Amount &) const;

    std::string to_string() const;
    ~Amount() = default;

   private:
    /**
     * Support function for comparison operators.
     * Returns 0 when equal, -1 when current Amount smaller, and 1 when it is
     * greater
     * @param other
     * @return
     */
    int compareTo(const Amount &other) const;

    /**
     * Sums two amounts.
     * @return
     */
    Amount add(const Amount &) const;
    /**
     * Subtracts one amount from another.
     * Requires to have the same scale between both amounts.
     * Otherwise nullopt is returned
     * @return
     */
    Amount subtract(const Amount &) const;

    boost::multiprecision::uint256_t value_{0};
    uint8_t precision_{0};
  };
}  // namespace iroha
#endif  // IROHA_AMOUNT_H


// ==========================================================================
// Munin Algorithm.
//
// Copyright (C) 2010 Matthew Chaplain, All Rights Reserved.
//
// Permission to reproduce, distribute, perform, display, and to prepare
// derivitive works from this file under the following conditions:
//
// 1. Any copy, reproduction or derivitive work of any part of this file
//    contains this copyright notice and licence in its entirety.
//
// 2. The rights granted to you under this license automatically terminate
//    should you attempt to assert any patent claims against the licensor
//    or contributors, which in any way restrict the ability of any party
//    from using this software or portions thereof in any form under the
//    terms of this license.
//
// Disclaimer: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
//             KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
//             WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
//             PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
//             OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
//             OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
//             OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
//             SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ==========================================================================
#include "munin/algorithm.hpp"
#include <terminalpp/canvas.hpp>
#include <terminalpp/canvas_view.hpp>
#include "odin/core.hpp"
#include <boost/bind.hpp>
#include <algorithm>

using namespace odin;
using namespace boost;
using namespace std;

namespace munin {

// ==========================================================================
// INTERSECTION
// ==========================================================================
optional<rectangle> intersection(rectangle const &lhs, rectangle const &rhs)
{
    // Check to see if the rectangles overlap.

    // Calculate the rectangle with the leftmost origin, and its counterpart.
    // Note: the rectangle that is not the leftmost is not necessarily the
    // rightmost, since the leftmost rectangle may extend further than its
    // counterpart.
    auto const &leftmost     = lhs.origin.x < rhs.origin.x ? lhs : rhs;
    auto const &not_leftmost = lhs.origin.x < rhs.origin.x ? rhs : lhs;

    // Calculate the rectangle with the topmost origin.
    auto const &topmost        = lhs.origin.y < rhs.origin.y ? lhs : rhs;
    auto const &not_topmost    = lhs.origin.y < rhs.origin.y ? rhs : lhs;

    bool overlaps =
        not_leftmost.origin.x >= leftmost.origin.x
     && not_leftmost.origin.x <  leftmost.origin.x + leftmost.size.width
     && not_topmost.origin.y  >= topmost.origin.y
     && not_topmost.origin.y  <  topmost.origin.y + topmost.size.height;

    if (!overlaps)
    {
        // There is no overlapping rectangle.
        return {};
    }

    // The overlapping rectangle has an origin that starts at the same x-
    // co-ordinate as the not-leftmost rectangle, and at the same y-co-ordinate
    // as the not-topmost rectangle.
    rectangle overlap;
    overlap.origin.x = not_leftmost.origin.x;
    overlap.origin.y = not_topmost.origin.y;

    // If the leftmost rectangle completely contains the other rectangle,
    // then the width is simply that of the enclosed rectangle.
    if (leftmost.origin.x + leftmost.size.width
      > not_leftmost.origin.x + not_leftmost.size.width)
    {
        overlap.size.width = not_leftmost.size.width;
    }
    // Otherwise, the calculated width is the width of the leftmost rectangle
    // minus the difference between the origins of the two rectangles.
    else
    {
        overlap.size.width =
            leftmost.size.width - (not_leftmost.origin.x - leftmost.origin.x);
    }

    // Same as above, but with height instead of width.
    if (topmost.origin.y + topmost.size.height
      > not_topmost.origin.y + not_topmost.size.height)
    {
        overlap.size.height = not_topmost.size.height;
    }
    else
    {
        overlap.size.height =
            topmost.size.height - (not_topmost.origin.y - topmost.origin.y);
    }

    return overlap;
}

// ==========================================================================
// CUT_SLICES
// ==========================================================================
static vector<rectangle> cut_slices(vector<rectangle> const &rectangles)
{
    vector<rectangle> slices;

    for (auto &&current_rectangle : rectangles)
    {
        for (odin::s32 row = 0; row < current_rectangle.size.height; ++row)
        {
            slices.push_back(
                rectangle{{ current_rectangle.origin.x
                          , current_rectangle.origin.y + row}
                        , {current_rectangle.size.width, 1}});
        }
    }

    return slices;
}

// ==========================================================================
// COMPARE_SLICES
// ==========================================================================
static bool compare_slices(rectangle const &lhs, rectangle const &rhs)
{
    if (lhs.origin.y == rhs.origin.y)
    {
        return lhs.origin.x < rhs.origin.x;
    }
    else
    {
        return lhs.origin.y < rhs.origin.y;
    }
}

// ==========================================================================
// HAS_EMPTY_HEIGHT
// ==========================================================================
static bool has_empty_height(rectangle const &rect)
{
    return rect.size.height == 0;
}

// ==========================================================================
// MERGE_OVERLAPPING_SLICES
// ==========================================================================
static vector<rectangle> merge_overlapping_slices(vector<rectangle> rectangles)
{
    sort(rectangles.begin(), rectangles.end(), compare_slices);

    auto first_slice = rectangles.begin();

    // Iterate through adjacent slices, merging any that overlap.
    for (; first_slice != rectangles.end(); ++first_slice)
    {
        // Skip over any slices that have been merged.
        if (first_slice->size.height == 0)
        {
            continue;
        }

        auto second_slice = first_slice + 1;

        // Iterate through all adjacent slices that share a y-coordinate
        // until we either run out of slices, or cannot merge a slice.
        for (;
             second_slice != rectangles.end()
          && first_slice->origin.y == second_slice->origin.y
          && first_slice->origin.x + first_slice->size.width >= second_slice->origin.x;
             ++second_slice)
        {
            // Set the width of the first slice to be equivalent to the
            // rightmost point of the two rectangles.
            first_slice->size.width = (std::max)(
                first_slice->origin.x + first_slice->size.width
              , second_slice->origin.x + second_slice->size.width)
              - first_slice->origin.x;

            // Mark the second slice as having been merged.
            second_slice->size.height = 0;
        }
    }

    // Snip out any rectangles that have been merged (have 0 height).
    rectangles.erase(remove_if(
            rectangles.begin()
          , rectangles.end()
          , has_empty_height)
      , rectangles.end());

    return rectangles;
}

// ==========================================================================
// RECTANGULAR_SLICE
// ==========================================================================
vector<rectangle> rectangular_slice(vector<rectangle> const &rectangles)
{
    return merge_overlapping_slices(cut_slices(rectangles));
}

// ==========================================================================
// CLIP_REGION
// ==========================================================================
static munin::rectangle clip_region(
    munin::rectangle region, terminalpp::extent size)
{
    if (region.origin.x < 0)
    {
        size.width += region.origin.x;
        region.origin.x = 0;
    }

    if (region.origin.y < 0)
    {
        size.height += region.origin.y;
        region.origin.y = 0;
    }

    if (region.origin.x >= size.width)
    {
        region.size.width = 0;
    }

    if (region.origin.y >= size.height)
    {
        region.size.height = 0;
    }

    if (region.origin.x < size.width
     && region.origin.y < size.height)
    {
        if (region.origin.x + region.size.width >= size.width)
        {
            region.size.width = size.width - region.origin.x;
        }

        if (region.origin.y + region.size.height >= size.height)
        {
            region.size.height = size.height - region.origin.y;
        }
    }

    return region;
}

// ==========================================================================
// HAS_ZERO_DIMENSION
// ==========================================================================
static bool has_zero_dimension(munin::rectangle const &region)
{
    return region.size.width == 0 || region.size.height == 0;
}

// ==========================================================================
// CLIP_REGIONS
// ==========================================================================
vector<rectangle> clip_regions(vector<rectangle> regions, terminalpp::extent size)
{
    // Returns a vector of rectangles that is identical to the regions
    // passed in, except that their extends are clipped to those of the
    // content's.
    transform(
        regions.begin()
      , regions.end()
      , regions.begin()
      , bind(clip_region, _1, size));

    return regions;
}

// ==========================================================================
// PRUNE_REGIONS
// ==========================================================================
vector<rectangle> prune_regions(vector<rectangle> regions)
{
    regions.erase(
        remove_if(
            regions.begin()
          , regions.end()
          , bind(has_zero_dimension, _1))
      , regions.end());

    return regions;
}

// ==========================================================================
// COPY_REGION
// ==========================================================================
void copy_region(
    rectangle               const &region
  , terminalpp::canvas      const &source
  , terminalpp::canvas_view       &destination)
{
    for (s32 y_coord = region.origin.y;
         y_coord < region.origin.y + region.size.height;
         ++y_coord)
    {
        for (s32 x_coord = region.origin.x;
             x_coord < region.origin.x + region.size.width;
             ++x_coord)
        {
            destination[x_coord][y_coord] = source[x_coord][y_coord];
        }
    }
}

}


#include <deal.II/base/conditional_ostream.h>
#include <deal.II/base/graph_coloring.h>
#include <deal.II/base/parameter_handler.h>
#include <deal.II/base/timer.h>
#include <deal.II/base/types.h>
#include <deal.II/lac/trilinos_precondition.h>
#include <deal.II/lac/trilinos_solver.h>
#include <deal.II/lac/trilinos_vector.h>
#include <deal.II/numerics/vector_tools.h>
// system includes ------------------------------------------------------------
#include <EpetraExt_HDF5.h>
#include <algorithm>
#include <boost/archive/binary_iarchive.hpp>
#include <boost/archive/binary_oarchive.hpp>
#include <boost/archive/text_oarchive.hpp>
#include <boost/filesystem.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/program_options.hpp>
#include <boost/static_assert.hpp>
#include <cmath>
#include <fstream>
#include <functional>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <string>
// own includes ---------------------------------------------------------------
#include "bte_config.h"
#include "app/app.hpp"
#include "aux/debug_output.hpp"
#include "aux/message.hpp"
#include "collision_tensor/collision_tensor.hpp"
#include "export/epetra_row_matrix.hpp"
//#include "collision_tensor/collision_tensor_factory.hpp"
#include "collision_tensor/collision_tensor_operator.hpp"
#include "export/export_dh.hpp"
#include "grid/dof_mapper_periodic_distributed.hpp"
#include "grid/grid_handler_periodic.hpp"
#include "init/import/load_coefficients.hpp"
#include "matrix/assembly/assembly.hpp"
#include "matrix/assembly/velocity_var_form.hpp"
#include "matrix/dofs/dofindex_sets.hpp"
#include "matrix/dofs/periodic_utility.hpp"
#include "matrix/system_matrix_handler.hpp"
#include "method/method.hpp"
#include "post_processing/xdmf_exporter.hpp"
#include "solver/aztecOO_condest.hpp"
#include "solver/solver_handler.hpp"
#include "spectral/basis/indexer.hpp"
#include "spectral/basis/spectral_basis_factory_ks.hpp"

using namespace std;
using namespace dealii;
using namespace boltzmann;
namespace bf = boost::filesystem;
namespace po = boost::program_options;

typedef SpectralBasisFactoryKS basis_factory_t;

const int dim = 2;
const double PI = dealii::numbers::PI;

// define the method
typedef Method<METHOD::MODLEASTSQUARES> method_t;
typedef App<dim> app_t;

typedef TrilinosWrappers::MPI::Vector vector_t;
// typedef TrilinosWrappers::Vector vector_local_t;
typedef TrilinosWrappers::SparseMatrix matrix_t;

int
main(int argc, char* argv[])
{
  boltzmann::Timer<> timer;
  int nthreads;
  // --------------------------------------------------
  // program options (read initial conditions from file)
  string scratch_dir;
  po::options_description options("options");
  options.add_options()("help", "produce help message")(
      "export-matrices", "export matrices to hdf5 file")("export-dofs", "export dofs and exit")(
      "petrov-galerkin", "use Galerkin disc. for scattering")(
      "threads,t", po::value<int>(&nthreads)->default_value(1), "number of threads")(
      "init,i", po::value<string>()->default_value(""), "initial distribution, dset='coeffs'")(
      "condest", "estimate condition number");
  po::variables_map vm;
  po::store(po::parse_command_line(argc, argv, options), vm);
  po::notify(vm);
  if (vm.count("help")) {
    cout << options << "\n";
    return 0;
  }

  Utilities::MPI::MPI_InitFinalize mpi_initialization(argc, argv, nthreads);
  const unsigned int process_id = Utilities::MPI::this_mpi_process(MPI_COMM_WORLD);
  const unsigned int nprocs = Utilities::MPI::n_mpi_processes(MPI_COMM_WORLD);

  ConditionalOStream pcout(std::cout, process_id == 0);
  pcout << "using " << nprocs << " MPI x " << nthreads << " OMP threads\n";
  pcout << "executable: " << argv[0] << endl << "periodic boundary conditions" << endl;

  std::string version_id = GIT_SHA1;
  pcout << "VersionID: " << version_id << "@" << GIT_BNAME << std::endl;

  // ----------------------------------------
  // yaml config
  if (!boost::filesystem::is_regular_file("config.yaml")) {
    pcout << "config file not found\n";
    return 1;
  }
  YAML::Node config = YAML::LoadFile("config.yaml");

  // make sure convergence messages etc. ware written only by proc 0
  if (process_id != 0) dealii::deallog.depth_console(0);
  pcout << "\n----------------------------------------\n";
  pcout << config << endl;
  pcout << "\n----------------------------------------\n";

  const size_t nK = config["SpectralBasis"]["deg"].as<size_t>();
  const double beta = 2;
  const double dt = config["TimeStepping"]["dt"].as<double>();
  // output frequency
  const size_t ntsteps = config["TimeStepping"]["N"].as<size_t>();

  // ----------------------------------------
  // MESH
  timer.start();
  GridHandlerPeriodic<2> grid_handler;
  grid_handler.init(config, nprocs);
  print_timer(timer.stop(), "initialize mesh", pcout);
  const auto& dof_handler = grid_handler.dofhandler();
  if (process_id == 0) export_dh(dof_handler);
  // ----------------------------------------------------------------------
  typedef basis_factory_t::basis_type basis_type;
  basis_type spectral_basis;
  basis_type spectral_test_basis;

  if (bf::exists("spectral_basis.desc")) {
    // read from file if possible
    // pcout << "reading basis from file" << std::endl;
    basis_factory_t::create(spectral_basis, "spectral_basis.desc");
  } else {
    bool sorted = true;
    basis_factory_t::create(spectral_basis, nK, nK, beta, sorted);
    if (process_id == 0)
      basis_factory_t::write_basis_descriptor(spectral_basis, "spectral_basis.desc");
  }

  if (bf::exists("spectral_basis_test.desc")) {
    basis_factory_t::create(spectral_test_basis, "spectral_basis_test.desc");
  } else {
    bool sorted = true;
    if (vm.count("petrov-galerkin"))
      basis_factory_t::create_test(spectral_test_basis, nK, nK, beta, sorted);
    else
      basis_factory_t::create(spectral_test_basis, nK, nK, beta, sorted);

    if (process_id == 0)
      basis_factory_t::write_basis_descriptor(spectral_test_basis, "spectral_basis_test.desc");
  }

  if (vm.count("export-dofs")) {
    pcout << "L: " << dof_handler.n_dofs() << "\n N: " << spectral_basis.n_dofs()
          << "\n L x N: " << dof_handler.n_dofs() * spectral_basis.n_dofs() << "\n";

    pcout << "\nvertex2dofix.dat/ dof.desc written to disk. Exit.\n";
    return 0;
  }

  // ----------------------------------------------------------------------------------------------------
  // DoFs
  const size_t n_phys_dofs = dof_handler.n_dofs();
  const size_t n_velo_dofs = spectral_basis.n_dofs();
  const size_t n_dofs = spectral_basis.n_dofs() * dof_handler.n_dofs();

  pcout << "#vertices on grid"
        << "\t" << n_phys_dofs << endl
        << "dim(V)"
        << "\t" << n_velo_dofs << endl;
  // regular indexer on full grid (used for post_processing and output)
  Indexer<> global_indexer(n_phys_dofs, n_velo_dofs);
  // indexer used for DoF enumeration
  typedef DoFMapperPeriodicDistributed<> dof_mapper_t;
  dof_mapper_t dof_mapper;
  dof_mapper.init(dof_handler, nprocs);
  IndexSet owned_phys_dofs = dof_mapper.owned_dofs(process_id);
  IndexSet relevant_phys_dofs = dof_mapper.relevant_dofs(process_id);

  DoFIndexSetsPeriodic index_sets(owned_phys_dofs, relevant_phys_dofs, n_velo_dofs, process_id);

  IndexSet owned_dofs = index_sets.locally_owned_dofs(process_id);
  IndexSet relevant_dofs = index_sets.locally_relevant_dofs();
  Indexer<dof_mapper_t> indexer(dof_mapper, n_phys_dofs, n_velo_dofs);
  pcout << "#DoFs = " << indexer.n_dofs() << endl;
  /// DoFIndexSets provides index_maps on distributed (unrestricted) grid
  // ----------------------------------------------------------------------
  // Velocity matrix entries
  VelocityVarForm<2> velocity_var_form;
  velocity_var_form.init(spectral_basis, beta);
  // --------------------------------------------------------------------------------------------------
  // Scattering
  bool has_scattering = (bool) config["Scattering"];
  CollisionTensorOperatorBase* QO = NULL;
  double kn = 1;  // Knudsen-number
  if (has_scattering) {
    std::string tensor_fname = config["Scattering"]["file"].as<std::string>().c_str();
    if (config["Scattering"]["Galerkin"] && !vm.count("petrov-galerkin")) {
      pcout << "CollisionTensor Galerkin\n";
      QO = new CollisionTensorOperatorG(
          dof_handler, spectral_basis, index_sets.locally_owned_dofs(process_id));
    } else if (config["Scattering"]["Petrov-Galerkin"] && vm.count("petrov-galerkin")) {
      pcout << "CollisionTensor Petrov-Galerkin\n";
      QO = new CollisionTensorOperatorPG(
          dof_handler, spectral_basis, index_sets.locally_owned_dofs(process_id));
    } else {
      // default
      pcout << "CollisionTensor Galerkin (DENSE)\n";
      QO = new CollisionTensorOperatorDense<>(
          dof_handler, spectral_basis, index_sets.locally_owned_dofs(process_id));
    }
    timer.start();
    QO->load_tensor(tensor_fname);
    print_timer(timer.stop(), "load collision_tensor", pcout);

    if (config["Scattering"]["kn"]) {
      kn = config["Scattering"]["kn"].as<double>();
    }
  }

  SystemMatrixHandler<method_t, app_t> system_matrix_handler(
      dof_handler, spectral_basis, indexer, index_sets, dt);

  const auto& A = system_matrix_handler.get_lhs();
  const auto& M = system_matrix_handler.get_rhs();

#ifdef HAVE_EPETRAEXT_HDF5
  if (vm.count("export-matrices")) {
    pcout << "Export matrices to hdf5\n";
    ExportEpetraHDF5 matrix_exporter(A.trilinos_matrix().Comm());
    matrix_exporter.write("A", A.trilinos_matrix());
    matrix_exporter.write("M", M.trilinos_matrix());
  }
#endif

  // estimate condition number
  if (vm.count("condest")) {
    int maxiters = 100;
    // estimate condest from BC
    AztecOOCondest condestCG;
    condestCG.initialize(A, "GMRES");
    double condCG = condestCG.compute(maxiters);
    pcout << "estimated condition number (GMRES): " << condCG << endl;
  }

  // ----------------------------------------------------------------------
  // Initial conditions
  // create f0 on all DoFs
  vector<double> f0_full_grid(n_velo_dofs * n_phys_dofs);
  pcout << "loading initial-distribution from file: " << vm["init"].as<string>() << endl;
  load_coefficients(f0_full_grid, vm["init"].as<string>(), dof_handler, global_indexer);
  // convert between periodic and non-periodic solution on phase space grid
  vector_t mu(owned_dofs);
  vector_t mu_ghosted(owned_dofs, relevant_dofs);
  // ----------------------------------------------------------------------
  // Output
  GridHelper grid_helper(n_velo_dofs, n_phys_dofs);
  grid_helper.to_restricted_grid(mu, f0_full_grid, global_indexer, indexer, owned_phys_dofs);

  /*
   *   hint: DoFIndexSets contains full grid indices
   */
  DoFIndexSets dof_map(nprocs);
  dof_map.init(dof_handler, n_velo_dofs);
  IndexSet ghosts = dof_map.locally_relevant_dofs();
  ghosts.subtract_set(dof_map.locally_owned_dofs(process_id));

  vector_t mu_f(dof_map.locally_owned_dofs(process_id));
  mu_ghosted = mu;
  auto dofs = dof_map.locally_owned_phys_dofs(process_id);
  unsigned int n_vtk = config["TimeStepping"]["export_vtk"]
                           ? config["TimeStepping"]["export_vtk"].as<unsigned int>()
                           : 1;
  XDMFH5Exporter xdmf_exporter(
      dof_handler, spectral_basis, dof_map.locally_owned_phys_dofs(process_id), 1, n_vtk);

  grid_helper.to_full_grid(mu_f, mu_ghosted, global_indexer, indexer, dofs);
  xdmf_exporter(mu_f, 0);
  // mu_f_ghosted = mu_f;
  //  output.run(mu_f_ghosted, 0, 0, global_indexer);
  // ----------------------------------------------------------------------------------------------------
  // Timestepping
  pcout << "\n\n-------------------- TIME STEPPING --------------------\n\n";
  vector_t rhs(owned_dofs);
  vector_t sc(owned_dofs);
  double current_time = 0;
  // dump full solution vector / export paraview data every xx timestep
  unsigned int n_dump =
      config["TimeStepping"]["dump"] ? config["TimeStepping"]["dump"].as<unsigned int>() : 0;

  SolverHandler solver_handler;
  timer.start();
  solver_handler.init(config, A);
  print_timer(timer.stop(), "Preconditioner", pcout);
  auto& solver = solver_handler.get_solver();
  auto& P = solver_handler.get_preconditioner();

#ifdef HAVE_EPETRAEXT_HDF5
  // write full solution to disk
  const auto& trilinos_vector = mu_f.trilinos_vector();
  EpetraExt::HDF5 epetra_exporter(trilinos_vector.Comm());
  epetra_exporter.Create("solution_vector" + boost::lexical_cast<string>(0) + ".h5");
  epetra_exporter.Write(boost::lexical_cast<string>(0), trilinos_vector);
  epetra_exporter.Flush();
  epetra_exporter.Close();
#endif

  for (unsigned int i = 1; i <= ntsteps; ++i) {
    timer.start();
    M.vmult(rhs, mu);
    print_timer(timer.stop(), "MV-product", pcout);
    timer.start();
    solver.solve(A, mu, rhs, P);
    auto ctr = solver.control();
    pcout << "Solver::GMRES " << ctr.last_step() << "\t" << ctr.last_value() << "\n";
    print_timer(timer.stop(), "timestep transport", pcout);

    if (has_scattering) {
      timer.start();
      QO->apply(mu, dt / kn);
      print_timer(timer.stop(), "timestep scattering", pcout);
    }

    current_time += dt;
    mu_ghosted = mu;
    grid_helper.to_full_grid(mu_f, mu_ghosted, global_indexer, indexer, dofs);
    xdmf_exporter(mu_f, current_time);

#ifdef HAVE_EPETRAEXT_HDF5
    if ((n_dump && (i % n_dump == 0)) || i == ntsteps) {
      timer.start();
      const auto& trilinos_vector = mu_f.trilinos_vector();
      EpetraExt::HDF5 epetra_exporter2(trilinos_vector.Comm());
      epetra_exporter2.Create("solution_vector" + boost::lexical_cast<string>(i) + ".h5");
      epetra_exporter2.Write(boost::lexical_cast<string>(i), trilinos_vector);
      epetra_exporter2.Flush();
      epetra_exporter2.Close();
      print_timer(timer.stop(), "EpetraExt IO", pcout);
    }
#endif
  }

  return 0;
}


//
//  Copyright (C) 2007-2013 Greg Landrum
//
//   @@ All Rights Reserved @@
//  This file is part of the RDKit.
//  The contents are covered by the terms of the BSD license
//  which is included in the file license.txt, found at the root
//  of the RDKit source tree.
//

#include <GraphMol/RDKitBase.h>
#include <GraphMol/Fingerprints/AtomPairs.h>
#include <GraphMol/Subgraphs/Subgraphs.h>
#include <DataStructs/SparseIntVect.h>
#include <RDGeneral/hash/hash.hpp>
#include <cstdint>
#include <boost/dynamic_bitset.hpp>
#include <boost/foreach.hpp>
#include <GraphMol/Fingerprints/FingerprintUtil.h>

namespace RDKit {
namespace AtomPairs {

template <typename T1, typename T2>
void updateElement(SparseIntVect<T1> &v, T2 elem) {
  v.setVal(elem, v.getVal(elem) + 1);
}

template <typename T1>
void updateElement(ExplicitBitVect &v, T1 elem) {
  v.setBit(elem % v.getNumBits());
}

template <typename T>
void setAtomPairBit(std::uint32_t i, std::uint32_t j,
                    std::uint32_t nAtoms,
                    const std::vector<std::uint32_t> &atomCodes,
                    const double *dm, T *bv, unsigned int minLength,
                    unsigned int maxLength, bool includeChirality) {
  auto dist = static_cast<unsigned int>(floor(dm[i * nAtoms + j]));
  if (dist >= minLength && dist <= maxLength) {
    std::uint32_t bitId =
        getAtomPairCode(atomCodes[i], atomCodes[j], dist, includeChirality);
    updateElement(*bv, static_cast<std::uint32_t>(bitId));
  }
}

SparseIntVect<std::int32_t> *getAtomPairFingerprint(
    const ROMol &mol, const std::vector<std::uint32_t> *fromAtoms,
    const std::vector<std::uint32_t> *ignoreAtoms,
    const std::vector<std::uint32_t> *atomInvariants, bool includeChirality,
    bool use2D, int confId) {
  return getAtomPairFingerprint(mol, 1, maxPathLen - 1, fromAtoms, ignoreAtoms,
                                atomInvariants, includeChirality, use2D,
                                confId);
};

SparseIntVect<std::int32_t> *getAtomPairFingerprint(
    const ROMol &mol, unsigned int minLength, unsigned int maxLength,
    const std::vector<std::uint32_t> *fromAtoms,
    const std::vector<std::uint32_t> *ignoreAtoms,
    const std::vector<std::uint32_t> *atomInvariants, bool includeChirality,
    bool use2D, int confId) {
  PRECONDITION(minLength <= maxLength, "bad lengths provided");
  PRECONDITION(!atomInvariants || atomInvariants->size() >= mol.getNumAtoms(),
               "bad atomInvariants size");

  const ROMol *lmol = &mol;
  std::unique_ptr<ROMol> tmol;
  if (includeChirality && !mol.hasProp(common_properties::_StereochemDone)) {
    tmol = std::unique_ptr<ROMol>(new ROMol(mol));
    MolOps::assignStereochemistry(*tmol);
    lmol = tmol.get();
  }

  auto *res = new SparseIntVect<std::int32_t>(
      1 << (numAtomPairFingerprintBits + 2 * (includeChirality ? 2 : 0)));
  const double *dm;
  if (use2D) {
    dm = MolOps::getDistanceMat(*lmol);
  } else {
    dm = MolOps::get3DDistanceMat(*lmol, confId);
  }
  const unsigned int nAtoms = lmol->getNumAtoms();

  std::vector<std::uint32_t> atomCodes;
  for (ROMol::ConstAtomIterator atomItI = lmol->beginAtoms();
       atomItI != lmol->endAtoms(); ++atomItI) {
    if (!atomInvariants) {
      atomCodes.push_back(getAtomCode(*atomItI, 0, includeChirality));
    } else {
      atomCodes.push_back((*atomInvariants)[(*atomItI)->getIdx()] %
                          ((1 << codeSize) - 1));
    }
  }

  for (ROMol::ConstAtomIterator atomItI = lmol->beginAtoms();
       atomItI != lmol->endAtoms(); ++atomItI) {
    unsigned int i = (*atomItI)->getIdx();
    if (ignoreAtoms && std::find(ignoreAtoms->begin(), ignoreAtoms->end(), i) !=
                           ignoreAtoms->end()) {
      continue;
    }
    if (!fromAtoms) {
      for (ROMol::ConstAtomIterator atomItJ = atomItI + 1;
           atomItJ != lmol->endAtoms(); ++atomItJ) {
        unsigned int j = (*atomItJ)->getIdx();
        if (ignoreAtoms && std::find(ignoreAtoms->begin(), ignoreAtoms->end(),
                                     j) != ignoreAtoms->end()) {
          continue;
        }
        setAtomPairBit(i, j, nAtoms, atomCodes, dm, res, minLength, maxLength,
                       includeChirality);
      }
    } else {
      BOOST_FOREACH (std::uint32_t j, *fromAtoms) {
        if (j != i) {
          if (ignoreAtoms && std::find(ignoreAtoms->begin(), ignoreAtoms->end(),
                                       j) != ignoreAtoms->end()) {
            continue;
          }
          setAtomPairBit(i, j, nAtoms, atomCodes, dm, res, minLength, maxLength,
                         includeChirality);
        }
      }
    }
  }
  return res;
}

SparseIntVect<std::int32_t> *getHashedAtomPairFingerprint(
    const ROMol &mol, unsigned int nBits, unsigned int minLength,
    unsigned int maxLength, const std::vector<std::uint32_t> *fromAtoms,
    const std::vector<std::uint32_t> *ignoreAtoms,
    const std::vector<std::uint32_t> *atomInvariants, bool includeChirality,
    bool use2D, int confId) {
  PRECONDITION(minLength <= maxLength, "bad lengths provided");
  PRECONDITION(!atomInvariants || atomInvariants->size() >= mol.getNumAtoms(),
               "bad atomInvariants size");
  const ROMol *lmol = &mol;
  std::unique_ptr<ROMol> tmol;
  if (includeChirality && !mol.hasProp(common_properties::_StereochemDone)) {
    tmol = std::unique_ptr<ROMol>(new ROMol(mol));
    MolOps::assignStereochemistry(*tmol);
    lmol = tmol.get();
  }
  auto *res = new SparseIntVect<std::int32_t>(nBits);
  const double *dm;
  try {
    if (use2D) {
      dm = MolOps::getDistanceMat(*lmol);
    } else {
      dm = MolOps::get3DDistanceMat(*lmol, confId);
    }
  } catch (const ConformerException &) {
    delete res;
    throw;
  }

  const unsigned int nAtoms = lmol->getNumAtoms();

  std::vector<std::uint32_t> atomCodes;
  atomCodes.reserve(nAtoms);
  for (ROMol::ConstAtomIterator atomItI = lmol->beginAtoms();
       atomItI != lmol->endAtoms(); ++atomItI) {
    if (!atomInvariants) {
      atomCodes.push_back(getAtomCode(*atomItI, 0, includeChirality));
    } else {
      atomCodes.push_back((*atomInvariants)[(*atomItI)->getIdx()]);
    }
  }

  for (ROMol::ConstAtomIterator atomItI = lmol->beginAtoms();
       atomItI != lmol->endAtoms(); ++atomItI) {
    unsigned int i = (*atomItI)->getIdx();
    if (ignoreAtoms && std::find(ignoreAtoms->begin(), ignoreAtoms->end(), i) !=
                           ignoreAtoms->end()) {
      continue;
    }
    if (!fromAtoms) {
      for (ROMol::ConstAtomIterator atomItJ = atomItI + 1;
           atomItJ != lmol->endAtoms(); ++atomItJ) {
        unsigned int j = (*atomItJ)->getIdx();
        if (ignoreAtoms && std::find(ignoreAtoms->begin(), ignoreAtoms->end(),
                                     j) != ignoreAtoms->end()) {
          continue;
        }
        auto dist = static_cast<unsigned int>(floor(dm[i * nAtoms + j]));
        if (dist >= minLength && dist <= maxLength) {
          std::uint32_t bit = 0;
          gboost::hash_combine(bit, std::min(atomCodes[i], atomCodes[j]));
          gboost::hash_combine(bit, dist);
          gboost::hash_combine(bit, std::max(atomCodes[i], atomCodes[j]));
          updateElement(*res, static_cast<std::int32_t>(bit % nBits));
        }
      }
    } else {
      BOOST_FOREACH (std::uint32_t j, *fromAtoms) {
        if (j != i) {
          if (ignoreAtoms && std::find(ignoreAtoms->begin(), ignoreAtoms->end(),
                                       j) != ignoreAtoms->end()) {
            continue;
          }
          auto dist = static_cast<unsigned int>(floor(dm[i * nAtoms + j]));
          if (dist >= minLength && dist <= maxLength) {
            std::uint32_t bit = 0;
            gboost::hash_combine(bit, std::min(atomCodes[i], atomCodes[j]));
            gboost::hash_combine(bit, dist);
            gboost::hash_combine(bit, std::max(atomCodes[i], atomCodes[j]));
            updateElement(*res, static_cast<std::int32_t>(bit % nBits));
          }
        }
      }
    }
  }
  return res;
}

ExplicitBitVect *getHashedAtomPairFingerprintAsBitVect(
    const ROMol &mol, unsigned int nBits, unsigned int minLength,
    unsigned int maxLength, const std::vector<std::uint32_t> *fromAtoms,
    const std::vector<std::uint32_t> *ignoreAtoms,
    const std::vector<std::uint32_t> *atomInvariants,
    unsigned int nBitsPerEntry, bool includeChirality, bool use2D, int confId) {
  PRECONDITION(minLength <= maxLength, "bad lengths provided");
  PRECONDITION(!atomInvariants || atomInvariants->size() >= mol.getNumAtoms(),
               "bad atomInvariants size");
  static int bounds[4] = {1, 2, 4, 8};

  unsigned int blockLength = nBits / nBitsPerEntry;
  SparseIntVect<std::int32_t> *sres = getHashedAtomPairFingerprint(
      mol, blockLength, minLength, maxLength, fromAtoms, ignoreAtoms,
      atomInvariants, includeChirality, use2D, confId);
  auto *res = new ExplicitBitVect(nBits);
  if (nBitsPerEntry != 4) {
    BOOST_FOREACH (SparseIntVect<boost::int64_t>::StorageType::value_type val,
                   sres->getNonzeroElements()) {
      for (unsigned int i = 0; i < nBitsPerEntry; ++i) {
        if (val.second > static_cast<int>(i)) {
          res->setBit(val.first * nBitsPerEntry + i);
        }
      }
    }
  } else {
    BOOST_FOREACH (SparseIntVect<boost::int64_t>::StorageType::value_type val,
                   sres->getNonzeroElements()) {
      for (unsigned int i = 0; i < nBitsPerEntry; ++i) {
        if (val.second >= bounds[i]) {
          res->setBit(val.first * nBitsPerEntry + i);
        }
      }
    }
  }
  delete sres;
  return res;
}

SparseIntVect<boost::int64_t> *getTopologicalTorsionFingerprint(
    const ROMol &mol, unsigned int targetSize,
    const std::vector<std::uint32_t> *fromAtoms,
    const std::vector<std::uint32_t> *ignoreAtoms,
    const std::vector<std::uint32_t> *atomInvariants, bool includeChirality) {
  PRECONDITION(!atomInvariants || atomInvariants->size() >= mol.getNumAtoms(),
               "bad atomInvariants size");
  const ROMol *lmol = &mol;
  std::unique_ptr<ROMol> tmol;
  if (includeChirality && !mol.hasProp(common_properties::_StereochemDone)) {
    tmol = std::unique_ptr<ROMol>(new ROMol(mol));
    MolOps::assignStereochemistry(*tmol);
    lmol = tmol.get();
  }
  boost::uint64_t sz = 1;
  sz = (sz << (targetSize *
               (codeSize + (includeChirality ? numChiralBits : 0))));
  // NOTE: this -1 is incorrect but it's needed for backwards compatibility.
  //  hopefully we'll never have a case with a torsion that hits this.
  //
  //  mmm, bug compatible.
  sz -= 1;
  auto *res = new SparseIntVect<boost::int64_t>(sz);

  std::vector<std::uint32_t> atomCodes;
  atomCodes.reserve(lmol->getNumAtoms());
  for (ROMol::ConstAtomIterator atomItI = lmol->beginAtoms();
       atomItI != lmol->endAtoms(); ++atomItI) {
    if (!atomInvariants) {
      atomCodes.push_back(getAtomCode(*atomItI, 0, includeChirality));
    } else {
      // need to add to the atomCode here because we subtract off up to 2 below
      // as part of the branch correction
      atomCodes.push_back(
          (*atomInvariants)[(*atomItI)->getIdx()] % ((1 << codeSize) - 1) + 2);
    }
  }

  boost::dynamic_bitset<> *fromAtomsBV = nullptr;
  if (fromAtoms) {
    fromAtomsBV = new boost::dynamic_bitset<>(lmol->getNumAtoms());
    BOOST_FOREACH (std::uint32_t fAt, *fromAtoms) { fromAtomsBV->set(fAt); }
  }
  boost::dynamic_bitset<> *ignoreAtomsBV = nullptr;
  if (ignoreAtoms) {
    ignoreAtomsBV = new boost::dynamic_bitset<>(mol.getNumAtoms());
    BOOST_FOREACH (std::uint32_t fAt, *ignoreAtoms) {
      ignoreAtomsBV->set(fAt);
    }
  }
  boost::dynamic_bitset<> pAtoms(lmol->getNumAtoms());
  PATH_LIST paths = findAllPathsOfLengthN(*lmol, targetSize, false);
  for (PATH_LIST::const_iterator pathIt = paths.begin(); pathIt != paths.end();
       ++pathIt) {
    bool keepIt = true;
    if (fromAtomsBV) {
      keepIt = false;
    }
    std::vector<std::uint32_t> pathCodes;
    const PATH_TYPE &path = *pathIt;
    if (fromAtomsBV) {
      if (fromAtomsBV->test(static_cast<std::uint32_t>(path.front())) ||
          fromAtomsBV->test(static_cast<std::uint32_t>(path.back()))) {
        keepIt = true;
      }
    }
    if (keepIt && ignoreAtomsBV) {
      BOOST_FOREACH (int pElem, path) {
        if (ignoreAtomsBV->test(pElem)) {
          keepIt = false;
          break;
        }
      }
    }
    if (keepIt) {
      pAtoms.reset();
      for (auto pIt = path.begin(); pIt < path.end(); ++pIt) {
        // look for a cycle that doesn't start at the first atom
        // we can't effectively canonicalize these at the moment
        // (was github #811)
        if (pIt != path.begin() && *pIt != *(path.begin()) && pAtoms[*pIt]) {
          pathCodes.clear();
          break;
        }
        pAtoms.set(*pIt);
        unsigned int code = atomCodes[*pIt] - 1;
        // subtract off the branching number:
        if (pIt != path.begin() && pIt + 1 != path.end()) {
          --code;
        }
        pathCodes.push_back(code);
      }
      if (pathCodes.size()) {
        boost::int64_t code =
            getTopologicalTorsionCode(pathCodes, includeChirality);
        updateElement(*res, code);
      }
    }
  }
  delete fromAtomsBV;
  delete ignoreAtomsBV;

  return res;
}

namespace {
template <typename T>
void TorsionFpCalc(T *res, const ROMol &mol, unsigned int nBits,
                   unsigned int targetSize,
                   const std::vector<std::uint32_t> *fromAtoms,
                   const std::vector<std::uint32_t> *ignoreAtoms,
                   const std::vector<std::uint32_t> *atomInvariants,
                   bool includeChirality) {
  PRECONDITION(!atomInvariants || atomInvariants->size() >= mol.getNumAtoms(),
               "bad atomInvariants size");
  const ROMol *lmol = &mol;
  std::unique_ptr<ROMol> tmol;
  if (includeChirality && !mol.hasProp(common_properties::_StereochemDone)) {
    tmol = std::unique_ptr<ROMol>(new ROMol(mol));
    MolOps::assignStereochemistry(*tmol);
    lmol = tmol.get();
  }
  std::vector<std::uint32_t> atomCodes;
  atomCodes.reserve(lmol->getNumAtoms());
  for (ROMol::ConstAtomIterator atomItI = lmol->beginAtoms();
       atomItI != lmol->endAtoms(); ++atomItI) {
    if (!atomInvariants) {
      atomCodes.push_back(getAtomCode(*atomItI, 0, includeChirality));
    } else {
      // need to add to the atomCode here because we subtract off up to 2 below
      // as part of the branch correction
      atomCodes.push_back(((*atomInvariants)[(*atomItI)->getIdx()] << 1) + 1);
    }
  }

  boost::dynamic_bitset<> *fromAtomsBV = nullptr;
  if (fromAtoms) {
    fromAtomsBV = new boost::dynamic_bitset<>(lmol->getNumAtoms());
    BOOST_FOREACH (std::uint32_t fAt, *fromAtoms) { fromAtomsBV->set(fAt); }
  }
  boost::dynamic_bitset<> *ignoreAtomsBV = nullptr;
  if (ignoreAtoms) {
    ignoreAtomsBV = new boost::dynamic_bitset<>(lmol->getNumAtoms());
    BOOST_FOREACH (std::uint32_t fAt, *ignoreAtoms) {
      ignoreAtomsBV->set(fAt);
    }
  }

  PATH_LIST paths = findAllPathsOfLengthN(*lmol, targetSize, false);
  for (PATH_LIST::const_iterator pathIt = paths.begin(); pathIt != paths.end();
       ++pathIt) {
    bool keepIt = true;
    if (fromAtomsBV) {
      keepIt = false;
    }
    const PATH_TYPE &path = *pathIt;
    if (fromAtomsBV) {
      if (fromAtomsBV->test(static_cast<std::uint32_t>(path.front())) ||
          fromAtomsBV->test(static_cast<std::uint32_t>(path.back()))) {
        keepIt = true;
      }
    }
    if (keepIt && ignoreAtomsBV) {
      BOOST_FOREACH (int pElem, path) {
        if (ignoreAtomsBV->test(pElem)) {
          keepIt = false;
          break;
        }
      }
    }
    if (keepIt) {
      std::vector<std::uint32_t> pathCodes(targetSize);
      for (unsigned int i = 0; i < targetSize; ++i) {
        unsigned int code = atomCodes[path[i]] - 1;
        // subtract off the branching number:
        if (i > 0 && i < targetSize - 1) {
          --code;
        }
        pathCodes[i] = code;
      }
      size_t bit = getTopologicalTorsionHash(pathCodes);
      updateElement(*res, bit % nBits);
    }
  }
  delete fromAtomsBV;
  delete ignoreAtomsBV;
}
}  // namespace
SparseIntVect<boost::int64_t> *getHashedTopologicalTorsionFingerprint(
    const ROMol &mol, unsigned int nBits, unsigned int targetSize,
    const std::vector<std::uint32_t> *fromAtoms,
    const std::vector<std::uint32_t> *ignoreAtoms,
    const std::vector<std::uint32_t> *atomInvariants, bool includeChirality) {
  PRECONDITION(!atomInvariants || atomInvariants->size() >= mol.getNumAtoms(),
               "bad atomInvariants size");
  auto *res = new SparseIntVect<boost::int64_t>(nBits);
  TorsionFpCalc(res, mol, nBits, targetSize, fromAtoms, ignoreAtoms,
                atomInvariants, includeChirality);
  return res;
}

ExplicitBitVect *getHashedTopologicalTorsionFingerprintAsBitVect(
    const ROMol &mol, unsigned int nBits, unsigned int targetSize,
    const std::vector<std::uint32_t> *fromAtoms,
    const std::vector<std::uint32_t> *ignoreAtoms,
    const std::vector<std::uint32_t> *atomInvariants,
    unsigned int nBitsPerEntry, bool includeChirality) {
  PRECONDITION(!atomInvariants || atomInvariants->size() >= mol.getNumAtoms(),
               "bad atomInvariants size");
  static int bounds[4] = {1, 2, 4, 8};
  unsigned int blockLength = nBits / nBitsPerEntry;
  auto *sres = new SparseIntVect<boost::int64_t>(blockLength);
  TorsionFpCalc(sres, mol, blockLength, targetSize, fromAtoms, ignoreAtoms,
                atomInvariants, includeChirality);
  auto *res = new ExplicitBitVect(nBits);

  if (nBitsPerEntry != 4) {
    BOOST_FOREACH (SparseIntVect<boost::int64_t>::StorageType::value_type val,
                   sres->getNonzeroElements()) {
      for (unsigned int i = 0; i < nBitsPerEntry; ++i) {
        if (val.second > static_cast<int>(i)) {
          res->setBit(val.first * nBitsPerEntry + i);
        }
      }
    }
  } else {
    BOOST_FOREACH (SparseIntVect<boost::int64_t>::StorageType::value_type val,
                   sres->getNonzeroElements()) {
      for (unsigned int i = 0; i < nBitsPerEntry; ++i) {
        if (val.second >= bounds[i]) {
          res->setBit(val.first * nBitsPerEntry + i);
        }
      }
    }
  }
  delete sres;
  return res;
}
}  // end of namespace AtomPairs
}  // end of namespace RDKit