/*
 * LikelihoodBasedTopPairReconstruction.cpp
 *
 *  Created on: 25 Aug 2011
 *      Author: kreczko
 */

#include "../../interface/ReconstructionModules/LikelihoodBasedTopPairReconstruction.h"
#include <boost/lexical_cast.hpp>

using namespace std;

namespace BAT {

LikelihoodBasedTopPairReconstruction::LikelihoodBasedTopPairReconstruction(const LeptonPointer lepton, const METPointer met, const JetCollection jets, const JetCollection bjets):
		solutions(),
		alreadyReconstructed(false),
		met(met),
		jets(jets),
		bjets(bjets),
		leptonFromW(lepton) {
}

bool LikelihoodBasedTopPairReconstruction::meetsInitialCriteria() const {
	return met != 0 && leptonFromW != 0 && jets.size() >= 4;
}

std::string LikelihoodBasedTopPairReconstruction::getDetailsOnFailure() const {
	std::string msg = "Initial Criteria not met: \n";
	if (leptonFromW == 0)
		msg += "Electron from W: not filled \n";
	else
		msg += "Electron from W: filled \n";

	if (met == 0)
		msg += "Missing transverse energy: not filled \n";
	else
		msg += "Missing transverse energy: filled \n";
	std::string nJets(boost::lexical_cast<std::string>(jets.size()));
	if (jets.size() < 4)
		msg += "Number of jets is too small:" + nJets + ", should be >= 4 \n";
	else
		msg += "Number of jets is OK:" + nJets + "\n";

	return msg;
}

LikelihoodBasedTopPairReconstruction::~LikelihoodBasedTopPairReconstruction() {
}

TtbarHypothesisCollection LikelihoodBasedTopPairReconstruction::getAllSolutions(){
	if(!alreadyReconstructed)
		reconstruct();

	SmallerDiscriminatorIsBetter solutionComparator = SmallerDiscriminatorIsBetter();
	std::sort(solutions.begin(), solutions.end(), solutionComparator);
	return solutions;
}

const TtbarHypothesisPointer LikelihoodBasedTopPairReconstruction::getBestSolution() {
	const TtbarHypothesisPointer bestSolution = getAllSolutions().front();//sorted by quality, front == best
	return bestSolution;
}

void LikelihoodBasedTopPairReconstruction::reconstruct() {
	typedef unsigned short ushort;

	// Loop b jets to get hadronic b candidate
	for (ushort hadBindex = 0; hadBindex < bjets.size(); ++hadBindex) {
		JetPointer hadBJet = bjets[hadBindex];
		LeptonPointer signallepton = leptonFromW;
		METPointer MET = met;

		if (!meetsHadronicBJetRequirement(hadBJet))
			continue;
		// Loop b jets to get leptonic b candidate
		for (ushort lepBindex = 0; lepBindex < bjets.size(); ++lepBindex) {
			JetPointer lepBJet = bjets[lepBindex];
			if (lepBindex == hadBindex || !meetsLeptonicBJetRequirement(lepBJet))
				continue;

			// Loop light jets to get jets from W

			for ( ushort jet1Index=0; jet1Index < (jets.size()-1); ++jet1Index ) {
				for ( ushort jet2Index=jet1Index+1; jet2Index < jets.size(); ++jet2Index ) {

			// for (ushort jet1Index = 0; jet1Index < jets.size(); ++jet1Index) {
			// 	for (ushort jet2Index = 0; jet2Index < jets.size(); ++jet2Index) {
					JetPointer jet1FromW = jets[jet1Index];
					JetPointer jet2FromW = jets[jet2Index];

					if (jet2Index == jet1Index || !meetsJetFromWRequirement(jet1FromW, jet2FromW))
						continue;

					// Put all particles together
					TtbarHypothesisPointer solution(new TtbarHypothesis());
					//leptons
					solution->leptonFromW = leptonFromW;
					double nuChi2 = -1;
					solution->neutrinoFromW = getNeutrinoSolution( lepBJet, signallepton, MET, nuChi2 );
					solution->neutrinoChi2 = nuChi2;

					//jets
					solution->hadronicBJet = hadBJet;
					solution->leptonicBjet = lepBJet;
					solution->jet1FromW = jet1FromW;
					solution->jet2FromW = jet2FromW;

					//combine reconstructed objects
					solution->combineReconstructedObjects();

					// Get discrimnant for this solution
					solution->discriminator = getDiscriminator(solution);

					// Store if event is physical (checks masses of tops and Ws > 0 )
					if (meetsGlobalRequirement(solution)){
						solutions.push_back(solution);
					}

					else
						continue;
				}
			}
		}
	}
	alreadyReconstructed = true;
}

ParticlePointer LikelihoodBasedTopPairReconstruction::getNeutrinoSolution(JetPointer BJet, LeptonPointer Lepton, METPointer met, double& neutrinoChi2) {
	// Do the neutrino reconstruction
	NeutrinoSolver neutrinoSolver( &(Lepton->getFourVector()), &(BJet->getFourVector()), 80, 173 );
	double test = -1;
	FourVector neutrino = neutrinoSolver.GetBest(met->getFourVector().Px(), met->getFourVector().Py(), 25., 25., 0., test );
	neutrinoChi2 = test;

	return ParticlePointer (new Particle(neutrino.Energy(), neutrino.Px(), neutrino.Py(), neutrino.Pz()));
}

bool LikelihoodBasedTopPairReconstruction::meetsHadronicBJetRequirement(JetPointer hadBJet){
	return true;
}

bool LikelihoodBasedTopPairReconstruction::meetsLeptonicBJetRequirement(JetPointer lepBJet){
	return true;
}

bool LikelihoodBasedTopPairReconstruction::meetsJetFromWRequirement(JetPointer jet1, JetPointer jet2){
	return true;
}

bool LikelihoodBasedTopPairReconstruction::meetsGlobalRequirement(const TtbarHypothesisPointer solution){
	return solution->isPhysical();
}

double LikelihoodBasedTopPairReconstruction::getDiscriminator(const TtbarHypothesisPointer solution) const{
	// Given all the info for this solution/permutation, calculate the discriminant
	// cout << "------------------------------------------------------" << endl;

	// Print csv of hadronic b jet and probablitity from correct b histogram
	JetPointer hadBJet = solution->hadronicBJet;
	JetPointer lepBJet = solution->leptonicBjet;
	ParticlePointer hadTop = solution->hadronicTop;
	ParticlePointer lepTop = solution->leptonicTop;
	ParticlePointer hadW = solution->hadronicW;
	ParticlePointer lepW = solution->leptonicW;

	double NeutrinoChi2 = solution->neutrinoChi2;

	double hadBJetCSV = hadBJet->getBTagDiscriminator(BAT::BtagAlgorithm::value::CombinedSecondaryVertexV2);
	double lepBJetCSV = lepBJet->getBTagDiscriminator(BAT::BtagAlgorithm::value::CombinedSecondaryVertexV2);

	double Wmass = hadW->mass();
	double Topmass = hadTop->mass();

	double probCorrectHadB = Globals::csvCorrectPermHistogram->Interpolate( hadBJetCSV );
	double probIncorrectHadB = Globals::csvIncorrectPermHistogram->Interpolate( hadBJetCSV );

	double probCorrectLepB = Globals::csvCorrectPermHistogram->Interpolate( lepBJetCSV );
	double probIncorrectLepB = Globals::csvIncorrectPermHistogram->Interpolate( lepBJetCSV );

	double probCorrectNuChi2 = Globals::NuChiCorrectPermHistogram->Interpolate( NeutrinoChi2 );
	double probIncorrectNuChi2 = Globals::NuChiIncorrectPermHistogram->Interpolate( NeutrinoChi2 );

	double probCorrectWBMass = Globals::HadronicRecoCorrectPermHistogram->Interpolate( Wmass , Topmass );
	double probIncorrectWBMass = Globals::HadronicRecoIncorrectPermHistogram->Interpolate( Wmass , Topmass );

	if (probCorrectWBMass == 0){
		probCorrectWBMass = 0.000000001;
	}
	if (probIncorrectWBMass == 0){
		probIncorrectWBMass = 0.000000001;
	}

	double NuChi2Disc =  - log(probCorrectNuChi2/probIncorrectNuChi2);
	double CSVDisc = - log((probCorrectHadB/probIncorrectHadB)*(probCorrectLepB/probIncorrectLepB));
	double MassDisc = - log(probCorrectWBMass/probIncorrectWBMass);
	double likelihoodratio = 20;
	// cout << "Mass Hadronic T : " << hadTop->mass() << ", Mass Hadronic W : " << hadW->getFourVector().M() << endl;
	// cout << "Mass Leptonic T : " << lepTop->getFourVector().M() << ", Mass Leptonic W : " << lepW->getFourVector().M() << endl;
	// cout << "Neutrino Chi Sq : " << NeutrinoChi2 << endl;

	if (NeutrinoChi2 >= 0.0 && Wmass <= 490 && Topmass <= 490){

		solution->CSVDiscriminator = CSVDisc;
		solution->MassDiscriminator = MassDisc;
		solution->NuChi2Discriminator = NuChi2Disc;

		likelihoodratio = - log(probCorrectWBMass/probIncorrectWBMass) - log(probCorrectNuChi2/probIncorrectNuChi2) - log((probCorrectHadB/probIncorrectHadB)*(probCorrectLepB/probIncorrectLepB));

		// if (solution->isCorrect()){

		// 	cout << "Mass Hadronic T : " << hadTop->mass() << ", Mass Hadronic W : " << hadW->mass() << endl;
		// 	cout << "Mass Leptonic T : " << lepTop->mass() << ", Mass Leptonic W : " << lepW->mass() << endl;

		// 	cout << "probCorrectHadB : " << hadBJetCSV << " prob : " << probCorrectHadB << endl;
		// 	cout << "probIncorrectHadB : " << hadBJetCSV << " prob : " << probIncorrectHadB << endl;
		// 	// cout << "probCorrectLepB : " << lepBJetCSV << " prob : " << probCorrectLepB << endl;
		// 	// cout << "probIncorrectLepB : " << lepBJetCSV << " prob : " << probIncorrectLepB << endl;

		// 	// cout << "CorrectNu : " << NeutrinoChi2 << " prob : " << probCorrectNuChi2 << endl;
		// 	// cout << "IncorrectNu : " << NeutrinoChi2 << " prob : " << probIncorrectNuChi2 << endl;

		// 	// cout << "CorrectMassReco : " << Wmass << " " << Topmass << " prob : " << probCorrectWBMass << endl;
		// 	// cout << "IncorrectMassReco : " << Wmass << " " << Topmass <<  " prob : " << probIncorrectWBMass << endl;

		// 	cout << "MassDisc : " << MassDisc << endl;
		// 	cout << "CSVDisc : " << CSVDisc << endl;
		// 	cout << "NuChi2Disc : " << NuChi2Disc << endl;

		// 	cout << "Likelihood Test : " << likelihoodratio << endl;
		// 	}
	}

	else{
		solution->CSVDiscriminator = 10;
		solution->MassDiscriminator = 10;
		solution->NuChi2Discriminator = 10;
	}
return likelihoodratio;
}

} /* namespace BAT */


#include <iostream>
#include <Eigen/Dense>

using Eigen::MatrixXd;
using namespace std;

int main()
{
	MatrixXd R(3,3);
	R << 1, 0, 0,
		 0, 0, -1,
		 0, 1, 0;
	cout << R;
}


#pragma once

#include <Eigen/Core>
#include <cassert>
#include <vector>

namespace boltzmann {
// homogeneous
namespace hg {
template <typename NUMERIC_T = double>
class RK4
{
 public:
  typedef NUMERIC_T numeric_t;

