Rivet analyses

Semileptonic ttbar with high pT top at 13 TeV, single- and double-differential cross-sections

Experiment: ATLAS (LHC)

Inspire ID: 2037744

Status: VALIDATED

Authors: - Jonathan Jamieson - Mark Owen

References: - JHEP 06 (2022) 063 - arXiv: 2202.12134 - Expt page: ATLAS-TOPQ-2019-23

Beams: p+ p+

Beam energies: (6500.0, 6500.0)GeV

Run details: - pp -> non-allhadronic ttbar production at 13 TeV

Cross-section measurements of top-quark pair production where the hadronically decaying top quark has transverse momentum greater than 355 GeV and the other top quark decays into νb are presented using 139 fb−1 of data collected by the ATLAS experiment during proton–proton collisions at the LHC. The fiducial cross-section at $\sqrt{s}=13$ TeV is measured to be σ = 1.267 ± 0.005 ± 0.053 pb, where the uncertainties reflect the limited number of data events and the systematic uncertainties, giving a total uncertainty of 4.2%. The cross-section is measured differentially as a function of variables characterising the t system and additional radiation in the events. The results are compared with various Monte Carlo generators, including comparisons where the generators are reweighted to match a parton-level calculation at next-to-next-to-leading order. The reweighting improves the agreement between data and theory. The measured distribution of the top-quark transverse momentum is used to search for new physics in the context of the effective field theory framework. No significant deviation from the Standard Model is observed and limits are set on the Wilson coefficients of the dimension-six operators OtG and Otq(8), where the limits on the latter are the most stringent to date.

Source code:ATLAS_2022_I2037744.cc

// -*- C++ -*
#include "Rivet/Analysis.hh"
#include "Rivet/Projections/FastJets.hh"
#include "Rivet/Projections/FinalState.hh"
#include "Rivet/Projections/IdentifiedFinalState.hh"
#include "Rivet/Projections/LeptonFinder.hh"
#include "Rivet/Projections/MissingMomentum.hh"
#include "Rivet/Projections/PromptFinalState.hh"
#include "Rivet/Projections/VetoedFinalState.hh"

namespace Rivet {


  /// Semileptonic ttbar single- and double-differential cross-sections with high pT top at 13 TeV
  class ATLAS_2022_I2037744 : public Analysis {
  public:

    /// Constructor
    RIVET_DEFAULT_ANALYSIS_CTOR(ATLAS_2022_I2037744);

