// Copyright 2015-2022 The ALMA Project Developers
//
// 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.

/// @file
/// General MC for 2d materials beyond RTA
/// The function definitions are also located in this file

#include <iostream>
#include <fstream>
#include <structures.hpp>
#include <boost/filesystem.hpp>
#include <boost/mpi.hpp>
#include <Eigen/Dense>
#include <collision_operator.hpp>
#include <sstream>
#include <sampling.hpp>
#include <constants.hpp>
#include <deviational_particle.hpp>
#include <sampling.hpp>
#include <interpolation.hpp>
#include <exceptions.hpp>
#include <functional>
#include <unordered_map>
#include <map>
#include <pcg_random.hpp>
#include <random>
#if BOOST_VERSION >= 106700
#include <boost/container_hash/hash.hpp>
#else
#include <boost/functional/hash.hpp>
#endif
#include <boost/format.hpp>
#include <geometry_2d.hpp>

#include <functional>
#include <io_utils.hpp>
#include <boost/property_tree/xml_parser.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <msgpack.hpp>

#define TBB_PREVIEW_GLOBAL_CONTROL true
#include <tbb/concurrent_vector.h>
#include <tbb/parallel_for.h>
#include <tbb/parallel_for_each.h>
#include <tbb/global_control.h>
#include <tbb/parallel_sort.h>
#include <tbb/enumerable_thread_specific.h>
#include <tbb/concurrent_unordered_set.h>
#include <tbb/concurrent_queue.h>
#include <tbb/task_group.h>
#include <tbb/concurrent_unordered_map.h>
#include <mutex>

///Some alias
using gridData = std::unordered_map<std::string,
        std::unique_ptr<alma::Gamma_grid>>;
using cellData = std::unordered_map<std::string,
        std::unique_ptr<alma::Crystal_structure>>;

using hist_type = std::vector<alma::propagator_H>;
using propagators = std::unordered_map<std::string,
    std::map<double,hist_type>>;

///Specialization of STD
namespace std {
/// Trivial implementation of std::hash for arrays,
/// required to create an unordered_set of arrays.
template <typename T> struct hash<std::array<T, 2>> {
    std::size_t operator()(const array<T, 2>& key) const {
        hash<T> backend;
        std::size_t nruter = 0;

        for (auto& e : key)
            boost::hash_combine(nruter, backend(e));
        return nruter;
    }
};

} // namespace std

///Computes the particle energy
/// @param[in] grid   - phonon spectrum on a regular grid
/// @param[in] Thot   - the maximum expected temperature
/// @param[in] Tcold  - the minimum expected temperature
/// @param[in] Tref   - the reference temperature of the box
/// @param[in] volume - box_volume/unitcell_volume
/// @param[in] nparticles - the number by which divide Eeff_box
inline double calc_Eeff(
    const alma::Gamma_grid&  grid,
    double Thot,
    double Tcold,
    double Tref,
    double volume,
    std::size_t nparticles
) {

    long double dE_hot  = 0.;
    long double dE_cold = 0.;
    for (std::size_t iq = 0; iq < grid.nqpoints; ++iq) {
        auto& spectrum = grid.get_spectrum_at_q(iq);
        for (auto im = 0; im <
            grid.get_spectrum_at_q(0).omega.size(); ++im) {
            if (alma::almost_equal(0., spectrum.omega(im)))
                continue;
            dE_hot  += 1e12 * alma::constants::hbar * spectrum.omega(im) *
                std::abs(alma::bose_einstein(spectrum.omega(im),Thot )
                -alma::bose_einstein(spectrum.omega(im),Tref));
            dE_cold += 1e12 * alma::constants::hbar * spectrum.omega(im) *
                std::abs(alma::bose_einstein(spectrum.omega(im),Tcold)
                -alma::bose_einstein(spectrum.omega(im),Tref));
        }
    }

    //The max is only in case the delta in temperature is not symetrical for some weirdo reason
    return volume*(dE_cold+dE_hot)/ grid.nqpoints / nparticles;
}

