Skip to content
32 changes: 28 additions & 4 deletions sbncode/CAFMaker/CAFMakerParams.h
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I have only a curiosity on how the TPC wire (tick) distance default is decided (a presentation to look at/a short explanation would suffice for me thanks!)

Copy link
Copy Markdown
Contributor Author

@rtriozzi rtriozzi Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default values sort of hint at a "3 cm" range around the reconstructed vertex. How such a distance translates onto the different wire planes is not obvious, so I just set default numbers that kind of make sense. They can be tuned at the FHiCL level, if anyone wants to play with the variables. An application of this is to reject HIP activity at the vertex, for example for selecting 1e0p0π final states; you can find something along these lines here (+ data/MC comparisons): https://sbn-docdb.fnal.gov/cgi-bin/sso/RetrieveFile?docid=45736&filename=EventSelection_20260220.pdf&version=1.

Original file line number Diff line number Diff line change
Expand Up @@ -350,10 +350,10 @@ namespace caf
"" //Empty by default, configured in icaruscode cafmaker_defs
};

Atom<art::InputTag> NuGraphSliceHitLabel {
Name("NuGraphSliceHitLabel"),
Comment("Label of NuGraph slice hit map."),
"" //Empty by default, please set to e.g. art::InputTag("nuslhits")
Atom<art::InputTag> NuGraphSlicesLabel {
Name("NuGraphSlicesLabel"),
Comment("Label of slices that have NuGraph inference."),
"" //Empty by default, please set to e.g. art::InputTag("NCCSlices")
};

Atom<art::InputTag> NuGraphFilterLabel {
Expand All @@ -368,6 +368,30 @@ namespace caf
"" //Empty by default, please set to e.g. art::InputTag("NuGraph","semantic")
};

Atom<bool> UsePandoraAfterNuGraph {
Name("UsePandoraAfterNuGraph"),
Comment("Whether to use the second pass Pandora outputs for NuGraph reco."),
false
};

Atom<float> NuGraphFilterCut {
Name("NuGraphFilterCut"),
Comment("Cut on the NuGraph2 filter score to define hit as signal or noise."),
0.5
};

Atom<float> NuGraphHIPTagWireDist {
Name("NuGraphHIPTagWireDist"),
Comment("TPC wire distance from the vertex used to count NuGraph2–tagged HIP hits."),
10
};

Atom<float> NuGraphHIPTagTickDist {
Name("NuGraphHIPTagTickDist"),
Comment("TPC tick distance from the vertex used to count NuGraph-2–tagged HIP hits."),
50
};

Atom<string> OpFlashLabel {
Name("OpFlashLabel"),
Comment("Label of PMT flash."),
Expand Down
127 changes: 91 additions & 36 deletions sbncode/CAFMaker/CAFMaker_module.cc
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the changes made here are overall consistent with my expectations and I didn't find anything specific explicitly wrong. I have just a couple of clarifications to ask:
-if the option to run twice Pandora reconstruction (one as input to nu-graph and one after nu-graph, profiting of the semantic classification) is enabled, then nu-graph slices will coincide with the final set of slices produced in the reconstruction so this case is clear;

  • if the option to run Pandora reconstruction twice is not selected then nu-graph slices will basically be identical to those Pandora generated in its first (and unique) run with additional features saved at hit-level, correct?
    Then I think the rest of the changes are meant to ensure all the semantic classification output from nu-graph is properly assigned to each slice and later on to each hit (and I guess here the same loop over nu-graph slices is repeated at pfp level because that's where info are stored, i.e. at slice level - I wonder if the second loop over nu-graph slices can be avoided, but perhaps this is not the case and I haven't add a chance to look at the details of this part of the code - could this be quickly checked? since I believe the loop is nested in a way that could allow this).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I wonder why slice vertex-related info are highlighted as a change with respect to the previous code version but I hope it's an accident (this info should not have been changed based on nu-graph correct?)

Copy link
Copy Markdown
Contributor Author

@rtriozzi rtriozzi Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the interpretation is correct. The flag was needed because the slices in the two paths change a bit, but we want NuGraph2 variables in the CAF regardless of the chosen reconstruction path. The NuGraph2 PID is very strong, and can be helpful for selections even if the reconstruction is the classic Pandora one.

Some of the variables I added rely on the vertex information (e.g., the HIP tagger on the three planes), so I had to move some things here and there to make sure, e.g., that the vertex information was retrieved before I needed it. Effectively, there are no changes for other CAF variables.

Original file line number Diff line number Diff line change
Expand Up @@ -1717,33 +1717,30 @@ void CAFMaker::produce(art::Event& evt) noexcept {

// collect the TPC slices
std::vector<art::Ptr<recob::Slice>> slices;
std::vector<art::Ptr<recob::Slice>> nuGraphSlices;
std::vector<std::string> slice_tag_suffixes;
std::vector<unsigned> slice_tag_indices;
for (unsigned i_tag = 0; i_tag < pandora_tag_suffixes.size(); i_tag++) {
const std::string &pandora_tag_suffix = pandora_tag_suffixes[i_tag];

// Get a handle on the slices
art::Handle<std::vector<recob::Slice>> thisSlices;
GetByLabelStrict(evt, fParams.PFParticleLabel() + pandora_tag_suffix, thisSlices);

if (thisSlices.isValid()) {
art::fill_ptr_vector(slices, thisSlices);
if (fParams.UsePandoraAfterNuGraph()) {
nuGraphSlices = slices;
} else {
nuGraphSlices = evt.getProduct<std::vector<art::Ptr<recob::Slice>>>(fParams.NuGraphSlicesLabel().label() + pandora_tag_suffix);
}
for (unsigned i = 0; i < thisSlices->size(); i++) {
slice_tag_suffixes.push_back(pandora_tag_suffix);
slice_tag_indices.push_back(i_tag);
}
}
}

// nu graph
std::vector< art::Handle<std::vector<unsigned int>> > ng2_slice_hit_map_handle(pandora_tag_suffixes.size());
std::vector< art::Handle<std::vector<anab::FeatureVector<1>>> > ng2_filter_handle(pandora_tag_suffixes.size());
std::vector< art::Handle<std::vector<anab::FeatureVector<5>>> > ng2_semantic_handle(pandora_tag_suffixes.size());
for (unsigned i_tag = 0; i_tag < pandora_tag_suffixes.size(); i_tag++) {
const std::string &pandora_tag_suffix = pandora_tag_suffixes[i_tag];
GetByLabelIfExists(evt, fParams.NuGraphSliceHitLabel().encode() + pandora_tag_suffix, ng2_slice_hit_map_handle[i_tag]);
GetByLabelIfExists(evt, fParams.NuGraphFilterLabel().label() + pandora_tag_suffix + ":" + fParams.NuGraphFilterLabel().instance(), ng2_filter_handle[i_tag]);
GetByLabelIfExists(evt, fParams.NuGraphSemanticLabel().label() + pandora_tag_suffix + ":" + fParams.NuGraphSemanticLabel().instance(), ng2_semantic_handle[i_tag]);
}

// The Standard Record
// Branch entry definition -- contains list of slices, CRT information, and truth information
StandardRecord rec;
Expand Down Expand Up @@ -1799,19 +1796,7 @@ void CAFMaker::produce(art::Event& evt) noexcept {
fmatch_assn_map.emplace(std::make_pair(fname_opdet, sfm_assn));
}
}

std::vector<art::Ptr<anab::FeatureVector<1>>> ng2_filter_vec;
std::vector<art::Ptr<anab::FeatureVector<5>>> ng2_semantic_vec;
if (ng2_filter_handle[producer].isValid()) {
art::fill_ptr_vector(ng2_filter_vec,ng2_filter_handle[producer]);
}
if (ng2_semantic_handle[producer].isValid()) {
art::fill_ptr_vector(ng2_semantic_vec,ng2_semantic_handle[producer]);
}
if (ng2_slice_hit_map_handle[producer].isValid()) {
FillSliceNuGraph(slcHits,*ng2_slice_hit_map_handle[producer],ng2_filter_vec,ng2_semantic_vec,recslc);
}


art::FindManyP<sbn::OpT0Finder> fmOpT0 =
FindManyPStrict<sbn::OpT0Finder>(sliceList, evt, fParams.OpT0Label() + slice_tag_suff);
std::vector<art::Ptr<sbn::OpT0Finder>> slcOpT0;
Expand Down Expand Up @@ -1865,15 +1850,15 @@ void CAFMaker::produce(art::Event& evt) noexcept {
// make Ptr's to clusters for cluster -> other object associations
if (fmPFPClusters.isValid()) {
for (size_t ipf=0; ipf<fmPFPart.size();++ipf) {
std::vector<art::Ptr<recob::Hit>> pfphits;
std::vector<art::Ptr<recob::Cluster>> pfclusters = fmPFPClusters.at(ipf);
art::FindManyP<recob::Hit> fmCluHits = FindManyPStrict<recob::Hit>(pfclusters, evt, fParams.PFParticleLabel() + slice_tag_suff);
for (size_t icl=0; icl<fmCluHits.size();icl++) {
for (auto hit : fmCluHits.at(icl)) {
pfphits.push_back(hit);
}
}
fmPFPartHits.push_back(pfphits);
std::vector<art::Ptr<recob::Hit>> pfphits;
std::vector<art::Ptr<recob::Cluster>> pfclusters = fmPFPClusters.at(ipf);
art::FindManyP<recob::Hit> fmCluHits = FindManyPStrict<recob::Hit>(pfclusters, evt, fParams.PFParticleLabel() + slice_tag_suff);
for (size_t icl=0; icl<fmCluHits.size();icl++) {
for (auto hit : fmCluHits.at(icl)) {
pfphits.push_back(hit);
}
}
fmPFPartHits.push_back(pfphits);
}
}

Expand Down Expand Up @@ -2043,8 +2028,8 @@ void CAFMaker::produce(art::Event& evt) noexcept {
// primary particle and meta-data
const recob::PFParticle *primary = (iPart == fmPFPart.size()) ? NULL : fmPFPart[iPart].get();
const larpandoraobj::PFParticleMetadata *primary_meta = (iPart == fmPFPart.size()) ? NULL : fmPFPMeta.at(iPart).at(0).get();
// get the flash match

// get the flash match
std::map<std::string, const sbn::SimpleFlashMatch*> fmatch_map;
std::map<std::string, art::FindManyP<sbn::SimpleFlashMatch>>::iterator fmatch_it;
for(fmatch_it = fmatch_assn_map.begin();fmatch_it != fmatch_assn_map.end();fmatch_it++) {
Expand All @@ -2060,12 +2045,58 @@ void CAFMaker::produce(art::Event& evt) noexcept {
}
}
}

// get the primary vertex
const recob::Vertex *vertex = (iPart == fmPFPart.size() || !fmVertex.at(iPart).size()) ? NULL : fmVertex.at(iPart).at(0).get();

//#######################################################
// Add slice info.
//#######################################################
if (std::find(nuGraphSlices.begin(), nuGraphSlices.end(), slice) != nuGraphSlices.end()) {
std::vector<art::Ptr<anab::FeatureVector<1>>> ng2_filter_vec;
std::vector<art::Ptr<anab::FeatureVector<5>>> ng2_semantic_vec;
art::FindOneP<anab::FeatureVector<1>> findOneFilter(slcHits, evt, fParams.NuGraphFilterLabel().label() + slice_tag_suff + ":" + fParams.NuGraphFilterLabel().instance());
art::FindOneP<anab::FeatureVector<5>> findOneSemantic(slcHits, evt, fParams.NuGraphSemanticLabel().label() + slice_tag_suff + ":" + fParams.NuGraphSemanticLabel().instance());

// filter
if (findOneFilter.isValid()) {
ng2_filter_vec.reserve(slcHits.size());
for (size_t hitIdx = 0; hitIdx < slcHits.size(); ++hitIdx) {
ng2_filter_vec.emplace_back(findOneFilter.at(hitIdx));
}
}

// semantic tagging
if (findOneSemantic.isValid()) {
ng2_semantic_vec.reserve(slcHits.size());
for (size_t hitIdx = 0; hitIdx < slcHits.size(); ++hitIdx) {
ng2_semantic_vec.emplace_back(findOneSemantic.at(hitIdx));
}
}

// vertex projection onto the three wire planes
float vtx_wire[3];
float vtx_tick[3];

if (vertex != NULL) {
auto const& tpcID = geom->FindTPCAtPosition(vertex->position());
if (tpcID.isValid) {
for (geo::PlaneID const& p : wireReadout.Iterate<geo::PlaneID>()) {
auto const& planeID = geo::PlaneID{tpcID, p.Plane};
const geo::PlaneGeo& planeGeo = wireReadout.Plane(planeID);
vtx_wire[p.Plane] = planeGeo.WireCoordinate(vertex->position()); ///< wire projection
vtx_tick[p.Plane] = dprop.ConvertXToTicks(vertex->position().X(), planeID); ///< drift projection
}
}
}

if (ng2_filter_vec.size() > 0 || ng2_semantic_vec.size() > 0) {
FillSliceNuGraph(slcHits, ng2_filter_vec, ng2_semantic_vec, fmPFPartHits,
vtx_wire, vtx_tick, fParams.NuGraphHIPTagWireDist(), fParams.NuGraphHIPTagTickDist(),
fParams.NuGraphFilterCut(), recslc);
}
}

FillSliceVars(*slice, primary, producer, recslc);
FillSliceMetadata(primary_meta, recslc);
FillSliceFlashMatch(fmatch_map["fmatch"], recslc.fmatch);
Expand Down Expand Up @@ -2189,8 +2220,32 @@ void CAFMaker::produce(art::Event& evt) noexcept {
FillCNNScores(thisParticle, cnnScores, pfp);
}

if (ng2_slice_hit_map_handle[producer].isValid()) {
FillPFPNuGraph(*ng2_slice_hit_map_handle[producer], ng2_filter_vec, ng2_semantic_vec, fmPFPartHits.at(iPart), pfp);
if (std::find(nuGraphSlices.begin(), nuGraphSlices.end(), slice) != nuGraphSlices.end()) {
std::vector<art::Ptr<recob::Hit>>& PFPHits = fmPFPartHits.at(iPart);
art::FindOneP<anab::FeatureVector<1>> findOneFilter(PFPHits, evt, fParams.NuGraphFilterLabel().label() + slice_tag_suff + ":" + fParams.NuGraphFilterLabel().instance());
art::FindOneP<anab::FeatureVector<5>> findOneSemantic(PFPHits, evt, fParams.NuGraphSemanticLabel().label() + slice_tag_suff + ":" + fParams.NuGraphSemanticLabel().instance());
std::vector<art::Ptr<anab::FeatureVector<1>>> ng2_filter_vec;
std::vector<art::Ptr<anab::FeatureVector<5>>> ng2_semantic_vec;

// filter
if (findOneFilter.isValid()) {
ng2_filter_vec.reserve(PFPHits.size());
for (size_t hitIdx = 0; hitIdx < PFPHits.size(); ++hitIdx) {
ng2_filter_vec.emplace_back(findOneFilter.at(hitIdx));
}
}

// semantic tagging
if (findOneSemantic.isValid()) {
ng2_semantic_vec.reserve(PFPHits.size());
for (size_t hitIdx = 0; hitIdx < PFPHits.size(); ++hitIdx) {
ng2_semantic_vec.emplace_back(findOneSemantic.at(hitIdx));
}
}

if (ng2_filter_vec.size() > 0 || ng2_semantic_vec.size() > 0) {
FillPFPNuGraph(PFPHits, ng2_filter_vec, ng2_semantic_vec, fParams.NuGraphFilterCut(), pfp);
}
}

if (!thisTrack.empty()) { // it has a track!
Expand Down
Loading