diff --git a/FixedOrderGen/src/PhaseSpacePoint.cc b/FixedOrderGen/src/PhaseSpacePoint.cc index 9e6e9b9..affdc09 100644 --- a/FixedOrderGen/src/PhaseSpacePoint.cc +++ b/FixedOrderGen/src/PhaseSpacePoint.cc @@ -1,670 +1,669 @@ #include "PhaseSpacePoint.hh" #include #include #include "RHEJ/Constants.hh" #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 != pid::gluon && p2.type == pid::gluon; } size_t count_gluons(std::vector::const_iterator first, std::vector::const_iterator last){ return std::count_if(first, last, [](Particle const & p) {return p.type == pid::gluon;}); } /** assumes FKL configurations between first and last, * else there can be a quark in a non-extreme position * e.g. uno configuration gqg would pass */ bool can_change_to_qqx( std::vector::const_iterator first, std::vector::const_iterator last){ return 1 < count_gluons(first,last); } bool is_AWZ_proccess(Process const & proc){ return proc.boson && is_AWZ_boson(*proc.boson); } } void PhaseSpacePoint::maybe_turn_to_subl( double chance, Process const & proc, RHEJ::RNG & ran ){ if(proc.njets <= 2) return; assert(outgoing_.size() >= 2); // decide what kind of subleading process is allowed bool allow_uno = false; const size_t nout = outgoing_.size(); const bool can_be_uno_backward = can_swap_to_uno( outgoing_[0], outgoing_[1] ); const bool can_be_uno_forward = can_swap_to_uno( outgoing_[nout-1], outgoing_[nout-2] ); allow_uno = can_be_uno_backward || can_be_uno_forward; bool allow_qqx = false; if(is_AWZ_proccess(proc)) { allow_qqx = can_change_to_qqx(outgoing_.cbegin(), outgoing_.cend()); if(incoming_[0].type == pid::gluon && incoming_[1].type == 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(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 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 { 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); } } void PhaseSpacePoint::turn_to_qqx(RHEJ::RNG & ran){ /// find first and last gluon in FKL chain auto first = std::find_if(outgoing_.begin(), outgoing_.end(), [](Particle const & p){return p.type == pid::gluon;}); auto last = std::find_if(first, outgoing_.end(), [](Particle const & p){return is_quark(p) || is_antiquark(p);}); const size_t ng = count_gluons(first,last); if(ng < 2) throw std::logic_error("not enough gluons to create qqx"); // select flavour of quark const double r1 = 2.*ran.flat()-1.; weight_ *= n_f*2; int flavour = pid::down; for (double sum = 1./n_f; sum < std::abs(r1); sum += 1./n_f) ++flavour; flavour*=r1<0.?-1:1; // select gluon for switch first += floor((ng-1) * ran.flat()); auto next = std::find_if(first+1, last, [](Particle const & p){return p.type == pid::gluon;}); assert(next != outgoing_.end()); weight_ *= (ng-1); first->type = ParticleID(flavour); next->type = ParticleID(-flavour); } 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 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::Wm, 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, 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; maybe_turn_to_subl(subl_chance, proc, ran); if(proc.boson) couple_boson(*proc.boson, ran); #ifndef NDEBUG if(is_AWZ_proccess(proc)){ // check that W has a quark to couple to auto const partons = filter_partons(outgoing_); - /// @TODO extend assert to also check flavour? Maybe easier in separate cmake test assert(partons.front().type != pid::gluon || partons.back().type != pid::gluon // extremal quark || ( (partons.size() > 3 ) // or central qqx && ( 1 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]; } namespace { /// partons are ordered: even = anti, 0 = gluon ParticleID index_to_pid(size_t i){ if(!i) return pid::gluon; return static_cast(i%2?(i+1)/2:-i/2); } /// decides which "index" (see index_to_pid) are allowed for process std::bitset<11> switch_allowed(Process const & proc){ std::bitset<11> allowed = ~0; allowed[0] = 0; // no gluon if(abs(*proc.boson) == pid::Wp){ // special case W+2j: // Wp: anti-down or up-type quark -> 1001100110(0) = 1228 // Wm: down or anti-up-type quark -> 0110011001(0) = 818 allowed &= (*proc.boson)>0?1228:818; } return allowed; } } void PhaseSpacePoint::reconstruct_incoming( Process const & proc, 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 */ auto const & ids = proc.incoming; std::array,2> allowed_partons{~0,~0}; // special case A/W/Z+2j: needs one quark initially if(proc.njets == 2 && is_AWZ_proccess(proc)){ // @TODO optimise: do not allow dd initial for Wp, also for 3j if(ids[0] != pid::gluon && ids[1] != pid::gluon){ // both can be quarks => force one to be a gluon weight_*=2.; allowed_partons[ran.flat() < 0.5] = switch_allowed(proc); } else if(ids[0] == pid::gluon) { if(ids[1] == pid::gluon) { throw RHEJ::not_implemented{ "W+2 jet process requires at least one initial quark" }; } // first gluon -> second has to be quark allowed_partons[1] = switch_allowed(proc); } else { // second gluon -> first has to be quark allowed_partons[0] = switch_allowed(proc); } } for(size_t i = 0; i < 2; ++i){ if(ids[i] == pid::proton || ids[i] == pid::p_bar){ incoming_[i].type = generate_incoming_id(i, i?xb:xa, uf, pdf, allowed_partons[i], ran); } else { incoming_[i].type = ids[i]; } } assert(momentum_conserved(1e-7)); } RHEJ::ParticleID PhaseSpacePoint::generate_incoming_id( size_t const beam_idx, double const x, double const uf, RHEJ::PDF & pdf, std::bitset<11> allowed_partons, RHEJ::RNG & ran ){ std::array pdf_wt; pdf_wt[0] = allowed_partons[0]?fabs(pdf.pdfpt(beam_idx,x,uf,pid::gluon)):0.; double pdftot = pdf_wt[0]; for(size_t i = 1; i < pdf_wt.size(); ++i){ pdf_wt[i] = allowed_partons[i]?4./9.*fabs(pdf.pdfpt(beam_idx,x,uf,index_to_pid(i))):0; pdftot += pdf_wt[i]; } const double r1 = pdftot * ran.flat(); double sum = 0; for(size_t i=0; i < pdf_wt.size(); ++i){ if (r1 < (sum+=pdf_wt[i])){ weight_*= pdftot/pdf_wt[i]; return index_to_pid(i); } } std::cerr << "Error in choosing incoming parton: "<0?1:-1; std::vector allowed_parts; for(auto & part: outgoing_){ // Wp -> up OR anti-down, Wm -> anti-up OR down if ( (sign_W*part.type > 0 && is_up_type(part)) || (sign_W*part.type < 0 && is_down_type(part)) ) allowed_parts.push_back(&part); } if(!allowed_parts.size()){ /// @TODO do not allow such states in the generation status_=wrong_final_state; return; } // select one and flip it size_t idx = 0; if(allowed_parts.size() > 1){ /// @TODO more efficient sampling idx = floor(ran.flat()*allowed_parts.size()); weight_ *= allowed_parts.size(); } allowed_parts[idx]->type = static_cast( allowed_parts[idx]->type - sign_W ); } double PhaseSpacePoint::random_normal( double stddev, RHEJ::RNG & ran ){ const double r1 = ran.flat(); const double r2 = ran.flat(); const double lninvr1 = -log(r1); const double result = stddev*sqrt(2.*lninvr1)*cos(2.*M_PI*r2); weight_ *= exp(result*result/(2*stddev*stddev))*sqrt(2.*M_PI)*stddev; return result; } bool PhaseSpacePoint::momentum_conserved(double ep) const{ fastjet::PseudoJet diff; for(auto const & in: incoming()) diff += in.p; for(auto const & out: outgoing()) diff -= out.p; return nearby_ep(diff, fastjet::PseudoJet{}, ep); } Decay PhaseSpacePoint::select_decay_channel( std::vector 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; } } diff --git a/FixedOrderGen/t/W_2j_classify.cc b/FixedOrderGen/t/W_2j_classify.cc index d65b954..9e7c2d8 100644 --- a/FixedOrderGen/t/W_2j_classify.cc +++ b/FixedOrderGen/t/W_2j_classify.cc @@ -1,105 +1,114 @@ #ifdef NDEBUG #undef NDEBUG #endif #include "PhaseSpacePoint.hh" #include "Process.hh" #include "JetParameters.hh" #include "HiggsProperties.hh" #include "RHEJ/Mixmax.hh" #include "RHEJ/PDF.hh" #include "RHEJ/utility.hh" using namespace HEJFOG; using namespace RHEJ; namespace { void print_event(PhaseSpacePoint const & psp){ std::cerr << "Process:\n" << psp.incoming()[0].type << " + "<< psp.incoming()[1].type << " -> "; for(auto const & out: psp.outgoing()){ std::cerr << out.type << " "; } std::cerr << "\n"; } void bail_out(PhaseSpacePoint const & psp, std::string msg){ print_event(psp); throw std::logic_error{msg}; } bool is_up_type(Particle const & part){ return RHEJ::is_anyquark(part) && !(abs(part.type)%2); } bool is_down_type(Particle const & part){ return RHEJ::is_anyquark(part) && abs(part.type)%2; } } int main(){ constexpr size_t n_psp_base = 1337; const JetParameters jet_para{ fastjet::JetDefinition(fastjet::JetAlgorithm::antikt_algorithm, 0.4), 20,5,30}; PDF pdf(11000, pid::proton, pid::proton); constexpr double E_cms = 13000.; constexpr double subl_change = 0.5; const HiggsProperties higgs_para{125., 0.001, {Decay{{pid::photon, pid::photon},1.}}}; RHEJ::Mixmax ran{}; // W2j const Process proc {{pid::proton,pid::proton}, 2, pid::Wp}; size_t n_psp = n_psp_base; size_t t_fail = 0; for( size_t i = 0; i out_partons; + std::vector Wp; + for(auto const & p: psp.outgoing()){ + if(p.type == pid::Wp) Wp.push_back(p); + else if(is_parton(p)) out_partons.push_back(p); + else bail_out(psp, "Found particle with is not Wp or parton"); + } + if(Wp.size() != 1 || out_partons.size() != 2){ + bail_out(psp, "Found wrong number of outgoing partons"); + } for(size_t j=0; j<2; ++j){ auto const & in = psp.incoming()[j]; auto const & out = out_partons[j]; if(is_quark(in) || is_antiquark(in)) { found_quark = true; if(in.type != out.type) { // switch in quark type -> Wp couples to it if(found_anti){ // already found qq for coupling to W bail_out(psp, "Found second up/down pair"); } found_anti = true; if( is_up_type(in)) { // "up" in // -> only allowed u -> Wp + d if(in.type < 0 || is_up_type(out) || out.type < 0){ // not in "q" or out "up" or out "qx" -> bail out bail_out(psp, "u -/> Wp + d"); } } else { // "down" in // -> only allowed dx -> Wp + ux if(in.type > 0 || is_down_type(out) || out.type > 0){ // not in "qx" or out "down" or out "q" -> bail out bail_out(psp, "dx -/> Wp + ux"); } } } } } if(!found_quark) { bail_out(psp, "Found no initial quarks"); } else if(!found_anti){ bail_out(psp, "Found no up/down pair"); } } else { // bad process -> try again ++n_psp; if(psp.status()==wrong_final_state) ++t_fail; } } // @TODO test final state for 3j std::cout << "All processes passed.\nTook " << n_psp << " to generate " << n_psp_base << " successfully PSP (" << 1.*n_psp/n_psp_base << " trials/PSP)" << std::endl; std::cout << "Failed for wrong final state " << t_fail << " (" << 100.*t_fail/(n_psp-n_psp_base) << " % of fails)" << std::endl; return EXIT_SUCCESS; }