namespace box_generators {

///Create particles from initial distribution
///@param[in] sys   - system boxes
///@param[in] grids - grid data for all materials
///@param[in] cells - cell data for all materials
///@param[in] given_dist - the given distributions
///@param[out] new_particles - particles generated by this generator
///@param[in] rng   - random number generator
///@param[in] thickness - thickness of all boxes
///@param[in] Eff - energy of each packet
void init_particles(std::vector<alma::geometry_2d>& sys,
                    gridData& grids,
                    cellData& cells,
                    std::unordered_map<std::size_t,Eigen::VectorXd>& given_dist,
                    std::vector<alma::D_particle>& new_particles,
                    pcg64& rng,
                    std::vector<double>& thickness,
                    double Eff){

    new_particles.clear();
    ///Allocate some memory
    new_particles.reserve(
        static_cast<std::size_t>(5.0e+6));

    if (given_dist.size()==0)
        return;

    for (auto &s : sys) {
        ///If reservoir or peridic ignore
        if (s.reservoir or
            s.periodic)
            continue;

        ///If not given ignore
        if (given_dist.count(s.get_id()) == 0)
            continue;

        auto mat = s.material;

        alma::ref_distribution init_dist(*(grids[mat]),
                                            given_dist[s.get_id()],
                                            rng);

        auto Vucell = cells.at(s.material)->V *
                      thickness[s.get_id()]/
                      cells.at(s.material)->lattvec(2,2);

        auto Vbox  = s.get_area() * thickness[s.get_id()];

        auto Ngen = init_dist.Ntogenerate(Vbox/Vucell,Eff);

        for (std::size_t i=0; i<Ngen; i++) {
            Eigen::Vector2d ppos = s.get_random_point(rng);

            auto pinfo = init_dist.sample_with_sign();

            alma::D_particle newp(ppos,
                                std::get<1>(pinfo),
                                std::get<0>(pinfo),
                                std::get<2>(pinfo),
                                0.,
                                s.get_id());

            new_particles.push_back(newp);
        }
    }

    ///Free some memory
    new_particles.shrink_to_fit();
}
///Creat particles at a T different from reference temperature:
///@param[in] sys   - system boxes
///@param[in] grids - grid data for all materials
///@param[in] cells - cell data for all materials
///@param[out] new_particles - particles generated by init generator
///@param[in] rng   - random number generator
///@param[in] thickness - thickness of all boxes
///@param[in] Eff - energy of each packet
void init_particles(std::vector<alma::geometry_2d>& sys,
                    gridData& grids,
                    cellData& cells,
                    std::vector<alma::D_particle>& new_particles,
                    pcg64& rng,
                    std::vector<double>& thickness,
                    double Eff){

    new_particles.clear();
    ///Allocate some memory
    new_particles.reserve(
        static_cast<std::size_t>(5.0e+6));

    for (auto &s : sys) {
        ///If reservoir or peridic ignore
        if (s.reservoir or
            s.periodic)
            continue;

        ///If init temperature and reference one are equal continue
        if (alma::almost_equal(s.Teq,s.Treal))
            continue;

        auto mat = s.material;

        alma::outTref_distribution init_dist(*(grids[mat]),
                                            s.Treal,
                                            s.Teq,
                                            rng);

        auto Vucell = cells.at(s.material)->V *
                      thickness[s.get_id()]/
                      cells.at(s.material)->lattvec(2,2);

        auto Vbox  = s.get_area() * thickness[s.get_id()];

        auto Ngen = init_dist.Ntogenerate(Vbox/Vucell,Eff);

        for (std::size_t i=0; i<Ngen; i++) {
            Eigen::Vector2d ppos = s.get_random_point(rng);

            auto pinfo = init_dist.sample_with_sign();

            alma::D_particle newp(ppos,
                                std::get<1>(pinfo),
                                std::get<0>(pinfo),
                                std::get<2>(pinfo),
                                0.,
                                s.get_id());

            new_particles.push_back(newp);
        }
    }

    ///Free some memory
    new_particles.shrink_to_fit();
}

///Create the particles in the adiabatic diffuse walls
///@param[in] sys   - system boxes
///@param[in] grids - grid data for all materials
///@param[in] cells - cell data for all materials
///@param[out] new_particles - particles generated by adiabatic diffuse walls
///@param[in] rng   - random number generator
///@param[in] thickness - thickness of all boxes
///@param[in] dt - timestep
///@param[in] Eff - energy of each packet
void adiabatic_diffuse_walls(std::vector<alma::geometry_2d>& sys,
                             gridData& grids,
                             cellData& cells,
                             std::vector<alma::D_particle>& new_particles,
                             pcg64& rng,
                             std::vector<double>& thickness,
                             double dt,
                             double Eff
){
    new_particles.clear();
    new_particles.reserve(100*sys.size());

    ///Generate temperature list
    static std::vector<double> Ts;
    if (Ts.empty()){
        Ts.reserve(100);
        for (auto i=0; i<100;i++){
            Ts.push_back(10.0+10.0*i);
        }
    }

    ///Interpolator to get border temperature
    static std::unordered_map<std::array<std::size_t,2>,
            alma::CubicSpline1D> bTinterpolator;

    ///Iterate through all system
    ///to build wall generators
    for (auto &s : sys) {
        ///If periodic or reservoir ignore
        if (s.reservoir or s.periodic)
            continue;
        std::size_t iborder = 0;

        auto& borders = s.get_borders();

        for (auto &b : borders) {

            std::array<std::size_t,2> genID =
                    {s.get_id(),iborder};

            ///This means is the first time we pass by
            ///we generate an interpolator of J vs Twall
            if (bTinterpolator.count(genID)==0) {
                b.Tboxeq = s.Teq;

                Eigen::VectorXd J(Ts.size()),
                    Tvec(Ts.size());

                for (std::size_t tind = 0; tind < Ts.size();
                    tind++) {

                    Tvec(tind) = Ts[tind];
                    Eigen::Vector3d nwall;
                    nwall <<  -b.np(0), -b.np(1), 0.;
                    alma::Isothermal_wall_distribution
                        myGenWall(*(grids.at(s.material)),Ts[tind],s.Teq,
                                  nwall,rng);

                    ///Calculating the flux
                    J(tind) =
                        myGenWall.get_flux(cells.at(s.material)->V *
                            thickness[s.get_id()]/
                            cells.at(s.material)->lattvec(2,2));
                }
                ///Generating interpolator
                alma::CubicSpline1D c(J,Tvec);
                ///Creating map entry

                bTinterpolator[genID] = c;
            }

            ///If change the equilibrium
            ///reconstruct
            if (b.Tboxeq!=s.Teq) {
                b.Tboxeq = s.Teq;
                ///If b is different from 0
                ///build up generator

                Eigen::VectorXd J(Ts.size()),
                    Tvec(Ts.size());

                for (std::size_t tind = 0; tind < Ts.size();
                    tind++) {
                    Tvec(tind) = Ts[tind];
                    Eigen::Vector3d nwall;
                    nwall <<  -b.np(0), -b.np(1), 0.;
                    alma::Isothermal_wall_distribution
                        myGenWall(*(grids.at(s.material)),Ts[tind],s.Teq,
                                  nwall,rng);

                    ///Calculating the flux
                    J(tind) =
                        myGenWall.get_flux(cells.at(s.material)->V *
                            thickness[s.get_id()]/
                            cells.at(s.material)->lattvec(2,2)
                        );
                }
                ///Generating interpolator
                alma::CubicSpline1D c(J,Tvec);
                ///Modify map entry
                bTinterpolator[genID] = c;
            }

            ///If the border has a flux
            ///add the particles

            if (b.Eborder != 0) {
                ///We calculate the flux
                ///from particle count

                ///std::cout << "# border " << iborder << std::endl;
                double Jborder = Eff *
                    b.Eborder / dt /
                    (b.get_length()*thickness[s.get_id()]);
                ///border temperature
                double Tborder =
                    bTinterpolator[genID].Interpolate(
                    Jborder);
                ///Building generator from
                ///interpolated temperature
                Eigen::Vector3d nwall;
                nwall <<  -b.np(0), -b.np(1), 0.;
                alma::Isothermal_wall_distribution
                    myWallGen(*(grids.at(s.material)),Tborder,s.Teq,
                              nwall,rng);

                ///Genrating particles the number is
                ///the same than incident particles
                std::size_t Ng = (std::abs(b.Eborder) + 1.0e-6);

                for (std::size_t Ni=0; Ni<Ng; Ni++){

                    Eigen::Vector2d ppos = b.get_random_point(rng);

                    auto pinfo = myWallGen.sample_with_sign();

                    alma::D_particle newp(ppos,
                                        std::get<1>(pinfo),
                                        std::get<0>(pinfo),
                                        std::get<2>(pinfo),
                                        0.,
                                        s.get_id());

                    new_particles.push_back(newp);
                }

                b.Eborder = 0;
            }

            iborder++;
        }
    }
}

///@param[in] sys   - system boxes
///@param[in] grids - grid data for all materials
///@param[in] cells - cell data for all materials
///@param[out] new_particles - particles generated by adiabatic diffuse walls
///@param[in] rng   - random number generator
///@param[in] thickness - thickness of all boxes
///@param[in] dt - timestep
///@param[in] Eff - energy of each packet
///@param[in] clear - if clear data
void Isothermal_walls(std::vector<alma::geometry_2d>& sys,
                             gridData& grids,
                             cellData& cells,
                             std::vector<alma::D_particle>& new_particles,
                             pcg64& rng,
                             std::vector<double>& thickness,
                             double dt,
                             double Eff,
                             bool   clear = false
){
    ///Make some room for the new particles
    new_particles.clear();
    new_particles.reserve(1000*sys.size());

    static std::vector<double> oldT;
    static std::unordered_map<std::array<std::size_t,2>,std::pair<
    alma::Isothermal_wall_distribution,
        alma::geom2d_border>> isogens;

    if (oldT.empty()) {
        for (auto s : sys)
            oldT.push_back(s.Teq);
    }

    ///Iterate through system
    for (auto &s : sys) {
        ///If not reservoir ignore
        if (!s.reservoir)
            continue;
        ///Retrive material and box id
        auto smat = s.material;
        auto sid  = s.get_id();

        ///Iterate through contacts:
        ///This is done each timestep
        ///only in case we need to recalculate
        ///the generator
        for (auto &[c,b] : s.get_contacts()){

            if (sys[c].reservoir or sys[c].periodic)
                continue;

            if (alma::almost_equal(b[0].get_length(),0.))
                continue;

            if (sys[c].material != smat) {
                throw  alma::input_error(
                    "Injection from material A to B "
                    "is not allowed\n");
            }
            ///Do not waste time if no particles need to be injected
	    if (alma::almost_equal(s.Teq,sys[c].Teq))
	        continue;

            std::array<std::size_t,2> ids =
                {sid,c};
            ///If there is change in
            ///reservoir or box reference
            ///temperature or at the first
            ///pass create generator
            if (isogens.count(ids) == 0. or
                oldT[sid] != s.Teq       or
                oldT[c] != sys[c].Teq) {

                Eigen::Vector3d nw;
                nw << b[0].np(0) , b[0].np(1), 0;

                alma::Isothermal_wall_distribution
                    igen(*(grids.at(smat)),
                        s.Teq,
                        sys[c].Teq,
                        nw,
                        rng);

                isogens[ids] = std::make_pair(
                    igen,b[0]);
            }
        }//contacts loop
    }

    ///Iterate through
    ///generators to
    ///insert particles
    for (auto &[ids,G] : isogens){
        auto idr  = ids[0];
        auto idi  = ids[1];
        auto mat  = sys[idr].material;
        auto& gen = G.first;
        auto b    = G.second;
        auto Lb   = b.get_length();

        auto spf = Lb*thickness[idr]/
            (cells.at(mat)->V * thickness[idr]/
            cells.at(mat)->lattvec(2,2));

        auto Ngen = gen.Ntogenerate(dt,
                       spf,Eff);

        ///As contact must insert in same material
        for (std::size_t ni=0; ni<Ngen; ni++) {
            Eigen::Vector2d ppos = b.get_random_point(rng);

            //Sanity check
            //if that fails is due to
            //numerical errors:
            if (!sys[idi].inside(ppos)) {
                auto d =
                    sys[idi].get_center() - ppos;
                ppos += 1.0e-6*d/d.norm();
                if (!sys[idi].inside(ppos)) {
                    throw alma::geometry_error(
                        "Generated particle "
                        "out of insertion box\n");
                }
            }

            auto pinfo = gen.sample_with_sign();

            alma::D_particle newp(ppos,
                                std::get<1>(pinfo),
                                std::get<0>(pinfo),
                                std::get<2>(pinfo),
                                0.,
                                idi);

            new_particles.push_back(newp);
        }

    }

    ///Update oldT;
    for (auto s : sys)
        oldT.push_back(s.Teq);

    if (clear) {
        oldT.clear();
        isogens.clear();
    }
}

///@param[in] sys   - system boxes
///@param[in] grids - grid data for all materials
///@param[in] cells - cell data for all materials
///@param[out] new_particles - particles generated by adiabatic diffuse walls
///@param[in] rng   - random number generator
///@param[in] thickness - thickness of all boxes
///@param[in] dt - timestep
///@param[in] Eff - energy of each packet
///@param[in] calc_gradients - should we calculate gradients
///@param[in] gradients - the gradients
void source_term_gradient(
    std::vector<alma::geometry_2d>& sys,
                             gridData& grids,
                             cellData& cells,
                             std::vector<alma::D_particle>& new_particles,
                             pcg64& rng,
                             std::vector<double>& thickness,
                             double dt,
                             double Eff,
                             bool   calc_gradients,
                             Eigen::MatrixXd& gradients
){

    ///If we want to calculate gradients
    ///each timestep
    if (calc_gradients)
        gradients = calculate_gradientT(sys);

    auto nboxes = sys.size();

    static std::unordered_map<std::size_t,
    alma::Nabla_T_distribution> gradient_generators;
    static std::vector<double> ograd_data;
    if (ograd_data.empty()) {
        for (auto i=0;i<gradients.size();i++)
            ograd_data.push_back(
                gradients.data()[i]);
    }

    Eigen::MatrixXd oldgradients(gradients);

    for (std::size_t i = 0; i< nboxes; i++) {
        auto mat = sys[i].material;
        Eigen::Vector3d boxGrad =
            gradients.col(i);
        Eigen::Vector3d oldboxGrad =
            oldgradients.col(i);
        if (alma::almost_equal(boxGrad.norm(),0.))
            continue;

        ///If gradient do not change
        ///and the generator
        ///was already created ignore
        if (alma::almost_equal((boxGrad-
            oldboxGrad).norm(),0) and
            gradient_generators.count(i)==1)
            continue;

        alma::Nabla_T_distribution myGradGen(*(grids[mat]),
                            boxGrad,sys[i].Teq,rng);
        gradient_generators.emplace(std::make_pair(i,myGradGen));
    };

    ///Here we will generate the new particles:
    new_particles.clear();
    for (auto &[igen,gen] : gradient_generators){
        auto mat   = sys[igen].material;
        auto myvol = sys[igen].get_area()*thickness[igen]/
        (cells[mat]->V*thickness[igen]/cells[mat]->lattvec(2,2));
        auto N2gen = gen.Ntogenerate(dt,myvol,Eff);

        for (std::size_t ip = 0; ip < N2gen; ip++) {
            Eigen::Vector2d ppos = sys[igen].get_random_point(rng);
            auto pinfo = gen.sample_with_sign();
            alma::D_particle newp(ppos,
                            std::get<1>(pinfo),
                            std::get<0>(pinfo),
                            std::get<2>(pinfo),
                            0.,
                            igen);
            new_particles.push_back(newp);
        }
    }

    ///Updating gradients data
    for (auto i=0;i<gradients.size();i++)
        ograd_data[i] = gradients.data()[i];
}
};

