//
// Copyright © 2017 Arm Ltd. All rights reserved.
// See LICENSE file in the project root for full license information.
//
#include <boost/test/unit_test.hpp>

#include "armnn/Descriptors.hpp"
#include "armnn/IRuntime.hpp"
#include "armnn/INetwork.hpp"

#include "backends/test/QuantizeHelper.hpp"
#include <boost/core/ignore_unused.hpp>

BOOST_AUTO_TEST_SUITE(EndToEnd)

namespace
{
template<typename T>
bool IsFloatIterFunc(T iter)
{
    boost::ignore_unused(iter);
    return IsFloatingPointIterator<T>::value;
}
} //namespace

BOOST_AUTO_TEST_CASE(QuantizedHelper)
{
    std::vector<float> fArray;
    BOOST_TEST(IsFloatIterFunc(fArray.begin()) == true);
    BOOST_TEST(IsFloatIterFunc(fArray.cbegin()) == true);

    std::vector<double> dArray;
    BOOST_TEST(IsFloatIterFunc(dArray.begin()) == true);

    std::vector<int> iArray;
    BOOST_TEST(IsFloatIterFunc(iArray.begin()) == false);

    float floats[5];
    BOOST_TEST(IsFloatIterFunc(&floats[0]) == true);

    int ints[5];
    BOOST_TEST(IsFloatIterFunc(&ints[0]) == false);
}

BOOST_AUTO_TEST_CASE(Unsigned8)
{
    using namespace armnn;

    // Create runtime in which test will run
    armnn::IRuntimePtr runtime(armnn::IRuntime::Create(armnn::Compute::CpuRef));

    // build up the structure of the network
    armnn::INetworkPtr net(INetwork::Create());

    IConnectableLayer* input = net->AddInputLayer(0, "input");
    IConnectableLayer* softmax = net->AddSoftmaxLayer(SoftmaxDescriptor(), "softmax");
    IConnectableLayer* output  = net->AddOutputLayer(0, "output");

    input->GetOutputSlot(0).Connect(softmax->GetInputSlot(0));
    softmax->GetOutputSlot(0).Connect(output->GetInputSlot(0));

    // set the tensors in the network
    TensorInfo inputTensorInfo(TensorShape({1, 5}), DataType::QuantisedAsymm8);
    inputTensorInfo.SetQuantizationOffset(100);
    inputTensorInfo.SetQuantizationScale(10000.0f);
    input->GetOutputSlot(0).SetTensorInfo(inputTensorInfo);

    TensorInfo outputTensorInfo(TensorShape({1, 5}), DataType::QuantisedAsymm8);
    outputTensorInfo.SetQuantizationOffset(0);
    outputTensorInfo.SetQuantizationScale(1.0f/255.0f);
    softmax->GetOutputSlot(0).SetTensorInfo(outputTensorInfo);

    // optimize the network
    IOptimizedNetworkPtr optNet = Optimize(*net, runtime->GetDeviceSpec());

    // load it into the runtime
    NetworkId netId;
    auto error = runtime->LoadNetwork(netId, std::move(optNet));
    BOOST_TEST(error == Status::Success);

    // create structures for input & output
    std::vector<uint8_t> inputData
    {
        1, 10, 3, 200, 5 // some inputs - one of which is sufficiently larger than the others to saturate softmax
    };
    std::vector<uint8_t> outputData(5);

    armnn::InputTensors inputTensors
    {
        {0, armnn::ConstTensor(runtime->GetInputTensorInfo(netId, 0), inputData.data())}
    };
    armnn::OutputTensors outputTensors
    {
        {0, armnn::Tensor(runtime->GetOutputTensorInfo(netId, 0), outputData.data())}
    };

    // do the inference
    runtime->EnqueueWorkload(netId, inputTensors, outputTensors);

    // check the results
    BOOST_TEST(outputData[0] == 0);
    BOOST_TEST(outputData[1] == 0);
    BOOST_TEST(outputData[2] == 0);
    BOOST_TEST(outputData[3] == 255); // softmax has been saturated
    BOOST_TEST(outputData[4] == 0);
}

template <typename T>
void ConstantUsageTest(armnn::Compute computeDevice,
    const armnn::TensorInfo& commonTensorInfo,
    const std::vector<T>& inputData,
    const std::vector<T>& constantData,
    const std::vector<T>& expectedOutputData)
{
    using namespace armnn;

    // Create runtime in which test will run
    armnn::IRuntimePtr runtime(armnn::IRuntime::Create(computeDevice));

    // build up the structure of the network
    INetworkPtr net(INetwork::Create());

    IConnectableLayer* input = net->AddInputLayer(0);
    IConnectableLayer* constant = net->AddConstantLayer(ConstTensor(commonTensorInfo, constantData));
    IConnectableLayer* add = net->AddAdditionLayer();
    IConnectableLayer* output = net->AddOutputLayer(0);

    input->GetOutputSlot(0).Connect(add->GetInputSlot(0));
    constant->GetOutputSlot(0).Connect(add->GetInputSlot(1));
    add->GetOutputSlot(0).Connect(output->GetInputSlot(0));

    // set the tensors in the network
    input->GetOutputSlot(0).SetTensorInfo(commonTensorInfo);
    constant->GetOutputSlot(0).SetTensorInfo(commonTensorInfo);
    add->GetOutputSlot(0).SetTensorInfo(commonTensorInfo);

    // optimize the network
    IOptimizedNetworkPtr optNet = Optimize(*net, runtime->GetDeviceSpec());

    // load it into the runtime
    NetworkId netId;
    runtime->LoadNetwork(netId, std::move(optNet));

    // create structures for input & output
    std::vector<T> outputData(inputData.size());

    InputTensors inputTensors
    {
        {0, armnn::ConstTensor(runtime->GetInputTensorInfo(netId, 0), inputData.data())}
    };
    OutputTensors outputTensors
    {
        {0, armnn::Tensor(runtime->GetOutputTensorInfo(netId, 0), outputData.data())}
    };

    // do the inference
    runtime->EnqueueWorkload(netId, inputTensors, outputTensors);

    // check the results
    BOOST_TEST(outputData == expectedOutputData);
}

static void ConstantUsageFloat32Test(armnn::Compute computeDevice)
{
    const armnn::TensorInfo commonTensorInfo({ 2, 3 }, armnn::DataType::Float32);

    ConstantUsageTest(computeDevice,
        commonTensorInfo,
        std::vector<float>{ 1.f, 2.f, 3.f, 4.f, 5.f, 6.f }, // input
        std::vector<float>{ 6.f, 5.f, 4.f, 3.f, 2.f, 1.f }, // const input
        std::vector<float>{ 7.f, 7.f, 7.f, 7.f, 7.f, 7.f }  // expected output
    );
}

static void ConstantUsageUint8Test(armnn::Compute computeDevice)
{
    armnn::TensorInfo commonTensorInfo({ 2, 3 }, armnn::DataType::QuantisedAsymm8);

    const float scale = 0.023529f;
    const int8_t offset = -43;

    commonTensorInfo.SetQuantizationScale(scale);
    commonTensorInfo.SetQuantizationOffset(offset);

    ConstantUsageTest(computeDevice,
        commonTensorInfo,
        QuantizedVector<uint8_t>(scale, offset, { 1.f, 2.f, 3.f, 4.f, 5.f, 6.f }), // input
        QuantizedVector<uint8_t>(scale, offset, { 6.f, 5.f, 4.f, 3.f, 2.f, 1.f }), // const input
        QuantizedVector<uint8_t>(scale, offset, { 7.f, 7.f, 7.f, 7.f, 7.f, 7.f })  // expected output
    );
}

BOOST_AUTO_TEST_CASE(ConstantUsage_Ref_Float32)
{
    ConstantUsageFloat32Test(armnn::Compute::CpuRef);
}

#if ARMCOMPUTENEON_ENABLED
BOOST_AUTO_TEST_CASE(ConstantUsage_Neon_Float32)
{
    ConstantUsageFloat32Test(armnn::Compute::CpuAcc);
}
#endif

#if ARMCOMPUTECL_ENABLED
BOOST_AUTO_TEST_CASE(ConstantUsage_Cl_Float32)
{
    ConstantUsageFloat32Test(armnn::Compute::GpuAcc);
}
#endif

BOOST_AUTO_TEST_CASE(ConstantUsage_Ref_Uint8)
{
    ConstantUsageUint8Test(armnn::Compute::CpuRef);
}

BOOST_AUTO_TEST_CASE(TrivialAdd)
{
    // This test was designed to match "AddTwo" in android nn/runtime/test/TestTrivialModel.cpp

    using namespace armnn;

    // Create runtime in which test will run
    armnn::IRuntimePtr runtime(armnn::IRuntime::Create(armnn::Compute::CpuRef));

    // build up the structure of the network
    armnn::INetworkPtr net(INetwork::Create());

    IConnectableLayer* input1 = net->AddInputLayer(0);
    IConnectableLayer* input2 = net->AddInputLayer(1);
    IConnectableLayer* add    = net->AddAdditionLayer();
    IConnectableLayer* output = net->AddOutputLayer(0);

    input1->GetOutputSlot(0).Connect(add->GetInputSlot(0));
    input2->GetOutputSlot(0).Connect(add->GetInputSlot(1));
    add->GetOutputSlot(0).Connect(output->GetInputSlot(0));

    // set the tensors in the network
    TensorInfo tensorInfo(TensorShape({3, 4}), DataType::Float32);
    input1->GetOutputSlot(0).SetTensorInfo(tensorInfo);
    input2->GetOutputSlot(0).SetTensorInfo(tensorInfo);
    add->GetOutputSlot(0).SetTensorInfo(tensorInfo);

    // optimize the network
    IOptimizedNetworkPtr optNet = Optimize(*net, runtime->GetDeviceSpec());

    // load it into the runtime
    NetworkId netId;
    runtime->LoadNetwork(netId, std::move(optNet));

    // create structures for input & output - matching android nn test
    std::vector<float> input1Data
    {
        1.f, 2.f, 3.f, 4.f, 5.f, 6.f, 7.f, 8.f, 9.f, 10.f, 11.f, 12.f
    };
    std::vector<float> input2Data
    {
        100.f, 200.f, 300.f, 400.f, 500.f, 600.f, 700.f, 800.f, 900.f, 1000.f, 1100.f, 1200.f
    };
    std::vector<float> outputData(12);

    InputTensors inputTensors
    {
        {0,armnn::ConstTensor(runtime->GetInputTensorInfo(netId, 0), input1Data.data())},
        {1,armnn::ConstTensor(runtime->GetInputTensorInfo(netId, 0), input2Data.data())}
    };
    OutputTensors outputTensors
    {
        {0,armnn::Tensor(runtime->GetOutputTensorInfo(netId, 0), outputData.data())}
    };

    // do the inference
    runtime->EnqueueWorkload(netId, inputTensors, outputTensors);

    // check the results
    BOOST_TEST(outputData[0] == 101);
    BOOST_TEST(outputData[1] == 202);
    BOOST_TEST(outputData[2] == 303);
    BOOST_TEST(outputData[3] == 404);
    BOOST_TEST(outputData[4] == 505);
    BOOST_TEST(outputData[5] == 606);
    BOOST_TEST(outputData[6] == 707);
    BOOST_TEST(outputData[7] == 808);
    BOOST_TEST(outputData[8] == 909);
    BOOST_TEST(outputData[9] == 1010);
    BOOST_TEST(outputData[10] == 1111);
    BOOST_TEST(outputData[11] == 1212);
}

