#include <string>
#include <vector>
#include <memory>
#include <map>
#include <iostream>
#include <iomanip>
#include <fstream>

#include <boost/filesystem.hpp>
#include <yaml-cpp/yaml.h>

#include "cantera/base/stringUtils.h"
#include "cantera/base/ct_defs.h"
#include "cantera/IdealGasMix.h"
#include "cantera/InterfaceLatInt.h"
#include "cantera/Interface.h"
#include "cantera/thermo/SurfLatIntPhase.h"
#include "cantera/thermo/SurfPhase.h"
#include "cantera/thermo/LateralInteraction.h"
#include "cantera/thermo/StoichSubstance.h"
#include "cantera/thermo/ThermoPhase.h"
#include "cantera/zeroD/Reactor.h"
#include "cantera/zeroD/Wall.h"
#include "IdealGasTRampReactor.h"
#include "cantera/zeroD/ReactorNet.h"
#include "cantera/zeroD/Reservoir.h"
#include "cantera/zeroD/ReactorSurface.h"
#include "cantera/zeroD/flowControllers.h"

#include "util.h"
#include "run_reactor.h"
#include "omkmexceptions.h"
#include "io.h"
#include "ReactorNetHybrid.h"

namespace OpenMKM
{

using namespace std;
using namespace Cantera;
namespace fs = boost::filesystem;

//TODO: Add nSurfaces() function to Reactor/ReactorBase to eliminate the need
// to pass surfaces argument
/*
void print_rctr_state(double z, Reactor* rctr, vector<SurfPhase*> surfaces,
                      ofstream& gas_mole_out, ofstream& gas_mass_out,
                      ofstream& gas_msdot_out, ofstream& surf_cov_out,
                      ofstream& state_var_out)
*/

void run_0d_reactor(ReactorParser& rctr_parser,
                    shared_ptr<IdealGasMix> gas,
                    vector<shared_ptr<InterfaceInteractions>> surfaces,
                    ofstream& gen_info_out)
{
    //Define the reactor based on the input file
    auto rctr = make_shared<IdealGasTRampReactor>();
    rctr->insert(*gas);
    gen_info_out << "Reactor density " << rctr->density() << endl;

    // Read the reactor dimensions
    double rctr_vol = rctr_parser.getVolume();
    auto rctr_type = rctr_parser.getReactorType();
    size_t rctr_nos = 1;
    if (rctr_type == PFR_0D) { // Modify rctr_nos only for PFR_0D
        rctr_nos = rctr_parser.getNodes();
        if (rctr_nos == 1) { // Raise warning to increase cstr number
            cout << "Number of nodes in 0d PFR simulation is 1. \n "
                 << "Suggestion: Input 'nodes' parameter if not given already or "
                 << "increase its value to greater than 1.";
        }
        rctr_vol /= rctr_nos;
    }

    rctr->setInitialVolume(rctr_vol);
    vector<shared_ptr<ReactorSurface>> cat_surfs;
    vector<SurfPhase*> surf_phases;

    double cat_abyv = rctr_parser.getCatalystAbyV();
    cout << "Catalyst loading (Area/Volume): " << cat_abyv << endl;

    if (cat_abyv == 0.0 && surfaces.size() > 0) {
        cout << "WARNING!!!\nCatalyst loading is zero.\n"
             << "Ignoring the surface phases given in the YAML file\n"
             << "--------------\n";
    }

    if (cat_abyv != 0.0) {
        auto cat_area = cat_abyv * rctr_vol;
        size_t surf_spec_no = 0;
        for (const auto surf : surfaces) {
            surf_spec_no += surf->nSpecies();
            auto cat_surf = make_shared<ReactorSurface>();
            cat_surf->setKinetics(surf.get());
            surf_phases.push_back(dynamic_cast<SurfPhase*> (surf.get()));
            vector<double> coverages(surf->nSpecies());
            surf->getCoverages(coverages.data());
            cat_surf->setCoverages(coverages.data());
            cat_surf->setArea(cat_area);
            cat_surfs.push_back(cat_surf);
            rctr->addSurface(cat_surf.get());
        }
    }
    cout << "# of surface phases considered: " << cat_surfs.size() << endl;

    // Only CSTR and PFR_0D require valves, controllers and
    // reservoirs to facilitate mass transfer. Attach them to reactor

    // The following are nominally defined in the case of BATCH reactor also
    // to prevent crashing the program
    auto in_rsrv = make_shared<Reservoir>();
    auto exhst = make_shared<Reservoir>();
    auto inlet_mfc = make_shared<MassFlowController>();
    auto outlet = make_shared<PressureController>();

    // Set the output type
    OutputFormat data_format = rctr_parser.printFormat();
    setOutputFormat(data_format);

    if (rctr_type != BATCH) {
        bool fr_defined = rctr_parser.FlowRateDefined();
        bool mfr_defined = rctr_parser.MassFlowRateDefined();
        bool rt_defined = rctr_parser.ResidenceTimeDefined();
        int no_var_defined = fr_defined + mfr_defined + rt_defined;
        if (no_var_defined > 1) {
            cout << "WARNING: Define only one of 'flow_rate', 'mass_flow_rate', "
                 << "'residence_time'. Only one of the variables is used "
                 << "in the order shown above" << endl;
        } else if (!no_var_defined) {
            cout << "WARNING: Define one of 'flow_rate', 'mass_flow_rate', "
                 << "'residence_time' within inlet_gas. Using 0 for flow "
                 << "rate, which is equivalent to simulating batch reactor." << endl;
        }
        double mfr{0};
        if (fr_defined) {
            auto flow_rate = rctr_parser.getFlowRate();
            mfr = rctr->mass() * flow_rate / rctr_vol;
        }
        else if (mfr_defined) {
            mfr = rctr_parser.getMassFlowRate();
        } else if (rt_defined) {
            auto rt = rctr_parser.getResidenceTime();
            mfr = rctr->mass()/rt;
        }

        inlet_mfc->setMassFlowRate(mfr);
        outlet->setMaster(inlet_mfc.get());
        outlet->setPressureCoeff(0.00001);
        in_rsrv->insert(*gas);
        exhst->insert(*gas);
        inlet_mfc->install(*in_rsrv, *rctr);
        outlet->install(*rctr, *exhst);
    }

    rctr->setChemistry();

    // Read the reactor mode and set corresponding parameters
    string mode = rctr_parser.getMode();
    cout << "Reactor temperature mode: " << mode << endl;
    gen_info_out << "Reactor Temperature mode: "  << mode << endl;

    // heat_rsrv and wall are nominally defined.
    // They are used if heat transfer is required.
    auto heat_rsrv = make_shared<Reservoir>();
    auto wall = make_shared<Wall>();
    if (mode == "isothermal" || mode == "tpd")
        rctr->setEnergy(0);
    else {
        rctr->setEnergy(1);
        if (mode == "heat"){
            double htc = rctr_parser.getWallHeatTransferCoeff();  // htc
            double wall_abyv = rctr_parser.getWallSpecificArea(); // wall_abyv
            double ext_temp = rctr_parser.getExternalTemp();      // Text
            auto press = gas->pressure();
            gas->setState_TP(ext_temp, press);
            heat_rsrv->insert(*gas);
            wall->setHeatTransferCoeff(htc);
            wall->setArea(wall_abyv * rctr->volume());
            wall->install(*heat_rsrv, *rctr);
        }
    }

    /* Sensitivity Analysis */
    bool sens_on = rctr_parser.isSensitivityAnalysisEnabled();
    vector<string> rxnids;

    if (sens_on) {
        rxnids = rctr_parser.getSensitivityReactions();
        for (auto& id : rxnids){
            // Identify which kinetics the reaction belong to
            // First gas phase kinetics
            bool rxnConsumed = false;
            for (size_t i = 0; i < gas->nReactions(); i++){
                if (gas->reaction(i)->id == id){
                    rctr->addSensitivityReaction(i);
                    rxnConsumed = true;
                    break;
                }
            }
            size_t kin_ind = 0;
            while(!rxnConsumed && kin_ind < surfaces.size()) {
                for (size_t i = 0; i < surfaces[kin_ind]->nReactions(); i++){
                    if (surfaces[kin_ind]->reaction(i)->id == id){
			cat_surfs[kin_ind]->addSensitivityReaction(i);
			rxnConsumed = true;
                        break;
                    }
                }
                kin_ind++;
            }
            if(!rxnConsumed) {
                throw CanteraError("Sensitivity reaction id {} not found in any mechanism", id);
            }
        }

    }
    // Read simulation parameters
    double end_time = 0;
    if (mode == "tpd"){
        auto beg_temp = rctr_parser.T();
        auto end_temp = rctr_parser.getTPDEndTemp();
        auto temp_ramp = rctr_parser.getTPDTempRamp();
        rctr->setBeta(temp_ramp);
        end_time = (end_temp - beg_temp)/temp_ramp;
        cout << "TPD simulation" << endl;
        cout << "Temperature ramp " << temp_ramp << endl;
        cout << "End temperature  " << end_temp << endl;
    } else {
        end_time = rctr_parser.getEndTime();
    }

    vector<double> times;
    bool transient_log = rctr_parser.logTransient();
    if (transient_log && rctr_nos == 1){ // Only for singler CSTR or a batch reactor
        auto step_size = rctr_parser.getInitStep();
        cout << "Simulation Step size " << step_size << endl;

        string stepping = rctr_parser.steppingType();
        if (stepping == "logarithmic"){
            times = get_log10_intervals(end_time, step_size);
        }
        else if (stepping == "regular"){
            times = get_reg_intervals(0, end_time, step_size);
        }
    } else{
        times.push_back(end_time);
    }
    cout << "Size of time vector " << times.size() << endl;

    // Setup simulation
    ReactorNet rnet;
    //ReactorNetHybrid rnet;
    rnet.addReactor(*rctr);

    // Pass any user defined numerical options to the solver
    if (rctr_parser.tolerancesDefined()){
        auto abs_tol = rctr_parser.get_atol();
        auto rel_tol = rctr_parser.get_rtol();
        rnet.setTolerances(rel_tol, abs_tol);
    }
    /* For these options, changes have to be made in Cantera
    if (rctr_parser.solverInitStepSizeDefined()){
        rnet.setInitialStepSize(rctr_parser.getSolverInitStepSize());
    }
    if (rctr_parser.solverMaxStepsDefined()){
        rnet.setMaxNumSteps(rctr_parser.getSolverMaxSteps());
    }*/

    auto gas_print_specie_header = [&gas, data_format](string ind_var, ostream& out) -> void
    {
        if (data_format == OutputFormat::CSV) {
            out << ind_var;
            for (const auto & sp_name : gas->speciesNames()) {
                out << "," << sp_name;
            }
        } else {
            out << setw(16) << left << ind_var;
            for (const auto & sp_name : gas->speciesNames()) {
                out << setw(16) << left << sp_name;
            }
        }
        out << endl;
    };

    // Steady state condition makes sense For CSTR and PFR_0D
    vector<double> T_params = rctr_parser.Ts();
    vector<double> P_params = rctr_parser.Ps();
    vector<double> fr_params = rctr_parser.FRs();
    if (!fr_params.size()){
        if (rctr_type != BATCH) {
            auto mfr = inlet_mfc->massFlowRate();
            fr_params.push_back(mfr/gas->density());
        }
        else {
            fr_params.push_back(0.0);
        }
    }

    fs::path curr_dir = ".";
    for (const auto& T : T_params){
        for (const auto& P : P_params){
            for (const auto& fr : fr_params){
                // Set the gas state TPX and sync the appropriate reactors
                auto X = rctr_parser.getGasPhaseComposition();
                gas->setState_TPX(T, P, X);
                if (rctr_type != BATCH){
                    in_rsrv->syncState();
                    exhst->syncState();
                    inlet_mfc->setMassFlowRate(fr * gas->density());
                }
                rctr->syncState();
                rnet.setInitialTime(0);
                rnet.reinitialize();

                string new_dir = "T-"  + to_string(T) + ",P-" + to_string(P)
                                       + ",fr-" + to_string(fr);
                fs::path out_dir = curr_dir;
                if (T_params.size() > 1 || P_params.size() > 1 || fr_params.size() > 1){
                    out_dir /= new_dir;
                    create_directory(out_dir);
                }

                string file_ext;
                if (data_format == OutputFormat::CSV) {
                    file_ext = "csv";
                } else {
                    file_ext = "dat";
                }

                ofstream gas_ss_mole_out ((out_dir / ("gas_mole_ss." + file_ext)).string(), ios::out);
                ofstream gas_ss_mass_out ((out_dir / ("gas_mass_ss." + file_ext)).string(), ios::out);
                ofstream gas_ss_msdot_out ((out_dir / ("gas_msdot_ss." + file_ext)).string(), ios::out);
                ofstream surf_ss_out ((out_dir / ("surf_cov_ss." + file_ext)).string(), ios::out);
                ofstream state_var_ss_out ((out_dir / ("rctr_state_ss." + file_ext)).string(), ios::out);
                ofstream rates_out ((out_dir / "rates_ss.out").string(), ios::out);

                if (data_format == OutputFormat::DAT) {
                    gas_ss_mole_out << "#Gas Mole fractions at Steady State\n";
                    gas_ss_mass_out << "#Gas Mass fractions at Steady State\n";
                    gas_ss_msdot_out << "#Surface Production Rates of  Gas Species at Steady State\n";
                    surf_ss_out << "#Surace Coverages at Steady State\n";
                    state_var_ss_out << "#Steady State Reactor State\n";
                }

                // Reaction path analysis (RPA) data, consisting of rates of progress.
                // By default these values are written for simulation end time (PFR or CSTR)
                // and for PFR exit. If enabled, RPA data is also written for all z-points
                // of PFR
                auto rpa_flag = rctr_parser.RPA();

                auto state_var_print_hdr = [data_format](ostream& out, const string ind0) -> void
                {
                    if (data_format == OutputFormat::CSV) {
                        out << ind0 << ","//"z(m)"
                            << "Temperature(K)" << ","
                            << "Pressure(Pa)" << ","
                            << "Density(kg/m3)" << ","
                            << "U(J/kg)"
                            << endl;
                    } else {
                        out << setw(16) << left << ind0 //"z(m)"
                            << setw(16) << left << "Temperature(K)"
                            << setw(16) << left << "Pressure(Pa)"
                            << setw(16) << left << "Density(kg/m3)"
                            << setw(16) << left << "U(J/kg)"
                            << endl;
                    }
                };

                auto surf_print_hdr = [&](ostream& out, const string ind0) -> void
                {
                    if (data_format == OutputFormat::CSV) {
                        out << ind0;
                        for (const auto surf : surfaces) {
                            for (const auto & sp_name : surf->speciesNames()) {
                                out << "," << sp_name;
                            }
                        }
                    } else {
                        out << setw(16) << left << ind0;
                        for (const auto surf : surfaces) {
                            for (const auto & sp_name : surf->speciesNames()) {
                                out << setw(16) << left << sp_name;
                            }
                        }
                    }
                    out << endl;
                };

                if (rctr_type == PFR_0D) {
                    gas_print_specie_header("z(m)", gas_ss_mole_out);
                    gas_print_specie_header("z(m)", gas_ss_mass_out);
                    gas_print_specie_header("z(m)", gas_ss_msdot_out);
                    surf_print_hdr(surf_ss_out, "z(m)");
                    state_var_print_hdr(state_var_ss_out, "z(m)");
                } else {
                    gas_print_specie_header("t(s)", gas_ss_mole_out);
                    gas_print_specie_header("t(s)", gas_ss_mass_out);
                    gas_print_specie_header("t(s)", gas_ss_msdot_out);
                    surf_print_hdr(surf_ss_out, "t(s)");
                    state_var_print_hdr(state_var_ss_out, "t(s)");
                }
                    /*
                    surf_ss_out
                        << setw(16) << left << "z(m)";
                    for (const auto surf : surfaces) {
                        for (const auto & sp_name : surf->speciesNames()) {
                            surf_ss_out << setw(16) << left << sp_name;
                        }
                    }
                    surf_ss_out << endl;
                    */

                    // Print the inlet state
                    //rnet.reinitialize();
                if (rctr_type != BATCH) {
                    print_0d_rctr_state(0, rctr.get(), surf_phases, gas_ss_mole_out,
                                        gas_ss_mass_out, gas_ss_msdot_out, surf_ss_out,
                                        state_var_ss_out);
                }

                // Transient state makes sense For BATCH and CSTR
                ofstream gas_tr_mole_out ((out_dir / ("gas_mole_tr." + file_ext)).string(), ios::out);
                ofstream gas_tr_mass_out ((out_dir / ("gas_mass_tr." + file_ext)).string(), ios::out);
                ofstream gas_tr_msdot_out ((out_dir / ("gas_msdot_tr." + file_ext)).string(), ios::out);
                ofstream surf_tr_out ((out_dir / ("surf_cov_tr." + file_ext)).string(), ios::out);
                ofstream state_var_tr_out ((out_dir / ("rctr_state_tr." + file_ext)).string(), ios::out);
                if (rctr_type != PFR_0D && times.size() > 1) {
                    if (data_format == OutputFormat::DAT) {
                        gas_tr_mole_out << "Transient Gas Mole fractions"  << endl;
                        gas_tr_mass_out << "Transient Gas Mass fractions"  << endl;
                        gas_tr_msdot_out << "Transient Surface Production Rates of  Gas Species"  << endl;
                        surf_tr_out << "Transient Surface Coverages"  << endl;
                    }
                    gas_print_specie_header("t(s)", gas_tr_mole_out);
                    gas_print_specie_header("t(s)", gas_tr_mass_out);
                    gas_print_specie_header("t(s)", gas_tr_msdot_out);
                    surf_print_hdr(surf_tr_out, "t(s)");
                    state_var_print_hdr(state_var_tr_out, "t(s)");
                }

                vector<double> gas_X(gas->nSpecies()); // Temporary work array
                for (size_t i = 0; i < rctr_nos; i++) {
                    if (rctr_nos > 1) {
                        gen_info_out << "CSTR #: " << i << endl;
                    }
                    // The next 9 lines redefine the TPX of gas for each reactor
                    double temp = rctr->contents().temperature();
                    double pressure = rctr->contents().pressure();
                    rctr->contents().getMoleFractions(gas_X.data());
                    gas->setState_TPX(temp, pressure, gas_X.data());
                    if (rctr_type != BATCH){
                        in_rsrv->syncState();
                    }
                    rnet.setInitialTime(0);
                    rnet.reinitialize();
                    rnet.setMaxTimeStep(1e-1);

                    /*
                    if (times.size() == 1) {
                        cout << "Solving with steady state solver" << endl;
                        rnet.setIntegratorEndTime(times[0]);
                        rnet.solve();
                    }
                    else*/ {
                        for (const auto & tm : times) {
                            rnet.advance(tm);
                            if (transient_log) {
                                print_0d_rctr_state(tm, rctr.get(), surf_phases,
                                                    gas_tr_mole_out, gas_tr_mass_out,
                                                    gas_tr_msdot_out, surf_tr_out,
                                                    state_var_tr_out);

                            }
                        }
                    }
                    rctr->restoreState();

                    double ind_val;
                    if (rctr_type == PFR_0D) {
                        ind_val = (i+0.5)*rctr_vol;
                    }
                    else {
                        ind_val = times.back();
                    }

                    print_0d_rctr_state(ind_val, rctr.get(), surf_phases,
                                        gas_ss_mole_out, gas_ss_mass_out,
                                        gas_ss_msdot_out, surf_ss_out, state_var_ss_out);

                    if (rpa_flag) {
                        string rpa_file_name = "rates_z-";
                        rpa_file_name += to_string((i+0.5)*rctr_vol);
                        rpa_file_name += ".out";
                        ofstream rates_out((out_dir / rpa_file_name).string(),
                                           ios::out); // Masks the name
                        print_rxn_rates_hdr(rates_out);

                        rates_out.precision(6);

                        print_rxn_rates(gas.get(), rates_out);
                        for (auto surf : surfaces) {
                            print_rxn_rates(surf.get(), rates_out);
                        }
                    }
                }
                // Print final rpa data
                print_rxn_rates_hdr(rates_out);
                rates_out.precision(6);
                print_rxn_rates(gas.get(), rates_out);
                for (auto surf : surfaces) {
                    print_rxn_rates(surf.get(), rates_out);
                }

            }
        }
    }
}
}


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

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