///Get v:
///We need to clean v values of small values
///@param[in] grid - qpoint grid
///@param[in] ib   - band index
///@param[in] iq   - qpoint index
///@return    phonon group velocity
inline Eigen::Vector2d get_v(const alma::Gamma_grid&  grid,
                             std::size_t ib,
                             std::size_t iq) {

    Eigen::Vector3d v3 = grid.get_spectrum_at_q(iq).vg.col(ib);
    Eigen::Vector2d v2;
    v2 << v3(0) , v3(1);
    for (auto i=0; i<2;i++) {
        if (alma::almost_equal(v2(i),0.))
            v2(i) = 0.;
    }

    return v2;
}

///Particle evolution:
///@param[in,out] particle - deviational particle to evolve
///@param[in] v        - particle velocity
///@param[in] sys      - geometry and box info
///@param[in] dt       - time to evolve
///@param[in] rnd     - random generator
///@param[in] pmutex  - mutex
///@param[in] Novoid  - flag to print info
template <class Random,class Mutex>
void evolparticle(alma::D_particle& particle,
                  Eigen::Vector2d& v,
                  std::vector<alma::geometry_2d>& sys,
                  double dt,
                  Random& rnd,
                  Mutex& pmutex){
    ///particle mutex
    ///to make function thread safe
    ///Save original vector
    Eigen::Vector2d opos(particle.pos);

    ///Check if in reservoir
    if (sys[particle.boxid].reservoir) {
        ///Set to be deleted
        particle.q = 0;
        particle.alpha = 0;
        return;
    }

    ///Apply PBC
    if (sys[particle.boxid].periodic) {
        //pmutex.lock();
        auto PBCsol =
            sys[particle.boxid].translate(opos,v,sys,rnd);
        //pmutex.unlock();

        particle.boxid = PBCsol.first;
        particle.pos   = PBCsol.second;
    }

    Eigen::Vector2d rnew =
        particle.pos + dt*v;

    ///Check if inside the same box:

    if (sys[particle.boxid].inside(rnew)) {

        if (sys[particle.boxid].periodic){
            std::cout << "Periodic boxes do not exist\n";
            std::cout << "r0\n" << opos << std::endl;
            std::cout << "rf\n" << rnew << std::endl;
            std::cout << "dt "  << dt << std::endl;
            std::cout << particle.pos << std::endl;
            std::cout << "v\n" << v << std::endl;
            throw alma::geometry_error("Peridic error\n");
        }

        particle.pos = rnew;
        return;
    }

    ///Check the place it left
    Eigen::Vector2d rp;
    rp << particle.pos(0),particle.pos(1);

    ///Here we calculate where the particle is
    ///going out
    std::tuple<double,
        Eigen::Vector2d,
        std::vector<int>> MRU_sol;

    try {
        MRU_sol =
        sys[particle.boxid].get_inter_side(rp,v,dt);
    }
    catch (const alma::geometry_error& geomerror) {
       Eigen::Vector2d d =
            sys[particle.boxid].get_center() -
            opos;
       particle.pos += 1.0e-6*d/d.norm();
       evolparticle(particle,v,sys,
                    dt,rnd,pmutex);
       return;
    }

    if (std::get<0>(MRU_sol) < 0) {
        throw alma::geometry_error("Error in kinematics\n");
        exit(EXIT_FAILURE);
    }
    ///Calculating leftime
    double left_time = dt - std::get<0>(MRU_sol);
    if (alma::almost_equal(left_time,0.)){
        left_time = 0.;
    }
    rnew = std::get<1>(MRU_sol);

    decltype(rnew) reps = rnew + 1.0e-6 * v/v.norm();

    ///Get contact boxes
    std::set<std::size_t> cboxes;
    for (auto &[ibc,border] :
        sys[particle.boxid].get_contacts()){
        cboxes.insert(ibc);
    }

    ///Check possible boxes to assign:
    std::vector<std::size_t> pboxes;

    for (auto test : cboxes) {
        if (test==particle.boxid) {
            continue;
        }

        auto border_contact = sys[particle.boxid].get_contacts()[test];
        ///If going along border we do not change
        bool parallel = alma::almost_equal((border_contact[0].np).dot(v),0.);

        bool oriented = ((border_contact[0].np).dot(v) > 0.);

        if ((sys[test].inside(rnew) or sys[test].inside(reps) )
             and !parallel and oriented) {
            pboxes.push_back(test);
        }
    }

    ///If going inside the void
    if (pboxes.size()==0) {
        ///Check if three or more
        auto corner_problem =
            in_corner3(rnew,sys);

        if (corner_problem.first){
            //pmutex.lock();
            auto csol =
                correct_corner_problem(
                        rnew,
                        v,
                        sys,
                        corner_problem.second,
                        rnd);
            //pmutex.unlock();
            particle.boxid = csol.first;
            particle.pos = rnew;

            sys[particle.boxid].add_border_Eborder(csol.second,
                            static_cast<int>(particle.sign));

            ///Set to be deleted
            particle.q = 0;
            particle.alpha = 0;
        }
        else {
            std::size_t iborder;
            auto iborders = std::get<2>(MRU_sol);

            if (iborders.size() > 1) {
                ///We first check the borders
                ///that touch void
                std::vector<std::size_t> voidborders;

                for (auto &tvb : iborders) {
                    auto& mb =
                        sys[particle.boxid].get_border(tvb);

                    Eigen::Vector2d rc =
                        rnew + 1.0e-4*mb.np;

                    bool isvoid = true;
                    for (auto &cb :  cboxes) {
                        if (sys[cb].inside(rc)) {
                            isvoid = false;
                            break;
                        }
                    }

                    if (isvoid) {
                        voidborders.push_back(tvb);
                    }
                }

                if (voidborders.empty()) {
                    throw alma::geometry_error("Error in void borders\n");
                }

                //In the case two borders pointing at void
                iborder = *(alma::choose(voidborders.begin(),
                                       voidborders.end(),rnd));
            }
            else {
                iborder = iborders[0];
            }

            sys[particle.boxid].add_border_Eborder(iborder,
                            static_cast<int>(particle.sign));
            ///Set to be deleted
            particle.q = 0;
            particle.alpha = 0;
        }

        return;
    }
    else if (pboxes.size() == 1) {
        ///Change material
        if (sys[particle.boxid].material !=
            sys[pboxes[0]].material){
            throw alma::geometry_error("Error not implemented");
        }

        particle.boxid = pboxes[0];

        if (cboxes.find(particle.boxid)==cboxes.end()) {
            throw alma::geometry_error("Teleport is not allowed");
        }
        particle.pos    = rnew;

        evolparticle(particle,v,sys,
                     left_time,rnd,pmutex);
        return;
    }
    else {
        std::vector<std::size_t> pbox2;

        double tau = 1.0e-6;
        while (true) {
            Eigen::Vector2d rcheck = rnew +
            tau*v;

            for (auto &PB :  pboxes) {
                if (sys[PB].inside(rcheck)) {
                    pbox2.push_back(PB);
                }
            }

            if (pbox2.empty()) {
                tau /= 10.0;
                if (tau < 1.0e-15) {
                    Eigen::Vector2d d =
                    sys[particle.boxid].get_center() -
                    rnew;
                    particle.pos = rnew + 1.0e-6*d/d.norm();
                    evolparticle(particle,v,sys,
                            left_time,rnd,pmutex);
                    return;
                }
            }
            else {
                break;
            }
        }

        particle.boxid = *(alma::choose(pbox2.begin(),
                                      pbox2.end(),rnd));

        if (cboxes.find(particle.boxid)==cboxes.end()) {
            throw alma::geometry_error("Teleport is not allowed");
        }
        particle.pos = rnew;
        evolparticle(particle,v,sys,
                     left_time,rnd,pmutex);

        return;
    }

    throw alma::geometry_error("This point should not be reached");

    return;
}