BOOST_AUTO_TEST_CASE(MultipleOutputs)
{
    using namespace armnn;

    // Create runtime in which test will run
    armnn::IRuntimePtr  runtime(armnn::IRuntime::Create(armnn::Compute::CpuRef));

    // build up the structure of the network
    INetworkPtr net(INetwork::Create());

    IConnectableLayer* input = net->AddInputLayer(0);

    // ReLu1
    ActivationDescriptor activation1Descriptor;
    activation1Descriptor.m_Function = ActivationFunction::BoundedReLu;
    activation1Descriptor.m_A = 1.f;
    activation1Descriptor.m_B = -1.f;
    IConnectableLayer* activation1 = net->AddActivationLayer(activation1Descriptor);

    // ReLu6
    ActivationDescriptor activation2Descriptor;
    activation2Descriptor.m_Function = ActivationFunction::BoundedReLu;
    activation2Descriptor.m_A = 6.0f;
    IConnectableLayer* activation2 = net->AddActivationLayer(activation2Descriptor);

    // BoundedReLu(min=2, max=5)
    ActivationDescriptor activation3Descriptor;
    activation3Descriptor.m_Function = ActivationFunction::BoundedReLu;
    activation3Descriptor.m_A = 5.0f;
    activation3Descriptor.m_B = 2.0f;
    IConnectableLayer* activation3 = net->AddActivationLayer(activation3Descriptor);

    IConnectableLayer* output1 = net->AddOutputLayer(0);
    IConnectableLayer* output2 = net->AddOutputLayer(1);
    IConnectableLayer* output3 = net->AddOutputLayer(2);

    input->GetOutputSlot(0).Connect(activation1->GetInputSlot(0));
    input->GetOutputSlot(0).Connect(activation2->GetInputSlot(0));
    input->GetOutputSlot(0).Connect(activation3->GetInputSlot(0));

    activation1->GetOutputSlot(0).Connect(output1->GetInputSlot(0));
    activation2->GetOutputSlot(0).Connect(output2->GetInputSlot(0));
    activation3->GetOutputSlot(0).Connect(output3->GetInputSlot(0));

    // set the tensors in the network
    TensorInfo tensorInfo(TensorShape({ 10 }), DataType::Float32);
    input->GetOutputSlot(0).SetTensorInfo(tensorInfo);
    activation1->GetOutputSlot(0).SetTensorInfo(tensorInfo);
    activation2->GetOutputSlot(0).SetTensorInfo(tensorInfo);
    activation3->GetOutputSlot(0).SetTensorInfo(tensorInfo);

    // optimize the network
    IOptimizedNetworkPtr optNet = Optimize(*net, runtime->GetDeviceSpec());

    // load it into the runtime
    NetworkId netId;
    runtime->LoadNetwork(netId, std::move(optNet));

    // create structures for input & output
    const std::vector<float> inputData{ 3.f, 5.f, 2.f, 3.f, 7.f, 0.f, -2.f, -1.f, 3.f, 3.f };

    std::vector<float> output1Data(inputData.size());
    std::vector<float> output2Data(inputData.size());
    std::vector<float> output3Data(inputData.size());

    InputTensors inputTensors
    {
        {0,armnn::ConstTensor(runtime->GetInputTensorInfo(netId, 0), inputData.data())}
    };
    OutputTensors outputTensors
    {
        {0,armnn::Tensor(runtime->GetOutputTensorInfo(netId, 0), output1Data.data())},
        {1,armnn::Tensor(runtime->GetOutputTensorInfo(netId, 1), output2Data.data())},
        {2,armnn::Tensor(runtime->GetOutputTensorInfo(netId, 2), output3Data.data())}
    };

    // do the inference
    runtime->EnqueueWorkload(netId, inputTensors, outputTensors);

    // check the results
    BOOST_TEST(output1Data == std::vector<float>({ 1.f, 1.f, 1.f, 1.f, 1.f, 0.f, -1.f, -1.f, 1.f, 1.f })); // ReLu1
    BOOST_TEST(output2Data == std::vector<float>({ 3.f, 5.f, 2.f, 3.f, 6.f, 0.f, 0.f, 0.f, 3.f, 3.f })); // ReLu6
    BOOST_TEST(output3Data == std::vector<float>({ 3.f, 5.f, 2.f, 3.f, 5.f, 2.f, 2.f, 2.f, 3.f, 3.f })); // [2, 5]
}

#if ARMCOMPUTENEON_ENABLED
BOOST_AUTO_TEST_CASE(ErrorOnLoadNetwork)
{
    using namespace armnn;

    // Create runtime in which test will run
    // Note we don't allow falling back to CpuRef if an operation (excluding inputs, outputs, etc.) isn't supported
    armnn::IRuntime::CreationOptions options(armnn::Compute::CpuAcc);
    options.m_UseCpuRefAsFallback = false;
    armnn::IRuntimePtr runtime(armnn::IRuntime::Create(options));

    // build up the structure of the network
    INetworkPtr net(INetwork::Create());

    IConnectableLayer* input = net->AddInputLayer(0);

    // This layer configuration isn't supported by CpuAcc and isn't allowed to fall back, so LoadNetwork will fail.
    NormalizationDescriptor descriptor;
    IConnectableLayer* pooling = net->AddNormalizationLayer(descriptor);

    IConnectableLayer* output = net->AddOutputLayer(0);

    input->GetOutputSlot(0).Connect(pooling->GetInputSlot(0));
    pooling->GetOutputSlot(0).Connect(output->GetInputSlot(0));

    input->GetOutputSlot(0).SetTensorInfo(TensorInfo({ 1, 1, 4, 4 }, DataType::Float32));
    pooling->GetOutputSlot(0).SetTensorInfo(TensorInfo({ 1, 1, 4, 4 }, DataType::Float32));

    // optimize the network
    IOptimizedNetworkPtr optNet = Optimize(*net, runtime->GetDeviceSpec());

    // Load it into the runtime. It should fail.
    NetworkId netId;
    BOOST_TEST(runtime->LoadNetwork(netId, std::move(optNet)) == Status::Failure);
}
#endif // ARMCOMPUTENEON_ENABLED

BOOST_AUTO_TEST_SUITE_END()


//          Copyright Gavin Band 2008 - 2012.
// Distributed under the Boost Software License, Version 1.0.
//    (See accompanying file LICENSE_1_0.txt or copy at
//          http://www.boost.org/LICENSE_1_0.txt)

#ifndef METRO_LIKELIHOOD_MIXTURE_HPP
#define METRO_LIKELIHOOD_MIXTURE_HPP

#include <cmath>
#include <boost/ptr_container/ptr_vector.hpp>
#include <Eigen/Core>
#include "metro/IndependentObservationLogLikelihood.hpp"
#include "metro/DataRange.hpp"
#include "metro/DataSubset.hpp"
#include "metro/log_sum_exp.hpp"

// #define DEBUG_METRO_MIXTURE 1

namespace metro {
	namespace likelihood {
		template< typename Scalar, typename Vector, typename Matrix >
		struct Mixture: public metro::IndependentObservationLogLikelihood< Scalar, Vector, Matrix > {
			typedef metro::IndependentObservationLogLikelihood< Scalar, Vector, Matrix > Component ;
			typedef typename metro::IndependentObservationLogLikelihood< Scalar, Vector, Matrix >::MatrixRef MatrixRef ;

			Mixture( Matrix const& data ):
				m_data( &data ),
				m_total_weight(0)
			{}

			// Set data to a given matrix.
			// This will invalidate the parameters iff it does for any of the components.
			void set_data( Matrix const& data ) {
				m_data = &data ;
				for( std::size_t i = 0; i < m_components.size(); ++i ) {
					m_components[i].set_data( data ) ;
				}
			}

			// Add a component with given name and weight to the mixture.
			// This takes ownership of the distribution, which will be destroyed
			// when this class goes out of scope.
			template< typename DistributionPtr >
			void add_component(
				std::string const& name,
				Scalar const weight,
				DistributionPtr distribution
			) {
				m_component_names.push_back( name ) ;
				m_components.push_back( distribution ) ;
				m_weights.push_back( weight ) ;
				m_total_weight += weight ;
			}

			// Evaluate at a given set of parameters expressed as a single vector.
			// Parameters are taken in order of the components.
			void evaluate_at( Vector const& parameters ) {
				evaluate_at( parameters, DataSubset( DataRange( 0, m_data->rows() ))) ;
			}

			// Evaluate at a given set of parameters expressed as a single vector,
			// on a given subset of the data. Parameters are taken in order of the components.
			// Parameters come in the order:
			// Parameters for first component, (normalised) weight for first component
			// Parameters for second component, (normalised) weight for second component
			// ...
			// Parameters for last component.  No weight is included for last component as weights sum to one.
			void evaluate_at( Vector const& parameters, DataSubset const& data_subset ) {
				{
					int param_i = 0 ;
					double total_weight = 0 ;
					for( std::size_t i = 0; i < m_components.size(); ++i ) {
						int const componentParameterSize = m_components[i].parameters().size() ;
						m_components[i].evaluate_at(
							parameters.segment(
								param_i,
								componentParameterSize
							),
							data_subset
						) ;
						if( i == ( m_components.size() - 1 ) ) {
							m_weights[i] = 1 - total_weight ;
							m_total_weight = 1 ;
						} else {
							m_weights[i] = parameters( param_i + componentParameterSize ) ;
							total_weight += m_weights[i] ;
						}
						param_i += m_components[i].parameters().size() + 1 ;
					}
					// sanity check
					assert((m_components.size() == 0) || (param_i == parameters.size() + 1)) ;
				}

				m_component_terms.resize( m_data->rows(), m_components.size() ) ;
				m_component_terms.setZero() ;
				for( std::size_t i = 0; i < m_components.size(); ++i ) {
					m_components[i].get_terms_of_function( m_component_terms.col(i) ) ;
					// Re-weight according to cluster weights.
					m_component_terms.col(i) += Vector::Constant( m_component_terms.rows(), std::log( m_weights[i] / m_total_weight ) ) ;
#if DEBUG_METRO_MIXTURE
					std::cerr << "m_weights[" << i << "] = " << m_weights[i] / m_total_weight << "\n" ;
#endif
				}

#if DEBUG_METRO_MIXTURE
				std::cerr << "m_component_terms = \n"
					<< m_component_terms.block( 0, 0, 10, m_component_terms.cols() ) << "\n" ;
					std::cerr << "m_component_terms = \n"
						<< m_component_terms.block( 0, 0, 10, m_component_terms.cols() ) << "\n" ;
#endif

				// Compute it
				m_terms.setZero( m_component_terms.rows() ) ;
				rowwise_log_sum_exp( m_component_terms, &m_terms ) ;
			}

			double get_value_of_function() const {
				return m_terms.sum() ;
			}

			void get_terms_of_function( MatrixRef result ) const {
				result = m_terms ;
			}

			// Return the parameter vector.
			// This comes in the order:
			// Parameters for first component, (normalised) weight for first component
			// Parameters for second component, (normalised) weight for second component
			// ...
			// Parameters for last component.  No weight is included for last component as weights sum to one.
			Vector parameters() const {
				return compute_parameters() ;
			}

			std::string get_spec() const {
				return "Mixture" ;
			}

			Vector get_value_of_first_derivative() const {
				assert(0) ;
			}

			Matrix get_value_of_second_derivative() const {
				assert(0) ;
			}

		private:
			Matrix const* m_data ;
			boost::ptr_vector< Component > m_components ;
			std::vector< std::string > m_component_names ;
			std::vector< Scalar > m_weights ;
			Scalar m_total_weight ;
			std::vector< Scalar > m_rescaled_weights ;
			Matrix m_component_terms ;
			Vector m_terms ;

		private:
			Vector compute_parameters() const {
				if( m_components.size() == 0 ) {
					return Vector() ;
				} else {
					int numberOfParameters = 0 ;
					for( std::size_t i = 0; i < m_components.size(); ++i ) {
						numberOfParameters += m_components[i].parameters().size() + 1 ;
					}
					Eigen::VectorXd parameters( numberOfParameters - 1 ) ;
					int parameterIndex = 0 ;
					for( std::size_t i = 0; i < m_components.size(); ++i ) {
						Vector const& componentParams = m_components[i].parameters() ;
						parameters.segment( parameterIndex, componentParams.size() ) = componentParams ;
						if( i != ( m_components.size() - 1 ) ) {
							parameters( parameterIndex + componentParams.size() ) = m_weights[i] / m_total_weight ;
						}
						parameterIndex += componentParams.size() + 1 ;
					}
					return parameters ;
				}
			}
		} ;
	}
}

#endif


#include "Util.h"
//#include <Eigen/Dense>
#include "cryptoTools/Common/Log.h"
#include "cryptoTools/Common/Matrix.h"
#include "cryptoTools/Crypto/PRNG.h"
#include "cryptoTools/Common/BitIterator.h"
#include <numeric>
#include <iomanip>
#include <future>
#include <cmath>

#include "Mtx.h"
#include <deque>
#include "LdpcSampler.h"

#ifdef ENABLE_ALGO994
extern "C" {
#include "libOTe/Tools/LDPC/Algo994/utils.h"
#include "libOTe/Tools/LDPC/Algo994/generations.h"
#include "libOTe/Tools/LDPC/Algo994/data_defs.h"
}
#endif

#include <fstream>

namespace osuCrypto
{

    //using Mtx = Eigen::Matrix<int, Eigen::Dynamic, Eigen::Dynamic>;

#ifdef ENABLE_ALGO994
    int alg994 = ALG_SAVED_UNROLLED;
    int num_saved_generators =  5;
    int num_cores = 4;
    int num_permutations = 5;
    int print_matrices = 0;
#endif

