Rivet analyses

pTmiss+jets cross-sections and ratios at 13 TeV

Experiment: ATLAS (LHC)

Inspire ID: 2765017

Status: VALIDATED

Authors: - Christian Gutschow

References: - Expt page: ATLAS-STDM-2018-55 - arXiv: 2403.02793

Beams: p+ p+

Beam energies: (6500.0, 6500.0)GeV

Run details: - pp -> 0/1/2 l + jet(s) (l = e, mu, nu) at 13 TeV

Measurements of inclusive, differential cross-sections for the production of events with missing transverse momentum in association with jets in proton-proton collisions at $\sqrt{s} =$ 13 TeV are presented. The measurements are made with the ATLAS detector using an integrated luminosity of 140 fb−1 and include measurements of dijet distributions in a region in which vector-boson fusion processes are enhanced. They are unfolded to correct for detector resolution and efficiency within the fiducial acceptance, and are designed to allow robust comparisons with a wide range of theoretical predictions. A measurement of differential cross sections for the Z → νν process is made. The measurements are generally well-described by Standard Model predictions except for the dijet invariant mass distribution. Auxiliary measurements of the hadronic system recoiling against isolated leptons, and photons, are also made in the same phase space. Ratios between the measured distributions are then derived, to take advantage of cancellations in modelling effects and some of the major systematic uncertainties. These measurements are sensitive to new phenomena, and provide a mechanism to easily set constraints on phenomenological models. To illustrate the robustness of the approach, these ratios are compared with two common Dark Matter models, where the constraints derived from the measurement are comparable to those set by dedicated detector-level searches.

Source code:ATLAS_2024_I2765017.cc

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

namespace Rivet {

  /// @brief pTmiss+jets cross-sections and ratios at 13 TeV
  class ATLAS_2024_I2765017 : public Analysis {

    public:

      /// Constructor
      RIVET_DEFAULT_ANALYSIS_CTOR(ATLAS_2024_I2765017);

    public:

