From 9a40556932ba7b7a0a37dc93bcfc47dad20dab52 Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Thu, 5 Mar 2026 10:20:24 +0100 Subject: [PATCH 01/53] Introduce compact truth graph data structure --- PhysicsTools/TruthInfo/interface/TruthGraph.h | 102 ++++++++++++++++++ PhysicsTools/TruthInfo/src/TruthGraph.cc | 24 +++++ 2 files changed, 126 insertions(+) create mode 100644 PhysicsTools/TruthInfo/interface/TruthGraph.h create mode 100644 PhysicsTools/TruthInfo/src/TruthGraph.cc diff --git a/PhysicsTools/TruthInfo/interface/TruthGraph.h b/PhysicsTools/TruthInfo/interface/TruthGraph.h new file mode 100644 index 0000000000000..8946f8cfc7025 --- /dev/null +++ b/PhysicsTools/TruthInfo/interface/TruthGraph.h @@ -0,0 +1,102 @@ +// Author: Felice Pantaleo - CERN +// Date: 03/2026 +// A compact, read-only graph representation of the truth information in an event. +// The graph is built in the TruthGraphProducer module, which also fills the node metadata and associations. +// The graph is intended to be a common data format for various use cases (e.g. validation, analysis, visualization). + + + +#ifndef PhysicsTools_TruthInfo_interface_TruthGraph_h +#define PhysicsTools_TruthInfo_interface_TruthGraph_h + +#include +#include +#include + +class TruthGraph { +public: + enum class NodeKind : uint8_t { + GenEvent = 0, + GenVertex = 1, + GenParticle = 2, + SimVertex = 3, + SimTrack = 4, + }; + + // Edge categories (for visualization / filtering) + enum class EdgeKind : uint8_t { + Gen = 0, // within GEN realm + Sim = 1, // within SIM realm + GenToSim = 2, // realm boundary GEN -> SIM + SimToGen = 3 // reserved (we don't produce these now) + }; + + struct NodeRef { + NodeKind kind = NodeKind::GenParticle; + int64_t key = 0; // GenParticle: index; SimTrack: trackId; SimVertex: index; GenVertex: barcode/index + }; + + TruthGraph() = default; + + // CSR out-edges: offsets.size() == nNodes+1 + // edges.size() == nEdges + // edgeKind.size() == nEdges + std::vector offsets; + std::vector edges; + std::vector edgeKind; // stores TruthGraph::EdgeKind as uint8_t + + // Node metadata: nodes.size() == nNodes + std::vector nodes; + + // Cached payload (optional) + std::vector pdgId; // 0 if not applicable + std::vector status; // 0 if not applicable + + // Packed EncodedEventId for SIM nodes; 0 for GEN nodes + std::vector eventId; + + std::vector genEventOfNode; // -1 for SIM; for GEN nodes = component id + + // Associations (nodeId -> nodeId). Only meaningful for SimTrack nodes. + // -1 means "no association". + std::vector simTrackToGen; // SimTrack nodeId -> GenParticle nodeId + std::vector simTrackToVtx; // SimTrack nodeId -> SimVertex nodeId + + uint32_t nNodes() const { return static_cast(nodes.size()); } + uint32_t nEdges() const { return static_cast(edges.size()); } + + uint32_t edgeBegin(uint32_t nodeId) const { return offsets.at(nodeId); } + uint32_t edgeEnd(uint32_t nodeId) const { return offsets.at(nodeId + 1); } + + std::span children(uint32_t nodeId) const { + const auto b = edgeBegin(nodeId); + const auto e = edgeEnd(nodeId); + return std::span(edges.data() + b, e - b); + } + + std::span childrenEdgeKinds(uint32_t nodeId) const { + const auto b = edgeBegin(nodeId); + const auto e = edgeEnd(nodeId); + return std::span(edgeKind.data() + b, e - b); + } + + const NodeRef& nodeRef(uint32_t nodeId) const { return nodes.at(nodeId); } + + int32_t nodePdgId(uint32_t nodeId) const { return (nodeId < pdgId.size()) ? pdgId[nodeId] : 0; } + + int16_t nodeStatus(uint32_t nodeId) const { return (nodeId < status.size()) ? status[nodeId] : 0; } + + uint64_t nodeEventId(uint32_t nodeId) const { return (nodeId < eventId.size()) ? eventId[nodeId] : 0ull; } + + int32_t nodeSimTrackToGen(uint32_t nodeId) const { + return (nodeId < simTrackToGen.size()) ? simTrackToGen[nodeId] : -1; + } + + int32_t nodeSimTrackToVtx(uint32_t nodeId) const { + return (nodeId < simTrackToVtx.size()) ? simTrackToVtx[nodeId] : -1; + } + + bool isConsistent() const; +}; + +#endif diff --git a/PhysicsTools/TruthInfo/src/TruthGraph.cc b/PhysicsTools/TruthInfo/src/TruthGraph.cc new file mode 100644 index 0000000000000..fe61a000c2c51 --- /dev/null +++ b/PhysicsTools/TruthInfo/src/TruthGraph.cc @@ -0,0 +1,24 @@ +// Author: Felice Pantaleo - CERN +// Date: 03/2026 +// A compact, read-only graph representation of the truth information in an event. +// The graph is built in the TruthGraphProducer module, which also fills the node metadata and associations. +// The graph is intended to be a common data format for various use cases (e.g. validation, analysis, visualization). + +#include "PhysicsTools/TruthInfo/interface/TruthGraph.h" + +bool TruthGraph::isConsistent() const { + if (offsets.size() != nodes.size() + 1) + return false; + if (!offsets.empty() && offsets.front() != 0) + return false; + if (!offsets.empty() && offsets.back() != edges.size()) + return false; + if (!edgeKind.empty() && edgeKind.size() != edges.size()) + return false; + + for (size_t i = 1; i < offsets.size(); ++i) { + if (offsets[i] < offsets[i - 1]) + return false; + } + return true; +} From cbf5b574680a816b93dc7925ddd35f76008858c7 Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Thu, 5 Mar 2026 10:20:24 +0100 Subject: [PATCH 02/53] Add necessary build and dictionary files for TruthGraph --- PhysicsTools/TruthInfo/BuildFile.xml | 7 +++++++ PhysicsTools/TruthInfo/src/classes.h | 14 ++++++++++++++ PhysicsTools/TruthInfo/src/classes_def.xml | 6 ++++++ 3 files changed, 27 insertions(+) create mode 100644 PhysicsTools/TruthInfo/BuildFile.xml create mode 100644 PhysicsTools/TruthInfo/src/classes.h create mode 100644 PhysicsTools/TruthInfo/src/classes_def.xml diff --git a/PhysicsTools/TruthInfo/BuildFile.xml b/PhysicsTools/TruthInfo/BuildFile.xml new file mode 100644 index 0000000000000..3bdd956ac66a1 --- /dev/null +++ b/PhysicsTools/TruthInfo/BuildFile.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/PhysicsTools/TruthInfo/src/classes.h b/PhysicsTools/TruthInfo/src/classes.h new file mode 100644 index 0000000000000..cd1768698f687 --- /dev/null +++ b/PhysicsTools/TruthInfo/src/classes.h @@ -0,0 +1,14 @@ +#ifndef PhysicsTools_TruthInfo_src_classes_h +#define PhysicsTools_TruthInfo_src_classes_h + +#include "PhysicsTools/TruthInfo/interface/TruthGraph.h" +#include "DataFormats/Common/interface/Wrapper.h" + +namespace { + struct dictionary { + TruthGraph graph; + edm::Wrapper wgraph; + }; +} // namespace + +#endif diff --git a/PhysicsTools/TruthInfo/src/classes_def.xml b/PhysicsTools/TruthInfo/src/classes_def.xml new file mode 100644 index 0000000000000..1bd8980c3e6a5 --- /dev/null +++ b/PhysicsTools/TruthInfo/src/classes_def.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file From eabdbc0a5a97939f1a741dda511e51fb9b27bc7d Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Thu, 5 Mar 2026 10:20:24 +0100 Subject: [PATCH 03/53] Implement TruthGraphProducer for event graph construction --- .../TruthInfo/plugins/TruthGraphProducer.cc | 595 ++++++++++++++++++ 1 file changed, 595 insertions(+) create mode 100644 PhysicsTools/TruthInfo/plugins/TruthGraphProducer.cc diff --git a/PhysicsTools/TruthInfo/plugins/TruthGraphProducer.cc b/PhysicsTools/TruthInfo/plugins/TruthGraphProducer.cc new file mode 100644 index 0000000000000..d8117c45eeb0a --- /dev/null +++ b/PhysicsTools/TruthInfo/plugins/TruthGraphProducer.cc @@ -0,0 +1,595 @@ +// Author: Felice Pantaleo - CERN +// Date: 03/2026 +#include +#include +#include +#include +#include +#include + +#include "FWCore/Framework/interface/stream/EDProducer.h" +#include "FWCore/Framework/interface/Event.h" +#include "FWCore/Framework/interface/EventSetup.h" +#include "FWCore/Framework/interface/MakerMacros.h" +#include "FWCore/ParameterSet/interface/ConfigurationDescriptions.h" +#include "FWCore/ParameterSet/interface/ParameterSet.h" +#include "FWCore/ParameterSet/interface/ParameterSetDescription.h" +#include "FWCore/Utilities/interface/InputTag.h" + +#include "SimDataFormats/EncodedEventId/interface/EncodedEventId.h" +#include "SimDataFormats/Track/interface/SimTrackContainer.h" +#include "SimDataFormats/Vertex/interface/SimVertexContainer.h" + +// Legacy HepMC (HepMC2) +#include "SimDataFormats/GeneratorProducts/interface/HepMCProduct.h" +#include "HepMC/GenEvent.h" +#include "HepMC/GenVertex.h" +#include "HepMC/GenParticle.h" + +// HepMC3 +#include "SimDataFormats/GeneratorProducts/interface/HepMC3Product.h" +#include "HepMC3/GenEvent.h" +#include "HepMC3/GenParticle.h" +#include "HepMC3/GenVertex.h" + +#include "PhysicsTools/TruthInfo/interface/TruthGraph.h" + +namespace { + + // Pack EncodedEventId into 64-bit without relying on a particular API + uint64_t packEventId(EncodedEventId const& id) { + uint64_t out = 0; + static_assert(sizeof(EncodedEventId) <= sizeof(uint64_t), "EncodedEventId larger than 64 bits, adjust packing"); + std::memcpy(&out, &id, sizeof(EncodedEventId)); + return out; + } + + // Disjoint-set union + struct DSU { + std::vector p, r; + explicit DSU(int n) : p(n), r(n, 0) { + for (int i = 0; i < n; ++i) + p[i] = i; + } + int find(int x) { + while (p[x] != x) { + p[x] = p[p[x]]; + x = p[x]; + } + return x; + } + void unite(int a, int b) { + a = find(a); + b = find(b); + if (a == b) + return; + if (r[a] < r[b]) + std::swap(a, b); + p[b] = a; + if (r[a] == r[b]) + ++r[a]; + } + }; + + // Unique key to index GEN nodes in maps (vertex vs particle + barcode) + inline int64_t genKeyVertex(int barcode) { return (int64_t(barcode) << 1) | 1LL; } + inline int64_t genKeyParticle(int barcode) { return (int64_t(barcode) << 1); } + + struct GenBuild { + // lists of unique barcodes (order of appearance) + std::vector vtxBarcodes; + std::vector partBarcodes; + + // index -> barcode for particles in the iteration order (for SimTrack::genpartIndex mapping) + std::vector particleBarcodeByIndex; + + // bipartite edges in barcode space + // vtx -> part (outgoing) + std::vector> vtxToPart; + // part -> vtx (end vertex) + std::vector> partToVtx; + }; + + // Build GEN info from HepMC2 + GenBuild buildFromHepMC2(HepMC::GenEvent const& ev) { + GenBuild gb; + + std::unordered_set seenV; + std::unordered_set seenP; + + // vertices + for (auto v = ev.vertices_begin(); v != ev.vertices_end(); ++v) { + const int vbc = (*v)->barcode(); + if (seenV.insert(vbc).second) + gb.vtxBarcodes.push_back(vbc); + + // outgoing particles + for (auto po = (*v)->particles_out_const_begin(); po != (*v)->particles_out_const_end(); ++po) { + const int pbc = (*po)->barcode(); + if (seenP.insert(pbc).second) + gb.partBarcodes.push_back(pbc); + gb.vtxToPart.emplace_back(vbc, pbc); + } + + // incoming particles (orphans are possible; we still model the edge) + for (auto pi = (*v)->particles_in_const_begin(); pi != (*v)->particles_in_const_end(); ++pi) { + const int pbc = (*pi)->barcode(); + if (seenP.insert(pbc).second) + gb.partBarcodes.push_back(pbc); + gb.partToVtx.emplace_back(pbc, vbc); + } + } + + // particle iteration order -> barcode (for genpartIndex) + for (auto p = ev.particles_begin(); p != ev.particles_end(); ++p) { + gb.particleBarcodeByIndex.push_back((*p)->barcode()); + } + + return gb; + } + + // Build GEN info from HepMC3 + GenBuild buildFromHepMC3(HepMC3::GenEvent const& ev) { + GenBuild gb; + + std::unordered_set seenV; + std::unordered_set seenP; + + // vertices + for (auto const& vptr : ev.vertices()) { + if (!vptr) + continue; + const int vbc = vptr->id(); // HepMC3 vertex id acts like barcode; in CMSSW it is typically negative + if (seenV.insert(vbc).second) + gb.vtxBarcodes.push_back(vbc); + + // outgoing + for (auto const& po : vptr->particles_out()) { + if (!po) + continue; + const int pbc = po->id(); // particle id (barcode) + if (seenP.insert(pbc).second) + gb.partBarcodes.push_back(pbc); + gb.vtxToPart.emplace_back(vbc, pbc); + } + // incoming + for (auto const& pi : vptr->particles_in()) { + if (!pi) + continue; + const int pbc = pi->id(); + if (seenP.insert(pbc).second) + gb.partBarcodes.push_back(pbc); + gb.partToVtx.emplace_back(pbc, vbc); + } + } + + // particle iteration order -> barcode (for genpartIndex) + gb.particleBarcodeByIndex.reserve(ev.particles().size()); + for (auto const& pptr : ev.particles()) { + if (!pptr) + continue; + gb.particleBarcodeByIndex.push_back(pptr->id()); + } + + return gb; + } + + template + bool validHandle(HandleT const& h) { + return h.isValid(); + } + +} // namespace + +class TruthGraphProducer : public edm::stream::EDProducer<> { +public: + explicit TruthGraphProducer(const edm::ParameterSet& cfg) + : hepmc3Token_(mayConsume(cfg.getParameter("genEventHepMC3"))), + hepmc2Token_(mayConsume(cfg.getParameter("genEventHepMC"))), + simTrackToken_(consumes(cfg.getParameter("simTracks"))), + simVertexToken_(consumes(cfg.getParameter("simVertices"))), + addGenToSimEdges_(cfg.getParameter("addGenToSimEdges")) { + produces(); + } + + static void fillDescriptions(edm::ConfigurationDescriptions& descriptions) { + edm::ParameterSetDescription desc; + + desc.add("genEventHepMC3", edm::InputTag("generatorSmeared")) + ->setComment("edm::HepMC3Product label (preferred when available)"); + desc.add("genEventHepMC", edm::InputTag("generatorSmeared")) + ->setComment("edm::HepMCProduct label (legacy fallback)"); + + desc.add("simTracks", edm::InputTag("g4SimHits")) + ->setComment("SimTrackContainer label (typically g4SimHits)"); + desc.add("simVertices", edm::InputTag("g4SimHits")) + ->setComment("SimVertexContainer label (typically g4SimHits)"); + + desc.add("addGenToSimEdges", true) + ->setComment("If true, add cross edges GenParticle -> SimTrack using SimTrack::genpartIndex()"); + + descriptions.addWithDefaultLabel(desc); + } + + void produce(edm::Event& evt, const edm::EventSetup&) override { + auto out = std::make_unique(); + + // --- Fetch SIM + const auto& simTracks = evt.get(simTrackToken_); + const auto& simVertices = evt.get(simVertexToken_); + + // --- Fetch GEN (HepMC3 preferred, fallback to HepMC2) + GenBuild gb; + bool haveGen = false; + + { + edm::Handle h3; + evt.getByToken(hepmc3Token_, h3); + if (validHandle(h3) && h3->GetEvent() != nullptr) { + const HepMC3::GenEventData* data = h3->GetEvent(); + HepMC3::GenEvent ev3; + ev3.read_data(*data); + gb = buildFromHepMC3(ev3); + haveGen = true; + } + } + + if (!haveGen) { + edm::Handle h2; + evt.getByToken(hepmc2Token_, h2); + if (validHandle(h2) && h2->GetEvent() != nullptr) { + const HepMC::GenEvent* ev2 = h2->GetEvent(); + gb = buildFromHepMC2(*ev2); + haveGen = true; + } + } + + const uint32_t nSimVtx = static_cast(simVertices.size()); + const uint32_t nSimTrk = static_cast(simTracks.size()); + + // ---------------------------- + // GEN components -> "GenEvent" + // ---------------------------- + int nGenEvents = 0; + + std::unordered_map tempIndex; // genKey -> temp idx + std::vector tempKeys; // idx -> genKey + + auto getTemp = [&](int64_t k) -> int { + auto it = tempIndex.find(k); + if (it != tempIndex.end()) + return it->second; + const int idx = static_cast(tempKeys.size()); + tempIndex.emplace(k, idx); + tempKeys.push_back(k); + return idx; + }; + + std::vector compOfTemp; + std::unordered_map repToComp; + + if (haveGen) { + for (int vbc : gb.vtxBarcodes) + (void)getTemp(genKeyVertex(vbc)); + for (int pbc : gb.partBarcodes) + (void)getTemp(genKeyParticle(pbc)); + + DSU dsu(static_cast(tempKeys.size())); + + for (auto const& e : gb.vtxToPart) { + dsu.unite(getTemp(genKeyVertex(e.first)), getTemp(genKeyParticle(e.second))); + } + for (auto const& e : gb.partToVtx) { + dsu.unite(getTemp(genKeyParticle(e.first)), getTemp(genKeyVertex(e.second))); + } + + compOfTemp.resize(tempKeys.size(), -1); + for (int i = 0; i < static_cast(tempKeys.size()); ++i) { + const int rep = dsu.find(i); + auto it = repToComp.find(rep); + if (it == repToComp.end()) { + const int cid = nGenEvents++; + repToComp.emplace(rep, cid); + compOfTemp[i] = cid; + } else { + compOfTemp[i] = it->second; + } + } + + if (nGenEvents == 0) + nGenEvents = 1; + } + + const uint32_t nGenVtx = haveGen ? static_cast(gb.vtxBarcodes.size()) : 0u; + const uint32_t nGenPar = haveGen ? static_cast(gb.partBarcodes.size()) : 0u; + + const uint32_t baseGenEvent = 0; + const uint32_t baseGenVtx = baseGenEvent + static_cast(nGenEvents); + const uint32_t baseGenPar = baseGenVtx + nGenVtx; + const uint32_t baseSimVtx = baseGenPar + nGenPar; + const uint32_t baseSimTrk = baseSimVtx + nSimVtx; + + const uint32_t nNodes = baseSimTrk + nSimTrk; + + out->nodes.resize(nNodes); + out->pdgId.assign(nNodes, 0); + out->status.assign(nNodes, 0); + out->eventId.assign(nNodes, 0); + + out->genEventOfNode.assign(nNodes, -1); + + out->simTrackToGen.assign(nNodes, -1); + out->simTrackToVtx.assign(nNodes, -1); + + // ---------------------------- + // Create GenEvent nodes (roots) + // ---------------------------- + for (int cid = 0; cid < nGenEvents; ++cid) { + const uint32_t nodeId = baseGenEvent + static_cast(cid); + out->nodes[nodeId] = TruthGraph::NodeRef{TruthGraph::NodeKind::GenEvent, static_cast(cid)}; + out->eventId[nodeId] = 0; + out->genEventOfNode[nodeId] = cid; + } + + // ---------------------------- + // Create GenVertex / GenParticle nodes + // ---------------------------- + std::unordered_map genVtxBarcodeToNode; + std::unordered_map genParBarcodeToNode; + + genVtxBarcodeToNode.reserve(nGenVtx * 2); + genParBarcodeToNode.reserve(nGenPar * 2); + + if (haveGen) { + for (uint32_t i = 0; i < nGenVtx; ++i) { + const int vbc = gb.vtxBarcodes[i]; + const uint32_t nodeId = baseGenVtx + i; + genVtxBarcodeToNode.emplace(vbc, nodeId); + out->nodes[nodeId] = TruthGraph::NodeRef{TruthGraph::NodeKind::GenVertex, static_cast(vbc)}; + out->eventId[nodeId] = 0; + + const int tidx = tempIndex.at(genKeyVertex(vbc)); + out->genEventOfNode[nodeId] = compOfTemp[tidx]; + } + + for (uint32_t i = 0; i < nGenPar; ++i) { + const int pbc = gb.partBarcodes[i]; + const uint32_t nodeId = baseGenPar + i; + genParBarcodeToNode.emplace(pbc, nodeId); + out->nodes[nodeId] = TruthGraph::NodeRef{TruthGraph::NodeKind::GenParticle, static_cast(pbc)}; + out->eventId[nodeId] = 0; + + const int tidx = tempIndex.at(genKeyParticle(pbc)); + out->genEventOfNode[nodeId] = compOfTemp[tidx]; + } + } + + // GenParticle barcode -> production GenVertex barcode (from vtx -> part) + std::unordered_map genParToProdVtx; + if (haveGen) { + genParToProdVtx.reserve(gb.partBarcodes.size() * 2); + for (auto const& vp : gb.vtxToPart) { + genParToProdVtx.emplace(vp.second, vp.first); + } + } + + // ---------------------------- + // Create SimVertex nodes + // ---------------------------- + std::vector simVtxIndexToNode(nSimVtx, 0); + for (uint32_t i = 0; i < nSimVtx; ++i) { + const uint32_t nodeId = baseSimVtx + i; + simVtxIndexToNode[i] = nodeId; + out->nodes[nodeId] = TruthGraph::NodeRef{TruthGraph::NodeKind::SimVertex, static_cast(i)}; + out->eventId[nodeId] = packEventId(simVertices[i].eventId()); + } + + // ---------------------------- + // Create SimTrack nodes + // ---------------------------- + std::unordered_map simTrackIdToNode; + simTrackIdToNode.reserve(nSimTrk * 2); + + for (uint32_t i = 0; i < nSimTrk; ++i) { + const uint32_t nodeId = baseSimTrk + i; + const uint32_t tid = simTracks[i].trackId(); + simTrackIdToNode.emplace(tid, nodeId); + + out->nodes[nodeId] = TruthGraph::NodeRef{TruthGraph::NodeKind::SimTrack, static_cast(tid)}; + out->pdgId[nodeId] = simTracks[i].type(); + out->eventId[nodeId] = packEventId(simTracks[i].eventId()); + + const int vtxIdx = simTracks[i].vertIndex(); + if (vtxIdx >= 0 && static_cast(vtxIdx) < nSimVtx) { + out->simTrackToVtx[nodeId] = static_cast(simVtxIndexToNode[static_cast(vtxIdx)]); + } + + if (haveGen && !simTracks[i].noGenpart()) { + const int ig = simTracks[i].genpartIndex(); + if (ig >= 0 && static_cast(ig) < gb.particleBarcodeByIndex.size()) { + const int barcode = gb.particleBarcodeByIndex[static_cast(ig)]; + auto it = genParBarcodeToNode.find(barcode); + if (it != genParBarcodeToNode.end()) { + out->simTrackToGen[nodeId] = static_cast(it->second); + } + } + } + } + + // ---------------------------- + // Build edges (+ kinds) and compress to CSR + // ---------------------------- + std::vector> edgePairs; + std::vector edgeKinds; + edgePairs.reserve(12 * (nGenVtx + nGenPar + nSimTrk)); + edgeKinds.reserve(edgePairs.capacity()); + + auto push_edge = [&](uint32_t src, uint32_t dst, TruthGraph::EdgeKind k) { + edgePairs.emplace_back(src, dst); + edgeKinds.emplace_back(static_cast(k)); + }; + + // Dedup GenVtx->SimVtx cross edges + std::unordered_set genVtxToSimVtxSeen; + genVtxToSimVtxSeen.reserve(2 * nSimTrk); + auto packPair = [](uint32_t a, uint32_t b) -> uint64_t { return (uint64_t(a) << 32) | uint64_t(b); }; + + // GEN edges: connect GenEvent(component) -> GenVertex roots (or all vertices if no roots found) + if (haveGen) { + std::unordered_map vtxIncoming; + vtxIncoming.reserve(nGenVtx * 2); + for (int vbc : gb.vtxBarcodes) + vtxIncoming.emplace(vbc, 0); + for (auto const& e : gb.partToVtx) { + auto it = vtxIncoming.find(e.second); + if (it != vtxIncoming.end()) + it->second++; + } + + std::vector> rootsByComp(nGenEvents); + std::vector> allVtxByComp(nGenEvents); + + for (int vbc : gb.vtxBarcodes) { + const int tidx = tempIndex.at(genKeyVertex(vbc)); + const int cid = compOfTemp[tidx]; + if (cid < 0 || cid >= nGenEvents) + continue; + allVtxByComp[cid].push_back(vbc); + if (vtxIncoming[vbc] == 0) + rootsByComp[cid].push_back(vbc); + } + + for (int cid = 0; cid < nGenEvents; ++cid) { + const uint32_t genEventNode = baseGenEvent + static_cast(cid); + auto roots = rootsByComp[cid]; + if (roots.empty()) + roots = allVtxByComp[cid]; + for (int vbc : roots) { + auto itV = genVtxBarcodeToNode.find(vbc); + if (itV != genVtxBarcodeToNode.end()) { + push_edge(genEventNode, itV->second, TruthGraph::EdgeKind::Gen); + } + } + } + + for (auto const& e : gb.vtxToPart) { + auto itV = genVtxBarcodeToNode.find(e.first); + auto itP = genParBarcodeToNode.find(e.second); + if (itV != genVtxBarcodeToNode.end() && itP != genParBarcodeToNode.end()) { + push_edge(itV->second, itP->second, TruthGraph::EdgeKind::Gen); + } + } + for (auto const& e : gb.partToVtx) { + auto itP = genParBarcodeToNode.find(e.first); + auto itV = genVtxBarcodeToNode.find(e.second); + if (itP != genParBarcodeToNode.end() && itV != genVtxBarcodeToNode.end()) { + push_edge(itP->second, itV->second, TruthGraph::EdgeKind::Gen); + } + } + } + + // SIM edges: + for (uint32_t i = 0; i < nSimTrk; ++i) { + const auto& t = simTracks[i]; + const uint32_t childNode = baseSimTrk + i; + + const int vtxIdx = t.vertIndex(); + if (vtxIdx < 0 || static_cast(vtxIdx) >= nSimVtx) + continue; + const uint32_t vtxNode = simVtxIndexToNode[static_cast(vtxIdx)]; + + push_edge(vtxNode, childNode, TruthGraph::EdgeKind::Sim); + + const int parentTid = simVertices[static_cast(vtxIdx)].parentIndex(); // trackId (not index) + if (parentTid > 0) { + auto itParent = simTrackIdToNode.find(static_cast(parentTid)); + if (itParent != simTrackIdToNode.end()) { + push_edge(itParent->second, vtxNode, TruthGraph::EdgeKind::Sim); + } + } + } + + // CROSS GEN->SIM: GenParticle -> SimTrack + if (addGenToSimEdges_ && haveGen) { + for (uint32_t i = 0; i < nSimTrk; ++i) { + const uint32_t simNode = baseSimTrk + i; + const int32_t genNode = out->simTrackToGen[simNode]; + if (genNode >= 0) { + push_edge(static_cast(genNode), simNode, TruthGraph::EdgeKind::GenToSim); + } + } + } + + // CROSS GEN->SIM: GenVertex(prod of GenParticle) -> SimVertex(prod of SimTrack) + if (haveGen) { + for (uint32_t i = 0; i < nSimTrk; ++i) { + const uint32_t simTrackNode = baseSimTrk + i; + + const int32_t genParNode = out->simTrackToGen[simTrackNode]; + if (genParNode < 0) + continue; + + const int32_t simVtxNode_i32 = out->simTrackToVtx[simTrackNode]; + if (simVtxNode_i32 < 0) + continue; + const uint32_t simVtxNode = static_cast(simVtxNode_i32); + + auto const& genParRef = out->nodes[static_cast(genParNode)]; + if (genParRef.kind != TruthGraph::NodeKind::GenParticle) + continue; + const int pbc = static_cast(genParRef.key); + + auto itProd = genParToProdVtx.find(pbc); + if (itProd == genParToProdVtx.end()) + continue; + + auto itV = genVtxBarcodeToNode.find(itProd->second); + if (itV == genVtxBarcodeToNode.end()) + continue; + + const uint32_t genVtxNode = itV->second; + + const uint64_t packed = packPair(genVtxNode, simVtxNode); + if (!genVtxToSimVtxSeen.insert(packed).second) + continue; + + push_edge(genVtxNode, simVtxNode, TruthGraph::EdgeKind::GenToSim); + } + } + + // CSR compress + out->offsets.assign(nNodes + 1, 0); + for (auto const& e : edgePairs) { + if (e.first < nNodes) + out->offsets[e.first + 1] += 1; + } + for (uint32_t i = 1; i <= nNodes; ++i) + out->offsets[i] += out->offsets[i - 1]; + + const uint32_t nEdges = out->offsets.back(); + out->edges.assign(nEdges, 0); + out->edgeKind.assign(nEdges, static_cast(TruthGraph::EdgeKind::Gen)); + + std::vector cursor = out->offsets; + for (size_t i = 0; i < edgePairs.size(); ++i) { + const uint32_t src = edgePairs[i].first; + const uint32_t dst = edgePairs[i].second; + if (src < nNodes && dst < nNodes) { + const uint32_t pos = cursor[src]++; + out->edges[pos] = dst; + out->edgeKind[pos] = edgeKinds[i]; + } + } + + evt.put(std::move(out)); + } + +private: + edm::EDGetTokenT hepmc3Token_; + edm::EDGetTokenT hepmc2Token_; + edm::EDGetTokenT simTrackToken_; + edm::EDGetTokenT simVertexToken_; + bool addGenToSimEdges_; +}; + +DEFINE_FWK_MODULE(TruthGraphProducer); From ac723ba84861bb2d7f9a5c27ca6ad139bbbd3fab Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Thu, 5 Mar 2026 10:20:25 +0100 Subject: [PATCH 04/53] Add dumper module for graph visualization and inspection --- .../TruthInfo/plugins/TruthGraphDumper.cc | 452 ++++++++++++++++++ 1 file changed, 452 insertions(+) create mode 100644 PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc diff --git a/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc b/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc new file mode 100644 index 0000000000000..ea60b1680b554 --- /dev/null +++ b/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc @@ -0,0 +1,452 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "FWCore/Framework/interface/one/EDAnalyzer.h" +#include "FWCore/Framework/interface/Event.h" +#include "FWCore/Framework/interface/EventSetup.h" +#include "FWCore/Framework/interface/MakerMacros.h" +#include "FWCore/ParameterSet/interface/ConfigurationDescriptions.h" +#include "FWCore/ParameterSet/interface/ParameterSet.h" +#include "FWCore/ParameterSet/interface/ParameterSetDescription.h" +#include "FWCore/Utilities/interface/EDGetToken.h" +#include "FWCore/Utilities/interface/InputTag.h" + +#include "SimDataFormats/Track/interface/SimTrackContainer.h" +#include "SimDataFormats/Vertex/interface/SimVertexContainer.h" + +#include "SimDataFormats/GeneratorProducts/interface/HepMCProduct.h" +#include "HepMC/GenEvent.h" +#include "HepMC/GenParticle.h" +#include "HepMC/GenVertex.h" + +#include "SimDataFormats/GeneratorProducts/interface/HepMC3Product.h" +#include "HepMC3/GenEvent.h" +#include "HepMC3/GenParticle.h" +#include "HepMC3/GenVertex.h" + +#include "PhysicsTools/TruthInfo/interface/TruthGraph.h" + +namespace { + + // --- PDG naming (UTF-8) + std::string pdgNameUtf8(int pdgId) { + const int ap = std::abs(pdgId); + + if (pdgId == 11) + return "e⁻"; + if (pdgId == -11) + return "e⁺"; + if (pdgId == 13) + return "μ⁻"; + if (pdgId == -13) + return "μ⁺"; + if (pdgId == 15) + return "τ⁻"; + if (pdgId == -15) + return "τ⁺"; + + if (pdgId == 12) + return "νₑ"; + if (pdgId == -12) + return "ν̄ₑ"; + if (pdgId == 14) + return "ν_μ"; + if (pdgId == -14) + return "ν̄_μ"; + if (pdgId == 16) + return "ν_τ"; + if (pdgId == -16) + return "ν̄_τ"; + + if (pdgId == 22) + return "γ"; + if (pdgId == 21) + return "g"; + if (pdgId == 23) + return "Z⁰"; + if (pdgId == 24) + return "W⁺"; + if (pdgId == -24) + return "W⁻"; + if (pdgId == 25) + return "H"; + + if (pdgId == 2212) + return "p"; + if (pdgId == -2212) + return "p̄"; + if (pdgId == 2112) + return "n"; + if (pdgId == -2112) + return "n̄"; + + if (pdgId == 111) + return "π⁰"; + if (pdgId == 211) + return "π⁺"; + if (pdgId == -211) + return "π⁻"; + if (pdgId == 321) + return "K⁺"; + if (pdgId == -321) + return "K⁻"; + if (pdgId == 130) + return "K⁰_L"; + if (pdgId == 310) + return "K⁰_S"; + + if (ap >= 1 && ap <= 6) { + static const char* qname[7] = {"", "d", "u", "s", "c", "b", "t"}; + std::string s = qname[ap]; + if (pdgId < 0) + s = "anti-" + s; + return s; + } + + return "pdg"; + } + + std::string pdgLabel(int pdgId) { + std::ostringstream ss; + const std::string name = pdgNameUtf8(pdgId); + if (name == "pdg") + ss << "pdg(" << pdgId << ")"; + else + ss << name << " (" << pdgId << ")"; + return ss.str(); + } + + template + std::string fmtP4(const P4T& p4) { + std::ostringstream ss; + ss.setf(std::ios::fixed); + ss << std::setprecision(3) << "(" << p4.px() << ", " << p4.py() << ", " << p4.pz() << ", " << p4.e() << ")"; + return ss.str(); + } + + template + std::string fmtX4(const X4T& x4) { + std::ostringstream ss; + ss.setf(std::ios::fixed); + ss << std::setprecision(3) << "(" << x4.x() << ", " << x4.y() << ", " << x4.z() << ", " << x4.t() << ")"; + return ss.str(); + } + + const char* kindName(TruthGraph::NodeKind k) { + switch (k) { + case TruthGraph::NodeKind::GenEvent: + return "GenEvent"; + case TruthGraph::NodeKind::GenVertex: + return "GenVertex"; + case TruthGraph::NodeKind::GenParticle: + return "GenParticle"; + case TruthGraph::NodeKind::SimVertex: + return "SimVertex"; + case TruthGraph::NodeKind::SimTrack: + return "SimTrack"; + } + return "Unknown"; + } + + const char* shapeFor(TruthGraph::NodeKind k) { + switch (k) { + case TruthGraph::NodeKind::GenEvent: + return "box"; + case TruthGraph::NodeKind::GenVertex: + return "diamond"; + case TruthGraph::NodeKind::GenParticle: + return "box"; + case TruthGraph::NodeKind::SimVertex: + return "diamond"; + case TruthGraph::NodeKind::SimTrack: + return "ellipse"; + } + return "box"; + } + + const char* edgeAttrs(uint8_t ek) { + using EK = TruthGraph::EdgeKind; + switch (static_cast(ek)) { + case EK::Gen: + return ""; + case EK::Sim: + return " [style=solid]"; + case EK::GenToSim: + return " [dir=both, style=dashed]"; + case EK::SimToGen: + return " [dir=both, style=dotted]"; + } + return ""; + } + +} // anonymous namespace + +class TruthGraphDumper : public edm::one::EDAnalyzer<> { +public: + explicit TruthGraphDumper(const edm::ParameterSet& cfg) + : token_(consumes(cfg.getParameter("src"))), + dotFile_(cfg.getParameter("dotFile")), + maxNodes_(cfg.getParameter("maxNodes")), + maxEdgesPerNode_(cfg.getParameter("maxEdgesPerNode")), + simTracksToken_(mayConsume(cfg.getParameter("simTracks"))), + simVerticesToken_(mayConsume(cfg.getParameter("simVertices"))), + hepmc2Token_(mayConsume(cfg.getParameter("genEventHepMC"))), + hepmc3Token_(mayConsume(cfg.getParameter("genEventHepMC3"))) {} + + static void fillDescriptions(edm::ConfigurationDescriptions& descriptions) { + edm::ParameterSetDescription desc; + desc.add("src", edm::InputTag("truthGraphProducer")); + desc.add("dotFile", "truthgraph.dot"); + desc.add("maxNodes", 5000)->setComment("Truncate to keep DOT manageable"); + desc.add("maxEdgesPerNode", 200)->setComment("Truncate fanout per node"); + + desc.add("simTracks", edm::InputTag("g4SimHits")) + ->setComment("SimTrackContainer (optional, used to enrich SimTrack nodes)"); + desc.add("simVertices", edm::InputTag("g4SimHits")) + ->setComment("SimVertexContainer (optional, used for future enrichment)"); + + // GEN record (for enriching GenParticle/GenVertex nodes) + desc.add("genEventHepMC", edm::InputTag("generatorSmeared")) + ->setComment("edm::HepMCProduct label (your step1.root shows this is present)"); + desc.add("genEventHepMC3", edm::InputTag("generatorSmeared")) + ->setComment("edm::HepMC3Product label (optional)"); + + descriptions.addWithDefaultLabel(desc); + } + + void analyze(const edm::Event& evt, const edm::EventSetup&) override { + auto const& g = evt.get(token_); + + // --- SIM handles (optional) + edm::Handle hSimTracks; + evt.getByToken(simTracksToken_, hSimTracks); + + std::unordered_map tidToIndex; + if (hSimTracks.isValid()) { + tidToIndex.reserve(hSimTracks->size() * 2); + for (uint32_t i = 0; i < hSimTracks->size(); ++i) { + tidToIndex.emplace((*hSimTracks)[i].trackId(), i); + } + } + + // --- GEN handles (optional) + // Prefer HepMC2 if present (it is in your step1.root); else HepMC3. + edm::Handle hHepMC2; + evt.getByToken(hepmc2Token_, hHepMC2); + + edm::Handle hHepMC3; + evt.getByToken(hepmc3Token_, hHepMC3); + + // HepMC2 lookup maps + std::unordered_map bc2p; + std::unordered_map bc2v; + HepMC::GenEvent const* ev2 = nullptr; + + if (hHepMC2.isValid() && hHepMC2->GetEvent() != nullptr) { + ev2 = hHepMC2->GetEvent(); + bc2p.reserve(ev2->particles_size() * 2); + bc2v.reserve(ev2->vertices_size() * 2); + + for (auto p = ev2->particles_begin(); p != ev2->particles_end(); ++p) { + bc2p.emplace((*p)->barcode(), *p); + } + for (auto v = ev2->vertices_begin(); v != ev2->vertices_end(); ++v) { + bc2v.emplace((*v)->barcode(), *v); + } + } + + // HepMC3 reconstruction + maps + HepMC3::GenEvent ev3; + std::unordered_map id3p; + std::unordered_map id3v; + bool have3 = false; + + if (!ev2 && hHepMC3.isValid() && hHepMC3->GetEvent() != nullptr) { + have3 = true; + const HepMC3::GenEventData* data = hHepMC3->GetEvent(); + ev3.read_data(*data); + + id3p.reserve(ev3.particles().size() * 2); + id3v.reserve(ev3.vertices().size() * 2); + + for (auto const& pptr : ev3.particles()) { + if (pptr) + id3p.emplace(pptr->id(), pptr); + } + for (auto const& vptr : ev3.vertices()) { + if (vptr) + id3v.emplace(vptr->id(), vptr); + } + } + + std::ofstream os(dotFile_); + os << "digraph TruthGraph {\n"; + os << " rankdir=LR;\n"; + os << " node [fontsize=10];\n"; + + const uint32_t n = std::min(g.nNodes(), maxNodes_); + + // nodes + for (uint32_t i = 0; i < n; ++i) { + auto const& r = g.nodeRef(i); + const auto pdg = g.nodePdgId(i); + const auto st = g.nodeStatus(i); + const auto eid = g.nodeEventId(i); + + // SimTrack enrichment + bool crossedBoundary = false; + bool haveSim = false; + SimTrack const* simt = nullptr; + + if (r.kind == TruthGraph::NodeKind::SimTrack && hSimTracks.isValid()) { + const int64_t key = r.key; // trackId + if (key >= 0 && key <= static_cast(std::numeric_limits::max())) { + auto it = tidToIndex.find(static_cast(key)); + if (it != tidToIndex.end()) { + simt = &(*hSimTracks)[it->second]; + haveSim = true; + crossedBoundary = simt->crossedBoundary(); + } + } + } + + // Node style + os << " n" << i << " [shape=" << shapeFor(r.kind); + if (crossedBoundary) + os << ", color=\"red\", penwidth=2"; + + // HTML label + os << ", label=<\n"; + os << " \n"; + os << " \n"; + + if (pdg != 0) + os << " \n"; + if (st != 0) + os << " \n"; + if (eid != 0) + os << " \n"; + + // --- GEN enrichment + if (r.kind == TruthGraph::NodeKind::GenEvent) { + if (ev2) { + os << " \n"; + } else if (have3) { + os << " \n"; + } + } else if (r.kind == TruthGraph::NodeKind::GenParticle) { + const int bc = static_cast(r.key); + if (ev2) { + auto it = bc2p.find(bc); + if (it != bc2p.end()) { + auto const* p = it->second; + os << " \n"; + os << " \n"; + os << " \n"; + os << " \n"; + const int prod = p->production_vertex() ? p->production_vertex()->barcode() : 0; + const int endv = p->end_vertex() ? p->end_vertex()->barcode() : 0; + os << " \n"; + } + } else if (have3) { + auto it = id3p.find(bc); + if (it != id3p.end() && it->second) { + auto const& p = it->second; + os << " \n"; + os << " \n"; + os << " \n"; + const int prod = p->production_vertex() ? p->production_vertex()->id() : 0; + const int endv = p->end_vertex() ? p->end_vertex()->id() : 0; + os << " \n"; + } + } + } else if (r.kind == TruthGraph::NodeKind::GenVertex) { + const int bc = static_cast(r.key); + if (ev2) { + auto it = bc2v.find(bc); + if (it != bc2v.end()) { + auto const* v = it->second; + os << " \n"; + os << " \n"; + os << " \n"; + } + } else if (have3) { + auto it = id3v.find(bc); + if (it != id3v.end() && it->second) { + auto const& v = it->second; + os << " \n"; + os << " \n"; + os << " \n"; + } + } + } + + // --- SIM enrichment + if (r.kind == TruthGraph::NodeKind::SimTrack && haveSim) { + os << " \n"; + + const int32_t gn = g.nodeSimTrackToGen(i); + if (gn >= 0) + os << " \n"; + + const int32_t vn = g.nodeSimTrackToVtx(i); + if (vn >= 0) + os << " \n"; + + if (crossedBoundary) { + os << " \n"; + os << " \n"; + os << " \n"; + } + } + + os << "
" << i << " " << kindName(r.kind) << " key=" << r.key << "
pid: " << pdgLabel(pdg) << "
status: " << st << "
eid: " << eid << "
HepMC2: event=" << ev2->event_number() << " spid=" << ev2->signal_process_id() + << "
HepMC3: event=" << ev3.event_number() << "
pid: " << pdgLabel(p->pdg_id()) << "
status: " << p->status() << "
p4: " << fmtP4(p->momentum()) << "
m: " << std::fixed << std::setprecision(3) << p->generated_mass() << "
prodVtx: " << prod << " endVtx: " << endv << "
pid: " << pdgLabel(p->pid()) << "
status: " << p->status() << "
p4: " << fmtP4(p->momentum()) << "
prodVtx: " << prod << " endVtx: " << endv << "
barcode: " << v->barcode() << "
x4: " << fmtX4(v->position()) << "
nIn: " << v->particles_in_size() << " nOut: " << v->particles_out_size() + << "
status: " << v->status() << "
x4: " << fmtX4(v->position()) << "
nIn: " << v->particles_in().size() << " nOut: " << v->particles_out().size() + << "
p4: " << fmtP4(simt->momentum()) << "
GenParticle nodeId: " << gn << "
SimVertex nodeId: " << vn << "
crossedBoundary: true" + << " idAtBoundary=" << simt->getIDAtBoundary() << "
x4@boundary: " << fmtX4(simt->getPositionAtBoundary()) + << "
p4@boundary: " << fmtP4(simt->getMomentumAtBoundary()) + << "
\n"; + os << " >];\n"; + } + + // edges + for (uint32_t src = 0; src < n; ++src) { + const uint32_t b = g.edgeBegin(src); + const uint32_t e = g.edgeEnd(src); + + unsigned kept = 0; + for (uint32_t pos = b; pos < e; ++pos) { + const uint32_t dst = g.edges[pos]; + if (dst >= n) + continue; + os << " n" << src << " -> n" << dst << edgeAttrs(g.edgeKind[pos]) << ";\n"; + if (++kept >= maxEdgesPerNode_) + break; + } + } + + os << "}\n"; + os.close(); + } + +private: + edm::EDGetTokenT token_; + std::string dotFile_; + unsigned maxNodes_; + unsigned maxEdgesPerNode_; + + edm::EDGetTokenT simTracksToken_; + edm::EDGetTokenT simVerticesToken_; + + edm::EDGetTokenT hepmc2Token_; + edm::EDGetTokenT hepmc3Token_; +}; + +DEFINE_FWK_MODULE(TruthGraphDumper); From ae3781d2871a0f0272c7bcfe33273924d92ce264 Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Thu, 5 Mar 2026 10:20:25 +0100 Subject: [PATCH 05/53] Add plugin build configuration for producer and dumper modules --- PhysicsTools/TruthInfo/plugins/BuildFile.xml | 21 ++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 PhysicsTools/TruthInfo/plugins/BuildFile.xml diff --git a/PhysicsTools/TruthInfo/plugins/BuildFile.xml b/PhysicsTools/TruthInfo/plugins/BuildFile.xml new file mode 100644 index 0000000000000..fe52fe690508d --- /dev/null +++ b/PhysicsTools/TruthInfo/plugins/BuildFile.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + From 278321b342f8424bd5c84f2da9aa86cad2d43fae Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Fri, 8 May 2026 15:20:13 +0200 Subject: [PATCH 06/53] Create standalone truth logical graph --- PhysicsTools/TruthInfo/BuildFile.xml | 2 +- PhysicsTools/TruthInfo/interface/Graph.h | 242 +++++++++ PhysicsTools/TruthInfo/plugins/BuildFile.xml | 10 +- .../plugins/TruthLogicalGraphDumper.cc | 341 ++++++++++++ .../plugins/TruthLogicalGraphProducer.cc | 507 ++++++++++++++++++ PhysicsTools/TruthInfo/src/Graph.cc | 451 ++++++++++++++++ PhysicsTools/TruthInfo/src/classes.h | 24 +- PhysicsTools/TruthInfo/src/classes_def.xml | 9 + 8 files changed, 1579 insertions(+), 7 deletions(-) create mode 100644 PhysicsTools/TruthInfo/interface/Graph.h create mode 100644 PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc create mode 100644 PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc create mode 100644 PhysicsTools/TruthInfo/src/Graph.cc diff --git a/PhysicsTools/TruthInfo/BuildFile.xml b/PhysicsTools/TruthInfo/BuildFile.xml index 3bdd956ac66a1..ae5185876d13a 100644 --- a/PhysicsTools/TruthInfo/BuildFile.xml +++ b/PhysicsTools/TruthInfo/BuildFile.xml @@ -4,4 +4,4 @@ - + \ No newline at end of file diff --git a/PhysicsTools/TruthInfo/interface/Graph.h b/PhysicsTools/TruthInfo/interface/Graph.h new file mode 100644 index 0000000000000..b12e0e53f8263 --- /dev/null +++ b/PhysicsTools/TruthInfo/interface/Graph.h @@ -0,0 +1,242 @@ +#ifndef PhysicsTools_TruthInfo_interface_Graph_h +#define PhysicsTools_TruthInfo_interface_Graph_h + +#include +#include +#include +#include + +#include "DataFormats/Math/interface/LorentzVector.h" + +namespace truth { + + struct Checkpoint { + uint32_t checkpointId = 0; + math::XYZTLorentzVectorF position; + math::XYZTLorentzVectorF momentum; + }; + + struct ParticleData { + // Optional provenance/debug back-references to the raw TruthGraph nodes. + // -1 means "not available". + int32_t genNode = -1; + int32_t simNode = -1; + + // Merged metadata. + int32_t pdgId = 0; + int16_t status = 0; + + // SIM event id when available, 0 otherwise. + uint64_t eventId = 0; + + // GEN connected component id from the raw TruthGraph, -1 if not applicable. + int32_t genEvent = -1; + + // Standalone payload. + // Convention: "best available" momentum. + // Prefer SIM if present, otherwise GEN, otherwise default-constructed. + math::XYZTLorentzVectorD momentum; + + // Optional trajectory checkpoints. + std::vector checkpoints; + + [[nodiscard]] bool hasGen() const { return genNode >= 0; } + [[nodiscard]] bool hasSim() const { return simNode >= 0; } + [[nodiscard]] bool valid() const { return hasGen() || hasSim(); } + }; + + struct VertexData { + // Optional provenance/debug back-references to the raw TruthGraph nodes. + // -1 means "not available". + int32_t genNode = -1; + int32_t simNode = -1; + + // SIM event id when available, 0 otherwise. + uint64_t eventId = 0; + + // GEN connected component id from the raw TruthGraph, -1 if not applicable. + int32_t genEvent = -1; + + // Standalone payload. + // Convention: "best available" position. + // Prefer SIM if present, otherwise GEN, otherwise default-constructed. + math::XYZTLorentzVectorD position; + + [[nodiscard]] bool hasGen() const { return genNode >= 0; } + [[nodiscard]] bool hasSim() const { return simNode >= 0; } + [[nodiscard]] bool valid() const { return hasGen() || hasSim(); } + }; + + class Graph; + class Particle; + class Vertex; + + class Particle { + public: + Particle() = default; + Particle(Graph const* graph, uint32_t id) : graph_(graph), id_(id) {} + + [[nodiscard]] bool valid() const { return graph_ != nullptr; } + [[nodiscard]] uint32_t id() const { return id_; } + + [[nodiscard]] const ParticleData& data() const; + + [[nodiscard]] bool hasGen() const; + [[nodiscard]] bool hasSim() const; + [[nodiscard]] int32_t pdgId() const; + [[nodiscard]] int16_t status() const; + [[nodiscard]] uint64_t eventId() const; + [[nodiscard]] int32_t genEvent() const; + [[nodiscard]] const math::XYZTLorentzVectorD& momentum() const; + + [[nodiscard]] std::span checkpoints() const; + [[nodiscard]] bool hasCheckpoints() const; + [[nodiscard]] std::optional checkpoint(uint32_t checkpointId) const; + + [[nodiscard]] bool isRoot() const; + [[nodiscard]] bool isLeaf() const; + + [[nodiscard]] std::vector productionVertices() const; + [[nodiscard]] std::vector decayVertices() const; + + [[nodiscard]] std::vector parents() const; + [[nodiscard]] std::vector children() const; + + [[nodiscard]] std::vector ancestors() const; + [[nodiscard]] std::vector descendants() const; + + [[nodiscard]] bool hasAncestorPdgId(int pdgId) const; + [[nodiscard]] std::optional firstAncestorWithPdgId(int pdgId) const; + [[nodiscard]] std::optional firstCommonAncestor(Particle other) const; + + [[nodiscard]] bool operator==(Particle const& other) const { + return graph_ == other.graph_ && id_ == other.id_; + } + [[nodiscard]] bool operator!=(Particle const& other) const { return !(*this == other); } + + private: + Graph const* graph_ = nullptr; + uint32_t id_ = 0; + }; + + class Vertex { + public: + Vertex() = default; + Vertex(Graph const* graph, uint32_t id) : graph_(graph), id_(id) {} + + [[nodiscard]] bool valid() const { return graph_ != nullptr; } + [[nodiscard]] uint32_t id() const { return id_; } + + [[nodiscard]] const VertexData& data() const; + + [[nodiscard]] bool hasGen() const; + [[nodiscard]] bool hasSim() const; + [[nodiscard]] uint64_t eventId() const; + [[nodiscard]] int32_t genEvent() const; + [[nodiscard]] const math::XYZTLorentzVectorD& position() const; + + [[nodiscard]] bool isSource() const; + [[nodiscard]] bool isSink() const; + + [[nodiscard]] std::vector incomingParticles() const; + [[nodiscard]] std::vector outgoingParticles() const; + + [[nodiscard]] bool operator==(Vertex const& other) const { + return graph_ == other.graph_ && id_ == other.id_; + } + [[nodiscard]] bool operator!=(Vertex const& other) const { return !(*this == other); } + + private: + Graph const* graph_ = nullptr; + uint32_t id_ = 0; + }; + + class Graph { + public: + using size_type = uint32_t; + + std::vector particles; + std::vector vertices; + + // Particle -> decay vertices + std::vector particleToDecayVertexOffsets; + std::vector particleToDecayVertices; + + // Particle -> production vertices + std::vector particleToProductionVertexOffsets; + std::vector particleToProductionVertices; + + // Vertex -> outgoing particles + std::vector vertexToOutgoingParticleOffsets; + std::vector vertexToOutgoingParticles; + + // Vertex -> incoming particles + std::vector vertexToIncomingParticleOffsets; + std::vector vertexToIncomingParticles; + + [[nodiscard]] size_type nParticles() const { return static_cast(particles.size()); } + [[nodiscard]] size_type nVertices() const { return static_cast(vertices.size()); } + + [[nodiscard]] bool empty() const { return particles.empty() && vertices.empty(); } + + [[nodiscard]] Particle particle(size_type id) const; + [[nodiscard]] Vertex vertex(size_type id) const; + + [[nodiscard]] std::vector particleViews() const; + [[nodiscard]] std::vector vertexViews() const; + + [[nodiscard]] std::vector roots() const; + [[nodiscard]] std::vector leaves() const; + + [[nodiscard]] std::vector sourceVertices() const; + [[nodiscard]] std::vector sinkVertices() const; + + [[nodiscard]] std::span decayVertices(size_type particleId) const { + const auto b = particleToDecayVertexOffsets.at(particleId); + const auto e = particleToDecayVertexOffsets.at(particleId + 1); + return std::span(particleToDecayVertices.data() + b, e - b); + } + + [[nodiscard]] std::span productionVertices(size_type particleId) const { + const auto b = particleToProductionVertexOffsets.at(particleId); + const auto e = particleToProductionVertexOffsets.at(particleId + 1); + return std::span(particleToProductionVertices.data() + b, e - b); + } + + [[nodiscard]] std::span outgoingParticles(size_type vertexId) const { + const auto b = vertexToOutgoingParticleOffsets.at(vertexId); + const auto e = vertexToOutgoingParticleOffsets.at(vertexId + 1); + return std::span(vertexToOutgoingParticles.data() + b, e - b); + } + + [[nodiscard]] std::span incomingParticles(size_type vertexId) const { + const auto b = vertexToIncomingParticleOffsets.at(vertexId); + const auto e = vertexToIncomingParticleOffsets.at(vertexId + 1); + return std::span(vertexToIncomingParticles.data() + b, e - b); + } + + [[nodiscard]] bool isConsistent() const; + + private: + friend class Particle; + friend class Vertex; + + [[nodiscard]] std::vector productionVerticesOf(size_type particleId) const; + [[nodiscard]] std::vector decayVerticesOf(size_type particleId) const; + + [[nodiscard]] std::vector parentsOf(size_type particleId) const; + [[nodiscard]] std::vector childrenOf(size_type particleId) const; + + [[nodiscard]] std::vector ancestorsOf(size_type particleId) const; + [[nodiscard]] std::vector descendantsOf(size_type particleId) const; + + [[nodiscard]] std::optional firstAncestorWithPdgIdOf(size_type particleId, int pdgId) const; + [[nodiscard]] std::optional firstCommonAncestorOf(size_type a, size_type b) const; + + [[nodiscard]] std::vector incomingParticlesOf(size_type vertexId) const; + [[nodiscard]] std::vector outgoingParticlesOf(size_type vertexId) const; + }; + +} // namespace truth + +#endif \ No newline at end of file diff --git a/PhysicsTools/TruthInfo/plugins/BuildFile.xml b/PhysicsTools/TruthInfo/plugins/BuildFile.xml index fe52fe690508d..d5910a81ec72b 100644 --- a/PhysicsTools/TruthInfo/plugins/BuildFile.xml +++ b/PhysicsTools/TruthInfo/plugins/BuildFile.xml @@ -10,12 +10,18 @@ - + + + + - + + + + \ No newline at end of file diff --git a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc new file mode 100644 index 0000000000000..f67bc4df53d61 --- /dev/null +++ b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc @@ -0,0 +1,341 @@ +#include +#include +#include +#include +#include + +#include "FWCore/Framework/interface/Event.h" +#include "FWCore/Framework/interface/EventSetup.h" +#include "FWCore/Framework/interface/MakerMacros.h" +#include "FWCore/Framework/interface/one/EDAnalyzer.h" +#include "FWCore/ParameterSet/interface/ConfigurationDescriptions.h" +#include "FWCore/ParameterSet/interface/ParameterSet.h" +#include "FWCore/ParameterSet/interface/ParameterSetDescription.h" +#include "FWCore/Utilities/interface/InputTag.h" + +#include "PhysicsTools/TruthInfo/interface/Graph.h" +#include "PhysicsTools/TruthInfo/interface/TruthGraph.h" + +namespace { + + std::string pdgNameUtf8(int pdgId) { + const int ap = std::abs(pdgId); + + if (pdgId == 11) + return "e⁻"; + if (pdgId == -11) + return "e⁺"; + if (pdgId == 13) + return "μ⁻"; + if (pdgId == -13) + return "μ⁺"; + if (pdgId == 15) + return "τ⁻"; + if (pdgId == -15) + return "τ⁺"; + + if (pdgId == 12) + return "νₑ"; + if (pdgId == -12) + return "ν̄ₑ"; + if (pdgId == 14) + return "ν_μ"; + if (pdgId == -14) + return "ν̄_μ"; + if (pdgId == 16) + return "ν_τ"; + if (pdgId == -16) + return "ν̄_τ"; + + if (pdgId == 22) + return "γ"; + if (pdgId == 21) + return "g"; + if (pdgId == 23) + return "Z⁰"; + if (pdgId == 24) + return "W⁺"; + if (pdgId == -24) + return "W⁻"; + if (pdgId == 25) + return "H"; + + if (pdgId == 2212) + return "p"; + if (pdgId == -2212) + return "p̄"; + if (pdgId == 2112) + return "n"; + if (pdgId == -2112) + return "n̄"; + + if (pdgId == 111) + return "π⁰"; + if (pdgId == 211) + return "π⁺"; + if (pdgId == -211) + return "π⁻"; + if (pdgId == 321) + return "K⁺"; + if (pdgId == -321) + return "K⁻"; + if (pdgId == 130) + return "K⁰_L"; + if (pdgId == 310) + return "K⁰_S"; + + if (ap >= 1 && ap <= 6) { + static const char* qname[7] = {"", "d", "u", "s", "c", "b", "t"}; + std::string s = qname[ap]; + if (pdgId < 0) + s = "anti-" + s; + return s; + } + + return "pdg"; + } + + std::string pdgLabel(int pdgId) { + std::ostringstream ss; + const std::string name = pdgNameUtf8(pdgId); + if (name == "pdg") + ss << "pdg(" << pdgId << ")"; + else + ss << name << " (" << pdgId << ")"; + return ss.str(); + } + + const char* rawKindName(TruthGraph::NodeKind k) { + switch (k) { + case TruthGraph::NodeKind::GenEvent: + return "GenEvent"; + case TruthGraph::NodeKind::GenVertex: + return "GenVertex"; + case TruthGraph::NodeKind::GenParticle: + return "GenParticle"; + case TruthGraph::NodeKind::SimVertex: + return "SimVertex"; + case TruthGraph::NodeKind::SimTrack: + return "SimTrack"; + } + return "Unknown"; + } + + std::string rawNodeSummary(TruthGraph const* raw, int32_t nodeId) { + if (raw == nullptr || nodeId < 0 || static_cast(nodeId) >= raw->nNodes()) + return "n/a"; + auto const& r = raw->nodeRef(static_cast(nodeId)); + std::ostringstream ss; + ss << rawKindName(r.kind) << " #" << nodeId << " key=" << r.key; + return ss.str(); + } + + template + std::string fmtP4(P4 const& p4) { + std::ostringstream ss; + ss.setf(std::ios::fixed); + ss.precision(3); + ss << "(" << p4.px() << ", " << p4.py() << ", " << p4.pz() << ", " << p4.e() << ")"; + return ss.str(); + } + + const char* logicalVertexDomain(truth::VertexData const& d) { + if (d.hasGen() && !d.hasSim()) + return "GEN"; + if (!d.hasGen() && d.hasSim()) + return "SIM"; + if (d.hasGen() && d.hasSim()) + return "GEN+SIM"; + return "UNKNOWN"; + } + +} // namespace + +class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { +public: + explicit TruthLogicalGraphDumper(const edm::ParameterSet& cfg) + : token_(consumes(cfg.getParameter("src"))), + rawToken_(mayConsume(cfg.getParameter("rawSrc"))), + dotFile_(cfg.getParameter("dotFile")), + maxParticles_(cfg.getParameter("maxParticles")), + maxVertices_(cfg.getParameter("maxVertices")), + maxEdgesPerNode_(cfg.getParameter("maxEdgesPerNode")) {} + + static void fillDescriptions(edm::ConfigurationDescriptions& descriptions) { + edm::ParameterSetDescription desc; + desc.add("src", edm::InputTag("truthLogicalGraphProducer")); + desc.add("rawSrc", edm::InputTag("truthGraphProducer")) + ->setComment("Optional raw TruthGraph used only to enrich labels"); + desc.add("dotFile", "truthlogicalgraph.dot"); + desc.add("maxParticles", 5000)->setComment("Truncate logical particle nodes"); + desc.add("maxVertices", 5000)->setComment("Truncate logical vertex nodes"); + desc.add("maxEdgesPerNode", 200)->setComment("Truncate fanout per node"); + descriptions.addWithDefaultLabel(desc); + } + + void analyze(const edm::Event& evt, const edm::EventSetup&) override { + auto const& g = evt.get(token_); + + edm::Handle hRaw; + evt.getByToken(rawToken_, hRaw); + TruthGraph const* raw = hRaw.isValid() ? &(*hRaw) : nullptr; + + std::ofstream os(dotFile_); + os << "digraph TruthLogicalGraph {\n"; + os << " rankdir=LR;\n"; + os << " node [fontsize=10];\n"; + + const uint32_t nParticles = std::min(g.nParticles(), maxParticles_); + const uint32_t nVertices = std::min(g.nVertices(), maxVertices_); + + // ------------------------------------------------------------------ + // Particle nodes + // ------------------------------------------------------------------ + for (uint32_t i = 0; i < nParticles; ++i) { + auto p = g.particle(i); + auto const& d = p.data(); + + os << " p" << i << " [shape=ellipse"; + + if (p.hasCheckpoints()) { + os << ", color=\"red\", penwidth=2"; + } else if (d.hasGen() && d.hasSim()) { + os << ", penwidth=2"; + } else if (d.hasGen()) { + os << ", color=\"blue\""; + } else if (d.hasSim()) { + os << ", color=\"darkgreen\""; + } + + os << ", label=<\n"; + os << " \n"; + os << " \n"; + + if (d.pdgId != 0) + os << " \n"; + if (d.status != 0) + os << " \n"; + if (d.eventId != 0) + os << " \n"; + if (d.genEvent >= 0) + os << " \n"; + + os << " \n"; + + os << " \n"; + + os << " \n"; + + os << " \n"; + + os << " \n"; + + os << " \n"; + + for (auto const& cp : d.checkpoints) { + os << " \n"; + os << " \n"; + os << " \n"; + } + + if (raw != nullptr) { + os << " \n"; + os << " \n"; + } + + os << "
Particle " << i << "
pid: " << pdgLabel(d.pdgId) << "
status: " << d.status << "
eid: " << d.eventId << "
genEvent: " << d.genEvent << "
hasGen: " << (d.hasGen() ? "yes" : "no") + << " hasSim: " << (d.hasSim() ? "yes" : "no") << "
isRoot: " << (p.isRoot() ? "yes" : "no") + << " isLeaf: " << (p.isLeaf() ? "yes" : "no") << "
p4: " << fmtP4(d.momentum) << "
nProdVtx: " << p.productionVertices().size() + << " nDecayVtx: " << p.decayVertices().size() << "
nParents: " << p.parents().size() + << " nChildren: " << p.children().size() << "
nCheckpoints: " << d.checkpoints.size() << "
checkpointId: " << cp.checkpointId << "
x4@checkpoint: " << fmtP4(cp.position) << "
p4@checkpoint: " << fmtP4(cp.momentum) << "
raw GEN: " << rawNodeSummary(raw, d.genNode) << "
raw SIM: " << rawNodeSummary(raw, d.simNode) << "
\n"; + os << " >];\n"; + } + + // ------------------------------------------------------------------ + // Vertex nodes + // ------------------------------------------------------------------ + for (uint32_t i = 0; i < nVertices; ++i) { + auto v = g.vertex(i); + auto const& d = v.data(); + + os << " v" << i << " [shape=diamond"; + + if (d.hasGen() && d.hasSim()) { + os << ", color=\"purple\", penwidth=2"; + } else if (d.hasGen()) { + os << ", color=\"blue\""; + } else if (d.hasSim()) { + os << ", color=\"darkgreen\""; + } + + os << ", label=<\n"; + os << " \n"; + os << " \n"; + + os << " \n"; + + if (d.eventId != 0) + os << " \n"; + if (d.genEvent >= 0) + os << " \n"; + + os << " \n"; + + os << " \n"; + + os << " \n"; + + os << " \n"; + + if (raw != nullptr) { + os << " \n"; + os << " \n"; + } + + os << "
Vertex " << i << "
domain: " << logicalVertexDomain(d) << "
eid: " << d.eventId << "
genEvent: " << d.genEvent << "
hasGen: " << (d.hasGen() ? "yes" : "no") + << " hasSim: " << (d.hasSim() ? "yes" : "no") << "
isSource: " << (v.isSource() ? "yes" : "no") + << " isSink: " << (v.isSink() ? "yes" : "no") << "
x4: " << fmtP4(d.position) << "
nIn: " << v.incomingParticles().size() + << " nOut: " << v.outgoingParticles().size() << "
raw GEN: " << rawNodeSummary(raw, d.genNode) << "
raw SIM: " << rawNodeSummary(raw, d.simNode) << "
\n"; + os << " >];\n"; + } + + // ------------------------------------------------------------------ + // Edges: physics-forward only + // ------------------------------------------------------------------ + for (uint32_t i = 0; i < nParticles; ++i) { + unsigned kept = 0; + for (uint32_t v : g.decayVertices(i)) { + if (v >= nVertices) + continue; + os << " p" << i << " -> v" << v << ";\n"; + if (++kept >= maxEdgesPerNode_) + break; + } + } + + for (uint32_t i = 0; i < nVertices; ++i) { + unsigned kept = 0; + for (uint32_t p : g.outgoingParticles(i)) { + if (p >= nParticles) + continue; + os << " v" << i << " -> p" << p << ";\n"; + if (++kept >= maxEdgesPerNode_) + break; + } + } + + os << "}\n"; + os.close(); + } + +private: + edm::EDGetTokenT token_; + edm::EDGetTokenT rawToken_; + + std::string dotFile_; + unsigned maxParticles_; + unsigned maxVertices_; + unsigned maxEdgesPerNode_; +}; + +DEFINE_FWK_MODULE(TruthLogicalGraphDumper); \ No newline at end of file diff --git a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc new file mode 100644 index 0000000000000..b49ccf1c298eb --- /dev/null +++ b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc @@ -0,0 +1,507 @@ +// Author: Felice Pantaleo - CERN +// Date: 03/2026 +// +// Build a logical truth::Graph from the raw heterogeneous TruthGraph. +// The topology comes from the raw TruthGraph. +// Standalone payload (momentum/position/checkpoints) is materialized from optional +// HepMC2 / HepMC3 / SimTrack / SimVertex inputs. +// +// For debugging purposes, GenVertex and SimVertex are intentionally NOT merged. +// Each raw vertex node becomes its own logical truth::Vertex. + +#include +#include +#include +#include +#include +#include + +#include "FWCore/Framework/interface/Event.h" +#include "FWCore/Framework/interface/EventSetup.h" +#include "FWCore/Framework/interface/MakerMacros.h" +#include "FWCore/Framework/interface/stream/EDProducer.h" +#include "FWCore/ParameterSet/interface/ConfigurationDescriptions.h" +#include "FWCore/ParameterSet/interface/ParameterSet.h" +#include "FWCore/ParameterSet/interface/ParameterSetDescription.h" +#include "FWCore/Utilities/interface/Exception.h" +#include "FWCore/Utilities/interface/InputTag.h" + +#include "DataFormats/Math/interface/LorentzVector.h" + +#include "SimDataFormats/Track/interface/SimTrackContainer.h" +#include "SimDataFormats/Vertex/interface/SimVertexContainer.h" + +// Legacy HepMC (HepMC2) +#include "SimDataFormats/GeneratorProducts/interface/HepMCProduct.h" +#include "HepMC/GenEvent.h" +#include "HepMC/GenParticle.h" +#include "HepMC/GenVertex.h" + +// HepMC3 +#include "SimDataFormats/GeneratorProducts/interface/HepMC3Product.h" +#include "HepMC3/GenEvent.h" +#include "HepMC3/GenParticle.h" +#include "HepMC3/GenVertex.h" + +#include "PhysicsTools/TruthInfo/interface/Graph.h" +#include "PhysicsTools/TruthInfo/interface/TruthGraph.h" + +namespace { + + struct DSU { + std::vector parent; + std::vector rank; + + explicit DSU(int n) : parent(n), rank(n, 0) { + for (int i = 0; i < n; ++i) + parent[i] = i; + } + + int find(int x) { + while (parent[x] != x) { + parent[x] = parent[parent[x]]; + x = parent[x]; + } + return x; + } + + void unite(int a, int b) { + a = find(a); + b = find(b); + if (a == b) + return; + if (rank[a] < rank[b]) + std::swap(a, b); + parent[b] = a; + if (rank[a] == rank[b]) + ++rank[a]; + } + }; + + struct GenParticlePayload { + int32_t pdgId = 0; + int16_t status = 0; + math::XYZTLorentzVectorD momentum; + }; + + struct GenVertexPayload { + math::XYZTLorentzVectorD position; + }; + + bool isParticleKind(TruthGraph::NodeKind kind) { + return kind == TruthGraph::NodeKind::GenParticle || kind == TruthGraph::NodeKind::SimTrack; + } + + bool isVertexKind(TruthGraph::NodeKind kind) { + return kind == TruthGraph::NodeKind::GenVertex || kind == TruthGraph::NodeKind::SimVertex; + } + + bool isGenParticleToSimTrack(TruthGraph const& g, uint32_t src, uint32_t dst) { + auto const& s = g.nodeRef(src); + auto const& d = g.nodeRef(dst); + return s.kind == TruthGraph::NodeKind::GenParticle && d.kind == TruthGraph::NodeKind::SimTrack; + } + + void buildCSR(uint32_t nSources, + std::vector>& pairs, + std::vector& offsets, + std::vector& flat) { + std::sort(pairs.begin(), pairs.end()); + pairs.erase(std::unique(pairs.begin(), pairs.end()), pairs.end()); + + offsets.assign(nSources + 1, 0); + for (auto const& e : pairs) { + ++offsets[e.first + 1]; + } + for (uint32_t i = 1; i <= nSources; ++i) { + offsets[i] += offsets[i - 1]; + } + + flat.assign(pairs.size(), 0); + auto cursor = offsets; + for (auto const& e : pairs) { + flat[cursor[e.first]++] = e.second; + } + } + + template + bool validHandle(HandleT const& h) { + return h.isValid(); + } + + void fillGenPayloadFromHepMC2(HepMC::GenEvent const& ev, + std::unordered_map& particlePayload, + std::unordered_map& vertexPayload) { + particlePayload.reserve(ev.particles_size() * 2); + vertexPayload.reserve(ev.vertices_size() * 2); + + for (auto p = ev.particles_begin(); p != ev.particles_end(); ++p) { + if (*p == nullptr) + continue; + const int bc = (*p)->barcode(); + GenParticlePayload payload; + payload.pdgId = (*p)->pdg_id(); + payload.status = static_cast((*p)->status()); + payload.momentum = math::XYZTLorentzVectorD( + (*p)->momentum().px(), (*p)->momentum().py(), (*p)->momentum().pz(), (*p)->momentum().e()); + particlePayload.emplace(bc, payload); + } + + for (auto v = ev.vertices_begin(); v != ev.vertices_end(); ++v) { + if (*v == nullptr) + continue; + const int bc = (*v)->barcode(); + GenVertexPayload payload; + payload.position = math::XYZTLorentzVectorD( + (*v)->position().x(), (*v)->position().y(), (*v)->position().z(), (*v)->position().t()); + vertexPayload.emplace(bc, payload); + } + } + + void fillGenPayloadFromHepMC3(HepMC3::GenEvent const& ev, + std::unordered_map& particlePayload, + std::unordered_map& vertexPayload) { + particlePayload.reserve(ev.particles().size() * 2); + vertexPayload.reserve(ev.vertices().size() * 2); + + for (auto const& pptr : ev.particles()) { + if (!pptr) + continue; + const int bc = pptr->id(); + GenParticlePayload payload; + payload.pdgId = pptr->pid(); + payload.status = static_cast(pptr->status()); + payload.momentum = + math::XYZTLorentzVectorD(pptr->momentum().px(), pptr->momentum().py(), pptr->momentum().pz(), pptr->momentum().e()); + particlePayload.emplace(bc, payload); + } + + for (auto const& vptr : ev.vertices()) { + if (!vptr) + continue; + const int bc = vptr->id(); + GenVertexPayload payload; + payload.position = + math::XYZTLorentzVectorD(vptr->position().x(), vptr->position().y(), vptr->position().z(), vptr->position().t()); + vertexPayload.emplace(bc, payload); + } + } + +} // namespace + +class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { +public: + explicit TruthLogicalGraphProducer(edm::ParameterSet const& cfg) + : srcToken_(consumes(cfg.getParameter("src"))), + simTrackToken_(mayConsume(cfg.getParameter("simTracks"))), + simVertexToken_(mayConsume(cfg.getParameter("simVertices"))), + hepmc3Token_(mayConsume(cfg.getParameter("genEventHepMC3"))), + hepmc2Token_(mayConsume(cfg.getParameter("genEventHepMC"))) { + produces(); + } + + static void fillDescriptions(edm::ConfigurationDescriptions& descriptions) { + edm::ParameterSetDescription desc; + desc.add("src", edm::InputTag("truthGraphProducer")); + desc.add("simTracks", edm::InputTag("g4SimHits")); + desc.add("simVertices", edm::InputTag("g4SimHits")); + desc.add("genEventHepMC3", edm::InputTag("generatorSmeared")); + desc.add("genEventHepMC", edm::InputTag("generatorSmeared")); + descriptions.addWithDefaultLabel(desc); + } + + void produce(edm::Event& evt, edm::EventSetup const&) override { + auto const& raw = evt.get(srcToken_); + + if (!raw.isConsistent()) { + throw cms::Exception("TruthLogicalGraphProducer") << "Input TruthGraph is not consistent"; + } + + auto out = std::make_unique(); + + const uint32_t nRawNodes = raw.nNodes(); + + edm::Handle hSimTracks; + evt.getByToken(simTrackToken_, hSimTracks); + + edm::Handle hSimVertices; + evt.getByToken(simVertexToken_, hSimVertices); + + std::unordered_map simTrackIdToIndex; + if (validHandle(hSimTracks)) { + simTrackIdToIndex.reserve(hSimTracks->size() * 2); + for (uint32_t i = 0; i < hSimTracks->size(); ++i) { + simTrackIdToIndex.emplace((*hSimTracks)[i].trackId(), i); + } + } + + std::unordered_map genParticlePayload; + std::unordered_map genVertexPayload; + + bool haveGenPayload = false; + + { + edm::Handle h3; + evt.getByToken(hepmc3Token_, h3); + if (validHandle(h3) && h3->GetEvent() != nullptr) { + const HepMC3::GenEventData* data = h3->GetEvent(); + HepMC3::GenEvent ev3; + ev3.read_data(*data); + fillGenPayloadFromHepMC3(ev3, genParticlePayload, genVertexPayload); + haveGenPayload = true; + } + } + + if (!haveGenPayload) { + edm::Handle h2; + evt.getByToken(hepmc2Token_, h2); + if (validHandle(h2) && h2->GetEvent() != nullptr) { + fillGenPayloadFromHepMC2(*h2->GetEvent(), genParticlePayload, genVertexPayload); + haveGenPayload = true; + } + } + + // ------------------------------------------------------------------ + // 1. Temporary ids + // ------------------------------------------------------------------ + std::vector rawToParticleTmp(nRawNodes, -1); + std::vector rawToVertex(nRawNodes, -1); + + int nParticleTmp = 0; + int nVertexLogical = 0; + + for (uint32_t nodeId = 0; nodeId < nRawNodes; ++nodeId) { + const auto kind = raw.nodeRef(nodeId).kind; + if (isParticleKind(kind)) { + rawToParticleTmp[nodeId] = nParticleTmp++; + } else if (isVertexKind(kind)) { + rawToVertex[nodeId] = nVertexLogical++; + } + } + + DSU particleDSU(nParticleTmp); + + // ------------------------------------------------------------------ + // 2. Merge only particles across GEN <-> SIM + // ------------------------------------------------------------------ + for (uint32_t nodeId = 0; nodeId < nRawNodes; ++nodeId) { + const auto& ref = raw.nodeRef(nodeId); + if (ref.kind != TruthGraph::NodeKind::SimTrack) + continue; + + const int32_t genNode = raw.nodeSimTrackToGen(nodeId); + if (genNode < 0) + continue; + if (static_cast(genNode) >= nRawNodes) + continue; + if (raw.nodeRef(static_cast(genNode)).kind != TruthGraph::NodeKind::GenParticle) + continue; + + particleDSU.unite(rawToParticleTmp[nodeId], rawToParticleTmp[static_cast(genNode)]); + } + + for (uint32_t src = 0; src < nRawNodes; ++src) { + const auto dsts = raw.children(src); + const auto ekinds = raw.childrenEdgeKinds(src); + + for (std::size_t i = 0; i < dsts.size(); ++i) { + const uint32_t dst = dsts[i]; + const auto ek = static_cast(ekinds[i]); + + if (ek != TruthGraph::EdgeKind::GenToSim && ek != TruthGraph::EdgeKind::SimToGen) + continue; + + if (isGenParticleToSimTrack(raw, src, dst)) { + particleDSU.unite(rawToParticleTmp[src], rawToParticleTmp[dst]); + } + } + } + + // ------------------------------------------------------------------ + // 3. Compress particle representatives + // ------------------------------------------------------------------ + std::unordered_map particleRepToLogical; + std::vector rawToParticle(nRawNodes, -1); + + for (uint32_t nodeId = 0; nodeId < nRawNodes; ++nodeId) { + if (rawToParticleTmp[nodeId] >= 0) { + const int rep = particleDSU.find(rawToParticleTmp[nodeId]); + auto result = particleRepToLogical.emplace(rep, static_cast(particleRepToLogical.size())); + rawToParticle[nodeId] = static_cast(result.first->second); + } + } + + out->particles.resize(particleRepToLogical.size()); + out->vertices.resize(nVertexLogical); + + // ------------------------------------------------------------------ + // 4. Fill payload + // ------------------------------------------------------------------ + for (uint32_t nodeId = 0; nodeId < nRawNodes; ++nodeId) { + const auto& ref = raw.nodeRef(nodeId); + + if (rawToParticle[nodeId] >= 0) { + auto& p = out->particles[static_cast(rawToParticle[nodeId])]; + + if (ref.kind == TruthGraph::NodeKind::GenParticle) { + p.genNode = static_cast(nodeId); + + if (nodeId < raw.genEventOfNode.size()) + p.genEvent = raw.genEventOfNode[nodeId]; + + if (p.pdgId == 0) + p.pdgId = raw.nodePdgId(nodeId); + if (p.status == 0) + p.status = raw.nodeStatus(nodeId); + + if (haveGenPayload) { + const int barcode = static_cast(ref.key); + auto it = genParticlePayload.find(barcode); + if (it != genParticlePayload.end()) { + if (p.pdgId == 0) + p.pdgId = it->second.pdgId; + if (p.status == 0) + p.status = it->second.status; + if (!p.hasSim()) + p.momentum = it->second.momentum; + } + } + + } else if (ref.kind == TruthGraph::NodeKind::SimTrack) { + p.simNode = static_cast(nodeId); + + if (p.pdgId == 0) + p.pdgId = raw.nodePdgId(nodeId); + if (p.status == 0) + p.status = raw.nodeStatus(nodeId); + if (p.eventId == 0) + p.eventId = raw.nodeEventId(nodeId); + + if (validHandle(hSimTracks)) { + const auto trackId = static_cast(ref.key); + auto it = simTrackIdToIndex.find(trackId); + if (it != simTrackIdToIndex.end()) { + auto const& t = (*hSimTracks)[it->second]; + p.momentum = + math::XYZTLorentzVectorD(t.momentum().px(), t.momentum().py(), t.momentum().pz(), t.momentum().e()); + + if (t.crossedBoundary()) { + truth::Checkpoint cp; + cp.checkpointId = 0; + + const auto& xb = t.getPositionAtBoundary(); + cp.position = math::XYZTLorentzVectorF(xb.x(), xb.y(), xb.z(), xb.t()); + + const auto& pb = t.getMomentumAtBoundary(); + cp.momentum = math::XYZTLorentzVectorF(pb.px(), pb.py(), pb.pz(), pb.e()); + + p.checkpoints.push_back(cp); + } + } + } + } + } + + if (rawToVertex[nodeId] >= 0) { + auto& v = out->vertices[static_cast(rawToVertex[nodeId])]; + + if (ref.kind == TruthGraph::NodeKind::GenVertex) { + v.genNode = static_cast(nodeId); + + if (nodeId < raw.genEventOfNode.size()) + v.genEvent = raw.genEventOfNode[nodeId]; + + if (haveGenPayload) { + const int barcode = static_cast(ref.key); + auto it = genVertexPayload.find(barcode); + if (it != genVertexPayload.end()) + v.position = it->second.position; + } + + } else if (ref.kind == TruthGraph::NodeKind::SimVertex) { + v.simNode = static_cast(nodeId); + + if (v.eventId == 0) + v.eventId = raw.nodeEventId(nodeId); + + if (validHandle(hSimVertices)) { + const auto simIndex = static_cast(ref.key); + if (simIndex < hSimVertices->size()) { + auto const& sv = (*hSimVertices)[simIndex]; + const auto& pos = sv.position(); + v.position = math::XYZTLorentzVectorD(pos.x(), pos.y(), pos.z(), pos.t()); + } + } + } + } + } + + // ------------------------------------------------------------------ + // 5. Rebuild logical graph + // ------------------------------------------------------------------ + std::vector> particleToDecayVertexPairs; + std::vector> particleToProductionVertexPairs; + std::vector> vertexToOutgoingParticlePairs; + std::vector> vertexToIncomingParticlePairs; + + for (uint32_t src = 0; src < nRawNodes; ++src) { + const auto& srcRef = raw.nodeRef(src); + const auto dsts = raw.children(src); + + for (uint32_t dst : dsts) { + const auto& dstRef = raw.nodeRef(dst); + + if (isVertexKind(srcRef.kind) && isParticleKind(dstRef.kind)) { + const int32_t lv = rawToVertex[src]; + const int32_t lp = rawToParticle[dst]; + if (lv >= 0 && lp >= 0) { + vertexToOutgoingParticlePairs.emplace_back(static_cast(lv), static_cast(lp)); + particleToProductionVertexPairs.emplace_back(static_cast(lp), static_cast(lv)); + } + } else if (isParticleKind(srcRef.kind) && isVertexKind(dstRef.kind)) { + const int32_t lp = rawToParticle[src]; + const int32_t lv = rawToVertex[dst]; + if (lp >= 0 && lv >= 0) { + particleToDecayVertexPairs.emplace_back(static_cast(lp), static_cast(lv)); + vertexToIncomingParticlePairs.emplace_back(static_cast(lv), static_cast(lp)); + } + } + } + } + + buildCSR(out->nParticles(), + particleToDecayVertexPairs, + out->particleToDecayVertexOffsets, + out->particleToDecayVertices); + + buildCSR(out->nParticles(), + particleToProductionVertexPairs, + out->particleToProductionVertexOffsets, + out->particleToProductionVertices); + + buildCSR(out->nVertices(), + vertexToOutgoingParticlePairs, + out->vertexToOutgoingParticleOffsets, + out->vertexToOutgoingParticles); + + buildCSR(out->nVertices(), + vertexToIncomingParticlePairs, + out->vertexToIncomingParticleOffsets, + out->vertexToIncomingParticles); + + if (!out->isConsistent()) { + throw cms::Exception("TruthLogicalGraphProducer") << "Produced truth::Graph is not consistent"; + } + + evt.put(std::move(out)); + } + +private: + edm::EDGetTokenT srcToken_; + edm::EDGetTokenT simTrackToken_; + edm::EDGetTokenT simVertexToken_; + edm::EDGetTokenT hepmc3Token_; + edm::EDGetTokenT hepmc2Token_; +}; + +DEFINE_FWK_MODULE(TruthLogicalGraphProducer); \ No newline at end of file diff --git a/PhysicsTools/TruthInfo/src/Graph.cc b/PhysicsTools/TruthInfo/src/Graph.cc new file mode 100644 index 0000000000000..c76a3c7879a50 --- /dev/null +++ b/PhysicsTools/TruthInfo/src/Graph.cc @@ -0,0 +1,451 @@ +#include "PhysicsTools/TruthInfo/interface/Graph.h" + +#include +#include +#include + +namespace { + bool checkCSR(std::vector const& offsets, std::vector const& edges, std::size_t nObjects) { + if (offsets.size() != nObjects + 1) + return false; + if (!offsets.empty() && offsets.front() != 0) + return false; + if (!offsets.empty() && offsets.back() != edges.size()) + return false; + + for (std::size_t i = 1; i < offsets.size(); ++i) { + if (offsets[i] < offsets[i - 1]) + return false; + } + return true; + } + + bool checkTargets(std::vector const& edges, uint32_t limit) { + for (auto v : edges) { + if (v >= limit) + return false; + } + return true; + } +} // namespace + +const truth::ParticleData& truth::Particle::data() const { + return graph_->particles.at(id_); +} + +bool truth::Particle::hasGen() const { + return data().hasGen(); +} + +bool truth::Particle::hasSim() const { + return data().hasSim(); +} + +int32_t truth::Particle::pdgId() const { + return data().pdgId; +} + +int16_t truth::Particle::status() const { + return data().status; +} + +uint64_t truth::Particle::eventId() const { + return data().eventId; +} + +int32_t truth::Particle::genEvent() const { + return data().genEvent; +} + +const math::XYZTLorentzVectorD& truth::Particle::momentum() const { + return data().momentum; +} + +std::span truth::Particle::checkpoints() const { + return std::span(data().checkpoints.data(), data().checkpoints.size()); +} + +bool truth::Particle::hasCheckpoints() const { + return !data().checkpoints.empty(); +} + +std::optional truth::Particle::checkpoint(uint32_t checkpointId) const { + for (auto const& cp : data().checkpoints) { + if (cp.checkpointId == checkpointId) + return cp; + } + return std::nullopt; +} + +bool truth::Particle::isRoot() const { + return valid() && graph_->productionVertices(id_).empty(); +} + +bool truth::Particle::isLeaf() const { + return valid() && graph_->decayVertices(id_).empty(); +} + +std::vector truth::Particle::productionVertices() const { + return valid() ? graph_->productionVerticesOf(id_) : std::vector{}; +} + +std::vector truth::Particle::decayVertices() const { + return valid() ? graph_->decayVerticesOf(id_) : std::vector{}; +} + +std::vector truth::Particle::parents() const { + return valid() ? graph_->parentsOf(id_) : std::vector{}; +} + +std::vector truth::Particle::children() const { + return valid() ? graph_->childrenOf(id_) : std::vector{}; +} + +std::vector truth::Particle::ancestors() const { + return valid() ? graph_->ancestorsOf(id_) : std::vector{}; +} + +std::vector truth::Particle::descendants() const { + return valid() ? graph_->descendantsOf(id_) : std::vector{}; +} + +bool truth::Particle::hasAncestorPdgId(int pdgId) const { + return firstAncestorWithPdgId(pdgId).has_value(); +} + +std::optional truth::Particle::firstAncestorWithPdgId(int pdgId) const { + return valid() ? graph_->firstAncestorWithPdgIdOf(id_, pdgId) : std::nullopt; +} + +std::optional truth::Particle::firstCommonAncestor(Particle other) const { + if (!valid() || !other.valid() || graph_ != other.graph_) + return std::nullopt; + return graph_->firstCommonAncestorOf(id_, other.id_); +} + +const truth::VertexData& truth::Vertex::data() const { + return graph_->vertices.at(id_); +} + +bool truth::Vertex::hasGen() const { + return data().hasGen(); +} + +bool truth::Vertex::hasSim() const { + return data().hasSim(); +} + +uint64_t truth::Vertex::eventId() const { + return data().eventId; +} + +int32_t truth::Vertex::genEvent() const { + return data().genEvent; +} + +const math::XYZTLorentzVectorD& truth::Vertex::position() const { + return data().position; +} + +bool truth::Vertex::isSource() const { + return valid() && graph_->incomingParticles(id_).empty(); +} + +bool truth::Vertex::isSink() const { + return valid() && graph_->outgoingParticles(id_).empty(); +} + +std::vector truth::Vertex::incomingParticles() const { + return valid() ? graph_->incomingParticlesOf(id_) : std::vector{}; +} + +std::vector truth::Vertex::outgoingParticles() const { + return valid() ? graph_->outgoingParticlesOf(id_) : std::vector{}; +} + +truth::Particle truth::Graph::particle(size_type id) const { + return id < nParticles() ? Particle(this, id) : Particle{}; +} + +truth::Vertex truth::Graph::vertex(size_type id) const { + return id < nVertices() ? Vertex(this, id) : Vertex{}; +} + +std::vector truth::Graph::particleViews() const { + std::vector out; + out.reserve(nParticles()); + for (size_type i = 0; i < nParticles(); ++i) + out.emplace_back(this, i); + return out; +} + +std::vector truth::Graph::vertexViews() const { + std::vector out; + out.reserve(nVertices()); + for (size_type i = 0; i < nVertices(); ++i) + out.emplace_back(this, i); + return out; +} + +std::vector truth::Graph::roots() const { + std::vector out; + for (size_type i = 0; i < nParticles(); ++i) { + if (productionVertices(i).empty()) + out.emplace_back(this, i); + } + return out; +} + +std::vector truth::Graph::leaves() const { + std::vector out; + for (size_type i = 0; i < nParticles(); ++i) { + if (decayVertices(i).empty()) + out.emplace_back(this, i); + } + return out; +} + +std::vector truth::Graph::sourceVertices() const { + std::vector out; + for (size_type i = 0; i < nVertices(); ++i) { + if (incomingParticles(i).empty()) + out.emplace_back(this, i); + } + return out; +} + +std::vector truth::Graph::sinkVertices() const { + std::vector out; + for (size_type i = 0; i < nVertices(); ++i) { + if (outgoingParticles(i).empty()) + out.emplace_back(this, i); + } + return out; +} + +std::vector truth::Graph::productionVerticesOf(size_type particleId) const { + std::vector out; + for (uint32_t v : productionVertices(particleId)) + out.emplace_back(this, v); + return out; +} + +std::vector truth::Graph::decayVerticesOf(size_type particleId) const { + std::vector out; + for (uint32_t v : decayVertices(particleId)) + out.emplace_back(this, v); + return out; +} + +std::vector truth::Graph::incomingParticlesOf(size_type vertexId) const { + std::vector out; + for (uint32_t p : incomingParticles(vertexId)) + out.emplace_back(this, p); + return out; +} + +std::vector truth::Graph::outgoingParticlesOf(size_type vertexId) const { + std::vector out; + for (uint32_t p : outgoingParticles(vertexId)) + out.emplace_back(this, p); + return out; +} + +std::vector truth::Graph::parentsOf(size_type particleId) const { + std::vector out; + if (particleId >= nParticles()) + return out; + + std::vector seen(nParticles(), 0); + + for (uint32_t v : productionVertices(particleId)) { + for (uint32_t p : incomingParticles(v)) { + if (p == particleId) + continue; + if (!seen[p]) { + seen[p] = 1; + out.emplace_back(this, p); + } + } + } + return out; +} + +std::vector truth::Graph::childrenOf(size_type particleId) const { + std::vector out; + if (particleId >= nParticles()) + return out; + + std::vector seen(nParticles(), 0); + + for (uint32_t v : decayVertices(particleId)) { + for (uint32_t p : outgoingParticles(v)) { + if (p == particleId) + continue; + if (!seen[p]) { + seen[p] = 1; + out.emplace_back(this, p); + } + } + } + return out; +} + +std::vector truth::Graph::ancestorsOf(size_type particleId) const { + std::vector out; + if (particleId >= nParticles()) + return out; + + std::vector dist(nParticles(), -1); + std::queue q; + + for (auto const& parent : parentsOf(particleId)) { + dist[parent.id()] = 1; + q.push(parent.id()); + out.push_back(parent); + } + + while (!q.empty()) { + const uint32_t cur = q.front(); + q.pop(); + + for (auto const& parent : parentsOf(cur)) { + if (dist[parent.id()] >= 0) + continue; + dist[parent.id()] = dist[cur] + 1; + q.push(parent.id()); + out.push_back(parent); + } + } + + return out; +} + +std::vector truth::Graph::descendantsOf(size_type particleId) const { + std::vector out; + if (particleId >= nParticles()) + return out; + + std::vector dist(nParticles(), -1); + std::queue q; + + for (auto const& child : childrenOf(particleId)) { + dist[child.id()] = 1; + q.push(child.id()); + out.push_back(child); + } + + while (!q.empty()) { + const uint32_t cur = q.front(); + q.pop(); + + for (auto const& child : childrenOf(cur)) { + if (dist[child.id()] >= 0) + continue; + dist[child.id()] = dist[cur] + 1; + q.push(child.id()); + out.push_back(child); + } + } + + return out; +} + +std::optional truth::Graph::firstAncestorWithPdgIdOf(size_type particleId, int pdgId) const { + if (particleId >= nParticles()) + return std::nullopt; + + std::vector seen(nParticles(), 0); + std::queue q; + + for (auto const& parent : parentsOf(particleId)) { + seen[parent.id()] = 1; + q.push(parent.id()); + } + + while (!q.empty()) { + const uint32_t cur = q.front(); + q.pop(); + + if (particles[cur].pdgId == pdgId) + return particle(cur); + + for (auto const& parent : parentsOf(cur)) { + if (seen[parent.id()]) + continue; + seen[parent.id()] = 1; + q.push(parent.id()); + } + } + + return std::nullopt; +} + +std::optional truth::Graph::firstCommonAncestorOf(size_type a, size_type b) const { + if (a >= nParticles() || b >= nParticles()) + return std::nullopt; + + std::vector distA(nParticles(), -1); + std::vector distB(nParticles(), -1); + + auto fillDistances = [this](uint32_t start, std::vector& dist) { + std::queue q; + dist[start] = 0; + q.push(start); + + while (!q.empty()) { + const uint32_t cur = q.front(); + q.pop(); + + for (auto const& parent : parentsOf(cur)) { + if (dist[parent.id()] >= 0) + continue; + dist[parent.id()] = dist[cur] + 1; + q.push(parent.id()); + } + } + }; + + fillDistances(a, distA); + fillDistances(b, distB); + + int bestId = -1; + int bestScore = -1; + int bestMaxDist = -1; + + for (uint32_t i = 0; i < nParticles(); ++i) { + if (distA[i] < 0 || distB[i] < 0) + continue; + + const int score = distA[i] + distB[i]; + const int maxDist = std::max(distA[i], distB[i]); + + if (bestId < 0 || score < bestScore || (score == bestScore && maxDist < bestMaxDist) || + (score == bestScore && maxDist == bestMaxDist && static_cast(i) < bestId)) { + bestId = static_cast(i); + bestScore = score; + bestMaxDist = maxDist; + } + } + + if (bestId < 0) + return std::nullopt; + + return particle(static_cast(bestId)); +} + +bool truth::Graph::isConsistent() const { + const bool p2dv = checkCSR(particleToDecayVertexOffsets, particleToDecayVertices, particles.size()) && + checkTargets(particleToDecayVertices, nVertices()); + + const bool p2pv = checkCSR(particleToProductionVertexOffsets, particleToProductionVertices, particles.size()) && + checkTargets(particleToProductionVertices, nVertices()); + + const bool v2op = checkCSR(vertexToOutgoingParticleOffsets, vertexToOutgoingParticles, vertices.size()) && + checkTargets(vertexToOutgoingParticles, nParticles()); + + const bool v2ip = checkCSR(vertexToIncomingParticleOffsets, vertexToIncomingParticles, vertices.size()) && + checkTargets(vertexToIncomingParticles, nParticles()); + + return p2dv && p2pv && v2op && v2ip; +} \ No newline at end of file diff --git a/PhysicsTools/TruthInfo/src/classes.h b/PhysicsTools/TruthInfo/src/classes.h index cd1768698f687..6262bd2b31383 100644 --- a/PhysicsTools/TruthInfo/src/classes.h +++ b/PhysicsTools/TruthInfo/src/classes.h @@ -1,14 +1,30 @@ #ifndef PhysicsTools_TruthInfo_src_classes_h #define PhysicsTools_TruthInfo_src_classes_h -#include "PhysicsTools/TruthInfo/interface/TruthGraph.h" #include "DataFormats/Common/interface/Wrapper.h" +#include "PhysicsTools/TruthInfo/interface/Graph.h" +#include "PhysicsTools/TruthInfo/interface/TruthGraph.h" namespace { struct dictionary { - TruthGraph graph; - edm::Wrapper wgraph; + TruthGraph rawTruthGraph; + edm::Wrapper wrappedRawTruthGraph; + + TruthGraph::NodeRef rawTruthNodeRef; + std::vector rawTruthNodeRefs; + + truth::Graph logicalTruthGraph; + edm::Wrapper wrappedLogicalTruthGraph; + + truth::Checkpoint logicalTruthCheckpoint; + std::vector logicalTruthCheckpoints; + + truth::ParticleData logicalTruthParticleData; + std::vector logicalTruthParticleDataVec; + + truth::VertexData logicalTruthVertexData; + std::vector logicalTruthVertexDataVec; }; } // namespace -#endif +#endif \ No newline at end of file diff --git a/PhysicsTools/TruthInfo/src/classes_def.xml b/PhysicsTools/TruthInfo/src/classes_def.xml index 1bd8980c3e6a5..8bc5f1ebe8202 100644 --- a/PhysicsTools/TruthInfo/src/classes_def.xml +++ b/PhysicsTools/TruthInfo/src/classes_def.xml @@ -3,4 +3,13 @@ + + + + + + + + + \ No newline at end of file From c816e203150ca575eb8f377c490b3434f77e211b Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Mon, 11 May 2026 10:24:58 +0200 Subject: [PATCH 07/53] Draft README --- PhysicsTools/TruthInfo/README.md | 675 ++++++++++++++++++ PhysicsTools/TruthInfo/interface/Graph.h | 10 +- PhysicsTools/TruthInfo/interface/TruthGraph.h | 4 +- .../plugins/TruthLogicalGraphDumper.cc | 30 +- .../plugins/TruthLogicalGraphProducer.cc | 16 +- PhysicsTools/TruthInfo/src/Graph.cc | 86 +-- PhysicsTools/TruthInfo/src/TruthGraph.cc | 2 +- PhysicsTools/TruthInfo/src/classes.h | 2 +- 8 files changed, 725 insertions(+), 100 deletions(-) create mode 100644 PhysicsTools/TruthInfo/README.md diff --git a/PhysicsTools/TruthInfo/README.md b/PhysicsTools/TruthInfo/README.md new file mode 100644 index 0000000000000..c9975ee48da81 --- /dev/null +++ b/PhysicsTools/TruthInfo/README.md @@ -0,0 +1,675 @@ +# TruthInfo prototype + +This package contains a prototype truth graph representation for CMSSW. The goal is to provide a compact, navigable, physics-oriented abstraction of the generator and simulation truth history of an event. + +The current implementation is split into two layers: + +1. `TruthGraph`: a compact raw graph built directly from existing CMS truth products. +2. `truth::Graph`: a higher-level logical graph exposing particles, vertices, payload, and navigation methods. + +The prototype is intended for validation, reconstruction studies, visualization, and future truth-reco association work. + +## Motivation + +CMS currently exposes truth information through several low-level collections, such as HepMC, GenParticles, SimTracks, SimVertices, TrackingParticles, SimClusters, and CaloParticles. These collections are useful, but they encode different views of the event history and are often tied to detector-specific or production-specific conventions. + +This package explores a different model: a single event-level truth graph that can be navigated using physics concepts. + +Typical questions this should make easier are: + +* Do two reconstructed objects come from the same parent particle? +* Did a given resonance, such as a Z boson, exist in the event history? +* Do two reconstructed objects come from the same Z boson? +* Which parton initiated a reconstructed jet? +* Is an object associated with the hard interaction or with pileup? +* Which detector-level interactions contributed to a reconstructed object? +* Should a reconstructed object be associated to a single truth particle, to a branch, or to an aggregated subgraph? + +The intended user-facing API should allow reconstruction and validation code to operate on stable physics abstractions rather than directly depending on the storage details of `GenParticle`, `SimTrack`, `GenVertex`, or `SimVertex`. + +## Package layout + +```text +PhysicsTools/TruthInfo/ + interface/ + TruthGraph.h + Graph.h + src/ + TruthGraph.cc + Graph.cc + classes.h + classes_def.xml + plugins/ + TruthGraphProducer.cc + TruthGraphDumper.cc + TruthLogicalGraphProducer.cc + TruthLogicalGraphDumper.cc + BuildFile.xml + python/ + truthGraphProducer_cfi.py + truthLogicalGraphDumper_cfi.py + BuildFile.xml +``` + +## Raw graph: `TruthGraph` + +`TruthGraph` is a compact, read-only graph representation of event truth information. It is designed as an intermediate event data product built from existing CMS collections. + +### Node types + +The raw graph supports the following node kinds: + +```cpp +enum class NodeKind : uint8_t { + GenEvent, + GenVertex, + GenParticle, + SimVertex, + SimTrack +}; +``` + +Each node stores a `NodeRef`: + +```cpp +struct NodeRef { + NodeKind kind; + int64_t key; +}; +``` + +The meaning of `key` depends on the node kind: + +* `GenEvent`: generator connected-component id. +* `GenVertex`: HepMC barcode or HepMC3 vertex id. +* `GenParticle`: HepMC barcode or HepMC3 particle id. +* `SimVertex`: index in the `SimVertexContainer`. +* `SimTrack`: `SimTrack::trackId()`. + +### Edge types + +The raw graph supports edge categories: + +```cpp +enum class EdgeKind : uint8_t { + Gen, + Sim, + GenToSim, + SimToGen +}; +``` + +At present: + +* `Gen` edges describe the generator graph. +* `Sim` edges describe the simulation graph. +* `GenToSim` edges connect matched generator particles or vertices to simulation nodes. +* `SimToGen` is reserved and is not produced by the current implementation. + +### Storage model + +Edges are stored in compressed sparse row form: + +```cpp +std::vector offsets; +std::vector edges; +std::vector edgeKind; +``` + +For node `i`, outgoing edges are stored in: + +```cpp +edges[offsets[i] ... offsets[i + 1]) +``` + +The corresponding edge kinds are stored in the same range of `edgeKind`. + +The class provides convenience accessors such as: + +```cpp +uint32_t nNodes() const; +uint32_t nEdges() const; +std::span children(uint32_t nodeId) const; +std::span childrenEdgeKinds(uint32_t nodeId) const; +const NodeRef& nodeRef(uint32_t nodeId) const; +bool isConsistent() const; +``` + +### Cached metadata + +The raw graph stores lightweight metadata arrays parallel to the node list: + +```cpp +std::vector pdgId; +std::vector status; +std::vector eventId; +std::vector genEventOfNode; +``` + +It also stores association arrays: + +```cpp +std::vector simTrackToGen; +std::vector simTrackToVtx; +``` + +These are indexed by raw node id. Entries that are not meaningful for a given node type are filled with default values, typically `0` or `-1`. + +## `TruthGraphProducer` + +`TruthGraphProducer` builds the raw `TruthGraph` from: + +* HepMC3, when available; +* HepMC2, as fallback; +* `SimTrackContainer`; +* `SimVertexContainer`. + +Default input tags are: + +```python +genEventHepMC3 = cms.InputTag("generatorSmeared") +genEventHepMC = cms.InputTag("generatorSmeared") +simTracks = cms.InputTag("g4SimHits") +simVertices = cms.InputTag("g4SimHits") +``` + +The producer creates: + +* one `GenEvent` node per connected generator component; +* one node per generator vertex; +* one node per generator particle; +* one node per simulation vertex; +* one node per simulation track. + +Generator components are computed using a disjoint-set union over the generator particle-vertex graph. + +Simulation topology is built from: + +* `SimTrack::vertIndex()` for `SimVertex -> SimTrack` edges; +* `SimVertex::parentIndex()` for `SimTrack -> SimVertex` edges. + +Generator-to-simulation particle associations are built using `SimTrack::genpartIndex()` when available. + +When enabled, cross-domain edges are also added: + +* `GenParticle -> SimTrack`; +* `GenVertex -> SimVertex`, using the production vertex of the associated generator particle. + +The cross edges are controlled by: + +```python +addGenToSimEdges = cms.bool(True) +``` + +## Logical graph: `truth::Graph` + +`truth::Graph` is the user-facing abstraction built from the raw `TruthGraph`. It is intended to expose a stable, physics-oriented API. + +The logical graph is bipartite: + +```text +Particle -> decay Vertex -> outgoing Particle +Particle <- production Vertex <- incoming Particle +``` + +The main public types are: + +```cpp +namespace truth { + class Graph; + class Particle; + class Vertex; + + struct ParticleData; + struct VertexData; + struct Checkpoint; +} +``` + +### `truth::Particle` + +A `truth::Particle` may combine generator-level and simulation-level information when a robust correspondence exists. + +The stored payload is: + +```cpp +struct ParticleData { + int32_t genNode = -1; + int32_t simNode = -1; + + int32_t pdgId = 0; + int16_t status = 0; + uint64_t eventId = 0; + int32_t genEvent = -1; + + math::XYZTLorentzVectorD momentum; + std::vector checkpoints; +}; +``` + +The convention for the momentum is "best available": + +1. prefer simulation momentum when a matched `SimTrack` exists; +2. otherwise use generator momentum; +3. otherwise keep the default value. + +Useful methods include: + +```cpp +bool hasGen() const; +bool hasSim() const; +int32_t pdgId() const; +int16_t status() const; +uint64_t eventId() const; +int32_t genEvent() const; +const math::XYZTLorentzVectorD& momentum() const; + +bool isRoot() const; +bool isLeaf() const; + +std::vector productionVertices() const; +std::vector decayVertices() const; + +std::vector parents() const; +std::vector children() const; +std::vector ancestors() const; +std::vector descendants() const; + +bool hasAncestorPdgId(int pdgId) const; +std::optional firstAncestorWithPdgId(int pdgId) const; +std::optional firstCommonAncestor(truth::Particle other) const; +``` + +### `truth::Vertex` + +A `truth::Vertex` stores vertex-level payload: + +```cpp +struct VertexData { + int32_t genNode = -1; + int32_t simNode = -1; + + uint64_t eventId = 0; + int32_t genEvent = -1; + + math::XYZTLorentzVectorD position; +}; +``` + +Useful methods include: + +```cpp +bool hasGen() const; +bool hasSim() const; +uint64_t eventId() const; +int32_t genEvent() const; +const math::XYZTLorentzVectorD& position() const; + +bool isSource() const; +bool isSink() const; + +std::vector incomingParticles() const; +std::vector outgoingParticles() const; +``` + +### Vertex treatment + +At present, generator-level and simulation-level particles may be merged when a robust association exists. + +Generator-level and simulation-level vertices are intentionally not merged. Each raw `GenVertex` and each raw `SimVertex` becomes a separate logical `truth::Vertex`. + +This is deliberate for the current debugging phase. Keeping vertices separate makes it easier to diagnose: + +* incorrect GEN-SIM associations; +* unexpected topologies; +* boundary-crossing behaviour; +* differences between generator and simulation vertex semantics. + +Vertex merging can be revisited once the raw graph construction and association policy are better understood. + +## Trajectory checkpoints + +The logical particle model supports trajectory checkpoints: + +```cpp +struct Checkpoint { + uint32_t checkpointId = 0; + math::XYZTLorentzVectorF position; + math::XYZTLorentzVectorF momentum; +}; +``` + +A checkpoint represents a relevant point along the propagation history of a particle. + +The current prototype uses checkpoint `0` to store boundary-crossing information from `SimTrack`: + +* position at boundary; +* momentum at boundary; +* boundary identifier. + +This is meant to be a generic mechanism. A natural long-term direction is to build the truth graph directly while Geant4 tracks are being created and simulated, rather than reconstructing it afterwards from final CMS products. That would allow the graph to record multiple checkpoints along the propagation history and preserve information that is difficult to recover a posteriori. + +## `TruthLogicalGraphProducer` + +`TruthLogicalGraphProducer` builds a standalone `truth::Graph` from the raw `TruthGraph`. + +Default input tags are: + +```python +src = cms.InputTag("truthGraphProducer") +simTracks = cms.InputTag("g4SimHits") +simVertices = cms.InputTag("g4SimHits") +genEventHepMC3 = cms.InputTag("generatorSmeared") +genEventHepMC = cms.InputTag("generatorSmeared") +``` + +The producer performs the following steps: + +1. Read and validate the raw `TruthGraph`. +2. Load optional payload from HepMC2, HepMC3, `SimTrackContainer`, and `SimVertexContainer`. +3. Assign temporary logical ids to raw particle and vertex nodes. +4. Merge only particle nodes across GEN and SIM when a robust association exists. +5. Keep GEN and SIM vertices separate. +6. Fill standalone `ParticleData` and `VertexData` payload. +7. Rebuild the logical bipartite graph in CSR form. +8. Validate the produced `truth::Graph`. + +The produced graph is independent of the raw graph for ordinary navigation, but it keeps optional back-references to raw node ids for debugging: + +```cpp +ParticleData::genNode +ParticleData::simNode +VertexData::genNode +VertexData::simNode +``` + +## Graph navigation examples + +### Iterate over particles + +```cpp +auto const& graph = event.get(truthGraphToken_); + +for (auto particle : graph.particleViews()) { + if (!particle.valid()) + continue; + + const auto pdgId = particle.pdgId(); + const auto p4 = particle.momentum(); +} +``` + +### Find particles from a Z boson + +```cpp +for (auto particle : graph.particleViews()) { + if (particle.hasAncestorPdgId(23)) { + // This particle has a Z boson somewhere in its ancestor chain. + } +} +``` + +### Find the first common ancestor of two particles + +```cpp +auto p1 = graph.particle(firstId); +auto p2 = graph.particle(secondId); + +auto common = p1.firstCommonAncestor(p2); +if (common.has_value()) { + const int pdgId = common->pdgId(); +} +``` + +### Access production and decay vertices + +```cpp +for (auto particle : graph.particleViews()) { + for (auto vertex : particle.productionVertices()) { + const auto x4 = vertex.position(); + } + + for (auto vertex : particle.decayVertices()) { + const auto x4 = vertex.position(); + } +} +``` + +### Navigate parent and child particles + +```cpp +for (auto particle : graph.particleViews()) { + auto parents = particle.parents(); + auto children = particle.children(); +} +``` + +### Access checkpoints + +```cpp +for (auto particle : graph.particleViews()) { + for (auto const& checkpoint : particle.checkpoints()) { + const auto id = checkpoint.checkpointId; + const auto position = checkpoint.position; + const auto momentum = checkpoint.momentum; + } +} +``` + +## Dumping and visualization + +Two dumper modules are provided. + +### Raw graph dumper + +`TruthGraphDumper` writes a DOT representation of the raw graph. + +It includes enriched labels using HepMC and simulation products when available. + +Default output: + +```text +truthgraph.dot +``` + +The dumper can be configured with: + +```python +src = cms.InputTag("truthGraphProducer") +dotFile = cms.string("truthgraph.dot") +maxNodes = cms.uint32(5000) +maxEdgesPerNode = cms.uint32(200) +``` + +### Logical graph dumper + +`TruthLogicalGraphDumper` writes a DOT representation of the logical graph. + +Default output: + +```text +truthlogicalgraph.dot +``` + +The dumper can also use the raw graph to enrich labels with raw node information: + +```python +truthLogicalGraphDumper = cms.EDAnalyzer( + "TruthLogicalGraphDumper", + src = cms.InputTag("truthLogicalGraphProducer"), + rawSrc = cms.InputTag("truthGraphProducer"), + dotFile = cms.string("truthlogicalgraph.dot"), + maxParticles = cms.uint32(5000), + maxVertices = cms.uint32(5000), + maxEdgesPerNode = cms.uint32(200), +) +``` + +DOT files can be converted with Graphviz, for example: + +```bash +dot -Tpdf truthlogicalgraph.dot -o truthlogicalgraph.pdf +``` + +or: + +```bash +dot -Tpng truthlogicalgraph.dot -o truthlogicalgraph.png +``` + +## Example configuration + +A minimal test path can be configured as follows: + +```python +import FWCore.ParameterSet.Config as cms + +process.load("PhysicsTools.TruthInfo.truthGraphProducer_cfi") + +process.truthLogicalGraphProducer = cms.EDProducer( + "TruthLogicalGraphProducer", + src = cms.InputTag("truthGraphProducer"), + simTracks = cms.InputTag("g4SimHits"), + simVertices = cms.InputTag("g4SimHits"), + genEventHepMC3 = cms.InputTag("generatorSmeared"), + genEventHepMC = cms.InputTag("generatorSmeared"), +) + +process.truthLogicalGraphDumper = cms.EDAnalyzer( + "TruthLogicalGraphDumper", + src = cms.InputTag("truthLogicalGraphProducer"), + rawSrc = cms.InputTag("truthGraphProducer"), + dotFile = cms.string("truthlogicalgraph.dot"), + maxParticles = cms.uint32(5000), + maxVertices = cms.uint32(5000), + maxEdgesPerNode = cms.uint32(200), +) + +process.p = cms.Path( + process.truthGraphProducer + + process.truthLogicalGraphProducer + + process.truthLogicalGraphDumper +) +``` + +The raw graph producer can be customized explicitly if needed: + +```python +process.truthGraphProducer = cms.EDProducer( + "TruthGraphProducer", + genEventHepMC3 = cms.InputTag("generatorSmeared"), + genEventHepMC = cms.InputTag("generatorSmeared"), + simTracks = cms.InputTag("g4SimHits"), + simVertices = cms.InputTag("g4SimHits"), + addGenToSimEdges = cms.bool(True), +) +``` + +## Intended matching strategy + +The long-term matching model is to build detector-level associators from hits to `truth::Particle`. + +In this model: + +* the graph provides the particle and vertex structure; +* detector-level truth association is performed through hits; +* reconstructed objects can be associated either to a single `truth::Particle` or to a larger `truth::Branch`; +* truth information can be aggregated over a coherent branch when a reconstructed object corresponds to a composite truth structure. + +Different reconstruction domains need different matching metrics: + +* tracking association is usually based on shared hits, not on hit energy; +* calorimeter clustering association can use energy fractions or energy-weighted metrics; +* other reconstruction objects may require detector-specific metrics. + +The graph should therefore act as the common truth substrate, while matching definitions remain detector-aware and use-case dependent. + +## Possible future `truth::Branch` abstraction + +A future `truth::Branch` abstraction is intended to represent a coherent subgraph selected from the full truth graph. + +This would provide a natural target for truth-reco association when a reconstructed object is not well described by a single truth particle. + +Possible branch-level operations include: + +* aggregate particles in a physically connected subgraph; +* collect all detector hits attached to particles in the branch; +* compute detector-specific matching scores; +* compare two branches; +* define stable references for composite truth structures. + +In this picture, `truth::Branch` avoids forcing an early collapse of the truth history into fixed reference objects. + +## Hits attached to particles + +A possible long-term direction is to attach detector hits of different types directly to `truth::Particle`, or to provide hit collections indexed by `truth::Particle` ids. + +This would make the graph the central truth data structure. Detector-specific truth objects such as `TrackingParticle`, `SimCluster`, and `CaloParticle` could then be expressed as views or derived abstractions on top of the same graph. + +This would make it possible to: + +* use one common truth structure across subsystems; +* preserve detector-specific information without fragmenting the truth model; +* define multiple matching strategies on top of the same graph; +* aggregate truth information dynamically over particles, vertices, or branches. + +## Current status + +The current prototype can: + +* build a raw `TruthGraph` from generator and simulation products; +* build a logical `truth::Graph` from the raw graph; +* merge matched generator and simulation particles; +* keep generator and simulation vertices separate for debugging; +* navigate particle and vertex relations; +* compute parents, children, ancestors, descendants, roots, and leaves; +* find ancestors with a given PDG id; +* find a common ancestor between two particles; +* store boundary-crossing checkpoints; +* dump raw and logical graphs to DOT for debugging. + +## Known limitations + +The current implementation is a prototype and several aspects are intentionally conservative. + +Known limitations include: + +* vertex merging is not yet attempted; +* only a limited set of GEN-SIM associations is currently used; +* checkpoint information is limited to boundary-crossing information from `SimTrack`; +* hit-to-particle association is not yet implemented; +* `truth::Branch` is part of the target design but not yet implemented; +* the logical API is still evolving; +* the raw graph construction needs further validation on realistic events and pileup scenarios. + +## Next steps + +Planned or natural next steps are: + +1. Continue debugging the raw truth graph construction. +2. Validate HepMC2 and HepMC3 behaviour on representative workflows. +3. Refine GEN-SIM particle association. +4. Study whether, when, and how generator and simulation vertices should be merged. +5. Extend the checkpoint model beyond boundary crossings. +6. Investigate direct graph construction during Geant4 simulation. +7. Define hit-to-`truth::Particle` associators. +8. Prototype detector-specific matching metrics on top of the graph. +9. Implement a `truth::Branch` abstraction. +10. Study how existing detector-specific truth containers could be represented as views over the graph. +11. Stabilize the logical API for downstream reconstruction and validation code. + +## Design principle + +The main design principle is to separate storage details from physics navigation. + +Low-level CMS products remain the source of truth for building the graph, but downstream code should be able to ask physics questions through a stable interface: + +```cpp +particle.parents(); +particle.children(); +particle.ancestors(); +particle.firstCommonAncestor(other); +particle.hasAncestorPdgId(23); +``` + +rather than reimplementing event-history navigation separately for each reconstruction or validation use case. diff --git a/PhysicsTools/TruthInfo/interface/Graph.h b/PhysicsTools/TruthInfo/interface/Graph.h index b12e0e53f8263..811bc94e2dfe5 100644 --- a/PhysicsTools/TruthInfo/interface/Graph.h +++ b/PhysicsTools/TruthInfo/interface/Graph.h @@ -109,9 +109,7 @@ namespace truth { [[nodiscard]] std::optional firstAncestorWithPdgId(int pdgId) const; [[nodiscard]] std::optional firstCommonAncestor(Particle other) const; - [[nodiscard]] bool operator==(Particle const& other) const { - return graph_ == other.graph_ && id_ == other.id_; - } + [[nodiscard]] bool operator==(Particle const& other) const { return graph_ == other.graph_ && id_ == other.id_; } [[nodiscard]] bool operator!=(Particle const& other) const { return !(*this == other); } private: @@ -141,9 +139,7 @@ namespace truth { [[nodiscard]] std::vector incomingParticles() const; [[nodiscard]] std::vector outgoingParticles() const; - [[nodiscard]] bool operator==(Vertex const& other) const { - return graph_ == other.graph_ && id_ == other.id_; - } + [[nodiscard]] bool operator==(Vertex const& other) const { return graph_ == other.graph_ && id_ == other.id_; } [[nodiscard]] bool operator!=(Vertex const& other) const { return !(*this == other); } private: @@ -239,4 +235,4 @@ namespace truth { } // namespace truth -#endif \ No newline at end of file +#endif diff --git a/PhysicsTools/TruthInfo/interface/TruthGraph.h b/PhysicsTools/TruthInfo/interface/TruthGraph.h index 8946f8cfc7025..a5e535ac32009 100644 --- a/PhysicsTools/TruthInfo/interface/TruthGraph.h +++ b/PhysicsTools/TruthInfo/interface/TruthGraph.h @@ -1,11 +1,9 @@ // Author: Felice Pantaleo - CERN // Date: 03/2026 // A compact, read-only graph representation of the truth information in an event. -// The graph is built in the TruthGraphProducer module, which also fills the node metadata and associations. +// The graph is built in the TruthGraphProducer module, which also fills the node metadata and associations. // The graph is intended to be a common data format for various use cases (e.g. validation, analysis, visualization). - - #ifndef PhysicsTools_TruthInfo_interface_TruthGraph_h #define PhysicsTools_TruthInfo_interface_TruthGraph_h diff --git a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc index f67bc4df53d61..20bcb894d5b38 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc @@ -220,19 +220,19 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { if (d.genEvent >= 0) os << " genEvent: " << d.genEvent << "\n"; - os << " hasGen: " << (d.hasGen() ? "yes" : "no") - << " hasSim: " << (d.hasSim() ? "yes" : "no") << "\n"; + os << " hasGen: " << (d.hasGen() ? "yes" : "no") << " hasSim: " << (d.hasSim() ? "yes" : "no") + << "\n"; - os << " isRoot: " << (p.isRoot() ? "yes" : "no") - << " isLeaf: " << (p.isLeaf() ? "yes" : "no") << "\n"; + os << " isRoot: " << (p.isRoot() ? "yes" : "no") << " isLeaf: " << (p.isLeaf() ? "yes" : "no") + << "\n"; os << " p4: " << fmtP4(d.momentum) << "\n"; - os << " nProdVtx: " << p.productionVertices().size() - << " nDecayVtx: " << p.decayVertices().size() << "\n"; + os << " nProdVtx: " << p.productionVertices().size() << " nDecayVtx: " << p.decayVertices().size() + << "\n"; - os << " nParents: " << p.parents().size() - << " nChildren: " << p.children().size() << "\n"; + os << " nParents: " << p.parents().size() << " nChildren: " << p.children().size() + << "\n"; os << " nCheckpoints: " << d.checkpoints.size() << "\n"; @@ -279,16 +279,16 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { if (d.genEvent >= 0) os << " genEvent: " << d.genEvent << "\n"; - os << " hasGen: " << (d.hasGen() ? "yes" : "no") - << " hasSim: " << (d.hasSim() ? "yes" : "no") << "\n"; + os << " hasGen: " << (d.hasGen() ? "yes" : "no") << " hasSim: " << (d.hasSim() ? "yes" : "no") + << "\n"; - os << " isSource: " << (v.isSource() ? "yes" : "no") - << " isSink: " << (v.isSink() ? "yes" : "no") << "\n"; + os << " isSource: " << (v.isSource() ? "yes" : "no") << " isSink: " << (v.isSink() ? "yes" : "no") + << "\n"; os << " x4: " << fmtP4(d.position) << "\n"; - os << " nIn: " << v.incomingParticles().size() - << " nOut: " << v.outgoingParticles().size() << "\n"; + os << " nIn: " << v.incomingParticles().size() << " nOut: " << v.outgoingParticles().size() + << "\n"; if (raw != nullptr) { os << " raw GEN: " << rawNodeSummary(raw, d.genNode) << "\n"; @@ -338,4 +338,4 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { unsigned maxEdgesPerNode_; }; -DEFINE_FWK_MODULE(TruthLogicalGraphDumper); \ No newline at end of file +DEFINE_FWK_MODULE(TruthLogicalGraphDumper); diff --git a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc index b49ccf1c298eb..ca44015b3387d 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc @@ -171,8 +171,8 @@ namespace { GenParticlePayload payload; payload.pdgId = pptr->pid(); payload.status = static_cast(pptr->status()); - payload.momentum = - math::XYZTLorentzVectorD(pptr->momentum().px(), pptr->momentum().py(), pptr->momentum().pz(), pptr->momentum().e()); + payload.momentum = math::XYZTLorentzVectorD( + pptr->momentum().px(), pptr->momentum().py(), pptr->momentum().pz(), pptr->momentum().e()); particlePayload.emplace(bc, payload); } @@ -181,8 +181,8 @@ namespace { continue; const int bc = vptr->id(); GenVertexPayload payload; - payload.position = - math::XYZTLorentzVectorD(vptr->position().x(), vptr->position().y(), vptr->position().z(), vptr->position().t()); + payload.position = math::XYZTLorentzVectorD( + vptr->position().x(), vptr->position().y(), vptr->position().z(), vptr->position().t()); vertexPayload.emplace(bc, payload); } } @@ -469,10 +469,8 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { } } - buildCSR(out->nParticles(), - particleToDecayVertexPairs, - out->particleToDecayVertexOffsets, - out->particleToDecayVertices); + buildCSR( + out->nParticles(), particleToDecayVertexPairs, out->particleToDecayVertexOffsets, out->particleToDecayVertices); buildCSR(out->nParticles(), particleToProductionVertexPairs, @@ -504,4 +502,4 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { edm::EDGetTokenT hepmc2Token_; }; -DEFINE_FWK_MODULE(TruthLogicalGraphProducer); \ No newline at end of file +DEFINE_FWK_MODULE(TruthLogicalGraphProducer); diff --git a/PhysicsTools/TruthInfo/src/Graph.cc b/PhysicsTools/TruthInfo/src/Graph.cc index c76a3c7879a50..2c335f6224885 100644 --- a/PhysicsTools/TruthInfo/src/Graph.cc +++ b/PhysicsTools/TruthInfo/src/Graph.cc @@ -29,45 +29,27 @@ namespace { } } // namespace -const truth::ParticleData& truth::Particle::data() const { - return graph_->particles.at(id_); -} +const truth::ParticleData& truth::Particle::data() const { return graph_->particles.at(id_); } -bool truth::Particle::hasGen() const { - return data().hasGen(); -} +bool truth::Particle::hasGen() const { return data().hasGen(); } -bool truth::Particle::hasSim() const { - return data().hasSim(); -} +bool truth::Particle::hasSim() const { return data().hasSim(); } -int32_t truth::Particle::pdgId() const { - return data().pdgId; -} +int32_t truth::Particle::pdgId() const { return data().pdgId; } -int16_t truth::Particle::status() const { - return data().status; -} +int16_t truth::Particle::status() const { return data().status; } -uint64_t truth::Particle::eventId() const { - return data().eventId; -} +uint64_t truth::Particle::eventId() const { return data().eventId; } -int32_t truth::Particle::genEvent() const { - return data().genEvent; -} +int32_t truth::Particle::genEvent() const { return data().genEvent; } -const math::XYZTLorentzVectorD& truth::Particle::momentum() const { - return data().momentum; -} +const math::XYZTLorentzVectorD& truth::Particle::momentum() const { return data().momentum; } std::span truth::Particle::checkpoints() const { return std::span(data().checkpoints.data(), data().checkpoints.size()); } -bool truth::Particle::hasCheckpoints() const { - return !data().checkpoints.empty(); -} +bool truth::Particle::hasCheckpoints() const { return !data().checkpoints.empty(); } std::optional truth::Particle::checkpoint(uint32_t checkpointId) const { for (auto const& cp : data().checkpoints) { @@ -77,13 +59,9 @@ std::optional truth::Particle::checkpoint(uint32_t checkpoint return std::nullopt; } -bool truth::Particle::isRoot() const { - return valid() && graph_->productionVertices(id_).empty(); -} +bool truth::Particle::isRoot() const { return valid() && graph_->productionVertices(id_).empty(); } -bool truth::Particle::isLeaf() const { - return valid() && graph_->decayVertices(id_).empty(); -} +bool truth::Particle::isLeaf() const { return valid() && graph_->decayVertices(id_).empty(); } std::vector truth::Particle::productionVertices() const { return valid() ? graph_->productionVerticesOf(id_) : std::vector{}; @@ -109,9 +87,7 @@ std::vector truth::Particle::descendants() const { return valid() ? graph_->descendantsOf(id_) : std::vector{}; } -bool truth::Particle::hasAncestorPdgId(int pdgId) const { - return firstAncestorWithPdgId(pdgId).has_value(); -} +bool truth::Particle::hasAncestorPdgId(int pdgId) const { return firstAncestorWithPdgId(pdgId).has_value(); } std::optional truth::Particle::firstAncestorWithPdgId(int pdgId) const { return valid() ? graph_->firstAncestorWithPdgIdOf(id_, pdgId) : std::nullopt; @@ -123,37 +99,21 @@ std::optional truth::Particle::firstCommonAncestor(Particle oth return graph_->firstCommonAncestorOf(id_, other.id_); } -const truth::VertexData& truth::Vertex::data() const { - return graph_->vertices.at(id_); -} +const truth::VertexData& truth::Vertex::data() const { return graph_->vertices.at(id_); } -bool truth::Vertex::hasGen() const { - return data().hasGen(); -} +bool truth::Vertex::hasGen() const { return data().hasGen(); } -bool truth::Vertex::hasSim() const { - return data().hasSim(); -} +bool truth::Vertex::hasSim() const { return data().hasSim(); } -uint64_t truth::Vertex::eventId() const { - return data().eventId; -} +uint64_t truth::Vertex::eventId() const { return data().eventId; } -int32_t truth::Vertex::genEvent() const { - return data().genEvent; -} +int32_t truth::Vertex::genEvent() const { return data().genEvent; } -const math::XYZTLorentzVectorD& truth::Vertex::position() const { - return data().position; -} +const math::XYZTLorentzVectorD& truth::Vertex::position() const { return data().position; } -bool truth::Vertex::isSource() const { - return valid() && graph_->incomingParticles(id_).empty(); -} +bool truth::Vertex::isSource() const { return valid() && graph_->incomingParticles(id_).empty(); } -bool truth::Vertex::isSink() const { - return valid() && graph_->outgoingParticles(id_).empty(); -} +bool truth::Vertex::isSink() const { return valid() && graph_->outgoingParticles(id_).empty(); } std::vector truth::Vertex::incomingParticles() const { return valid() ? graph_->incomingParticlesOf(id_) : std::vector{}; @@ -167,9 +127,7 @@ truth::Particle truth::Graph::particle(size_type id) const { return id < nParticles() ? Particle(this, id) : Particle{}; } -truth::Vertex truth::Graph::vertex(size_type id) const { - return id < nVertices() ? Vertex(this, id) : Vertex{}; -} +truth::Vertex truth::Graph::vertex(size_type id) const { return id < nVertices() ? Vertex(this, id) : Vertex{}; } std::vector truth::Graph::particleViews() const { std::vector out; @@ -448,4 +406,4 @@ bool truth::Graph::isConsistent() const { checkTargets(vertexToIncomingParticles, nParticles()); return p2dv && p2pv && v2op && v2ip; -} \ No newline at end of file +} diff --git a/PhysicsTools/TruthInfo/src/TruthGraph.cc b/PhysicsTools/TruthInfo/src/TruthGraph.cc index fe61a000c2c51..ae7cb5454b8ba 100644 --- a/PhysicsTools/TruthInfo/src/TruthGraph.cc +++ b/PhysicsTools/TruthInfo/src/TruthGraph.cc @@ -1,7 +1,7 @@ // Author: Felice Pantaleo - CERN // Date: 03/2026 // A compact, read-only graph representation of the truth information in an event. -// The graph is built in the TruthGraphProducer module, which also fills the node metadata and associations. +// The graph is built in the TruthGraphProducer module, which also fills the node metadata and associations. // The graph is intended to be a common data format for various use cases (e.g. validation, analysis, visualization). #include "PhysicsTools/TruthInfo/interface/TruthGraph.h" diff --git a/PhysicsTools/TruthInfo/src/classes.h b/PhysicsTools/TruthInfo/src/classes.h index 6262bd2b31383..9de2599c71a33 100644 --- a/PhysicsTools/TruthInfo/src/classes.h +++ b/PhysicsTools/TruthInfo/src/classes.h @@ -27,4 +27,4 @@ namespace { }; } // namespace -#endif \ No newline at end of file +#endif From cd8c93cc90f90d5e60e191358019ee19ab1782b7 Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Mon, 11 May 2026 11:10:00 +0200 Subject: [PATCH 08/53] Add possibility to filter mother by pdgid --- .../plugins/TruthLogicalGraphProducer.cc | 131 +++++++++++++++++- 1 file changed, 126 insertions(+), 5 deletions(-) diff --git a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc index ca44015b3387d..c379b153cb768 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -138,22 +139,28 @@ namespace { for (auto p = ev.particles_begin(); p != ev.particles_end(); ++p) { if (*p == nullptr) continue; + const int bc = (*p)->barcode(); + GenParticlePayload payload; payload.pdgId = (*p)->pdg_id(); payload.status = static_cast((*p)->status()); payload.momentum = math::XYZTLorentzVectorD( (*p)->momentum().px(), (*p)->momentum().py(), (*p)->momentum().pz(), (*p)->momentum().e()); + particlePayload.emplace(bc, payload); } for (auto v = ev.vertices_begin(); v != ev.vertices_end(); ++v) { if (*v == nullptr) continue; + const int bc = (*v)->barcode(); + GenVertexPayload payload; payload.position = math::XYZTLorentzVectorD( (*v)->position().x(), (*v)->position().y(), (*v)->position().z(), (*v)->position().t()); + vertexPayload.emplace(bc, payload); } } @@ -167,26 +174,101 @@ namespace { for (auto const& pptr : ev.particles()) { if (!pptr) continue; + const int bc = pptr->id(); + GenParticlePayload payload; payload.pdgId = pptr->pid(); payload.status = static_cast(pptr->status()); payload.momentum = math::XYZTLorentzVectorD( pptr->momentum().px(), pptr->momentum().py(), pptr->momentum().pz(), pptr->momentum().e()); + particlePayload.emplace(bc, payload); } for (auto const& vptr : ev.vertices()) { if (!vptr) continue; + const int bc = vptr->id(); + GenVertexPayload payload; payload.position = math::XYZTLorentzVectorD( vptr->position().x(), vptr->position().y(), vptr->position().z(), vptr->position().t()); + vertexPayload.emplace(bc, payload); } } + int32_t genParticlePdgId(TruthGraph const& raw, + uint32_t nodeId, + std::unordered_map const& genParticlePayload) { + auto const& ref = raw.nodeRef(nodeId); + + if (ref.kind != TruthGraph::NodeKind::GenParticle) + return 0; + + const auto rawPdgId = raw.nodePdgId(nodeId); + if (rawPdgId != 0) + return rawPdgId; + + const int barcode = static_cast(ref.key); + auto it = genParticlePayload.find(barcode); + if (it != genParticlePayload.end()) + return it->second.pdgId; + + return 0; + } + + std::vector buildKeepMaskForMotherPdgId(TruthGraph const& raw, + std::unordered_map const& genParticlePayload, + int32_t motherPdgId) { + const uint32_t nRawNodes = raw.nNodes(); + + std::vector keep(nRawNodes, 1); + + if (motherPdgId == 0) + return keep; + + keep.assign(nRawNodes, 0); + + std::queue queue; + + for (uint32_t nodeId = 0; nodeId < nRawNodes; ++nodeId) { + auto const& ref = raw.nodeRef(nodeId); + + if (ref.kind != TruthGraph::NodeKind::GenParticle) + continue; + + if (genParticlePdgId(raw, nodeId, genParticlePayload) != motherPdgId) + continue; + + // Start from the selected mother itself. + // This keeps the mother, its decay vertices, daughters, and all descendants. + if (!keep[nodeId]) { + keep[nodeId] = 1; + queue.push(nodeId); + } + } + + while (!queue.empty()) { + const uint32_t src = queue.front(); + queue.pop(); + + for (uint32_t dst : raw.children(src)) { + if (dst >= nRawNodes) + continue; + + if (!keep[dst]) { + keep[dst] = 1; + queue.push(dst); + } + } + } + + return keep; + } + } // namespace class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { @@ -196,7 +278,8 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { simTrackToken_(mayConsume(cfg.getParameter("simTracks"))), simVertexToken_(mayConsume(cfg.getParameter("simVertices"))), hepmc3Token_(mayConsume(cfg.getParameter("genEventHepMC3"))), - hepmc2Token_(mayConsume(cfg.getParameter("genEventHepMC"))) { + hepmc2Token_(mayConsume(cfg.getParameter("genEventHepMC"))), + motherPdgId_(cfg.getParameter("motherPdgId")) { produces(); } @@ -207,6 +290,8 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { desc.add("simVertices", edm::InputTag("g4SimHits")); desc.add("genEventHepMC3", edm::InputTag("generatorSmeared")); desc.add("genEventHepMC", edm::InputTag("generatorSmeared")); + desc.add("motherPdgId", 0) + ->setComment("If non-zero, keep only GEN particles with this exact PDG id and all their descendants."); descriptions.addWithDefaultLabel(desc); } @@ -261,6 +346,8 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { } } + const auto keepRawNode = buildKeepMaskForMotherPdgId(raw, genParticlePayload, motherPdgId_); + // ------------------------------------------------------------------ // 1. Temporary ids // ------------------------------------------------------------------ @@ -271,6 +358,9 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { int nVertexLogical = 0; for (uint32_t nodeId = 0; nodeId < nRawNodes; ++nodeId) { + if (!keepRawNode[nodeId]) + continue; + const auto kind = raw.nodeRef(nodeId).kind; if (isParticleKind(kind)) { rawToParticleTmp[nodeId] = nParticleTmp++; @@ -285,7 +375,10 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { // 2. Merge only particles across GEN <-> SIM // ------------------------------------------------------------------ for (uint32_t nodeId = 0; nodeId < nRawNodes; ++nodeId) { - const auto& ref = raw.nodeRef(nodeId); + if (!keepRawNode[nodeId]) + continue; + + auto const& ref = raw.nodeRef(nodeId); if (ref.kind != TruthGraph::NodeKind::SimTrack) continue; @@ -294,24 +387,39 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { continue; if (static_cast(genNode) >= nRawNodes) continue; + if (!keepRawNode[static_cast(genNode)]) + continue; if (raw.nodeRef(static_cast(genNode)).kind != TruthGraph::NodeKind::GenParticle) continue; + if (rawToParticleTmp[nodeId] < 0 || rawToParticleTmp[static_cast(genNode)] < 0) + continue; + particleDSU.unite(rawToParticleTmp[nodeId], rawToParticleTmp[static_cast(genNode)]); } for (uint32_t src = 0; src < nRawNodes; ++src) { + if (!keepRawNode[src]) + continue; + const auto dsts = raw.children(src); const auto ekinds = raw.childrenEdgeKinds(src); for (std::size_t i = 0; i < dsts.size(); ++i) { const uint32_t dst = dsts[i]; + + if (dst >= nRawNodes || !keepRawNode[dst]) + continue; + const auto ek = static_cast(ekinds[i]); if (ek != TruthGraph::EdgeKind::GenToSim && ek != TruthGraph::EdgeKind::SimToGen) continue; if (isGenParticleToSimTrack(raw, src, dst)) { + if (rawToParticleTmp[src] < 0 || rawToParticleTmp[dst] < 0) + continue; + particleDSU.unite(rawToParticleTmp[src], rawToParticleTmp[dst]); } } @@ -324,6 +432,9 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { std::vector rawToParticle(nRawNodes, -1); for (uint32_t nodeId = 0; nodeId < nRawNodes; ++nodeId) { + if (!keepRawNode[nodeId]) + continue; + if (rawToParticleTmp[nodeId] >= 0) { const int rep = particleDSU.find(rawToParticleTmp[nodeId]); auto result = particleRepToLogical.emplace(rep, static_cast(particleRepToLogical.size())); @@ -338,7 +449,10 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { // 4. Fill payload // ------------------------------------------------------------------ for (uint32_t nodeId = 0; nodeId < nRawNodes; ++nodeId) { - const auto& ref = raw.nodeRef(nodeId); + if (!keepRawNode[nodeId]) + continue; + + auto const& ref = raw.nodeRef(nodeId); if (rawToParticle[nodeId] >= 0) { auto& p = out->particles[static_cast(rawToParticle[nodeId])]; @@ -445,11 +559,17 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { std::vector> vertexToIncomingParticlePairs; for (uint32_t src = 0; src < nRawNodes; ++src) { - const auto& srcRef = raw.nodeRef(src); + if (!keepRawNode[src]) + continue; + + auto const& srcRef = raw.nodeRef(src); const auto dsts = raw.children(src); for (uint32_t dst : dsts) { - const auto& dstRef = raw.nodeRef(dst); + if (dst >= nRawNodes || !keepRawNode[dst]) + continue; + + auto const& dstRef = raw.nodeRef(dst); if (isVertexKind(srcRef.kind) && isParticleKind(dstRef.kind)) { const int32_t lv = rawToVertex[src]; @@ -500,6 +620,7 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { edm::EDGetTokenT simVertexToken_; edm::EDGetTokenT hepmc3Token_; edm::EDGetTokenT hepmc2Token_; + int32_t motherPdgId_; }; DEFINE_FWK_MODULE(TruthLogicalGraphProducer); From 8e92e0fc99cf50dce272ca34e78664eed0d2ae88 Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Mon, 11 May 2026 14:54:23 +0200 Subject: [PATCH 09/53] Add generator status flags to truth graphs Store packed reco::GenStatusFlags in the raw TruthGraph and propagate them to the logical truth::Graph particle payload. Decode and print the active status flags in the logical graph dumper to make hard-process and copy-history information visible in DOT dumps. --- PhysicsTools/TruthInfo/interface/Graph.h | 5 + PhysicsTools/TruthInfo/interface/TruthGraph.h | 8 +- .../TruthInfo/plugins/TruthGraphProducer.cc | 65 ++++- .../plugins/TruthLogicalGraphDumper.cc | 46 ++++ .../plugins/TruthLogicalGraphProducer.cc | 229 +++++++++++++++++- PhysicsTools/TruthInfo/src/Graph.cc | 2 + 6 files changed, 346 insertions(+), 9 deletions(-) diff --git a/PhysicsTools/TruthInfo/interface/Graph.h b/PhysicsTools/TruthInfo/interface/Graph.h index 811bc94e2dfe5..67feeb4197391 100644 --- a/PhysicsTools/TruthInfo/interface/Graph.h +++ b/PhysicsTools/TruthInfo/interface/Graph.h @@ -26,6 +26,10 @@ namespace truth { int32_t pdgId = 0; int16_t status = 0; + // Packed reco::GenStatusFlags bitfield, when available. + // 0 means "not available" or "no flags set". + uint16_t statusFlags = 0; + // SIM event id when available, 0 otherwise. uint64_t eventId = 0; @@ -85,6 +89,7 @@ namespace truth { [[nodiscard]] bool hasSim() const; [[nodiscard]] int32_t pdgId() const; [[nodiscard]] int16_t status() const; + [[nodiscard]] uint16_t statusFlags() const; [[nodiscard]] uint64_t eventId() const; [[nodiscard]] int32_t genEvent() const; [[nodiscard]] const math::XYZTLorentzVectorD& momentum() const; diff --git a/PhysicsTools/TruthInfo/interface/TruthGraph.h b/PhysicsTools/TruthInfo/interface/TruthGraph.h index a5e535ac32009..ad5a1ec134da6 100644 --- a/PhysicsTools/TruthInfo/interface/TruthGraph.h +++ b/PhysicsTools/TruthInfo/interface/TruthGraph.h @@ -47,9 +47,9 @@ class TruthGraph { std::vector nodes; // Cached payload (optional) - std::vector pdgId; // 0 if not applicable - std::vector status; // 0 if not applicable - + std::vector pdgId; // 0 if not applicable + std::vector status; // 0 if not applicable + std::vector statusFlags; // packed reco::GenStatusFlags, 0 if not available // Packed EncodedEventId for SIM nodes; 0 for GEN nodes std::vector eventId; @@ -83,7 +83,7 @@ class TruthGraph { int32_t nodePdgId(uint32_t nodeId) const { return (nodeId < pdgId.size()) ? pdgId[nodeId] : 0; } int16_t nodeStatus(uint32_t nodeId) const { return (nodeId < status.size()) ? status[nodeId] : 0; } - + uint16_t nodeStatusFlags(uint32_t nodeId) const { return (nodeId < statusFlags.size()) ? statusFlags[nodeId] : 0; } uint64_t nodeEventId(uint32_t nodeId) const { return (nodeId < eventId.size()) ? eventId[nodeId] : 0ull; } int32_t nodeSimTrackToGen(uint32_t nodeId) const { diff --git a/PhysicsTools/TruthInfo/plugins/TruthGraphProducer.cc b/PhysicsTools/TruthInfo/plugins/TruthGraphProducer.cc index d8117c45eeb0a..e28841f141de9 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthGraphProducer.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthGraphProducer.cc @@ -32,7 +32,10 @@ #include "HepMC3/GenParticle.h" #include "HepMC3/GenVertex.h" +#include "DataFormats/HepMCCandidate/interface/GenParticle.h" + #include "PhysicsTools/TruthInfo/interface/TruthGraph.h" +#include "FWCore/MessageLogger/interface/MessageLogger.h" namespace { @@ -188,6 +191,7 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { hepmc2Token_(mayConsume(cfg.getParameter("genEventHepMC"))), simTrackToken_(consumes(cfg.getParameter("simTracks"))), simVertexToken_(consumes(cfg.getParameter("simVertices"))), + genParticlesToken_(mayConsume(cfg.getParameter("genParticles"))), addGenToSimEdges_(cfg.getParameter("addGenToSimEdges")) { produces(); } @@ -204,7 +208,8 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { ->setComment("SimTrackContainer label (typically g4SimHits)"); desc.add("simVertices", edm::InputTag("g4SimHits")) ->setComment("SimVertexContainer label (typically g4SimHits)"); - + desc.add("genParticles", edm::InputTag("genParticles")) + ->setComment("reco::GenParticleCollection used to retrieve packed statusFlags"); desc.add("addGenToSimEdges", true) ->setComment("If true, add cross edges GenParticle -> SimTrack using SimTrack::genpartIndex()"); @@ -221,7 +226,8 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { // --- Fetch GEN (HepMC3 preferred, fallback to HepMC2) GenBuild gb; bool haveGen = false; - + edm::Handle hGenParticles; + evt.getByToken(genParticlesToken_, hGenParticles); { edm::Handle h3; evt.getByToken(hepmc3Token_, h3); @@ -315,7 +321,7 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { out->pdgId.assign(nNodes, 0); out->status.assign(nNodes, 0); out->eventId.assign(nNodes, 0); - + out->statusFlags.assign(nNodes, 0); out->genEventOfNode.assign(nNodes, -1); out->simTrackToGen.assign(nNodes, -1); @@ -352,15 +358,37 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { out->genEventOfNode[nodeId] = compOfTemp[tidx]; } + std::unordered_map genParticleBarcodeToIndex; + genParticleBarcodeToIndex.reserve(gb.particleBarcodeByIndex.size() * 2); + + for (uint32_t i = 0; i < gb.particleBarcodeByIndex.size(); ++i) { + genParticleBarcodeToIndex.emplace(gb.particleBarcodeByIndex[i], i); + } + for (uint32_t i = 0; i < nGenPar; ++i) { const int pbc = gb.partBarcodes[i]; const uint32_t nodeId = baseGenPar + i; + genParBarcodeToNode.emplace(pbc, nodeId); + out->nodes[nodeId] = TruthGraph::NodeRef{TruthGraph::NodeKind::GenParticle, static_cast(pbc)}; out->eventId[nodeId] = 0; const int tidx = tempIndex.at(genKeyParticle(pbc)); out->genEventOfNode[nodeId] = compOfTemp[tidx]; + + auto itIndex = genParticleBarcodeToIndex.find(pbc); + if (hGenParticles.isValid() && itIndex != genParticleBarcodeToIndex.end()) { + const uint32_t genParticleIndex = itIndex->second; + + if (genParticleIndex < hGenParticles->size()) { + auto const& gp = (*hGenParticles)[genParticleIndex]; + + out->pdgId[nodeId] = gp.pdgId(); + out->status[nodeId] = static_cast(gp.status()); + out->statusFlags[nodeId] = static_cast(gp.statusFlags().flags_.to_ulong()); + } + } } } @@ -580,7 +608,37 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { out->edgeKind[pos] = edgeKinds[i]; } } + unsigned nGenEvent = 0; + unsigned nGenVertex = 0; + unsigned nGenParticle = 0; + unsigned nSimVertex = 0; + unsigned nSimTrack = 0; + + for (uint32_t i = 0; i < out->nNodes(); ++i) { + switch (out->nodeRef(i).kind) { + case TruthGraph::NodeKind::GenEvent: + ++nGenEvent; + break; + case TruthGraph::NodeKind::GenVertex: + ++nGenVertex; + break; + case TruthGraph::NodeKind::GenParticle: + ++nGenParticle; + break; + case TruthGraph::NodeKind::SimVertex: + ++nSimVertex; + break; + case TruthGraph::NodeKind::SimTrack: + ++nSimTrack; + break; + } + } + edm::LogPrint("TruthGraphProducer") << "TruthGraph nodes: " + << "GenEvent=" << nGenEvent << " GenVertex=" << nGenVertex + << " GenParticle=" << nGenParticle << " SimVertex=" << nSimVertex + << " SimTrack=" << nSimTrack << " total=" << out->nNodes() + << " edges=" << out->nEdges(); evt.put(std::move(out)); } @@ -589,6 +647,7 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { edm::EDGetTokenT hepmc2Token_; edm::EDGetTokenT simTrackToken_; edm::EDGetTokenT simVertexToken_; + edm::EDGetTokenT genParticlesToken_; bool addGenToSimEdges_; }; diff --git a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc index 20bcb894d5b38..3d59e2bdaa3ac 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc @@ -149,6 +149,48 @@ namespace { return "UNKNOWN"; } + std::string statusFlagsLabel(uint16_t flags) { + struct FlagInfo { + uint16_t bit; + const char* name; + }; + + static constexpr FlagInfo flagInfos[] = { + {1u << 0, "isPrompt"}, + {1u << 1, "isDecayedLeptonHadron"}, + {1u << 2, "isTauDecayProduct"}, + {1u << 3, "isPromptTauDecayProduct"}, + {1u << 4, "isDirectTauDecayProduct"}, + {1u << 5, "isDirectPromptTauDecayProduct"}, + {1u << 6, "isDirectHadronDecayProduct"}, + {1u << 7, "isHardProcess"}, + {1u << 8, "fromHardProcess"}, + {1u << 9, "isHardProcessTauDecayProduct"}, + {1u << 10, "isDirectHardProcessTauDecayProduct"}, + {1u << 11, "fromHardProcessBeforeFSR"}, + {1u << 12, "isFirstCopy"}, + {1u << 13, "isLastCopy"}, + {1u << 14, "isLastCopyBeforeFSR"}, + }; + + std::ostringstream ss; + bool first = true; + + for (auto const& flag : flagInfos) { + if ((flags & flag.bit) == 0) + continue; + + if (!first) + ss << ", "; + ss << flag.name; + first = false; + } + + if (first) + return "none"; + + return ss.str(); + } } // namespace class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { @@ -215,6 +257,10 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { os << " pid: " << pdgLabel(d.pdgId) << "\n"; if (d.status != 0) os << " status: " << d.status << "\n"; + if (d.statusFlags != 0) { + os << " statusFlags: " << d.statusFlags << "\n"; + os << " flags: " << statusFlagsLabel(d.statusFlags) << "\n"; + } if (d.eventId != 0) os << " eid: " << d.eventId << "\n"; if (d.genEvent >= 0) diff --git a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc index c379b153cb768..a07734a7d9442 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -269,6 +270,220 @@ namespace { return keep; } + bool directCollapsibleGenParticleChain(truth::Graph const& graph, + uint32_t particleId, + uint32_t& childId, + uint32_t& decayVertexId) { + if (particleId >= graph.nParticles()) + return false; + + auto const& particle = graph.particles[particleId]; + + if (!particle.hasGen()) + return false; + + if (particle.status == 1) + return false; + + if (particle.pdgId == 0) + return false; + + const auto decayVertices = graph.decayVertices(particleId); + if (decayVertices.size() != 1) + return false; + + const uint32_t vertexId = decayVertices.front(); + if (vertexId >= graph.nVertices()) + return false; + + auto const& vertex = graph.vertices[vertexId]; + + // Keep this pass GEN-only. SIM vertices are left untouched. + if (!vertex.hasGen() || vertex.hasSim()) + return false; + + const auto incoming = graph.incomingParticles(vertexId); + const auto outgoing = graph.outgoingParticles(vertexId); + + if (incoming.size() != 1 || incoming.front() != particleId) + return false; + + if (outgoing.size() != 1) + return false; + + const uint32_t candidateChild = outgoing.front(); + if (candidateChild >= graph.nParticles()) + return false; + + if (candidateChild == particleId) + return false; + + auto const& child = graph.particles[candidateChild]; + + if (!child.hasGen()) + return false; + + if (child.pdgId != particle.pdgId) + return false; + + childId = candidateChild; + decayVertexId = vertexId; + return true; + } + + truth::Graph collapseIntermediateGenParticleChains(truth::Graph const& input) { + if (input.empty()) + return input; + + const uint32_t nParticles = input.nParticles(); + const uint32_t nVertices = input.nVertices(); + + std::vector directChild(nParticles, -1); + std::vector skipVertex(nVertices, 0); + + for (uint32_t particleId = 0; particleId < nParticles; ++particleId) { + uint32_t childId = 0; + uint32_t decayVertexId = 0; + + if (!directCollapsibleGenParticleChain(input, particleId, childId, decayVertexId)) + continue; + + directChild[particleId] = static_cast(childId); + skipVertex[decayVertexId] = 1; + } + + std::vector particleRepresentative(nParticles, 0); + + for (uint32_t particleId = 0; particleId < nParticles; ++particleId) { + uint32_t current = particleId; + + // Protect against unexpected cycles. + for (uint32_t step = 0; step < nParticles; ++step) { + const int32_t next = directChild[current]; + if (next < 0) + break; + + current = static_cast(next); + } + + particleRepresentative[particleId] = current; + } + + truth::Graph output; + + std::unordered_map representativeToNewParticle; + representativeToNewParticle.reserve(nParticles); + + std::vector oldParticleToNew(nParticles, -1); + + for (uint32_t oldParticle = 0; oldParticle < nParticles; ++oldParticle) { + const uint32_t representative = particleRepresentative[oldParticle]; + + auto inserted = + representativeToNewParticle.emplace(representative, static_cast(output.particles.size())); + const uint32_t newParticle = inserted.first->second; + + if (inserted.second) { + output.particles.push_back(input.particles[representative]); + } + + oldParticleToNew[oldParticle] = static_cast(newParticle); + } + + std::vector keepVertex(nVertices, 0); + + for (uint32_t oldVertex = 0; oldVertex < nVertices; ++oldVertex) { + if (skipVertex[oldVertex]) + continue; + + for (uint32_t oldParticle : input.incomingParticles(oldVertex)) { + if (oldParticle < oldParticleToNew.size() && oldParticleToNew[oldParticle] >= 0) { + keepVertex[oldVertex] = 1; + break; + } + } + + if (keepVertex[oldVertex]) + continue; + + for (uint32_t oldParticle : input.outgoingParticles(oldVertex)) { + if (oldParticle < oldParticleToNew.size() && oldParticleToNew[oldParticle] >= 0) { + keepVertex[oldVertex] = 1; + break; + } + } + } + + std::vector oldVertexToNew(nVertices, -1); + + for (uint32_t oldVertex = 0; oldVertex < nVertices; ++oldVertex) { + if (!keepVertex[oldVertex]) + continue; + + oldVertexToNew[oldVertex] = static_cast(output.vertices.size()); + output.vertices.push_back(input.vertices[oldVertex]); + } + + std::vector> particleToDecayVertexPairs; + std::vector> particleToProductionVertexPairs; + std::vector> vertexToOutgoingParticlePairs; + std::vector> vertexToIncomingParticlePairs; + + for (uint32_t oldVertex = 0; oldVertex < nVertices; ++oldVertex) { + const int32_t newVertex = oldVertexToNew[oldVertex]; + if (newVertex < 0) + continue; + + for (uint32_t oldParticle : input.incomingParticles(oldVertex)) { + if (oldParticle >= oldParticleToNew.size()) + continue; + + const int32_t newParticle = oldParticleToNew[oldParticle]; + if (newParticle < 0) + continue; + + particleToDecayVertexPairs.emplace_back(static_cast(newParticle), static_cast(newVertex)); + vertexToIncomingParticlePairs.emplace_back(static_cast(newVertex), + static_cast(newParticle)); + } + + for (uint32_t oldParticle : input.outgoingParticles(oldVertex)) { + if (oldParticle >= oldParticleToNew.size()) + continue; + + const int32_t newParticle = oldParticleToNew[oldParticle]; + if (newParticle < 0) + continue; + + vertexToOutgoingParticlePairs.emplace_back(static_cast(newVertex), + static_cast(newParticle)); + particleToProductionVertexPairs.emplace_back(static_cast(newParticle), + static_cast(newVertex)); + } + } + + buildCSR(output.nParticles(), + particleToDecayVertexPairs, + output.particleToDecayVertexOffsets, + output.particleToDecayVertices); + + buildCSR(output.nParticles(), + particleToProductionVertexPairs, + output.particleToProductionVertexOffsets, + output.particleToProductionVertices); + + buildCSR(output.nVertices(), + vertexToOutgoingParticlePairs, + output.vertexToOutgoingParticleOffsets, + output.vertexToOutgoingParticles); + + buildCSR(output.nVertices(), + vertexToIncomingParticlePairs, + output.vertexToIncomingParticleOffsets, + output.vertexToIncomingParticles); + + return output; + } } // namespace class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { @@ -279,7 +494,8 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { simVertexToken_(mayConsume(cfg.getParameter("simVertices"))), hepmc3Token_(mayConsume(cfg.getParameter("genEventHepMC3"))), hepmc2Token_(mayConsume(cfg.getParameter("genEventHepMC"))), - motherPdgId_(cfg.getParameter("motherPdgId")) { + motherPdgId_(cfg.getParameter("motherPdgId")), + collapseIntermediateGenParticles_(cfg.getParameter("collapseIntermediateGenParticles")) { produces(); } @@ -292,6 +508,10 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { desc.add("genEventHepMC", edm::InputTag("generatorSmeared")); desc.add("motherPdgId", 0) ->setComment("If non-zero, keep only GEN particles with this exact PDG id and all their descendants."); + desc.add("collapseIntermediateGenParticles", true) + ->setComment( + "If true, collapse GEN chains P -> V -> C where P has status != 1, C is the only daughter, " + "and P and C have the same PDG id."); descriptions.addWithDefaultLabel(desc); } @@ -467,6 +687,8 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { p.pdgId = raw.nodePdgId(nodeId); if (p.status == 0) p.status = raw.nodeStatus(nodeId); + if (p.statusFlags == 0) + p.statusFlags = raw.nodeStatusFlags(nodeId); if (haveGenPayload) { const int barcode = static_cast(ref.key); @@ -606,7 +828,9 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { vertexToIncomingParticlePairs, out->vertexToIncomingParticleOffsets, out->vertexToIncomingParticles); - + if (collapseIntermediateGenParticles_) { + *out = collapseIntermediateGenParticleChains(*out); + } if (!out->isConsistent()) { throw cms::Exception("TruthLogicalGraphProducer") << "Produced truth::Graph is not consistent"; } @@ -621,6 +845,7 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { edm::EDGetTokenT hepmc3Token_; edm::EDGetTokenT hepmc2Token_; int32_t motherPdgId_; + bool collapseIntermediateGenParticles_ = true; }; DEFINE_FWK_MODULE(TruthLogicalGraphProducer); diff --git a/PhysicsTools/TruthInfo/src/Graph.cc b/PhysicsTools/TruthInfo/src/Graph.cc index 2c335f6224885..84831a8948872 100644 --- a/PhysicsTools/TruthInfo/src/Graph.cc +++ b/PhysicsTools/TruthInfo/src/Graph.cc @@ -59,6 +59,8 @@ std::optional truth::Particle::checkpoint(uint32_t checkpoint return std::nullopt; } +uint16_t truth::Particle::statusFlags() const { return data().statusFlags; } + bool truth::Particle::isRoot() const { return valid() && graph_->productionVertices(id_).empty(); } bool truth::Particle::isLeaf() const { return valid() && graph_->decayVertices(id_).empty(); } From d71d50a0db560abe39b106b59c68c4a24e72201b Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Mon, 11 May 2026 14:55:00 +0200 Subject: [PATCH 10/53] Collapse intermediate same-PDG GEN particle chains Add an optional post-processing pass for the logical truth graph. The pass collapses GEN-only chains of the form P -> V -> C when P has status different from 1, C is the only daughter, and P and C have the same PDG id. This removes intermediate generator-history copies while preserving the physics-forward graph topology. --- .../TruthInfo/plugins/TruthGraphDumper.cc | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc b/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc index ea60b1680b554..139cda76852f7 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc @@ -185,6 +185,49 @@ namespace { return ""; } + std::string statusFlagsLabel(uint16_t flags) { + struct FlagInfo { + uint16_t bit; + const char* name; + }; + + static constexpr FlagInfo flagInfos[] = { + {1u << 0, "isPrompt"}, + {1u << 1, "isDecayedLeptonHadron"}, + {1u << 2, "isTauDecayProduct"}, + {1u << 3, "isPromptTauDecayProduct"}, + {1u << 4, "isDirectTauDecayProduct"}, + {1u << 5, "isDirectPromptTauDecayProduct"}, + {1u << 6, "isDirectHadronDecayProduct"}, + {1u << 7, "isHardProcess"}, + {1u << 8, "fromHardProcess"}, + {1u << 9, "isHardProcessTauDecayProduct"}, + {1u << 10, "isDirectHardProcessTauDecayProduct"}, + {1u << 11, "fromHardProcessBeforeFSR"}, + {1u << 12, "isFirstCopy"}, + {1u << 13, "isLastCopy"}, + {1u << 14, "isLastCopyBeforeFSR"}, + }; + + std::ostringstream ss; + bool first = true; + + for (auto const& flag : flagInfos) { + if ((flags & flag.bit) == 0) + continue; + + if (!first) + ss << ", "; + ss << flag.name; + first = false; + } + + if (first) + return "none"; + + return ss.str(); + } + } // anonymous namespace class TruthGraphDumper : public edm::one::EDAnalyzer<> { From d35af430c2adf2a2cb3863a9e690393a1237f7ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Cuisset?= Date: Mon, 11 May 2026 14:56:05 +0200 Subject: [PATCH 11/53] Dump information also as DOT file attributes * Dump information also as DOT file attributes * Dump type= code format --- .../TruthInfo/plugins/TruthGraphDumper.cc | 73 ++++++++++++++++++- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc b/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc index 139cda76852f7..d4d5b425cf9d7 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc @@ -360,12 +360,79 @@ class TruthGraphDumper : public edm::one::EDAnalyzer<> { } // Node style - os << " n" << i << " [shape=" << shapeFor(r.kind); + os << " n" << i << " [shape=" << shapeFor(r.kind) << ", type=" << kindName(r.kind) << ", "; + os << "crossedBoundary=" << crossedBoundary << ","; if (crossedBoundary) - os << ", color=\"red\", penwidth=2"; + os << "color=\"red\", penwidth=2, "; + + os << "pdg=" << pdg << ", status=" << st << ", eid=" << eid << ","; + // --- GEN enrichment + if (r.kind == TruthGraph::NodeKind::GenEvent) { + if (ev2) { + os << "HepMCversion=2, event=" << ev2->event_number() << ", spid=" << ev2->signal_process_id() << ","; + } else if (have3) { + os << "HepMCversion=3, event=" << ev3.event_number() << ","; + } + } else if (r.kind == TruthGraph::NodeKind::GenParticle) { + const int bc = static_cast(r.key); + if (ev2) { + auto it = bc2p.find(bc); + if (it != bc2p.end()) { + auto const* p = it->second; + const int prod = p->production_vertex() ? p->production_vertex()->barcode() : 0; + const int endv = p->end_vertex() ? p->end_vertex()->barcode() : 0; + os << "pid=" << p->pdg_id() << ",status=" << p->status() << ", p4=\"" << fmtP4(p->momentum()) + << "\", m=" << std::fixed << std::setprecision(3) << p->generated_mass() << ", prodVtx=" << prod + << ", endVtx=" << endv << ","; + } + } else if (have3) { + auto it = id3p.find(bc); + if (it != id3p.end() && it->second) { + auto const& p = it->second; + const int prod = p->production_vertex() ? p->production_vertex()->id() : 0; + const int endv = p->end_vertex() ? p->end_vertex()->id() : 0; + os << "pid=" << p->pdg_id() << ",status=" << p->status() << ", p4=\"" << fmtP4(p->momentum()) + << "\", prodVtx=" << prod << ", endVtx=" << endv << ","; + } + } + } else if (r.kind == TruthGraph::NodeKind::GenVertex) { + const int bc = static_cast(r.key); + if (ev2) { + auto it = bc2v.find(bc); + if (it != bc2v.end()) { + auto const* v = it->second; + os << "barcode=" << v->barcode() << ", x4=<" << fmtX4(v->position()) << ">, nIn=" << v->particles_in_size() + << ", nOut=" << v->particles_out_size() << ","; + } + } else if (have3) { + auto it = id3v.find(bc); + if (it != id3v.end() && it->second) { + auto const& v = it->second; + os << "status=" << v->status() << ", x4=<" << fmtX4(v->position()) << ">, nIn=" << v->particles_in_size() + << ", nOut=" << v->particles_out_size() << ","; + } + } + } + + // --- SIM enrichment + if (r.kind == TruthGraph::NodeKind::SimTrack && haveSim) { + os << "x4=" << fmtX4(simt->momentum()) << ","; + + const int32_t gn = g.nodeSimTrackToGen(i); + os << "GenParticle_nodeId=" << gn << ","; + + const int32_t vn = g.nodeSimTrackToVtx(i); + os << "SimVertex_nodeId=" << vn << ","; + + if (crossedBoundary) { + os << "idAtBoundary=" << simt->getIDAtBoundary() << "," << "x4boundary=\"" + << fmtX4(simt->getPositionAtBoundary()) << "\", p4boundary=\"" << fmtP4(simt->getMomentumAtBoundary()) + << "\","; + } + } // HTML label - os << ", label=<\n"; + os << "label=<\n"; os << " \n"; os << " \n"; From 7e13c7a3db48dae22971e43e39d3d09dad966d6c Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Mon, 11 May 2026 15:29:09 +0200 Subject: [PATCH 12/53] Quote DOT attributes in truth graph dumper Quote non-trivial custom DOT attributes emitted by TruthGraphDumper, including four-vectors, positions, node type strings, and boundary information. This avoids Graphviz parse errors from parentheses, commas, spaces, and angle-bracket syntax in generated DOT files. --- .../TruthInfo/plugins/TruthGraphDumper.cc | 53 ++++++++++++++----- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc b/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc index d4d5b425cf9d7..5cfe8a4aad410 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc @@ -228,6 +228,32 @@ namespace { return ss.str(); } + std::string dotQuote(std::string const& input) { + std::string out; + out.reserve(input.size() + 2); + + out.push_back('"'); + for (char c : input) { + switch (c) { + case '\\': + out += "\\\\"; + break; + case '"': + out += "\\\""; + break; + case '\n': + out += "\\n"; + break; + default: + out.push_back(c); + break; + } + } + out.push_back('"'); + + return out; + } + } // anonymous namespace class TruthGraphDumper : public edm::one::EDAnalyzer<> { @@ -360,7 +386,7 @@ class TruthGraphDumper : public edm::one::EDAnalyzer<> { } // Node style - os << " n" << i << " [shape=" << shapeFor(r.kind) << ", type=" << kindName(r.kind) << ", "; + os << " n" << i << " [shape=" << shapeFor(r.kind) << ", type=" << dotQuote(kindName(r.kind)) << ", "; os << "crossedBoundary=" << crossedBoundary << ","; if (crossedBoundary) os << "color=\"red\", penwidth=2, "; @@ -381,8 +407,8 @@ class TruthGraphDumper : public edm::one::EDAnalyzer<> { auto const* p = it->second; const int prod = p->production_vertex() ? p->production_vertex()->barcode() : 0; const int endv = p->end_vertex() ? p->end_vertex()->barcode() : 0; - os << "pid=" << p->pdg_id() << ",status=" << p->status() << ", p4=\"" << fmtP4(p->momentum()) - << "\", m=" << std::fixed << std::setprecision(3) << p->generated_mass() << ", prodVtx=" << prod + os << "pid=" << p->pdg_id() << ", status=" << p->status() << ", p4=" << dotQuote(fmtP4(p->momentum())) + << ", m=" << std::fixed << std::setprecision(3) << p->generated_mass() << ", prodVtx=" << prod << ", endVtx=" << endv << ","; } } else if (have3) { @@ -391,8 +417,8 @@ class TruthGraphDumper : public edm::one::EDAnalyzer<> { auto const& p = it->second; const int prod = p->production_vertex() ? p->production_vertex()->id() : 0; const int endv = p->end_vertex() ? p->end_vertex()->id() : 0; - os << "pid=" << p->pdg_id() << ",status=" << p->status() << ", p4=\"" << fmtP4(p->momentum()) - << "\", prodVtx=" << prod << ", endVtx=" << endv << ","; + os << "pid=" << p->pid() << ", status=" << p->status() << ", p4=" << dotQuote(fmtP4(p->momentum())) + << ", prodVtx=" << prod << ", endVtx=" << endv << ","; } } } else if (r.kind == TruthGraph::NodeKind::GenVertex) { @@ -401,23 +427,22 @@ class TruthGraphDumper : public edm::one::EDAnalyzer<> { auto it = bc2v.find(bc); if (it != bc2v.end()) { auto const* v = it->second; - os << "barcode=" << v->barcode() << ", x4=<" << fmtX4(v->position()) << ">, nIn=" << v->particles_in_size() - << ", nOut=" << v->particles_out_size() << ","; + os << "barcode=" << v->barcode() << ", x4=" << dotQuote(fmtX4(v->position())) + << ", nIn=" << v->particles_in_size() << ", nOut=" << v->particles_out_size() << ","; } } else if (have3) { auto it = id3v.find(bc); if (it != id3v.end() && it->second) { auto const& v = it->second; - os << "status=" << v->status() << ", x4=<" << fmtX4(v->position()) << ">, nIn=" << v->particles_in_size() - << ", nOut=" << v->particles_out_size() << ","; + os << "status=" << v->status() << ", x4=" << dotQuote(fmtX4(v->position())) + << ", nIn=" << v->particles_in().size() << ", nOut=" << v->particles_out().size() << ","; } } } // --- SIM enrichment if (r.kind == TruthGraph::NodeKind::SimTrack && haveSim) { - os << "x4=" << fmtX4(simt->momentum()) << ","; - + os << "p4=" << dotQuote(fmtP4(simt->momentum())) << ","; const int32_t gn = g.nodeSimTrackToGen(i); os << "GenParticle_nodeId=" << gn << ","; @@ -425,9 +450,9 @@ class TruthGraphDumper : public edm::one::EDAnalyzer<> { os << "SimVertex_nodeId=" << vn << ","; if (crossedBoundary) { - os << "idAtBoundary=" << simt->getIDAtBoundary() << "," << "x4boundary=\"" - << fmtX4(simt->getPositionAtBoundary()) << "\", p4boundary=\"" << fmtP4(simt->getMomentumAtBoundary()) - << "\","; + os << "idAtBoundary=" << simt->getIDAtBoundary() + << ", x4boundary=" << dotQuote(fmtX4(simt->getPositionAtBoundary())) + << ", p4boundary=" << dotQuote(fmtP4(simt->getMomentumAtBoundary())) << ","; } } From e5cd3cca296e5a8b8412ecb66bd88854b02b03fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Cuisset?= Date: Mon, 11 May 2026 15:43:35 +0200 Subject: [PATCH 13/53] Add extra DOT file labels to TruthLogicalGraphDumper (#64) --- .../TruthInfo/plugins/TruthLogicalGraphDumper.cc | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc index 3d59e2bdaa3ac..7dd0164c3dfc8 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc @@ -237,7 +237,7 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { auto p = g.particle(i); auto const& d = p.data(); - os << " p" << i << " [shape=ellipse"; + os << " p" << i << " [shape=ellipse, hasCheckpoints=" << p.hasCheckpoints() << ", hasGen=" << p.hasGen() << ", hasSim=" << d.hasSim(); if (p.hasCheckpoints()) { os << ", color=\"red\", penwidth=2"; @@ -249,6 +249,15 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { os << ", color=\"darkgreen\""; } + os << ", pid=" << d.pdgId << ", status=" << d.status << ", statusFlags=" << d.statusFlags << ", flags=<" << statusFlagsLabel(d.statusFlags) << ">" + << ", eid=" << d.eventId << ", genEvent=" << d.genEvent << ", isRoot=" << p.isRoot() << ", isLeaf=" << p.isLeaf() + << ", p4=\"" << fmtP4(d.momentum) << "\", nProdVtx=" << p.productionVertices().size() << ", DecayVtx=" << p.decayVertices().size() + << ", nParents=" << p.parents().size() << ", nChildren=" << p.children().size() << ", nCheckpoints=" << d.checkpoints.size(); + + if (raw != nullptr) { + os << ", raw_GEN=<" << rawNodeSummary(raw, d.genNode) << ">, raw_SIM=<" << rawNodeSummary(raw, d.simNode) << ">"; + } + os << ", label=<\n"; os << "
" << i << " " << kindName(r.kind) << " key=" << r.key << "
\n"; os << " \n"; From 0515f181788cfb163b23bed2fa077702dc6cf1e9 Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Mon, 11 May 2026 15:44:58 +0200 Subject: [PATCH 14/53] format --- .../plugins/TruthLogicalGraphDumper.cc | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc index 7dd0164c3dfc8..9e0ba69cbf0bf 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc @@ -237,7 +237,8 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { auto p = g.particle(i); auto const& d = p.data(); - os << " p" << i << " [shape=ellipse, hasCheckpoints=" << p.hasCheckpoints() << ", hasGen=" << p.hasGen() << ", hasSim=" << d.hasSim(); + os << " p" << i << " [shape=ellipse, hasCheckpoints=" << p.hasCheckpoints() << ", hasGen=" << p.hasGen() + << ", hasSim=" << d.hasSim(); if (p.hasCheckpoints()) { os << ", color=\"red\", penwidth=2"; @@ -249,13 +250,17 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { os << ", color=\"darkgreen\""; } - os << ", pid=" << d.pdgId << ", status=" << d.status << ", statusFlags=" << d.statusFlags << ", flags=<" << statusFlagsLabel(d.statusFlags) << ">" - << ", eid=" << d.eventId << ", genEvent=" << d.genEvent << ", isRoot=" << p.isRoot() << ", isLeaf=" << p.isLeaf() - << ", p4=\"" << fmtP4(d.momentum) << "\", nProdVtx=" << p.productionVertices().size() << ", DecayVtx=" << p.decayVertices().size() - << ", nParents=" << p.parents().size() << ", nChildren=" << p.children().size() << ", nCheckpoints=" << d.checkpoints.size(); - + os << ", pid=" << d.pdgId << ", status=" << d.status << ", statusFlags=" << d.statusFlags << ", flags=<" + << statusFlagsLabel(d.statusFlags) << ">" + << ", eid=" << d.eventId << ", genEvent=" << d.genEvent << ", isRoot=" << p.isRoot() + << ", isLeaf=" << p.isLeaf() << ", p4=\"" << fmtP4(d.momentum) + << "\", nProdVtx=" << p.productionVertices().size() << ", DecayVtx=" << p.decayVertices().size() + << ", nParents=" << p.parents().size() << ", nChildren=" << p.children().size() + << ", nCheckpoints=" << d.checkpoints.size(); + if (raw != nullptr) { - os << ", raw_GEN=<" << rawNodeSummary(raw, d.genNode) << ">, raw_SIM=<" << rawNodeSummary(raw, d.simNode) << ">"; + os << ", raw_GEN=<" << rawNodeSummary(raw, d.genNode) << ">, raw_SIM=<" << rawNodeSummary(raw, d.simNode) + << ">"; } os << ", label=<\n"; From 798fe8d2a6270e976b80a478d982572ebbf7f2cd Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Mon, 11 May 2026 15:53:09 +0200 Subject: [PATCH 15/53] Introduce logical graph post-processing step Route logical truth graph cleanups through a dedicated postProcessGraph() function and a post-processing configuration object. The existing same-PDG intermediate GEN particle collapse is kept as the first post-processing pass, making it easier to add future graph filters and treatments without cluttering the producer control flow. --- .../plugins/TruthLogicalGraphProducer.cc | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc index a07734a7d9442..0b74080804467 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc @@ -269,7 +269,9 @@ namespace { return keep; } - + struct GraphPostProcessingConfig { + bool collapseIntermediateGenParticles = true; + }; bool directCollapsibleGenParticleChain(truth::Graph const& graph, uint32_t particleId, uint32_t& childId, @@ -331,6 +333,10 @@ namespace { return true; } + // Post-processing pass: + // Collapse GEN-only chains P -> V -> C when P has status != 1, + // C is the only daughter, and P and C have the same PDG id. + // The kept particle is the last representative in the collapsed chain. truth::Graph collapseIntermediateGenParticleChains(truth::Graph const& input) { if (input.empty()) return input; @@ -484,6 +490,14 @@ namespace { return output; } + + truth::Graph postProcessGraph(truth::Graph input, GraphPostProcessingConfig const& config) { + if (config.collapseIntermediateGenParticles) { + input = collapseIntermediateGenParticleChains(input); + } + + return input; + } } // namespace class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { @@ -494,8 +508,9 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { simVertexToken_(mayConsume(cfg.getParameter("simVertices"))), hepmc3Token_(mayConsume(cfg.getParameter("genEventHepMC3"))), hepmc2Token_(mayConsume(cfg.getParameter("genEventHepMC"))), - motherPdgId_(cfg.getParameter("motherPdgId")), - collapseIntermediateGenParticles_(cfg.getParameter("collapseIntermediateGenParticles")) { + motherPdgId_(cfg.getParameter("motherPdgId")) { + postProcessing_.collapseIntermediateGenParticles = cfg.getParameter("collapseIntermediateGenParticles"); + produces(); } @@ -828,9 +843,7 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { vertexToIncomingParticlePairs, out->vertexToIncomingParticleOffsets, out->vertexToIncomingParticles); - if (collapseIntermediateGenParticles_) { - *out = collapseIntermediateGenParticleChains(*out); - } + *out = postProcessGraph(std::move(*out), postProcessing_); if (!out->isConsistent()) { throw cms::Exception("TruthLogicalGraphProducer") << "Produced truth::Graph is not consistent"; } @@ -845,6 +858,7 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { edm::EDGetTokenT hepmc3Token_; edm::EDGetTokenT hepmc2Token_; int32_t motherPdgId_; + GraphPostProcessingConfig postProcessing_; bool collapseIntermediateGenParticles_ = true; }; From d1c8b0338abf649ccb2dd3b29ee51ac148dffe3f Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Mon, 11 May 2026 16:23:17 +0200 Subject: [PATCH 16/53] Keep GEN momentum for merged logical truth particles Avoid overwriting the nominal GEN four-momentum with the SimTrack momentum when building logical particles that merge GEN and SIM information. SimTrack momentum is now used only for SIM-only logical particles, while boundary information remains stored as checkpoints. Also clean up the logical graph producer post-processing flow. --- PhysicsTools/TruthInfo/interface/Graph.h | 5 ++-- .../TruthInfo/plugins/TruthGraphProducer.cc | 15 ++++++++++ .../plugins/TruthLogicalGraphProducer.cc | 28 +++++++++++++------ 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/PhysicsTools/TruthInfo/interface/Graph.h b/PhysicsTools/TruthInfo/interface/Graph.h index 67feeb4197391..3a58f5d530e4c 100644 --- a/PhysicsTools/TruthInfo/interface/Graph.h +++ b/PhysicsTools/TruthInfo/interface/Graph.h @@ -37,8 +37,9 @@ namespace truth { int32_t genEvent = -1; // Standalone payload. - // Convention: "best available" momentum. - // Prefer SIM if present, otherwise GEN, otherwise default-constructed. + // Nominal physics four-momentum. + // For GEN+SIM particles, this is the GEN four-momentum. + // For SIM-only particles, this is the SimTrack four-momentum. math::XYZTLorentzVectorD momentum; // Optional trajectory checkpoints. diff --git a/PhysicsTools/TruthInfo/plugins/TruthGraphProducer.cc b/PhysicsTools/TruthInfo/plugins/TruthGraphProducer.cc index e28841f141de9..196f625f4364f 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthGraphProducer.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthGraphProducer.cc @@ -438,6 +438,21 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { const int barcode = gb.particleBarcodeByIndex[static_cast(ig)]; auto it = genParBarcodeToNode.find(barcode); if (it != genParBarcodeToNode.end()) { + const int simPdgId = simTracks[i].type(); + const int genPdgId = out->nodePdgId(it->second); + + // SimTrack::genpartIndex() is only meaningful for primary G4 tracks. + // Protect the GEN-SIM association against ordering mismatches by requiring + // PDG id consistency whenever GEN PDG information is available. + if (genPdgId != 0 && genPdgId != simPdgId) { + edm::LogPrint("TruthGraphProducer") + << "Rejecting SimTrack->GenParticle association with mismatched PDG id: " + << "simTrack index=" << i << " trackId=" << simTracks[i].trackId() << " genpartIndex=" << ig + << " simPdgId=" << simPdgId << " genNode=" << it->second << " genBarcode=" << barcode + << " genPdgId=" << genPdgId; + continue; + } + out->simTrackToGen[nodeId] = static_cast(it->second); } } diff --git a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc index 0b74080804467..5629aac18e542 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc @@ -14,7 +14,6 @@ #include #include #include -#include #include #include @@ -90,6 +89,10 @@ namespace { math::XYZTLorentzVectorD position; }; + struct GraphPostProcessingConfig { + bool collapseIntermediateGenParticles = true; + }; + bool isParticleKind(TruthGraph::NodeKind kind) { return kind == TruthGraph::NodeKind::GenParticle || kind == TruthGraph::NodeKind::SimTrack; } @@ -269,9 +272,7 @@ namespace { return keep; } - struct GraphPostProcessingConfig { - bool collapseIntermediateGenParticles = true; - }; + bool directCollapsibleGenParticleChain(truth::Graph const& graph, uint32_t particleId, uint32_t& childId, @@ -498,6 +499,7 @@ namespace { return input; } + } // namespace class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { @@ -510,7 +512,6 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { hepmc2Token_(mayConsume(cfg.getParameter("genEventHepMC"))), motherPdgId_(cfg.getParameter("motherPdgId")) { postProcessing_.collapseIntermediateGenParticles = cfg.getParameter("collapseIntermediateGenParticles"); - produces(); } @@ -731,10 +732,19 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { if (validHandle(hSimTracks)) { const auto trackId = static_cast(ref.key); auto it = simTrackIdToIndex.find(trackId); + if (it != simTrackIdToIndex.end()) { auto const& t = (*hSimTracks)[it->second]; - p.momentum = - math::XYZTLorentzVectorD(t.momentum().px(), t.momentum().py(), t.momentum().pz(), t.momentum().e()); + + const math::XYZTLorentzVectorD simMomentum( + t.momentum().px(), t.momentum().py(), t.momentum().pz(), t.momentum().e()); + + // For merged GEN+SIM particles, keep the GEN four-momentum as the nominal + // physics momentum. The SIM momentum is a simulation-state quantity. + // For SIM-only particles, use the SimTrack momentum. + if (!p.hasGen()) { + p.momentum = simMomentum; + } if (t.crossedBoundary()) { truth::Checkpoint cp; @@ -843,7 +853,9 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { vertexToIncomingParticlePairs, out->vertexToIncomingParticleOffsets, out->vertexToIncomingParticles); + *out = postProcessGraph(std::move(*out), postProcessing_); + if (!out->isConsistent()) { throw cms::Exception("TruthLogicalGraphProducer") << "Produced truth::Graph is not consistent"; } @@ -857,9 +869,9 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { edm::EDGetTokenT simVertexToken_; edm::EDGetTokenT hepmc3Token_; edm::EDGetTokenT hepmc2Token_; + int32_t motherPdgId_; GraphPostProcessingConfig postProcessing_; - bool collapseIntermediateGenParticles_ = true; }; DEFINE_FWK_MODULE(TruthLogicalGraphProducer); From 8b5532f354126cec1f3c7a65f4de289f3cc367bd Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Tue, 12 May 2026 13:10:50 +0200 Subject: [PATCH 17/53] Process multiple files at same time --- .../TruthInfo/plugins/TruthGraphDumper.cc | 23 +- .../TruthInfo/plugins/TruthGraphProducer.cc | 375 +++++++++--------- .../plugins/TruthLogicalGraphDumper.cc | 24 +- .../plugins/TruthLogicalGraphProducer.cc | 249 ++++++++++-- 4 files changed, 451 insertions(+), 220 deletions(-) diff --git a/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc b/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc index 5cfe8a4aad410..e05098f41d5ba 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc @@ -254,6 +254,26 @@ namespace { return out; } + std::string appendEventIdToFilename(std::string const& filename, edm::EventID const& id) { + const auto dotPos = filename.rfind('.'); + + std::ostringstream ss; + if (dotPos == std::string::npos) { + ss << filename; + ss << "_run" << id.run(); + ss << "_lumi" << id.luminosityBlock(); + ss << "_event" << id.event(); + return ss.str(); + } + + ss << filename.substr(0, dotPos); + ss << "_run" << id.run(); + ss << "_lumi" << id.luminosityBlock(); + ss << "_event" << id.event(); + ss << filename.substr(dotPos); + + return ss.str(); + } } // anonymous namespace class TruthGraphDumper : public edm::one::EDAnalyzer<> { @@ -354,7 +374,8 @@ class TruthGraphDumper : public edm::one::EDAnalyzer<> { } } - std::ofstream os(dotFile_); + const std::string eventDotFile = appendEventIdToFilename(dotFile_, evt.id()); + std::ofstream os(eventDotFile); os << "digraph TruthGraph {\n"; os << " rankdir=LR;\n"; os << " node [fontsize=10];\n"; diff --git a/PhysicsTools/TruthInfo/plugins/TruthGraphProducer.cc b/PhysicsTools/TruthInfo/plugins/TruthGraphProducer.cc index 196f625f4364f..fbf616cd9de60 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthGraphProducer.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthGraphProducer.cc @@ -1,16 +1,19 @@ // Author: Felice Pantaleo - CERN // Date: 03/2026 + #include #include +#include #include #include #include #include -#include "FWCore/Framework/interface/stream/EDProducer.h" #include "FWCore/Framework/interface/Event.h" #include "FWCore/Framework/interface/EventSetup.h" #include "FWCore/Framework/interface/MakerMacros.h" +#include "FWCore/Framework/interface/stream/EDProducer.h" +#include "FWCore/MessageLogger/interface/MessageLogger.h" #include "FWCore/ParameterSet/interface/ConfigurationDescriptions.h" #include "FWCore/ParameterSet/interface/ParameterSet.h" #include "FWCore/ParameterSet/interface/ParameterSetDescription.h" @@ -20,26 +23,23 @@ #include "SimDataFormats/Track/interface/SimTrackContainer.h" #include "SimDataFormats/Vertex/interface/SimVertexContainer.h" -// Legacy HepMC (HepMC2) +// Legacy HepMC, HepMC2. #include "SimDataFormats/GeneratorProducts/interface/HepMCProduct.h" #include "HepMC/GenEvent.h" -#include "HepMC/GenVertex.h" #include "HepMC/GenParticle.h" +#include "HepMC/GenVertex.h" -// HepMC3 +// HepMC3. #include "SimDataFormats/GeneratorProducts/interface/HepMC3Product.h" #include "HepMC3/GenEvent.h" #include "HepMC3/GenParticle.h" #include "HepMC3/GenVertex.h" -#include "DataFormats/HepMCCandidate/interface/GenParticle.h" - #include "PhysicsTools/TruthInfo/interface/TruthGraph.h" -#include "FWCore/MessageLogger/interface/MessageLogger.h" namespace { - // Pack EncodedEventId into 64-bit without relying on a particular API + // Pack EncodedEventId into 64 bit without relying on a particular public API. uint64_t packEventId(EncodedEventId const& id) { uint64_t out = 0; static_assert(sizeof(EncodedEventId) <= sizeof(uint64_t), "EncodedEventId larger than 64 bits, adjust packing"); @@ -47,13 +47,15 @@ namespace { return out; } - // Disjoint-set union struct DSU { - std::vector p, r; + std::vector p; + std::vector r; + explicit DSU(int n) : p(n), r(n, 0) { for (int i = 0; i < n; ++i) p[i] = i; } + int find(int x) { while (p[x] != x) { p[x] = p[p[x]]; @@ -61,117 +63,159 @@ namespace { } return x; } + void unite(int a, int b) { a = find(a); b = find(b); if (a == b) return; + if (r[a] < r[b]) std::swap(a, b); + p[b] = a; + if (r[a] == r[b]) ++r[a]; } }; - // Unique key to index GEN nodes in maps (vertex vs particle + barcode) inline int64_t genKeyVertex(int barcode) { return (int64_t(barcode) << 1) | 1LL; } + inline int64_t genKeyParticle(int barcode) { return (int64_t(barcode) << 1); } struct GenBuild { - // lists of unique barcodes (order of appearance) std::vector vtxBarcodes; std::vector partBarcodes; - // index -> barcode for particles in the iteration order (for SimTrack::genpartIndex mapping) + // index -> barcode in HepMC iteration order. Kept for diagnostics only. + // Do not use it to interpret SimTrack::genpartIndex(). std::vector particleBarcodeByIndex; - // bipartite edges in barcode space - // vtx -> part (outgoing) std::vector> vtxToPart; - // part -> vtx (end vertex) std::vector> partToVtx; + + std::unordered_map particlePdgIdByBarcode; + std::unordered_map particleStatusByBarcode; }; - // Build GEN info from HepMC2 GenBuild buildFromHepMC2(HepMC::GenEvent const& ev) { GenBuild gb; std::unordered_set seenV; std::unordered_set seenP; - // vertices + gb.particlePdgIdByBarcode.reserve(ev.particles_size() * 2); + gb.particleStatusByBarcode.reserve(ev.particles_size() * 2); + gb.particleBarcodeByIndex.reserve(ev.particles_size()); + for (auto v = ev.vertices_begin(); v != ev.vertices_end(); ++v) { + if (*v == nullptr) + continue; + const int vbc = (*v)->barcode(); + if (seenV.insert(vbc).second) gb.vtxBarcodes.push_back(vbc); - // outgoing particles for (auto po = (*v)->particles_out_const_begin(); po != (*v)->particles_out_const_end(); ++po) { + if (*po == nullptr) + continue; + const int pbc = (*po)->barcode(); + if (seenP.insert(pbc).second) gb.partBarcodes.push_back(pbc); + gb.vtxToPart.emplace_back(vbc, pbc); } - // incoming particles (orphans are possible; we still model the edge) for (auto pi = (*v)->particles_in_const_begin(); pi != (*v)->particles_in_const_end(); ++pi) { + if (*pi == nullptr) + continue; + const int pbc = (*pi)->barcode(); + if (seenP.insert(pbc).second) gb.partBarcodes.push_back(pbc); + gb.partToVtx.emplace_back(pbc, vbc); } } - // particle iteration order -> barcode (for genpartIndex) for (auto p = ev.particles_begin(); p != ev.particles_end(); ++p) { - gb.particleBarcodeByIndex.push_back((*p)->barcode()); + if (*p == nullptr) + continue; + + const int pbc = (*p)->barcode(); + + gb.particleBarcodeByIndex.push_back(pbc); + gb.particlePdgIdByBarcode.emplace(pbc, (*p)->pdg_id()); + gb.particleStatusByBarcode.emplace(pbc, static_cast((*p)->status())); + + if (seenP.insert(pbc).second) + gb.partBarcodes.push_back(pbc); } return gb; } - // Build GEN info from HepMC3 GenBuild buildFromHepMC3(HepMC3::GenEvent const& ev) { GenBuild gb; std::unordered_set seenV; std::unordered_set seenP; - // vertices + gb.particlePdgIdByBarcode.reserve(ev.particles().size() * 2); + gb.particleStatusByBarcode.reserve(ev.particles().size() * 2); + gb.particleBarcodeByIndex.reserve(ev.particles().size()); + for (auto const& vptr : ev.vertices()) { if (!vptr) continue; - const int vbc = vptr->id(); // HepMC3 vertex id acts like barcode; in CMSSW it is typically negative + + const int vbc = vptr->id(); + if (seenV.insert(vbc).second) gb.vtxBarcodes.push_back(vbc); - // outgoing for (auto const& po : vptr->particles_out()) { if (!po) continue; - const int pbc = po->id(); // particle id (barcode) + + const int pbc = po->id(); + if (seenP.insert(pbc).second) gb.partBarcodes.push_back(pbc); + gb.vtxToPart.emplace_back(vbc, pbc); } - // incoming + for (auto const& pi : vptr->particles_in()) { if (!pi) continue; + const int pbc = pi->id(); + if (seenP.insert(pbc).second) gb.partBarcodes.push_back(pbc); + gb.partToVtx.emplace_back(pbc, vbc); } } - // particle iteration order -> barcode (for genpartIndex) - gb.particleBarcodeByIndex.reserve(ev.particles().size()); for (auto const& pptr : ev.particles()) { if (!pptr) continue; - gb.particleBarcodeByIndex.push_back(pptr->id()); + + const int pbc = pptr->id(); + + gb.particleBarcodeByIndex.push_back(pbc); + gb.particlePdgIdByBarcode.emplace(pbc, pptr->pid()); + gb.particleStatusByBarcode.emplace(pbc, static_cast(pptr->status())); + + if (seenP.insert(pbc).second) + gb.partBarcodes.push_back(pbc); } return gb; @@ -191,7 +235,6 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { hepmc2Token_(mayConsume(cfg.getParameter("genEventHepMC"))), simTrackToken_(consumes(cfg.getParameter("simTracks"))), simVertexToken_(consumes(cfg.getParameter("simVertices"))), - genParticlesToken_(mayConsume(cfg.getParameter("genParticles"))), addGenToSimEdges_(cfg.getParameter("addGenToSimEdges")) { produces(); } @@ -200,18 +243,19 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { edm::ParameterSetDescription desc; desc.add("genEventHepMC3", edm::InputTag("generatorSmeared")) - ->setComment("edm::HepMC3Product label (preferred when available)"); + ->setComment("edm::HepMC3Product label, preferred when available"); desc.add("genEventHepMC", edm::InputTag("generatorSmeared")) - ->setComment("edm::HepMCProduct label (legacy fallback)"); + ->setComment("edm::HepMCProduct label, legacy fallback"); desc.add("simTracks", edm::InputTag("g4SimHits")) - ->setComment("SimTrackContainer label (typically g4SimHits)"); + ->setComment("SimTrackContainer label, typically g4SimHits"); desc.add("simVertices", edm::InputTag("g4SimHits")) - ->setComment("SimVertexContainer label (typically g4SimHits)"); - desc.add("genParticles", edm::InputTag("genParticles")) - ->setComment("reco::GenParticleCollection used to retrieve packed statusFlags"); + ->setComment("SimVertexContainer label, typically g4SimHits"); + desc.add("addGenToSimEdges", true) - ->setComment("If true, add cross edges GenParticle -> SimTrack using SimTrack::genpartIndex()"); + ->setComment( + "If true, add GenParticle -> SimTrack cross edges. The association is built only for primary " + "SimTracks, interpreting SimTrack::genpartIndex() as a HepMC barcode."); descriptions.addWithDefaultLabel(desc); } @@ -219,22 +263,22 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { void produce(edm::Event& evt, const edm::EventSetup&) override { auto out = std::make_unique(); - // --- Fetch SIM const auto& simTracks = evt.get(simTrackToken_); const auto& simVertices = evt.get(simVertexToken_); - // --- Fetch GEN (HepMC3 preferred, fallback to HepMC2) GenBuild gb; bool haveGen = false; - edm::Handle hGenParticles; - evt.getByToken(genParticlesToken_, hGenParticles); + { edm::Handle h3; evt.getByToken(hepmc3Token_, h3); + if (validHandle(h3) && h3->GetEvent() != nullptr) { const HepMC3::GenEventData* data = h3->GetEvent(); + HepMC3::GenEvent ev3; ev3.read_data(*data); + gb = buildFromHepMC3(ev3); haveGen = true; } @@ -243,9 +287,9 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { if (!haveGen) { edm::Handle h2; evt.getByToken(hepmc2Token_, h2); + if (validHandle(h2) && h2->GetEvent() != nullptr) { - const HepMC::GenEvent* ev2 = h2->GetEvent(); - gb = buildFromHepMC2(*ev2); + gb = buildFromHepMC2(*h2->GetEvent()); haveGen = true; } } @@ -253,18 +297,16 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { const uint32_t nSimVtx = static_cast(simVertices.size()); const uint32_t nSimTrk = static_cast(simTracks.size()); - // ---------------------------- - // GEN components -> "GenEvent" - // ---------------------------- int nGenEvents = 0; - std::unordered_map tempIndex; // genKey -> temp idx - std::vector tempKeys; // idx -> genKey + std::unordered_map tempIndex; + std::vector tempKeys; auto getTemp = [&](int64_t k) -> int { auto it = tempIndex.find(k); if (it != tempIndex.end()) return it->second; + const int idx = static_cast(tempKeys.size()); tempIndex.emplace(k, idx); tempKeys.push_back(k); @@ -277,6 +319,7 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { if (haveGen) { for (int vbc : gb.vtxBarcodes) (void)getTemp(genKeyVertex(vbc)); + for (int pbc : gb.partBarcodes) (void)getTemp(genKeyParticle(pbc)); @@ -285,13 +328,16 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { for (auto const& e : gb.vtxToPart) { dsu.unite(getTemp(genKeyVertex(e.first)), getTemp(genKeyParticle(e.second))); } + for (auto const& e : gb.partToVtx) { dsu.unite(getTemp(genKeyParticle(e.first)), getTemp(genKeyVertex(e.second))); } compOfTemp.resize(tempKeys.size(), -1); + for (int i = 0; i < static_cast(tempKeys.size()); ++i) { const int rep = dsu.find(i); + auto it = repToComp.find(rep); if (it == repToComp.end()) { const int cid = nGenEvents++; @@ -318,6 +364,7 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { const uint32_t nNodes = baseSimTrk + nSimTrk; out->nodes.resize(nNodes); + out->pdgId.assign(nNodes, 0); out->status.assign(nNodes, 0); out->eventId.assign(nNodes, 0); @@ -327,19 +374,14 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { out->simTrackToGen.assign(nNodes, -1); out->simTrackToVtx.assign(nNodes, -1); - // ---------------------------- - // Create GenEvent nodes (roots) - // ---------------------------- for (int cid = 0; cid < nGenEvents; ++cid) { const uint32_t nodeId = baseGenEvent + static_cast(cid); + out->nodes[nodeId] = TruthGraph::NodeRef{TruthGraph::NodeKind::GenEvent, static_cast(cid)}; out->eventId[nodeId] = 0; out->genEventOfNode[nodeId] = cid; } - // ---------------------------- - // Create GenVertex / GenParticle nodes - // ---------------------------- std::unordered_map genVtxBarcodeToNode; std::unordered_map genParBarcodeToNode; @@ -350,7 +392,9 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { for (uint32_t i = 0; i < nGenVtx; ++i) { const int vbc = gb.vtxBarcodes[i]; const uint32_t nodeId = baseGenVtx + i; + genVtxBarcodeToNode.emplace(vbc, nodeId); + out->nodes[nodeId] = TruthGraph::NodeRef{TruthGraph::NodeKind::GenVertex, static_cast(vbc)}; out->eventId[nodeId] = 0; @@ -358,13 +402,6 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { out->genEventOfNode[nodeId] = compOfTemp[tidx]; } - std::unordered_map genParticleBarcodeToIndex; - genParticleBarcodeToIndex.reserve(gb.particleBarcodeByIndex.size() * 2); - - for (uint32_t i = 0; i < gb.particleBarcodeByIndex.size(); ++i) { - genParticleBarcodeToIndex.emplace(gb.particleBarcodeByIndex[i], i); - } - for (uint32_t i = 0; i < nGenPar; ++i) { const int pbc = gb.partBarcodes[i]; const uint32_t nodeId = baseGenPar + i; @@ -377,94 +414,81 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { const int tidx = tempIndex.at(genKeyParticle(pbc)); out->genEventOfNode[nodeId] = compOfTemp[tidx]; - auto itIndex = genParticleBarcodeToIndex.find(pbc); - if (hGenParticles.isValid() && itIndex != genParticleBarcodeToIndex.end()) { - const uint32_t genParticleIndex = itIndex->second; + auto itPdg = gb.particlePdgIdByBarcode.find(pbc); + if (itPdg != gb.particlePdgIdByBarcode.end()) + out->pdgId[nodeId] = itPdg->second; - if (genParticleIndex < hGenParticles->size()) { - auto const& gp = (*hGenParticles)[genParticleIndex]; + auto itStatus = gb.particleStatusByBarcode.find(pbc); + if (itStatus != gb.particleStatusByBarcode.end()) + out->status[nodeId] = itStatus->second; - out->pdgId[nodeId] = gp.pdgId(); - out->status[nodeId] = static_cast(gp.status()); - out->statusFlags[nodeId] = static_cast(gp.statusFlags().flags_.to_ulong()); - } - } + // Do not fill statusFlags from reco::GenParticle unless we have a validated + // barcode-to-reco::GenParticle association. + out->statusFlags[nodeId] = 0; } } - // GenParticle barcode -> production GenVertex barcode (from vtx -> part) - std::unordered_map genParToProdVtx; - if (haveGen) { - genParToProdVtx.reserve(gb.partBarcodes.size() * 2); - for (auto const& vp : gb.vtxToPart) { - genParToProdVtx.emplace(vp.second, vp.first); - } - } - - // ---------------------------- - // Create SimVertex nodes - // ---------------------------- std::vector simVtxIndexToNode(nSimVtx, 0); + for (uint32_t i = 0; i < nSimVtx; ++i) { const uint32_t nodeId = baseSimVtx + i; + simVtxIndexToNode[i] = nodeId; + out->nodes[nodeId] = TruthGraph::NodeRef{TruthGraph::NodeKind::SimVertex, static_cast(i)}; out->eventId[nodeId] = packEventId(simVertices[i].eventId()); } - // ---------------------------- - // Create SimTrack nodes - // ---------------------------- std::unordered_map simTrackIdToNode; simTrackIdToNode.reserve(nSimTrk * 2); for (uint32_t i = 0; i < nSimTrk; ++i) { + auto const& simTrack = simTracks[i]; + const uint32_t nodeId = baseSimTrk + i; - const uint32_t tid = simTracks[i].trackId(); + const uint32_t tid = simTrack.trackId(); + simTrackIdToNode.emplace(tid, nodeId); out->nodes[nodeId] = TruthGraph::NodeRef{TruthGraph::NodeKind::SimTrack, static_cast(tid)}; - out->pdgId[nodeId] = simTracks[i].type(); - out->eventId[nodeId] = packEventId(simTracks[i].eventId()); + out->pdgId[nodeId] = simTrack.type(); + out->eventId[nodeId] = packEventId(simTrack.eventId()); - const int vtxIdx = simTracks[i].vertIndex(); + const int vtxIdx = simTrack.vertIndex(); if (vtxIdx >= 0 && static_cast(vtxIdx) < nSimVtx) { out->simTrackToVtx[nodeId] = static_cast(simVtxIndexToNode[static_cast(vtxIdx)]); } - if (haveGen && !simTracks[i].noGenpart()) { - const int ig = simTracks[i].genpartIndex(); - if (ig >= 0 && static_cast(ig) < gb.particleBarcodeByIndex.size()) { - const int barcode = gb.particleBarcodeByIndex[static_cast(ig)]; - auto it = genParBarcodeToNode.find(barcode); - if (it != genParBarcodeToNode.end()) { - const int simPdgId = simTracks[i].type(); - const int genPdgId = out->nodePdgId(it->second); - - // SimTrack::genpartIndex() is only meaningful for primary G4 tracks. - // Protect the GEN-SIM association against ordering mismatches by requiring - // PDG id consistency whenever GEN PDG information is available. - if (genPdgId != 0 && genPdgId != simPdgId) { - edm::LogPrint("TruthGraphProducer") - << "Rejecting SimTrack->GenParticle association with mismatched PDG id: " - << "simTrack index=" << i << " trackId=" << simTracks[i].trackId() << " genpartIndex=" << ig - << " simPdgId=" << simPdgId << " genNode=" << it->second << " genBarcode=" << barcode - << " genPdgId=" << genPdgId; - continue; - } - - out->simTrackToGen[nodeId] = static_cast(it->second); + // SimTrack::genpartIndex() must be used only for primary G4 tracks. + // For non-primary tracks, SimTrack::getPrimaryOrLastStoredID() can still + // contain a generator barcode, but that is only ancestry information for + // orphan/backscattered tracks and must not be interpreted as a direct + // SimTrack -> GenParticle association. + if (haveGen && simTrack.isPrimary() && !simTrack.noGenpart()) { + const int barcode = simTrack.genpartIndex(); + + auto it = genParBarcodeToNode.find(barcode); + if (it != genParBarcodeToNode.end()) { + const int simPdgId = simTrack.type(); + const int genPdgId = out->nodePdgId(it->second); + + if (genPdgId != 0 && genPdgId != simPdgId) { + edm::LogPrint("TruthGraphProducer") + << "Rejecting primary SimTrack->GenParticle association with mismatched PDG id: " + << "simTrack index=" << i << " trackId=" << simTrack.trackId() << " genpartBarcode=" << barcode + << " simPdgId=" << simPdgId << " genNode=" << it->second << " genPdgId=" << genPdgId; + continue; } + + out->simTrackToGen[nodeId] = static_cast(it->second); } } } - // ---------------------------- - // Build edges (+ kinds) and compress to CSR - // ---------------------------- std::vector> edgePairs; std::vector edgeKinds; - edgePairs.reserve(12 * (nGenVtx + nGenPar + nSimTrk)); + + edgePairs.reserve(8 * (nGenVtx + nGenPar + nSimTrk)); edgeKinds.reserve(edgePairs.capacity()); auto push_edge = [&](uint32_t src, uint32_t dst, TruthGraph::EdgeKind k) { @@ -472,21 +496,17 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { edgeKinds.emplace_back(static_cast(k)); }; - // Dedup GenVtx->SimVtx cross edges - std::unordered_set genVtxToSimVtxSeen; - genVtxToSimVtxSeen.reserve(2 * nSimTrk); - auto packPair = [](uint32_t a, uint32_t b) -> uint64_t { return (uint64_t(a) << 32) | uint64_t(b); }; - - // GEN edges: connect GenEvent(component) -> GenVertex roots (or all vertices if no roots found) if (haveGen) { std::unordered_map vtxIncoming; vtxIncoming.reserve(nGenVtx * 2); + for (int vbc : gb.vtxBarcodes) vtxIncoming.emplace(vbc, 0); + for (auto const& e : gb.partToVtx) { auto it = vtxIncoming.find(e.second); if (it != vtxIncoming.end()) - it->second++; + ++it->second; } std::vector> rootsByComp(nGenEvents); @@ -495,18 +515,23 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { for (int vbc : gb.vtxBarcodes) { const int tidx = tempIndex.at(genKeyVertex(vbc)); const int cid = compOfTemp[tidx]; + if (cid < 0 || cid >= nGenEvents) continue; + allVtxByComp[cid].push_back(vbc); + if (vtxIncoming[vbc] == 0) rootsByComp[cid].push_back(vbc); } for (int cid = 0; cid < nGenEvents; ++cid) { const uint32_t genEventNode = baseGenEvent + static_cast(cid); + auto roots = rootsByComp[cid]; if (roots.empty()) roots = allVtxByComp[cid]; + for (int vbc : roots) { auto itV = genVtxBarcodeToNode.find(vbc); if (itV != genVtxBarcodeToNode.end()) { @@ -518,32 +543,36 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { for (auto const& e : gb.vtxToPart) { auto itV = genVtxBarcodeToNode.find(e.first); auto itP = genParBarcodeToNode.find(e.second); + if (itV != genVtxBarcodeToNode.end() && itP != genParBarcodeToNode.end()) { push_edge(itV->second, itP->second, TruthGraph::EdgeKind::Gen); } } + for (auto const& e : gb.partToVtx) { auto itP = genParBarcodeToNode.find(e.first); auto itV = genVtxBarcodeToNode.find(e.second); + if (itP != genParBarcodeToNode.end() && itV != genVtxBarcodeToNode.end()) { push_edge(itP->second, itV->second, TruthGraph::EdgeKind::Gen); } } } - // SIM edges: for (uint32_t i = 0; i < nSimTrk; ++i) { - const auto& t = simTracks[i]; + auto const& t = simTracks[i]; const uint32_t childNode = baseSimTrk + i; const int vtxIdx = t.vertIndex(); if (vtxIdx < 0 || static_cast(vtxIdx) >= nSimVtx) continue; + const uint32_t vtxNode = simVtxIndexToNode[static_cast(vtxIdx)]; push_edge(vtxNode, childNode, TruthGraph::EdgeKind::Sim); - const int parentTid = simVertices[static_cast(vtxIdx)].parentIndex(); // trackId (not index) + const int parentTid = simVertices[static_cast(vtxIdx)].parentIndex(); + if (parentTid > 0) { auto itParent = simTrackIdToNode.find(static_cast(parentTid)); if (itParent != simTrackIdToNode.end()) { @@ -552,108 +581,86 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { } } - // CROSS GEN->SIM: GenParticle -> SimTrack + // Cross-domain particle associations only. These edges are created only for + // primary SimTracks that carry a validated HepMC barcode. + // + // GenVertex -> SimVertex edges are intentionally not created here because + // shared Geant4 source or injection vertices can create artificial many-to-one topology. if (addGenToSimEdges_ && haveGen) { for (uint32_t i = 0; i < nSimTrk; ++i) { const uint32_t simNode = baseSimTrk + i; const int32_t genNode = out->simTrackToGen[simNode]; + if (genNode >= 0) { push_edge(static_cast(genNode), simNode, TruthGraph::EdgeKind::GenToSim); } } } - // CROSS GEN->SIM: GenVertex(prod of GenParticle) -> SimVertex(prod of SimTrack) - if (haveGen) { - for (uint32_t i = 0; i < nSimTrk; ++i) { - const uint32_t simTrackNode = baseSimTrk + i; - - const int32_t genParNode = out->simTrackToGen[simTrackNode]; - if (genParNode < 0) - continue; - - const int32_t simVtxNode_i32 = out->simTrackToVtx[simTrackNode]; - if (simVtxNode_i32 < 0) - continue; - const uint32_t simVtxNode = static_cast(simVtxNode_i32); - - auto const& genParRef = out->nodes[static_cast(genParNode)]; - if (genParRef.kind != TruthGraph::NodeKind::GenParticle) - continue; - const int pbc = static_cast(genParRef.key); - - auto itProd = genParToProdVtx.find(pbc); - if (itProd == genParToProdVtx.end()) - continue; - - auto itV = genVtxBarcodeToNode.find(itProd->second); - if (itV == genVtxBarcodeToNode.end()) - continue; - - const uint32_t genVtxNode = itV->second; - - const uint64_t packed = packPair(genVtxNode, simVtxNode); - if (!genVtxToSimVtxSeen.insert(packed).second) - continue; - - push_edge(genVtxNode, simVtxNode, TruthGraph::EdgeKind::GenToSim); - } - } - - // CSR compress out->offsets.assign(nNodes + 1, 0); + for (auto const& e : edgePairs) { if (e.first < nNodes) - out->offsets[e.first + 1] += 1; + ++out->offsets[e.first + 1]; } + for (uint32_t i = 1; i <= nNodes; ++i) out->offsets[i] += out->offsets[i - 1]; const uint32_t nEdges = out->offsets.back(); + out->edges.assign(nEdges, 0); out->edgeKind.assign(nEdges, static_cast(TruthGraph::EdgeKind::Gen)); std::vector cursor = out->offsets; - for (size_t i = 0; i < edgePairs.size(); ++i) { + + for (std::size_t i = 0; i < edgePairs.size(); ++i) { const uint32_t src = edgePairs[i].first; const uint32_t dst = edgePairs[i].second; + if (src < nNodes && dst < nNodes) { const uint32_t pos = cursor[src]++; out->edges[pos] = dst; out->edgeKind[pos] = edgeKinds[i]; } } - unsigned nGenEvent = 0; - unsigned nGenVertex = 0; - unsigned nGenParticle = 0; - unsigned nSimVertex = 0; - unsigned nSimTrack = 0; + + unsigned nGenEventOut = 0; + unsigned nGenVertexOut = 0; + unsigned nGenParticleOut = 0; + unsigned nSimVertexOut = 0; + unsigned nSimTrackOut = 0; + unsigned nGenToSimParticleLinks = 0; for (uint32_t i = 0; i < out->nNodes(); ++i) { switch (out->nodeRef(i).kind) { case TruthGraph::NodeKind::GenEvent: - ++nGenEvent; + ++nGenEventOut; break; case TruthGraph::NodeKind::GenVertex: - ++nGenVertex; + ++nGenVertexOut; break; case TruthGraph::NodeKind::GenParticle: - ++nGenParticle; + ++nGenParticleOut; break; case TruthGraph::NodeKind::SimVertex: - ++nSimVertex; + ++nSimVertexOut; break; case TruthGraph::NodeKind::SimTrack: - ++nSimTrack; + ++nSimTrackOut; + if (out->simTrackToGen[i] >= 0) + ++nGenToSimParticleLinks; break; } } edm::LogPrint("TruthGraphProducer") << "TruthGraph nodes: " - << "GenEvent=" << nGenEvent << " GenVertex=" << nGenVertex - << " GenParticle=" << nGenParticle << " SimVertex=" << nSimVertex - << " SimTrack=" << nSimTrack << " total=" << out->nNodes() - << " edges=" << out->nEdges(); + << "GenEvent=" << nGenEventOut << " GenVertex=" << nGenVertexOut + << " GenParticle=" << nGenParticleOut << " SimVertex=" << nSimVertexOut + << " SimTrack=" << nSimTrackOut << " total=" << out->nNodes() + << " edges=" << out->nEdges() + << " primaryGenToSimParticleLinks=" << nGenToSimParticleLinks; + evt.put(std::move(out)); } @@ -662,7 +669,7 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { edm::EDGetTokenT hepmc2Token_; edm::EDGetTokenT simTrackToken_; edm::EDGetTokenT simVertexToken_; - edm::EDGetTokenT genParticlesToken_; + bool addGenToSimEdges_; }; diff --git a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc index 9e0ba69cbf0bf..cb74b5f54a281 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc @@ -191,6 +191,27 @@ namespace { return ss.str(); } + + std::string appendEventIdToFilename(std::string const& filename, edm::EventID const& id) { + const auto dotPos = filename.rfind('.'); + + std::ostringstream ss; + if (dotPos == std::string::npos) { + ss << filename; + ss << "_run" << id.run(); + ss << "_lumi" << id.luminosityBlock(); + ss << "_event" << id.event(); + return ss.str(); + } + + ss << filename.substr(0, dotPos); + ss << "_run" << id.run(); + ss << "_lumi" << id.luminosityBlock(); + ss << "_event" << id.event(); + ss << filename.substr(dotPos); + + return ss.str(); + } } // namespace class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { @@ -222,7 +243,8 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { evt.getByToken(rawToken_, hRaw); TruthGraph const* raw = hRaw.isValid() ? &(*hRaw) : nullptr; - std::ofstream os(dotFile_); + const std::string eventDotFile = appendEventIdToFilename(dotFile_, evt.id()); + std::ofstream os(eventDotFile); os << "digraph TruthLogicalGraph {\n"; os << " rankdir=LR;\n"; os << " node [fontsize=10];\n"; diff --git a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc index 5629aac18e542..5b5b20aeec585 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc @@ -6,8 +6,11 @@ // Standalone payload (momentum/position/checkpoints) is materialized from optional // HepMC2 / HepMC3 / SimTrack / SimVertex inputs. // -// For debugging purposes, GenVertex and SimVertex are intentionally NOT merged. -// Each raw vertex node becomes its own logical truth::Vertex. +// GenParticle and SimTrack nodes are merged when a robust association exists. +// The corresponding production GenVertex and SimVertex nodes are also merged, +// but only for locally one-to-one GenVertex <-> SimVertex associations induced +// by the same GenParticle <-> SimTrack match. This avoids creating artificial +// high-degree merged vertices from transitive many-to-one vertex matches. #include #include @@ -247,8 +250,6 @@ namespace { if (genParticlePdgId(raw, nodeId, genParticlePayload) != motherPdgId) continue; - // Start from the selected mother itself. - // This keeps the mother, its decay vertices, daughters, and all descendants. if (!keep[nodeId]) { keep[nodeId] = 1; queue.push(nodeId); @@ -273,6 +274,79 @@ namespace { return keep; } + std::vector buildGenParticleToProductionGenVertexMap(TruthGraph const& raw, + std::vector const& keepRawNode) { + const uint32_t nRawNodes = raw.nNodes(); + std::vector genParticleToProductionGenVertex(nRawNodes, -1); + + for (uint32_t src = 0; src < nRawNodes; ++src) { + if (!keepRawNode[src]) + continue; + + if (raw.nodeRef(src).kind != TruthGraph::NodeKind::GenVertex) + continue; + + const auto dsts = raw.children(src); + const auto edgeKinds = raw.childrenEdgeKinds(src); + + for (std::size_t i = 0; i < dsts.size(); ++i) { + const uint32_t dst = dsts[i]; + + if (dst >= nRawNodes || !keepRawNode[dst]) + continue; + + if (raw.nodeRef(dst).kind != TruthGraph::NodeKind::GenParticle) + continue; + + if (static_cast(edgeKinds[i]) != TruthGraph::EdgeKind::Gen) + continue; + + if (genParticleToProductionGenVertex[dst] < 0) { + genParticleToProductionGenVertex[dst] = static_cast(src); + } + } + } + + return genParticleToProductionGenVertex; + } + + void buildRawSimVertexDegrees(TruthGraph const& raw, + std::vector const& keepRawNode, + std::vector& simVertexIncomingSimTracks, + std::vector& simVertexOutgoingSimTracks) { + const uint32_t nRawNodes = raw.nNodes(); + + simVertexIncomingSimTracks.assign(nRawNodes, 0); + simVertexOutgoingSimTracks.assign(nRawNodes, 0); + + for (uint32_t src = 0; src < nRawNodes; ++src) { + if (!keepRawNode[src]) + continue; + + const auto dsts = raw.children(src); + const auto edgeKinds = raw.childrenEdgeKinds(src); + + for (std::size_t i = 0; i < dsts.size(); ++i) { + const uint32_t dst = dsts[i]; + + if (dst >= nRawNodes || !keepRawNode[dst]) + continue; + + if (static_cast(edgeKinds[i]) != TruthGraph::EdgeKind::Sim) + continue; + + const auto srcKind = raw.nodeRef(src).kind; + const auto dstKind = raw.nodeRef(dst).kind; + + if (srcKind == TruthGraph::NodeKind::SimTrack && dstKind == TruthGraph::NodeKind::SimVertex) { + ++simVertexIncomingSimTracks[dst]; + } else if (srcKind == TruthGraph::NodeKind::SimVertex && dstKind == TruthGraph::NodeKind::SimTrack) { + ++simVertexOutgoingSimTracks[src]; + } + } + } + } + bool directCollapsibleGenParticleChain(truth::Graph const& graph, uint32_t particleId, uint32_t& childId, @@ -301,7 +375,6 @@ namespace { auto const& vertex = graph.vertices[vertexId]; - // Keep this pass GEN-only. SIM vertices are left untouched. if (!vertex.hasGen() || vertex.hasSim()) return false; @@ -334,10 +407,6 @@ namespace { return true; } - // Post-processing pass: - // Collapse GEN-only chains P -> V -> C when P has status != 1, - // C is the only daughter, and P and C have the same PDG id. - // The kept particle is the last representative in the collapsed chain. truth::Graph collapseIntermediateGenParticleChains(truth::Graph const& input) { if (input.empty()) return input; @@ -364,7 +433,6 @@ namespace { for (uint32_t particleId = 0; particleId < nParticles; ++particleId) { uint32_t current = particleId; - // Protect against unexpected cycles. for (uint32_t step = 0; step < nParticles; ++step) { const int32_t next = directChild[current]; if (next < 0) @@ -510,7 +578,8 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { simVertexToken_(mayConsume(cfg.getParameter("simVertices"))), hepmc3Token_(mayConsume(cfg.getParameter("genEventHepMC3"))), hepmc2Token_(mayConsume(cfg.getParameter("genEventHepMC"))), - motherPdgId_(cfg.getParameter("motherPdgId")) { + motherPdgId_(cfg.getParameter("motherPdgId")), + mergeGenSimVertices_(cfg.getParameter("mergeGenSimVertices")) { postProcessing_.collapseIntermediateGenParticles = cfg.getParameter("collapseIntermediateGenParticles"); produces(); } @@ -524,6 +593,10 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { desc.add("genEventHepMC", edm::InputTag("generatorSmeared")); desc.add("motherPdgId", 0) ->setComment("If non-zero, keep only GEN particles with this exact PDG id and all their descendants."); + desc.add("mergeGenSimVertices", true) + ->setComment( + "If true, merge production GenVertex and SimVertex only for locally one-to-one matches induced by " + "GenParticle <-> SimTrack associations."); desc.add("collapseIntermediateGenParticles", true) ->setComment( "If true, collapse GEN chains P -> V -> C where P has status != 1, C is the only daughter, " @@ -583,15 +656,20 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { } const auto keepRawNode = buildKeepMaskForMotherPdgId(raw, genParticlePayload, motherPdgId_); + const auto genParticleToProductionGenVertex = buildGenParticleToProductionGenVertexMap(raw, keepRawNode); + + std::vector rawSimVertexIncomingSimTracks; + std::vector rawSimVertexOutgoingSimTracks; + buildRawSimVertexDegrees(raw, keepRawNode, rawSimVertexIncomingSimTracks, rawSimVertexOutgoingSimTracks); // ------------------------------------------------------------------ // 1. Temporary ids // ------------------------------------------------------------------ std::vector rawToParticleTmp(nRawNodes, -1); - std::vector rawToVertex(nRawNodes, -1); + std::vector rawToVertexTmp(nRawNodes, -1); int nParticleTmp = 0; - int nVertexLogical = 0; + int nVertexTmp = 0; for (uint32_t nodeId = 0; nodeId < nRawNodes; ++nodeId) { if (!keepRawNode[nodeId]) @@ -601,14 +679,53 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { if (isParticleKind(kind)) { rawToParticleTmp[nodeId] = nParticleTmp++; } else if (isVertexKind(kind)) { - rawToVertex[nodeId] = nVertexLogical++; + rawToVertexTmp[nodeId] = nVertexTmp++; } } DSU particleDSU(nParticleTmp); + DSU vertexDSU(nVertexTmp); + + std::vector> productionVertexMergeCandidates; + + auto addProductionVertexMergeCandidate = [&](uint32_t simTrackNode, uint32_t genParticleNode) { + if (!mergeGenSimVertices_) + return; + + const int32_t simVertexNode = raw.nodeSimTrackToVtx(simTrackNode); + if (simVertexNode < 0) + return; + + if (genParticleNode >= genParticleToProductionGenVertex.size()) + return; + + const int32_t genVertexNode = genParticleToProductionGenVertex[genParticleNode]; + if (genVertexNode < 0) + return; + + const uint32_t gv = static_cast(genVertexNode); + const uint32_t sv = static_cast(simVertexNode); + + if (gv >= nRawNodes || sv >= nRawNodes) + return; + + if (!keepRawNode[gv] || !keepRawNode[sv]) + return; + + if (rawToVertexTmp[gv] < 0 || rawToVertexTmp[sv] < 0) + return; + + if (raw.nodeRef(gv).kind != TruthGraph::NodeKind::GenVertex) + return; + + if (raw.nodeRef(sv).kind != TruthGraph::NodeKind::SimVertex) + return; + + productionVertexMergeCandidates.emplace_back(gv, sv); + }; // ------------------------------------------------------------------ - // 2. Merge only particles across GEN <-> SIM + // 2. Merge particles across GEN <-> SIM and collect vertex merge candidates // ------------------------------------------------------------------ for (uint32_t nodeId = 0; nodeId < nRawNodes; ++nodeId) { if (!keepRawNode[nodeId]) @@ -621,17 +738,22 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { const int32_t genNode = raw.nodeSimTrackToGen(nodeId); if (genNode < 0) continue; - if (static_cast(genNode) >= nRawNodes) + + const uint32_t genNodeU32 = static_cast(genNode); + if (genNodeU32 >= nRawNodes) continue; - if (!keepRawNode[static_cast(genNode)]) + + if (!keepRawNode[genNodeU32]) continue; - if (raw.nodeRef(static_cast(genNode)).kind != TruthGraph::NodeKind::GenParticle) + + if (raw.nodeRef(genNodeU32).kind != TruthGraph::NodeKind::GenParticle) continue; - if (rawToParticleTmp[nodeId] < 0 || rawToParticleTmp[static_cast(genNode)] < 0) + if (rawToParticleTmp[nodeId] < 0 || rawToParticleTmp[genNodeU32] < 0) continue; - particleDSU.unite(rawToParticleTmp[nodeId], rawToParticleTmp[static_cast(genNode)]); + particleDSU.unite(rawToParticleTmp[nodeId], rawToParticleTmp[genNodeU32]); + addProductionVertexMergeCandidate(nodeId, genNodeU32); } for (uint32_t src = 0; src < nRawNodes; ++src) { @@ -652,17 +774,54 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { if (ek != TruthGraph::EdgeKind::GenToSim && ek != TruthGraph::EdgeKind::SimToGen) continue; - if (isGenParticleToSimTrack(raw, src, dst)) { - if (rawToParticleTmp[src] < 0 || rawToParticleTmp[dst] < 0) - continue; + if (!isGenParticleToSimTrack(raw, src, dst)) + continue; - particleDSU.unite(rawToParticleTmp[src], rawToParticleTmp[dst]); - } + if (rawToParticleTmp[src] < 0 || rawToParticleTmp[dst] < 0) + continue; + + particleDSU.unite(rawToParticleTmp[src], rawToParticleTmp[dst]); + addProductionVertexMergeCandidate(dst, src); + } + } + + if (mergeGenSimVertices_) { + std::sort(productionVertexMergeCandidates.begin(), productionVertexMergeCandidates.end()); + productionVertexMergeCandidates.erase( + std::unique(productionVertexMergeCandidates.begin(), productionVertexMergeCandidates.end()), + productionVertexMergeCandidates.end()); + + std::vector genVertexCandidateMultiplicity(nRawNodes, 0); + std::vector simVertexCandidateMultiplicity(nRawNodes, 0); + + for (auto const& candidate : productionVertexMergeCandidates) { + if (genVertexCandidateMultiplicity[candidate.first] < UINT16_MAX) + ++genVertexCandidateMultiplicity[candidate.first]; + if (simVertexCandidateMultiplicity[candidate.second] < UINT16_MAX) + ++simVertexCandidateMultiplicity[candidate.second]; + } + + for (auto const& candidate : productionVertexMergeCandidates) { + const uint32_t gv = candidate.first; + const uint32_t sv = candidate.second; + + // Only accept locally one-to-one GenVertex <-> SimVertex matches. + // The extra SIM topology requirement rejects shared Geant4 injection/source vertices. + if (genVertexCandidateMultiplicity[gv] != 1 || simVertexCandidateMultiplicity[sv] != 1) + continue; + + if (rawSimVertexIncomingSimTracks[sv] > 1) + continue; + + if (rawSimVertexOutgoingSimTracks[sv] != 1) + continue; + + vertexDSU.unite(rawToVertexTmp[gv], rawToVertexTmp[sv]); } } // ------------------------------------------------------------------ - // 3. Compress particle representatives + // 3. Compress particle and vertex representatives // ------------------------------------------------------------------ std::unordered_map particleRepToLogical; std::vector rawToParticle(nRawNodes, -1); @@ -678,8 +837,22 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { } } + std::unordered_map vertexRepToLogical; + std::vector rawToVertex(nRawNodes, -1); + + for (uint32_t nodeId = 0; nodeId < nRawNodes; ++nodeId) { + if (!keepRawNode[nodeId]) + continue; + + if (rawToVertexTmp[nodeId] >= 0) { + const int rep = vertexDSU.find(rawToVertexTmp[nodeId]); + auto result = vertexRepToLogical.emplace(rep, static_cast(vertexRepToLogical.size())); + rawToVertex[nodeId] = static_cast(result.first->second); + } + } + out->particles.resize(particleRepToLogical.size()); - out->vertices.resize(nVertexLogical); + out->vertices.resize(vertexRepToLogical.size()); // ------------------------------------------------------------------ // 4. Fill payload @@ -714,8 +887,9 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { p.pdgId = it->second.pdgId; if (p.status == 0) p.status = it->second.status; - if (!p.hasSim()) - p.momentum = it->second.momentum; + + // Keep the GEN four-momentum as nominal for GEN and GEN+SIM logical particles. + p.momentum = it->second.momentum; } } @@ -739,9 +913,8 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { const math::XYZTLorentzVectorD simMomentum( t.momentum().px(), t.momentum().py(), t.momentum().pz(), t.momentum().e()); - // For merged GEN+SIM particles, keep the GEN four-momentum as the nominal - // physics momentum. The SIM momentum is a simulation-state quantity. - // For SIM-only particles, use the SimTrack momentum. + // For SIM-only logical particles, use the SimTrack momentum. + // For GEN+SIM logical particles, the GEN momentum remains the nominal one. if (!p.hasGen()) { p.momentum = simMomentum; } @@ -775,8 +948,10 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { if (haveGenPayload) { const int barcode = static_cast(ref.key); auto it = genVertexPayload.find(barcode); - if (it != genVertexPayload.end()) + if (it != genVertexPayload.end()) { + // Keep the GEN position as nominal for GEN and GEN+SIM logical vertices. v.position = it->second.position; + } } } else if (ref.kind == TruthGraph::NodeKind::SimVertex) { @@ -790,7 +965,12 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { if (simIndex < hSimVertices->size()) { auto const& sv = (*hSimVertices)[simIndex]; const auto& pos = sv.position(); - v.position = math::XYZTLorentzVectorD(pos.x(), pos.y(), pos.z(), pos.t()); + + // For SIM-only logical vertices, use the SimVertex position. + // For GEN+SIM logical vertices, the GEN position remains the nominal one. + if (!v.hasGen()) { + v.position = math::XYZTLorentzVectorD(pos.x(), pos.y(), pos.z(), pos.t()); + } } } } @@ -871,6 +1051,7 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { edm::EDGetTokenT hepmc2Token_; int32_t motherPdgId_; + bool mergeGenSimVertices_; GraphPostProcessingConfig postProcessing_; }; From b8aff3c07dffa1a3e03bf25bb9fc6cd22956b867 Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Tue, 12 May 2026 13:48:32 +0200 Subject: [PATCH 18/53] Build GEN-SIM particle links only for primary SimTracks --- .../TruthInfo/plugins/TruthGraphProducer.cc | 50 +++++++++++-------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/PhysicsTools/TruthInfo/plugins/TruthGraphProducer.cc b/PhysicsTools/TruthInfo/plugins/TruthGraphProducer.cc index fbf616cd9de60..f8a4d37b5419b 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthGraphProducer.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthGraphProducer.cc @@ -67,6 +67,7 @@ namespace { void unite(int a, int b) { a = find(a); b = find(b); + if (a == b) return; @@ -88,8 +89,9 @@ namespace { std::vector vtxBarcodes; std::vector partBarcodes; - // index -> barcode in HepMC iteration order. Kept for diagnostics only. - // Do not use it to interpret SimTrack::genpartIndex(). + // index -> barcode in HepMC iteration order. Kept only for diagnostics. + // SimTrack::genpartIndex() is not an index into this vector: for primary + // SimTracks it is a HepMC barcode. std::vector particleBarcodeByIndex; std::vector> vtxToPart; @@ -460,27 +462,32 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { } // SimTrack::genpartIndex() must be used only for primary G4 tracks. - // For non-primary tracks, SimTrack::getPrimaryOrLastStoredID() can still - // contain a generator barcode, but that is only ancestry information for - // orphan/backscattered tracks and must not be interpreted as a direct - // SimTrack -> GenParticle association. - if (haveGen && simTrack.isPrimary() && !simTrack.noGenpart()) { + // For non-primary tracks, getPrimaryOrLastStoredID() can still contain + // a generator barcode, but that is ancestry information for orphan or + // backscattered tracks, not a direct SimTrack -> GenParticle association. + if (addGenToSimEdges_ && haveGen && simTrack.isPrimary()) { const int barcode = simTrack.genpartIndex(); - auto it = genParBarcodeToNode.find(barcode); - if (it != genParBarcodeToNode.end()) { - const int simPdgId = simTrack.type(); - const int genPdgId = out->nodePdgId(it->second); - - if (genPdgId != 0 && genPdgId != simPdgId) { + if (barcode != -1) { + auto it = genParBarcodeToNode.find(barcode); + + if (it != genParBarcodeToNode.end()) { + const int simPdgId = simTrack.type(); + const int genPdgId = out->nodePdgId(it->second); + + if (genPdgId == 0 || genPdgId == simPdgId) { + out->simTrackToGen[nodeId] = static_cast(it->second); + } else { + edm::LogPrint("TruthGraphProducer") + << "Rejecting primary SimTrack->GenParticle association with mismatched PDG id: " + << "simTrack index=" << i << " trackId=" << simTrack.trackId() << " genBarcode=" << barcode + << " simPdgId=" << simPdgId << " genNode=" << it->second << " genPdgId=" << genPdgId; + } + } else { edm::LogPrint("TruthGraphProducer") - << "Rejecting primary SimTrack->GenParticle association with mismatched PDG id: " - << "simTrack index=" << i << " trackId=" << simTrack.trackId() << " genpartBarcode=" << barcode - << " simPdgId=" << simPdgId << " genNode=" << it->second << " genPdgId=" << genPdgId; - continue; + << "Rejecting primary SimTrack->GenParticle association with missing GEN barcode: " + << "simTrack index=" << i << " trackId=" << simTrack.trackId() << " genBarcode=" << barcode; } - - out->simTrackToGen[nodeId] = static_cast(it->second); } } } @@ -560,10 +567,11 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { } for (uint32_t i = 0; i < nSimTrk; ++i) { - auto const& t = simTracks[i]; + auto const& simTrack = simTracks[i]; + const uint32_t childNode = baseSimTrk + i; - const int vtxIdx = t.vertIndex(); + const int vtxIdx = simTrack.vertIndex(); if (vtxIdx < 0 || static_cast(vtxIdx) >= nSimVtx) continue; From b3bcbf59b344a7d108b1b87b79714fadab7abb06 Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Tue, 12 May 2026 13:49:32 +0200 Subject: [PATCH 19/53] Improve TruthLogicalGraph DOT dumping --- .../plugins/TruthLogicalGraphDumper.cc | 112 +++++++++++++----- .../plugins/TruthLogicalGraphProducer.cc | 12 +- 2 files changed, 93 insertions(+), 31 deletions(-) diff --git a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc index cb74b5f54a281..4cdf6db25f783 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc @@ -3,6 +3,7 @@ #include #include #include +#include #include "FWCore/Framework/interface/Event.h" #include "FWCore/Framework/interface/EventSetup.h" @@ -22,67 +23,67 @@ namespace { const int ap = std::abs(pdgId); if (pdgId == 11) - return "e⁻"; + return "e-"; if (pdgId == -11) - return "e⁺"; + return "e+"; if (pdgId == 13) - return "μ⁻"; + return "mu-"; if (pdgId == -13) - return "μ⁺"; + return "mu+"; if (pdgId == 15) - return "τ⁻"; + return "tau-"; if (pdgId == -15) - return "τ⁺"; + return "tau+"; if (pdgId == 12) - return "νₑ"; + return "nu_e"; if (pdgId == -12) - return "ν̄ₑ"; + return "anti-nu_e"; if (pdgId == 14) - return "ν_μ"; + return "nu_mu"; if (pdgId == -14) - return "ν̄_μ"; + return "anti-nu_mu"; if (pdgId == 16) - return "ν_τ"; + return "nu_tau"; if (pdgId == -16) - return "ν̄_τ"; + return "anti-nu_tau"; if (pdgId == 22) - return "γ"; + return "gamma"; if (pdgId == 21) return "g"; if (pdgId == 23) - return "Z⁰"; + return "Z0"; if (pdgId == 24) - return "W⁺"; + return "W+"; if (pdgId == -24) - return "W⁻"; + return "W-"; if (pdgId == 25) return "H"; if (pdgId == 2212) return "p"; if (pdgId == -2212) - return "p̄"; + return "anti-p"; if (pdgId == 2112) return "n"; if (pdgId == -2112) - return "n̄"; + return "anti-n"; if (pdgId == 111) - return "π⁰"; + return "pi0"; if (pdgId == 211) - return "π⁺"; + return "pi+"; if (pdgId == -211) - return "π⁻"; + return "pi-"; if (pdgId == 321) - return "K⁺"; + return "K+"; if (pdgId == -321) - return "K⁻"; + return "K-"; if (pdgId == 130) - return "K⁰_L"; + return "K0_L"; if (pdgId == 310) - return "K⁰_S"; + return "K0_S"; if (ap >= 1 && ap <= 6) { static const char* qname[7] = {"", "d", "u", "s", "c", "b", "t"}; @@ -124,7 +125,9 @@ namespace { std::string rawNodeSummary(TruthGraph const* raw, int32_t nodeId) { if (raw == nullptr || nodeId < 0 || static_cast(nodeId) >= raw->nNodes()) return "n/a"; + auto const& r = raw->nodeRef(static_cast(nodeId)); + std::ostringstream ss; ss << rawKindName(r.kind) << " #" << nodeId << " key=" << r.key; return ss.str(); @@ -196,6 +199,7 @@ namespace { const auto dotPos = filename.rfind('.'); std::ostringstream ss; + if (dotPos == std::string::npos) { ss << filename; ss << "_run" << id.run(); @@ -212,6 +216,7 @@ namespace { return ss.str(); } + } // namespace class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { @@ -222,17 +227,29 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { dotFile_(cfg.getParameter("dotFile")), maxParticles_(cfg.getParameter("maxParticles")), maxVertices_(cfg.getParameter("maxVertices")), - maxEdgesPerNode_(cfg.getParameter("maxEdgesPerNode")) {} + maxEdgesPerNode_(cfg.getParameter("maxEdgesPerNode")), + hideLargeSimSourceVertices_(cfg.getParameter("hideLargeSimSourceVertices")), + largeSimSourceVertexMinOutgoing_(cfg.getParameter("largeSimSourceVertexMinOutgoing")) {} static void fillDescriptions(edm::ConfigurationDescriptions& descriptions) { edm::ParameterSetDescription desc; + desc.add("src", edm::InputTag("truthLogicalGraphProducer")); desc.add("rawSrc", edm::InputTag("truthGraphProducer")) ->setComment("Optional raw TruthGraph used only to enrich labels"); + desc.add("dotFile", "truthlogicalgraph.dot"); + desc.add("maxParticles", 5000)->setComment("Truncate logical particle nodes"); desc.add("maxVertices", 5000)->setComment("Truncate logical vertex nodes"); desc.add("maxEdgesPerNode", 200)->setComment("Truncate fanout per node"); + + desc.add("hideLargeSimSourceVertices", true) + ->setComment("If true, do not print large SIM-only source vertices in the DOT output"); + + desc.add("largeSimSourceVertexMinOutgoing", 50) + ->setComment("Minimum outgoing multiplicity for hiding a SIM-only source vertex"); + descriptions.addWithDefaultLabel(desc); } @@ -244,7 +261,9 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { TruthGraph const* raw = hRaw.isValid() ? &(*hRaw) : nullptr; const std::string eventDotFile = appendEventIdToFilename(dotFile_, evt.id()); + std::ofstream os(eventDotFile); + os << "digraph TruthLogicalGraph {\n"; os << " rankdir=LR;\n"; os << " node [fontsize=10];\n"; @@ -252,6 +271,22 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { const uint32_t nParticles = std::min(g.nParticles(), maxParticles_); const uint32_t nVertices = std::min(g.nVertices(), maxVertices_); + std::vector hideVertex(nVertices, 0); + + if (hideLargeSimSourceVertices_) { + for (uint32_t i = 0; i < nVertices; ++i) { + auto v = g.vertex(i); + auto const& d = v.data(); + + const auto incoming = v.incomingParticles(); + const auto outgoing = v.outgoingParticles(); + + if (!d.hasGen() && d.hasSim() && incoming.empty() && outgoing.size() >= largeSimSourceVertexMinOutgoing_) { + hideVertex[i] = 1; + } + } + } + // ------------------------------------------------------------------ // Particle nodes // ------------------------------------------------------------------ @@ -276,7 +311,7 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { << statusFlagsLabel(d.statusFlags) << ">" << ", eid=" << d.eventId << ", genEvent=" << d.genEvent << ", isRoot=" << p.isRoot() << ", isLeaf=" << p.isLeaf() << ", p4=\"" << fmtP4(d.momentum) - << "\", nProdVtx=" << p.productionVertices().size() << ", DecayVtx=" << p.decayVertices().size() + << "\", nProdVtx=" << p.productionVertices().size() << ", nDecayVtx=" << p.decayVertices().size() << ", nParents=" << p.parents().size() << ", nChildren=" << p.children().size() << ", nCheckpoints=" << d.checkpoints.size(); @@ -291,14 +326,18 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { if (d.pdgId != 0) os << " \n"; + if (d.status != 0) os << " \n"; + if (d.statusFlags != 0) { os << " \n"; os << " \n"; } + if (d.eventId != 0) os << " \n"; + if (d.genEvent >= 0) os << " \n"; @@ -337,6 +376,9 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { // Vertex nodes // ------------------------------------------------------------------ for (uint32_t i = 0; i < nVertices; ++i) { + if (hideVertex[i]) + continue; + auto v = g.vertex(i); auto const& d = v.data(); @@ -358,6 +400,7 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { if (d.eventId != 0) os << " \n"; + if (d.genEvent >= 0) os << " \n"; @@ -386,28 +429,39 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { // ------------------------------------------------------------------ for (uint32_t i = 0; i < nParticles; ++i) { unsigned kept = 0; + for (uint32_t v : g.decayVertices(i)) { if (v >= nVertices) continue; + + if (hideVertex[v]) + continue; + os << " p" << i << " -> v" << v << ";\n"; + if (++kept >= maxEdgesPerNode_) break; } } for (uint32_t i = 0; i < nVertices; ++i) { + if (hideVertex[i]) + continue; + unsigned kept = 0; + for (uint32_t p : g.outgoingParticles(i)) { if (p >= nParticles) continue; + os << " v" << i << " -> p" << p << ";\n"; + if (++kept >= maxEdgesPerNode_) break; } } os << "}\n"; - os.close(); } private: @@ -418,6 +472,8 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { unsigned maxParticles_; unsigned maxVertices_; unsigned maxEdgesPerNode_; + bool hideLargeSimSourceVertices_; + unsigned largeSimSourceVertexMinOutgoing_; }; DEFINE_FWK_MODULE(TruthLogicalGraphDumper); diff --git a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc index 5b5b20aeec585..d52126f406516 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc @@ -797,6 +797,7 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { for (auto const& candidate : productionVertexMergeCandidates) { if (genVertexCandidateMultiplicity[candidate.first] < UINT16_MAX) ++genVertexCandidateMultiplicity[candidate.first]; + if (simVertexCandidateMultiplicity[candidate.second] < UINT16_MAX) ++simVertexCandidateMultiplicity[candidate.second]; } @@ -806,14 +807,16 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { const uint32_t sv = candidate.second; // Only accept locally one-to-one GenVertex <-> SimVertex matches. - // The extra SIM topology requirement rejects shared Geant4 injection/source vertices. + // This prevents one busy SimVertex from absorbing many unrelated GenVertices. if (genVertexCandidateMultiplicity[gv] != 1 || simVertexCandidateMultiplicity[sv] != 1) continue; - if (rawSimVertexIncomingSimTracks[sv] > 1) + // Do not merge secondary SIM vertices produced by an existing SimTrack. + // Primary/injection SIM vertices can legitimately have multiple outgoing primary tracks. + if (rawSimVertexIncomingSimTracks[sv] != 0) continue; - if (rawSimVertexOutgoingSimTracks[sv] != 1) + if (rawSimVertexOutgoingSimTracks[sv] == 0) continue; vertexDSU.unite(rawToVertexTmp[gv], rawToVertexTmp[sv]); @@ -1001,13 +1004,16 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { if (isVertexKind(srcRef.kind) && isParticleKind(dstRef.kind)) { const int32_t lv = rawToVertex[src]; const int32_t lp = rawToParticle[dst]; + if (lv >= 0 && lp >= 0) { vertexToOutgoingParticlePairs.emplace_back(static_cast(lv), static_cast(lp)); particleToProductionVertexPairs.emplace_back(static_cast(lp), static_cast(lv)); } + } else if (isParticleKind(srcRef.kind) && isVertexKind(dstRef.kind)) { const int32_t lp = rawToParticle[src]; const int32_t lv = rawToVertex[dst]; + if (lp >= 0 && lv >= 0) { particleToDecayVertexPairs.emplace_back(static_cast(lp), static_cast(lv)); vertexToIncomingParticlePairs.emplace_back(static_cast(lv), static_cast(lp)); From 6a06ef5293c4c5eeb3fc890066c465af23bb9297 Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Tue, 12 May 2026 14:46:19 +0200 Subject: [PATCH 20/53] Label TruthGraph DOT edges with explicit edge types --- .../TruthInfo/plugins/TruthGraphDumper.cc | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc b/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc index e05098f41d5ba..a96632da0fd9f 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc @@ -170,19 +170,24 @@ namespace { return "box"; } - const char* edgeAttrs(uint8_t ek) { - using EK = TruthGraph::EdgeKind; - switch (static_cast(ek)) { - case EK::Gen: - return ""; - case EK::Sim: - return " [style=solid]"; - case EK::GenToSim: - return " [dir=both, style=dashed]"; - case EK::SimToGen: - return " [dir=both, style=dotted]"; + std::string edgeAttrs(TruthGraph::EdgeKind kind) { + using EdgeKind = TruthGraph::EdgeKind; + + switch (kind) { + case EdgeKind::Gen: + return " [style=solid, edgeType=\"Gen\"]"; + + case EdgeKind::Sim: + return " [style=solid, edgeType=\"Sim\"]"; + + case EdgeKind::GenToSim: + return " [dir=both, style=dashed, label=\"GenToSim\", edgeType=\"GenToSim\"]"; + + case EdgeKind::SimToGen: + return " [dir=both, style=dashed, label=\"SimToGen\", edgeType=\"SimToGen\"]"; } - return ""; + + return " [style=solid, edgeType=\"Unknown\"]"; } std::string statusFlagsLabel(uint16_t flags) { @@ -582,7 +587,7 @@ class TruthGraphDumper : public edm::one::EDAnalyzer<> { const uint32_t dst = g.edges[pos]; if (dst >= n) continue; - os << " n" << src << " -> n" << dst << edgeAttrs(g.edgeKind[pos]) << ";\n"; + os << " n" << src << " -> n" << dst << edgeAttrs(static_cast(g.edgeKind[pos])) << ";\n"; if (++kept >= maxEdgesPerNode_) break; } From f9066650e9fc4f7cdb629bdcbc2ee7a9908bd3a0 Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Tue, 12 May 2026 14:59:45 +0200 Subject: [PATCH 21/53] Dot simToGen edge --- PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc b/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc index a96632da0fd9f..5d95788119b05 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc @@ -184,7 +184,7 @@ namespace { return " [dir=both, style=dashed, label=\"GenToSim\", edgeType=\"GenToSim\"]"; case EdgeKind::SimToGen: - return " [dir=both, style=dashed, label=\"SimToGen\", edgeType=\"SimToGen\"]"; + return " [dir=both, style=dotted, label=\"SimToGen\", edgeType=\"SimToGen\"]"; } return " [style=solid, edgeType=\"Unknown\"]"; From ac8e3ca23ec56b026e9afc7e79a45486d53c5ed7 Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Tue, 12 May 2026 16:51:36 +0200 Subject: [PATCH 22/53] Add SimHits to SimTracks code format --- PhysicsTools/TruthInfo/BuildFile.xml | 1 + .../interface/LogicalGraphHitIndex.h | 83 +++++ .../interface/LogicalGraphHitIndexBuilder.h | 70 ++++ PhysicsTools/TruthInfo/plugins/BuildFile.xml | 18 + .../plugins/LogicalGraphHitIndexProducer.cc | 323 ++++++++++++++++++ .../plugins/TruthLogicalGraphDumper.cc | 51 +++ .../src/LogicalGraphHitIndexBuilder.cc | 245 +++++++++++++ PhysicsTools/TruthInfo/src/classes.h | 5 + PhysicsTools/TruthInfo/src/classes_def.xml | 6 + 9 files changed, 802 insertions(+) create mode 100644 PhysicsTools/TruthInfo/interface/LogicalGraphHitIndex.h create mode 100644 PhysicsTools/TruthInfo/interface/LogicalGraphHitIndexBuilder.h create mode 100644 PhysicsTools/TruthInfo/plugins/LogicalGraphHitIndexProducer.cc create mode 100644 PhysicsTools/TruthInfo/src/LogicalGraphHitIndexBuilder.cc diff --git a/PhysicsTools/TruthInfo/BuildFile.xml b/PhysicsTools/TruthInfo/BuildFile.xml index ae5185876d13a..d7421d27abc81 100644 --- a/PhysicsTools/TruthInfo/BuildFile.xml +++ b/PhysicsTools/TruthInfo/BuildFile.xml @@ -1,6 +1,7 @@ + diff --git a/PhysicsTools/TruthInfo/interface/LogicalGraphHitIndex.h b/PhysicsTools/TruthInfo/interface/LogicalGraphHitIndex.h new file mode 100644 index 0000000000000..a709210e15309 --- /dev/null +++ b/PhysicsTools/TruthInfo/interface/LogicalGraphHitIndex.h @@ -0,0 +1,83 @@ +#ifndef PhysicsTools_TruthInfo_LogicalGraphHitIndex_h +#define PhysicsTools_TruthInfo_LogicalGraphHitIndex_h + +#include +#include +#include + +namespace truth { + + class LogicalGraphHitIndex { + public: + struct Hit { + uint32_t detId = 0; + float energy = 0.f; + }; + + LogicalGraphHitIndex() = default; + + uint32_t nParticles() const { return nParticles_; } + + bool empty() const { return nParticles_ == 0; } + + std::span directHits(uint32_t particleId) const { + return range(directOffsets_, directHits_, particleId); + } + + std::span subgraphHits(uint32_t particleId) const { + return range(subgraphOffsets_, subgraphHits_, particleId); + } + + std::span totalSimHitEnergyByDetId() const { return totalSimHitEnergyByDetId_; } + + uint32_t directHitSize(uint32_t particleId) const { + return directOffsets_.at(particleId + 1) - directOffsets_.at(particleId); + } + + uint32_t subgraphHitSize(uint32_t particleId) const { + return subgraphOffsets_.at(particleId + 1) - subgraphOffsets_.at(particleId); + } + + void setData(uint32_t nParticles, + std::vector directOffsets, + std::vector directHits, + std::vector subgraphOffsets, + std::vector subgraphHits, + std::vector totalSimHitEnergyByDetId) { + nParticles_ = nParticles; + directOffsets_ = std::move(directOffsets); + directHits_ = std::move(directHits); + subgraphOffsets_ = std::move(subgraphOffsets); + subgraphHits_ = std::move(subgraphHits); + totalSimHitEnergyByDetId_ = std::move(totalSimHitEnergyByDetId); + } + + std::vector const& directOffsets() const { return directOffsets_; } + std::vector const& directHitStorage() const { return directHits_; } + + std::vector const& subgraphOffsets() const { return subgraphOffsets_; } + std::vector const& subgraphHitStorage() const { return subgraphHits_; } + + private: + static std::span range(std::vector const& offsets, + std::vector const& hits, + uint32_t particleId) { + const uint32_t begin = offsets.at(particleId); + const uint32_t end = offsets.at(particleId + 1); + return std::span(hits.data() + begin, end - begin); + } + + uint32_t nParticles_ = 0; + + std::vector directOffsets_; + std::vector directHits_; + + std::vector subgraphOffsets_; + std::vector subgraphHits_; + + std::vector totalSimHitEnergyByDetId_; + }; + +} // namespace truth + +#endif diff --git a/PhysicsTools/TruthInfo/interface/LogicalGraphHitIndexBuilder.h b/PhysicsTools/TruthInfo/interface/LogicalGraphHitIndexBuilder.h new file mode 100644 index 0000000000000..620731541d280 --- /dev/null +++ b/PhysicsTools/TruthInfo/interface/LogicalGraphHitIndexBuilder.h @@ -0,0 +1,70 @@ +#ifndef PhysicsTools_TruthInfo_LogicalGraphHitIndexBuilder_h +#define PhysicsTools_TruthInfo_LogicalGraphHitIndexBuilder_h + +#include +#include +#include +#include + +#include "PhysicsTools/TruthInfo/interface/LogicalGraphHitIndex.h" + +namespace truth { + + class LogicalGraphHitIndexBuilder { + public: + using Hit = LogicalGraphHitIndex::Hit; + + LogicalGraphHitIndexBuilder() = default; + + explicit LogicalGraphHitIndexBuilder(uint32_t nParticles) { reset(nParticles); } + + void reset(uint32_t nParticles); + + uint32_t nParticles() const { return nParticles_; } + + void setSimTrackForParticle(uint32_t particleId, uint32_t trackId); + + void addParticleChild(uint32_t parentParticleId, uint32_t childParticleId); + + void addHitForTrack(uint32_t trackId, uint32_t detId, float energy); + + void addHitForParticle(uint32_t particleId, uint32_t detId, float energy); + + void sortAndReduceDirectHits(); + + LogicalGraphHitIndex finish(); + + std::vector collectMergedDirectHits(std::span particles) const; + + std::vector collectMergedSubgraphHits(std::span particles) const; + + private: + static void sortAndReduce(std::vector& hits); + + static std::vector mergeSortedHitLists(std::span a, std::span b); + + static void mergeInto(std::vector& dst, std::span src); + + void computeSubgraphHits(); + + void buildCsr(std::vector> const& perParticleHits, + std::vector& offsets, + std::vector& storage) const; + + void collectSubgraphParticles(uint32_t rootParticleId, std::vector& visited) const; + + uint32_t nParticles_ = 0; + + std::unordered_map trackIdToParticle_; + + std::vector> children_; + + std::vector> directHits_; + std::vector> subgraphHits_; + + std::vector totalSimHitEnergyByDetId_; + }; + +} // namespace truth + +#endif diff --git a/PhysicsTools/TruthInfo/plugins/BuildFile.xml b/PhysicsTools/TruthInfo/plugins/BuildFile.xml index d5910a81ec72b..1f50a9ad6e09d 100644 --- a/PhysicsTools/TruthInfo/plugins/BuildFile.xml +++ b/PhysicsTools/TruthInfo/plugins/BuildFile.xml @@ -10,6 +10,20 @@ + + + + + + + + + + + + + + @@ -24,4 +38,8 @@ + + + + \ No newline at end of file diff --git a/PhysicsTools/TruthInfo/plugins/LogicalGraphHitIndexProducer.cc b/PhysicsTools/TruthInfo/plugins/LogicalGraphHitIndexProducer.cc new file mode 100644 index 0000000000000..49bc2bbe1a084 --- /dev/null +++ b/PhysicsTools/TruthInfo/plugins/LogicalGraphHitIndexProducer.cc @@ -0,0 +1,323 @@ +#include +#include +#include +#include +#include +#include + +#include "DataFormats/Common/interface/Handle.h" +#include "DataFormats/DetId/interface/DetId.h" +#include "DataFormats/ForwardDetId/interface/HGCalDetId.h" +#include "DataFormats/HcalDetId/interface/HcalDetId.h" + +#include "FWCore/Framework/interface/Event.h" +#include "FWCore/Framework/interface/EventSetup.h" +#include "FWCore/Framework/interface/global/EDProducer.h" +#include "FWCore/Framework/interface/MakerMacros.h" +#include "FWCore/ParameterSet/interface/ConfigurationDescriptions.h" +#include "FWCore/ParameterSet/interface/ParameterSet.h" +#include "FWCore/ParameterSet/interface/ParameterSetDescription.h" +#include "FWCore/Utilities/interface/EDGetToken.h" +#include "FWCore/Utilities/interface/InputTag.h" +#include "FWCore/MessageLogger/interface/MessageLogger.h" + +#include "Geometry/CaloGeometry/interface/CaloGeometry.h" +#include "Geometry/HGCalGeometry/interface/HGCalGeometry.h" +#include "Geometry/HcalCommonData/interface/HcalHitRelabeller.h" +#include "Geometry/HcalTowerAlgo/interface/HcalGeometry.h" +#include "Geometry/Records/interface/CaloGeometryRecord.h" + +#include "SimDataFormats/CaloHit/interface/PCaloHit.h" +#include "SimDataFormats/CaloTest/interface/HGCalTestNumbering.h" +#include "SimDataFormats/Track/interface/SimTrack.h" +#include "SimDataFormats/Track/interface/SimTrackContainer.h" + +#include "PhysicsTools/TruthInfo/interface/Graph.h" +#include "PhysicsTools/TruthInfo/interface/LogicalGraphHitIndex.h" +#include "PhysicsTools/TruthInfo/interface/LogicalGraphHitIndexBuilder.h" +#include "PhysicsTools/TruthInfo/interface/TruthGraph.h" + +namespace { + + struct LogicalGraphView { + explicit LogicalGraphView(truth::Graph const& graph) : graph_(graph) {} + + uint32_t nParticles() const { return graph_.nParticles(); } + + bool particleHasSim(uint32_t particleId) const { + return particleId < graph_.particles.size() && graph_.particles[particleId].hasSim(); + } + + int32_t particleSimNode(uint32_t particleId) const { return graph_.particles[particleId].simNode; } + + template + void forEachParticleChild(uint32_t parentParticleId, F&& f) const { + if (parentParticleId >= graph_.nParticles()) + return; + + for (const uint32_t vertexId : graph_.decayVertices(parentParticleId)) { + if (vertexId >= graph_.nVertices()) + continue; + + for (const uint32_t childId : graph_.outgoingParticles(vertexId)) { + f(childId); + } + } + } + + truth::Graph const& graph_; + }; + + uint32_t checkedTrackId(int64_t key) { + if (key < 0 || key > static_cast(std::numeric_limits::max())) + throw std::runtime_error("Invalid SimTrack key in logical graph"); + + return static_cast(key); + } + +} // namespace + +class TruthLogicalGraphHitIndexProducer : public edm::global::EDProducer<> { +public: + explicit TruthLogicalGraphHitIndexProducer(edm::ParameterSet const& cfg) + : graphToken_(consumes(cfg.getParameter("src"))), + rawGraphToken_(consumes(cfg.getParameter("rawSrc"))), + + simTracksToken_(consumes(cfg.getParameter("simTracks"))), + simHitCollections_(cfg.getParameter>("simHitCollections")), + doHGCal_(cfg.getParameter("doHGCal")), + doHGCalRelabelling_(cfg.getParameter("doHGCalRelabelling")), + geomToken_(esConsumes()) { + for (auto const& tag : simHitCollections_) { + simHitTokens_.push_back(consumes>(tag)); + } + + produces(); + } + + static void fillDescriptions(edm::ConfigurationDescriptions& descriptions) { + edm::ParameterSetDescription desc; + + desc.add("src", edm::InputTag("truthLogicalGraphProducer")); + desc.add("rawSrc", edm::InputTag("truthGraphProducer")); + + desc.add("simTracks", edm::InputTag("g4SimHits")); + + desc.add>("simHitCollections", + { + edm::InputTag("g4SimHits", "HGCHitsEE"), + edm::InputTag("g4SimHits", "HGCHitsHEfront"), + edm::InputTag("g4SimHits", "HGCHitsHEback"), + }); + + desc.add("doHGCal", true); + desc.add("doHGCalRelabelling", true); + + descriptions.addWithDefaultLabel(desc); + } + + void produce(edm::StreamID, edm::Event& event, edm::EventSetup const& setup) const override { + auto const& graph = event.get(graphToken_); + auto const& rawGraph = event.get(rawGraphToken_); + + LogicalGraphView graphView(graph); + + truth::LogicalGraphHitIndexBuilder builder(graphView.nParticles()); + + fillTrackToParticleMap(graphView, rawGraph, builder); + fillSimHits(event, setup, builder); + + auto output = std::make_unique(builder.finish()); + event.put(std::move(output)); + } + +private: + void fillTrackToParticleMap(LogicalGraphView const& graph, + TruthGraph const& rawGraph, + truth::LogicalGraphHitIndexBuilder& builder) const { + for (uint32_t particleId = 0; particleId < graph.nParticles(); ++particleId) { + if (!graph.particleHasSim(particleId)) + continue; + + const int32_t simNode = graph.particleSimNode(particleId); + if (simNode < 0) + continue; + + const uint32_t simNodeU32 = static_cast(simNode); + if (simNodeU32 >= rawGraph.nNodes()) + continue; + + auto const& ref = rawGraph.nodeRef(simNodeU32); + if (ref.kind != TruthGraph::NodeKind::SimTrack) + continue; + + builder.setSimTrackForParticle(particleId, checkedTrackId(ref.key)); + } + + for (uint32_t parentId = 0; parentId < graph.nParticles(); ++parentId) { + graph.forEachParticleChild(parentId, [&](uint32_t childId) { builder.addParticleChild(parentId, childId); }); + } + } + + void fillSimHits(edm::Event const& event, + edm::EventSetup const& setup, + truth::LogicalGraphHitIndexBuilder& builder) const { + GeometryCache geometry; + if (doHGCalRelabelling_) + geometry = makeGeometryCache(setup); + + for (std::size_t i = 0; i < simHitTokens_.size(); ++i) { + edm::Handle> hits; + event.getByToken(simHitTokens_[i], hits); + + if (!hits.isValid()) + continue; + + const auto& tag = simHitCollections_[i]; + const bool isHcal = tag.instance().find("HcalHits") != std::string::npos; + const bool isHGCal = tag.instance().find("HGCHits") != std::string::npos; + + for (auto const& hit : *hits) { + if (hit.geantTrackId() == 0) + continue; + + const DetId detId = makeRecoDetId(hit, isHGCal, isHcal, geometry); + if (detId == DetId(0)) + continue; + + builder.addHitForTrack(hit.geantTrackId(), detId.rawId(), hit.energy()); + } + } + } + + struct GeometryCache { + int geometryType = -1; + + HGCalTopology const* hgtopo[3] = {nullptr, nullptr, nullptr}; + HGCalDDDConstants const* hgddd[3] = {nullptr, nullptr, nullptr}; + + HcalDDDRecConstants const* hcddd = nullptr; + }; + + GeometryCache makeGeometryCache(edm::EventSetup const& setup) const { + GeometryCache cache; + + auto const& geom = setup.getData(geomToken_); + + auto const* hcalGeom = static_cast(geom.getSubdetectorGeometry(DetId::Hcal, HcalEndcap)); + if (hcalGeom) + cache.hcddd = hcalGeom->topology().dddConstants(); + + if (!doHGCal_) + return cache; + + auto const* eeGeom = static_cast( + geom.getSubdetectorGeometry(DetId::HGCalEE, ForwardSubdetector::ForwardEmpty)); + + if (eeGeom) { + cache.geometryType = 1; + + auto const* fhGeom = static_cast( + geom.getSubdetectorGeometry(DetId::HGCalHSi, ForwardSubdetector::ForwardEmpty)); + auto const* bhGeom = static_cast( + geom.getSubdetectorGeometry(DetId::HGCalHSc, ForwardSubdetector::ForwardEmpty)); + + cache.hgtopo[0] = &eeGeom->topology(); + if (fhGeom) + cache.hgtopo[1] = &fhGeom->topology(); + if (bhGeom) + cache.hgtopo[2] = &bhGeom->topology(); + } else { + cache.geometryType = 0; + + eeGeom = static_cast(geom.getSubdetectorGeometry(DetId::Forward, HGCEE)); + auto const* fhGeom = static_cast(geom.getSubdetectorGeometry(DetId::Forward, HGCHEF)); + + if (eeGeom) + cache.hgtopo[0] = &eeGeom->topology(); + if (fhGeom) + cache.hgtopo[1] = &fhGeom->topology(); + } + + for (unsigned i = 0; i < 3; ++i) { + if (cache.hgtopo[i]) + cache.hgddd[i] = &cache.hgtopo[i]->dddConstants(); + } + + return cache; + } + + DetId makeRecoDetId(PCaloHit const& hit, bool isHGCal, bool isHcal, GeometryCache const& geometry) const { + if (!doHGCalRelabelling_) { + return DetId(hit.id()); + } + + if (isHGCal) { + const uint32_t simId = hit.id(); + + if (geometry.geometryType == 1) { + return DetId(simId); + } + + if (isHcal) { + if (!geometry.hcddd) + return DetId(0); + + HcalDetId hid = HcalHitRelabeller::relabel(simId, geometry.hcddd); + if (hid.subdet() == HcalEndcap) + return hid; + + return DetId(0); + } + + int subdet = 0; + int layer = 0; + int cell = 0; + int sec = 0; + int subsec = 0; + int zp = 0; + + HGCalTestNumbering::unpackHexagonIndex(simId, subdet, zp, layer, sec, subsec, cell); + + if (subdet < 3 || subdet > 5) + return DetId(0); + + const unsigned idx = static_cast(subdet - 3); + if (!geometry.hgddd[idx] || !geometry.hgtopo[idx]) + return DetId(0); + + auto const recoLayerCell = geometry.hgddd[idx]->simToReco(cell, layer, sec, geometry.hgtopo[idx]->detectorType()); + cell = recoLayerCell.first; + layer = recoLayerCell.second; + + if (layer == -1) + return DetId(0); + + return HGCalDetId(static_cast(subdet), zp, layer, subsec, sec, cell); + } + + if (isHcal) { + if (!geometry.hcddd) + return DetId(0); + + return HcalHitRelabeller::relabel(hit.id(), geometry.hcddd); + } + + return DetId(hit.id()); + } + + edm::EDGetTokenT graphToken_; + edm::EDGetTokenT rawGraphToken_; + + edm::EDGetTokenT simTracksToken_; + + std::vector simHitCollections_; + std::vector>> simHitTokens_; + + bool doHGCal_; + bool doHGCalRelabelling_; + + edm::ESGetToken geomToken_; +}; + +DEFINE_FWK_MODULE(TruthLogicalGraphHitIndexProducer); diff --git a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc index 4cdf6db25f783..9fbe390a35a36 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -15,6 +16,7 @@ #include "FWCore/Utilities/interface/InputTag.h" #include "PhysicsTools/TruthInfo/interface/Graph.h" +#include "PhysicsTools/TruthInfo/interface/LogicalGraphHitIndex.h" #include "PhysicsTools/TruthInfo/interface/TruthGraph.h" namespace { @@ -195,6 +197,21 @@ namespace { return ss.str(); } + float sumHitEnergy(std::span hits) { + float sum = 0.f; + for (auto const& hit : hits) { + sum += hit.energy; + } + return sum; + } + + std::string fmtEnergy(float energy) { + std::ostringstream ss; + ss.setf(std::ios::fixed); + ss << std::setprecision(6) << energy; + return ss.str(); + } + std::string appendEventIdToFilename(std::string const& filename, edm::EventID const& id) { const auto dotPos = filename.rfind('.'); @@ -224,6 +241,9 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { explicit TruthLogicalGraphDumper(const edm::ParameterSet& cfg) : token_(consumes(cfg.getParameter("src"))), rawToken_(mayConsume(cfg.getParameter("rawSrc"))), + hitIndexTag_(cfg.getParameter("hitIndex")), + hitIndexToken_(mayConsume(hitIndexTag_)), + useHitIndex_(!hitIndexTag_.label().empty()), dotFile_(cfg.getParameter("dotFile")), maxParticles_(cfg.getParameter("maxParticles")), maxVertices_(cfg.getParameter("maxVertices")), @@ -237,6 +257,8 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { desc.add("src", edm::InputTag("truthLogicalGraphProducer")); desc.add("rawSrc", edm::InputTag("truthGraphProducer")) ->setComment("Optional raw TruthGraph used only to enrich labels"); + desc.add("hitIndex", edm::InputTag("")) + ->setComment("Optional LogicalGraphHitIndex used to annotate particles with direct/subgraph SimHit summaries"); desc.add("dotFile", "truthlogicalgraph.dot"); @@ -260,6 +282,12 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { evt.getByToken(rawToken_, hRaw); TruthGraph const* raw = hRaw.isValid() ? &(*hRaw) : nullptr; + edm::Handle hHitIndex; + if (useHitIndex_) { + evt.getByToken(hitIndexToken_, hHitIndex); + } + truth::LogicalGraphHitIndex const* hitIndex = hHitIndex.isValid() ? &(*hHitIndex) : nullptr; + const std::string eventDotFile = appendEventIdToFilename(dotFile_, evt.id()); std::ofstream os(eventDotFile); @@ -294,6 +322,14 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { auto p = g.particle(i); auto const& d = p.data(); + const bool hasHitInfo = hitIndex != nullptr && i < hitIndex->nParticles(); + const auto directHits = + hasHitInfo ? hitIndex->directHits(i) : std::span(); + const auto subgraphHits = + hasHitInfo ? hitIndex->subgraphHits(i) : std::span(); + const float directHitEnergy = hasHitInfo ? sumHitEnergy(directHits) : 0.f; + const float subgraphHitEnergy = hasHitInfo ? sumHitEnergy(subgraphHits) : 0.f; + os << " p" << i << " [shape=ellipse, hasCheckpoints=" << p.hasCheckpoints() << ", hasGen=" << p.hasGen() << ", hasSim=" << d.hasSim(); @@ -315,6 +351,11 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { << ", nParents=" << p.parents().size() << ", nChildren=" << p.children().size() << ", nCheckpoints=" << d.checkpoints.size(); + if (hasHitInfo) { + os << ", nDirectHits=" << directHits.size() << ", directHitEnergy=" << fmtEnergy(directHitEnergy) + << ", nSubgraphHits=" << subgraphHits.size() << ", subgraphHitEnergy=" << fmtEnergy(subgraphHitEnergy); + } + if (raw != nullptr) { os << ", raw_GEN=<" << rawNodeSummary(raw, d.genNode) << ">, raw_SIM=<" << rawNodeSummary(raw, d.simNode) << ">"; @@ -357,6 +398,13 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { os << " \n"; + if (hasHitInfo) { + os << " \n"; + os << " \n"; + } + for (auto const& cp : d.checkpoints) { os << " \n"; os << " \n"; @@ -467,6 +515,9 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { private: edm::EDGetTokenT token_; edm::EDGetTokenT rawToken_; + edm::InputTag hitIndexTag_; + edm::EDGetTokenT hitIndexToken_; + bool useHitIndex_; std::string dotFile_; unsigned maxParticles_; diff --git a/PhysicsTools/TruthInfo/src/LogicalGraphHitIndexBuilder.cc b/PhysicsTools/TruthInfo/src/LogicalGraphHitIndexBuilder.cc new file mode 100644 index 0000000000000..89884abe00e54 --- /dev/null +++ b/PhysicsTools/TruthInfo/src/LogicalGraphHitIndexBuilder.cc @@ -0,0 +1,245 @@ +#include "PhysicsTools/TruthInfo/interface/LogicalGraphHitIndexBuilder.h" + +#include +#include + +namespace truth { + + void LogicalGraphHitIndexBuilder::reset(uint32_t nParticles) { + nParticles_ = nParticles; + + trackIdToParticle_.clear(); + trackIdToParticle_.reserve(nParticles * 2); + + children_.assign(nParticles, {}); + directHits_.assign(nParticles, {}); + subgraphHits_.assign(nParticles, {}); + + totalSimHitEnergyByDetId_.clear(); + } + + void LogicalGraphHitIndexBuilder::setSimTrackForParticle(uint32_t particleId, uint32_t trackId) { + if (particleId >= nParticles_) + throw std::out_of_range("LogicalGraphHitIndexBuilder::setSimTrackForParticle: particle index out of range"); + + trackIdToParticle_.emplace(trackId, particleId); + } + + void LogicalGraphHitIndexBuilder::addParticleChild(uint32_t parentParticleId, uint32_t childParticleId) { + if (parentParticleId >= nParticles_ || childParticleId >= nParticles_) + throw std::out_of_range("LogicalGraphHitIndexBuilder::addParticleChild: particle index out of range"); + + children_[parentParticleId].push_back(childParticleId); + } + + void LogicalGraphHitIndexBuilder::addHitForTrack(uint32_t trackId, uint32_t detId, float energy) { + if (energy == 0.f) + return; + + auto const it = trackIdToParticle_.find(trackId); + if (it == trackIdToParticle_.end()) + return; + + addHitForParticle(it->second, detId, energy); + } + + void LogicalGraphHitIndexBuilder::addHitForParticle(uint32_t particleId, uint32_t detId, float energy) { + if (particleId >= nParticles_) + throw std::out_of_range("LogicalGraphHitIndexBuilder::addHitForParticle: particle index out of range"); + + if (energy == 0.f) + return; + + directHits_[particleId].push_back({detId, energy}); + totalSimHitEnergyByDetId_.push_back({detId, energy}); + } + + void LogicalGraphHitIndexBuilder::sortAndReduce(std::vector& hits) { + std::sort(hits.begin(), hits.end(), [](Hit const& a, Hit const& b) { return a.detId < b.detId; }); + + std::size_t out = 0; + for (auto const& hit : hits) { + if (hit.energy == 0.f) + continue; + + if (out != 0 && hits[out - 1].detId == hit.detId) { + hits[out - 1].energy += hit.energy; + } else { + hits[out++] = hit; + } + } + + hits.resize(out); + } + + void LogicalGraphHitIndexBuilder::sortAndReduceDirectHits() { + for (auto& hits : directHits_) { + sortAndReduce(hits); + } + + sortAndReduce(totalSimHitEnergyByDetId_); + + for (auto& daughters : children_) { + std::sort(daughters.begin(), daughters.end()); + daughters.erase(std::unique(daughters.begin(), daughters.end()), daughters.end()); + } + } + + std::vector LogicalGraphHitIndexBuilder::mergeSortedHitLists( + std::span a, std::span b) { + std::vector out; + out.reserve(a.size() + b.size()); + + auto ia = a.begin(); + auto ib = b.begin(); + + while (ia != a.end() && ib != b.end()) { + if (ia->detId < ib->detId) { + out.push_back(*ia++); + } else if (ib->detId < ia->detId) { + out.push_back(*ib++); + } else { + out.push_back({ia->detId, ia->energy + ib->energy}); + ++ia; + ++ib; + } + } + + out.insert(out.end(), ia, a.end()); + out.insert(out.end(), ib, b.end()); + + return out; + } + + void LogicalGraphHitIndexBuilder::mergeInto(std::vector& dst, std::span src) { + if (src.empty()) + return; + + if (dst.empty()) { + dst.assign(src.begin(), src.end()); + return; + } + + dst = mergeSortedHitLists(dst, src); + } + + void LogicalGraphHitIndexBuilder::collectSubgraphParticles(uint32_t rootParticleId, + std::vector& visited) const { + std::vector stack; + stack.push_back(rootParticleId); + + while (!stack.empty()) { + const uint32_t particleId = stack.back(); + stack.pop_back(); + + if (particleId >= nParticles_ || visited[particleId]) + continue; + + visited[particleId] = 1; + + for (uint32_t child : children_[particleId]) { + stack.push_back(child); + } + } + } + + void LogicalGraphHitIndexBuilder::computeSubgraphHits() { + subgraphHits_.assign(nParticles_, {}); + + std::vector visited; + + for (uint32_t particleId = 0; particleId < nParticles_; ++particleId) { + visited.assign(nParticles_, 0); + collectSubgraphParticles(particleId, visited); + + auto& out = subgraphHits_[particleId]; + + for (uint32_t other = 0; other < nParticles_; ++other) { + if (!visited[other]) + continue; + + mergeInto(out, directHits_[other]); + } + } + } + + void LogicalGraphHitIndexBuilder::buildCsr(std::vector> const& perParticleHits, + std::vector& offsets, + std::vector& storage) const { + offsets.clear(); + storage.clear(); + + offsets.reserve(nParticles_ + 1); + offsets.push_back(0); + + std::size_t totalSize = 0; + for (auto const& hits : perParticleHits) { + totalSize += hits.size(); + } + storage.reserve(totalSize); + + for (auto const& hits : perParticleHits) { + storage.insert(storage.end(), hits.begin(), hits.end()); + offsets.push_back(static_cast(storage.size())); + } + } + + LogicalGraphHitIndex LogicalGraphHitIndexBuilder::finish() { + sortAndReduceDirectHits(); + computeSubgraphHits(); + + std::vector directOffsets; + std::vector directStorage; + buildCsr(directHits_, directOffsets, directStorage); + + std::vector subgraphOffsets; + std::vector subgraphStorage; + buildCsr(subgraphHits_, subgraphOffsets, subgraphStorage); + + LogicalGraphHitIndex out; + out.setData(nParticles_, + std::move(directOffsets), + std::move(directStorage), + std::move(subgraphOffsets), + std::move(subgraphStorage), + std::move(totalSimHitEnergyByDetId_)); + + return out; + } + + std::vector LogicalGraphHitIndexBuilder::collectMergedDirectHits( + std::span particles) const { + std::vector out; + + for (uint32_t particleId : particles) { + if (particleId >= nParticles_) + continue; + + mergeInto(out, directHits_[particleId]); + } + + return out; + } + + std::vector LogicalGraphHitIndexBuilder::collectMergedSubgraphHits( + std::span particles) const { + std::vector visited(nParticles_, 0); + + for (uint32_t particleId : particles) { + if (particleId < nParticles_) + collectSubgraphParticles(particleId, visited); + } + + std::vector out; + + for (uint32_t particleId = 0; particleId < nParticles_; ++particleId) { + if (!visited[particleId]) + continue; + + mergeInto(out, directHits_[particleId]); + } + + return out; + } + +} // namespace truth diff --git a/PhysicsTools/TruthInfo/src/classes.h b/PhysicsTools/TruthInfo/src/classes.h index 9de2599c71a33..8ddcc2323fe40 100644 --- a/PhysicsTools/TruthInfo/src/classes.h +++ b/PhysicsTools/TruthInfo/src/classes.h @@ -4,6 +4,7 @@ #include "DataFormats/Common/interface/Wrapper.h" #include "PhysicsTools/TruthInfo/interface/Graph.h" #include "PhysicsTools/TruthInfo/interface/TruthGraph.h" +#include "PhysicsTools/TruthInfo/interface/LogicalGraphHitIndex.h" namespace { struct dictionary { @@ -24,6 +25,10 @@ namespace { truth::VertexData logicalTruthVertexData; std::vector logicalTruthVertexDataVec; + + truth::LogicalGraphHitIndex logicalGraphHitIndex; + truth::LogicalGraphHitIndex::Hit logicalGraphHitIndexHit; + std::vector logicalGraphHitIndexHitVector; }; } // namespace diff --git a/PhysicsTools/TruthInfo/src/classes_def.xml b/PhysicsTools/TruthInfo/src/classes_def.xml index 8bc5f1ebe8202..7f113e85a0154 100644 --- a/PhysicsTools/TruthInfo/src/classes_def.xml +++ b/PhysicsTools/TruthInfo/src/classes_def.xml @@ -12,4 +12,10 @@ + + + + + + \ No newline at end of file From 4cf6f9c4959f2de8389af953e480b7620daf2c9f Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Tue, 12 May 2026 18:35:40 +0200 Subject: [PATCH 23/53] Add new SimHitToRecHitMap --- .../HGCalAssociatorProducers/BuildFile.xml | 3 + .../interface/DetIdRecHitMap.h | 23 ++++ .../plugins/SimHitToRecHitMapProducer.cc | 124 ++++++++++++++++++ .../HGCalAssociatorProducers/src/classes.h | 15 +++ .../src/classes_def.xml | 4 + 5 files changed, 169 insertions(+) create mode 100644 SimCalorimetry/HGCalAssociatorProducers/BuildFile.xml create mode 100644 SimCalorimetry/HGCalAssociatorProducers/interface/DetIdRecHitMap.h create mode 100644 SimCalorimetry/HGCalAssociatorProducers/plugins/SimHitToRecHitMapProducer.cc create mode 100644 SimCalorimetry/HGCalAssociatorProducers/src/classes.h create mode 100644 SimCalorimetry/HGCalAssociatorProducers/src/classes_def.xml diff --git a/SimCalorimetry/HGCalAssociatorProducers/BuildFile.xml b/SimCalorimetry/HGCalAssociatorProducers/BuildFile.xml new file mode 100644 index 0000000000000..98175ecd0ebc1 --- /dev/null +++ b/SimCalorimetry/HGCalAssociatorProducers/BuildFile.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/SimCalorimetry/HGCalAssociatorProducers/interface/DetIdRecHitMap.h b/SimCalorimetry/HGCalAssociatorProducers/interface/DetIdRecHitMap.h new file mode 100644 index 0000000000000..3b6a982259669 --- /dev/null +++ b/SimCalorimetry/HGCalAssociatorProducers/interface/DetIdRecHitMap.h @@ -0,0 +1,23 @@ +#ifndef SimCalorimetry_HGCalAssociatorProducers_DetIdRecHitMap_h +#define SimCalorimetry_HGCalAssociatorProducers_DetIdRecHitMap_h + +#include +#include + +namespace hgcal { + + // Maps DetId::rawId() to a global recHit index. + // + // The index is defined by the concatenation order used by + // SimHitToRecHitMapProducer: + // + // 1. all configured HGCRecHitCollection inputs, in cfg order + // 2. all configured reco::PFRecHitCollection inputs, in cfg order + // + // It is not an index into a single EDM collection unless only one collection + // is configured. + using DetIdRecHitMap = std::unordered_map; + +} // namespace hgcal + +#endif \ No newline at end of file diff --git a/SimCalorimetry/HGCalAssociatorProducers/plugins/SimHitToRecHitMapProducer.cc b/SimCalorimetry/HGCalAssociatorProducers/plugins/SimHitToRecHitMapProducer.cc new file mode 100644 index 0000000000000..485a0b5737e11 --- /dev/null +++ b/SimCalorimetry/HGCalAssociatorProducers/plugins/SimHitToRecHitMapProducer.cc @@ -0,0 +1,124 @@ +#include +#include +#include + +#include "FWCore/Framework/interface/Event.h" +#include "FWCore/Framework/interface/EventSetup.h" +#include "FWCore/Framework/interface/MakerMacros.h" +#include "FWCore/Framework/interface/global/EDProducer.h" +#include "FWCore/MessageLogger/interface/MessageLogger.h" +#include "FWCore/ParameterSet/interface/ConfigurationDescriptions.h" +#include "FWCore/ParameterSet/interface/ParameterSet.h" +#include "FWCore/ParameterSet/interface/ParameterSetDescription.h" +#include "FWCore/Utilities/interface/EDGetToken.h" +#include "FWCore/Utilities/interface/InputTag.h" + +#include "DataFormats/HGCRecHit/interface/HGCRecHitCollections.h" +#include "DataFormats/ParticleFlowReco/interface/PFRecHit.h" + +#include "SimCalorimetry/HGCalAssociatorProducers/interface/DetIdRecHitMap.h" + +class SimHitToRecHitMapProducer : public edm::global::EDProducer<> { +public: + explicit SimHitToRecHitMapProducer(edm::ParameterSet const& ps); + ~SimHitToRecHitMapProducer() override = default; + + static void fillDescriptions(edm::ConfigurationDescriptions& descriptions); + +private: + void produce(edm::StreamID, edm::Event&, edm::EventSetup const&) const override; + + std::vector> hgcalRecHitTokens_; + std::vector> pfRecHitTokens_; +}; + +SimHitToRecHitMapProducer::SimHitToRecHitMapProducer(edm::ParameterSet const& ps) { + const auto hgcalTags = ps.getParameter>("hgcalRecHits"); + const auto pfTags = ps.getParameter>("pfRecHits"); + + hgcalRecHitTokens_.reserve(hgcalTags.size()); + for (auto const& tag : hgcalTags) { + hgcalRecHitTokens_.push_back(consumes(tag)); + } + + pfRecHitTokens_.reserve(pfTags.size()); + for (auto const& tag : pfTags) { + pfRecHitTokens_.push_back(consumes(tag)); + } + + produces(); +} + +void SimHitToRecHitMapProducer::produce(edm::StreamID, edm::Event& event, edm::EventSetup const&) const { + auto output = std::make_unique(); + + uint32_t globalRecHitIndex = 0; + + for (auto const& token : hgcalRecHitTokens_) { + edm::Handle handle; + event.getByToken(token, handle); + + if (!handle.isValid()) { + edm::LogWarning("SimHitToRecHitMapProducer") << "Missing HGCRecHitCollection. Skipping it."; + continue; + } + + output->reserve(output->size() + handle->size()); + + for (auto const& hit : *handle) { + const uint32_t rawId = hit.detid().rawId(); + + const auto [_, inserted] = output->emplace(rawId, globalRecHitIndex); + if (!inserted) { + edm::LogWarning("SimHitToRecHitMapProducer") + << "Duplicate HGCAL DetId rawId=" << rawId << ". Keeping the first recHit index."; + } + + ++globalRecHitIndex; + } + } + + for (auto const& token : pfRecHitTokens_) { + edm::Handle handle; + event.getByToken(token, handle); + + if (!handle.isValid()) { + edm::LogWarning("SimHitToRecHitMapProducer") << "Missing reco::PFRecHitCollection. Skipping it."; + continue; + } + + output->reserve(output->size() + handle->size()); + + for (auto const& hit : *handle) { + const uint32_t rawId = hit.detId(); + + const auto [_, inserted] = output->emplace(rawId, globalRecHitIndex); + if (!inserted) { + edm::LogWarning("SimHitToRecHitMapProducer") + << "Duplicate PFRecHit DetId rawId=" << rawId << ". Keeping the first recHit index."; + } + + ++globalRecHitIndex; + } + } + + event.put(std::move(output)); +} + +void SimHitToRecHitMapProducer::fillDescriptions(edm::ConfigurationDescriptions& descriptions) { + edm::ParameterSetDescription desc; + + desc.add>("hgcalRecHits", + {edm::InputTag("HGCalRecHit", "HGCEERecHits"), + edm::InputTag("HGCalRecHit", "HGCHEFRecHits"), + edm::InputTag("HGCalRecHit", "HGCHEBRecHits")}); + + desc.add>("pfRecHits", + {edm::InputTag("particleFlowRecHitECAL"), + edm::InputTag("particleFlowRecHitHBHE"), + edm::InputTag("particleFlowRecHitHO")}); + + descriptions.addWithDefaultLabel(desc); +} + +DEFINE_FWK_MODULE(SimHitToRecHitMapProducer); \ No newline at end of file diff --git a/SimCalorimetry/HGCalAssociatorProducers/src/classes.h b/SimCalorimetry/HGCalAssociatorProducers/src/classes.h new file mode 100644 index 0000000000000..2833ae679b90c --- /dev/null +++ b/SimCalorimetry/HGCalAssociatorProducers/src/classes.h @@ -0,0 +1,15 @@ +#ifndef SimCalorimetry_HGCalAssociatorProducers_classes_h +#define SimCalorimetry_HGCalAssociatorProducers_classes_h + +#include + +#include "DataFormats/Common/interface/Wrapper.h" + +namespace SimCalorimetry_HGCalAssociatorProducers { + struct dictionary { + std::unordered_map simHitToRecHitMap_; + edm::Wrapper> simHitToRecHitMapWrapper_; + }; +} // namespace SimCalorimetry_HGCalAssociatorProducers + +#endif \ No newline at end of file diff --git a/SimCalorimetry/HGCalAssociatorProducers/src/classes_def.xml b/SimCalorimetry/HGCalAssociatorProducers/src/classes_def.xml new file mode 100644 index 0000000000000..28bd90432cb7e --- /dev/null +++ b/SimCalorimetry/HGCalAssociatorProducers/src/classes_def.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file From 0d96248960ba318e51720bf95fba474dbf883e62 Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Tue, 12 May 2026 18:36:14 +0200 Subject: [PATCH 24/53] Add associated rechits and energy to truth particles --- .../interface/LogicalGraphHitIndex.h | 77 ++-- .../interface/LogicalGraphHitIndexBuilder.h | 50 +-- .../plugins/LogicalGraphHitIndexProducer.cc | 415 ++++++++++-------- .../plugins/TruthLogicalGraphDumper.cc | 153 ++++++- .../src/LogicalGraphHitIndexBuilder.cc | 254 ++++------- 5 files changed, 488 insertions(+), 461 deletions(-) diff --git a/PhysicsTools/TruthInfo/interface/LogicalGraphHitIndex.h b/PhysicsTools/TruthInfo/interface/LogicalGraphHitIndex.h index a709210e15309..103f0088ca194 100644 --- a/PhysicsTools/TruthInfo/interface/LogicalGraphHitIndex.h +++ b/PhysicsTools/TruthInfo/interface/LogicalGraphHitIndex.h @@ -2,6 +2,7 @@ #define PhysicsTools_TruthInfo_LogicalGraphHitIndex_h #include +#include #include #include @@ -10,63 +11,49 @@ namespace truth { class LogicalGraphHitIndex { public: struct Hit { + static constexpr uint32_t invalidRecHitIndex = std::numeric_limits::max(); + uint32_t detId = 0; + uint32_t recHitIndex = invalidRecHitIndex; float energy = 0.f; + + [[nodiscard]] bool hasRecHit() const { return recHitIndex != invalidRecHitIndex; } }; LogicalGraphHitIndex() = default; - uint32_t nParticles() const { return nParticles_; } - - bool empty() const { return nParticles_ == 0; } - - std::span directHits(uint32_t particleId) const { - return range(directOffsets_, directHits_, particleId); - } - - std::span subgraphHits(uint32_t particleId) const { - return range(subgraphOffsets_, subgraphHits_, particleId); - } - - std::span totalSimHitEnergyByDetId() const { return totalSimHitEnergyByDetId_; } - - uint32_t directHitSize(uint32_t particleId) const { - return directOffsets_.at(particleId + 1) - directOffsets_.at(particleId); - } - - uint32_t subgraphHitSize(uint32_t particleId) const { - return subgraphOffsets_.at(particleId + 1) - subgraphOffsets_.at(particleId); + LogicalGraphHitIndex(uint32_t nParticles, + std::vector directOffsets, + std::vector directHits, + std::vector subgraphOffsets, + std::vector subgraphHits) + : nParticles_(nParticles), + directOffsets_(std::move(directOffsets)), + directHits_(std::move(directHits)), + subgraphOffsets_(std::move(subgraphOffsets)), + subgraphHits_(std::move(subgraphHits)) {} + + [[nodiscard]] uint32_t nParticles() const { return nParticles_; } + + [[nodiscard]] std::span directHits(uint32_t particleId) const { + const auto b = directOffsets_.at(particleId); + const auto e = directOffsets_.at(particleId + 1); + return std::span(directHits_.data() + b, e - b); } - void setData(uint32_t nParticles, - std::vector directOffsets, - std::vector directHits, - std::vector subgraphOffsets, - std::vector subgraphHits, - std::vector totalSimHitEnergyByDetId) { - nParticles_ = nParticles; - directOffsets_ = std::move(directOffsets); - directHits_ = std::move(directHits); - subgraphOffsets_ = std::move(subgraphOffsets); - subgraphHits_ = std::move(subgraphHits); - totalSimHitEnergyByDetId_ = std::move(totalSimHitEnergyByDetId); + [[nodiscard]] std::span subgraphHits(uint32_t particleId) const { + const auto b = subgraphOffsets_.at(particleId); + const auto e = subgraphOffsets_.at(particleId + 1); + return std::span(subgraphHits_.data() + b, e - b); } - std::vector const& directOffsets() const { return directOffsets_; } - std::vector const& directHitStorage() const { return directHits_; } + [[nodiscard]] const std::vector& directOffsets() const { return directOffsets_; } + [[nodiscard]] const std::vector& directHitStorage() const { return directHits_; } - std::vector const& subgraphOffsets() const { return subgraphOffsets_; } - std::vector const& subgraphHitStorage() const { return subgraphHits_; } + [[nodiscard]] const std::vector& subgraphOffsets() const { return subgraphOffsets_; } + [[nodiscard]] const std::vector& subgraphHitStorage() const { return subgraphHits_; } private: - static std::span range(std::vector const& offsets, - std::vector const& hits, - uint32_t particleId) { - const uint32_t begin = offsets.at(particleId); - const uint32_t end = offsets.at(particleId + 1); - return std::span(hits.data() + begin, end - begin); - } - uint32_t nParticles_ = 0; std::vector directOffsets_; @@ -74,8 +61,6 @@ namespace truth { std::vector subgraphOffsets_; std::vector subgraphHits_; - - std::vector totalSimHitEnergyByDetId_; }; } // namespace truth diff --git a/PhysicsTools/TruthInfo/interface/LogicalGraphHitIndexBuilder.h b/PhysicsTools/TruthInfo/interface/LogicalGraphHitIndexBuilder.h index 620731541d280..36cf3f647a89c 100644 --- a/PhysicsTools/TruthInfo/interface/LogicalGraphHitIndexBuilder.h +++ b/PhysicsTools/TruthInfo/interface/LogicalGraphHitIndexBuilder.h @@ -2,7 +2,6 @@ #define PhysicsTools_TruthInfo_LogicalGraphHitIndexBuilder_h #include -#include #include #include @@ -12,57 +11,38 @@ namespace truth { class LogicalGraphHitIndexBuilder { public: - using Hit = LogicalGraphHitIndex::Hit; - - LogicalGraphHitIndexBuilder() = default; - - explicit LogicalGraphHitIndexBuilder(uint32_t nParticles) { reset(nParticles); } - - void reset(uint32_t nParticles); - - uint32_t nParticles() const { return nParticles_; } + explicit LogicalGraphHitIndexBuilder(uint32_t nParticles); void setSimTrackForParticle(uint32_t particleId, uint32_t trackId); - void addParticleChild(uint32_t parentParticleId, uint32_t childParticleId); - void addHitForTrack(uint32_t trackId, uint32_t detId, float energy); - - void addHitForParticle(uint32_t particleId, uint32_t detId, float energy); + void addHitForTrack(uint32_t trackId, uint32_t detId, uint32_t recHitIndex, float energy); - void sortAndReduceDirectHits(); - - LogicalGraphHitIndex finish(); - - std::vector collectMergedDirectHits(std::span particles) const; - - std::vector collectMergedSubgraphHits(std::span particles) const; + [[nodiscard]] LogicalGraphHitIndex finish(); private: - static void sortAndReduce(std::vector& hits); - - static std::vector mergeSortedHitLists(std::span a, std::span b); + using Hit = LogicalGraphHitIndex::Hit; - static void mergeInto(std::vector& dst, std::span src); + struct HitAccumulator { + uint32_t recHitIndex = Hit::invalidRecHitIndex; + float energy = 0.f; + }; - void computeSubgraphHits(); + using HitMap = std::unordered_map; - void buildCsr(std::vector> const& perParticleHits, - std::vector& offsets, - std::vector& storage) const; + static void addHit(HitMap& hits, uint32_t detId, uint32_t recHitIndex, float energy); + static void addHit(HitMap& hits, Hit const& hit); + static std::vector sortedHits(HitMap const& hits); - void collectSubgraphParticles(uint32_t rootParticleId, std::vector& visited) const; + void fillSubgraphHits(uint32_t particleId, std::vector& state); uint32_t nParticles_ = 0; std::unordered_map trackIdToParticle_; - std::vector> children_; - std::vector> directHits_; - std::vector> subgraphHits_; - - std::vector totalSimHitEnergyByDetId_; + std::vector directHits_; + std::vector subgraphHits_; }; } // namespace truth diff --git a/PhysicsTools/TruthInfo/plugins/LogicalGraphHitIndexProducer.cc b/PhysicsTools/TruthInfo/plugins/LogicalGraphHitIndexProducer.cc index 49bc2bbe1a084..1b07a032f89c4 100644 --- a/PhysicsTools/TruthInfo/plugins/LogicalGraphHitIndexProducer.cc +++ b/PhysicsTools/TruthInfo/plugins/LogicalGraphHitIndexProducer.cc @@ -1,42 +1,39 @@ +#include #include #include #include -#include #include +#include #include -#include "DataFormats/Common/interface/Handle.h" -#include "DataFormats/DetId/interface/DetId.h" -#include "DataFormats/ForwardDetId/interface/HGCalDetId.h" -#include "DataFormats/HcalDetId/interface/HcalDetId.h" - #include "FWCore/Framework/interface/Event.h" #include "FWCore/Framework/interface/EventSetup.h" -#include "FWCore/Framework/interface/global/EDProducer.h" #include "FWCore/Framework/interface/MakerMacros.h" +#include "FWCore/Framework/interface/global/EDProducer.h" +#include "FWCore/MessageLogger/interface/MessageLogger.h" #include "FWCore/ParameterSet/interface/ConfigurationDescriptions.h" #include "FWCore/ParameterSet/interface/ParameterSet.h" #include "FWCore/ParameterSet/interface/ParameterSetDescription.h" #include "FWCore/Utilities/interface/EDGetToken.h" #include "FWCore/Utilities/interface/InputTag.h" -#include "FWCore/MessageLogger/interface/MessageLogger.h" +#include "DataFormats/DetId/interface/DetId.h" +#include "DataFormats/ForwardDetId/interface/HGCalDetId.h" #include "Geometry/CaloGeometry/interface/CaloGeometry.h" #include "Geometry/HGCalGeometry/interface/HGCalGeometry.h" #include "Geometry/HcalCommonData/interface/HcalHitRelabeller.h" #include "Geometry/HcalTowerAlgo/interface/HcalGeometry.h" #include "Geometry/Records/interface/CaloGeometryRecord.h" - #include "SimDataFormats/CaloHit/interface/PCaloHit.h" #include "SimDataFormats/CaloTest/interface/HGCalTestNumbering.h" -#include "SimDataFormats/Track/interface/SimTrack.h" -#include "SimDataFormats/Track/interface/SimTrackContainer.h" #include "PhysicsTools/TruthInfo/interface/Graph.h" #include "PhysicsTools/TruthInfo/interface/LogicalGraphHitIndex.h" #include "PhysicsTools/TruthInfo/interface/LogicalGraphHitIndexBuilder.h" #include "PhysicsTools/TruthInfo/interface/TruthGraph.h" +#include "SimCalorimetry/HGCalAssociatorProducers/interface/DetIdRecHitMap.h" + namespace { struct LogicalGraphView { @@ -70,254 +67,300 @@ namespace { uint32_t checkedTrackId(int64_t key) { if (key < 0 || key > static_cast(std::numeric_limits::max())) - throw std::runtime_error("Invalid SimTrack key in logical graph"); + return 0; return static_cast(key); } + bool inputTagLooksLikeHGCal(edm::InputTag const& tag) { + const std::string instance = tag.instance(); + return instance.find("HGCHits") != std::string::npos || instance.find("HGCEE") != std::string::npos || + instance.find("HGCHE") != std::string::npos; + } + + bool inputTagLooksLikeHcal(edm::InputTag const& tag) { + const std::string instance = tag.instance(); + return instance.find("HcalHits") != std::string::npos || instance.find("Hcal") != std::string::npos; + } + + struct RelabelContext { + int geometryType = -1; + + std::array hgTopologies = {nullptr, nullptr, nullptr}; + std::array hgConstants = {nullptr, nullptr, nullptr}; + + HcalDDDRecConstants const* hcalConstants = nullptr; + }; + } // namespace class TruthLogicalGraphHitIndexProducer : public edm::global::EDProducer<> { public: - explicit TruthLogicalGraphHitIndexProducer(edm::ParameterSet const& cfg) - : graphToken_(consumes(cfg.getParameter("src"))), - rawGraphToken_(consumes(cfg.getParameter("rawSrc"))), - - simTracksToken_(consumes(cfg.getParameter("simTracks"))), - simHitCollections_(cfg.getParameter>("simHitCollections")), - doHGCal_(cfg.getParameter("doHGCal")), - doHGCalRelabelling_(cfg.getParameter("doHGCalRelabelling")), - geomToken_(esConsumes()) { - for (auto const& tag : simHitCollections_) { - simHitTokens_.push_back(consumes>(tag)); - } + explicit TruthLogicalGraphHitIndexProducer(edm::ParameterSet const& cfg); + ~TruthLogicalGraphHitIndexProducer() override = default; - produces(); - } + static void fillDescriptions(edm::ConfigurationDescriptions& descriptions); + +private: + void produce(edm::StreamID, edm::Event&, edm::EventSetup const&) const override; + + void fillTrackToParticleMap(LogicalGraphView const& graph, + TruthGraph const& rawGraph, + truth::LogicalGraphHitIndexBuilder& builder) const; + + void fillSimHits(edm::Event& event, + edm::EventSetup const& setup, + truth::LogicalGraphHitIndexBuilder& builder, + hgcal::DetIdRecHitMap const* recHitMap) const; - static void fillDescriptions(edm::ConfigurationDescriptions& descriptions) { - edm::ParameterSetDescription desc; + RelabelContext makeRelabelContext(edm::EventSetup const& setup) const; - desc.add("src", edm::InputTag("truthLogicalGraphProducer")); - desc.add("rawSrc", edm::InputTag("truthGraphProducer")); + uint32_t recoDetIdForSimHit(PCaloHit const& simHit, + bool isHGCalCollection, + bool isHcalCollection, + RelabelContext const& context) const; + + edm::EDGetTokenT graphToken_; + edm::EDGetTokenT rawGraphToken_; + edm::EDGetTokenT recHitMapToken_; - desc.add("simTracks", edm::InputTag("g4SimHits")); + std::vector simHitTags_; + std::vector>> simHitTokens_; - desc.add>("simHitCollections", - { - edm::InputTag("g4SimHits", "HGCHitsEE"), - edm::InputTag("g4SimHits", "HGCHitsHEfront"), - edm::InputTag("g4SimHits", "HGCHitsHEback"), - }); + edm::ESGetToken geomToken_; - desc.add("doHGCal", true); - desc.add("doHGCalRelabelling", true); + bool doHGCalRelabelling_ = true; +}; - descriptions.addWithDefaultLabel(desc); +TruthLogicalGraphHitIndexProducer::TruthLogicalGraphHitIndexProducer(edm::ParameterSet const& cfg) + : graphToken_(consumes(cfg.getParameter("src"))), + rawGraphToken_(consumes(cfg.getParameter("rawSrc"))), + recHitMapToken_(consumes(cfg.getParameter("recHitMap"))), + simHitTags_(cfg.getParameter>("simHitCollections")), + geomToken_(esConsumes()), + doHGCalRelabelling_(cfg.getParameter("doHGCalRelabelling")) { + simHitTokens_.reserve(simHitTags_.size()); + for (auto const& tag : simHitTags_) { + simHitTokens_.push_back(consumes>(tag)); } - void produce(edm::StreamID, edm::Event& event, edm::EventSetup const& setup) const override { - auto const& graph = event.get(graphToken_); - auto const& rawGraph = event.get(rawGraphToken_); + produces(); +} - LogicalGraphView graphView(graph); +void TruthLogicalGraphHitIndexProducer::fillDescriptions(edm::ConfigurationDescriptions& descriptions) { + edm::ParameterSetDescription desc; - truth::LogicalGraphHitIndexBuilder builder(graphView.nParticles()); + desc.add("src", edm::InputTag("truthLogicalGraphProducer")); + desc.add("rawSrc", edm::InputTag("truthGraphProducer")); + desc.add("recHitMap", edm::InputTag("simHitToRecHitMapProducer")); - fillTrackToParticleMap(graphView, rawGraph, builder); - fillSimHits(event, setup, builder); + desc.add>("simHitCollections", + {edm::InputTag("g4SimHits", "HGCHitsEE"), + edm::InputTag("g4SimHits", "HGCHitsHEfront"), + edm::InputTag("g4SimHits", "HGCHitsHEback")}); - auto output = std::make_unique(builder.finish()); - event.put(std::move(output)); - } + desc.add("doHGCalRelabelling", true) + ->setComment("Convert old HGCAL simulation DetIds to reco DetIds before looking up recHits"); -private: - void fillTrackToParticleMap(LogicalGraphView const& graph, - TruthGraph const& rawGraph, - truth::LogicalGraphHitIndexBuilder& builder) const { - for (uint32_t particleId = 0; particleId < graph.nParticles(); ++particleId) { - if (!graph.particleHasSim(particleId)) - continue; + descriptions.addWithDefaultLabel(desc); +} - const int32_t simNode = graph.particleSimNode(particleId); - if (simNode < 0) - continue; +void TruthLogicalGraphHitIndexProducer::produce(edm::StreamID, edm::Event& event, edm::EventSetup const& setup) const { + auto const& graph = event.get(graphToken_); + auto const& rawGraph = event.get(rawGraphToken_); - const uint32_t simNodeU32 = static_cast(simNode); - if (simNodeU32 >= rawGraph.nNodes()) - continue; + edm::Handle hRecHitMap; + event.getByToken(recHitMapToken_, hRecHitMap); + auto const* recHitMap = hRecHitMap.isValid() ? &(*hRecHitMap) : nullptr; - auto const& ref = rawGraph.nodeRef(simNodeU32); - if (ref.kind != TruthGraph::NodeKind::SimTrack) - continue; + LogicalGraphView graphView(graph); - builder.setSimTrackForParticle(particleId, checkedTrackId(ref.key)); - } + truth::LogicalGraphHitIndexBuilder builder(graphView.nParticles()); - for (uint32_t parentId = 0; parentId < graph.nParticles(); ++parentId) { - graph.forEachParticleChild(parentId, [&](uint32_t childId) { builder.addParticleChild(parentId, childId); }); - } - } + fillTrackToParticleMap(graphView, rawGraph, builder); + fillSimHits(event, setup, builder, recHitMap); - void fillSimHits(edm::Event const& event, - edm::EventSetup const& setup, - truth::LogicalGraphHitIndexBuilder& builder) const { - GeometryCache geometry; - if (doHGCalRelabelling_) - geometry = makeGeometryCache(setup); + auto output = std::make_unique(builder.finish()); + event.put(std::move(output)); +} - for (std::size_t i = 0; i < simHitTokens_.size(); ++i) { - edm::Handle> hits; - event.getByToken(simHitTokens_[i], hits); +void TruthLogicalGraphHitIndexProducer::fillTrackToParticleMap(LogicalGraphView const& graph, + TruthGraph const& rawGraph, + truth::LogicalGraphHitIndexBuilder& builder) const { + for (uint32_t particleId = 0; particleId < graph.nParticles(); ++particleId) { + if (!graph.particleHasSim(particleId)) + continue; - if (!hits.isValid()) - continue; + const int32_t simNode = graph.particleSimNode(particleId); + if (simNode < 0) + continue; - const auto& tag = simHitCollections_[i]; - const bool isHcal = tag.instance().find("HcalHits") != std::string::npos; - const bool isHGCal = tag.instance().find("HGCHits") != std::string::npos; + const uint32_t simNodeU32 = static_cast(simNode); + if (simNodeU32 >= rawGraph.nNodes()) + continue; - for (auto const& hit : *hits) { - if (hit.geantTrackId() == 0) - continue; + auto const& ref = rawGraph.nodeRef(simNodeU32); + if (ref.kind != TruthGraph::NodeKind::SimTrack) + continue; - const DetId detId = makeRecoDetId(hit, isHGCal, isHcal, geometry); - if (detId == DetId(0)) - continue; + const uint32_t trackId = checkedTrackId(ref.key); + if (trackId == 0) + continue; - builder.addHitForTrack(hit.geantTrackId(), detId.rawId(), hit.energy()); - } - } + builder.setSimTrackForParticle(particleId, trackId); } - struct GeometryCache { - int geometryType = -1; + for (uint32_t parentId = 0; parentId < graph.nParticles(); ++parentId) { + graph.forEachParticleChild(parentId, [&](uint32_t childId) { builder.addParticleChild(parentId, childId); }); + } +} - HGCalTopology const* hgtopo[3] = {nullptr, nullptr, nullptr}; - HGCalDDDConstants const* hgddd[3] = {nullptr, nullptr, nullptr}; +RelabelContext TruthLogicalGraphHitIndexProducer::makeRelabelContext(edm::EventSetup const& setup) const { + RelabelContext context; - HcalDDDRecConstants const* hcddd = nullptr; - }; + if (!doHGCalRelabelling_) + return context; + + auto const& geom = setup.getData(geomToken_); - GeometryCache makeGeometryCache(edm::EventSetup const& setup) const { - GeometryCache cache; + auto const* hcalGeometry = static_cast(geom.getSubdetectorGeometry(DetId::Hcal, HcalEndcap)); + if (hcalGeometry != nullptr) { + context.hcalConstants = hcalGeometry->topology().dddConstants(); + } - auto const& geom = setup.getData(geomToken_); + auto const* eeGeometry = + static_cast(geom.getSubdetectorGeometry(DetId::HGCalEE, ForwardSubdetector::ForwardEmpty)); - auto const* hcalGeom = static_cast(geom.getSubdetectorGeometry(DetId::Hcal, HcalEndcap)); - if (hcalGeom) - cache.hcddd = hcalGeom->topology().dddConstants(); + if (eeGeometry != nullptr) { + context.geometryType = 1; - if (!doHGCal_) - return cache; + auto const* fhGeometry = static_cast( + geom.getSubdetectorGeometry(DetId::HGCalHSi, ForwardSubdetector::ForwardEmpty)); + auto const* bhGeometry = static_cast( + geom.getSubdetectorGeometry(DetId::HGCalHSc, ForwardSubdetector::ForwardEmpty)); - auto const* eeGeom = static_cast( - geom.getSubdetectorGeometry(DetId::HGCalEE, ForwardSubdetector::ForwardEmpty)); + context.hgTopologies[0] = &eeGeometry->topology(); + context.hgTopologies[1] = fhGeometry != nullptr ? &fhGeometry->topology() : nullptr; + context.hgTopologies[2] = bhGeometry != nullptr ? &bhGeometry->topology() : nullptr; - if (eeGeom) { - cache.geometryType = 1; + for (unsigned i = 0; i < context.hgTopologies.size(); ++i) { + if (context.hgTopologies[i] != nullptr) + context.hgConstants[i] = &context.hgTopologies[i]->dddConstants(); + } - auto const* fhGeom = static_cast( - geom.getSubdetectorGeometry(DetId::HGCalHSi, ForwardSubdetector::ForwardEmpty)); - auto const* bhGeom = static_cast( - geom.getSubdetectorGeometry(DetId::HGCalHSc, ForwardSubdetector::ForwardEmpty)); + return context; + } - cache.hgtopo[0] = &eeGeom->topology(); - if (fhGeom) - cache.hgtopo[1] = &fhGeom->topology(); - if (bhGeom) - cache.hgtopo[2] = &bhGeom->topology(); - } else { - cache.geometryType = 0; + context.geometryType = 0; - eeGeom = static_cast(geom.getSubdetectorGeometry(DetId::Forward, HGCEE)); - auto const* fhGeom = static_cast(geom.getSubdetectorGeometry(DetId::Forward, HGCHEF)); + eeGeometry = static_cast(geom.getSubdetectorGeometry(DetId::Forward, HGCEE)); + auto const* fhGeometry = static_cast(geom.getSubdetectorGeometry(DetId::Forward, HGCHEF)); - if (eeGeom) - cache.hgtopo[0] = &eeGeom->topology(); - if (fhGeom) - cache.hgtopo[1] = &fhGeom->topology(); - } + context.hgTopologies[0] = eeGeometry != nullptr ? &eeGeometry->topology() : nullptr; + context.hgTopologies[1] = fhGeometry != nullptr ? &fhGeometry->topology() : nullptr; - for (unsigned i = 0; i < 3; ++i) { - if (cache.hgtopo[i]) - cache.hgddd[i] = &cache.hgtopo[i]->dddConstants(); - } + for (unsigned i = 0; i < context.hgTopologies.size(); ++i) { + if (context.hgTopologies[i] != nullptr) + context.hgConstants[i] = &context.hgTopologies[i]->dddConstants(); + } + + return context; +} + +uint32_t TruthLogicalGraphHitIndexProducer::recoDetIdForSimHit(PCaloHit const& simHit, + bool isHGCalCollection, + bool isHcalCollection, + RelabelContext const& context) const { + const uint32_t simId = simHit.id(); - return cache; + if (!doHGCalRelabelling_) { + return simId; } - DetId makeRecoDetId(PCaloHit const& hit, bool isHGCal, bool isHcal, GeometryCache const& geometry) const { - if (!doHGCalRelabelling_) { - return DetId(hit.id()); + if (isHGCalCollection) { + if (context.geometryType == 1) { + return simId; } - if (isHGCal) { - const uint32_t simId = hit.id(); + int subdet = 0; + int layer = 0; + int cell = 0; + int sec = 0; + int subsec = 0; + int zp = 0; - if (geometry.geometryType == 1) { - return DetId(simId); - } + HGCalTestNumbering::unpackHexagonIndex(simId, subdet, zp, layer, sec, subsec, cell); - if (isHcal) { - if (!geometry.hcddd) - return DetId(0); + const int hgcalIndex = subdet - 3; + if (hgcalIndex < 0 || hgcalIndex >= static_cast(context.hgConstants.size())) + return 0; - HcalDetId hid = HcalHitRelabeller::relabel(simId, geometry.hcddd); - if (hid.subdet() == HcalEndcap) - return hid; + auto const* constants = context.hgConstants[hgcalIndex]; + auto const* topology = context.hgTopologies[hgcalIndex]; - return DetId(0); - } + if (constants == nullptr || topology == nullptr) + return 0; - int subdet = 0; - int layer = 0; - int cell = 0; - int sec = 0; - int subsec = 0; - int zp = 0; + const auto recoLayerCell = constants->simToReco(cell, layer, sec, topology->detectorType()); + cell = recoLayerCell.first; + layer = recoLayerCell.second; - HGCalTestNumbering::unpackHexagonIndex(simId, subdet, zp, layer, sec, subsec, cell); + if (layer < 0) + return 0; - if (subdet < 3 || subdet > 5) - return DetId(0); + return HGCalDetId(static_cast(subdet), zp, layer, subsec, sec, cell).rawId(); + } - const unsigned idx = static_cast(subdet - 3); - if (!geometry.hgddd[idx] || !geometry.hgtopo[idx]) - return DetId(0); + if (isHcalCollection && context.hcalConstants != nullptr) { + return HcalHitRelabeller::relabel(simId, context.hcalConstants).rawId(); + } - auto const recoLayerCell = geometry.hgddd[idx]->simToReco(cell, layer, sec, geometry.hgtopo[idx]->detectorType()); - cell = recoLayerCell.first; - layer = recoLayerCell.second; + return simId; +} - if (layer == -1) - return DetId(0); +void TruthLogicalGraphHitIndexProducer::fillSimHits(edm::Event& event, + edm::EventSetup const& setup, + truth::LogicalGraphHitIndexBuilder& builder, + hgcal::DetIdRecHitMap const* recHitMap) const { + const RelabelContext relabelContext = makeRelabelContext(setup); - return HGCalDetId(static_cast(subdet), zp, layer, subsec, sec, cell); - } + for (uint32_t tokenIndex = 0; tokenIndex < simHitTokens_.size(); ++tokenIndex) { + auto const& token = simHitTokens_[tokenIndex]; + auto const& tag = simHitTags_[tokenIndex]; - if (isHcal) { - if (!geometry.hcddd) - return DetId(0); + edm::Handle> hSimHits; + event.getByToken(token, hSimHits); - return HcalHitRelabeller::relabel(hit.id(), geometry.hcddd); + if (!hSimHits.isValid()) { + edm::LogWarning("TruthLogicalGraphHitIndexProducer") + << "Missing PCaloHit collection " << tag.encode() << ". Skipping it."; + continue; } - return DetId(hit.id()); - } + const bool isHGCalCollection = inputTagLooksLikeHGCal(tag); + const bool isHcalCollection = inputTagLooksLikeHcal(tag); - edm::EDGetTokenT graphToken_; - edm::EDGetTokenT rawGraphToken_; + for (auto const& simHit : *hSimHits) { + const int geantTrackId = simHit.geantTrackId(); + if (geantTrackId <= 0) + continue; - edm::EDGetTokenT simTracksToken_; + const uint32_t detId = recoDetIdForSimHit(simHit, isHGCalCollection, isHcalCollection, relabelContext); + if (detId == 0) + continue; - std::vector simHitCollections_; - std::vector>> simHitTokens_; + uint32_t recHitIndex = truth::LogicalGraphHitIndex::Hit::invalidRecHitIndex; - bool doHGCal_; - bool doHGCalRelabelling_; + if (recHitMap != nullptr) { + const auto it = recHitMap->find(detId); + if (it != recHitMap->end()) { + recHitIndex = it->second; + } + } - edm::ESGetToken geomToken_; -}; + builder.addHitForTrack(static_cast(geantTrackId), detId, recHitIndex, simHit.energy()); + } + } +} DEFINE_FWK_MODULE(TruthLogicalGraphHitIndexProducer); diff --git a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc index 9fbe390a35a36..1de365b7a2a50 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -10,11 +11,15 @@ #include "FWCore/Framework/interface/EventSetup.h" #include "FWCore/Framework/interface/MakerMacros.h" #include "FWCore/Framework/interface/one/EDAnalyzer.h" +#include "FWCore/MessageLogger/interface/MessageLogger.h" #include "FWCore/ParameterSet/interface/ConfigurationDescriptions.h" #include "FWCore/ParameterSet/interface/ParameterSet.h" #include "FWCore/ParameterSet/interface/ParameterSetDescription.h" #include "FWCore/Utilities/interface/InputTag.h" +#include "DataFormats/HGCRecHit/interface/HGCRecHitCollections.h" +#include "DataFormats/ParticleFlowReco/interface/PFRecHit.h" + #include "PhysicsTools/TruthInfo/interface/Graph.h" #include "PhysicsTools/TruthInfo/interface/LogicalGraphHitIndex.h" #include "PhysicsTools/TruthInfo/interface/TruthGraph.h" @@ -197,14 +202,6 @@ namespace { return ss.str(); } - float sumHitEnergy(std::span hits) { - float sum = 0.f; - for (auto const& hit : hits) { - sum += hit.energy; - } - return sum; - } - std::string fmtEnergy(float energy) { std::ostringstream ss; ss.setf(std::ios::fixed); @@ -212,6 +209,35 @@ namespace { return ss.str(); } + struct HitSummary { + uint32_t nSimHits = 0; + uint32_t nMatchedRecHits = 0; + uint32_t nMissingRecHits = 0; + float simHitEnergy = 0.f; + float recHitEnergy = 0.f; + }; + + HitSummary summarizeHits(std::span hits, + std::vector const& recHitEnergies) { + HitSummary summary; + summary.nSimHits = static_cast(hits.size()); + + for (auto const& hit : hits) { + summary.simHitEnergy += hit.energy; + + if (hit.recHitIndex == truth::LogicalGraphHitIndex::Hit::invalidRecHitIndex || + hit.recHitIndex >= recHitEnergies.size()) { + ++summary.nMissingRecHits; + continue; + } + + ++summary.nMatchedRecHits; + summary.recHitEnergy += recHitEnergies[hit.recHitIndex]; + } + + return summary; + } + std::string appendEventIdToFilename(std::string const& filename, edm::EventID const& id) { const auto dotPos = filename.rfind('.'); @@ -249,7 +275,25 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { maxVertices_(cfg.getParameter("maxVertices")), maxEdgesPerNode_(cfg.getParameter("maxEdgesPerNode")), hideLargeSimSourceVertices_(cfg.getParameter("hideLargeSimSourceVertices")), - largeSimSourceVertexMinOutgoing_(cfg.getParameter("largeSimSourceVertexMinOutgoing")) {} + largeSimSourceVertexMinOutgoing_(cfg.getParameter("largeSimSourceVertexMinOutgoing")) { + const auto hgcalRecHitTags = cfg.getParameter>("hgcalRecHits"); + hgcalRecHitTags_.reserve(hgcalRecHitTags.size()); + hgcalRecHitTokens_.reserve(hgcalRecHitTags.size()); + + for (auto const& tag : hgcalRecHitTags) { + hgcalRecHitTags_.push_back(tag); + hgcalRecHitTokens_.push_back(mayConsume(tag)); + } + + const auto pfRecHitTags = cfg.getParameter>("pfRecHits"); + pfRecHitTags_.reserve(pfRecHitTags.size()); + pfRecHitTokens_.reserve(pfRecHitTags.size()); + + for (auto const& tag : pfRecHitTags) { + pfRecHitTags_.push_back(tag); + pfRecHitTokens_.push_back(mayConsume(tag)); + } + } static void fillDescriptions(edm::ConfigurationDescriptions& descriptions) { edm::ParameterSetDescription desc; @@ -257,8 +301,22 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { desc.add("src", edm::InputTag("truthLogicalGraphProducer")); desc.add("rawSrc", edm::InputTag("truthGraphProducer")) ->setComment("Optional raw TruthGraph used only to enrich labels"); + desc.add("hitIndex", edm::InputTag("")) - ->setComment("Optional LogicalGraphHitIndex used to annotate particles with direct/subgraph SimHit summaries"); + ->setComment("Optional LogicalGraphHitIndex used to annotate particles with SimHit and RecHit summaries"); + + desc.add>("hgcalRecHits", + {edm::InputTag("HGCalRecHit", "HGCEERecHits", "RECO"), + edm::InputTag("HGCalRecHit", "HGCHEFRecHits", "RECO"), + edm::InputTag("HGCalRecHit", "HGCHEBRecHits", "RECO")}) + ->setComment("HGCRecHit collections, in the same order used by SimHitToRecHitMapProducer"); + + desc.add>("pfRecHits", + {edm::InputTag("particleFlowRecHitECAL", "Cleaned", "RECO"), + edm::InputTag("particleFlowRecHitHBHE", "Cleaned", "RECO"), + edm::InputTag("particleFlowRecHitHF", "Cleaned", "RECO"), + edm::InputTag("particleFlowRecHitHO", "Cleaned", "RECO")}) + ->setComment("PFRecHit collections, in the same order used by SimHitToRecHitMapProducer"); desc.add("dotFile", "truthlogicalgraph.dot"); @@ -288,6 +346,8 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { } truth::LogicalGraphHitIndex const* hitIndex = hHitIndex.isValid() ? &(*hHitIndex) : nullptr; + const std::vector recHitEnergies = collectRecHitEnergies(evt); + const std::string eventDotFile = appendEventIdToFilename(dotFile_, evt.id()); std::ofstream os(eventDotFile); @@ -323,12 +383,14 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { auto const& d = p.data(); const bool hasHitInfo = hitIndex != nullptr && i < hitIndex->nParticles(); + const auto directHits = hasHitInfo ? hitIndex->directHits(i) : std::span(); const auto subgraphHits = hasHitInfo ? hitIndex->subgraphHits(i) : std::span(); - const float directHitEnergy = hasHitInfo ? sumHitEnergy(directHits) : 0.f; - const float subgraphHitEnergy = hasHitInfo ? sumHitEnergy(subgraphHits) : 0.f; + + const HitSummary directSummary = hasHitInfo ? summarizeHits(directHits, recHitEnergies) : HitSummary(); + const HitSummary subgraphSummary = hasHitInfo ? summarizeHits(subgraphHits, recHitEnergies) : HitSummary(); os << " p" << i << " [shape=ellipse, hasCheckpoints=" << p.hasCheckpoints() << ", hasGen=" << p.hasGen() << ", hasSim=" << d.hasSim(); @@ -352,8 +414,13 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { << ", nCheckpoints=" << d.checkpoints.size(); if (hasHitInfo) { - os << ", nDirectHits=" << directHits.size() << ", directHitEnergy=" << fmtEnergy(directHitEnergy) - << ", nSubgraphHits=" << subgraphHits.size() << ", subgraphHitEnergy=" << fmtEnergy(subgraphHitEnergy); + os << ", nDirectSimHits=" << directSummary.nSimHits << ", nDirectRecHits=" << directSummary.nMatchedRecHits + << ", directSimHitEnergy=" << fmtEnergy(directSummary.simHitEnergy) + << ", directRecHitEnergy=" << fmtEnergy(directSummary.recHitEnergy) + << ", nSubgraphSimHits=" << subgraphSummary.nSimHits + << ", nSubgraphRecHits=" << subgraphSummary.nMatchedRecHits + << ", subgraphSimHitEnergy=" << fmtEnergy(subgraphSummary.simHitEnergy) + << ", subgraphRecHitEnergy=" << fmtEnergy(subgraphSummary.recHitEnergy); } if (raw != nullptr) { @@ -399,9 +466,16 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { os << " \n"; if (hasHitInfo) { - os << " \n"; + os << " \n"; - os << " \n"; + os << " \n"; } @@ -513,12 +587,59 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { } private: + std::vector collectRecHitEnergies(const edm::Event& evt) const { + std::vector energies; + + // This must match the global recHit indexing order used by SimHitToRecHitMapProducer: + // first all HGCRecHit collections, then all PFRecHit collections. + for (uint32_t i = 0; i < hgcalRecHitTokens_.size(); ++i) { + edm::Handle handle; + evt.getByToken(hgcalRecHitTokens_[i], handle); + + if (!handle.isValid()) { + edm::LogWarning("TruthLogicalGraphDumper") << "Missing HGCRecHit collection " << hgcalRecHitTags_[i].encode() + << ". Skipping it while rebuilding recHit energies."; + continue; + } + + energies.reserve(energies.size() + handle->size()); + for (auto const& hit : *handle) { + energies.push_back(hit.energy()); + } + } + + for (uint32_t i = 0; i < pfRecHitTokens_.size(); ++i) { + edm::Handle handle; + evt.getByToken(pfRecHitTokens_[i], handle); + + if (!handle.isValid()) { + edm::LogWarning("TruthLogicalGraphDumper") << "Missing reco::PFRecHitCollection " << pfRecHitTags_[i].encode() + << ". Skipping it while rebuilding recHit energies."; + continue; + } + + energies.reserve(energies.size() + handle->size()); + for (auto const& hit : *handle) { + energies.push_back(hit.energy()); + } + } + + return energies; + } + edm::EDGetTokenT token_; edm::EDGetTokenT rawToken_; + edm::InputTag hitIndexTag_; edm::EDGetTokenT hitIndexToken_; bool useHitIndex_; + std::vector hgcalRecHitTags_; + std::vector> hgcalRecHitTokens_; + + std::vector pfRecHitTags_; + std::vector> pfRecHitTokens_; + std::string dotFile_; unsigned maxParticles_; unsigned maxVertices_; diff --git a/PhysicsTools/TruthInfo/src/LogicalGraphHitIndexBuilder.cc b/PhysicsTools/TruthInfo/src/LogicalGraphHitIndexBuilder.cc index 89884abe00e54..8fe6de8c339fc 100644 --- a/PhysicsTools/TruthInfo/src/LogicalGraphHitIndexBuilder.cc +++ b/PhysicsTools/TruthInfo/src/LogicalGraphHitIndexBuilder.cc @@ -1,245 +1,143 @@ #include "PhysicsTools/TruthInfo/interface/LogicalGraphHitIndexBuilder.h" #include -#include +#include namespace truth { - void LogicalGraphHitIndexBuilder::reset(uint32_t nParticles) { - nParticles_ = nParticles; - - trackIdToParticle_.clear(); - trackIdToParticle_.reserve(nParticles * 2); - - children_.assign(nParticles, {}); - directHits_.assign(nParticles, {}); - subgraphHits_.assign(nParticles, {}); - - totalSimHitEnergyByDetId_.clear(); - } + LogicalGraphHitIndexBuilder::LogicalGraphHitIndexBuilder(uint32_t nParticles) + : nParticles_(nParticles), children_(nParticles), directHits_(nParticles), subgraphHits_(nParticles) {} void LogicalGraphHitIndexBuilder::setSimTrackForParticle(uint32_t particleId, uint32_t trackId) { if (particleId >= nParticles_) - throw std::out_of_range("LogicalGraphHitIndexBuilder::setSimTrackForParticle: particle index out of range"); + return; - trackIdToParticle_.emplace(trackId, particleId); + trackIdToParticle_[trackId] = particleId; } void LogicalGraphHitIndexBuilder::addParticleChild(uint32_t parentParticleId, uint32_t childParticleId) { if (parentParticleId >= nParticles_ || childParticleId >= nParticles_) - throw std::out_of_range("LogicalGraphHitIndexBuilder::addParticleChild: particle index out of range"); + return; children_[parentParticleId].push_back(childParticleId); } - void LogicalGraphHitIndexBuilder::addHitForTrack(uint32_t trackId, uint32_t detId, float energy) { - if (energy == 0.f) + void LogicalGraphHitIndexBuilder::addHitForTrack(uint32_t trackId, + uint32_t detId, + uint32_t recHitIndex, + float energy) { + if (energy <= 0.f) return; - auto const it = trackIdToParticle_.find(trackId); + auto it = trackIdToParticle_.find(trackId); if (it == trackIdToParticle_.end()) return; - addHitForParticle(it->second, detId, energy); + addHit(directHits_[it->second], detId, recHitIndex, energy); } - void LogicalGraphHitIndexBuilder::addHitForParticle(uint32_t particleId, uint32_t detId, float energy) { - if (particleId >= nParticles_) - throw std::out_of_range("LogicalGraphHitIndexBuilder::addHitForParticle: particle index out of range"); - - if (energy == 0.f) - return; + void LogicalGraphHitIndexBuilder::addHit(HitMap& hits, uint32_t detId, uint32_t recHitIndex, float energy) { + auto& entry = hits[detId]; + entry.energy += energy; - directHits_[particleId].push_back({detId, energy}); - totalSimHitEnergyByDetId_.push_back({detId, energy}); - } - - void LogicalGraphHitIndexBuilder::sortAndReduce(std::vector& hits) { - std::sort(hits.begin(), hits.end(), [](Hit const& a, Hit const& b) { return a.detId < b.detId; }); - - std::size_t out = 0; - for (auto const& hit : hits) { - if (hit.energy == 0.f) - continue; - - if (out != 0 && hits[out - 1].detId == hit.detId) { - hits[out - 1].energy += hit.energy; - } else { - hits[out++] = hit; - } + if (entry.recHitIndex == Hit::invalidRecHitIndex && recHitIndex != Hit::invalidRecHitIndex) { + entry.recHitIndex = recHitIndex; } - - hits.resize(out); } - void LogicalGraphHitIndexBuilder::sortAndReduceDirectHits() { - for (auto& hits : directHits_) { - sortAndReduce(hits); - } - - sortAndReduce(totalSimHitEnergyByDetId_); - - for (auto& daughters : children_) { - std::sort(daughters.begin(), daughters.end()); - daughters.erase(std::unique(daughters.begin(), daughters.end()), daughters.end()); - } + void LogicalGraphHitIndexBuilder::addHit(HitMap& hits, Hit const& hit) { + addHit(hits, hit.detId, hit.recHitIndex, hit.energy); } - std::vector LogicalGraphHitIndexBuilder::mergeSortedHitLists( - std::span a, std::span b) { + std::vector LogicalGraphHitIndexBuilder::sortedHits(HitMap const& hits) { std::vector out; - out.reserve(a.size() + b.size()); - - auto ia = a.begin(); - auto ib = b.begin(); - - while (ia != a.end() && ib != b.end()) { - if (ia->detId < ib->detId) { - out.push_back(*ia++); - } else if (ib->detId < ia->detId) { - out.push_back(*ib++); - } else { - out.push_back({ia->detId, ia->energy + ib->energy}); - ++ia; - ++ib; - } + out.reserve(hits.size()); + + for (auto const& [detId, acc] : hits) { + if (acc.energy <= 0.f) + continue; + + Hit hit; + hit.detId = detId; + hit.recHitIndex = acc.recHitIndex; + hit.energy = acc.energy; + out.push_back(hit); } - out.insert(out.end(), ia, a.end()); - out.insert(out.end(), ib, b.end()); + std::sort(out.begin(), out.end(), [](Hit const& a, Hit const& b) { + if (a.detId != b.detId) + return a.detId < b.detId; + return a.recHitIndex < b.recHitIndex; + }); return out; } - void LogicalGraphHitIndexBuilder::mergeInto(std::vector& dst, std::span src) { - if (src.empty()) + void LogicalGraphHitIndexBuilder::fillSubgraphHits(uint32_t particleId, std::vector& state) { + if (particleId >= nParticles_) return; - if (dst.empty()) { - dst.assign(src.begin(), src.end()); + if (state[particleId] == 2) return; - } - dst = mergeSortedHitLists(dst, src); - } - - void LogicalGraphHitIndexBuilder::collectSubgraphParticles(uint32_t rootParticleId, - std::vector& visited) const { - std::vector stack; - stack.push_back(rootParticleId); - - while (!stack.empty()) { - const uint32_t particleId = stack.back(); - stack.pop_back(); + if (state[particleId] == 1) + return; - if (particleId >= nParticles_ || visited[particleId]) - continue; + state[particleId] = 1; - visited[particleId] = 1; + auto& out = subgraphHits_[particleId]; - for (uint32_t child : children_[particleId]) { - stack.push_back(child); - } + for (auto const& [detId, acc] : directHits_[particleId]) { + addHit(out, detId, acc.recHitIndex, acc.energy); } - } - - void LogicalGraphHitIndexBuilder::computeSubgraphHits() { - subgraphHits_.assign(nParticles_, {}); - std::vector visited; - - for (uint32_t particleId = 0; particleId < nParticles_; ++particleId) { - visited.assign(nParticles_, 0); - collectSubgraphParticles(particleId, visited); - - auto& out = subgraphHits_[particleId]; + for (uint32_t childId : children_[particleId]) { + if (childId >= nParticles_) + continue; - for (uint32_t other = 0; other < nParticles_; ++other) { - if (!visited[other]) - continue; + fillSubgraphHits(childId, state); - mergeInto(out, directHits_[other]); + for (auto const& [detId, acc] : subgraphHits_[childId]) { + addHit(out, detId, acc.recHitIndex, acc.energy); } } - } - - void LogicalGraphHitIndexBuilder::buildCsr(std::vector> const& perParticleHits, - std::vector& offsets, - std::vector& storage) const { - offsets.clear(); - storage.clear(); - - offsets.reserve(nParticles_ + 1); - offsets.push_back(0); - - std::size_t totalSize = 0; - for (auto const& hits : perParticleHits) { - totalSize += hits.size(); - } - storage.reserve(totalSize); - for (auto const& hits : perParticleHits) { - storage.insert(storage.end(), hits.begin(), hits.end()); - offsets.push_back(static_cast(storage.size())); - } + state[particleId] = 2; } LogicalGraphHitIndex LogicalGraphHitIndexBuilder::finish() { - sortAndReduceDirectHits(); - computeSubgraphHits(); - - std::vector directOffsets; - std::vector directStorage; - buildCsr(directHits_, directOffsets, directStorage); - - std::vector subgraphOffsets; - std::vector subgraphStorage; - buildCsr(subgraphHits_, subgraphOffsets, subgraphStorage); - - LogicalGraphHitIndex out; - out.setData(nParticles_, - std::move(directOffsets), - std::move(directStorage), - std::move(subgraphOffsets), - std::move(subgraphStorage), - std::move(totalSimHitEnergyByDetId_)); - - return out; - } - - std::vector LogicalGraphHitIndexBuilder::collectMergedDirectHits( - std::span particles) const { - std::vector out; - - for (uint32_t particleId : particles) { - if (particleId >= nParticles_) - continue; - - mergeInto(out, directHits_[particleId]); + std::vector state(nParticles_, 0); + for (uint32_t particleId = 0; particleId < nParticles_; ++particleId) { + fillSubgraphHits(particleId, state); } - return out; - } - - std::vector LogicalGraphHitIndexBuilder::collectMergedSubgraphHits( - std::span particles) const { - std::vector visited(nParticles_, 0); + std::vector directOffsets; + std::vector directHitStorage; + directOffsets.reserve(nParticles_ + 1); + directOffsets.push_back(0); - for (uint32_t particleId : particles) { - if (particleId < nParticles_) - collectSubgraphParticles(particleId, visited); + for (uint32_t particleId = 0; particleId < nParticles_; ++particleId) { + auto hits = sortedHits(directHits_[particleId]); + directHitStorage.insert(directHitStorage.end(), hits.begin(), hits.end()); + directOffsets.push_back(static_cast(directHitStorage.size())); } - std::vector out; + std::vector subgraphOffsets; + std::vector subgraphHitStorage; + subgraphOffsets.reserve(nParticles_ + 1); + subgraphOffsets.push_back(0); for (uint32_t particleId = 0; particleId < nParticles_; ++particleId) { - if (!visited[particleId]) - continue; - - mergeInto(out, directHits_[particleId]); + auto hits = sortedHits(subgraphHits_[particleId]); + subgraphHitStorage.insert(subgraphHitStorage.end(), hits.begin(), hits.end()); + subgraphOffsets.push_back(static_cast(subgraphHitStorage.size())); } - return out; + return LogicalGraphHitIndex(nParticles_, + std::move(directOffsets), + std::move(directHitStorage), + std::move(subgraphOffsets), + std::move(subgraphHitStorage)); } } // namespace truth From 66b1e7dc64ebd4bc410f3a3d0aee0319355144bd Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Tue, 12 May 2026 18:41:46 +0200 Subject: [PATCH 25/53] Update README --- PhysicsTools/TruthInfo/README.md | 584 +++++++++++++++++++++++++++---- 1 file changed, 520 insertions(+), 64 deletions(-) diff --git a/PhysicsTools/TruthInfo/README.md b/PhysicsTools/TruthInfo/README.md index c9975ee48da81..ca2faabdd8f94 100644 --- a/PhysicsTools/TruthInfo/README.md +++ b/PhysicsTools/TruthInfo/README.md @@ -1,19 +1,20 @@ # TruthInfo prototype -This package contains a prototype truth graph representation for CMSSW. The goal is to provide a compact, navigable, physics-oriented abstraction of the generator and simulation truth history of an event. +This package contains a prototype truth graph representation for CMSSW. The goal is to provide a compact, navigable, physics-oriented abstraction of the generator, simulation, and detector-hit truth history of an event. -The current implementation is split into two layers: +The current implementation is split into three layers: 1. `TruthGraph`: a compact raw graph built directly from existing CMS truth products. 2. `truth::Graph`: a higher-level logical graph exposing particles, vertices, payload, and navigation methods. +3. `truth::LogicalGraphHitIndex`: an auxiliary hit index associating logical particles to calorimeter SimHits and, when available, to reconstructed RecHits. The prototype is intended for validation, reconstruction studies, visualization, and future truth-reco association work. ## Motivation -CMS currently exposes truth information through several low-level collections, such as HepMC, GenParticles, SimTracks, SimVertices, TrackingParticles, SimClusters, and CaloParticles. These collections are useful, but they encode different views of the event history and are often tied to detector-specific or production-specific conventions. +CMS currently exposes truth information through several low-level collections, such as HepMC, GenParticles, SimTracks, SimVertices, TrackingParticles, SimClusters, CaloParticles, SimHits, and detector-specific RecHits. These collections are useful, but they encode different views of the event history and are often tied to detector-specific or production-specific conventions. -This package explores a different model: a single event-level truth graph that can be navigated using physics concepts. +This package explores a different model: a single event-level truth graph that can be navigated using physics concepts, with optional detector-hit indices layered on top. Typical questions this should make easier are: @@ -23,9 +24,12 @@ Typical questions this should make easier are: * Which parton initiated a reconstructed jet? * Is an object associated with the hard interaction or with pileup? * Which detector-level interactions contributed to a reconstructed object? +* Which SimHits were produced directly by a particle? +* Which SimHits were produced by the full subgraph starting from a particle? +* Which RecHits correspond to those SimHits through a DetId association? * Should a reconstructed object be associated to a single truth particle, to a branch, or to an aggregated subgraph? -The intended user-facing API should allow reconstruction and validation code to operate on stable physics abstractions rather than directly depending on the storage details of `GenParticle`, `SimTrack`, `GenVertex`, or `SimVertex`. +The intended user-facing API should allow reconstruction and validation code to operate on stable physics abstractions rather than directly depending on the storage details of `GenParticle`, `SimTrack`, `GenVertex`, `SimVertex`, `PCaloHit`, or detector-specific RecHit collections. ## Package layout @@ -34,9 +38,12 @@ PhysicsTools/TruthInfo/ interface/ TruthGraph.h Graph.h + LogicalGraphHitIndex.h + LogicalGraphHitIndexBuilder.h src/ TruthGraph.cc Graph.cc + LogicalGraphHitIndexBuilder.cc classes.h classes_def.xml plugins/ @@ -44,13 +51,26 @@ PhysicsTools/TruthInfo/ TruthGraphDumper.cc TruthLogicalGraphProducer.cc TruthLogicalGraphDumper.cc + TruthLogicalGraphHitIndexProducer.cc BuildFile.xml python/ truthGraphProducer_cfi.py truthLogicalGraphDumper_cfi.py BuildFile.xml +```` + +The auxiliary RecHit lookup used by the hit index is produced separately in: + +```text +SimCalorimetry/HGCalAssociatorProducers/ + interface/ + DetIdRecHitMap.h + plugins/ + SimHitToRecHitMapProducer.cc ``` +Despite living under `HGCalAssociatorProducers`, the current `SimHitToRecHitMapProducer` is not HGCal-only. It accepts both `HGCRecHitCollection` inputs and `reco::PFRecHitCollection` inputs. + ## Raw graph: `TruthGraph` `TruthGraph` is a compact, read-only graph representation of event truth information. It is designed as an intermediate event data product built from existing CMS collections. @@ -104,7 +124,14 @@ At present: * `Gen` edges describe the generator graph. * `Sim` edges describe the simulation graph. * `GenToSim` edges connect matched generator particles or vertices to simulation nodes. -* `SimToGen` is reserved and is not produced by the current implementation. +* `SimToGen` is reserved. + +The DOT dumper labels cross-domain edges explicitly, for example: + +```text +GenToSim +SimToGen +``` ### Storage model @@ -142,6 +169,7 @@ The raw graph stores lightweight metadata arrays parallel to the node list: ```cpp std::vector pdgId; std::vector status; +std::vector statusFlags; std::vector eventId; std::vector genEventOfNode; ``` @@ -188,12 +216,12 @@ Simulation topology is built from: * `SimTrack::vertIndex()` for `SimVertex -> SimTrack` edges; * `SimVertex::parentIndex()` for `SimTrack -> SimVertex` edges. -Generator-to-simulation particle associations are built using `SimTrack::genpartIndex()` when available. +Generator-to-simulation particle associations are built using the available `SimTrack` to generator information. The implementation keeps this association explicit in the raw graph instead of assuming that raw generator iteration indices can always be interpreted as stable GenParticle indices. When enabled, cross-domain edges are also added: * `GenParticle -> SimTrack`; -* `GenVertex -> SimVertex`, using the production vertex of the associated generator particle. +* `GenVertex -> SimVertex`, using the production vertex of the associated generator particle when available. The cross edges are controlled by: @@ -239,18 +267,24 @@ struct ParticleData { int32_t pdgId = 0; int16_t status = 0; + uint16_t statusFlags = 0; + uint64_t eventId = 0; int32_t genEvent = -1; math::XYZTLorentzVectorD momentum; std::vector checkpoints; + + bool hasGen() const; + bool hasSim() const; + bool valid() const; }; ``` The convention for the momentum is "best available": -1. prefer simulation momentum when a matched `SimTrack` exists; -2. otherwise use generator momentum; +1. for GEN+SIM particles, use the GEN four-momentum; +2. for SIM-only particles, use the `SimTrack` four-momentum; 3. otherwise keep the default value. Useful methods include: @@ -260,6 +294,7 @@ bool hasGen() const; bool hasSim() const; int32_t pdgId() const; int16_t status() const; +uint16_t statusFlags() const; uint64_t eventId() const; int32_t genEvent() const; const math::XYZTLorentzVectorD& momentum() const; @@ -293,6 +328,10 @@ struct VertexData { int32_t genEvent = -1; math::XYZTLorentzVectorD position; + + bool hasGen() const; + bool hasSim() const; + bool valid() const; }; ``` @@ -314,18 +353,49 @@ std::vector outgoingParticles() const; ### Vertex treatment -At present, generator-level and simulation-level particles may be merged when a robust association exists. +Generator-level and simulation-level particles may be merged when a robust association exists. + +Generator-level and simulation-level vertices can be merged by configuration when the producer has enough information to do so: + +```python +mergeGenSimVertices = cms.bool(True) +``` + +This is useful for compact visualization and for downstream navigation. During debugging, it can be disabled to inspect generator and simulation vertex semantics separately. + +### Intermediate GEN particle collapsing + +The logical graph producer can collapse simple generator-only chains of the form: + +```text +P -> V -> C +``` + +when: + +* `P` is not final-state; +* `C` is the only daughter; +* `P` and `C` have the same PDG id. + +This is controlled by: + +```python +collapseIntermediateGenParticles = cms.bool(True) +``` -Generator-level and simulation-level vertices are intentionally not merged. Each raw `GenVertex` and each raw `SimVertex` becomes a separate logical `truth::Vertex`. +This helps reduce visual clutter from intermediate generator copies while preserving the physically relevant branching structure. -This is deliberate for the current debugging phase. Keeping vertices separate makes it easier to diagnose: +### Filtering by mother PDG id -* incorrect GEN-SIM associations; -* unexpected topologies; -* boundary-crossing behaviour; -* differences between generator and simulation vertex semantics. +The logical graph can optionally be restricted to a selected particle species and its descendants: -Vertex merging can be revisited once the raw graph construction and association policy are better understood. +```python +motherPdgId = cms.int32(0) +``` + +The default value `0` keeps the full graph. + +A non-zero value keeps particles matching the requested PDG id and the corresponding descendant subgraphs. ## Trajectory checkpoints @@ -368,11 +438,12 @@ The producer performs the following steps: 1. Read and validate the raw `TruthGraph`. 2. Load optional payload from HepMC2, HepMC3, `SimTrackContainer`, and `SimVertexContainer`. 3. Assign temporary logical ids to raw particle and vertex nodes. -4. Merge only particle nodes across GEN and SIM when a robust association exists. -5. Keep GEN and SIM vertices separate. -6. Fill standalone `ParticleData` and `VertexData` payload. -7. Rebuild the logical bipartite graph in CSR form. -8. Validate the produced `truth::Graph`. +4. Merge particle nodes across GEN and SIM when a robust association exists. +5. Optionally merge GEN and SIM vertices. +6. Optionally collapse intermediate GEN-only particle copies. +7. Fill standalone `ParticleData` and `VertexData` payload. +8. Rebuild the logical bipartite graph in CSR-like adjacency vectors. +9. Validate the produced `truth::Graph`. The produced graph is independent of the raw graph for ordinary navigation, but it keeps optional back-references to raw node ids for debugging: @@ -383,6 +454,162 @@ VertexData::genNode VertexData::simNode ``` +## Logical hit index: `truth::LogicalGraphHitIndex` + +`truth::LogicalGraphHitIndex` is an auxiliary data product that associates logical graph particles to calorimeter SimHits and, when possible, to RecHits. + +The key idea is that SimHits are directly associated to the particle that produced them, while subgraph hit collections are computed by aggregating over descendants. + +For each logical particle, the hit index can answer two different questions: + +1. Which SimHits were produced directly by this particle? +2. Which SimHits were produced by this particle and by all particles in the subgraph below it? + +This is important because both views are useful: + +* direct hits preserve local detector contributions from one particle; +* subgraph hits represent the full detector footprint of a shower, decay branch, or composite truth object. + +### Hit payload + +The hit index stores compact hit records: + +```cpp +struct Hit { + uint32_t detId; + uint32_t recHitIndex; + float energy; +}; +``` + +The `detId` is the reco DetId used for matching to RecHits. + +The `recHitIndex` is the index in the global RecHit ordering produced by `SimHitToRecHitMapProducer`. If no RecHit was found for the SimHit DetId, it is set to: + +```cpp +truth::LogicalGraphHitIndex::Hit::invalidRecHitIndex +``` + +The `energy` is the accumulated SimHit energy for that logical particle and DetId. + +### Direct and subgraph access + +The hit index exposes spans of hits per logical particle, for example: + +```cpp +auto directHits = hitIndex.directHits(particleId); +auto subgraphHits = hitIndex.subgraphHits(particleId); +``` + +The direct hits are attached only to the particle that directly produced them. + +The subgraph hits are accumulated from the particle and all its descendants in the logical graph. + +This makes merging two particles or two subgraphs straightforward: one can merge the corresponding hit spans by DetId or by RecHit index, depending on the intended matching metric. + +## `TruthLogicalGraphHitIndexProducer` + +`TruthLogicalGraphHitIndexProducer` builds `truth::LogicalGraphHitIndex`. + +It consumes: + +* the logical `truth::Graph`; +* the raw `TruthGraph`; +* selected `PCaloHit` collections; +* an optional DetId to RecHit index map produced by `SimHitToRecHitMapProducer`. + +Typical configuration: + +```python +process.truthLogicalGraphHitIndexProducer = cms.EDProducer( + "TruthLogicalGraphHitIndexProducer", + + src = cms.InputTag("truthLogicalGraphProducer"), + rawSrc = cms.InputTag("truthGraphProducer"), + + recHitMap = cms.InputTag("simHitToRecHitMapProducer"), + + simHitCollections = cms.VInputTag( + cms.InputTag("g4SimHits", "HGCHitsEE", "SIM"), + cms.InputTag("g4SimHits", "HGCHitsHEfront", "SIM"), + cms.InputTag("g4SimHits", "HGCHitsHEback", "SIM"), + cms.InputTag("g4SimHits", "EcalHitsEB", "SIM"), + cms.InputTag("g4SimHits", "HcalHits", "SIM"), + ), + + doHGCalRelabelling = cms.bool(False), +) +``` + +The producer performs the following steps: + +1. Build a `SimTrack::trackId()` to logical particle id map. +2. Read the configured `PCaloHit` collections. +3. Convert SimHit DetIds to reco DetIds when requested. +4. Look up the corresponding RecHit index through the DetId map, if available. +5. Fill direct hits for the logical particle associated to the Geant track id. +6. Propagate and merge hit collections upward to build subgraph hit collections. + +The hit index is intentionally separate from `truth::Graph`. This keeps the graph compact and allows detector-specific hit indices to evolve independently. + +## `SimHitToRecHitMapProducer` + +`SimHitToRecHitMapProducer` builds the DetId to RecHit index lookup consumed by `TruthLogicalGraphHitIndexProducer`. + +The produced type is: + +```cpp +hgcal::DetIdRecHitMap +``` + +with the current alias defined in: + +```cpp +SimCalorimetry/HGCalAssociatorProducers/interface/DetIdRecHitMap.h +``` + +Conceptually it is: + +```cpp +std::unordered_map +``` + +mapping: + +```text +reco DetId rawId -> global RecHit index +``` + +The global RecHit index is built by concatenating the configured RecHit collections in a deterministic order: + +1. all configured `HGCRecHitCollection` inputs; +2. all configured `reco::PFRecHitCollection` inputs. + +Typical configuration for RECO step3 output: + +```python +process.simHitToRecHitMapProducer = cms.EDProducer( + "SimHitToRecHitMapProducer", + + hgcalRecHits = cms.VInputTag( + cms.InputTag("HGCalRecHit", "HGCEERecHits", "RECO"), + cms.InputTag("HGCalRecHit", "HGCHEFRecHits", "RECO"), + cms.InputTag("HGCalRecHit", "HGCHEBRecHits", "RECO"), + ), + + pfRecHits = cms.VInputTag( + cms.InputTag("particleFlowRecHitECAL", "Cleaned", "RECO"), + cms.InputTag("particleFlowRecHitHBHE", "Cleaned", "RECO"), + cms.InputTag("particleFlowRecHitHF", "Cleaned", "RECO"), + cms.InputTag("particleFlowRecHitHO", "Cleaned", "RECO"), + ), +) +``` + +Do not include both `HGCalRecHit` and `particleFlowRecHitHGC` unless the intended indexing and double-counting policy is explicit. In the current workflow, HGCAL RecHits are taken from `HGCalRecHit`, while barrel and forward PF RecHits are taken from the cleaned `particleFlowRecHit*` collections. + +The map needs a ROOT dictionary because it is an EDM product, even if it is not written to the output file. + ## Graph navigation examples ### Iterate over particles @@ -456,9 +683,30 @@ for (auto particle : graph.particleViews()) { } ``` +### Access direct and subgraph hits + +```cpp +auto const& hitIndex = event.get(hitIndexToken_); + +for (uint32_t particleId = 0; particleId < hitIndex.nParticles(); ++particleId) { + auto directHits = hitIndex.directHits(particleId); + auto subgraphHits = hitIndex.subgraphHits(particleId); + + float directEnergy = 0.f; + for (auto const& hit : directHits) { + directEnergy += hit.energy; + } + + float subgraphEnergy = 0.f; + for (auto const& hit : subgraphHits) { + subgraphEnergy += hit.energy; + } +} +``` + ## Dumping and visualization -Two dumper modules are provided. +Two graph dumper modules are provided. ### Raw graph dumper @@ -475,10 +723,18 @@ truthgraph.dot The dumper can be configured with: ```python -src = cms.InputTag("truthGraphProducer") -dotFile = cms.string("truthgraph.dot") -maxNodes = cms.uint32(5000) -maxEdgesPerNode = cms.uint32(200) +process.truthGraphDumper = cms.EDAnalyzer( + "TruthGraphDumper", + src = cms.InputTag("truthGraphProducer"), + dotFile = cms.string("truthgraph.dot"), + maxNodes = cms.uint32(5000), + maxEdgesPerNode = cms.uint32(200), + + simTracks = cms.InputTag("g4SimHits"), + simVertices = cms.InputTag("g4SimHits"), + genEventHepMC = cms.InputTag("generatorSmeared"), + genEventHepMC3 = cms.InputTag("generatorSmeared"), +) ``` ### Logical graph dumper @@ -491,40 +747,118 @@ Default output: truthlogicalgraph.dot ``` -The dumper can also use the raw graph to enrich labels with raw node information: +The dumper can also use: + +* the raw `TruthGraph`, to enrich labels with raw node information; +* the `LogicalGraphHitIndex`, to annotate particles with direct and subgraph SimHit summaries; +* the RecHit collections, to compute RecHit energy summaries from `recHitIndex`. + +Example: ```python -truthLogicalGraphDumper = cms.EDAnalyzer( +process.truthLogicalGraphDumper = cms.EDAnalyzer( "TruthLogicalGraphDumper", src = cms.InputTag("truthLogicalGraphProducer"), rawSrc = cms.InputTag("truthGraphProducer"), + hitIndex = cms.InputTag("truthLogicalGraphHitIndexProducer"), + + hgcalRecHits = cms.VInputTag( + cms.InputTag("HGCalRecHit", "HGCEERecHits", "RECO"), + cms.InputTag("HGCalRecHit", "HGCHEFRecHits", "RECO"), + cms.InputTag("HGCalRecHit", "HGCHEBRecHits", "RECO"), + ), + + pfRecHits = cms.VInputTag( + cms.InputTag("particleFlowRecHitECAL", "Cleaned", "RECO"), + cms.InputTag("particleFlowRecHitHBHE", "Cleaned", "RECO"), + cms.InputTag("particleFlowRecHitHF", "Cleaned", "RECO"), + cms.InputTag("particleFlowRecHitHO", "Cleaned", "RECO"), + ), + dotFile = cms.string("truthlogicalgraph.dot"), + maxParticles = cms.uint32(5000), maxVertices = cms.uint32(5000), maxEdgesPerNode = cms.uint32(200), + + hideLargeSimSourceVertices = cms.bool(True), + largeSimSourceVertexMinOutgoing = cms.uint32(50), ) ``` +Particle labels include summaries such as: + +```text +direct simHits: N simE=... +direct recHits: N missing=... recoE=... +subgraph simHits: N simE=... +subgraph recHits: N missing=... recoE=... +``` + DOT files can be converted with Graphviz, for example: ```bash -dot -Tpdf truthlogicalgraph.dot -o truthlogicalgraph.pdf +dot -Tsvg truthlogicalgraph_run1_lumi1_event1.dot -o truthlogicalgraph_run1_lumi1_event1.svg ``` -or: +To convert all DOT files in a directory: ```bash -dot -Tpng truthlogicalgraph.dot -o truthlogicalgraph.png +for f in *.dot; do dot -Tsvg "$f" -o "${f%.dot}.svg"; done ``` -## Example configuration +To inspect whether hit information is present in the DOT output: + +```bash +grep -n "simHits\|recHits\|simE\|recoE" truthlogicalgraph_run1_lumi1_event1.dot | head -80 +``` -A minimal test path can be configured as follows: +## Example configuration on step3.root + +A typical standalone test configuration running on a `step3.root` file is: ```python import FWCore.ParameterSet.Config as cms -process.load("PhysicsTools.TruthInfo.truthGraphProducer_cfi") +process = cms.Process("TRUTHGRAPH") + +process.load("FWCore.MessageService.MessageLogger_cfi") + +process.load("Configuration.Geometry.GeometryExtendedRun4D120Reco_cff") + +process.maxEvents = cms.untracked.PSet( + input = cms.untracked.int32(-1) +) + +process.source = cms.Source( + "PoolSource", + fileNames = cms.untracked.vstring("file:step3.root") +) + +process.options = cms.untracked.PSet( + wantSummary = cms.untracked.bool(True) +) + +process.truthGraphProducer = cms.EDProducer( + "TruthGraphProducer", + genEventHepMC3 = cms.InputTag("generatorSmeared"), + genEventHepMC = cms.InputTag("generatorSmeared"), + simTracks = cms.InputTag("g4SimHits"), + simVertices = cms.InputTag("g4SimHits"), + addGenToSimEdges = cms.bool(True), +) + +process.truthGraphDumper = cms.EDAnalyzer( + "TruthGraphDumper", + src = cms.InputTag("truthGraphProducer"), + dotFile = cms.string("truthgraph.dot"), + maxNodes = cms.uint32(20000), + maxEdgesPerNode = cms.uint32(50), + simTracks = cms.InputTag("g4SimHits"), + simVertices = cms.InputTag("g4SimHits"), + genEventHepMC = cms.InputTag("generatorSmeared"), + genEventHepMC3 = cms.InputTag("generatorSmeared"), +) process.truthLogicalGraphProducer = cms.EDProducer( "TruthLogicalGraphProducer", @@ -533,36 +867,138 @@ process.truthLogicalGraphProducer = cms.EDProducer( simVertices = cms.InputTag("g4SimHits"), genEventHepMC3 = cms.InputTag("generatorSmeared"), genEventHepMC = cms.InputTag("generatorSmeared"), + + motherPdgId = cms.int32(0), + mergeGenSimVertices = cms.bool(True), + collapseIntermediateGenParticles = cms.bool(True), +) + +process.simHitToRecHitMapProducer = cms.EDProducer( + "SimHitToRecHitMapProducer", + + hgcalRecHits = cms.VInputTag( + cms.InputTag("HGCalRecHit", "HGCEERecHits", "RECO"), + cms.InputTag("HGCalRecHit", "HGCHEFRecHits", "RECO"), + cms.InputTag("HGCalRecHit", "HGCHEBRecHits", "RECO"), + ), + + pfRecHits = cms.VInputTag( + cms.InputTag("particleFlowRecHitECAL", "Cleaned", "RECO"), + cms.InputTag("particleFlowRecHitHBHE", "Cleaned", "RECO"), + cms.InputTag("particleFlowRecHitHF", "Cleaned", "RECO"), + cms.InputTag("particleFlowRecHitHO", "Cleaned", "RECO"), + ), +) + +process.truthLogicalGraphHitIndexProducer = cms.EDProducer( + "TruthLogicalGraphHitIndexProducer", + + src = cms.InputTag("truthLogicalGraphProducer"), + rawSrc = cms.InputTag("truthGraphProducer"), + + recHitMap = cms.InputTag("simHitToRecHitMapProducer"), + + simHitCollections = cms.VInputTag( + cms.InputTag("g4SimHits", "HGCHitsEE", "SIM"), + cms.InputTag("g4SimHits", "HGCHitsHEfront", "SIM"), + cms.InputTag("g4SimHits", "HGCHitsHEback", "SIM"), + cms.InputTag("g4SimHits", "EcalHitsEB", "SIM"), + cms.InputTag("g4SimHits", "HcalHits", "SIM"), + ), + + doHGCalRelabelling = cms.bool(False), ) process.truthLogicalGraphDumper = cms.EDAnalyzer( "TruthLogicalGraphDumper", src = cms.InputTag("truthLogicalGraphProducer"), rawSrc = cms.InputTag("truthGraphProducer"), + hitIndex = cms.InputTag("truthLogicalGraphHitIndexProducer"), + + hgcalRecHits = cms.VInputTag( + cms.InputTag("HGCalRecHit", "HGCEERecHits", "RECO"), + cms.InputTag("HGCalRecHit", "HGCHEFRecHits", "RECO"), + cms.InputTag("HGCalRecHit", "HGCHEBRecHits", "RECO"), + ), + + pfRecHits = cms.VInputTag( + cms.InputTag("particleFlowRecHitECAL", "Cleaned", "RECO"), + cms.InputTag("particleFlowRecHitHBHE", "Cleaned", "RECO"), + cms.InputTag("particleFlowRecHitHF", "Cleaned", "RECO"), + cms.InputTag("particleFlowRecHitHO", "Cleaned", "RECO"), + ), + dotFile = cms.string("truthlogicalgraph.dot"), - maxParticles = cms.uint32(5000), - maxVertices = cms.uint32(5000), - maxEdgesPerNode = cms.uint32(200), + + maxParticles = cms.uint32(20000), + maxVertices = cms.uint32(20000), + maxEdgesPerNode = cms.uint32(300), ) -process.p = cms.Path( +process.MessageLogger.cerr.threshold = "INFO" +process.MessageLogger.cerr.default = cms.untracked.PSet( + limit = cms.untracked.int32(0) +) +process.MessageLogger.cerr.TruthGraphProducer = cms.untracked.PSet( + limit = cms.untracked.int32(-1) +) +process.MessageLogger.cerr.TruthLogicalGraphProducer = cms.untracked.PSet( + limit = cms.untracked.int32(-1) +) +process.MessageLogger.cerr.TruthLogicalGraphHitIndexProducer = cms.untracked.PSet( + limit = cms.untracked.int32(-1) +) +process.MessageLogger.cerr.SimHitToRecHitMapProducer = cms.untracked.PSet( + limit = cms.untracked.int32(-1) +) + +process.truthGraph_step = cms.Path( process.truthGraphProducer + + process.truthGraphDumper + process.truthLogicalGraphProducer + + process.simHitToRecHitMapProducer + + process.truthLogicalGraphHitIndexProducer + process.truthLogicalGraphDumper ) ``` -The raw graph producer can be customized explicitly if needed: +## Event content checks -```python -process.truthGraphProducer = cms.EDProducer( - "TruthGraphProducer", - genEventHepMC3 = cms.InputTag("generatorSmeared"), - genEventHepMC = cms.InputTag("generatorSmeared"), - simTracks = cms.InputTag("g4SimHits"), - simVertices = cms.InputTag("g4SimHits"), - addGenToSimEdges = cms.bool(True), -) +Useful commands to inspect available products are: + +```bash +edmDumpEventContent step3.root | grep -E 'PFRecHit|particleFlowRecHit' +``` + +and: + +```bash +edmDumpEventContent step3.root | grep -E 'HGCRecHit|HGCalRecHit|HGCEERecHits|HGCHEFRecHits|HGCHEBRecHits' +``` + +For a typical Phase-2 RECO file, the useful collections are: + +```text +vector "HGCalRecHit" "HGCEERecHits" "RECO" +vector "HGCalRecHit" "HGCHEFRecHits" "RECO" +vector "HGCalRecHit" "HGCHEBRecHits" "RECO" + +vector "particleFlowRecHitECAL" "Cleaned" "RECO" +vector "particleFlowRecHitHBHE" "Cleaned" "RECO" +vector "particleFlowRecHitHF" "Cleaned" "RECO" +vector "particleFlowRecHitHO" "Cleaned" "RECO" +``` + +The relevant SimHit collections include: + +```text +vector "g4SimHits" "HGCHitsEE" "SIM" +vector "g4SimHits" "HGCHitsHEfront" "SIM" +vector "g4SimHits" "HGCHitsHEback" "SIM" +vector "g4SimHits" "EcalHitsEB" "SIM" +vector "g4SimHits" "HcalHits" "SIM" +vector "g4SimHits" "" "SIM" +vector "g4SimHits" "" "SIM" ``` ## Intended matching strategy @@ -573,13 +1009,14 @@ In this model: * the graph provides the particle and vertex structure; * detector-level truth association is performed through hits; -* reconstructed objects can be associated either to a single `truth::Particle` or to a larger `truth::Branch`; +* reconstructed objects can be associated either to a single `truth::Particle` or to a larger truth branch; * truth information can be aggregated over a coherent branch when a reconstructed object corresponds to a composite truth structure. Different reconstruction domains need different matching metrics: * tracking association is usually based on shared hits, not on hit energy; * calorimeter clustering association can use energy fractions or energy-weighted metrics; +* timing objects may need time-aware matching; * other reconstruction objects may require detector-specific metrics. The graph should therefore act as the common truth substrate, while matching definitions remain detector-aware and use-case dependent. @@ -596,15 +1033,24 @@ Possible branch-level operations include: * collect all detector hits attached to particles in the branch; * compute detector-specific matching scores; * compare two branches; -* define stable references for composite truth structures. +* define stable references for composite truth structures; +* merge two selected truth structures and their hit content. In this picture, `truth::Branch` avoids forcing an early collapse of the truth history into fixed reference objects. ## Hits attached to particles -A possible long-term direction is to attach detector hits of different types directly to `truth::Particle`, or to provide hit collections indexed by `truth::Particle` ids. +The current prototype already implements the first version of hit attachment through `truth::LogicalGraphHitIndex`. + +This keeps hit information outside the main logical graph data product, while allowing the graph dumper and future associators to query: -This would make the graph the central truth data structure. Detector-specific truth objects such as `TrackingParticle`, `SimCluster`, and `CaloParticle` could then be expressed as views or derived abstractions on top of the same graph. +* direct SimHits from a particle; +* subgraph SimHits from a particle and all descendants; +* matched RecHit indices; +* SimHit energy sums; +* RecHit energy sums. + +A possible long-term direction is to generalize this further so that detector-specific truth objects such as `TrackingParticle`, `SimCluster`, and `CaloParticle` can be expressed as views or derived abstractions on top of the same graph and hit-index infrastructure. This would make it possible to: @@ -620,13 +1066,17 @@ The current prototype can: * build a raw `TruthGraph` from generator and simulation products; * build a logical `truth::Graph` from the raw graph; * merge matched generator and simulation particles; -* keep generator and simulation vertices separate for debugging; +* optionally merge generator and simulation vertices; +* optionally collapse intermediate GEN-only particle copies; * navigate particle and vertex relations; * compute parents, children, ancestors, descendants, roots, and leaves; * find ancestors with a given PDG id; * find a common ancestor between two particles; * store boundary-crossing checkpoints; -* dump raw and logical graphs to DOT for debugging. +* build a direct and subgraph calorimeter SimHit index per logical particle; +* map SimHits to RecHit indices through DetId when a RecHit map is available; +* dump raw and logical graphs to DOT for debugging; +* annotate logical graph DOT nodes with SimHit and RecHit energy summaries. ## Known limitations @@ -634,10 +1084,12 @@ The current implementation is a prototype and several aspects are intentionally Known limitations include: -* vertex merging is not yet attempted; -* only a limited set of GEN-SIM associations is currently used; -* checkpoint information is limited to boundary-crossing information from `SimTrack`; -* hit-to-particle association is not yet implemented; +* GEN-SIM association still needs broader validation; +* the semantics of vertex merging require further study; +* checkpoint information is currently limited to boundary-crossing information from `SimTrack`; +* the hit index currently targets calorimeter `PCaloHit` inputs; +* RecHit association is currently DetId-based and does not encode more detailed detector response information; +* duplicate DetId handling depends on the configured RecHit input ordering and map policy; * `truth::Branch` is part of the target design but not yet implemented; * the logical API is still evolving; * the raw graph construction needs further validation on realistic events and pileup scenarios. @@ -649,11 +1101,11 @@ Planned or natural next steps are: 1. Continue debugging the raw truth graph construction. 2. Validate HepMC2 and HepMC3 behaviour on representative workflows. 3. Refine GEN-SIM particle association. -4. Study whether, when, and how generator and simulation vertices should be merged. +4. Study the vertex merging policy in realistic events. 5. Extend the checkpoint model beyond boundary crossings. 6. Investigate direct graph construction during Geant4 simulation. -7. Define hit-to-`truth::Particle` associators. -8. Prototype detector-specific matching metrics on top of the graph. +7. Extend hit indexing beyond calorimeter `PCaloHit` where appropriate. +8. Prototype detector-specific truth-reco matching metrics on top of the hit index. 9. Implement a `truth::Branch` abstraction. 10. Study how existing detector-specific truth containers could be represented as views over the graph. 11. Stabilize the logical API for downstream reconstruction and validation code. @@ -670,6 +1122,10 @@ particle.children(); particle.ancestors(); particle.firstCommonAncestor(other); particle.hasAncestorPdgId(23); +hitIndex.directHits(particle.id()); +hitIndex.subgraphHits(particle.id()); ``` -rather than reimplementing event-history navigation separately for each reconstruction or validation use case. +rather than reimplementing event-history navigation and hit aggregation separately for each reconstruction or validation use case. + + From d273fe1cce754f5f73f894ca84e283eeb9c7ee1a Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Wed, 13 May 2026 10:21:52 +0200 Subject: [PATCH 26/53] adding rechits --- .../HGCalAssociatorProducers/interface/DetIdRecHitMap.h | 2 +- .../plugins/SimHitToRecHitMapProducer.cc | 2 +- SimCalorimetry/HGCalAssociatorProducers/src/classes.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/SimCalorimetry/HGCalAssociatorProducers/interface/DetIdRecHitMap.h b/SimCalorimetry/HGCalAssociatorProducers/interface/DetIdRecHitMap.h index 3b6a982259669..fdb2cf7b044ba 100644 --- a/SimCalorimetry/HGCalAssociatorProducers/interface/DetIdRecHitMap.h +++ b/SimCalorimetry/HGCalAssociatorProducers/interface/DetIdRecHitMap.h @@ -20,4 +20,4 @@ namespace hgcal { } // namespace hgcal -#endif \ No newline at end of file +#endif diff --git a/SimCalorimetry/HGCalAssociatorProducers/plugins/SimHitToRecHitMapProducer.cc b/SimCalorimetry/HGCalAssociatorProducers/plugins/SimHitToRecHitMapProducer.cc index 485a0b5737e11..306446c3aa36a 100644 --- a/SimCalorimetry/HGCalAssociatorProducers/plugins/SimHitToRecHitMapProducer.cc +++ b/SimCalorimetry/HGCalAssociatorProducers/plugins/SimHitToRecHitMapProducer.cc @@ -121,4 +121,4 @@ void SimHitToRecHitMapProducer::fillDescriptions(edm::ConfigurationDescriptions& descriptions.addWithDefaultLabel(desc); } -DEFINE_FWK_MODULE(SimHitToRecHitMapProducer); \ No newline at end of file +DEFINE_FWK_MODULE(SimHitToRecHitMapProducer); diff --git a/SimCalorimetry/HGCalAssociatorProducers/src/classes.h b/SimCalorimetry/HGCalAssociatorProducers/src/classes.h index 2833ae679b90c..6ce2489b35235 100644 --- a/SimCalorimetry/HGCalAssociatorProducers/src/classes.h +++ b/SimCalorimetry/HGCalAssociatorProducers/src/classes.h @@ -12,4 +12,4 @@ namespace SimCalorimetry_HGCalAssociatorProducers { }; } // namespace SimCalorimetry_HGCalAssociatorProducers -#endif \ No newline at end of file +#endif From 94c2fd59dec6f6ece17c3b350e66638051222ab0 Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Wed, 13 May 2026 10:22:25 +0200 Subject: [PATCH 27/53] adding possibility to hide truth particles with no simhits --- .../plugins/TruthLogicalGraphDumper.cc | 118 +++++++++++++- .../test/dumpTruthGraphsFromGENSIMRECO_cfg.py | 149 ++++++++++++++++++ 2 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py diff --git a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc index 1de365b7a2a50..5e00c7a450268 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc @@ -238,6 +238,67 @@ namespace { return summary; } + template + void forEachDescendantParticle(truth::Graph const& g, uint32_t particleId, F&& f) { + if (particleId >= g.nParticles()) + return; + + for (const uint32_t vertexId : g.decayVertices(particleId)) { + if (vertexId >= g.nVertices()) + continue; + + for (const uint32_t childId : g.outgoingParticles(vertexId)) { + if (childId >= g.nParticles()) + continue; + + f(childId); + forEachDescendantParticle(g, childId, f); + } + } + } + + bool hasVisibleIncomingParticle(truth::Graph const& g, uint32_t vertexId, std::vector const& hideParticle) { + for (const uint32_t p : g.incomingParticles(vertexId)) { + if (p < hideParticle.size() && !hideParticle[p]) + return true; + } + + return false; + } + + bool hasVisibleOutgoingParticle(truth::Graph const& g, uint32_t vertexId, std::vector const& hideParticle) { + for (const uint32_t p : g.outgoingParticles(vertexId)) { + if (p < hideParticle.size() && !hideParticle[p]) + return true; + } + + return false; + } + + bool shouldHideVertexAfterParticleFiltering(truth::Graph const& g, + uint32_t vertexId, + std::vector const& hideParticle) { + const bool hasIncoming = !g.incomingParticles(vertexId).empty(); + const bool hasOutgoing = !g.outgoingParticles(vertexId).empty(); + + const bool hasVisibleIncoming = hasVisibleIncomingParticle(g, vertexId, hideParticle); + const bool hasVisibleOutgoing = hasVisibleOutgoingParticle(g, vertexId, hideParticle); + + // Hide vertices fully disconnected by the particle filter. + if (!hasVisibleIncoming && !hasVisibleOutgoing) + return true; + + // Hide decay vertices that no longer have visible daughters. + if (hasOutgoing && !hasVisibleOutgoing) + return true; + + // Hide production/source vertices that no longer have visible outgoing particles. + if (!hasIncoming && hasOutgoing && !hasVisibleOutgoing) + return true; + + return false; + } + std::string appendEventIdToFilename(std::string const& filename, edm::EventID const& id) { const auto dotPos = filename.rfind('.'); @@ -275,7 +336,8 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { maxVertices_(cfg.getParameter("maxVertices")), maxEdgesPerNode_(cfg.getParameter("maxEdgesPerNode")), hideLargeSimSourceVertices_(cfg.getParameter("hideLargeSimSourceVertices")), - largeSimSourceVertexMinOutgoing_(cfg.getParameter("largeSimSourceVertexMinOutgoing")) { + largeSimSourceVertexMinOutgoing_(cfg.getParameter("largeSimSourceVertexMinOutgoing")), + hideZeroSimHitSubgraphs_(cfg.getParameter("hideZeroSimHitSubgraphs")) { const auto hgcalRecHitTags = cfg.getParameter>("hgcalRecHits"); hgcalRecHitTags_.reserve(hgcalRecHitTags.size()); hgcalRecHitTokens_.reserve(hgcalRecHitTags.size()); @@ -330,6 +392,11 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { desc.add("largeSimSourceVertexMinOutgoing", 50) ->setComment("Minimum outgoing multiplicity for hiding a SIM-only source vertex"); + desc.add("hideZeroSimHitSubgraphs", false) + ->setComment( + "If true, hide every SIM-backed particle whose subgraph has zero SimHits, together with its descendant " + "subgraph. Requires hitIndex to be configured."); + descriptions.addWithDefaultLabel(desc); } @@ -375,10 +442,52 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { } } + std::vector hideParticle(nParticles, 0); + + if (hideZeroSimHitSubgraphs_) { + if (hitIndex == nullptr) { + edm::LogWarning("TruthLogicalGraphDumper") + << "hideZeroSimHitSubgraphs is enabled, but no valid LogicalGraphHitIndex was provided. " + << "No zero-hit subgraphs will be hidden."; + } else { + for (uint32_t i = 0; i < nParticles; ++i) { + auto const& d = g.particle(i).data(); + + if (!d.hasSim()) + continue; + + if (i >= hitIndex->nParticles()) + continue; + + if (!hitIndex->subgraphHits(i).empty()) + continue; + + hideParticle[i] = 1; + + forEachDescendantParticle(g, i, [&](uint32_t childId) { + if (childId < hideParticle.size()) + hideParticle[childId] = 1; + }); + } + } + } + + for (uint32_t i = 0; i < nVertices; ++i) { + if (hideVertex[i]) + continue; + + if (shouldHideVertexAfterParticleFiltering(g, i, hideParticle)) { + hideVertex[i] = 1; + } + } + // ------------------------------------------------------------------ // Particle nodes // ------------------------------------------------------------------ for (uint32_t i = 0; i < nParticles; ++i) { + if (hideParticle[i]) + continue; + auto p = g.particle(i); auto const& d = p.data(); @@ -550,6 +659,9 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { // Edges: physics-forward only // ------------------------------------------------------------------ for (uint32_t i = 0; i < nParticles; ++i) { + if (hideParticle[i]) + continue; + unsigned kept = 0; for (uint32_t v : g.decayVertices(i)) { @@ -576,6 +688,9 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { if (p >= nParticles) continue; + if (hideParticle[p]) + continue; + os << " v" << i << " -> p" << p << ";\n"; if (++kept >= maxEdgesPerNode_) @@ -646,6 +761,7 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { unsigned maxEdgesPerNode_; bool hideLargeSimSourceVertices_; unsigned largeSimSourceVertexMinOutgoing_; + bool hideZeroSimHitSubgraphs_; }; DEFINE_FWK_MODULE(TruthLogicalGraphDumper); diff --git a/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py b/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py new file mode 100644 index 0000000000000..d8b50dce923c7 --- /dev/null +++ b/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py @@ -0,0 +1,149 @@ +import FWCore.ParameterSet.Config as cms + +process = cms.Process("TRUTHGRAPH") + +process.load("FWCore.MessageService.MessageLogger_cfi") + +# Needed if TruthLogicalGraphHitIndexProducer does HGCal simId -> reco DetId relabelling. +# Keep this consistent with the geometry used to produce step3.root. +process.load("Configuration.Geometry.GeometryExtendedRun4D120Reco_cff") + +process.maxEvents = cms.untracked.PSet( + input=cms.untracked.int32(-1) +) + +process.source = cms.Source( + "PoolSource", + fileNames=cms.untracked.vstring( + "file:step3.root" + ) +) + +process.options = cms.untracked.PSet( + wantSummary=cms.untracked.bool(True) +) + +process.truthGraphProducer = cms.EDProducer( + "TruthGraphProducer", + genEventHepMC3=cms.InputTag("generatorSmeared"), + genEventHepMC=cms.InputTag("generatorSmeared"), + simTracks=cms.InputTag("g4SimHits"), + simVertices=cms.InputTag("g4SimHits"), + addGenToSimEdges=cms.bool(True), +) + +process.truthGraphDumper = cms.EDAnalyzer( + "TruthGraphDumper", + src=cms.InputTag("truthGraphProducer"), + dotFile=cms.string("truthgraph.dot"), + maxNodes=cms.uint32(20000), + maxEdgesPerNode=cms.uint32(50), + simTracks=cms.InputTag("g4SimHits"), + simVertices=cms.InputTag("g4SimHits"), + genEventHepMC=cms.InputTag("generatorSmeared"), + genEventHepMC3=cms.InputTag("generatorSmeared"), +) + +process.truthLogicalGraphProducer = cms.EDProducer( + "TruthLogicalGraphProducer", + src=cms.InputTag("truthGraphProducer"), + simTracks=cms.InputTag("g4SimHits"), + simVertices=cms.InputTag("g4SimHits"), + genEventHepMC3=cms.InputTag("generatorSmeared"), + genEventHepMC=cms.InputTag("generatorSmeared"), + motherPdgId=cms.int32(0), + mergeGenSimVertices=cms.bool(True), + collapseIntermediateGenParticles=cms.bool(True), +) + +process.simHitToRecHitMapProducer = cms.EDProducer( + "SimHitToRecHitMapProducer", + + hgcalRecHits=cms.VInputTag( + cms.InputTag("HGCalRecHit", "HGCEERecHits", "RECO"), + cms.InputTag("HGCalRecHit", "HGCHEFRecHits", "RECO"), + cms.InputTag("HGCalRecHit", "HGCHEBRecHits", "RECO"), + ), + + pfRecHits=cms.VInputTag( + cms.InputTag("particleFlowRecHitECAL", "Cleaned", "RECO"), + cms.InputTag("particleFlowRecHitHBHE", "Cleaned", "RECO"), + cms.InputTag("particleFlowRecHitHF", "Cleaned", "RECO"), + cms.InputTag("particleFlowRecHitHO", "Cleaned", "RECO"), + ), +) +process.truthLogicalGraphHitIndexProducer = cms.EDProducer( + "TruthLogicalGraphHitIndexProducer", + + src=cms.InputTag("truthLogicalGraphProducer"), + rawSrc=cms.InputTag("truthGraphProducer"), + + recHitMap=cms.InputTag("simHitToRecHitMapProducer"), + + simHitCollections=cms.VInputTag( + cms.InputTag("g4SimHits", "HGCHitsEE", "SIM"), + cms.InputTag("g4SimHits", "HGCHitsHEfront", "SIM"), + cms.InputTag("g4SimHits", "HGCHitsHEback", "SIM"), + cms.InputTag("g4SimHits", "EcalHitsEB", "SIM"), + cms.InputTag("g4SimHits", "HcalHits", "SIM"), + ), + + doHGCalRelabelling=cms.bool(False), +) + +process.truthLogicalGraphDumper = cms.EDAnalyzer( + "TruthLogicalGraphDumper", + src=cms.InputTag("truthLogicalGraphProducer"), + rawSrc=cms.InputTag("truthGraphProducer"), + hitIndex=cms.InputTag("truthLogicalGraphHitIndexProducer"), + + hgcalRecHits=cms.VInputTag( + cms.InputTag("HGCalRecHit", "HGCEERecHits", "RECO"), + cms.InputTag("HGCalRecHit", "HGCHEFRecHits", "RECO"), + cms.InputTag("HGCalRecHit", "HGCHEBRecHits", "RECO"), + ), + + pfRecHits=cms.VInputTag( + cms.InputTag("particleFlowRecHitECAL", "Cleaned", "RECO"), + cms.InputTag("particleFlowRecHitHBHE", "Cleaned", "RECO"), + cms.InputTag("particleFlowRecHitHF", "Cleaned", "RECO"), + cms.InputTag("particleFlowRecHitHO", "Cleaned", "RECO"), + ), + + dotFile=cms.string("truthlogicalgraph.dot"), + + maxParticles=cms.uint32(20000), + maxVertices=cms.uint32(20000), + maxEdgesPerNode=cms.uint32(300), + + hideLargeSimSourceVertices=cms.bool(True), + largeSimSourceVertexMinOutgoing=cms.uint32(50), + + hideZeroSimHitSubgraphs=cms.bool(True), +) + +process.MessageLogger.cerr.threshold = "INFO" +process.MessageLogger.cerr.default = cms.untracked.PSet( + limit=cms.untracked.int32(0) +) +process.MessageLogger.cerr.TruthGraphProducer = cms.untracked.PSet( + limit=cms.untracked.int32(-1) +) +process.MessageLogger.cerr.TruthLogicalGraphProducer = cms.untracked.PSet( + limit=cms.untracked.int32(-1) +) +process.MessageLogger.cerr.TruthLogicalGraphHitIndexProducer = cms.untracked.PSet( + limit=cms.untracked.int32(-1) +) +process.MessageLogger.cerr.SimHitToRecHitMapProducer = cms.untracked.PSet( + limit=cms.untracked.int32(-1) +) + +process.truthGraph_step = cms.Path( + process.truthGraphProducer + + process.truthGraphDumper + + process.truthLogicalGraphProducer + + process.simHitToRecHitMapProducer + + process.truthLogicalGraphHitIndexProducer + + process.truthLogicalGraphDumper +) \ No newline at end of file From 52cb3dc6c317b3bb2ba605032959e61b88f78828 Mon Sep 17 00:00:00 2001 From: Izaak <13318254+IzaakWN@users.noreply.github.com> Date: Wed, 13 May 2026 13:33:27 +0200 Subject: [PATCH 28/53] add argparser --- .../test/dumpTruthGraphsFromGENSIMRECO_cfg.py | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py b/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py index d8b50dce923c7..737bda66b39d2 100644 --- a/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py +++ b/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py @@ -1,5 +1,25 @@ import FWCore.ParameterSet.Config as cms +# user options +import os +from argparse import ArgumentParser +parser = ArgumentParser() +parser.add_argument("inputFile", default="file:step3.root", + metavar='FILE', help="Input file, default=%(default)r" ) +parser.add_argument('-o', "--outdir", default='', + help="output directory, default=%(default)r" ) +parser.add_argument('-n', "--maxevts", type=int, default=-1, + help="maximum number of events to process, default=%(default)s" ) +parser.add_argument('-m', "--merge", dest='mergeGenSim', action='store_true', + help="merge GEN-SIM nodes of duplicates" ) +parser.add_argument('-c', "--collapse", action='store_true', help="collapse GenParticle copies" ) +parser.add_argument('-t', "--tag", default='', help="tag for out put file" ) +args = parser.parse_args() +if '/' not in args.inputFile: + args.inputFile = 'file:'+args.inputFile +if args.outdir and not os.path.exists(args.outdir): + os.makedirs(args.outdir, exist_ok=True) + process = cms.Process("TRUTHGRAPH") process.load("FWCore.MessageService.MessageLogger_cfi") @@ -9,13 +29,13 @@ process.load("Configuration.Geometry.GeometryExtendedRun4D120Reco_cff") process.maxEvents = cms.untracked.PSet( - input=cms.untracked.int32(-1) + input=cms.untracked.int32(args.maxevts) ) process.source = cms.Source( "PoolSource", fileNames=cms.untracked.vstring( - "file:step3.root" + args.inputFile #"file:step3.root" ) ) @@ -35,7 +55,7 @@ process.truthGraphDumper = cms.EDAnalyzer( "TruthGraphDumper", src=cms.InputTag("truthGraphProducer"), - dotFile=cms.string("truthgraph.dot"), + dotFile=cms.string(os.path.join(args.outdir,f"truthgraph{args.tag}.dot")), # output file maxNodes=cms.uint32(20000), maxEdgesPerNode=cms.uint32(50), simTracks=cms.InputTag("g4SimHits"), @@ -52,8 +72,8 @@ genEventHepMC3=cms.InputTag("generatorSmeared"), genEventHepMC=cms.InputTag("generatorSmeared"), motherPdgId=cms.int32(0), - mergeGenSimVertices=cms.bool(True), - collapseIntermediateGenParticles=cms.bool(True), + mergeGenSimVertices=cms.bool(args.mergeGenSim), + collapseIntermediateGenParticles=cms.bool(args.collapse), ) process.simHitToRecHitMapProducer = cms.EDProducer( @@ -110,7 +130,7 @@ cms.InputTag("particleFlowRecHitHO", "Cleaned", "RECO"), ), - dotFile=cms.string("truthlogicalgraph.dot"), + dotFile=cms.string(os.path.join(args.outdir,f"truthlogicalgraph{args.tag}.dot")), # output file maxParticles=cms.uint32(20000), maxVertices=cms.uint32(20000), @@ -146,4 +166,4 @@ + process.simHitToRecHitMapProducer + process.truthLogicalGraphHitIndexProducer + process.truthLogicalGraphDumper -) \ No newline at end of file +) From 7077f72c51d3302b44810b30694d0e1b5eef1367 Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Wed, 13 May 2026 15:59:38 +0200 Subject: [PATCH 29/53] Separate postprocessing code format --- PhysicsTools/TruthInfo/BuildFile.xml | 1 + .../TruthLogicalGraphPostProcessor.h | 51 + .../plugins/TruthLogicalGraphDumper.cc | 2 +- .../plugins/TruthLogicalGraphProducer.cc | 409 ++------ .../src/TruthLogicalGraphPostProcessor.cc | 908 ++++++++++++++++++ .../test/dumpTruthGraphsFromGENSIMRECO_cfg.py | 27 +- 6 files changed, 1061 insertions(+), 337 deletions(-) create mode 100644 PhysicsTools/TruthInfo/interface/TruthLogicalGraphPostProcessor.h create mode 100644 PhysicsTools/TruthInfo/src/TruthLogicalGraphPostProcessor.cc diff --git a/PhysicsTools/TruthInfo/BuildFile.xml b/PhysicsTools/TruthInfo/BuildFile.xml index d7421d27abc81..6726efdfce6e0 100644 --- a/PhysicsTools/TruthInfo/BuildFile.xml +++ b/PhysicsTools/TruthInfo/BuildFile.xml @@ -2,6 +2,7 @@ + diff --git a/PhysicsTools/TruthInfo/interface/TruthLogicalGraphPostProcessor.h b/PhysicsTools/TruthInfo/interface/TruthLogicalGraphPostProcessor.h new file mode 100644 index 0000000000000..601d60b245256 --- /dev/null +++ b/PhysicsTools/TruthInfo/interface/TruthLogicalGraphPostProcessor.h @@ -0,0 +1,51 @@ +#ifndef PhysicsTools_TruthInfo_interface_TruthLogicalGraphPostProcessor_h +#define PhysicsTools_TruthInfo_interface_TruthLogicalGraphPostProcessor_h + +#include +#include + +#include "FWCore/ParameterSet/interface/ParameterSet.h" +#include "FWCore/ParameterSet/interface/ParameterSetDescription.h" + +#include "PhysicsTools/TruthInfo/interface/Graph.h" + +namespace truth { + + struct LogicalGraphPostProcessingConfig { + bool collapseIntermediateGenParticles = true; + + // If empty, no seed-based graph cut is applied. + std::vector seedPdgIds; + + // For each selected seed particle, keep this many generations of parents + // above the seed before keeping the full downstream graph. + uint32_t seedParentDepth = 0; + + // Particles with these exact PDG ids are removed from the final logical graph. + // If such a particle is internal, its production and decay vertices are merged + // so that the graph remains navigable. + std::vector ignoredPdgIds; + + // Exact logical particle ids to remove from the final logical graph. + // These ids refer to the graph state at the moment the ignored-particle + // collapsing step is applied. + std::vector ignoredParticleIds; + }; + + class TruthLogicalGraphPostProcessor { + public: + TruthLogicalGraphPostProcessor() = default; + explicit TruthLogicalGraphPostProcessor(LogicalGraphPostProcessingConfig config); + + static edm::ParameterSetDescription psetDescription(); + static LogicalGraphPostProcessingConfig configFromPSet(edm::ParameterSet const& pset); + + [[nodiscard]] Graph process(Graph input) const; + + private: + LogicalGraphPostProcessingConfig config_; + }; + +} // namespace truth + +#endif diff --git a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc index 5e00c7a450268..b69cb962b1b5f 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc @@ -722,10 +722,10 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { energies.push_back(hit.energy()); } } - for (uint32_t i = 0; i < pfRecHitTokens_.size(); ++i) { edm::Handle handle; evt.getByToken(pfRecHitTokens_[i], handle); + std::cout << pfRecHitTags_[i].label() << " size() " << handle->size() << std::endl; if (!handle.isValid()) { edm::LogWarning("TruthLogicalGraphDumper") << "Missing reco::PFRecHitCollection " << pfRecHitTags_[i].encode() diff --git a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc index d52126f406516..62e2a8cde8a2f 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc @@ -15,7 +15,6 @@ #include #include #include -#include #include #include #include @@ -49,6 +48,7 @@ #include "PhysicsTools/TruthInfo/interface/Graph.h" #include "PhysicsTools/TruthInfo/interface/TruthGraph.h" +#include "PhysicsTools/TruthInfo/interface/TruthLogicalGraphPostProcessor.h" namespace { @@ -72,11 +72,15 @@ namespace { void unite(int a, int b) { a = find(a); b = find(b); + if (a == b) return; + if (rank[a] < rank[b]) std::swap(a, b); + parent[b] = a; + if (rank[a] == rank[b]) ++rank[a]; } @@ -92,10 +96,6 @@ namespace { math::XYZTLorentzVectorD position; }; - struct GraphPostProcessingConfig { - bool collapseIntermediateGenParticles = true; - }; - bool isParticleKind(TruthGraph::NodeKind kind) { return kind == TruthGraph::NodeKind::GenParticle || kind == TruthGraph::NodeKind::SimTrack; } @@ -107,6 +107,7 @@ namespace { bool isGenParticleToSimTrack(TruthGraph const& g, uint32_t src, uint32_t dst) { auto const& s = g.nodeRef(src); auto const& d = g.nodeRef(dst); + return s.kind == TruthGraph::NodeKind::GenParticle && d.kind == TruthGraph::NodeKind::SimTrack; } @@ -117,24 +118,31 @@ namespace { std::sort(pairs.begin(), pairs.end()); pairs.erase(std::unique(pairs.begin(), pairs.end()), pairs.end()); + pairs.erase( + std::remove_if(pairs.begin(), pairs.end(), [nSources](auto const& edge) { return edge.first >= nSources; }), + pairs.end()); + offsets.assign(nSources + 1, 0); - for (auto const& e : pairs) { - ++offsets[e.first + 1]; + + for (auto const& edge : pairs) { + ++offsets[edge.first + 1]; } + for (uint32_t i = 1; i <= nSources; ++i) { offsets[i] += offsets[i - 1]; } flat.assign(pairs.size(), 0); + auto cursor = offsets; - for (auto const& e : pairs) { - flat[cursor[e.first]++] = e.second; + for (auto const& edge : pairs) { + flat[cursor[edge.first]++] = edge.second; } } template - bool validHandle(HandleT const& h) { - return h.isValid(); + bool validHandle(HandleT const& handle) { + return handle.isValid(); } void fillGenPayloadFromHepMC2(HepMC::GenEvent const& ev, @@ -147,7 +155,7 @@ namespace { if (*p == nullptr) continue; - const int bc = (*p)->barcode(); + const int barcode = (*p)->barcode(); GenParticlePayload payload; payload.pdgId = (*p)->pdg_id(); @@ -155,20 +163,20 @@ namespace { payload.momentum = math::XYZTLorentzVectorD( (*p)->momentum().px(), (*p)->momentum().py(), (*p)->momentum().pz(), (*p)->momentum().e()); - particlePayload.emplace(bc, payload); + particlePayload.emplace(barcode, payload); } for (auto v = ev.vertices_begin(); v != ev.vertices_end(); ++v) { if (*v == nullptr) continue; - const int bc = (*v)->barcode(); + const int barcode = (*v)->barcode(); GenVertexPayload payload; payload.position = math::XYZTLorentzVectorD( (*v)->position().x(), (*v)->position().y(), (*v)->position().z(), (*v)->position().t()); - vertexPayload.emplace(bc, payload); + vertexPayload.emplace(barcode, payload); } } @@ -182,7 +190,7 @@ namespace { if (!pptr) continue; - const int bc = pptr->id(); + const int id = pptr->id(); GenParticlePayload payload; payload.pdgId = pptr->pid(); @@ -190,93 +198,31 @@ namespace { payload.momentum = math::XYZTLorentzVectorD( pptr->momentum().px(), pptr->momentum().py(), pptr->momentum().pz(), pptr->momentum().e()); - particlePayload.emplace(bc, payload); + particlePayload.emplace(id, payload); } for (auto const& vptr : ev.vertices()) { if (!vptr) continue; - const int bc = vptr->id(); + const int id = vptr->id(); GenVertexPayload payload; payload.position = math::XYZTLorentzVectorD( vptr->position().x(), vptr->position().y(), vptr->position().z(), vptr->position().t()); - vertexPayload.emplace(bc, payload); + vertexPayload.emplace(id, payload); } } - int32_t genParticlePdgId(TruthGraph const& raw, - uint32_t nodeId, - std::unordered_map const& genParticlePayload) { - auto const& ref = raw.nodeRef(nodeId); - - if (ref.kind != TruthGraph::NodeKind::GenParticle) - return 0; - - const auto rawPdgId = raw.nodePdgId(nodeId); - if (rawPdgId != 0) - return rawPdgId; - - const int barcode = static_cast(ref.key); - auto it = genParticlePayload.find(barcode); - if (it != genParticlePayload.end()) - return it->second.pdgId; - - return 0; - } - - std::vector buildKeepMaskForMotherPdgId(TruthGraph const& raw, - std::unordered_map const& genParticlePayload, - int32_t motherPdgId) { - const uint32_t nRawNodes = raw.nNodes(); - - std::vector keep(nRawNodes, 1); - - if (motherPdgId == 0) - return keep; - - keep.assign(nRawNodes, 0); - - std::queue queue; - - for (uint32_t nodeId = 0; nodeId < nRawNodes; ++nodeId) { - auto const& ref = raw.nodeRef(nodeId); - - if (ref.kind != TruthGraph::NodeKind::GenParticle) - continue; - - if (genParticlePdgId(raw, nodeId, genParticlePayload) != motherPdgId) - continue; - - if (!keep[nodeId]) { - keep[nodeId] = 1; - queue.push(nodeId); - } - } - - while (!queue.empty()) { - const uint32_t src = queue.front(); - queue.pop(); - - for (uint32_t dst : raw.children(src)) { - if (dst >= nRawNodes) - continue; - - if (!keep[dst]) { - keep[dst] = 1; - queue.push(dst); - } - } - } - - return keep; + std::vector buildKeepMaskForAllRawNodes(TruthGraph const& raw) { + return std::vector(raw.nNodes(), 1); } std::vector buildGenParticleToProductionGenVertexMap(TruthGraph const& raw, std::vector const& keepRawNode) { const uint32_t nRawNodes = raw.nNodes(); + std::vector genParticleToProductionGenVertex(nRawNodes, -1); for (uint32_t src = 0; src < nRawNodes; ++src) { @@ -301,9 +247,8 @@ namespace { if (static_cast(edgeKinds[i]) != TruthGraph::EdgeKind::Gen) continue; - if (genParticleToProductionGenVertex[dst] < 0) { + if (genParticleToProductionGenVertex[dst] < 0) genParticleToProductionGenVertex[dst] = static_cast(src); - } } } @@ -347,227 +292,6 @@ namespace { } } - bool directCollapsibleGenParticleChain(truth::Graph const& graph, - uint32_t particleId, - uint32_t& childId, - uint32_t& decayVertexId) { - if (particleId >= graph.nParticles()) - return false; - - auto const& particle = graph.particles[particleId]; - - if (!particle.hasGen()) - return false; - - if (particle.status == 1) - return false; - - if (particle.pdgId == 0) - return false; - - const auto decayVertices = graph.decayVertices(particleId); - if (decayVertices.size() != 1) - return false; - - const uint32_t vertexId = decayVertices.front(); - if (vertexId >= graph.nVertices()) - return false; - - auto const& vertex = graph.vertices[vertexId]; - - if (!vertex.hasGen() || vertex.hasSim()) - return false; - - const auto incoming = graph.incomingParticles(vertexId); - const auto outgoing = graph.outgoingParticles(vertexId); - - if (incoming.size() != 1 || incoming.front() != particleId) - return false; - - if (outgoing.size() != 1) - return false; - - const uint32_t candidateChild = outgoing.front(); - if (candidateChild >= graph.nParticles()) - return false; - - if (candidateChild == particleId) - return false; - - auto const& child = graph.particles[candidateChild]; - - if (!child.hasGen()) - return false; - - if (child.pdgId != particle.pdgId) - return false; - - childId = candidateChild; - decayVertexId = vertexId; - return true; - } - - truth::Graph collapseIntermediateGenParticleChains(truth::Graph const& input) { - if (input.empty()) - return input; - - const uint32_t nParticles = input.nParticles(); - const uint32_t nVertices = input.nVertices(); - - std::vector directChild(nParticles, -1); - std::vector skipVertex(nVertices, 0); - - for (uint32_t particleId = 0; particleId < nParticles; ++particleId) { - uint32_t childId = 0; - uint32_t decayVertexId = 0; - - if (!directCollapsibleGenParticleChain(input, particleId, childId, decayVertexId)) - continue; - - directChild[particleId] = static_cast(childId); - skipVertex[decayVertexId] = 1; - } - - std::vector particleRepresentative(nParticles, 0); - - for (uint32_t particleId = 0; particleId < nParticles; ++particleId) { - uint32_t current = particleId; - - for (uint32_t step = 0; step < nParticles; ++step) { - const int32_t next = directChild[current]; - if (next < 0) - break; - - current = static_cast(next); - } - - particleRepresentative[particleId] = current; - } - - truth::Graph output; - - std::unordered_map representativeToNewParticle; - representativeToNewParticle.reserve(nParticles); - - std::vector oldParticleToNew(nParticles, -1); - - for (uint32_t oldParticle = 0; oldParticle < nParticles; ++oldParticle) { - const uint32_t representative = particleRepresentative[oldParticle]; - - auto inserted = - representativeToNewParticle.emplace(representative, static_cast(output.particles.size())); - const uint32_t newParticle = inserted.first->second; - - if (inserted.second) { - output.particles.push_back(input.particles[representative]); - } - - oldParticleToNew[oldParticle] = static_cast(newParticle); - } - - std::vector keepVertex(nVertices, 0); - - for (uint32_t oldVertex = 0; oldVertex < nVertices; ++oldVertex) { - if (skipVertex[oldVertex]) - continue; - - for (uint32_t oldParticle : input.incomingParticles(oldVertex)) { - if (oldParticle < oldParticleToNew.size() && oldParticleToNew[oldParticle] >= 0) { - keepVertex[oldVertex] = 1; - break; - } - } - - if (keepVertex[oldVertex]) - continue; - - for (uint32_t oldParticle : input.outgoingParticles(oldVertex)) { - if (oldParticle < oldParticleToNew.size() && oldParticleToNew[oldParticle] >= 0) { - keepVertex[oldVertex] = 1; - break; - } - } - } - - std::vector oldVertexToNew(nVertices, -1); - - for (uint32_t oldVertex = 0; oldVertex < nVertices; ++oldVertex) { - if (!keepVertex[oldVertex]) - continue; - - oldVertexToNew[oldVertex] = static_cast(output.vertices.size()); - output.vertices.push_back(input.vertices[oldVertex]); - } - - std::vector> particleToDecayVertexPairs; - std::vector> particleToProductionVertexPairs; - std::vector> vertexToOutgoingParticlePairs; - std::vector> vertexToIncomingParticlePairs; - - for (uint32_t oldVertex = 0; oldVertex < nVertices; ++oldVertex) { - const int32_t newVertex = oldVertexToNew[oldVertex]; - if (newVertex < 0) - continue; - - for (uint32_t oldParticle : input.incomingParticles(oldVertex)) { - if (oldParticle >= oldParticleToNew.size()) - continue; - - const int32_t newParticle = oldParticleToNew[oldParticle]; - if (newParticle < 0) - continue; - - particleToDecayVertexPairs.emplace_back(static_cast(newParticle), static_cast(newVertex)); - vertexToIncomingParticlePairs.emplace_back(static_cast(newVertex), - static_cast(newParticle)); - } - - for (uint32_t oldParticle : input.outgoingParticles(oldVertex)) { - if (oldParticle >= oldParticleToNew.size()) - continue; - - const int32_t newParticle = oldParticleToNew[oldParticle]; - if (newParticle < 0) - continue; - - vertexToOutgoingParticlePairs.emplace_back(static_cast(newVertex), - static_cast(newParticle)); - particleToProductionVertexPairs.emplace_back(static_cast(newParticle), - static_cast(newVertex)); - } - } - - buildCSR(output.nParticles(), - particleToDecayVertexPairs, - output.particleToDecayVertexOffsets, - output.particleToDecayVertices); - - buildCSR(output.nParticles(), - particleToProductionVertexPairs, - output.particleToProductionVertexOffsets, - output.particleToProductionVertices); - - buildCSR(output.nVertices(), - vertexToOutgoingParticlePairs, - output.vertexToOutgoingParticleOffsets, - output.vertexToOutgoingParticles); - - buildCSR(output.nVertices(), - vertexToIncomingParticlePairs, - output.vertexToIncomingParticleOffsets, - output.vertexToIncomingParticles); - - return output; - } - - truth::Graph postProcessGraph(truth::Graph input, GraphPostProcessingConfig const& config) { - if (config.collapseIntermediateGenParticles) { - input = collapseIntermediateGenParticleChains(input); - } - - return input; - } - } // namespace class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { @@ -578,29 +302,29 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { simVertexToken_(mayConsume(cfg.getParameter("simVertices"))), hepmc3Token_(mayConsume(cfg.getParameter("genEventHepMC3"))), hepmc2Token_(mayConsume(cfg.getParameter("genEventHepMC"))), - motherPdgId_(cfg.getParameter("motherPdgId")), - mergeGenSimVertices_(cfg.getParameter("mergeGenSimVertices")) { - postProcessing_.collapseIntermediateGenParticles = cfg.getParameter("collapseIntermediateGenParticles"); + mergeGenSimVertices_(cfg.getParameter("mergeGenSimVertices")), + postProcessor_(truth::TruthLogicalGraphPostProcessor::configFromPSet( + cfg.getParameter("postProcessing"))) { produces(); } static void fillDescriptions(edm::ConfigurationDescriptions& descriptions) { edm::ParameterSetDescription desc; + desc.add("src", edm::InputTag("truthGraphProducer")); desc.add("simTracks", edm::InputTag("g4SimHits")); desc.add("simVertices", edm::InputTag("g4SimHits")); desc.add("genEventHepMC3", edm::InputTag("generatorSmeared")); desc.add("genEventHepMC", edm::InputTag("generatorSmeared")); - desc.add("motherPdgId", 0) - ->setComment("If non-zero, keep only GEN particles with this exact PDG id and all their descendants."); + desc.add("mergeGenSimVertices", true) ->setComment( "If true, merge production GenVertex and SimVertex only for locally one-to-one matches induced by " "GenParticle <-> SimTrack associations."); - desc.add("collapseIntermediateGenParticles", true) - ->setComment( - "If true, collapse GEN chains P -> V -> C where P has status != 1, C is the only daughter, " - "and P and C have the same PDG id."); + + desc.add("postProcessing", truth::TruthLogicalGraphPostProcessor::psetDescription()) + ->setComment("Logical graph post-processing configuration."); + descriptions.addWithDefaultLabel(desc); } @@ -622,8 +346,10 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { evt.getByToken(simVertexToken_, hSimVertices); std::unordered_map simTrackIdToIndex; + if (validHandle(hSimTracks)) { simTrackIdToIndex.reserve(hSimTracks->size() * 2); + for (uint32_t i = 0; i < hSimTracks->size(); ++i) { simTrackIdToIndex.emplace((*hSimTracks)[i].trackId(), i); } @@ -637,10 +363,13 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { { edm::Handle h3; evt.getByToken(hepmc3Token_, h3); + if (validHandle(h3) && h3->GetEvent() != nullptr) { const HepMC3::GenEventData* data = h3->GetEvent(); + HepMC3::GenEvent ev3; ev3.read_data(*data); + fillGenPayloadFromHepMC3(ev3, genParticlePayload, genVertexPayload); haveGenPayload = true; } @@ -649,13 +378,14 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { if (!haveGenPayload) { edm::Handle h2; evt.getByToken(hepmc2Token_, h2); + if (validHandle(h2) && h2->GetEvent() != nullptr) { fillGenPayloadFromHepMC2(*h2->GetEvent(), genParticlePayload, genVertexPayload); haveGenPayload = true; } } - const auto keepRawNode = buildKeepMaskForMotherPdgId(raw, genParticlePayload, motherPdgId_); + const auto keepRawNode = buildKeepMaskForAllRawNodes(raw); const auto genParticleToProductionGenVertex = buildGenParticleToProductionGenVertexMap(raw, keepRawNode); std::vector rawSimVertexIncomingSimTracks; @@ -676,6 +406,7 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { continue; const auto kind = raw.nodeRef(nodeId).kind; + if (isParticleKind(kind)) { rawToParticleTmp[nodeId] = nParticleTmp++; } else if (isVertexKind(kind)) { @@ -732,6 +463,7 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { continue; auto const& ref = raw.nodeRef(nodeId); + if (ref.kind != TruthGraph::NodeKind::SimTrack) continue; @@ -836,6 +568,7 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { if (rawToParticleTmp[nodeId] >= 0) { const int rep = particleDSU.find(rawToParticleTmp[nodeId]); auto result = particleRepToLogical.emplace(rep, static_cast(particleRepToLogical.size())); + rawToParticle[nodeId] = static_cast(result.first->second); } } @@ -850,6 +583,7 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { if (rawToVertexTmp[nodeId] >= 0) { const int rep = vertexDSU.find(rawToVertexTmp[nodeId]); auto result = vertexRepToLogical.emplace(rep, static_cast(vertexRepToLogical.size())); + rawToVertex[nodeId] = static_cast(result.first->second); } } @@ -877,17 +611,21 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { if (p.pdgId == 0) p.pdgId = raw.nodePdgId(nodeId); + if (p.status == 0) p.status = raw.nodeStatus(nodeId); + if (p.statusFlags == 0) p.statusFlags = raw.nodeStatusFlags(nodeId); if (haveGenPayload) { const int barcode = static_cast(ref.key); auto it = genParticlePayload.find(barcode); + if (it != genParticlePayload.end()) { if (p.pdgId == 0) p.pdgId = it->second.pdgId; + if (p.status == 0) p.status = it->second.status; @@ -901,8 +639,10 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { if (p.pdgId == 0) p.pdgId = raw.nodePdgId(nodeId); + if (p.status == 0) p.status = raw.nodeStatus(nodeId); + if (p.eventId == 0) p.eventId = raw.nodeEventId(nodeId); @@ -951,6 +691,7 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { if (haveGenPayload) { const int barcode = static_cast(ref.key); auto it = genVertexPayload.find(barcode); + if (it != genVertexPayload.end()) { // Keep the GEN position as nominal for GEN and GEN+SIM logical vertices. v.position = it->second.position; @@ -965,6 +706,7 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { if (validHandle(hSimVertices)) { const auto simIndex = static_cast(ref.key); + if (simIndex < hSimVertices->size()) { auto const& sv = (*hSimVertices)[simIndex]; const auto& pos = sv.position(); @@ -1002,21 +744,25 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { auto const& dstRef = raw.nodeRef(dst); if (isVertexKind(srcRef.kind) && isParticleKind(dstRef.kind)) { - const int32_t lv = rawToVertex[src]; - const int32_t lp = rawToParticle[dst]; - - if (lv >= 0 && lp >= 0) { - vertexToOutgoingParticlePairs.emplace_back(static_cast(lv), static_cast(lp)); - particleToProductionVertexPairs.emplace_back(static_cast(lp), static_cast(lv)); + const int32_t logicalVertex = rawToVertex[src]; + const int32_t logicalParticle = rawToParticle[dst]; + + if (logicalVertex >= 0 && logicalParticle >= 0) { + vertexToOutgoingParticlePairs.emplace_back(static_cast(logicalVertex), + static_cast(logicalParticle)); + particleToProductionVertexPairs.emplace_back(static_cast(logicalParticle), + static_cast(logicalVertex)); } } else if (isParticleKind(srcRef.kind) && isVertexKind(dstRef.kind)) { - const int32_t lp = rawToParticle[src]; - const int32_t lv = rawToVertex[dst]; - - if (lp >= 0 && lv >= 0) { - particleToDecayVertexPairs.emplace_back(static_cast(lp), static_cast(lv)); - vertexToIncomingParticlePairs.emplace_back(static_cast(lv), static_cast(lp)); + const int32_t logicalParticle = rawToParticle[src]; + const int32_t logicalVertex = rawToVertex[dst]; + + if (logicalParticle >= 0 && logicalVertex >= 0) { + particleToDecayVertexPairs.emplace_back(static_cast(logicalParticle), + static_cast(logicalVertex)); + vertexToIncomingParticlePairs.emplace_back(static_cast(logicalVertex), + static_cast(logicalParticle)); } } } @@ -1040,7 +786,7 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { out->vertexToIncomingParticleOffsets, out->vertexToIncomingParticles); - *out = postProcessGraph(std::move(*out), postProcessing_); + *out = postProcessor_.process(std::move(*out)); if (!out->isConsistent()) { throw cms::Exception("TruthLogicalGraphProducer") << "Produced truth::Graph is not consistent"; @@ -1056,9 +802,8 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { edm::EDGetTokenT hepmc3Token_; edm::EDGetTokenT hepmc2Token_; - int32_t motherPdgId_; bool mergeGenSimVertices_; - GraphPostProcessingConfig postProcessing_; + truth::TruthLogicalGraphPostProcessor postProcessor_; }; DEFINE_FWK_MODULE(TruthLogicalGraphProducer); diff --git a/PhysicsTools/TruthInfo/src/TruthLogicalGraphPostProcessor.cc b/PhysicsTools/TruthInfo/src/TruthLogicalGraphPostProcessor.cc new file mode 100644 index 0000000000000..1cf6e1013eda1 --- /dev/null +++ b/PhysicsTools/TruthInfo/src/TruthLogicalGraphPostProcessor.cc @@ -0,0 +1,908 @@ +#include "PhysicsTools/TruthInfo/interface/TruthLogicalGraphPostProcessor.h" + +#include +#include +#include +#include +#include +#include + +namespace { + + struct DSU { + std::vector parent; + std::vector rank; + + explicit DSU(int n) : parent(n), rank(n, 0) { + for (int i = 0; i < n; ++i) + parent[i] = i; + } + + int find(int x) { + while (parent[x] != x) { + parent[x] = parent[parent[x]]; + x = parent[x]; + } + return x; + } + + void unite(int a, int b) { + a = find(a); + b = find(b); + + if (a == b) + return; + + if (rank[a] < rank[b]) + std::swap(a, b); + + parent[b] = a; + + if (rank[a] == rank[b]) + ++rank[a]; + } + }; + + bool containsPdgId(std::vector const& pdgIds, int32_t pdgId) { + return std::find(pdgIds.begin(), pdgIds.end(), pdgId) != pdgIds.end(); + } + + bool containsParticleId(std::vector const& particleIds, uint32_t particleId) { + return std::find(particleIds.begin(), particleIds.end(), particleId) != particleIds.end(); + } + + bool isIgnoredParticle(truth::Graph const& graph, + uint32_t particleId, + std::vector const& ignoredPdgIds, + std::vector const& ignoredParticleIds) { + if (particleId >= graph.nParticles()) + return false; + + if (containsParticleId(ignoredParticleIds, particleId)) + return true; + + if (containsPdgId(ignoredPdgIds, graph.particles[particleId].pdgId)) + return true; + + return false; + } + + bool isStableGenParticle(truth::Graph const& graph, uint32_t particleId) { + if (particleId >= graph.nParticles()) + return false; + + auto const& particle = graph.particles[particleId]; + + return particle.hasGen() && particle.status == 1; + } + + void buildCSR(uint32_t nSources, + std::vector>& pairs, + std::vector& offsets, + std::vector& flat) { + std::sort(pairs.begin(), pairs.end()); + pairs.erase(std::unique(pairs.begin(), pairs.end()), pairs.end()); + + pairs.erase( + std::remove_if(pairs.begin(), pairs.end(), [nSources](auto const& edge) { return edge.first >= nSources; }), + pairs.end()); + + offsets.assign(nSources + 1, 0); + + for (auto const& edge : pairs) { + ++offsets[edge.first + 1]; + } + + for (uint32_t i = 1; i <= nSources; ++i) { + offsets[i] += offsets[i - 1]; + } + + flat.assign(pairs.size(), 0); + + auto cursor = offsets; + for (auto const& edge : pairs) { + flat[cursor[edge.first]++] = edge.second; + } + } + + bool directCollapsibleGenParticleChain(truth::Graph const& graph, + uint32_t particleId, + uint32_t& childId, + uint32_t& decayVertexId) { + if (particleId >= graph.nParticles()) + return false; + + auto const& particle = graph.particles[particleId]; + + if (!particle.hasGen()) + return false; + + // Never collapse stable final-state GEN particles. + if (particle.status == 1) + return false; + + if (particle.pdgId == 0) + return false; + + const auto decayVertices = graph.decayVertices(particleId); + if (decayVertices.size() != 1) + return false; + + const uint32_t vertexId = decayVertices.front(); + if (vertexId >= graph.nVertices()) + return false; + + auto const& vertex = graph.vertices[vertexId]; + + if (!vertex.hasGen() || vertex.hasSim()) + return false; + + const auto incoming = graph.incomingParticles(vertexId); + const auto outgoing = graph.outgoingParticles(vertexId); + + if (incoming.size() != 1 || incoming.front() != particleId) + return false; + + if (outgoing.size() != 1) + return false; + + const uint32_t candidateChild = outgoing.front(); + if (candidateChild >= graph.nParticles()) + return false; + + if (candidateChild == particleId) + return false; + + auto const& child = graph.particles[candidateChild]; + + if (!child.hasGen()) + return false; + + if (child.pdgId != particle.pdgId) + return false; + + childId = candidateChild; + decayVertexId = vertexId; + + return true; + } + + truth::Graph collapseIntermediateGenParticleChains(truth::Graph const& input) { + if (input.empty()) + return input; + + const uint32_t nParticles = input.nParticles(); + const uint32_t nVertices = input.nVertices(); + + std::vector directChild(nParticles, -1); + std::vector skipVertex(nVertices, 0); + + for (uint32_t particleId = 0; particleId < nParticles; ++particleId) { + uint32_t childId = 0; + uint32_t decayVertexId = 0; + + if (!directCollapsibleGenParticleChain(input, particleId, childId, decayVertexId)) + continue; + + directChild[particleId] = static_cast(childId); + skipVertex[decayVertexId] = 1; + } + + std::vector particleRepresentative(nParticles, 0); + + for (uint32_t particleId = 0; particleId < nParticles; ++particleId) { + uint32_t current = particleId; + + for (uint32_t step = 0; step < nParticles; ++step) { + const int32_t next = directChild[current]; + if (next < 0) + break; + + current = static_cast(next); + } + + particleRepresentative[particleId] = current; + } + + truth::Graph output; + + std::unordered_map representativeToNewParticle; + representativeToNewParticle.reserve(nParticles); + + std::vector oldParticleToNew(nParticles, -1); + + for (uint32_t oldParticle = 0; oldParticle < nParticles; ++oldParticle) { + const uint32_t representative = particleRepresentative[oldParticle]; + + auto inserted = + representativeToNewParticle.emplace(representative, static_cast(output.particles.size())); + + const uint32_t newParticle = inserted.first->second; + + if (inserted.second) { + output.particles.push_back(input.particles[representative]); + } + + oldParticleToNew[oldParticle] = static_cast(newParticle); + } + + std::vector keepVertex(nVertices, 0); + + for (uint32_t oldVertex = 0; oldVertex < nVertices; ++oldVertex) { + if (skipVertex[oldVertex]) + continue; + + for (uint32_t oldParticle : input.incomingParticles(oldVertex)) { + if (oldParticle < oldParticleToNew.size() && oldParticleToNew[oldParticle] >= 0) { + keepVertex[oldVertex] = 1; + break; + } + } + + if (keepVertex[oldVertex]) + continue; + + for (uint32_t oldParticle : input.outgoingParticles(oldVertex)) { + if (oldParticle < oldParticleToNew.size() && oldParticleToNew[oldParticle] >= 0) { + keepVertex[oldVertex] = 1; + break; + } + } + } + + std::vector oldVertexToNew(nVertices, -1); + + for (uint32_t oldVertex = 0; oldVertex < nVertices; ++oldVertex) { + if (!keepVertex[oldVertex]) + continue; + + oldVertexToNew[oldVertex] = static_cast(output.vertices.size()); + output.vertices.push_back(input.vertices[oldVertex]); + } + + std::vector> particleToDecayVertexPairs; + std::vector> particleToProductionVertexPairs; + std::vector> vertexToOutgoingParticlePairs; + std::vector> vertexToIncomingParticlePairs; + + for (uint32_t oldVertex = 0; oldVertex < nVertices; ++oldVertex) { + const int32_t newVertex = oldVertexToNew[oldVertex]; + if (newVertex < 0) + continue; + + for (uint32_t oldParticle : input.incomingParticles(oldVertex)) { + if (oldParticle >= oldParticleToNew.size()) + continue; + + const int32_t newParticle = oldParticleToNew[oldParticle]; + if (newParticle < 0) + continue; + + particleToDecayVertexPairs.emplace_back(static_cast(newParticle), static_cast(newVertex)); + vertexToIncomingParticlePairs.emplace_back(static_cast(newVertex), + static_cast(newParticle)); + } + + for (uint32_t oldParticle : input.outgoingParticles(oldVertex)) { + if (oldParticle >= oldParticleToNew.size()) + continue; + + const int32_t newParticle = oldParticleToNew[oldParticle]; + if (newParticle < 0) + continue; + + vertexToOutgoingParticlePairs.emplace_back(static_cast(newVertex), + static_cast(newParticle)); + particleToProductionVertexPairs.emplace_back(static_cast(newParticle), + static_cast(newVertex)); + } + } + + buildCSR(output.nParticles(), + particleToDecayVertexPairs, + output.particleToDecayVertexOffsets, + output.particleToDecayVertices); + + buildCSR(output.nParticles(), + particleToProductionVertexPairs, + output.particleToProductionVertexOffsets, + output.particleToProductionVertices); + + buildCSR(output.nVertices(), + vertexToOutgoingParticlePairs, + output.vertexToOutgoingParticleOffsets, + output.vertexToOutgoingParticles); + + buildCSR(output.nVertices(), + vertexToIncomingParticlePairs, + output.vertexToIncomingParticleOffsets, + output.vertexToIncomingParticles); + + return output; + } + + void markDownstreamFromParticle(truth::Graph const& graph, + uint32_t particleId, + std::vector& keepParticle, + std::vector& keepVertex) { + if (particleId >= graph.nParticles()) + return; + + std::queue queue; + + if (!keepParticle[particleId]) { + keepParticle[particleId] = 1; + queue.push(particleId); + } + + while (!queue.empty()) { + const uint32_t currentParticle = queue.front(); + queue.pop(); + + for (uint32_t vertexId : graph.decayVertices(currentParticle)) { + if (vertexId >= graph.nVertices()) + continue; + + keepVertex[vertexId] = 1; + + for (uint32_t childId : graph.outgoingParticles(vertexId)) { + if (childId >= graph.nParticles()) + continue; + + if (!keepParticle[childId]) { + keepParticle[childId] = 1; + queue.push(childId); + } + } + } + } + } + + void markUpstreamToParticle(truth::Graph const& graph, + uint32_t particleId, + std::vector& keepParticle, + std::vector& keepVertex) { + if (particleId >= graph.nParticles()) + return; + + std::queue queue; + + if (!keepParticle[particleId]) { + keepParticle[particleId] = 1; + } + + queue.push(particleId); + + while (!queue.empty()) { + const uint32_t currentParticle = queue.front(); + queue.pop(); + + for (uint32_t vertexId : graph.productionVertices(currentParticle)) { + if (vertexId >= graph.nVertices()) + continue; + + keepVertex[vertexId] = 1; + + for (uint32_t parentId : graph.incomingParticles(vertexId)) { + if (parentId >= graph.nParticles()) + continue; + + if (!keepParticle[parentId]) { + keepParticle[parentId] = 1; + queue.push(parentId); + } + } + } + } + } + + std::vector expandSeedsWithParents(truth::Graph const& graph, + std::vector const& seedParticles, + uint32_t parentDepth) { + std::vector startParticles = seedParticles; + + std::vector seen(graph.nParticles(), 0); + std::queue> queue; + + for (const uint32_t seed : seedParticles) { + if (seed >= graph.nParticles()) + continue; + + if (!seen[seed]) { + seen[seed] = 1; + queue.emplace(seed, 0); + } + } + + while (!queue.empty()) { + const auto [particleId, depth] = queue.front(); + queue.pop(); + + if (depth >= parentDepth) + continue; + + for (const uint32_t vertexId : graph.productionVertices(particleId)) { + if (vertexId >= graph.nVertices()) + continue; + + for (const uint32_t parentId : graph.incomingParticles(vertexId)) { + if (parentId >= graph.nParticles()) + continue; + + if (!seen[parentId]) { + seen[parentId] = 1; + startParticles.push_back(parentId); + queue.emplace(parentId, depth + 1); + } + } + } + } + + std::sort(startParticles.begin(), startParticles.end()); + startParticles.erase(std::unique(startParticles.begin(), startParticles.end()), startParticles.end()); + + return startParticles; + } + + void closeKeptVerticesWithAllParents(truth::Graph const& graph, + std::vector& keepParticle, + std::vector& keepVertex) { + bool changed = true; + + while (changed) { + changed = false; + + for (uint32_t vertexId = 0; vertexId < graph.nVertices(); ++vertexId) { + if (!keepVertex[vertexId]) + continue; + + for (uint32_t parentId : graph.incomingParticles(vertexId)) { + if (parentId >= graph.nParticles()) + continue; + + if (keepParticle[parentId]) + continue; + + markUpstreamToParticle(graph, parentId, keepParticle, keepVertex); + changed = true; + } + } + } + } + + void dropVerticesWithoutVisibleParticles(truth::Graph const& graph, + std::vector const& keepParticle, + std::vector& keepVertex) { + for (uint32_t vertexId = 0; vertexId < graph.nVertices(); ++vertexId) { + if (!keepVertex[vertexId]) + continue; + + bool hasVisibleIncoming = false; + bool hasVisibleOutgoing = false; + + for (uint32_t particleId : graph.incomingParticles(vertexId)) { + if (particleId < graph.nParticles() && keepParticle[particleId]) { + hasVisibleIncoming = true; + break; + } + } + + for (uint32_t particleId : graph.outgoingParticles(vertexId)) { + if (particleId < graph.nParticles() && keepParticle[particleId]) { + hasVisibleOutgoing = true; + break; + } + } + + if (!hasVisibleIncoming && !hasVisibleOutgoing) + keepVertex[vertexId] = 0; + } + } + + truth::Graph rebuildFilteredGraph(truth::Graph const& input, + std::vector const& keepParticle, + std::vector const& keepVertex, + std::vector const& connectToCollapsedStableVertex) { + const uint32_t nParticles = input.nParticles(); + const uint32_t nVertices = input.nVertices(); + + const bool needCollapsedStableVertex = std::any_of(connectToCollapsedStableVertex.begin(), + connectToCollapsedStableVertex.end(), + [](uint8_t value) { return value != 0; }); + + truth::Graph output; + + std::vector oldParticleToNew(nParticles, -1); + std::vector oldVertexToNew(nVertices, -1); + + output.particles.reserve(nParticles); + output.vertices.reserve(nVertices + (needCollapsedStableVertex ? 1 : 0)); + + for (uint32_t oldParticle = 0; oldParticle < nParticles; ++oldParticle) { + if (!keepParticle[oldParticle]) + continue; + + oldParticleToNew[oldParticle] = static_cast(output.particles.size()); + output.particles.push_back(input.particles[oldParticle]); + } + + for (uint32_t oldVertex = 0; oldVertex < nVertices; ++oldVertex) { + if (!keepVertex[oldVertex]) + continue; + + oldVertexToNew[oldVertex] = static_cast(output.vertices.size()); + output.vertices.push_back(input.vertices[oldVertex]); + } + + int32_t collapsedStableVertex = -1; + + if (needCollapsedStableVertex) { + collapsedStableVertex = static_cast(output.vertices.size()); + + truth::VertexData vertex; + vertex.genNode = -1; + vertex.simNode = -1; + vertex.eventId = 0; + vertex.genEvent = -1; + + output.vertices.push_back(vertex); + } + + std::vector> particleToDecayVertexPairs; + std::vector> particleToProductionVertexPairs; + std::vector> vertexToOutgoingParticlePairs; + std::vector> vertexToIncomingParticlePairs; + + for (uint32_t oldVertex = 0; oldVertex < nVertices; ++oldVertex) { + const int32_t newVertex = oldVertexToNew[oldVertex]; + if (newVertex < 0) + continue; + + for (uint32_t oldParticle : input.incomingParticles(oldVertex)) { + if (oldParticle >= nParticles) + continue; + + const int32_t newParticle = oldParticleToNew[oldParticle]; + if (newParticle < 0) + continue; + + particleToDecayVertexPairs.emplace_back(static_cast(newParticle), static_cast(newVertex)); + vertexToIncomingParticlePairs.emplace_back(static_cast(newVertex), + static_cast(newParticle)); + } + + for (uint32_t oldParticle : input.outgoingParticles(oldVertex)) { + if (oldParticle >= nParticles) + continue; + + const int32_t newParticle = oldParticleToNew[oldParticle]; + if (newParticle < 0) + continue; + + vertexToOutgoingParticlePairs.emplace_back(static_cast(newVertex), + static_cast(newParticle)); + particleToProductionVertexPairs.emplace_back(static_cast(newParticle), + static_cast(newVertex)); + } + } + + if (collapsedStableVertex >= 0) { + const uint32_t newVertex = static_cast(collapsedStableVertex); + + for (uint32_t oldParticle = 0; oldParticle < nParticles; ++oldParticle) { + if (!connectToCollapsedStableVertex[oldParticle]) + continue; + + const int32_t newParticle = oldParticleToNew[oldParticle]; + if (newParticle < 0) + continue; + + vertexToOutgoingParticlePairs.emplace_back(newVertex, static_cast(newParticle)); + particleToProductionVertexPairs.emplace_back(static_cast(newParticle), newVertex); + } + } + + buildCSR(output.nParticles(), + particleToDecayVertexPairs, + output.particleToDecayVertexOffsets, + output.particleToDecayVertices); + + buildCSR(output.nParticles(), + particleToProductionVertexPairs, + output.particleToProductionVertexOffsets, + output.particleToProductionVertices); + + buildCSR(output.nVertices(), + vertexToOutgoingParticlePairs, + output.vertexToOutgoingParticleOffsets, + output.vertexToOutgoingParticles); + + buildCSR(output.nVertices(), + vertexToIncomingParticlePairs, + output.vertexToIncomingParticleOffsets, + output.vertexToIncomingParticles); + + return output; + } + + truth::Graph filterGraphBySeedPdgIds(truth::Graph const& input, + truth::LogicalGraphPostProcessingConfig const& config) { + if (input.empty()) + return input; + + if (config.seedPdgIds.empty()) + return input; + + const uint32_t nParticles = input.nParticles(); + const uint32_t nVertices = input.nVertices(); + + std::vector seedParticles; + + for (uint32_t particleId = 0; particleId < nParticles; ++particleId) { + if (containsPdgId(config.seedPdgIds, input.particles[particleId].pdgId)) + seedParticles.push_back(particleId); + } + + if (seedParticles.empty()) + return input; + + const auto startParticles = expandSeedsWithParents(input, seedParticles, config.seedParentDepth); + + std::vector keepParticle(nParticles, 0); + std::vector keepVertex(nVertices, 0); + + for (const uint32_t particleId : startParticles) { + markDownstreamFromParticle(input, particleId, keepParticle, keepVertex); + } + + // The graph is a DAG, not a tree. If a kept downstream vertex has parents + // that are not in the selected subgraph, keep those parents and their full + // upstream history to avoid creating misleading partial vertices. + closeKeptVerticesWithAllParents(input, keepParticle, keepVertex); + + // Keep every final-state GEN particle unless the user explicitly asked to + // ignore/collapse it. Stable particles outside the interesting subgraph are + // attached to one artificial source vertex. + std::vector connectToCollapsedStableVertex(nParticles, 0); + + for (uint32_t particleId = 0; particleId < nParticles; ++particleId) { + if (!isStableGenParticle(input, particleId)) + continue; + + if (isIgnoredParticle(input, particleId, config.ignoredPdgIds, config.ignoredParticleIds)) + continue; + + if (keepParticle[particleId]) + continue; + + keepParticle[particleId] = 1; + connectToCollapsedStableVertex[particleId] = 1; + } + + dropVerticesWithoutVisibleParticles(input, keepParticle, keepVertex); + + return rebuildFilteredGraph(input, keepParticle, keepVertex, connectToCollapsedStableVertex); + } + + truth::Graph collapseIgnoredParticles(truth::Graph const& input, + std::vector const& ignoredPdgIds, + std::vector const& ignoredParticleIds) { + if (input.empty()) + return input; + + if (ignoredPdgIds.empty() && ignoredParticleIds.empty()) + return input; + + const uint32_t nParticles = input.nParticles(); + const uint32_t nVertices = input.nVertices(); + + std::vector removeParticle(nParticles, 0); + + for (uint32_t particleId = 0; particleId < nParticles; ++particleId) { + if (isIgnoredParticle(input, particleId, ignoredPdgIds, ignoredParticleIds)) + removeParticle[particleId] = 1; + } + + if (std::none_of(removeParticle.begin(), removeParticle.end(), [](uint8_t value) { return value != 0; })) + return input; + + DSU vertexDSU(static_cast(nVertices)); + + for (uint32_t particleId = 0; particleId < nParticles; ++particleId) { + if (!removeParticle[particleId]) + continue; + + std::vector connectedVertices; + + for (uint32_t vertexId : input.productionVertices(particleId)) { + if (vertexId < nVertices) + connectedVertices.push_back(vertexId); + } + + for (uint32_t vertexId : input.decayVertices(particleId)) { + if (vertexId < nVertices) + connectedVertices.push_back(vertexId); + } + + if (connectedVertices.size() < 2) + continue; + + const uint32_t first = connectedVertices.front(); + + for (std::size_t i = 1; i < connectedVertices.size(); ++i) { + vertexDSU.unite(static_cast(first), static_cast(connectedVertices[i])); + } + } + + truth::Graph output; + + std::vector oldParticleToNew(nParticles, -1); + + for (uint32_t oldParticle = 0; oldParticle < nParticles; ++oldParticle) { + if (removeParticle[oldParticle]) + continue; + + oldParticleToNew[oldParticle] = static_cast(output.particles.size()); + output.particles.push_back(input.particles[oldParticle]); + } + + std::unordered_map vertexRepToNew; + vertexRepToNew.reserve(nVertices); + + std::vector oldVertexToNew(nVertices, -1); + + auto getNewVertex = [&](uint32_t oldVertex) -> uint32_t { + if (oldVertexToNew[oldVertex] >= 0) + return static_cast(oldVertexToNew[oldVertex]); + + const int rep = vertexDSU.find(static_cast(oldVertex)); + auto inserted = vertexRepToNew.emplace(rep, static_cast(output.vertices.size())); + + if (inserted.second) { + output.vertices.push_back(input.vertices[oldVertex]); + } + + oldVertexToNew[oldVertex] = static_cast(inserted.first->second); + return inserted.first->second; + }; + + std::vector> particleToDecayVertexPairs; + std::vector> particleToProductionVertexPairs; + std::vector> vertexToOutgoingParticlePairs; + std::vector> vertexToIncomingParticlePairs; + + for (uint32_t oldVertex = 0; oldVertex < nVertices; ++oldVertex) { + bool hasVisibleIncoming = false; + bool hasVisibleOutgoing = false; + + for (uint32_t oldParticle : input.incomingParticles(oldVertex)) { + if (oldParticle < nParticles && oldParticleToNew[oldParticle] >= 0) { + hasVisibleIncoming = true; + break; + } + } + + for (uint32_t oldParticle : input.outgoingParticles(oldVertex)) { + if (oldParticle < nParticles && oldParticleToNew[oldParticle] >= 0) { + hasVisibleOutgoing = true; + break; + } + } + + if (!hasVisibleIncoming && !hasVisibleOutgoing) + continue; + + const uint32_t newVertex = getNewVertex(oldVertex); + + for (uint32_t oldParticle : input.incomingParticles(oldVertex)) { + if (oldParticle >= nParticles) + continue; + + const int32_t newParticle = oldParticleToNew[oldParticle]; + if (newParticle < 0) + continue; + + particleToDecayVertexPairs.emplace_back(static_cast(newParticle), newVertex); + vertexToIncomingParticlePairs.emplace_back(newVertex, static_cast(newParticle)); + } + + for (uint32_t oldParticle : input.outgoingParticles(oldVertex)) { + if (oldParticle >= nParticles) + continue; + + const int32_t newParticle = oldParticleToNew[oldParticle]; + if (newParticle < 0) + continue; + + vertexToOutgoingParticlePairs.emplace_back(newVertex, static_cast(newParticle)); + particleToProductionVertexPairs.emplace_back(static_cast(newParticle), newVertex); + } + } + + buildCSR(output.nParticles(), + particleToDecayVertexPairs, + output.particleToDecayVertexOffsets, + output.particleToDecayVertices); + + buildCSR(output.nParticles(), + particleToProductionVertexPairs, + output.particleToProductionVertexOffsets, + output.particleToProductionVertices); + + buildCSR(output.nVertices(), + vertexToOutgoingParticlePairs, + output.vertexToOutgoingParticleOffsets, + output.vertexToOutgoingParticles); + + buildCSR(output.nVertices(), + vertexToIncomingParticlePairs, + output.vertexToIncomingParticleOffsets, + output.vertexToIncomingParticles); + + return output; + } + +} // namespace + +namespace truth { + + TruthLogicalGraphPostProcessor::TruthLogicalGraphPostProcessor(LogicalGraphPostProcessingConfig config) + : config_(std::move(config)) {} + + edm::ParameterSetDescription TruthLogicalGraphPostProcessor::psetDescription() { + edm::ParameterSetDescription desc; + + desc.add("collapseIntermediateGenParticles", true) + ->setComment( + "If true, collapse GEN chains P -> V -> C where P has status != 1, C is the only daughter, " + "and P and C have the same PDG id. Status-1 GEN particles are never collapsed by this rule."); + + desc.add>("seedPdgIds", {}) + ->setComment( + "If non-empty, keep particles with these exact PDG ids, keep seedParentDepth generations above them, " + "then keep the full downstream subgraph. Stable GEN particles outside the selected subgraph are kept " + "and attached to one artificial collapsed vertex."); + + desc.add("seedParentDepth", 0) + ->setComment("Number of parent generations to keep above each seed particle before keeping downstream."); + + desc.add>("ignoredPdgIds", {}) + ->setComment( + "Particles with these exact PDG ids are always removed from the final logical graph. If internal, " + "their production and decay vertices are merged so the graph remains connected."); + + desc.add>("ignoredParticleIds", {}) + ->setComment( + "Logical particle ids to remove from the final logical graph. These ids refer to the graph state at " + "the moment the ignored-particle collapsing step is applied."); + + return desc; + } + + LogicalGraphPostProcessingConfig TruthLogicalGraphPostProcessor::configFromPSet(edm::ParameterSet const& pset) { + LogicalGraphPostProcessingConfig config; + + config.collapseIntermediateGenParticles = pset.getParameter("collapseIntermediateGenParticles"); + config.seedPdgIds = pset.getParameter>("seedPdgIds"); + config.seedParentDepth = pset.getParameter("seedParentDepth"); + config.ignoredPdgIds = pset.getParameter>("ignoredPdgIds"); + config.ignoredParticleIds = pset.getParameter>("ignoredParticleIds"); + + return config; + } + + Graph TruthLogicalGraphPostProcessor::process(Graph input) const { + if (config_.collapseIntermediateGenParticles) { + input = collapseIntermediateGenParticleChains(input); + } + + input = filterGraphBySeedPdgIds(input, config_); + + if (!config_.ignoredPdgIds.empty() || !config_.ignoredParticleIds.empty()) { + input = collapseIgnoredParticles(input, config_.ignoredPdgIds, config_.ignoredParticleIds); + } + + return input; + } + +} // namespace truth diff --git a/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py b/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py index 737bda66b39d2..055d33623cef4 100644 --- a/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py +++ b/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py @@ -71,11 +71,30 @@ simVertices=cms.InputTag("g4SimHits"), genEventHepMC3=cms.InputTag("generatorSmeared"), genEventHepMC=cms.InputTag("generatorSmeared"), - motherPdgId=cms.int32(0), - mergeGenSimVertices=cms.bool(args.mergeGenSim), - collapseIntermediateGenParticles=cms.bool(args.collapse), -) + mergeGenSimVertices=cms.bool(True), + + postProcessing=cms.PSet( + collapseIntermediateGenParticles=cms.bool(True), + + # Empty means: keep the full logical graph. + # Example: cms.vint32(22) keeps photons as seeds, + # keeps seedParentDepth parent generations above each seed, + # then keeps everything downstream. + seedPdgIds=cms.vint32(23,15,-15,25,4,5,6), + + seedParentDepth=cms.uint32(1), + + # Remove particles by PDG id. + # Example: cms.vint32(22) removes all photons from the final logical graph. + ignoredPdgIds=cms.vint32(), + + # Remove particles by logical particle id. + # These ids refer to the graph after the previous postprocessing steps + # and before ignored-particle collapsing. + ignoredParticleIds=cms.vuint32(), + ), +) process.simHitToRecHitMapProducer = cms.EDProducer( "SimHitToRecHitMapProducer", From e188cf733772da79c215018f6c70950f41824579 Mon Sep 17 00:00:00 2001 From: Izaak <13318254+IzaakWN@users.noreply.github.com> Date: Wed, 13 May 2026 16:02:56 +0200 Subject: [PATCH 30/53] make input file optional (#66) --- .../TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py b/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py index 055d33623cef4..0080adaf1d1f0 100644 --- a/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py +++ b/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py @@ -4,7 +4,7 @@ import os from argparse import ArgumentParser parser = ArgumentParser() -parser.add_argument("inputFile", default="file:step3.root", +parser.add_argument("inputFile", nargs='?', default="step3.root", metavar='FILE', help="Input file, default=%(default)r" ) parser.add_argument('-o', "--outdir", default='', help="output directory, default=%(default)r" ) @@ -15,7 +15,7 @@ parser.add_argument('-c', "--collapse", action='store_true', help="collapse GenParticle copies" ) parser.add_argument('-t', "--tag", default='', help="tag for out put file" ) args = parser.parse_args() -if '/' not in args.inputFile: +if '/' not in args.inputFile and ':' not in args.inputFile: args.inputFile = 'file:'+args.inputFile if args.outdir and not os.path.exists(args.outdir): os.makedirs(args.outdir, exist_ok=True) From 318dd62492b4e7ac15f179ae474415722df87dcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Cuisset?= Date: Wed, 13 May 2026 16:03:15 +0200 Subject: [PATCH 31/53] dump simhits & rechits (#67) --- PhysicsTools/TruthInfo/plugins/BuildFile.xml | 7 ++ .../plugins/RecHitFlatTableProducer.cc | 99 +++++++++++++++++++ .../plugins/TruthLogicalGraphDumper.cc | 15 +++ .../TruthInfo/python/recHitTable_cff.py | 4 + .../test/dumpTruthGraphsFromGENSIMRECO_cfg.py | 25 +++++ 5 files changed, 150 insertions(+) create mode 100644 PhysicsTools/TruthInfo/plugins/RecHitFlatTableProducer.cc create mode 100644 PhysicsTools/TruthInfo/python/recHitTable_cff.py diff --git a/PhysicsTools/TruthInfo/plugins/BuildFile.xml b/PhysicsTools/TruthInfo/plugins/BuildFile.xml index 1f50a9ad6e09d..8e4288cc9aedc 100644 --- a/PhysicsTools/TruthInfo/plugins/BuildFile.xml +++ b/PhysicsTools/TruthInfo/plugins/BuildFile.xml @@ -24,6 +24,9 @@ + + + @@ -42,4 +45,8 @@ + + + + \ No newline at end of file diff --git a/PhysicsTools/TruthInfo/plugins/RecHitFlatTableProducer.cc b/PhysicsTools/TruthInfo/plugins/RecHitFlatTableProducer.cc new file mode 100644 index 0000000000000..c46a8c30844ba --- /dev/null +++ b/PhysicsTools/TruthInfo/plugins/RecHitFlatTableProducer.cc @@ -0,0 +1,99 @@ +#include "FWCore/Framework/interface/stream/EDProducer.h" +#include "FWCore/Framework/interface/Event.h" +#include "FWCore/Framework/interface/stream/moduleAbilities.h" +#include "FWCore/Utilities/interface/ESGetToken.h" +#include "FWCore/Utilities/interface/transform.h" +#include "FWCore/ParameterSet/interface/ParameterSet.h" +#include "FWCore/ParameterSet/interface/ConfigurationDescriptions.h" +#include "FWCore/ParameterSet/interface/ParameterSetDescription.h" +#include "DataFormats/NanoAOD/interface/FlatTable.h" + +#include "RecoLocalCalo/HGCalRecAlgos/interface/RecHitTools.h" +#include "DataFormats/HGCRecHit/interface/HGCRecHitCollections.h" + +#include +#include + +class RecHitFlatTableProducer : public edm::stream::EDProducer { +public: + RecHitFlatTableProducer(edm::ParameterSet const& params) + : objName_(params.getParameter("objName")), + rechits_tokens_{ + edm::vector_transform(params.getParameter>("label_rechits"), + [this](const edm::InputTag& lab) { return consumes(lab); })}, + caloGeometry_token_(esConsumes()) { + produces(); + } + + ~RecHitFlatTableProducer() override {} + + void produce(edm::Event& event, edm::EventSetup const& iSetup) override { + std::vector rechit_ID; + std::vector rechit_energy; + std::vector rechit_x; + std::vector rechit_y; + std::vector rechit_z; + std::vector rechit_time; + std::vector rechit_radius; + std::vector rechit_simEnergy; + std::vector rechit_simEnergyEM; + std::vector rechit_simEnergyHad; + + for (auto const& rh_token : rechits_tokens_) { + edm::Handle rechit_handle; + event.getByToken(rh_token, rechit_handle); + const auto& rhColl = *rechit_handle; + for (auto const& rh : rhColl) { + rechit_energy.push_back(rh.energy()); + auto const rhPosition = rhtools_.getPosition(rh.detid()); + rechit_x.push_back(rhPosition.x()); + rechit_y.push_back(rhPosition.y()); + rechit_z.push_back(rhPosition.z()); + rechit_ID.push_back(rh.detid().rawId()); + rechit_time.push_back(rh.time()); + rechit_radius.push_back(rhtools_.getRadiusToSide(rh.detid())); + // const auto hitId = hitMap->find(DetId(rh.detid())); + // if (hitId != hitMap->end()) { + // rechit_simEnergy.push_back(hitIdToEnergies[hitId->second].energy); + // rechit_simEnergyEM.push_back(hitIdToEnergies[hitId->second].energyEM); + // rechit_simEnergyHad.push_back(hitIdToEnergies[hitId->second].energyHad); + // } + } + } + + auto tab = std::make_unique(rechit_ID.size(), objName_, false, false); + tab->addColumn("rechit_ID", rechit_ID, "Rechit ID"); + tab->addColumn("rechit_x", rechit_x, "Rechit X from rechittools"); + tab->addColumn("rechit_y", rechit_y, "Rechit Y from rechittools"); + tab->addColumn("rechit_z", rechit_z, "Rechit Z from rechittools"); + tab->addColumn("rechit_radius", rechit_radius, "Rechit radius to side from rechittools"); + + event.put(std::move(tab)); + } + + void beginRun(edm::Run const&, edm::EventSetup const& es) override { + edm::ESHandle geom = es.getHandle(caloGeometry_token_); + rhtools_.setGeometry(*geom); + } + + static void fillDescriptions(edm::ConfigurationDescriptions& descriptions) { + edm::ParameterSetDescription desc; + desc.add("objName", "rechits")->setComment("name of the nanoaod::FlatTable to extend with this table"); + desc.add>("label_rechits", + {edm::InputTag("HGCalRecHit", "HGCEERecHits"), + edm::InputTag("HGCalRecHit", "HGCHEFRecHits"), + edm::InputTag("HGCalRecHit", "HGCHEBRecHits")}); + descriptions.add("recHitTable", desc); + } + +protected: + const std::string objName_; + // const edm::EDGetTokenT> src_; + const std::vector> rechits_tokens_; + + edm::ESGetToken caloGeometry_token_; + hgcal::RecHitTools rhtools_; +}; + +#include "FWCore/Framework/interface/MakerMacros.h" +DEFINE_FWK_MODULE(RecHitFlatTableProducer); diff --git a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc index b69cb962b1b5f..50f3ea5bd6361 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc @@ -336,6 +336,7 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { maxVertices_(cfg.getParameter("maxVertices")), maxEdgesPerNode_(cfg.getParameter("maxEdgesPerNode")), hideLargeSimSourceVertices_(cfg.getParameter("hideLargeSimSourceVertices")), + dumpSimHits_(cfg.getParameter("dumpSimHits")), largeSimSourceVertexMinOutgoing_(cfg.getParameter("largeSimSourceVertexMinOutgoing")), hideZeroSimHitSubgraphs_(cfg.getParameter("hideZeroSimHitSubgraphs")) { const auto hgcalRecHitTags = cfg.getParameter>("hgcalRecHits"); @@ -388,6 +389,7 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { desc.add("hideLargeSimSourceVertices", true) ->setComment("If true, do not print large SIM-only source vertices in the DOT output"); + desc.add("dumpSimHits", true)->setComment("If true, dump all simhits"); desc.add("largeSimSourceVertexMinOutgoing", 50) ->setComment("Minimum outgoing multiplicity for hiding a SIM-only source vertex"); @@ -530,6 +532,18 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { << ", nSubgraphRecHits=" << subgraphSummary.nMatchedRecHits << ", subgraphSimHitEnergy=" << fmtEnergy(subgraphSummary.simHitEnergy) << ", subgraphRecHitEnergy=" << fmtEnergy(subgraphSummary.recHitEnergy); + if (dumpSimHits_) { + os << ", directHitsDetIds=\""; + for (auto h : directHits) { + os << h.detId << ","; + } + os << "\""; + os << ", directHitsEnergies=\""; + for (auto h : directHits) { + os << h.energy << ","; + } + os << "\""; + } } if (raw != nullptr) { @@ -760,6 +774,7 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { unsigned maxVertices_; unsigned maxEdgesPerNode_; bool hideLargeSimSourceVertices_; + bool dumpSimHits_; unsigned largeSimSourceVertexMinOutgoing_; bool hideZeroSimHitSubgraphs_; }; diff --git a/PhysicsTools/TruthInfo/python/recHitTable_cff.py b/PhysicsTools/TruthInfo/python/recHitTable_cff.py new file mode 100644 index 0000000000000..2ea45926a5791 --- /dev/null +++ b/PhysicsTools/TruthInfo/python/recHitTable_cff.py @@ -0,0 +1,4 @@ +from PhysicsTools.TruthInfo.recHitTable_cfi import recHitTable as _recHitTable + + +recHitTable = _recHitTable.clone() \ No newline at end of file diff --git a/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py b/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py index 0080adaf1d1f0..df785bc6cc54c 100644 --- a/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py +++ b/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py @@ -161,6 +161,28 @@ hideZeroSimHitSubgraphs=cms.bool(True), ) + +process.load("PhysicsTools.TruthInfo.recHitTable_cff") +# process.load('Configuration.EventContent.EventContent_cff') +process.NANOAODSIMoutput = cms.OutputModule("NanoAODOutputModule", + compressionAlgorithm = cms.untracked.string('ZLIB'), + compressionLevel = cms.untracked.int32(9), + dataset = cms.untracked.PSet( + dataTier = cms.untracked.string('NANOAODSIM'), + filterName = cms.untracked.string('') + ), + fileName = cms.untracked.string(os.path.join(args.outdir,f"rechits_nano{args.tag}.root")), + outputCommands = cms.untracked.vstring( + 'drop *', + 'keep nanoaodFlatTable_*Table_*_*', + # 'keep edmTriggerResults_*_*_*', + 'keep String_*_genModel_*', + 'keep nanoaodMergeableCounterTable_*Table_*_*', + 'keep nanoaodUniqueString_nanoMetadata_*_*', + 'keep nanoaodFlatTable_*Table*_*_*' +) +) + process.MessageLogger.cerr.threshold = "INFO" process.MessageLogger.cerr.default = cms.untracked.PSet( limit=cms.untracked.int32(0) @@ -185,4 +207,7 @@ + process.simHitToRecHitMapProducer + process.truthLogicalGraphHitIndexProducer + process.truthLogicalGraphDumper + + process.recHitTable ) + +process.nano_step = cms.EndPath(process.NANOAODSIMoutput) From 087456866d4bca4275f10adb3b657a2d9545c46c Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Fri, 15 May 2026 10:18:35 +0200 Subject: [PATCH 32/53] Add unit tests for TruthLogicalGraphPostProcessor make code check happy format --- .../plugins/LogicalGraphHitIndexProducer.cc | 4 +- PhysicsTools/TruthInfo/test/BuildFile.xml | 4 + .../test/TruthLogicalGraphPostProcessor_t.cpp | 663 ++++++++++++++++++ 3 files changed, 669 insertions(+), 2 deletions(-) create mode 100644 PhysicsTools/TruthInfo/test/BuildFile.xml create mode 100644 PhysicsTools/TruthInfo/test/TruthLogicalGraphPostProcessor_t.cpp diff --git a/PhysicsTools/TruthInfo/plugins/LogicalGraphHitIndexProducer.cc b/PhysicsTools/TruthInfo/plugins/LogicalGraphHitIndexProducer.cc index 1b07a032f89c4..831be2a45d06b 100644 --- a/PhysicsTools/TruthInfo/plugins/LogicalGraphHitIndexProducer.cc +++ b/PhysicsTools/TruthInfo/plugins/LogicalGraphHitIndexProducer.cc @@ -73,13 +73,13 @@ namespace { } bool inputTagLooksLikeHGCal(edm::InputTag const& tag) { - const std::string instance = tag.instance(); + const std::string& instance = tag.instance(); return instance.find("HGCHits") != std::string::npos || instance.find("HGCEE") != std::string::npos || instance.find("HGCHE") != std::string::npos; } bool inputTagLooksLikeHcal(edm::InputTag const& tag) { - const std::string instance = tag.instance(); + const std::string& instance = tag.instance(); return instance.find("HcalHits") != std::string::npos || instance.find("Hcal") != std::string::npos; } diff --git a/PhysicsTools/TruthInfo/test/BuildFile.xml b/PhysicsTools/TruthInfo/test/BuildFile.xml new file mode 100644 index 0000000000000..191307ada5470 --- /dev/null +++ b/PhysicsTools/TruthInfo/test/BuildFile.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/PhysicsTools/TruthInfo/test/TruthLogicalGraphPostProcessor_t.cpp b/PhysicsTools/TruthInfo/test/TruthLogicalGraphPostProcessor_t.cpp new file mode 100644 index 0000000000000..337fcbf660da2 --- /dev/null +++ b/PhysicsTools/TruthInfo/test/TruthLogicalGraphPostProcessor_t.cpp @@ -0,0 +1,663 @@ +#include "Utilities/Testing/interface/CppUnit_testdriver.icpp" +#include "cppunit/extensions/HelperMacros.h" + +#include +#include +#include +#include +#include + +#include "FWCore/Utilities/interface/Exception.h" +#include "PhysicsTools/TruthInfo/interface/Graph.h" +#include "PhysicsTools/TruthInfo/interface/TruthLogicalGraphPostProcessor.h" + +namespace { + + void buildCSR(uint32_t nSources, + std::vector>& pairs, + std::vector& offsets, + std::vector& flat) { + std::sort(pairs.begin(), pairs.end()); + pairs.erase(std::unique(pairs.begin(), pairs.end()), pairs.end()); + + offsets.assign(nSources + 1, 0); + + for (auto const& pair : pairs) { + CPPUNIT_ASSERT(pair.first < nSources); + ++offsets[pair.first + 1]; + } + + for (uint32_t i = 1; i <= nSources; ++i) { + offsets[i] += offsets[i - 1]; + } + + flat.assign(pairs.size(), 0); + auto cursor = offsets; + + for (auto const& pair : pairs) { + flat[cursor[pair.first]++] = pair.second; + } + } + + struct GraphBuilder { + explicit GraphBuilder(uint32_t nParticles, uint32_t nVertices) { + graph.particles.resize(nParticles); + graph.vertices.resize(nVertices); + } + + void setGenParticle(uint32_t particleId, int32_t pdgId, int16_t status, int32_t genNode) { + CPPUNIT_ASSERT(particleId < graph.nParticles()); + + auto& particle = graph.particles[particleId]; + particle.genNode = genNode; + particle.simNode = -1; + particle.pdgId = pdgId; + particle.status = status; + particle.statusFlags = 0; + particle.eventId = 0; + particle.genEvent = 0; + } + + void setSimParticle(uint32_t particleId, int32_t pdgId, int32_t simNode) { + CPPUNIT_ASSERT(particleId < graph.nParticles()); + + auto& particle = graph.particles[particleId]; + particle.genNode = -1; + particle.simNode = simNode; + particle.pdgId = pdgId; + particle.status = 0; + particle.statusFlags = 0; + particle.eventId = 1; + particle.genEvent = -1; + } + + void setGenSimParticle(uint32_t particleId, int32_t pdgId, int16_t status, int32_t genNode, int32_t simNode) { + CPPUNIT_ASSERT(particleId < graph.nParticles()); + + auto& particle = graph.particles[particleId]; + particle.genNode = genNode; + particle.simNode = simNode; + particle.pdgId = pdgId; + particle.status = status; + particle.statusFlags = 0; + particle.eventId = 1; + particle.genEvent = 0; + } + + void setGenVertex(uint32_t vertexId, int32_t genNode) { + CPPUNIT_ASSERT(vertexId < graph.nVertices()); + + auto& vertex = graph.vertices[vertexId]; + vertex.genNode = genNode; + vertex.simNode = -1; + vertex.eventId = 0; + vertex.genEvent = 0; + } + + void setSimVertex(uint32_t vertexId, int32_t simNode) { + CPPUNIT_ASSERT(vertexId < graph.nVertices()); + + auto& vertex = graph.vertices[vertexId]; + vertex.genNode = -1; + vertex.simNode = simNode; + vertex.eventId = 1; + vertex.genEvent = -1; + } + + void setGenSimVertex(uint32_t vertexId, int32_t genNode, int32_t simNode) { + CPPUNIT_ASSERT(vertexId < graph.nVertices()); + + auto& vertex = graph.vertices[vertexId]; + vertex.genNode = genNode; + vertex.simNode = simNode; + vertex.eventId = 1; + vertex.genEvent = 0; + } + + void addDecay(uint32_t particleId, uint32_t vertexId) { + CPPUNIT_ASSERT(particleId < graph.nParticles()); + CPPUNIT_ASSERT(vertexId < graph.nVertices()); + + particleToDecayVertexPairs.emplace_back(particleId, vertexId); + vertexToIncomingParticlePairs.emplace_back(vertexId, particleId); + } + + void addProduction(uint32_t vertexId, uint32_t particleId) { + CPPUNIT_ASSERT(vertexId < graph.nVertices()); + CPPUNIT_ASSERT(particleId < graph.nParticles()); + + vertexToOutgoingParticlePairs.emplace_back(vertexId, particleId); + particleToProductionVertexPairs.emplace_back(particleId, vertexId); + } + + truth::Graph finish() { + buildCSR(graph.nParticles(), + particleToDecayVertexPairs, + graph.particleToDecayVertexOffsets, + graph.particleToDecayVertices); + + buildCSR(graph.nParticles(), + particleToProductionVertexPairs, + graph.particleToProductionVertexOffsets, + graph.particleToProductionVertices); + + buildCSR(graph.nVertices(), + vertexToOutgoingParticlePairs, + graph.vertexToOutgoingParticleOffsets, + graph.vertexToOutgoingParticles); + + buildCSR(graph.nVertices(), + vertexToIncomingParticlePairs, + graph.vertexToIncomingParticleOffsets, + graph.vertexToIncomingParticles); + + CPPUNIT_ASSERT(graph.isConsistent()); + + return graph; + } + + truth::Graph graph; + + std::vector> particleToDecayVertexPairs; + std::vector> particleToProductionVertexPairs; + std::vector> vertexToOutgoingParticlePairs; + std::vector> vertexToIncomingParticlePairs; + }; + + uint32_t countParticlesWithPdgId(truth::Graph const& graph, int32_t pdgId) { + uint32_t count = 0; + + for (auto const& particle : graph.particles) { + if (particle.pdgId == pdgId) + ++count; + } + + return count; + } + + uint32_t countStableGenParticles(truth::Graph const& graph) { + uint32_t count = 0; + + for (auto const& particle : graph.particles) { + if (particle.hasGen() && particle.status == 1) + ++count; + } + + return count; + } + + bool hasGenSimParticleWithPdgId(truth::Graph const& graph, int32_t pdgId) { + return std::any_of(graph.particles.begin(), graph.particles.end(), [pdgId](auto const& particle) { + return particle.pdgId == pdgId && particle.hasGen() && particle.hasSim(); + }); + } + + bool hasArtificialVertex(truth::Graph const& graph) { + return std::any_of(graph.vertices.begin(), graph.vertices.end(), [](auto const& vertex) { + return !vertex.hasGen() && !vertex.hasSim(); + }); + } + + uint32_t artificialVertexId(truth::Graph const& graph) { + for (uint32_t i = 0; i < graph.nVertices(); ++i) { + auto const& vertex = graph.vertices[i]; + + if (!vertex.hasGen() && !vertex.hasSim()) + return i; + } + + CPPUNIT_ASSERT(false); + return 0; + } + + uint32_t findParticleWithPdgId(truth::Graph const& graph, int32_t pdgId) { + for (uint32_t i = 0; i < graph.nParticles(); ++i) { + if (graph.particles[i].pdgId == pdgId) + return i; + } + + CPPUNIT_ASSERT(false); + return 0; + } + + truth::LogicalGraphPostProcessingConfig defaultConfig() { + truth::LogicalGraphPostProcessingConfig config; + config.collapseIntermediateGenParticles = false; + config.seedPdgIds = {}; + config.seedParentDepth = 0; + config.ignoredPdgIds = {}; + config.ignoredParticleIds = {}; + return config; + } + + truth::Graph runPostProcessing(truth::Graph graph, truth::LogicalGraphPostProcessingConfig const& config) { + truth::TruthLogicalGraphPostProcessor processor(config); + return processor.process(std::move(graph)); + } + +} // namespace + +class TestTruthLogicalGraphPostProcessor : public CppUnit::TestFixture { + CPPUNIT_TEST_SUITE(TestTruthLogicalGraphPostProcessor); + CPPUNIT_TEST(testStatusOneGenParticlesAreNeverCollapsed); + CPPUNIT_TEST(testStableGenSimParticlesSurviveIntermediateCollapse); + CPPUNIT_TEST(testSeedCutKeepsUnrelatedStableGenSimParticlesThroughArtificialVertex); + CPPUNIT_TEST(testDagClosureKeepsAllParentsOfKeptVertices); + CPPUNIT_TEST(testIgnoredParticlesAreCollapsedAway); + CPPUNIT_TEST(testSeedCutWithIgnoredParticles); + CPPUNIT_TEST(testIgnoredParticleIdsAreCollapsedAway); + CPPUNIT_TEST_SUITE_END(); + +public: + void testStatusOneGenParticlesAreNeverCollapsed(); + void testStableGenSimParticlesSurviveIntermediateCollapse(); + void testSeedCutKeepsUnrelatedStableGenSimParticlesThroughArtificialVertex(); + void testDagClosureKeepsAllParentsOfKeptVertices(); + void testIgnoredParticlesAreCollapsedAway(); + void testSeedCutWithIgnoredParticles(); + void testIgnoredParticleIdsAreCollapsedAway(); +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestTruthLogicalGraphPostProcessor); + +void TestTruthLogicalGraphPostProcessor::testStatusOneGenParticlesAreNeverCollapsed() { + try { + GraphBuilder builder(2, 1); + + // This topology is intentionally unphysical: a status-1 GEN particle has a + // decay vertex to another same-PDG status-1 particle. The postprocessor must + // still never collapse a status-1 GEN particle. + builder.setGenParticle(0, 22, 1, 100); + builder.setGenParticle(1, 22, 1, 101); + builder.setGenVertex(0, 200); + + builder.addDecay(0, 0); + builder.addProduction(0, 1); + + auto graph = builder.finish(); + + auto config = defaultConfig(); + config.collapseIntermediateGenParticles = true; + + auto output = runPostProcessing(std::move(graph), config); + + CPPUNIT_ASSERT(output.isConsistent()); + CPPUNIT_ASSERT_EQUAL(uint32_t(2), output.nParticles()); + CPPUNIT_ASSERT_EQUAL(uint32_t(2), countParticlesWithPdgId(output, 22)); + CPPUNIT_ASSERT_EQUAL(uint32_t(2), countStableGenParticles(output)); + } catch (cms::Exception const& ex) { + std::cerr << ex.what() << std::endl; + CPPUNIT_ASSERT(false); + } +} + +void TestTruthLogicalGraphPostProcessor::testStableGenSimParticlesSurviveIntermediateCollapse() { + try { + GraphBuilder builder(3, 2); + + // gamma(status 2) -> gamma(status 1, GEN+SIM) + // e-(status 1, GEN+SIM) is an independent stable final-state particle. + builder.setGenParticle(0, 22, 2, 100); + builder.setGenSimParticle(1, 22, 1, 101, 1001); + builder.setGenSimParticle(2, 11, 1, 102, 1002); + + builder.setGenVertex(0, 200); + builder.setGenSimVertex(1, 201, 2001); + + builder.addDecay(0, 0); + builder.addProduction(0, 1); + builder.addProduction(1, 2); + + auto graph = builder.finish(); + + auto config = defaultConfig(); + config.collapseIntermediateGenParticles = true; + + auto output = runPostProcessing(std::move(graph), config); + + CPPUNIT_ASSERT(output.isConsistent()); + + // The intermediate gamma can collapse into the final stable gamma, but the + // status-1 GEN+SIM gamma must remain materialized. + CPPUNIT_ASSERT_EQUAL(uint32_t(2), output.nParticles()); + CPPUNIT_ASSERT(hasGenSimParticleWithPdgId(output, 22)); + CPPUNIT_ASSERT(hasGenSimParticleWithPdgId(output, 11)); + CPPUNIT_ASSERT_EQUAL(uint32_t(2), countStableGenParticles(output)); + } catch (cms::Exception const& ex) { + std::cerr << ex.what() << std::endl; + CPPUNIT_ASSERT(false); + } +} + +void TestTruthLogicalGraphPostProcessor::testSeedCutKeepsUnrelatedStableGenSimParticlesThroughArtificialVertex() { + try { + GraphBuilder builder(4, 3); + + // Interesting branch: + // Z -> gamma(status 1, GEN+SIM) + // + // Unrelated stable final state: + // artificial filtering must keep e-(status 1, GEN+SIM), but attach it to + // one artificial vertex instead of keeping its unrelated production chain. + builder.setGenParticle(0, 23, 2, 100); + builder.setGenSimParticle(1, 22, 1, 101, 1001); + builder.setGenParticle(2, 999, 2, 102); + builder.setGenSimParticle(3, 11, 1, 103, 1003); + + builder.setGenSimVertex(0, 200, 2000); + builder.setGenVertex(1, 201); + builder.setGenSimVertex(2, 202, 2002); + + builder.addDecay(0, 0); + builder.addProduction(0, 1); + + builder.addDecay(2, 1); + builder.addProduction(1, 3); + + // A second production vertex for the stable electron, to make sure the + // filtered graph does not keep unrelated upstream structure. + builder.addProduction(2, 3); + + auto graph = builder.finish(); + + auto config = defaultConfig(); + config.seedPdgIds = {22}; + config.seedParentDepth = 1; + + auto output = runPostProcessing(std::move(graph), config); + + CPPUNIT_ASSERT(output.isConsistent()); + + CPPUNIT_ASSERT(hasGenSimParticleWithPdgId(output, 22)); + CPPUNIT_ASSERT(hasGenSimParticleWithPdgId(output, 11)); + CPPUNIT_ASSERT(hasArtificialVertex(output)); + + const uint32_t electron = findParticleWithPdgId(output, 11); + const auto productionVertices = output.productionVertices(electron); + + CPPUNIT_ASSERT_EQUAL(std::size_t(1), productionVertices.size()); + + const uint32_t collapsedVertex = artificialVertexId(output); + CPPUNIT_ASSERT_EQUAL(collapsedVertex, productionVertices.front()); + + const auto artificialOutgoing = output.outgoingParticles(collapsedVertex); + CPPUNIT_ASSERT(std::find(artificialOutgoing.begin(), artificialOutgoing.end(), electron) != + artificialOutgoing.end()); + + const auto artificialIncoming = output.incomingParticles(collapsedVertex); + CPPUNIT_ASSERT(artificialIncoming.empty()); + } catch (cms::Exception const& ex) { + std::cerr << ex.what() << std::endl; + CPPUNIT_ASSERT(false); + } +} + +void TestTruthLogicalGraphPostProcessor::testDagClosureKeepsAllParentsOfKeptVertices() { + try { + GraphBuilder builder(5, 3); + + // DAG topology: + // + // H -> v0 -> pi0 + // Z --------^ + // pi0 -> v1 -> gamma + // e- stable, unrelated + // + // The seed is H. Keeping downstream from H keeps v0. Since v0 also has Z as + // an incoming parent, the postprocessor must include Z and the upstream path + // to Z instead of showing v0 as if it had only H as parent. + builder.setGenParticle(0, 25, 2, 100); + builder.setGenParticle(1, 23, 2, 101); + builder.setGenParticle(2, 111, 2, 102); + builder.setGenSimParticle(3, 22, 1, 103, 1003); + builder.setGenSimParticle(4, 11, 1, 104, 1004); + + builder.setGenVertex(0, 200); + builder.setGenVertex(1, 201); + builder.setGenSimVertex(2, 202, 2002); + + builder.addDecay(0, 0); + builder.addDecay(1, 0); + builder.addProduction(0, 2); + + builder.addDecay(2, 1); + builder.addProduction(1, 3); + + builder.addProduction(2, 4); + + auto graph = builder.finish(); + + auto config = defaultConfig(); + config.seedPdgIds = {25}; + config.seedParentDepth = 0; + + auto output = runPostProcessing(std::move(graph), config); + + CPPUNIT_ASSERT(output.isConsistent()); + + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 25)); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 23)); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 111)); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 22)); + + // The unrelated stable electron is still kept, but via the artificial vertex. + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 11)); + CPPUNIT_ASSERT(hasArtificialVertex(output)); + + const uint32_t pi0 = findParticleWithPdgId(output, 111); + const auto pi0ProductionVertices = output.productionVertices(pi0); + + CPPUNIT_ASSERT_EQUAL(std::size_t(1), pi0ProductionVertices.size()); + + const auto incoming = output.incomingParticles(pi0ProductionVertices.front()); + + CPPUNIT_ASSERT_EQUAL(std::size_t(2), incoming.size()); + + std::vector incomingPdgIds; + incomingPdgIds.reserve(incoming.size()); + + for (uint32_t parent : incoming) { + incomingPdgIds.push_back(output.particles[parent].pdgId); + } + + std::sort(incomingPdgIds.begin(), incomingPdgIds.end()); + + CPPUNIT_ASSERT_EQUAL(int32_t(23), incomingPdgIds[0]); + CPPUNIT_ASSERT_EQUAL(int32_t(25), incomingPdgIds[1]); + } catch (cms::Exception const& ex) { + std::cerr << ex.what() << std::endl; + CPPUNIT_ASSERT(false); + } +} + +void TestTruthLogicalGraphPostProcessor::testIgnoredParticlesAreCollapsedAway() { + try { + GraphBuilder builder(3, 2); + + // Z -> gamma -> e- + // + // If gamma is ignored, it should disappear and the two vertices around it + // should be merged, preserving a navigable Z -> e- connection. + builder.setGenParticle(0, 23, 2, 100); + builder.setGenParticle(1, 22, 2, 101); + builder.setGenSimParticle(2, 11, 1, 102, 1002); + + builder.setGenVertex(0, 200); + builder.setGenSimVertex(1, 201, 2001); + + builder.addDecay(0, 0); + builder.addProduction(0, 1); + + builder.addDecay(1, 1); + builder.addProduction(1, 2); + + auto graph = builder.finish(); + + auto config = defaultConfig(); + config.ignoredPdgIds = {22}; + + auto output = runPostProcessing(std::move(graph), config); + + CPPUNIT_ASSERT(output.isConsistent()); + + CPPUNIT_ASSERT_EQUAL(uint32_t(0), countParticlesWithPdgId(output, 22)); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 23)); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 11)); + + const uint32_t z = findParticleWithPdgId(output, 23); + const uint32_t electron = findParticleWithPdgId(output, 11); + + const auto decayVertices = output.decayVertices(z); + CPPUNIT_ASSERT_EQUAL(std::size_t(1), decayVertices.size()); + + const auto outgoing = output.outgoingParticles(decayVertices.front()); + CPPUNIT_ASSERT(std::find(outgoing.begin(), outgoing.end(), electron) != outgoing.end()); + } catch (cms::Exception const& ex) { + std::cerr << ex.what() << std::endl; + CPPUNIT_ASSERT(false); + } +} + +void TestTruthLogicalGraphPostProcessor::testSeedCutWithIgnoredParticles() { + try { + GraphBuilder builder(6, 4); + + // Interesting branch: + // H -> pi0 -> gamma(status 1, GEN+SIM) + // + // Extra parent on the kept pi0 production vertex: + // Z --------^ + // + // Unrelated stable final-state e- is kept through the artificial vertex. + // + // Then ignoredPdgIds removes pi0, merging H/Z directly to gamma. + builder.setGenParticle(0, 25, 2, 100); + builder.setGenParticle(1, 23, 2, 101); + builder.setGenParticle(2, 111, 2, 102); + builder.setGenSimParticle(3, 22, 1, 103, 1003); + builder.setGenParticle(4, 999, 2, 104); + builder.setGenSimParticle(5, 11, 1, 105, 1005); + + builder.setGenVertex(0, 200); + builder.setGenSimVertex(1, 201, 2001); + builder.setGenVertex(2, 202); + builder.setGenSimVertex(3, 203, 2003); + + builder.addDecay(0, 0); + builder.addDecay(1, 0); + builder.addProduction(0, 2); + + builder.addDecay(2, 1); + builder.addProduction(1, 3); + + builder.addDecay(4, 2); + builder.addProduction(2, 5); + + builder.addProduction(3, 5); + + auto graph = builder.finish(); + + auto config = defaultConfig(); + config.seedPdgIds = {25}; + config.seedParentDepth = 0; + config.ignoredPdgIds = {111}; + + auto output = runPostProcessing(std::move(graph), config); + + CPPUNIT_ASSERT(output.isConsistent()); + + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 25)); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 23)); + CPPUNIT_ASSERT_EQUAL(uint32_t(0), countParticlesWithPdgId(output, 111)); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 22)); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 11)); + + CPPUNIT_ASSERT(hasGenSimParticleWithPdgId(output, 22)); + CPPUNIT_ASSERT(hasGenSimParticleWithPdgId(output, 11)); + CPPUNIT_ASSERT(hasArtificialVertex(output)); + + const uint32_t gamma = findParticleWithPdgId(output, 22); + const auto gammaProductionVertices = output.productionVertices(gamma); + + CPPUNIT_ASSERT_EQUAL(std::size_t(1), gammaProductionVertices.size()); + + const auto incoming = output.incomingParticles(gammaProductionVertices.front()); + + std::vector incomingPdgIds; + incomingPdgIds.reserve(incoming.size()); + + for (uint32_t parent : incoming) { + incomingPdgIds.push_back(output.particles[parent].pdgId); + } + + std::sort(incomingPdgIds.begin(), incomingPdgIds.end()); + + CPPUNIT_ASSERT_EQUAL(std::size_t(2), incomingPdgIds.size()); + CPPUNIT_ASSERT_EQUAL(int32_t(23), incomingPdgIds[0]); + CPPUNIT_ASSERT_EQUAL(int32_t(25), incomingPdgIds[1]); + + const uint32_t electron = findParticleWithPdgId(output, 11); + const auto electronProductionVertices = output.productionVertices(electron); + + CPPUNIT_ASSERT_EQUAL(std::size_t(1), electronProductionVertices.size()); + CPPUNIT_ASSERT_EQUAL(artificialVertexId(output), electronProductionVertices.front()); + } catch (cms::Exception const& ex) { + std::cerr << ex.what() << std::endl; + CPPUNIT_ASSERT(false); + } +} + +void TestTruthLogicalGraphPostProcessor::testIgnoredParticleIdsAreCollapsedAway() { + try { + GraphBuilder builder(4, 3); + + // Z -> a -> gamma -> e- + // + // Only particle id 2 is ignored. This verifies that ignoredParticleIds is + // independent from PDG id matching: the status-1 gamma is removed because its + // logical id is explicitly listed, not because all photons are ignored. + builder.setGenParticle(0, 23, 2, 100); + builder.setGenParticle(1, 36, 2, 101); + builder.setGenSimParticle(2, 22, 1, 102, 1002); + builder.setGenSimParticle(3, 11, 1, 103, 1003); + + builder.setGenVertex(0, 200); + builder.setGenSimVertex(1, 201, 2001); + builder.setGenSimVertex(2, 202, 2002); + + builder.addDecay(0, 0); + builder.addProduction(0, 1); + + builder.addDecay(1, 1); + builder.addProduction(1, 2); + + builder.addDecay(2, 2); + builder.addProduction(2, 3); + + auto graph = builder.finish(); + + auto config = defaultConfig(); + config.ignoredParticleIds = {2}; + + auto output = runPostProcessing(std::move(graph), config); + + CPPUNIT_ASSERT(output.isConsistent()); + + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 23)); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 36)); + CPPUNIT_ASSERT_EQUAL(uint32_t(0), countParticlesWithPdgId(output, 22)); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 11)); + + const uint32_t a = findParticleWithPdgId(output, 36); + const uint32_t electron = findParticleWithPdgId(output, 11); + + const auto decayVertices = output.decayVertices(a); + CPPUNIT_ASSERT_EQUAL(std::size_t(1), decayVertices.size()); + + const auto outgoing = output.outgoingParticles(decayVertices.front()); + CPPUNIT_ASSERT(std::find(outgoing.begin(), outgoing.end(), electron) != outgoing.end()); + } catch (cms::Exception const& ex) { + std::cerr << ex.what() << std::endl; + CPPUNIT_ASSERT(false); + } +} From 33f1721d1fce3c34b2f5871ea9ba2d7901d73d13 Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Fri, 15 May 2026 19:38:22 +0200 Subject: [PATCH 33/53] Create enableTruth ProcessModifier --- Configuration/ProcessModifiers/python/enableTruth_cff.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Configuration/ProcessModifiers/python/enableTruth_cff.py diff --git a/Configuration/ProcessModifiers/python/enableTruth_cff.py b/Configuration/ProcessModifiers/python/enableTruth_cff.py new file mode 100644 index 0000000000000..3b82cbd1f5a10 --- /dev/null +++ b/Configuration/ProcessModifiers/python/enableTruth_cff.py @@ -0,0 +1,3 @@ +import FWCore.ParameterSet.Config as cms + +enableTruth = cms.Modifier() \ No newline at end of file From 871370d2db2f445bb2851dcddf8ea4df8f694124 Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Fri, 15 May 2026 19:39:05 +0200 Subject: [PATCH 34/53] Create enableTruth Run4 workflows --- .../python/upgradeWorkflowComponents.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Configuration/PyReleaseValidation/python/upgradeWorkflowComponents.py b/Configuration/PyReleaseValidation/python/upgradeWorkflowComponents.py index fb1e68b543434..869aa9459435b 100644 --- a/Configuration/PyReleaseValidation/python/upgradeWorkflowComponents.py +++ b/Configuration/PyReleaseValidation/python/upgradeWorkflowComponents.py @@ -1006,6 +1006,33 @@ def condition(self, fragment, stepList, key, hasHarvest): upgradeWFs['ticlv5_TrackLinkingGNN'].step3 = {'--procModifiers': 'ticlv5_TrackLinkingGNN'} upgradeWFs['ticlv5_TrackLinkingGNN'].step4 = {'--procModifiers': 'ticlv5_TrackLinkingGNN'} + + +class UpgradeWorkflow_enableTruth(UpgradeWorkflow): + def setup_(self, step, stepName, stepDict, k, properties): + if 'RecoGlobal' in step: + stepDict[stepName][k] = deepcopy(stepDict[step][k]) + + if '--procModifiers' in stepDict[stepName][k]: + stepDict[stepName][k]['--procModifiers'] += ',enableTruth' + else: + stepDict[stepName][k]['--procModifiers'] = 'enableTruth' + + def condition(self, fragment, stepList, key, hasHarvest): + return 'Run4' in key + + +upgradeWFs['enableTruth'] = UpgradeWorkflow_enableTruth( + steps = [ + 'RecoGlobal', + ], + PU = [ + 'RecoGlobal', + ], + suffix = '_enableTruth', + offset = 0.88, +) + # L3 Tracker Muon Outside-In reconstruction first class UpgradeWorkflow_phase2L3MuonsOIFirst(UpgradeWorkflow): def setup_(self, step, stepName, stepDict, k, properties): From d463a312dfcd14f648c795598e5390cb1fa00b45 Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Thu, 4 Jun 2026 16:53:30 +0200 Subject: [PATCH 35/53] Merge vertices if overlapping --- .../TruthLogicalGraphPostProcessor.h | 12 ++ .../src/TruthLogicalGraphPostProcessor.cc | 187 ++++++++++++++++++ 2 files changed, 199 insertions(+) diff --git a/PhysicsTools/TruthInfo/interface/TruthLogicalGraphPostProcessor.h b/PhysicsTools/TruthInfo/interface/TruthLogicalGraphPostProcessor.h index 601d60b245256..d54b5c7bc62a7 100644 --- a/PhysicsTools/TruthInfo/interface/TruthLogicalGraphPostProcessor.h +++ b/PhysicsTools/TruthInfo/interface/TruthLogicalGraphPostProcessor.h @@ -30,6 +30,18 @@ namespace truth { // These ids refer to the graph state at the moment the ignored-particle // collapsing step is applied. std::vector ignoredParticleIds; + + // If true, post-processing is allowed to merge a GEN-only vertex and a + // SIM-only vertex when they are connected to the same visible particle and + // their four-positions are compatible within genSimVertexPositionTolerance. + // + // This is intentionally done at logical-graph level: the raw graph can still + // keep GenVertex and SimVertex as distinct provenance objects. + bool mergeGenSimVerticesByPosition = true; + + // Absolute tolerance used for each x, y, z, t component when matching + // GEN-only and SIM-only vertices by position. + double genSimVertexPositionTolerance = 1e-6; }; class TruthLogicalGraphPostProcessor { diff --git a/PhysicsTools/TruthInfo/src/TruthLogicalGraphPostProcessor.cc b/PhysicsTools/TruthInfo/src/TruthLogicalGraphPostProcessor.cc index 1cf6e1013eda1..35e80502f6ad9 100644 --- a/PhysicsTools/TruthInfo/src/TruthLogicalGraphPostProcessor.cc +++ b/PhysicsTools/TruthInfo/src/TruthLogicalGraphPostProcessor.cc @@ -1,6 +1,7 @@ #include "PhysicsTools/TruthInfo/interface/TruthLogicalGraphPostProcessor.h" #include +#include #include #include #include @@ -76,6 +77,37 @@ namespace { return particle.hasGen() && particle.status == 1; } + bool isGenOnlyVertex(truth::VertexData const& vertex) { return vertex.hasGen() && !vertex.hasSim(); } + + bool isSimOnlyVertex(truth::VertexData const& vertex) { return !vertex.hasGen() && vertex.hasSim(); } + + bool compatibleVertexPositions(truth::VertexData const& a, truth::VertexData const& b, double tolerance) { + return std::abs(a.position.px() - b.position.px()) <= tolerance && + std::abs(a.position.py() - b.position.py()) <= tolerance && + std::abs(a.position.pz() - b.position.pz()) <= tolerance && + std::abs(a.position.e() - b.position.e()) <= tolerance; + } + + bool canMergeGenSimVerticesByPosition(truth::VertexData const& a, truth::VertexData const& b, double tolerance) { + const bool genSimPair = (isGenOnlyVertex(a) && isSimOnlyVertex(b)) || (isSimOnlyVertex(a) && isGenOnlyVertex(b)); + + if (!genSimPair) + return false; + bool areCompatible = compatibleVertexPositions(a, b, tolerance); + if (!areCompatible){ + std::cout << "vertices not merged: (" << a.position.px() << " , " << a.position.py() << " , " << a.position.pz() + << ") too far from " << "(" << b.position.px() << " , " << b.position.py() << " , " << b.position.pz() + << ")" << std::endl; + } + else + { + std::cout << "vertices successfully merged: (" << a.position.px() << " , " << a.position.py() << " , " << a.position.pz() + << ") too far from " << "(" << b.position.px() << " , " << b.position.py() << " , " << b.position.pz() + << ")" << std::endl; + } + return areCompatible; + } + void buildCSR(uint32_t nSources, std::vector>& pairs, std::vector& offsets, @@ -684,6 +716,147 @@ namespace { return rebuildFilteredGraph(input, keepParticle, keepVertex, connectToCollapsedStableVertex); } + void mergeVertexPayload(truth::VertexData& output, truth::VertexData const& input) { + if (input.hasGen()) { + output.genNode = input.genNode; + output.genEvent = input.genEvent; + + // Prefer the GEN position for merged GEN+SIM vertices. + output.position = input.position; + } + + if (input.hasSim()) { + output.simNode = input.simNode; + output.eventId = input.eventId; + + if (!output.hasGen()) { + output.position = input.position; + } + } + } + + truth::Graph mergeGenSimVerticesByPosition(truth::Graph const& input, double tolerance) { + if (input.empty()) + return input; + + const uint32_t nParticles = input.nParticles(); + const uint32_t nVertices = input.nVertices(); + + if (nVertices == 0) + return input; + + DSU vertexDSU(static_cast(nVertices)); + + auto tryMergeVertexList = [&](std::vector const& vertices) { + for (std::size_t i = 0; i < vertices.size(); ++i) { + const uint32_t first = vertices[i].id(); + + if (first >= nVertices) + continue; + + for (std::size_t j = i + 1; j < vertices.size(); ++j) { + const uint32_t second = vertices[j].id(); + + if (second >= nVertices) + continue; + + if (canMergeGenSimVerticesByPosition(input.vertices[first], input.vertices[second], tolerance)) { + vertexDSU.unite(static_cast(first), static_cast(second)); + } + } + } + }; + + for (uint32_t particleId = 0; particleId < nParticles; ++particleId) { + tryMergeVertexList(input.particle(particleId).productionVertices()); + tryMergeVertexList(input.particle(particleId).decayVertices()); + } + + bool changed = false; + + for (uint32_t vertexId = 0; vertexId < nVertices; ++vertexId) { + if (vertexDSU.find(static_cast(vertexId)) != static_cast(vertexId)) { + changed = true; + break; + } + } + + if (!changed) + return input; + + truth::Graph output; + + output.particles = input.particles; + + std::unordered_map vertexRepToNew; + vertexRepToNew.reserve(nVertices); + + std::vector oldVertexToNew(nVertices, -1); + + for (uint32_t oldVertex = 0; oldVertex < nVertices; ++oldVertex) { + const int rep = vertexDSU.find(static_cast(oldVertex)); + + auto inserted = vertexRepToNew.emplace(rep, static_cast(output.vertices.size())); + const uint32_t newVertex = inserted.first->second; + + if (inserted.second) { + output.vertices.emplace_back(); + } + + oldVertexToNew[oldVertex] = static_cast(newVertex); + mergeVertexPayload(output.vertices[newVertex], input.vertices[oldVertex]); + } + + std::vector> particleToDecayVertexPairs; + std::vector> particleToProductionVertexPairs; + std::vector> vertexToOutgoingParticlePairs; + std::vector> vertexToIncomingParticlePairs; + + for (uint32_t oldVertex = 0; oldVertex < nVertices; ++oldVertex) { + const int32_t newVertex = oldVertexToNew[oldVertex]; + if (newVertex < 0) + continue; + + for (uint32_t particleId : input.incomingParticles(oldVertex)) { + if (particleId >= nParticles) + continue; + + particleToDecayVertexPairs.emplace_back(particleId, static_cast(newVertex)); + vertexToIncomingParticlePairs.emplace_back(static_cast(newVertex), particleId); + } + + for (uint32_t particleId : input.outgoingParticles(oldVertex)) { + if (particleId >= nParticles) + continue; + + vertexToOutgoingParticlePairs.emplace_back(static_cast(newVertex), particleId); + particleToProductionVertexPairs.emplace_back(particleId, static_cast(newVertex)); + } + } + + buildCSR(output.nParticles(), + particleToDecayVertexPairs, + output.particleToDecayVertexOffsets, + output.particleToDecayVertices); + + buildCSR(output.nParticles(), + particleToProductionVertexPairs, + output.particleToProductionVertexOffsets, + output.particleToProductionVertices); + + buildCSR(output.nVertices(), + vertexToOutgoingParticlePairs, + output.vertexToOutgoingParticleOffsets, + output.vertexToOutgoingParticles); + + buildCSR(output.nVertices(), + vertexToIncomingParticlePairs, + output.vertexToIncomingParticleOffsets, + output.vertexToIncomingParticles); + + return output; + } + truth::Graph collapseIgnoredParticles(truth::Graph const& input, std::vector const& ignoredPdgIds, std::vector const& ignoredParticleIds) { @@ -876,6 +1049,14 @@ namespace truth { "Logical particle ids to remove from the final logical graph. These ids refer to the graph state at " "the moment the ignored-particle collapsing step is applied."); + desc.add("mergeGenSimVerticesByPosition", true) + ->setComment( + "If true, merge GEN-only and SIM-only logical vertices connected to the same particle when their " + "positions are compatible within genSimVertexPositionTolerance."); + + desc.add("genSimVertexPositionTolerance", 1e-6) + ->setComment("Absolute tolerance applied independently to x, y, z, and t when merging GEN and SIM vertices."); + return desc; } @@ -887,6 +1068,8 @@ namespace truth { config.seedParentDepth = pset.getParameter("seedParentDepth"); config.ignoredPdgIds = pset.getParameter>("ignoredPdgIds"); config.ignoredParticleIds = pset.getParameter>("ignoredParticleIds"); + config.mergeGenSimVerticesByPosition = pset.getParameter("mergeGenSimVerticesByPosition"); + config.genSimVertexPositionTolerance = pset.getParameter("genSimVertexPositionTolerance"); return config; } @@ -898,6 +1081,10 @@ namespace truth { input = filterGraphBySeedPdgIds(input, config_); + if (config_.mergeGenSimVerticesByPosition) { + input = mergeGenSimVerticesByPosition(input, config_.genSimVertexPositionTolerance); + } + if (!config_.ignoredPdgIds.empty() || !config_.ignoredParticleIds.empty()) { input = collapseIgnoredParticles(input, config_.ignoredPdgIds, config_.ignoredParticleIds); } From e39b648895591ac55d2504a5e401b881ac9cb378 Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Thu, 4 Jun 2026 16:59:12 +0200 Subject: [PATCH 36/53] Produce Truth graph with validation format --- .../src/TruthLogicalGraphPostProcessor.cc | 12 +- .../python/globalValidation_cff.py | 11 ++ .../python/truthPrevalidation_cff.py | 114 ++++++++++++++++++ 3 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 Validation/Configuration/python/truthPrevalidation_cff.py diff --git a/PhysicsTools/TruthInfo/src/TruthLogicalGraphPostProcessor.cc b/PhysicsTools/TruthInfo/src/TruthLogicalGraphPostProcessor.cc index 35e80502f6ad9..05719f015fc99 100644 --- a/PhysicsTools/TruthInfo/src/TruthLogicalGraphPostProcessor.cc +++ b/PhysicsTools/TruthInfo/src/TruthLogicalGraphPostProcessor.cc @@ -94,16 +94,14 @@ namespace { if (!genSimPair) return false; bool areCompatible = compatibleVertexPositions(a, b, tolerance); - if (!areCompatible){ + if (!areCompatible) { std::cout << "vertices not merged: (" << a.position.px() << " , " << a.position.py() << " , " << a.position.pz() << ") too far from " << "(" << b.position.px() << " , " << b.position.py() << " , " << b.position.pz() << ")" << std::endl; - } - else - { - std::cout << "vertices successfully merged: (" << a.position.px() << " , " << a.position.py() << " , " << a.position.pz() - << ") too far from " << "(" << b.position.px() << " , " << b.position.py() << " , " << b.position.pz() - << ")" << std::endl; + } else { + std::cout << "vertices successfully merged: (" << a.position.px() << " , " << a.position.py() << " , " + << a.position.pz() << ") too far from " << "(" << b.position.px() << " , " << b.position.py() << " , " + << b.position.pz() << ")" << std::endl; } return areCompatible; } diff --git a/Validation/Configuration/python/globalValidation_cff.py b/Validation/Configuration/python/globalValidation_cff.py index 9fcf1c848e41c..b219010db119a 100644 --- a/Validation/Configuration/python/globalValidation_cff.py +++ b/Validation/Configuration/python/globalValidation_cff.py @@ -48,12 +48,15 @@ from Validation.Configuration.ecalSimValid_cff import * from Validation.SiTrackerPhase2V.Phase2TrackerValidationFirstStep_cff import * + # filter/producer "pre-" sequence for globalValidation globalPrevalidationTracking = cms.Sequence( simHitTPAssocProducer * tracksValidation * vertexValidation ) + + globalPrevalidation = cms.Sequence( globalPrevalidationTracking * photonPrevalidationSequence @@ -61,6 +64,14 @@ * prebTagSequenceMC ) +from Configuration.ProcessModifiers.enableTruth_cff import enableTruth +from Validation.Configuration.truthPrevalidation_cff import * + +_globalPrevalidationWithTruth = globalPrevalidation.copy() +_globalPrevalidationWithTruth += truthGraphPrevalidation + +enableTruth.toReplaceWith(globalPrevalidation, _globalPrevalidationWithTruth) + # filter/producer "pre-" sequence for validation_preprod preprodPrevalidation = cms.Sequence( tracksPreValidation diff --git a/Validation/Configuration/python/truthPrevalidation_cff.py b/Validation/Configuration/python/truthPrevalidation_cff.py new file mode 100644 index 0000000000000..ce5ca6ac631b2 --- /dev/null +++ b/Validation/Configuration/python/truthPrevalidation_cff.py @@ -0,0 +1,114 @@ +import FWCore.ParameterSet.Config as cms + +from PhysicsTools.TruthInfo.truthGraphProducer_cfi import truthGraphProducer + +truthLogicalGraphProducer = cms.EDProducer( + "TruthLogicalGraphProducer", + + src = cms.InputTag("truthGraphProducer"), + + simTracks = cms.InputTag("g4SimHits"), + simVertices = cms.InputTag("g4SimHits"), + + genEventHepMC3 = cms.InputTag("generatorSmeared"), + genEventHepMC = cms.InputTag("generatorSmeared"), + + mergeGenSimVertices = cms.bool(True), + + postProcessing = cms.PSet( + collapseIntermediateGenParticles = cms.bool(True), + seedPdgIds = cms.vint32(), + seedParentDepth = cms.uint32(0), + ignoredPdgIds = cms.vint32(), + ignoredParticleIds = cms.vuint32(), + + mergeGenSimVerticesByPosition = cms.bool(True), + genSimVertexPositionTolerance = cms.double(1e-6), + ), +) + +simHitToRecHitMapProducer = cms.EDProducer( + "SimHitToRecHitMapProducer", + + hgcalRecHits = cms.VInputTag( + cms.InputTag("HGCalRecHit", "HGCEERecHits"), + cms.InputTag("HGCalRecHit", "HGCHEFRecHits"), + cms.InputTag("HGCalRecHit", "HGCHEBRecHits"), + ), + + pfRecHits = cms.VInputTag( + cms.InputTag("particleFlowRecHitECAL", "Cleaned"), + cms.InputTag("particleFlowRecHitHBHE", "Cleaned"), + cms.InputTag("particleFlowRecHitHF", "Cleaned"), + cms.InputTag("particleFlowRecHitHO", "Cleaned"), + ), +) + +truthLogicalGraphHitIndexProducer = cms.EDProducer( + "TruthLogicalGraphHitIndexProducer", + + src = cms.InputTag("truthLogicalGraphProducer"), + rawSrc = cms.InputTag("truthGraphProducer"), + recHitMap = cms.InputTag("simHitToRecHitMapProducer"), + + simHitCollections = cms.VInputTag( + cms.InputTag("g4SimHits", "HGCHitsEE"), + cms.InputTag("g4SimHits", "HGCHitsHEfront"), + cms.InputTag("g4SimHits", "HGCHitsHEback"), + cms.InputTag("g4SimHits", "EcalHitsEB"), + cms.InputTag("g4SimHits", "HcalHits"), + ), + + doHGCalRelabelling = cms.bool(False), +) + +truthGraphDumper = cms.EDAnalyzer( + "TruthGraphDumper", + src=cms.InputTag("truthGraphProducer"), + dotFile=cms.string("truthgraph.dot"), # output file + maxNodes=cms.uint32(20000), + maxEdgesPerNode=cms.uint32(50), + simTracks=cms.InputTag("g4SimHits"), + simVertices=cms.InputTag("g4SimHits"), + genEventHepMC=cms.InputTag("generatorSmeared"), + genEventHepMC3=cms.InputTag("generatorSmeared"), +) + + +truthLogicalGraphDumper = cms.EDAnalyzer( + "TruthLogicalGraphDumper", + src=cms.InputTag("truthLogicalGraphProducer"), + rawSrc=cms.InputTag("truthGraphProducer"), + hitIndex=cms.InputTag("truthLogicalGraphHitIndexProducer"), + + hgcalRecHits=cms.VInputTag( + cms.InputTag("HGCalRecHit", "HGCEERecHits", "RECO"), + cms.InputTag("HGCalRecHit", "HGCHEFRecHits", "RECO"), + cms.InputTag("HGCalRecHit", "HGCHEBRecHits", "RECO"), + ), + + pfRecHits=cms.VInputTag( + cms.InputTag("particleFlowRecHitECAL", "Cleaned", "RECO"), + cms.InputTag("particleFlowRecHitHBHE", "Cleaned", "RECO"), + cms.InputTag("particleFlowRecHitHF", "Cleaned", "RECO"), + cms.InputTag("particleFlowRecHitHO", "Cleaned", "RECO"), + ), + + dotFile=cms.string("truthlogicalgraph.dot"), # output file + + maxParticles=cms.uint32(20000), + maxVertices=cms.uint32(20000), + maxEdgesPerNode=cms.uint32(300), + + hideLargeSimSourceVertices=cms.bool(True), + largeSimSourceVertexMinOutgoing=cms.uint32(50), + + hideZeroSimHitSubgraphs=cms.bool(True), +) + +truthGraphPrevalidation = cms.Sequence( + truthGraphProducer + + truthLogicalGraphProducer + + simHitToRecHitMapProducer + + truthLogicalGraphHitIndexProducer +) \ No newline at end of file From 7290e6e62ce63eaccaa2f32c4706ac016ff2539f Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Mon, 8 Jun 2026 16:22:42 +0200 Subject: [PATCH 37/53] Adding statusFlags --- PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc b/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc index 5d95788119b05..92cbc924d67f3 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc @@ -393,6 +393,8 @@ class TruthGraphDumper : public edm::one::EDAnalyzer<> { const auto pdg = g.nodePdgId(i); const auto st = g.nodeStatus(i); const auto eid = g.nodeEventId(i); + const auto flags = g.nodeStatusFlags(i); + const std::string flagsLabel = statusFlagsLabel(flags); // SimTrack enrichment bool crossedBoundary = false; @@ -417,8 +419,8 @@ class TruthGraphDumper : public edm::one::EDAnalyzer<> { if (crossedBoundary) os << "color=\"red\", penwidth=2, "; - os << "pdg=" << pdg << ", status=" << st << ", eid=" << eid << ","; - // --- GEN enrichment + os << "pdg=" << pdg << ", status=" << st << ", statusFlags=" << flags + << ", statusFlagsLabel=" << dotQuote(flagsLabel) << ", eid=" << eid << ","; // --- GEN enrichment if (r.kind == TruthGraph::NodeKind::GenEvent) { if (ev2) { os << "HepMCversion=2, event=" << ev2->event_number() << ", spid=" << ev2->signal_process_id() << ","; @@ -491,6 +493,10 @@ class TruthGraphDumper : public edm::one::EDAnalyzer<> { os << " \n"; if (st != 0) os << " \n"; + if (flags != 0) { + os << " \n"; + os << " \n"; + } if (eid != 0) os << " \n"; From 71996fb0c785ce1748d187abbd0e1d109f5d6907 Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Fri, 12 Jun 2026 16:38:35 +0200 Subject: [PATCH 38/53] TruthInfo: fix vertex time units, add SimVertex->GenVertex link, enrich SimVertex dumps Vertex time normalization (cm, ns): - SimVertex position().t() is stored in seconds and HepMC GenVertex time is c*t in mm; previously only x/y/z were converted (mm->cm) while t was left raw, so GEN and SIM times were incomparable (and SimVertex t printed as 0). - TruthLogicalGraphProducer now converts GEN t (mm/c -> ns) and SIM t (s -> ns) so VertexData.position is consistently (cm, ns). - Re-enable the t term in compatibleVertexPositions; GEN/SIM primary vertices now agree to ~1e-5 ns and merge correctly. Raw SimVertex -> GenVertex provenance connection: - New TruthGraph::simVtxToGen association (SimVertex node -> GenVertex node), derived from primary SimTracks (production SimVertex <-> GenParticle production GenVertex). Conflicts are logged. Emitted as SimToGen edges. - TruthGraphProducer fills it and reports simVtxToGenVertexLinks. Dumper enrichment: - TruthGraphDumper now fetches the SimVertexContainer and annotates SimVertex nodes with position (cm,ns), vertexId, processType, parentTrackId, noParent, eventId (bx/evtInBx) and the associated GenVertex_nodeId. - TruthLogicalGraphDumper vertex labels use a 4D (x,y,z,t) formatter. Also fix a stale GEN/SIM vertex-merge debug print that used px()/py()/pz() and a misleading message. format --- PhysicsTools/TruthInfo/interface/TruthGraph.h | 8 +++ .../TruthInfo/plugins/TruthGraphDumper.cc | 38 ++++++++++++++ .../TruthInfo/plugins/TruthGraphProducer.cc | 50 ++++++++++++++++++- .../plugins/TruthLogicalGraphDumper.cc | 31 +++++++++--- .../plugins/TruthLogicalGraphProducer.cc | 23 ++++++--- .../src/TruthLogicalGraphPostProcessor.cc | 22 ++++---- 6 files changed, 147 insertions(+), 25 deletions(-) diff --git a/PhysicsTools/TruthInfo/interface/TruthGraph.h b/PhysicsTools/TruthInfo/interface/TruthGraph.h index ad5a1ec134da6..9e652c83e25af 100644 --- a/PhysicsTools/TruthInfo/interface/TruthGraph.h +++ b/PhysicsTools/TruthInfo/interface/TruthGraph.h @@ -60,6 +60,12 @@ class TruthGraph { std::vector simTrackToGen; // SimTrack nodeId -> GenParticle nodeId std::vector simTrackToVtx; // SimTrack nodeId -> SimVertex nodeId + // SimVertex nodeId -> GenVertex nodeId provenance association, -1 if none. + // Derived from primary SimTracks: a SimTrack's production SimVertex corresponds + // to the production GenVertex of its associated GenParticle. Only meaningful for + // SimVertex nodes. + std::vector simVtxToGen; + uint32_t nNodes() const { return static_cast(nodes.size()); } uint32_t nEdges() const { return static_cast(edges.size()); } @@ -94,6 +100,8 @@ class TruthGraph { return (nodeId < simTrackToVtx.size()) ? simTrackToVtx[nodeId] : -1; } + int32_t nodeSimVtxToGen(uint32_t nodeId) const { return (nodeId < simVtxToGen.size()) ? simVtxToGen[nodeId] : -1; } + bool isConsistent() const; }; diff --git a/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc b/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc index 92cbc924d67f3..a2a34a233b36c 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthGraphDumper.cc @@ -138,6 +138,16 @@ namespace { return ss.str(); } + // SimVertex position is in cm but stores time in seconds; display as (cm, ns). + std::string fmtSimVertexX4(SimVertex const& sv) { + auto const& p = sv.position(); + constexpr double sToNs = 1e9; + std::ostringstream ss; + ss.setf(std::ios::fixed); + ss << std::setprecision(3) << "(" << p.x() << ", " << p.y() << ", " << p.z() << ", " << p.t() * sToNs << ")"; + return ss.str(); + } + const char* kindName(TruthGraph::NodeKind k) { switch (k) { case TruthGraph::NodeKind::GenEvent: @@ -321,6 +331,10 @@ class TruthGraphDumper : public edm::one::EDAnalyzer<> { edm::Handle hSimTracks; evt.getByToken(simTracksToken_, hSimTracks); + // SimVertexContainer was already consumed; fetch it to enrich SimVertex nodes. + edm::Handle hSimVertices; + evt.getByToken(simVerticesToken_, hSimVertices); + std::unordered_map tidToIndex; if (hSimTracks.isValid()) { tidToIndex.reserve(hSimTracks->size() * 2); @@ -466,6 +480,16 @@ class TruthGraphDumper : public edm::one::EDAnalyzer<> { << ", nIn=" << v->particles_in().size() << ", nOut=" << v->particles_out().size() << ","; } } + } else if (r.kind == TruthGraph::NodeKind::SimVertex && hSimVertices.isValid()) { + // SimVertex node key == index into the SimVertexContainer (see TruthGraphProducer). + const int64_t idx = r.key; + if (idx >= 0 && static_cast(idx) < hSimVertices->size()) { + auto const& sv = (*hSimVertices)[static_cast(idx)]; + os << "x4=" << dotQuote(fmtSimVertexX4(sv)) << ", vertexId=" << sv.vertexId() + << ", processType=" << sv.processType() << ", parentTrackId=" << sv.parentIndex() + << ", noParent=" << sv.noParent() << ", bx=" << sv.eventId().bunchCrossing() + << ", evtInBx=" << sv.eventId().event() << ", GenVertex_nodeId=" << g.nodeSimVtxToGen(i) << ","; + } } // --- SIM enrichment @@ -555,6 +579,20 @@ class TruthGraphDumper : public edm::one::EDAnalyzer<> { << "\n"; } } + } else if (r.kind == TruthGraph::NodeKind::SimVertex && hSimVertices.isValid()) { + const int64_t idx = r.key; + if (idx >= 0 && static_cast(idx) < hSimVertices->size()) { + auto const& sv = (*hSimVertices)[static_cast(idx)]; + os << " \n"; + os << " \n"; + os << " \n"; + os << " \n"; + const int32_t gv = g.nodeSimVtxToGen(i); + if (gv >= 0) + os << " \n"; + } } // --- SIM enrichment diff --git a/PhysicsTools/TruthInfo/plugins/TruthGraphProducer.cc b/PhysicsTools/TruthInfo/plugins/TruthGraphProducer.cc index f8a4d37b5419b..e7bf0706fedc8 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthGraphProducer.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthGraphProducer.cc @@ -375,6 +375,7 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { out->simTrackToGen.assign(nNodes, -1); out->simTrackToVtx.assign(nNodes, -1); + out->simVtxToGen.assign(nNodes, -1); for (int cid = 0; cid < nGenEvents; ++cid) { const uint32_t nodeId = baseGenEvent + static_cast(cid); @@ -430,6 +431,16 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { } } + // Map each GEN particle barcode to its production GenVertex barcode. + // gb.vtxToPart holds (vertex barcode -> outgoing particle barcode), i.e. the + // production vertex of each outgoing particle. + std::unordered_map genPartToProdVtxBarcode; + if (haveGen) { + genPartToProdVtxBarcode.reserve(gb.vtxToPart.size() * 2); + for (auto const& e : gb.vtxToPart) + genPartToProdVtxBarcode.emplace(e.second, e.first); + } + std::vector simVtxIndexToNode(nSimVtx, 0); for (uint32_t i = 0; i < nSimVtx; ++i) { @@ -477,6 +488,27 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { if (genPdgId == 0 || genPdgId == simPdgId) { out->simTrackToGen[nodeId] = static_cast(it->second); + + // Provenance SimVertex -> GenVertex association: the SimTrack's production + // SimVertex corresponds to the production GenVertex of its GenParticle. + const int32_t simVtxNode = out->simTrackToVtx[nodeId]; + if (simVtxNode >= 0) { + auto itProd = genPartToProdVtxBarcode.find(barcode); + if (itProd != genPartToProdVtxBarcode.end()) { + auto itGV = genVtxBarcodeToNode.find(itProd->second); + if (itGV != genVtxBarcodeToNode.end()) { + const int32_t gvNode = static_cast(itGV->second); + int32_t& slot = out->simVtxToGen[simVtxNode]; + if (slot < 0) { + slot = gvNode; + } else if (slot != gvNode) { + edm::LogPrint("TruthGraphProducer") + << "SimVertex node " << simVtxNode << " associated to multiple GenVertex nodes (" << slot + << " and " << gvNode << "); keeping the first"; + } + } + } + } } else { edm::LogPrint("TruthGraphProducer") << "Rejecting primary SimTrack->GenParticle association with mismatched PDG id: " @@ -603,6 +635,18 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { push_edge(static_cast(genNode), simNode, TruthGraph::EdgeKind::GenToSim); } } + + // SimVertex -> GenVertex provenance edges. Unlike the GenVertex -> SimVertex + // direction warned about above, these are derived from per-track primary + // associations and stored as a single edge per SimVertex (simVtxToGen). + for (uint32_t i = 0; i < nSimVtx; ++i) { + const uint32_t simVtxNode = baseSimVtx + i; + const int32_t genVtxNode = out->simVtxToGen[simVtxNode]; + + if (genVtxNode >= 0) { + push_edge(simVtxNode, static_cast(genVtxNode), TruthGraph::EdgeKind::SimToGen); + } + } } out->offsets.assign(nNodes + 1, 0); @@ -639,6 +683,7 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { unsigned nSimVertexOut = 0; unsigned nSimTrackOut = 0; unsigned nGenToSimParticleLinks = 0; + unsigned nSimVtxToGenLinks = 0; for (uint32_t i = 0; i < out->nNodes(); ++i) { switch (out->nodeRef(i).kind) { @@ -653,6 +698,8 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { break; case TruthGraph::NodeKind::SimVertex: ++nSimVertexOut; + if (out->simVtxToGen[i] >= 0) + ++nSimVtxToGenLinks; break; case TruthGraph::NodeKind::SimTrack: ++nSimTrackOut; @@ -667,7 +714,8 @@ class TruthGraphProducer : public edm::stream::EDProducer<> { << " GenParticle=" << nGenParticleOut << " SimVertex=" << nSimVertexOut << " SimTrack=" << nSimTrackOut << " total=" << out->nNodes() << " edges=" << out->nEdges() - << " primaryGenToSimParticleLinks=" << nGenToSimParticleLinks; + << " primaryGenToSimParticleLinks=" << nGenToSimParticleLinks + << " simVtxToGenVertexLinks=" << nSimVtxToGenLinks; evt.put(std::move(out)); } diff --git a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc index 50f3ea5bd6361..a24ea566c5e45 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc @@ -140,6 +140,15 @@ namespace { return ss.str(); } + template + std::string fmtX4(X4 const& x4) { + std::ostringstream ss; + ss.setf(std::ios::fixed); + ss.precision(3); + ss << "(" << x4.x() << ", " << x4.y() << ", " << x4.z() << ", " << x4.t() << ")"; + return ss.str(); + } + template std::string fmtP4(P4 const& p4) { std::ostringstream ss; @@ -389,7 +398,7 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { desc.add("hideLargeSimSourceVertices", true) ->setComment("If true, do not print large SIM-only source vertices in the DOT output"); - desc.add("dumpSimHits", true)->setComment("If true, dump all simhits"); + desc.add("dumpSimHits", false)->setComment("If true, dump all simhits"); desc.add("largeSimSourceVertexMinOutgoing", 50) ->setComment("Minimum outgoing multiplicity for hiding a SIM-only source vertex"); @@ -453,9 +462,7 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { << "No zero-hit subgraphs will be hidden."; } else { for (uint32_t i = 0; i < nParticles; ++i) { - auto const& d = g.particle(i).data(); - - if (!d.hasSim()) + if (!g.particle(i).data().hasSim()) continue; if (i >= hitIndex->nParticles()) @@ -627,8 +634,12 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { auto v = g.vertex(i); auto const& d = v.data(); - os << " v" << i << " [shape=diamond"; + const auto& incoming = v.incomingParticles(); + const auto& outgoing = v.outgoingParticles(); + os << " v" << i << " [shape=diamond, domain=<" << logicalVertexDomain(d) << ">, hasGen=" << d.hasGen() + << ", hasSim=" << d.hasSim() << ", eid=" << d.eventId << ", genEvent=" << d.genEvent + << ", isSource=" << v.isSource() << ", isSink=" << v.isSink(); if (d.hasGen() && d.hasSim()) { os << ", color=\"purple\", penwidth=2"; } else if (d.hasGen()) { @@ -636,6 +647,14 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { } else if (d.hasSim()) { os << ", color=\"darkgreen\""; } + os << ", x=" << std::fixed << std::setprecision(6) << d.position.x() << ", y=" << d.position.y() + << ", z=" << d.position.z() << ", t=" << d.position.t() << ", x4=\"" << fmtX4(d.position) << "\"" + << ", nIn=" << incoming.size() << ", nOut=" << outgoing.size(); + + if (raw != nullptr) { + os << ", raw_GEN=<" << rawNodeSummary(raw, d.genNode) << ">, raw_SIM=<" << rawNodeSummary(raw, d.simNode) + << ">"; + } os << ", label=<\n"; os << "
Particle " << i << "
pid: " << pdgLabel(d.pdgId) << "
status: " << d.status << "
statusFlags: " << d.statusFlags << "
flags: " << statusFlagsLabel(d.statusFlags) << "
eid: " << d.eventId << "
genEvent: " << d.genEvent << "
eid: " << d.eventId << "
genEvent: " << d.genEvent << "
nCheckpoints: " << d.checkpoints.size() << "
direct hits: " << directHits.size() << " E=" << fmtEnergy(directHitEnergy) + << "
subgraph hits: " << subgraphHits.size() << " E=" << fmtEnergy(subgraphHitEnergy) + << "
checkpointId: " << cp.checkpointId << "
x4@checkpoint: " << fmtP4(cp.position) << "
nCheckpoints: " << d.checkpoints.size() << "
direct hits: " << directHits.size() << " E=" << fmtEnergy(directHitEnergy) + os << "
direct simHits: " << directSummary.nSimHits + << " simE=" << fmtEnergy(directSummary.simHitEnergy) << "
direct recHits: " << directSummary.nMatchedRecHits + << " missing=" << directSummary.nMissingRecHits << " recoE=" << fmtEnergy(directSummary.recHitEnergy) << "
subgraph hits: " << subgraphHits.size() << " E=" << fmtEnergy(subgraphHitEnergy) + + os << "
subgraph simHits: " << subgraphSummary.nSimHits + << " simE=" << fmtEnergy(subgraphSummary.simHitEnergy) << "
subgraph recHits: " << subgraphSummary.nMatchedRecHits + << " missing=" << subgraphSummary.nMissingRecHits << " recoE=" << fmtEnergy(subgraphSummary.recHitEnergy) << "
pid: " << pdgLabel(pdg) << "
status: " << st << "
statusFlags: " << flags << "
flags: " << flagsLabel << "
eid: " << eid << "
x4 (cm,ns): " << fmtSimVertexX4(sv) << "
vertexId: " << sv.vertexId() << " processType: " << sv.processType() << "
parent trackId: " << sv.parentIndex() << " noParent: " << (sv.noParent() ? "yes" : "no") + << "
bx: " << sv.eventId().bunchCrossing() << " evtInBx: " << sv.eventId().event() + << "
GenVertex nodeId: " << gv << "
\n"; @@ -655,7 +674,7 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { os << " \n"; - os << " \n"; + os << " \n"; os << " \n"; diff --git a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc index 62e2a8cde8a2f..ff5a53489e243 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphProducer.cc @@ -150,6 +150,8 @@ namespace { std::unordered_map& vertexPayload) { particlePayload.reserve(ev.particles_size() * 2); vertexPayload.reserve(ev.vertices_size() * 2); + constexpr double mmTocm = 0.1; + constexpr double mmOverCToNs = 1.0 / 299.792458; // HepMC vertex time is c*t in mm -> ns for (auto p = ev.particles_begin(); p != ev.particles_end(); ++p) { if (*p == nullptr) @@ -171,10 +173,11 @@ namespace { continue; const int barcode = (*v)->barcode(); - GenVertexPayload payload; - payload.position = math::XYZTLorentzVectorD( - (*v)->position().x(), (*v)->position().y(), (*v)->position().z(), (*v)->position().t()); + payload.position = math::XYZTLorentzVectorD((*v)->position().x() * mmTocm, + (*v)->position().y() * mmTocm, + (*v)->position().z() * mmTocm, + (*v)->position().t() * mmOverCToNs); vertexPayload.emplace(barcode, payload); } @@ -185,7 +188,8 @@ namespace { std::unordered_map& vertexPayload) { particlePayload.reserve(ev.particles().size() * 2); vertexPayload.reserve(ev.vertices().size() * 2); - + constexpr double mmTocm = 0.1; + constexpr double mmOverCToNs = 1.0 / 299.792458; // HepMC vertex time is c*t in mm -> ns for (auto const& pptr : ev.particles()) { if (!pptr) continue; @@ -208,8 +212,10 @@ namespace { const int id = vptr->id(); GenVertexPayload payload; - payload.position = math::XYZTLorentzVectorD( - vptr->position().x(), vptr->position().y(), vptr->position().z(), vptr->position().t()); + payload.position = math::XYZTLorentzVectorD(vptr->position().x() * mmTocm, + vptr->position().y() * mmTocm, + vptr->position().z() * mmTocm, + vptr->position().t() * mmOverCToNs); vertexPayload.emplace(id, payload); } @@ -710,11 +716,14 @@ class TruthLogicalGraphProducer : public edm::stream::EDProducer<> { if (simIndex < hSimVertices->size()) { auto const& sv = (*hSimVertices)[simIndex]; const auto& pos = sv.position(); + constexpr double sToNs = 1e9; // SimVertex time is stored in seconds -> ns // For SIM-only logical vertices, use the SimVertex position. + // Position is in cm; SimVertex time is converted from seconds to ns so it + // shares the (cm, ns) convention used for GEN vertices. // For GEN+SIM logical vertices, the GEN position remains the nominal one. if (!v.hasGen()) { - v.position = math::XYZTLorentzVectorD(pos.x(), pos.y(), pos.z(), pos.t()); + v.position = math::XYZTLorentzVectorD(pos.x(), pos.y(), pos.z(), pos.t() * sToNs); } } } diff --git a/PhysicsTools/TruthInfo/src/TruthLogicalGraphPostProcessor.cc b/PhysicsTools/TruthInfo/src/TruthLogicalGraphPostProcessor.cc index 05719f015fc99..268bc6fdfd240 100644 --- a/PhysicsTools/TruthInfo/src/TruthLogicalGraphPostProcessor.cc +++ b/PhysicsTools/TruthInfo/src/TruthLogicalGraphPostProcessor.cc @@ -82,10 +82,10 @@ namespace { bool isSimOnlyVertex(truth::VertexData const& vertex) { return !vertex.hasGen() && vertex.hasSim(); } bool compatibleVertexPositions(truth::VertexData const& a, truth::VertexData const& b, double tolerance) { - return std::abs(a.position.px() - b.position.px()) <= tolerance && - std::abs(a.position.py() - b.position.py()) <= tolerance && - std::abs(a.position.pz() - b.position.pz()) <= tolerance && - std::abs(a.position.e() - b.position.e()) <= tolerance; + return std::abs(a.position.x() - b.position.x()) <= tolerance && + std::abs(a.position.y() - b.position.y()) <= tolerance && + std::abs(a.position.z() - b.position.z()) <= tolerance && + std::abs(a.position.t() - b.position.t()) <= tolerance; } bool canMergeGenSimVerticesByPosition(truth::VertexData const& a, truth::VertexData const& b, double tolerance) { @@ -95,13 +95,13 @@ namespace { return false; bool areCompatible = compatibleVertexPositions(a, b, tolerance); if (!areCompatible) { - std::cout << "vertices not merged: (" << a.position.px() << " , " << a.position.py() << " , " << a.position.pz() - << ") too far from " << "(" << b.position.px() << " , " << b.position.py() << " , " << b.position.pz() - << ")" << std::endl; + std::cout << "vertices not merged: (" << a.position.x() << " , " << a.position.y() << " , " << a.position.z() + << " , " << a.position.t() << ") too far from " << "(" << b.position.x() << " , " << b.position.y() + << " , " << b.position.z() << " , " << b.position.t() << ")" << std::endl; } else { - std::cout << "vertices successfully merged: (" << a.position.px() << " , " << a.position.py() << " , " - << a.position.pz() << ") too far from " << "(" << b.position.px() << " , " << b.position.py() << " , " - << b.position.pz() << ")" << std::endl; + std::cout << "vertices successfully merged: (" << a.position.x() << " , " << a.position.y() << " , " + << a.position.z() << " , " << a.position.t() << ") compatible with " << "(" << b.position.x() << " , " + << b.position.y() << " , " << b.position.z() << " , " << b.position.t() << ")" << std::endl; } return areCompatible; } @@ -1052,7 +1052,7 @@ namespace truth { "If true, merge GEN-only and SIM-only logical vertices connected to the same particle when their " "positions are compatible within genSimVertexPositionTolerance."); - desc.add("genSimVertexPositionTolerance", 1e-6) + desc.add("genSimVertexPositionTolerance", 5e-3) ->setComment("Absolute tolerance applied independently to x, y, z, and t when merging GEN and SIM vertices."); return desc; From e5c47ffed157933670e4c9bfc2e5dd00c5d98d2e Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Fri, 12 Jun 2026 17:51:00 +0200 Subject: [PATCH 39/53] TruthInfo: add barrel/forward PFRecHit and tracker SimHit tables, match tracker simhits to particles New NanoAOD flat tables: - PFRecHitFlatTableProducer: barrel/forward calorimeter PFRecHits (ECAL/HBHE/HF/HO). Positions are recomputed from CaloGeometry via the detId because PFRecHit::position()/positionREP() read a non-persisted CaloCellGeometry and segfault on rechits read back from a file. The offline (RECO) PFRecHit collections are empty in the test sample, so the HLT-tier collections are used. - TrackerSimHitFlatTableProducer: tracker PSimHits with global positions from the TrackerGeometry, plus trackId/pdgId/energyLoss/tof/pabs/processType. Tracker simhits in the truth matching: - LogicalGraphHitIndex/Builder gain a separate tracker-hit channel (direct and subgraph), filled from PSimHit::trackId() in TruthLogicalGraphHitIndexProducer. - TruthLogicalGraphDumper reports per-particle tracker simhit counts and energy. Test config wires both tables into the path and uses the ideal tracker geometry (applyAlignment=False) so no alignment conditions are needed standalone. --- .../interface/LogicalGraphHitIndex.h | 39 +++++- .../interface/LogicalGraphHitIndexBuilder.h | 14 +- PhysicsTools/TruthInfo/plugins/BuildFile.xml | 12 ++ .../plugins/LogicalGraphHitIndexProducer.cc | 48 +++++++ .../plugins/PFRecHitFlatTableProducer.cc | 107 +++++++++++++++ .../plugins/TrackerSimHitFlatTableProducer.cc | 122 ++++++++++++++++++ .../plugins/TruthLogicalGraphDumper.cc | 26 ++++ .../src/LogicalGraphHitIndexBuilder.cc | 85 ++++++++---- .../test/dumpTruthGraphsFromGENSIMRECO_cfg.py | 41 ++++++ 9 files changed, 467 insertions(+), 27 deletions(-) create mode 100644 PhysicsTools/TruthInfo/plugins/PFRecHitFlatTableProducer.cc create mode 100644 PhysicsTools/TruthInfo/plugins/TrackerSimHitFlatTableProducer.cc diff --git a/PhysicsTools/TruthInfo/interface/LogicalGraphHitIndex.h b/PhysicsTools/TruthInfo/interface/LogicalGraphHitIndex.h index 103f0088ca194..09eb9a9494261 100644 --- a/PhysicsTools/TruthInfo/interface/LogicalGraphHitIndex.h +++ b/PhysicsTools/TruthInfo/interface/LogicalGraphHitIndex.h @@ -26,12 +26,20 @@ namespace truth { std::vector directOffsets, std::vector directHits, std::vector subgraphOffsets, - std::vector subgraphHits) + std::vector subgraphHits, + std::vector trackerDirectOffsets = {}, + std::vector trackerDirectHits = {}, + std::vector trackerSubgraphOffsets = {}, + std::vector trackerSubgraphHits = {}) : nParticles_(nParticles), directOffsets_(std::move(directOffsets)), directHits_(std::move(directHits)), subgraphOffsets_(std::move(subgraphOffsets)), - subgraphHits_(std::move(subgraphHits)) {} + subgraphHits_(std::move(subgraphHits)), + trackerDirectOffsets_(std::move(trackerDirectOffsets)), + trackerDirectHits_(std::move(trackerDirectHits)), + trackerSubgraphOffsets_(std::move(trackerSubgraphOffsets)), + trackerSubgraphHits_(std::move(trackerSubgraphHits)) {} [[nodiscard]] uint32_t nParticles() const { return nParticles_; } @@ -53,6 +61,27 @@ namespace truth { [[nodiscard]] const std::vector& subgraphOffsets() const { return subgraphOffsets_; } [[nodiscard]] const std::vector& subgraphHitStorage() const { return subgraphHits_; } + // Tracker simhits are kept in a separate channel. They carry no recHit + // association (recHitIndex stays invalid) and their energy is the PSimHit + // energy loss. Spans are empty when tracker matching is not configured. + [[nodiscard]] std::span trackerDirectHits(uint32_t particleId) const { + if (particleId + 1 >= trackerDirectOffsets_.size()) + return {}; + const auto b = trackerDirectOffsets_[particleId]; + const auto e = trackerDirectOffsets_[particleId + 1]; + return std::span(trackerDirectHits_.data() + b, e - b); + } + + [[nodiscard]] std::span trackerSubgraphHits(uint32_t particleId) const { + if (particleId + 1 >= trackerSubgraphOffsets_.size()) + return {}; + const auto b = trackerSubgraphOffsets_[particleId]; + const auto e = trackerSubgraphOffsets_[particleId + 1]; + return std::span(trackerSubgraphHits_.data() + b, e - b); + } + + [[nodiscard]] bool hasTrackerHits() const { return !trackerDirectHits_.empty(); } + private: uint32_t nParticles_ = 0; @@ -61,6 +90,12 @@ namespace truth { std::vector subgraphOffsets_; std::vector subgraphHits_; + + std::vector trackerDirectOffsets_; + std::vector trackerDirectHits_; + + std::vector trackerSubgraphOffsets_; + std::vector trackerSubgraphHits_; }; } // namespace truth diff --git a/PhysicsTools/TruthInfo/interface/LogicalGraphHitIndexBuilder.h b/PhysicsTools/TruthInfo/interface/LogicalGraphHitIndexBuilder.h index 36cf3f647a89c..9767e2eccf62c 100644 --- a/PhysicsTools/TruthInfo/interface/LogicalGraphHitIndexBuilder.h +++ b/PhysicsTools/TruthInfo/interface/LogicalGraphHitIndexBuilder.h @@ -18,6 +18,9 @@ namespace truth { void addHitForTrack(uint32_t trackId, uint32_t detId, uint32_t recHitIndex, float energy); + // Tracker simhits: separate channel, no recHit association. + void addTrackerHitForTrack(uint32_t trackId, uint32_t detId, float energy); + [[nodiscard]] LogicalGraphHitIndex finish(); private: @@ -34,7 +37,13 @@ namespace truth { static void addHit(HitMap& hits, Hit const& hit); static std::vector sortedHits(HitMap const& hits); - void fillSubgraphHits(uint32_t particleId, std::vector& state); + // Aggregate direct hits of a particle and all its descendants into subgraph. + void fillSubgraphHits(uint32_t particleId, + std::vector const& direct, + std::vector& subgraph, + std::vector& state); + + static void buildHitCSR(std::vector const& maps, std::vector& offsets, std::vector& storage); uint32_t nParticles_ = 0; @@ -43,6 +52,9 @@ namespace truth { std::vector directHits_; std::vector subgraphHits_; + + std::vector trackerDirectHits_; + std::vector trackerSubgraphHits_; }; } // namespace truth diff --git a/PhysicsTools/TruthInfo/plugins/BuildFile.xml b/PhysicsTools/TruthInfo/plugins/BuildFile.xml index 8e4288cc9aedc..6dd0c2958f5d2 100644 --- a/PhysicsTools/TruthInfo/plugins/BuildFile.xml +++ b/PhysicsTools/TruthInfo/plugins/BuildFile.xml @@ -26,6 +26,10 @@ + + + + @@ -49,4 +53,12 @@ + + + + + + + + \ No newline at end of file diff --git a/PhysicsTools/TruthInfo/plugins/LogicalGraphHitIndexProducer.cc b/PhysicsTools/TruthInfo/plugins/LogicalGraphHitIndexProducer.cc index 831be2a45d06b..82b7cfcf28472 100644 --- a/PhysicsTools/TruthInfo/plugins/LogicalGraphHitIndexProducer.cc +++ b/PhysicsTools/TruthInfo/plugins/LogicalGraphHitIndexProducer.cc @@ -26,6 +26,7 @@ #include "Geometry/Records/interface/CaloGeometryRecord.h" #include "SimDataFormats/CaloHit/interface/PCaloHit.h" #include "SimDataFormats/CaloTest/interface/HGCalTestNumbering.h" +#include "SimDataFormats/TrackingHit/interface/PSimHitContainer.h" #include "PhysicsTools/TruthInfo/interface/Graph.h" #include "PhysicsTools/TruthInfo/interface/LogicalGraphHitIndex.h" @@ -113,6 +114,8 @@ class TruthLogicalGraphHitIndexProducer : public edm::global::EDProducer<> { truth::LogicalGraphHitIndexBuilder& builder, hgcal::DetIdRecHitMap const* recHitMap) const; + void fillTrackerSimHits(edm::Event& event, truth::LogicalGraphHitIndexBuilder& builder) const; + RelabelContext makeRelabelContext(edm::EventSetup const& setup) const; uint32_t recoDetIdForSimHit(PCaloHit const& simHit, @@ -127,6 +130,9 @@ class TruthLogicalGraphHitIndexProducer : public edm::global::EDProducer<> { std::vector simHitTags_; std::vector>> simHitTokens_; + std::vector trackerSimHitTags_; + std::vector> trackerSimHitTokens_; + edm::ESGetToken geomToken_; bool doHGCalRelabelling_ = true; @@ -137,6 +143,7 @@ TruthLogicalGraphHitIndexProducer::TruthLogicalGraphHitIndexProducer(edm::Parame rawGraphToken_(consumes(cfg.getParameter("rawSrc"))), recHitMapToken_(consumes(cfg.getParameter("recHitMap"))), simHitTags_(cfg.getParameter>("simHitCollections")), + trackerSimHitTags_(cfg.getParameter>("trackerSimHitCollections")), geomToken_(esConsumes()), doHGCalRelabelling_(cfg.getParameter("doHGCalRelabelling")) { simHitTokens_.reserve(simHitTags_.size()); @@ -144,6 +151,11 @@ TruthLogicalGraphHitIndexProducer::TruthLogicalGraphHitIndexProducer(edm::Parame simHitTokens_.push_back(consumes>(tag)); } + trackerSimHitTokens_.reserve(trackerSimHitTags_.size()); + for (auto const& tag : trackerSimHitTags_) { + trackerSimHitTokens_.push_back(consumes(tag)); + } + produces(); } @@ -159,6 +171,21 @@ void TruthLogicalGraphHitIndexProducer::fillDescriptions(edm::ConfigurationDescr edm::InputTag("g4SimHits", "HGCHitsHEfront"), edm::InputTag("g4SimHits", "HGCHitsHEback")}); + desc.add>("trackerSimHitCollections", + {edm::InputTag("g4SimHits", "TrackerHitsPixelBarrelLowTof"), + edm::InputTag("g4SimHits", "TrackerHitsPixelBarrelHighTof"), + edm::InputTag("g4SimHits", "TrackerHitsPixelEndcapLowTof"), + edm::InputTag("g4SimHits", "TrackerHitsPixelEndcapHighTof"), + edm::InputTag("g4SimHits", "TrackerHitsTIBLowTof"), + edm::InputTag("g4SimHits", "TrackerHitsTIBHighTof"), + edm::InputTag("g4SimHits", "TrackerHitsTIDLowTof"), + edm::InputTag("g4SimHits", "TrackerHitsTIDHighTof"), + edm::InputTag("g4SimHits", "TrackerHitsTOBLowTof"), + edm::InputTag("g4SimHits", "TrackerHitsTOBHighTof"), + edm::InputTag("g4SimHits", "TrackerHitsTECLowTof"), + edm::InputTag("g4SimHits", "TrackerHitsTECHighTof")}) + ->setComment("Tracker PSimHit collections matched to particles via PSimHit::trackId()"); + desc.add("doHGCalRelabelling", true) ->setComment("Convert old HGCAL simulation DetIds to reco DetIds before looking up recHits"); @@ -179,6 +206,7 @@ void TruthLogicalGraphHitIndexProducer::produce(edm::StreamID, edm::Event& event fillTrackToParticleMap(graphView, rawGraph, builder); fillSimHits(event, setup, builder, recHitMap); + fillTrackerSimHits(event, builder); auto output = std::make_unique(builder.finish()); event.put(std::move(output)); @@ -363,4 +391,24 @@ void TruthLogicalGraphHitIndexProducer::fillSimHits(edm::Event& event, } } +void TruthLogicalGraphHitIndexProducer::fillTrackerSimHits(edm::Event& event, + truth::LogicalGraphHitIndexBuilder& builder) const { + for (uint32_t tokenIndex = 0; tokenIndex < trackerSimHitTokens_.size(); ++tokenIndex) { + edm::Handle hSimHits; + event.getByToken(trackerSimHitTokens_[tokenIndex], hSimHits); + + if (!hSimHits.isValid()) { + edm::LogWarning("TruthLogicalGraphHitIndexProducer") + << "Missing tracker PSimHit collection " << trackerSimHitTags_[tokenIndex].encode() << ". Skipping it."; + continue; + } + + for (auto const& simHit : *hSimHits) { + // PSimHit::trackId() is the G4 trackId of the SimTrack that made the hit, + // the same id space used to associate calorimeter simhits to particles. + builder.addTrackerHitForTrack(simHit.trackId(), simHit.detUnitId(), simHit.energyLoss()); + } + } +} + DEFINE_FWK_MODULE(TruthLogicalGraphHitIndexProducer); diff --git a/PhysicsTools/TruthInfo/plugins/PFRecHitFlatTableProducer.cc b/PhysicsTools/TruthInfo/plugins/PFRecHitFlatTableProducer.cc new file mode 100644 index 0000000000000..c108523257e56 --- /dev/null +++ b/PhysicsTools/TruthInfo/plugins/PFRecHitFlatTableProducer.cc @@ -0,0 +1,107 @@ +// Author: Felice Pantaleo - CERN +// Flat-table dump of reco::PFRecHit collections (barrel/forward calorimeters: +// ECAL, HBHE, HF, HO). HGCal rechits are dumped separately by +// RecHitFlatTableProducer. +// +// NOTE: reco::PFRecHit::position()/positionREP() read the cached CaloCellGeometry, +// which is NOT persisted; calling them on rechits read back from a file segfaults. +// Positions are therefore recomputed from CaloGeometry using the (persisted) detId. + +#include "FWCore/Framework/interface/stream/EDProducer.h" +#include "FWCore/Framework/interface/Event.h" +#include "FWCore/Framework/interface/EventSetup.h" +#include "FWCore/Framework/interface/MakerMacros.h" +#include "FWCore/Utilities/interface/transform.h" +#include "FWCore/Utilities/interface/ESGetToken.h" +#include "FWCore/ParameterSet/interface/ParameterSet.h" +#include "FWCore/ParameterSet/interface/ConfigurationDescriptions.h" +#include "FWCore/ParameterSet/interface/ParameterSetDescription.h" + +#include "DataFormats/NanoAOD/interface/FlatTable.h" +#include "DataFormats/DetId/interface/DetId.h" +#include "DataFormats/ParticleFlowReco/interface/PFRecHit.h" +#include "DataFormats/ParticleFlowReco/interface/PFRecHitFwd.h" +#include "Geometry/CaloGeometry/interface/CaloGeometry.h" +#include "Geometry/Records/interface/CaloGeometryRecord.h" + +#include +#include + +class PFRecHitFlatTableProducer : public edm::stream::EDProducer<> { +public: + explicit PFRecHitFlatTableProducer(edm::ParameterSet const& params) + : objName_(params.getParameter("objName")), + rechits_tokens_{edm::vector_transform( + params.getParameter>("label_rechits"), + [this](edm::InputTag const& tag) { return consumes(tag); })}, + geomToken_(esConsumes()) { + produces(); + } + + void produce(edm::Event& event, edm::EventSetup const& setup) override { + auto const& geom = setup.getData(geomToken_); + + std::vector rechit_ID; + std::vector rechit_energy; + std::vector rechit_time; + std::vector rechit_x; + std::vector rechit_y; + std::vector rechit_z; + std::vector rechit_eta; + std::vector rechit_phi; + std::vector rechit_depth; + + for (auto const& token : rechits_tokens_) { + edm::Handle handle; + event.getByToken(token, handle); + if (!handle.isValid()) + continue; + + for (auto const& rh : *handle) { + const GlobalPoint pos = geom.getPosition(DetId(rh.detId())); + rechit_ID.push_back(rh.detId()); + rechit_energy.push_back(rh.energy()); + rechit_time.push_back(rh.time()); + rechit_x.push_back(pos.x()); + rechit_y.push_back(pos.y()); + rechit_z.push_back(pos.z()); + rechit_eta.push_back(pos.eta()); + rechit_phi.push_back(pos.phi()); + rechit_depth.push_back(rh.depth()); + } + } + + auto tab = std::make_unique(rechit_ID.size(), objName_, false, false); + tab->addColumn("rechit_ID", rechit_ID, "PFRecHit DetId rawId"); + tab->addColumn("rechit_energy", rechit_energy, "PFRecHit energy [GeV]"); + tab->addColumn("rechit_time", rechit_time, "PFRecHit time [ns]"); + tab->addColumn("rechit_x", rechit_x, "Global x from CaloGeometry [cm]"); + tab->addColumn("rechit_y", rechit_y, "Global y from CaloGeometry [cm]"); + tab->addColumn("rechit_z", rechit_z, "Global z from CaloGeometry [cm]"); + tab->addColumn("rechit_eta", rechit_eta, "PFRecHit eta"); + tab->addColumn("rechit_phi", rechit_phi, "PFRecHit phi"); + tab->addColumn("rechit_depth", rechit_depth, "PFRecHit depth"); + + event.put(std::move(tab)); + } + + static void fillDescriptions(edm::ConfigurationDescriptions& descriptions) { + edm::ParameterSetDescription desc; + desc.add("objName", "pfrechits") + ->setComment("name of the nanoaod::FlatTable produced for barrel/forward calo PFRecHits"); + desc.add>("label_rechits", + {edm::InputTag("hltParticleFlowRecHitECALUnseeded", "", "HLT"), + edm::InputTag("hltParticleFlowRecHitHBHE", "", "HLT"), + edm::InputTag("hltParticleFlowRecHitHF", "", "HLT"), + edm::InputTag("hltParticleFlowRecHitHO", "", "HLT")}) + ->setComment("reco::PFRecHit collections to dump (barrel/forward calorimeters)"); + descriptions.add("pfRecHitTable", desc); + } + +private: + const std::string objName_; + const std::vector> rechits_tokens_; + const edm::ESGetToken geomToken_; +}; + +DEFINE_FWK_MODULE(PFRecHitFlatTableProducer); diff --git a/PhysicsTools/TruthInfo/plugins/TrackerSimHitFlatTableProducer.cc b/PhysicsTools/TruthInfo/plugins/TrackerSimHitFlatTableProducer.cc new file mode 100644 index 0000000000000..856caff13b7eb --- /dev/null +++ b/PhysicsTools/TruthInfo/plugins/TrackerSimHitFlatTableProducer.cc @@ -0,0 +1,122 @@ +// Author: Felice Pantaleo - CERN +// Flat-table dump of tracker PSimHit collections (g4SimHits TrackerHits*). +// Global positions are computed from the local PSimHit position using the +// TrackerGeometry. trackId() links each hit back to a SimTrack, i.e. to a +// truth-graph particle. + +#include "FWCore/Framework/interface/stream/EDProducer.h" +#include "FWCore/Framework/interface/Event.h" +#include "FWCore/Framework/interface/EventSetup.h" +#include "FWCore/Framework/interface/MakerMacros.h" +#include "FWCore/Utilities/interface/transform.h" +#include "FWCore/Utilities/interface/ESGetToken.h" +#include "FWCore/ParameterSet/interface/ParameterSet.h" +#include "FWCore/ParameterSet/interface/ConfigurationDescriptions.h" +#include "FWCore/ParameterSet/interface/ParameterSetDescription.h" + +#include "DataFormats/NanoAOD/interface/FlatTable.h" +#include "DataFormats/DetId/interface/DetId.h" +#include "SimDataFormats/TrackingHit/interface/PSimHitContainer.h" + +#include "Geometry/TrackerGeometryBuilder/interface/TrackerGeometry.h" +#include "Geometry/CommonDetUnit/interface/GeomDet.h" +#include "Geometry/Records/interface/TrackerDigiGeometryRecord.h" + +#include +#include + +class TrackerSimHitFlatTableProducer : public edm::stream::EDProducer<> { +public: + explicit TrackerSimHitFlatTableProducer(edm::ParameterSet const& params) + : objName_(params.getParameter("objName")), + simhits_tokens_{edm::vector_transform( + params.getParameter>("label_simhits"), + [this](edm::InputTag const& tag) { return consumes(tag); })}, + trackerGeomToken_(esConsumes()) { + produces(); + } + + void produce(edm::Event& event, edm::EventSetup const& setup) override { + auto const& trackerGeom = setup.getData(trackerGeomToken_); + + std::vector simhit_detId; + std::vector simhit_trackId; + std::vector simhit_pdgId; + std::vector simhit_energyLoss; + std::vector simhit_tof; + std::vector simhit_pabs; + std::vector simhit_processType; + std::vector simhit_x; + std::vector simhit_y; + std::vector simhit_z; + + for (auto const& token : simhits_tokens_) { + edm::Handle handle; + event.getByToken(token, handle); + if (!handle.isValid()) + continue; + + for (auto const& hit : *handle) { + const DetId detId(hit.detUnitId()); + auto const* det = trackerGeom.idToDet(detId); + + GlobalPoint gp; + if (det != nullptr) + gp = det->surface().toGlobal(hit.localPosition()); + + simhit_detId.push_back(hit.detUnitId()); + simhit_trackId.push_back(hit.trackId()); + simhit_pdgId.push_back(hit.particleType()); + simhit_energyLoss.push_back(hit.energyLoss()); + simhit_tof.push_back(hit.tof()); + simhit_pabs.push_back(hit.pabs()); + simhit_processType.push_back(hit.processType()); + simhit_x.push_back(gp.x()); + simhit_y.push_back(gp.y()); + simhit_z.push_back(gp.z()); + } + } + + auto tab = std::make_unique(simhit_detId.size(), objName_, false, false); + tab->addColumn("simhit_detId", simhit_detId, "Tracker PSimHit detUnitId rawId"); + tab->addColumn("simhit_trackId", simhit_trackId, "G4 trackId of the SimTrack that made the hit"); + tab->addColumn("simhit_pdgId", simhit_pdgId, "PDG id of the particle (particleType)"); + tab->addColumn("simhit_energyLoss", simhit_energyLoss, "Energy loss in the sensor [GeV]"); + tab->addColumn("simhit_tof", simhit_tof, "Time of flight [ns]"); + tab->addColumn("simhit_pabs", simhit_pabs, "Momentum magnitude at entry [GeV]"); + tab->addColumn("simhit_processType", simhit_processType, "Geant process type"); + tab->addColumn("simhit_x", simhit_x, "Global x of the hit center [cm]"); + tab->addColumn("simhit_y", simhit_y, "Global y of the hit center [cm]"); + tab->addColumn("simhit_z", simhit_z, "Global z of the hit center [cm]"); + + event.put(std::move(tab)); + } + + static void fillDescriptions(edm::ConfigurationDescriptions& descriptions) { + edm::ParameterSetDescription desc; + desc.add("objName", "trackersimhits") + ->setComment("name of the nanoaod::FlatTable produced for tracker PSimHits"); + desc.add>("label_simhits", + {edm::InputTag("g4SimHits", "TrackerHitsPixelBarrelLowTof"), + edm::InputTag("g4SimHits", "TrackerHitsPixelBarrelHighTof"), + edm::InputTag("g4SimHits", "TrackerHitsPixelEndcapLowTof"), + edm::InputTag("g4SimHits", "TrackerHitsPixelEndcapHighTof"), + edm::InputTag("g4SimHits", "TrackerHitsTIBLowTof"), + edm::InputTag("g4SimHits", "TrackerHitsTIBHighTof"), + edm::InputTag("g4SimHits", "TrackerHitsTIDLowTof"), + edm::InputTag("g4SimHits", "TrackerHitsTIDHighTof"), + edm::InputTag("g4SimHits", "TrackerHitsTOBLowTof"), + edm::InputTag("g4SimHits", "TrackerHitsTOBHighTof"), + edm::InputTag("g4SimHits", "TrackerHitsTECLowTof"), + edm::InputTag("g4SimHits", "TrackerHitsTECHighTof")}) + ->setComment("Tracker PSimHit collections to dump"); + descriptions.add("trackerSimHitTable", desc); + } + +private: + const std::string objName_; + const std::vector> simhits_tokens_; + const edm::ESGetToken trackerGeomToken_; +}; + +DEFINE_FWK_MODULE(TrackerSimHitFlatTableProducer); diff --git a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc index a24ea566c5e45..149f448da9cba 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc @@ -510,6 +510,19 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { const HitSummary directSummary = hasHitInfo ? summarizeHits(directHits, recHitEnergies) : HitSummary(); const HitSummary subgraphSummary = hasHitInfo ? summarizeHits(subgraphHits, recHitEnergies) : HitSummary(); + // Tracker simhits (separate channel, no recHit association). Reusing + // summarizeHits is fine: tracker hits have an invalid recHitIndex, so only + // nSimHits and simHitEnergy (energy loss) carry meaning. + const bool hasTrackerInfo = hasHitInfo && hitIndex->hasTrackerHits(); + const auto trackerDirectHits = + hasTrackerInfo ? hitIndex->trackerDirectHits(i) : std::span(); + const auto trackerSubgraphHits = + hasTrackerInfo ? hitIndex->trackerSubgraphHits(i) : std::span(); + const HitSummary trackerDirectSummary = + hasTrackerInfo ? summarizeHits(trackerDirectHits, recHitEnergies) : HitSummary(); + const HitSummary trackerSubgraphSummary = + hasTrackerInfo ? summarizeHits(trackerSubgraphHits, recHitEnergies) : HitSummary(); + os << " p" << i << " [shape=ellipse, hasCheckpoints=" << p.hasCheckpoints() << ", hasGen=" << p.hasGen() << ", hasSim=" << d.hasSim(); @@ -539,6 +552,12 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { << ", nSubgraphRecHits=" << subgraphSummary.nMatchedRecHits << ", subgraphSimHitEnergy=" << fmtEnergy(subgraphSummary.simHitEnergy) << ", subgraphRecHitEnergy=" << fmtEnergy(subgraphSummary.recHitEnergy); + if (hasTrackerInfo) { + os << ", nDirectTrackerSimHits=" << trackerDirectSummary.nSimHits + << ", directTrackerSimHitEnergy=" << fmtEnergy(trackerDirectSummary.simHitEnergy) + << ", nSubgraphTrackerSimHits=" << trackerSubgraphSummary.nSimHits + << ", subgraphTrackerSimHitEnergy=" << fmtEnergy(trackerSubgraphSummary.simHitEnergy); + } if (dumpSimHits_) { os << ", directHitsDetIds=\""; for (auto h : directHits) { @@ -609,6 +628,13 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { << "\n"; } + if (hasTrackerInfo) { + os << " \n"; + os << " \n"; + } + for (auto const& cp : d.checkpoints) { os << " \n"; os << " \n"; diff --git a/PhysicsTools/TruthInfo/src/LogicalGraphHitIndexBuilder.cc b/PhysicsTools/TruthInfo/src/LogicalGraphHitIndexBuilder.cc index 8fe6de8c339fc..417fbe96f71f3 100644 --- a/PhysicsTools/TruthInfo/src/LogicalGraphHitIndexBuilder.cc +++ b/PhysicsTools/TruthInfo/src/LogicalGraphHitIndexBuilder.cc @@ -6,7 +6,12 @@ namespace truth { LogicalGraphHitIndexBuilder::LogicalGraphHitIndexBuilder(uint32_t nParticles) - : nParticles_(nParticles), children_(nParticles), directHits_(nParticles), subgraphHits_(nParticles) {} + : nParticles_(nParticles), + children_(nParticles), + directHits_(nParticles), + subgraphHits_(nParticles), + trackerDirectHits_(nParticles), + trackerSubgraphHits_(nParticles) {} void LogicalGraphHitIndexBuilder::setSimTrackForParticle(uint32_t particleId, uint32_t trackId) { if (particleId >= nParticles_) @@ -36,6 +41,17 @@ namespace truth { addHit(directHits_[it->second], detId, recHitIndex, energy); } + void LogicalGraphHitIndexBuilder::addTrackerHitForTrack(uint32_t trackId, uint32_t detId, float energy) { + if (energy <= 0.f) + return; + + auto it = trackIdToParticle_.find(trackId); + if (it == trackIdToParticle_.end()) + return; + + addHit(trackerDirectHits_[it->second], detId, Hit::invalidRecHitIndex, energy); + } + void LogicalGraphHitIndexBuilder::addHit(HitMap& hits, uint32_t detId, uint32_t recHitIndex, float energy) { auto& entry = hits[detId]; entry.energy += energy; @@ -73,7 +89,10 @@ namespace truth { return out; } - void LogicalGraphHitIndexBuilder::fillSubgraphHits(uint32_t particleId, std::vector& state) { + void LogicalGraphHitIndexBuilder::fillSubgraphHits(uint32_t particleId, + std::vector const& direct, + std::vector& subgraph, + std::vector& state) { if (particleId >= nParticles_) return; @@ -85,9 +104,9 @@ namespace truth { state[particleId] = 1; - auto& out = subgraphHits_[particleId]; + auto& out = subgraph[particleId]; - for (auto const& [detId, acc] : directHits_[particleId]) { + for (auto const& [detId, acc] : direct[particleId]) { addHit(out, detId, acc.recHitIndex, acc.energy); } @@ -95,9 +114,9 @@ namespace truth { if (childId >= nParticles_) continue; - fillSubgraphHits(childId, state); + fillSubgraphHits(childId, direct, subgraph, state); - for (auto const& [detId, acc] : subgraphHits_[childId]) { + for (auto const& [detId, acc] : subgraph[childId]) { addHit(out, detId, acc.recHitIndex, acc.energy); } } @@ -105,39 +124,57 @@ namespace truth { state[particleId] = 2; } + void LogicalGraphHitIndexBuilder::buildHitCSR(std::vector const& maps, + std::vector& offsets, + std::vector& storage) { + offsets.clear(); + storage.clear(); + offsets.reserve(maps.size() + 1); + offsets.push_back(0); + + for (auto const& map : maps) { + auto hits = sortedHits(map); + storage.insert(storage.end(), hits.begin(), hits.end()); + offsets.push_back(static_cast(storage.size())); + } + } + LogicalGraphHitIndex LogicalGraphHitIndexBuilder::finish() { - std::vector state(nParticles_, 0); + std::vector caloState(nParticles_, 0); for (uint32_t particleId = 0; particleId < nParticles_; ++particleId) { - fillSubgraphHits(particleId, state); + fillSubgraphHits(particleId, directHits_, subgraphHits_, caloState); } - std::vector directOffsets; - std::vector directHitStorage; - directOffsets.reserve(nParticles_ + 1); - directOffsets.push_back(0); - + std::vector trackerState(nParticles_, 0); for (uint32_t particleId = 0; particleId < nParticles_; ++particleId) { - auto hits = sortedHits(directHits_[particleId]); - directHitStorage.insert(directHitStorage.end(), hits.begin(), hits.end()); - directOffsets.push_back(static_cast(directHitStorage.size())); + fillSubgraphHits(particleId, trackerDirectHits_, trackerSubgraphHits_, trackerState); } + std::vector directOffsets; + std::vector directHitStorage; + buildHitCSR(directHits_, directOffsets, directHitStorage); + std::vector subgraphOffsets; std::vector subgraphHitStorage; - subgraphOffsets.reserve(nParticles_ + 1); - subgraphOffsets.push_back(0); + buildHitCSR(subgraphHits_, subgraphOffsets, subgraphHitStorage); - for (uint32_t particleId = 0; particleId < nParticles_; ++particleId) { - auto hits = sortedHits(subgraphHits_[particleId]); - subgraphHitStorage.insert(subgraphHitStorage.end(), hits.begin(), hits.end()); - subgraphOffsets.push_back(static_cast(subgraphHitStorage.size())); - } + std::vector trackerDirectOffsets; + std::vector trackerDirectHitStorage; + buildHitCSR(trackerDirectHits_, trackerDirectOffsets, trackerDirectHitStorage); + + std::vector trackerSubgraphOffsets; + std::vector trackerSubgraphHitStorage; + buildHitCSR(trackerSubgraphHits_, trackerSubgraphOffsets, trackerSubgraphHitStorage); return LogicalGraphHitIndex(nParticles_, std::move(directOffsets), std::move(directHitStorage), std::move(subgraphOffsets), - std::move(subgraphHitStorage)); + std::move(subgraphHitStorage), + std::move(trackerDirectOffsets), + std::move(trackerDirectHitStorage), + std::move(trackerSubgraphOffsets), + std::move(trackerSubgraphHitStorage)); } } // namespace truth diff --git a/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py b/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py index df785bc6cc54c..8df7cda6db3f7 100644 --- a/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py +++ b/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py @@ -28,6 +28,10 @@ # Keep this consistent with the geometry used to produce step3.root. process.load("Configuration.Geometry.GeometryExtendedRun4D120Reco_cff") +# Use the ideal tracker geometry so the tracker simhit table needs no alignment +# conditions (GlobalPositionRcd) when running standalone without a GlobalTag. +process.trackerGeometry.applyAlignment = cms.bool(False) + process.maxEvents = cms.untracked.PSet( input=cms.untracked.int32(args.maxevts) ) @@ -163,6 +167,41 @@ process.load("PhysicsTools.TruthInfo.recHitTable_cff") + +# Barrel/forward calorimeter PFRecHits as a separate NanoAOD collection. +# HGCal rechits stay in recHitTable above. NOTE: the offline (RECO) PFRecHit +# collections are empty in this sample, so the HLT-tier PFRecHits (which contain +# the hits) are used here. +process.pfRecHitTable = cms.EDProducer( + "PFRecHitFlatTableProducer", + objName=cms.string("pfrechits"), + label_rechits=cms.VInputTag( + cms.InputTag("hltParticleFlowRecHitECALUnseeded", "", "HLT"), + cms.InputTag("hltParticleFlowRecHitHBHE", "", "HLT"), + cms.InputTag("hltParticleFlowRecHitHF", "", "HLT"), + cms.InputTag("hltParticleFlowRecHitHO", "", "HLT"), + ), +) + +# Tracker PSimHits as a separate NanoAOD collection. +process.trackerSimHitTable = cms.EDProducer( + "TrackerSimHitFlatTableProducer", + objName=cms.string("trackersimhits"), + label_simhits=cms.VInputTag( + cms.InputTag("g4SimHits", "TrackerHitsPixelBarrelLowTof"), + cms.InputTag("g4SimHits", "TrackerHitsPixelBarrelHighTof"), + cms.InputTag("g4SimHits", "TrackerHitsPixelEndcapLowTof"), + cms.InputTag("g4SimHits", "TrackerHitsPixelEndcapHighTof"), + cms.InputTag("g4SimHits", "TrackerHitsTIBLowTof"), + cms.InputTag("g4SimHits", "TrackerHitsTIBHighTof"), + cms.InputTag("g4SimHits", "TrackerHitsTIDLowTof"), + cms.InputTag("g4SimHits", "TrackerHitsTIDHighTof"), + cms.InputTag("g4SimHits", "TrackerHitsTOBLowTof"), + cms.InputTag("g4SimHits", "TrackerHitsTOBHighTof"), + cms.InputTag("g4SimHits", "TrackerHitsTECLowTof"), + cms.InputTag("g4SimHits", "TrackerHitsTECHighTof"), + ), +) # process.load('Configuration.EventContent.EventContent_cff') process.NANOAODSIMoutput = cms.OutputModule("NanoAODOutputModule", compressionAlgorithm = cms.untracked.string('ZLIB'), @@ -208,6 +247,8 @@ + process.truthLogicalGraphHitIndexProducer + process.truthLogicalGraphDumper + process.recHitTable + + process.pfRecHitTable + + process.trackerSimHitTable ) process.nano_step = cms.EndPath(process.NANOAODSIMoutput) From 3a6aa2ec6be1fa74344ee81767011e4ff6e1aedf Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Sat, 13 Jun 2026 07:10:30 +0200 Subject: [PATCH 40/53] TruthInfo: clang-format TrackerSimHitFlatTableProducer --- .../TruthInfo/plugins/TrackerSimHitFlatTableProducer.cc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/PhysicsTools/TruthInfo/plugins/TrackerSimHitFlatTableProducer.cc b/PhysicsTools/TruthInfo/plugins/TrackerSimHitFlatTableProducer.cc index 856caff13b7eb..4170f2ca81630 100644 --- a/PhysicsTools/TruthInfo/plugins/TrackerSimHitFlatTableProducer.cc +++ b/PhysicsTools/TruthInfo/plugins/TrackerSimHitFlatTableProducer.cc @@ -29,9 +29,9 @@ class TrackerSimHitFlatTableProducer : public edm::stream::EDProducer<> { public: explicit TrackerSimHitFlatTableProducer(edm::ParameterSet const& params) : objName_(params.getParameter("objName")), - simhits_tokens_{edm::vector_transform( - params.getParameter>("label_simhits"), - [this](edm::InputTag const& tag) { return consumes(tag); })}, + simhits_tokens_{ + edm::vector_transform(params.getParameter>("label_simhits"), + [this](edm::InputTag const& tag) { return consumes(tag); })}, trackerGeomToken_(esConsumes()) { produces(); } From 6e0b5904afd34413699360cdc2588ace03f8bdb5 Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Sat, 13 Jun 2026 07:10:30 +0200 Subject: [PATCH 41/53] TruthInfo: add seed and decay-pattern selection to the logical truth graph --- PhysicsTools/TruthInfo/BuildFile.xml | 1 + PhysicsTools/TruthInfo/README.md | 34 +- .../TruthLogicalGraphPostProcessor.h | 26 +- .../src/TruthLogicalGraphPostProcessor.cc | 736 ++++++++++-------- .../test/TruthLogicalGraphPostProcessor_t.cpp | 466 ++++++++++- .../test/dumpTruthGraphsFromGENSIMRECO_cfg.py | 72 +- .../python/truthPrevalidation_cff.py | 1 + 7 files changed, 936 insertions(+), 400 deletions(-) diff --git a/PhysicsTools/TruthInfo/BuildFile.xml b/PhysicsTools/TruthInfo/BuildFile.xml index 6726efdfce6e0..9334d0809c479 100644 --- a/PhysicsTools/TruthInfo/BuildFile.xml +++ b/PhysicsTools/TruthInfo/BuildFile.xml @@ -1,4 +1,5 @@ + diff --git a/PhysicsTools/TruthInfo/README.md b/PhysicsTools/TruthInfo/README.md index ca2faabdd8f94..da0f3e02643bc 100644 --- a/PhysicsTools/TruthInfo/README.md +++ b/PhysicsTools/TruthInfo/README.md @@ -385,17 +385,41 @@ collapseIntermediateGenParticles = cms.bool(True) This helps reduce visual clutter from intermediate generator copies while preserving the physically relevant branching structure. -### Filtering by mother PDG id +### Selecting the interesting physics subgraph -The logical graph can optionally be restricted to a selected particle species and its descendants: +The logical graph can optionally be restricted to a physics selection configured in the `postProcessing` PSet: ```python -motherPdgId = cms.int32(0) +postProcessing = cms.PSet( + seedPdgIds = cms.vint32(23), + seedParentDepth = cms.uint32(1), + decayPdgIdGroups = cms.VPSet( + cms.PSet(pdgIds = cms.vint32(13, -13)), + ), +) ``` -The default value `0` keeps the full graph. +Selection by seed PDG id (`seedPdgIds`): + +* the most upstream particle of each matching chain becomes a root of the selected graph (for a `Z0 -> Z0 -> Z0` chain, the earliest copy); +* the full downstream subgraph of each root is kept; +* `seedParentDepth` generations of ancestors are kept above each root as context only, without their other descendants; +* stable status-1 GEN particles outside the selection are always kept, attached to one artificial source vertex, unless explicitly ignored; +* kept particles whose production vertices all fall outside the selection are attached to the same artificial source vertex, which conceptually represents ISR or other uninteresting upstream activity; +* the special value `0` disables the selection and keeps the full graph (debugging escape hatch); +* an empty list applies no seed-based cut. + +Selection by decay pattern (`decayPdgIdGroups`): + +* each group is an unordered, charge-sensitive multiset of PDG ids (`[13, -13]` differs from `[13, 13]`); groups are OR-ed; +* matching is local to one decay vertex after following same-PDG radiating copy chains (`Z -> Z gamma`), with extra products such as FSR photons allowed; unrelated particles from different branches can never be combined, and `Z -> tau tau -> mu nu nu mu nu nu` does not match `[13, -13]`. + +Combination semantics: -A non-zero value keeps particles matching the requested PDG id and the corresponding descendant subgraphs. +* only `seedPdgIds`: keep all decays of the selected roots; +* only `decayPdgIdGroups`: select vertices whose outgoing PDG ids contain a group, keep the matched particles, their downstream subgraphs, and the matched vertex as common production context; +* both: keep only roots whose effective decay products match a group (`Z -> mu mu` but not `Z -> e e`); if the event contains no particle with a seed PDG id at all, fall back to the direct vertex search (for generators that do not write the resonance explicitly); +* if a selection is configured but nothing matches, the output contains only the stable GEN particles attached to the artificial source vertex, and a warning is logged. ## Trajectory checkpoints diff --git a/PhysicsTools/TruthInfo/interface/TruthLogicalGraphPostProcessor.h b/PhysicsTools/TruthInfo/interface/TruthLogicalGraphPostProcessor.h index d54b5c7bc62a7..4eaea1566e0ff 100644 --- a/PhysicsTools/TruthInfo/interface/TruthLogicalGraphPostProcessor.h +++ b/PhysicsTools/TruthInfo/interface/TruthLogicalGraphPostProcessor.h @@ -15,12 +15,29 @@ namespace truth { bool collapseIntermediateGenParticles = true; // If empty, no seed-based graph cut is applied. + // The most upstream particle of each matching chain becomes a root of the + // selected graph. The special value 0 disables the selection and keeps the + // full graph (debugging escape hatch). std::vector seedPdgIds; - // For each selected seed particle, keep this many generations of parents - // above the seed before keeping the full downstream graph. + // For each selected root, keep this many generations of ancestors above it + // as context only: the ancestors and connecting vertices are kept, but not + // their other descendants. uint32_t seedParentDepth = 0; + // Decay patterns of interest. Each group is an unordered, charge-sensitive + // multiset of PDG ids; groups are OR-ed. + // + // Without seedPdgIds: a vertex whose outgoing PDG ids contain a group as a + // sub-multiset is selected, and the matched particles plus their downstream + // subgraphs are kept. + // + // With seedPdgIds: only seed roots whose effective decay products (after + // following same-PDG radiating copy chains) contain a group are kept. If + // the event contains no particle with a seed PDG id at all, the direct + // vertex search is used as a fallback. + std::vector> decayPdgIdGroups; + // Particles with these exact PDG ids are removed from the final logical graph. // If such a particle is internal, its production and decay vertices are merged // so that the graph remains navigable. @@ -40,8 +57,9 @@ namespace truth { bool mergeGenSimVerticesByPosition = true; // Absolute tolerance used for each x, y, z, t component when matching - // GEN-only and SIM-only vertices by position. - double genSimVertexPositionTolerance = 1e-6; + // GEN-only and SIM-only vertices by position. Keep in sync with the + // default in psetDescription(). + double genSimVertexPositionTolerance = 5e-3; }; class TruthLogicalGraphPostProcessor { diff --git a/PhysicsTools/TruthInfo/src/TruthLogicalGraphPostProcessor.cc b/PhysicsTools/TruthInfo/src/TruthLogicalGraphPostProcessor.cc index 268bc6fdfd240..992801cb7015e 100644 --- a/PhysicsTools/TruthInfo/src/TruthLogicalGraphPostProcessor.cc +++ b/PhysicsTools/TruthInfo/src/TruthLogicalGraphPostProcessor.cc @@ -3,11 +3,14 @@ #include #include #include +#include #include #include #include #include +#include "FWCore/MessageLogger/interface/MessageLogger.h" + namespace { struct DSU { @@ -93,16 +96,11 @@ namespace { if (!genSimPair) return false; - bool areCompatible = compatibleVertexPositions(a, b, tolerance); - if (!areCompatible) { - std::cout << "vertices not merged: (" << a.position.x() << " , " << a.position.y() << " , " << a.position.z() - << " , " << a.position.t() << ") too far from " << "(" << b.position.x() << " , " << b.position.y() - << " , " << b.position.z() << " , " << b.position.t() << ")" << std::endl; - } else { - std::cout << "vertices successfully merged: (" << a.position.x() << " , " << a.position.y() << " , " - << a.position.z() << " , " << a.position.t() << ") compatible with " << "(" << b.position.x() << " , " - << b.position.y() << " , " << b.position.z() << " , " << b.position.t() << ")" << std::endl; - } + const bool areCompatible = compatibleVertexPositions(a, b, tolerance); + LogDebug("TruthLogicalGraphPostProcessor") + << "GEN/SIM vertex pair (" << a.position.x() << ", " << a.position.y() << ", " << a.position.z() << ", " + << a.position.t() << ") vs (" << b.position.x() << ", " << b.position.y() << ", " << b.position.z() << ", " + << b.position.t() << "): " << (areCompatible ? "merged" : "not merged"); return areCompatible; } @@ -135,6 +133,83 @@ namespace { } } + // Rebuild the four CSR adjacency arrays of `output` from the edges of + // `input`, remapping particle and vertex ids and dropping edges with an + // unmapped endpoint. `extraProductionEdges` are additional + // (newVertex, newParticle) production-side edges, e.g. those attaching + // particles to the artificial source vertex. buildCSR sorts and deduplicates, + // so the collection order here does not affect the result. + void rebuildAdjacency(truth::Graph const& input, + std::vector const& oldParticleToNew, + std::vector const& oldVertexToNew, + std::vector> const& extraProductionEdges, + truth::Graph& output) { + const uint32_t nParticles = input.nParticles(); + + std::vector> particleToDecayVertexPairs; + std::vector> particleToProductionVertexPairs; + std::vector> vertexToOutgoingParticlePairs; + std::vector> vertexToIncomingParticlePairs; + + for (uint32_t oldVertex = 0; oldVertex < input.nVertices(); ++oldVertex) { + const int32_t newVertex = oldVertexToNew[oldVertex]; + if (newVertex < 0) + continue; + + for (const uint32_t oldParticle : input.incomingParticles(oldVertex)) { + if (oldParticle >= nParticles) + continue; + + const int32_t newParticle = oldParticleToNew[oldParticle]; + if (newParticle < 0) + continue; + + particleToDecayVertexPairs.emplace_back(static_cast(newParticle), static_cast(newVertex)); + vertexToIncomingParticlePairs.emplace_back(static_cast(newVertex), + static_cast(newParticle)); + } + + for (const uint32_t oldParticle : input.outgoingParticles(oldVertex)) { + if (oldParticle >= nParticles) + continue; + + const int32_t newParticle = oldParticleToNew[oldParticle]; + if (newParticle < 0) + continue; + + vertexToOutgoingParticlePairs.emplace_back(static_cast(newVertex), + static_cast(newParticle)); + particleToProductionVertexPairs.emplace_back(static_cast(newParticle), + static_cast(newVertex)); + } + } + + for (auto const& [newVertex, newParticle] : extraProductionEdges) { + vertexToOutgoingParticlePairs.emplace_back(newVertex, newParticle); + particleToProductionVertexPairs.emplace_back(newParticle, newVertex); + } + + buildCSR(output.nParticles(), + particleToDecayVertexPairs, + output.particleToDecayVertexOffsets, + output.particleToDecayVertices); + + buildCSR(output.nParticles(), + particleToProductionVertexPairs, + output.particleToProductionVertexOffsets, + output.particleToProductionVertices); + + buildCSR(output.nVertices(), + vertexToOutgoingParticlePairs, + output.vertexToOutgoingParticleOffsets, + output.vertexToOutgoingParticles); + + buildCSR(output.nVertices(), + vertexToIncomingParticlePairs, + output.vertexToIncomingParticleOffsets, + output.vertexToIncomingParticles); + } + bool directCollapsibleGenParticleChain(truth::Graph const& graph, uint32_t particleId, uint32_t& childId, @@ -290,63 +365,7 @@ namespace { output.vertices.push_back(input.vertices[oldVertex]); } - std::vector> particleToDecayVertexPairs; - std::vector> particleToProductionVertexPairs; - std::vector> vertexToOutgoingParticlePairs; - std::vector> vertexToIncomingParticlePairs; - - for (uint32_t oldVertex = 0; oldVertex < nVertices; ++oldVertex) { - const int32_t newVertex = oldVertexToNew[oldVertex]; - if (newVertex < 0) - continue; - - for (uint32_t oldParticle : input.incomingParticles(oldVertex)) { - if (oldParticle >= oldParticleToNew.size()) - continue; - - const int32_t newParticle = oldParticleToNew[oldParticle]; - if (newParticle < 0) - continue; - - particleToDecayVertexPairs.emplace_back(static_cast(newParticle), static_cast(newVertex)); - vertexToIncomingParticlePairs.emplace_back(static_cast(newVertex), - static_cast(newParticle)); - } - - for (uint32_t oldParticle : input.outgoingParticles(oldVertex)) { - if (oldParticle >= oldParticleToNew.size()) - continue; - - const int32_t newParticle = oldParticleToNew[oldParticle]; - if (newParticle < 0) - continue; - - vertexToOutgoingParticlePairs.emplace_back(static_cast(newVertex), - static_cast(newParticle)); - particleToProductionVertexPairs.emplace_back(static_cast(newParticle), - static_cast(newVertex)); - } - } - - buildCSR(output.nParticles(), - particleToDecayVertexPairs, - output.particleToDecayVertexOffsets, - output.particleToDecayVertices); - - buildCSR(output.nParticles(), - particleToProductionVertexPairs, - output.particleToProductionVertexOffsets, - output.particleToProductionVertices); - - buildCSR(output.nVertices(), - vertexToOutgoingParticlePairs, - output.vertexToOutgoingParticleOffsets, - output.vertexToOutgoingParticles); - - buildCSR(output.nVertices(), - vertexToIncomingParticlePairs, - output.vertexToIncomingParticleOffsets, - output.vertexToIncomingParticles); + rebuildAdjacency(input, oldParticleToNew, oldVertexToNew, {}, output); return output; } @@ -388,116 +407,215 @@ namespace { } } - void markUpstreamToParticle(truth::Graph const& graph, - uint32_t particleId, - std::vector& keepParticle, - std::vector& keepVertex) { - if (particleId >= graph.nParticles()) + // Keep up to parentDepth generations of ancestors above each root as context + // only: the ancestor particles and the connecting vertices are kept, but + // their other descendants and their own deeper ancestry are not. + void markAncestorContext(truth::Graph const& graph, + std::vector const& roots, + uint32_t parentDepth, + std::vector& keepParticle, + std::vector& keepVertex) { + if (parentDepth == 0) return; - std::queue queue; + std::vector seen(graph.nParticles(), 0); + std::queue> queue; - if (!keepParticle[particleId]) { - keepParticle[particleId] = 1; - } + for (const uint32_t root : roots) { + if (root >= graph.nParticles()) + continue; - queue.push(particleId); + if (!seen[root]) { + seen[root] = 1; + queue.emplace(root, 0); + } + } while (!queue.empty()) { - const uint32_t currentParticle = queue.front(); + const auto [particleId, depth] = queue.front(); queue.pop(); - for (uint32_t vertexId : graph.productionVertices(currentParticle)) { + if (depth >= parentDepth) + continue; + + for (const uint32_t vertexId : graph.productionVertices(particleId)) { if (vertexId >= graph.nVertices()) continue; keepVertex[vertexId] = 1; - for (uint32_t parentId : graph.incomingParticles(vertexId)) { + for (const uint32_t parentId : graph.incomingParticles(vertexId)) { if (parentId >= graph.nParticles()) continue; - if (!keepParticle[parentId]) { - keepParticle[parentId] = 1; - queue.push(parentId); + keepParticle[parentId] = 1; + + if (!seen[parentId]) { + seen[parentId] = 1; + queue.emplace(parentId, depth + 1); } } } } } - std::vector expandSeedsWithParents(truth::Graph const& graph, - std::vector const& seedParticles, - uint32_t parentDepth) { - std::vector startParticles = seedParticles; - - std::vector seen(graph.nParticles(), 0); - std::queue> queue; + // Restrict matches to the most upstream ones: a match that is a strict + // descendant of another match is covered by that match's subgraph and is not + // an independent root. Single multi-source downstream BFS, O(V + E). + std::vector mostUpstreamOf(truth::Graph const& graph, std::vector const& matches) { + const uint32_t nParticles = graph.nParticles(); - for (const uint32_t seed : seedParticles) { - if (seed >= graph.nParticles()) - continue; + std::vector strictDescendant(nParticles, 0); + std::vector visited(nParticles, 0); + std::queue queue; - if (!seen[seed]) { - seen[seed] = 1; - queue.emplace(seed, 0); + for (const uint32_t match : matches) { + if (match < nParticles && !visited[match]) { + visited[match] = 1; + queue.push(match); } } while (!queue.empty()) { - const auto [particleId, depth] = queue.front(); + const uint32_t particleId = queue.front(); queue.pop(); - if (depth >= parentDepth) - continue; - - for (const uint32_t vertexId : graph.productionVertices(particleId)) { + for (const uint32_t vertexId : graph.decayVertices(particleId)) { if (vertexId >= graph.nVertices()) continue; - for (const uint32_t parentId : graph.incomingParticles(vertexId)) { - if (parentId >= graph.nParticles()) + for (const uint32_t childId : graph.outgoingParticles(vertexId)) { + if (childId >= nParticles) continue; - if (!seen[parentId]) { - seen[parentId] = 1; - startParticles.push_back(parentId); - queue.emplace(parentId, depth + 1); + strictDescendant[childId] = 1; + + if (!visited[childId]) { + visited[childId] = 1; + queue.push(childId); } } } } - std::sort(startParticles.begin(), startParticles.end()); - startParticles.erase(std::unique(startParticles.begin(), startParticles.end()), startParticles.end()); + std::vector roots; + roots.reserve(matches.size()); - return startParticles; + for (const uint32_t match : matches) { + if (match < nParticles && !strictDescendant[match]) + roots.push_back(match); + } + + return roots; } - void closeKeptVerticesWithAllParents(truth::Graph const& graph, - std::vector& keepParticle, - std::vector& keepVertex) { - bool changed = true; + // Follow the radiating-copy chain of a particle: while the current copy has + // exactly one decay vertex with exactly one same-PDG daughter, advance to it. + // Pure 1 -> 1 copy chains are already gone if collapseIntermediateGenParticles + // ran before; this handles surviving chains like Z -> Z gamma. Any ambiguity + // (several decay vertices, several same-PDG daughters) stops the walk. + uint32_t lastCopyOf(truth::Graph const& graph, uint32_t rootId) { + const int32_t pdgId = graph.particles[rootId].pdgId; + uint32_t current = rootId; + + for (uint32_t guard = 0; guard < graph.nParticles(); ++guard) { + if (graph.particles[current].status == 1) + break; + + const auto decayVertices = graph.decayVertices(current); + if (decayVertices.size() != 1) + break; - while (changed) { - changed = false; + uint32_t sameIdChild = 0; + uint32_t nSameId = 0; - for (uint32_t vertexId = 0; vertexId < graph.nVertices(); ++vertexId) { - if (!keepVertex[vertexId]) - continue; + for (const uint32_t childId : graph.outgoingParticles(decayVertices.front())) { + if (childId < graph.nParticles() && childId != current && graph.particles[childId].pdgId == pdgId) { + sameIdChild = childId; + ++nSameId; + } + } - for (uint32_t parentId : graph.incomingParticles(vertexId)) { - if (parentId >= graph.nParticles()) - continue; + if (nSameId != 1) + break; - if (keepParticle[parentId]) - continue; + current = sameIdChild; + } + + return current; + } + + // Sorted PDG ids of the effective decay products of a root: the outgoing + // particles of the decay vertices of its last radiating copy. + std::vector effectiveDecayProductPdgIds(truth::Graph const& graph, uint32_t rootId) { + const uint32_t lastCopy = lastCopyOf(graph, rootId); + + std::vector pdgIds; + + for (const uint32_t vertexId : graph.decayVertices(lastCopy)) { + if (vertexId >= graph.nVertices()) + continue; + + for (const uint32_t childId : graph.outgoingParticles(vertexId)) { + if (childId < graph.nParticles()) + pdgIds.push_back(graph.particles[childId].pdgId); + } + } + + std::sort(pdgIds.begin(), pdgIds.end()); + + return pdgIds; + } + + // Multiset containment on sorted ranges: extras in `have` are allowed. + bool multisetContains(std::vector const& sortedHave, std::vector const& sortedNeed) { + return std::includes(sortedHave.begin(), sortedHave.end(), sortedNeed.begin(), sortedNeed.end()); + } + + // Direct decay-pattern search: a vertex whose outgoing PDG id multiset + // contains a group selects that vertex (as common production context) and the + // matched outgoing particles as roots. Matching is local to one vertex, so + // unrelated particles from different branches can never be combined. + void findDecayPatternMatches(truth::Graph const& graph, + std::vector> const& sortedGroups, + std::vector& roots, + std::vector& matchedVertices) { + std::vector outgoingPdgIds; + + for (uint32_t vertexId = 0; vertexId < graph.nVertices(); ++vertexId) { + const auto outgoing = graph.outgoingParticles(vertexId); + if (outgoing.empty()) + continue; + + outgoingPdgIds.clear(); + + for (const uint32_t childId : outgoing) { + if (childId < graph.nParticles()) + outgoingPdgIds.push_back(graph.particles[childId].pdgId); + } + + std::sort(outgoingPdgIds.begin(), outgoingPdgIds.end()); + + bool vertexMatched = false; + + for (auto const& group : sortedGroups) { + if (!multisetContains(outgoingPdgIds, group)) + continue; + + vertexMatched = true; - markUpstreamToParticle(graph, parentId, keepParticle, keepVertex); - changed = true; + for (const uint32_t childId : outgoing) { + if (childId < graph.nParticles() && containsPdgId(group, graph.particles[childId].pdgId)) + roots.push_back(childId); } } + + if (vertexMatched) + matchedVertices.push_back(vertexId); } + + std::sort(roots.begin(), roots.end()); + roots.erase(std::unique(roots.begin(), roots.end()), roots.end()); } void dropVerticesWithoutVisibleParticles(truth::Graph const& graph, @@ -532,13 +650,13 @@ namespace { truth::Graph rebuildFilteredGraph(truth::Graph const& input, std::vector const& keepParticle, std::vector const& keepVertex, - std::vector const& connectToCollapsedStableVertex) { + std::vector const& connectToArtificialSourceVertex) { const uint32_t nParticles = input.nParticles(); const uint32_t nVertices = input.nVertices(); - const bool needCollapsedStableVertex = std::any_of(connectToCollapsedStableVertex.begin(), - connectToCollapsedStableVertex.end(), - [](uint8_t value) { return value != 0; }); + const bool needArtificialSourceVertex = std::any_of(connectToArtificialSourceVertex.begin(), + connectToArtificialSourceVertex.end(), + [](uint8_t value) { return value != 0; }); truth::Graph output; @@ -546,7 +664,7 @@ namespace { std::vector oldVertexToNew(nVertices, -1); output.particles.reserve(nParticles); - output.vertices.reserve(nVertices + (needCollapsedStableVertex ? 1 : 0)); + output.vertices.reserve(nVertices + (needArtificialSourceVertex ? 1 : 0)); for (uint32_t oldParticle = 0; oldParticle < nParticles; ++oldParticle) { if (!keepParticle[oldParticle]) @@ -564,10 +682,10 @@ namespace { output.vertices.push_back(input.vertices[oldVertex]); } - int32_t collapsedStableVertex = -1; + int32_t artificialSourceVertex = -1; - if (needCollapsedStableVertex) { - collapsedStableVertex = static_cast(output.vertices.size()); + if (needArtificialSourceVertex) { + artificialSourceVertex = static_cast(output.vertices.size()); truth::VertexData vertex; vertex.genNode = -1; @@ -578,122 +696,115 @@ namespace { output.vertices.push_back(vertex); } - std::vector> particleToDecayVertexPairs; - std::vector> particleToProductionVertexPairs; - std::vector> vertexToOutgoingParticlePairs; - std::vector> vertexToIncomingParticlePairs; + std::vector> artificialEdges; - for (uint32_t oldVertex = 0; oldVertex < nVertices; ++oldVertex) { - const int32_t newVertex = oldVertexToNew[oldVertex]; - if (newVertex < 0) - continue; + if (artificialSourceVertex >= 0) { + const uint32_t newVertex = static_cast(artificialSourceVertex); - for (uint32_t oldParticle : input.incomingParticles(oldVertex)) { - if (oldParticle >= nParticles) - continue; - - const int32_t newParticle = oldParticleToNew[oldParticle]; - if (newParticle < 0) - continue; - - particleToDecayVertexPairs.emplace_back(static_cast(newParticle), static_cast(newVertex)); - vertexToIncomingParticlePairs.emplace_back(static_cast(newVertex), - static_cast(newParticle)); - } - - for (uint32_t oldParticle : input.outgoingParticles(oldVertex)) { - if (oldParticle >= nParticles) + for (uint32_t oldParticle = 0; oldParticle < nParticles; ++oldParticle) { + if (!connectToArtificialSourceVertex[oldParticle]) continue; const int32_t newParticle = oldParticleToNew[oldParticle]; if (newParticle < 0) continue; - vertexToOutgoingParticlePairs.emplace_back(static_cast(newVertex), - static_cast(newParticle)); - particleToProductionVertexPairs.emplace_back(static_cast(newParticle), - static_cast(newVertex)); + artificialEdges.emplace_back(newVertex, static_cast(newParticle)); } } - if (collapsedStableVertex >= 0) { - const uint32_t newVertex = static_cast(collapsedStableVertex); + rebuildAdjacency(input, oldParticleToNew, oldVertexToNew, artificialEdges, output); - for (uint32_t oldParticle = 0; oldParticle < nParticles; ++oldParticle) { - if (!connectToCollapsedStableVertex[oldParticle]) - continue; + return output; + } - const int32_t newParticle = oldParticleToNew[oldParticle]; - if (newParticle < 0) - continue; + truth::Graph filterGraphBySelection(truth::Graph const& input, + truth::LogicalGraphPostProcessingConfig const& config) { + if (input.empty()) + return input; + + // Skip empty groups: they would match every vertex. + std::vector> sortedGroups; + sortedGroups.reserve(config.decayPdgIdGroups.size()); - vertexToOutgoingParticlePairs.emplace_back(newVertex, static_cast(newParticle)); - particleToProductionVertexPairs.emplace_back(static_cast(newParticle), newVertex); + for (auto const& group : config.decayPdgIdGroups) { + if (!group.empty()) { + sortedGroups.push_back(group); + std::sort(sortedGroups.back().begin(), sortedGroups.back().end()); } } - buildCSR(output.nParticles(), - particleToDecayVertexPairs, - output.particleToDecayVertexOffsets, - output.particleToDecayVertices); - - buildCSR(output.nParticles(), - particleToProductionVertexPairs, - output.particleToProductionVertexOffsets, - output.particleToProductionVertices); + const bool haveSeeds = !config.seedPdgIds.empty(); + const bool haveGroups = !sortedGroups.empty(); - buildCSR(output.nVertices(), - vertexToOutgoingParticlePairs, - output.vertexToOutgoingParticleOffsets, - output.vertexToOutgoingParticles); - - buildCSR(output.nVertices(), - vertexToIncomingParticlePairs, - output.vertexToIncomingParticleOffsets, - output.vertexToIncomingParticles); - - return output; - } - - truth::Graph filterGraphBySeedPdgIds(truth::Graph const& input, - truth::LogicalGraphPostProcessingConfig const& config) { - if (input.empty()) + if (!haveSeeds && !haveGroups) return input; - if (config.seedPdgIds.empty()) + // Debug escape hatch: no real particle has PDG id 0, so seedPdgIds = {0} + // explicitly requests the full, unfiltered graph. + if (containsPdgId(config.seedPdgIds, 0)) return input; const uint32_t nParticles = input.nParticles(); const uint32_t nVertices = input.nVertices(); - std::vector seedParticles; + std::vector roots; + std::vector patternVertices; - for (uint32_t particleId = 0; particleId < nParticles; ++particleId) { - if (containsPdgId(config.seedPdgIds, input.particles[particleId].pdgId)) - seedParticles.push_back(particleId); - } + if (haveSeeds) { + std::vector matches; - if (seedParticles.empty()) - return input; + for (uint32_t particleId = 0; particleId < nParticles; ++particleId) { + if (containsPdgId(config.seedPdgIds, input.particles[particleId].pdgId)) + matches.push_back(particleId); + } - const auto startParticles = expandSeedsWithParents(input, seedParticles, config.seedParentDepth); + if (!matches.empty()) { + roots = mostUpstreamOf(input, matches); + + if (haveGroups) { + // Keep only seed roots whose effective decay matches a group, e.g. + // Z -> mu+ mu- but not Z -> e+ e-. + std::erase_if(roots, [&](uint32_t root) { + const auto products = effectiveDecayProductPdgIds(input, root); + return std::none_of(sortedGroups.begin(), sortedGroups.end(), [&](auto const& group) { + return multisetContains(products, group); + }); + }); + } + } else if (haveGroups) { + // The generator did not write the requested resonance explicitly: + // fall back to the direct decay-pattern search. + findDecayPatternMatches(input, sortedGroups, roots, patternVertices); + } + } else { + findDecayPatternMatches(input, sortedGroups, roots, patternVertices); + } + + if (roots.empty()) { + edm::LogWarning("TruthLogicalGraphPostProcessor") + << "Configured truth graph selection (seedPdgIds and/or decayPdgIdGroups) matched nothing in this event; " + "keeping only stable GEN particles attached to the artificial source vertex."; + } std::vector keepParticle(nParticles, 0); std::vector keepVertex(nVertices, 0); - for (const uint32_t particleId : startParticles) { - markDownstreamFromParticle(input, particleId, keepParticle, keepVertex); + for (const uint32_t root : roots) { + markDownstreamFromParticle(input, root, keepParticle, keepVertex); } - // The graph is a DAG, not a tree. If a kept downstream vertex has parents - // that are not in the selected subgraph, keep those parents and their full - // upstream history to avoid creating misleading partial vertices. - closeKeptVerticesWithAllParents(input, keepParticle, keepVertex); + // Pattern-matched vertices are kept as the common production context of + // their matched outgoing particles. + for (const uint32_t vertexId : patternVertices) { + keepVertex[vertexId] = 1; + } + + markAncestorContext(input, roots, config.seedParentDepth, keepParticle, keepVertex); // Keep every final-state GEN particle unless the user explicitly asked to - // ignore/collapse it. Stable particles outside the interesting subgraph are - // attached to one artificial source vertex. - std::vector connectToCollapsedStableVertex(nParticles, 0); + // ignore/collapse it. + std::vector stableSpectator(nParticles, 0); for (uint32_t particleId = 0; particleId < nParticles; ++particleId) { if (!isStableGenParticle(input, particleId)) @@ -706,12 +817,36 @@ namespace { continue; keepParticle[particleId] = 1; - connectToCollapsedStableVertex[particleId] = 1; + stableSpectator[particleId] = 1; } dropVerticesWithoutVisibleParticles(input, keepParticle, keepVertex); - return rebuildFilteredGraph(input, keepParticle, keepVertex, connectToCollapsedStableVertex); + // Attach to the artificial source vertex every kept particle whose real + // production vertices were all dropped: stable spectators and particles at + // the truncated upstream boundary (conceptually ISR or other uninteresting + // upstream activity). True sources of the input graph stay sources. + std::vector connectToArtificialSourceVertex(nParticles, 0); + + for (uint32_t particleId = 0; particleId < nParticles; ++particleId) { + if (!keepParticle[particleId]) + continue; + + const auto productionVertices = input.productionVertices(particleId); + + if (productionVertices.empty() && !stableSpectator[particleId]) + continue; + + const bool hasKeptProduction = + std::any_of(productionVertices.begin(), productionVertices.end(), [&](uint32_t vertexId) { + return vertexId < nVertices && keepVertex[vertexId]; + }); + + if (!hasKeptProduction) + connectToArtificialSourceVertex[particleId] = 1; + } + + return rebuildFilteredGraph(input, keepParticle, keepVertex, connectToArtificialSourceVertex); } void mergeVertexPayload(truth::VertexData& output, truth::VertexData const& input) { @@ -805,52 +940,11 @@ namespace { mergeVertexPayload(output.vertices[newVertex], input.vertices[oldVertex]); } - std::vector> particleToDecayVertexPairs; - std::vector> particleToProductionVertexPairs; - std::vector> vertexToOutgoingParticlePairs; - std::vector> vertexToIncomingParticlePairs; - - for (uint32_t oldVertex = 0; oldVertex < nVertices; ++oldVertex) { - const int32_t newVertex = oldVertexToNew[oldVertex]; - if (newVertex < 0) - continue; - - for (uint32_t particleId : input.incomingParticles(oldVertex)) { - if (particleId >= nParticles) - continue; - - particleToDecayVertexPairs.emplace_back(particleId, static_cast(newVertex)); - vertexToIncomingParticlePairs.emplace_back(static_cast(newVertex), particleId); - } - - for (uint32_t particleId : input.outgoingParticles(oldVertex)) { - if (particleId >= nParticles) - continue; - - vertexToOutgoingParticlePairs.emplace_back(static_cast(newVertex), particleId); - particleToProductionVertexPairs.emplace_back(particleId, static_cast(newVertex)); - } - } - - buildCSR(output.nParticles(), - particleToDecayVertexPairs, - output.particleToDecayVertexOffsets, - output.particleToDecayVertices); + // Particles are untouched by vertex merging: identity map. + std::vector oldParticleToNew(nParticles); + std::iota(oldParticleToNew.begin(), oldParticleToNew.end(), 0); - buildCSR(output.nParticles(), - particleToProductionVertexPairs, - output.particleToProductionVertexOffsets, - output.particleToProductionVertices); - - buildCSR(output.nVertices(), - vertexToOutgoingParticlePairs, - output.vertexToOutgoingParticleOffsets, - output.vertexToOutgoingParticles); - - buildCSR(output.nVertices(), - vertexToIncomingParticlePairs, - output.vertexToIncomingParticleOffsets, - output.vertexToIncomingParticles); + rebuildAdjacency(input, oldParticleToNew, oldVertexToNew, {}, output); return output; } @@ -922,9 +1016,17 @@ namespace { std::vector oldVertexToNew(nVertices, -1); - auto getNewVertex = [&](uint32_t oldVertex) -> uint32_t { - if (oldVertexToNew[oldVertex] >= 0) - return static_cast(oldVertexToNew[oldVertex]); + // Vertices in one DSU group share the id of the first visible member; + // vertices with no visible particle at all stay unmapped and disappear. + for (uint32_t oldVertex = 0; oldVertex < nVertices; ++oldVertex) { + const auto hasVisible = [&](auto const& particles) { + return std::any_of(particles.begin(), particles.end(), [&](uint32_t oldParticle) { + return oldParticle < nParticles && oldParticleToNew[oldParticle] >= 0; + }); + }; + + if (!hasVisible(input.incomingParticles(oldVertex)) && !hasVisible(input.outgoingParticles(oldVertex))) + continue; const int rep = vertexDSU.find(static_cast(oldVertex)); auto inserted = vertexRepToNew.emplace(rep, static_cast(output.vertices.size())); @@ -934,81 +1036,9 @@ namespace { } oldVertexToNew[oldVertex] = static_cast(inserted.first->second); - return inserted.first->second; - }; - - std::vector> particleToDecayVertexPairs; - std::vector> particleToProductionVertexPairs; - std::vector> vertexToOutgoingParticlePairs; - std::vector> vertexToIncomingParticlePairs; - - for (uint32_t oldVertex = 0; oldVertex < nVertices; ++oldVertex) { - bool hasVisibleIncoming = false; - bool hasVisibleOutgoing = false; - - for (uint32_t oldParticle : input.incomingParticles(oldVertex)) { - if (oldParticle < nParticles && oldParticleToNew[oldParticle] >= 0) { - hasVisibleIncoming = true; - break; - } - } - - for (uint32_t oldParticle : input.outgoingParticles(oldVertex)) { - if (oldParticle < nParticles && oldParticleToNew[oldParticle] >= 0) { - hasVisibleOutgoing = true; - break; - } - } - - if (!hasVisibleIncoming && !hasVisibleOutgoing) - continue; - - const uint32_t newVertex = getNewVertex(oldVertex); - - for (uint32_t oldParticle : input.incomingParticles(oldVertex)) { - if (oldParticle >= nParticles) - continue; - - const int32_t newParticle = oldParticleToNew[oldParticle]; - if (newParticle < 0) - continue; - - particleToDecayVertexPairs.emplace_back(static_cast(newParticle), newVertex); - vertexToIncomingParticlePairs.emplace_back(newVertex, static_cast(newParticle)); - } - - for (uint32_t oldParticle : input.outgoingParticles(oldVertex)) { - if (oldParticle >= nParticles) - continue; - - const int32_t newParticle = oldParticleToNew[oldParticle]; - if (newParticle < 0) - continue; - - vertexToOutgoingParticlePairs.emplace_back(newVertex, static_cast(newParticle)); - particleToProductionVertexPairs.emplace_back(static_cast(newParticle), newVertex); - } } - buildCSR(output.nParticles(), - particleToDecayVertexPairs, - output.particleToDecayVertexOffsets, - output.particleToDecayVertices); - - buildCSR(output.nParticles(), - particleToProductionVertexPairs, - output.particleToProductionVertexOffsets, - output.particleToProductionVertices); - - buildCSR(output.nVertices(), - vertexToOutgoingParticlePairs, - output.vertexToOutgoingParticleOffsets, - output.vertexToOutgoingParticles); - - buildCSR(output.nVertices(), - vertexToIncomingParticlePairs, - output.vertexToIncomingParticleOffsets, - output.vertexToIncomingParticles); + rebuildAdjacency(input, oldParticleToNew, oldVertexToNew, {}, output); return output; } @@ -1030,12 +1060,30 @@ namespace truth { desc.add>("seedPdgIds", {}) ->setComment( - "If non-empty, keep particles with these exact PDG ids, keep seedParentDepth generations above them, " - "then keep the full downstream subgraph. Stable GEN particles outside the selected subgraph are kept " - "and attached to one artificial collapsed vertex."); + "If non-empty, particles with these exact PDG ids seed the selection: the most upstream particle of " + "each matching chain becomes a root and its full downstream subgraph is kept. The special value 0 " + "disables the selection and keeps the full graph (debugging). Stable GEN particles outside the " + "selection are kept and attached to one artificial source vertex."); desc.add("seedParentDepth", 0) - ->setComment("Number of parent generations to keep above each seed particle before keeping downstream."); + ->setComment( + "Number of ancestor generations kept above each selected root as context only: the ancestors and " + "connecting vertices are kept, but not their other descendants. Kept particles whose production " + "vertices all fall outside the selection are attached to the artificial source vertex."); + + { + edm::ParameterSetDescription groupDesc; + groupDesc.add>("pdgIds", {}) + ->setComment("Unordered, charge-sensitive multiset of required PDG ids, e.g. (13, -13)."); + + desc.addVPSet("decayPdgIdGroups", groupDesc, {}) + ->setComment( + "Decay patterns of interest; groups are OR-ed. Without seedPdgIds: a vertex whose outgoing PDG ids " + "contain a group as a sub-multiset is selected and the matched particles plus their downstream " + "subgraphs are kept. With seedPdgIds: only seed roots whose effective decay products (after following " + "same-PDG radiating copy chains) contain a group are kept; if the event has no particle with a seed " + "PDG id at all, the direct vertex search is used as a fallback."); + } desc.add>("ignoredPdgIds", {}) ->setComment( @@ -1064,6 +1112,10 @@ namespace truth { config.collapseIntermediateGenParticles = pset.getParameter("collapseIntermediateGenParticles"); config.seedPdgIds = pset.getParameter>("seedPdgIds"); config.seedParentDepth = pset.getParameter("seedParentDepth"); + + for (auto const& groupPSet : pset.getParameter>("decayPdgIdGroups")) { + config.decayPdgIdGroups.push_back(groupPSet.getParameter>("pdgIds")); + } config.ignoredPdgIds = pset.getParameter>("ignoredPdgIds"); config.ignoredParticleIds = pset.getParameter>("ignoredParticleIds"); config.mergeGenSimVerticesByPosition = pset.getParameter("mergeGenSimVerticesByPosition"); @@ -1077,7 +1129,7 @@ namespace truth { input = collapseIntermediateGenParticleChains(input); } - input = filterGraphBySeedPdgIds(input, config_); + input = filterGraphBySelection(input, config_); if (config_.mergeGenSimVerticesByPosition) { input = mergeGenSimVerticesByPosition(input, config_.genSimVertexPositionTolerance); diff --git a/PhysicsTools/TruthInfo/test/TruthLogicalGraphPostProcessor_t.cpp b/PhysicsTools/TruthInfo/test/TruthLogicalGraphPostProcessor_t.cpp index 337fcbf660da2..a9ce18a6fd671 100644 --- a/PhysicsTools/TruthInfo/test/TruthLogicalGraphPostProcessor_t.cpp +++ b/PhysicsTools/TruthInfo/test/TruthLogicalGraphPostProcessor_t.cpp @@ -242,20 +242,32 @@ class TestTruthLogicalGraphPostProcessor : public CppUnit::TestFixture { CPPUNIT_TEST(testStatusOneGenParticlesAreNeverCollapsed); CPPUNIT_TEST(testStableGenSimParticlesSurviveIntermediateCollapse); CPPUNIT_TEST(testSeedCutKeepsUnrelatedStableGenSimParticlesThroughArtificialVertex); - CPPUNIT_TEST(testDagClosureKeepsAllParentsOfKeptVertices); + CPPUNIT_TEST(testSeedCutHidesUnselectedParentsOfKeptVertices); CPPUNIT_TEST(testIgnoredParticlesAreCollapsedAway); CPPUNIT_TEST(testSeedCutWithIgnoredParticles); CPPUNIT_TEST(testIgnoredParticleIdsAreCollapsedAway); + CPPUNIT_TEST(testSeedRootIsMostUpstreamThroughRadiatingCopyChain); + CPPUNIT_TEST(testSeedParentDepthKeepsAncestorContextOnly); + CPPUNIT_TEST(testSeedWithDecayGroupKeepsOnlyMatchingDecays); + CPPUNIT_TEST(testZToTauTauDoesNotMatchMuonDecayGroup); + CPPUNIT_TEST(testDecayGroupFallbackWhenSeedAbsent); + CPPUNIT_TEST(testSeedPdgIdZeroKeepsFullGraphForDebugging); CPPUNIT_TEST_SUITE_END(); public: void testStatusOneGenParticlesAreNeverCollapsed(); void testStableGenSimParticlesSurviveIntermediateCollapse(); void testSeedCutKeepsUnrelatedStableGenSimParticlesThroughArtificialVertex(); - void testDagClosureKeepsAllParentsOfKeptVertices(); + void testSeedCutHidesUnselectedParentsOfKeptVertices(); void testIgnoredParticlesAreCollapsedAway(); void testSeedCutWithIgnoredParticles(); void testIgnoredParticleIdsAreCollapsedAway(); + void testSeedRootIsMostUpstreamThroughRadiatingCopyChain(); + void testSeedParentDepthKeepsAncestorContextOnly(); + void testSeedWithDecayGroupKeepsOnlyMatchingDecays(); + void testZToTauTauDoesNotMatchMuonDecayGroup(); + void testDecayGroupFallbackWhenSeedAbsent(); + void testSeedPdgIdZeroKeepsFullGraphForDebugging(); }; CPPUNIT_TEST_SUITE_REGISTRATION(TestTruthLogicalGraphPostProcessor); @@ -392,7 +404,7 @@ void TestTruthLogicalGraphPostProcessor::testSeedCutKeepsUnrelatedStableGenSimPa } } -void TestTruthLogicalGraphPostProcessor::testDagClosureKeepsAllParentsOfKeptVertices() { +void TestTruthLogicalGraphPostProcessor::testSeedCutHidesUnselectedParentsOfKeptVertices() { try { GraphBuilder builder(5, 3); @@ -403,9 +415,9 @@ void TestTruthLogicalGraphPostProcessor::testDagClosureKeepsAllParentsOfKeptVert // pi0 -> v1 -> gamma // e- stable, unrelated // - // The seed is H. Keeping downstream from H keeps v0. Since v0 also has Z as - // an incoming parent, the postprocessor must include Z and the upstream path - // to Z instead of showing v0 as if it had only H as parent. + // The seed is H. Keeping downstream from H keeps v0. The unselected Z parent + // of v0 is not part of the selection and seedParentDepth is 0, so it must be + // hidden: v0 appears with H as its only incoming particle. builder.setGenParticle(0, 25, 2, 100); builder.setGenParticle(1, 23, 2, 101); builder.setGenParticle(2, 111, 2, 102); @@ -436,7 +448,7 @@ void TestTruthLogicalGraphPostProcessor::testDagClosureKeepsAllParentsOfKeptVert CPPUNIT_ASSERT(output.isConsistent()); CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 25)); - CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 23)); + CPPUNIT_ASSERT_EQUAL(uint32_t(0), countParticlesWithPdgId(output, 23)); CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 111)); CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 22)); @@ -451,19 +463,14 @@ void TestTruthLogicalGraphPostProcessor::testDagClosureKeepsAllParentsOfKeptVert const auto incoming = output.incomingParticles(pi0ProductionVertices.front()); - CPPUNIT_ASSERT_EQUAL(std::size_t(2), incoming.size()); - - std::vector incomingPdgIds; - incomingPdgIds.reserve(incoming.size()); - - for (uint32_t parent : incoming) { - incomingPdgIds.push_back(output.particles[parent].pdgId); - } + CPPUNIT_ASSERT_EQUAL(std::size_t(1), incoming.size()); + CPPUNIT_ASSERT_EQUAL(int32_t(25), output.particles[incoming.front()].pdgId); - std::sort(incomingPdgIds.begin(), incomingPdgIds.end()); + const uint32_t electron = findParticleWithPdgId(output, 11); + const auto electronProductionVertices = output.productionVertices(electron); - CPPUNIT_ASSERT_EQUAL(int32_t(23), incomingPdgIds[0]); - CPPUNIT_ASSERT_EQUAL(int32_t(25), incomingPdgIds[1]); + CPPUNIT_ASSERT_EQUAL(std::size_t(1), electronProductionVertices.size()); + CPPUNIT_ASSERT_EQUAL(artificialVertexId(output), electronProductionVertices.front()); } catch (cms::Exception const& ex) { std::cerr << ex.what() << std::endl; CPPUNIT_ASSERT(false); @@ -518,6 +525,409 @@ void TestTruthLogicalGraphPostProcessor::testIgnoredParticlesAreCollapsedAway() } } +void TestTruthLogicalGraphPostProcessor::testSeedRootIsMostUpstreamThroughRadiatingCopyChain() { + try { + GraphBuilder builder(6, 3); + + // q -> vq -> Z0 + // Z0 -> v0 -> { Z1, gamma } (radiating copy chain, survives chain collapse) + // Z1 -> v1 -> { mu+, mu- } + // + // With seedPdgIds = {23} only Z0 is a root: Z1 is a strict descendant of + // another match. The q parent is outside the selection (depth 0), so Z0 is + // attached to the artificial source vertex while Z1 keeps its real + // production vertex. + builder.setGenParticle(0, 1, 2, 100); + builder.setGenParticle(1, 23, 2, 101); + builder.setGenParticle(2, 23, 2, 102); + builder.setGenSimParticle(3, 22, 1, 103, 1003); + builder.setGenSimParticle(4, -13, 1, 104, 1004); + builder.setGenSimParticle(5, 13, 1, 105, 1005); + + builder.setGenVertex(0, 200); + builder.setGenVertex(1, 201); + builder.setGenSimVertex(2, 202, 2002); + + builder.addDecay(0, 0); + builder.addProduction(0, 1); + + builder.addDecay(1, 1); + builder.addProduction(1, 2); + builder.addProduction(1, 3); + + builder.addDecay(2, 2); + builder.addProduction(2, 4); + builder.addProduction(2, 5); + + auto graph = builder.finish(); + + auto config = defaultConfig(); + config.seedPdgIds = {23}; + config.seedParentDepth = 0; + + auto output = runPostProcessing(std::move(graph), config); + + CPPUNIT_ASSERT(output.isConsistent()); + + CPPUNIT_ASSERT_EQUAL(uint32_t(2), countParticlesWithPdgId(output, 23)); + CPPUNIT_ASSERT_EQUAL(uint32_t(0), countParticlesWithPdgId(output, 1)); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 22)); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, -13)); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 13)); + CPPUNIT_ASSERT(hasArtificialVertex(output)); + + // Exactly one Z (the most upstream root, with its q parent dropped) hangs + // off the artificial source vertex; the downstream copy keeps its real one. + const uint32_t artificial = artificialVertexId(output); + uint32_t nZAttachedToArtificial = 0; + + for (uint32_t particleId = 0; particleId < output.nParticles(); ++particleId) { + if (output.particles[particleId].pdgId != 23) + continue; + + const auto productionVertices = output.productionVertices(particleId); + + if (productionVertices.size() == 1 && productionVertices.front() == artificial) + ++nZAttachedToArtificial; + } + + CPPUNIT_ASSERT_EQUAL(uint32_t(1), nZAttachedToArtificial); + } catch (cms::Exception const& ex) { + std::cerr << ex.what() << std::endl; + CPPUNIT_ASSERT(false); + } +} + +void TestTruthLogicalGraphPostProcessor::testSeedParentDepthKeepsAncestorContextOnly() { + try { + GraphBuilder builder(7, 3); + + // g g -> vp -> { Z, q } + // Z -> vz -> { mu+, mu- } + // q -> vq -> { pi+ (stable) } + // + // With seedParentDepth = 1 the gluons and vp are kept as context, but the + // sibling q and its decay chain are not: ancestors no longer pull in their + // own downstream. The stable pion survives through the artificial vertex. + builder.setGenParticle(0, 21, 2, 100); + builder.setGenParticle(1, 21, 2, 101); + builder.setGenParticle(2, 23, 2, 102); + builder.setGenParticle(3, 1, 2, 103); + builder.setGenSimParticle(4, 211, 1, 104, 1004); + builder.setGenSimParticle(5, -13, 1, 105, 1005); + builder.setGenSimParticle(6, 13, 1, 106, 1006); + + builder.setGenVertex(0, 200); + builder.setGenVertex(1, 201); + builder.setGenSimVertex(2, 202, 2002); + + builder.addDecay(0, 0); + builder.addDecay(1, 0); + builder.addProduction(0, 2); + builder.addProduction(0, 3); + + builder.addDecay(3, 1); + builder.addProduction(1, 4); + + builder.addDecay(2, 2); + builder.addProduction(2, 5); + builder.addProduction(2, 6); + + auto graph = builder.finish(); + + auto config = defaultConfig(); + config.seedPdgIds = {23}; + config.seedParentDepth = 1; + + auto output = runPostProcessing(std::move(graph), config); + + CPPUNIT_ASSERT(output.isConsistent()); + + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 23)); + CPPUNIT_ASSERT_EQUAL(uint32_t(2), countParticlesWithPdgId(output, 21)); + CPPUNIT_ASSERT_EQUAL(uint32_t(0), countParticlesWithPdgId(output, 1)); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 211)); + CPPUNIT_ASSERT(hasArtificialVertex(output)); + + const uint32_t z = findParticleWithPdgId(output, 23); + const auto zProductionVertices = output.productionVertices(z); + + CPPUNIT_ASSERT_EQUAL(std::size_t(1), zProductionVertices.size()); + + // Both gluons are visible as context above the Z, and the hidden sibling + // quark leaves the production vertex with the Z as its only outgoing. + CPPUNIT_ASSERT_EQUAL(std::size_t(2), output.incomingParticles(zProductionVertices.front()).size()); + CPPUNIT_ASSERT_EQUAL(std::size_t(1), output.outgoingParticles(zProductionVertices.front()).size()); + + const uint32_t pion = findParticleWithPdgId(output, 211); + const auto pionProductionVertices = output.productionVertices(pion); + + CPPUNIT_ASSERT_EQUAL(std::size_t(1), pionProductionVertices.size()); + CPPUNIT_ASSERT_EQUAL(artificialVertexId(output), pionProductionVertices.front()); + } catch (cms::Exception const& ex) { + std::cerr << ex.what() << std::endl; + CPPUNIT_ASSERT(false); + } +} + +void TestTruthLogicalGraphPostProcessor::testSeedWithDecayGroupKeepsOnlyMatchingDecays() { + try { + GraphBuilder builder(7, 3); + + // Za -> v0 -> { Za', gamma } (radiating copy) + // Za' -> v1 -> { mu+, mu- } + // Zb -> v2 -> { e- } + // + // seedPdgIds = {23}, decayPdgIdGroups = {{13, -13}}: the decay match is + // evaluated at the most upstream root after following the copy chain, so + // Za (-> mu mu) is kept and Zb (-> e e) is dropped. The stable electrons + // survive only as spectators on the artificial vertex. + builder.setGenParticle(0, 23, 2, 100); + builder.setGenParticle(1, 23, 2, 101); + builder.setGenParticle(2, 23, 2, 102); + builder.setGenSimParticle(3, 22, 1, 103, 1003); + builder.setGenSimParticle(4, -13, 1, 104, 1004); + builder.setGenSimParticle(5, 13, 1, 105, 1005); + builder.setGenSimParticle(6, 11, 1, 106, 1006); + + builder.setGenVertex(0, 200); + builder.setGenSimVertex(1, 201, 2001); + builder.setGenSimVertex(2, 202, 2002); + + builder.addDecay(0, 0); + builder.addProduction(0, 1); + builder.addProduction(0, 3); + + builder.addDecay(1, 1); + builder.addProduction(1, 4); + builder.addProduction(1, 5); + + builder.addDecay(2, 2); + builder.addProduction(2, 6); + + auto graph = builder.finish(); + + auto config = defaultConfig(); + config.seedPdgIds = {23}; + config.decayPdgIdGroups = {{13, -13}}; + config.seedParentDepth = 0; + + auto output = runPostProcessing(std::move(graph), config); + + CPPUNIT_ASSERT(output.isConsistent()); + + // Both copies of the matching Z chain are kept; the Z -> e e one is gone. + CPPUNIT_ASSERT_EQUAL(uint32_t(2), countParticlesWithPdgId(output, 23)); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, -13)); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 13)); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 11)); + CPPUNIT_ASSERT(hasArtificialVertex(output)); + + const uint32_t muon = findParticleWithPdgId(output, 13); + const auto muonProductionVertices = output.productionVertices(muon); + + CPPUNIT_ASSERT_EQUAL(std::size_t(1), muonProductionVertices.size()); + CPPUNIT_ASSERT(muonProductionVertices.front() != artificialVertexId(output)); + + const uint32_t electron = findParticleWithPdgId(output, 11); + const auto electronProductionVertices = output.productionVertices(electron); + + CPPUNIT_ASSERT_EQUAL(std::size_t(1), electronProductionVertices.size()); + CPPUNIT_ASSERT_EQUAL(artificialVertexId(output), electronProductionVertices.front()); + } catch (cms::Exception const& ex) { + std::cerr << ex.what() << std::endl; + CPPUNIT_ASSERT(false); + } +} + +void TestTruthLogicalGraphPostProcessor::testZToTauTauDoesNotMatchMuonDecayGroup() { + try { + GraphBuilder builder(7, 3); + + // Z -> v0 -> { tau+, tau- } + // tau+ -> v1 -> { mu+, anti-nu } + // tau- -> v2 -> { mu-, nu } + // + // The decay match is local to the (copy-collapsed) decay vertex of the + // root: the muons from the tau decays must NOT make Z -> tau tau match + // {13, -13}. Nothing matches, so only stable particles survive, attached + // to the artificial vertex. + builder.setGenParticle(0, 23, 2, 100); + builder.setGenParticle(1, -15, 2, 101); + builder.setGenParticle(2, 15, 2, 102); + builder.setGenSimParticle(3, -13, 1, 103, 1003); + builder.setGenSimParticle(4, 13, 1, 104, 1004); + builder.setGenParticle(5, -16, 1, 105); + builder.setGenParticle(6, 16, 1, 106); + + builder.setGenVertex(0, 200); + builder.setGenSimVertex(1, 201, 2001); + builder.setGenSimVertex(2, 202, 2002); + + builder.addDecay(0, 0); + builder.addProduction(0, 1); + builder.addProduction(0, 2); + + builder.addDecay(1, 1); + builder.addProduction(1, 3); + builder.addProduction(1, 5); + + builder.addDecay(2, 2); + builder.addProduction(2, 4); + builder.addProduction(2, 6); + + auto graph = builder.finish(); + + auto config = defaultConfig(); + config.seedPdgIds = {23}; + config.decayPdgIdGroups = {{13, -13}}; + + auto output = runPostProcessing(std::move(graph), config); + + CPPUNIT_ASSERT(output.isConsistent()); + + CPPUNIT_ASSERT_EQUAL(uint32_t(0), countParticlesWithPdgId(output, 23)); + CPPUNIT_ASSERT_EQUAL(uint32_t(0), countParticlesWithPdgId(output, 15)); + CPPUNIT_ASSERT_EQUAL(uint32_t(0), countParticlesWithPdgId(output, -15)); + + // Stable muons and neutrinos survive as spectators only. + CPPUNIT_ASSERT_EQUAL(uint32_t(4), output.nParticles()); + CPPUNIT_ASSERT(hasArtificialVertex(output)); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), output.nVertices()); + + const uint32_t muon = findParticleWithPdgId(output, 13); + const auto muonProductionVertices = output.productionVertices(muon); + + CPPUNIT_ASSERT_EQUAL(std::size_t(1), muonProductionVertices.size()); + CPPUNIT_ASSERT_EQUAL(artificialVertexId(output), muonProductionVertices.front()); + } catch (cms::Exception const& ex) { + std::cerr << ex.what() << std::endl; + CPPUNIT_ASSERT(false); + } +} + +void TestTruthLogicalGraphPostProcessor::testDecayGroupFallbackWhenSeedAbsent() { + try { + GraphBuilder builder(5, 2); + + // The generator wrote no explicit Z: vp -> { mu+, mu-, gamma }. + // Unrelated branch: X -> vx -> { pi+ (stable) }. + // + // seedPdgIds = {23} finds nothing, so the decay-pattern fallback selects + // the mu+ mu- vertex (extra photon allowed). The matched vertex is kept as + // the common production context; the pion survives via the artificial + // vertex and X is dropped. + builder.setGenSimParticle(0, -13, 1, 100, 1000); + builder.setGenSimParticle(1, 13, 1, 101, 1001); + builder.setGenSimParticle(2, 22, 1, 102, 1002); + builder.setGenParticle(3, 999, 2, 103); + builder.setGenSimParticle(4, 211, 1, 104, 1004); + + builder.setGenVertex(0, 200); + builder.setGenVertex(1, 201); + + builder.addProduction(0, 0); + builder.addProduction(0, 1); + builder.addProduction(0, 2); + + builder.addDecay(3, 1); + builder.addProduction(1, 4); + + auto graph = builder.finish(); + + auto config = defaultConfig(); + config.seedPdgIds = {23}; + config.decayPdgIdGroups = {{13, -13}}; + + auto output = runPostProcessing(std::move(graph), config); + + CPPUNIT_ASSERT(output.isConsistent()); + + CPPUNIT_ASSERT_EQUAL(uint32_t(0), countParticlesWithPdgId(output, 999)); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, -13)); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 13)); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 22)); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 211)); + CPPUNIT_ASSERT(hasArtificialVertex(output)); + + const uint32_t artificial = artificialVertexId(output); + + // The muons hang off their real, kept production vertex, not the + // artificial one; the stable photon at the same vertex stays there too. + const uint32_t muon = findParticleWithPdgId(output, 13); + const auto muonProductionVertices = output.productionVertices(muon); + + CPPUNIT_ASSERT_EQUAL(std::size_t(1), muonProductionVertices.size()); + CPPUNIT_ASSERT(muonProductionVertices.front() != artificial); + + const uint32_t gamma = findParticleWithPdgId(output, 22); + const auto gammaProductionVertices = output.productionVertices(gamma); + + CPPUNIT_ASSERT_EQUAL(std::size_t(1), gammaProductionVertices.size()); + CPPUNIT_ASSERT_EQUAL(muonProductionVertices.front(), gammaProductionVertices.front()); + + const uint32_t pion = findParticleWithPdgId(output, 211); + const auto pionProductionVertices = output.productionVertices(pion); + + CPPUNIT_ASSERT_EQUAL(std::size_t(1), pionProductionVertices.size()); + CPPUNIT_ASSERT_EQUAL(artificial, pionProductionVertices.front()); + } catch (cms::Exception const& ex) { + std::cerr << ex.what() << std::endl; + CPPUNIT_ASSERT(false); + } +} + +void TestTruthLogicalGraphPostProcessor::testSeedPdgIdZeroKeepsFullGraphForDebugging() { + try { + GraphBuilder builder(7, 3); + + // Same topology as the Z -> tau tau test, where the selection would match + // nothing. The PDG id 0 wildcard must bypass the selection entirely and + // keep the full graph. + builder.setGenParticle(0, 23, 2, 100); + builder.setGenParticle(1, -15, 2, 101); + builder.setGenParticle(2, 15, 2, 102); + builder.setGenSimParticle(3, -13, 1, 103, 1003); + builder.setGenSimParticle(4, 13, 1, 104, 1004); + builder.setGenParticle(5, -16, 1, 105); + builder.setGenParticle(6, 16, 1, 106); + + builder.setGenVertex(0, 200); + builder.setGenSimVertex(1, 201, 2001); + builder.setGenSimVertex(2, 202, 2002); + + builder.addDecay(0, 0); + builder.addProduction(0, 1); + builder.addProduction(0, 2); + + builder.addDecay(1, 1); + builder.addProduction(1, 3); + builder.addProduction(1, 5); + + builder.addDecay(2, 2); + builder.addProduction(2, 4); + builder.addProduction(2, 6); + + auto graph = builder.finish(); + + auto config = defaultConfig(); + config.seedPdgIds = {0}; + config.decayPdgIdGroups = {{13, -13}}; + + auto output = runPostProcessing(std::move(graph), config); + + CPPUNIT_ASSERT(output.isConsistent()); + + CPPUNIT_ASSERT_EQUAL(uint32_t(7), output.nParticles()); + CPPUNIT_ASSERT_EQUAL(uint32_t(3), output.nVertices()); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 23)); + CPPUNIT_ASSERT(!hasArtificialVertex(output)); + } catch (cms::Exception const& ex) { + std::cerr << ex.what() << std::endl; + CPPUNIT_ASSERT(false); + } +} + void TestTruthLogicalGraphPostProcessor::testSeedCutWithIgnoredParticles() { try { GraphBuilder builder(6, 4); @@ -526,11 +936,11 @@ void TestTruthLogicalGraphPostProcessor::testSeedCutWithIgnoredParticles() { // H -> pi0 -> gamma(status 1, GEN+SIM) // // Extra parent on the kept pi0 production vertex: - // Z --------^ + // Z --------^ (unselected, hidden by the seed cut) // // Unrelated stable final-state e- is kept through the artificial vertex. // - // Then ignoredPdgIds removes pi0, merging H/Z directly to gamma. + // Then ignoredPdgIds removes pi0, merging H directly to gamma. builder.setGenParticle(0, 25, 2, 100); builder.setGenParticle(1, 23, 2, 101); builder.setGenParticle(2, 111, 2, 102); @@ -567,7 +977,7 @@ void TestTruthLogicalGraphPostProcessor::testSeedCutWithIgnoredParticles() { CPPUNIT_ASSERT(output.isConsistent()); CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 25)); - CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 23)); + CPPUNIT_ASSERT_EQUAL(uint32_t(0), countParticlesWithPdgId(output, 23)); CPPUNIT_ASSERT_EQUAL(uint32_t(0), countParticlesWithPdgId(output, 111)); CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 22)); CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 11)); @@ -583,18 +993,8 @@ void TestTruthLogicalGraphPostProcessor::testSeedCutWithIgnoredParticles() { const auto incoming = output.incomingParticles(gammaProductionVertices.front()); - std::vector incomingPdgIds; - incomingPdgIds.reserve(incoming.size()); - - for (uint32_t parent : incoming) { - incomingPdgIds.push_back(output.particles[parent].pdgId); - } - - std::sort(incomingPdgIds.begin(), incomingPdgIds.end()); - - CPPUNIT_ASSERT_EQUAL(std::size_t(2), incomingPdgIds.size()); - CPPUNIT_ASSERT_EQUAL(int32_t(23), incomingPdgIds[0]); - CPPUNIT_ASSERT_EQUAL(int32_t(25), incomingPdgIds[1]); + CPPUNIT_ASSERT_EQUAL(std::size_t(1), incoming.size()); + CPPUNIT_ASSERT_EQUAL(int32_t(25), output.particles[incoming.front()].pdgId); const uint32_t electron = findParticleWithPdgId(output, 11); const auto electronProductionVertices = output.productionVertices(electron); diff --git a/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py b/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py index 8df7cda6db3f7..a391a31735bbf 100644 --- a/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py +++ b/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py @@ -2,7 +2,7 @@ # user options import os -from argparse import ArgumentParser +from argparse import ArgumentParser, BooleanOptionalAction parser = ArgumentParser() parser.add_argument("inputFile", nargs='?', default="step3.root", metavar='FILE', help="Input file, default=%(default)r" ) @@ -10,11 +10,30 @@ help="output directory, default=%(default)r" ) parser.add_argument('-n', "--maxevts", type=int, default=-1, help="maximum number of events to process, default=%(default)s" ) -parser.add_argument('-m', "--merge", dest='mergeGenSim', action='store_true', - help="merge GEN-SIM nodes of duplicates" ) -parser.add_argument('-c', "--collapse", action='store_true', help="collapse GenParticle copies" ) +parser.add_argument('-m', "--merge", dest='mergeGenSim', action=BooleanOptionalAction, default=True, + help="merge GEN and SIM vertices (producer-level and by position)" ) +parser.add_argument('-c', "--collapse", action=BooleanOptionalAction, default=True, + help="collapse intermediate GenParticle copies" ) parser.add_argument('-t', "--tag", default='', help="tag for out put file" ) +parser.add_argument('-s', "--seeds", default=None, + help="comma-separated seed PDG ids, e.g. '15,-15'; '0' keeps the full graph; " + "default=%(default)s uses the hardcoded list" ) +parser.add_argument('-g', "--groups", default=None, + help="semicolon-separated decay PDG id groups, e.g. '13,-14,16;-13,14,-16'" ) +parser.add_argument('-d', "--parentDepth", type=int, default=1, + help="ancestor generations kept above each root as context, default=%(default)s" ) +parser.add_argument('-i', "--ignore", default=None, + help="comma-separated PDG ids to remove from the final logical graph, e.g. '22'" ) +parser.add_argument("--showAll", action='store_true', + help="do not hide zero-simhit subgraphs or large SIM source vertices in the logical DOT dump" ) args = parser.parse_args() + +def _parsePdgIds(text): + return [int(token) for token in text.replace(' ', '').split(',') if token] + +seedPdgIds = _parsePdgIds(args.seeds) if args.seeds is not None else [23, 15, -15, 25, 4, 5, 6] +decayPdgIdGroups = [_parsePdgIds(group) for group in args.groups.split(';')] if args.groups else [] +ignoredPdgIds = _parsePdgIds(args.ignore) if args.ignore else [] if '/' not in args.inputFile and ':' not in args.inputFile: args.inputFile = 'file:'+args.inputFile if args.outdir and not os.path.exists(args.outdir): @@ -76,22 +95,38 @@ genEventHepMC3=cms.InputTag("generatorSmeared"), genEventHepMC=cms.InputTag("generatorSmeared"), - mergeGenSimVertices=cms.bool(True), + mergeGenSimVertices=cms.bool(args.mergeGenSim), postProcessing=cms.PSet( - collapseIntermediateGenParticles=cms.bool(True), + collapseIntermediateGenParticles=cms.bool(args.collapse), + mergeGenSimVerticesByPosition=cms.bool(args.mergeGenSim), # Empty means: keep the full logical graph. - # Example: cms.vint32(22) keeps photons as seeds, - # keeps seedParentDepth parent generations above each seed, - # then keeps everything downstream. - seedPdgIds=cms.vint32(23,15,-15,25,4,5,6), - - seedParentDepth=cms.uint32(1), + # The most upstream particle of each matching chain becomes a root and + # its full downstream subgraph is kept; unselected upstream activity is + # collapsed into one artificial source vertex. + # The special value 0 disables the selection and keeps the full graph + # (debugging escape hatch). + seedPdgIds=cms.vint32(*seedPdgIds), + + # Ancestor generations kept above each root as context only: their + # other descendants are not pulled in. + seedParentDepth=cms.uint32(args.parentDepth), + + # Decay patterns of interest: unordered, charge-sensitive PDG id + # multisets, OR-ed. With seedPdgIds set, only roots whose effective + # decay products contain a group are kept, e.g. + # cms.PSet(pdgIds=cms.vint32(13, -13)) + # keeps Z -> mu mu but drops Z -> e e. Without seedPdgIds, or when the + # event contains no seed particle at all, vertices whose outgoing PDG + # ids contain a group are selected directly. + decayPdgIdGroups=cms.VPSet( + *[cms.PSet(pdgIds=cms.vint32(*group)) for group in decayPdgIdGroups] + ), # Remove particles by PDG id. # Example: cms.vint32(22) removes all photons from the final logical graph. - ignoredPdgIds=cms.vint32(), + ignoredPdgIds=cms.vint32(*ignoredPdgIds), # Remove particles by logical particle id. # These ids refer to the graph after the previous postprocessing steps @@ -157,12 +192,14 @@ maxParticles=cms.uint32(20000), maxVertices=cms.uint32(20000), - maxEdgesPerNode=cms.uint32(300), + # --showAll lifts the per-node edge cap: with large events the artificial + # source vertex legitimately has more than 300 outgoing spectators. + maxEdgesPerNode=cms.uint32(1000000 if args.showAll else 300), - hideLargeSimSourceVertices=cms.bool(True), + hideLargeSimSourceVertices=cms.bool(not args.showAll), largeSimSourceVertexMinOutgoing=cms.uint32(50), - hideZeroSimHitSubgraphs=cms.bool(True), + hideZeroSimHitSubgraphs=cms.bool(not args.showAll), ) @@ -232,6 +269,9 @@ process.MessageLogger.cerr.TruthLogicalGraphProducer = cms.untracked.PSet( limit=cms.untracked.int32(-1) ) +process.MessageLogger.cerr.TruthLogicalGraphPostProcessor = cms.untracked.PSet( + limit=cms.untracked.int32(-1) +) process.MessageLogger.cerr.TruthLogicalGraphHitIndexProducer = cms.untracked.PSet( limit=cms.untracked.int32(-1) ) diff --git a/Validation/Configuration/python/truthPrevalidation_cff.py b/Validation/Configuration/python/truthPrevalidation_cff.py index ce5ca6ac631b2..82348a9f77a51 100644 --- a/Validation/Configuration/python/truthPrevalidation_cff.py +++ b/Validation/Configuration/python/truthPrevalidation_cff.py @@ -19,6 +19,7 @@ collapseIntermediateGenParticles = cms.bool(True), seedPdgIds = cms.vint32(), seedParentDepth = cms.uint32(0), + decayPdgIdGroups = cms.VPSet(), ignoredPdgIds = cms.vint32(), ignoredParticleIds = cms.vuint32(), From 14475e488a73a50ed8ae7bd33c3d19fb074bfd6f Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Sat, 13 Jun 2026 10:54:20 +0200 Subject: [PATCH 42/53] enableTruth: keep full SimTrack ancestry in SIM so the truth graph stays connected --- .../python/upgradeWorkflowComponents.py | 14 +++++++++++++- SimG4Core/Application/python/g4SimHits_cfi.py | 16 ++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/Configuration/PyReleaseValidation/python/upgradeWorkflowComponents.py b/Configuration/PyReleaseValidation/python/upgradeWorkflowComponents.py index 869aa9459435b..5468fed206e58 100644 --- a/Configuration/PyReleaseValidation/python/upgradeWorkflowComponents.py +++ b/Configuration/PyReleaseValidation/python/upgradeWorkflowComponents.py @@ -1010,7 +1010,11 @@ def condition(self, fragment, stepList, key, hasHarvest): class UpgradeWorkflow_enableTruth(UpgradeWorkflow): def setup_(self, step, stepName, stepDict, k, properties): - if 'RecoGlobal' in step: + # enableTruth runs the truth-graph producers in RecoGlobal (step3) and, + # in GenSim (step1), keeps the full ancestor branch of every stored + # SimTrack (g4SimHits PersistencyEmin -> 0 via the modifier) so the + # truth graph stays connected to the generator. + if 'GenSim' in step or 'RecoGlobal' in step: stepDict[stepName][k] = deepcopy(stepDict[step][k]) if '--procModifiers' in stepDict[stepName][k]: @@ -1024,9 +1028,17 @@ def condition(self, fragment, stepList, key, hasHarvest): upgradeWFs['enableTruth'] = UpgradeWorkflow_enableTruth( steps = [ + 'GenSim', + 'GenSimHLBeamSpot', + 'GenSimHLBeamSpot14', + 'GenSimHLBeamSpotCloseBy', 'RecoGlobal', ], PU = [ + 'GenSim', + 'GenSimHLBeamSpot', + 'GenSimHLBeamSpot14', + 'GenSimHLBeamSpotCloseBy', 'RecoGlobal', ], suffix = '_enableTruth', diff --git a/SimG4Core/Application/python/g4SimHits_cfi.py b/SimG4Core/Application/python/g4SimHits_cfi.py index 0bfcec8626752..2777641c4a658 100644 --- a/SimG4Core/Application/python/g4SimHits_cfi.py +++ b/SimG4Core/Application/python/g4SimHits_cfi.py @@ -812,3 +812,19 @@ fixLongLivedSleptonSim.toModify( g4SimHits, Generator = dict(IsSlepton = True) ) + +## +## Truth-graph workflows: keep every track in the history so that the stored +## ancestor branch of each persisted SimTrack is complete. With the default +## PersistencyEmin (50 GeV) intermediate low-energy ancestors are dropped and +## the production SimVertex of a stored secondary gets parentIndex = -1, +## fragmenting the truth graph into components disconnected from the generator. +## Setting PersistencyEmin = 0 keeps a mother whenever any daughter is kept, so +## SimVertex::parentIndex always resolves to a stored ancestor (or a primary). +## This increases the SimTrack/SimVertex multiplicity and is therefore scoped to +## the enableTruth workflows only. +## +from Configuration.ProcessModifiers.enableTruth_cff import enableTruth +enableTruth.toModify( g4SimHits, + TrackingAction = dict(PersistencyEmin = 0.0) +) From 2883a16ce640cf00e1d1d78fc2e8a57f8df2a127 Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Sat, 13 Jun 2026 14:11:11 +0200 Subject: [PATCH 43/53] TruthInfo: focused selection with typed ISR/underlying-event source nodes, pile-up provenance, heavy-flavor seeding and jet-origin navigation --- PhysicsTools/TruthInfo/interface/Graph.h | 25 +++ .../TruthLogicalGraphPostProcessor.h | 14 ++ .../plugins/TruthLogicalGraphDumper.cc | 21 +- PhysicsTools/TruthInfo/src/Graph.cc | 76 +++++++ .../src/TruthLogicalGraphPostProcessor.cc | 154 ++++++++----- .../test/TruthLogicalGraphPostProcessor_t.cpp | 208 ++++++++++++++++++ .../test/dumpTruthGraphsFromGENSIMRECO_cfg.py | 15 ++ .../python/truthPrevalidation_cff.py | 2 + 8 files changed, 462 insertions(+), 53 deletions(-) diff --git a/PhysicsTools/TruthInfo/interface/Graph.h b/PhysicsTools/TruthInfo/interface/Graph.h index 3a58f5d530e4c..4d714f800ff16 100644 --- a/PhysicsTools/TruthInfo/interface/Graph.h +++ b/PhysicsTools/TruthInfo/interface/Graph.h @@ -50,6 +50,17 @@ namespace truth { [[nodiscard]] bool valid() const { return hasGen() || hasSim(); } }; + // Role of a logical vertex. Normal vertices are real GEN/SIM vertices. + // Artificial source vertices summarize activity that was cut from a focused + // selection but is kept for context/consistency: + // Upstream - truncated production context of the selected roots (ISR, + // beam/initial-state activity that led to the selection); + // UnderlyingEvent - stable final-state particles not in any selected + // subgraph (underlying event, unrelated to the selection). + // Artificial vertices carry the genEvent/eventId of the activity they + // summarize, so that overlaid pile-up graphs stay distinguishable. + enum class VertexRole : uint8_t { Normal = 0, Upstream = 1, UnderlyingEvent = 2 }; + struct VertexData { // Optional provenance/debug back-references to the raw TruthGraph nodes. // -1 means "not available". @@ -62,6 +73,9 @@ namespace truth { // GEN connected component id from the raw TruthGraph, -1 if not applicable. int32_t genEvent = -1; + // VertexRole stored as its underlying type for dictionary simplicity. + uint8_t role = static_cast(VertexRole::Normal); + // Standalone payload. // Convention: "best available" position. // Prefer SIM if present, otherwise GEN, otherwise default-constructed. @@ -70,6 +84,9 @@ namespace truth { [[nodiscard]] bool hasGen() const { return genNode >= 0; } [[nodiscard]] bool hasSim() const { return simNode >= 0; } [[nodiscard]] bool valid() const { return hasGen() || hasSim(); } + + [[nodiscard]] VertexRole vertexRole() const { return static_cast(role); } + [[nodiscard]] bool isArtificial() const { return vertexRole() != VertexRole::Normal; } }; class Graph; @@ -190,6 +207,14 @@ namespace truth { [[nodiscard]] std::vector roots() const; [[nodiscard]] std::vector leaves() const; + // Lowest (closest) common ancestor of a set of particles: the single truth + // particle from which all of them descend, minimizing the total number of + // generations. This answers "which particle did this jet come from" given + // the jet's truth constituents (e.g. the b quark of a b-jet); walk further + // up with Particle::firstAncestorWithPdgId to reach a specific origin + // species (e.g. the top). Returns nullopt if the inputs share no ancestor. + [[nodiscard]] std::optional lowestCommonAncestor(std::vector const& particles) const; + [[nodiscard]] std::vector sourceVertices() const; [[nodiscard]] std::vector sinkVertices() const; diff --git a/PhysicsTools/TruthInfo/interface/TruthLogicalGraphPostProcessor.h b/PhysicsTools/TruthInfo/interface/TruthLogicalGraphPostProcessor.h index 4eaea1566e0ff..0eb3a6b532ead 100644 --- a/PhysicsTools/TruthInfo/interface/TruthLogicalGraphPostProcessor.h +++ b/PhysicsTools/TruthInfo/interface/TruthLogicalGraphPostProcessor.h @@ -20,11 +20,25 @@ namespace truth { // full graph (debugging escape hatch). std::vector seedPdgIds; + // Seed on hadrons by heavy-flavor content instead of (or in addition to) + // exact PDG ids: a particle whose PDG id is a hadron containing any of these + // quark flavors becomes a seed. Use 5 for b hadrons, 4 for c hadrons. This + // lets the user select e.g. all B-hadron decay subgraphs without listing + // every B species. OR-ed with seedPdgIds. + std::vector seedHadronFlavors; + // For each selected root, keep this many generations of ancestors above it // as context only: the ancestors and connecting vertices are kept, but not // their other descendants. uint32_t seedParentDepth = 0; + // If true (default), stable final-state GEN particles outside the selected + // subgraph are kept and attached to an artificial UnderlyingEvent source + // vertex. If false, they are dropped, giving a focused subgraph that + // contains only the selection and its truncated upstream (ISR) context. + // Only meaningful when a selection is active (seedPdgIds/decayPdgIdGroups). + bool keepStableSpectators = true; + // Decay patterns of interest. Each group is an unordered, charge-sensitive // multiset of PDG ids; groups are OR-ed. // diff --git a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc index 149f448da9cba..d489a3f960262 100644 --- a/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc +++ b/PhysicsTools/TruthInfo/plugins/TruthLogicalGraphDumper.cc @@ -663,10 +663,25 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { const auto& incoming = v.incomingParticles(); const auto& outgoing = v.outgoingParticles(); + const char* roleName = nullptr; + switch (d.vertexRole()) { + case truth::VertexRole::Upstream: + roleName = "ISR/upstream"; + break; + case truth::VertexRole::UnderlyingEvent: + roleName = "underlying event"; + break; + default: + break; + } + os << " v" << i << " [shape=diamond, domain=<" << logicalVertexDomain(d) << ">, hasGen=" << d.hasGen() << ", hasSim=" << d.hasSim() << ", eid=" << d.eventId << ", genEvent=" << d.genEvent << ", isSource=" << v.isSource() << ", isSink=" << v.isSink(); - if (d.hasGen() && d.hasSim()) { + if (roleName != nullptr) { + os << ", role=\"" << roleName << "\", style=filled, fillcolor=\"" + << (d.vertexRole() == truth::VertexRole::Upstream ? "navajowhite" : "lightgrey") << "\""; + } else if (d.hasGen() && d.hasSim()) { os << ", color=\"purple\", penwidth=2"; } else if (d.hasGen()) { os << ", color=\"blue\""; @@ -686,6 +701,10 @@ class TruthLogicalGraphDumper : public edm::one::EDAnalyzer<> { os << "
isSource: " << (v.isSource() ? "yes" : "no") << " isSink: " << (v.isSink() ? "yes" : "no") << "
x4: " << fmtP4(d.position) << "
x4: " << fmtX4(d.position) << "
nIn: " << v.incomingParticles().size() << " nOut: " << v.outgoingParticles().size() << "
direct tracker simHits: " << trackerDirectSummary.nSimHits + << " dE=" << fmtEnergy(trackerDirectSummary.simHitEnergy) << "
subgraph tracker simHits: " << trackerSubgraphSummary.nSimHits + << " dE=" << fmtEnergy(trackerSubgraphSummary.simHitEnergy) << "
checkpointId: " << cp.checkpointId << "
x4@checkpoint: " << fmtP4(cp.position) << "
\n"; os << " \n"; + if (roleName != nullptr) + os << " \n"; + os << " \n"; if (d.eventId != 0) diff --git a/PhysicsTools/TruthInfo/src/Graph.cc b/PhysicsTools/TruthInfo/src/Graph.cc index 84831a8948872..f9d511773f3ed 100644 --- a/PhysicsTools/TruthInfo/src/Graph.cc +++ b/PhysicsTools/TruthInfo/src/Graph.cc @@ -394,6 +394,82 @@ std::optional truth::Graph::firstCommonAncestorOf(size_type a, return particle(static_cast(bestId)); } +std::optional truth::Graph::lowestCommonAncestor(std::vector const& parts) const { + std::vector ids; + ids.reserve(parts.size()); + for (auto const& p : parts) { + if (p.valid() && p.id() < nParticles()) + ids.push_back(p.id()); + } + + if (ids.empty()) + return std::nullopt; + if (ids.size() == 1) + return particle(ids.front()); + + const uint32_t n = nParticles(); + + // Upward BFS distance from each input particle to every ancestor (itself at 0). + std::vector> dist(ids.size(), std::vector(n, -1)); + + auto fillDistances = [this](uint32_t start, std::vector& d) { + std::queue q; + d[start] = 0; + q.push(start); + + while (!q.empty()) { + const uint32_t cur = q.front(); + q.pop(); + + for (auto const& parent : parentsOf(cur)) { + if (d[parent.id()] >= 0) + continue; + d[parent.id()] = d[cur] + 1; + q.push(parent.id()); + } + } + }; + + for (std::size_t k = 0; k < ids.size(); ++k) + fillDistances(ids[k], dist[k]); + + // Among ancestors reachable from ALL inputs, pick the closest (min total + // distance, then min worst-case distance, then lowest id for determinism). + int bestId = -1; + int bestTotal = -1; + int bestMax = -1; + + for (uint32_t i = 0; i < n; ++i) { + int total = 0; + int maxDist = 0; + bool common = true; + + for (std::size_t k = 0; k < ids.size(); ++k) { + if (dist[k][i] < 0) { + common = false; + break; + } + total += dist[k][i]; + maxDist = std::max(maxDist, dist[k][i]); + } + + if (!common) + continue; + + if (bestId < 0 || total < bestTotal || (total == bestTotal && maxDist < bestMax) || + (total == bestTotal && maxDist == bestMax && static_cast(i) < bestId)) { + bestId = static_cast(i); + bestTotal = total; + bestMax = maxDist; + } + } + + if (bestId < 0) + return std::nullopt; + + return particle(static_cast(bestId)); +} + bool truth::Graph::isConsistent() const { const bool p2dv = checkCSR(particleToDecayVertexOffsets, particleToDecayVertices, particles.size()) && checkTargets(particleToDecayVertices, nVertices()); diff --git a/PhysicsTools/TruthInfo/src/TruthLogicalGraphPostProcessor.cc b/PhysicsTools/TruthInfo/src/TruthLogicalGraphPostProcessor.cc index 992801cb7015e..7ecb63844b221 100644 --- a/PhysicsTools/TruthInfo/src/TruthLogicalGraphPostProcessor.cc +++ b/PhysicsTools/TruthInfo/src/TruthLogicalGraphPostProcessor.cc @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -55,6 +56,34 @@ namespace { return std::find(particleIds.begin(), particleIds.end(), particleId) != particleIds.end(); } + // True if pdgId is an ordinary hadron whose quark content includes the given + // flavor (5 = b, 4 = c, ...), using the PDG hadron-numbering digits. + bool hadronHasQuark(int32_t pdgId, int32_t flavor) { + const int32_t id = std::abs(pdgId); + if (id < 100 || id >= 1000000000) // leptons/bosons/diquark-free codes and nuclei are not hadrons here + return false; + const int32_t nq1 = (id / 1000) % 10; + const int32_t nq2 = (id / 100) % 10; + const int32_t nq3 = (id / 10) % 10; + return nq1 == flavor || nq2 == flavor || nq3 == flavor; + } + + bool matchesSeed(truth::Graph const& graph, + uint32_t particleId, + truth::LogicalGraphPostProcessingConfig const& config) { + const int32_t pdgId = graph.particles[particleId].pdgId; + + if (containsPdgId(config.seedPdgIds, pdgId)) + return true; + + for (const int32_t flavor : config.seedHadronFlavors) { + if (hadronHasQuark(pdgId, flavor)) + return true; + } + + return false; + } + bool isIgnoredParticle(truth::Graph const& graph, uint32_t particleId, std::vector const& ignoredPdgIds, @@ -647,24 +676,25 @@ namespace { } } + // attachRole[i] is 0 for particles that are not attached to an artificial + // source, or the uint8_t value of a non-Normal VertexRole otherwise. One + // artificial source vertex is created per (role, genEvent) so that overlaid + // pile-up graphs stay distinguishable; it carries the genEvent/eventId of the + // activity it summarizes. truth::Graph rebuildFilteredGraph(truth::Graph const& input, std::vector const& keepParticle, std::vector const& keepVertex, - std::vector const& connectToArtificialSourceVertex) { + std::vector const& attachRole) { const uint32_t nParticles = input.nParticles(); const uint32_t nVertices = input.nVertices(); - const bool needArtificialSourceVertex = std::any_of(connectToArtificialSourceVertex.begin(), - connectToArtificialSourceVertex.end(), - [](uint8_t value) { return value != 0; }); - truth::Graph output; std::vector oldParticleToNew(nParticles, -1); std::vector oldVertexToNew(nVertices, -1); output.particles.reserve(nParticles); - output.vertices.reserve(nVertices + (needArtificialSourceVertex ? 1 : 0)); + output.vertices.reserve(nVertices + 2); for (uint32_t oldParticle = 0; oldParticle < nParticles; ++oldParticle) { if (!keepParticle[oldParticle]) @@ -682,35 +712,35 @@ namespace { output.vertices.push_back(input.vertices[oldVertex]); } - int32_t artificialSourceVertex = -1; - - if (needArtificialSourceVertex) { - artificialSourceVertex = static_cast(output.vertices.size()); - - truth::VertexData vertex; - vertex.genNode = -1; - vertex.simNode = -1; - vertex.eventId = 0; - vertex.genEvent = -1; - - output.vertices.push_back(vertex); - } - + std::map, uint32_t> artificialNode; std::vector> artificialEdges; - if (artificialSourceVertex >= 0) { - const uint32_t newVertex = static_cast(artificialSourceVertex); + for (uint32_t oldParticle = 0; oldParticle < nParticles; ++oldParticle) { + const uint8_t role = attachRole[oldParticle]; + if (role == 0) + continue; - for (uint32_t oldParticle = 0; oldParticle < nParticles; ++oldParticle) { - if (!connectToArtificialSourceVertex[oldParticle]) - continue; + const int32_t newParticle = oldParticleToNew[oldParticle]; + if (newParticle < 0) + continue; - const int32_t newParticle = oldParticleToNew[oldParticle]; - if (newParticle < 0) - continue; + const int32_t genEvent = input.particles[oldParticle].genEvent; + const std::pair key{role, genEvent}; - artificialEdges.emplace_back(newVertex, static_cast(newParticle)); + auto it = artificialNode.find(key); + if (it == artificialNode.end()) { + truth::VertexData vertex; + vertex.genNode = -1; + vertex.simNode = -1; + vertex.role = role; + vertex.genEvent = genEvent; + vertex.eventId = input.particles[oldParticle].eventId; + + it = artificialNode.emplace(key, static_cast(output.vertices.size())).first; + output.vertices.push_back(vertex); } + + artificialEdges.emplace_back(it->second, static_cast(newParticle)); } rebuildAdjacency(input, oldParticleToNew, oldVertexToNew, artificialEdges, output); @@ -734,7 +764,7 @@ namespace { } } - const bool haveSeeds = !config.seedPdgIds.empty(); + const bool haveSeeds = !config.seedPdgIds.empty() || !config.seedHadronFlavors.empty(); const bool haveGroups = !sortedGroups.empty(); if (!haveSeeds && !haveGroups) @@ -755,7 +785,7 @@ namespace { std::vector matches; for (uint32_t particleId = 0; particleId < nParticles; ++particleId) { - if (containsPdgId(config.seedPdgIds, input.particles[particleId].pdgId)) + if (matchesSeed(input, particleId, config)) matches.push_back(particleId); } @@ -784,7 +814,8 @@ namespace { if (roots.empty()) { edm::LogWarning("TruthLogicalGraphPostProcessor") << "Configured truth graph selection (seedPdgIds and/or decayPdgIdGroups) matched nothing in this event; " - "keeping only stable GEN particles attached to the artificial source vertex."; + << (config.keepStableSpectators ? "keeping only stable GEN particles as underlying-event spectators." + : "the selected graph is empty."); } std::vector keepParticle(nParticles, 0); @@ -802,31 +833,34 @@ namespace { markAncestorContext(input, roots, config.seedParentDepth, keepParticle, keepVertex); - // Keep every final-state GEN particle unless the user explicitly asked to - // ignore/collapse it. + // Optionally keep every stable final-state GEN particle outside the + // selection; these become the underlying-event spectators. Disabled by + // keepStableSpectators=false for a focused subgraph. std::vector stableSpectator(nParticles, 0); - for (uint32_t particleId = 0; particleId < nParticles; ++particleId) { - if (!isStableGenParticle(input, particleId)) - continue; + if (config.keepStableSpectators) { + for (uint32_t particleId = 0; particleId < nParticles; ++particleId) { + if (!isStableGenParticle(input, particleId)) + continue; - if (isIgnoredParticle(input, particleId, config.ignoredPdgIds, config.ignoredParticleIds)) - continue; + if (isIgnoredParticle(input, particleId, config.ignoredPdgIds, config.ignoredParticleIds)) + continue; - if (keepParticle[particleId]) - continue; + if (keepParticle[particleId]) + continue; - keepParticle[particleId] = 1; - stableSpectator[particleId] = 1; + keepParticle[particleId] = 1; + stableSpectator[particleId] = 1; + } } dropVerticesWithoutVisibleParticles(input, keepParticle, keepVertex); - // Attach to the artificial source vertex every kept particle whose real - // production vertices were all dropped: stable spectators and particles at - // the truncated upstream boundary (conceptually ISR or other uninteresting - // upstream activity). True sources of the input graph stay sources. - std::vector connectToArtificialSourceVertex(nParticles, 0); + // Assign an artificial-source role to every kept particle whose real + // production vertices were all dropped: stable spectators -> UnderlyingEvent, + // selected roots / truncated ancestors at the upstream boundary -> Upstream + // (ISR). True sources of the input graph stay sources. + std::vector attachRole(nParticles, 0); for (uint32_t particleId = 0; particleId < nParticles; ++particleId) { if (!keepParticle[particleId]) @@ -842,11 +876,13 @@ namespace { return vertexId < nVertices && keepVertex[vertexId]; }); - if (!hasKeptProduction) - connectToArtificialSourceVertex[particleId] = 1; + if (!hasKeptProduction) { + attachRole[particleId] = static_cast(stableSpectator[particleId] ? truth::VertexRole::UnderlyingEvent + : truth::VertexRole::Upstream); + } } - return rebuildFilteredGraph(input, keepParticle, keepVertex, connectToArtificialSourceVertex); + return rebuildFilteredGraph(input, keepParticle, keepVertex, attachRole); } void mergeVertexPayload(truth::VertexData& output, truth::VertexData const& input) { @@ -1069,7 +1105,19 @@ namespace truth { ->setComment( "Number of ancestor generations kept above each selected root as context only: the ancestors and " "connecting vertices are kept, but not their other descendants. Kept particles whose production " - "vertices all fall outside the selection are attached to the artificial source vertex."); + "vertices all fall outside the selection are attached to an artificial Upstream (ISR) source vertex."); + + desc.add>("seedHadronFlavors", {}) + ->setComment( + "Seed on hadrons by heavy-flavor content (5 = b, 4 = c): a hadron whose quark content includes any of " + "these flavors becomes a seed, e.g. {5} selects all B-hadron decay subgraphs. OR-ed with seedPdgIds."); + + desc.add("keepStableSpectators", true) + ->setComment( + "If true, stable final-state GEN particles outside the selected subgraph are kept and attached to an " + "artificial UnderlyingEvent source vertex (tagged with their genEvent/eventId for pile-up provenance). " + "If false, they are dropped, giving a focused subgraph with only the selection and its Upstream (ISR) " + "context. Only meaningful when a selection (seedPdgIds/decayPdgIdGroups) is active."); { edm::ParameterSetDescription groupDesc; @@ -1111,7 +1159,9 @@ namespace truth { config.collapseIntermediateGenParticles = pset.getParameter("collapseIntermediateGenParticles"); config.seedPdgIds = pset.getParameter>("seedPdgIds"); + config.seedHadronFlavors = pset.getParameter>("seedHadronFlavors"); config.seedParentDepth = pset.getParameter("seedParentDepth"); + config.keepStableSpectators = pset.getParameter("keepStableSpectators"); for (auto const& groupPSet : pset.getParameter>("decayPdgIdGroups")) { config.decayPdgIdGroups.push_back(groupPSet.getParameter>("pdgIds")); diff --git a/PhysicsTools/TruthInfo/test/TruthLogicalGraphPostProcessor_t.cpp b/PhysicsTools/TruthInfo/test/TruthLogicalGraphPostProcessor_t.cpp index a9ce18a6fd671..eedb92fdad91b 100644 --- a/PhysicsTools/TruthInfo/test/TruthLogicalGraphPostProcessor_t.cpp +++ b/PhysicsTools/TruthInfo/test/TruthLogicalGraphPostProcessor_t.cpp @@ -220,6 +220,15 @@ namespace { return 0; } + uint32_t countArtificialVerticesWithRole(truth::Graph const& graph, truth::VertexRole role) { + uint32_t count = 0; + for (auto const& vertex : graph.vertices) { + if (vertex.isArtificial() && vertex.vertexRole() == role) + ++count; + } + return count; + } + truth::LogicalGraphPostProcessingConfig defaultConfig() { truth::LogicalGraphPostProcessingConfig config; config.collapseIntermediateGenParticles = false; @@ -252,6 +261,10 @@ class TestTruthLogicalGraphPostProcessor : public CppUnit::TestFixture { CPPUNIT_TEST(testZToTauTauDoesNotMatchMuonDecayGroup); CPPUNIT_TEST(testDecayGroupFallbackWhenSeedAbsent); CPPUNIT_TEST(testSeedPdgIdZeroKeepsFullGraphForDebugging); + CPPUNIT_TEST(testArtificialSourceRolesAndProvenance); + CPPUNIT_TEST(testKeepStableSpectatorsFalseDropsSpectators); + CPPUNIT_TEST(testSeedHadronFlavorSelectsBHadron); + CPPUNIT_TEST(testJetOriginLowestCommonAncestor); CPPUNIT_TEST_SUITE_END(); public: @@ -268,6 +281,10 @@ class TestTruthLogicalGraphPostProcessor : public CppUnit::TestFixture { void testZToTauTauDoesNotMatchMuonDecayGroup(); void testDecayGroupFallbackWhenSeedAbsent(); void testSeedPdgIdZeroKeepsFullGraphForDebugging(); + void testArtificialSourceRolesAndProvenance(); + void testKeepStableSpectatorsFalseDropsSpectators(); + void testSeedHadronFlavorSelectsBHadron(); + void testJetOriginLowestCommonAncestor(); }; CPPUNIT_TEST_SUITE_REGISTRATION(TestTruthLogicalGraphPostProcessor); @@ -1061,3 +1078,194 @@ void TestTruthLogicalGraphPostProcessor::testIgnoredParticleIdsAreCollapsedAway( CPPUNIT_ASSERT(false); } } + +void TestTruthLogicalGraphPostProcessor::testArtificialSourceRolesAndProvenance() { + try { + GraphBuilder builder(5, 3); + + // q -> Z -> mu+ mu- ; plus an unrelated stable pi+ (underlying event). + builder.setGenParticle(0, 1, 2, 100); // q + builder.setGenParticle(1, 23, 2, 101); // Z + builder.setGenSimParticle(2, -13, 1, 102, 1002); + builder.setGenSimParticle(3, 13, 1, 103, 1003); + builder.setGenSimParticle(4, 211, 1, 104, 1004); // stable spectator + + builder.setGenVertex(0, 200); // q -> Z (dropped at depth 0) + builder.setGenSimVertex(1, 201, 2001); // Z -> mu mu + builder.setGenVertex(2, 202); // -> pi+ (dropped) + + builder.addDecay(0, 0); + builder.addProduction(0, 1); + builder.addDecay(1, 1); + builder.addProduction(1, 2); + builder.addProduction(1, 3); + builder.addProduction(2, 4); + + auto graph = builder.finish(); + + auto config = defaultConfig(); + config.seedPdgIds = {23}; + config.seedParentDepth = 0; + config.keepStableSpectators = true; + + auto output = runPostProcessing(std::move(graph), config); + CPPUNIT_ASSERT(output.isConsistent()); + + // Z (root with truncated upstream) -> Upstream node; pi+ -> UnderlyingEvent node. + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countArtificialVerticesWithRole(output, truth::VertexRole::Upstream)); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countArtificialVerticesWithRole(output, truth::VertexRole::UnderlyingEvent)); + + CPPUNIT_ASSERT_EQUAL(uint32_t(0), countParticlesWithPdgId(output, 1)); // q dropped at depth 0 + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 23)); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 211)); + + // Provenance: artificial sources carry the genEvent of the activity they summarize. + for (auto const& vertex : output.vertices) { + if (vertex.isArtificial()) + CPPUNIT_ASSERT_EQUAL(int32_t(0), vertex.genEvent); + } + + const uint32_t z = findParticleWithPdgId(output, 23); + const auto zProd = output.productionVertices(z); + CPPUNIT_ASSERT_EQUAL(std::size_t(1), zProd.size()); + CPPUNIT_ASSERT(output.vertices[zProd.front()].vertexRole() == truth::VertexRole::Upstream); + } catch (cms::Exception const& ex) { + std::cerr << ex.what() << std::endl; + CPPUNIT_ASSERT(false); + } +} + +void TestTruthLogicalGraphPostProcessor::testKeepStableSpectatorsFalseDropsSpectators() { + try { + GraphBuilder builder(5, 3); + + builder.setGenParticle(0, 1, 2, 100); + builder.setGenParticle(1, 23, 2, 101); + builder.setGenSimParticle(2, -13, 1, 102, 1002); + builder.setGenSimParticle(3, 13, 1, 103, 1003); + builder.setGenSimParticle(4, 211, 1, 104, 1004); // stable spectator + + builder.setGenVertex(0, 200); + builder.setGenSimVertex(1, 201, 2001); + builder.setGenVertex(2, 202); + + builder.addDecay(0, 0); + builder.addProduction(0, 1); + builder.addDecay(1, 1); + builder.addProduction(1, 2); + builder.addProduction(1, 3); + builder.addProduction(2, 4); + + auto graph = builder.finish(); + + auto config = defaultConfig(); + config.seedPdgIds = {23}; + config.seedParentDepth = 0; + config.keepStableSpectators = false; + + auto output = runPostProcessing(std::move(graph), config); + CPPUNIT_ASSERT(output.isConsistent()); + + // Spectator pion dropped; no UnderlyingEvent node. + CPPUNIT_ASSERT_EQUAL(uint32_t(0), countParticlesWithPdgId(output, 211)); + CPPUNIT_ASSERT_EQUAL(uint32_t(0), countArtificialVerticesWithRole(output, truth::VertexRole::UnderlyingEvent)); + + // Focused subgraph: Z + two muons, the Z hanging off an Upstream (ISR) node. + CPPUNIT_ASSERT_EQUAL(uint32_t(3), output.nParticles()); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 23)); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countArtificialVerticesWithRole(output, truth::VertexRole::Upstream)); + } catch (cms::Exception const& ex) { + std::cerr << ex.what() << std::endl; + CPPUNIT_ASSERT(false); + } +} + +void TestTruthLogicalGraphPostProcessor::testSeedHadronFlavorSelectsBHadron() { + try { + GraphBuilder builder(5, 3); + + // b -> B0 -> D- mu+ ; unrelated stable pi+. + builder.setGenParticle(0, 5, 2, 100); // b quark (not a hadron) + builder.setGenParticle(1, 511, 2, 101); // B0 (b-hadron) + builder.setGenSimParticle(2, -411, 2, 102, 1002); // D- + builder.setGenSimParticle(3, -13, 1, 103, 1003); // mu+ + builder.setGenSimParticle(4, 211, 1, 104, 1004); // stable spectator + + builder.setGenVertex(0, 200); // b -> B0 + builder.setGenSimVertex(1, 201, 2001); // B0 -> D- mu+ + builder.setGenVertex(2, 202); // -> pi+ + + builder.addDecay(0, 0); + builder.addProduction(0, 1); + builder.addDecay(1, 1); + builder.addProduction(1, 2); + builder.addProduction(1, 3); + builder.addProduction(2, 4); + + auto graph = builder.finish(); + + auto config = defaultConfig(); + config.seedHadronFlavors = {5}; // seed all b-hadrons + config.seedParentDepth = 0; + config.keepStableSpectators = false; + + auto output = runPostProcessing(std::move(graph), config); + CPPUNIT_ASSERT(output.isConsistent()); + + // The B0 (flavor-5 hadron) is the seed; its decay products are kept. + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, 511)); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, -411)); + CPPUNIT_ASSERT_EQUAL(uint32_t(1), countParticlesWithPdgId(output, -13)); + + // The bare b quark is NOT a hadron, so it is not a seed and is dropped at depth 0. + CPPUNIT_ASSERT_EQUAL(uint32_t(0), countParticlesWithPdgId(output, 5)); + // Spectator dropped (keepStableSpectators = false). + CPPUNIT_ASSERT_EQUAL(uint32_t(0), countParticlesWithPdgId(output, 211)); + } catch (cms::Exception const& ex) { + std::cerr << ex.what() << std::endl; + CPPUNIT_ASSERT(false); + } +} + +void TestTruthLogicalGraphPostProcessor::testJetOriginLowestCommonAncestor() { + try { + GraphBuilder builder(5, 2); + + // ttbar-like: top -> W+ b ; b -> pi+ pi- (a b-jet's truth constituents). + builder.setGenParticle(0, 6, 2, 100); // top + builder.setGenParticle(1, 24, 2, 101); // W+ + builder.setGenParticle(2, 5, 2, 102); // b + builder.setGenSimParticle(3, 211, 1, 103, 1003); // pi+ + builder.setGenSimParticle(4, -211, 1, 104, 1004); // pi- + + builder.setGenVertex(0, 200); + builder.setGenSimVertex(1, 201, 2001); + + builder.addDecay(0, 0); + builder.addProduction(0, 1); + builder.addProduction(0, 2); + builder.addDecay(2, 1); + builder.addProduction(1, 3); + builder.addProduction(1, 4); + + auto graph = builder.finish(); + + // The b-jet constituents come from the b quark (closest common origin). + auto lcaB = graph.lowestCommonAncestor({graph.particle(3), graph.particle(4)}); + CPPUNIT_ASSERT(lcaB.has_value()); + CPPUNIT_ASSERT_EQUAL(int32_t(5), lcaB->pdgId()); + + // Walk up to the originating top. + auto top = graph.particle(3).firstAncestorWithPdgId(6); + CPPUNIT_ASSERT(top.has_value()); + CPPUNIT_ASSERT_EQUAL(int32_t(6), top->pdgId()); + + // Mixing constituents from the b and W sides yields the top as common origin. + auto lcaTop = graph.lowestCommonAncestor({graph.particle(3), graph.particle(4), graph.particle(1)}); + CPPUNIT_ASSERT(lcaTop.has_value()); + CPPUNIT_ASSERT_EQUAL(int32_t(6), lcaTop->pdgId()); + } catch (cms::Exception const& ex) { + std::cerr << ex.what() << std::endl; + CPPUNIT_ASSERT(false); + } +} diff --git a/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py b/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py index a391a31735bbf..8cae596ca6ddf 100644 --- a/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py +++ b/PhysicsTools/TruthInfo/test/dumpTruthGraphsFromGENSIMRECO_cfg.py @@ -20,10 +20,15 @@ "default=%(default)s uses the hardcoded list" ) parser.add_argument('-g', "--groups", default=None, help="semicolon-separated decay PDG id groups, e.g. '13,-14,16;-13,14,-16'" ) +parser.add_argument('-f', "--flavors", default=None, + help="comma-separated heavy-flavor quark ids to seed hadrons on, e.g. '5' for B hadrons, '4' for D" ) parser.add_argument('-d', "--parentDepth", type=int, default=1, help="ancestor generations kept above each root as context, default=%(default)s" ) parser.add_argument('-i', "--ignore", default=None, help="comma-separated PDG ids to remove from the final logical graph, e.g. '22'" ) +parser.add_argument("--keepSpectators", action=BooleanOptionalAction, default=True, + help="keep stable final-state spectators (underlying event) outside the selection; " + "use --no-keepSpectators for a focused subgraph" ) parser.add_argument("--showAll", action='store_true', help="do not hide zero-simhit subgraphs or large SIM source vertices in the logical DOT dump" ) args = parser.parse_args() @@ -34,6 +39,7 @@ def _parsePdgIds(text): seedPdgIds = _parsePdgIds(args.seeds) if args.seeds is not None else [23, 15, -15, 25, 4, 5, 6] decayPdgIdGroups = [_parsePdgIds(group) for group in args.groups.split(';')] if args.groups else [] ignoredPdgIds = _parsePdgIds(args.ignore) if args.ignore else [] +seedHadronFlavors = _parsePdgIds(args.flavors) if args.flavors else [] if '/' not in args.inputFile and ':' not in args.inputFile: args.inputFile = 'file:'+args.inputFile if args.outdir and not os.path.exists(args.outdir): @@ -109,10 +115,19 @@ def _parsePdgIds(text): # (debugging escape hatch). seedPdgIds=cms.vint32(*seedPdgIds), + # Seed on hadrons by heavy-flavor content (5=b, 4=c), OR-ed with + # seedPdgIds. E.g. -f 5 selects all B-hadron decay subgraphs. + seedHadronFlavors=cms.vint32(*seedHadronFlavors), + # Ancestor generations kept above each root as context only: their # other descendants are not pulled in. seedParentDepth=cms.uint32(args.parentDepth), + # Keep stable spectators (underlying event) on an artificial + # UnderlyingEvent vertex; --no-keepSpectators drops them for a focused + # subgraph (only the selection + its Upstream/ISR context). + keepStableSpectators=cms.bool(args.keepSpectators), + # Decay patterns of interest: unordered, charge-sensitive PDG id # multisets, OR-ed. With seedPdgIds set, only roots whose effective # decay products contain a group are kept, e.g. diff --git a/Validation/Configuration/python/truthPrevalidation_cff.py b/Validation/Configuration/python/truthPrevalidation_cff.py index 82348a9f77a51..79eb1e560fe3e 100644 --- a/Validation/Configuration/python/truthPrevalidation_cff.py +++ b/Validation/Configuration/python/truthPrevalidation_cff.py @@ -18,7 +18,9 @@ postProcessing = cms.PSet( collapseIntermediateGenParticles = cms.bool(True), seedPdgIds = cms.vint32(), + seedHadronFlavors = cms.vint32(), seedParentDepth = cms.uint32(0), + keepStableSpectators = cms.bool(True), decayPdgIdGroups = cms.VPSet(), ignoredPdgIds = cms.vint32(), ignoredParticleIds = cms.vuint32(), From c1d921fbe3a316bef222b52b0e865ae688e557ff Mon Sep 17 00:00:00 2001 From: Felice Pantaleo Date: Sat, 13 Jun 2026 14:11:11 +0200 Subject: [PATCH 44/53] TruthInfo: add truth-graph connectivity, gallery and relval tooling --- PhysicsTools/TruthInfo/test/README_tools.md | 57 ++++++++ .../TruthInfo/test/makeTruthGallery.sh | 68 ++++++++++ .../TruthInfo/test/runTruthRelvals.sh | 29 ++++ .../TruthInfo/test/truthGraphConnectivity.py | 124 ++++++++++++++++++ 4 files changed, 278 insertions(+) create mode 100644 PhysicsTools/TruthInfo/test/README_tools.md create mode 100755 PhysicsTools/TruthInfo/test/makeTruthGallery.sh create mode 100755 PhysicsTools/TruthInfo/test/runTruthRelvals.sh create mode 100755 PhysicsTools/TruthInfo/test/truthGraphConnectivity.py diff --git a/PhysicsTools/TruthInfo/test/README_tools.md b/PhysicsTools/TruthInfo/test/README_tools.md new file mode 100644 index 0000000000000..d09fe6e572d51 --- /dev/null +++ b/PhysicsTools/TruthInfo/test/README_tools.md @@ -0,0 +1,57 @@ +# TruthInfo test & debugging tools + +Reusable helpers for producing and inspecting truth graphs. All require `cmsenv` +(run from `CMSSW_17_0_0_pre2/src`). + +| Tool | Purpose | +|---|---| +| `dumpTruthGraphsFromGENSIMRECO_cfg.py` | cmsRun config: build the raw + logical truth graph from a GEN-SIM/RECO file and dump DOT (+NanoAOD rechit/simhit tables). Selection flags: `-s/--seeds`, `-g/--groups`, `-d/--parentDepth`, `-i/--ignore`, `-m/--merge`, `-c/--collapse`, `--showAll`, `-n`, `-o`, `-t`. `-s 0` keeps the full graph. | +| `truthGraphConnectivity.py` | FWLite debugger: per-event count of weakly-connected components and how many SimTrack/SimVertex are disconnected from a generator primary (the orphans). Exits non-zero if any event has orphans. `--link {parentIndex,ancestor,combined}`. | +| `makeTruthGallery.sh` | Build the per-process DOT/SVG gallery (full + natural-seed selection) from a relval library dir. | +| `runTruthRelvals.sh` | Run the 8 enableTruth Run4 D120 no-PU truth-validation workflows via `runTheMatrix`. | +| `TruthLogicalGraphPostProcessor_t.cpp` | cppunit tests for the logical-graph postprocessing (selection, merging, collapsing). | + +## Typical flow +```bash +cmsenv # from CMSSW_17_0_0_pre2/src +runTruthRelvals.sh /path/library # produce the sample library (step1..5) +truthGraphConnectivity.py /path/library/34050.88_*/step3.root # sanity: orphans == 0 +makeTruthGallery.sh /path/library /path/dot_gallery # DOT + SVG gallery +``` + +## Focused selections (phases 1-3) +The postprocessing supports focused, physics-oriented views: +- `--no-keepSpectators` drops underlying-event spectators, leaving the selection + plus its truncated upstream attached to a labeled **ISR/upstream** source node. + Spectators (when kept) sit on a separate **underlying event** node; both + artificial nodes carry the genEvent/eventId of the activity they summarize + (pile-up provenance). +- `-f/--flavors` seeds on hadrons by heavy-flavor content (`-f 5` = B hadrons, + `-f 4` = D hadrons), OR-ed with `-s/--seeds`. +```bash +# clean Z -> mu mu view with an explicit ISR node: +cmsRun dumpTruthGraphsFromGENSIMRECO_cfg.py file:step3.root -s 23 -d 1 --no-keepSpectators +# all B-hadron decay subgraphs: +cmsRun dumpTruthGraphsFromGENSIMRECO_cfg.py file:step3.root -f 5 --no-keepSpectators +``` + +## Jet -> originating particle +The graph answers "which particle did this jet come from" once you have the +jet's truth constituents (GEN-jet constituents now; hit-matched reco +constituents via the LogicalGraphHitIndex later): +```cpp +// jetParticles: the truth::Particle of each constituent +auto origin = graph.lowestCommonAncestor(jetParticles); // e.g. the b quark of a b-jet +auto top = jetParticles.front().firstAncestorWithPdgId(6); // the originating top +``` +`lowestCommonAncestor` returns the closest shared ancestor; `firstAncestorWithPdgId` +walks up to a specific origin species. + +## One-off selection / coherence scan +`dumpTruthGraphsFromGENSIMRECO_cfg.py` drives all selection studies, e.g.: +```bash +# Z -> mu mu only (drop Z -> ee), depth-0 context: +cmsRun dumpTruthGraphsFromGENSIMRECO_cfg.py file:step3.root -s 23 -g 13,-13 -d 0 +# full graph for debugging: +cmsRun dumpTruthGraphsFromGENSIMRECO_cfg.py file:step3.root -s 0 --showAll +``` diff --git a/PhysicsTools/TruthInfo/test/makeTruthGallery.sh b/PhysicsTools/TruthInfo/test/makeTruthGallery.sh new file mode 100755 index 0000000000000..cec9f67372c72 --- /dev/null +++ b/PhysicsTools/TruthInfo/test/makeTruthGallery.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# +# Build a DOT/SVG gallery of logical truth graphs for the enableTruth relval +# samples, one folder per physics process. For each sample it dumps: +#
Vertex " << i << "
" << roleName << " (genEvent=" << d.genEvent << ", eid=" << d.eventId + << ")
domain: " << logicalVertexDomain(d) << "