// VOTCA includes
#include <votca/tools/constants.h>
#include <votca/tools/elements.h>

// Local VOTCA includes
#include "votca/xtp/IncrementalFockBuilder.h"
#include "votca/xtp/aomatrix.h"
#include "votca/xtp/aopotential.h"
#include "votca/xtp/density_integration.h"
#include "votca/xtp/dftengine.h"
#include "votca/xtp/eeinteractor.h"
#include "votca/xtp/logger.h"
#include "votca/xtp/mmregion.h"
#include "votca/xtp/orbitals.h"

namespace votca {
namespace xtp {

void DFTEngine::Initialize(tools::Property& options) {

  const std::string key_xtpdft = "xtpdft";
  dftbasis_name_ = options.get(".basisset").as<std::string>();

  if (options.exists(".auxbasisset")) {
    auxbasis_name_ = options.get(".auxbasisset").as<std::string>();
  }

  if (!auxbasis_name_.empty()) {
    screening_eps_ = options.get(key_xtpdft + ".screening_eps").as<double>();
    fock_matrix_reset_ =
        options.get(key_xtpdft + ".fock_matrix_reset").as<Index>();
  }
  if (options.exists(".ecp")) {
    ecp_name_ = options.get(".ecp").as<std::string>();
  }

  initial_guess_ = options.get(".initial_guess").as<std::string>();

  grid_name_ = options.get(key_xtpdft + ".integration_grid").as<std::string>();
  xc_functional_name_ = options.get(".functional").as<std::string>();

  if (options.exists(key_xtpdft + ".externaldensity")) {
    integrate_ext_density_ = true;
    orbfilename_ =
        options.get(key_xtpdft + ".externaldensity.orbfile").as<std::string>();
    gridquality_ = options.get(key_xtpdft + ".externaldensity.gridquality")
                       .as<std::string>();
    state_ =
        options.get(key_xtpdft + ".externaldensity.state").as<std::string>();
  }

  if (options.exists(".externalfield")) {
    integrate_ext_field_ = true;
    extfield_ = options.get(".externalfield").as<Eigen::Vector3d>();
  }

  conv_opt_.Econverged =
      options.get(key_xtpdft + ".convergence.energy").as<double>();
  conv_opt_.error_converged =
      options.get(key_xtpdft + ".convergence.error").as<double>();
  max_iter_ =
      options.get(key_xtpdft + ".convergence.max_iterations").as<Index>();

  std::string method =
      options.get(key_xtpdft + ".convergence.method").as<std::string>();
  if (method == "DIIS") {
    conv_opt_.usediis = true;
  } else if (method == "mixing") {
    conv_opt_.usediis = false;
  }
  if (!conv_opt_.usediis) {
    conv_opt_.histlength = 1;
    conv_opt_.maxout = false;
  }
  conv_opt_.mixingparameter =
      options.get(key_xtpdft + ".convergence.mixing").as<double>();
  conv_opt_.levelshift =
      options.get(key_xtpdft + ".convergence.levelshift").as<double>();
  conv_opt_.levelshiftend =
      options.get(key_xtpdft + ".convergence.levelshift_end").as<double>();
  conv_opt_.maxout =
      options.get(key_xtpdft + ".convergence.DIIS_maxout").as<bool>();
  conv_opt_.histlength =
      options.get(key_xtpdft + ".convergence.DIIS_length").as<Index>();
  conv_opt_.diis_start =
      options.get(key_xtpdft + ".convergence.DIIS_start").as<double>();
  conv_opt_.adiis_start =
      options.get(key_xtpdft + ".convergence.ADIIS_start").as<double>();
}

void DFTEngine::PrintMOs(const Eigen::VectorXd& MOEnergies, Log::Level level) {
  XTP_LOG(level, *pLog_) << "  Orbital energies: " << std::flush;
  XTP_LOG(level, *pLog_) << "  index occupation energy(Hartree) " << std::flush;
  for (Index i = 0; i < MOEnergies.size(); i++) {
    Index occupancy = 0;
    if (i < numofelectrons_ / 2) {
      occupancy = 2;
    }
    XTP_LOG(level, *pLog_) << (boost::format(" %1$5d      %2$1d   %3$+1.10f") %
                               i % occupancy % MOEnergies(i))
                                  .str()
                           << std::flush;
  }
  return;
}

void DFTEngine::CalcElDipole(const Orbitals& orb) const {
  QMState state = QMState("n");
  Eigen::Vector3d result = orb.CalcElDipole(state);
  XTP_LOG(Log::error, *pLog_)
      << TimeStamp() << " Electric Dipole is[e*bohr]:\n\t\t dx=" << result[0]
      << "\n\t\t dy=" << result[1] << "\n\t\t dz=" << result[2] << std::flush;
  return;
}

std::array<Eigen::MatrixXd, 2> DFTEngine::CalcERIs_EXX(
    const Eigen::MatrixXd& MOCoeff, const Eigen::MatrixXd& Dmat,
    double error) const {
  if (!auxbasis_name_.empty()) {
    if (conv_accelerator_.getUseMixing() || MOCoeff.rows() == 0) {
      return ERIs_.CalculateERIs_EXX_3c(Eigen::MatrixXd::Zero(0, 0), Dmat);
    } else {
      Eigen::MatrixXd occblock = MOCoeff.leftCols(numofelectrons_ / 2);
      return ERIs_.CalculateERIs_EXX_3c(occblock, Dmat);
    }
  } else {
    return ERIs_.CalculateERIs_EXX_4c(Dmat, error);
  }
}

Eigen::MatrixXd DFTEngine::CalcERIs(const Eigen::MatrixXd& Dmat,
                                    double error) const {
  if (!auxbasis_name_.empty()) {
    return ERIs_.CalculateERIs_3c(Dmat);
  } else {
    return ERIs_.CalculateERIs_4c(Dmat, error);
  }
}

tools::EigenSystem DFTEngine::IndependentElectronGuess(
    const Mat_p_Energy& H0) const {
  return conv_accelerator_.SolveFockmatrix(H0.matrix());
}

tools::EigenSystem DFTEngine::ModelPotentialGuess(
    const Mat_p_Energy& H0, const QMMolecule& mol,
    const Vxc_Potential<Vxc_Grid>& vxcpotential) const {
  Eigen::MatrixXd Dmat = AtomicGuess(mol);
  Mat_p_Energy e_vxc = vxcpotential.IntegrateVXC(Dmat);
  XTP_LOG(Log::info, *pLog_)
      << TimeStamp() << " Filled DFT Vxc matrix " << std::flush;

  Eigen::MatrixXd H = H0.matrix() + e_vxc.matrix();

  if (ScaHFX_ > 0) {
    std::array<Eigen::MatrixXd, 2> both =
        CalcERIs_EXX(Eigen::MatrixXd::Zero(0, 0), Dmat, 1e-12);
    H += both[0];
    H += ScaHFX_ * both[1];
  } else {
    H += CalcERIs(Dmat, 1e-12);
  }
  return conv_accelerator_.SolveFockmatrix(H);
}

bool DFTEngine::Evaluate(Orbitals& orb) {
  Prepare(orb);
  Mat_p_Energy H0 = SetupH0(orb.QMAtoms());
  tools::EigenSystem MOs;
  MOs.eigenvalues() = Eigen::VectorXd::Zero(H0.cols());
  MOs.eigenvectors() = Eigen::MatrixXd::Zero(H0.rows(), H0.cols());
  Vxc_Potential<Vxc_Grid> vxcpotential = SetupVxc(orb.QMAtoms());
  ConfigOrbfile(orb);

  if (initial_guess_ == "orbfile") {
    XTP_LOG(Log::error, *pLog_)
        << TimeStamp() << " Reading guess from orbitals object/file"
        << std::flush;
    MOs = orb.MOs();
    MOs.eigenvectors() = OrthogonalizeGuess(MOs.eigenvectors());
  } else {
    XTP_LOG(Log::error, *pLog_)
        << TimeStamp() << " Setup Initial Guess using: " << initial_guess_
        << std::flush;
    if (initial_guess_ == "independent") {
      MOs = IndependentElectronGuess(H0);
    } else if (initial_guess_ == "atom") {
      MOs = ModelPotentialGuess(H0, orb.QMAtoms(), vxcpotential);
    } else {
      throw std::runtime_error("Initial guess method not known/implemented");
    }
  }

  Eigen::MatrixXd Dmat = conv_accelerator_.DensityMatrix(MOs);
  XTP_LOG(Log::info, *pLog_)
      << TimeStamp() << " Guess Matrix gives N=" << std::setprecision(9)
      << Dmat.cwiseProduct(dftAOoverlap_.Matrix()).sum() << " electrons."
      << std::flush;

  XTP_LOG(Log::error, *pLog_)
      << TimeStamp() << " STARTING SCF cycle" << std::flush;
  XTP_LOG(Log::error, *pLog_)
      << " ----------------------------------------------"
         "----------------------------"
      << std::flush;

  Eigen::MatrixXd J = Eigen::MatrixXd::Zero(Dmat.rows(), Dmat.cols());
  Eigen::MatrixXd K;
  if (ScaHFX_ > 0) {
    K = Eigen::MatrixXd::Zero(Dmat.rows(), Dmat.cols());
  }

  double start_incremental_F_threshold = 1e-4;
  if (!auxbasis_name_.empty()) {
    start_incremental_F_threshold = 0.0;  // Disable if RI is used
  }
  IncrementalFockBuilder incremental_fock(*pLog_, start_incremental_F_threshold,
                                          fock_matrix_reset_);
  incremental_fock.Configure(Dmat);

  for (Index this_iter = 0; this_iter < max_iter_; this_iter++) {
    XTP_LOG(Log::error, *pLog_) << std::flush;
    XTP_LOG(Log::error, *pLog_) << TimeStamp() << " Iteration " << this_iter + 1
                                << " of " << max_iter_ << std::flush;

    Mat_p_Energy e_vxc = vxcpotential.IntegrateVXC(Dmat);
    XTP_LOG(Log::info, *pLog_)
        << TimeStamp() << " Filled DFT Vxc matrix " << std::flush;

    Eigen::MatrixXd H = H0.matrix() + e_vxc.matrix();
    double Eone = Dmat.cwiseProduct(H0.matrix()).sum();
    double Etwo = e_vxc.energy();
    double exx = 0.0;

    incremental_fock.Start(this_iter, conv_accelerator_.getDIIsError());
    incremental_fock.resetMatrices(J, K, Dmat);
    incremental_fock.UpdateCriteria(conv_accelerator_.getDIIsError(),
                                    this_iter);

    double integral_error =
        std::min(conv_accelerator_.getDIIsError() * 1e-5, 1e-5);
    if (ScaHFX_ > 0) {
      std::array<Eigen::MatrixXd, 2> both = CalcERIs_EXX(
          MOs.eigenvectors(), incremental_fock.getDmat_diff(), integral_error);
      J += both[0];
      H += J;
      Etwo += 0.5 * Dmat.cwiseProduct(J).sum();
      K += both[1];
      H += 0.5 * ScaHFX_ * K;
      exx = 0.25 * ScaHFX_ * Dmat.cwiseProduct(K).sum();
      XTP_LOG(Log::info, *pLog_)
          << TimeStamp() << " Filled F+K matrix " << std::flush;
    } else {
      J += CalcERIs(incremental_fock.getDmat_diff(), integral_error);
      XTP_LOG(Log::info, *pLog_)
          << TimeStamp() << " Filled F matrix " << std::flush;
      H += J;
      Etwo += 0.5 * Dmat.cwiseProduct(J).sum();
    }

    Etwo += exx;
    double totenergy = Eone + H0.energy() + Etwo;
    XTP_LOG(Log::info, *pLog_) << TimeStamp() << " Single particle energy "
                               << std::setprecision(12) << Eone << std::flush;
    XTP_LOG(Log::info, *pLog_) << TimeStamp() << " Two particle energy "
                               << std::setprecision(12) << Etwo << std::flush;
    XTP_LOG(Log::info, *pLog_)
        << TimeStamp() << std::setprecision(12) << " Local Exc contribution "
        << e_vxc.energy() << std::flush;
    if (ScaHFX_ > 0) {
      XTP_LOG(Log::info, *pLog_)
          << TimeStamp() << std::setprecision(12)
          << " Non local Ex contribution " << exx << std::flush;
    }
    XTP_LOG(Log::error, *pLog_)
        << TimeStamp() << " Total Energy " << std::setprecision(12) << totenergy
        << std::flush;

    Dmat = conv_accelerator_.Iterate(Dmat, H, MOs, totenergy);
    incremental_fock.UpdateDmats(Dmat, conv_accelerator_.getDIIsError(),
                                 this_iter);

    PrintMOs(MOs.eigenvalues(), Log::info);

    XTP_LOG(Log::info, *pLog_) << "\t\tGAP "
                               << MOs.eigenvalues()(numofelectrons_ / 2) -
                                      MOs.eigenvalues()(numofelectrons_ / 2 - 1)
                               << std::flush;

    if (conv_accelerator_.isConverged()) {
      XTP_LOG(Log::error, *pLog_)
          << TimeStamp() << " Total Energy has converged to "
          << std::setprecision(9) << conv_accelerator_.getDeltaE()
          << "[Ha] after " << this_iter + 1
          << " iterations. DIIS error is converged up to "
          << conv_accelerator_.getDIIsError() << std::flush;
      XTP_LOG(Log::error, *pLog_)
          << TimeStamp() << " Final Single Point Energy "
          << std::setprecision(12) << totenergy << " Ha" << std::flush;
      XTP_LOG(Log::error, *pLog_) << TimeStamp() << std::setprecision(12)
                                  << " Final Local Exc contribution "
                                  << e_vxc.energy() << " Ha" << std::flush;
      if (ScaHFX_ > 0) {
        XTP_LOG(Log::error, *pLog_) << TimeStamp() << std::setprecision(12)
                                    << " Final Non Local Ex contribution "
                                    << exx << " Ha" << std::flush;
      }

      PrintMOs(MOs.eigenvalues(), Log::error);
      orb.setQMEnergy(totenergy);
      orb.MOs() = MOs;
      CalcElDipole(orb);
      break;
    } else if (this_iter == max_iter_ - 1) {
      XTP_LOG(Log::error, *pLog_)
          << TimeStamp() << " DFT calculation has not converged after "
          << max_iter_
          << " iterations. Use more iterations or another convergence "
             "acceleration scheme."
          << std::flush;
      return false;
    }
  }
  return true;
}

Mat_p_Energy DFTEngine::SetupH0(const QMMolecule& mol) const {

  AOKinetic dftAOkinetic;

  dftAOkinetic.Fill(dftbasis_);
  XTP_LOG(Log::info, *pLog_)
      << TimeStamp() << " Filled DFT Kinetic energy matrix ." << std::flush;

  AOMultipole dftAOESP;
  dftAOESP.FillPotential(dftbasis_, mol);
  XTP_LOG(Log::info, *pLog_)
      << TimeStamp() << " Filled DFT nuclear potential matrix." << std::flush;

  Eigen::MatrixXd H0 = dftAOkinetic.Matrix() + dftAOESP.Matrix();
  XTP_LOG(Log::error, *pLog_)
      << TimeStamp() << " Constructed independent particle hamiltonian "
      << std::flush;
  double E0 = NuclearRepulsion(mol);
  XTP_LOG(Log::error, *pLog_) << TimeStamp() << " Nuclear Repulsion Energy is "
                              << std::setprecision(9) << E0 << std::flush;

  if (!ecp_name_.empty()) {
    AOECP dftAOECP;
    dftAOECP.FillPotential(dftbasis_, ecp_);
    H0 += dftAOECP.Matrix();
    XTP_LOG(Log::info, *pLog_)
        << TimeStamp() << " Filled DFT ECP matrix" << std::flush;
  }

  if (externalsites_ != nullptr) {
    XTP_LOG(Log::error, *pLog_) << TimeStamp() << " " << externalsites_->size()
                                << " External sites" << std::flush;
    bool has_quadrupoles = std::any_of(
        externalsites_->begin(), externalsites_->end(),
        [](const std::unique_ptr<StaticSite>& s) { return s->getRank() == 2; });
    std::string header =
        " Name      Coordinates[a0]     charge[e]         dipole[e*a0]    ";
    if (has_quadrupoles) {
      header += "              quadrupole[e*a0^2]";
    }
    XTP_LOG(Log::error, *pLog_) << header << std::flush;
    Index limit = 50;
    Index counter = 0;
    for (const std::unique_ptr<StaticSite>& site : *externalsites_) {
      if (counter == limit) {
        break;
      }
      std::string output =
          (boost::format("  %1$s"
                         "   %2$+1.4f %3$+1.4f %4$+1.4f"
                         "   %5$+1.4f") %
           site->getElement() % site->getPos()[0] % site->getPos()[1] %
           site->getPos()[2] % site->getCharge())
              .str();
      const Eigen::Vector3d& dipole = site->getDipole();
      output += (boost::format("   %1$+1.4f %2$+1.4f %3$+1.4f") % dipole[0] %
                 dipole[1] % dipole[2])
                    .str();
      if (site->getRank() > 1) {
        Eigen::VectorXd quadrupole = site->Q().tail<5>();
        output +=
            (boost::format("   %1$+1.4f %2$+1.4f %3$+1.4f %4$+1.4f %5$+1.4f") %
             quadrupole[0] % quadrupole[1] % quadrupole[2] % quadrupole[3] %
             quadrupole[4])
                .str();
      }
      XTP_LOG(Log::error, *pLog_) << output << std::flush;
      counter++;
    }
    if (counter == limit) {
      XTP_LOG(Log::error, *pLog_)
          << "              ... (" << externalsites_->size() - limit
          << " sites not displayed)\n"
          << std::flush;
    }

    Mat_p_Energy ext_multipoles =
        IntegrateExternalMultipoles(mol, *externalsites_);
    XTP_LOG(Log::error, *pLog_)
        << TimeStamp() << " Nuclei-external site interaction energy "
        << std::setprecision(9) << ext_multipoles.energy() << std::flush;
    E0 += ext_multipoles.energy();
    H0 += ext_multipoles.matrix();
  }

  if (integrate_ext_density_) {
    Orbitals extdensity;
    extdensity.ReadFromCpt(orbfilename_);
    Mat_p_Energy extdensity_result = IntegrateExternalDensity(mol, extdensity);
    E0 += extdensity_result.energy();
    XTP_LOG(Log::error, *pLog_)
        << TimeStamp() << " Nuclei-external density interaction energy "
        << std::setprecision(9) << extdensity_result.energy() << std::flush;
    H0 += extdensity_result.matrix();
  }

  if (integrate_ext_field_) {

    XTP_LOG(Log::error, *pLog_)
        << TimeStamp() << " Integrating external electric field with F[Hrt]="
        << extfield_.transpose() << std::flush;
    H0 += IntegrateExternalField(mol);
  }

  return Mat_p_Energy(E0, H0);
}

void DFTEngine::SetupInvariantMatrices() {

  dftAOoverlap_.Fill(dftbasis_);

  XTP_LOG(Log::info, *pLog_)
      << TimeStamp() << " Filled DFT Overlap matrix." << std::flush;

  conv_opt_.numberofelectrons = numofelectrons_;
  conv_accelerator_.Configure(conv_opt_);
  conv_accelerator_.setLogger(pLog_);
  conv_accelerator_.setOverlap(dftAOoverlap_, 1e-8);
  conv_accelerator_.PrintConfigOptions();

  if (!auxbasis_name_.empty()) {
    // prepare invariant part of electron repulsion integrals
    ERIs_.Initialize(dftbasis_, auxbasis_);
    XTP_LOG(Log::info, *pLog_)
        << TimeStamp() << " Inverted AUX Coulomb matrix, removed "
        << ERIs_.Removedfunctions() << " functions from aux basis"
        << std::flush;
    XTP_LOG(Log::error, *pLog_)
        << TimeStamp()
        << " Setup invariant parts of Electron Repulsion integrals "
        << std::flush;
  } else {
    XTP_LOG(Log::info, *pLog_)
        << TimeStamp() << " Calculating 4c diagonals. " << std::flush;
    ERIs_.Initialize_4c(dftbasis_);
    XTP_LOG(Log::info, *pLog_)
        << TimeStamp() << " Calculated 4c diagonals. " << std::flush;
  }

  return;
}

Eigen::MatrixXd DFTEngine::RunAtomicDFT_unrestricted(
    const QMAtom& uniqueAtom) const {
  bool with_ecp = !ecp_name_.empty();
  if (uniqueAtom.getElement() == "H" || uniqueAtom.getElement() == "He") {
    with_ecp = false;
  }

  QMMolecule atom = QMMolecule("individual_atom", 0);
  atom.push_back(uniqueAtom);

  BasisSet basisset;
  basisset.Load(dftbasis_name_);
  AOBasis dftbasis;
  dftbasis.Fill(basisset, atom);
  Vxc_Grid grid;
  grid.GridSetup(grid_name_, atom, dftbasis);
  Vxc_Potential<Vxc_Grid> gridIntegration(grid);
  gridIntegration.setXCfunctional(xc_functional_name_);

  ECPAOBasis ecp;
  if (with_ecp) {
    ECPBasisSet ecps;
    ecps.Load(ecp_name_);
    ecp.Fill(ecps, atom);
  }

  Index numofelectrons = uniqueAtom.getNuccharge();
  Index alpha_e = 0;
  Index beta_e = 0;

  if ((numofelectrons % 2) != 0) {
    alpha_e = numofelectrons / 2 + numofelectrons % 2;
    beta_e = numofelectrons / 2;
  } else {
    alpha_e = numofelectrons / 2;
    beta_e = alpha_e;
  }

  AOOverlap dftAOoverlap;
  AOKinetic dftAOkinetic;
  AOMultipole dftAOESP;
  AOECP dftAOECP;
  ERIs ERIs_atom;

  // DFT AOOverlap matrix

  dftAOoverlap.Fill(dftbasis);
  dftAOkinetic.Fill(dftbasis);

  dftAOESP.FillPotential(dftbasis, atom);
  ERIs_atom.Initialize_4c(dftbasis);

  ConvergenceAcc Convergence_alpha;
  ConvergenceAcc Convergence_beta;
  ConvergenceAcc::options opt_alpha = conv_opt_;
  opt_alpha.mode = ConvergenceAcc::KSmode::open;
  opt_alpha.histlength = 20;
  opt_alpha.levelshift = 0.1;
  opt_alpha.levelshiftend = 0.0;
  opt_alpha.usediis = true;
  opt_alpha.adiis_start = 0.0;
  opt_alpha.diis_start = 0.0;
  opt_alpha.numberofelectrons = alpha_e;

  ConvergenceAcc::options opt_beta = opt_alpha;
  opt_beta.numberofelectrons = beta_e;

  Logger log;
  Convergence_alpha.Configure(opt_alpha);
  Convergence_alpha.setLogger(&log);
  Convergence_alpha.setOverlap(dftAOoverlap, 1e-8);
  Convergence_beta.Configure(opt_beta);
  Convergence_beta.setLogger(&log);
  Convergence_beta.setOverlap(dftAOoverlap, 1e-8);
  /**** Construct initial density  ****/

  Eigen::MatrixXd H0 = dftAOkinetic.Matrix() + dftAOESP.Matrix();
  if (with_ecp) {
    dftAOECP.FillPotential(dftbasis, ecp);
    H0 += dftAOECP.Matrix();
  }
  tools::EigenSystem MOs_alpha = Convergence_alpha.SolveFockmatrix(H0);

  Eigen::MatrixXd dftAOdmat_alpha = Convergence_alpha.DensityMatrix(MOs_alpha);
  if (uniqueAtom.getElement() == "H") {
    return dftAOdmat_alpha;
  }
  tools::EigenSystem MOs_beta = Convergence_beta.SolveFockmatrix(H0);
  Eigen::MatrixXd dftAOdmat_beta = Convergence_beta.DensityMatrix(MOs_beta);

  Index maxiter = 80;
  for (Index this_iter = 0; this_iter < maxiter; this_iter++) {

    Eigen::MatrixXd H_alpha = H0;
    Eigen::MatrixXd H_beta = H_alpha;

    double E_two_alpha = 0.0;
    double E_two_beta = 0.0;

    double integral_error = std::min(1e-5 * 0.5 *
                                         (Convergence_alpha.getDIIsError() +
                                          Convergence_beta.getDIIsError()),
                                     1e-5);
    if (ScaHFX_ > 0) {
      std::array<Eigen::MatrixXd, 2> both_alpha =
          ERIs_atom.CalculateERIs_EXX_4c(dftAOdmat_alpha, integral_error);

      std::array<Eigen::MatrixXd, 2> both_beta =
          ERIs_atom.CalculateERIs_EXX_4c(dftAOdmat_beta, integral_error);
      Eigen::MatrixXd Hartree = both_alpha[0] + both_beta[0];
      E_two_alpha += Hartree.cwiseProduct(dftAOdmat_alpha).sum();
      E_two_beta += Hartree.cwiseProduct(dftAOdmat_beta).sum();
      E_two_alpha += 0.5 * both_alpha[1].cwiseProduct(dftAOdmat_alpha).sum();
      E_two_beta += 0.5 * both_beta[1].cwiseProduct(dftAOdmat_beta).sum();
      H_alpha += Hartree + ScaHFX_ * both_alpha[1];
      H_beta += Hartree + ScaHFX_ * both_beta[1];

    } else {
      Eigen::MatrixXd Hartree = ERIs_atom.CalculateERIs_4c(
          dftAOdmat_alpha + dftAOdmat_beta, integral_error);
      E_two_alpha += Hartree.cwiseProduct(dftAOdmat_alpha).sum();
      E_two_beta += Hartree.cwiseProduct(dftAOdmat_beta).sum();
      H_alpha += Hartree;
      H_beta += Hartree;
    }

    Mat_p_Energy e_vxc_alpha = gridIntegration.IntegrateVXC(dftAOdmat_alpha);
    H_alpha += e_vxc_alpha.matrix();
    E_two_alpha += e_vxc_alpha.energy();

    Mat_p_Energy e_vxc_beta = gridIntegration.IntegrateVXC(dftAOdmat_beta);
    H_beta += e_vxc_beta.matrix();
    E_two_beta += e_vxc_beta.energy();

    double E_one_alpha = dftAOdmat_alpha.cwiseProduct(H0).sum();
    double E_one_beta = dftAOdmat_beta.cwiseProduct(H0).sum();
    double E_alpha = E_one_alpha + E_two_alpha;
    double E_beta = E_one_beta + E_two_beta;
    double totenergy = E_alpha + E_beta;
    // evolve alpha
    dftAOdmat_alpha =
        Convergence_alpha.Iterate(dftAOdmat_alpha, H_alpha, MOs_alpha, E_alpha);
    // evolve beta
    dftAOdmat_beta =
        Convergence_beta.Iterate(dftAOdmat_beta, H_beta, MOs_beta, E_beta);

    XTP_LOG(Log::debug, *pLog_)
        << TimeStamp() << " Iter " << this_iter << " of " << maxiter << " Etot "
        << totenergy << " diise_a " << Convergence_alpha.getDIIsError()
        << " diise_b " << Convergence_beta.getDIIsError() << "\n\t\t a_gap "
        << MOs_alpha.eigenvalues()(alpha_e) -
               MOs_alpha.eigenvalues()(alpha_e - 1)
        << " b_gap "
        << MOs_beta.eigenvalues()(beta_e) - MOs_beta.eigenvalues()(beta_e - 1)
        << " Nalpha="
        << dftAOoverlap.Matrix().cwiseProduct(dftAOdmat_alpha).sum()
        << " Nbeta=" << dftAOoverlap.Matrix().cwiseProduct(dftAOdmat_beta).sum()
        << std::flush;

    bool converged =
        Convergence_alpha.isConverged() && Convergence_beta.isConverged();
    if (converged || this_iter == maxiter - 1) {

      if (converged) {
        XTP_LOG(Log::info, *pLog_)
            << TimeStamp() << " Converged after " << this_iter + 1
            << " iterations" << std::flush;
      } else {
        XTP_LOG(Log::info, *pLog_)
            << TimeStamp() << " Not converged after " << this_iter + 1
            << " iterations. Unconverged density.\n\t\t\t"
            << " DIIsError_alpha=" << Convergence_alpha.getDIIsError()
            << " DIIsError_beta=" << Convergence_beta.getDIIsError()
            << std::flush;
      }
      break;
    }
  }
  Eigen::MatrixXd avgmatrix =
      SphericalAverageShells(dftAOdmat_alpha + dftAOdmat_beta, dftbasis);
  XTP_LOG(Log::info, *pLog_)
      << TimeStamp() << " Atomic density Matrix for " << uniqueAtom.getElement()
      << " gives N=" << std::setprecision(9)
      << avgmatrix.cwiseProduct(dftAOoverlap.Matrix()).sum() << " electrons."
      << std::flush;
  return avgmatrix;
}

Eigen::MatrixXd DFTEngine::AtomicGuess(const QMMolecule& mol) const {

  std::vector<std::string> elements = mol.FindUniqueElements();
  XTP_LOG(Log::info, *pLog_)
      << TimeStamp() << " Scanning molecule of size " << mol.size()
      << " for unique elements" << std::flush;
  QMMolecule uniqueelements = QMMolecule("uniqueelements", 0);
  for (auto element : elements) {
    uniqueelements.push_back(QMAtom(0, element, Eigen::Vector3d::Zero()));
  }

  XTP_LOG(Log::info, *pLog_) << TimeStamp() << " " << uniqueelements.size()
                             << " unique elements found" << std::flush;
  std::vector<Eigen::MatrixXd> uniqueatom_guesses;
  for (QMAtom& unique_atom : uniqueelements) {
    XTP_LOG(Log::error, *pLog_)
        << TimeStamp() << " Calculating atom density for "
        << unique_atom.getElement() << std::flush;
    Eigen::MatrixXd dmat_unrestricted = RunAtomicDFT_unrestricted(unique_atom);
    uniqueatom_guesses.push_back(dmat_unrestricted);
  }

  Eigen::MatrixXd guess =
      Eigen::MatrixXd::Zero(dftbasis_.AOBasisSize(), dftbasis_.AOBasisSize());
  Index start = 0;
  for (const QMAtom& atom : mol) {
    Index index = 0;
    for (; index < uniqueelements.size(); index++) {
      if (atom.getElement() == uniqueelements[index].getElement()) {
        break;
      }
    }
    Eigen::MatrixXd& dmat_unrestricted = uniqueatom_guesses[index];
    guess.block(start, start, dmat_unrestricted.rows(),
                dmat_unrestricted.cols()) = dmat_unrestricted;
    start += dmat_unrestricted.rows();
  }

  return guess;
}

void DFTEngine::ConfigOrbfile(Orbitals& orb) {
  if (initial_guess_ == "orbfile") {

    if (orb.hasDFTbasisName()) {
      if (orb.getDFTbasisName() != dftbasis_name_) {
        throw std::runtime_error(
            (boost::format("Basisset Name in guess orb file "
                           "and in dftengine option file differ %1% vs %2%") %
             orb.getDFTbasisName() % dftbasis_name_)
                .str());
      }
    } else {
      XTP_LOG(Log::error, *pLog_)
          << TimeStamp()
          << " WARNING: "
             "Orbital file has no basisset information,"
             "using it as a guess might work or not for calculation with "
          << dftbasis_name_ << std::flush;
    }
  }
  orb.setXCFunctionalName(xc_functional_name_);
  orb.setXCGrid(grid_name_);
  orb.setScaHFX(ScaHFX_);
  if (!ecp_name_.empty()) {
    orb.setECPName(ecp_name_);
  }
  if (!auxbasis_name_.empty()) {
    orb.SetupAuxBasis(auxbasis_name_);
  }

  if (initial_guess_ == "orbfile") {
    if (orb.hasECPName() || !ecp_name_.empty()) {
      if (orb.getECPName() != ecp_name_) {
        throw std::runtime_error(
            (boost::format("ECPs in orb file: %1% and options %2% differ") %
             orb.getECPName() % ecp_name_)
                .str());
      }
    }
    if (orb.getNumberOfAlphaElectrons() != numofelectrons_ / 2) {
      throw std::runtime_error(
          (boost::format("Number of electron in guess orb file: %1% and in "
                         "dftengine: %2% differ.") %
           orb.getNumberOfAlphaElectrons() % (numofelectrons_ / 2))
              .str());
    }
    if (orb.getBasisSetSize() != dftbasis_.AOBasisSize()) {
      throw std::runtime_error(
          (boost::format("Number of levels in guess orb file: "
                         "%1% and in dftengine: %2% differ.") %
           orb.getBasisSetSize() % dftbasis_.AOBasisSize())
              .str());
    }
  } else {
    orb.setNumberOfAlphaElectrons(numofelectrons_ / 2);
    orb.setNumberOfOccupiedLevels(numofelectrons_ / 2);
  }
  return;
}

void DFTEngine::Prepare(Orbitals& orb) {
  QMMolecule& mol = orb.QMAtoms();
  XTP_LOG(Log::error, *pLog_)
      << TimeStamp() << " Using " << OPENMP::getMaxThreads() << " threads"
      << std::flush;

  if (XTP_HAS_MKL_OVERLOAD()) {
    XTP_LOG(Log::error, *pLog_)
        << TimeStamp() << " Using MKL overload for Eigen " << std::flush;
  } else {
    XTP_LOG(Log::error, *pLog_)
        << TimeStamp()
        << " Using native Eigen implementation, no BLAS overload "
        << std::flush;
  }

  XTP_LOG(Log::error, *pLog_) << " Molecule Coordinates [A] " << std::flush;
  for (const QMAtom& atom : mol) {
    const Eigen::Vector3d pos = atom.getPos() * tools::conv::bohr2ang;
    std::string output = (boost::format("  %1$s"
                                        "   %2$+1.4f %3$+1.4f %4$+1.4f") %
                          atom.getElement() % pos[0] % pos[1] % pos[2])
                             .str();

    XTP_LOG(Log::error, *pLog_) << output << std::flush;
  }

  orb.SetupDftBasis(dftbasis_name_);
  dftbasis_ = orb.getDftBasis();

  XTP_LOG(Log::error, *pLog_)
      << TimeStamp() << " Loaded DFT Basis Set " << dftbasis_name_ << " with "
      << dftbasis_.AOBasisSize() << " functions" << std::flush;

  if (!auxbasis_name_.empty()) {
    BasisSet auxbasisset;
    auxbasisset.Load(auxbasis_name_);
    auxbasis_.Fill(auxbasisset, mol);
    XTP_LOG(Log::error, *pLog_)
        << TimeStamp() << " Loaded AUX Basis Set " << auxbasis_name_ << " with "
        << auxbasis_.AOBasisSize() << " functions" << std::flush;
  }
  if (!ecp_name_.empty()) {
    ECPBasisSet ecpbasisset;
    ecpbasisset.Load(ecp_name_);
    XTP_LOG(Log::error, *pLog_)
        << TimeStamp() << " Loaded ECP library " << ecp_name_ << std::flush;

    std::vector<std::string> results = ecp_.Fill(ecpbasisset, mol);
    XTP_LOG(Log::info, *pLog_)
        << TimeStamp() << " Filled ECP Basis" << std::flush;
    if (results.size() > 0) {
      std::string message = "";
      for (const std::string& element : results) {
        message += " " + element;
      }
      XTP_LOG(Log::error, *pLog_)
          << TimeStamp() << " Found no ECPs for elements" << message
          << std::flush;
    }
  }

  for (const QMAtom& atom : mol) {
    numofelectrons_ += atom.getNuccharge();
  }

  // here number of electrons is actually the total number, everywhere else in
  // votca it is just alpha_electrons
  XTP_LOG(Log::error, *pLog_)
      << TimeStamp() << " Total number of electrons: " << numofelectrons_
      << std::flush;

  SetupInvariantMatrices();
  return;
}

Vxc_Potential<Vxc_Grid> DFTEngine::SetupVxc(const QMMolecule& mol) {
  ScaHFX_ = Vxc_Potential<Vxc_Grid>::getExactExchange(xc_functional_name_);
  if (ScaHFX_ > 0) {
    XTP_LOG(Log::error, *pLog_)
        << TimeStamp() << " Using hybrid functional with alpha=" << ScaHFX_
        << std::flush;
  }
  Vxc_Grid grid;
  grid.GridSetup(grid_name_, mol, dftbasis_);
  Vxc_Potential<Vxc_Grid> vxc(grid);
  vxc.setXCfunctional(xc_functional_name_);
  XTP_LOG(Log::error, *pLog_)
      << TimeStamp() << " Setup numerical integration grid " << grid_name_
      << " for vxc functional " << xc_functional_name_ << std::flush;
  XTP_LOG(Log::info, *pLog_)
      << "\t\t "
      << " with " << grid.getGridSize() << " points"
      << " divided into " << grid.getBoxesSize() << " boxes" << std::flush;
  return vxc;
}

double DFTEngine::NuclearRepulsion(const QMMolecule& mol) const {
  double E_nucnuc = 0.0;

  for (Index i = 0; i < mol.size(); i++) {
    const Eigen::Vector3d& r1 = mol[i].getPos();
    double charge1 = double(mol[i].getNuccharge());
    for (Index j = 0; j < i; j++) {
      const Eigen::Vector3d& r2 = mol[j].getPos();
      double charge2 = double(mol[j].getNuccharge());
      E_nucnuc += charge1 * charge2 / (r1 - r2).norm();
    }
  }
  return E_nucnuc;
}

// spherically average the density matrix belonging to two shells
Eigen::MatrixXd DFTEngine::SphericalAverageShells(
    const Eigen::MatrixXd& dmat, const AOBasis& dftbasis) const {
  Eigen::MatrixXd avdmat = Eigen::MatrixXd::Zero(dmat.rows(), dmat.cols());
  for (const AOShell& shellrow : dftbasis) {
    Index size_row = shellrow.getNumFunc();
    Index start_row = shellrow.getStartIndex();
    for (const AOShell& shellcol : dftbasis) {
      Index size_col = shellcol.getNumFunc();
      Index start_col = shellcol.getStartIndex();
      Eigen::MatrixXd shelldmat =
          dmat.block(start_row, start_col, size_row, size_col);
      if (shellrow.getL() == shellcol.getL()) {
        double diagavg = shelldmat.diagonal().sum() / double(shelldmat.rows());
        Index offdiagelements =
            shelldmat.rows() * shelldmat.cols() - shelldmat.cols();
        double offdiagavg = (shelldmat.sum() - shelldmat.diagonal().sum()) /
                            double(offdiagelements);
        avdmat.block(start_row, start_col, size_row, size_col).array() =
            offdiagavg;
        avdmat.block(start_row, start_col, size_row, size_col)
            .diagonal()
            .array() = diagavg;
      } else {
        double avg = shelldmat.sum() / double(shelldmat.size());
        avdmat.block(start_row, start_col, size_row, size_col).array() = avg;
      }
    }
  }
  return avdmat;
}

double DFTEngine::ExternalRepulsion(
    const QMMolecule& mol,
    const std::vector<std::unique_ptr<StaticSite> >& multipoles) const {

  if (multipoles.size() == 0) {
    return 0;
  }

  double E_ext = 0;
  eeInteractor interactor;
  for (const QMAtom& atom : mol) {
    StaticSite nucleus = StaticSite(atom, double(atom.getNuccharge()));
    for (const std::unique_ptr<StaticSite>& site : *externalsites_) {
      if ((site->getPos() - nucleus.getPos()).norm() < 1e-7) {
        XTP_LOG(Log::error, *pLog_) << TimeStamp()
                                    << " External site sits on nucleus, "
                                       "interaction between them is ignored."
                                    << std::flush;
        continue;
      }
      E_ext += interactor.CalcStaticEnergy_site(*site, nucleus);
    }
  }
  return E_ext;
}

Eigen::MatrixXd DFTEngine::IntegrateExternalField(const QMMolecule& mol) const {

  AODipole dipole;
  dipole.setCenter(mol.getPos());
  dipole.Fill(dftbasis_);
  Eigen::MatrixXd result =
      Eigen::MatrixXd::Zero(dipole.Dimension(), dipole.Dimension());
  for (Index i = 0; i < 3; i++) {
    result -= dipole.Matrix()[i] * extfield_[i];
  }
  return result;
}

Mat_p_Energy DFTEngine::IntegrateExternalMultipoles(
    const QMMolecule& mol,
    const std::vector<std::unique_ptr<StaticSite> >& multipoles) const {

  Mat_p_Energy result(dftbasis_.AOBasisSize(), dftbasis_.AOBasisSize());
  AOMultipole dftAOESP;

  dftAOESP.FillPotential(dftbasis_, multipoles);
  XTP_LOG(Log::error, *pLog_)
      << TimeStamp() << " Filled DFT external multipole potential matrix"
      << std::flush;
  result.matrix() = dftAOESP.Matrix();
  result.energy() = ExternalRepulsion(mol, multipoles);

  return result;
}

Mat_p_Energy DFTEngine::IntegrateExternalDensity(
    const QMMolecule& mol, const Orbitals& extdensity) const {
  BasisSet basis;
  basis.Load(extdensity.getDFTbasisName());
  AOBasis aobasis;
  aobasis.Fill(basis, extdensity.QMAtoms());
  Vxc_Grid grid;
  grid.GridSetup(gridquality_, extdensity.QMAtoms(), aobasis);
  DensityIntegration<Vxc_Grid> numint(grid);
  Eigen::MatrixXd dmat = extdensity.DensityMatrixFull(state_);

  numint.IntegrateDensity(dmat);
  XTP_LOG(Log::error, *pLog_)
      << TimeStamp() << " Calculated external density" << std::flush;
  Eigen::MatrixXd e_contrib = numint.IntegratePotential(dftbasis_);
  XTP_LOG(Log::error, *pLog_)
      << TimeStamp() << " Calculated potential from electron density"
      << std::flush;
  AOMultipole esp;
  esp.FillPotential(dftbasis_, extdensity.QMAtoms());

  double nuc_energy = 0.0;
  for (const QMAtom& atom : mol) {
    nuc_energy +=
        numint.IntegratePotential(atom.getPos()) * double(atom.getNuccharge());
    for (const QMAtom& extatom : extdensity.QMAtoms()) {
      const double dist = (atom.getPos() - extatom.getPos()).norm();
      nuc_energy +=
          double(atom.getNuccharge()) * double(extatom.getNuccharge()) / dist;
    }
  }
  XTP_LOG(Log::error, *pLog_)
      << TimeStamp() << " Calculated potential from nuclei" << std::flush;
  XTP_LOG(Log::error, *pLog_)
      << TimeStamp() << " Electrostatic: " << nuc_energy << std::flush;
  return Mat_p_Energy(nuc_energy, e_contrib + esp.Matrix());
}

Eigen::MatrixXd DFTEngine::OrthogonalizeGuess(
    const Eigen::MatrixXd& GuessMOs) const {
  Eigen::MatrixXd nonortho =
      GuessMOs.transpose() * dftAOoverlap_.Matrix() * GuessMOs;
  Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> es(nonortho);
  Eigen::MatrixXd result = GuessMOs * es.operatorInverseSqrt();
  return result;
}

}  // namespace xtp
}  // namespace votca