      // initialise
      void init() {

        _extra = getOption("MODE", "FIDUCIAL") == "EXTRAPOLATED";
        _isBSM = getOption("TYPE", "BSM") != "SM";

        ecal_cuts = Cuts::abseta < 2.47 && !(Cuts::abseta > 1.37 && Cuts::abseta < 1.52);

        // prompt photons
        PromptFinalState photons(Cuts::abspid == PID::PHOTON && Cuts::abseta < 4.9);
        declare(photons, "Photons");

        // prompt (bare) leptons
        Cut lep_inc_cuts = Cuts::abseta < 4.9 && (Cuts::abspid == PID::MUON || Cuts::abspid == PID::ELECTRON);
        PromptFinalState leps(lep_inc_cuts, TauDecaysAs::PROMPT);

        // dressed leptons
        Cut el_fid_cuts  = (ecal_cuts && Cuts::abspid == PID::ELECTRON);
        Cut mu_fid_cuts  = (Cuts::abseta < 2.5 && Cuts::abspid == PID::MUON);
        Cut lep_fid_cuts = (Cuts::pT > 7*GeV) && (mu_fid_cuts || el_fid_cuts);
        LeptonFinder dressed_leps(leps, photons, 0.1, lep_fid_cuts);
        declare(dressed_leps, "DressedLeptons");

        // jet collection
        VetoedFinalState jet_fs(FinalState(Cuts::abseta < 4.9));
        if (_extra)  jet_fs.addVetoOnThisFinalState(dressed_leps);
        FastJets jets(jet_fs, JetAlg::ANTIKT, 0.4,
                      _extra? JetMuons::DECAY : JetMuons::NONE,
                      _extra? JetInvisibles::DECAY : JetInvisibles::NONE);
        declare(jets, "Jets");

        // MET
        if (_extra) declare(InvisibleFinalState(OnlyPrompt::YES), "promptNeutrinos");
        else {
          FinalState met_fs(!(Cuts::abspid == PID::MUON && (Cuts::abseta > 2.5 || Cuts::pT < 7*GeV)));
          declare(MissingMomentum(met_fs), "actualMET");
        }

        // Calorimeter particles for photon isolation
        VetoedFinalState calo_fs(VisibleFinalState(Cuts::abspid != PID::MUON));
        declare(calo_fs, "CaloParticles");

        // Jets for UE subtraction with jet-area method
        if (_extra) {
          FastJets ktjets(FinalState(), JetAlg::KT, 0.5, JetMuons::NONE, JetInvisibles::NONE);
          ktjets.useJetArea(new fastjet::AreaDefinition(fastjet::VoronoiAreaSpec(0.9)));
          declare(ktjets, "kTjets");
        }

        // Book Histograms
        cats.init("sr0l");
        cats.init("sr0l_cr1ie_0v"); cats.init("sr0l_cr1im_0v");
        cats.init("sr0l_cr2ie_0v"); cats.init("sr0l_cr2im_0v");
        if (_extra) cats.init("sr0l_cr1ig_0v");

        for (const auto& cat : cats.regions) {
          const string mod = cat.first + "_";

          // HepData-based booking
          const string phsp1 = "met_mono";
          const string phsp2 = "met_vbf";
          const string phsp3 = "mjj_vbf";
          const string phsp4 = "dphijj_vbf";
          const string phsp4dbp = "dphij_vbf"; // Silly typo :(
          const string suff = _extra? "_dbp" : "";
          // book the ones in the paper from yoda file
          if (mod=="sr0l_") {
            book(_h[mod + phsp1], mod + phsp1 + suff);
            book(_h[mod + phsp2], mod + phsp2 + suff);
            book(_h[mod + phsp3], mod + phsp3 + suff);
            book(_h[mod + phsp4], mod + phsp4 + suff);
          } else if (mod=="sr0l_cr2im_0v_") {
            book(_h[mod + phsp1], mod + phsp1 + suff);
            book(_h[mod + phsp2], mod + phsp2 + suff);
            book(_h[mod + phsp3], mod + phsp3 + suff);
            book(_h[mod + phsp4], mod + phsp4 + suff);
          } else if (mod=="sr0l_cr1im_0v_") {
            book(_h[mod + phsp1], mod + phsp1 + suff);
            book(_h[mod + phsp2], mod + phsp2 + suff);
            book(_h[mod + phsp3], mod + phsp3 + suff);
            book(_h[mod + phsp4], mod + phsp4 + suff);
          } else if (mod=="sr0l_cr2ie_0v_") {
            book(_h[mod + phsp1], mod + phsp1 + suff);
            book(_h[mod + phsp2], mod + phsp2 + suff);
            book(_h[mod + phsp3], mod + phsp3 + suff);
            book(_h[mod + phsp4], mod + (_extra? phsp4dbp : phsp4) + suff);
          } else if (mod=="sr0l_cr1ie_0v_") {
            book(_h[mod + phsp1], mod + phsp1 + suff);
            book(_h[mod + phsp2], mod + phsp2 + suff);
            book(_h[mod + phsp3], mod + phsp3 + suff);
            book(_h[mod + phsp4], mod + phsp4 + suff);
          }
          else if (_extra && mod=="sr0l_cr1ig_0v_") {
            book(_h[mod + phsp1], mod + phsp1 + suff);
            book(_h[mod + phsp2], mod + phsp2 + suff);
            book(_h[mod + phsp3], mod + phsp3 + suff);
            book(_h[mod + phsp4], mod + phsp4 + suff);
          }

        }
      } // end of initialize

