Scfi Img CrossSystem Topo Clustering (#2861) - #2865
Conversation
### Briefly, what does this PR introduce? Please link to any relevant presentations or discussions. ### What is the urgency of this PR? - [ ] High (please describe reason below) - [x] Medium - [ ] Low ### What kind of change does this PR introduce? - [ ] Bug fix (issue #__) - [x] New feature (issue #__) - [ ] Optimization (issue #__) - [ ] Updated parameters, constants (issue #__) - [ ] Updated documentation - [ ] other: __ ### Please check if any of the following apply - [ ] This PR requires changes to geometry (epic PR: __) - [ ] This PR requires changes to EDM4eic (EDM PR: __) - [ ] This PR introduces breaking changes. Please describe changes users need to make below. - [ ] This PR changes default behavior. Please describe changes below. - [ ] AI was used in preparing this PR. Please describe usage below. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Wouter Deconinck <wdconinc@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR enables topological clustering across SciFi and Imaging calorimeter hits (notably in BEMC) by introducing combined hit collections and extending the topo clustering + CoG reconstruction logic to be “system-aware” via DD4hep cellID decoding.
Changes:
- Add
readoutfields to clustering/reco configs and use DD4hepIDDescriptorto decodesystemfor cross-system / system-specific behavior. - Extend
ImagingTopoClusterto cluster per system and optionally merge SciFi+Imaging clusters, and add combined hit/link/association collections in the BEMC plugin. - Update algorithm unit tests to provide the new
readoutconfiguration.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/tests/algorithms_test/particle_flow_TrackProtoClusterMatchPromoter.cc | Updates test config to set readout for CoG cluster reco. |
| src/tests/algorithms_test/calorimetry_ImagingTopoCluster.cc | Updates topo clustering test to set readout and new mode/distance fields. |
| src/tests/algorithms_test/calorimetry_CalorimeterClusterRecoCoG.cc | Updates CoG clustering test to set readout. |
| src/detectors/BEMC/BEMC.cc | Adds merged SciFi+Imaging hit/link/association collections and runs topo clustering + CoG on the combined collection. |
| src/algorithms/calorimetry/ImagingTopoClusterConfig.h | Expands config to include readout, additional system-specific parameters, and new xz layer mode. |
| src/algorithms/calorimetry/ImagingTopoCluster.h | Adds geometry/ID decoding state and cross-system neighbor helper declarations. |
| src/algorithms/calorimetry/ImagingTopoCluster.cc | Implements system-aware grouping/merging and readout-based ID decoding. |
| src/algorithms/calorimetry/CalorimeterClusterRecoCoGConfig.h | Adds readout to support system-aware behavior. |
| src/algorithms/calorimetry/CalorimeterClusterRecoCoG.h | Adds geometry/ID decoding state used by system-aware weighting. |
| src/algorithms/calorimetry/CalorimeterClusterRecoCoG.cc | Implements system-aware energy/position weighting based on decoded system IDs. |
Suppressed comments (2)
src/algorithms/calorimetry/ImagingTopoCluster.cc:337
- Typo in the exception message: it says the expected tuple is "(x_dist, y_dist, y_dist)" but the third component is z_dist.
const std::string msg =
"Expected 3 values (x_dist, y_dist, y_dist) for ScFi_diffLayerDistXYZ";
error(msg);
src/algorithms/calorimetry/ImagingTopoCluster.cc:432
- Typo in the exception message: it says the expected tuple is "(x_dist, y_dist, y_dist)" but the third component is z_dist.
const std::string msg = "Expected 3 values (x_dist, y_dist, y_dist) for Img_diffLayerDistXYZ";
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| inline int getSystemID(const edm4eic::CalorimeterHit& hit, const dd4hep::IDDescriptor& m_idSpec) { | ||
| static thread_local auto* sys_field = m_idSpec.field("system"); | ||
| return sys_field->value(hit.getCellID()); | ||
| } |
| bool hasSciFi = false; | ||
| bool hasImaging = false; | ||
|
|
||
| for (auto& hit : pcl.getHits()) { | ||
| if (isSciFiHit(hit, m_idSpec)) | ||
| hasSciFi = true; | ||
| if (isImagingHit(hit, m_idSpec)) | ||
| hasImaging = true; | ||
| } | ||
|
|
||
| bool specialMode = hasSciFi && hasImaging; | ||
|
|
|
|
||
| totalE += energy; | ||
| time += (hit.getTime() - time) * energy / totalE; | ||
|
|
| m_detector = algorithms::GeoSvc::instance().detector(); | ||
| if (!m_detector) { | ||
| error("Failed to get detector from GeometrySvc for readout '{}'", m_cfg.readout); | ||
| throw std::runtime_error("Detector not available"); | ||
| } | ||
|
|
||
| m_idSpec = m_detector->readout(m_cfg.readout).idSpec(); | ||
|
|
| case ImagingTopoClusterConfig::ELayerMode::xyz: | ||
| if (m_cfg.diffLayerDistXYZ.size() != 3) { | ||
| const std::string msg = "Expected 3 values (x_dist, y_dist, z_dist) for diffLayerDistXYZ"; | ||
| const std::string msg = "Expected 3 values (x_dist, y_dist, y_dist) for diffLayerDistXYZ"; |
| } else if (s == "xz" or s == "2") { | ||
| layerMode = ImagingTopoClusterConfig::ELayerMode::xz; | ||
| } else if (s == "xyz" or s == "3") { |
This PR applies the include-what-you-use fixes as suggested by https://github.com/eic/EICrecon/actions/runs/31633457160. Please merge this PR into the branch `Hybrid-Clustering-Algorithm-for-BIC` to resolve failures in PR #2865. Auto-generated by [create-pull-request][1] [1]: https://github.com/peter-evans/create-pull-request Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Suppressed comments (6)
src/algorithms/calorimetry/CalorimeterClusterRecoCoG.cc:120
getSystemIDcachessys_fieldasthread_localand dereferences it without a null check. This can crash when the configured readout (or IDSpec) doesn't have asystemfield, and thethread_localcache can also become stale if different instances use differentIDDescriptors on the same thread.
inline int getSystemID(const edm4eic::CalorimeterHit& hit, const dd4hep::IDDescriptor& m_idSpec) {
static thread_local auto* sys_field = m_idSpec.field("system");
return sys_field->value(hit.getCellID());
}
src/algorithms/calorimetry/CalorimeterClusterRecoCoG.cc:186
- When
specialModeis enabled, some hits can contributeenergy = 0. If the first hits processed have zero energy,totalEstays 0 andenergy / totalEbecomes0/0, producing NaNs/undefined behavior in the incremental time update.
totalE += energy;
time += (hit.getTime() - time) * energy / totalE;
src/algorithms/calorimetry/ImagingTopoCluster.cc:474
process()currently returns early when the configured IDSpec lacks asystemfield. Combined withinit()unconditionally doingm_detector->readout(m_cfg.readout)this makesreadout/systemeffectively mandatory and will cause existing configurations to stop producing clusters (e.g.ZDC.cc:190-205andFHCAL.cc:98-120configureImagingTopoCluster_factorywithout.readout). Either update those callers to set.readout, or add a backward-compatible fallback path whensystemisn't available.
auto* sys_field = m_idSpec.field("system");
if (!sys_field) {
error("Field 'system' not found in IDSpec for readout {}", m_cfg.readout);
return;
}
src/algorithms/calorimetry/ImagingTopoCluster.cc:220
- Typo in error message: the third component should be
z_dist, noty_dist(this message is user-facing and currently misleading).
const std::string msg = "Expected 3 values (x_dist, y_dist, y_dist) for diffLayerDistXYZ";
src/algorithms/calorimetry/ImagingTopoCluster.cc:339
- Typo in error message: the third component should be
z_dist, noty_dist(this message is user-facing and currently misleading).
"Expected 3 values (x_dist, y_dist, y_dist) for ScFi_diffLayerDistXYZ";
error(msg);
src/algorithms/calorimetry/ImagingTopoCluster.cc:434
- Typo in error message: the third component should be
z_dist, noty_dist(this message is user-facing and currently misleading).
const std::string msg = "Expected 3 values (x_dist, y_dist, y_dist) for Img_diffLayerDistXYZ";
| // Check against all ScFi clusters | ||
| for (size_t idx_ScFi : ScFi) { | ||
|
|
||
| const auto& cl_ScFi = all_clusters[idx_ScFi]; | ||
| bool neighbor_found = false; | ||
|
|
Capybara summary for PR 2865
Last updated 2026-08-14T16:09-04:00 122650a |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Suppressed comments (6)
src/algorithms/calorimetry/CalorimeterClusterRecoCoG.cc:188
- In specialMode, Imaging hits are forced to contribute
energy = 0, so the first few iterations can leavetotalE == 0and trigger0/0in the running time update (time += ... * energy / totalE), producing NaN/UB.
energy = hit.getEnergy() * weight;
}
totalE += energy;
time += (hit.getTime() - time) * energy / totalE;
src/algorithms/calorimetry/ImagingTopoCluster.cc:58
- init() unconditionally does
m_detector->readout(m_cfg.readout).idSpec(). SinceImagingTopoClusterConfig::readoutdefaults to an empty string, this will fail at runtime with a less actionable DD4hep error when callers forget to set it.
void ImagingTopoCluster::init() {
m_detector = algorithms::GeoSvc::instance().detector();
if (!m_detector) {
error("Failed to get detector from GeometrySvc for readout '{}'", m_cfg.readout);
throw std::runtime_error("Detector not available");
}
m_idSpec = m_detector->readout(m_cfg.readout).idSpec();
src/algorithms/calorimetry/ImagingTopoCluster.cc:223
- The exception text for diffLayerDistXYZ says "(x_dist, y_dist, y_dist)" but the third component is z. This makes configuration/debugging confusing.
case ImagingTopoClusterConfig::ELayerMode::xyz:
if (m_cfg.diffLayerDistXYZ.size() != 3) {
const std::string msg = "Expected 3 values (x_dist, y_dist, y_dist) for diffLayerDistXYZ";
error(msg);
throw std::runtime_error(msg);
}
src/algorithms/calorimetry/ImagingTopoCluster.cc:623
- Cross-system merging can duplicate the same ScFi cluster into multiple merged outputs:
used_ScFiis set but never used to skip already-merged ScFi clusters in the inner loop, so later Img clusters can merge the same ScFi hits again.
// Check against all ScFi clusters
for (size_t idx_ScFi : ScFi) {
const auto& cl_ScFi = all_clusters[idx_ScFi];
bool neighbor_found = false;
src/algorithms/calorimetry/ImagingTopoCluster.cc:474
- If the configured readout does not contain a
systemfield, the algorithm currently logs an error and silently returns without producing any proto-clusters. This is easy to miss and turns a configuration problem into an empty-output failure mode.
auto* sys_field = m_idSpec.field("system");
if (!sys_field) {
error("Field 'system' not found in IDSpec for readout {}", m_cfg.readout);
return;
}
src/algorithms/calorimetry/ImagingTopoCluster.cc:627
- The cross-system merge checks every hit in every Img cluster against every hit in every ScFi cluster (nested loops over clusters and hits). In high-occupancy events this can become very expensive (worst-case O(N^2) in hits). Consider precomputing per-cluster bounding boxes / representative points or building a spatial index to shortlist candidate cluster pairs before doing hit-by-hit checks.
for (auto h1 : cl_Img) {
for (auto h2 : cl_ScFi) {
if (cross_system_is_neighbour((*hits)[h1], (*hits)[h2])) {
debug(" Cross system neighbour: Img cluster {} hit {} <-> ScFi cluster {} hit {}",
| inline int getSystemID(const edm4eic::CalorimeterHit& hit, const dd4hep::IDDescriptor& m_idSpec) { | ||
| static thread_local const auto* sys_field = m_idSpec.field("system"); | ||
| return sys_field->value(hit.getCellID()); | ||
| } |
No Clang-Tidy warnings found so I assume my comments were addressed
Briefly, what does this PR introduce? Please link to any relevant presentations or discussions.
Allows clustering of ScFi and Imaging Hits together based on the Topological Clustering Algorithm
What is the urgency of this PR?
What kind of change does this PR introduce?
Please check if any of the following apply
Briefly, what does this PR introduce? Please link to any relevant presentations or discussions.
What is the urgency of this PR?
What kind of change does this PR introduce?
Please check if any of the following apply