    int minDist(std::string path, u64 numTHreads, bool verbose)
    {
#ifdef ENABLE_ALGO994

        char* inputMatrix;
        int   info, k, n, dist;
        num_cores = numTHreads;

        // Read input matrix.
        info = read_char_matrix((char*)path.c_str(), &inputMatrix, &k, &n);
        if (info != 0) {
            fprintf(stderr, "\n");
            fprintf(stderr, "ERROR in read_char_matrix: ");
            fprintf(stderr, "Error while reading input file with matrix.\n\n\n");
        }

        assert(
            alg994 == ALG_BASIC || // 1
            alg994 == ALG_OPTIMIZED || // 2
            alg994 == ALG_STACK || // 3
            alg994 == ALG_SAVED || // 4
            alg994 == ALG_SAVED_UNROLLED || // 5
            alg994 == ALG_GRAY  // 10
        );

        // Compute distance of input matrix.
        dist = compute_distance_of_matrix_impl(inputMatrix, k, n,
            alg994,
            num_saved_generators,
            num_cores,
            num_permutations,
            print_matrices);

        // Remove matrices.
        free(inputMatrix);

        return dist;
#else
        throw std::runtime_error("also 994 not enabled. " LOCATION);
#endif
    }

    int minDist2(const DenseMtx& mtx, u64 nt, bool verbose)
    {
        std::string outPath("./deleteMe");
        std::fstream out(outPath, std::fstream::trunc | std::fstream::out);

        if (out.is_open() == false)
        {
            std::cout << "failed to open: " << outPath << std::endl;
            return 0;
        }

        std::vector<std::pair<u64, u64>> swaps;
        auto G = computeGen(mtx, swaps);

        out << G.rows() << " " << G.cols() << " matrix dimensions\n"
            << G << std::endl;

        out.close();

        u64 d;

        d = minDist(outPath, nt, false);

        std::remove(outPath.c_str());
        return (int)d;
    }

    u64 numNonzeroRows(const DenseMtx& mtx)
    {
        u64 r = 0;
        for (u64 i = 0; i < mtx.rows(); ++i)
        {
            if (mtx.row(i).isZero())
                ++r;
        }

        return mtx.rows() - r;
    }

    //u64 rank(const DenseMtx& mtx)
    //{
    //    DenseMtx m2 = gaussianElim(mtx);
    //    return numNonzeroRows(m2);
    //}

    void ithCombination(u64 index, u64 n, std::vector<u64>& set)
    {
        //'''Yields the items of the single combination that would be at the provided
        //(0-based) index in a lexicographically sorted list of combinations of choices
        //of k items from n items [0,n), given the combinations were sorted in
        //descending order. Yields in descending order.
        //'''
        u64 nCk = 1;
        u64 nMinusI = n;
        u64 iPlus1 = 1;

        auto k = set.size();

        // nMinusI, iPlus1 in zip(range(n, n - k, -1), range(1, k + 1)):
        for (; nMinusI != n - k; --nMinusI, ++iPlus1)
        {
            nCk *= nMinusI;
            nCk /= iPlus1;
        }

        //std::cout << "nCk " << nCk << std::endl;

        auto curIndex = nCk;
        for (auto kk = k; kk != 0ull; --kk)//in range(k, 0, -1):
        {
            //std::cout << "kk " << kk << " " <<  nCk << std::endl;
            nCk *= kk;
            nCk /= n;
            while (curIndex - nCk > index) {
                curIndex -= nCk;
                nCk *= (n - kk);
                nCk -= nCk % kk;
                n -= 1;
                nCk /= n;
            }
            n -= 1;

            set[kk - 1] = n;
        }
    }

    std::vector<u64> ithCombination(u64 index, u64 n, u64 k)
    {
        std::vector<u64> set(k);
        ithCombination(index, n, set);
        return set;
    }

    u64 choose(u64 n, u64 k)
    {
        if (k == 0) return 1;
        return (n * choose(n - 1, k - 1)) / k;
    }

    struct NChooseK
    {
        u64 mN;
        u64 mK;
        u64 mI, mEnd;
        std::vector<u64> mSet;

        NChooseK(u64 n, u64 k, u64 begin = 0, u64 end = -1)
            : mN(n)
            , mK(k)
            , mI(begin)
            , mEnd(std::min<u64>(choose(n, k), end))
        {
            assert(k <= n);
            mSet = ithCombination(begin, n, k);
        }

        const std::vector<u64>& operator*() const
        {
            return mSet;
        }

        void operator++()
        {
            ++mI;
            assert(mI <= mEnd);

            u64 i = 0;
            while (i < mK - 1 && mSet[i] + 1 == mSet[i + 1])
                ++i;

            //if (i == mK - 1 && mSet.back() == mN - 1)
            //{
            //    mSet.clear();
            //    return;
            //    //assert(mSet.back() != mN - 1);
            //}

            ++mSet[i];
            for (u64 j = 0; j < i; ++j)
                mSet[j] = j;
        }

        explicit operator bool() const
        {
            return mI < mEnd;
        }

    };

    DenseMtx computeGen(DenseMtx& H)
    {
        assert(H.rows() < H.cols());

        auto n = H.cols();
        auto m = H.rows();
        auto k = n - m;

        auto mtx = H.subMatrix(0, k, m, m);

        auto P = H.subMatrix(0, 0, m, k);

        for (u64 i = 0; i < m; ++i)
        {
            if (mtx(i, i) == 0)
            {
                for (u64 j = i + 1; j < m; ++j)
                {
                    if (mtx(j, i) == 1)
                    {
                        mtx.row(i).swap(mtx.row(j));
                        P.row(i).swap(P.row(j));
                        break;
                    }
                }

                if (mtx(i, i) == 0)
                {
                    //std::cout << mtx << std::endl;
                    return {};
                }
            }

            for (u64 j = 0; j < m; ++j)
            {
                if (j != i && mtx(j, i))
                {
                    for (u64 l = 0; l < m; ++l)
                    {
                        mtx(j, l) ^= mtx(i, l);
                    }

                    for (u64 l = 0; l < k; ++l)
                        P(j, l) ^= P(i, l);
                }
            }

        }

        DenseMtx G(k, n);
        for (u64 i = 0; i < k; ++i)
            G(i, i) = 1;

        for (u64 i = 0; i < m; ++i)
        {
            for (u64 j = 0; j < k; ++j)
            {
                G(j, i + k) = P(i, j);
            }
        }

        return G;
    }

    DenseMtx computeGen(DenseMtx H, std::vector<std::pair<u64, u64>>& colSwaps)
    {
        assert(H.rows() < H.cols());

        auto n = H.cols();
        auto m = H.rows();
        auto k = n - m;
        colSwaps.clear();

        for (u64 row = 0, col = k; row < m; ++row, ++col)
        {
            //std::cout << row << std::endl << H << std::endl;;

            if (H(row, col) == 0)
            {
                bool found = false;
                // look fow a row swap
                for (u64 row2 = row + 1; row2 < m && found == false; ++row2)
                {
                    if (H(row2, col) == 1)
                    {
                        H.row(row).swap(H.row(row2));
                        found = true;
                    }
                }

                if (found == false)
                {
                    // look for a col swap

                    for (u64 col2 = 0; col2 < k && found == false; ++col2)
                    {
                        for (u64 row2 = row; row2 < m && found == false; ++row2)
                        {
                            if (H(row2, col2) == 1)
                            {
                                H.row(row).swap(H.row(row2));

                                // swap columns.
                                colSwaps.push_back({ col,col2 });
                                auto c0 = H.col(col);
                                auto c1 = H.col(col2);
                                std::swap_ranges(c0.begin(), c0.end(), c1.begin());
                                found = true;
                            }
                        }
                    }
                }

                if (found == false)
                {
                    // can not be put in systematic form.
                    //std::cout  << H << std::endl;

                    return {};
                }
            }

            // clear all other ones from the current column.
            for (u64 row2 = 0; row2 < m; ++row2)
            {
                if (row2 != row && H(row2, col))
                {
                    // row2 = row ^ row2
                    for (u64 col2 = 0; col2 < n; ++col2)
                    {
                        H(row2, col2) ^= H(row, col2);
                    }
                }
            }

        }

        auto P = H.subMatrix(0, 0, m, k);

        DenseMtx G(k, n);
        for (u64 i = 0; i < k; ++i)
            G(i, i) = 1;

        for (u64 i = 0; i < m; ++i)
        {
            for (u64 j = 0; j < k; ++j)
            {
                G(j, i + k) = P(i, j);
            }
        }

        return G;
    }

}


#define BOOST_TEST_MODULE "test_3spn2_base_base_interaction"

#ifdef BOOST_TEST_DYN_LINK
#include <boost/test/unit_test.hpp>
#else
#include <boost/test/included/unit_test.hpp>
#endif

#include <mjolnir/math/constants.hpp>
#include <mjolnir/util/make_unique.hpp>
#include <mjolnir/core/BoundaryCondition.hpp>
#include <mjolnir/core/SimulatorTraits.hpp>
#include <mjolnir/core/VerletList.hpp>
#include <mjolnir/forcefield/3SPN2/ThreeSPN2BaseBaseInteraction.hpp>
#include <mjolnir/input/read_units.hpp>
#include <random>

using parameters_to_test = std::tuple<
    mjolnir::ThreeSPN2BaseBaseGlobalPotentialParameter<double>,
    mjolnir::ThreeSPN2CBaseBaseGlobalPotentialParameter<double>
>;