/*
 * BSD 2-Clause License
 *
 * Copyright (c) 2020, Christoph Neuhauser
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * * Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <boost/algorithm/string/predicate.hpp>
#include <regex>

#include <Utils/Convert.hpp>
#include <Utils/File/Logfile.hpp>
#include <Math/Math.hpp>
#include <Math/Geometry/MatrixUtil.hpp>

#include "Tokens.hpp"
#include "TransformString.hpp"

glm::mat4 parseTransformString(std::string transformString) {
    glm::mat4 matrix = sgl::matrixIdentity();

    // Iterate over all of the concatenated transformations (pattern: "type(content)").
    // First match: Transform type. Second match: Transform information in the brackets.
    std::regex transformRegex("\\s*(\\S+)\\s*\\(\\s*(.+)\\s*\\)(.*)");
    std::smatch what;
    while (std::regex_search(transformString, what, transformRegex)) {
        std::string transformType = what[1];
        std::string transformContent = what[2];
        transformString = what[3]; // Continue with next part of string

        // Now split the transform content in the brackets (multiple float values separated by space or commas).
        std::vector<std::string> transformData = getTokenList(transformContent, NUMBER_AND_DEGREES_REGEX_STRING);

        if (transformType == "translate") {
            float x = sgl::fromString<float>(transformData.at(0));
            float y = sgl::fromString<float>(transformData.at(1));
            float z = sgl::fromString<float>(transformData.at(1));
            matrix = matrix * sgl::matrixTranslation(glm::vec3(x, y, z));
        } else if (transformType == "scale") {
            float scaleX = sgl::fromString<float>(transformData.at(0));
            if (transformData.size() == 1) {
                matrix = matrix * sgl::matrixScaling(glm::vec3(scaleX, scaleX, scaleX));
            } else {
                float scaleY = sgl::fromString<float>(transformData.at(1));
                float scaleZ = sgl::fromString<float>(transformData.at(2));
                matrix = matrix * sgl::matrixScaling(glm::vec3(scaleX, scaleY, scaleZ));
            }
        } else if (transformType == "rotate") {
            float rotationAngleRadians = 0.0f;
            // Degree or radians?
            if (boost::ends_with(transformData.at(0), "°")) {
                std::string numberString =
                        transformData.at(0).substr(0, transformData.at(0).find_last_of("°") - 1);
                rotationAngleRadians = sgl::fromString<float>(numberString) / 180.0f * sgl::PI;
            } else {
                rotationAngleRadians = sgl::fromString<float>(transformData.at(0));
            }
            float axisX = sgl::fromString<float>(transformData.at(1));
            float axisY = sgl::fromString<float>(transformData.at(2));
            float axisZ = sgl::fromString<float>(transformData.at(3));
            matrix = matrix * glm::rotate(rotationAngleRadians, glm::vec3(axisX, axisY, axisZ));
        } else if (transformType == "matrix") {
            float m[16];
            for (size_t i = 0; i < transformData.size(); i++) {
                m[i] = sgl::fromString<float>(transformData.at(i));
            }

            if (transformData.size() == 9) {
                matrix = matrix * glm::mat4(
                        m[0], m[3], m[6], 0.0f,
                        m[1], m[4], m[7], 0.0f,
                        m[2], m[5], m[8], 0.0f,
                        0.0f, 0.0f, 0.0f, 1.0f
                );
            } else if (transformData.size() == 12) {
                matrix = matrix * glm::mat4(
                        m[0], m[4], m[8], 0.0f,
                        m[1], m[5], m[9], 0.0f,
                        m[2], m[6], m[10], 0.0f,
                        m[3], m[7], m[11], 1.0f
                );
            } else if (transformData.size() == 16) {
                matrix = matrix * glm::mat4(
                        m[0], m[4], m[8], m[12],
                        m[1], m[5], m[9], m[13],
                        m[2], m[6], m[10], m[14],
                        m[3], m[7], m[11], m[15]
                );
            } else {
                sgl::Logfile::get()->writeError(
                        "ERROR in parseTransformString: Invalid number of elements in matrix.");
            }
        } else if (transformType == "matrixc") {
            float m[16];
            for (size_t i = 0; i < transformData.size(); i++) {
                m[i] = sgl::fromString<float>(transformData.at(i));
            }

            if (transformData.size() == 9) {
                matrix = matrix * glm::mat4(
                        m[0], m[1], m[2], 0.0f,
                        m[3], m[4], m[5], 0.0f,
                        m[6], m[7], m[8], 0.0f,
                        0.0f, 0.0f, 0.0f, 1.0f
                );
            } else if (transformData.size() == 12) {
                matrix = matrix * glm::mat4(
                        m[0], m[1], m[2], 0.0f,
                        m[3], m[4], m[5], 0.0f,
                        m[6], m[7], m[8], 0.0f,
                        m[9], m[10], m[11], 1.0f
                );
            } else if (transformData.size() == 16) {
                matrix = matrix * glm::mat4(
                        m[0], m[1], m[2], m[3],
                        m[4], m[5], m[6], m[7],
                        m[8], m[9], m[10], m[11],
                        m[12], m[13], m[14], m[15]
                );
            } else {
                sgl::Logfile::get()->writeError(
                        "ERROR in parseTransformString: Invalid number of elements in matrix.");
            }
        }
    }

    return matrix;
}


// Copyright 2014 Nicolas Mellado
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// -------------------------------------------------------------------------- //
//
// Authors: Nicolas Mellado
//
// An implementation of the Super 4-points Congruent Sets (Super 4PCS)
// algorithm presented in:
//
// Super 4PCS: Fast Global Pointcloud Registration via Smart Indexing
// Nicolas Mellado, Dror Aiger, Niloy J. Mitra
// Symposium on Geometry Processing 2014.
//
// Data acquisition in large-scale scenes regularly involves accumulating
// information across multiple scans. A common approach is to locally align scan
// pairs using Iterative Closest Point (ICP) algorithm (or its variants), but
// requires static scenes and small motion between scan pairs. This prevents
// accumulating data across multiple scan sessions and/or different acquisition
// modalities (e.g., stereo, depth scans). Alternatively, one can use a global
// registration algorithm allowing scans to be in arbitrary initial poses. The
// state-of-the-art global registration algorithm, 4PCS, however has a quadratic
// time complexity in the number of data points. This vastly limits its
// applicability to acquisition of large environments. We present Super 4PCS for
// global pointcloud registration that is optimal, i.e., runs in linear time (in
// the number of data points) and is also output sensitive in the complexity of
// the alignment problem based on the (unknown) overlap across scan pairs.
// Technically, we map the algorithm as an 'instance problem' and solve it
// efficiently using a smart indexing data organization. The algorithm is
// simple, memory-efficient, and fast. We demonstrate that Super 4PCS results in
// significant speedup over alternative approaches and allows unstructured
// efficient acquisition of scenes at scales previously not possible. Complete
// source code and datasets are available for research use at
// http://geometry.cs.ucl.ac.uk/projects/2014/super4PCS/.

#ifndef _SUPER4PCS_ACCELERATORS_INDEXED_NORMAL_SET_HPP_
#define _SUPER4PCS_ACCELERATORS_INDEXED_NORMAL_SET_HPP_

#include <math.h>
#include <set>
#include <Eigen/Geometry>

namespace GlobalRegistration{

template <class Point, int dim, int _ngSize, typename Scalar>
IndexedNormalSet<Point, dim, _ngSize, Scalar>::~IndexedNormalSet(){
  for(unsigned int i = 0; i != _grid.size(); i++)
    delete(_grid[i]);
}

/*!
 \return Cell id corresponding to p in the euclidean grid
 \warning p must be normalized between 0 and 1
 */