    void init() {

      // Define cut objects on eta, eta neutrino and leptons
      Cut eta_full = Cuts::abseta < 5.0;
      Cut lep_cuts = Cuts::abseta < 2.5 && Cuts::pT > 27 * GeV;

      // All final state particles
      const FinalState fs(eta_full);

      // Final state photons for loose lepton dressing for inputs to jets and MET
      IdentifiedFinalState all_photons(fs, PID::PHOTON);

      // Final state photons, not from taus, for analysis lepton dressing
      PromptFinalState photons(all_photons, TauDecaysAs::NONPROMPT);
      declare(photons, "photons");

      // Final state electrons, including from prompt tau decays
      PromptFinalState electrons(Cuts::abspid == PID::ELECTRON, TauDecaysAs::PROMPT);
      declare(electrons, "electrons");

      // Analysis dressed electrons
      LeptonFinder dressedelectrons(electrons, photons, 0.1, lep_cuts);
      declare(dressedelectrons, "dressedelectrons");

      // "All" dressed electrons to be removed from input to jetbuilder
      LeptonFinder ewdressedelectrons(electrons, all_photons, 0.1, eta_full);
      declare(ewdressedelectrons, "ewdressedelectrons");

      //Final state muons, including from prompt tau decays
      PromptFinalState muons(Cuts::abspid == PID::MUON, TauDecaysAs::PROMPT);
      declare(muons, "muons");

      //Analysis dressed muons
      LeptonFinder dressedmuons(muons, photons, 0.1, lep_cuts);
      declare(dressedmuons, "dressedmuons");

      //"All" dressed muons to be removed from input to jetbuilder and for use in METbuilder
      LeptonFinder ewdressedmuons(muons, all_photons, 0.1, eta_full);
      declare(ewdressedmuons, "ewdressedmuons");

      //Neutrinos to be removed from input to jetbuilder, acceptTauDecays=true
      IdentifiedFinalState nu_id;
      nu_id.acceptNeutrinos();
      PromptFinalState neutrinos(nu_id, TauDecaysAs::PROMPT);

      //Small-R jets
      VetoedFinalState vfs(fs);
      vfs.addVetoOnThisFinalState(ewdressedelectrons);
      vfs.addVetoOnThisFinalState(ewdressedmuons);
      vfs.addVetoOnThisFinalState(neutrinos);
      FastJets jets(vfs, JetAlg::ANTIKT, 0.4);
      jets.useInvisibles(JetInvisibles::DECAY);
      declare(jets, "jets");

      //MET
      declare(MissingMomentum(), "MissingMomentum");

      // External bins for 2D plots
      vector<double> n_jet_2D_bins = {
          0.5, 1.5, 2.5, 10.0}; //Extra wide final bin mistakenly used in analysis, replicated here
      vector<double> Top_boosted_rc_pt_2D_bins = {355.0, 398.0, 496.0, 2000.0};

      //Book Histograms using custom function (handles HEPData offset and relative hists)
      book_hist("sigma_ttbar", 1, false);
      book_hist("Top_boosted_rc_pt", 2);
      book_hist("Top_boosted_leptonic_pt", 5);
      book_hist("ttbar_boosted_rc_m", 8);
      book_hist("hadTop_boosted_rc_y", 11);
      book_hist("lepTop_boosted_y", 14);
      book_hist("ttbar_boosted_rc_y", 17);
      book_hist("boosted_rc_HT", 20);
      book_hist("dphi_lepb_hadTop", 23);
      book_hist("ttbar_boosted_rc_pt", 26);
      book_hist("dphi_hadTop_lepTop", 29);
      book_hist("HTall", 32);
      book(_njets, 36, 1, 1);
      book_hist("LeadAddJet_pt", 38);
      book_hist("LeadAddJet_hadTop_m", 41);
      book_hist("dphi_LeadAddJet_hadTop", 44);
      book_hist("dphi_SubLeadAddJet_hadTop", 47);
      book_hist("dphi_LeadAddJet_SubLeadAddJet", 50);
      book_hist("SubLeadAddJet_pt", 53);

      book_2Dhist("LeadAddJet_pt_2D_Nextrajets", n_jet_2D_bins, 56);
      book_2Dhist("LeadAddJet_pt_2D_Top_boosted_rc_pt", Top_boosted_rc_pt_2D_bins, 68);
      book_2Dhist("dphi_LeadAddJet_hadTop_2D_Top_boosted_rc_pt", Top_boosted_rc_pt_2D_bins, 80);
      book_2Dhist("dphi_LeadAddJet_hadTop_2D_Nextrajets", n_jet_2D_bins, 92);
    }