      /// Perform the per-event analysis
      void analyze(const Event& event) {

        cats.reset();

        // get prompt photons
        const Particles &photons = apply<PromptFinalState>(event, "Photons").particlesByPt(Cuts::abseta < 2.47 && Cuts::pT > 7*GeV);

        // get dressed leptons
        DressedLeptons leptons = apply<LeptonFinder>(event, "DressedLeptons").dressedLeptons(cmpMomByPt);
        // additional lepton veto
        if (leptons.size() > 2)  vetoEvent;

        // veto on *prompt* hadronic tau candidates
        for (const auto& jet : apply<FastJets>(event, "Jets").jetsByPt(Cuts::pT > 20*GeV && ecal_cuts)) {
          for (const auto& p : jet.particles()) {
            if (p.fromHadronicTau(true))  vetoEvent; // true = only consider prompt taus
          }
        }

        // get anti-kT 0.4 jets
        Jets jets = apply<FastJets>(event, "Jets").jetsByPt(Cuts::pT > 30*GeV && Cuts::absrap < 4.4);

        // remove jets within dR = 0.2 cone of a dressed lepton
        idiscardIfAnyDeltaRLess(jets, leptons, 0.2);
        // remove dressed leptons within dR = 0.4 cone of a jet
        idiscardIfAnyDeltaRLess(leptons, jets, 0.4);
        // remove jets within dR = 0.2 of a prompt photon
        if (_extra && leptons.empty() && photons.size()) {
          idiscardIfAnyDeltaRLess(jets, photons, 0.2);
        }

        const size_t njets = jets.size();
        const size_t njets_gap = centralJetVeto(jets);

        // calculate MET
        Vector3 METvec;
        if (_extra) {
          METvec = sum(apply<InvisibleFinalState>(event, "promptNeutrinos").particles(), Kin::pTvec, METvec).setZ(0);
        }
        else {
          METvec = apply<MissingMomentum>(event, "actualMET").vectorMissingPt();
        }
        const double actualMET = METvec.mod()/GeV; // actual pTmiss
        Vector3 pMETvec = sum(leptons, Kin::pTvec, METvec).setZ(0); // pseudo-MET with 'invisible' leptons
        if (_extra && leptons.empty() && photons.size()) {
          pMETvec = (photons[0].pTvec() + pMETvec).setZ(0); // pseudo-MET with 'invisible' photon
        }
        const double ptmiss = pMETvec.mod()/GeV;

        // lepton-MET kinematics
        const double lep_pt = leptons.size()? leptons[0].pT()/GeV : 0.;
        const bool hasZ = hasZcandidate(leptons);
        double dphi_metl = -1., mT_l = 0.;
        if (leptons.size()) {
          dphi_metl = deltaPhi(leptons[0], METvec); // actual MET here since SR1 and SR2 have visible leps
          mT_l = sqrt( 2 * lep_pt * actualMET * (1 - cos(dphi_metl) ) );
          if (leptons.size() > 1) {
            FourMomentum Z = leptons[0].mom() + leptons[1].mom();
          }
        }

        // photon kinematics
        const double photon_pt = photons.size()? photons[0].pT()/GeV : 0.;

        // photon isolation
        bool is_iso_gamma = true;
        if (_extra) {
          const vector<double> eta_bins = {0.0, 1.5, 3.0};
          vector<double> rho(eta_bins.size()-1, 0.0);
          FastJets kTjets = apply<FastJets>(event, "kTjets");
          for (size_t ieta = 0; ieta < eta_bins.size()-1; ++ieta) {
            fastjet::Selector fjselector(fastjet::SelectorAbsRapRange(eta_bins[ieta], eta_bins[ieta+1]));
            double sigma, area;
            kTjets.clusterSeqArea()->get_median_rho_and_sigma(fjselector, true, rho[ieta], sigma, area);
          }
          const double isoRCone = 0.4;
          for (const Particle& photon : photons) {
            // Compute calo isolation via particles within a cone around the photon
            FourMomentum mom_in_EtCone;
            for (const Particle& p : apply<VetoedFinalState>(event, "CaloParticles").particles()) {
              if (deltaR(photon, p) > isoRCone)  continue; // reject if not in cone
              mom_in_EtCone += p; // sum momentum
            }
            mom_in_EtCone -= photon; // subtract core photon
            // UE subtraction energy
            const double UEpT = M_PI * sqr(isoRCone) * rho[binIndex(photon.abseta(), eta_bins)];
            // Use photon if energy in isolation cone is low enough
            if (mom_in_EtCone.Et()/GeV - UEpT > 0.044*photon.mom().pT()/GeV + 2.45) is_iso_gamma = false;
            break;
          }
        }

        // jet kinematics
        double jpt1 = 0, jeta1 = 0., jpt2 = 0.;
        double mjj = 0., drapjj = 0., dphijj = 0.;
        //double dphi_metj = 0., mT_j = 0.;
        if (njets) {
          jpt1 = jets[0].pT()/GeV;
          jeta1 = jets[0].eta();
          if (njets >= 2) {
            mjj = (jets[0].mom() + jets[1].mom()).mass()/GeV;
            jpt2 = jets[1].pT()/GeV;
            drapjj = deltaRap(jets[0], jets[1]);
            dphijj = signedDeltaPhi(jets[0], jets[1]);
          }
        }

        // jet-MET balance (check dPhi between MET and first 4 jets)
        bool fail_dphi_jet_MET = false;
        for (size_t i = 0; i < jets.size() && i < 4; ++i) {
          fail_dphi_jet_MET |= (deltaPhi(jets[i], pMETvec) < 0.4);
        }

        // start categorising
        if (leptons.empty() && ptmiss > 200. && !fail_dphi_jet_MET) { // 0-lep SRs
          cats.trigger("sr0l");
        }

        if (leptons.size() == 1 && ptmiss > 200. && !fail_dphi_jet_MET) { // CR for 0-lep SR using W
          // higher leading electron pT cut due to trigger, cut on actual MET to suppress fakes
          if (leptons[0].abspid() == PID::ELECTRON && lep_pt > 30. && actualMET > 60. && inRange(mT_l, 30., 100.)) {
            cats.trigger("sr0l_cr1ie_0v");
          }
          else if (leptons[0].abspid() == PID::MUON) {
            cats.trigger("sr0l_cr1im_0v");
          }
        }
        if (hasZ && ptmiss > 200. && lep_pt > 80. && !fail_dphi_jet_MET) { // CR for 0-lep SR using Z
          if (leptons[0].abspid() == PID::ELECTRON) {
            cats.trigger("sr0l_cr2ie_0v");
          }
          else if (leptons[0].abspid() == PID::MUON) {
            cats.trigger("sr0l_cr2im_0v");
          }
        }
        if (_extra && leptons.empty() && ptmiss > 200. && photons.size() && photon_pt > 160.) {
          if (!fail_dphi_jet_MET && is_iso_gamma) { // CR for 0-lep SR using gamma
            cats.trigger("sr0l_cr1ig_0v");
          }
        }

        // identify jet topology
        const bool pass_monojet = jpt1 > 120*GeV && fabs(jeta1) < 2.4;
        const bool pass_vbf = njets >= 2 && mjj > 200*GeV && jpt1 > 80. && jpt2 > 50. && fabs(drapjj) > 1.; // CJV applied below

        // fill histograms for all categories
        for (const auto& cat : cats.regions) {

          // check if category was triggered
          if (!cat.second)  continue;
          // construct prefix for histogram name
          const string mod = cat.first + "_";

          if (pass_monojet) {
            _h[mod + "met_mono"]->fill(ptmiss);
          } // end of pass monojet

          if (pass_vbf) {
            if (!njets_gap) { // gap-jet veto
              // This is the VBF region!
              _h[mod + "met_vbf"]->fill(ptmiss);
              _h[mod + "mjj_vbf"]->fill(mjj);
              _h[mod + "dphijj_vbf"]->fill(dphijj/pi);
            }
          } // end of pass VBF

        } // end of loop over categories

      }// end of analyze