///RTA Scattering
///Scattering
///@param[in,out] particle - deviational particles to scatter
///@param[in] sys      - geometry and box info
///@param[in] grids     - qgrid data for each material
///@param[in] cells   - cells data for each material
///@param[in] processes_map - map to vector with processes
///@param[in] dt      - time step [ps]
///@param[in] rng     - random number generator
///@param[in] pmutex  - mutex
///@param[in] world   - communicator
void ScatteringRTA(std::vector<alma::D_particle>& particles,
                std::vector<alma::geometry_2d>& sys,
                gridData& grids,
                cellData& cells,
                std::unordered_map<std::string,
                    std::unique_ptr<std::vector<alma::Threeph_process>>>& processes_map,
                double dt,
                tbb::enumerable_thread_specific<pcg64>& rng,
                std::mutex& smutex,
                boost::mpi::communicator& world
                ){
    static tbb::enumerable_thread_specific<
            std::unordered_map<std::string,Eigen::ArrayXXd>> w0;
    static tbb::enumerable_thread_specific<
            std::unordered_map<std::string,alma::BE_derivative_distribution>>
            material_sampler;

    tbb::concurrent_vector<alma::D_particle> newelements;
    newelements.reserve(static_cast<std::size_t>(
        particles.size()));
        tbb::parallel_for(tbb::blocked_range<std::size_t>(0, particles.size()),
                          [&](tbb::blocked_range<std::size_t> ir) {
        for (auto i=ir.begin(); i<ir.end();i++){
            ///Scatter particle i
            alma::D_particle p(particles[i]);
            ///Material
            auto ibox   = p.boxid;
            auto mat    = sys[p.boxid].material;
            //auto nbands = static_cast<std::size_t>(
            //    grids[mat]->
            //    get_spectrum_at_q(0).omega.size());

            auto iq       = p.q;
            auto ib       = p.alpha;

            if (sys[ibox].reservoir or sys[ibox].periodic) {
                std::cout << p.pos << std::endl;
                std::cout << p.boxid << std::endl;
                std::cout << p.q << '\t' << p.alpha << std::endl;
                std::cout << grids[mat]->get_spectrum_at_q(p.q).vg.col(p.alpha) << std::endl;
                exit(EXIT_FAILURE);
            }

            ///If not present calculate
            if (w0.local().count(mat+std::to_string(sys[ibox].Teq)) == 0) {
                Eigen::ArrayXXd w3(
                alma::calc_w0_threeph(*(grids[mat]), *(processes_map[mat]), sys[ibox].Teq, world));
                auto twoph_processes = alma::find_allowed_twoph(*(grids[mat]), world);
                Eigen::ArrayXXd w2(
                    alma::calc_w0_twoph(*(cells[mat]), *(grids[mat]), twoph_processes, world));
                w0.local()[mat+std::to_string(sys[ibox].Teq)] = w3 + w2;
                material_sampler.local()[mat+std::to_string(sys[ibox].Teq)] =
                    alma::BE_derivative_distribution(
                    *(grids[mat]), w0.local()[mat+std::to_string(sys[ibox].Teq)], sys[ibox].Teq, rng.local());
            }

            double my_w0 = w0.local()[mat+std::to_string(sys[ibox].Teq)](ib,iq);
            auto R        = std::uniform_real_distribution(0., 1.)(rng.local());
            bool scatter  = R < (1 - std::exp(-my_w0*dt));
            if (!scatter) {
                newelements.push_back(p);
            }
            else {
                ///Scatter
                auto new_mode = material_sampler.local().at(mat+
                    std::to_string(sys[ibox].Teq)).sample();
                p.q     = new_mode[1];
                p.alpha = new_mode[0];
                newelements.push_back(p);
            }
        }
    });

    ///Storing to STL vector
    particles.clear();
    particles.reserve(newelements.size());
    particles.assign(newelements.begin(),
                     newelements.end());
}

///Scattering
///@param[in,out] particle - deviational particles to scatter
///@param[in] sys      - geometry and box info
///@param[in] modes_histo - histograms for scattering
///@param[in] grids     - qgrid data for each material
///@param[in] rng     - random number generator
///@param[in] pmutex  - mutex
void Scattering(std::vector<alma::D_particle>& particles,
                std::vector<alma::geometry_2d>& sys,
                propagators& modes_histo,
                gridData& grids,
                tbb::enumerable_thread_specific<pcg64>& rng,
                std::mutex& smutex
               ){
    ///Random generators are by
    ///means of their internal state
    ///thread unsafe. Consequenty we are
    ///protecting it with mutex

    ///tbb concurrent vector with new additions
    ///In first iteration it have particles size
    tbb::concurrent_vector<alma::D_particle>
        elements;
    elements.reserve(particles.size());
    elements.assign(particles.begin(),
                    particles.end());

    ///output vector (reserving enough space for each):
    tbb::concurrent_vector<alma::D_particle>
        outputvector;
    outputvector.reserve(static_cast<std::size_t>(
        2*particles.size()));

    ///Exit when no further elements are left
    ///to process
    while(!elements.empty()) {
        tbb::concurrent_vector<alma::D_particle>
            newelements;
        newelements.reserve(2*elements.size());
        tbb::parallel_for(tbb::blocked_range<std::size_t>(0, elements.size()),
                          [&](tbb::blocked_range<std::size_t> ir) {
        for (auto i=ir.begin(); i<ir.end();i++){
            ///Scatter particle i
            alma::D_particle p(elements[i]);
            ///Material
            auto ibox   = p.boxid;
            auto mat    = sys[p.boxid].material;
            auto nbands = static_cast<std::size_t>(
                grids[mat]->
                get_spectrum_at_q(0).omega.size());

            auto iq       = p.q;
            auto ib       = p.alpha;
            auto isign    = static_cast<double>(p.sign);
            auto imode    = iq*nbands + ib;

            if (sys[ibox].reservoir or sys[ibox].periodic) {
                std::cout << p.pos << std::endl;
                std::cout << p.boxid << std::endl;
                std::cout << p.q << '\t' << p.alpha << std::endl;
                std::cout << grids[mat]->get_spectrum_at_q(p.q).vg.col(p.alpha) << std::endl;
                exit(EXIT_FAILURE);
            }

            ///Check if in map if not try to interpolate
            std::shared_ptr<alma::propagator_H> myP;
            if (modes_histo[mat].count(sys[ibox].Teq)==1){
                ///The deleter is designed to do nothing
                ///as we dont want the data to be deleted
                myP.reset(&(modes_histo[mat][sys[ibox].Teq][imode]),
                          [](alma::propagator_H*){});
            }
            else {
                ///Interpolate the propagator
                auto m1 = modes_histo[mat].lower_bound(
                    sys[ibox].Teq);

                auto Tlow = modes_histo[mat].lower_bound(0.)->first;

                auto T1 = m1->first;
                auto m2 = m1;
                std::advance(m2,1);
                if (m2 != modes_histo[mat].end() and
                    sys[ibox].Teq >= Tlow) {

                    auto T2 = m2->first;

                    myP =   std::make_shared<alma::propagator_H>(
                            alma::lirp(modes_histo[mat][T1][imode],
                                     modes_histo[mat][T2][imode],
                                     T1,
                                     T2,
                                     sys[ibox].Teq));
                }
                else{
                    std::cerr << "Extrapolation is not allowed" << std::endl;
                    std::cerr << "Limit Tref between the min and max temperatures\n";
                    std::cerr << "used in Propagator calculation\n" << std::endl;
                    exit(EXIT_FAILURE);
                }
            }

            //smutex.lock();
            auto R        = std::uniform_real_distribution(0., 1.)(rng.local());
            //smutex.unlock();

            try {
                auto solution = myP->get(R);

                auto newq    = solution.second / nbands;
                auto newb    = solution.second % nbands;

                int newsign  = (isign*solution.first) < 0 ? -1 : 1;

                alma::D_particle s1(p);
                s1.q       = newq;
                s1.alpha   = newb;
                s1.sign    = alma::get_particle_sign(newsign);
                outputvector.push_back(s1);

                ///If the particle sign changes we add two p-like particles
                if (newsign*isign < 0.) {
                    newelements.push_back(p);
                    newelements.push_back(p);
                }
            }
            catch (const std::out_of_range) {
                std::cout << "This particle should be deleted" << std::endl;
                std::cout << iq << '\t' << ib << std::endl;
                exit(EXIT_FAILURE);
            }
        }
        });
        ///Getting elements left to process
        std::swap(elements,newelements);
    }

    ///Storing to STL vector
    particles.clear();
    particles.reserve(outputvector.size());
    particles.assign(outputvector.begin(),
                     outputvector.end());
}