BOOST_AUTO_TEST_CASE_TEMPLATE(ThreeSPN2BasePairIntearction_numerical_diff,
        ParameterSet, parameters_to_test)
{
    mjolnir::LoggerManager::set_default_logger(
            "test_3spn2_base_base_interaction.log");

    using traits_type       = mjolnir::SimulatorTraits<double, mjolnir::UnlimitedBoundary>;
    using real_type         = traits_type::real_type;
    using coord_type        = traits_type::coordinate_type;
    using boundary_type     = traits_type::boundary_type;
    using system_type       = mjolnir::System<traits_type>;
    using topology_type     = mjolnir::Topology;

    using potential_type      = mjolnir::ThreeSPN2BaseBaseInteractionPotential<traits_type>;
    using base_kind           = typename potential_type::base_kind;
    using parameter_type      = typename potential_type::parameter_type;
    using partition_type      = mjolnir::VerletList<traits_type, potential_type>;

    using interaction_type     = mjolnir::ThreeSPN2BaseBaseInteraction<traits_type>;
    using ignore_group_type    = typename potential_type::ignore_group_type;
    using ignore_molecule_type = typename potential_type::ignore_molecule_type;

    constexpr real_type pi = mjolnir::math::constants<real_type>::pi();

    {
        using unit_type = mjolnir::unit::constants<real_type>;
        using phys_type = mjolnir::physics::constants<real_type>;
        const std::string energy = "kcal/mol";
        const std::string length = "angstrom";
        phys_type::set_kB(phys_type::kB() * (unit_type::J_to_cal() / 1000.0) *
                          unit_type::avogadro_constant());
        phys_type::set_eps0(phys_type::eps0() * (1000.0 / unit_type::J_to_cal()) /
                            unit_type::avogadro_constant());
        phys_type::set_energy_unit(energy);

        phys_type::set_eps0(phys_type::eps0() / unit_type::m_to_angstrom());

        phys_type::set_m_to_length(unit_type::m_to_angstrom());
        phys_type::set_length_to_m(unit_type::angstrom_to_m());

        phys_type::set_L_to_volume(1e-3 * std::pow(unit_type::m_to_angstrom(), 3));
        phys_type::set_volume_to_L(1e+3 * std::pow(unit_type::angstrom_to_m(), 3));

        phys_type::set_length_unit(length);
    }

    // Here, this test case checks base pairing interaction only.
    //
    //  theta1    theta2
    //       |    |
    // Si o  v    v o Sj
    //     \-.   .-/
    //      o === o
    //     Bi  r  Bj
    //
    // Required test cases are the combination of the following conditions
    //
    // BasePairing:
    // rij:
    //  1. (r < r0)
    //  2. (r0 < r)
    // theta1:
    //  1. theta < pi/2K
    //  2. pi/2K < theta < pi/K
    //  3. pi/K  < theta
    // theta2:
    //  1. theta < pi/2K
    //  2. pi/2K < theta < pi/K
    //  3. pi/K  < theta

    std::mt19937 mt(123456789);
    std::uniform_real_distribution<real_type> uni(-1.0, 1.0);

    for(const auto bases : {
            std::vector<base_kind>{base_kind::A, base_kind::T},
            std::vector<base_kind>{base_kind::T, base_kind::A},
            std::vector<base_kind>{base_kind::C, base_kind::G},
            std::vector<base_kind>{base_kind::G, base_kind::C}
            })
    {
        //  theta1    theta2
        //       |    |
        //  0 o  v    v o 2
        //     \-.   .-/
        //      o === o
        //     1       3
        //
        constexpr auto invalid = potential_type::invalid();

        potential_type potential(ParameterSet{}, {
                {1, parameter_type{bases.at(0), 0, 0, invalid, invalid}},
                {3, parameter_type{bases.at(1), 1, 2, invalid, invalid}}
            }, {}, ignore_molecule_type("Nothing"), ignore_group_type({})
        );

        interaction_type interaction(potential_type(potential),
            mjolnir::SpatialPartition<traits_type, potential_type>(
                mjolnir::make_unique<partition_type>()));

        system_type sys(4, boundary_type{});
        topology_type topol(4);

        topol.add_connection(0, 1, "bond");
        topol.add_connection(2, 3, "bond");
        topol.construct_molecules();

        for(std::size_t i=0; i<4; ++i)
        {
            sys.mass(i)  = 1.0;
            sys.rmass(i) = 1.0;
            sys.position(i) = coord_type(0.0, 0.0, 0.0);
            sys.velocity(i) = coord_type(0.0, 0.0, 0.0);
            sys.force(i)    = coord_type(0.0, 0.0, 0.0);
            sys.name(i)  = "DNA";
            sys.group(i) = "DNA";
        }
        potential  .initialize(sys, topol);
        interaction.initialize(sys, topol);

        const auto bp_kind      = potential.bp_kind (bases.at(0), bases.at(1));
        const auto rbp_0        = potential.r0(bp_kind);
        const auto theta1_0     = potential.theta1_0(bp_kind);
        const auto theta2_0     = potential.theta2_0(bp_kind);
        const auto pi_over_K_BP = potential.pi_over_K_BP();

        for(const auto rbp  : {rbp_0 - 0.2, rbp_0 + 0.5})
        {
        for(const auto theta1 : {theta1_0 + pi_over_K_BP * 0.2,
                                 theta1_0 + pi_over_K_BP * 0.7,
                                 theta1_0 + pi_over_K_BP * 1.2,
                                 theta1_0 - pi_over_K_BP * 1.2,
                                 theta1_0 - pi_over_K_BP * 0.7,
                                 theta1_0 - pi_over_K_BP * 0.2})
        {
        for(const auto theta2 : {theta2_0 + pi_over_K_BP * 0.2,
                                 theta2_0 + pi_over_K_BP * 0.7,
                                 theta2_0 + pi_over_K_BP * 1.2,
                                 theta2_0 - pi_over_K_BP * 1.2,
                                 theta2_0 - pi_over_K_BP * 0.7,
                                 theta2_0 - pi_over_K_BP * 0.2})
        {
            BOOST_TEST_MESSAGE("===========================================");
            BOOST_TEST_MESSAGE("rbp    = " << rbp);
            BOOST_TEST_MESSAGE("theta1 = " << theta1);
            BOOST_TEST_MESSAGE("theta2 = " << theta2);

            //  theta1    theta2
            //       |    |
            //  0 o  v    v o 2
            //     \-.   .-/
            //      o === o
            //     1       3
            //
            sys.position(1) = coord_type(0.0, 0.0, 0.0);
            sys.position(3) = coord_type(rbp, 0.0, 0.0);

            sys.position(0) = coord_type(std::cos(theta1),          std::sin(theta1),    0.0);
            sys.position(2) = coord_type(std::cos(pi-theta2) + rbp, std::sin(pi-theta2), 0.0);

            // rotate in random direction
            {
                using matrix33_type = typename traits_type::matrix33_type;

                const auto rot_x = uni(mt) * pi;
                const auto rot_y = uni(mt) * pi;
                const auto rot_z = uni(mt) * pi;

                matrix33_type rotm_x(1.0,             0.0,              0.0,
                                     0.0, std::cos(rot_x), -std::sin(rot_x),
                                     0.0, std::sin(rot_x),  std::cos(rot_x));
                matrix33_type rotm_y( std::cos(rot_y), 0.0,  std::sin(rot_y),
                                                  0.0, 1.0,              0.0,
                                     -std::sin(rot_y), 0.0,  std::cos(rot_y));
                matrix33_type rotm_z(std::cos(rot_z), -std::sin(rot_z), 0.0,
                                     std::sin(rot_z),  std::cos(rot_z), 0.0,
                                                 0.0,              0.0, 1.0);

                const matrix33_type rotm = rotm_x * rotm_y * rotm_z;
                for(std::size_t idx=0; idx<sys.size(); ++idx)
                {
                    sys.position(idx) = rotm * sys.position(idx);
                }
            }
            // check configuration is okay
            {
                const auto v13 = sys.position(3) - sys.position(1);
                BOOST_TEST_REQUIRE(mjolnir::math::length(v13) == rbp,
                                   boost::test_tools::tolerance(1e-3));

                const auto v10 = sys.position(0) - sys.position(1);
                const auto cos_013 = mjolnir::math::dot_product(v13, v10) /
                    (mjolnir::math::length(v13) * mjolnir::math::length(v10));
                BOOST_TEST_REQUIRE(std::acos(cos_013) == theta1,
                                   boost::test_tools::tolerance(1e-3));

                const auto v23 = sys.position(3) - sys.position(2);
                const auto cos_132 = mjolnir::math::dot_product(v13, v23) /
                    (mjolnir::math::length(v13) * mjolnir::math::length(v23));
                BOOST_TEST_REQUIRE(std::acos(cos_132) == theta2,
                                   boost::test_tools::tolerance(1e-3));
            }

            for(std::size_t idx=0; idx<sys.size(); ++idx)
            {
                sys.position(idx) += coord_type(0.01 * uni(mt), 0.01 * uni(mt), 0.01 * uni(mt));
                sys.force(idx)     = coord_type(0.0, 0.0, 0.0);
            }
            const system_type init = sys;

            constexpr real_type tol = 1e-4;
            constexpr real_type dr  = 1e-5;
            for(std::size_t idx=0; idx<sys.size(); ++idx)
            {
                {
                    // ----------------------------------------------------------------
                    // reset positions
                    sys = init;

                    // calc U(x-dx)
                    const auto E0 = interaction.calc_energy(sys);

                    mjolnir::math::X(sys.position(idx)) += dr;

                    // calc F(x)
                    interaction.calc_force(sys);

                    mjolnir::math::X(sys.position(idx)) += dr;

                    // calc U(x+dx)
                    const auto E1 = interaction.calc_energy(sys);

                    // central difference
                    const auto dE = (E1 - E0) * 0.5;

                    BOOST_TEST(-dE / dr == mjolnir::math::X(sys.force(idx)),
                               boost::test_tools::tolerance(tol));
                }
                {
                    // ----------------------------------------------------------------
                    // reset positions
                    sys = init;

                    // calc U(x-dx)
                    const auto E0 = interaction.calc_energy(sys);

                    mjolnir::math::Y(sys.position(idx)) += dr;

                    // calc F(x)
                    interaction.calc_force(sys);

                    mjolnir::math::Y(sys.position(idx)) += dr;

                    // calc U(x+dx)
                    const auto E1 = interaction.calc_energy(sys);

                    // central difference
                    const auto dE = (E1 - E0) * 0.5;

                    BOOST_TEST(-dE / dr == mjolnir::math::Y(sys.force(idx)),
                               boost::test_tools::tolerance(tol));
                }
                {
                    // ----------------------------------------------------------------
                    // reset positions
                    sys = init;

                    // calc U(x-dx)
                    const auto E0 = interaction.calc_energy(sys);

                    mjolnir::math::Z(sys.position(idx)) += dr;

                    // calc F(x)
                    interaction.calc_force(sys);

                    mjolnir::math::Z(sys.position(idx)) += dr;

                    // calc U(x+dx)
                    const auto E1 = interaction.calc_energy(sys);

                    // central difference
                    const auto dE = (E1 - E0) * 0.5;

                    BOOST_TEST(-dE / dr == mjolnir::math::Z(sys.force(idx)),
                               boost::test_tools::tolerance(tol));
                }
            }
        } // theta2
        } // theta1
        } // rbp
    }

}