    void analyze(const Event& event) {

      //----------Projections
      DressedLeptons electrons = apply<LeptonFinder>(event, "dressedelectrons").dressedLeptons();
      DressedLeptons muons = apply<LeptonFinder>(event, "dressedmuons").dressedLeptons();

      // We a need seperate jet collection with 25GeV cut to perform OR with leptons
      const Jets& smalljets_25 = apply<FastJets>(event, "jets")
                                     .jetsByPt(Cuts::pT > 25 * GeV && Cuts::abseta <= 2.5);

      idiscardIfAnyDeltaRLess(electrons, smalljets_25, 0.4);
      idiscardIfAnyDeltaRLess(muons, smalljets_25, 0.4);

      // Analysis small-R jets, 26GeV cut
      const Jets smalljets = apply<FastJets>(event, "jets")
                                 .jetsByPt(Cuts::pT > 26 * GeV && Cuts::abseta <= 2.5);

      // Need jets for re-clustering with a 30GeV cut (default in AnalysisTop), so make a seperate collection
      PseudoJets smalljets_for_rc;
      for (const Jet& jet : smalljets) {
        if (jet.pT() < 30 * GeV) continue;
        smalljets_for_rc += jet.pseudojet();
        bool b_tagged = jet.bTagged(Cuts::pT > 5 * GeV);
        smalljets_for_rc[smalljets_for_rc.size() - 1].set_user_index(b_tagged);
      }

      const FourMomentum met = apply<MissingMomentum>(event, "MissingMomentum").missingMomentum();

      // Define reclustered jets AntiKT 1.0
      fastjet::ClusterSequence antikt_cs(smalljets_for_rc,
                                         fastjet::JetDefinition(fastjet::antikt_algorithm, 1.0));
      PseudoJets reclustered_jets = antikt_cs.inclusive_jets();

      // trim the jets
      PseudoJets TrimmedReclusteredJets;

      /// @todo Store rather than rebuild for every event
      fastjet::Filter trimmer(fastjet::JetDefinition(fastjet::kt_algorithm, 0.01),
                              fastjet::SelectorPtFractionMin(0.05));
      for (const PseudoJet& pjet : reclustered_jets) {
        fastjet::PseudoJet candidate_trim = trimmer(pjet);
        const vector<fastjet::PseudoJet> constituents = candidate_trim.constituents();
        FourMomentum trfj_mom = momentum(candidate_trim);
        if (trfj_mom.pt() <= 355 * GeV) continue;
        if (trfj_mom.abseta() < 2.0) {
          TrimmedReclusteredJets.push_back(candidate_trim);
        }
      }
      TrimmedReclusteredJets = fastjet::sorted_by_pt(TrimmedReclusteredJets);

      //----------Event selection

      // SINGLE LEPTON
      const bool single_electron = electrons.size() == 1 && muons.empty();
      const bool single_muon = muons.size() == 1 && electrons.empty();
      DressedLepton* lepton = NULL;
      if (single_electron)
        lepton = &electrons[0];
      else if (single_muon)
        lepton = &muons[0];
      else
        vetoEvent;

      //MET
      if (met.pt() < 20 * GeV) vetoEvent;

      //MET+MWT
      const double transmass = mT(momentum(*lepton), met);
      if ((met.pt() + transmass) < 60 * GeV) vetoEvent;

      //SMALL-R JET MULTIPLICITY
      if (smalljets.size() < 2) vetoEvent;
      if (TrimmedReclusteredJets.empty()) vetoEvent;

      // TOP-TAGGED RC JET
      PseudoJet HadTopJet;
      bool ThereIsHadTop = false;
      for (const PseudoJet& rc_jet : TrimmedReclusteredJets) {
        FourMomentum rc_jet_mom = momentum(rc_jet);
        double dR_lepJet = deltaR(rc_jet_mom, momentum(*lepton));
        if (single_electron && dR_lepJet < 1.) continue;
        if (!rc_jet.user_index()) continue;
        if (rc_jet_mom.mass() > 120 * GeV && rc_jet_mom.mass() < 220 * GeV) {
          HadTopJet = rc_jet;
          ThereIsHadTop = true;
          break; //Pick highest pT trimmed RC jet passing requirements
        }
      }
      if (!ThereIsHadTop) vetoEvent;

      // BTAGGED JET ON LEPTONIC SIDE
      Jet LepbJet;
      double smallest_dR_bjetlep = 2.0;
      bool ThereIsLepbJet = false;
      size_t bjet_index;
      PseudoJets smalljets_for_HT;
      for (const Jet& jet : smalljets) {
        smalljets_for_HT += jet.pseudojet();
        smalljets_for_HT[smalljets_for_HT.size() - 1].set_user_index(0);

        // leptonic bjet cannot be constituent of top-jet
        const vector<fastjet::PseudoJet>& constituents = HadTopJet.constituents();
        bool issubjet = false;
        for (const PseudoJet& subjet : constituents) {
          // can't do direct equality because smalljets and RCsubjets are
          // different jet collections, so we do an ugly pT comparison
          if (fuzzyEquals(jet.pt(), momentum(subjet).pt())) {
            issubjet = true;
            smalljets_for_HT[smalljets_for_HT.size() - 1].set_user_index(1);
          }
        }
        if (issubjet) continue;
        if (!jet.bTagged(Cuts::pT > 5 * GeV))
          continue; // Must be b-tagged (do after so we can also fill addjet veto in same loop)

        const double dR_bjetlep = deltaR(jet, *lepton);
        if (dR_bjetlep > smallest_dR_bjetlep)
          continue;
        else {
          smallest_dR_bjetlep = dR_bjetlep;
          LepbJet = jet; //Take b-tagged non-subjet small-R jet closest in dR to lepton
          bjet_index = smalljets_for_HT.size() - 1;
          ThereIsLepbJet = true;
        }
      }
      if (!ThereIsLepbJet) vetoEvent;
      smalljets_for_HT[bjet_index].set_user_index(1);

      // MLB
      double mlb = (lepton->mom() + LepbJet.mom()).mass();
      if (mlb >= 180 * GeV) vetoEvent;

      // Reconstruct leptonically decaying top-jet
      const double nu_pz = computeneutrinoz(lepton->mom(), met, LepbJet.mom());
      FourMomentum neutrino(sqrt(sqr(met.px()) + sqr(met.py()) + sqr(nu_pz)), met.px(), met.py(), nu_pz);
      FourMomentum LeptonicTop = lepton->mom() + neutrino + LepbJet.mom();
      FourMomentum HadronicTop = momentum(HadTopJet);
      FourMomentum pttbar = HadronicTop + LeptonicTop;

      // Lastly find additional jets
      Jets addJets;
      double HT_all = 0.0; //Set up variable for pT sum of ttbar and all additional jets
      for (const PseudoJet& jet : smalljets_for_HT) {
        // ignore all sub-jets of hadronic top and the b-tagged jet on the leptonic side
        if (jet.user_index()) continue;
        addJets += jet;
        HT_all += jet.pt();
      }
      FourMomentum leading_addjet;
      FourMomentum subleading_addjet;
      FourMomentum p_hadtop_leading_addjet; // = HadronicTop;
      if (addJets.size() > 0) {
        leading_addjet = addJets[0].mom();
        p_hadtop_leading_addjet = HadronicTop + leading_addjet;
        if (addJets.size() > 1) {
          subleading_addjet = addJets[1].mom();
        }
      }

      // calculate some observables
      const double HT_ttbar = HadronicTop.pt() + LeptonicTop.pt();
      HT_all += HT_ttbar;
      const double dphi_lepb_hadTop = deltaPhi(LepbJet.mom(), HadronicTop) / PI;
      const double dphi_hadTop_lepTop = deltaPhi(HadronicTop, LeptonicTop) / PI;

      // Observables
      fillHist("sigma_ttbar", 0.5, false);
      fillHist("Top_boosted_rc_pt", HadronicTop.pt() / GeV);
      fillHist("Top_boosted_leptonic_pt", LeptonicTop.pt() / GeV);
      fillHist("ttbar_boosted_rc_m", pttbar.mass() / GeV);
      fillHist("hadTop_boosted_rc_y", HadronicTop.absrap());
      fillHist("lepTop_boosted_y", LeptonicTop.absrap());
      fillHist("ttbar_boosted_rc_y", pttbar.absrap());
      fillHist("boosted_rc_HT", HT_ttbar / GeV);
      fillHist("dphi_lepb_hadTop", dphi_lepb_hadTop);
      fillHist("ttbar_boosted_rc_pt", pttbar.pt() / GeV);
      fillHist("dphi_hadTop_lepTop", dphi_hadTop_lepTop);
      fillHist("HTall", HT_all / GeV);
      _njets->fill(map2string(min(addJets.size(), 6u)));

      if (addJets.size() > 0) {
        const double dphi_leadaddjet_hadTop = deltaPhi(leading_addjet, HadronicTop) / PI;
        fillHist("LeadAddJet_pt", leading_addjet.pt() / GeV);
        fillHist("LeadAddJet_hadTop_m", p_hadtop_leading_addjet.mass() / GeV);
        fillHist("dphi_LeadAddJet_hadTop", dphi_leadaddjet_hadTop);

        // 2D Observables
        fillHist2D("LeadAddJet_pt_2D_Nextrajets", min(addJets.size(), 6u), leading_addjet.pt() / GeV);
        fillHist2D("LeadAddJet_pt_2D_Top_boosted_rc_pt", HadronicTop.pt() / GeV, leading_addjet.pt() / GeV);
        fillHist2D("dphi_LeadAddJet_hadTop_2D_Top_boosted_rc_pt", HadronicTop.pt() / GeV,
                   dphi_leadaddjet_hadTop);
        fillHist2D("dphi_LeadAddJet_hadTop_2D_Nextrajets", min(addJets.size(), 6u), dphi_leadaddjet_hadTop);
      }

      if (addJets.size() > 1) {
        const double dphi_subleadaddjet_hadTop = deltaPhi(subleading_addjet, HadronicTop) / PI;
        const double dphi_leadaddjet_subleadaddjet = deltaPhi(leading_addjet, subleading_addjet) / PI;
        fillHist("dphi_SubLeadAddJet_hadTop", dphi_subleadaddjet_hadTop);
        fillHist("dphi_LeadAddJet_SubLeadAddJet", dphi_leadaddjet_subleadaddjet);
        fillHist("SubLeadAddJet_pt", subleading_addjet.pt() / GeV);
      }
    }