      /// Normalise, scale and otherwise manipulate histograms here
      void finalize() {
        scale(_h, crossSection() / sumOfWeights() / femtobarn);

        if (_isBSM && !_extra) {
          for (const string& auxil : vector<string>{"cr1ie_"s, "cr1im_"s, "cr2ie_"s, "cr2im_"s}) {
            for (const string& obs : vector<string>{"met_mono"s, "met_vbf"s, "mjj_vbf"s, "dphijj_vbf"s}) {
              const string rmisslabel("rmiss_"s+auxil+obs);
              MSG_DEBUG("Constructing Rmiss for " << rmisslabel);
              if (_e.find(rmisslabel) == _e.end())  book(_e[rmisslabel], rmisslabel); // book for first weight
              // load SM prediction
              // inject BSM component
              YODA::Estimate1D numer = injectBSM("sr0l_"s+obs);
              YODA::Estimate1D denom = injectBSM("sr0l_"s+auxil+"0v_"s+obs);
              // construct Rmiss
              if (numer != denom)  numer.rebinXTo(denom.xEdges()); // harmonise binning if need be
              YODA::Estimate1D ratio = numer / denom; // assumes uncorrelated error breakdowns
              // copy over old Rmiss error breakdown
              const string path = _e[rmisslabel]->path();
              *_e[rmisslabel] = refData(rmisslabel+"_thy_nlo"s);
              _e[rmisslabel]->setPath(path);
              // update central value and stats error component
              for (auto& b : _e[rmisslabel]->bins()) {
                const auto& injb = ratio.bin(b.index());
                b.set(injb.val(), injb.err("stats"), "stats");
              }
            }
          }
        }
      }