BOOST_AUTO_TEST_CASE_TEMPLATE(ThreeSPN2CrossStackingIntearction_numerical_diff,
        ParameterSet, parameters_to_test)
{
    mjolnir::LoggerManager::set_default_logger(
            "test_3spn2_base_base_interaction.log");

    using traits_type       = mjolnir::SimulatorTraits<double, mjolnir::UnlimitedBoundary>;
    using real_type         = traits_type::real_type;
    using coord_type        = traits_type::coordinate_type;
    using boundary_type     = traits_type::boundary_type;
    using system_type       = mjolnir::System<traits_type>;
    using topology_type     = mjolnir::Topology;

    using potential_type    = mjolnir::ThreeSPN2BaseBaseInteractionPotential<traits_type>;
    using base_kind         = typename potential_type::base_kind;
    using parameter_type    = typename potential_type::parameter_type;
    using partition_type      = mjolnir::VerletList<traits_type, potential_type>;

    using interaction_type     = mjolnir::ThreeSPN2BaseBaseInteraction<traits_type>;
    using ignore_group_type    = typename potential_type::ignore_group_type;
    using ignore_molecule_type = typename potential_type::ignore_molecule_type;

    constexpr real_type pi = mjolnir::math::constants<real_type>::pi();

    {
        using unit_type = mjolnir::unit::constants<real_type>;
        using phys_type = mjolnir::physics::constants<real_type>;
        const std::string energy = "kcal/mol";
        const std::string length = "angstrom";
        phys_type::set_kB(phys_type::kB() * (unit_type::J_to_cal() / 1000.0) *
                          unit_type::avogadro_constant());
        phys_type::set_eps0(phys_type::eps0() * (1000.0 / unit_type::J_to_cal()) /
                            unit_type::avogadro_constant());
        phys_type::set_energy_unit(energy);

        phys_type::set_eps0(phys_type::eps0() / unit_type::m_to_angstrom());

        phys_type::set_m_to_length(unit_type::m_to_angstrom());
        phys_type::set_length_to_m(unit_type::angstrom_to_m());

        phys_type::set_L_to_volume(1e-3 * std::pow(unit_type::m_to_angstrom(), 3));
        phys_type::set_volume_to_L(1e+3 * std::pow(unit_type::angstrom_to_m(), 3));

        phys_type::set_length_unit(length);
    }

    //       Si   Bi   Bj   Sj
    //  5'    o -- o===o -- o     3'
    //  ^    /      \ /      \    |
    //  | P o        X        o P |
    //  |    \      / \      /    v
    //  3'    o -- o===o -- o     5'
    //       Bj_next   Bj_next
    //
    // Required test cases are the combination of the following conditions
    //
    // CrossStacking
    // theta3:
    //  1. theta < pi/2K
    //  2. pi/2K < theta < pi/K
    //  3. pi/K  < theta
    // thetaCS_i:
    //  1. theta < pi/2K
    //  2. pi/2K < theta < pi/K
    //  3. pi/K  < theta
    // thetaCS_j:
    //  1. theta < pi/2K
    //  2. pi/2K < theta < pi/K
    //  3. pi/K  < theta
    // thetaCS_j:
    //  1. theta < pi/2K
    //  2. pi/2K < theta < pi/K
    //  3. pi/K  < theta
    // ri_jnext
    //  1. (r < r0)
    //  2. (r0 < r)
    // rj_inext
    //  1. (r < r0)
    //  2. (r0 < r)
    //
    // Oh, okay, we have 3^4 * 2^2 = 324 test configurations.
    //
    // Also, this interaction is a sum of two, BasePairing and CrossStacking.
    // This makes numeric differentiation unstable.

    std::mt19937 mt(123456789);
    std::uniform_real_distribution<real_type> uni(-1.0, 1.0);

    for(const auto bases : {
            std::vector<base_kind>{base_kind::A, base_kind::T, base_kind::G, base_kind::G},
            std::vector<base_kind>{base_kind::A, base_kind::T, base_kind::C, base_kind::C},
            std::vector<base_kind>{base_kind::T, base_kind::A, base_kind::G, base_kind::G},
            std::vector<base_kind>{base_kind::T, base_kind::A, base_kind::C, base_kind::C},
            std::vector<base_kind>{base_kind::C, base_kind::G, base_kind::A, base_kind::A},
            std::vector<base_kind>{base_kind::C, base_kind::G, base_kind::T, base_kind::T},
            std::vector<base_kind>{base_kind::G, base_kind::C, base_kind::A, base_kind::A},
            std::vector<base_kind>{base_kind::G, base_kind::C, base_kind::T, base_kind::T}
        })
    {
        //        0    1   9    8
        //  5'    o -- o===o -- o     3'
        //  ^    /      \ /      \    |
        //  | 2 o        X        o 7 |
        //  |    \      / \      /    v
        //  3'    o -- o===o -- o     5'
        //        3    4   6    5

        constexpr auto invalid = potential_type::invalid();

        potential_type potential(ParameterSet{}, {
                {1, parameter_type{bases.at(0), 0, 0, 4, invalid}},
                {9, parameter_type{bases.at(1), 3, 8, invalid, 6}},
                {4, parameter_type{bases.at(2), 1, 3, invalid, 1}},
                {6, parameter_type{bases.at(3), 2, 5, 9, invalid}},
            }, {}, ignore_molecule_type("Nothing"), ignore_group_type({})
        );

        interaction_type interaction(potential_type(potential),
            mjolnir::SpatialPartition<traits_type, potential_type>(
                mjolnir::make_unique<partition_type>()));

        system_type sys(10, boundary_type{});
        topology_type topol(10);

        topol.add_connection(0, 1, "bond");
        topol.add_connection(0, 2, "bond");
        topol.add_connection(2, 3, "bond");
        topol.add_connection(3, 4, "bond");

        topol.add_connection(5, 6, "bond");
        topol.add_connection(5, 7, "bond");
        topol.add_connection(7, 8, "bond");
        topol.add_connection(8, 9, "bond");

        topol.add_connection(0, 2, "next_nucl");
        topol.add_connection(1, 2, "next_nucl");
        topol.add_connection(0, 3, "next_nucl");
        topol.add_connection(1, 3, "next_nucl");
        topol.add_connection(0, 4, "next_nucl");
        topol.add_connection(1, 4, "next_nucl");

        topol.add_connection(5, 7, "next_nucl");
        topol.add_connection(6, 7, "next_nucl");
        topol.add_connection(5, 8, "next_nucl");
        topol.add_connection(6, 8, "next_nucl");
        topol.add_connection(5, 9, "next_nucl");
        topol.add_connection(6, 9, "next_nucl");

        topol.construct_molecules();

        for(std::size_t i=0; i<10; ++i)
        {
            sys.mass(i)  = 1.0;
            sys.rmass(i) = 1.0;
            sys.position(i) = coord_type(0.0, 0.0, 0.0);
            sys.velocity(i) = coord_type(0.0, 0.0, 0.0);
            sys.force(i)    = coord_type(0.0, 0.0, 0.0);
            sys.name(i)  = "DNA";
            sys.group(i) = "DNA";
        }
        potential  .initialize(sys, topol);
        interaction.initialize(sys, topol);

        const auto bp_kind      = potential.bp_kind (bases.at(0), bases.at(1));
        const auto cs_i_kind    = potential.cs5_kind(bases.at(0), bases.at(3));
        const auto cs_j_kind    = potential.cs3_kind(bases.at(1), bases.at(2));

        const auto rbp          = potential.r0(bp_kind) + 0.2;
        const auto rcs_i_0      = potential.r0(cs_i_kind);
        const auto rcs_j_0      = potential.r0(cs_j_kind);
        const auto theta1       = potential.theta1_0(bp_kind);
        const auto theta2       = potential.theta2_0(bp_kind);
        const auto theta3_0     = potential.theta3_0(bp_kind);
        const auto thetaCS_i_0  = potential.thetaCS_0(cs_i_kind);
        const auto thetaCS_j_0  = potential.thetaCS_0(cs_j_kind);

        const auto pi_over_K_BP = potential.pi_over_K_BP();
        const auto pi_over_K_CS = potential.pi_over_K_CS();

        for(const auto rcsi : {rcs_i_0 - 0.2, rcs_i_0 + 0.5})
        {
        for(const auto rcsj : {rcs_j_0 - 0.2, rcs_j_0 + 0.5})
        {
        for(const auto theta3 : {theta3_0 + pi_over_K_BP * 0.1, theta3_0 + pi_over_K_BP * 0.6, theta3_0 + pi_over_K_BP * 1.1})
        {
        for(const auto thetaCS_i : {thetaCS_i_0 - pi_over_K_CS * 0.1, thetaCS_i_0 - pi_over_K_CS * 0.6, thetaCS_i_0 - pi_over_K_CS * 1.1})
        {
        for(const auto thetaCS_j : {thetaCS_j_0 + pi_over_K_CS * 0.1, thetaCS_j_0 + pi_over_K_CS * 0.6, thetaCS_j_0 + pi_over_K_CS * 1.1})
        {
            BOOST_TEST_REQUIRE(0.0 <= theta3   );BOOST_TEST_REQUIRE(theta3    <= pi);
            BOOST_TEST_REQUIRE(0.0 <= thetaCS_i);BOOST_TEST_REQUIRE(thetaCS_i <= pi);
            BOOST_TEST_REQUIRE(0.0 <= thetaCS_j);BOOST_TEST_REQUIRE(thetaCS_j <= pi);

            // generate positions...
            //        0    1   9    8
            //  5'    o -- o===o -- o     3'
            //  ^    /      \ /      \    |
            //  | 2 o        X        o 7 |
            //  |    \      / \      /    v
            //  3'    o -- o===o -- o     5'
            //        3    4   6    5

            sys.position(1) = coord_type(0.0, 0.0, 0.0);
            sys.position(9) = coord_type(0.0, 0.0, 0.0);

            sys.position(0) = coord_type(4.0 * std::cos(theta1),
                                         4.0 * std::sin(theta1), 0.0);
            sys.position(8) = coord_type(4.0 * std::cos(pi - theta2),
                                         4.0 * std::sin(pi - theta2), 0.0);
            // rotate around x axis
            // to make angle between 0->1 and 8->9 theta3
            {
                using matrix33_type = typename traits_type::matrix33_type;
                real_type best_phi = 0.0;
                real_type dtheta   = std::numeric_limits<real_type>::max();
                const auto bck = sys;
                for(int i=-999; i<1000; ++i)
                {
                    const real_type phi = pi * 0.001 * i;
                    const matrix33_type rot(
                       1.0,           0.0,           0.0,
                       0.0, std::cos(phi), -std::sin(phi),
                       0.0, std::sin(phi),  std::cos(phi));

                    sys.position(8) = rot * sys.position(8);
                    sys.position(9) = rot * sys.position(9);

                    const auto v01 = sys.position(1) - sys.position(0);
                    const auto v89 = sys.position(9) - sys.position(8);
                    const auto theta_0189 = std::acos(mjolnir::math::clamp<real_type>(
                         mjolnir::math::dot_product(v01, v89) /
                        (mjolnir::math::length(v01) * mjolnir::math::length(v89)),
                        -1.0, 1.0));

                    BOOST_TEST_MESSAGE("dtheta = " << dtheta << ", current configuration = " << std::abs(theta_0189 - theta3));

                    if(std::abs(theta_0189 - theta3) < dtheta)
                    {
                        dtheta   = std::abs(theta_0189 - theta3);
                        best_phi = phi;
                    }
                    sys = bck;
                }

                BOOST_TEST_MESSAGE("best_phi = " << best_phi << ", dtheta = " << dtheta);

                {
                    const matrix33_type rot(
                            1.0,                0.0,                 0.0,
                            0.0, std::cos(best_phi), -std::sin(best_phi),
                            0.0, std::sin(best_phi),  std::cos(best_phi));
                    sys.position(8) = rot * sys.position(8);
                    sys.position(9) = rot * sys.position(9);
                }

                mjolnir::math::X(sys.position(4)) += rbp;
                mjolnir::math::X(sys.position(8)) += rbp;
                mjolnir::math::X(sys.position(9)) += rbp;
            }

            {
                using matrix33_type = typename traits_type::matrix33_type;
                const auto v01 = sys.position(1) - sys.position(0);
                const auto v89 = sys.position(9) - sys.position(8);
                const auto n4 = mjolnir::math::cross_product(coord_type(0.0, 0.0, 1.0), v89);
                const auto n6 = mjolnir::math::cross_product(coord_type(0.0, 0.0, 1.0), v01);

                const auto n4_reg = n4 / mjolnir::math::length(n4);
                const auto n6_reg = n6 / mjolnir::math::length(n6);

                coord_type v16 = v01 / mjolnir::math::length(v01);
                coord_type v94 = v89 / mjolnir::math::length(v89);
                {
                    const auto cos_theta = std::cos(pi - thetaCS_i);
                    const auto sin_theta = std::sin(pi - thetaCS_i);

                    BOOST_TEST_REQUIRE(mjolnir::math::length(n6_reg) == 1.0, boost::test_tools::tolerance(1e-8));
                    const auto nx = mjolnir::math::X(n6_reg);
                    const auto ny = mjolnir::math::Y(n6_reg);
                    const auto nz = mjolnir::math::Z(n6_reg);
                    const matrix33_type rot_16(
                        cos_theta + nx*nx*(1-cos_theta),    nx*ny*(1-cos_theta) - nz*sin_theta, nx*nz*(1-cos_theta) + ny*sin_theta,
                        ny*nx*(1-cos_theta) + nz*sin_theta, cos_theta + ny*ny*(1-cos_theta),    ny*nz*(1-cos_theta) - nx*sin_theta,
                        nz*nx*(1-cos_theta) - ny*sin_theta, nz*ny*(1-cos_theta) + nx*sin_theta, cos_theta + nz*nz*(1-cos_theta));
                    BOOST_TEST_REQUIRE(mjolnir::math::length(v16) == 1.0, boost::test_tools::tolerance(1e-8));
                    v16 = rot_16 * v16;
                    BOOST_TEST_REQUIRE(mjolnir::math::length(v16) == 1.0, boost::test_tools::tolerance(1e-8));
                }
                {
                    const auto cos_theta = std::cos(pi - thetaCS_j);
                    const auto sin_theta = std::sin(pi - thetaCS_j);

                    BOOST_TEST_REQUIRE(mjolnir::math::length(n4_reg) == 1.0, boost::test_tools::tolerance(1e-8));
                    const auto nx = mjolnir::math::X(n4_reg);
                    const auto ny = mjolnir::math::Y(n4_reg);
                    const auto nz = mjolnir::math::Z(n4_reg);

                    const matrix33_type rot_94(
                        cos_theta + nx*nx*(1-cos_theta),    nx*ny*(1-cos_theta) - nz*sin_theta, nx*nz*(1-cos_theta) + ny*sin_theta,
                        ny*nx*(1-cos_theta) + nz*sin_theta, cos_theta + ny*ny*(1-cos_theta),    ny*nz*(1-cos_theta) - nx*sin_theta,
                        nz*nx*(1-cos_theta) - ny*sin_theta, nz*ny*(1-cos_theta) + nx*sin_theta, cos_theta + nz*nz*(1-cos_theta));

                    BOOST_TEST_REQUIRE(mjolnir::math::length(v94) == 1.0, boost::test_tools::tolerance(1e-8));
                    v94 = rot_94 * v94;
                    BOOST_TEST_REQUIRE(mjolnir::math::length(v94) == 1.0, boost::test_tools::tolerance(1e-8));
                }
                sys.position(4) = sys.position(9) + v94 * rcsj;
                sys.position(6) = sys.position(1) + v16 * rcsi;
            }

            // check the configurations are okay
            {
                const auto v19 = sys.position(9) - sys.position(1);
                BOOST_TEST_REQUIRE(mjolnir::math::length(v19) == rbp,
                           boost::test_tools::tolerance(0.01));

                const auto v01 = sys.position(1) - sys.position(0);
                const auto v89 = sys.position(9) - sys.position(8);
                {
                    const auto dot = mjolnir::math::dot_product(v01, v89);
                    const auto cos_theta = dot /
                        (mjolnir::math::length(v01) * mjolnir::math::length(v89));
                    const auto theta = std::acos(cos_theta);
                    BOOST_TEST_REQUIRE(theta == theta3, boost::test_tools::tolerance(0.01));
                }

                {
                    const auto dot = mjolnir::math::dot_product(-v01, v19);
                    const auto cos_theta = dot /
                        (mjolnir::math::length(v01) * mjolnir::math::length(v19));
                    const auto theta = std::acos(cos_theta);
                    BOOST_TEST_REQUIRE(theta == theta1, boost::test_tools::tolerance(0.01));
                }
                {
                    const auto dot = mjolnir::math::dot_product(v89, v19);
                    const auto cos_theta = dot /
                        (mjolnir::math::length(v89) * mjolnir::math::length(v19));
                    const auto theta = std::acos(cos_theta);
                    BOOST_TEST_REQUIRE(theta == theta2, boost::test_tools::tolerance(0.01));
                }

                const auto v61 = sys.position(1) - sys.position(6);
                BOOST_TEST_REQUIRE(mjolnir::math::length(v61) == rcsi,
                                   boost::test_tools::tolerance(0.01));
                {
                    const auto dot = mjolnir::math::dot_product(
                            v01 / mjolnir::math::length(v01),
                            v61 / mjolnir::math::length(v61));
                    const auto theta = std::acos(dot);
                    BOOST_TEST_REQUIRE(theta == thetaCS_i,
                            boost::test_tools::tolerance(0.01));
                }
                const auto v49 = sys.position(9) - sys.position(4);
                BOOST_TEST_REQUIRE(mjolnir::math::length(v49) == rcsj,
                           boost::test_tools::tolerance(0.01));
                {
                    const auto dot = mjolnir::math::dot_product(
                            v89 / mjolnir::math::length(v89),
                            v49 / mjolnir::math::length(v49));
                    const auto theta = std::acos(dot);
                    BOOST_TEST_REQUIRE(theta == thetaCS_j, boost::test_tools::tolerance(0.01));
                }
            }

            // positions generated!
            // ... and then rotate random direction to remove special axis
            {
                using matrix33_type = typename traits_type::matrix33_type;

                const auto rot_x = uni(mt) * pi;
                const auto rot_y = uni(mt) * pi;
                const auto rot_z = uni(mt) * pi;

                matrix33_type rotm_x(1.0,             0.0,              0.0,
                                     0.0, std::cos(rot_x), -std::sin(rot_x),
                                     0.0, std::sin(rot_x),  std::cos(rot_x));
                matrix33_type rotm_y( std::cos(rot_y), 0.0,  std::sin(rot_y),
                                                  0.0, 1.0,              0.0,
                                     -std::sin(rot_y), 0.0,  std::cos(rot_y));
                matrix33_type rotm_z(std::cos(rot_z), -std::sin(rot_z), 0.0,
                                     std::sin(rot_z),  std::cos(rot_z), 0.0,
                                                 0.0,              0.0, 1.0);

                const matrix33_type rotm = rotm_x * rotm_y * rotm_z;
                for(std::size_t idx=0; idx<sys.size(); ++idx)
                {
                    sys.position(idx) = rotm * sys.position(idx);
                }
            }

            // add perturbation and reset force
            for(std::size_t idx=0; idx<10; ++idx)
            {
                sys.position(idx) += coord_type(0.01 * uni(mt), 0.01 * uni(mt), 0.01 * uni(mt));
                sys.force(idx)     = coord_type(0.0, 0.0, 0.0);
            }
            const system_type init = sys;

            constexpr real_type tol = 1e-4;
            constexpr real_type dr  = 1e-5;
            for(const std::size_t idx : {0, 1, 8, 9, 4, 6})
            {
                {
                    // ----------------------------------------------------------------
                    // reset positions
                    sys = init;

                    // calc U(x-dx)
                    const auto E0 = interaction.calc_energy(sys);

                    mjolnir::math::X(sys.position(idx)) += dr;

                    // calc F(x)
                    interaction.calc_force(sys);

                    mjolnir::math::X(sys.position(idx)) += dr;

                    // calc U(x+dx)
                    const auto E1 = interaction.calc_energy(sys);

                    // central difference
                    const auto dE = (E1 - E0) * 0.5;

                    BOOST_TEST(-dE / dr == mjolnir::math::X(sys.force(idx)),
                               boost::test_tools::tolerance(tol));
                }
                {
                    // ----------------------------------------------------------------
                    // reset positions
                    sys = init;

                    // calc U(x-dx)
                    const auto E0 = interaction.calc_energy(sys);

                    mjolnir::math::Y(sys.position(idx)) += dr;

                    // calc F(x)
                    interaction.calc_force(sys);

                    mjolnir::math::Y(sys.position(idx)) += dr;

                    // calc U(x+dx)
                    const auto E1 = interaction.calc_energy(sys);

                    // central difference
                    const auto dE = (E1 - E0) * 0.5;

                    BOOST_TEST(-dE / dr == mjolnir::math::Y(sys.force(idx)),
                               boost::test_tools::tolerance(tol));
                }
                {
                    // ----------------------------------------------------------------
                    // reset positions
                    sys = init;

                    // calc U(x-dx)
                    const auto E0 = interaction.calc_energy(sys);

                    mjolnir::math::Z(sys.position(idx)) += dr;

                    // calc F(x)
                    interaction.calc_force(sys);

                    mjolnir::math::Z(sys.position(idx)) += dr;

                    // calc U(x+dx)
                    const auto E1 = interaction.calc_energy(sys);

                    // central difference
                    const auto dE = (E1 - E0) * 0.5;

                    BOOST_TEST(-dE / dr == mjolnir::math::Z(sys.force(idx)),
                               boost::test_tools::tolerance(tol));
                }
            }
        } // thetaCS_j
        } // thetaCS_i
        } // theta3
        } // rcsj
        } // rcsi
    } // bp_kind
}

