diff --git a/FixedOrderGen/include/PhaseSpacePoint.hh b/FixedOrderGen/include/PhaseSpacePoint.hh index 136a42f..506fb0f 100644 --- a/FixedOrderGen/include/PhaseSpacePoint.hh +++ b/FixedOrderGen/include/PhaseSpacePoint.hh @@ -1,184 +1,199 @@ /** \file PhaseSpacePoint.hh * \brief Contains the PhaseSpacePoint Class */ #pragma once #include #include "RHEJ/utility.hh" #include "RHEJ/Event.hh" #include "RHEJ/PDG_codes.hh" #include "RHEJ/PDF.hh" #include "RHEJ/RNG.hh" #include "Status.hh" #include "JetParameters.hh" #include "HiggsProperties.hh" namespace HEJFOG{ class Process; using RHEJ::Particle; //! A point in resummation phase space class PhaseSpacePoint{ public: //! Default PhaseSpacePoint Constructor PhaseSpacePoint() = default; //! PhaseSpacePoint Constructor /** * @param proc The process to generate * @param jet_def The Jet Definition Used * @param jetptmin Minimum Jet Transverse Momentum * @param rapmax Maximum parton rapidity * @param pdf The pdf set (used for sampling) - * @param uno_chance Chance to turn a potentially unordered - * emission into an actual one + * @param subl_chance Chance to turn a potentially subleading + * emission (uno or central qqx) into an actual one * - * Initially, only FKL phase space points are generated. If the most - * extremal emission in any direction is a quark or anti-quark and the - * next emission is a gluon, uno_chance is the chance to turn this into - * an unordered emission event by swapping the two flavours. At most one - * unordered emission will be generated in this way. + * Initially, only FKL phase space points are generated. subl_chance gives + * the change of turning one emissions into a subleading configuration, + * i.e. either unordered or central quark/anti-quark pair. Unordered + * emissions require that the most extremal emission in any direction is + * a quark or anti-quark and the next emission is a gluon. Quark/anti-quark + * pairs are only generated for W processes. At most one subleading + * emission will be generated in this way. */ PhaseSpacePoint( Process const & proc, JetParameters const & jet_properties, RHEJ::PDF & pdf, double E_beam, - double uno_chance, + double subl_chance, HiggsProperties const & higgs_properties, RHEJ::RNG & ran ); //! Get Weight Function /** * @returns Weight of Event */ double weight() const{ return weight_; } Status status() const{ return status_; } //! Get Incoming Function /** * @returns Incoming Particles */ std::array const & incoming() const{ return incoming_; } //! Get Outgoing Function /** * @returns Outgoing Particles */ std::vector const & outgoing() const{ return outgoing_; } std::unordered_map> const & decays() const{ return decays_; } private: /** * \internal * \brief Generate LO parton momentum * * @param count Number of partons to generate * @param is_pure_jets If true ensures momentum conservation in x and y * @param jet_param Jet properties to fulfil * @param max_pt max allowed pt for a parton (typically E_CMS) * @param ran Random Number Generator * * @returns Momentum of partons * * Ensures that each parton is in its own jet. * Generation is independent of parton flavour. Output is sorted in rapidity. */ std::vector gen_LO_partons( int count, bool is_pure_jets, JetParameters const & jet_param, double max_pt, RHEJ::RNG & ran ); Particle gen_boson( RHEJ::ParticleID bosonid, double mass, double width, RHEJ::RNG & ran ); template fastjet::PseudoJet gen_last_momentum( ParticleMomenta const & other_momenta, double mass_square, double y ) const; bool jets_ok( std::vector const & Born_jets, std::vector const & partons ) const; /** * \internal * \brief Generate incoming partons according to the PDF * * @param uf Scale used in the PDF */ void reconstruct_incoming( std::array const & ids, RHEJ::PDF & pdf, double E_beam, double uf, RHEJ::RNG & ran ); RHEJ::ParticleID generate_incoming_id( double x, double uf, RHEJ::PDF & pdf, RHEJ::RNG & ran ); bool momentum_conserved(double ep) const; RHEJ::Particle const & most_backward_FKL( std::vector const & partons ) const; RHEJ::Particle const & most_forward_FKL( std::vector const & partons ) const; RHEJ::Particle & most_backward_FKL(std::vector & partons) const; RHEJ::Particle & most_forward_FKL(std::vector & partons) const; bool extremal_FKL_ok( std::vector const & partons ) const; double random_normal(double stddev, RHEJ::RNG & ran); - void maybe_turn_to_uno(double chance, RHEJ::RNG & ran); + /** + * @brief Turns a FKL configuration into a subleading one + * + * @param chance Change to switch to subleading configuration + * @param proc Process to decide which subleading + * configurations are allowed + * + * With a chance of "chance" the FKL configuration is either turned into + * a unordered configuration or, for A/W/Z bosons, a configuration with + * a central quark/anti-quark pair. + */ + void maybe_turn_to_subl(double chance, Process const & proc, RHEJ::RNG & ran); + void turn_to_uno(bool can_be_uno_backward, bool can_be_uno_forward, RHEJ::RNG & ran); + void turn_to_qqx(RHEJ::RNG & ran); std::vector decay_boson( RHEJ::Particle const & parent, std::vector const & decays, RHEJ::RNG & ran ); Decay select_decay_channel( std::vector const & decays, RHEJ::RNG & ran ); double gen_hard_pt( int np, double ptmin, double ptmax, double y, RHEJ::RNG & ran ); double gen_soft_pt(int np, double ptmax, RHEJ::RNG & ran); double gen_parton_pt( int count, JetParameters const & jet_param, double ptmax, double y, RHEJ::RNG & ran ); double weight_; Status status_; std::array incoming_; std::vector outgoing_; //! Particle decays in the format {outgoing index, decay products} std::unordered_map> decays_; }; RHEJ::UnclusteredEvent to_UnclusteredEvent(PhaseSpacePoint const & psp); } diff --git a/FixedOrderGen/src/PhaseSpacePoint.cc b/FixedOrderGen/src/PhaseSpacePoint.cc index bba80d2..9df6e4d 100644 --- a/FixedOrderGen/src/PhaseSpacePoint.cc +++ b/FixedOrderGen/src/PhaseSpacePoint.cc @@ -1,537 +1,584 @@ #include "PhaseSpacePoint.hh" #include #include "RHEJ/kinematics.hh" #include "RHEJ/utility.hh" #include "RHEJ/exceptions.hh" #include "Process.hh" #include #include using namespace RHEJ; namespace HEJFOG{ static_assert( std::numeric_limits::has_quiet_NaN, "no quiet NaN for double" ); constexpr double NaN = std::numeric_limits::quiet_NaN(); RHEJ::UnclusteredEvent to_UnclusteredEvent(PhaseSpacePoint const & psp){ RHEJ::UnclusteredEvent result; result.incoming = psp.incoming(); std::sort( begin(result.incoming), end(result.incoming), [](Particle o1, Particle o2){return o1.p.pz()= 2); result.decays = psp.decays(); result.central.mur = NaN; result.central.muf = NaN; result.central.weight = psp.weight(); return result; } namespace{ bool can_swap_to_uno( RHEJ::Particle const & p1, RHEJ::Particle const & p2 ){ return is_parton(p1) && p1.type != RHEJ::pid::gluon && p2.type == RHEJ::pid::gluon; } } - void PhaseSpacePoint::maybe_turn_to_uno( + void PhaseSpacePoint::maybe_turn_to_subl( double chance, + Process const & proc, RHEJ::RNG & ran ){ assert(outgoing_.size() >= 2); - const size_t nout = outgoing_.size(); - const bool can_be_uno_backward = can_swap_to_uno( + + // decide what kind of subleading process is allowed + bool allow_uno = false; + bool can_be_uno_backward = false; + bool can_be_uno_forward = false; + if(proc.njets <= 2) { + allow_uno = false; + } else { + const size_t nout = outgoing_.size(); + can_be_uno_backward = can_swap_to_uno( outgoing_[0], outgoing_[1] - ); - const bool can_be_uno_forward = can_swap_to_uno( + ); + can_be_uno_forward = can_swap_to_uno( outgoing_[nout-1], outgoing_[nout-2] - ); - if(!can_be_uno_backward && !can_be_uno_forward) return; + ); + allow_uno = can_be_uno_backward || can_be_uno_forward; + } + + bool allow_qqx = false; + if(proc.boson && RHEJ::is_AWZ_boson(*(proc.boson))) { + allow_qqx = true; + if(incoming_[0].type == RHEJ::pid::gluon && incoming_[1].type == RHEJ::pid::gluon) { + /// enforce qqx for initial gg + allow_uno = false; + chance = 1.; + } + } + + if(!allow_uno && !allow_qqx) return; if(ran.flat() < chance){ weight_ /= chance; - if(can_be_uno_backward && can_be_uno_forward){ - if(ran.flat() < 0.5){ - std::swap(outgoing_[0].type, outgoing_[1].type); - } - else{ - std::swap(outgoing_[nout-1].type, outgoing_[nout-2].type); - } + if(allow_uno && !allow_qqx){ + turn_to_uno(can_be_uno_backward, can_be_uno_forward, ran); + } else if (!allow_uno && allow_qqx) { + std::cout << "only qqx" << std::endl; + turn_to_qqx(ran); + } else { + assert( allow_uno && allow_qqx); + std::cout << "both qqx and uno" << std::endl; + if(ran.flat() < 0.5) turn_to_uno(can_be_uno_backward, can_be_uno_forward, ran); + else turn_to_qqx(ran); weight_ *= 2.; } - else if(can_be_uno_backward){ + } else weight_ /= 1 - chance; + + } + + void PhaseSpacePoint::turn_to_uno( + const bool can_be_uno_backward, const bool can_be_uno_forward, + RHEJ::RNG & ran + ){ + if(!can_be_uno_backward && !can_be_uno_forward) return; + const size_t nout = outgoing_.size(); + if(can_be_uno_backward && can_be_uno_forward){ + if(ran.flat() < 0.5){ std::swap(outgoing_[0].type, outgoing_[1].type); - } - else{ - assert(can_be_uno_forward); + } else { std::swap(outgoing_[nout-1].type, outgoing_[nout-2].type); } + weight_ *= 2.; + } else if(can_be_uno_backward){ + std::swap(outgoing_[0].type, outgoing_[1].type); + } else { + assert(can_be_uno_forward); + std::swap(outgoing_[nout-1].type, outgoing_[nout-2].type); } - else weight_ /= 1 - chance; + } + + void PhaseSpacePoint::turn_to_qqx(RHEJ::RNG & ran){ + /// @TODO implement + std::cerr << "turn_to_qqx not implemented yet. Here is a random number instead " + << ran.flat() <<".\n"; } template fastjet::PseudoJet PhaseSpacePoint::gen_last_momentum( ParticleMomenta const & other_momenta, const double mass_square, const double y ) const { std::array pt{0.,0.}; for (auto const & p: other_momenta) { pt[0]-= p.px(); pt[1]-= p.py(); } const double mperp = sqrt(pt[0]*pt[0]+pt[1]*pt[1]+mass_square); const double pz=mperp*sinh(y); const double E=mperp*cosh(y); return {pt[0], pt[1], pz, E}; } PhaseSpacePoint::PhaseSpacePoint( Process const & proc, JetParameters const & jet_param, RHEJ::PDF & pdf, double E_beam, - double uno_chance, + double subl_chance, HiggsProperties const & h, RHEJ::RNG & ran ) { assert(proc.njets >= 2); status_ = good; weight_ = 1; const int nout = proc.njets + (proc.boson?1:0); outgoing_.reserve(nout); // generate parton momenta const bool is_pure_jets = !proc.boson; auto partons = gen_LO_partons( proc.njets, is_pure_jets, jet_param, E_beam, ran ); // pre fill flavour with gluons for(auto&& p_out: partons) { outgoing_.emplace_back(Particle{pid::gluon, std::move(p_out)}); } if(status_ != good) return; if(proc.boson){ switch (*proc.boson) { case pid::Higgs:{ auto Hparticle=gen_boson(pid::higgs, h.mass, h.width, ran); auto pos=std::upper_bound( begin(outgoing_),end(outgoing_),Hparticle,rapidity_less{} ); outgoing_.insert(pos, Hparticle); if(! h.decays.empty()){ const int boson_idx = std::distance(begin(outgoing_), pos); decays_.emplace( boson_idx, decay_boson(outgoing_[boson_idx], h.decays, ran) ); } break; } case pid::Wp:{ /// @TODO implement, currently this is just a relabel Higgs ///@{ std::cout << "Emission of Wp not implemented yet.\nThis is just a place holder" << std::endl; auto Hparticle=gen_boson(pid::Wp, h.mass, h.width, ran); auto pos=std::upper_bound( begin(outgoing_),end(outgoing_),Hparticle,rapidity_less{} ); outgoing_.insert(pos, Hparticle); if(! h.decays.empty()){ const int boson_idx = std::distance(begin(outgoing_), pos); decays_.emplace( boson_idx, decay_boson(outgoing_[boson_idx], h.decays, ran) ); } ///@} break; } case pid::Wm:{ /// @TODO implement, currently this is just a relabel Higgs ///@{ std::cout << "Emission of Wm not implemented yet.\nThis is just a place holder" << std::endl; auto Hparticle=gen_boson(pid::higgs, h.mass, h.width, ran); auto pos=std::upper_bound( begin(outgoing_),end(outgoing_),Hparticle,rapidity_less{} ); outgoing_.insert(pos, Hparticle); if(! h.decays.empty()){ const int boson_idx = std::distance(begin(outgoing_), pos); decays_.emplace( boson_idx, decay_boson(outgoing_[boson_idx], h.decays, ran) ); } ///@} break; } case pid::photon: case pid::Z: default: throw std::logic_error("Emission of boson of unsupported type"); } } // normalisation of momentum-conserving delta function weight_ *= pow(2*M_PI, 4); reconstruct_incoming(proc.incoming, pdf, E_beam, jet_param.min_pt, ran); if(status_ != good) return; // set outgoing states most_backward_FKL(outgoing_).type = incoming_[0].type; most_forward_FKL(outgoing_).type = incoming_[1].type; - if(proc.njets > 2) maybe_turn_to_uno(uno_chance, ran); + /// @TODO if W && only gluon: subl_change = 1 + + maybe_turn_to_subl(subl_chance, proc, ran); } double PhaseSpacePoint::gen_hard_pt( int np , double ptmin, double ptmax, double y, RHEJ::RNG & ran ) { // heuristic parameters for pt sampling const double ptpar = ptmin + np/5.; const double arg_small_y = atan((ptmax - ptmin)/ptpar); const double y_cut = 3.; const double r1 = ran.flat(); if(y < y_cut){ const double pt = ptmin + ptpar*tan(r1*arg_small_y); const double temp = cos(r1*arg_small_y); weight_ *= pt*ptpar*arg_small_y/(temp*temp); return pt; } const double ptpar2 = ptpar/(1 + 5*(y-y_cut)); const double temp = 1. - std::exp((ptmin-ptmax)/ptpar2); const double pt = ptmin - ptpar2*std::log(1-r1*temp); weight_ *= pt*ptpar2*temp/(1-r1*temp); return pt; } double PhaseSpacePoint::gen_soft_pt(int np, double max_pt, RHEJ::RNG & ran) { constexpr double ptpar = 4.; const double r = ran.flat(); const double pt = max_pt + ptpar/np*std::log(r); weight_ *= pt*ptpar/(np*r); return pt; } double PhaseSpacePoint::gen_parton_pt( int count, JetParameters const & jet_param, double max_pt, double y, RHEJ::RNG & ran ) { constexpr double p_small_pt = 0.02; if(! jet_param.peak_pt) { return gen_hard_pt(count, jet_param.min_pt, max_pt, y, ran); } const double r = ran.flat(); if(r > p_small_pt) { weight_ /= 1. - p_small_pt; return gen_hard_pt(count, *jet_param.peak_pt, max_pt, y, ran); } weight_ /= p_small_pt; const double pt = gen_soft_pt(count, *jet_param.peak_pt, ran); if(pt < jet_param.min_pt) { weight_=0.0; status_ = not_enough_jets; return jet_param.min_pt; } return pt; } std::vector PhaseSpacePoint::gen_LO_partons( int np, bool is_pure_jets, JetParameters const & jet_param, double max_pt, RHEJ::RNG & ran ){ if (np<2) throw std::invalid_argument{"Not enough partons in gen_LO_partons"}; weight_ /= pow(16.*pow(M_PI,3),np); weight_ /= std::tgamma(np+1); //remove rapidity ordering std::vector partons; partons.reserve(np); for(int i = 0; i < np; ++i){ const double y = -jet_param.max_y + 2*jet_param.max_y*ran.flat(); weight_ *= 2*jet_param.max_y; const bool is_last_parton = i+1 == np; if(is_pure_jets && is_last_parton) { constexpr double parton_mass_sq = 0.; partons.emplace_back(gen_last_momentum(partons, parton_mass_sq, y)); break; } const double phi = 2*M_PI*ran.flat(); weight_ *= 2.0*M_PI; const double pt = gen_parton_pt(np, jet_param, max_pt, y, ran); if(weight_ == 0.0) return {}; partons.emplace_back(fastjet::PtYPhiM(pt, y, phi)); assert(jet_param.min_pt <= partons[i].pt()); assert(partons[i].pt() <= max_pt+1e-5); } // Need to check that at LO, the number of jets = number of partons; fastjet::ClusterSequence cs(partons, jet_param.def); auto cluster_jets=cs.inclusive_jets(jet_param.min_pt); if (cluster_jets.size()!=unsigned(np)){ weight_=0.0; status_ = not_enough_jets; return {}; } std::sort(begin(partons), end(partons), rapidity_less{}); return partons; } Particle PhaseSpacePoint::gen_boson( RHEJ::ParticleID bosonid, double mass, double width, RHEJ::RNG & ran ){ // Usual phase space measure weight_ /= 16.*pow(M_PI, 3); // Generate a y Gaussian distributed around 0 // TODO: magic number only for Higgs const double y = random_normal(1.6, ran); const double r1 = ran.flat(); const double sH = mass*( mass + width*tan(M_PI/2.*r1 + (r1-1.)*atan(mass/width)) ); auto p = gen_last_momentum(outgoing_, sH, y); return Particle{bosonid, std::move(p)}; } Particle const & PhaseSpacePoint::most_backward_FKL( std::vector const & partons ) const{ if(!RHEJ::is_parton(partons[0])) return partons[1]; return partons[0]; } Particle const & PhaseSpacePoint::most_forward_FKL( std::vector const & partons ) const{ const size_t last_idx = partons.size() - 1; if(!RHEJ::is_parton(partons[last_idx])) return partons[last_idx-1]; return partons[last_idx]; } Particle & PhaseSpacePoint::most_backward_FKL( std::vector & partons ) const{ if(!RHEJ::is_parton(partons[0])) return partons[1]; return partons[0]; } Particle & PhaseSpacePoint::most_forward_FKL( std::vector & partons ) const{ const size_t last_idx = partons.size() - 1; if(!RHEJ::is_parton(partons[last_idx])) return partons[last_idx-1]; return partons[last_idx]; } void PhaseSpacePoint::reconstruct_incoming( std::array const & ids, RHEJ::PDF & pdf, double E_beam, double uf, RHEJ::RNG & ran ){ std::tie(incoming_[0].p, incoming_[1].p) = incoming_momenta(outgoing_); // calculate xa, xb const double sqrts=2*E_beam; const double xa=(incoming_[0].p.e()-incoming_[0].p.pz())/sqrts; const double xb=(incoming_[1].p.e()+incoming_[1].p.pz())/sqrts; // abort if phase space point is outside of collider energy reach if (xa>1. || xb>1.){ weight_=0; status_ = too_much_energy; return; } // pick pdfs /** @TODO * ufa, ufb don't correspond to our final scale choice. * The reversed HEJ scale generators currently expect a full event as input, * so fixing this is not completely trivial */ if(ids[0] == pid::proton || ids[0] == pid::p_bar){ const double ufa=uf; incoming_[0].type = generate_incoming_id(xa, ufa, pdf, ran); } else { incoming_[0].type = ids[0]; } if(ids[1] == pid::proton || ids[1] == pid::p_bar){ const double ufb=uf; incoming_[1].type = generate_incoming_id(xb, ufb, pdf, ran); } else { incoming_[1].type = ids[1]; } assert(momentum_conserved(1e-7)); } RHEJ::ParticleID PhaseSpacePoint::generate_incoming_id( double x, double uf, RHEJ::PDF & pdf, RHEJ::RNG & ran ){ const double pdfg=fabs(pdf.pdfpt(0,x,uf,pid::gluon)); const double pdfu=fabs(pdf.pdfpt(0,x,uf,pid::up)); const double pdfd=fabs(pdf.pdfpt(0,x,uf,pid::down)); const double pdfux=fabs(pdf.pdfpt(0,x,uf,pid::u_bar)); const double pdfdx=fabs(pdf.pdfpt(0,x,uf,pid::d_bar)); const double pdfc=fabs(pdf.pdfpt(0,x,uf,pid::charm)); const double pdfs=fabs(pdf.pdfpt(0,x,uf,pid::strange)); const double pdfsx=fabs(pdf.pdfpt(0,x,uf,pid::s_bar)); const double pdfb=fabs(pdf.pdfpt(0,x,uf,pid::b)); const double pdftot=pdfg+4.0/9.0*(pdfu + pdfd + pdfux + pdfdx +pdfs +pdfsx + 2.0*(pdfc +pdfb )); const double r1=pdftot*ran.flat(); double sum; if (r1<(sum=pdfg)) { weight_*=pdftot/pdfg; return pid::gluon; } if (r1<(sum+=(4./9.)*pdfu)) { weight_*=pdftot/((4./9.)*pdfu); return pid::up; } else if (r1<(sum+=(4./9.)*pdfd)) { weight_*=pdftot/((4./9.)*pdfd); return pid::down; } else if (r1<(sum+=(4./9.)*pdfux)) { weight_*=pdftot/((4./9.)*pdfux); return pid::u_bar; } else if (r1<(sum+=(4./9.)*pdfdx)) { weight_*=pdftot/((4./9.)*pdfdx); return pid::d_bar; } else if (r1<(sum+=(4./9.)*pdfc)) { weight_*=pdftot/((4./9.)*pdfc); return pid::c; } else if (r1<(sum+=(4./9.)*pdfc)){ weight_*=pdftot/((4./9.)*pdfc); return pid::c_bar; } else if (r1<(sum+=(4./9.)*pdfs)) { weight_*=pdftot/((4./9.)*pdfs); return pid::s; } else if (r1<(sum+=(4./9.)*pdfsx)) { weight_*=pdftot/((4./9.)*pdfsx); return pid::s_bar; } else if (r1<(sum+=(4./9.)*pdfb)) { weight_*=pdftot/((4./9.)*pdfb); return pid::b; } else if (r1<=(sum+=(4./9.)*pdfb)) { weight_*=pdftot/((4./9.)*pdfb); return pid::b_bar; } std::cerr << "Error in choosing incoming parton: "< const & decays, RHEJ::RNG & ran ){ double br_total = 0.; for(auto const & decay: decays) br_total += decay.branching_ratio; // adjust weight // this is given by (channel branching ratio)/(chance to pick channel) // where (chance to pick channel) = // (channel branching ratio)/(total branching ratio) weight_ *= br_total; const double r1 = br_total*ran.flat(); double br_sum = 0.; for(auto const & decay: decays){ br_sum += decay.branching_ratio; if(r1 < br_sum) return decay; } throw std::logic_error{"unreachable"}; } std::vector PhaseSpacePoint::decay_boson( RHEJ::Particle const & parent, std::vector const & decays, RHEJ::RNG & ran ){ const auto channel = select_decay_channel(decays, ran); if(channel.products.size() != 2){ throw RHEJ::not_implemented{ "only decays into two particles are implemented" }; } std::vector decay_products(channel.products.size()); for(size_t i = 0; i < channel.products.size(); ++i){ decay_products[i].type = channel.products[i]; } // choose polar and azimuth angle in parent rest frame const double E = parent.m()/2; const double theta = 2.*M_PI*ran.flat(); const double cos_phi = 2.*ran.flat()-1.; const double sin_phi = sqrt(1. - cos_phi*cos_phi); // Know 0 < phi < pi const double px = E*cos(theta)*sin_phi; const double py = E*sin(theta)*sin_phi; const double pz = E*cos_phi; decay_products[0].p.reset(px, py, pz, E); decay_products[1].p.reset(-px, -py, -pz, E); for(auto & particle: decay_products) particle.p.boost(parent.p); return decay_products; } }