 private:
  typedef Eigen::VectorXd vec_t;

 public:
  RK4(int N_)
      : N(N_)
      , v_k1(N_)
      , v_k2(N_)
      , v_k3(N_)
      , v_k4(N_)
  { /* empty */
  }

  void apply(NUMERIC_T* dst,
             const NUMERIC_T* src,
             const std::function<void(NUMERIC_T* dst, const NUMERIC_T* src)>& f,
             double dt);

 private:
  int N;
  vec_t v_k1;
  vec_t v_k2;
  vec_t v_k3;
  vec_t v_k4;
};

// ----------------------------------------------------------------------
template <typename NUMERIC_T>
void
RK4<NUMERIC_T>::apply(NUMERIC_T* dst,
                      const NUMERIC_T* src,
                      const std::function<void(NUMERIC_T* dst, const NUMERIC_T* src)>& f,
                      double dt)
{
  assert(src != dst);

  typedef Eigen::Map<vec_t> map_vec_t;
  typedef Eigen::Map<const vec_t> map_const_vec_t;

  map_const_vec_t v_in(src, N);
  map_vec_t v_dst(dst, N);

  f(v_k1.data(), v_in.data());

  v_dst = v_in + 0.5 * dt * v_k1;
  f(v_k2.data(), v_dst.data());

  v_dst = v_in + 0.5 * dt * v_k2;
  f(v_k3.data(), v_dst.data());

  v_dst = v_in + dt * v_k3;
  f(v_k4.data(), v_dst.data());

  v_dst = v_in + dt / 6 * (v_k1 + 2 * v_k2 + 2 * v_k3 + v_k4);
}

}  // end hg
}  // end boltzmann


/*
 *  VSCSoundParameters.cpp
 *  SynthStation
 *
 *  Created by Jonathan Thorpe on 12/11/2011.
 *  Copyright 2011 NXP. All rights reserved.
 *
 */

#include "VSCSoundParameters.h"
#include "VSCException.h"
#include "VSCMath.h"

#include <map>
#include <string>
#include <boost/lexical_cast.hpp>

const unsigned int VSCSParameter::kChannelNotFound = kVSCSVoidChannel;
const unsigned int VSCSParameter::kIndexAll = UINT_MAX;

bool VSCSParameter::Key::operator<(const Key& otherKey) const {
	if (domain < otherKey.domain) return true;
	else return false;
	if (code < otherKey.code) return true;
	else return false;
	if (index < otherKey.index) return true;
	return false;
}

bool VSCSParameter::Key::operator==(const Key& otherKey) const {
	if (domain != otherKey.domain) return false;
	if (code != otherKey.code) return false;
	if (index != otherKey.index) return false;
	return true;
}

VSCSParameter::VSCSParameter() {

}

VSCSParameter::~VSCSParameter() {

}

VSCSParameter& VSCSParameter::sharedInstance(void) {
	static VSCSParameter singletonInstance;
	return singletonInstance;
}

/*
 *	This should convert so that linear volume gain [0 : 1] becomes [-infinity : 0]
 */
VSCSFloat VSCSParameter::linearToDB(VSCSFloat linear) {
	VSCSFloat dB = 10.0*std::log10(linear);
    return  dB;
}

VSCSFloat VSCSParameter::dBToLinear(VSCSFloat dB) {
    return std::pow(10.0, 0.1*dB);
}

/*
 *	Parameter labels
 */
std::string VSCSParameter::getLabelForParameterWithKey(VSCSParameter::Key k) {

	/*
	 *	Check if there are exact matches
	 */
	KeyLabelMap::iterator labelIterator = customizedKeyLabels.find(k);
	if (labelIterator != customizedKeyLabels.end())
		return labelIterator->second;

	switch (k.code) {
		case CodeGain:
			return "Gain (Lin)";
		case CodeDBGain:
			return "Gain (dB)";
		case CodeFrequency:
			return "Freq (Hz)";
		case CodeLogFrequency:
			return "Freq (Log Hz)";
//		case CodeDegPhase:
//			return "Phase (deg)";
		case CodeRadPhase:
			return "Phase (rad)";
		case CodeHarmonics:
			return "Harmonics";
		default:
			return "";
			break;
	}

	throw VSCSBadParameterException();

}

void VSCSParameter::setLabelForParameterWithKey(std::string label, Key k) {

	KeyLabelMap::iterator labelIterator = customizedKeyLabels.find(k);
	if (labelIterator != customizedKeyLabels.end())
		customizedKeyLabels.erase(k);

	customizedKeyLabels.insert(KeyLabelPair (k, label));

}

void VSCSParameter::revertLabelForParameterWithKeyToDefault(Key k) {

	KeyLabelMap::iterator labelIterator = customizedKeyLabels.find(k);
	if (labelIterator != customizedKeyLabels.end())
		customizedKeyLabels.erase(k);

}

VSCSParameter::ValueRange VSCSParameter::getRangeForParameterWithKey(Key k) {

	KeyRangeMap::iterator rangeIterator = customizedKeyRanges.find(k);
	if (rangeIterator != customizedKeyRanges.end())
		return rangeIterator->second;

	switch (k.code) {
		case CodeGain:
			return ValueRange (0.0, 1.0);
		case CodeDBGain:
			return ValueRange (-30.0, 0.0);
		case CodeFrequency:
			return ValueRange (20.0, 20000.0);
		case CodeLogFrequency:
			return ValueRange (std::log10(20.0), std::log10(20000.0));
//		case CodeDegPhase:
//			return ValueRange (-180.0, 180.0);
		case CodeRadPhase:
			return ValueRange (-vsc::kPI, vsc::kPI);
		case CodeHarmonics:
			return ValueRange (0.0, 10.0);
		default:
			return ValueRange (0.0, 0.0);
			break;
	}

	throw VSCSBadParameterException();

}

void VSCSParameter::setRangeForParameterWithKey(ValueRange valRange, Key k) {

	KeyRangeMap::iterator rangeIterator = customizedKeyRanges.find(k);
	if (rangeIterator != customizedKeyRanges.end())
		customizedKeyRanges.erase(k);

	customizedKeyRanges.insert(KeyRangePair (k, valRange));

}

void VSCSParameter::revertRangeForParameterWithKeyToDefault(Key k) {

	KeyRangeMap::iterator rangeIterator = customizedKeyRanges.find(k);
	if (rangeIterator != customizedKeyRanges.end())
		customizedKeyRanges.erase(k);

}


#include <iostream>
#include <iterator>
#include <sstream>
#include <iomanip>
#include <map>

#include <boost/program_options.hpp>
#include <boost/log/sources/logger.hpp>
#include <boost/archive/binary_iarchive.hpp>
#include <boost/archive/binary_oarchive.hpp>
#include <boost/serialization/map.hpp>
#include <boost/mpi.hpp>

#include <benchmark.hpp>
#include <logging.hpp>

#include <constants.hpp>
#include <lapack.hpp>
#include <matrix_serialization.hpp>
#include <dielectric_function_v2.hpp>

namespace po  = boost::program_options;
namespace mpi = boost::mpi;

using R = double;

#ifdef ASSUME_REAL
using C = R;
#else
using C = std::complex<R>;
#endif

///////////////////////////////////////////////////////////////////////////////
/// \brief Returns the rank of the admin process.
///////////////////////////////////////////////////////////////////////////////
constexpr auto admin_rank() -> int { return 0; }

// ============================================================================
//                                   SETTINGS
// ============================================================================

///////////////////////////////////////////////////////////////////////////////
/// \brief Class representing the configurations options.

/// It is a small wrapper around boost::program_options, which read the
/// options from command line during construction.
///////////////////////////////////////////////////////////////////////////////

auto init_options() -> po::options_description
{
	po::options_description description;
	description.add_options()
		( "help", "Produce the help message." )
		( "out.file.log"
		, po::value<std::string>()->default_value("sample")
		, "Name of the file that will be used for logging. This is "
		  "not the full name, but rather a 'base'. The actual file name "
		  "will be \"[log-file].[PROCESS_RANK].log\"." )
		( "out.file.eps"
		, po::value<std::string>()->default_value("Epsilon")
		, "Name of the file that will be used for saving the computed "
		  "dielectric functions. This is, again, a base rather than the "
		  "actual name. The actual file name will be "
		  "\"[eps-file].[PROCESS_RANK].bin\", which indicates that "
		  "dielectric functions will be stored in binary format of the "
		  "boost::serialization library." )
		( "in.file.energies"
		, po::value<std::string>()->required()
		, "Name of the BIN file where the eigenenergies of the hamiltonian"
		  " are read from. This file must be in the format of the "
		  "boost::serialization library. This option is REQUIRED." )
		( "in.file.states"
		, po::value<std::string>()->required()
		, "Name of the BIN file where the eigenstates of the hamiltonian "
		  "are read from. This file must be in the format of the "
		  "boost::serialization library. This option is REQUIRED." )
		( "in.file.potential"
		, po::value<std::string>()->required()
		, "Name of the BIN file where the interaction potential "
		  "is read from. This file must be in the format of the "
		  "boost::serialization library. This option is REQUIRED." )
		( "in.frequency.start"
		, po::value<R>()->required()
		, "Starting frequency in eV. Must be a real value. This option "
		  "is REQUIRED." )
		( "in.frequency.stop"
		, po::value<R>()->required()
		, "Stopping frequency in eV. Must be a real value. This option "
		  "is REQUIRED." )
		( "in.frequency.step"
		, po::value<R>()->required()
		, "Step in frequency in eV. Must be a real value." );
	description.add(tcm::init_constants_options<double>());
	return description;
}

template <class _Help, class _Proceed>
auto parse_command_line( int argc, char** argv
                       , _Help&& help
                       , _Proceed&& proceed ) -> bool
{
	auto const desc = init_options();
	po::variables_map vm;

	po::store(po::command_line_parser(argc, argv).options(desc).run(), vm);

	if (vm.count("help")) {
		help(desc);
		return false;
	}

	po::notify(vm);
	proceed(vm);
	return true;
}

auto log_file_name(int const rank, std::string file_name_base)
{
	return file_name_base + "." + std::to_string(rank) + ".log";
}

auto initialize_logging( int const rank
                       , std::string const& file_name_base) -> void
{
	using namespace boost::log;
	register_simple_formatter_factory<tcm::severity_level, char>("Severity");
	add_file_log
	(
		keywords::file_name = log_file_name(rank, file_name_base),
		keywords::format = "%LineID%: [%TimeStamp%] [%Severity%] %Message%",
		keywords::auto_flush = true
	);

	add_common_attributes();
}

template <class _R, class _C>
struct IPackage {
	std::tuple<_R, _R, _R>                frequency_range;
	std::string                           log_file_name_base;
	std::string                           eps_file_name_base;
	tcm::Matrix<_R>                       E;
	tcm::Matrix<_C>                       Psi;
	tcm::Matrix<std::complex<_R>>         V;
	std::map<std::string, _R>             constants;

private:
	friend boost::serialization::access;

	template<class _Archive>
	auto save(_Archive & ar, unsigned int const version) const -> void
	{
		ar << std::get<0>(frequency_range)
		   << std::get<1>(frequency_range)
		   << std::get<2>(frequency_range)
		   << log_file_name_base
		   << eps_file_name_base
		   << E
		   << Psi
		   << V
		   << constants;
	}

	template<class _Archive>
	auto load(_Archive & ar, unsigned int const version) -> void
	{
		ar >> std::get<0>(frequency_range)
		   >> std::get<1>(frequency_range)
		   >> std::get<2>(frequency_range)
		   >> log_file_name_base
		   >> eps_file_name_base
		   >> E
		   >> Psi
		   >> V
		   >> constants;
	}

