diff --git a/Configuration/Applications/python/ConfigBuilder.py b/Configuration/Applications/python/ConfigBuilder.py
index 83dc0655c033e..c76eb2bee9dd5 100644
--- a/Configuration/Applications/python/ConfigBuilder.py
+++ b/Configuration/Applications/python/ConfigBuilder.py
@@ -1830,6 +1830,47 @@ def prepare_RECOBEFMIX(self, stepSpec = "reconstruction"):
def prepare_PAT(self, stepSpec = "patTask"):
''' Enrich the schedule with PAT '''
+
+ # Handle @-prefixed flavors (e.g., @Scout for scouting MiniAOD)
+ if '@' in stepSpec:
+ from PhysicsTools.PatFromScouting.autoPAT import autoPAT, expandPATMapping
+
+ _patSeq = stepSpec.split('+')
+ _patCustoms = stepSpec.split('+')
+ expandPATMapping(_patSeq, autoPAT, 'sequence')
+ expandPATMapping(_patCustoms, autoPAT, 'customize')
+
+ # Remove duplicates while preserving order
+ _patSeq = list(sorted(set(_patSeq), key=_patSeq.index))
+ _patCustoms = list(sorted(set(_patCustoms), key=_patCustoms.index))
+ # Remove empty strings
+ _patSeq = [s for s in _patSeq if s]
+ _patCustoms = [c for c in _patCustoms if c]
+
+ # Load and schedule sequences
+ _seqToSchedule = []
+ for _subSeq in _patSeq:
+ if '.' in _subSeq:
+ _cff, _seq = _subSeq.rsplit('.', 1)
+ print(f"PAT: scheduling: {_seq} from {_cff}")
+ self.loadAndRemember(_cff)
+ _seqToSchedule.append(_seq)
+ elif '/' in _subSeq:
+ self.loadAndRemember(_subSeq)
+
+ if _seqToSchedule:
+ self.scheduleSequence('+'.join(_seqToSchedule), 'patMiniAOD_step')
+
+ # Add customizations
+ for custom in _patCustoms:
+ self._options.customisation_file.append(custom)
+
+ # cpu efficiency boost when running PAT by itself
+ if self.stepKeys[0] == 'PAT':
+ self._customise_coms.append( 'process.source.delayReadingEventProducts = cms.untracked.bool(False)')
+
+ return
+
_,pat_sequence,pat_cff = self.loadDefaultOrSpecifiedCFF(stepSpec,self.PATDefaultCFF)
## handle the noise filters as Flag_* path that were loaded
for existing_path,path_ in self.process.paths_().items():
@@ -1874,6 +1915,12 @@ def prepare_NANO(self, stepSpec = '' ):
print(_nanoSeq)
# create full specified sequence using autoNANO
from PhysicsTools.NanoAOD.autoNANO import autoNANO, expandNanoMapping
+ # Extend with scouting-specific NANO flavors if available
+ try:
+ from PhysicsTools.PatFromScouting.autoPAT import autoNANO_scouting
+ autoNANO.update(autoNANO_scouting)
+ except ImportError:
+ pass # PatFromScouting not available
# if not a autoNANO mapping, load an empty customization, which later will be converted into the default.
_nanoCustoms = _nanoSeq.split('+') if '@' in stepSpec else ['']
_nanoSeq = _nanoSeq.split('+')
diff --git a/DataFormats/MuonReco/interface/Muon.h b/DataFormats/MuonReco/interface/Muon.h
index 6f19b0fa784d8..c0b70c30b14e8 100644
--- a/DataFormats/MuonReco/interface/Muon.h
+++ b/DataFormats/MuonReco/interface/Muon.h
@@ -294,6 +294,7 @@ namespace reco {
static const unsigned int RPCMuon = 1 << 6;
static const unsigned int GEMMuon = 1 << 7;
static const unsigned int ME0Muon = 1 << 8;
+ static const unsigned int ScoutingMuon = 1 << 9;
void setType(unsigned int type) { type_ = type; }
unsigned int type() const { return type_; }
@@ -308,6 +309,7 @@ namespace reco {
bool isRPCMuon() const { return type_ & RPCMuon; }
bool isGEMMuon() const { return type_ & GEMMuon; }
bool isME0Muon() const { return type_ & ME0Muon; }
+ bool isScoutingMuon() const { return type_ & ScoutingMuon; }
private:
/// check overlap with another candidate
diff --git a/DataFormats/PatCandidates/BuildFile.xml b/DataFormats/PatCandidates/BuildFile.xml
index 0b876ed1a519b..39a90ce086873 100644
--- a/DataFormats/PatCandidates/BuildFile.xml
+++ b/DataFormats/PatCandidates/BuildFile.xml
@@ -23,6 +23,7 @@
+
diff --git a/DataFormats/PatCandidates/interface/Electron.h b/DataFormats/PatCandidates/interface/Electron.h
index 177fa0bd7fc13..d1928ee1029a4 100644
--- a/DataFormats/PatCandidates/interface/Electron.h
+++ b/DataFormats/PatCandidates/interface/Electron.h
@@ -31,6 +31,9 @@
#include "DataFormats/PatCandidates/interface/PackedCandidate.h"
#include "DataFormats/Common/interface/AtomicPtrCache.h"
+// Forward declaration for scouting
+class Run3ScoutingElectron;
+
// Define typedefs for convenience
namespace pat {
class Electron;
@@ -61,6 +64,8 @@ namespace pat {
Electron(const edm::RefToBase& anElectronRef);
/// constructor from a Ptr to a reco::GsfElectron
Electron(const edm::Ptr& anElectronRef);
+ /// constructor from Run3ScoutingElectron
+ Electron(const Run3ScoutingElectron& scoutingElectron);
/// destructor
~Electron() override;
@@ -269,6 +274,29 @@ namespace pat {
associatedPackedFCandidateIndices_.insert(associatedPackedFCandidateIndices_.end(), beginIndexItr, endIndexItr);
}
+ // ---- Scouting flag ----
+ bool isScoutingElectron() const { return isScoutingElectron_; }
+
+ // ---- Universal accessors with scouting dispatch ----
+ // Track-related: dispatches to gsfTrack() for standard, scouting members for scouting
+ float trkEta() const;
+ float trkPhi() const;
+ float trkpMode() const;
+ float trketaMode() const;
+ float trkphiMode() const;
+ float trkqoverpModeError() const;
+
+ // ECAL crystal-level: dispatches to superCluster() for standard, scouting members for scouting
+ uint32_t seedId() const;
+ uint32_t nClusters() const;
+ uint32_t nCrystals() const;
+
+ // ---- Scouting-only accessors (no standard equivalent) ----
+ std::vector const& scoutingEnergyMatrix() const { return scoutingEnergyMatrix_; }
+ std::vector const& scoutingTimingMatrix() const { return scoutingTimingMatrix_; }
+ std::vector const& scoutingDetIds() const { return scoutingDetIds_; }
+ bool scoutingRechitZeroSuppression() const { return scoutingRechitZeroSuppression_; }
+
friend class PATElectronSlimmer;
friend class PATElectronCandidatesRekeyer;
@@ -377,6 +405,22 @@ namespace pat {
// ---- link to PackedPFCandidates
edm::RefProd packedPFCandidates_;
std::vector associatedPackedFCandidateIndices_;
+
+ // ---- scouting-specific members ----
+ bool isScoutingElectron_{false};
+ std::vector scoutingTrkEta_;
+ std::vector scoutingTrkPhi_;
+ std::vector scoutingTrkpMode_;
+ std::vector scoutingTrketaMode_;
+ std::vector scoutingTrkphiMode_;
+ std::vector scoutingTrkqoverpModeError_;
+ uint32_t scoutingSeedId_{0};
+ uint32_t scoutingNClusters_{0};
+ uint32_t scoutingNCrystals_{0};
+ std::vector scoutingEnergyMatrix_;
+ std::vector scoutingTimingMatrix_;
+ std::vector scoutingDetIds_;
+ bool scoutingRechitZeroSuppression_{false};
};
} // namespace pat
diff --git a/DataFormats/PatCandidates/interface/Jet.h b/DataFormats/PatCandidates/interface/Jet.h
index 57c6e2a491d7f..bbfe49e07e58a 100644
--- a/DataFormats/PatCandidates/interface/Jet.h
+++ b/DataFormats/PatCandidates/interface/Jet.h
@@ -48,6 +48,9 @@
#include
+// Forward declaration for scouting
+class Run3ScoutingPFJet;
+
// Define typedefs for convenience
namespace pat {
class Jet;
@@ -95,6 +98,8 @@ namespace pat {
Jet(const edm::RefToBase& aJetRef);
/// constructure from ref to pat::Jet
Jet(const edm::Ptr& aJetRef);
+ /// constructor from Run3ScoutingPFJet
+ Jet(const Run3ScoutingPFJet& scoutingJet);
/// destructor
~Jet() override;
/// required reimplementation of the Candidate's clone method
diff --git a/DataFormats/PatCandidates/interface/Muon.h b/DataFormats/PatCandidates/interface/Muon.h
index a9054c8927350..032e644512c4f 100644
--- a/DataFormats/PatCandidates/interface/Muon.h
+++ b/DataFormats/PatCandidates/interface/Muon.h
@@ -25,12 +25,15 @@
#include "DataFormats/MuonReco/interface/MuonTimeExtra.h"
#include "DataFormats/TrackReco/interface/Track.h"
+#include "DataFormats/TrackReco/interface/HitPattern.h"
#include "DataFormats/PatCandidates/interface/Lepton.h"
#include "DataFormats/ParticleFlowCandidate/interface/IsolatedPFCandidateFwd.h"
#include "DataFormats/ParticleFlowCandidate/interface/IsolatedPFCandidate.h"
#include "DataFormats/MuonReco/interface/MuonSimInfo.h"
// Define typedefs for convenience
+class Run3ScoutingMuon;
+
namespace pat {
class Muon;
typedef std::vector MuonCollection;
@@ -59,6 +62,8 @@ namespace pat {
Muon(const edm::RefToBase& aMuonRef);
/// constructor from a Ptr to a reco muon
Muon(const edm::Ptr& aMuonRef);
+ /// constructor from a scouting muon
+ Muon(const Run3ScoutingMuon& aMuon);
/// destructor
~Muon() override;
@@ -344,6 +349,46 @@ namespace pat {
}
bool triggered(const char* pathName) const { return triggerObjectMatchByPath(pathName, true, true) != nullptr; }
+ // ---- Methods that hide reco::Muon methods for scouting muon compatibility ----
+ // For ScoutingMuon, these return cached values; otherwise delegate to reco::Muon
+
+ /// Number of chambers (hides reco::Muon::numberOfChambers)
+ int numberOfChambers() const;
+ /// Number of chambers CSC or DT only (hides reco::Muon::numberOfChambersCSCorDT)
+ int numberOfChambersCSCorDT() const;
+ /// Number of muon stations with matches (hides reco::Muon::numberOfMatches)
+ int numberOfMatches(reco::Muon::ArbitrationType type = reco::Muon::SegmentAndTrackArbitration) const;
+ /// Number of matched stations (hides reco::Muon::numberOfMatchedStations)
+ int numberOfMatchedStations(reco::Muon::ArbitrationType type = reco::Muon::SegmentAndTrackArbitration) const;
+ /// Expected number of matched stations (hides reco::Muon::expectedNnumberOfMatchedStations)
+ unsigned int expectedNnumberOfMatchedStations(float minDistanceFromEdge = 10.0) const;
+ /// Muon station mask (hides reco::Muon::stationMask)
+ unsigned int stationMask(reco::Muon::ArbitrationType type = reco::Muon::SegmentAndTrackArbitration) const;
+ /// Number of matched RPC layers (hides reco::Muon::numberOfMatchedRPCLayers)
+ int numberOfMatchedRPCLayers(reco::Muon::ArbitrationType type = reco::Muon::RPCHitAndTrackArbitration) const;
+ /// RPC layer mask (hides reco::Muon::RPClayerMask)
+ unsigned int RPClayerMask(reco::Muon::ArbitrationType type = reco::Muon::RPCHitAndTrackArbitration) const;
+
+ // ---- Hit information accessors ----
+ // For scouting muons return cached values; for standard muons compute from tracks
+
+ /// Number of valid muon hits (from global track hitPattern for standard muons)
+ int numberOfValidMuonHits() const;
+ /// Number of valid standalone muon hits
+ int numberOfValidStandAloneMuonHits() const;
+ /// Number of standalone muon matched stations
+ int numberOfStandAloneMuonMatchedStations() const;
+ /// Number of valid pixel hits (from inner track hitPattern)
+ int numberOfValidPixelHits() const;
+ /// Number of valid strip hits (from inner track hitPattern)
+ int numberOfValidStripHits() const;
+ /// Number of pixel layers with measurement
+ int numberOfPixelLayersWithMeasurement() const;
+ /// Number of tracker layers with measurement
+ int numberOfTrackerLayersWithMeasurement() const;
+ /// Track hit pattern (from inner track for standard muons, from scouting data for scouting muons)
+ reco::HitPattern const& trkHitPattern() const;
+
protected:
// ---- for content embedding ----
@@ -439,6 +484,22 @@ namespace pat {
float simEta_;
float simPhi_;
float simMatchQuality_;
+
+ private:
+ // ---- Cached values for scouting muons ----
+ // Filled by constructor from Run3ScoutingMuon; accessors check isScoutingMuon()
+ int scoutingNChambers_{0};
+ int scoutingNChambersCSCorDT_{0};
+ int scoutingNMatches_{0};
+ int scoutingNMatchedStations_{0};
+ unsigned int scoutingExpectedMatchedStations_{0};
+ unsigned int scoutingStationMask_{0};
+ int scoutingNMatchedRPCLayers_{0};
+ unsigned int scoutingRPCLayerMask_{0};
+ int scoutingNValidMuonHits_{0};
+ int scoutingNValidStandAloneMuonHits_{0};
+ int scoutingNStandAloneMuonMatchedStations_{0};
+ reco::HitPattern scoutingTrkHitPattern_;
};
} // namespace pat
diff --git a/DataFormats/PatCandidates/interface/Photon.h b/DataFormats/PatCandidates/interface/Photon.h
index 4ce2d99821825..0989de01ec99b 100644
--- a/DataFormats/PatCandidates/interface/Photon.h
+++ b/DataFormats/PatCandidates/interface/Photon.h
@@ -17,6 +17,7 @@
\author Steven Lowette, Giovanni Petrucciani, Frederic Ronga
*/
+#include
#include "DataFormats/PatCandidates/interface/PATObject.h"
#include "DataFormats/EgammaCandidates/interface/Photon.h"
#include "DataFormats/EgammaReco/interface/SuperClusterFwd.h"
@@ -26,6 +27,9 @@
#include "DataFormats/EcalRecHit/interface/EcalRecHitCollections.h"
#include "DataFormats/Common/interface/AtomicPtrCache.h"
+// Forward declaration for scouting
+class Run3ScoutingPhoton;
+
// Define typedefs for convenience
namespace pat {
class Photon;
@@ -56,6 +60,8 @@ namespace pat {
Photon(const edm::RefToBase& aPhotonRef);
/// constructor from a Ptr to a reco photon
Photon(const edm::Ptr& aPhotonRef);
+ /// constructor from Run3ScoutingPhoton
+ Photon(const Run3ScoutingPhoton& scoutingPhoton);
/// destructor
~Photon() override;
@@ -328,6 +334,21 @@ namespace pat {
/// get the source candidate pointer with index i
reco::CandidatePtr sourceCandidatePtr(size_type i) const override;
+ // ---- Scouting flag ----
+ bool isScoutingPhoton() const { return isScoutingPhoton_; }
+
+ // ---- Universal accessors with scouting dispatch ----
+ // ECAL crystal-level: dispatches to superCluster() for standard, scouting members for scouting
+ uint32_t seedId() const;
+ uint32_t nClusters() const;
+ uint32_t nCrystals() const;
+
+ // ---- Scouting-only accessors (no standard equivalent) ----
+ std::vector const& scoutingEnergyMatrix() const { return scoutingEnergyMatrix_; }
+ std::vector const& scoutingTimingMatrix() const { return scoutingTimingMatrix_; }
+ std::vector const& scoutingDetIds() const { return scoutingDetIds_; }
+ bool scoutingRechitZeroSuppression() const { return scoutingRechitZeroSuppression_; }
+
friend class PATPhotonSlimmer;
friend class PATPhotonCandidatesRekeyer;
@@ -404,6 +425,16 @@ namespace pat {
// ---- link to PackedPFCandidates
edm::RefProd packedPFCandidates_;
std::vector associatedPackedFCandidateIndices_;
+
+ // ---- scouting-specific members ----
+ bool isScoutingPhoton_{false};
+ uint32_t scoutingSeedId_{0};
+ uint32_t scoutingNClusters_{0};
+ uint32_t scoutingNCrystals_{0};
+ std::vector scoutingEnergyMatrix_;
+ std::vector scoutingTimingMatrix_;
+ std::vector scoutingDetIds_;
+ bool scoutingRechitZeroSuppression_{false};
};
} // namespace pat
diff --git a/DataFormats/PatCandidates/interface/ScoutingDataHandling.h b/DataFormats/PatCandidates/interface/ScoutingDataHandling.h
new file mode 100644
index 0000000000000..2b63161237659
--- /dev/null
+++ b/DataFormats/PatCandidates/interface/ScoutingDataHandling.h
@@ -0,0 +1,28 @@
+#ifndef DataFormats_PatCandidates_ScoutingDataHandling_h
+#define DataFormats_PatCandidates_ScoutingDataHandling_h
+
+#include "DataFormats/TrackReco/interface/Track.h"
+#include "DataFormats/VertexReco/interface/Vertex.h"
+#include "DataFormats/Scouting/interface/Run3ScoutingTrack.h"
+#include "DataFormats/PatCandidates/interface/Muon.h"
+#include "DataFormats/Scouting/interface/Run3ScoutingMuon.h"
+#include "DataFormats/Scouting/interface/Run3ScoutingVertex.h"
+
+namespace pat {
+ typedef reco::Candidate::LorentzVector LorentzVector;
+ typedef reco::Candidate::PolarLorentzVector PolarLorentzVector;
+
+ // Tracks
+ reco::Track makeRecoTrack(const Run3ScoutingTrack&);
+ reco::Track makeRecoTrack(const Run3ScoutingMuon&);
+ Run3ScoutingTrack makeScoutingTrack(const reco::Track&);
+ PolarLorentzVector makePolarLorentzVector(const Run3ScoutingTrack&, float mass);
+
+ // Muons
+ pat::Muon makePatMuon(const Run3ScoutingMuon&);
+
+ // Vertex
+ reco::Vertex makeRecoVertex(const Run3ScoutingVertex&);
+} // namespace pat
+
+#endif
diff --git a/DataFormats/PatCandidates/src/Electron.cc b/DataFormats/PatCandidates/src/Electron.cc
index b115c3e7af54d..5ed4e19000907 100644
--- a/DataFormats/PatCandidates/src/Electron.cc
+++ b/DataFormats/PatCandidates/src/Electron.cc
@@ -2,10 +2,12 @@
//
#include "DataFormats/PatCandidates/interface/Electron.h"
+#include "DataFormats/Scouting/interface/Run3ScoutingElectron.h"
#include "FWCore/Utilities/interface/Exception.h"
#include "DataFormats/Common/interface/RefToPtr.h"
#include
+#include
using namespace pat;
@@ -81,6 +83,104 @@ Electron::Electron(const edm::Ptr& anElectronRef)
initImpactParameters();
}
+/// constructor from Run3ScoutingElectron
+Electron::Electron(const Run3ScoutingElectron& sElec)
+ : Lepton(),
+ embeddedGsfElectronCore_(false),
+ embeddedGsfTrack_(false),
+ embeddedSuperCluster_(false),
+ embeddedPflowSuperCluster_(false),
+ embeddedTrack_(false),
+ embeddedSeedCluster_(false),
+ embeddedRecHits_(false),
+ embeddedPFCandidate_(false),
+ ecalDrivenMomentum_(Candidate::LorentzVector(0., 0., 0., 0.)),
+ ecalRegressionEnergy_(0.0),
+ ecalTrackRegressionEnergy_(0.0),
+ ecalRegressionError_(0.0),
+ ecalTrackRegressionError_(0.0),
+ ecalScale_(-99999.),
+ ecalSmear_(-99999.),
+ ecalRegressionScale_(-99999.),
+ ecalRegressionSmear_(-99999.),
+ ecalTrackRegressionScale_(-99999.),
+ ecalTrackRegressionSmear_(-99999.),
+ packedPFCandidates_(),
+ associatedPackedFCandidateIndices_() {
+ initImpactParameters();
+ isScoutingElectron_ = true;
+
+ // Set kinematics
+ float px = sElec.pt() * std::cos(sElec.phi());
+ float py = sElec.pt() * std::sin(sElec.phi());
+ float pz = sElec.pt() * std::sinh(sElec.eta());
+ float energy = std::sqrt(px * px + py * py + pz * pz + sElec.m() * sElec.m());
+ reco::GsfElectron::LorentzVector p4(px, py, pz, energy);
+
+ // Get charge from first track
+ int charge = 0;
+ if (!sElec.trkcharge().empty()) {
+ charge = sElec.trkcharge()[0];
+ }
+
+ this->setCharge(charge);
+ this->setP4(p4);
+ this->setVertex(math::XYZPoint(0, 0, 0));
+
+ // Store shower shape variables as userFloats (GsfElectron native members not writable)
+ this->addUserFloat("sigmaIetaIeta", sElec.sigmaIetaIeta());
+ this->addUserFloat("hOverE", sElec.hOverE());
+ this->addUserFloat("r9", sElec.r9());
+ this->addUserFloat("sMin", sElec.sMin());
+ this->addUserFloat("sMaj", sElec.sMaj());
+
+ // Store ID variables
+ this->addUserFloat("dEtaIn", sElec.dEtaIn());
+ this->addUserFloat("dPhiIn", sElec.dPhiIn());
+ this->addUserFloat("ooEMOop", sElec.ooEMOop());
+ this->addUserInt("missingHits", sElec.missingHits());
+
+ // Store track variables
+ this->addUserFloat("trackfbrem", sElec.trackfbrem());
+ if (!sElec.trkd0().empty()) {
+ this->addUserFloat("trkd0", sElec.trkd0()[0]);
+ }
+ if (!sElec.trkdz().empty()) {
+ this->addUserFloat("trkdz", sElec.trkdz()[0]);
+ }
+ if (!sElec.trkpt().empty()) {
+ this->addUserFloat("trkpt", sElec.trkpt()[0]);
+ }
+ if (!sElec.trkchi2overndf().empty()) {
+ this->addUserFloat("trkchi2overndf", sElec.trkchi2overndf()[0]);
+ }
+
+ // Store energy variables
+ this->addUserFloat("rawEnergy", sElec.rawEnergy());
+ this->addUserFloat("preshowerEnergy", sElec.preshowerEnergy());
+ this->addUserFloat("corrEcalEnergyError", sElec.corrEcalEnergyError());
+
+ // Store isolation
+ this->addUserFloat("ecalIso", sElec.ecalIso());
+ this->addUserFloat("hcalIso", sElec.hcalIso());
+ this->addUserFloat("trackIso", sElec.trackIso());
+
+ // Store scouting-specific track and ECAL information
+ scoutingTrkEta_ = sElec.trketa();
+ scoutingTrkPhi_ = sElec.trkphi();
+ scoutingTrkpMode_ = sElec.trkpMode();
+ scoutingTrketaMode_ = sElec.trketaMode();
+ scoutingTrkphiMode_ = sElec.trkphiMode();
+ scoutingTrkqoverpModeError_ = sElec.trkqoverpModeError();
+ scoutingSeedId_ = sElec.seedId();
+ scoutingNClusters_ = sElec.nClusters();
+ scoutingNCrystals_ = sElec.nCrystals();
+ scoutingEnergyMatrix_ = sElec.energyMatrix();
+ scoutingTimingMatrix_ = sElec.timingMatrix();
+ scoutingDetIds_ = sElec.detIds();
+ scoutingRechitZeroSuppression_ = sElec.rechitZeroSuppression();
+}
+
/// destructor
Electron::~Electron() {}
@@ -430,3 +530,80 @@ edm::RefVector Electron::associatedPackedPFCandi
}
return ret;
}
+
+// ---- Universal accessors with scouting dispatch ----
+
+float Electron::trkEta() const {
+ if (isScoutingElectron_) {
+ return !scoutingTrkEta_.empty() ? scoutingTrkEta_[0] : 0.f;
+ }
+ auto trk = gsfTrack();
+ return trk.isNonnull() ? trk->eta() : 0.f;
+}
+
+float Electron::trkPhi() const {
+ if (isScoutingElectron_) {
+ return !scoutingTrkPhi_.empty() ? scoutingTrkPhi_[0] : 0.f;
+ }
+ auto trk = gsfTrack();
+ return trk.isNonnull() ? trk->phi() : 0.f;
+}
+
+float Electron::trkpMode() const {
+ if (isScoutingElectron_) {
+ return !scoutingTrkpMode_.empty() ? scoutingTrkpMode_[0] : 0.f;
+ }
+ auto trk = gsfTrack();
+ return trk.isNonnull() ? trk->pMode() : 0.f;
+}
+
+float Electron::trketaMode() const {
+ if (isScoutingElectron_) {
+ return !scoutingTrketaMode_.empty() ? scoutingTrketaMode_[0] : 0.f;
+ }
+ auto trk = gsfTrack();
+ return trk.isNonnull() ? trk->etaMode() : 0.f;
+}
+
+float Electron::trkphiMode() const {
+ if (isScoutingElectron_) {
+ return !scoutingTrkphiMode_.empty() ? scoutingTrkphiMode_[0] : 0.f;
+ }
+ auto trk = gsfTrack();
+ return trk.isNonnull() ? trk->phiMode() : 0.f;
+}
+
+float Electron::trkqoverpModeError() const {
+ if (isScoutingElectron_) {
+ return !scoutingTrkqoverpModeError_.empty() ? scoutingTrkqoverpModeError_[0] : 0.f;
+ }
+ auto trk = gsfTrack();
+ return trk.isNonnull() ? trk->qoverpModeError() : 0.f;
+}
+
+uint32_t Electron::seedId() const {
+ if (isScoutingElectron_) {
+ return scoutingSeedId_;
+ }
+ auto sc = superCluster();
+ if (sc.isNonnull() && sc->seed().isNonnull()) {
+ return sc->seed()->seed().rawId();
+ }
+ return 0;
+}
+
+uint32_t Electron::nClusters() const {
+ if (isScoutingElectron_) {
+ return scoutingNClusters_;
+ }
+ auto sc = superCluster();
+ return sc.isNonnull() ? sc->clustersSize() : 0;
+}
+
+uint32_t Electron::nCrystals() const {
+ if (isScoutingElectron_) {
+ return scoutingNCrystals_;
+ }
+ auto sc = superCluster();
+ return sc.isNonnull() ? sc->size() : 0;
+}
diff --git a/DataFormats/PatCandidates/src/Jet.cc b/DataFormats/PatCandidates/src/Jet.cc
index e3e7d790aaed3..7c4a62ced70a2 100644
--- a/DataFormats/PatCandidates/src/Jet.cc
+++ b/DataFormats/PatCandidates/src/Jet.cc
@@ -2,10 +2,13 @@
//
#include "DataFormats/PatCandidates/interface/Jet.h"
+#include "DataFormats/Scouting/interface/Run3ScoutingPFJet.h"
#include "DataFormats/RecoCandidate/interface/RecoCaloTowerCandidate.h"
#include "DataFormats/ParticleFlowCandidate/interface/PFCandidate.h"
#include "FWCore/MessageLogger/interface/MessageLogger.h"
+#include
+
using namespace pat;
/// default constructor
@@ -38,6 +41,62 @@ Jet::Jet(const edm::RefToBase& aJetRef) : Jet(*aJetRef) {
/// constructure from ref to pat::Jet
Jet::Jet(const edm::Ptr& aJetRef) : Jet(*aJetRef) { refToOrig_ = aJetRef; }
+/// constructor from Run3ScoutingPFJet
+Jet::Jet(const Run3ScoutingPFJet& sJet)
+ : PATObject(reco::Jet()), embeddedCaloTowers_(false), embeddedPFCandidates_(false), jetCharge_(0.) {
+ // Set kinematics
+ float px = sJet.pt() * std::cos(sJet.phi());
+ float py = sJet.pt() * std::sin(sJet.phi());
+ float pz = sJet.pt() * std::sinh(sJet.eta());
+ float energy = std::sqrt(px * px + py * py + pz * pz + sJet.m() * sJet.m());
+ reco::Particle::LorentzVector p4(px, py, pz, energy);
+
+ this->setP4(p4);
+ this->setVertex(math::XYZPoint(0, 0, 0));
+ this->setJetArea(sJet.jetArea());
+
+ // Build PFJet::Specific
+ reco::PFJet::Specific pfSpecific;
+ pfSpecific.mChargedHadronEnergy = sJet.chargedHadronEnergy();
+ pfSpecific.mNeutralHadronEnergy = sJet.neutralHadronEnergy();
+ pfSpecific.mPhotonEnergy = sJet.photonEnergy();
+ pfSpecific.mElectronEnergy = sJet.electronEnergy();
+ pfSpecific.mMuonEnergy = sJet.muonEnergy();
+ pfSpecific.mHFHadronEnergy = sJet.HFHadronEnergy();
+ pfSpecific.mHFEMEnergy = sJet.HFEMEnergy();
+
+ pfSpecific.mChargedHadronMultiplicity = sJet.chargedHadronMultiplicity();
+ pfSpecific.mNeutralHadronMultiplicity = sJet.neutralHadronMultiplicity();
+ pfSpecific.mPhotonMultiplicity = sJet.photonMultiplicity();
+ pfSpecific.mElectronMultiplicity = sJet.electronMultiplicity();
+ pfSpecific.mMuonMultiplicity = sJet.muonMultiplicity();
+ pfSpecific.mHFHadronMultiplicity = sJet.HFHadronMultiplicity();
+ pfSpecific.mHFEMMultiplicity = sJet.HFEMMultiplicity();
+
+ pfSpecific.mHOEnergy = sJet.HOEnergy();
+
+ pfSpecific.mChargedEmEnergy = sJet.electronEnergy();
+ pfSpecific.mChargedMuEnergy = sJet.muonEnergy();
+ pfSpecific.mNeutralEmEnergy = sJet.photonEnergy() + sJet.HFEMEnergy();
+
+ int chargedMultiplicity = sJet.chargedHadronMultiplicity() + sJet.electronMultiplicity() + sJet.muonMultiplicity();
+ int neutralMultiplicity = sJet.neutralHadronMultiplicity() + sJet.photonMultiplicity() + sJet.HFHadronMultiplicity() +
+ sJet.HFEMMultiplicity();
+
+ pfSpecific.mChargedMultiplicity = chargedMultiplicity;
+ pfSpecific.mNeutralMultiplicity = neutralMultiplicity;
+
+ specificPF_.push_back(pfSpecific);
+
+ // Add b-tagging discriminators
+ this->addBDiscriminatorPair(std::make_pair("pfCombinedSecondaryVertexV2BJetTags", sJet.csv()));
+ this->addBDiscriminatorPair(std::make_pair("pfDeepCSVJetTags:probb", sJet.mvaDiscriminator()));
+
+ // Also store as userFloats for convenience
+ this->addUserFloat("csv", sJet.csv());
+ this->addUserFloat("mvaDiscriminator", sJet.mvaDiscriminator());
+}
+
std::ostream& reco::operator<<(std::ostream& out, const pat::Jet& obj) {
if (!out)
return out;
diff --git a/DataFormats/PatCandidates/src/Muon.cc b/DataFormats/PatCandidates/src/Muon.cc
index cfdaa2253d900..73d2d71a8df1c 100644
--- a/DataFormats/PatCandidates/src/Muon.cc
+++ b/DataFormats/PatCandidates/src/Muon.cc
@@ -5,6 +5,9 @@
#include "DataFormats/MuonReco/interface/MuonSelectors.h"
#include "FWCore/Utilities/interface/Exception.h"
#include "DataFormats/Common/interface/RefToPtr.h"
+#include "DataFormats/Scouting/interface/Run3ScoutingMuon.h"
+#include "DataFormats/PatCandidates/interface/ScoutingDataHandling.h"
+#include "DataFormats/TrackReco/interface/HitPattern.h"
#include
using namespace pat;
@@ -129,6 +132,86 @@ Muon::Muon(const edm::Ptr& aMuonRef)
initSimInfo();
}
+/// constructor from reco::Muon
+Muon::Muon(const Run3ScoutingMuon& aMuon)
+ : Lepton(),
+ embeddedMuonBestTrack_(false),
+ embeddedTunePMuonBestTrack_(false),
+ embeddedTrack_(false),
+ embeddedStandAloneMuon_(false),
+ embeddedCombinedMuon_(false),
+ embeddedTCMETMuonCorrs_(false),
+ embeddedCaloMETMuonCorrs_(false),
+ embeddedPickyMuon_(false),
+ embeddedTpfmsMuon_(false),
+ embeddedDytMuon_(false),
+ embeddedPFCandidate_(false),
+ pfCandidateRef_(),
+ cachedNormChi2_(false),
+ normChi2_(0.0),
+ cachedNumberOfValidHits_(false),
+ numberOfValidHits_(0),
+ pfEcalEnergy_(0),
+ jetPtRatio_(0),
+ jetPtRel_(0),
+ mvaIDValue_(0),
+ softMvaValue_(0),
+ inverseBeta_(0),
+ inverseBetaErr_(0) {
+ initImpactParameters();
+ initSimInfo();
+
+ reco::Candidate::PolarLorentzVector p4(aMuon.pt(), aMuon.eta(), aMuon.phi(), aMuon.m());
+ auto track = makeRecoTrack(aMuon);
+
+ this->setCharge(aMuon.charge());
+ this->setP4(reco::Particle::LorentzVector(p4));
+ this->setVertex(track.vertex());
+
+ // Set muon type from scouting flags - always set ScoutingMuon bit
+ unsigned int muonType = reco::Muon::ScoutingMuon;
+ if (aMuon.isGlobalMuon())
+ muonType |= reco::Muon::GlobalMuon;
+ if (aMuon.isTrackerMuon())
+ muonType |= reco::Muon::TrackerMuon;
+ this->setType(muonType);
+
+ // Directly embed the track without creating a temporary TrackRef
+ // This avoids the transient reference issue that prevents serialization
+ combinedMuon_.clear();
+ combinedMuon_.push_back(track);
+ embeddedCombinedMuon_ = true;
+ this->setBestTrack(reco::Muon::CombinedTrack);
+
+ // Set isolation from scouting muon (call reco::Muon method directly)
+ reco::MuonIsolation isolation;
+ isolation.sumPt = aMuon.trackIso();
+ isolation.emEt = aMuon.ecalIso();
+ isolation.hadEt = aMuon.hcalIso();
+ reco::Muon::setIsolation(isolation, isolation);
+
+ this->setMiniPFIsolation(pat::PFIsolation());
+
+ // Store scouting-specific muon information directly
+ scoutingNChambers_ = aMuon.nRecoMuonChambers();
+ scoutingNChambersCSCorDT_ = aMuon.nRecoMuonChambersCSCorDT();
+ scoutingNMatches_ = aMuon.nRecoMuonMatches();
+ scoutingNMatchedStations_ = aMuon.nRecoMuonMatchedStations();
+ scoutingExpectedMatchedStations_ = aMuon.nRecoMuonExpectedMatchedStations();
+ scoutingStationMask_ = aMuon.recoMuonStationMask();
+ scoutingNMatchedRPCLayers_ = aMuon.nRecoMuonMatchedRPCLayers();
+ scoutingRPCLayerMask_ = aMuon.recoMuonRPClayerMask();
+ scoutingNValidMuonHits_ = aMuon.nValidRecoMuonHits();
+ scoutingNValidStandAloneMuonHits_ = aMuon.nValidStandAloneMuonHits();
+ scoutingNStandAloneMuonMatchedStations_ = aMuon.nStandAloneMuonMatchedStations();
+ // Cache normalizedChi2 from scouting muon
+ cachedNormChi2_ = true;
+ normChi2_ = aMuon.normalizedChi2();
+
+ // Store hit pattern
+ scoutingTrkHitPattern_ = reco::HitPattern(aMuon.trk_hitPattern());
+}
+
/// destructor
Muon::~Muon() {}
@@ -546,3 +629,119 @@ bool Muon::isMediumMuon() const { return muon::isMediumMuon(*this); }
bool Muon::isSoftMuon(const reco::Vertex& vtx) const { return muon::isSoftMuon(*this, vtx); }
bool Muon::isHighPtMuon(const reco::Vertex& vtx) const { return muon::isHighPtMuon(*this, vtx); }
+
+// ---- Methods that hide reco::Muon for scouting muon compatibility ----
+
+int Muon::numberOfChambers() const {
+ if (isScoutingMuon()) {
+ return scoutingNChambers_;
+ }
+ return reco::Muon::numberOfChambers();
+}
+
+int Muon::numberOfChambersCSCorDT() const {
+ if (isScoutingMuon()) {
+ return scoutingNChambersCSCorDT_;
+ }
+ return reco::Muon::numberOfChambersCSCorDT();
+}
+
+int Muon::numberOfMatches(reco::Muon::ArbitrationType type) const {
+ if (isScoutingMuon()) {
+ return scoutingNMatches_;
+ }
+ return reco::Muon::numberOfMatches(type);
+}
+
+int Muon::numberOfMatchedStations(reco::Muon::ArbitrationType type) const {
+ if (isScoutingMuon()) {
+ return scoutingNMatchedStations_;
+ }
+ return reco::Muon::numberOfMatchedStations(type);
+}
+
+unsigned int Muon::stationMask(reco::Muon::ArbitrationType type) const {
+ if (isScoutingMuon()) {
+ return scoutingStationMask_;
+ }
+ return reco::Muon::stationMask(type);
+}
+
+unsigned int Muon::expectedNnumberOfMatchedStations(float minDistanceFromEdge) const {
+ if (isScoutingMuon()) {
+ return scoutingExpectedMatchedStations_;
+ }
+ return reco::Muon::expectedNnumberOfMatchedStations(minDistanceFromEdge);
+}
+
+int Muon::numberOfMatchedRPCLayers(reco::Muon::ArbitrationType type) const {
+ if (isScoutingMuon()) {
+ return scoutingNMatchedRPCLayers_;
+ }
+ return reco::Muon::numberOfMatchedRPCLayers(type);
+}
+
+unsigned int Muon::RPClayerMask(reco::Muon::ArbitrationType type) const {
+ if (isScoutingMuon()) {
+ return scoutingRPCLayerMask_;
+ }
+ return reco::Muon::RPClayerMask(type);
+}
+
+// ---- Hit information accessors ----
+
+int Muon::numberOfValidMuonHits() const {
+ if (isScoutingMuon()) {
+ return scoutingNValidMuonHits_;
+ }
+ // For standard muons, get from global track hit pattern
+ reco::TrackRef glbTrack = globalTrack();
+ if (glbTrack.isNonnull()) {
+ return glbTrack->hitPattern().numberOfValidMuonHits();
+ }
+ return 0;
+}
+
+int Muon::numberOfValidStandAloneMuonHits() const {
+ if (isScoutingMuon()) {
+ return scoutingNValidStandAloneMuonHits_;
+ }
+ // For standard muons, get from outer track hit pattern
+ reco::TrackRef saTrack = standAloneMuon();
+ if (saTrack.isNonnull()) {
+ return saTrack->hitPattern().numberOfValidMuonHits();
+ }
+ return 0;
+}
+
+int Muon::numberOfStandAloneMuonMatchedStations() const {
+ if (isScoutingMuon()) {
+ return scoutingNStandAloneMuonMatchedStations_;
+ }
+ // For standard muons, get from outer track hit pattern
+ reco::TrackRef saTrack = standAloneMuon();
+ if (saTrack.isNonnull()) {
+ return saTrack->hitPattern().muonStationsWithValidHits();
+ }
+ return 0;
+}
+
+int Muon::numberOfValidPixelHits() const { return trkHitPattern().numberOfValidPixelHits(); }
+
+int Muon::numberOfValidStripHits() const { return trkHitPattern().numberOfValidStripHits(); }
+
+int Muon::numberOfPixelLayersWithMeasurement() const { return trkHitPattern().pixelLayersWithMeasurement(); }
+
+int Muon::numberOfTrackerLayersWithMeasurement() const { return trkHitPattern().trackerLayersWithMeasurement(); }
+
+reco::HitPattern const& Muon::trkHitPattern() const {
+ if (isScoutingMuon()) {
+ return scoutingTrkHitPattern_;
+ }
+ const reco::Track* trk = bestTrack();
+ if (trk) {
+ return trk->hitPattern();
+ }
+ static const reco::HitPattern empty;
+ return empty;
+}
diff --git a/DataFormats/PatCandidates/src/Photon.cc b/DataFormats/PatCandidates/src/Photon.cc
index cb72aeca7c9e7..a1496f462320f 100644
--- a/DataFormats/PatCandidates/src/Photon.cc
+++ b/DataFormats/PatCandidates/src/Photon.cc
@@ -2,8 +2,11 @@
//
#include "DataFormats/PatCandidates/interface/Photon.h"
+#include "DataFormats/Scouting/interface/Run3ScoutingPhoton.h"
#include "DataFormats/Common/interface/RefToPtr.h"
+#include
+
using pat::Photon;
/// default constructor
@@ -154,6 +157,90 @@ Photon::Photon(const edm::Ptr& aPhotonRef)
iEta_(-999),
iPhi_(-999) {}
+// Helper to create reco::Photon from scouting photon
+namespace {
+ reco::Photon makeRecoPhoton(const Run3ScoutingPhoton& sPhoton) {
+ float px = sPhoton.pt() * std::cos(sPhoton.phi());
+ float py = sPhoton.pt() * std::sin(sPhoton.phi());
+ float pz = sPhoton.pt() * std::sinh(sPhoton.eta());
+ float energy = std::sqrt(px * px + py * py + pz * pz + sPhoton.m() * sPhoton.m());
+ reco::Photon::LorentzVector p4(px, py, pz, energy);
+ reco::Photon::Point caloPos(0, 0, 0);
+ reco::Photon::Point vtx(0, 0, 0);
+ return reco::Photon(p4, caloPos, reco::PhotonCoreRef(), vtx);
+ }
+} // namespace
+
+/// constructor from Run3ScoutingPhoton
+Photon::Photon(const Run3ScoutingPhoton& sPhoton)
+ : PATObject(makeRecoPhoton(sPhoton)),
+ embeddedSuperCluster_(false),
+ embeddedSeedCluster_(false),
+ embeddedRecHits_(false),
+ passElectronVeto_(false),
+ hasPixelSeed_(false),
+ seedEnergy_(0.0),
+ eMax_(0.0),
+ e2nd_(0.0),
+ e3x3_(0.0),
+ eTop_(0.0),
+ eBottom_(0.0),
+ eLeft_(0.0),
+ eRight_(0.0),
+ see_(-999.),
+ spp_(-999.),
+ sep_(-999.),
+ maxDR_(-999.),
+ maxDRDPhi_(-999.),
+ maxDRDEta_(-999.),
+ maxDRRawEnergy_(-999.),
+ subClusRawE1_(-999.),
+ subClusRawE2_(-999.),
+ subClusRawE3_(-999.),
+ subClusDPhi1_(-999.),
+ subClusDPhi2_(-999.),
+ subClusDPhi3_(-999.),
+ subClusDEta1_(-999.),
+ subClusDEta2_(-999.),
+ subClusDEta3_(-999.),
+ cryEta_(-999.),
+ cryPhi_(-999),
+ iEta_(-999),
+ iPhi_(-999) {
+ isScoutingPhoton_ = true;
+
+ // Store shower shape variables as userFloats
+ this->addUserFloat("sigmaIetaIeta", sPhoton.sigmaIetaIeta());
+ this->addUserFloat("hOverE", sPhoton.hOverE());
+ this->addUserFloat("r9", sPhoton.r9());
+ this->addUserFloat("sMin", sPhoton.sMin());
+ this->addUserFloat("sMaj", sPhoton.sMaj());
+
+ // Store energy variables
+ this->addUserFloat("rawEnergy", sPhoton.rawEnergy());
+ this->addUserFloat("preshowerEnergy", sPhoton.preshowerEnergy());
+ this->addUserFloat("corrEcalEnergyError", sPhoton.corrEcalEnergyError());
+
+ // Store isolation as userFloats (for NanoAOD access)
+ this->addUserFloat("ecalIso", sPhoton.ecalIso());
+ this->addUserFloat("hcalIso", sPhoton.hcalIso());
+ this->addUserFloat("trkIso", sPhoton.trkIso());
+
+ // Also set isolation using PAT isolation keys
+ this->setIsolation(pat::TrackIso, sPhoton.trkIso());
+ this->setIsolation(pat::EcalIso, sPhoton.ecalIso());
+ this->setIsolation(pat::HcalIso, sPhoton.hcalIso());
+
+ // Store scouting-specific ECAL crystal and rechit information
+ scoutingSeedId_ = sPhoton.seedId();
+ scoutingNClusters_ = sPhoton.nClusters();
+ scoutingNCrystals_ = sPhoton.nCrystals();
+ scoutingEnergyMatrix_ = sPhoton.energyMatrix();
+ scoutingTimingMatrix_ = sPhoton.timingMatrix();
+ scoutingDetIds_ = sPhoton.detIds();
+ scoutingRechitZeroSuppression_ = sPhoton.rechitZeroSuppression();
+}
+
/// destructor
Photon::~Photon() {}
@@ -305,3 +392,32 @@ reco::CandidatePtr Photon::sourceCandidatePtr(size_type i) const {
edm::Ref(packedPFCandidates_, associatedPackedFCandidateIndices_[i])));
}
}
+
+// ---- Universal accessors with scouting dispatch ----
+
+uint32_t Photon::seedId() const {
+ if (isScoutingPhoton_) {
+ return scoutingSeedId_;
+ }
+ auto sc = superCluster();
+ if (sc.isNonnull() && sc->seed().isNonnull()) {
+ return sc->seed()->seed().rawId();
+ }
+ return 0;
+}
+
+uint32_t Photon::nClusters() const {
+ if (isScoutingPhoton_) {
+ return scoutingNClusters_;
+ }
+ auto sc = superCluster();
+ return sc.isNonnull() ? sc->clustersSize() : 0;
+}
+
+uint32_t Photon::nCrystals() const {
+ if (isScoutingPhoton_) {
+ return scoutingNCrystals_;
+ }
+ auto sc = superCluster();
+ return sc.isNonnull() ? sc->size() : 0;
+}
diff --git a/DataFormats/PatCandidates/src/ScoutingDataHandling.cc b/DataFormats/PatCandidates/src/ScoutingDataHandling.cc
new file mode 100644
index 0000000000000..70eecf3102d8d
--- /dev/null
+++ b/DataFormats/PatCandidates/src/ScoutingDataHandling.cc
@@ -0,0 +1,130 @@
+#include "DataFormats/PatCandidates/interface/ScoutingDataHandling.h"
+#include "DataFormats/Math/interface/Error.h"
+#include "DataFormats/Math/interface/Point3D.h"
+#include "DataFormats/Math/interface/libminifloat.h"
+
+float reduce_precision(float f, int mantissaPrecision = 10) {
+ return MiniFloatConverter::reduceMantissaToNbitsRounding(f, mantissaPrecision);
+}
+
+reco::Track pat::makeRecoTrack(const Run3ScoutingTrack& sTrack) {
+ reco::Track::Point v(sTrack.tk_vx(), sTrack.tk_vy(), sTrack.tk_vz());
+ reco::Track::Vector p(math::RhoEtaPhiVector(sTrack.tk_pt(), sTrack.tk_eta(), sTrack.tk_phi()));
+
+ reco::TrackBase::CovarianceMatrix cov;
+ cov(0, 0) = pow(sTrack.tk_qoverp_Error(), 2);
+ cov(0, 1) = sTrack.tk_qoverp_lambda_cov();
+ cov(0, 2) = sTrack.tk_qoverp_phi_cov();
+ cov(0, 3) = sTrack.tk_qoverp_dxy_cov();
+ cov(0, 4) = sTrack.tk_qoverp_dsz_cov();
+ cov(1, 1) = pow(sTrack.tk_lambda_Error(), 2);
+ cov(1, 2) = sTrack.tk_lambda_phi_cov();
+ cov(1, 3) = sTrack.tk_lambda_dxy_cov();
+ cov(1, 4) = sTrack.tk_lambda_dsz_cov();
+ cov(2, 2) = pow(sTrack.tk_phi_Error(), 2);
+ cov(2, 3) = sTrack.tk_phi_dxy_cov();
+ cov(2, 4) = sTrack.tk_phi_dsz_cov();
+ cov(3, 3) = pow(sTrack.tk_dxy_Error(), 2);
+ cov(3, 4) = sTrack.tk_dxy_dsz_cov();
+ cov(4, 4) = pow(sTrack.tk_dsz_Error(), 2);
+
+ return reco::Track(sTrack.tk_chi2(), sTrack.tk_ndof(), v, p, sTrack.tk_charge(), cov);
+}
+
+reco::Track pat::makeRecoTrack(const Run3ScoutingMuon& sMuon) {
+ reco::Track::Point vtx(sMuon.trk_vx(), sMuon.trk_vy(), sMuon.trk_vz());
+ reco::Track::Vector p3(math::RhoEtaPhiVector(sMuon.trk_pt(), sMuon.trk_eta(), sMuon.trk_phi()));
+
+ reco::TrackBase::CovarianceMatrix cov;
+ cov(0, 0) = pow(sMuon.trk_qoverpError(), 2);
+ cov(0, 1) = sMuon.trk_qoverp_lambda_cov();
+ cov(0, 2) = sMuon.trk_qoverp_phi_cov();
+ cov(0, 3) = sMuon.trk_qoverp_dxy_cov();
+ cov(0, 4) = sMuon.trk_qoverp_dsz_cov();
+ cov(1, 1) = pow(sMuon.trk_lambdaError(), 2);
+ cov(1, 2) = sMuon.trk_lambda_phi_cov();
+ cov(1, 3) = sMuon.trk_lambda_dxy_cov();
+ cov(1, 4) = sMuon.trk_lambda_dsz_cov();
+ cov(2, 2) = pow(sMuon.trk_phiError(), 2);
+ cov(2, 3) = sMuon.trk_phi_dxy_cov();
+ cov(2, 4) = sMuon.trk_phi_dsz_cov();
+ cov(3, 3) = pow(sMuon.trk_dxyError(), 2);
+ cov(3, 4) = sMuon.trk_dxy_dsz_cov();
+ cov(4, 4) = pow(sMuon.trk_dszError(), 2);
+
+ return reco::Track(sMuon.trk_chi2(), sMuon.trk_ndof(), vtx, p3, sMuon.charge(), cov);
+}
+
+Run3ScoutingTrack pat::makeScoutingTrack(const reco::Track& trk) {
+ return Run3ScoutingTrack(reduce_precision(trk.pt()),
+ reduce_precision(trk.eta()),
+ reduce_precision(trk.phi()),
+ reduce_precision(trk.chi2()),
+ trk.ndof(),
+ trk.charge(),
+ reduce_precision(trk.dxy()),
+ reduce_precision(trk.dz()),
+ trk.hitPattern().numberOfValidPixelHits(),
+ trk.hitPattern().trackerLayersWithMeasurement(),
+ trk.hitPattern().numberOfValidStripHits(),
+ reduce_precision(trk.qoverp()),
+ reduce_precision(trk.lambda()),
+ reduce_precision(trk.dxyError()),
+ reduce_precision(trk.dzError()),
+ reduce_precision(trk.qoverpError()),
+ reduce_precision(trk.lambdaError()),
+ reduce_precision(trk.phiError()),
+ reduce_precision(trk.dsz()),
+ reduce_precision(trk.dszError()),
+ reduce_precision(trk.covariance(0, 1)),
+ reduce_precision(trk.covariance(0, 2)),
+ reduce_precision(trk.covariance(0, 3)),
+ reduce_precision(trk.covariance(0, 4)),
+ reduce_precision(trk.covariance(1, 2)),
+ reduce_precision(trk.covariance(1, 3)),
+ reduce_precision(trk.covariance(1, 4)),
+ reduce_precision(trk.covariance(2, 3)),
+ reduce_precision(trk.covariance(2, 4)),
+ reduce_precision(trk.covariance(3, 4)),
+ 0,
+ reduce_precision(trk.vx()),
+ reduce_precision(trk.vy()),
+ reduce_precision(trk.vz()));
+}
+
+pat::Muon pat::makePatMuon(const Run3ScoutingMuon& sMuon) {
+ reco::Candidate::PolarLorentzVector p4(sMuon.pt(), sMuon.eta(), sMuon.phi(), sMuon.m());
+
+ auto track = makeRecoTrack(sMuon);
+
+ pat::Muon muon(reco::Muon(track.charge(), reco::Candidate::LorentzVector(p4), track.vertex()));
+
+ muon.setType(sMuon.type());
+
+ std::vector tracks;
+ tracks.push_back(track);
+ muon.setGlobalTrack(reco::TrackRef(&tracks, 0));
+ muon.embedCombinedMuon();
+ muon.setBestTrack(reco::Muon::CombinedTrack);
+
+ return muon;
+}
+
+reco::Vertex pat::makeRecoVertex(const Run3ScoutingVertex& sVertex) {
+ reco::Vertex::Error err;
+ err(0, 0) = pow(sVertex.xError(), 2);
+ err(1, 1) = pow(sVertex.yError(), 2);
+ err(2, 2) = pow(sVertex.zError(), 2);
+ err(0, 1) = sVertex.xyCov();
+ err(0, 2) = sVertex.xzCov();
+ err(1, 2) = sVertex.yzCov();
+ return reco::Vertex(reco::Vertex::Point(sVertex.x(), sVertex.y(), sVertex.z()),
+ err,
+ sVertex.chi2(),
+ sVertex.ndof(),
+ sVertex.tracksSize());
+}
+
+pat::PolarLorentzVector pat::makePolarLorentzVector(const Run3ScoutingTrack& trk, float mass) {
+ return PolarLorentzVector(trk.tk_pt(), trk.tk_eta(), trk.tk_phi(), mass);
+}
diff --git a/DataFormats/PatCandidates/src/classes_def_objects.xml b/DataFormats/PatCandidates/src/classes_def_objects.xml
index 1be32aeba5dc0..7f6e91dbffe48 100644
--- a/DataFormats/PatCandidates/src/classes_def_objects.xml
+++ b/DataFormats/PatCandidates/src/classes_def_objects.xml
@@ -16,7 +16,8 @@
-
+
+
@@ -63,7 +64,8 @@
-
+
+
@@ -214,7 +216,8 @@
-
+
+
diff --git a/PhysicsTools/PatFromScouting/README.md b/PhysicsTools/PatFromScouting/README.md
new file mode 100644
index 0000000000000..d96a59fd12236
--- /dev/null
+++ b/PhysicsTools/PatFromScouting/README.md
@@ -0,0 +1,347 @@
+# Scouting to MiniAOD/NanoAOD Conversion
+
+This package converts Run3 scouting data to MiniAOD-compatible format, enabling NanoAOD production from scouting data.
+
+## Overview
+
+```
+Scouting RAW → Scouting MiniAOD → Scouting NanoAOD
+ (Step 1: PAT) (Step 2: NANO)
+```
+
+The workflow produces NanoAOD with standard branch naming (Muon_pt, Jet_eta, etc.) from scouting data, enabling analysis with standard NanoAOD tools.
+
+## Quick Start
+
+### Two-Step Production Workflow with cmsDriver
+
+**Step 1: Scouting HLTSCOUT → MiniAOD**
+```bash
+cmsDriver.py scoutMini \
+ --scenario pp \
+ --conditions auto:run3_data_prompt \
+ --era Run3_2024 \
+ --eventcontent MINIAOD \
+ --datatier MINIAOD \
+ --step PAT:@Scout \
+ --filein file:scouting.root \
+ --fileout file:scoutingMiniAOD.root \
+ --data -n 100
+```
+
+**Step 2: MiniAOD → NanoAOD**
+
+Use our custom NanoAOD tables that read from PAT objects:
+```bash
+cmsDriver.py scoutNano \
+ --scenario pp \
+ --conditions auto:run3_data_prompt \
+ --era Run3_2024 \
+ --eventcontent NANOAOD \
+ --datatier NANOAOD \
+ --step NANO:@ScoutMini \
+ --filein file:scoutingMiniAOD.root \
+ --fileout file:scoutingNanoAOD.root \
+ --processName NANO \
+ --data -n 100
+```
+
+Note:
+- Use `--era Run3_2025` for 2025 data
+- The `--processName NANO` avoids conflicts with the MiniAOD process name
+- `@Scout` and `@ScoutMini` flavors are defined in `autoPAT.py`
+- For 2024+ data, the muon producer automatically switches to `hltScoutingMuonPackerVtx` via the `run3_scouting_2024` era modifier
+
+### Test Configurations (for development)
+
+```bash
+# Step 1 test
+cmsRun PhysicsTools/PatFromScouting/test/test_scoutingToMiniAOD_cfg.py
+
+# Step 2 test
+cmsRun PhysicsTools/PatFromScouting/test/test_scoutingNanoAOD_cfg.py
+
+# Standard NanoAOD on scouting MiniAOD (with customizations)
+cmsRun PhysicsTools/PatFromScouting/test/test_standardNanoAOD_cfg.py
+```
+
+## MiniAOD Collections
+
+| Collection Name | Type | Description |
+|-----------------|------|-------------|
+| `packedPFCandidates` | `pat::PackedCandidateCollection` | PF candidates from scouting particles |
+| `offlineSlimmedPrimaryVertices` | `reco::VertexCollection` | Primary vertices |
+| `slimmedMuons` | `pat::MuonCollection` | Muons with isolation and track info |
+| `slimmedElectrons` | `pat::ElectronCollection` | Electrons with shower shape in userFloats |
+| `slimmedPhotons` | `pat::PhotonCollection` | Photons with shower shape in userFloats |
+| `slimmedJets` | `pat::JetCollection` | AK4 PF jets with b-tagging |
+| `slimmedMETs` | `pat::METCollection` | MET from scouting PF data |
+| `scoutingTracks` | `reco::TrackCollection` | Tracks with hit info as ValueMaps |
+| `offlineBeamSpot` | `reco::BeamSpot` | From conditions database |
+| `fixedGridRhoFastjetAll` | `double` | Rho copied from HLT scouting |
+| `gtStage2Digis` | `GlobalAlgBlkBxCollection` | L1 trigger decisions (unpacked from raw) |
+| `gmtStage2Digis` | `l1t::MuonBxCollection` | L1 muons (copied from gtStage2Digis) |
+| `caloStage2Digis` | `l1t::Jet/EGamma/Tau/EtSumBxCollection` | L1 calo objects (copied from gtStage2Digis) |
+
+## NanoAOD Tables
+
+| Category | Key Variables |
+|----------|---------------|
+| **Muon** | pt, eta, phi, mass, charge, pdgId, dxy, dz, trkChi2, nValidHits, isGlobal, isTracker, isStandalone, isPF, nStations, nTrackerLayers, nPixelLayers, nChambers, nChambersCSCorDT, nValidMuonHits, nValidPixelHits, nValidStripHits, relIso, ecalIso, hcalIso, trkIso, tkRelIso |
+| **Electron** | pt, eta, phi, mass, charge, pdgId, dxy, dz, sieie, hoe, dEtaIn, dPhiIn, pfRelIso03_all, ecalIso, hcalIso, trkIso |
+| **Photon** | pt, eta, phi, mass, sieie, hoe, ecalIso, hcalIso |
+| **Jet** | pt, eta, phi, mass, area, chHEF, neHEF, chEmEF, neEmEF, muEF, nConstituents, chMultiplicity, neMultiplicity, btagCSV, btagDeepB |
+| **MET** | pt, phi, sumEt |
+| **PV** | x, y, z, xErr, yErr, zErr, chi2, ndof |
+| **Event** | fixedGridRhoFastjetAll |
+| **HLT** | All HLT trigger decisions (HLT_*, DST_*) |
+| **L1** | All L1 trigger seeds (L1_*) |
+
+## What's Available vs. Standard MiniAOD/NanoAOD
+
+### Available (Full Support)
+
+| Feature | Notes |
+|---------|-------|
+| Muon kinematics | pt, eta, phi, mass, charge |
+| Muon track quality | chi2, nValidHits, dxy, dz |
+| Muon ID flags | isGlobal, isTracker, isStandalone, isPF |
+| Muon isolation | ecalIso, hcalIso, trkIso (calorimeter-based, not PF components) |
+| Muon station/layer counts | nStations, nTrackerLayers, nPixelLayers, nChambers |
+| Muon hit counts | nValidMuonHits, nValidPixelHits, nValidStripHits |
+| Electron kinematics | pt, eta, phi, mass, charge |
+| Electron shower shape | sigmaIetaIeta, H/E, dEtaIn, dPhiIn |
+| Electron isolation | ecalIso, hcalIso, trackIso, PF relIso |
+| Photon kinematics | pt, eta, phi |
+| Photon shower shape | sigmaIetaIeta, H/E |
+| Photon isolation | ecalIso, hcalIso |
+| Jet kinematics | pt, eta, phi, mass, area |
+| Jet composition | Energy fractions (chHEF, neHEF, etc.), multiplicities |
+| Jet b-tagging | CSV, DeepCSV (pre-computed at HLT) |
+| MET | pt, phi, sumEt |
+| Primary vertices | Position, errors, chi2, ndof |
+| HLT triggers | All trigger decisions as booleans |
+| L1 triggers | All L1 seeds as booleans |
+| L1 objects | Muons, jets, EGamma, taus, EtSum (via gmtStage2Digis/caloStage2Digis) |
+| Pileup density | fixedGridRhoFastjetAll from HLT |
+| Beam spot | From conditions database |
+
+### Not Available (Scouting Limitations)
+
+| Feature | Reason |
+|---------|--------|
+| Jet constituents | Not stored in scouting format |
+| Pileup Jet ID | Requires iterating over jet daughters |
+| Deep flavor b-tagging | Requires track-level constituent info |
+| ParticleNet/DeepJet | Requires full constituent info |
+| AK8/Fat jets | Not produced in scouting |
+| Tau leptons | Not reconstructed in scouting |
+| HLT trigger objects | hltTriggerSummaryAOD not saved in scouting |
+| GenParticles | Data only (no MC scouting) |
+| MET corrections | No Type-1 corrections available (raw MET only) |
+| Electron/Photon MVA ID | Requires full shower info |
+
+### Partially Available
+
+| Feature | What Works | What Doesn't |
+|---------|------------|--------------|
+| Electron track | d0, dz, chi2 | No GSF track reference |
+| Photon R9 | Stored if available | May be 0 for some photons |
+| Jet corrections | Jets already corrected at HLT | No JEC uncertainties |
+
+## Data Mapping Details
+
+### Muons
+- Uses `pat::Muon(const Run3ScoutingMuon&)` constructor from DataFormats/PatCandidates
+- Track info embedded directly (fix applied for persistence)
+- Isolation mapped to `reco::MuonIsolation` structure (ecalIso, hcalIso, trkIso)
+- Station and hit count information derived from scouting muon data
+
+### Electrons
+Variables stored as userFloats (direct PAT electron construction requires detector references):
+```
+sigmaIetaIeta, hOverE, r9, sMin, sMaj, seedId
+dEtaIn, dPhiIn, ooEMOop, missingHits
+trkd0, trkdz, trkpt, trketa, trkphi, trkchi2overndf
+rawEnergy, preshowerEnergy, corrEcalEnergyError, trackfbrem
+ecalIso, hcalIso, trackIso
+```
+
+### Photons
+Variables stored as userFloats:
+```
+sigmaIetaIeta, hOverE, r9, sMin, sMaj, seedId
+rawEnergy, preshowerEnergy, corrEcalEnergyError
+ecalIso, hcalIso, trkIso
+```
+
+### Jets
+- Full `reco::PFJet::Specific` populated (energy fractions, multiplicities)
+- B-discriminators: `pfCombinedSecondaryVertexV2BJetTags` (csv), `pfDeepCSVJetTags:probb` (mva)
+- Jet area preserved
+
+### Vertices
+- Uses `pat::makeRecoVertex()` helper from DataFormats/PatCandidates
+- Only valid vertices included (`isValidVtx() == true`)
+- Full covariance matrix preserved
+
+### MET
+- Uses precomputed MET from scouting data (pfMetPt, pfMetPhi)
+
+### Tracks
+- Uses `pat::makeRecoTrack()` helper
+- Hit pattern info stored as ValueMaps (nValidPixelHits, nValidStripHits, nTrackerLayersWithMeasurement)
+
+### L1 Objects
+- `gtStage2Digis`: L1 trigger decisions unpacked from raw FED data (`hltFEDSelectorL1`)
+- `gmtStage2Digis`: L1 muons copied from gtStage2Digis Muon collection
+- `caloStage2Digis`: L1 jets, EGamma, taus, EtSum copied from gtStage2Digis
+
+## File Structure
+
+```
+PhysicsTools/PatFromScouting/
+├── plugins/
+│ ├── BuildFile.xml
+│ ├── PatFromScoutingMuonProducer.cc
+│ ├── PatFromScoutingElectronProducer.cc
+│ ├── PatFromScoutingPhotonProducer.cc
+│ ├── PatFromScoutingJetProducer.cc
+│ ├── Run3ScoutingVertexToRecoVertexProducer.cc
+│ ├── Run3ScoutingMETProducer.cc
+│ ├── Run3ScoutingTrackToRecoTrackProducer.cc
+│ ├── Run3ScoutingParticleToPackedCandidateProducer.cc
+│ ├── Run3ScoutingL1MuonProducer.cc
+│ ├── Run3ScoutingL1CaloProducer.cc
+│ └── ScoutingRhoProducer.cc
+├── python/
+│ ├── autoPAT.py # cmsDriver flavor definitions (@Scout, @ScoutMini)
+│ ├── scoutingToMiniAOD_cff.py # Step 1: RAW → MiniAOD producers
+│ ├── scoutingMiniAOD_cff.py # Alternative MiniAOD config with rho variants
+│ ├── scoutingNanoAOD_cff.py # Step 2: MiniAOD → NanoAOD tables
+│ ├── nanoAOD_scouting_cff.py # Customizations for standard NanoAOD on scouting MiniAOD
+│ └── nanoAOD_customizations_cff.py # Additional NanoAOD customization helpers
+├── test/
+│ ├── BuildFile.xml
+│ ├── test_scoutingToMiniAOD_cfg.py # Step 1 test config
+│ ├── test_scoutingNanoAOD_cfg.py # Step 2 test config
+│ ├── test_standardNanoAOD_cfg.py # Standard NanoAOD on scouting MiniAOD test
+│ ├── test_catch2_PatFromScouting.cc # Unit tests
+│ └── test_catch2_main.cc # Catch2 test main
+└── README.md
+```
+
+## Python Configuration Reference
+
+### autoPAT.py
+
+Defines `@`-prefixed flavor mappings for `cmsDriver --step PAT:@... / NANO:@...`:
+
+| Flavor | Step | Description |
+|--------|------|-------------|
+| `@Scout` | PAT | Scouting HLTSCOUT → MiniAOD |
+| `@ScoutVtx` | PAT | Same as `@Scout` but with vertex-aware muons (2024+) |
+| `@ScoutMini` | NANO | Scouting MiniAOD → NanoAOD |
+
+### scoutingToMiniAOD_cff.py
+
+Producers for Step 1 (Scouting RAW → MiniAOD):
+
+```python
+from PhysicsTools.PatFromScouting.scoutingToMiniAOD_cff import scoutingToMiniAODTask
+
+# Task includes all producers with standard MiniAOD names:
+# packedPFCandidates, offlineSlimmedPrimaryVertices, offlineBeamSpot,
+# slimmedMuons, slimmedElectrons, slimmedPhotons, slimmedJets, slimmedMETs,
+# scoutingTracks, fixedGridRhoFastjetAll, gtStage2Digis, gmtStage2Digis, caloStage2Digis
+```
+
+### scoutingNanoAOD_cff.py
+
+NanoAOD tables for Step 2:
+
+```python
+from PhysicsTools.PatFromScouting.scoutingNanoAOD_cff import customiseScoutingNanoAOD
+process = customiseScoutingNanoAOD(process)
+```
+
+### nanoAOD_scouting_cff.py
+
+Customizations for running **standard NanoAOD** on scouting MiniAOD (disables modules that require unavailable information):
+
+```python
+from PhysicsTools.PatFromScouting.nanoAOD_scouting_cff import customiseNanoForScoutingMiniAOD
+process = customiseNanoForScoutingMiniAOD(process)
+```
+
+## Input Tags (HLT Defaults)
+
+| Producer | Default Input Tag |
+|----------|-------------------|
+| PF Candidates | `hltScoutingPFPacker` |
+| Vertices | `hltScoutingPrimaryVertexPacker:primaryVtx` |
+| Muons | `hltScoutingMuonPacker` (2024+: `hltScoutingMuonPackerVtx` via era modifier) |
+| Electrons | `hltScoutingEgammaPacker` |
+| Photons | `hltScoutingEgammaPacker` |
+| Jets | `hltScoutingPFPacker` |
+| Tracks | `hltScoutingTrackPacker` |
+| L1 Raw | `hltFEDSelectorL1` |
+| Rho | `hltScoutingPFPacker:rho` |
+
+## Dependencies
+
+- `DataFormats/PatCandidates`
+- `DataFormats/Scouting`
+- `DataFormats/METReco`
+- `DataFormats/JetReco`
+- `DataFormats/VertexReco`
+- `DataFormats/TrackReco`
+- `DataFormats/MuonReco`
+- `PhysicsTools/NanoAOD`
+- `L1Trigger/L1TGlobal` (for L1TRawToDigi)
+
+## Testing
+
+```bash
+# Build
+cd $CMSSW_BASE/src
+scram b -j8
+
+# Run unit tests
+scram b runtests
+
+# Run full workflow
+cmsRun PhysicsTools/PatFromScouting/test/test_scoutingToMiniAOD_cfg.py
+cmsRun PhysicsTools/PatFromScouting/test/test_scoutingNanoAOD_cfg.py
+
+# Verify output
+python3 -c "
+import ROOT
+f = ROOT.TFile.Open('scoutingNanoAOD_test.root')
+t = f.Get('Events')
+print(f'Events: {t.GetEntries()}, Branches: {len(t.GetListOfBranches())}')
+"
+```
+
+## Known Issues and Workarounds
+
+### Standard NanoAOD Compatibility
+
+Running standard NanoAOD (`cmsDriver --step NANO`) on scouting MiniAOD requires the customizations in `nanoAOD_scouting_cff.py` because:
+
+1. **Jet update chains** - Standard NanoAOD tries to recompute jets from constituents
+2. **Deep taggers** - Require constituent-level information
+3. **Tau reconstruction** - Not available in scouting
+4. **PPS protons** - Not in scouting data
+
+The custom scouting NanoAOD (`NANO:@ScoutMini`) bypasses these issues by producing tables directly from the scouting MiniAOD collections.
+
+### Muon Persistence Fix
+
+A fix was applied to `DataFormats/PatCandidates/src/Muon.cc` to enable persistence of `pat::Muon` objects created from `Run3ScoutingMuon`. The original constructor created a `TrackRef` to a temporary local vector which could not be serialized.
+
+## References
+
+- [Run3 Scouting Data Formats](https://github.com/cms-sw/cmssw/tree/master/DataFormats/Scouting)
+- [PAT Scouting Helpers](https://github.com/cms-sw/cmssw/blob/master/DataFormats/PatCandidates/interface/ScoutingDataHandling.h)
+- [PhysicsTools/Scouting](https://github.com/cms-sw/cmssw/tree/master/PhysicsTools/Scouting) - Existing scouting tools
diff --git a/PhysicsTools/PatFromScouting/plugins/BuildFile.xml b/PhysicsTools/PatFromScouting/plugins/BuildFile.xml
new file mode 100644
index 0000000000000..8c3fea0e97ed3
--- /dev/null
+++ b/PhysicsTools/PatFromScouting/plugins/BuildFile.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PhysicsTools/PatFromScouting/plugins/PatFromScoutingElectronProducer.cc b/PhysicsTools/PatFromScouting/plugins/PatFromScoutingElectronProducer.cc
new file mode 100644
index 0000000000000..f684e219ff6e0
--- /dev/null
+++ b/PhysicsTools/PatFromScouting/plugins/PatFromScoutingElectronProducer.cc
@@ -0,0 +1,75 @@
+// -*- C++ -*-
+//
+// Package: PhysicsTools/PatFromScouting
+// Class: PatFromScoutingElectronProducer
+//
+/**\class PatFromScoutingElectronProducer PatFromScoutingElectronProducer.cc PhysicsTools/PatFromScouting/plugins/PatFromScoutingElectronProducer.cc
+
+ Description: Converts Run3ScoutingElectron to pat::Electron
+
+ Implementation:
+ Uses the pat::Electron(const Run3ScoutingElectron&) constructor which sets:
+ - Kinematics (pt, eta, phi, mass, charge)
+ - Shower shape variables as userFloats (sigmaIetaIeta, hOverE, r9, sMin, sMaj)
+ - ID variables as userFloats (dEtaIn, dPhiIn, ooEMOop, missingHits)
+ - Track variables as userFloats (trkd0, trkdz, trkpt, trkchi2overndf, trackfbrem)
+ - Energy variables as userFloats (rawEnergy, preshowerEnergy, corrEcalEnergyError)
+ - Isolation as userFloats (ecalIso, hcalIso, trackIso)
+*/
+//
+// Original Author: Dmytro Kovalskyi
+// Created: Thu, 05 Dec 2024 15:27:09 GMT
+//
+//
+
+#include
+
+#include "FWCore/Framework/interface/Frameworkfwd.h"
+#include "FWCore/Framework/interface/stream/EDProducer.h"
+#include "FWCore/Framework/interface/Event.h"
+#include "FWCore/Framework/interface/MakerMacros.h"
+#include "FWCore/ParameterSet/interface/ParameterSet.h"
+#include "FWCore/ParameterSet/interface/ConfigurationDescriptions.h"
+#include "FWCore/ParameterSet/interface/ParameterSetDescription.h"
+
+#include "DataFormats/PatCandidates/interface/Electron.h"
+#include "DataFormats/Scouting/interface/Run3ScoutingElectron.h"
+
+class PatFromScoutingElectronProducer : public edm::stream::EDProducer<> {
+public:
+ explicit PatFromScoutingElectronProducer(const edm::ParameterSet&);
+ ~PatFromScoutingElectronProducer() override = default;
+
+ static void fillDescriptions(edm::ConfigurationDescriptions& descriptions);
+
+private:
+ void produce(edm::Event&, const edm::EventSetup&) override;
+
+ const edm::EDGetTokenT electronToken_;
+};
+
+PatFromScoutingElectronProducer::PatFromScoutingElectronProducer(const edm::ParameterSet& iConfig)
+ : electronToken_(consumes(iConfig.getParameter("src"))) {
+ produces();
+}
+
+void PatFromScoutingElectronProducer::produce(edm::Event& iEvent, const edm::EventSetup& iSetup) {
+ auto patElectrons = std::make_unique();
+
+ const auto& scoutingElectrons = iEvent.get(electronToken_);
+
+ for (const auto& sElec : scoutingElectrons) {
+ // Constructor now handles kinematics, shower shape, ID, and isolation
+ patElectrons->push_back(pat::Electron(sElec));
+ }
+
+ iEvent.put(std::move(patElectrons));
+}
+
+void PatFromScoutingElectronProducer::fillDescriptions(edm::ConfigurationDescriptions& descriptions) {
+ edm::ParameterSetDescription desc;
+ desc.add("src", edm::InputTag("hltScoutingEgammaPacker"));
+ descriptions.addWithDefaultLabel(desc);
+}
+
+DEFINE_FWK_MODULE(PatFromScoutingElectronProducer);
diff --git a/PhysicsTools/PatFromScouting/plugins/PatFromScoutingJetProducer.cc b/PhysicsTools/PatFromScouting/plugins/PatFromScoutingJetProducer.cc
new file mode 100644
index 0000000000000..cfad49ece43f2
--- /dev/null
+++ b/PhysicsTools/PatFromScouting/plugins/PatFromScoutingJetProducer.cc
@@ -0,0 +1,90 @@
+// -*- C++ -*-
+//
+// Package: PhysicsTools/PatFromScouting
+// Class: PatFromScoutingJetProducer
+//
+/**\class PatFromScoutingJetProducer PatFromScoutingJetProducer.cc PhysicsTools/PatFromScouting/plugins/PatFromScoutingJetProducer.cc
+
+ Description: Converts Run3ScoutingPFJet to pat::Jet
+
+ Implementation:
+ Uses the pat::Jet(const Run3ScoutingPFJet&) constructor which sets:
+ - Kinematics (pt, eta, phi, mass)
+ - Jet area
+ - PFJet::Specific (energy fractions, multiplicities)
+ - B-tagging discriminators (CSV, DeepCSV)
+
+ Constituent indices from the scouting jet are resolved to
+ CandidatePtr daughters pointing into the PackedCandidateCollection.
+*/
+//
+// Original Author: Dmytro Kovalskyi
+// Created: Thu, 05 Dec 2024 15:27:09 GMT
+//
+//
+
+#include
+
+#include "FWCore/Framework/interface/Frameworkfwd.h"
+#include "FWCore/Framework/interface/stream/EDProducer.h"
+#include "FWCore/Framework/interface/Event.h"
+#include "FWCore/Framework/interface/MakerMacros.h"
+#include "FWCore/ParameterSet/interface/ParameterSet.h"
+#include "FWCore/ParameterSet/interface/ConfigurationDescriptions.h"
+#include "FWCore/ParameterSet/interface/ParameterSetDescription.h"
+
+#include "DataFormats/PatCandidates/interface/Jet.h"
+#include "DataFormats/PatCandidates/interface/PackedCandidate.h"
+#include "DataFormats/Scouting/interface/Run3ScoutingPFJet.h"
+
+class PatFromScoutingJetProducer : public edm::stream::EDProducer<> {
+public:
+ explicit PatFromScoutingJetProducer(const edm::ParameterSet&);
+ ~PatFromScoutingJetProducer() override = default;
+
+ static void fillDescriptions(edm::ConfigurationDescriptions& descriptions);
+
+private:
+ void produce(edm::Event&, const edm::EventSetup&) override;
+
+ const edm::EDGetTokenT jetToken_;
+ const edm::EDGetTokenT pfCandToken_;
+};
+
+PatFromScoutingJetProducer::PatFromScoutingJetProducer(const edm::ParameterSet& iConfig)
+ : jetToken_(consumes(iConfig.getParameter("src"))),
+ pfCandToken_(consumes(iConfig.getParameter("pfCandidates"))) {
+ produces();
+}
+
+void PatFromScoutingJetProducer::produce(edm::Event& iEvent, const edm::EventSetup& iSetup) {
+ auto patJets = std::make_unique();
+
+ const auto& scoutingJets = iEvent.get(jetToken_);
+ auto pfCandHandle = iEvent.getHandle(pfCandToken_);
+
+ for (const auto& sJet : scoutingJets) {
+ pat::Jet jet(sJet);
+
+ // Resolve constituent indices to CandidatePtr daughters
+ for (int idx : sJet.constituents()) {
+ if (idx >= 0 && idx < static_cast(pfCandHandle->size())) {
+ reco::CandidatePtr ptr(pfCandHandle, idx);
+ jet.addDaughter(ptr);
+ }
+ }
+
+ patJets->push_back(jet);
+ }
+
+ iEvent.put(std::move(patJets));
+}
+
+void PatFromScoutingJetProducer::fillDescriptions(edm::ConfigurationDescriptions& descriptions) {
+ edm::ParameterSetDescription desc;
+ desc.add("src", edm::InputTag("hltScoutingPFPacker"));
+ desc.add("pfCandidates", edm::InputTag("packedPFCandidates"));
+ descriptions.addWithDefaultLabel(desc);
+}
+
+DEFINE_FWK_MODULE(PatFromScoutingJetProducer);
diff --git a/PhysicsTools/PatFromScouting/plugins/PatFromScoutingMuonProducer.cc b/PhysicsTools/PatFromScouting/plugins/PatFromScoutingMuonProducer.cc
new file mode 100644
index 0000000000000..f9ae8ad8aceb8
--- /dev/null
+++ b/PhysicsTools/PatFromScouting/plugins/PatFromScoutingMuonProducer.cc
@@ -0,0 +1,72 @@
+// -*- C++ -*-
+//
+// Package: PhysicsTools/PatFromScouting
+// Class: PatFromScoutingMuonProducer
+//
+/**\class PatFromScoutingMuonProducer PatFromScoutingMuonProducer.cc PhysicsTools/PatFromScouting/plugins/PatFromScoutingMuonProducer.cc
+
+ Description: Converts Run3ScoutingMuon to pat::Muon
+
+ Implementation:
+ Uses the pat::Muon(const Run3ScoutingMuon&) constructor which sets:
+ - Kinematics (pt, eta, phi, mass, charge)
+ - Embedded track with vertex
+ - Isolation (trackIso, ecalIso, hcalIso -> isolationR03)
+*/
+//
+// Original Author: Dmytro Kovalskyi
+// Created: Thu, 05 Dec 2024 15:27:09 GMT
+//
+//
+
+#include
+
+#include "FWCore/Framework/interface/Frameworkfwd.h"
+#include "FWCore/Framework/interface/stream/EDProducer.h"
+#include "FWCore/Framework/interface/Event.h"
+#include "FWCore/Framework/interface/MakerMacros.h"
+#include "FWCore/ParameterSet/interface/ParameterSet.h"
+#include "FWCore/ParameterSet/interface/ConfigurationDescriptions.h"
+#include "FWCore/ParameterSet/interface/ParameterSetDescription.h"
+
+#include "DataFormats/PatCandidates/interface/Muon.h"
+#include "DataFormats/Scouting/interface/Run3ScoutingMuon.h"
+
+class PatFromScoutingMuonProducer : public edm::stream::EDProducer<> {
+public:
+ explicit PatFromScoutingMuonProducer(const edm::ParameterSet&);
+ ~PatFromScoutingMuonProducer() override = default;
+
+ static void fillDescriptions(edm::ConfigurationDescriptions& descriptions);
+
+private:
+ void produce(edm::Event&, const edm::EventSetup&) override;
+
+ const edm::EDGetTokenT muonToken_;
+};
+
+PatFromScoutingMuonProducer::PatFromScoutingMuonProducer(const edm::ParameterSet& iConfig)
+ : muonToken_(consumes(iConfig.getParameter("src"))) {
+ produces();
+}
+
+void PatFromScoutingMuonProducer::produce(edm::Event& iEvent, const edm::EventSetup& iSetup) {
+ auto patMuons = std::make_unique();
+
+ const auto& scoutingMuons = iEvent.get(muonToken_);
+
+ for (const auto& sMuon : scoutingMuons) {
+ // Constructor now handles kinematics, track embedding, and isolation
+ patMuons->push_back(pat::Muon(sMuon));
+ }
+
+ iEvent.put(std::move(patMuons));
+}
+
+void PatFromScoutingMuonProducer::fillDescriptions(edm::ConfigurationDescriptions& descriptions) {
+ edm::ParameterSetDescription desc;
+ desc.add("src", edm::InputTag("hltScoutingMuonPacker"));
+ descriptions.addWithDefaultLabel(desc);
+}
+
+DEFINE_FWK_MODULE(PatFromScoutingMuonProducer);
diff --git a/PhysicsTools/PatFromScouting/plugins/PatFromScoutingPhotonProducer.cc b/PhysicsTools/PatFromScouting/plugins/PatFromScoutingPhotonProducer.cc
new file mode 100644
index 0000000000000..b4ba287dc96d0
--- /dev/null
+++ b/PhysicsTools/PatFromScouting/plugins/PatFromScoutingPhotonProducer.cc
@@ -0,0 +1,73 @@
+// -*- C++ -*-
+//
+// Package: PhysicsTools/PatFromScouting
+// Class: PatFromScoutingPhotonProducer
+//
+/**\class PatFromScoutingPhotonProducer PatFromScoutingPhotonProducer.cc PhysicsTools/PatFromScouting/plugins/PatFromScoutingPhotonProducer.cc
+
+ Description: Converts Run3ScoutingPhoton to pat::Photon
+
+ Implementation:
+ Uses the pat::Photon(const Run3ScoutingPhoton&) constructor which sets:
+ - Kinematics (pt, eta, phi, mass)
+ - Shower shape variables as userFloats (sigmaIetaIeta, hOverE, r9, sMin, sMaj)
+ - Energy variables as userFloats (rawEnergy, preshowerEnergy, corrEcalEnergyError)
+ - Isolation as userFloats and PAT isolation keys (ecalIso, hcalIso, trkIso)
+*/
+//
+// Original Author: Dmytro Kovalskyi
+// Created: Thu, 05 Dec 2024 15:27:09 GMT
+//
+//
+
+#include
+
+#include "FWCore/Framework/interface/Frameworkfwd.h"
+#include "FWCore/Framework/interface/stream/EDProducer.h"
+#include "FWCore/Framework/interface/Event.h"
+#include "FWCore/Framework/interface/MakerMacros.h"
+#include "FWCore/ParameterSet/interface/ParameterSet.h"
+#include "FWCore/ParameterSet/interface/ConfigurationDescriptions.h"
+#include "FWCore/ParameterSet/interface/ParameterSetDescription.h"
+
+#include "DataFormats/PatCandidates/interface/Photon.h"
+#include "DataFormats/Scouting/interface/Run3ScoutingPhoton.h"
+
+class PatFromScoutingPhotonProducer : public edm::stream::EDProducer<> {
+public:
+ explicit PatFromScoutingPhotonProducer(const edm::ParameterSet&);
+ ~PatFromScoutingPhotonProducer() override = default;
+
+ static void fillDescriptions(edm::ConfigurationDescriptions& descriptions);
+
+private:
+ void produce(edm::Event&, const edm::EventSetup&) override;
+
+ const edm::EDGetTokenT photonToken_;
+};
+
+PatFromScoutingPhotonProducer::PatFromScoutingPhotonProducer(const edm::ParameterSet& iConfig)
+ : photonToken_(consumes(iConfig.getParameter("src"))) {
+ produces();
+}
+
+void PatFromScoutingPhotonProducer::produce(edm::Event& iEvent, const edm::EventSetup& iSetup) {
+ auto patPhotons = std::make_unique();
+
+ const auto& scoutingPhotons = iEvent.get(photonToken_);
+
+ for (const auto& sPhoton : scoutingPhotons) {
+ // Constructor now handles kinematics, shower shape, and isolation
+ patPhotons->push_back(pat::Photon(sPhoton));
+ }
+
+ iEvent.put(std::move(patPhotons));
+}
+
+void PatFromScoutingPhotonProducer::fillDescriptions(edm::ConfigurationDescriptions& descriptions) {
+ edm::ParameterSetDescription desc;
+ desc.add("src", edm::InputTag("hltScoutingEgammaPacker"));
+ descriptions.addWithDefaultLabel(desc);
+}
+
+DEFINE_FWK_MODULE(PatFromScoutingPhotonProducer);
diff --git a/PhysicsTools/PatFromScouting/plugins/Run3ScoutingL1CaloProducer.cc b/PhysicsTools/PatFromScouting/plugins/Run3ScoutingL1CaloProducer.cc
new file mode 100644
index 0000000000000..6467805fa9641
--- /dev/null
+++ b/PhysicsTools/PatFromScouting/plugins/Run3ScoutingL1CaloProducer.cc
@@ -0,0 +1,114 @@
+/**
+ * Run3ScoutingL1CaloProducer
+ *
+ * Copies L1 calorimeter objects (jets, EGamma, tau, EtSum) from gtStage2Digis
+ * to produce collections with the standard caloStage2Digis module label
+ * expected by downstream code.
+ *
+ * This allows scouting MiniAOD to be compatible with standard workflows
+ * that expect L1 calo objects from caloStage2Digis.
+ */
+
+#include "FWCore/Framework/interface/stream/EDProducer.h"
+#include "FWCore/Framework/interface/Event.h"
+#include "FWCore/Framework/interface/MakerMacros.h"
+#include "FWCore/ParameterSet/interface/ParameterSet.h"
+#include "DataFormats/L1Trigger/interface/Jet.h"
+#include "DataFormats/L1Trigger/interface/EGamma.h"
+#include "DataFormats/L1Trigger/interface/Tau.h"
+#include "DataFormats/L1Trigger/interface/EtSum.h"
+#include "DataFormats/L1Trigger/interface/BXVector.h"
+
+class Run3ScoutingL1CaloProducer : public edm::stream::EDProducer<> {
+public:
+ explicit Run3ScoutingL1CaloProducer(const edm::ParameterSet&);
+ static void fillDescriptions(edm::ConfigurationDescriptions& descriptions);
+
+private:
+ void produce(edm::Event&, const edm::EventSetup&) override;
+
+ const edm::EDGetTokenT> jetToken_;
+ const edm::EDGetTokenT> egammaToken_;
+ const edm::EDGetTokenT> tauToken_;
+ const edm::EDGetTokenT> etsumToken_;
+
+ const edm::EDPutTokenT> jetPutToken_;
+ const edm::EDPutTokenT> egammaPutToken_;
+ const edm::EDPutTokenT> tauPutToken_;
+ const edm::EDPutTokenT> etsumPutToken_;
+};
+
+Run3ScoutingL1CaloProducer::Run3ScoutingL1CaloProducer(const edm::ParameterSet& iConfig)
+ : jetToken_(consumes>(iConfig.getParameter("jetSource"))),
+ egammaToken_(consumes>(iConfig.getParameter("egammaSource"))),
+ tauToken_(consumes>(iConfig.getParameter("tauSource"))),
+ etsumToken_(consumes>(iConfig.getParameter("etsumSource"))),
+ jetPutToken_(produces>("Jet")),
+ egammaPutToken_(produces>("EGamma")),
+ tauPutToken_(produces>("Tau")),
+ etsumPutToken_(produces>("EtSum")) {}
+
+void Run3ScoutingL1CaloProducer::produce(edm::Event& iEvent, const edm::EventSetup&) {
+ // Copy jets
+ {
+ auto const& jetsIn = iEvent.get(jetToken_);
+ BXVector jetsOut;
+ jetsOut.setBXRange(jetsIn.getFirstBX(), jetsIn.getLastBX());
+ for (int bx = jetsIn.getFirstBX(); bx <= jetsIn.getLastBX(); ++bx) {
+ for (auto it = jetsIn.begin(bx); it != jetsIn.end(bx); ++it) {
+ jetsOut.push_back(bx, *it);
+ }
+ }
+ iEvent.emplace(jetPutToken_, std::move(jetsOut));
+ }
+
+ // Copy EGamma
+ {
+ auto const& egammaIn = iEvent.get(egammaToken_);
+ BXVector egammaOut;
+ egammaOut.setBXRange(egammaIn.getFirstBX(), egammaIn.getLastBX());
+ for (int bx = egammaIn.getFirstBX(); bx <= egammaIn.getLastBX(); ++bx) {
+ for (auto it = egammaIn.begin(bx); it != egammaIn.end(bx); ++it) {
+ egammaOut.push_back(bx, *it);
+ }
+ }
+ iEvent.emplace(egammaPutToken_, std::move(egammaOut));
+ }
+
+ // Copy Tau
+ {
+ auto const& tauIn = iEvent.get(tauToken_);
+ BXVector tauOut;
+ tauOut.setBXRange(tauIn.getFirstBX(), tauIn.getLastBX());
+ for (int bx = tauIn.getFirstBX(); bx <= tauIn.getLastBX(); ++bx) {
+ for (auto it = tauIn.begin(bx); it != tauIn.end(bx); ++it) {
+ tauOut.push_back(bx, *it);
+ }
+ }
+ iEvent.emplace(tauPutToken_, std::move(tauOut));
+ }
+
+ // Copy EtSum
+ {
+ auto const& etsumIn = iEvent.get(etsumToken_);
+ BXVector etsumOut;
+ etsumOut.setBXRange(etsumIn.getFirstBX(), etsumIn.getLastBX());
+ for (int bx = etsumIn.getFirstBX(); bx <= etsumIn.getLastBX(); ++bx) {
+ for (auto it = etsumIn.begin(bx); it != etsumIn.end(bx); ++it) {
+ etsumOut.push_back(bx, *it);
+ }
+ }
+ iEvent.emplace(etsumPutToken_, std::move(etsumOut));
+ }
+}
+
+void Run3ScoutingL1CaloProducer::fillDescriptions(edm::ConfigurationDescriptions& descriptions) {
+ edm::ParameterSetDescription desc;
+ desc.add("jetSource", edm::InputTag("gtStage2Digis", "Jet"));
+ desc.add("egammaSource", edm::InputTag("gtStage2Digis", "EGamma"));
+ desc.add("tauSource", edm::InputTag("gtStage2Digis", "Tau"));
+ desc.add("etsumSource", edm::InputTag("gtStage2Digis", "EtSum"));
+ descriptions.addWithDefaultLabel(desc);
+}
+
+DEFINE_FWK_MODULE(Run3ScoutingL1CaloProducer);
diff --git a/PhysicsTools/PatFromScouting/plugins/Run3ScoutingL1MuonProducer.cc b/PhysicsTools/PatFromScouting/plugins/Run3ScoutingL1MuonProducer.cc
new file mode 100644
index 0000000000000..8cb57d2a75cd6
--- /dev/null
+++ b/PhysicsTools/PatFromScouting/plugins/Run3ScoutingL1MuonProducer.cc
@@ -0,0 +1,54 @@
+/**
+ * Run3ScoutingL1MuonProducer
+ *
+ * Copies L1 muons from gtStage2Digis to produce collections with
+ * the standard gmtStage2Digis module label expected by downstream code.
+ *
+ * This allows scouting MiniAOD to be compatible with standard workflows
+ * that expect L1 muons from gmtStage2Digis.
+ */
+
+#include "FWCore/Framework/interface/stream/EDProducer.h"
+#include "FWCore/Framework/interface/Event.h"
+#include "FWCore/Framework/interface/MakerMacros.h"
+#include "FWCore/ParameterSet/interface/ParameterSet.h"
+#include "DataFormats/L1Trigger/interface/Muon.h"
+#include "DataFormats/L1Trigger/interface/BXVector.h"
+
+class Run3ScoutingL1MuonProducer : public edm::stream::EDProducer<> {
+public:
+ explicit Run3ScoutingL1MuonProducer(const edm::ParameterSet&);
+ static void fillDescriptions(edm::ConfigurationDescriptions& descriptions);
+
+private:
+ void produce(edm::Event&, const edm::EventSetup&) override;
+
+ const edm::EDGetTokenT> muonToken_;
+ const edm::EDPutTokenT> muonPutToken_;
+};
+
+Run3ScoutingL1MuonProducer::Run3ScoutingL1MuonProducer(const edm::ParameterSet& iConfig)
+ : muonToken_(consumes>(iConfig.getParameter("muonSource"))),
+ muonPutToken_(produces>("Muon")) {}
+
+void Run3ScoutingL1MuonProducer::produce(edm::Event& iEvent, const edm::EventSetup&) {
+ auto const& muonsIn = iEvent.get(muonToken_);
+ BXVector muonsOut;
+
+ muonsOut.setBXRange(muonsIn.getFirstBX(), muonsIn.getLastBX());
+ for (int bx = muonsIn.getFirstBX(); bx <= muonsIn.getLastBX(); ++bx) {
+ for (auto it = muonsIn.begin(bx); it != muonsIn.end(bx); ++it) {
+ muonsOut.push_back(bx, *it);
+ }
+ }
+
+ iEvent.emplace(muonPutToken_, std::move(muonsOut));
+}
+
+void Run3ScoutingL1MuonProducer::fillDescriptions(edm::ConfigurationDescriptions& descriptions) {
+ edm::ParameterSetDescription desc;
+ desc.add("muonSource", edm::InputTag("gtStage2Digis", "Muon"));
+ descriptions.addWithDefaultLabel(desc);
+}
+
+DEFINE_FWK_MODULE(Run3ScoutingL1MuonProducer);
diff --git a/PhysicsTools/PatFromScouting/plugins/Run3ScoutingMETProducer.cc b/PhysicsTools/PatFromScouting/plugins/Run3ScoutingMETProducer.cc
new file mode 100644
index 0000000000000..eddd2f4146818
--- /dev/null
+++ b/PhysicsTools/PatFromScouting/plugins/Run3ScoutingMETProducer.cc
@@ -0,0 +1,90 @@
+// -*- C++ -*-
+//
+// Package: PhysicsTools/PatFromScouting
+// Class: Run3ScoutingMETProducer
+//
+/**\class Run3ScoutingMETProducer Run3ScoutingMETProducer.cc PhysicsTools/PatFromScouting/plugins/Run3ScoutingMETProducer.cc
+
+ Description: Creates pat::MET from scouting MET (pt, phi) stored in event
+
+ Implementation:
+ Reads precomputed MET pt and phi from hltScoutingPFPacker and creates pat::MET
+*/
+//
+// Original Author: Dmytro Kovalskyi
+// Created: Thu, 05 Dec 2024 15:27:09 GMT
+//
+//
+
+#include
+#include
+
+#include "FWCore/Framework/interface/Frameworkfwd.h"
+#include "FWCore/Framework/interface/stream/EDProducer.h"
+#include "FWCore/Framework/interface/Event.h"
+#include "FWCore/Framework/interface/MakerMacros.h"
+#include "FWCore/ParameterSet/interface/ParameterSet.h"
+#include "FWCore/ParameterSet/interface/ConfigurationDescriptions.h"
+#include "FWCore/ParameterSet/interface/ParameterSetDescription.h"
+
+#include "DataFormats/PatCandidates/interface/MET.h"
+
+class Run3ScoutingMETProducer : public edm::stream::EDProducer<> {
+public:
+ explicit Run3ScoutingMETProducer(const edm::ParameterSet&);
+ ~Run3ScoutingMETProducer() override = default;
+
+ static void fillDescriptions(edm::ConfigurationDescriptions& descriptions);
+
+private:
+ void produce(edm::Event&, const edm::EventSetup&) override;
+
+ const edm::EDGetTokenT metPtToken_;
+ const edm::EDGetTokenT metPhiToken_;
+};
+
+Run3ScoutingMETProducer::Run3ScoutingMETProducer(const edm::ParameterSet& iConfig)
+ : metPtToken_(consumes(iConfig.getParameter("metPt"))),
+ metPhiToken_(consumes(iConfig.getParameter("metPhi"))) {
+ produces();
+}
+
+void Run3ScoutingMETProducer::produce(edm::Event& iEvent, const edm::EventSetup& iSetup) {
+ auto patMETs = std::make_unique();
+
+ double metPt = iEvent.get(metPtToken_);
+ double metPhi = iEvent.get(metPhiToken_);
+
+ double metPx = metPt * std::cos(metPhi);
+ double metPy = metPt * std::sin(metPhi);
+
+ // sumEt is not available in scouting, use metPt as approximation
+ double sumEt = metPt;
+
+ reco::MET::LorentzVector p4(metPx, metPy, 0.0, metPt);
+ reco::MET::Point vtx(0.0, 0.0, 0.0);
+
+ reco::MET recoMET(sumEt, p4, vtx);
+ pat::MET patMET(recoMET);
+
+ // Initialize MET corrections to make NanoAOD happy
+ // Using the same values since scouting MET is already the "raw" PF MET
+ patMET.setCorShift(metPx, metPy, sumEt, pat::MET::None);
+ patMET.setCorShift(metPx, metPy, sumEt, pat::MET::T1);
+ patMET.setCorShift(metPx, metPy, sumEt, pat::MET::Calo);
+ patMET.setCorShift(metPx, metPy, sumEt, pat::MET::Chs);
+ patMET.setCorShift(metPx, metPy, sumEt, pat::MET::Trk);
+
+ patMETs->push_back(patMET);
+
+ iEvent.put(std::move(patMETs));
+}
+
+void Run3ScoutingMETProducer::fillDescriptions(edm::ConfigurationDescriptions& descriptions) {
+ edm::ParameterSetDescription desc;
+ desc.add("metPt", edm::InputTag("hltScoutingPFPacker", "pfMetPt"));
+ desc.add("metPhi", edm::InputTag("hltScoutingPFPacker", "pfMetPhi"));
+ descriptions.addWithDefaultLabel(desc);
+}
+
+DEFINE_FWK_MODULE(Run3ScoutingMETProducer);
diff --git a/PhysicsTools/PatFromScouting/plugins/Run3ScoutingParticleToPackedCandidateProducer.cc b/PhysicsTools/PatFromScouting/plugins/Run3ScoutingParticleToPackedCandidateProducer.cc
new file mode 100644
index 0000000000000..f5c663aeea6a8
--- /dev/null
+++ b/PhysicsTools/PatFromScouting/plugins/Run3ScoutingParticleToPackedCandidateProducer.cc
@@ -0,0 +1,197 @@
+/**
+ * Run3ScoutingParticleToPackedCandidateProducer
+ *
+ * Converts Run3ScoutingParticle to pat::PackedCandidate for MiniAOD compatibility.
+ * Matches charged candidates to reco::Tracks to embed track details
+ * (hasTrackDetails() == true, dxyError/dzError/normalizedChi2/hit counts available).
+ *
+ * Requires vertices and scoutingTracks to be produced first.
+ */
+
+#include
+#include
+
+#include "FWCore/Framework/interface/Frameworkfwd.h"
+#include "FWCore/Framework/interface/stream/EDProducer.h"
+#include "FWCore/Framework/interface/Event.h"
+#include "FWCore/Framework/interface/MakerMacros.h"
+#include "FWCore/ParameterSet/interface/ParameterSet.h"
+#include "FWCore/ParameterSet/interface/ConfigurationDescriptions.h"
+#include "FWCore/ParameterSet/interface/ParameterSetDescription.h"
+
+#include "DataFormats/PatCandidates/interface/PackedCandidate.h"
+#include "DataFormats/Scouting/interface/Run3ScoutingParticle.h"
+#include "DataFormats/VertexReco/interface/Vertex.h"
+#include "DataFormats/VertexReco/interface/VertexFwd.h"
+#include "DataFormats/TrackReco/interface/Track.h"
+#include "DataFormats/TrackReco/interface/TrackFwd.h"
+#include "DataFormats/Math/interface/LorentzVector.h"
+#include "DataFormats/Math/interface/deltaR.h"
+
+#include "SimGeneral/HepPDTRecord/interface/ParticleDataTable.h"
+
+class Run3ScoutingParticleToPackedCandidateProducer : public edm::stream::EDProducer<> {
+public:
+ explicit Run3ScoutingParticleToPackedCandidateProducer(const edm::ParameterSet&);
+ ~Run3ScoutingParticleToPackedCandidateProducer() override = default;
+
+ static void fillDescriptions(edm::ConfigurationDescriptions& descriptions);
+
+private:
+ void produce(edm::Event&, const edm::EventSetup&) override;
+
+ const edm::EDGetTokenT> particleToken_;
+ const edm::EDGetTokenT vertexToken_;
+ const edm::EDGetTokenT trackToken_;
+ const edm::ESGetToken pdtToken_;
+ const bool useCHS_;
+ const int covarianceVersion_;
+ const int covarianceSchema_;
+};
+
+Run3ScoutingParticleToPackedCandidateProducer::Run3ScoutingParticleToPackedCandidateProducer(
+ const edm::ParameterSet& iConfig)
+ : particleToken_(consumes>(iConfig.getParameter("src"))),
+ vertexToken_(consumes(iConfig.getParameter("vertices"))),
+ trackToken_(consumes(iConfig.getParameter("tracks"))),
+ pdtToken_(esConsumes()),
+ useCHS_(iConfig.getParameter("CHS")),
+ covarianceVersion_(iConfig.getParameter("covarianceVersion")),
+ covarianceSchema_(iConfig.getParameter("covarianceSchema")) {
+ produces();
+}
+
+void Run3ScoutingParticleToPackedCandidateProducer::produce(edm::Event& iEvent, const edm::EventSetup& iSetup) {
+ auto output = std::make_unique();
+
+ const auto& pdt = iSetup.getData(pdtToken_);
+ const auto& particles = iEvent.get(particleToken_);
+
+ auto verticesHandle = iEvent.getHandle(vertexToken_);
+ const auto& vertices = *verticesHandle;
+ reco::VertexRefProd vertexRefProd(verticesHandle);
+
+ const auto& tracks = iEvent.get(trackToken_);
+
+ // Build a "used" flag so each reco::Track is matched at most once
+ std::vector trackUsed(tracks.size(), false);
+
+ output->reserve(particles.size());
+
+ for (const auto& particle : particles) {
+ if (useCHS_ && particle.vertex() > 0) {
+ continue;
+ }
+
+ const HepPDT::ParticleData* pdtData = pdt.particle(HepPDT::ParticleID(particle.pdgId()));
+ if (!pdtData) {
+ continue;
+ }
+ float mass = pdtData->mass().value();
+
+ float pt = particle.pt();
+ float eta = particle.eta();
+ float phi = particle.phi();
+ float px = pt * std::cos(phi);
+ float py = pt * std::sin(phi);
+ float pz = pt * std::sinh(eta);
+ float energy = std::sqrt(px * px + py * py + pz * pz + mass * mass);
+ math::XYZTLorentzVector p4(px, py, pz, energy);
+
+ bool relativeTrackVars = particle.relative_trk_vars();
+ float trkPt = relativeTrackVars ? particle.trk_pt() + particle.pt() : particle.trk_pt();
+ float trkEta = relativeTrackVars ? particle.trk_eta() + particle.eta() : particle.trk_eta();
+ float trkPhi = relativeTrackVars ? particle.trk_phi() + particle.phi() : particle.trk_phi();
+
+ int vtxIdx = particle.vertex();
+ reco::VertexRef::key_type pvKey = 0;
+ if (vtxIdx >= 0 && static_cast(vtxIdx) < vertices.size()) {
+ pvKey = static_cast(vtxIdx);
+ }
+
+ math::XYZPoint pvPos(0, 0, 0);
+ if (pvKey < vertices.size()) {
+ pvPos = vertices[pvKey].position();
+ }
+
+ float dxy = particle.dxy();
+ float dz = particle.dz();
+ float sinPhi = std::sin(phi);
+ float cosPhi = std::cos(phi);
+
+ math::XYZPoint vtxPos(pvPos.X() - dxy * sinPhi, pvPos.Y() + dxy * cosPhi, pvPos.Z() + dz);
+
+ pat::PackedCandidate cand(p4, vtxPos, trkPt, trkEta, trkPhi, particle.pdgId(), vertexRefProd, pvKey);
+
+ // Set lost inner hits
+ pat::PackedCandidate::LostInnerHits lostHits = pat::PackedCandidate::noLostInnerHits;
+ uint8_t scoutingLostHits = particle.lostInnerHits();
+ if (scoutingLostHits == 0) {
+ lostHits = pat::PackedCandidate::noLostInnerHits;
+ } else if (scoutingLostHits == 1) {
+ lostHits = pat::PackedCandidate::oneLostInnerHit;
+ } else if (scoutingLostHits >= 2) {
+ lostHits = pat::PackedCandidate::moreLostInnerHits;
+ }
+ cand.setLostInnerHits(lostHits);
+
+ // Set track quality
+ int quality = particle.quality();
+ bool highPurity = (quality & 4);
+ cand.setTrackHighPurity(highPurity);
+
+ // Set PV association quality
+ if (vtxIdx == 0) {
+ cand.setAssociationQuality(pat::PackedCandidate::UsedInFitTight);
+ } else if (vtxIdx > 0) {
+ cand.setAssociationQuality(pat::PackedCandidate::CompatibilityDz);
+ } else {
+ cand.setAssociationQuality(pat::PackedCandidate::NotReconstructedPrimary);
+ }
+
+ // Match charged candidates to reco::Tracks and embed track details
+ if (particle.pdgId() != 22 && particle.pdgId() != 130 && abs(particle.pdgId()) > 2 && trkPt > 0) {
+ int bestIdx = -1;
+ float bestMetric = 999.f;
+ for (size_t iTk = 0; iTk < tracks.size(); ++iTk) {
+ if (trackUsed[iTk])
+ continue;
+ const auto& tk = tracks[iTk];
+ float dEta = trkEta - tk.eta();
+ float dPhi = reco::deltaPhi(trkPhi, tk.phi());
+ float dR2 = dEta * dEta + dPhi * dPhi;
+ float dPtRel = std::abs(trkPt - tk.pt()) / trkPt;
+ float metric = dR2 + dPtRel * dPtRel;
+ if (metric < bestMetric) {
+ bestMetric = metric;
+ bestIdx = iTk;
+ }
+ }
+ // Require reasonable match quality
+ if (bestIdx >= 0 && bestMetric < 0.01f) {
+ cand.setTrackProperties(tracks[bestIdx], covarianceSchema_, covarianceVersion_);
+ trackUsed[bestIdx] = true;
+ }
+ }
+
+ output->push_back(cand);
+ }
+
+ iEvent.put(std::move(output));
+}
+
+void Run3ScoutingParticleToPackedCandidateProducer::fillDescriptions(edm::ConfigurationDescriptions& descriptions) {
+ edm::ParameterSetDescription desc;
+ desc.add("src", edm::InputTag("hltScoutingPFPacker"))
+ ->setComment("Input scouting particle collection");
+ desc.add("vertices", edm::InputTag("offlineSlimmedPrimaryVertices"))
+ ->setComment("Input vertex collection for vertex references");
+ desc.add("tracks", edm::InputTag("scoutingTracks"))
+ ->setComment("Input reco::Track collection for embedding track details");
+ desc.add("CHS", false)->setComment("Apply Charged Hadron Subtraction (skip vtx > 0)");
+ desc.add("covarianceVersion", 1)->setComment("Covariance parameterization version (0=Phase0, 1=Phase1)");
+ desc.add("covarianceSchema", 520)->setComment("Covariance packing schema");
+ descriptions.addWithDefaultLabel(desc);
+}
+
+DEFINE_FWK_MODULE(Run3ScoutingParticleToPackedCandidateProducer);
diff --git a/PhysicsTools/PatFromScouting/plugins/Run3ScoutingTrackToRecoTrackProducer.cc b/PhysicsTools/PatFromScouting/plugins/Run3ScoutingTrackToRecoTrackProducer.cc
new file mode 100644
index 0000000000000..093467771fcbf
--- /dev/null
+++ b/PhysicsTools/PatFromScouting/plugins/Run3ScoutingTrackToRecoTrackProducer.cc
@@ -0,0 +1,124 @@
+// -*- C++ -*-
+//
+// Package: PhysicsTools/PatFromScouting
+// Class: Run3ScoutingTrackToRecoTrackProducer
+//
+/**\class Run3ScoutingTrackToRecoTrackProducer Run3ScoutingTrackToRecoTrackProducer.cc PhysicsTools/PatFromScouting/plugins/Run3ScoutingTrackToRecoTrackProducer.cc
+
+ Description: Converts Run3ScoutingTrack to reco::Track with populated hit pattern
+
+*/
+//
+// Original Author: Dmytro Kovalskyi
+// Created: Thu, 05 Dec 2024 15:27:09 GMT
+//
+//
+
+#include
+#include
+
+#include "FWCore/Framework/interface/Frameworkfwd.h"
+#include "FWCore/Framework/interface/stream/EDProducer.h"
+#include "FWCore/Framework/interface/Event.h"
+#include "FWCore/Framework/interface/MakerMacros.h"
+#include "FWCore/ParameterSet/interface/ParameterSet.h"
+#include "FWCore/ParameterSet/interface/ConfigurationDescriptions.h"
+#include "FWCore/ParameterSet/interface/ParameterSetDescription.h"
+
+#include "DataFormats/TrackReco/interface/Track.h"
+#include "DataFormats/TrackReco/interface/TrackFwd.h"
+#include "DataFormats/Common/interface/ValueMap.h"
+#include "DataFormats/Common/interface/OrphanHandle.h"
+#include "DataFormats/Scouting/interface/Run3ScoutingTrack.h"
+#include "DataFormats/PatCandidates/interface/ScoutingDataHandling.h"
+#include "DataFormats/SiPixelDetId/interface/PixelSubdetector.h"
+#include "DataFormats/SiStripDetId/interface/StripSubdetector.h"
+#include "DataFormats/TrackingRecHit/interface/TrackingRecHit.h"
+
+class Run3ScoutingTrackToRecoTrackProducer : public edm::stream::EDProducer<> {
+public:
+ explicit Run3ScoutingTrackToRecoTrackProducer(const edm::ParameterSet&);
+ ~Run3ScoutingTrackToRecoTrackProducer() override = default;
+
+ static void fillDescriptions(edm::ConfigurationDescriptions& descriptions);
+
+private:
+ void produce(edm::Event&, const edm::EventSetup&) override;
+
+ const edm::EDGetTokenT trackToken_;
+
+ std::vector vertexIndex_;
+};
+
+Run3ScoutingTrackToRecoTrackProducer::Run3ScoutingTrackToRecoTrackProducer(const edm::ParameterSet& iConfig)
+ : trackToken_(consumes(iConfig.getParameter("src"))) {
+ produces();
+ produces>("vertexIndex");
+}
+
+void Run3ScoutingTrackToRecoTrackProducer::produce(edm::Event& iEvent, const edm::EventSetup& iSetup) {
+ auto recoTracks = std::make_unique();
+
+ vertexIndex_.clear();
+
+ const auto& scoutingTracks = iEvent.get(trackToken_);
+
+ for (const auto& sTrack : scoutingTracks) {
+ reco::Track recoTrack = pat::makeRecoTrack(sTrack);
+
+ // Populate hit pattern from scouting hit counts.
+ // Prefer pixLayers = min(nPixHits, 4) but adjust to guarantee:
+ // pixLayers + stripLayers = totalLayers, pixLayers <= nPixHits, stripLayers <= nStripHits
+ int nPixHits = sTrack.tk_nValidPixelHits();
+ int nStripHits = sTrack.tk_nValidStripHits();
+ int totalLayers = sTrack.tk_nTrackerLayersWithMeasurement();
+ int lower = std::max(0, totalLayers - nStripHits);
+ int upper = nStripHits > 0 ? std::min(nPixHits, totalLayers - 1) : std::min(nPixHits, totalLayers);
+ int pixLayers = std::clamp(std::min(4, totalLayers), lower, upper);
+ int stripLayers = totalLayers - pixLayers;
+
+ // Pixel: BPIX(1-4), FPIX(1-3), extras on BPIX layer 2
+ for (int i = 0; i < pixLayers; ++i) {
+ if (i < 4)
+ recoTrack.appendTrackerHitPattern(PixelSubdetector::PixelBarrel, i + 1, 0, TrackingRecHit::valid);
+ else
+ recoTrack.appendTrackerHitPattern(PixelSubdetector::PixelEndcap, i - 3, 0, TrackingRecHit::valid);
+ }
+ for (int i = pixLayers; i < nPixHits; ++i)
+ recoTrack.appendTrackerHitPattern(PixelSubdetector::PixelBarrel, 2, 0, TrackingRecHit::valid);
+
+ // Strip: TIB(1-4), TID(1-3), TOB(1-6), TEC(1-9), extras on TIB 1
+ for (int i = 0; i < stripLayers; ++i) {
+ if (i < 4)
+ recoTrack.appendTrackerHitPattern(StripSubdetector::TIB, i + 1, 1, TrackingRecHit::valid);
+ else if (i < 7)
+ recoTrack.appendTrackerHitPattern(StripSubdetector::TID, i - 3, 1, TrackingRecHit::valid);
+ else if (i < 13)
+ recoTrack.appendTrackerHitPattern(StripSubdetector::TOB, i - 6, 1, TrackingRecHit::valid);
+ else
+ recoTrack.appendTrackerHitPattern(StripSubdetector::TEC, i - 12, 1, TrackingRecHit::valid);
+ }
+ for (int i = stripLayers; i < nStripHits; ++i)
+ recoTrack.appendTrackerHitPattern(StripSubdetector::TIB, 1, 1, TrackingRecHit::valid);
+
+ recoTracks->push_back(recoTrack);
+
+ vertexIndex_.push_back(sTrack.tk_vtxInd());
+ }
+
+ edm::OrphanHandle tracksHandle = iEvent.put(std::move(recoTracks));
+
+ auto vertexIndexMap = std::make_unique>();
+ edm::ValueMap::Filler fillerVtx(*vertexIndexMap);
+ fillerVtx.insert(tracksHandle, vertexIndex_.begin(), vertexIndex_.end());
+ fillerVtx.fill();
+ iEvent.put(std::move(vertexIndexMap), "vertexIndex");
+}
+
+void Run3ScoutingTrackToRecoTrackProducer::fillDescriptions(edm::ConfigurationDescriptions& descriptions) {
+ edm::ParameterSetDescription desc;
+ desc.add("src", edm::InputTag("hltScoutingTrackPacker"));
+ descriptions.addWithDefaultLabel(desc);
+}
+
+DEFINE_FWK_MODULE(Run3ScoutingTrackToRecoTrackProducer);
diff --git a/PhysicsTools/PatFromScouting/plugins/Run3ScoutingVertexToRecoVertexProducer.cc b/PhysicsTools/PatFromScouting/plugins/Run3ScoutingVertexToRecoVertexProducer.cc
new file mode 100644
index 0000000000000..6dd78a559f177
--- /dev/null
+++ b/PhysicsTools/PatFromScouting/plugins/Run3ScoutingVertexToRecoVertexProducer.cc
@@ -0,0 +1,75 @@
+// -*- C++ -*-
+//
+// Package: PhysicsTools/PatFromScouting
+// Class: Run3ScoutingVertexToRecoVertexProducer
+//
+/**\class Run3ScoutingVertexToRecoVertexProducer Run3ScoutingVertexToRecoVertexProducer.cc PhysicsTools/PatFromScouting/plugins/Run3ScoutingVertexToRecoVertexProducer.cc
+
+ Description: Converts Run3ScoutingVertex to reco::Vertex
+
+ Implementation:
+ Uses the existing pat::makeRecoVertex helper function
+*/
+//
+// Original Author: Dmytro Kovalskyi
+// Created: Thu, 05 Dec 2024 15:27:09 GMT
+//
+//
+
+#include
+
+#include "FWCore/Framework/interface/Frameworkfwd.h"
+#include "FWCore/Framework/interface/stream/EDProducer.h"
+#include "FWCore/Framework/interface/Event.h"
+#include "FWCore/Framework/interface/MakerMacros.h"
+#include "FWCore/ParameterSet/interface/ParameterSet.h"
+#include "FWCore/ParameterSet/interface/ConfigurationDescriptions.h"
+#include "FWCore/ParameterSet/interface/ParameterSetDescription.h"
+
+#include "DataFormats/VertexReco/interface/Vertex.h"
+#include "DataFormats/VertexReco/interface/VertexFwd.h"
+#include "DataFormats/Scouting/interface/Run3ScoutingVertex.h"
+#include "DataFormats/PatCandidates/interface/ScoutingDataHandling.h"
+
+class Run3ScoutingVertexToRecoVertexProducer : public edm::stream::EDProducer<> {
+public:
+ explicit Run3ScoutingVertexToRecoVertexProducer(const edm::ParameterSet&);
+ ~Run3ScoutingVertexToRecoVertexProducer() override = default;
+
+ static void fillDescriptions(edm::ConfigurationDescriptions& descriptions);
+
+private:
+ void produce(edm::Event&, const edm::EventSetup&) override;
+
+ const edm::EDGetTokenT vertexToken_;
+};
+
+Run3ScoutingVertexToRecoVertexProducer::Run3ScoutingVertexToRecoVertexProducer(const edm::ParameterSet& iConfig)
+ : vertexToken_(consumes(iConfig.getParameter("src"))) {
+ produces();
+}
+
+void Run3ScoutingVertexToRecoVertexProducer::produce(edm::Event& iEvent, const edm::EventSetup& iSetup) {
+ auto recoVertices = std::make_unique();
+
+ const auto& scoutingVertices = iEvent.get(vertexToken_);
+
+ for (const auto& sVtx : scoutingVertices) {
+ if (!sVtx.isValidVtx()) {
+ continue;
+ }
+
+ reco::Vertex recoVtx = pat::makeRecoVertex(sVtx);
+ recoVertices->push_back(recoVtx);
+ }
+
+ iEvent.put(std::move(recoVertices));
+}
+
+void Run3ScoutingVertexToRecoVertexProducer::fillDescriptions(edm::ConfigurationDescriptions& descriptions) {
+ edm::ParameterSetDescription desc;
+ desc.add("src", edm::InputTag("hltScoutingPrimaryVertexPacker", "primaryVtx"));
+ descriptions.addWithDefaultLabel(desc);
+}
+
+DEFINE_FWK_MODULE(Run3ScoutingVertexToRecoVertexProducer);
diff --git a/PhysicsTools/PatFromScouting/plugins/ScoutingDimuonVtxProducer.cc b/PhysicsTools/PatFromScouting/plugins/ScoutingDimuonVtxProducer.cc
new file mode 100644
index 0000000000000..5d2a30e51f132
--- /dev/null
+++ b/PhysicsTools/PatFromScouting/plugins/ScoutingDimuonVtxProducer.cc
@@ -0,0 +1,131 @@
+// -*- C++ -*-
+//
+// Package: PhysicsTools/PatFromScouting
+// Class: ScoutingDimuonVtxProducer
+//
+/**\class ScoutingDimuonVtxProducer ScoutingDimuonVtxProducer.cc
+
+ Description: Converts scouting displaced dimuon vertices to
+ reco::VertexCompositePtrCandidate with CandidatePtr daughters
+ pointing into the pat::Muon collection.
+
+ The association between muons and displaced vertices is read from
+ Run3ScoutingMuon::vtxIndx(), which stores the indices of the
+ displaced vertices each muon belongs to (filled by the HLT
+ scouting muon packer).
+*/
+//
+// Original Author: Dmytro Kovalskyi
+//
+
+#include
+#include
+#include
+
+#include "FWCore/Framework/interface/Frameworkfwd.h"
+#include "FWCore/Framework/interface/stream/EDProducer.h"
+#include "FWCore/Framework/interface/Event.h"
+#include "FWCore/Framework/interface/MakerMacros.h"
+#include "FWCore/ParameterSet/interface/ParameterSet.h"
+#include "FWCore/ParameterSet/interface/ConfigurationDescriptions.h"
+#include "FWCore/ParameterSet/interface/ParameterSetDescription.h"
+
+#include "DataFormats/Candidate/interface/VertexCompositePtrCandidate.h"
+#include "DataFormats/PatCandidates/interface/Muon.h"
+#include "DataFormats/Scouting/interface/Run3ScoutingMuon.h"
+#include "DataFormats/Scouting/interface/Run3ScoutingVertex.h"
+#include "DataFormats/PatCandidates/interface/ScoutingDataHandling.h"
+
+class ScoutingDimuonVtxProducer : public edm::stream::EDProducer<> {
+public:
+ explicit ScoutingDimuonVtxProducer(const edm::ParameterSet&);
+ ~ScoutingDimuonVtxProducer() override = default;
+
+ static void fillDescriptions(edm::ConfigurationDescriptions& descriptions);
+
+private:
+ void produce(edm::Event&, const edm::EventSetup&) override;
+
+ const edm::EDGetTokenT scoutMuonToken_;
+ const edm::EDGetTokenT scoutVtxToken_;
+ const edm::EDGetTokenT> patMuonToken_;
+};
+
+ScoutingDimuonVtxProducer::ScoutingDimuonVtxProducer(const edm::ParameterSet& iConfig)
+ : scoutMuonToken_(consumes(iConfig.getParameter("scoutingMuons"))),
+ scoutVtxToken_(consumes(iConfig.getParameter("scoutingVertices"))),
+ patMuonToken_(consumes>(iConfig.getParameter("patMuons"))) {
+ produces();
+}
+
+void ScoutingDimuonVtxProducer::produce(edm::Event& iEvent, const edm::EventSetup&) {
+ auto output = std::make_unique();
+
+ const auto& scoutMuons = iEvent.get(scoutMuonToken_);
+ const auto& scoutVtxs = iEvent.get(scoutVtxToken_);
+ auto patMuonHandle = iEvent.getHandle(patMuonToken_);
+
+ // Build vertex → muon index map by inverting muon.vtxIndx()
+ // vtxIndx stores displaced vertex indices that each muon belongs to
+ std::unordered_map> vtxToMuons;
+ for (unsigned int iMu = 0; iMu < scoutMuons.size(); ++iMu) {
+ for (int vtxIdx : scoutMuons[iMu].vtxIndx()) {
+ vtxToMuons[vtxIdx].push_back(iMu);
+ }
+ }
+
+ // Convert each scouting displaced vertex
+ for (unsigned int iVtx = 0; iVtx < scoutVtxs.size(); ++iVtx) {
+ const auto& sVtx = scoutVtxs[iVtx];
+ if (!sVtx.isValidVtx())
+ continue;
+
+ // Build vertex covariance
+ reco::Vertex::Error err;
+ err(0, 0) = sVtx.xError() * sVtx.xError();
+ err(1, 1) = sVtx.yError() * sVtx.yError();
+ err(2, 2) = sVtx.zError() * sVtx.zError();
+ err(0, 1) = sVtx.xyCov();
+ err(0, 2) = sVtx.xzCov();
+ err(1, 2) = sVtx.yzCov();
+
+ // Sum p4 of daughter muons and determine charge
+ reco::Candidate::LorentzVector p4;
+ int charge = 0;
+ auto it = vtxToMuons.find(iVtx);
+ if (it != vtxToMuons.end()) {
+ for (unsigned int iMu : it->second) {
+ if (iMu < patMuonHandle->size()) {
+ p4 += patMuonHandle->at(iMu).p4();
+ charge += patMuonHandle->at(iMu).charge();
+ }
+ }
+ }
+
+ reco::VertexCompositePtrCandidate vtxCand(
+ charge, p4, reco::Candidate::Point(sVtx.x(), sVtx.y(), sVtx.z()), err, sVtx.chi2(), sVtx.ndof());
+
+ // Add daughter CandidatePtrs
+ if (it != vtxToMuons.end()) {
+ for (unsigned int iMu : it->second) {
+ if (iMu < patMuonHandle->size()) {
+ vtxCand.addDaughter(patMuonHandle->ptrAt(iMu));
+ }
+ }
+ }
+
+ output->push_back(vtxCand);
+ }
+
+ iEvent.put(std::move(output));
+}
+
+void ScoutingDimuonVtxProducer::fillDescriptions(edm::ConfigurationDescriptions& descriptions) {
+ edm::ParameterSetDescription desc;
+ desc.add("scoutingMuons", edm::InputTag("hltScoutingMuonPackerVtx"));
+ desc.add("scoutingVertices", edm::InputTag("hltScoutingMuonPackerVtx", "displacedVtx"));
+ desc.add("patMuons", edm::InputTag("slimmedMuons"));
+ descriptions.addWithDefaultLabel(desc);
+}
+
+DEFINE_FWK_MODULE(ScoutingDimuonVtxProducer);
diff --git a/PhysicsTools/PatFromScouting/plugins/ScoutingRhoProducer.cc b/PhysicsTools/PatFromScouting/plugins/ScoutingRhoProducer.cc
new file mode 100644
index 0000000000000..595b97b286096
--- /dev/null
+++ b/PhysicsTools/PatFromScouting/plugins/ScoutingRhoProducer.cc
@@ -0,0 +1,40 @@
+/**
+ * ScoutingRhoProducer
+ *
+ * Simple producer that copies the rho value from scouting data
+ * to produce it with standard module names expected by downstream code.
+ */
+
+#include "FWCore/Framework/interface/stream/EDProducer.h"
+#include "FWCore/Framework/interface/Event.h"
+#include "FWCore/Framework/interface/MakerMacros.h"
+#include "FWCore/ParameterSet/interface/ParameterSet.h"
+#include "FWCore/ParameterSet/interface/ConfigurationDescriptions.h"
+
+class ScoutingRhoProducer : public edm::stream::EDProducer<> {
+public:
+ explicit ScoutingRhoProducer(const edm::ParameterSet&);
+ static void fillDescriptions(edm::ConfigurationDescriptions& descriptions);
+
+private:
+ void produce(edm::Event&, const edm::EventSetup&) override;
+
+ const edm::EDGetTokenT rhoToken_;
+};
+
+ScoutingRhoProducer::ScoutingRhoProducer(const edm::ParameterSet& iConfig)
+ : rhoToken_(consumes(iConfig.getParameter("src"))) {
+ produces();
+}
+
+void ScoutingRhoProducer::produce(edm::Event& iEvent, const edm::EventSetup&) {
+ iEvent.put(std::make_unique(iEvent.get(rhoToken_)));
+}
+
+void ScoutingRhoProducer::fillDescriptions(edm::ConfigurationDescriptions& descriptions) {
+ edm::ParameterSetDescription desc;
+ desc.add("src", edm::InputTag("hltScoutingPFPacker", "rho"));
+ descriptions.addWithDefaultLabel(desc);
+}
+
+DEFINE_FWK_MODULE(ScoutingRhoProducer);
diff --git a/PhysicsTools/PatFromScouting/python/autoPAT.py b/PhysicsTools/PatFromScouting/python/autoPAT.py
new file mode 100644
index 0000000000000..29ed1ed73b009
--- /dev/null
+++ b/PhysicsTools/PatFromScouting/python/autoPAT.py
@@ -0,0 +1,65 @@
+"""
+autoPAT - Automatic PAT/MiniAOD and NanoAOD configuration mappings for scouting.
+
+Defines flavors for PAT and NANO steps:
+ --step PAT:@Scout (HLTSCOUT -> MiniAOD)
+ --step NANO:@ScoutMini (Scouting MiniAOD -> NanoAOD)
+
+Usage:
+ cmsDriver.py ... --step PAT:@Scout ...
+ cmsDriver.py ... --step NANO:@ScoutMini ...
+"""
+
+def expandPATMapping(seqList, mapping, key):
+ """Expand @-prefixed mappings in seqList using the mapping dictionary."""
+ maxLevel = 30
+ level = 0
+ while '@' in repr(seqList) and level < maxLevel:
+ level += 1
+ for specifiedCommand in seqList:
+ if specifiedCommand.startswith('@'):
+ location = specifiedCommand[1:]
+ if not location in mapping:
+ raise Exception("Impossible to map " + location + " from " + repr(mapping))
+ mappedTo = mapping[location]
+ insertAt = seqList.index(specifiedCommand)
+ seqList.remove(specifiedCommand)
+ if key in mappedTo and mappedTo[key] is not None:
+ allToInsert = mappedTo[key].split('+')
+ for offset, toInsert in enumerate(allToInsert):
+ seqList.insert(insertAt + offset, toInsert)
+ break
+ if level == maxLevel:
+ raise Exception("Could not fully expand " + repr(seqList) + " from " + repr(mapping))
+
+
+# PAT flavor definitions for --step PAT:@...
+autoPAT = {
+ # Standard MiniAOD (default)
+ 'PHYS': {'sequence': '',
+ 'customize': ''},
+
+ # Scouting MiniAOD from HLTSCOUT input
+ 'Scout': {
+ 'sequence': 'PhysicsTools/PatFromScouting/scoutingToMiniAOD_cff.scoutingToMiniAODTask',
+ 'customize': 'PhysicsTools/PatFromScouting/scoutingToMiniAOD_cff.customiseScoutingToMiniAOD'
+ },
+
+ # Scouting MiniAOD with vertex muons (2024+)
+ 'ScoutVtx': {
+ 'sequence': '@Scout',
+ 'customize': '@Scout+PhysicsTools/PatFromScouting/scoutingToMiniAOD_cff.customiseScoutingToMiniAOD_withMuonVtx'
+ },
+}
+
+
+# NANO flavor definitions for --step NANO:@...
+# These extend the central autoNANO with scouting-specific flavors
+autoNANO_scouting = {
+ # NanoAOD from Scouting MiniAOD (PAT objects)
+ # This reads from slimmedMuons, slimmedJets, etc. produced by PAT:@Scout
+ 'ScoutMini': {
+ 'sequence': 'PhysicsTools/PatFromScouting/scoutingNanoAOD_cff.scoutingNanoAODTask',
+ 'customize': 'PhysicsTools/PatFromScouting/scoutingNanoAOD_cff.customiseScoutingNanoAOD'
+ },
+}
diff --git a/PhysicsTools/PatFromScouting/python/nanoAOD_customizations_cff.py b/PhysicsTools/PatFromScouting/python/nanoAOD_customizations_cff.py
new file mode 100644
index 0000000000000..37584cce333b0
--- /dev/null
+++ b/PhysicsTools/PatFromScouting/python/nanoAOD_customizations_cff.py
@@ -0,0 +1,161 @@
+"""
+Customizations for running NanoAOD on scouting data converted to MiniAOD format.
+
+Scouting data has limited information compared to full reconstruction,
+so some NanoAOD modules need to be disabled or modified.
+"""
+
+import FWCore.ParameterSet.Config as cms
+
+def customizeNanoAODForScouting(process):
+ """
+ Customize NanoAOD for scouting data input.
+
+ Disables modules that require information not available in scouting:
+ - Pileup jet ID (requires jet constituents)
+ - Deep jet taggers (requires jet constituents)
+ - b-tag shape corrections
+ - AK8 jets (not available in scouting)
+ - PPS forward protons (not in scouting)
+ """
+
+ # Remove pileup jet ID modules (they crash without jet constituents)
+ modulesToRemove = [
+ 'pileupJetId94X',
+ 'pileupJetIdPuppi',
+ 'pileupJetId',
+ 'pileupJetIdUpdated',
+ 'updatedJets',
+ 'updatedJetsWithUserData',
+ 'updatedJetsPuppi',
+ 'updatedJetsPuppiWithUserData',
+ # AK8 jets (not available in scouting)
+ 'fatJetTable',
+ 'subJetTable',
+ 'saTable',
+ 'saJetTable',
+ # Deep taggers that need constituents
+ 'btagDeepFlavPuppi',
+ 'btagDeepFlavCMVAPuppi',
+ # B-tag corrections
+ 'btagSFPuppi',
+ 'ctagSFPuppi',
+ ]
+
+ # Remove modules from tasks
+ for moduleName in modulesToRemove:
+ if hasattr(process, moduleName):
+ module = getattr(process, moduleName)
+ for taskName in dir(process):
+ task = getattr(process, taskName)
+ if isinstance(task, cms.Task):
+ try:
+ task.remove(module)
+ except:
+ pass
+
+ # Alternative approach: modify the slimmedJets producer to skip pileup jet ID
+ # by providing empty jet ID information
+ if hasattr(process, 'updatedPatJets'):
+ process.updatedPatJets.addJetID = cms.bool(False)
+
+ if hasattr(process, 'updatedPatJetsPuppi'):
+ process.updatedPatJetsPuppi.addJetID = cms.bool(False)
+
+ # Fix jet tables to not depend on pileup jet ID
+ if hasattr(process, 'jetTable'):
+ # Remove variables that depend on pileup jet ID
+ try:
+ del process.jetTable.variables.puId
+ del process.jetTable.variables.puIdDisc
+ except:
+ pass
+
+ if hasattr(process, 'jetPuppiTable'):
+ try:
+ del process.jetPuppiTable.variables.puId
+ del process.jetPuppiTable.variables.puIdDisc
+ except:
+ pass
+
+ # Disable PPS proton tables (not available in scouting)
+ # Remove from output and tasks instead of trying to configure empty sources
+ ppsModules = ['protonTable', 'multiRPTable', 'singleRPTable']
+ for moduleName in ppsModules:
+ if hasattr(process, moduleName):
+ module = getattr(process, moduleName)
+ # Remove from tasks
+ for taskName in dir(process):
+ task = getattr(process, taskName)
+ if isinstance(task, cms.Task):
+ try:
+ task.remove(module)
+ except:
+ pass
+ # Also try removing from sequences
+ for seqName in dir(process):
+ seq = getattr(process, seqName)
+ if isinstance(seq, cms.Sequence):
+ try:
+ seq.remove(module)
+ except:
+ pass
+
+ # Redirect jet modules that depend on updatedJetsPuppiWithUserData
+ # to use slimmedJets directly (scouting doesn't have separate Puppi jets)
+ if hasattr(process, 'corrT1METJetPuppiTable'):
+ process.corrT1METJetPuppiTable.src = cms.InputTag("slimmedJets")
+ # Remove variables that need user data
+ try:
+ del process.corrT1METJetPuppiTable.variables.muonSubtrFactor
+ del process.corrT1METJetPuppiTable.variables.muonSubtrDeltaEta
+ del process.corrT1METJetPuppiTable.variables.muonSubtrDeltaPhi
+ except:
+ pass
+
+ # Redirect jetPuppiTable if it exists (scouting doesn't have separate Puppi jets)
+ if hasattr(process, 'jetPuppiTable'):
+ process.jetPuppiTable.src = cms.InputTag("slimmedJets")
+
+ # Redirect jetTable to use slimmedJets
+ if hasattr(process, 'jetTable'):
+ process.jetTable.src = cms.InputTag("slimmedJets")
+
+ # Disable modules that need data not available in scouting
+ modulesToDisable = [
+ 'beamSpotTable', # No beam spot
+ 'l1bits', # L1 trigger bits (gtStage2Digis)
+ 'L1PreFiringWeight', # L1 prefiring
+ 'L1PreFiringWeightProducer',
+ # Additional L1/HLT related
+ 'l1TriggerPathTable',
+ 'triggerObjectTable',
+ 'TrigObjFlatTableProducer',
+ ]
+
+ for moduleName in modulesToDisable:
+ if hasattr(process, moduleName):
+ module = getattr(process, moduleName)
+ for taskName in dir(process):
+ task = getattr(process, taskName)
+ if isinstance(task, cms.Task):
+ try:
+ task.remove(module)
+ except:
+ pass
+
+ return process
+
+
+def customizeNanoAODForScoutingMinimal(process):
+ """
+ Minimal customization that only disables crashing modules.
+ """
+ # Simply remove the pileup jet ID producers from all paths and tasks
+ toRemove = ['pileupJetIdPuppi', 'pileupJetId', 'pileupJetIdUpdated']
+
+ for name in toRemove:
+ if hasattr(process, name):
+ delattr(process, name)
+
+ return process
diff --git a/PhysicsTools/PatFromScouting/python/nanoAOD_scouting_cff.py b/PhysicsTools/PatFromScouting/python/nanoAOD_scouting_cff.py
new file mode 100644
index 0000000000000..c72e0b4914ad2
--- /dev/null
+++ b/PhysicsTools/PatFromScouting/python/nanoAOD_scouting_cff.py
@@ -0,0 +1,375 @@
+"""
+NanoAOD customizations for scouting MiniAOD input.
+
+This module provides era-based modifiers and customization functions to run
+standard NanoAOD on MiniAOD produced from scouting data.
+
+Usage with cmsDriver:
+
+ # Step 1: Scouting RAW -> MiniAOD
+ cmsDriver.py step1 --conditions auto:run3_data_prompt \
+ --era Run3 --step MINI:PhysicsTools/PatFromScouting/scoutingMiniAOD_cff.scoutingMiniAODTask \
+ --filein file:scouting.root --fileout file:scoutingMiniAOD.root
+
+ # Step 2: MiniAOD -> NanoAOD with scouting modifier
+ cmsDriver.py NANO --conditions auto:run3_data_prompt \
+ --era Run3 --step NANO \
+ --customise PhysicsTools/PatFromScouting/nanoAOD_scouting_cff.customiseNanoForScoutingMiniAOD \
+ --filein file:scoutingMiniAOD.root --fileout file:nanoAOD.root
+
+Scouting MiniAOD limitations handled:
+- No jet constituents -> disable pileup jet ID, deep taggers
+- No AK8 jets -> disable fat jet tables
+- No PPS data -> disable proton tables
+- Limited trigger objects -> use empty trigger object collection
+- No beam spot constraint -> disable beam spot table
+"""
+
+import FWCore.ParameterSet.Config as cms
+
+
+def _removeModuleFromTasks(process, moduleName):
+ """Helper to remove a module from all tasks in the process."""
+ if hasattr(process, moduleName):
+ module = getattr(process, moduleName)
+ for taskName in dir(process):
+ task = getattr(process, taskName, None)
+ if isinstance(task, cms.Task):
+ try:
+ task.remove(module)
+ except:
+ pass
+
+
+def _removeModulesFromProcess(process, moduleNames):
+ """Remove multiple modules from tasks."""
+ for name in moduleNames:
+ _removeModuleFromTasks(process, name)
+
+
+def customiseNanoForScoutingMiniAOD(process):
+ """
+ Main customization function for running standard NanoAOD on scouting MiniAOD.
+
+ This is the recommended entry point for cmsDriver --customise flag.
+ """
+
+ # ============================================================
+ # 1. Disable modules requiring jet constituents
+ # ============================================================
+
+ # Pileup Jet ID - requires iterating over jet daughters
+ pileupJetIdModules = [
+ 'pileupJetId94X',
+ 'pileupJetIdPuppi',
+ 'pileupJetId',
+ 'pileupJetIdUpdated',
+ ]
+ _removeModulesFromProcess(process, pileupJetIdModules)
+
+ # Jet update chain - these try to recompute jet properties
+ jetUpdateModules = [
+ 'updatedJets',
+ 'updatedJetsWithUserData',
+ 'updatedJetsPuppi',
+ 'updatedJetsPuppiWithUserData',
+ # Full jet recreation chain
+ 'patJetsPuppi',
+ 'patJets',
+ 'selectedPatJetsPuppi',
+ 'selectedPatJets',
+ 'slimmedJetsPuppiNoDeepTags',
+ 'updatedPatJetsSlimmedDeepFlavour',
+ 'updatedPatJetsTransientCorrectedSlimmedDeepFlavour',
+ # Additional jet modules
+ 'tightJetId',
+ 'tightJetIdLepVeto',
+ 'jetCorrFactorsNano',
+ 'jetCorrFactorsAK8',
+ ]
+ _removeModulesFromProcess(process, jetUpdateModules)
+
+ # Deep taggers requiring constituent information
+ deepTaggerModules = [
+ 'btagDeepFlavPuppi',
+ 'btagDeepFlavCMVAPuppi',
+ 'btagSFPuppi',
+ 'ctagSFPuppi',
+ ]
+ _removeModulesFromProcess(process, deepTaggerModules)
+
+ # ============================================================
+ # 2. Disable AK8/fat jet modules (not available in scouting)
+ # ============================================================
+
+ fatJetModules = [
+ 'fatJetTable',
+ 'subJetTable',
+ 'saTable',
+ 'saJetTable',
+ 'softActivityJetTable',
+ 'fatJetMCTable',
+ 'subJetMCTable',
+ ]
+ _removeModulesFromProcess(process, fatJetModules)
+
+ # ============================================================
+ # 2b. Disable tau modules (not available in scouting)
+ # ============================================================
+
+ tauModules = [
+ 'slimmedTaus',
+ 'slimmedTausUpdated',
+ 'slimmedTausWithPNetCHS',
+ 'finalTaus',
+ 'tauTable',
+ 'tauMCTable',
+ 'boostedTauTable',
+ 'boostedTauMCTable',
+ 'tauIdMVAIsoTask',
+ 'tauIdDeepTauTask',
+ ]
+ _removeModulesFromProcess(process, tauModules)
+
+ # ============================================================
+ # 3. Disable PPS proton modules (not in scouting data)
+ # ============================================================
+
+ ppsModules = [
+ 'protonTable',
+ 'multiRPTable',
+ 'singleRPTable',
+ ]
+ _removeModulesFromProcess(process, ppsModules)
+
+ # ============================================================
+ # 4. Redirect jet tables to use input jets directly
+ # ============================================================
+
+ # Skip the jet update chain, use slimmed jets directly
+ if hasattr(process, 'jetTable'):
+ process.jetTable.src = cms.InputTag("slimmedJets")
+ # Remove pileup jet ID variables
+ if hasattr(process.jetTable, 'variables'):
+ vars_to_remove = ['puId', 'puIdDisc']
+ for var in vars_to_remove:
+ if hasattr(process.jetTable.variables, var):
+ delattr(process.jetTable.variables, var)
+
+ if hasattr(process, 'jetPuppiTable'):
+ # Scouting doesn't have separate Puppi jets, use slimmedJets
+ process.jetPuppiTable.src = cms.InputTag("slimmedJets")
+ if hasattr(process.jetPuppiTable, 'variables'):
+ vars_to_remove = ['puId', 'puIdDisc']
+ for var in vars_to_remove:
+ if hasattr(process.jetPuppiTable.variables, var):
+ delattr(process.jetPuppiTable.variables, var)
+
+ # Redirect corrT1METJetPuppiTable and remove unavailable variables
+ if hasattr(process, 'corrT1METJetPuppiTable'):
+ # Scouting doesn't have separate Puppi jets, use slimmedJets
+ process.corrT1METJetPuppiTable.src = cms.InputTag("slimmedJets")
+ if hasattr(process.corrT1METJetPuppiTable, 'variables'):
+ vars_to_remove = ['muonSubtrFactor', 'muonSubtrDeltaEta', 'muonSubtrDeltaPhi']
+ for var in vars_to_remove:
+ if hasattr(process.corrT1METJetPuppiTable.variables, var):
+ delattr(process.corrT1METJetPuppiTable.variables, var)
+
+ # ============================================================
+ # 5. Handle trigger-related modules
+ # ============================================================
+
+ # L1 prefiring weights not available
+ l1Modules = [
+ 'L1PreFiringWeight',
+ 'L1PreFiringWeightProducer',
+ 'l1TriggerPathTable',
+ ]
+ _removeModulesFromProcess(process, l1Modules)
+
+ # Trigger object table needs caloStage2Digis and gmtStage2Digis which we don't have
+ # We only have gtStage2Digis from scouting. Remove the trigger object table.
+ triggerModules = [
+ 'triggerObjectTable',
+ 'l1MuTable',
+ 'l1EGTable',
+ 'l1TauTable',
+ 'l1JetTable',
+ 'l1EtSumTable',
+ ]
+ _removeModulesFromProcess(process, triggerModules)
+
+ # Keep l1bits if gtStage2Digis is available (we produce it in scoutingToMiniAOD)
+
+ # ============================================================
+ # 6. Handle MET modules (we only have slimmedMETs, not Puppi variants)
+ # ============================================================
+
+ # Remove/redirect modules that need slimmedMETsPuppi
+ puppiMetModules = [
+ 'rawPuppiMetTable',
+ 'puppiMetTable',
+ 'metPuppiTable',
+ 'corrT1METJetPuppiTable',
+ ]
+ _removeModulesFromProcess(process, puppiMetModules)
+
+ # Redirect MET table to use slimmedMETs
+ if hasattr(process, 'metTable'):
+ process.metTable.src = cms.InputTag("slimmedMETs")
+
+ # ============================================================
+ # 7. Handle muons and vertices
+ # ============================================================
+
+ # Remove slimmedMuonsUpdated which might need additional info
+ _removeModuleFromTasks(process, 'slimmedMuonsUpdated')
+
+ # Replace pvbsTable to use offlineSlimmedPrimaryVertices
+ # (we don't have offlineSlimmedPrimaryVerticesWithBS)
+ if hasattr(process, 'pvbsTable'):
+ process.pvbsTable = process.pvTable.clone(
+ src = cms.InputTag("offlineSlimmedPrimaryVertices"),
+ name = cms.string("PVBS"),
+ doc = cms.string("PV with beam spot (same as PV for scouting)")
+ )
+
+ # Redirect muon table to use slimmedMuons directly
+ if hasattr(process, 'muonTable'):
+ process.muonTable.src = cms.InputTag("slimmedMuons")
+
+ # ============================================================
+ # 8. Handle rho variables
+ # ============================================================
+
+ # Scouting MiniAOD has fixedGridRhoFastjetAll but NanoAOD also needs
+ # fixedGridRhoFastjetCentral. We need to either add it to MiniAOD
+ # or redirect the rho table.
+
+ # Remove the rhoTable if it causes issues (it needs fixedGridRhoFastjetCentral)
+ # Alternative: add fixedGridRhoFastjetCentral to the process
+ if hasattr(process, 'rhoTable'):
+ # Check if fixedGridRhoFastjetCentral is being produced
+ if not hasattr(process, 'fixedGridRhoFastjetCentral'):
+ # Create the producer
+ process.fixedGridRhoFastjetCentral = cms.EDProducer("FixedGridRhoProducerFastjet",
+ pfCandidatesTag = cms.InputTag("packedPFCandidates"),
+ maxRapidity = cms.double(2.5),
+ gridSpacing = cms.double(0.55)
+ )
+ # Add to any existing task that runs before nanoSequence
+ if hasattr(process, 'nanoSequence'):
+ # Insert at the beginning of nanoSequence
+ try:
+ process.nanoSequence.insert(0, process.fixedGridRhoFastjetCentral)
+ except:
+ pass
+
+ # ============================================================
+ # 9. Handle electron/photon updates
+ # ============================================================
+
+ # Remove electron updater that might need unavailable info
+ electronUpdateModules = [
+ 'slimmedElectronsUpdated',
+ 'slimmedElectronsWithUserData',
+ ]
+ _removeModulesFromProcess(process, electronUpdateModules)
+
+ # Redirect electron table to use slimmedElectrons
+ if hasattr(process, 'electronTable'):
+ process.electronTable.src = cms.InputTag("slimmedElectrons")
+
+ # Same for photons
+ photonUpdateModules = [
+ 'slimmedPhotonsUpdated',
+ 'slimmedPhotonsWithUserData',
+ ]
+ _removeModulesFromProcess(process, photonUpdateModules)
+
+ # Replace photon table with a simpler version for scouting
+ # Our scouting photons don't have VID, MVA, or most standard variables
+ if hasattr(process, 'photonTable'):
+ from PhysicsTools.NanoAOD.common_cff import Var, P3Vars
+ process.photonTable = cms.EDProducer("SimplePATPhotonFlatTableProducer",
+ src = cms.InputTag("slimmedPhotons"),
+ cut = cms.string(""),
+ name = cms.string("Photon"),
+ doc = cms.string("Photons from scouting"),
+ singleton = cms.bool(False),
+ extension = cms.bool(False),
+ variables = cms.PSet(
+ P3Vars,
+ # Shower shape from userFloats (set in our producer)
+ sieie = Var("userFloat('sigmaIetaIeta')", float, doc="sigma_IetaIeta", precision=10),
+ hoe = Var("userFloat('hOverE')", float, doc="H/E", precision=8),
+ r9 = Var("userFloat('r9')", float, doc="R9", precision=10),
+ # Isolation from userFloats
+ ecalIso = Var("userFloat('ecalIso')", float, doc="ECAL isolation", precision=6),
+ hcalIso = Var("userFloat('hcalIso')", float, doc="HCAL isolation", precision=6),
+ trkIso = Var("userFloat('trkIso')", float, doc="track isolation", precision=6),
+ )
+ )
+
+ # Replace electron table with a simpler version for scouting
+ if hasattr(process, 'electronTable'):
+ from PhysicsTools.NanoAOD.common_cff import Var, P4Vars
+ process.electronTable = cms.EDProducer("SimplePATElectronFlatTableProducer",
+ src = cms.InputTag("slimmedElectrons"),
+ cut = cms.string(""),
+ name = cms.string("Electron"),
+ doc = cms.string("Electrons from scouting"),
+ singleton = cms.bool(False),
+ extension = cms.bool(False),
+ variables = cms.PSet(
+ P4Vars,
+ charge = Var("charge", int, doc="charge"),
+ pdgId = Var("pdgId", int, doc="PDG ID"),
+ # Shower shape from userFloats
+ sieie = Var("userFloat('sigmaIetaIeta')", float, doc="sigma_IetaIeta", precision=10),
+ hoe = Var("userFloat('hOverE')", float, doc="H/E", precision=8),
+ r9 = Var("userFloat('r9')", float, doc="R9", precision=10),
+ # ID variables from userFloats
+ dEtaIn = Var("userFloat('dEtaIn')", float, doc="dEta(SC,track)", precision=10),
+ dPhiIn = Var("userFloat('dPhiIn')", float, doc="dPhi(SC,track)", precision=10),
+ ooEMOop = Var("userFloat('ooEMOop')", float, doc="1/E - 1/p", precision=10),
+ # Track from userFloats
+ dxy = Var("userFloat('trkd0')", float, doc="track d0", precision=10),
+ dz = Var("userFloat('trkdz')", float, doc="track dz", precision=10),
+ # Isolation from userFloats
+ ecalIso = Var("userFloat('ecalIso')", float, doc="ECAL isolation", precision=6),
+ hcalIso = Var("userFloat('hcalIso')", float, doc="HCAL isolation", precision=6),
+ trackIso = Var("userFloat('trackIso')", float, doc="track isolation", precision=6),
+ )
+ )
+
+ # ============================================================
+ # 10. Use TryToContinue for remaining missing products
+ # ============================================================
+
+ # This allows the job to continue if some products are missing
+ if hasattr(process, 'options'):
+ if not hasattr(process.options, 'TryToContinue'):
+ process.options.TryToContinue = cms.untracked.vstring()
+ process.options.TryToContinue.append('ProductNotFound')
+
+ return process
+
+
+def customiseNanoForScoutingMiniAOD_nol1bits(process):
+ """
+ Customization that also removes L1 bits (for testing without L1 unpacking).
+ """
+ process = customiseNanoForScoutingMiniAOD(process)
+ _removeModuleFromTasks(process, 'l1bits')
+ return process
+
+
+def customiseNanoForScoutingMiniAOD_keepFatJets(process):
+ """
+ Variant that keeps fat jet modules (if you recluster AK8 jets from PF candidates).
+ """
+ process = customiseNanoForScoutingMiniAOD(process)
+ # Re-add fat jet modules that were removed
+ # (they would need reclustered AK8 jets as input)
+ return process
diff --git a/PhysicsTools/PatFromScouting/python/scoutingMiniAOD_cff.py b/PhysicsTools/PatFromScouting/python/scoutingMiniAOD_cff.py
new file mode 100644
index 0000000000000..bac88d641756f
--- /dev/null
+++ b/PhysicsTools/PatFromScouting/python/scoutingMiniAOD_cff.py
@@ -0,0 +1,286 @@
+"""
+Scouting to MiniAOD conversion configuration.
+
+This module converts Run3 scouting data to MiniAOD-compatible format with
+standard collection names for downstream processing with standard NanoAOD.
+
+Output collections (matching standard MiniAOD names):
+- packedPFCandidates: pat::PackedCandidateCollection from scouting particles
+- offlineSlimmedPrimaryVertices: reco::VertexCollection from scouting vertices
+- slimmedMuons: pat::MuonCollection from scouting muons
+- slimmedMuonsNoVtx: pat::MuonCollection from displaced scouting muons (2024+)
+- slimmedElectrons: pat::ElectronCollection from scouting electrons
+- slimmedPhotons: pat::PhotonCollection from scouting photons
+- slimmedJets: pat::JetCollection from scouting PF jets
+- slimmedMETs: pat::METCollection computed from scouting PF candidates
+- fixedGridRhoAll: double for pileup density
+- fixedGridRhoFastjetAll: double for fastjet rho
+- gtStage2Digis: L1 trigger unpacking from raw FED data
+- scoutingTracks: reco::TrackCollection from scouting tracks
+
+Usage:
+ from PhysicsTools.PatFromScouting.scoutingMiniAOD_cff import scoutingMiniAODTask
+ process.scoutingMiniAODPath = cms.Path()
+ process.scoutingMiniAODPath.associate(scoutingMiniAODTask)
+
+Or via cmsDriver customization:
+ --customise PhysicsTools/PatFromScouting/scoutingMiniAOD_cff.customiseForScoutingMiniAOD
+"""
+
+import FWCore.ParameterSet.Config as cms
+
+# ============================================================
+# Primary Vertices (must be produced before PF candidates)
+# ============================================================
+
+offlineSlimmedPrimaryVertices = cms.EDProducer("Run3ScoutingVertexToRecoVertexProducer",
+ src = cms.InputTag("hltScoutingPrimaryVertexPacker", "primaryVtx")
+)
+
+# ============================================================
+# PF Candidates from scouting particles
+# ============================================================
+
+# Using pat::PackedCandidate for MiniAOD compatibility
+# Requires vertices to be produced first for proper vertex references
+packedPFCandidates = cms.EDProducer("Run3ScoutingParticleToPackedCandidateProducer",
+ src = cms.InputTag("hltScoutingPFPacker"),
+ vertices = cms.InputTag("offlineSlimmedPrimaryVertices"),
+ CHS = cms.bool(False)
+)
+
+# ============================================================
+# Muons
+# ============================================================
+
+slimmedMuons = cms.EDProducer("PatFromScoutingMuonProducer",
+ src = cms.InputTag("hltScoutingMuonPacker")
+)
+
+# Displaced muons (no vertex constraint) - available from 2024+
+slimmedMuonsNoVtx = cms.EDProducer("PatFromScoutingMuonProducer",
+ src = cms.InputTag("hltScoutingMuonPackerNoVtx")
+)
+
+# ============================================================
+# Electrons
+# ============================================================
+
+slimmedElectrons = cms.EDProducer("PatFromScoutingElectronProducer",
+ src = cms.InputTag("hltScoutingEgammaPacker")
+)
+
+# ============================================================
+# Photons
+# ============================================================
+
+slimmedPhotons = cms.EDProducer("PatFromScoutingPhotonProducer",
+ src = cms.InputTag("hltScoutingEgammaPacker")
+)
+
+# ============================================================
+# Jets
+# ============================================================
+
+slimmedJets = cms.EDProducer("PatFromScoutingJetProducer",
+ src = cms.InputTag("hltScoutingPFPacker"),
+ pfCandidates = cms.InputTag("packedPFCandidates")
+)
+
+# ============================================================
+# MET
+# ============================================================
+
+slimmedMETs = cms.EDProducer("Run3ScoutingMETProducer",
+ metPt = cms.InputTag("hltScoutingPFPacker", "pfMetPt"),
+ metPhi = cms.InputTag("hltScoutingPFPacker", "pfMetPhi")
+)
+
+# ============================================================
+# Tracks
+# ============================================================
+
+scoutingTracks = cms.EDProducer("Run3ScoutingTrackToRecoTrackProducer",
+ src = cms.InputTag("hltScoutingTrackPacker")
+)
+
+# ============================================================
+# Beam Spot (from conditions database)
+# ============================================================
+
+# BeamSpotProducer reads from conditions - works with GlobalTag
+offlineBeamSpot = cms.EDProducer("BeamSpotProducer")
+
+# ============================================================
+# Pileup Density (rho) - all variants needed by NanoAOD
+# ============================================================
+
+# All eta range
+fixedGridRhoAll = cms.EDProducer("FixedGridRhoProducer",
+ pfCandidatesTag = cms.InputTag("packedPFCandidates"),
+ EtaRegion = cms.string("All")
+)
+
+fixedGridRhoFastjetAll = cms.EDProducer("FixedGridRhoProducerFastjet",
+ pfCandidatesTag = cms.InputTag("packedPFCandidates"),
+ maxRapidity = cms.double(5.0),
+ gridSpacing = cms.double(0.55)
+)
+
+# Central region (|eta| < 2.5)
+fixedGridRhoFastjetCentral = cms.EDProducer("FixedGridRhoProducerFastjet",
+ pfCandidatesTag = cms.InputTag("packedPFCandidates"),
+ maxRapidity = cms.double(2.5),
+ gridSpacing = cms.double(0.55)
+)
+
+# Central "calo-like" - use PF candidates in central region
+# (scouting doesn't have calo towers, so we approximate with PF)
+fixedGridRhoFastjetCentralCalo = cms.EDProducer("FixedGridRhoProducerFastjet",
+ pfCandidatesTag = cms.InputTag("packedPFCandidates"),
+ maxRapidity = cms.double(2.5),
+ gridSpacing = cms.double(0.55)
+)
+
+# Central charged-only (approximate - uses all PF candidates for scouting)
+fixedGridRhoFastjetCentralChargedPileUp = cms.EDProducer("FixedGridRhoProducerFastjet",
+ pfCandidatesTag = cms.InputTag("packedPFCandidates"),
+ maxRapidity = cms.double(2.5),
+ gridSpacing = cms.double(0.55)
+)
+
+# Neutral-only variant (approximate - uses all PF candidates for scouting)
+fixedGridRhoFastjetCentralNeutral = cms.EDProducer("FixedGridRhoProducerFastjet",
+ pfCandidatesTag = cms.InputTag("packedPFCandidates"),
+ maxRapidity = cms.double(2.5),
+ gridSpacing = cms.double(0.55)
+)
+
+# ============================================================
+# L1 Trigger Unpacking
+# ============================================================
+
+gtStage2Digis = cms.EDProducer("L1TRawToDigi",
+ InputLabel = cms.InputTag("hltFEDSelectorL1"),
+ Setup = cms.string("stage2::GTSetup"),
+ FedIds = cms.vint32(1404),
+)
+
+# ============================================================
+# Task and Sequence Definitions
+# ============================================================
+
+# Core task - objects needed for standard NanoAOD
+# Note: slimmedMuonsNoVtx is not included here; it is only available from 2024+
+# and should be added explicitly when processing 2024+ data
+scoutingMiniAODCoreTask = cms.Task(
+ packedPFCandidates,
+ offlineSlimmedPrimaryVertices,
+ offlineBeamSpot,
+ slimmedMuons,
+ slimmedElectrons,
+ slimmedPhotons,
+ slimmedJets,
+ slimmedMETs,
+ # All rho variants needed by NanoAOD
+ fixedGridRhoAll,
+ fixedGridRhoFastjetAll,
+ fixedGridRhoFastjetCentral,
+ fixedGridRhoFastjetCentralCalo,
+ fixedGridRhoFastjetCentralChargedPileUp,
+ fixedGridRhoFastjetCentralNeutral,
+)
+
+# Full task including L1 unpacking and tracks
+scoutingMiniAODTask = cms.Task(
+ scoutingMiniAODCoreTask,
+ gtStage2Digis,
+ scoutingTracks,
+)
+
+# Sequence for backward compatibility
+scoutingMiniAODSequence = cms.Sequence(scoutingMiniAODTask)
+
+
+# ============================================================
+# Customization Functions
+# ============================================================
+
+def customiseForScoutingMiniAOD(process):
+ """
+ Customization function for cmsDriver to add scouting MiniAOD production.
+
+ Usage:
+ cmsDriver.py MINI --customise PhysicsTools/PatFromScouting/scoutingMiniAOD_cff.customiseForScoutingMiniAOD
+ """
+
+ # Load particle data table (needed for PF candidate producer)
+ process.load("SimGeneral.HepPDTESSource.pdt_cfi")
+
+ # Add all producers to the process
+ process.packedPFCandidates = packedPFCandidates.clone()
+ process.offlineSlimmedPrimaryVertices = offlineSlimmedPrimaryVertices.clone()
+ process.slimmedMuons = slimmedMuons.clone()
+ process.slimmedElectrons = slimmedElectrons.clone()
+ process.slimmedPhotons = slimmedPhotons.clone()
+ process.slimmedJets = slimmedJets.clone()
+ process.slimmedMETs = slimmedMETs.clone()
+ process.scoutingTracks = scoutingTracks.clone()
+ process.fixedGridRhoAll = fixedGridRhoAll.clone()
+ process.fixedGridRhoFastjetAll = fixedGridRhoFastjetAll.clone()
+ process.gtStage2Digis = gtStage2Digis.clone()
+
+ # Create task and path
+ process.scoutingMiniAODTask = cms.Task(
+ process.packedPFCandidates,
+ process.offlineSlimmedPrimaryVertices,
+ process.slimmedMuons,
+ process.slimmedElectrons,
+ process.slimmedPhotons,
+ process.slimmedJets,
+ process.slimmedMETs,
+ process.scoutingTracks,
+ process.fixedGridRhoAll,
+ process.fixedGridRhoFastjetAll,
+ process.gtStage2Digis,
+ )
+
+ process.scoutingMiniAOD_step = cms.Path()
+ process.scoutingMiniAOD_step.associate(process.scoutingMiniAODTask)
+
+ # Add to schedule if it exists
+ if hasattr(process, 'schedule') and process.schedule is not None:
+ process.schedule.insert(0, process.scoutingMiniAOD_step)
+
+ return process
+
+
+def customiseOutputForScoutingMiniAOD(process):
+ """
+ Customize output module for scouting MiniAOD content.
+ """
+
+ outputCommands = cms.untracked.vstring(
+ 'drop *',
+ 'keep *_packedPFCandidates_*_*',
+ 'keep *_offlineSlimmedPrimaryVertices_*_*',
+ 'keep *_slimmedMuons_*_*',
+ 'keep *_slimmedElectrons_*_*',
+ 'keep *_slimmedPhotons_*_*',
+ 'keep *_slimmedJets_*_*',
+ 'keep *_slimmedMETs_*_*',
+ 'keep *_scoutingTracks_*_*',
+ 'keep *_TriggerResults_*_*',
+ 'keep *_fixedGridRhoAll_*_*',
+ 'keep *_fixedGridRhoFastjetAll_*_*',
+ 'keep *_gtStage2Digis_*_*',
+ )
+
+ # Find and configure output module
+ for name in ['MINIAODoutput', 'MINIAODSIMoutput', 'output', 'out']:
+ if hasattr(process, name):
+ output = getattr(process, name)
+ output.outputCommands = outputCommands
+ break
+
+ return process
diff --git a/PhysicsTools/PatFromScouting/python/scoutingNanoAOD_cff.py b/PhysicsTools/PatFromScouting/python/scoutingNanoAOD_cff.py
new file mode 100644
index 0000000000000..3f8f10a90e603
--- /dev/null
+++ b/PhysicsTools/PatFromScouting/python/scoutingNanoAOD_cff.py
@@ -0,0 +1,338 @@
+"""
+Scouting NanoAOD configuration.
+
+Produces NanoAOD flat tables directly from scouting MiniAOD collections,
+bypassing the standard NanoAOD jet update chains that require full reconstruction.
+
+Tables produced:
+- Muon: pt, eta, phi, mass, charge, isolation, ID flags
+- MuonNoVtx: displaced muons without vertex constraint (2024+)
+- Electron: pt, eta, phi, mass, charge, shower shape, isolation
+- Photon: pt, eta, phi, mass, shower shape, isolation
+- Jet: pt, eta, phi, mass, energy fractions, multiplicities, b-tags
+- MET: pt, phi, sumEt
+- PV: x, y, z, errors, chi2, ndof
+- DimuonVtx: displaced dimuon vertices with muon index cross-references
+- DimuonVtxNoVtx: displaced dimuon vertices from NoVtx muons (2024+)
+- Event: fixedGridRhoAll
+
+To include HLT trigger decisions, add to outputCommands:
+ 'keep edmTriggerResults_*_*_*'
+This will create HLT_* and DST_* boolean branches for all trigger paths.
+"""
+
+import FWCore.ParameterSet.Config as cms
+from Configuration.Eras.Modifier_run3_scouting_2024_cff import run3_scouting_2024
+from PhysicsTools.NanoAOD.common_cff import Var
+
+# Muon table
+scoutingMuonTable = cms.EDProducer("SimplePATMuonFlatTableProducer",
+ src = cms.InputTag("slimmedMuons"),
+ cut = cms.string(""),
+ name = cms.string("Muon"),
+ doc = cms.string("Muons from scouting"),
+ singleton = cms.bool(False),
+ extension = cms.bool(False),
+ variables = cms.PSet(
+ pt = Var("pt", float, doc="pt", precision=10),
+ eta = Var("eta", float, doc="eta", precision=12),
+ phi = Var("phi", float, doc="phi", precision=12),
+ mass = Var("mass", float, doc="mass", precision=10),
+ charge = Var("charge", int, doc="charge"),
+ pdgId = Var("pdgId", int, doc="PDG ID"),
+ # Use global track dxy/dz - for scouting muons the track is embedded as combinedMuon
+ dxy = Var("? globalTrack.isNonnull ? globalTrack.dxy : 0", float, doc="dxy w.r.t. track reference point", precision=10),
+ dxyErr = Var("? globalTrack.isNonnull ? globalTrack.dxyError : 0", float, doc="dxy uncertainty", precision=10),
+ dz = Var("? globalTrack.isNonnull ? globalTrack.dz : 0", float, doc="dz w.r.t. track reference point", precision=10),
+ dzErr = Var("? globalTrack.isNonnull ? globalTrack.dzError : 0", float, doc="dz uncertainty", precision=10),
+ trkChi2 = Var("? globalTrack.isNonnull ? globalTrack.normalizedChi2 : -1", float, doc="track chi2/ndof", precision=6),
+ nValidHits = Var("? globalTrack.isNonnull ? globalTrack.numberOfValidHits : -1", int, doc="number of valid hits"),
+ # ID flags
+ isGlobal = Var("isGlobalMuon", bool, doc="is global muon"),
+ isTracker = Var("isTrackerMuon", bool, doc="is tracker muon"),
+ isStandalone = Var("isStandAloneMuon", bool, doc="is standalone muon"),
+ isPF = Var("isPFMuon", bool, doc="is PF muon"),
+ # Station and layer counts
+ nStations = Var("numberOfMatchedStations()", int, doc="number of matched stations with default arbitration"),
+ nTrackerLayers = Var("numberOfTrackerLayersWithMeasurement()", int, doc="number of tracker layers with measurement"),
+ nPixelLayers = Var("numberOfPixelLayersWithMeasurement()", int, doc="number of pixel layers with measurement"),
+ nChambers = Var("numberOfChambers()", int, doc="number of chambers"),
+ nChambersCSCorDT = Var("numberOfChambersCSCorDT()", int, doc="number of CSC or DT chambers"),
+ # Hit counts
+ nValidMuonHits = Var("numberOfValidMuonHits()", int, doc="number of valid muon hits"),
+ nValidPixelHits = Var("numberOfValidPixelHits()", int, doc="number of valid pixel hits"),
+ nValidStripHits = Var("numberOfValidStripHits()", int, doc="number of valid strip hits"),
+ # Isolation - scouting provides trackIso, ecalIso, hcalIso (not PF components)
+ # Use calorimeter isolation sum as proxy for relIso
+ relIso = Var("(isolationR03().emEt + isolationR03().hadEt)/pt", float, doc="relative calo isolation (ecal+hcal)/pt", precision=6),
+ ecalIso = Var("isolationR03().emEt", float, doc="ECAL isolation", precision=6),
+ hcalIso = Var("isolationR03().hadEt", float, doc="HCAL isolation", precision=6),
+ trkIso = Var("isolationR03().sumPt", float, doc="tracker isolation", precision=6),
+ tkRelIso = Var("isolationR03().sumPt/pt", float, doc="tracker relative isolation", precision=6),
+ )
+)
+
+# NoVtx muon table (2024+ only)
+scoutingMuonNoVtxTable = scoutingMuonTable.clone(
+ src = cms.InputTag("slimmedMuonsNoVtx"),
+ name = cms.string("MuonNoVtx"),
+ doc = cms.string("Displaced muons from scouting (no vertex constraint)"),
+)
+
+# Electron table
+scoutingElectronTable = cms.EDProducer("SimplePATElectronFlatTableProducer",
+ src = cms.InputTag("slimmedElectrons"),
+ cut = cms.string(""),
+ name = cms.string("Electron"),
+ doc = cms.string("Electrons from scouting"),
+ singleton = cms.bool(False),
+ extension = cms.bool(False),
+ variables = cms.PSet(
+ pt = Var("pt", float, doc="pt", precision=10),
+ eta = Var("eta", float, doc="eta", precision=12),
+ phi = Var("phi", float, doc="phi", precision=12),
+ mass = Var("mass", float, doc="mass", precision=10),
+ charge = Var("charge", int, doc="charge"),
+ pdgId = Var("pdgId", int, doc="PDG ID"),
+ dxy = Var("dB('PV2D')", float, doc="dxy wrt PV", precision=10),
+ dz = Var("dB('PVDZ')", float, doc="dz wrt PV", precision=10),
+ # Shower shape
+ sieie = Var("full5x5_sigmaIetaIeta", float, doc="sigma_IetaIeta", precision=10),
+ hoe = Var("hadronicOverEm", float, doc="H/E", precision=8),
+ # ID
+ dEtaIn = Var("deltaEtaSuperClusterTrackAtVtx", float, doc="dEta(SC, track)", precision=10),
+ dPhiIn = Var("deltaPhiSuperClusterTrackAtVtx", float, doc="dPhi(SC, track)", precision=10),
+ # Isolation
+ pfRelIso03_all = Var("(pfIsolationVariables().sumChargedHadronPt + max(pfIsolationVariables().sumNeutralHadronEt + pfIsolationVariables().sumPhotonEt - 0.5*pfIsolationVariables().sumPUPt, 0.0))/pt", float, doc="PF relative isolation dR=0.3", precision=6),
+ ecalIso = Var("ecalPFClusterIso", float, doc="ECAL PF cluster isolation", precision=6),
+ hcalIso = Var("hcalPFClusterIso", float, doc="HCAL PF cluster isolation", precision=6),
+ trkIso = Var("trackIso", float, doc="tracker isolation", precision=6),
+ )
+)
+
+# Photon table
+scoutingPhotonTable = cms.EDProducer("SimplePATPhotonFlatTableProducer",
+ src = cms.InputTag("slimmedPhotons"),
+ cut = cms.string(""),
+ name = cms.string("Photon"),
+ doc = cms.string("Photons from scouting"),
+ singleton = cms.bool(False),
+ extension = cms.bool(False),
+ variables = cms.PSet(
+ pt = Var("pt", float, doc="pt", precision=10),
+ eta = Var("eta", float, doc="eta", precision=12),
+ phi = Var("phi", float, doc="phi", precision=12),
+ mass = Var("mass", float, doc="mass", precision=10),
+ # Shower shape - use userFloats from our producer
+ sieie = Var("userFloat('sigmaIetaIeta')", float, doc="sigma_IetaIeta", precision=10),
+ hoe = Var("userFloat('hOverE')", float, doc="H/E", precision=8),
+ # Isolation from userFloats
+ ecalIso = Var("userFloat('ecalIso')", float, doc="ECAL isolation", precision=6),
+ hcalIso = Var("userFloat('hcalIso')", float, doc="HCAL isolation", precision=6),
+ )
+)
+
+# Jet table
+scoutingJetTable = cms.EDProducer("SimplePATJetFlatTableProducer",
+ src = cms.InputTag("slimmedJets"),
+ cut = cms.string(""),
+ name = cms.string("Jet"),
+ doc = cms.string("Jets from scouting (AK4 PF jets)"),
+ singleton = cms.bool(False),
+ extension = cms.bool(False),
+ variables = cms.PSet(
+ pt = Var("pt", float, doc="pt", precision=10),
+ eta = Var("eta", float, doc="eta", precision=12),
+ phi = Var("phi", float, doc="phi", precision=12),
+ mass = Var("mass", float, doc="mass", precision=10),
+ area = Var("jetArea", float, doc="jet area", precision=6),
+ # Energy fractions
+ chHEF = Var("chargedHadronEnergyFraction", float, doc="charged hadron energy fraction", precision=6),
+ neHEF = Var("neutralHadronEnergyFraction", float, doc="neutral hadron energy fraction", precision=6),
+ chEmEF = Var("chargedEmEnergyFraction", float, doc="charged EM energy fraction", precision=6),
+ neEmEF = Var("neutralEmEnergyFraction", float, doc="neutral EM energy fraction", precision=6),
+ muEF = Var("muonEnergyFraction", float, doc="muon energy fraction", precision=6),
+ # Multiplicities
+ nConstituents = Var("numberOfDaughters", int, doc="number of constituents"),
+ chMultiplicity = Var("chargedMultiplicity", int, doc="charged multiplicity"),
+ neMultiplicity = Var("neutralMultiplicity", int, doc="neutral multiplicity"),
+ # B-tagging (from scouting)
+ btagCSV = Var("bDiscriminator('pfCombinedSecondaryVertexV2BJetTags')", float, doc="CSV b-tag discriminator", precision=10),
+ btagDeepB = Var("bDiscriminator('pfDeepCSVJetTags:probb')", float, doc="DeepCSV b-tag discriminator", precision=10),
+ )
+)
+
+# MET table
+scoutingMETTable = cms.EDProducer("SimplePATMETFlatTableProducer",
+ src = cms.InputTag("slimmedMETs"),
+ name = cms.string("MET"),
+ doc = cms.string("MET from scouting PF candidates"),
+ singleton = cms.bool(True),
+ extension = cms.bool(False),
+ variables = cms.PSet(
+ pt = Var("pt", float, doc="MET pt", precision=10),
+ phi = Var("phi", float, doc="MET phi", precision=10),
+ sumEt = Var("sumEt", float, doc="scalar sum of Et", precision=10),
+ )
+)
+
+# Primary vertex table
+scoutingPVTable = cms.EDProducer("SimpleVertexFlatTableProducer",
+ src = cms.InputTag("offlineSlimmedPrimaryVertices"),
+ cut = cms.string(""),
+ name = cms.string("PV"),
+ doc = cms.string("Primary vertices from scouting"),
+ singleton = cms.bool(False),
+ extension = cms.bool(False),
+ variables = cms.PSet(
+ x = Var("x", float, doc="x position", precision=10),
+ y = Var("y", float, doc="y position", precision=10),
+ z = Var("z", float, doc="z position", precision=10),
+ ndof = Var("ndof", float, doc="number of degrees of freedom", precision=6),
+ chi2 = Var("chi2", float, doc="chi2", precision=6),
+ # Error
+ xErr = Var("xError", float, doc="x position error", precision=10),
+ yErr = Var("yError", float, doc="y position error", precision=10),
+ zErr = Var("zError", float, doc="z position error", precision=10),
+ )
+)
+
+# L1 trigger bits conversion (GlobalAlgBlk -> TriggerResults)
+# This converts the gtStage2Digis output to a format NanoAOD can use
+l1bits = cms.EDProducer("L1TriggerResultsConverter",
+ src = cms.InputTag("gtStage2Digis"),
+ legacyL1 = cms.bool(False),
+ storeUnprefireableBits = cms.bool(True),
+)
+
+# Event-level variables
+scoutingEventTable = cms.EDProducer("GlobalVariablesTableProducer",
+ name = cms.string(""),
+ variables = cms.PSet(
+ fixedGridRhoFastjetAll = cms.PSet(
+ src = cms.InputTag("fixedGridRhoFastjetAll"),
+ doc = cms.string("rho from HLT scouting"),
+ type = cms.string("double"),
+ precision = cms.int32(8)
+ ),
+ )
+)
+
+# Dimuon displaced vertex table
+scoutingDimuonVtxTable = cms.EDProducer("SimpleSecondaryVertexFlatTableProducer",
+ src = cms.InputTag("scoutingDimuonVertices"),
+ cut = cms.string(""),
+ name = cms.string("DimuonVtx"),
+ doc = cms.string("Displaced dimuon vertices from scouting"),
+ singleton = cms.bool(False),
+ extension = cms.bool(False),
+ variables = cms.PSet(
+ pt = Var("pt", float, doc="pt", precision=10),
+ eta = Var("eta", float, doc="eta", precision=12),
+ phi = Var("phi", float, doc="phi", precision=12),
+ mass = Var("mass", float, doc="dimuon invariant mass", precision=10),
+ x = Var("position().x()", float, doc="vertex X position, in cm", precision=10),
+ y = Var("position().y()", float, doc="vertex Y position, in cm", precision=10),
+ z = Var("position().z()", float, doc="vertex Z position, in cm", precision=14),
+ ndof = Var("vertexNdof()", float, doc="number of degrees of freedom", precision=8),
+ chi2 = Var("vertexNormalizedChi2()", float, doc="reduced chi2, i.e. chi/ndof", precision=8),
+ nMuons = Var("numberOfDaughters()", "uint8", doc="number of daughter muons"),
+ mu1Idx = Var("?numberOfDaughters()>0?daughterPtr(0).key():-1", "int16", doc="index of first muon in Muon collection"),
+ mu2Idx = Var("?numberOfDaughters()>1?daughterPtr(1).key():-1", "int16", doc="index of second muon in Muon collection"),
+ ),
+)
+
+# Dimuon displaced vertex table for NoVtx muons (2024+ only)
+scoutingDimuonVtxNoVtxTable = cms.EDProducer("SimpleSecondaryVertexFlatTableProducer",
+ src = cms.InputTag("scoutingDimuonVerticesNoVtx"),
+ cut = cms.string(""),
+ name = cms.string("DimuonVtxNoVtx"),
+ doc = cms.string("Displaced dimuon vertices from scouting (NoVtx muons)"),
+ singleton = cms.bool(False),
+ extension = cms.bool(False),
+ variables = cms.PSet(
+ pt = Var("pt", float, doc="pt", precision=10),
+ eta = Var("eta", float, doc="eta", precision=12),
+ phi = Var("phi", float, doc="phi", precision=12),
+ mass = Var("mass", float, doc="dimuon invariant mass", precision=10),
+ x = Var("position().x()", float, doc="vertex X position, in cm", precision=10),
+ y = Var("position().y()", float, doc="vertex Y position, in cm", precision=10),
+ z = Var("position().z()", float, doc="vertex Z position, in cm", precision=14),
+ ndof = Var("vertexNdof()", float, doc="number of degrees of freedom", precision=8),
+ chi2 = Var("vertexNormalizedChi2()", float, doc="reduced chi2, i.e. chi/ndof", precision=8),
+ nMuons = Var("numberOfDaughters()", "uint8", doc="number of daughter muons"),
+ mu1Idx = Var("?numberOfDaughters()>0?daughterPtr(0).key():-1", "int16", doc="index of first muon in MuonNoVtx collection"),
+ mu2Idx = Var("?numberOfDaughters()>1?daughterPtr(1).key():-1", "int16", doc="index of second muon in MuonNoVtx collection"),
+ ),
+)
+
+# Scouting NanoAOD task - core tables
+scoutingNanoAODTask = cms.Task(
+ scoutingMuonTable,
+ scoutingElectronTable,
+ scoutingPhotonTable,
+ scoutingJetTable,
+ scoutingMETTable,
+ scoutingPVTable,
+ scoutingEventTable,
+ scoutingDimuonVtxTable,
+ l1bits,
+)
+
+# For 2024+, add NoVtx muon table and its dimuon vertex table
+_scoutingNanoAODTask_2024 = scoutingNanoAODTask.copy()
+_scoutingNanoAODTask_2024.add(scoutingMuonNoVtxTable)
+_scoutingNanoAODTask_2024.add(scoutingDimuonVtxNoVtxTable)
+run3_scouting_2024.toReplaceWith(scoutingNanoAODTask, _scoutingNanoAODTask_2024)
+
+scoutingNanoAODSequence = cms.Sequence(scoutingNanoAODTask)
+
+
+# ============================================================
+# Customization function for cmsDriver
+# ============================================================
+
+def customiseScoutingNanoAOD(process):
+ """
+ Customization function to add scouting NanoAOD production.
+
+ Usage with cmsDriver (two-step workflow):
+
+ Step 1 - Scouting RAW to MiniAOD:
+ cmsRun test_scoutingToMiniAOD_cfg.py
+
+ Step 2 - MiniAOD to NanoAOD:
+ cmsDriver.py NANO --conditions auto:run3_data_prompt \\
+ --era Run3 --step NANO --datatier NANOAOD \\
+ --filein file:scoutingToMiniAOD_test.root \\
+ --fileout file:scoutingNanoAOD.root \\
+ --customise PhysicsTools/PatFromScouting/scoutingNanoAOD_cff.customiseScoutingNanoAOD \\
+ -n 100
+
+ Or for standalone usage:
+ from PhysicsTools.PatFromScouting.scoutingNanoAOD_cff import customiseScoutingNanoAOD
+ process = customiseScoutingNanoAOD(process)
+ """
+
+ # When called via cmsDriver @ScoutMini, the task is already loaded
+ # and scheduled. Only add modules if they are not yet in the process.
+ if not hasattr(process, 'scoutingNanoAODTask'):
+ process.load('PhysicsTools.PatFromScouting.scoutingNanoAOD_cff')
+
+ process.scoutingNanoAOD_step = cms.Path()
+ process.scoutingNanoAOD_step.associate(process.scoutingNanoAODTask)
+
+ if hasattr(process, 'schedule') and process.schedule is not None:
+ process.schedule.insert(0, process.scoutingNanoAOD_step)
+
+ # Configure output
+ if hasattr(process, 'NANOAODoutput'):
+ process.NANOAODoutput.outputCommands = cms.untracked.vstring(
+ 'drop *',
+ 'keep nanoaodFlatTable_*_*_*',
+ 'keep edmTriggerResults_*_*_*',
+ 'keep nanoaodMergeableCounterTable_*_*_*',
+ 'keep nanoaodUniqueString_*_*_*',
+ )
+
+ return process
diff --git a/PhysicsTools/PatFromScouting/python/scoutingToMiniAOD_cff.py b/PhysicsTools/PatFromScouting/python/scoutingToMiniAOD_cff.py
new file mode 100644
index 0000000000000..40f7e6923e080
--- /dev/null
+++ b/PhysicsTools/PatFromScouting/python/scoutingToMiniAOD_cff.py
@@ -0,0 +1,215 @@
+import FWCore.ParameterSet.Config as cms
+from Configuration.Eras.Modifier_run3_scouting_2024_cff import run3_scouting_2024
+
+# Use standard MiniAOD collection names for NanoAOD compatibility
+
+# Vertices - standard MiniAOD name (must be produced before PF candidates)
+offlineSlimmedPrimaryVertices = cms.EDProducer("Run3ScoutingVertexToRecoVertexProducer",
+ src=cms.InputTag("hltScoutingPrimaryVertexPacker", "primaryVtx")
+)
+
+# PF Candidates - using pat::PackedCandidate for MiniAOD compatibility
+# Requires vertices to be produced first for proper vertex references
+packedPFCandidates = cms.EDProducer("Run3ScoutingParticleToPackedCandidateProducer",
+ src=cms.InputTag("hltScoutingPFPacker"),
+ vertices=cms.InputTag("offlineSlimmedPrimaryVertices"),
+ tracks=cms.InputTag("scoutingTracks"),
+ CHS=cms.bool(False),
+ covarianceVersion=cms.int32(1),
+ covarianceSchema=cms.int32(520)
+)
+
+
+# Muons - standard MiniAOD name
+# Note: From 2024, there are two muon collections (with/without vertex)
+# Default uses hltScoutingMuonPacker, 2024+ uses hltScoutingMuonPackerVtx
+slimmedMuons = cms.EDProducer("PatFromScoutingMuonProducer",
+ src=cms.InputTag("hltScoutingMuonPacker")
+)
+# For 2024 data, use hltScoutingMuonPackerVtx which includes vertex information
+run3_scouting_2024.toModify(slimmedMuons, src="hltScoutingMuonPackerVtx")
+
+# Displaced muons (no vertex constraint) - only available from 2024+
+# Before 2024, there was only hltScoutingMuonPacker (no Vtx/NoVtx split)
+slimmedMuonsNoVtx = cms.EDProducer("PatFromScoutingMuonProducer",
+ src=cms.InputTag("hltScoutingMuonPackerNoVtx")
+)
+
+# Dimuon displaced vertices as VertexCompositePtrCandidate (like slimmedSecondaryVertices)
+# with CandidatePtr daughters pointing into the corresponding muon collection.
+# The muon-vertex link is derived from Run3ScoutingMuon::vtxIndx().
+# Default (pre-2024): single muon collection → single dimuon vertex collection
+scoutingDimuonVertices = cms.EDProducer("ScoutingDimuonVtxProducer",
+ scoutingMuons=cms.InputTag("hltScoutingMuonPacker"),
+ scoutingVertices=cms.InputTag("hltScoutingMuonPacker", "displacedVtx"),
+ patMuons=cms.InputTag("slimmedMuons")
+)
+# For 2024+, Vtx muons have their own displaced vertices
+run3_scouting_2024.toModify(scoutingDimuonVertices,
+ scoutingMuons="hltScoutingMuonPackerVtx",
+ scoutingVertices=cms.InputTag("hltScoutingMuonPackerVtx", "displacedVtx")
+)
+
+# Dimuon vertices for NoVtx muons - only available from 2024+
+scoutingDimuonVerticesNoVtx = cms.EDProducer("ScoutingDimuonVtxProducer",
+ scoutingMuons=cms.InputTag("hltScoutingMuonPackerNoVtx"),
+ scoutingVertices=cms.InputTag("hltScoutingMuonPackerNoVtx", "displacedVtx"),
+ patMuons=cms.InputTag("slimmedMuonsNoVtx")
+)
+
+# Electrons - standard MiniAOD name
+slimmedElectrons = cms.EDProducer("PatFromScoutingElectronProducer",
+ src=cms.InputTag("hltScoutingEgammaPacker")
+)
+
+# Photons - standard MiniAOD name
+slimmedPhotons = cms.EDProducer("PatFromScoutingPhotonProducer",
+ src=cms.InputTag("hltScoutingEgammaPacker")
+)
+
+# Jets - standard MiniAOD name
+slimmedJets = cms.EDProducer("PatFromScoutingJetProducer",
+ src=cms.InputTag("hltScoutingPFPacker"),
+ pfCandidates=cms.InputTag("packedPFCandidates")
+)
+
+# MET - standard MiniAOD name
+# Uses precomputed MET from scouting data
+slimmedMETs = cms.EDProducer("Run3ScoutingMETProducer",
+ metPt=cms.InputTag("hltScoutingPFPacker", "pfMetPt"),
+ metPhi=cms.InputTag("hltScoutingPFPacker", "pfMetPhi")
+)
+
+# Tracks
+scoutingTracks = cms.EDProducer("Run3ScoutingTrackToRecoTrackProducer",
+ src=cms.InputTag("hltScoutingTrackPacker")
+)
+
+# Beam spot from conditions database
+offlineBeamSpot = cms.EDProducer("BeamSpotProducer")
+
+# Rho (pileup density) - copy from HLT scouting data
+fixedGridRhoFastjetAll = cms.EDProducer("ScoutingRhoProducer",
+ src=cms.InputTag("hltScoutingPFPacker", "rho")
+)
+
+# L1 trigger unpacking from raw FED data
+# Scouting stores L1 raw data in hltFEDSelectorL1
+gtStage2Digis = cms.EDProducer("L1TRawToDigi",
+ InputLabel = cms.InputTag("hltFEDSelectorL1"),
+ Setup = cms.string("stage2::GTSetup"),
+ FedIds = cms.vint32(1404),
+)
+
+# L1 objects with standard module names for downstream compatibility
+# Standard MiniAOD has L1 muons from gmtStage2Digis and L1 calo objects from caloStage2Digis
+# These producers copy from gtStage2Digis to provide the expected module labels
+gmtStage2Digis = cms.EDProducer("Run3ScoutingL1MuonProducer",
+ muonSource = cms.InputTag("gtStage2Digis", "Muon")
+)
+
+caloStage2Digis = cms.EDProducer("Run3ScoutingL1CaloProducer",
+ jetSource = cms.InputTag("gtStage2Digis", "Jet"),
+ egammaSource = cms.InputTag("gtStage2Digis", "EGamma"),
+ tauSource = cms.InputTag("gtStage2Digis", "Tau"),
+ etsumSource = cms.InputTag("gtStage2Digis", "EtSum")
+)
+
+# Task containing all producers
+scoutingToMiniAODTask = cms.Task(
+ packedPFCandidates,
+ offlineSlimmedPrimaryVertices,
+ offlineBeamSpot,
+ slimmedMuons,
+ scoutingDimuonVertices,
+ slimmedElectrons,
+ slimmedPhotons,
+ slimmedJets,
+ slimmedMETs,
+ scoutingTracks,
+ fixedGridRhoFastjetAll,
+ gtStage2Digis,
+ gmtStage2Digis,
+ caloStage2Digis
+)
+
+# For 2024+, add NoVtx muon collection and its dimuon vertices
+_scoutingToMiniAODTask_2024 = scoutingToMiniAODTask.copy()
+_scoutingToMiniAODTask_2024.add(slimmedMuonsNoVtx)
+_scoutingToMiniAODTask_2024.add(scoutingDimuonVerticesNoVtx)
+run3_scouting_2024.toReplaceWith(scoutingToMiniAODTask, _scoutingToMiniAODTask_2024)
+
+# Sequence for backward compatibility
+scoutingToMiniAODSequence = cms.Sequence(scoutingToMiniAODTask)
+
+
+# ============================================================
+# Customization function for cmsDriver
+# ============================================================
+
+def customiseScoutingToMiniAOD(process):
+ """
+ Minimal customization for scouting to MiniAOD conversion.
+
+ Usage with cmsDriver (for 2024 data):
+
+ cmsDriver.py scoutMini \\
+ --scenario pp \\
+ --conditions auto:run3_data_prompt \\
+ --era Run3_2024 \\
+ --eventcontent MINIAOD \\
+ --datatier MINIAOD \\
+ --step USER:PhysicsTools/PatFromScouting/scoutingToMiniAOD_cff.scoutingToMiniAODTask \\
+ --filein file:scouting.root \\
+ --fileout file:scoutingMiniAOD.root \\
+ --customise PhysicsTools/PatFromScouting/scoutingToMiniAOD_cff.customiseScoutingToMiniAOD \\
+ --data --no_exec -n 100
+
+ For 2025 data, use --era Run3_2025
+
+ The customization only:
+ - Loads particle data table (needed for PackedCandidate producer)
+ - Extends output to include scoutingTracks and L1 collections
+
+ The --step USER:...Task handles adding the producers.
+ The --eventcontent MINIAOD provides standard MiniAOD output commands.
+ """
+
+ # Load particle data table (needed for PackedCandidate producer)
+ process.load("SimGeneral.HepPDTESSource.pdt_cfi")
+
+ # Handle missing collections gracefully (not all scouting triggers save all objects)
+ # This allows processing datasets where some events don't have egamma, etc.
+ if hasattr(process, 'options'):
+ if not hasattr(process.options, 'TryToContinue'):
+ process.options.TryToContinue = cms.untracked.vstring()
+ process.options.TryToContinue.append('ProductNotFound')
+ else:
+ process.options = cms.untracked.PSet(
+ TryToContinue = cms.untracked.vstring('ProductNotFound')
+ )
+
+ # Extend output commands to include scouting-specific collections
+ # Standard MINIAOD eventcontent already keeps slimmedMuons, slimmedJets, etc.
+ # We add scoutingTracks, L1 collections, rho, and drop raw scouting collections
+ for name in ['MINIAODoutput', 'MINIAODSIMoutput', 'output', 'out']:
+ if hasattr(process, name):
+ outputModule = getattr(process, name)
+ outputModule.outputCommands.extend([
+ # Keep scouting-specific collections
+ 'keep patMuons_slimmedMuonsNoVtx_*_*',
+ 'keep *_scoutingDimuonVertices_*_*',
+ 'keep *_scoutingDimuonVerticesNoVtx_*_*',
+ 'keep recoTracks_scoutingTracks__*',
+ 'keep *_scoutingTracks_vertexIndex_*',
+ 'keep *_gtStage2Digis_*_*',
+ 'keep *_gmtStage2Digis_*_*',
+ 'keep *_caloStage2Digis_*_*',
+ 'keep *_fixedGridRhoFastjetAll_*_*',
+ # Drop raw scouting collections to save space
+ 'drop *_hltScouting*_*_*',
+ 'drop *_hltFEDSelectorL1_*_*',
+ ])
+ break
+
+ return process
diff --git a/PhysicsTools/PatFromScouting/test/BuildFile.xml b/PhysicsTools/PatFromScouting/test/BuildFile.xml
new file mode 100644
index 0000000000000..cecad47014a29
--- /dev/null
+++ b/PhysicsTools/PatFromScouting/test/BuildFile.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/PhysicsTools/PatFromScouting/test/test_catch2_PatFromScouting.cc b/PhysicsTools/PatFromScouting/test/test_catch2_PatFromScouting.cc
new file mode 100644
index 0000000000000..4106e2c39dd0b
--- /dev/null
+++ b/PhysicsTools/PatFromScouting/test/test_catch2_PatFromScouting.cc
@@ -0,0 +1,130 @@
+#include "catch2/catch_all.hpp"
+#include "FWCore/TestProcessor/interface/TestProcessor.h"
+#include "FWCore/Utilities/interface/Exception.h"
+
+static constexpr auto s_tag = "[PatFromScouting]";
+
+TEST_CASE("Standard checks of PatFromScoutingMuonProducer", s_tag) {
+ const std::string baseConfig{
+ R"_(from FWCore.TestProcessor.TestProcess import *
+process = TestProcess()
+process.toTest = cms.EDProducer("PatFromScoutingMuonProducer",
+ src = cms.InputTag("hltScoutingMuonPacker")
+)
+process.moduleToTest(process.toTest)
+)_"};
+
+ edm::test::TestProcessor::Config config{baseConfig};
+ SECTION("base configuration is OK") { REQUIRE_NOTHROW(edm::test::TestProcessor(config)); }
+
+ SECTION("beginJob and endJob only") {
+ edm::test::TestProcessor tester(config);
+ REQUIRE_NOTHROW(tester.testBeginAndEndJobOnly());
+ }
+
+ SECTION("Run with no LuminosityBlocks") {
+ edm::test::TestProcessor tester(config);
+ REQUIRE_NOTHROW(tester.testRunWithNoLuminosityBlocks());
+ }
+
+ SECTION("LuminosityBlock with no Events") {
+ edm::test::TestProcessor tester(config);
+ REQUIRE_NOTHROW(tester.testLuminosityBlockWithNoEvents());
+ }
+}
+
+TEST_CASE("Standard checks of PatFromScoutingElectronProducer", s_tag) {
+ const std::string baseConfig{
+ R"_(from FWCore.TestProcessor.TestProcess import *
+process = TestProcess()
+process.toTest = cms.EDProducer("PatFromScoutingElectronProducer",
+ src = cms.InputTag("hltScoutingEgammaPacker")
+)
+process.moduleToTest(process.toTest)
+)_"};
+
+ edm::test::TestProcessor::Config config{baseConfig};
+ SECTION("base configuration is OK") { REQUIRE_NOTHROW(edm::test::TestProcessor(config)); }
+
+ SECTION("beginJob and endJob only") {
+ edm::test::TestProcessor tester(config);
+ REQUIRE_NOTHROW(tester.testBeginAndEndJobOnly());
+ }
+}
+
+TEST_CASE("Standard checks of PatFromScoutingPhotonProducer", s_tag) {
+ const std::string baseConfig{
+ R"_(from FWCore.TestProcessor.TestProcess import *
+process = TestProcess()
+process.toTest = cms.EDProducer("PatFromScoutingPhotonProducer",
+ src = cms.InputTag("hltScoutingEgammaPacker")
+)
+process.moduleToTest(process.toTest)
+)_"};
+
+ edm::test::TestProcessor::Config config{baseConfig};
+ SECTION("base configuration is OK") { REQUIRE_NOTHROW(edm::test::TestProcessor(config)); }
+
+ SECTION("beginJob and endJob only") {
+ edm::test::TestProcessor tester(config);
+ REQUIRE_NOTHROW(tester.testBeginAndEndJobOnly());
+ }
+}
+
+TEST_CASE("Standard checks of PatFromScoutingJetProducer", s_tag) {
+ const std::string baseConfig{
+ R"_(from FWCore.TestProcessor.TestProcess import *
+process = TestProcess()
+process.toTest = cms.EDProducer("PatFromScoutingJetProducer",
+ src = cms.InputTag("hltScoutingPFPacker")
+)
+process.moduleToTest(process.toTest)
+)_"};
+
+ edm::test::TestProcessor::Config config{baseConfig};
+ SECTION("base configuration is OK") { REQUIRE_NOTHROW(edm::test::TestProcessor(config)); }
+
+ SECTION("beginJob and endJob only") {
+ edm::test::TestProcessor tester(config);
+ REQUIRE_NOTHROW(tester.testBeginAndEndJobOnly());
+ }
+}
+
+TEST_CASE("Standard checks of Run3ScoutingVertexToRecoVertexProducer", s_tag) {
+ const std::string baseConfig{
+ R"_(from FWCore.TestProcessor.TestProcess import *
+process = TestProcess()
+process.toTest = cms.EDProducer("Run3ScoutingVertexToRecoVertexProducer",
+ src = cms.InputTag("hltScoutingPrimaryVertexPacker", "primaryVtx")
+)
+process.moduleToTest(process.toTest)
+)_"};
+
+ edm::test::TestProcessor::Config config{baseConfig};
+ SECTION("base configuration is OK") { REQUIRE_NOTHROW(edm::test::TestProcessor(config)); }
+
+ SECTION("beginJob and endJob only") {
+ edm::test::TestProcessor tester(config);
+ REQUIRE_NOTHROW(tester.testBeginAndEndJobOnly());
+ }
+}
+
+TEST_CASE("Standard checks of Run3ScoutingMETProducer", s_tag) {
+ const std::string baseConfig{
+ R"_(from FWCore.TestProcessor.TestProcess import *
+process = TestProcess()
+process.toTest = cms.EDProducer("Run3ScoutingMETProducer",
+ metPt = cms.InputTag("hltScoutingPFPacker", "pfMetPt"),
+ metPhi = cms.InputTag("hltScoutingPFPacker", "pfMetPhi")
+)
+process.moduleToTest(process.toTest)
+)_"};
+
+ edm::test::TestProcessor::Config config{baseConfig};
+ SECTION("base configuration is OK") { REQUIRE_NOTHROW(edm::test::TestProcessor(config)); }
+
+ SECTION("beginJob and endJob only") {
+ edm::test::TestProcessor tester(config);
+ REQUIRE_NOTHROW(tester.testBeginAndEndJobOnly());
+ }
+}
diff --git a/PhysicsTools/PatFromScouting/test/test_catch2_main.cc b/PhysicsTools/PatFromScouting/test/test_catch2_main.cc
new file mode 100644
index 0000000000000..b3ea47c29c7a7
--- /dev/null
+++ b/PhysicsTools/PatFromScouting/test/test_catch2_main.cc
@@ -0,0 +1,2 @@
+#define CATCH_CONFIG_MAIN
+#include "catch2/catch_all.hpp"
diff --git a/PhysicsTools/PatFromScouting/test/test_scoutingNanoAOD_cfg.py b/PhysicsTools/PatFromScouting/test/test_scoutingNanoAOD_cfg.py
new file mode 100644
index 0000000000000..1bfcbe5603958
--- /dev/null
+++ b/PhysicsTools/PatFromScouting/test/test_scoutingNanoAOD_cfg.py
@@ -0,0 +1,80 @@
+"""
+Scouting NanoAOD production test.
+
+This produces NanoAOD from scouting MiniAOD using the custom scouting tables.
+
+Prerequisites:
+ First run test_scoutingToMiniAOD_cfg.py to create scoutingToMiniAOD_test.root
+
+Usage:
+ cmsRun test_scoutingNanoAOD_cfg.py
+
+Output:
+ scoutingNanoAOD_test.root - NanoAOD format with physics objects and triggers
+"""
+
+import FWCore.ParameterSet.Config as cms
+
+process = cms.Process("NANO")
+
+process.load("FWCore.MessageService.MessageLogger_cfi")
+process.MessageLogger.cerr.FwkReport.reportEvery = 10
+
+# Load conditions for L1 trigger menu
+process.load('Configuration.StandardSequences.FrontierConditions_GlobalTag_cff')
+from Configuration.AlCa.GlobalTag import GlobalTag
+process.GlobalTag = GlobalTag(process.GlobalTag, 'auto:run3_data_prompt', '')
+
+process.maxEvents = cms.untracked.PSet(input = cms.untracked.int32(100))
+
+process.source = cms.Source("PoolSource",
+ fileNames = cms.untracked.vstring(
+ 'file:scoutingToMiniAOD_test.root'
+ )
+)
+
+# Load the scouting NanoAOD configuration
+from PhysicsTools.PatFromScouting.scoutingNanoAOD_cff import (
+ scoutingMuonTable,
+ scoutingElectronTable,
+ scoutingPhotonTable,
+ scoutingJetTable,
+ scoutingMETTable,
+ scoutingPVTable,
+ scoutingEventTable,
+ l1bits,
+)
+
+process.scoutingMuonTable = scoutingMuonTable.clone()
+process.scoutingElectronTable = scoutingElectronTable.clone()
+process.scoutingPhotonTable = scoutingPhotonTable.clone()
+process.scoutingJetTable = scoutingJetTable.clone()
+process.scoutingMETTable = scoutingMETTable.clone()
+process.scoutingPVTable = scoutingPVTable.clone()
+process.scoutingEventTable = scoutingEventTable.clone()
+process.l1bits = l1bits.clone()
+
+process.p = cms.Path(
+ process.scoutingMuonTable +
+ process.scoutingElectronTable +
+ process.scoutingPhotonTable +
+ process.scoutingJetTable +
+ process.scoutingMETTable +
+ process.scoutingPVTable +
+ process.scoutingEventTable +
+ process.l1bits
+)
+
+# NanoAOD output
+process.out = cms.OutputModule("NanoAODOutputModule",
+ fileName = cms.untracked.string('scoutingNanoAOD_test.root'),
+ outputCommands = cms.untracked.vstring(
+ 'drop *',
+ 'keep nanoaodFlatTable_*_*_*',
+ 'keep edmTriggerResults_*_*_*',
+ ),
+ compressionAlgorithm = cms.untracked.string('LZMA'),
+ compressionLevel = cms.untracked.int32(9),
+)
+
+process.e = cms.EndPath(process.out)
diff --git a/PhysicsTools/PatFromScouting/test/test_scoutingToMiniAOD_cfg.py b/PhysicsTools/PatFromScouting/test/test_scoutingToMiniAOD_cfg.py
new file mode 100644
index 0000000000000..d75665093e99d
--- /dev/null
+++ b/PhysicsTools/PatFromScouting/test/test_scoutingToMiniAOD_cfg.py
@@ -0,0 +1,98 @@
+import FWCore.ParameterSet.Config as cms
+from FWCore.ParameterSet.VarParsing import VarParsing
+
+options = VarParsing('analysis')
+options.inputFiles = ['file:/data/dmytro/data/store+data+Run2024C+ScoutingPFRun3+HLTSCOUT+v1+000+380+197+00000+05943107-3d39-4fcd-bea9-2b71ca2c3890.root']
+options.maxEvents = 100
+options.parseArguments()
+
+process = cms.Process("TEST")
+
+process.load("FWCore.MessageService.MessageLogger_cfi")
+process.MessageLogger.cerr.FwkReport.reportEvery = 10
+
+# Load particle data table (needed for PF candidate producer)
+process.load("SimGeneral.HepPDTESSource.pdt_cfi")
+
+# Load conditions for beam spot
+process.load('Configuration.StandardSequences.FrontierConditions_GlobalTag_cff')
+from Configuration.AlCa.GlobalTag import GlobalTag
+process.GlobalTag = GlobalTag(process.GlobalTag, 'auto:run3_data_prompt', '')
+
+process.maxEvents = cms.untracked.PSet(input = cms.untracked.int32(options.maxEvents))
+
+process.source = cms.Source("PoolSource",
+ fileNames = cms.untracked.vstring(options.inputFiles)
+)
+
+# Load the scouting to MiniAOD configuration
+from PhysicsTools.PatFromScouting.scoutingToMiniAOD_cff import *
+
+# Use standard MiniAOD collection names
+process.packedPFCandidates = packedPFCandidates
+process.offlineSlimmedPrimaryVertices = offlineSlimmedPrimaryVertices
+process.slimmedMuons = slimmedMuons.clone(
+ src = cms.InputTag("hltScoutingMuonPackerVtx")
+)
+process.slimmedMuonsNoVtx = slimmedMuonsNoVtx
+process.scoutingDimuonVertices = scoutingDimuonVertices.clone(
+ scoutingMuons = cms.InputTag("hltScoutingMuonPackerVtx"),
+ scoutingVertices = cms.InputTag("hltScoutingMuonPackerVtx", "displacedVtx"),
+)
+process.scoutingDimuonVerticesNoVtx = scoutingDimuonVerticesNoVtx
+process.slimmedElectrons = slimmedElectrons
+process.slimmedPhotons = slimmedPhotons
+process.slimmedJets = slimmedJets
+process.slimmedMETs = slimmedMETs
+process.scoutingTracks = scoutingTracks
+process.offlineBeamSpot = offlineBeamSpot
+process.fixedGridRhoFastjetAll = fixedGridRhoFastjetAll
+process.gtStage2Digis = gtStage2Digis
+process.gmtStage2Digis = gmtStage2Digis
+process.caloStage2Digis = caloStage2Digis
+
+process.p = cms.Path(
+ process.offlineSlimmedPrimaryVertices +
+ process.scoutingTracks +
+ process.packedPFCandidates +
+ process.offlineBeamSpot +
+ process.slimmedMuons +
+ process.slimmedMuonsNoVtx +
+ process.scoutingDimuonVertices +
+ process.scoutingDimuonVerticesNoVtx +
+ process.slimmedElectrons +
+ process.slimmedPhotons +
+ process.slimmedJets +
+ process.slimmedMETs +
+ process.fixedGridRhoFastjetAll +
+ process.gtStage2Digis +
+ process.gmtStage2Digis +
+ process.caloStage2Digis
+)
+
+# Output
+process.out = cms.OutputModule("PoolOutputModule",
+ fileName = cms.untracked.string('scoutingToMiniAOD_test.root'),
+ outputCommands = cms.untracked.vstring(
+ 'drop *',
+ 'keep *_packedPFCandidates_*_*',
+ 'keep *_offlineSlimmedPrimaryVertices_*_*',
+ 'keep *_slimmedMuons_*_*',
+ 'keep *_slimmedMuonsNoVtx_*_*',
+ 'keep *_scoutingDimuonVertices_*_*',
+ 'keep *_scoutingDimuonVerticesNoVtx_*_*',
+ 'keep *_slimmedElectrons_*_*',
+ 'keep *_slimmedPhotons_*_*',
+ 'keep *_slimmedJets_*_*',
+ 'keep *_slimmedMETs_*_*',
+ 'keep *_scoutingTracks_*_*',
+ 'keep *_TriggerResults_*_*',
+ 'keep *_offlineBeamSpot_*_*',
+ 'keep *_fixedGridRho*_*_*',
+ 'keep *_gtStage2Digis_*_*',
+ 'keep *_gmtStage2Digis_*_*',
+ 'keep *_caloStage2Digis_*_*',
+ )
+)
+
+process.e = cms.EndPath(process.out)
diff --git a/PhysicsTools/PatFromScouting/test/test_standardNanoAOD_cfg.py b/PhysicsTools/PatFromScouting/test/test_standardNanoAOD_cfg.py
new file mode 100644
index 0000000000000..47068657174e5
--- /dev/null
+++ b/PhysicsTools/PatFromScouting/test/test_standardNanoAOD_cfg.py
@@ -0,0 +1,122 @@
+"""
+Test standard NanoAOD production on scouting MiniAOD.
+
+This is the recommended production workflow:
+1. First run test_scoutingToMiniAOD_cfg.py to create scoutingToMiniAOD_test.root
+2. Then run this config to produce standard NanoAOD
+
+This uses cmsDriver-style configuration with our customizations.
+"""
+
+import FWCore.ParameterSet.Config as cms
+
+from Configuration.Eras.Era_Run3_cff import Run3
+from Configuration.Eras.Modifier_run3_nanoAOD_pre142X_cff import run3_nanoAOD_pre142X
+
+process = cms.Process('NANO', Run3, run3_nanoAOD_pre142X)
+
+# ============================================================
+# Standard configurations
+# ============================================================
+
+process.load('Configuration.StandardSequences.Services_cff')
+process.load('SimGeneral.HepPDTESSource.pythiapdt_cfi')
+process.load('FWCore.MessageService.MessageLogger_cfi')
+process.load('Configuration.EventContent.EventContent_cff')
+process.load('Configuration.StandardSequences.GeometryRecoDB_cff')
+process.load('Configuration.StandardSequences.MagneticField_cff')
+process.load('PhysicsTools.NanoAOD.nano_cff')
+process.load('Configuration.StandardSequences.EndOfProcess_cff')
+process.load('Configuration.StandardSequences.FrontierConditions_GlobalTag_cff')
+
+process.MessageLogger.cerr.FwkReport.reportEvery = 10
+
+process.maxEvents = cms.untracked.PSet(
+ input = cms.untracked.int32(100)
+)
+
+# ============================================================
+# Input: Scouting MiniAOD
+# ============================================================
+
+process.source = cms.Source("PoolSource",
+ fileNames = cms.untracked.vstring('file:/work/CMSSW_15_0_18/src/scoutingToMiniAOD_test.root'),
+ secondaryFileNames = cms.untracked.vstring()
+)
+
+process.options = cms.untracked.PSet(
+ TryToContinue = cms.untracked.vstring('ProductNotFound'),
+)
+
+# ============================================================
+# Global Tag
+# ============================================================
+
+from Configuration.AlCa.GlobalTag import GlobalTag
+process.GlobalTag = GlobalTag(process.GlobalTag, 'auto:run3_data_prompt', '')
+
+# ============================================================
+# Output
+# ============================================================
+
+process.NANOAODoutput = cms.OutputModule("NanoAODOutputModule",
+ compressionAlgorithm = cms.untracked.string('LZMA'),
+ compressionLevel = cms.untracked.int32(9),
+ dataset = cms.untracked.PSet(
+ dataTier = cms.untracked.string('NANOAOD'),
+ filterName = cms.untracked.string('')
+ ),
+ fileName = cms.untracked.string('file:standardNanoAOD_test.root'),
+ outputCommands = process.NANOAODEventContent.outputCommands
+)
+
+# ============================================================
+# Paths
+# ============================================================
+
+process.nanoAOD_step = cms.Path(process.nanoSequence)
+process.endjob_step = cms.EndPath(process.endOfProcess)
+process.NANOAODoutput_step = cms.EndPath(process.NANOAODoutput)
+
+process.schedule = cms.Schedule(
+ process.nanoAOD_step,
+ process.endjob_step,
+ process.NANOAODoutput_step
+)
+
+from PhysicsTools.PatAlgos.tools.helpers import associatePatAlgosToolsTask
+associatePatAlgosToolsTask(process)
+
+# ============================================================
+# Apply customizations
+# ============================================================
+
+# Standard NanoAOD customization
+from PhysicsTools.NanoAOD.nano_cff import nanoAOD_customizeCommon
+process = nanoAOD_customizeCommon(process)
+
+# Scouting-specific customizations
+from PhysicsTools.PatFromScouting.nanoAOD_scouting_cff import customiseNanoForScoutingMiniAOD
+process = customiseNanoForScoutingMiniAOD(process)
+
+# ============================================================
+# Additional fixes for scouting MiniAOD
+# ============================================================
+
+# Add additional rho producers that NanoAOD expects
+# (our MiniAOD has fixedGridRhoFastjetAll, but NanoAOD also needs fixedGridRhoFastjetCentral)
+process.fixedGridRhoFastjetCentral = cms.EDProducer("FixedGridRhoProducerFastjet",
+ pfCandidatesTag = cms.InputTag("packedPFCandidates"),
+ maxRapidity = cms.double(2.5), # Central only
+ gridSpacing = cms.double(0.55)
+)
+
+# Add to the beginning of nanoAOD path
+process.nanoAOD_step.insert(0, process.fixedGridRhoFastjetCentral)
+
+print("=" * 60)
+print("Standard NanoAOD on Scouting MiniAOD")
+print("=" * 60)
+print("Input: scoutingToMiniAOD_test.root")
+print("Output: standardNanoAOD_test.root")
+print("=" * 60)