template <class Point, int dim, int _ngSize, typename Scalar>
int
IndexedNormalSet<Point, dim, _ngSize, Scalar>::indexPos(
  const Point& p) const
{
  // Unroll the loop over the different dimensions at compile time
  return Utils::UnrollIndexLoop<VALIDATE_INDICES>( coordinatesPos(p),  dim-1,  _egSize );
}

/*!
 \return Cell id corresponding to p in the euclidean grid
 \warning p must be normalized between 0 and 1
 */
template <class Point, int dim, int _ngSize, typename Scalar>
int
IndexedNormalSet<Point, dim, _ngSize, Scalar>::indexNormal(
  const Point& n) const
{
  // Unroll the loop over the different dimension at compile time
  return Utils::UnrollIndexLoop<VALIDATE_INDICES>( coordinatesNormal(n), dim-1, _ngSize);
}

template <class Point, int dim, int _ngSize, typename Scalar>
int
IndexedNormalSet<Point, dim, _ngSize, Scalar>::indexCoordinatesPos(
  const Point& pCoord) const
{
  // Unroll the loop over the different dimensions at compile time
  return Utils::UnrollIndexLoop<VALIDATE_INDICES>( pCoord,  dim-1,  _egSize );
}

template <class Point, int dim, int _ngSize, typename Scalar>
int
IndexedNormalSet<Point, dim, _ngSize, Scalar>::indexCoordinatesNormal(
  const Point& nCoord) const
{
  // Unroll the loop over the different dimension at compile time
  return Utils::UnrollIndexLoop<VALIDATE_INDICES>( nCoord,  dim-1, _ngSize);
}