///Delete particles
///@param[in,out] particles - list with particles to clean out
void delete_particles(std::vector<alma::D_particle>& particles) {
    particles.erase(std::remove_if(particles.begin(), particles.end(),
                    [&](const alma::D_particle& p)->bool {
                        return (p.q==0 and p.alpha==0);
                    }
    ),particles.end());
}

template <class Iterator>
Iterator search_Dparticle(Iterator first, Iterator last, std::size_t b, std::size_t q, int sign,std::size_t ibox) {
    Eigen::Vector3d pos;
    alma::D_particle search_particle(pos,q,b,alma::get_particle_sign(sign),0.,ibox);
    return std::lower_bound(first,last,search_particle,std::less<alma::D_particle>());
}

///Parameters of input file
struct input_parameters {
    ///Gradient
    bool calc_gradients = true;
    Eigen::Vector2d homoGradient;
    Eigen::MatrixXd gradients;

    ///Geometry
    std::vector<alma::geometry_2d>
        system;

    ///Time
    double dt , maxtime;
    ///Material data
    cellData system_cell;
    gridData system_grid;
    propagators system_P;

    ///Energy factor:
    std::size_t Efactor;
    double Edeviational = -1.;
    ///Vector of thickness
    std::vector<double>
        thicknesses;

    ///Some optional stuff
    double Tmax = 0,Tmin = 0;

    ///Init distribution info
    std::unordered_map<std::size_t,Eigen::VectorXd> init_distribution;

    ///Information if RTA
    bool RTA = false;
    std::unordered_map<std::string,
         std::unique_ptr<std::vector
         <alma::Threeph_process> >> processes_map;
};

inline Eigen::VectorXd get_column(std::string name,int col) {
    std::ifstream ifile;
    ifile.open(name);
    std::string line;

    std::vector<double> f_;

    while(std::getline(ifile,line)){
        ///Ignore commented lines
        if (line.find("#")!=std::string::npos)
            continue;
        auto sline = alma::tokenize_homogeneous_line<double>(line,"\t");
        f_.push_back(sline[col]);
    }

    Eigen::VectorXd f = Eigen::Map<Eigen::VectorXd>(f_.data(), f_.size());

    ifile.close();

    return f;
}

///This reads the input file
///@param[in] filename - input filename with path
///@param[in] world    - mpi communicator
///@return structure with input parameters
input_parameters
process_input(std::string& filename,
              boost::mpi::communicator& world
){
    // Create empty property tree object
    boost::property_tree::ptree tree;

    // Parse XML input file into the tree
    boost::property_tree::read_xml(filename, tree);

    input_parameters inpars;
    ///Map to store thickness
    std::map<std::string,double> zmap;

    ///TODO: interface stuff
    for (const auto& v : tree.get_child("beRTAMC2D")) {
        ///Reading geometry
        if (v.first == "geometry") {
            std::string gfname =
                alma::parseXMLfield<std::string>(v, "file");
            inpars.system =
                alma::read_geometry_XML(gfname);
        }
        if (v.first == "time"){
            inpars.dt =
                alma::parseXMLfield<double>(v, "dt");
            inpars.maxtime =
                alma::parseXMLfield<double>(v, "maxtime");
        }
        if (v.first == "gradient") {
            inpars.calc_gradients = false;
            inpars.homoGradient(0) =
               alma::parseXMLfield<double>(v, "x");
            inpars.homoGradient(1) =
               alma::parseXMLfield<double>(v, "y");
        }
        if (v.first == "material") {
            std::string name =
                alma::parseXMLfield<std::string>(v, "name");
            std::string hdf5file =
                alma::parseXMLfield<std::string>(v, "database");
            double zsize =
                alma::parseXMLfield<double>(v, "thickness");
            auto data =
                alma::load_bulk_hdf5(hdf5file.c_str(), world);
            inpars.system_cell[name] =
                std::move(std::get<1>(data));
            inpars.system_grid[name] =
                std::move(std::get<3>(data));

            inpars.processes_map[name] =
                std::move(std::get<4>(data));

            zmap[name] = zsize;
            std::map<double,hist_type> histoByT;

            ///Now reading the propagators
            for (auto it = v.second.begin(); it != v.second.end(); it++) {
                if (it->first == "propagator") {
                    double T =
                        alma::parseXMLfield<double>(*it, "T");
                    std::string Pfname =
                        alma::parseXMLfield<std::string>(*it, "file");
                    Eigen::MatrixXd P;
                    alma::load_P(Pfname,P,world);
                    hist_type that_histo;
                    that_histo.reserve(3+P.cols());

                    ///Fill up gamma acoustic modes
                    ///It will fail if accessed through member functions
                    for (auto ax : {0,1,2})
                        that_histo.emplace_back();
                    ///Fill up other modes
                    for (auto ic = 0; ic < P.cols(); ic++)  {
                        Eigen::VectorXd pcol = P.col(ic);
                        that_histo.emplace_back(pcol);
                    }
                    histoByT[T] = that_histo;
                }
            }

            inpars.system_P[name] = histoByT;
        }

        if (v.first == "Eeff") {
            if (alma::probeXMLfield<double>(v,"particles")) {
            	inpars.Efactor =
                	alma::parseXMLfield<std::size_t>(v, "particles");
            }
            if (alma::probeXMLfield<double>(v,"Ed")) {
            	inpars.Edeviational =
                	alma::parseXMLfield<double>(v, "Ed");
	    }

            if (alma::probeXMLfield<double>(v,"Tmax")) {
                inpars.Tmax =
                    alma::parseXMLfield<double>(v, "Tmax");
                inpars.Tmin =
                    alma::parseXMLfield<double>(v, "Tmin");
            }
        }

        if (v.first == "RTA") {
            inpars.RTA = true;
        }

        if (v.first == "transmission") {
            std::cout << "This is currently not supported\n";
            world.abort(1);
        }

        if (v.first == "init_distribution") {
            auto ibox =
                alma::parseXMLfield<std::size_t>(v, "box");
            std::string fd_name =
                alma::parseXMLfield<std::string>(v, "file");
            inpars.init_distribution[ibox] = get_column(fd_name,0);
        }

    }

    if (inpars.system_cell.size()>1){
        std::cout << "This is currently not supported\n";
        world.abort(1);
    }

    ///Fill thicknesses
    for (std::size_t i=0;i<inpars.system.size();i++) {
        auto mat = inpars.system[i].material;
        //std::cout << mat << '\t' << zmap[mat] << std::endl;
        inpars.thicknesses.push_back(
            zmap[mat]);
    }

    if (!inpars.RTA)
        inpars.processes_map.clear();

//     std::cout << "Thickness\n";
//     for (auto i=0;i<inpars.system.size();i++)
//         std::cout << inpars.thicknesses[i] << std::endl;

    inpars.gradients.resize(3,inpars.system.size());
    inpars.gradients.setZero();

    ///If gradient is calc
    if (inpars.calc_gradients) {
      inpars.gradients  =
        calculate_gradientT(inpars.system);
    }
    else{ ///Otherwise we set it to a given value
       for (std::size_t i=0;i<inpars.system.size();i++) {
           if (!inpars.system[i].reservoir
               and !inpars.system[i].periodic) {
               inpars.gradients(0,i) =
                 inpars.homoGradient(0);
               inpars.gradients(1,i) =
                 inpars.homoGradient(1);
           }
       }
    }

    return inpars;
}

class D_particle_map {
public:
    std::vector<Eigen::VectorXi> histograma;
//

    D_particle_map() = default;

    D_particle_map(const D_particle_map& A) {
        histograma.reserve(A.histograma.size());
        for (auto &i : A.histograma){
            histograma.emplace_back(i);
        }
    }

    D_particle_map(const std::vector<std::pair<std::size_t,std::size_t>>& histosizes) {
        histograma.resize(histosizes.size());
        for (std::size_t ibox = 0 ; ibox < histosizes.size(); ibox++) {
                histograma[ibox].resize(
                    histosizes[ibox].first *
                    histosizes[ibox].second );
                histograma[ibox].setZero();
        }
    }

};