	BOOST_SERIALIZATION_SPLIT_MEMBER()
};

template<class _T>
auto load_matrix(std::string const& file_name) -> tcm::Matrix<_T>
{
	std::ifstream in_stream{file_name};
	if(not in_stream)
		throw std::runtime_error{"Failed to open `" + file_name + "`."};
	boost::archive::binary_iarchive in_archive{in_stream};

	tcm::Matrix<_T> A;
	in_archive >> A;
	return A;
}

auto load_ipackage(po::variables_map const& vm) -> IPackage<R, C>
{
	return { std::make_tuple( vm["in.frequency.start"].as<R>()
	                        , vm["in.frequency.stop"].as<R>()
	                        , vm["in.frequency.step"].as<R>() )
	       , vm["out.file.log"].as<std::string>()
	       , vm["out.file.eps"].as<std::string>()
	       , load_matrix<R>(vm["in.file.energies"].as<std::string>())
	       , load_matrix<C>(vm["in.file.states"].as<std::string>())
	       , load_matrix<std::complex<R>>(vm["in.file.potential"].as<std::string>())
		   , tcm::load_constants<R, double, std::map<std::string, R>>(vm)
		   };
}

template <class _T, class _Logger>
auto cache( std::string const& message
          , tcm::Matrix<_T> const& X
		  , std::string const& file_name
          , _Logger & lg ) -> void
{
	LOG(lg, info) << "Caching: " << message << "...";

	std::ofstream out_stream{file_name};
	if(not out_stream)
		throw std::runtime_error{"Could not open `" + file_name + "`."};
	boost::archive::binary_oarchive out_archive{out_stream};
	out_archive << X;

	LOG(lg, info) << "Caching successfully finished.";
}

template<class _Real, class _Logger>
auto get_job( mpi::communicator const& world
            , std::tuple<_Real, _Real, _Real> const& range
			, _Logger & lg ) -> std::vector<_Real>
{
	LOG(lg, info) << "Calculating homework...";

	auto const  rank  = world.rank();
	auto const  size  = world.size();
	auto const& begin = std::get<0>(range);
	auto const& end   = std::get<1>(range);
	auto const& step  = std::get<2>(range);

	std::vector<_Real> homework;
	for(auto i = 0; begin + i * step <= end; ++i) {
		if(i % size == rank)
			homework.push_back(begin + i * step);
	}

	auto record = lg.open_record(boost::log::keywords::severity =
	                                 tcm::severity_level::info);
	if (record) {
		boost::log::record_ostream stream{record};
		stream << "Need to perform calculations for the following "
			   << "frequencies: {";
		if(not homework.empty()) {
			for(std::size_t i = 0; i < homework.size() - 1; ++i)
				stream << homework[i] << ", ";
			stream << homework.back();
		}
		stream << "}";
		stream.flush();
		lg.push_record(std::move(record));
	}

	return homework;
}

template<class _Number, class _R, class _C, class _Logger>
auto calculate_single( _Number const omega
                     , tcm::Matrix<_R> const& E
					 , tcm::Matrix<_C> const& Psi
					 , tcm::Matrix<std::complex<_R>> const& V
					 , std::map<std::string, _R> const& cs
                     , _Logger & lg
					 , std::string const& file_name_base ) -> void
{
	using namespace std::complex_literals;
	LOG(lg, info) << "Calculating dielectric function for omega = "
	              << omega << "...";

	auto const file_name_matrix =
		file_name_base + "." + std::to_string(std::real(omega))
		+ ".matrix.bin";
	auto const file_name_eigenvalues =
		file_name_base + "." + std::to_string(std::real(omega))
		+ ".eigenvalues.bin";
	auto const file_name_eigenstates =
		file_name_base + "." + std::to_string(std::real(omega))
		+ ".eigenstates.bin";

	auto epsilon = tcm::dielectric_function::make(omega, E, Psi, V, cs, lg);
	cache("Dielectric function matrix", epsilon, file_name_matrix, lg);

	LOG(lg, info) << "Diagonalizing dielectric function for omega = "
	              << omega << "...";

	using epsilon_type = typename decltype(epsilon)::value_type;
	tcm::Matrix<epsilon_type> W{epsilon.height(), 1};
	tcm::Matrix<epsilon_type> Z{epsilon.height(), epsilon.height()};
	tcm::lapack::geev(epsilon, W, Z);

	cache("Dielectric function eigenvalues", W, file_name_eigenvalues, lg);
	cache("Dielectric function eigenstates", Z, file_name_eigenstates, lg);

	LOG(lg, info) << "Done for omega = " << omega << "!";
}

auto run( mpi::communicator & world
        , IPackage<R, C> & input ) -> void
{
	mpi::broadcast(world, input, admin_rank());

	initialize_logging(world.rank(), input.log_file_name_base);
	boost::log::sources::severity_logger<tcm::severity_level> lg;

	auto const homework = get_job<R>(world, input.frequency_range, lg);
	if (homework.empty())
		return;
	for(auto const& w : homework) {
		calculate_single( std::complex<R>{w, input.constants.at("tau")}
		                , input.E
						, input.Psi
						, input.V
						, input.constants
						, lg
						, input.eps_file_name_base );
	}

	auto record = lg.open_record(boost::log::keywords::severity =
	                                 tcm::severity_level::info);
	if (record) {
		boost::log::record_ostream stream{record};
		stream << "Timings:\n";
		tcm::timing::report(stream);
		stream.flush();
		lg.push_record(std::move(record));
	}
}

int main(int argc, char** argv)
{
	mpi::environment env;
	mpi::communicator world;

	IPackage<R, C> input;
	bool proceed_with_calculation;
	if (world.rank() == admin_rank()) {
		proceed_with_calculation =
			parse_command_line( argc, argv
			                  , [&env] (auto _desc) { std::cout << _desc << '\n'; }
			                  , [&input] (auto _vm) { input = load_ipackage(_vm); } );
	}

	mpi::broadcast(world, proceed_with_calculation, admin_rank());
	if(not proceed_with_calculation)
		return EXIT_SUCCESS;

	run(world, input);

	return EXIT_SUCCESS;
}


/*
 Copyright (C) 2016 Quaternion Risk Management Ltd
 All rights reserved.

 This file is part of ORE, a free-software/open-source library
 for transparent pricing and risk analysis - http://opensourcerisk.org

 ORE is free software: you can redistribute it and/or modify it
 under the terms of the Modified BSD License.  You should have received a
 copy of the license along with this program.
 The license is also available online at <http://opensourcerisk.org>

 This program is distributed on the basis that it will form a useful
 contribution to risk analytics and model standardisation, but WITHOUT
 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 FITNESS FOR A PARTICULAR PURPOSE. See the license for more details.
*/

#include <ored/model/crcirbuilder.hpp>
#include <ored/model/crlgmbuilder.hpp>
#include <ored/model/crossassetmodelbuilder.hpp>
#include <ored/model/eqbsbuilder.hpp>
#include <ored/model/fxbsbuilder.hpp>
#include <ored/model/inflation/infdkbuilder.hpp>
#include <ored/model/inflation/infjybuilder.hpp>
#include <ored/model/inflation/infjydata.hpp>
#include <ored/model/lgmbuilder.hpp>
#include <ored/model/structuredmodelerror.hpp>
#include <ored/model/utilities.hpp>
#include <ored/utilities/correlationmatrix.hpp>
#include <ored/utilities/log.hpp>
#include <ored/utilities/parsers.hpp>

#include <qle/cashflows/jyyoyinflationcouponpricer.hpp>
#include <qle/models/cpicapfloorhelper.hpp>
#include <qle/models/fxbsconstantparametrization.hpp>
#include <qle/models/fxbspiecewiseconstantparametrization.hpp>
#include <qle/models/fxeqoptionhelper.hpp>
#include <qle/models/irlgm1fconstantparametrization.hpp>
#include <qle/models/irlgm1fpiecewiseconstanthullwhiteadaptor.hpp>
#include <qle/models/irlgm1fpiecewiseconstantparametrization.hpp>
#include <qle/models/irlgm1fpiecewiselinearparametrization.hpp>
#include <qle/models/yoycapfloorhelper.hpp>
#include <qle/models/yoyswaphelper.hpp>
#include <qle/pricingengines/analyticcclgmfxoptionengine.hpp>
#include <qle/pricingengines/analyticdkcpicapfloorengine.hpp>
#include <qle/pricingengines/analyticjycpicapfloorengine.hpp>
#include <qle/pricingengines/analyticjyyoycapfloorengine.hpp>
#include <qle/pricingengines/analyticlgmswaptionengine.hpp>
#include <qle/pricingengines/analyticxassetlgmeqoptionengine.hpp>

#include <ql/math/optimization/levenbergmarquardt.hpp>
#include <ql/models/shortrate/calibrationhelpers/swaptionhelper.hpp>
#include <ql/pricingengines/swap/discountingswapengine.hpp>
#include <ql/quotes/simplequote.hpp>
#include <ql/utilities/dataformatters.hpp>

#include <boost/algorithm/string/case_conv.hpp>
#include <boost/lexical_cast.hpp>

using QuantExt::AnalyticJyCpiCapFloorEngine;
using QuantExt::AnalyticJyYoYCapFloorEngine;
using QuantExt::CpiCapFloorHelper;
using QuantExt::InfDkParametrization;
using QuantExt::IrLgm1fParametrization;
using QuantExt::JyYoYInflationCouponPricer;
using QuantExt::YoYCapFloorHelper;
using QuantExt::YoYSwapHelper;
using QuantLib::DiscountingSwapEngine;
using std::vector;