    void finalize() {

      // Normalize to cross-section
      const double sf = crossSection() / picobarn / sumOfWeights();

      for (auto& hist : _h) {
        scale(hist.second, sf);
        if (hist.first.find("_norm") != string::npos) normalize(hist.second, 1.0, false);
      }
      scale(_njets, sf);
      for (auto& hist : _h_multi) {
        scale(hist.second, sf);
        if (hist.first.find("_norm") != string::npos) {
          normalize(hist.second, 1.0, false);
        }
        divByGroupWidth(hist.second);
      }
    }


  private:

    // HepData entry has dummy "Table of Contents", for both 1D and 2D hists need to offset tables by one unit
    void book_hist(const string& name, unsigned int table, bool do_norm = true) {
      book(_h[name], table + 1, 1, 1);
      if (do_norm) {
        book(_h[name + "_norm"], table + 3, 1, 1);
      }
    }

    void book_2Dhist(const string& name, const std::vector<double>& doubleDiff_bins, unsigned int table) {
      book(_h_multi[name + "_norm"], doubleDiff_bins);
      book(_h_multi[name], doubleDiff_bins);
      for (size_t i = 0; i < _h_multi[name]->numBins(); ++i) {
        book(_h_multi[name + "_norm"]->bin(i + 1), table + i + 1, 1, 1);
        book(_h_multi[name]->bin(i + 1), table + i + 4, 1, 1);
      }
    }