std::pair<
std::vector<double>,
std::vector<Eigen::Vector3d>>
calculate_prop(gridData& grids,
               std::vector<alma::geometry_2d>& sys,
               std::vector<double>& volume,
               double Eff,
               std::size_t nboxes,
               std::vector<alma::D_particle>& particles) {

    std::vector<Eigen::Vector3d> flux(nboxes);
    std::vector<double> ed(nboxes);
    for (std::size_t i=0;i<nboxes;i++)
        flux[i].setZero();

    ///vg is in nm/ps
    ///volume is in nm**3
    ///Energy is in J
    ///Flux units J/(ps * nm**2)

    for (auto &p : particles) {
        auto mat = sys[p.boxid].material;
        auto iq = p.q;
        auto ib = p.alpha;
        auto& spectrum = grids[mat]->
            get_spectrum_at_q(iq);
        Eigen::VectorXd vg = spectrum.vg.col(ib);
        int signum = static_cast<int>(p.sign);
        auto ibox = p.boxid;

        ed[ibox]   += signum;
        flux[ibox] += signum*vg;
    }
    ///We want SI
    for (std::size_t i=0;i<nboxes;i++){
        ed[i]   *= Eff/volume[i] * 1.0e+27;
        flux[i] *= Eff/volume[i] * 1e12 * 1e9 * 1e9;
    }
    return std::make_pair(ed,flux);
};

std::pair<
std::vector<double>,
std::vector<Eigen::Vector3d>>
calculate_prop(gridData& grids,
               std::vector<alma::geometry_2d>& sys,
               std::vector<double>& volume,
               double Eff,
               D_particle_map& H) {

    std::mutex blocker;
    static std::map<std::string,
                    Eigen::VectorXd> vx, vy;
    static std::size_t nboxes =
        H.histograma.size();

    ///Quantities to store
    std::vector<Eigen::Vector3d> flux(nboxes);
    std::vector<double> ed(nboxes);
    for (std::size_t i=0;i<nboxes;i++)
        flux[i].setZero();

    tbb::parallel_for(tbb::blocked_range<std::size_t>(0, nboxes),[&](tbb::blocked_range<std::size_t> box_range) {
        for (auto ibox = box_range.begin(); ibox < box_range.end(); ibox++){
            ///Ignore reservoirs and periodic boxes
            if (sys[ibox].reservoir or sys[ibox].periodic)
                continue;

            auto material = sys[ibox].material;
            auto nq = grids.at(material)->nqpoints;
            auto nb = grids.at(material)->get_spectrum_at_q(0).omega.size();

            ///If empty create vx and vy
            blocker.lock();
            if (vx.count(material) == 0) {
                Eigen::VectorXd vx_(nq * nb) , vy_(nq*nb);

                for ( decltype(nq) iq = 0; iq < nq; iq ++ )  {
                    auto sp = grids.at(material)->get_spectrum_at_q(iq);
                    for (int ib = 0; ib < nb; ib++) {
                        vx_(iq*nb+ib) = sp.vg(0,ib);
                        vy_(iq*nb+ib) = sp.vg(1,ib);
                    }
                }

                vx.emplace(material,vx_);
                vy.emplace(material,vy_);

            }
            blocker.unlock();

            double edfactor = Eff / volume[ibox] * 1.0e+27;
            double fluxfactor = Eff / volume[ibox]  * 1.0e+30;

            ed[ibox] = edfactor *
                static_cast<double>(H.histograma[ibox].sum());

            flux[ibox](0) = fluxfactor * (H.histograma[ibox].cast<double>().array() *
                vx[material].array()).sum();

            flux[ibox](1) = fluxfactor * (H.histograma[ibox].cast<double>().array() *
                vy[material].array()).sum();

        }
    });

    return std::make_pair(ed,flux);

}

class data {
public:
    double time;
    double Eeff;
    std::vector<double> temperatures;
    D_particle_map histo;

    template <typename Packer>
    void msgpack_pack(Packer& pk) const {

        std::size_t nboxes = histo.histograma.size();

        std::vector<std::size_t> nzeros(
            nboxes);

        for (std::size_t i = 0; i < nzeros.size();i++) {
           nzeros[i] =  (histo.histograma[i].array() != 0).count();
        }

        ///Count elements 3 + 2*nboxes
        std::size_t nelements = 3 + 2*nboxes;
        // make array of two elements, by the number of class fields
        pk.pack_array(nelements);

        // pack the first field, time
        pk.pack_double(time);
        // pack the second field Eeff
        pk.pack_double(Eeff);
        // since it is array of doubles, we can't use direct conversion or copying
        // memory because it would be a machine-dependent representation of floats
        // instead, we converting this POD array to some msgpack array, like this:

        pk.pack_array(temperatures.size());
        for (const auto &t : temperatures) {
            pk.pack_double(t);
        }

        /// We are now working the histograms:
        for (std::size_t ibox = 0; ibox<nzeros.size();ibox++){
            if (nzeros[ibox] == 0) {
                pk.pack_uint64(0);
                pk.pack_array(1);
                pk.pack_double(42.0);
                continue;
            }

            ///This block is solved at compile time
            if constexpr (sizeof(std::size_t)==sizeof(std::uint8_t)) {
                pk.pack_uint8(static_cast<std::uint8_t>(2*nzeros[ibox]));
            }
            else if constexpr (sizeof(std::size_t)==sizeof(std::uint16_t)) {
                pk.pack_uint16(static_cast<std::uint16_t>(2*nzeros[ibox]));
            }
            else if constexpr (sizeof(std::size_t)==sizeof(std::uint32_t)) {
                pk.pack_uint32(static_cast<std::uint32_t>(2*nzeros[ibox]));
            }
            else if constexpr (sizeof(std::size_t)==sizeof(std::uint64_t)) {
                pk.pack_uint64(static_cast<std::uint64_t>(2*nzeros[ibox]));
            }
            else {
                static_assert(sizeof(std::size_t)==sizeof(std::uint8_t)||
                                      sizeof(std::size_t)==sizeof(std::uint16_t)||
                                      sizeof(std::size_t)==sizeof(std::uint32_t)||
                                      sizeof(std::size_t)==sizeof(std::uint64_t),
                                      "std::size_t cannot be mapped to msgpack type"
                        );
                exit(1);
            }

            pk.pack_array(2*nzeros[ibox]);
            //for (std::size_t j=0; j<histo[ibox].count.size(); j++){
            for (int j=0;j<histo.histograma[ibox].rows();j++) {
                int val = histo.histograma[ibox][j];
                if (val!=0) {

                    ///This block is solved at compile time
                    if constexpr (sizeof(std::size_t)==sizeof(std::uint8_t)) {
                        pk.pack_uint8(static_cast<std::uint8_t>(j));
                    }
                    else if constexpr (sizeof(std::size_t)==sizeof(std::uint16_t)) {
                        pk.pack_uint16(static_cast<std::uint16_t>(j));
                    }
                    else if constexpr (sizeof(std::size_t)==sizeof(std::uint32_t)) {
                        pk.pack_uint32(static_cast<std::uint32_t>(j));
                    }
                    else if constexpr (sizeof(std::size_t)==sizeof(std::uint64_t)) {
                        pk.pack_uint64(static_cast<std::uint64_t>(j));
                    }
                    else {
                        static_assert(sizeof(std::size_t)==sizeof(std::uint8_t)||
                                      sizeof(std::size_t)==sizeof(std::uint16_t)||
                                      sizeof(std::size_t)==sizeof(std::uint32_t)||
                                      sizeof(std::size_t)==sizeof(std::uint64_t),
                                      "std::size_t cannot be mapped to msgpack type"
                        );
                        exit(1);
                    }

                    ///This block is solved at compile time
                    if constexpr (sizeof(int)==sizeof(std::int8_t)) {
                        pk.pack_int8(static_cast<std::int8_t>(val));
                    }
                    else if constexpr (sizeof(int)==sizeof(std::int16_t)) {
                        pk.pack_int16(static_cast<std::int16_t>(val));
                    }
                    else if constexpr (sizeof(int)==sizeof(std::int32_t)) {
                        pk.pack_int32(static_cast<std::int32_t>(val));
                    }
                    else if constexpr (sizeof(int)==sizeof(std::int64_t)) {
                        pk.pack_int64(static_cast<std::int64_t>(val));
                    }
                    else {
                        static_assert(sizeof(int)==sizeof(std::int8_t)||
                                      sizeof(int)==sizeof(std::int16_t)||
                                      sizeof(int)==sizeof(std::int32_t)||
                                      sizeof(int)==sizeof(std::int64_t),
                                      "std::ptrdiff_t cannot be mapped to msgpack type"
                        );
                        exit(1);
                    }

                }
            }
        }
    }
};

class saver {
private:
    std::ofstream out;
public:

    saver(std::string fname) {
        out.open(fname.c_str());
    }
    ~saver(){
        out << "0#" ;
        out.close();
    }

    bool save_frame(double time,
                    double Eeff,
                    std::vector<double> temperatures,
                    D_particle_map& histo) {

        data frame_data;
        frame_data.time = time;
        frame_data.Eeff = Eeff;
        frame_data.temperatures = temperatures;
        frame_data.histo = std::move(histo);

        std::stringstream buffer;
        msgpack::pack(buffer, frame_data);

        std::size_t block_size = buffer.str().size();

        out << block_size << "#" << buffer.str();
        return false;
    }

};