namespace ore {
namespace data {

CrossAssetModelBuilder::CrossAssetModelBuilder(
    const boost::shared_ptr<ore::data::Market>& market, const boost::shared_ptr<CrossAssetModelData>& config,
    const std::string& configurationLgmCalibration, const std::string& configurationFxCalibration,
    const std::string& configurationEqCalibration, const std::string& configurationInfCalibration,
    const std::string& configurationCrCalibration, const std::string& configurationFinalModel,
    const DayCounter& dayCounter, const bool dontCalibrate, const bool continueOnError,
    const std::string& referenceCalibrationGrid)
    : market_(market), config_(config), configurationLgmCalibration_(configurationLgmCalibration),
      configurationFxCalibration_(configurationFxCalibration), configurationEqCalibration_(configurationEqCalibration),
      configurationInfCalibration_(configurationInfCalibration),
      configurationCrCalibration_(configurationCrCalibration), configurationFinalModel_(configurationFinalModel),
      dayCounter_(dayCounter), dontCalibrate_(dontCalibrate), continueOnError_(continueOnError),
      referenceCalibrationGrid_(referenceCalibrationGrid),
      optimizationMethod_(boost::shared_ptr<OptimizationMethod>(new LevenbergMarquardt(1E-8, 1E-8, 1E-8))),
      endCriteria_(EndCriteria(1000, 500, 1E-8, 1E-8, 1E-8)) {
    buildModel();
    registerWithSubBuilders();
    // register market observer with correlations
    marketObserver_ = boost::make_shared<MarketObserver>();
    for (auto const& c : config->correlations())
        marketObserver_->addObservable(c.second);
    // reset market observer's updated flag
    marketObserver_->hasUpdated(true);
}

Handle<QuantExt::CrossAssetModel> CrossAssetModelBuilder::model() const {
    calculate();
    return model_;
}

const std::vector<Real>& CrossAssetModelBuilder::swaptionCalibrationErrors() {
    calculate();
    return swaptionCalibrationErrors_;
}
const std::vector<Real>& CrossAssetModelBuilder::fxOptionCalibrationErrors() {
    calculate();
    return fxOptionCalibrationErrors_;
}
const std::vector<Real>& CrossAssetModelBuilder::eqOptionCalibrationErrors() {
    calculate();
    return eqOptionCalibrationErrors_;
}
const std::vector<Real>& CrossAssetModelBuilder::inflationCalibrationErrors() {
    calculate();
    return inflationCalibrationErrors_;
}

void CrossAssetModelBuilder::unregisterWithSubBuilders() {
    for (auto okv : subBuilders_)
        for (auto ikv : okv.second)
            unregisterWith(ikv.second);
}

void CrossAssetModelBuilder::registerWithSubBuilders() {
    for (auto okv : subBuilders_)
        for (auto ikv : okv.second)
            registerWith(ikv.second);
}

bool CrossAssetModelBuilder::requiresRecalibration() const {
    for (auto okv : subBuilders_)
        for (auto ikv : okv.second)
            if (ikv.second->requiresRecalibration())
                return true;

    return marketObserver_->hasUpdated(false);
}

void CrossAssetModelBuilder::performCalculations() const {
    // if any of the sub models requires a recalibration, we rebuilt the model
    // TODO we could do this more selectively
    if (!dontCalibrate_ && requiresRecalibration()) {
        // reset market observer update flag
        marketObserver_->hasUpdated(true);
        // the cast is a bit ugly, but we pretty much know what we are doing here
        const_cast<CrossAssetModelBuilder*>(this)->unregisterWithSubBuilders();
        buildModel();
        const_cast<CrossAssetModelBuilder*>(this)->registerWithSubBuilders();
    }
}

void CrossAssetModelBuilder::buildModel() const {

    QL_REQUIRE(market_ != NULL, "CrossAssetModelBuilder: no market given");
    LOG("Start building CrossAssetModel");
    DLOG("configurations: LgmCalibration "
         << configurationLgmCalibration_ << ", FxCalibration " << configurationFxCalibration_ << ", EqCalibration "
         << configurationEqCalibration_ << ", InfCalibration " << configurationInfCalibration_ << ", CrCalibration"
         << configurationCrCalibration_ << ", FinalModel " << configurationFinalModel_);
    if (dontCalibrate_) {
        DLOG("Calibration of the model is disabled.");
    }

    QL_REQUIRE(config_->irConfigs().size() > 0, "missing IR configurations");
    QL_REQUIRE(config_->irConfigs().size() == config_->fxConfigs().size() + 1,
               "FX configuration size " << config_->fxConfigs().size() << " inconsisitent with IR configuration size "
                                        << config_->irConfigs().size());

    swaptionBaskets_.resize(config_->irConfigs().size());
    optionExpiries_.resize(config_->irConfigs().size());
    swaptionMaturities_.resize(config_->irConfigs().size());
    swaptionCalibrationErrors_.resize(config_->irConfigs().size());
    fxOptionBaskets_.resize(config_->fxConfigs().size());
    fxOptionExpiries_.resize(config_->fxConfigs().size());
    fxOptionCalibrationErrors_.resize(config_->fxConfigs().size());
    eqOptionBaskets_.resize(config_->eqConfigs().size());
    eqOptionExpiries_.resize(config_->eqConfigs().size());
    eqOptionCalibrationErrors_.resize(config_->eqConfigs().size());
    inflationCalibrationErrors_.resize(config_->infConfigs().size());

    subBuilders_.clear();

    // Store information on the number of factors for each process. This is used when requesting a correlation matrix
    // from the CorrelationMatrixBuilder below.
    using ProcessInfo = CorrelationMatrixBuilder::ProcessInfo;
    namespace CT = QuantExt::CrossAssetModelTypes;
    ProcessInfo processInfo;

    /*******************************************************
     * Build the IR parametrizations and calibration baskets
     */
    std::vector<boost::shared_ptr<QuantExt::IrLgm1fParametrization>> irParametrizations;
    std::vector<RelinkableHandle<YieldTermStructure>> irDiscountCurves;
    std::vector<std::string> currencies, regions, crNames, eqNames, infIndices;
    std::vector<boost::shared_ptr<LgmBuilder>> irBuilder;

    for (Size i = 0; i < config_->irConfigs().size(); i++) {
        boost::shared_ptr<IrLgmData> ir = config_->irConfigs()[i];
        DLOG("IR Parametrization " << i << " ccy " << ir->ccy());
        boost::shared_ptr<LgmBuilder> builder =
            boost::make_shared<LgmBuilder>(market_, ir, configurationLgmCalibration_, config_->bootstrapTolerance(),
                                           continueOnError_, referenceCalibrationGrid_);
        if (dontCalibrate_)
            builder->freeze();
        irBuilder.push_back(builder);
        boost::shared_ptr<QuantExt::IrLgm1fParametrization> parametrization = builder->parametrization();
        swaptionBaskets_[i] = builder->swaptionBasket();
        currencies.push_back(ir->ccy());
        irParametrizations.push_back(parametrization);
        irDiscountCurves.push_back(builder->discountCurve());
        subBuilders_[CT::IR][i] = builder;
        processInfo[CT::IR].emplace_back(ir->ccy(), 1);
    }

    QL_REQUIRE(irParametrizations.size() > 0, "missing IR parametrizations");

    QuantLib::Currency domesticCcy = irParametrizations[0]->currency();

    /*******************************************************
     * Build the FX parametrizations and calibration baskets
     */
    std::vector<boost::shared_ptr<QuantExt::FxBsParametrization>> fxParametrizations;
    for (Size i = 0; i < config_->fxConfigs().size(); i++) {
        DLOG("FX Parametrization " << i);
        boost::shared_ptr<FxBsData> fx = config_->fxConfigs()[i];
        QuantLib::Currency ccy = ore::data::parseCurrency(fx->foreignCcy());
        QuantLib::Currency domCcy = ore::data::parseCurrency(fx->domesticCcy());

        QL_REQUIRE(ccy.code() == irParametrizations[i + 1]->currency().code(),
                   "FX parametrization currency[" << i << "]=" << ccy << " does not match IR currrency[" << i + 1
                                                  << "]=" << irParametrizations[i + 1]->currency().code());

        QL_REQUIRE(domCcy == domesticCcy, "FX parametrization [" << i << "]=" << ccy << "/" << domCcy
                                                                 << " does not match domestic ccy " << domesticCcy);

        boost::shared_ptr<FxBsBuilder> builder =
            boost::make_shared<FxBsBuilder>(market_, fx, configurationFxCalibration_, referenceCalibrationGrid_);
        boost::shared_ptr<QuantExt::FxBsParametrization> parametrization = builder->parametrization();

        fxOptionBaskets_[i] = builder->optionBasket();
        fxParametrizations.push_back(parametrization);
        subBuilders_[CT::FX][i] = builder;
        processInfo[CT::FX].emplace_back(ccy.code() + domCcy.code(), 1);
    }

    /*******************************************************
     * Build the EQ parametrizations and calibration baskets
     */
    std::vector<boost::shared_ptr<QuantExt::EqBsParametrization>> eqParametrizations;
    for (Size i = 0; i < config_->eqConfigs().size(); i++) {
        DLOG("EQ Parametrization " << i);
        boost::shared_ptr<EqBsData> eq = config_->eqConfigs()[i];
        string eqName = eq->eqName();
        QuantLib::Currency eqCcy = ore::data::parseCurrency(eq->currency());
        QL_REQUIRE(std::find(currencies.begin(), currencies.end(), eqCcy.code()) != currencies.end(),
                   "Currency (" << eqCcy << ") for equity " << eqName << " not covered by CrossAssetModelData");
        boost::shared_ptr<EqBsBuilder> builder = boost::make_shared<EqBsBuilder>(
            market_, eq, domesticCcy, configurationEqCalibration_, referenceCalibrationGrid_);
        boost::shared_ptr<QuantExt::EqBsParametrization> parametrization = builder->parametrization();
        eqOptionBaskets_[i] = builder->optionBasket();
        eqParametrizations.push_back(parametrization);
        eqNames.push_back(eqName);
        subBuilders_[CT::EQ][i] = builder;
        processInfo[CT::EQ].emplace_back(eqName, 1);
    }

    // Build the INF parametrizations and calibration baskets
    vector<boost::shared_ptr<Parametrization>> infParameterizations;
    for (Size i = 0; i < config_->infConfigs().size(); i++) {
        boost::shared_ptr<InflationModelData> imData = config_->infConfigs()[i];
        DLOG("Inflation parameterisation (" << i << ") for index " << imData->index());
        if (auto dkData = boost::dynamic_pointer_cast<InfDkData>(imData)) {
            boost::shared_ptr<InfDkBuilder> builder = boost::make_shared<InfDkBuilder>(
                market_, dkData, configurationInfCalibration_, referenceCalibrationGrid_, dontCalibrate_);
            if (dontCalibrate_)
                builder->freeze();
            infParameterizations.push_back(builder->parametrization());
            subBuilders_[CT::INF][i] = builder;
            processInfo[CT::INF].emplace_back(dkData->index(), 1);
        } else if (auto jyData = boost::dynamic_pointer_cast<InfJyData>(imData)) {
            boost::shared_ptr<InfJyBuilder> builder = boost::make_shared<InfJyBuilder>(
                market_, jyData, configurationInfCalibration_, referenceCalibrationGrid_);
            infParameterizations.push_back(builder->parameterization());
            subBuilders_[CT::INF][i] = builder;
            processInfo[CT::INF].emplace_back(jyData->index(), 2);
        } else {
            QL_FAIL("CrossAssetModelBuilder expects either DK or JY inflation model data.");
        }
        infIndices.push_back(imData->index());
    }

    /*******************************************************
     * Build the CR parametrizations and calibration baskets
     */
    // LGM (if any)
    std::vector<boost::shared_ptr<QuantExt::CrLgm1fParametrization>> crLgmParametrizations;
    for (Size i = 0; i < config_->crLgmConfigs().size(); ++i) {
        LOG("CR LGM Parametrization " << i);
        boost::shared_ptr<CrLgmData> cr = config_->crLgmConfigs()[i];
        string crName = cr->name();
        boost::shared_ptr<CrLgmBuilder> builder =
            boost::make_shared<CrLgmBuilder>(market_, cr, configurationCrCalibration_);
        boost::shared_ptr<QuantExt::CrLgm1fParametrization> parametrization = builder->parametrization();
        crLgmParametrizations.push_back(parametrization);
        crNames.push_back(crName);
        subBuilders_[CT::CR][i] = builder;
        processInfo[CT::CR].emplace_back(crName, 1);
    }

    // CIR (if any)
    std::vector<boost::shared_ptr<QuantExt::CrCirppParametrization>> crCirParametrizations;
    for (Size i = 0; i < config_->crCirConfigs().size(); ++i) {
        LOG("CR CIR Parametrization " << i);
        boost::shared_ptr<CrCirData> cr = config_->crCirConfigs()[i];
        string crName = cr->name();
        boost::shared_ptr<CrCirBuilder> builder =
            boost::make_shared<CrCirBuilder>(market_, cr, configurationCrCalibration_);
        boost::shared_ptr<QuantExt::CrCirppParametrization> parametrization = builder->parametrization();
        crCirParametrizations.push_back(parametrization);
        crNames.push_back(crName);
        subBuilders_[CT::CR][i] = builder;
        processInfo[CT::CR].emplace_back(crName, 1);
    }

    std::vector<boost::shared_ptr<QuantExt::Parametrization>> parametrizations;
    for (Size i = 0; i < irParametrizations.size(); i++)
        parametrizations.push_back(irParametrizations[i]);
    for (Size i = 0; i < fxParametrizations.size(); i++)
        parametrizations.push_back(fxParametrizations[i]);
    for (Size i = 0; i < eqParametrizations.size(); i++)
        parametrizations.push_back(eqParametrizations[i]);
    parametrizations.insert(parametrizations.end(), infParameterizations.begin(), infParameterizations.end());
    for (Size i = 0; i < crLgmParametrizations.size(); i++)
        parametrizations.push_back(crLgmParametrizations[i]);
    for (Size i = 0; i < crCirParametrizations.size(); i++)
        parametrizations.push_back(crCirParametrizations[i]);

    QL_REQUIRE(fxParametrizations.size() == irParametrizations.size() - 1, "mismatch in IR/FX parametrization sizes");

    Measure::Type measure = Measure::LGM;
    if (config_->measure() == "BA") {
        measure = Measure::BA;
        DLOG("Setting measure to BA");
    } else if (config_->measure() == "LGM") {
        measure = Measure::LGM;
        DLOG("Setting measure to BA");
    } else if (config_->measure() == "") {
        DLOG("Defaulting to LGM measure");
    } else {
        QL_FAIL("Measure " << config_->measure() << " not recognized");
    }

    // Tag on the parametrization and process info for the auxiliary state variable in the bank account measure
    if (measure == Measure::BA) {
        parametrizations.push_back(
            irParametrizations[0]); // FIXME: Is index 0 safe to reference the domestic IR parameters?
        processInfo[CT::AUX].emplace_back(config_->domesticCurrency(), 1);
    }

    /******************************
     * Build the correlation matrix
     */
    DLOG("CrossAssetModelBuilder: adding correlations.");
    CorrelationMatrixBuilder cmb;

    // Perfect instantaneous correlation of auxiliary variable and domestic LGM state variable in the bank account
    // measure
    CorrelationFactor domesticFactorIR = {CrossAssetModelTypes::IR, config_->domesticCurrency(), 0};
    CorrelationFactor domesticFactorAUX = {CrossAssetModelTypes::AUX, config_->domesticCurrency(), 0};
    if (measure == Measure::BA)
        cmb.addCorrelation(domesticFactorAUX, domesticFactorIR, 1.0);

    for (auto it = config_->correlations().begin(); it != config_->correlations().end(); it++) {
        cmb.addCorrelation(it->first.first, it->first.second, it->second);
        if (measure == Measure::BA) {
            // Copy correlation(domesticIR, other) to correlation(domesticAUX, other)
            if (it->first.first == domesticFactorIR)
                cmb.addCorrelation(domesticFactorAUX, it->first.second, it->second);
            if (it->first.second == domesticFactorIR)
                cmb.addCorrelation(it->first.first, domesticFactorAUX, it->second);
        }
    }

    Matrix corrMatrix = cmb.correlationMatrix(processInfo);

    TLOG("CAM correlation matrix:");
    TLOGGERSTREAM << corrMatrix;

    /*****************************
     * Build the cross asset model
     */

    SalvagingAlgorithm::Type salvaging = SalvagingAlgorithm::None;

    model_.linkTo(boost::make_shared<QuantExt::CrossAssetModel>(parametrizations, corrMatrix, salvaging, measure));

    /*************************
     * Calibrate IR components
     */

    for (Size i = 0; i < irBuilder.size(); i++) {
        DLOG("IR Calibration " << i);
        swaptionCalibrationErrors_[i] = irBuilder[i]->error();
    }

    /*************************
     * Relink LGM discount curves to curves used for FX calibration
     */

    for (Size i = 0; i < irParametrizations.size(); i++) {
        auto p = irParametrizations[i];
        irDiscountCurves[i].linkTo(*market_->discountCurve(p->currency().code(), configurationFxCalibration_));
        DLOG("Relinked discounting curve for " << p->currency().code() << " for FX calibration");
    }

    /*************************
     * Calibrate FX components
     */

    for (Size i = 0; i < fxParametrizations.size(); i++) {
        boost::shared_ptr<FxBsData> fx = config_->fxConfigs()[i];

        if (fx->calibrationType() == CalibrationType::None || !fx->calibrateSigma()) {
            DLOG("FX Calibration " << i << " skipped");
            continue;
        }

        DLOG("FX Calibration " << i);

        // attach pricing engines to helpers
        boost::shared_ptr<QuantExt::AnalyticCcLgmFxOptionEngine> engine =
            boost::make_shared<QuantExt::AnalyticCcLgmFxOptionEngine>(*model_, i);
        // enable caching for calibration
        // TODO: review this
        engine->cache(true);
        for (Size j = 0; j < fxOptionBaskets_[i].size(); j++)
            fxOptionBaskets_[i][j]->setPricingEngine(engine);

        if (!dontCalibrate_) {

            if (fx->calibrationType() == CalibrationType::Bootstrap && fx->sigmaParamType() == ParamType::Piecewise)
                model_->calibrateBsVolatilitiesIterative(CrossAssetModelTypes::FX, i, fxOptionBaskets_[i],
                                                         *optimizationMethod_, endCriteria_);
            else
                model_->calibrateBsVolatilitiesGlobal(CrossAssetModelTypes::FX, i, fxOptionBaskets_[i],
                                                      *optimizationMethod_, endCriteria_);

            DLOG("FX " << fx->foreignCcy() << " calibration errors:");
            fxOptionCalibrationErrors_[i] = getCalibrationError(fxOptionBaskets_[i]);
            if (fx->calibrationType() == CalibrationType::Bootstrap) {
                if (fabs(fxOptionCalibrationErrors_[i]) < config_->bootstrapTolerance()) {
                    // we check the log level here to avoid unncessary computations
                    if (Log::instance().filter(ORE_DATA)) {
                        TLOGGERSTREAM << "Calibration details:";
                        TLOGGERSTREAM << getCalibrationDetails(fxOptionBaskets_[i], fxParametrizations[i],
                                                               irParametrizations[0]);
                        TLOGGERSTREAM << "rmse = " << fxOptionCalibrationErrors_[i];
                    }
                } else {
                    std::string exceptionMessage = "FX BS " + std::to_string(i) + " calibration error " +
                                                   std::to_string(fxOptionCalibrationErrors_[i]) +
                                                   " exceeds tolerance " +
                                                   std::to_string(config_->bootstrapTolerance());
                    WLOG(StructuredModelErrorMessage("Failed to calibrate FX BS Model", exceptionMessage));
                    WLOGGERSTREAM << "Calibration details:";
                    WLOGGERSTREAM << getCalibrationDetails(fxOptionBaskets_[i], fxParametrizations[i],
                                                           irParametrizations[0]);
                    WLOGGERSTREAM << "rmse = " << fxOptionCalibrationErrors_[i];
                    if (!continueOnError_)
                        QL_FAIL(exceptionMessage);
                }
            }
        }
    }

    /*************************
     * Relink LGM discount curves to curves used for EQ calibration
     */

    for (Size i = 0; i < irParametrizations.size(); i++) {
        auto p = irParametrizations[i];
        irDiscountCurves[i].linkTo(*market_->discountCurve(p->currency().code(), configurationEqCalibration_));
        DLOG("Relinked discounting curve for " << p->currency().code() << " for EQ calibration");
    }

    /*************************
     * Calibrate EQ components
     */

    for (Size i = 0; i < eqParametrizations.size(); i++) {
        boost::shared_ptr<EqBsData> eq = config_->eqConfigs()[i];
        if (!eq->calibrateSigma()) {
            DLOG("EQ Calibration " << i << " skipped");
            continue;
        }
        DLOG("EQ Calibration " << i);
        // attach pricing engines to helpers
        Currency eqCcy = eqParametrizations[i]->currency();
        Size eqCcyIdx = model_->ccyIndex(eqCcy);
        boost::shared_ptr<QuantExt::AnalyticXAssetLgmEquityOptionEngine> engine =
            boost::make_shared<QuantExt::AnalyticXAssetLgmEquityOptionEngine>(*model_, i, eqCcyIdx);
        for (Size j = 0; j < eqOptionBaskets_[i].size(); j++)
            eqOptionBaskets_[i][j]->setPricingEngine(engine);

        if (!dontCalibrate_) {

            if (eq->calibrationType() == CalibrationType::Bootstrap && eq->sigmaParamType() == ParamType::Piecewise)
                model_->calibrateBsVolatilitiesIterative(CrossAssetModelTypes::EQ, i, eqOptionBaskets_[i],
                                                         *optimizationMethod_, endCriteria_);
            else
                model_->calibrateBsVolatilitiesGlobal(CrossAssetModelTypes::EQ, i, eqOptionBaskets_[i],
                                                      *optimizationMethod_, endCriteria_);
            DLOG("EQ " << eq->eqName() << " calibration errors:");
            eqOptionCalibrationErrors_[i] = getCalibrationError(eqOptionBaskets_[i]);
            if (eq->calibrationType() == CalibrationType::Bootstrap) {
                if (fabs(eqOptionCalibrationErrors_[i]) < config_->bootstrapTolerance()) {
                    // we check the log level here to avoid unncessary computations
                    if (Log::instance().filter(ORE_DATA)) {
                        TLOGGERSTREAM << "Calibration details:";
                        TLOGGERSTREAM << getCalibrationDetails(eqOptionBaskets_[i], eqParametrizations[i],
                                                               irParametrizations[0]);
                        TLOGGERSTREAM << "rmse = " << eqOptionCalibrationErrors_[i];
                    }
                } else {
                    std::string exceptionMessage = "EQ BS " + std::to_string(i) + " calibration error " +
                                                   std::to_string(eqOptionCalibrationErrors_[i]) +
                                                   " exceeds tolerance " +
                                                   std::to_string(config_->bootstrapTolerance());
                    WLOG(StructuredModelErrorMessage("Failed to calibrate EQ BS Model", exceptionMessage));
                    WLOGGERSTREAM << "Calibration details:";
                    WLOGGERSTREAM << getCalibrationDetails(eqOptionBaskets_[i], eqParametrizations[i],
                                                           irParametrizations[0]);
                    WLOGGERSTREAM << "rmse = " << eqOptionCalibrationErrors_[i];
                    if (!continueOnError_)
                        QL_FAIL(exceptionMessage);
                }
            }
        }
    }

    /*************************
     * Relink LGM discount curves to curves used for INF calibration
     */

    for (Size i = 0; i < irParametrizations.size(); i++) {
        auto p = irParametrizations[i];
        irDiscountCurves[i].linkTo(*market_->discountCurve(p->currency().code(), configurationInfCalibration_));
        DLOG("Relinked discounting curve for " << p->currency().code() << " for INF calibration");
    }

    // Calibrate INF components
    for (Size i = 0; i < infParameterizations.size(); i++) {
        boost::shared_ptr<InflationModelData> imData = config_->infConfigs()[i];
        if (auto dkData = boost::dynamic_pointer_cast<InfDkData>(imData)) {
            auto dkParam = boost::dynamic_pointer_cast<InfDkParametrization>(infParameterizations[i]);
            QL_REQUIRE(dkParam, "Expected DK model data to have given a DK parameterisation.");
            const auto& builder = subBuilders_.at(CT::INF).at(i);
            const auto& dkBuilder = boost::dynamic_pointer_cast<InfDkBuilder>(builder);
            calibrateInflation(*dkData, i, dkBuilder->optionBasket(), dkParam);
        } else if (auto jyData = boost::dynamic_pointer_cast<InfJyData>(imData)) {
            auto jyParam = boost::dynamic_pointer_cast<InfJyParameterization>(infParameterizations[i]);
            QL_REQUIRE(jyParam, "Expected JY model data to have given a JY parameterisation.");
            const auto& builder = subBuilders_.at(CT::INF).at(i);
            const auto& jyBuilder = boost::dynamic_pointer_cast<InfJyBuilder>(builder);
            calibrateInflation(*jyData, i, jyBuilder, jyParam);
        } else {
            QL_FAIL("CrossAssetModelBuilder expects either DK or JY inflation model data.");
        }
    }

    /*************************
     * Relink LGM discount curves to final model curves
     */

    for (Size i = 0; i < irParametrizations.size(); i++) {
        auto p = irParametrizations[i];
        irDiscountCurves[i].linkTo(*market_->discountCurve(p->currency().code(), configurationFinalModel_));
        DLOG("Relinked discounting curve for " << p->currency().code() << " as final model curves");
    }

    // play safe (although the cache of the model should be empty at
    // this point from all what we know...)
    model_->update();

    DLOG("Building CrossAssetModel done");
}

void CrossAssetModelBuilder::forceRecalculate() {
    forceCalibration_ = true;
    ModelBuilder::forceRecalculate();
    forceCalibration_ = false;
}

void CrossAssetModelBuilder::calibrateInflation(const InfDkData& data, Size modelIdx,
                                                const vector<boost::shared_ptr<BlackCalibrationHelper>>& cb,
                                                const boost::shared_ptr<InfDkParametrization>& inflationParam) const {

    LOG("Calibrate DK inflation model for inflation index " << data.index());

    if ((!data.volatility().calibrate() && !data.reversion().calibrate()) ||
        (data.calibrationType() == CalibrationType::None)) {
        LOG("Calibration of DK inflation model for inflation index " << data.index() << " not requested.");
        return;
    }

    Handle<ZeroInflationIndex> zInfIndex =
        market_->zeroInflationIndex(model_->infdk(modelIdx)->name(), configurationInfCalibration_);
    Real baseCPI = dontCalibrate_ ? 100. : zInfIndex->fixing(zInfIndex->zeroInflationTermStructure()->baseDate());
    auto engine = boost::make_shared<QuantExt::AnalyticDkCpiCapFloorEngine>(*model_, modelIdx, baseCPI);
    for (Size j = 0; j < cb.size(); j++)
        cb[j]->setPricingEngine(engine);

    if (dontCalibrate_)
        return;

    if (data.volatility().calibrate() && !data.reversion().calibrate()) {
        if (data.calibrationType() == CalibrationType::Bootstrap && data.volatility().type() == ParamType::Piecewise) {
            model_->calibrateInfDkVolatilitiesIterative(modelIdx, cb, *optimizationMethod_, endCriteria_);
        } else {
            model_->calibrateInfDkVolatilitiesGlobal(modelIdx, cb, *optimizationMethod_, endCriteria_);
        }
    } else if (!data.volatility().calibrate() && data.reversion().calibrate()) {
        if (data.calibrationType() == CalibrationType::Bootstrap && data.reversion().type() == ParamType::Piecewise) {
            model_->calibrateInfDkReversionsIterative(modelIdx, cb, *optimizationMethod_, endCriteria_);
        } else {
            model_->calibrateInfDkReversionsGlobal(modelIdx, cb, *optimizationMethod_, endCriteria_);
        }
    } else {
        model_->calibrate(cb, *optimizationMethod_, endCriteria_);
    }

    DLOG("INF (DK) " << data.index() << " calibration errors:");
    inflationCalibrationErrors_[modelIdx] = getCalibrationError(cb);
    if (data.calibrationType() == CalibrationType::Bootstrap) {
        if (fabs(inflationCalibrationErrors_[modelIdx]) < config_->bootstrapTolerance()) {
            // we check the log level here to avoid unncessary computations
            if (Log::instance().filter(ORE_DATA)) {
                TLOGGERSTREAM << "Calibration details:";
                TLOGGERSTREAM << getCalibrationDetails(cb, inflationParam);
                TLOGGERSTREAM << "rmse = " << inflationCalibrationErrors_[modelIdx];
            }
        } else {
            string exceptionMessage = "INF (DK) " + std::to_string(modelIdx) + " calibration error " +
                                      std::to_string(inflationCalibrationErrors_[modelIdx]) + " exceeds tolerance " +
                                      std::to_string(config_->bootstrapTolerance());
            WLOG(StructuredModelErrorMessage("Failed to calibrate INF DK Model", exceptionMessage));
            WLOGGERSTREAM << "Calibration details:";
            WLOGGERSTREAM << getCalibrationDetails(cb, inflationParam);
            WLOGGERSTREAM << "rmse = " << inflationCalibrationErrors_[modelIdx];
            if (!continueOnError_)
                QL_FAIL(exceptionMessage);
        }
    }
}

void CrossAssetModelBuilder::calibrateInflation(const InfJyData& data, Size modelIdx,
                                                const boost::shared_ptr<InfJyBuilder>& jyBuilder,
                                                const boost::shared_ptr<InfJyParameterization>& inflationParam) const {

    LOG("Calibrate JY inflation model for inflation index " << data.index());

    const auto& rrVol = data.realRateVolatility();
    const auto& rrRev = data.realRateReversion();
    const auto& idxVol = data.indexVolatility();

    // Check if calibration is needed at all.
    if ((!rrVol.calibrate() && !rrRev.calibrate() && !idxVol.calibrate()) ||
        (data.calibrationType() == CalibrationType::None)) {
        LOG("Calibration of JY inflation model for inflation index " << data.index() << " not requested.");
        return;
    }

    // We will need the 2 baskets of helpers
    auto rrBasket = jyBuilder->realRateBasket();
    auto idxBasket = jyBuilder->indexBasket();

    // Attach engines to the helpers.
    setJyPricingEngine(modelIdx, rrBasket);
    setJyPricingEngine(modelIdx, idxBasket);

    if (dontCalibrate_)
        return;

    // Single basket of helpers is useful in various places below.
    vector<boost::shared_ptr<CalibrationHelper>> allHelpers = rrBasket;
    allHelpers.insert(allHelpers.end(), idxBasket.begin(), idxBasket.end());

    // Calibration configuration.
    const auto& cc = data.calibrationConfiguration();

    if (data.calibrationType() == CalibrationType::BestFit) {

        // If calibration type is BestFit, do a global optimisation on the parameters that need to be calibrated.
        DLOG("Calibration BestFit of JY inflation model for inflation index " << data.index() << " requested.");

        // Indicate the parameters to calibrate
        map<Size, bool> toCalibrate;
        toCalibrate[0] = rrVol.calibrate();
        toCalibrate[1] = rrRev.calibrate();
        toCalibrate[2] = idxVol.calibrate();

        // Calibrate the model.
        model_->calibrateInfJyGlobal(modelIdx, allHelpers, *optimizationMethod_, endCriteria_, toCalibrate);

    } else {

        // Calibration type is now Bootstrap, there are multiple options.
        QL_REQUIRE(data.calibrationType() == CalibrationType::Bootstrap,
                   "JY inflation calibration expected a "
                       << "calibration type of None, BestFit or Bootstrap.");
        QL_REQUIRE(!(rrRev.calibrate() && rrVol.calibrate()),
                   "Calibrating both the "
                       << "real rate reversion and real rate volatility using Bootstrap is not supported.");

        if ((!rrVol.calibrate() && !rrRev.calibrate()) && idxVol.calibrate()) {

            // Bootstrap the inflation index volatility only.
            DLOG("Bootstrap calibration of JY index volatility for index " << data.index() << ".");
            QL_REQUIRE(idxVol.type() == ParamType::Piecewise, "Index volatility parameter should be Piecewise for "
                                                                  << "a Bootstrap calibration.");
            model_->calibrateInfJyIterative(modelIdx, 2, idxBasket, *optimizationMethod_, endCriteria_);

        } else if (rrVol.calibrate() && !idxVol.calibrate()) {

            // Bootstrap the real rate volatility only
            DLOG("Bootstrap calibration of JY real rate volatility for index " << data.index() << ".");
            QL_REQUIRE(rrVol.type() == ParamType::Piecewise, "Real rate volatility parameter should be "
                                                                 << "Piecewise for a Bootstrap calibration.");
            model_->calibrateInfJyIterative(modelIdx, 0, rrBasket, *optimizationMethod_, endCriteria_);

        } else if (rrRev.calibrate() && !idxVol.calibrate()) {

            // Bootstrap the real rate reversion only
            DLOG("Bootstrap calibration of JY real rate reversion for index " << data.index() << ".");
            QL_REQUIRE(rrRev.type() == ParamType::Piecewise, "Real rate reversion parameter should be "
                                                                 << "Piecewise for a Bootstrap calibration.");
            model_->calibrateInfJyIterative(modelIdx, 1, rrBasket, *optimizationMethod_, endCriteria_);

        } else if ((rrVol.calibrate() && idxVol.calibrate()) || (rrRev.calibrate() && idxVol.calibrate())) {

            if (rrVol.calibrate()) {
                DLOG("Bootstrap calibration of JY real rate volatility and index volatility for index " << data.index()
                                                                                << ".");
            } else {
                DLOG("Bootstrap calibration of JY real rate reversion and index volatility for index " << data.index()
                                                                                << ".");
            }

            // Bootstrap the real rate volatility and the index volatility
            Size rrIdx = rrVol.calibrate() ? 0 : 1;
            Size numIts = 0;
            inflationCalibrationErrors_[modelIdx] = getCalibrationError(allHelpers);

            while (inflationCalibrationErrors_[modelIdx] > cc.rmseTolerance() && numIts < cc.maxIterations()) {
                model_->calibrateInfJyIterative(modelIdx, 2, idxBasket, *optimizationMethod_, endCriteria_);
                model_->calibrateInfJyIterative(modelIdx, rrIdx, rrBasket, *optimizationMethod_, endCriteria_);
                numIts++;
                inflationCalibrationErrors_[modelIdx] = getCalibrationError(allHelpers);
            }

            DLOG("Bootstrap calibration of JY model stopped with number of iterations "
                 << numIts << " and rmse equal to " << std::scientific << std::setprecision(6)
                 << inflationCalibrationErrors_[modelIdx] << ".");

        } else {
            QL_FAIL("JY inflation bootstrap calibration does not support the combination of real rate volatility = "
                    << std::boolalpha << rrVol.calibrate() << ", real rate reversion = " << rrRev.calibrate() << " and "
                    << "index volatility = " << idxVol.calibrate() << ".");
        }
    }

    // Log the calibration details.
    DLOG("INF (JY) " << data.index() << " calibration errors:");
    inflationCalibrationErrors_[modelIdx] = getCalibrationError(allHelpers);
    if (data.calibrationType() == CalibrationType::Bootstrap) {
        if (fabs(inflationCalibrationErrors_[modelIdx]) < cc.rmseTolerance()) {
            // we check the log level here to avoid unncessary computations
            if (Log::instance().filter(ORE_DATA)) {
                TLOGGERSTREAM << "Calibration details:";
                TLOGGERSTREAM << getCalibrationDetails(rrBasket, idxBasket, inflationParam, rrVol.calibrate());
                TLOGGERSTREAM << "rmse = " << inflationCalibrationErrors_[modelIdx];
            }
        } else {
            std::stringstream ss;
            ss << "INF (JY) " << modelIdx << " calibration error " << std::scientific
               << inflationCalibrationErrors_[modelIdx] << " exceeds tolerance " << cc.rmseTolerance();
            string exceptionMessage = ss.str();
            WLOG(StructuredModelErrorMessage("Failed to calibrate INF JY Model", exceptionMessage));
            WLOGGERSTREAM << "Calibration details:";
            WLOGGERSTREAM << getCalibrationDetails(rrBasket, idxBasket, inflationParam, rrVol.calibrate());
            WLOGGERSTREAM << "rmse = " << inflationCalibrationErrors_[modelIdx];
            if (!continueOnError_)
                QL_FAIL(exceptionMessage);
        }
    }

    LOG("Finished calibrating JY inflation model for inflation index " << data.index());
}

void CrossAssetModelBuilder::setJyPricingEngine(
    Size modelIdx, const vector<boost::shared_ptr<CalibrationHelper>>& calibrationBasket) const {

    DLOG("Start setting pricing engines on JY calibration instruments.");

    // JY supports three types of calibration helpers. Generally, all of the calibration instruments in a basket will
    // be of the same type but we support all three here.
    boost::shared_ptr<PricingEngine> cpiCapFloorEngine;
    boost::shared_ptr<PricingEngine> yoyCapFloorEngine;
    boost::shared_ptr<PricingEngine> yoySwapEngine;
    boost::shared_ptr<InflationCouponPricer> yoyCouponPricer;

    for (auto& ci : calibrationBasket) {

        if (boost::shared_ptr<CpiCapFloorHelper> h = boost::dynamic_pointer_cast<CpiCapFloorHelper>(ci)) {
            if (!cpiCapFloorEngine) {
                cpiCapFloorEngine = boost::make_shared<AnalyticJyCpiCapFloorEngine>(*model_, modelIdx);
            }
            h->setPricingEngine(cpiCapFloorEngine);
            continue;
        }

        if (boost::shared_ptr<YoYCapFloorHelper> h = boost::dynamic_pointer_cast<YoYCapFloorHelper>(ci)) {
            if (!yoyCapFloorEngine) {
                yoyCapFloorEngine = boost::make_shared<AnalyticJyYoYCapFloorEngine>(*model_, modelIdx);
            }
            h->setPricingEngine(yoyCapFloorEngine);
            continue;
        }

        if (boost::shared_ptr<YoYSwapHelper> h = boost::dynamic_pointer_cast<YoYSwapHelper>(ci)) {
            // Here we need to attach the coupon pricer to all the YoY coupons and then the generic discounting swap
            // engine to the helper.
            if (!yoyCouponPricer) {
                yoyCouponPricer = boost::make_shared<JyYoYInflationCouponPricer>(*model_, modelIdx);

                Size irIdx = model_->ccyIndex(model_->infjy(modelIdx)->currency());
                auto yts = model_->irlgm1f(irIdx)->termStructure();
                yoySwapEngine = boost::make_shared<DiscountingSwapEngine>(yts);
            }

            const auto& yoyLeg = h->yoySwap()->yoyLeg();
            for (const auto& cf : yoyLeg) {
                if (auto yoyCoupon = boost::dynamic_pointer_cast<YoYInflationCoupon>(cf))
                    yoyCoupon->setPricer(yoyCouponPricer);
            }

            h->setPricingEngine(yoySwapEngine);
            continue;
        }

        QL_FAIL("Only CPI cap floors, YoY cap floors and YoY swaps are supported for JY calibration.");
    }

    DLOG("Finished setting pricing engines on JY calibration instruments.");
}

} // namespace data
} // namespace ore


