Rivet analyses

Jet mass of W bosons decaying to qq in pp collisions at 13 TeV

Experiment: CMS (LHC)

Inspire ID: 3132411

Status: VALIDATED

Authors: - cms-pag-conveners-smp@cern.ch - Andreas Hinzmann

References: - arXiv: 2603.19963 - Expt page: CMS-SMP-24-012 - submitted to JHEP

Beams: p+ p+

Beam energies: (6500.0, 6500.0)GeV

Run details: - pp to W+jets to all-jets final state at $\sqrt{s}=13$ TeV. Data collected by CMS during the years 2016-2018. Set MATCHING=TRUE to enable “W-match” distributions where the q/g combinatorial background is removed.

The jet mass of W bosons decaying to a quark-antiquark pair is measured in W+jets events from proton-proton collisions at a center-of-mass energy of 13 TeV. The data used were collected by the CMS experiment at the CERN LHC and correspond to an integrated luminosity of 138 fb. Hadronic decays of W bosons with high momenta produce strongly collimated decay products due to the large Lorentz boost, and are reconstructed as single large-radius jets. These jets have a characteristic substructure that is exploited to distinguish them from the large background of quark- and gluon-initiated jets. The jet mass is computed using the soft-drop algorithm, which suppresses soft wide-angle radiation that leads to a broadening of the jet mass distribution. For the first time, unfolded measurements are presented of the double-differential W+jets cross section as a function of the jet transverse momentum and soft-drop mass.

Source code:CMS_2026_I3132411.cc

// -*- C++ -*-
#include "Rivet/Analysis.hh"
#include "Rivet/Projections/FinalState.hh"
#include "Rivet/Projections/FastJets.hh"
#include "fastjet/contrib/SoftDrop.hh"
#include "fastjet/contrib/EnergyCorrelator.hh"
#include <algorithm>

namespace Rivet {


  /// @brief Boosted W jet mass at 13 TeV
  class CMS_2026_I3132411 : public Analysis {
  public:

    /// Constructor
    RIVET_DEFAULT_ANALYSIS_CTOR(CMS_2026_I3132411);


    /// @name Analysis methods
    /// @{

    /// Book histograms and initialise projections before the run
    void init() {

      _doMatching = getOption("MATCHING", false);

      // Initialise and register projections
      FinalState fs(Cuts::abseta < 5 && Cuts::pT > 0*GeV);

      declare(FastJets(fs, JetAlg::ANTIKT, 0.8, JetMuons::ALL, JetInvisibles::NONE), "JetsAK8");

      // Book histograms
      // resize vectors appropriately
      uint nHistsPt = _ptBinsGen.size()+1; // including inclusive bin
      _h_wjm.resize(nHistsPt);
      _h_wjm_n2.resize(nHistsPt);
      if (_doMatching) {
        _h_wjm_match.resize(nHistsPt);
        _h_wjm_n2_match.resize(nHistsPt);
      }
      // Now book histos and map to numbering in HepData
      const vector<int> hepdataindex = {6, 7, 8, 9};
      const vector<int> hepdataindex_n2 = {5, 1, 2, 3};
      for (uint ptInd=0; ptInd < nHistsPt; ptInd++) {
        book(_h_wjm[ptInd], hepdataindex[ptInd], 1, 1);
        book(_h_wjm_n2[ptInd], hepdataindex_n2[ptInd], 1, 1);
        if (_doMatching) {
          const vector<int> hepdataindex_match = {14, 15, 16, 17};
          const vector<int> hepdataindex_n2_match = {13, 10, 11, 12};
          book(_h_wjm_match[ptInd], hepdataindex_match[ptInd], 1, 1);
          book(_h_wjm_n2_match[ptInd], hepdataindex_n2_match[ptInd], 1, 1);
        }
      }

    }


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

      const Jets& jets = apply<FastJets>(event, "JetsAK8").jetsByPt(Cuts::pT > 650*GeV);
      if (jets.empty()) vetoEvent;

      const Jet& rjet1 = jets[0];
      const PseudoJet& jet1 = rjet1.pseudojet();

      fastjet::contrib::SoftDrop sd(0, 0.1, 0.8);
      PseudoJet groomedJet = sd(jet1);

      auto itr = std::lower_bound(_ptBinsGen.begin(), _ptBinsGen.end(), jet1.pt());
      uint ptBinInd = itr - _ptBinsGen.begin() - 1;
      _h_wjm[0]->fill(groomedJet.m()/GeV); // inclusive bin
      _h_wjm[ptBinInd+1]->fill(groomedJet.m()/GeV);

      fastjet::contrib::EnergyCorrelatorN2 N2(1,fastjet::contrib::EnergyCorrelator::pt_R);
      double n2 = N2(jet1);
      if (n2<0.2) {
        _h_wjm_n2[0]->fill(groomedJet.m()/GeV); // inclusive bin
        _h_wjm_n2[ptBinInd+1]->fill(groomedJet.m()/GeV);
      }

      // for matching studies
      if (_doMatching) {
        bool match = false;
        std::vector<Particle> genWs = event.allParticles(Cuts::abspid == 24);
        if (genWs.size() > 0) {
          Particle genW = genWs[0];
          while (genW.children().size() > 0 && genW.children().size() < 2) genW = genW.children()[0];  // find last copy of W
          if (genW.children().size() == 2) {
            Particle genQ1 = genW.children()[0];
            Particle genQ2 = genW.children()[1];
            double dR1 = deltaR(genQ1, rjet1);
            double dR2 = deltaR(genQ2, rjet1);
            match = ((dR1 < 0.8) && (dR2 < 0.8));
          }
        }
        if (match) {
          _h_wjm_match[0]->fill(groomedJet.m() / GeV);  // inclusive bin
          _h_wjm_match[ptBinInd + 1]->fill(groomedJet.m() / GeV);
          if (n2 < 0.2) {
            _h_wjm_n2_match[0]->fill(groomedJet.m() / GeV);  // inclusive bin
            _h_wjm_n2_match[ptBinInd + 1]->fill(groomedJet.m() / GeV);
          }
        }
      }
    }

    /// Normalise histograms etc., after the run
    void finalize() {

      const double sf = crossSection() / femtobarn / sumOfWeights();
      scale(_h_wjm, sf);
      scale(_h_wjm_n2, sf);
      if (_doMatching) {
        scale(_h_wjm_match, sf);
        scale(_h_wjm_n2_match, sf);
      }

    }

    /// @}


    /// @name Histograms
    /// @{
    const std::vector<float> _ptBinsGen = {
      650,800,1200
    };

    bool _doMatching;

    // since each pt bin has its own normalised distribution
    vector<Histo1DPtr> _h_wjm;
    vector<Histo1DPtr> _h_wjm_n2;
    vector<Histo1DPtr> _h_wjm_match;
    vector<Histo1DPtr> _h_wjm_n2_match;

    /// @}


  };


  RIVET_DECLARE_PLUGIN(CMS_2026_I3132411);

}