BOOST_AUTO_TEST_CASE_TEMPLATE(ThreeSPN2BasePairIntearction_energy_and_force,
        ParameterSet, parameters_to_test)
{
    mjolnir::LoggerManager::set_default_logger(
            "test_3spn2_base_base_interaction.log");

    using traits_type       = mjolnir::SimulatorTraits<double, mjolnir::UnlimitedBoundary>;
    using real_type         = traits_type::real_type;
    using coord_type        = traits_type::coordinate_type;
    using boundary_type     = traits_type::boundary_type;
    using system_type       = mjolnir::System<traits_type>;
    using topology_type     = mjolnir::Topology;

    using potential_type      = mjolnir::ThreeSPN2BaseBaseInteractionPotential<traits_type>;
    using base_kind           = typename potential_type::base_kind;
    using parameter_type      = typename potential_type::parameter_type;
    using partition_type      = mjolnir::VerletList<traits_type, potential_type>;

    using interaction_type     = mjolnir::ThreeSPN2BaseBaseInteraction<traits_type>;
    using ignore_group_type    = typename potential_type::ignore_group_type;
    using ignore_molecule_type = typename potential_type::ignore_molecule_type;

    constexpr real_type pi = mjolnir::math::constants<real_type>::pi();

    {
        using unit_type = mjolnir::unit::constants<real_type>;
        using phys_type = mjolnir::physics::constants<real_type>;
        const std::string energy = "kcal/mol";
        const std::string length = "angstrom";
        phys_type::set_kB(phys_type::kB() * (unit_type::J_to_cal() / 1000.0) *
                          unit_type::avogadro_constant());
        phys_type::set_eps0(phys_type::eps0() * (1000.0 / unit_type::J_to_cal()) /
                            unit_type::avogadro_constant());
        phys_type::set_energy_unit(energy);

        phys_type::set_eps0(phys_type::eps0() / unit_type::m_to_angstrom());

        phys_type::set_m_to_length(unit_type::m_to_angstrom());
        phys_type::set_length_to_m(unit_type::angstrom_to_m());

        phys_type::set_L_to_volume(1e-3 * std::pow(unit_type::m_to_angstrom(), 3));
        phys_type::set_volume_to_L(1e+3 * std::pow(unit_type::angstrom_to_m(), 3));

        phys_type::set_length_unit(length);
    }

    // Here, this test case checks base pairing interaction only.
    //
    //  theta1    theta2
    //       |    |
    // Si o  v    v o Sj
    //     \-.   .-/
    //      o === o
    //     Bi  r  Bj
    //
    // Required test cases are the combination of the following conditions
    //
    // BasePairing:
    // rij:
    //  1. (r < r0)
    //  2. (r0 < r)
    // theta1:
    //  1. theta < pi/2K
    //  2. pi/2K < theta < pi/K
    //  3. pi/K  < theta
    // theta2:
    //  1. theta < pi/2K
    //  2. pi/2K < theta < pi/K
    //  3. pi/K  < theta

    std::mt19937 mt(123456789);
    std::uniform_real_distribution<real_type> uni(-1.0, 1.0);

    for(const auto bases : {
            std::vector<base_kind>{base_kind::A, base_kind::T},
            std::vector<base_kind>{base_kind::T, base_kind::A},
            std::vector<base_kind>{base_kind::C, base_kind::G},
            std::vector<base_kind>{base_kind::G, base_kind::C}
            })
    {
        //  theta1    theta2
        //       |    |
        //  0 o  v    v o 2
        //     \-.   .-/
        //      o === o
        //     1       3
        //
        constexpr auto invalid = potential_type::invalid();

        potential_type potential(ParameterSet{}, {
                {1, parameter_type{bases.at(0), 0, 0, invalid, invalid}},
                {3, parameter_type{bases.at(1), 1, 2, invalid, invalid}}
            }, {}, ignore_molecule_type("Nothing"), ignore_group_type({})
        );

        interaction_type interaction(potential_type(potential),
            mjolnir::SpatialPartition<traits_type, potential_type>(
                mjolnir::make_unique<partition_type>()));

        system_type sys(4, boundary_type{});
        topology_type topol(4);

        topol.add_connection(0, 1, "bond");
        topol.add_connection(2, 3, "bond");
        topol.construct_molecules();

        for(std::size_t i=0; i<4; ++i)
        {
            sys.mass(i)  = 1.0;
            sys.rmass(i) = 1.0;
            sys.position(i) = coord_type(0.0, 0.0, 0.0);
            sys.velocity(i) = coord_type(0.0, 0.0, 0.0);
            sys.force(i)    = coord_type(0.0, 0.0, 0.0);
            sys.name(i)  = "DNA";
            sys.group(i) = "DNA";
        }
        potential  .initialize(sys, topol);
        interaction.initialize(sys, topol);

        const auto bp_kind      = potential.bp_kind (bases.at(0), bases.at(1));
        const auto rbp_0        = potential.r0(bp_kind);
        const auto theta1_0     = potential.theta1_0(bp_kind);
        const auto theta2_0     = potential.theta2_0(bp_kind);
        const auto pi_over_K_BP = potential.pi_over_K_BP();

        for(const auto rbp  : {rbp_0 - 0.2, rbp_0 + 0.5})
        {
        for(const auto theta1 : {theta1_0 + pi_over_K_BP * 0.2,
                                 theta1_0 + pi_over_K_BP * 0.7,
                                 theta1_0 + pi_over_K_BP * 1.2,
                                 theta1_0 - pi_over_K_BP * 1.2,
                                 theta1_0 - pi_over_K_BP * 0.7,
                                 theta1_0 - pi_over_K_BP * 0.2})
        {
        for(const auto theta2 : {theta2_0 + pi_over_K_BP * 0.2,
                                 theta2_0 + pi_over_K_BP * 0.7,
                                 theta2_0 + pi_over_K_BP * 1.2,
                                 theta2_0 - pi_over_K_BP * 1.2,
                                 theta2_0 - pi_over_K_BP * 0.7,
                                 theta2_0 - pi_over_K_BP * 0.2})
        {
            BOOST_TEST_MESSAGE("===========================================");
            BOOST_TEST_MESSAGE("rbp    = " << rbp);
            BOOST_TEST_MESSAGE("theta1 = " << theta1);
            BOOST_TEST_MESSAGE("theta2 = " << theta2);

            //  theta1    theta2
            //       |    |
            //  0 o  v    v o 2
            //     \-.   .-/
            //      o === o
            //     1       3
            //
            sys.position(1) = coord_type(0.0, 0.0, 0.0);
            sys.position(3) = coord_type(rbp, 0.0, 0.0);

            sys.position(0) = coord_type(std::cos(theta1),          std::sin(theta1),    0.0);
            sys.position(2) = coord_type(std::cos(pi-theta2) + rbp, std::sin(pi-theta2), 0.0);

            // rotate in random direction
            {
                using matrix33_type = typename traits_type::matrix33_type;

                const auto rot_x = uni(mt) * pi;
                const auto rot_y = uni(mt) * pi;
                const auto rot_z = uni(mt) * pi;

                matrix33_type rotm_x(1.0,             0.0,              0.0,
                                     0.0, std::cos(rot_x), -std::sin(rot_x),
                                     0.0, std::sin(rot_x),  std::cos(rot_x));
                matrix33_type rotm_y( std::cos(rot_y), 0.0,  std::sin(rot_y),
                                                  0.0, 1.0,              0.0,
                                     -std::sin(rot_y), 0.0,  std::cos(rot_y));
                matrix33_type rotm_z(std::cos(rot_z), -std::sin(rot_z), 0.0,
                                     std::sin(rot_z),  std::cos(rot_z), 0.0,
                                                 0.0,              0.0, 1.0);

                const matrix33_type rotm = rotm_x * rotm_y * rotm_z;
                for(std::size_t idx=0; idx<sys.size(); ++idx)
                {
                    sys.position(idx) = rotm * sys.position(idx);
                }
            }
            // check configuration is okay
            {
                const auto v13 = sys.position(3) - sys.position(1);
                BOOST_TEST_REQUIRE(mjolnir::math::length(v13) == rbp,
                                   boost::test_tools::tolerance(1e-3));

                const auto v10 = sys.position(0) - sys.position(1);
                const auto cos_013 = mjolnir::math::dot_product(v13, v10) /
                    (mjolnir::math::length(v13) * mjolnir::math::length(v10));
                BOOST_TEST_REQUIRE(std::acos(cos_013) == theta1,
                                   boost::test_tools::tolerance(1e-3));

                const auto v23 = sys.position(3) - sys.position(2);
                const auto cos_132 = mjolnir::math::dot_product(v13, v23) /
                    (mjolnir::math::length(v13) * mjolnir::math::length(v23));
                BOOST_TEST_REQUIRE(std::acos(cos_132) == theta2,
                                   boost::test_tools::tolerance(1e-3));
            }

            for(std::size_t idx=0; idx<sys.size(); ++idx)
            {
                sys.position(idx) += coord_type(0.01 * uni(mt), 0.01 * uni(mt), 0.01 * uni(mt));
                sys.force(idx)     = coord_type(0.0, 0.0, 0.0);
            }
            system_type ref_sys = sys;

            constexpr real_type tol = 1e-4;

            const auto energy = interaction.calc_force_and_energy(sys);
            const auto ref_energy = interaction.calc_energy(ref_sys);
            interaction.calc_force(ref_sys);
            BOOST_TEST(ref_energy == energy, boost::test_tools::tolerance(tol));

            for(std::size_t idx=0; idx<sys.size(); ++idx)
            {
                BOOST_TEST(mjolnir::math::X(sys.force(idx)) == mjolnir::math::X(ref_sys.force(idx)), boost::test_tools::tolerance(tol));
                BOOST_TEST(mjolnir::math::Y(sys.force(idx)) == mjolnir::math::Y(ref_sys.force(idx)), boost::test_tools::tolerance(tol));
                BOOST_TEST(mjolnir::math::Z(sys.force(idx)) == mjolnir::math::Z(ref_sys.force(idx)), boost::test_tools::tolerance(tol));
            }
        } // theta2
        } // theta1
        } // rbp
    }
}