///To do asyncronous operations for I/O
///is built on top of tbb/concurrent_queue and
///tbb tasks_group
namespace Queue {

///@param[in,out] tasks - queue containing the jobs to do
///@param[in]     kill  - kill signal
void work_list(tbb::concurrent_bounded_queue<std::function<bool()>>& tasks,bool& kill){
    while (true) {
        if (!tasks.empty()) {
            std::function<bool()> task;
            tasks.pop(task);
            if (task()){
                break;
            }
        }
        else {
            if(kill)
                break;
        }
    }
}

///This class allows one to use TBB to
///set a thread to IO operations:

class single_worker {
private:
    ///Task taking care of the list
    tbb::task_group my_group;
    ///The queue of jobs
    tbb::concurrent_bounded_queue<std::function<bool()>> task_list;
    ///Kill signal
    bool kill = false;
    ///Helper to hold the task function-loop
    std::function<void()> exec_internal;

public:
    ///Basic constructor
    single_worker() {
        exec_internal = std::bind(work_list,std::ref(task_list),std::ref(kill));
        my_group.run(exec_internal);
    }

    ///Be sure all threads have finished
    ~single_worker(){
        my_group.wait();
    };

    ///Pushing function job to list
    ///@param[in] job - function with the job
    void push_job(std::function<bool()>&& job) {
        task_list.push(std::move(job));
    }

    ///To finish (it is kill signal)
    void finish() {
        this->kill = true;
    }

};

};

///Here we will generate

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

    if (world.size()>1) {
        std::cout << "Cannot run in MPI" << std::endl;
        std::cout << "Multithreading is allowed via TBB" << std::endl;
        world.abort(1);
    }

    auto my_id = world.rank();

    if (my_id == 0) {
        std::cout << "********************************************"
                  << std::endl;
        std::cout << "This is ALMA/beyondRTA_MC2d version "
                  << ALMA_VERSION_MAJOR << "." << ALMA_VERSION_MINOR
                  << std::endl;
        std::cout << "********************************************"
                  << std::endl;
    }

    // Check that the right number of arguments have been provided.
    if (argc != 3) {
        if (my_id == 0) {
            std::cerr << boost::format(
                             "Usage: %1% <inputfile.xml> nthreads") %
                             argv[0]
                      << std::endl;
        }
        return 1;
    }

    std::size_t nthreadsTBB = std::atoi(argv[2]);

    if (nthreadsTBB < 2) {
        throw alma::input_error("Two threads is the minum required");
    }

    tbb::global_control control(
        tbb::global_control::max_allowed_parallelism,
                                nthreadsTBB);

    ///Create the worker:
    Queue::single_worker writer;

    ///Process the file:
    std::string inputfile = argv[1];

    input_parameters inpars =
        std::move(process_input(inputfile,world));

    bool calc_gradients =
        inpars.calc_gradients;
    Eigen::MatrixXd gradients =
        inpars.gradients;

    //std::cout << gradients << std::endl;

    ///Geometry
    std::vector<alma::geometry_2d>
        system;
    std::swap(inpars.system,system);
    auto nboxes =
        system.size();