      YODA::Estimate1D injectBSM(const string& label) {
        YODA::Estimate1D SM = refData(label+"_thy_nlo"s);
        for (auto& b : SM.bins())  b.scale(b.dVol());
        return SM + _h[label]->mkEstimate("", "stats", false);
      }

      // check if jet is between tagging jets
      bool isBetween(const Jet& probe, const Jet& boundary1, const Jet& boundary2) const {
        double y_p = probe.rapidity();
        double y_b1 = boundary1.rapidity();
        double y_b2 = boundary2.rapidity();

        double y_min = std::min(y_b1, y_b2);
        double y_max = std::max(y_b1, y_b2);

        return  (y_p > y_min && y_p < y_max);
      }


      // count number of gap jets for central jet veto
      size_t centralJetVeto(const Jets &jets) const {
        if (jets.size() < 2) return 0;
        const Jet& bj1 = jets.at(0);
        const Jet& bj2 = jets.at(1);

        size_t n_between = 0;
        // start loop at the 3rd hardest pT jet
        for (size_t i = 2; i < jets.size(); ++i) {
          const Jet& j = jets.at(i);
          if (isBetween(j, bj1, bj2))  ++n_between;
        }
        return n_between;
      }


      bool hasZcandidate(const DressedLeptons& leptons) const {
        if (leptons.size() != 2)  return false; // ask for exactly two leptons
        if (leptons[0].pid() != -leptons[1].pid())  return false; // check for SFOS
        double Zmass = (leptons[0].mom() + leptons[1].mom()).mass();
        return inRange(Zmass, 66*GeV,  116*GeV); // check dilepton mass
      }


      double signedDeltaPhi(const Jet& j1, const Jet& j2) const {
        double dphijj;
        if (j1.rap() > j2.rap()) {
          dphijj = j1.phi() - j2.phi();
        }
        else {
          dphijj = j2.phi() - j1.phi();
        }
        return mapAngleMPiToPi(dphijj);
      }


      struct Categories {
        map<string, bool> regions;

        Categories () { }

        void init(const string& name) { regions[name] = false; }

        void trigger(const string& name) {
          regions[name] = true;
        }

        void reset() {
          for (auto& reg : regions) { reg.second = false; }
        }
      };


    private:

      // Data members like post-cuts event weight counters go here
      /// @name Histograms
      //@{
      map<string, Histo1DPtr> _h;

      map<string, Estimate1DPtr> _e;

      Categories cats;

      Cut ecal_cuts;

      size_t _extra, _isBSM;
      //@}

  };

  // The hook for the plugin system
  RIVET_DECLARE_PLUGIN(ATLAS_2024_I2765017);
}