//---------------------------------------------------------------------------//
// Copyright (c) 2020 Mikhail Komarov <nemo@nil.foundation>
// Copyright (c) 2019 Alexey Moskvin
// Copyright (c) 2020 Ilias Khairullin <ilias@nil.foundation>
//
// 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 BOOST_MULTIPRECISION_MODULAR_ADAPTOR_HPP
#define BOOST_MULTIPRECISION_MODULAR_ADAPTOR_HPP

#include <boost/cstdint.hpp>
#include <boost/functional/hash_fwd.hpp>

#include <nil/crypto3/multiprecision/detail/digits.hpp>
#include <nil/crypto3/multiprecision/number.hpp>

#include <nil/crypto3/multiprecision/modular/modular_params.hpp>
#include <nil/crypto3/multiprecision/modular/modular_adaptor_fixed.hpp>

#include <boost/container/small_vector.hpp>

#include <algorithm>
#include <cmath>
#include <vector>

namespace nil {
    namespace crypto3 {
        namespace multiprecision {
            namespace backends {

                template<typename Backend>
                struct modular_adaptor {
                    typedef modular_params<Backend> modulus_type;
                    typedef Backend backend_type;

                protected:
                    Backend m_base;
                    modulus_type m_mod;

                public:
                    inline Backend& base_data() {
                        return m_base;
                    }
                    inline const Backend& base_data() const {
                        return m_base;
                    }
                    inline modulus_type& mod_data() {
                        return m_mod;
                    }
                    inline const modulus_type& mod_data() const {
                        return m_mod;
                    }