BOOST_AUTO_TEST_CASE_TEMPLATE(ThreeSPN2CrossStackingIntearction_energy_and_force,
        ParameterSet, parameters_to_test)
{
    mjolnir::LoggerManager::set_default_logger(
            "test_3spn2_base_base_interaction.log");

    using traits_type       = mjolnir::SimulatorTraits<double, mjolnir::UnlimitedBoundary>;
    using real_type         = traits_type::real_type;
    using coord_type        = traits_type::coordinate_type;
    using boundary_type     = traits_type::boundary_type;
    using system_type       = mjolnir::System<traits_type>;
    using topology_type     = mjolnir::Topology;

    using potential_type    = mjolnir::ThreeSPN2BaseBaseInteractionPotential<traits_type>;
    using base_kind         = typename potential_type::base_kind;
    using parameter_type    = typename potential_type::parameter_type;
    using partition_type      = mjolnir::VerletList<traits_type, potential_type>;

    using interaction_type     = mjolnir::ThreeSPN2BaseBaseInteraction<traits_type>;
    using ignore_group_type    = typename potential_type::ignore_group_type;
    using ignore_molecule_type = typename potential_type::ignore_molecule_type;

    constexpr real_type pi = mjolnir::math::constants<real_type>::pi();

    {
        using unit_type = mjolnir::unit::constants<real_type>;
        using phys_type = mjolnir::physics::constants<real_type>;
        const std::string energy = "kcal/mol";
        const std::string length = "angstrom";
        phys_type::set_kB(phys_type::kB() * (unit_type::J_to_cal() / 1000.0) *
                          unit_type::avogadro_constant());
        phys_type::set_eps0(phys_type::eps0() * (1000.0 / unit_type::J_to_cal()) /
                            unit_type::avogadro_constant());
        phys_type::set_energy_unit(energy);

        phys_type::set_eps0(phys_type::eps0() / unit_type::m_to_angstrom());

        phys_type::set_m_to_length(unit_type::m_to_angstrom());
        phys_type::set_length_to_m(unit_type::angstrom_to_m());

        phys_type::set_L_to_volume(1e-3 * std::pow(unit_type::m_to_angstrom(), 3));
        phys_type::set_volume_to_L(1e+3 * std::pow(unit_type::angstrom_to_m(), 3));

        phys_type::set_length_unit(length);
    }

    //       Si   Bi   Bj   Sj
    //  5'    o -- o===o -- o     3'
    //  ^    /      \ /      \    |
    //  | P o        X        o P |
    //  |    \      / \      /    v
    //  3'    o -- o===o -- o     5'
    //       Bj_next   Bj_next
    //
    // Required test cases are the combination of the following conditions
    //
    // CrossStacking
    // theta3:
    //  1. theta < pi/2K
    //  2. pi/2K < theta < pi/K
    //  3. pi/K  < theta
    // thetaCS_i:
    //  1. theta < pi/2K
    //  2. pi/2K < theta < pi/K
    //  3. pi/K  < theta
    // thetaCS_j:
    //  1. theta < pi/2K
    //  2. pi/2K < theta < pi/K
    //  3. pi/K  < theta
    // thetaCS_j:
    //  1. theta < pi/2K
    //  2. pi/2K < theta < pi/K
    //  3. pi/K  < theta
    // ri_jnext
    //  1. (r < r0)
    //  2. (r0 < r)
    // rj_inext
    //  1. (r < r0)
    //  2. (r0 < r)
    //
    // Oh, okay, we have 3^4 * 2^2 = 324 test configurations.
    //
    // Also, this interaction is a sum of two, BasePairing and CrossStacking.
    // This makes numeric differentiation unstable.

    std::mt19937 mt(123456789);
    std::uniform_real_distribution<real_type> uni(-1.0, 1.0);

    for(const auto bases : {
            std::vector<base_kind>{base_kind::A, base_kind::T, base_kind::G, base_kind::G},
            std::vector<base_kind>{base_kind::A, base_kind::T, base_kind::C, base_kind::C},
            std::vector<base_kind>{base_kind::T, base_kind::A, base_kind::G, base_kind::G},
            std::vector<base_kind>{base_kind::T, base_kind::A, base_kind::C, base_kind::C},
            std::vector<base_kind>{base_kind::C, base_kind::G, base_kind::A, base_kind::A},
            std::vector<base_kind>{base_kind::C, base_kind::G, base_kind::T, base_kind::T},
            std::vector<base_kind>{base_kind::G, base_kind::C, base_kind::A, base_kind::A},
            std::vector<base_kind>{base_kind::G, base_kind::C, base_kind::T, base_kind::T}
        })
    {
        //        0    1   9    8
        //  5'    o -- o===o -- o     3'
        //  ^    /      \ /      \    |
        //  | 2 o        X        o 7 |
        //  |    \      / \      /    v
        //  3'    o -- o===o -- o     5'
        //        3    4   6    5

        constexpr auto invalid = potential_type::invalid();

        potential_type potential(ParameterSet{}, {
                {1, parameter_type{bases.at(0), 0, 0, 4, invalid}},
                {9, parameter_type{bases.at(1), 3, 8, invalid, 6}},
                {4, parameter_type{bases.at(2), 1, 3, invalid, 1}},
                {6, parameter_type{bases.at(3), 2, 5, 9, invalid}},
            }, {}, ignore_molecule_type("Nothing"), ignore_group_type({})
        );

        interaction_type interaction(potential_type(potential),
            mjolnir::SpatialPartition<traits_type, potential_type>(
                mjolnir::make_unique<partition_type>()));

        system_type sys(10, boundary_type{});
        topology_type topol(10);

        topol.add_connection(0, 1, "bond");
        topol.add_connection(0, 2, "bond");
        topol.add_connection(2, 3, "bond");
        topol.add_connection(3, 4, "bond");

        topol.add_connection(5, 6, "bond");
        topol.add_connection(5, 7, "bond");
        topol.add_connection(7, 8, "bond");
        topol.add_connection(8, 9, "bond");

        topol.add_connection(0, 2, "next_nucl");
        topol.add_connection(1, 2, "next_nucl");
        topol.add_connection(0, 3, "next_nucl");
        topol.add_connection(1, 3, "next_nucl");
        topol.add_connection(0, 4, "next_nucl");
        topol.add_connection(1, 4, "next_nucl");

        topol.add_connection(5, 7, "next_nucl");
        topol.add_connection(6, 7, "next_nucl");
        topol.add_connection(5, 8, "next_nucl");
        topol.add_connection(6, 8, "next_nucl");
        topol.add_connection(5, 9, "next_nucl");
        topol.add_connection(6, 9, "next_nucl");

        topol.construct_molecules();

        for(std::size_t i=0; i<10; ++i)
        {
            sys.mass(i)  = 1.0;
            sys.rmass(i) = 1.0;
            sys.position(i) = coord_type(0.0, 0.0, 0.0);
            sys.velocity(i) = coord_type(0.0, 0.0, 0.0);
            sys.force(i)    = coord_type(0.0, 0.0, 0.0);
            sys.name(i)  = "DNA";
            sys.group(i) = "DNA";
        }
        potential  .initialize(sys, topol);
        interaction.initialize(sys, topol);

        const auto bp_kind      = potential.bp_kind (bases.at(0), bases.at(1));
        const auto cs_i_kind    = potential.cs5_kind(bases.at(0), bases.at(3));
        const auto cs_j_kind    = potential.cs3_kind(bases.at(1), bases.at(2));

        const auto rbp          = potential.r0(bp_kind) + 0.2;
        const auto rcs_i_0      = potential.r0(cs_i_kind);
        const auto rcs_j_0      = potential.r0(cs_j_kind);
        const auto theta1       = potential.theta1_0(bp_kind);
        const auto theta2       = potential.theta2_0(bp_kind);
        const auto theta3_0     = potential.theta3_0(bp_kind);
        const auto thetaCS_i_0  = potential.thetaCS_0(cs_i_kind);
        const auto thetaCS_j_0  = potential.thetaCS_0(cs_j_kind);

        const auto pi_over_K_BP = potential.pi_over_K_BP();
        const auto pi_over_K_CS = potential.pi_over_K_CS();

        for(const auto rcsi : {rcs_i_0 - 0.2, rcs_i_0 + 0.5})
        {
        for(const auto rcsj : {rcs_j_0 - 0.2, rcs_j_0 + 0.5})
        {
        for(const auto theta3 : {theta3_0 + pi_over_K_BP * 0.1, theta3_0 + pi_over_K_BP * 0.6, theta3_0 + pi_over_K_BP * 1.1})
        {
        for(const auto thetaCS_i : {thetaCS_i_0 - pi_over_K_CS * 0.1, thetaCS_i_0 - pi_over_K_CS * 0.6, thetaCS_i_0 - pi_over_K_CS * 1.1})
        {
        for(const auto thetaCS_j : {thetaCS_j_0 + pi_over_K_CS * 0.1, thetaCS_j_0 + pi_over_K_CS * 0.6, thetaCS_j_0 + pi_over_K_CS * 1.1})
        {
            BOOST_TEST_REQUIRE(0.0 <= theta3   );BOOST_TEST_REQUIRE(theta3    <= pi);
            BOOST_TEST_REQUIRE(0.0 <= thetaCS_i);BOOST_TEST_REQUIRE(thetaCS_i <= pi);
            BOOST_TEST_REQUIRE(0.0 <= thetaCS_j);BOOST_TEST_REQUIRE(thetaCS_j <= pi);

            // generate positions...
            //        0    1   9    8
            //  5'    o -- o===o -- o     3'
            //  ^    /      \ /      \    |
            //  | 2 o        X        o 7 |
            //  |    \      / \      /    v
            //  3'    o -- o===o -- o     5'
            //        3    4   6    5

            sys.position(1) = coord_type(0.0, 0.0, 0.0);
            sys.position(9) = coord_type(0.0, 0.0, 0.0);

            sys.position(0) = coord_type(4.0 * std::cos(theta1),
                                         4.0 * std::sin(theta1), 0.0);
            sys.position(8) = coord_type(4.0 * std::cos(pi - theta2),
                                         4.0 * std::sin(pi - theta2), 0.0);
            // rotate around x axis
            // to make angle between 0->1 and 8->9 theta3
            {
                using matrix33_type = typename traits_type::matrix33_type;
                real_type best_phi = 0.0;
                real_type dtheta   = std::numeric_limits<real_type>::max();
                const auto bck = sys;
                for(int i=-999; i<1000; ++i)
                {
                    const real_type phi = pi * 0.001 * i;
                    const matrix33_type rot(
                       1.0,           0.0,           0.0,
                       0.0, std::cos(phi), -std::sin(phi),
                       0.0, std::sin(phi),  std::cos(phi));

                    sys.position(8) = rot * sys.position(8);
                    sys.position(9) = rot * sys.position(9);

                    const auto v01 = sys.position(1) - sys.position(0);
                    const auto v89 = sys.position(9) - sys.position(8);
                    const auto theta_0189 = std::acos(mjolnir::math::clamp<real_type>(
                         mjolnir::math::dot_product(v01, v89) /
                        (mjolnir::math::length(v01) * mjolnir::math::length(v89)),
                        -1.0, 1.0));

                    BOOST_TEST_MESSAGE("dtheta = " << dtheta << ", current configuration = " << std::abs(theta_0189 - theta3));

                    if(std::abs(theta_0189 - theta3) < dtheta)
                    {
                        dtheta   = std::abs(theta_0189 - theta3);
                        best_phi = phi;
                    }
                    sys = bck;
                }

                BOOST_TEST_MESSAGE("best_phi = " << best_phi << ", dtheta = " << dtheta);

                {
                    const matrix33_type rot(
                            1.0,                0.0,                 0.0,
                            0.0, std::cos(best_phi), -std::sin(best_phi),
                            0.0, std::sin(best_phi),  std::cos(best_phi));
                    sys.position(8) = rot * sys.position(8);
                    sys.position(9) = rot * sys.position(9);
                }

                mjolnir::math::X(sys.position(4)) += rbp;
                mjolnir::math::X(sys.position(8)) += rbp;
                mjolnir::math::X(sys.position(9)) += rbp;
            }

            {
                using matrix33_type = typename traits_type::matrix33_type;
                const auto v01 = sys.position(1) - sys.position(0);
                const auto v89 = sys.position(9) - sys.position(8);
                const auto n4 = mjolnir::math::cross_product(coord_type(0.0, 0.0, 1.0), v89);
                const auto n6 = mjolnir::math::cross_product(coord_type(0.0, 0.0, 1.0), v01);

                const auto n4_reg = n4 / mjolnir::math::length(n4);
                const auto n6_reg = n6 / mjolnir::math::length(n6);

                coord_type v16 = v01 / mjolnir::math::length(v01);
                coord_type v94 = v89 / mjolnir::math::length(v89);
                {
                    const auto cos_theta = std::cos(pi - thetaCS_i);
                    const auto sin_theta = std::sin(pi - thetaCS_i);

                    BOOST_TEST_REQUIRE(mjolnir::math::length(n6_reg) == 1.0, boost::test_tools::tolerance(1e-8));
                    const auto nx = mjolnir::math::X(n6_reg);
                    const auto ny = mjolnir::math::Y(n6_reg);
                    const auto nz = mjolnir::math::Z(n6_reg);
                    const matrix33_type rot_16(
                        cos_theta + nx*nx*(1-cos_theta),    nx*ny*(1-cos_theta) - nz*sin_theta, nx*nz*(1-cos_theta) + ny*sin_theta,
                        ny*nx*(1-cos_theta) + nz*sin_theta, cos_theta + ny*ny*(1-cos_theta),    ny*nz*(1-cos_theta) - nx*sin_theta,
                        nz*nx*(1-cos_theta) - ny*sin_theta, nz*ny*(1-cos_theta) + nx*sin_theta, cos_theta + nz*nz*(1-cos_theta));
                    BOOST_TEST_REQUIRE(mjolnir::math::length(v16) == 1.0, boost::test_tools::tolerance(1e-8));
                    v16 = rot_16 * v16;
                    BOOST_TEST_REQUIRE(mjolnir::math::length(v16) == 1.0, boost::test_tools::tolerance(1e-8));
                }
                {
                    const auto cos_theta = std::cos(pi - thetaCS_j);
                    const auto sin_theta = std::sin(pi - thetaCS_j);

                    BOOST_TEST_REQUIRE(mjolnir::math::length(n4_reg) == 1.0, boost::test_tools::tolerance(1e-8));
                    const auto nx = mjolnir::math::X(n4_reg);
                    const auto ny = mjolnir::math::Y(n4_reg);
                    const auto nz = mjolnir::math::Z(n4_reg);

                    const matrix33_type rot_94(
                        cos_theta + nx*nx*(1-cos_theta),    nx*ny*(1-cos_theta) - nz*sin_theta, nx*nz*(1-cos_theta) + ny*sin_theta,
                        ny*nx*(1-cos_theta) + nz*sin_theta, cos_theta + ny*ny*(1-cos_theta),    ny*nz*(1-cos_theta) - nx*sin_theta,
                        nz*nx*(1-cos_theta) - ny*sin_theta, nz*ny*(1-cos_theta) + nx*sin_theta, cos_theta + nz*nz*(1-cos_theta));

                    BOOST_TEST_REQUIRE(mjolnir::math::length(v94) == 1.0, boost::test_tools::tolerance(1e-8));
                    v94 = rot_94 * v94;
                    BOOST_TEST_REQUIRE(mjolnir::math::length(v94) == 1.0, boost::test_tools::tolerance(1e-8));
                }
                sys.position(4) = sys.position(9) + v94 * rcsj;
                sys.position(6) = sys.position(1) + v16 * rcsi;
            }

            // check the configurations are okay
            {
                const auto v19 = sys.position(9) - sys.position(1);
                BOOST_TEST_REQUIRE(mjolnir::math::length(v19) == rbp,
                           boost::test_tools::tolerance(0.01));

                const auto v01 = sys.position(1) - sys.position(0);
                const auto v89 = sys.position(9) - sys.position(8);
                {
                    const auto dot = mjolnir::math::dot_product(v01, v89);
                    const auto cos_theta = dot /
                        (mjolnir::math::length(v01) * mjolnir::math::length(v89));
                    const auto theta = std::acos(cos_theta);
                    BOOST_TEST_REQUIRE(theta == theta3, boost::test_tools::tolerance(0.01));
                }

                {
                    const auto dot = mjolnir::math::dot_product(-v01, v19);
                    const auto cos_theta = dot /
                        (mjolnir::math::length(v01) * mjolnir::math::length(v19));
                    const auto theta = std::acos(cos_theta);
                    BOOST_TEST_REQUIRE(theta == theta1, boost::test_tools::tolerance(0.01));
                }
                {
                    const auto dot = mjolnir::math::dot_product(v89, v19);
                    const auto cos_theta = dot /
                        (mjolnir::math::length(v89) * mjolnir::math::length(v19));
                    const auto theta = std::acos(cos_theta);
                    BOOST_TEST_REQUIRE(theta == theta2, boost::test_tools::tolerance(0.01));
                }

                const auto v61 = sys.position(1) - sys.position(6);
                BOOST_TEST_REQUIRE(mjolnir::math::length(v61) == rcsi,
                                   boost::test_tools::tolerance(0.01));
                {
                    const auto dot = mjolnir::math::dot_product(
                            v01 / mjolnir::math::length(v01),
                            v61 / mjolnir::math::length(v61));
                    const auto theta = std::acos(dot);
                    BOOST_TEST_REQUIRE(theta == thetaCS_i,
                            boost::test_tools::tolerance(0.01));
                }
                const auto v49 = sys.position(9) - sys.position(4);
                BOOST_TEST_REQUIRE(mjolnir::math::length(v49) == rcsj,
                           boost::test_tools::tolerance(0.01));
                {
                    const auto dot = mjolnir::math::dot_product(
                            v89 / mjolnir::math::length(v89),
                            v49 / mjolnir::math::length(v49));
                    const auto theta = std::acos(dot);
                    BOOST_TEST_REQUIRE(theta == thetaCS_j, boost::test_tools::tolerance(0.01));
                }
            }

            // positions generated!
            // ... and then rotate random direction to remove special axis
            {
                using matrix33_type = typename traits_type::matrix33_type;

                const auto rot_x = uni(mt) * pi;
                const auto rot_y = uni(mt) * pi;
                const auto rot_z = uni(mt) * pi;

                matrix33_type rotm_x(1.0,             0.0,              0.0,
                                     0.0, std::cos(rot_x), -std::sin(rot_x),
                                     0.0, std::sin(rot_x),  std::cos(rot_x));
                matrix33_type rotm_y( std::cos(rot_y), 0.0,  std::sin(rot_y),
                                                  0.0, 1.0,              0.0,
                                     -std::sin(rot_y), 0.0,  std::cos(rot_y));
                matrix33_type rotm_z(std::cos(rot_z), -std::sin(rot_z), 0.0,
                                     std::sin(rot_z),  std::cos(rot_z), 0.0,
                                                 0.0,              0.0, 1.0);

                const matrix33_type rotm = rotm_x * rotm_y * rotm_z;
                for(std::size_t idx=0; idx<sys.size(); ++idx)
                {
                    sys.position(idx) = rotm * sys.position(idx);
                }
            }

            // add perturbation and reset force
            for(std::size_t idx=0; idx<10; ++idx)
            {
                sys.position(idx) += coord_type(0.01 * uni(mt), 0.01 * uni(mt), 0.01 * uni(mt));
                sys.force(idx)     = coord_type(0.0, 0.0, 0.0);
            }
            system_type ref_sys = sys;

            constexpr real_type tol = 1e-4;

            const auto energy = interaction.calc_force_and_energy(sys);
            const auto ref_energy = interaction.calc_energy(ref_sys);
            interaction.calc_force(ref_sys);
            BOOST_TEST(ref_energy == energy, boost::test_tools::tolerance(tol));

            for(std::size_t idx=0; idx<sys.size(); ++idx)
            {
                BOOST_TEST(mjolnir::math::X(sys.force(idx)) == mjolnir::math::X(ref_sys.force(idx)), boost::test_tools::tolerance(tol));
                BOOST_TEST(mjolnir::math::Y(sys.force(idx)) == mjolnir::math::Y(ref_sys.force(idx)), boost::test_tools::tolerance(tol));
                BOOST_TEST(mjolnir::math::Z(sys.force(idx)) == mjolnir::math::Z(ref_sys.force(idx)), boost::test_tools::tolerance(tol));
            }
        } // thetaCS_j
        } // thetaCS_i
        } // theta3
        } // rcsj
        } // rcsi
    } // bp_kind
}