    // Fill abs and nomralised hists at same time
    void fillHist(const string& name, double value, bool do_norm = true) {
      _h[name]->fill(value);
      if (do_norm) _h[name + "_norm"]->fill(value);
    }

    void fillHist2D(const string& name, double externalbin, double val) {
      _h_multi[name]->fill(externalbin, val);
      _h_multi[name + "_norm"]->fill(externalbin, val);
    }

    string map2string(const size_t njets) const {
      if (njets == 0) return "0";
      if (njets == 1) return "1";
      if (njets == 2) return "2";
      if (njets < 5) return "3.0 - 4.0";
      return "$>$4";
    }


    double computeneutrinoz(const FourMomentum& lepton,
                            const FourMomentum& met,
                            const FourMomentum& lbjet) const {
      const double m_W = 80.385 * GeV; // mW
      const double beta = m_W * m_W - lepton.mass() * lepton.mass() + 2.0 * lepton.px() * met.px()
          + 2.0 * lepton.py() * met.py();
      const double delta = lepton.E() * lepton.E()
          * (beta * beta + (2.0 * lepton.pz() * met.pt()) * (2.0 * lepton.pz() * met.pt())
             - (2.0 * lepton.E() * met.pt()) * (2.0 * lepton.E() * met.pt()));
      if (delta <= 0) {
        //imaginary solution, use real part
        double pzneutrino = 0.5 * lepton.pz() * beta / (lepton.E() * lepton.E() - lepton.pz() * lepton.pz());
        return pzneutrino;
      }
      double pzneutrinos[2] = {
          0.5 * (lepton.pz() * beta + sqrt(delta)) / (lepton.E() * lepton.E() - lepton.pz() * lepton.pz()),
          0.5 * (lepton.pz() * beta - sqrt(delta)) / (lepton.E() * lepton.E() - lepton.pz() * lepton.pz())};
      FourMomentum topCands[2];
      for (int i = 0; i < 2; ++i) {
        FourMomentum neutrino;
        neutrino.setXYZM(met.px(), met.py(), pzneutrinos[i], 0.0);
        topCands[i] = neutrino + lbjet + lepton;
      }
      // Pick neutrino solution that results in smallest top mass
      if (topCands[0].mass() <= topCands[1].mass()) {
        return pzneutrinos[0];
      }
      else {
        return pzneutrinos[1];
      }
    }

    // Histogram pointer maps
    map<string, Histo1DPtr> _h;
    BinnedHistoPtr<string> _njets;
    map<string, Histo1DGroupPtr> _h_multi;
  };


  RIVET_DECLARE_PLUGIN(ATLAS_2022_I2037744);

}