template <class Point, int dim, int _ngSize, typename Scalar>
bool
IndexedNormalSet<Point, dim, _ngSize, Scalar>::addElement(
  const Point& p,
  const Point& n,
  unsigned int id)
{
  const int pId = indexPos(p);
  if (pId == -1) return false;

  const int nId = indexNormal(n);
  if (nId == -1) return false;

  if (_grid[pId] == NULL) _grid[pId] = new AngularGrid;
  (_grid[pId])->at(nId).push_back(id);

  return true;
}

template <class Point, int dim, int _ngSize, typename Scalar>
void
IndexedNormalSet<Point, dim, _ngSize, Scalar>::getNeighbors(
  const Point& p,
  std::vector<unsigned int>&nei)
{
  AngularGrid* grid = angularGrid(p);
  if ( grid == NULL ) return;

  for(typename AngularGrid::const_iterator it = grid->cbegin();
      it != grid->cend(); it++){
    const std::vector<unsigned int>& lnei = *it;
    nei.insert( nei.end(), lnei.begin(), lnei.end() );
  }
}

template <class Point, int dim, int _ngSize, typename Scalar>
void
IndexedNormalSet<Point, dim, _ngSize, Scalar>::getNeighbors(
  const Point& p,
  const Point& n,
  std::vector<unsigned int>&nei)
{
  AngularGrid* grid = angularGrid(p);
  if ( grid == NULL ) return;

  const std::vector<unsigned int>& lnei = grid->at(indexNormal(n));
  nei.insert( nei.end(), lnei.begin(), lnei.end() );
}