                    typedef typename Backend::signed_types signed_types;
                    typedef typename Backend::unsigned_types unsigned_types;

                    modular_adaptor() {
                    }

                    modular_adaptor(const modular_adaptor& o) : m_base(o.base_data()), m_mod(o.mod_data()) {
                    }

#ifndef BOOST_NO_CXX11_RVALUE_REFERENCES

                    modular_adaptor(modular_adaptor&& o) :
                        m_base(std::move(o.base_data())), m_mod(std::move(o.mod_data())) {
                    }

#endif
                    // TODO: implement create_internal_representation
                    // modular_adaptor(const Backend& val, const modular_params<Backend>& mod) : m_mod(mod),
                    // m_base(mod.create_internal_representation(val))
                    // {
                    //
                    // }

                    modular_adaptor(const Backend& val, const Backend& mod) : m_base(val), m_mod(mod) {
                    }

                    modular_adaptor(Backend& val, Backend& mod) : m_base(val), m_mod(mod) {
                    }

                    // TODO: maybe initialize modulus rather than base
                    modular_adaptor(const Backend& val) :
                        m_base(val), m_mod(typename boost::mpl::front<unsigned_types>::type(0u)) {
                    }

                    modular_adaptor(const modular_params<Backend>& mod) :
                        m_base(typename boost::mpl::front<unsigned_types>::type(0u)), m_mod(mod) {
                    }