//     std::cout << nboxes << std::endl;
//     for (auto &s :  system) {
//         std::cout << s.get_id() <<std::endl;
//         std::cout << s.Teq << std::endl;
//         std::cout << s.periodic << std::endl;
//         if (s.periodic)
//             std::cout << "T\n" << s.translation << std::endl;
//     }

    std::vector<double>
        thicknesses;
    std::swap(thicknesses,
              inpars.thicknesses);

    ///Time
    double dt = inpars.dt;
    double maxtime = inpars.maxtime;

    ///Eeff:
    auto Efactor = inpars.Efactor;

    ///Material data
    cellData system_cell =
        std::move(inpars.system_cell);
    gridData system_grid =
        std::move(inpars.system_grid);
    propagators system_P =
        std::move(inpars.system_P);

    ///The random generator
    pcg64 rng;
    tbb::enumerable_thread_specific<pcg64>rng_;

    ///Get the Eeff from guess
    double maxT  = 0.;
    double minT  = 1.416784e+32;
    double meanT = 0.;
    std::size_t nb_ = 0;
    std::vector<double> volsf(nboxes);
    std::vector<double> vols(nboxes);

    for (std::size_t i=0; i<nboxes; i++) {
        auto mat = system[i].material;
        auto area =
            system[i].get_area();

//         std::cout << "box "<< i << '\t' << area << '\n';
        double corrected_vuc =
            thicknesses[i] *
            system_cell[mat]->V /
            system_cell[mat]->lattvec(2,2);
        vols[i]  = area *
            thicknesses[i];
        volsf[i] = area *
            thicknesses[i]/corrected_vuc;

    }

    for (std::size_t i=0; i<nboxes; i++) {

        if (system[i].periodic)
            continue;

        auto boxT = system[i].Teq;

        meanT += boxT;
        nb_++;

        if (boxT < minT)
            minT = boxT;
        if (boxT > maxT)
            maxT = boxT;
    }

    if (inpars.Tmax != 0) {
        maxT = inpars.Tmax;
        minT = inpars.Tmin;
    }

    meanT /= nb_;

    std::vector<double> effs;
    double Eeff;

    if (inpars.Edeviational < 0.) {

    	for (std::size_t i=0; i<nboxes; i++) {

    	    if (system[i].periodic or
    	        system[i].reservoir
    	    )
    	        continue;
    	    auto mat = system[i].material;
    	    effs.push_back(calc_Eeff(
    	            *(system_grid[mat]),
    	            maxT,
    	            minT,
    	            meanT,
    	            volsf[i],
    	            Efactor));
    	}

    	Eeff = *(std::min_element(
    	    effs.begin(),effs.end()));
    }
    else {
	Eeff = inpars.Edeviational;
    }
    ///Print inf
    if (inpars.RTA)
        std::cout << "#RTA version activated\n";
    ///particles
    std::vector<alma::D_particle> particles;
    auto time = 0.;
    std::size_t istep = 0;
    ///Some definitions:
    using vpartIt = decltype(particles.begin());
    std::function<vpartIt(vpartIt,
                          vpartIt,
                          std::size_t,
                          std::size_t,
                          int,
                          std::size_t)> seeker =
                          search_Dparticle<vpartIt>;
    std::mutex cerberus;
    std::size_t pcancel_each = 4;//static_cast<std::size_t>(std::floor(2.5/dt) +1.0e-6);

    std::unordered_map<
        std::array<std::size_t,2>,
        std::ptrdiff_t> particle_count;

    std::vector<double> temperatures(system.size());
    for (auto &s : system)
        temperatures[s.get_id()] = s.Teq;

    saver Register("properties.msgpack.bin");

    ///Init particles from init given distributions
    if (inpars.init_distribution.size()!=0)
        box_generators::init_particles(system,system_grid,system_cell,
                                       inpars.init_distribution,
                                       particles,rng,thicknesses,Eeff);

    ///Init particles from temperature different from that
    ///of equilibrium (it will discard any previous loaded distribution)
    box_generators::init_particles(system,system_grid,system_cell,
		                   particles,rng,thicknesses,Eeff);
    std::cout << "#Inited deviational particles from initial conditions" << std::endl;
    std::cout << "#Init particles: "<< particles.size() << std::endl;
    std::cout << "#Eeff is: "<< Eeff << " J" << std::endl;

    auto prop0 = calculate_prop(system_grid,system,vols,Eeff,system.size(),particles);
    auto flux0 = prop0.second;
    auto ed0   = prop0.first;

    std::cout << istep << '\t' << time << '\t' << particles.size()  << '\t';
    for (std::size_t ibox = 0; ibox < system.size(); ibox++)
        std::cout << ed0[ibox] << '\t' << flux0[ibox](0) << '\t' << flux0[ibox](1) << '\t';
    std::cout << std::endl;
    std::cout << std::flush;

    std::vector<std::pair<std::size_t,std::size_t>> histosizes;
    for (auto &s : system) {
        auto mat = s.material;
        auto nq  = system_grid[mat]->nqpoints;
        auto nb  = system_grid[mat]->get_spectrum_at_q(0).omega.size();
        if (!s.reservoir and !s.periodic) {
            histosizes.push_back({nq,nb});
        }
        else {
            histosizes.push_back({1,1});
        }
    }

    const auto sign_minus = alma::get_particle_sign(-1.0);

    while (time <= maxtime) {
        std::cout << "#Advection" << std::endl;
        auto tA = std::chrono::high_resolution_clock::now();
        ///Move particles until non-periodic boundary or the end
        tbb::parallel_for(tbb::blocked_range<std::size_t>(0, particles.size()),[&](tbb::blocked_range<std::size_t> ir) {
            for (auto i=ir.begin(); i<ir.end();i++){
                auto mat = system[
                    particles[i].boxid].material;
                auto v = get_v(*(system_grid[mat]),
                               particles[i].alpha,particles[i].q);
                evolparticle(particles[i],v,system,dt,rng_.local(),cerberus);
            }
        });
        auto tB = std::chrono::high_resolution_clock::now();
        std::cout << "#Advection  => DONE in "<<
        (static_cast<std::chrono::duration<double>>(tB - tA)).count()
        << " s" << std::endl;

        std::cout << "#Generators" << std::endl;
        ///2.Add particle from source term and evolve it a random time
        std::vector<alma::D_particle> grad_particles;
        grad_particles.reserve(particles.size());
        box_generators::source_term_gradient(
                             system,
                             system_grid,
                             system_cell,
                             grad_particles,
                             rng,
                             thicknesses,
                             dt,
                             Eeff,
                             calc_gradients,
                             gradients);

        tbb::parallel_for(tbb::blocked_range<std::size_t>(0, grad_particles.size()),[&](tbb::blocked_range<std::size_t> ir) {
            for (auto i=ir.begin(); i<ir.end();i++){
                auto mat = system[
                    grad_particles[i].boxid].material;
                auto v = get_v(*(system_grid[mat]),
                               grad_particles[i].alpha,grad_particles[i].q);
                auto rdt = dt * std::uniform_real_distribution<double>(0., 1.)(rng_.local());
                evolparticle(grad_particles[i],v,system,rdt,rng_.local(),cerberus);
            }
        });

        std::cout <<"##Source term: " << grad_particles.size() << std::endl;

        particles.insert( particles.end(),grad_particles.begin(),
                          grad_particles.end());

        ///3. Add particles from Isothermal walls and evolve random time
        std::vector<alma::D_particle> isowall_particles;
        box_generators::Isothermal_walls(
                         system,
                         system_grid,
                         system_cell,
                         isowall_particles,
                         rng,
                         thicknesses,
                         dt,
                         Eeff);
        tbb::parallel_for(tbb::blocked_range<std::size_t>(0, isowall_particles.size()),[&](tbb::blocked_range<std::size_t> ir) {
            for (auto i=ir.begin(); i<ir.end();i++){
                auto mat = system[
                    isowall_particles[i].boxid].material;
                auto v = get_v(*(system_grid[mat]),
                               isowall_particles[i].alpha,isowall_particles[i].q);
                auto rdt = dt * std::uniform_real_distribution<double>(0., 1.)(rng_.local());
                evolparticle(isowall_particles[i],v,system,rdt,rng_.local(),cerberus);
            }
        });
        std::cout <<"##Isothermal walls: " << isowall_particles.size() << std::endl;
        particles.insert( particles.end(),isowall_particles.begin(),
                          isowall_particles.end());

        ///4. Add particles from adiabatic diffuse walls and evolve random time
        std::vector<alma::D_particle> adiabatic_particles;
        box_generators::adiabatic_diffuse_walls(
                             system,
                             system_grid,
                             system_cell,
                             adiabatic_particles,
                             rng,
                             thicknesses,
                             dt,
                             Eeff);

        tbb::parallel_for(tbb::blocked_range<std::size_t>(0, adiabatic_particles.size()),[&](tbb::blocked_range<std::size_t> ir) {
            for (auto i=ir.begin(); i<ir.end();i++){
                auto mat = system[
                    adiabatic_particles[i].boxid].material;
                auto v = get_v(*(system_grid[mat]),
                               adiabatic_particles[i].alpha,adiabatic_particles[i].q);
                auto rdt = dt * std::uniform_real_distribution<double>(0., 1.)(rng_.local());
                evolparticle(adiabatic_particles[i],v,system,rdt,rng_.local(),cerberus);
            }
        });
        std::cout << "##Adiabatic diffuse walls: " << adiabatic_particles.size() << std::endl;
        particles.insert( particles.end(),adiabatic_particles.begin(),
                          adiabatic_particles.end());
        auto tC = std::chrono::high_resolution_clock::now();
        std::cout << "#Generators => DONE in "<<
        (static_cast<std::chrono::duration<double>>(tC - tB)).count()
        << " s" << std::endl;

        ///Clean particles vector from those marked to delete
        delete_particles(particles);

        ///Scattering section
        std::cout << "#Scattering" << std::endl;
        if (inpars.RTA) {
            ScatteringRTA(particles,system,system_grid,system_cell,inpars.processes_map,dt,rng_,cerberus,world);
        }
        else{
            Scattering(particles,system,system_P,system_grid,rng_,cerberus);
        }
        auto tD = std::chrono::high_resolution_clock::now();
        std::cout << "#Scattering => DONE in "<<
        (static_cast<std::chrono::duration<double>>(tD - tC)).count()
        << " s" << std::endl;

        ///Cancel section:
        bool cancel_flag = (istep % pcancel_each == 0);
        std::cout << "#Canceling and save" << std::endl;

        D_particle_map H(histosizes);

        auto tD1 = std::chrono::high_resolution_clock::now();

        for (auto &p : particles) {
            auto ibox = p.boxid;
                    auto nb   = histosizes[ibox].second;
            H.histograma[ibox](p.q*nb+p.alpha) += static_cast<int>(p.sign);
        }

        auto tD2 = std::chrono::high_resolution_clock::now();

        if (cancel_flag) {

             ///Getting one histo to modify
             D_particle_map Hc(H);

             ///We work each box by separate:
             order_particles_multithread(particles);
             std::vector<alma::D_particle> particles_;
             std::vector<std::pair<std::ptrdiff_t,std::ptrdiff_t>> boxlimits;

             for (std::size_t i = 0; i< system.size(); i++ ) {
                 ///Search block limits
                 alma::D_particle lbp(0,0,sign_minus,i);
                 alma::D_particle ubp(0,0,sign_minus,i+1);

                 auto lb = std::lower_bound(particles.begin(),particles.end(),
                                             lbp,std::less<alma::D_particle>());
                 auto ub = std::lower_bound(particles.begin(),particles.end(),
                                             ubp,std::less<alma::D_particle>());

                 boxlimits.push_back({
                     std::distance(particles.begin(),lb),
                     std::distance(particles.begin(),ub)
                 });

             }

             tbb::parallel_for(tbb::blocked_range<std::size_t>(0,system.size()),[&](tbb::blocked_range<std::size_t> ir){
                 for (auto i = ir.begin(); i<ir.end(); i++) {
                     ///If reservoir or periodic ignore
                     if (system[i].reservoir or system[i].periodic)
                         continue;

                     auto lb = particles.begin() + boxlimits[i].first;
                     auto ub = particles.begin() + boxlimits[i].second;

                     //If no particles in the box continue
                     if (std::distance(lb,ub)==0)
                         continue;

                     auto& hbox = Hc.histograma[i];
                     const auto nb_ = histosizes[i].second;

                     auto newend = std::remove_if(lb,ub,
                         [&](const alma::D_particle& p)->bool {
                             auto iq = p.q;
                             auto ib = p.alpha;
                             auto imode = iq*nb_ + ib;
                             int  s   = static_cast<int>(p.sign);
                             int& h   = hbox[imode];

                             if (h == 0)  {
                                 return true;
                             }
                             if ( (s<0)!=(h<0) ){
                                 return true;
                             }
                             h -= s;
                             return false;
                         }
                     );

                     cerberus.lock();
                     particles_.insert(particles_.end(),lb,newend);

                     cerberus.unlock();
                 }
             });

             std::swap(particles,particles_);
        }
        auto tD3 = std::chrono::high_resolution_clock::now();

        /// Properties are extracted from histograms
        auto prop = calculate_prop(system_grid,system,vols,Eeff,H);

        auto tD4 = std::chrono::high_resolution_clock::now();

        /// Save here the properties
        auto frame_saving = std::bind(&saver::save_frame,&Register,
                                      time+dt,Eeff,temperatures,H);
        writer.push_job(frame_saving);

        auto tE = std::chrono::high_resolution_clock::now();
        std::cout << "###Canceling and saving=> DONE in "<<
        (static_cast<std::chrono::duration<double>>(tE - tD)).count()
        << " s" << std::endl;
        std::cout << "###Shuffle and allocate "<<
        (static_cast<std::chrono::duration<double>>(tD1 - tD)).count()
        << " s" << std::endl;
        std::cout << "###Histogram building "<<
        (static_cast<std::chrono::duration<double>>(tD2 - tD1)).count()
        << " s" << std::endl;
        if (cancel_flag and inpars.RTA) {
            std::cout << "###Cancelling "<<
            (static_cast<std::chrono::duration<double>>(tD3 - tD2)).count()
            << " s" << std::endl;
        }
        std::cout << "###Calculating properties " <<
        (static_cast<std::chrono::duration<double>>(tD4 - tD3)).count()
        << " s" << std::endl;
        std::cout << "###Saving "<<
        (static_cast<std::chrono::duration<double>>(tE - tD4)).count()
        << " s" << std::endl;

        istep++;
        time += dt;

        ///Here we print the properties:
        auto flux = prop.second;
        auto ed   = prop.first;
        std::cout << istep << '\t' << time << '\t'<< particles.size() <<'\t';
        for (std::size_t ibox = 0; ibox < system.size(); ibox++) {
            std::cout << ed[ibox] << '\t' <<
            flux[ibox](0) << '\t' << flux[ibox](1) << '\t';
        }
        std::cout << std::endl;

    }

    writer.finish();

    return EXIT_SUCCESS;
}