template <class Point, int dim, int _ngSize, typename Scalar>
void
IndexedNormalSet<Point, dim, _ngSize, Scalar>::getNeighbors(
  const Point& p,
  const Point& n,
  Scalar cosAlpha,
  std::vector<unsigned int>&nei,
  bool tryReverse)
{
  AngularGrid* grid = angularGrid(p);
  if ( grid == NULL ) return;

  const Scalar alpha          = std::acos(cosAlpha);
  const Scalar perimeter      = Scalar(2) * M_PI * std::atan(alpha);
  const unsigned int nbSample = 2*std::ceil(perimeter*Scalar(_ngSize) /Scalar(2.));
  const Scalar angleStep      = Scalar(2) * M_PI / Scalar(nbSample);

  const Scalar sinAlpha       = std::sin(alpha);

  Eigen::Quaternion<Scalar> q;
  q.setFromTwoVectors(Point(0.,0.,1.), n);

  std::set<unsigned int> colored;

  // Do the rendering independently of the content
  for(unsigned int a = 0; a != nbSample; a++){
    Scalar theta    = Scalar(a) * angleStep;
    const Point dir = ( q * Point(sinAlpha*std::cos(theta),
                              sinAlpha*std::sin(theta),
                              cosAlpha ) ).normalized();
    int id = indexNormal( dir );
    if(grid->at(id).size() != 0){
      colored.insert(id);
    }

    if (tryReverse){
      id = indexNormal( -dir );
      if(grid->at(id).size() != 0){
        colored.insert(id);
      }
    }
  }

  for( std::set<unsigned int>::const_iterator it = colored.cbegin();
       it != colored.cend(); it++){
    const std::vector<unsigned int>& lnei = grid->at(*it);
    nei.insert( nei.end(), lnei.begin(), lnei.end() );
  }
}

} // namespace Super4CS