                    modular_adaptor& operator=(const modular_adaptor& o) {
                        m_base = o.base_data();
                        m_mod = o.mod_data();
                        return *this;
                    }

#ifndef BOOST_NO_CXX11_RVALUE_REFERENCES

                    modular_adaptor& operator=(modular_adaptor&& o)

                        BOOST_NOEXCEPT {
                        m_base = std::move(o.base_data());
                        m_mod = std::move(o.mod_data());
                        return *this;
                    }
#endif

                    modular_adaptor& operator=(const char* s) {
                        typedef typename boost::mpl::front<unsigned_types>::type ui_type;
                        ui_type zero = 0u;

                        using default_ops::eval_fpclassify;

                        if (s && (*s == '(')) {
                            std::string part;
                            const char* p = ++s;
                            while (*p && (*p != ',') && (*p != ')'))
                                ++p;
                            part.assign(s, p);
                            if (!part.empty())
                                m_base() = part.c_str();
                            else
                                m_base() = zero;
                            s = p;
                            if (*p && (*p != ')')) {
                                ++p;
                                while (*p && (*p != ')'))
                                    ++p;
                                part.assign(s + 1, p);
                            } else
                                part.erase();
                            if (!part.empty())
                                m_mod() = part.c_str();
                            else
                                m_mod() = zero;
                        } else {
                            base_data() = s;
                            m_mod() = zero;
                        }
                        return *this;
                    }

                    // TODO: maybe change behaviour to check a congruence relation
                    int compare(const modular_adaptor& o) const {
                        // They are either equal or not:<
                        if (m_mod.compare(o.mod_data()) != 0) {
                            BOOST_THROW_EXCEPTION(
                                std::runtime_error("Could not compare modular number with different mod."));
                        }
                        Backend tmp1, tmp2;
                        mod_data().adjust_regular(tmp1, base_data());
                        mod_data().adjust_regular(tmp2, o.base_data());
                        return tmp1.compare(tmp2);
                    }

                    // TODO: maybe change behaviour to check a congruence relation
                    template<class T>
                    int compare(const T& val) const {
                        using default_ops::eval_lt;
                        if (!eval_lt(m_mod, val)) {
                            BOOST_THROW_EXCEPTION(std::runtime_error(
                                "Could not compare modular number with mod bigger than compared number."));
                        }
                        Backend tmp;
                        mod_data().adjust_regular(tmp, base_data());
                        return tmp.compare(val);
                    }

                    inline void swap(modular_adaptor& o) {
                        base_data().swap(o.base_data());
                        std::swap(mod_data(), o.mod_data());
                    }

                    inline std::string str(std::streamsize dig, std::ios_base::fmtflags f) const {
                        Backend tmp;
                        mod_data().adjust_regular(tmp, base_data());
                        return tmp.str(dig, f);
                    }

                    inline void negate() {
                        base_data().negate();
                        eval_add(base_data(), mod_data().get_mod().backend());
                    }

                    template<typename BackendT, expression_template_option ExpressionTemplates>
                    operator number<BackendT, ExpressionTemplates>() {
                        return base_data();
                    };
                };

                template<class Result, class Backend>
                constexpr void eval_convert_to(Result* result, const modular_adaptor<Backend>& val) {
                    using default_ops::eval_convert_to;
                    eval_convert_to(result, val.base_data());
                }

                template<class Backend, class T>
                constexpr typename boost::enable_if<boost::is_arithmetic<T>, bool>::type eval_eq(const modular_adaptor<Backend>& a,
                                                                                const T& b) {
                    return a.compare(b) == 0;
                }

                template<class Backend1, class Backend2>
                constexpr void eval_redc(Backend1& result, const modular_params<Backend2>& mod) {
                    mod.reduce(result);
                    eval_modulus(result, mod.get_mod().backend());
                }

                template<class Backend>
                constexpr void eval_add(modular_adaptor<Backend>& result, const modular_adaptor<Backend>& o) {
                    BOOST_ASSERT(result.mod_data().get_mod() == o.mod_data().get_mod());
                    using default_ops::eval_lt;

                    eval_add(result.base_data(), o.base_data());
                    if (!eval_lt(result.base_data(), result.mod_data().get_mod())) {
                        eval_subtract(result.base_data(), result.mod_data().get_mod().backend());
                    }
                }

                template<class Backend>
                constexpr void eval_subtract(modular_adaptor<Backend>& result, const modular_adaptor<Backend>& o) {
                    BOOST_ASSERT(result.mod_data().get_mod() == o.mod_data().get_mod());
                    typedef typename boost::mpl::front<typename Backend::unsigned_types>::type ui_type;
                    using default_ops::eval_lt;
                    eval_subtract(result.base_data(), o.base_data());
                    if (eval_lt(result.base_data(), ui_type(0u))) {
                        eval_add(result.base_data(), result.mod_data().get_mod().backend());
                    }
                }

                template<class Backend>
                constexpr void eval_multiply(modular_adaptor<Backend>& result, const modular_adaptor<Backend>& o) {
                    BOOST_ASSERT(result.mod_data().get_mod() == o.mod_data().get_mod());
                    eval_multiply(result.base_data(), o.base_data());
                    eval_redc(result.base_data(), result.mod_data());
                }

                template<class Backend>
                constexpr void eval_divide(modular_adaptor<Backend>& result, const modular_adaptor<Backend>& o) {
                    BOOST_ASSERT(result.mod_data().get_mod() == o.mod_data().get_mod());
                    Backend tmp1, tmp2;
                    result.mod_data().adjust_regular(tmp1, result.base_data());
                    result.mod_data().adjust_regular(tmp2, o.base_data());
                    eval_divide(tmp1, tmp2);
                    result.base_data() = tmp1;
                    result.mod_data().adjust_modular(result.base_data());
                    result.mod_data().adjust_regular(tmp2, result.base_data());
                }