#include <iostream>
#include <iomanip>
#include <math.h>
#include <memory>
#include <stdexcept>
#include <stdio.h>
#include <stdlib.h>
#include <vector>

#include <Eigen/Core>
#include <Eigen/Dense>
#include <unsupported/Eigen/CXX11/Tensor>

#include "shtoolswrapper.h"

int
main(int argc, char** argv)
{

  std::string infile = "../examples/ExampleDataFiles/MarsTopo719.shape";

  int lmax = 15;
  int n = lmax+1;
  std::vector<double> mars = cpp_sh_read(infile, lmax );

  Eigen::TensorMap<Cilm> mars_tensor(&mars[0], 2, n, n);

  for(int i = 0; i < 2; ++i){
      for(int j = 0; j < n; ++j){
          for(int k = 0; k < n; ++k){
              std::cout << std::setw(12) << std::setprecision(3) << mars_tensor(i,j,k) << " ";
          }
          std::cout << std::endl;
      }
      std::cout << std::string(13*n,'-') << std::endl;
  }

  double val = cpp_make_grid_point( mars, 10.0, 30.0);
  std::cout <<  std::setprecision(16) << val << std::endl;
  std::cout <<  "diff to python " << val-3395259.548270001 << std::endl;

//   std::vector<double> cindex(90,1);
//   std::vector<double> cilm(2*8*8);
//
//
//   cpp_sh_cindex_to_cilm(cindex, cilm, degmax);

  return 0;

}