#endif // _INDEXED_NORMAL_SET_HPP_


#include "simulate.hpp"

#include <iostream>
#include <sstream>
#include <boost/program_options.hpp>
#include <fstream>
#include <vector>
#include <algorithm>
#include <boost/math/distributions.hpp>
#include <ctime>
#include <string>
#include <cstdlib>
#include <sys/time.h>
#include <omp.h>
#include <stdio.h>
#include <stdlib.h>

#include "helpers.hpp"
#include "policy.hpp"
#include "stats.hpp"

using namespace std;
using namespace boost::math;
using namespace boost::random;

static const int MAX_ARMS = 10;
static const string WEIGHTS_FILE = "/home/gutin/research/mab/cpp/rewards.config";

void simulate(int arms,
              unsigned long num_trials,
              unsigned long horizon,
              bool gaussian,
              double a_param,
              double b_param,
              const string& tablefile,
              const string& curvefile,
              bool p_stdout)
{
  timeval tv;
  gettimeofday(&tv,NULL);

  boost::mt19937 seed(tv.tv_sec);
  boost::uniform_real<> dist(0.0,1.0);
  boost::normal_distribution<> normdist(0.0,1.0);
  boost::uniform_int<> idist(1,1000000);
  RandomSampleHelper bh(tv.tv_sec);
  RandUnif urandom(seed,dist);
  RandNorm nrandom(seed,normdist);

  RandIntUnif urandomint(seed,idist);

  #ifdef _OPENMP
  int max_threads = omp_get_max_threads();
  RandUnif* rngs[max_threads];
  RandNorm* nrngs[max_threads];
  for(int i = 0; i < max_threads; ++i)
  {
    long randomint = urandomint();
    boost::mt19937 subseed(randomint);
    rngs[i] = new RandUnif(subseed, dist);
    nrngs[i] = new RandNorm(subseed, normdist);
  }
  #endif

  Table3D indices99;
  if (!gaussian && horizon == 999)
  {
    for (unsigned int t = 1; t <= horizon+200; ++t)
    {
      Table indices;
      stringstream ss;
      ss << t;
      string number = ss.str();
      loadFromCSV("/dev/shm/gixs/hor_" + number + ".csv", indices, horizon, horizon);
      indices99.push_back(indices);
    }
  }

  #ifdef SIMMULT
  const int minalpha = 50;
  const int maxalpha = 150;
  const int alphainc = 50;
  vector<Stats> regretmult;
  vector<Stats> g_regretmult;
  int maxsteps = 1;
  for (double j = minalpha; j <= maxalpha; j += alphainc)
  {
    for (int ksteps = 1; ksteps <= maxsteps; ksteps += 2)
    {
      ostringstream os;
      os << "OGI(" << ksteps << ")-" << j;
      string name = os.str();
      regretmult.push_back(Stats(horizon, num_trials, name));
    }
    ostringstream os;
    os << "OGI-" << "-" << j;
    string name = os.str();
    //g_regretmult.push_back(Stats(horizon, num_trials, name));
  }
  #endif
  #ifdef SIMSHD
  Stats regretshd(horizon, num_trials, "OGI-1");
  #endif
  #ifdef SIMSHD2
  Stats regretshd2(horizon, num_trials, "OGI-2");
  #endif
  #ifdef SIMSHD3
  Stats regretshd3(horizon, num_trials, "OGI-3");
  #endif
  #ifdef SIMSHD5
  Stats regretshd5(horizon, num_trials, "OGI-5");
  #endif
  #ifdef SIMNEW
  Stats regretgit1(horizon, num_trials, "OGI-Gaussian");
  #endif
  #ifdef SIMGIT
  Stats regretgit(horizon, num_trials, "OGI");
  #endif
  #ifdef SIMTOM
  Stats regrettom(horizon, num_trials, "TS");
  #endif
  #ifdef SIMNAIVE
  vector<double> regretnaive(horizon, 0);
  #endif
  #ifdef SIMIDS
  Stats regretids(horizon, num_trials, "IDS");
  #endif
  #ifdef SIMIDS2
  Stats regretids2(horizon, num_trials, "IDS2");
  #endif
  #ifdef SIMBUCB
  Stats regretbucb(horizon, num_trials, "Bayes UCB");
  #endif
  #ifdef SIMUCB1
  Stats regretucb1(horizon, num_trials, "UCB1");
  #endif
  #ifdef SHOWTIMING
  clock_t timing = clock();
  #endif

  vector<double> default_mus(arms, 0);
  loadLineFromCSV(WEIGHTS_FILE, default_mus, arms);
  #pragma omp parallel
  {
    #ifdef _OPENMP
    int threadid = omp_get_thread_num();
    #endif

    vector<double> mus(arms, 0);
    for(int i = 0; i < arms; ++i)
      mus[i] = default_mus[i];
    #pragma omp for
    for(unsigned int trial = 0; trial < num_trials; ++trial)
    {
      double mustar = -1;
      int astar = -1;
      Table rewards(arms, vector<double>(horizon, 0));
      vector<double> uniforms(horizon, 0);
      for (int a = 0; a < arms; ++a)
      {
        #ifdef RANDARMS
        #ifdef _OPENMP
	      double mu = gaussian ? (*nrngs[threadid])() : (*rngs[threadid])();
        #endif
        #ifndef _OPENMP
        double mu = gaussian ? nrandom() : urandom();
        #endif
        #else
        double mu = default_mus[a];
        #endif

        if (mu > mustar)
        {
          mustar = mu;
          astar = a;
        }

        for (unsigned long t = 0; t < horizon; ++t)
        {
          #ifdef _OPENMP
          rewards[a][t] = gaussian ? (mu + (*nrngs[threadid])()) : ((*rngs[threadid])() < mu ? 1 : 0);
          #endif
          #ifndef _OPENMP
          rewards[a][t] = gaussian ? (mu + nrandom()) : (urandom() < mu ? 1 : 0);
          #endif
        }
      }
      for (unsigned long t = 0; t < horizon; ++t)
      {
        #ifdef OPENMP
        uniforms[t] = (*rngs[threadif])();
        #else
        uniforms[t] = urandom();
        #endif
      }
      #ifdef SIMMULT
      PolicyCollection<InterestingPolicy> pcl;
      PolicyCollection<GittinsPolicy> gcl;
      for (double j = minalpha; j <= maxalpha; j += alphainc)
      {
        for (int ksteps = 1; ksteps <= maxsteps; ksteps += 2)
          pcl.pcs.push_back( shared_ptr<InterestingPolicy>( new InterestingPolicy(horizon, arms, 1, j, gaussian, ksteps) ) );
        //gcl.pcs.push_back( shared_ptr<GittinsPolicy>( new GittinsPolicy(indices99, horizon, arms, j, gaussian)));
      }
      #endif
      #ifdef SIMNAIVE
      NaivePolicy naip(horizon, arms);
      #endif
      #ifdef SIMTOM
      ThompsonPolicy tomp(bh, horizon, arms, gaussian);
      #endif
      #ifdef SIMIDS
      #ifdef _OPENMP
      IDSPolicy ids(horizon, uniforms, arms, gaussian, 10000);
      #endif
      #ifndef _OPENMP
      IDSPolicy ids(horizon, uniforms, arms, gaussian, 10000);
      #endif
      #endif
      #ifdef SIMIDS2
      IDSPolicy ids2(horizon, uniforms, arms, gaussian, 2000);
      #endif
      #ifdef SIMSHD
        #ifdef _OPENMP
      InterestingPolicy nitp(horizon, arms, a_param, b_param, gaussian);
        #else
      InterestingPolicy nitp(horizon, arms, a_param, b_param, gaussian);
        #endif
      #endif
      #ifdef SIMSHD2
        #ifdef _OPENMP
      InterestingPolicy nitp2(horizon, arms, a_param, b_param, gaussian, 2);
        #else
      InterestingPolicy nitp2(horizon, arms, a_param, b_param, gaussian, 2);
        #endif
      #endif
      #ifdef SIMSHD3
      InterestingPolicy nitp3(horizon, arms, a_param, b_param, gaussian, 3);
      #endif
      #ifdef SIMSHD5
      InterestingPolicy nitp5(horizon, arms, a_param, b_param, gaussian, 5);
      #endif
      #ifdef SIMNEW
      GittinsPolicy gitp99(indices99, horizon, arms, b_param, gaussian);
      #endif
      #ifdef SIMGIT
      GittinsPolicy gitp(indices99, horizon, arms, b_param, gaussian);
      #endif
      #ifdef SIMBUCB
      BayesUCBPolicy bucb(horizon, arms, gaussian);
      #endif
      #ifdef SIMUCB1
      UCB1Policy ucb1(horizon, arms);
      #endif
      for(unsigned long t = 1; t <= horizon; ++t)
      {
        #ifdef SIMMULT
        pcl.simulate(rewards, t, mustar, regretmult);
        gcl.simulate(rewards, t, mustar, g_regretmult);
        #endif
        #ifdef SIMNAIVE
        naip.simulate(rewards, t, mustar, regretnaive);
        #endif
        #ifdef SIMTOM
        tomp.simulate(rewards, t, mustar, regrettom);
        #endif
        #ifdef SIMIDS
        ids.simulate(rewards, t, mustar, regretids);
        #endif
        #ifdef SIMIDS2
        ids2.simulate(rewards, t, mustar, regretids2);
        #endif
        #ifdef SIMSHD
        nitp.simulate(rewards, t, mustar, regretshd);
        #endif
        #ifdef SIMSHD2
        nitp2.simulate(rewards, t, mustar, regretshd2);
        #endif
        #ifdef SIMSHD3
        nitp3.simulate(rewards, t, mustar, regretshd3);
        #endif
        #ifdef SIMSHD5
        nitp5.simulate(rewards, t, mustar, regretshd5);
        #endif
        #ifdef SIMNEW
        gitp99.simulate(rewards, t, mustar, regretgit1);
        #endif
        #ifdef SIMGIT
        if (!gaussian && horizon == 1000)
          gitp.simulate(rewards, t, mustar, regretgit);
        #endif
        #ifdef SIMBUCB
        bucb.simulate(rewards, t, mustar, regretbucb);
        #endif
        #ifdef SIMUCB1
        ucb1.simulate(rewards, t, mustar, regretucb1);
        #endif
      }
    }
  }
  #ifdef SHOWTIMING
  timing = clock() - timing;
  cout << "took " << ((float)timing)/CLOCKS_PER_SEC << " seconds of cpu time" << endl;
  #endif

  vector<Stats*> algo_stats;
  #ifndef PRINT_POLICY
  #ifdef SIMMULT
  for (unsigned int i = 0; i < regretmult.size(); ++i)
    algo_stats.push_back(&regretmult[i]);
  for (unsigned int i = 0; i < g_regretmult.size(); ++i)
    algo_stats.push_back(&g_regretmult[i]);
  #endif
  #ifdef SIMNAIVE
  printRegret(regretnaive, (double)num_trials);
  #endif
  #ifdef SIMNEW
  algo_stats.push_back(&regretgit1);
  #endif
  #ifdef SIMGIT
  if (!gaussian && horizon == 1000)
    algo_stats.push_back(&regretgit);
  #endif
  #ifdef SIMSHD
  algo_stats.push_back(&regretshd);
  #endif
  #ifdef SIMSHD2
  algo_stats.push_back(&regretshd2);
  #endif
  #ifdef SIMSHD3
  algo_stats.push_back(&regretshd3);
  #endif
  #ifdef SIMSHD5
  algo_stats.push_back(&regretshd5);
  #endif
  #ifdef SIMIDS
  algo_stats.push_back(&regretids);
  #endif
  #ifdef SIMIDS2
  algo_stats.push_back(&regretids2);
  #endif
  #ifdef SIMTOM
  algo_stats.push_back(&regrettom);
  #endif
  #ifdef SIMBUCB
  algo_stats.push_back(&regretbucb);
  #endif
  #ifdef SIMUCB1
  algo_stats.push_back(&regretucb1);
  #endif
  #endif
  int num_algos = algo_stats.size();
  for (int i = 0; i < num_algos; ++i)
  {
    Stats* stats = algo_stats[i];
    for (int t = 0; t < stats->clen-1; ++t)
    {
      cout << stats->total_regret[t] << ", ";
    }
    cout << stats->total_regret[stats->clen-1] << endl;
  }
  #ifdef _OPENMP
  for(int i = 0; i < max_threads; ++i)
  {
    delete rngs[i];
    delete nrngs[i];
  }
  #endif
}