                template<class Backend>
                constexpr void eval_modulus(modular_adaptor<Backend>& result, const modular_adaptor<Backend>& o) {
                    BOOST_ASSERT(result.mod_data().get_mod() == o.mod_data().get_mod());
                    Backend tmp1, tmp2;
                    result.mod_data().adjust_regular(tmp1, result.base_data());
                    result.mod_data().adjust_regular(tmp2, o.base_data());
                    eval_modulus(tmp1, tmp2);
                    result.base_data() = tmp1;
                    result.mod_data().adjust_modular(result.base_data());
                    result.mod_data().adjust_regular(tmp2, result.base_data());
                }

                template<class Backend>
                constexpr bool eval_is_zero(const modular_adaptor<Backend>& val)

                    BOOST_NOEXCEPT {
                    using default_ops::eval_is_zero;
                    return eval_is_zero(val.base_data());
                }

                // TODO: check returned value
                template<class Backend>
                constexpr int eval_get_sign(const modular_adaptor<Backend>&) {
                    return 1;
                }

                // TODO: is the function required
                // template <class Result, class Backend>
                // constexpr typename boost::disable_if_c<boost::is_complex<Result>::value>::type
                // eval_convert_to(Result* result, const modular_adaptor<Backend>& val)
                // {
                //    using default_ops::eval_convert_to;
                //    eval_convert_to(result, val.base_data());
                // }

                template<class Backend, class T, class V>
                constexpr void assign_components(modular_adaptor<Backend>& result, const T& a, const V& b) {
                    result.base_data() = a;
                    result.mod_data() = b;
                    result.mod_data().adjust_modular(result.base_data());
                }

                template<class Backend>
                constexpr void eval_sqrt(modular_adaptor<Backend>& result, const modular_adaptor<Backend>& val) {
                    eval_sqrt(result.base_data(), val.base_data());
                }

                template<class Backend>
                constexpr void eval_abs(modular_adaptor<Backend>& result, const modular_adaptor<Backend>& val) {
                    result = val;
                }

                size_t window_bits(size_t exp_bits) {
                    BOOST_STATIC_CONSTEXPR size_t wsize_count = 6;
                    BOOST_STATIC_CONSTEXPR size_t wsize[wsize_count][2] = {{1434, 7}, {539, 6}, {197, 4},
                                                                           {70, 3},   {17, 2},  {0, 0}};

                    size_t window_bits = 1;

                    size_t j = wsize_count - 1;
                    while (wsize[j][0] > exp_bits) {
                        --j;
                    }
                    window_bits += wsize[j][1];

                    return window_bits;
                };

                template<class Backend>
                inline void find_modular_pow(modular_adaptor<Backend>& result,
                                             const modular_adaptor<Backend>& b,
                                             const Backend& exp) {
                    using default_ops::eval_bit_set;
                    using default_ops::eval_convert_to;
                    using default_ops::eval_decrement;
                    using default_ops::eval_multiply;

                    typedef number<modular_adaptor<Backend>> modular_type;
                    modular_params<Backend> mod = b.mod_data();
                    size_t m_window_bits;
                    unsigned long cur_exp_index;
                    size_t exp_bits = eval_msb(exp);
                    m_window_bits = window_bits(exp_bits + 1);

                    std::vector<modular_type> m_g(1U << m_window_bits);
                    modular_type* p_g = m_g.data();
                    modular_type x(1, mod);
                    Backend nibble = exp;
                    Backend mask;
                    eval_bit_set(mask, m_window_bits);
                    eval_decrement(mask);
                    *p_g = x;
                    ++p_g;
                    *p_g = b;
                    ++p_g;
                    for (size_t i = 2; i < (1U << m_window_bits); i++) {
                        eval_multiply((*p_g).backend(), m_g[i - 1].backend(), b);
                        ++p_g;
                    }
                    size_t exp_nibbles = (exp_bits + 1 + m_window_bits - 1) / m_window_bits;
                    std::vector<size_t> exp_index;

                    for (size_t i = 0; i < exp_nibbles; ++i) {
                        Backend tmp = nibble;
                        eval_bitwise_and(tmp, mask);
                        eval_convert_to(&cur_exp_index, tmp);
                        eval_right_shift(nibble, m_window_bits);
                        exp_index.push_back(cur_exp_index);
                    }

                    x = x * m_g[exp_index[exp_nibbles - 1]];
                    for (size_t i = exp_nibbles - 1; i > 0; --i) {

                        for (size_t j = 0; j != m_window_bits; ++j) {
                            x = x * x;
                        }

                        x = x * m_g[exp_index[i - 1]];
                    }
                    result = x.backend();
                }

                template<class Backend, typename T>
                constexpr void eval_pow(modular_adaptor<Backend>& result, const modular_adaptor<Backend>& b,
                                        const T& e) {
                    find_modular_pow(result, b, e);
                }

                template<class Backend>
                constexpr void eval_pow(modular_adaptor<Backend>& result,
                                        const modular_adaptor<Backend>& b,
                                        const modular_adaptor<Backend>& e) {
                    Backend exp;
                    e.mod_data().adjust_regular(exp, e.base_data());
                    find_modular_pow(result, b, exp);
                }

                template<typename Backend1, typename Backend2, typename T>
                constexpr void eval_powm(modular_adaptor<Backend1>& result, const modular_adaptor<Backend2>& b,
                                         const T& e) {
                    eval_pow(result, b, e);
                }

                template<typename Backend1, typename Backend2, typename Backend3>
                constexpr void eval_powm(modular_adaptor<Backend1>& result,
                                         const modular_adaptor<Backend2>& b,
                                         const modular_adaptor<Backend3>& e) {
                    eval_pow(result, b, e);
                }

                template<class Backend, class UI>
                constexpr void eval_left_shift(modular_adaptor<Backend>& t, UI i) {
                    using default_ops::eval_left_shift;
                    Backend tmp;
                    t.mod_data().adjust_regular(tmp, t.base_data());
                    eval_left_shift(tmp, i);
                    t.base_data() = tmp;
                    t.mod_data().adjust_modular(t.base_data());
                }

                template<class Backend, class UI>
                constexpr void eval_right_shift(modular_adaptor<Backend>& t, UI i) {
                    using default_ops::eval_right_shift;
                    Backend tmp;
                    t.mod_data().adjust_regular(tmp, t.base_data());
                    eval_right_shift(tmp, i);
                    t.base_data() = tmp;
                    t.mod_data().adjust_modular(t.base_data());
                }

                template<class Backend, class UI>
                constexpr void eval_left_shift(modular_adaptor<Backend>& t, const modular_adaptor<Backend>& v, UI i) {
                    using default_ops::eval_left_shift;
                    Backend tmp1, tmp2;
                    t.mod_data().adjust_regular(tmp1, t.base_data());
                    t.mod_data().adjust_regular(tmp2, v.base_data());
                    eval_left_shift(tmp1, tmp2, static_cast<unsigned long>(i));
                    t.base_data() = tmp1;
                    t.mod_data().adjust_modular(t.base_data());
                }

                template<class Backend, class UI>
                constexpr void eval_right_shift(modular_adaptor<Backend>& t, const modular_adaptor<Backend>& v, UI i) {
                    using default_ops::eval_right_shift;
                    Backend tmp1, tmp2;
                    t.mod_data().adjust_regular(tmp1, t.base_data());
                    t.mod_data().adjust_regular(tmp2, v.base_data());
                    eval_right_shift(tmp1, tmp2, static_cast<unsigned long>(i));
                    t.base_data() = tmp1;
                    t.mod_data().adjust_modular(t.base_data());
                }

                template<class Backend>
                constexpr void eval_bitwise_and(modular_adaptor<Backend>& result, const modular_adaptor<Backend>& v) {
                    using default_ops::eval_bitwise_and;
                    BOOST_ASSERT(result.mod_data().get_mod() == v.mod_data().get_mod());

                    Backend tmp1, tmp2;
                    result.mod_data().adjust_regular(tmp1, result.base_data());
                    v.mod_data().adjust_regular(tmp2, v.base_data());
                    eval_bitwise_and(tmp1, tmp1, tmp2);
                    result.base_data() = tmp1;
                    result.mod_data().adjust_modular(result.base_data());
                }

                template<class Backend>
                constexpr void eval_bitwise_or(modular_adaptor<Backend>& result, const modular_adaptor<Backend>& v) {
                    using default_ops::eval_bitwise_or;
                    BOOST_ASSERT(result.mod_data().get_mod() == v.mod_data().get_mod());

                    Backend tmp1, tmp2;
                    result.mod_data().adjust_regular(tmp1, result.base_data());
                    v.mod_data().adjust_regular(tmp2, v.base_data());
                    eval_bitwise_or(tmp1, tmp1, tmp2);
                    result.base_data() = tmp1;
                    result.mod_data().adjust_modular(result.base_data());
                }

                template<class Backend>
                constexpr void eval_bitwise_xor(modular_adaptor<Backend>& result, const modular_adaptor<Backend>& v) {
                    using default_ops::eval_bitwise_xor;
                    BOOST_ASSERT(result.mod_data().get_mod() == v.mod_data().get_mod());

                    Backend tmp1, tmp2;
                    result.mod_data().adjust_regular(tmp1, result.base_data());
                    v.mod_data().adjust_regular(tmp2, v.base_data());
                    eval_bitwise_xor(tmp1, tmp1, tmp2);
                    result.base_data() = tmp1;
                    result.mod_data().adjust_modular(result.base_data());
                }

                template<typename Backend>
                constexpr int eval_msb(const modular_adaptor<Backend>& m) {
                    using default_ops::eval_msb;
                    Backend tmp;
                    m.mod_data().adjust_regular(tmp, m.base_data());
                    return eval_msb(tmp);
                }

                template<typename Backend>
                constexpr unsigned eval_lsb(const modular_adaptor<Backend>& m) {
                    using default_ops::eval_lsb;
                    Backend tmp;
                    m.mod_data().adjust_regular(tmp, m.base_data());
                    return eval_lsb(tmp);
                }

                template<typename Backend>
                constexpr bool eval_bit_test(const modular_adaptor<Backend>& m, unsigned index) {
                    using default_ops::eval_bit_test;
                    Backend tmp;
                    m.mod_data().adjust_regular(tmp, m.base_data());
                    return eval_bit_test(tmp, index);
                }

                template<typename Backend>
                constexpr void eval_bit_set(modular_adaptor<Backend>& result, unsigned index) {
                    using default_ops::eval_bit_set;
                    Backend tmp;
                    result.mod_data().adjust_regular(tmp, result.base_data());
                    eval_bit_set(tmp, index);
                    result.mod_data().adjust_modular(result.base_data(), tmp);
                }

                template<typename Backend>
                constexpr void eval_bit_unset(modular_adaptor<Backend>& result, unsigned index) {
                    using default_ops::eval_bit_unset;
                    Backend tmp;
                    result.mod_data().adjust_regular(tmp, result.base_data());
                    eval_bit_unset(tmp, index);
                    result.mod_data().adjust_modular(result.base_data(), tmp);
                }

                template<typename Backend>
                constexpr void eval_bit_flip(modular_adaptor<Backend>& result, unsigned index) {
                    using default_ops::eval_bit_flip;
                    Backend tmp;
                    result.mod_data().adjust_regular(tmp, result.base_data());
                    eval_bit_flip(tmp, index);
                    result.mod_data().adjust_modular(result.base_data(), tmp);
                }

            }    // namespace backends

            using nil::crypto3::multiprecision::backends::modular_adaptor;

            template<class Backend>
            struct number_category<modular_adaptor<Backend>>
                : public boost::mpl::int_<nil::crypto3::multiprecision::number_kind_modular> { };

            template<class Backend, expression_template_option ExpressionTemplates>
            struct component_type<number<modular_adaptor<Backend>, ExpressionTemplates>> {
                typedef number<Backend, ExpressionTemplates> type;
            };

        }    // namespace multiprecision
    }        // namespace crypto3
}    // namespace nil

#endif