#include "Common/Defines.h"
#include "Crypto/Commit.h"
#include "Common/BitVector.h"
//#include "Common/Timer.h"
#include <boost/math/special_functions/binomial.hpp>
#include <boost/multiprecision/cpp_bin_float.hpp>

namespace bOPRF {

	Timer gTimer;
	const block ZeroBlock = _mm_set_epi64x(0, 0);
	const block OneBlock = _mm_set_epi64x(0, 1);
	const block AllOneBlock = _mm_set_epi64x(u64(-1), u64(-1));
	const block CCBlock = ([]() {block cc; memset(&cc, 0xcc, sizeof(block)); return cc; })();

	std::ostream& operator<<(std::ostream& out, const block& blk)
	{
		out << std::hex;
		u64* data = (u64*)&blk;

		out << std::setw(16) << std::setfill('0') << data[0]
			<< std::setw(16) << std::setfill('0') << data[1];

		out << std::dec << std::setw(0);

		return out;
	}

	std::ostream& operator<<(std::ostream& out, const blockBop& blk)
	{
		out << std::hex;
		u64* data = (u64*)&blk;

		out << std::setw(16) << std::setfill('0') << data[0] << "..."
			//<< std::setw(16) << std::setfill('0') << data[1]
			//<< std::setw(16) << std::setfill('0') << data[2]
			//<< std::setw(16) << std::setfill('0') << data[3]
			//<< std::setw(16) << std::setfill('0') << data[4]
			//<< std::setw(16) << std::setfill('0') << data[5]
			<< std::setw(16) << std::setfill('0') << data[6];

		out << std::dec << std::setw(0);

		return out;
	}

	std::ostream* gOut = &std::cout;

	std::ostream& operator<<(std::ostream& out, const Commit& comm)
	{
		out << std::hex;

		u32* data = (u32*)comm.data();

		out << std::setw(8) << std::setfill('0') << data[0]
			<< std::setw(8) << std::setfill('0') << data[1]
			<< std::setw(8) << std::setfill('0') << data[2]
			<< std::setw(8) << std::setfill('0') << data[3]
			<< std::setw(8) << std::setfill('0') << data[4];

		out << std::dec << std::setw(0);

		return out;
	}
	block PRF(const block& b, u64 i)
	{
		//TODO("REMOVE THIS!!");
		//return b;

		block ret, tweak = _mm_set1_epi64x(i), enc;

		ret = b ^ tweak;

		mAesFixedKey.ecbEncBlock(ret, enc);

		ret = ret ^ enc; // H( a0 )

		return ret;
	}

	void split(const std::string &s, char delim, std::vector<std::string> &elems) {
		std::stringstream ss(s);
		std::string item;
		while (std::getline(ss, item, delim)) {
			elems.push_back(item);
		}
	}

	std::vector<std::string> split(const std::string &s, char delim) {
		std::vector<std::string> elems;
		split(s, delim, elems);
		return elems;
	}

	const int tab64[64] = {
		63,  0, 58,  1, 59, 47, 53,  2,
		60, 39, 48, 27, 54, 33, 42,  3,
		61, 51, 37, 40, 49, 18, 28, 20,
		55, 30, 34, 11, 43, 14, 22,  4,
		62, 57, 46, 52, 38, 26, 32, 41,
		50, 36, 17, 19, 29, 10, 13, 21,
		56, 45, 25, 31, 35, 16,  9, 12,
		44, 24, 15,  8, 23,  7,  6,  5 };

	u64 log2floor(u64 value)
	{
		value |= value >> 1;
		value |= value >> 2;
		value |= value >> 4;
		value |= value >> 8;
		value |= value >> 16;
		value |= value >> 32;
		return tab64[((uint64_t)((value - (value >> 1)) * 0x07EDD5E59A4E28C2)) >> 58];
	}

	u64 log2ceil(u64 value)
	{
		return u64(std::ceil(std::log2(value)));
	}

	u64 get_stash_size(u64 neles) {
		if (neles >= (1 << 24))
			return 2;
		if (neles >= (1 << 20))
			return 3;
		if (neles >= (1 << 16))
			return 4;
		if (neles >= (1 << 12))
			return 6;
		if (neles >= (1 << 8))
			return 12;

		return 12; //other

		throw std::runtime_error("get_stash_size: rt error at " LOCATION);

	}

	//template<unsigned int N = 16>
	double getBinOverflowProb(u64 numBins, u64 numBalls, u64 binSize, double epsilon = 0.0001)
	{
		if (numBalls <= binSize)
			return std::numeric_limits<double>::max();

		if (numBalls > unsigned(-1))
		{
			auto msg = ("boost::math::binomial_coefficient(...) only supports " + std::to_string(sizeof(unsigned) * 8) + " bit inputs which was exceeded." LOCATION);
			std::cout << msg << std::endl;
			throw std::runtime_error(msg);
		}

		//try
		//{

			//std::cout << numBalls << " " << numBins << " " << binSize << std::endl;
		typedef boost::multiprecision::number<boost::multiprecision::backends::cpp_bin_float<16>> T;
		T sum = 0.0;
		T sec = 0.0;// minSec + 1;
		T diff = 1;
		u64 i = binSize + 1;

		while (diff > T(epsilon) && numBalls >= i /*&& sec > minSec*/)
		{
			sum += numBins * boost::math::binomial_coefficient<T>(numBalls, i)
				* boost::multiprecision::pow(T(1.0) / numBins, i) * boost::multiprecision::pow(1 - T(1.0) / numBins, numBalls - i);

			//std::cout << "sum[" << i << "] " << sum << std::endl;

			T sec2 = boost::multiprecision::log2(sum);
			diff = boost::multiprecision::abs(sec - sec2);
			//std::cout << diff << std::endl;
			sec = sec2;

			i++;
		}

		return std::max<double>(0, (double)-sec);
		//}
		//catch (std::exception& e)
		//{
		//	if (N == 16)
		//	{
		//		std::cout << "percision failure at " << LOCATION << "\n tring again with high percision (performance penalty)" << std::endl;
		//		// try again with higher percition
		//		return getBinOverflowProb<128>(numBins, numBalls, binSize);
		//	}
		//
		//	std::cout << "retry percision failure at " << LOCATION << "\n" << e.what() << std::endl;
		//	throw;
		//}
	}

	u64 get_bin_size(u64 numBins, u64 numBalls, u64 statSecParam)
	{

		auto B = std::max<u64>(1, numBalls / numBins);

		double currentProb = 0;
		u64 step = 1;

		bool doubling = true;

		while (currentProb < statSecParam || step > 1)
		{
			if (!step)
				throw std::runtime_error(LOCATION);

			if (statSecParam > currentProb)
			{
				if (doubling) step = std::max<u64>(1, step * 2);
				else          step = std::max<u64>(1, step / 2);

				B += step;
			}
			else
			{
				doubling = false;
				step = std::max<u64>(1, step / 2);
				B -= step;
			}
			currentProb = getBinOverflowProb(numBins, numBalls, B);
		}

		return B;
		//if (simpleSize <= cuckooSize)
		//{

		//	if (simpleSize >= (1 << 24))
		//		return 28;
		//	if (simpleSize >= (1 << 20))
		//		return 27;
		//	if (simpleSize >= (1 << 16))
		//		return 26;
		//	if (simpleSize >= (1 << 12))
		//		return 25;
		//	if (simpleSize >= (1 << 8))
		//		return 24;

		//	//return 30;  //other
		//}
		//else
		//{
		//	return simpleSize / cuckooSize + 16 + 4 * std::sqrt(simpleSize * std::log2(cuckooSize) / cuckooSize);
		//}

		//throw std::runtime_error("get_bin_size: rt error at " LOCATION);
	}

	u64 get_codeword_size(u64 neles) {
		if (neles >= (1 << 24))
			return 448 / 8; // in byte
		if (neles >= (1 << 20))
			return 448 / 8;
		if (neles >= (1 << 16))
			return 440 / 8;
		if (neles >= (1 << 12))
			return 432 / 8;
		if (neles >= (1 << 8))
			return 424 / 8;

		return 424 / 8;

		//throw std::runtime_error("get_codeword_size: rt error at " LOCATION);
	}
	u64 get_mask_size(u64 neles, u64 othersize, u64 statSecParam) {

		return (statSecParam + log2(neles * othersize) + 7 ) / 8;

		//if (neles >= (1 << 24))
		//	return 88 / 8;  // in byte
		//if (neles >= (1 << 20))
		//	return 80 / 8;
		//if (neles >= (1 << 16))
		//	return 72 / 8;
		//if (neles >= (1 << 12))
		//	return 64 / 8;
		//if (neles >= (1 << 8))
		//	return 56 / 8;

		//return 56 / 8;
		////return (40 + 2 * log(neles)) / 8;

	//	throw std::runtime_error("get_codeword_size: rt error at " LOCATION);
	}
}