Skip to content

Add CLUE clustering #1699

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
May 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bcfba75
manually copy over CLUE clustering files from Ella's developments
tomeichlersmith May 5, 2025
ce75999
store the track ID of the 'origin' particle which may be more precise…
tomeichlersmith May 5, 2025
0c4d9ea
bump ClassDef version after introducing new member
tomeichlersmith May 5, 2025
6f62ea1
use variable that was determined earlier
tomeichlersmith May 5, 2025
1f8b0a0
move CLUE into logging, transition member variables to snake_case
tomeichlersmith May 7, 2025
df49df7
dont override, that interacts poorly with ROOT's ClassDef macro
tomeichlersmith May 7, 2025
91cf649
purge debug_ flag, drop debug messages to trace
tomeichlersmith May 7, 2025
e174c4c
Apply clang-format
github-actions[bot] May 7, 2025
965bbda
parameter naming in python to snake_case
tomeichlersmith May 7, 2025
944ac69
add example config to test-run clustering
tomeichlersmith May 7, 2025
bc0b4ab
merge Working clusters and prefer pointers to save on copying
tomeichlersmith May 12, 2025
f63f743
Apply clang-format
github-actions[bot] May 12, 2025
bff3594
remove unused variables
tomeichlersmith May 12, 2025
bab9bd3
rename WorkingCluster to IntermediateCluster for clarity
tomeichlersmith May 13, 2025
929dac7
move cluster analyzer into DQM
tomeichlersmith May 13, 2025
76337ee
move max origin track ID from hardcode to config parameter
tomeichlersmith May 13, 2025
236cef6
add ecal clustering to ecal_pn, inclusive, it_pileup samples
tomeichlersmith May 13, 2025
eab73fd
clang-format
tomeichlersmith May 13, 2025
9dd8195
fix typo in ecal_pn config
tomeichlersmith May 13, 2025
68543d8
Merge branch 'trunk' into 1411-add-clue-clustering
tomeichlersmith May 14, 2025
6b1b1b7
update declaration macro from Factory updates
tomeichlersmith May 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/validation_samples/ecal_pn/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import LDMX.Ecal.ecal_hardcoded_conditions
import LDMX.Ecal.digi as ecal_digi
import LDMX.Ecal.vetos as ecal_vetos
import LDMX.Ecal.ecalClusters as ecal_cluster

# Load the HCAL modules
import LDMX.Hcal.HcalGeometry
Expand Down Expand Up @@ -83,6 +84,7 @@
p.sequence.extend([
ecal_digi.EcalDigiProducer(),
ecal_digi.EcalRecProducer(),
ecal_cluster.EcalClusterProducer(),
ecal_veto,
hcal_digi_reco,
hcal_veto,
Expand All @@ -91,6 +93,7 @@
trigScintTrack,
count, TriggerProcessor('trigger', 8000.),
dqm.PhotoNuclearDQM(),
dqm.EcalClusterAnalyzer()
])

p.sequence.extend(dqm.all_dqm)
Expand Down
3 changes: 3 additions & 0 deletions .github/validation_samples/inclusive/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import LDMX.Ecal.ecal_hardcoded_conditions
import LDMX.Ecal.digi as ecal_digi
import LDMX.Ecal.vetos as ecal_vetos
import LDMX.Ecal.ecalClusters as ecal_cluster

# Load the HCAL modules
import LDMX.Hcal.HcalGeometry
Expand Down Expand Up @@ -83,6 +84,7 @@
p.sequence.extend([
ecal_digi.EcalDigiProducer(),
ecal_digi.EcalRecProducer(),
ecal_cluster.EcalClusterProducer(),
ecalVeto,
hcal_digi_reco,
hcal_veto,
Expand All @@ -91,6 +93,7 @@
trigScintTrack,
count, TriggerProcessor('trigger', 8000.),
dqm.PhotoNuclearDQM(),
dqm.EcalClusterAnalyzer()
])

p.sequence.extend(dqm.all_dqm)
4 changes: 3 additions & 1 deletion .github/validation_samples/it_pileup/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@

from LDMX.Ecal import digi as eDigi
from LDMX.Ecal import vetos
import LDMX.Ecal.ecalClusters as ecal_cluster
from LDMX.Hcal import digi as hDigi

# this is hardwired into the code to be appended to the sim hits collections
Expand Down Expand Up @@ -194,14 +195,15 @@
p.sequence.extend(full_tracking_sequence.dqm_sequence)

p.sequence.extend([
ecalDigi, ecalReco, ecalVeto,
ecalDigi, ecalReco, ecalVeto, ecal_cluster.EcalClusterProducer(),
hcal_digi_reco,
hcal_veto,
*ts_digis,
*ts_clusters,
trigScintTrack,
count, TriggerProcessor('trigger', 8000.),
dqm.PhotoNuclearDQM(),
dqm.EcalClusterAnalyzer()
])

p.sequence.extend(dqm_with_overlay)
Expand Down
52 changes: 52 additions & 0 deletions DQM/include/DQM/EcalClusterAnalyzer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* @file EcalClusterAnalyzer.h
* @brief Analysis of cluster performance
* @author Ella Viirola, Lund University
*/

#ifndef DQM_ECALCLUSTERANALYZER_H
#define DQM_ECALCLUSTERANALYZER_H

// LDMX Framework
#include "Framework/Configure/Parameters.h"
#include "Framework/EventProcessor.h"

namespace dqm {

/**
* @class EcalClusterAnalyzer
* @brief
*/
class EcalClusterAnalyzer : public framework::Analyzer {
public:
EcalClusterAnalyzer(const std::string& name, framework::Process& process)
: Analyzer(name, process) {}
~EcalClusterAnalyzer() override = default;
void configure(framework::config::Parameters& ps) override;
void analyze(const framework::Event& event) override;

private:
int nbr_of_electrons_;

// Collection Name for SimHits
std::string ecal_sim_hit_coll_;

// Pass Name for SimHits
std::string ecal_sim_hit_pass_;

// Collection Name for RecHits
std::string rec_hit_coll_name_;

// Pass Name for RecHits
std::string rec_hit_pass_name_;

// Collection Name for clusters
std::string cluster_coll_name_;

// Pass Name for clusters
std::string cluster_pass_name_;
};

} // namespace dqm

#endif
34 changes: 33 additions & 1 deletion DQM/python/dqm.py
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,38 @@ def __init__(self, name='SampleValidation') :
self.build1DHistogram("pdgid_hardbremdaughters", "ID of hard brem daughters", 20, 0, 20)
self.build1DHistogram("startZ_hardbremdaughters", "Start z position of hard brem daughters [mm]", 200, -1000, 1000)

class EcalClusterAnalyzer(ldmxcfg.Analyzer) :
"""Analyze clustering"""

def __init__(self,name='EcalClusterAnalyzer') :
super().__init__(name, "dqm::EcalClusterAnalyzer", 'DQM')

self.nbr_of_electrons = 2

self.ecal_sim_hit_coll = "EcalSimHits"
self.ecal_sim_hit_pass = "" #use whatever pass is available

# Pass name for ecal digis and rec hits
self.rec_hit_coll_name = 'EcalRecHits'
self.rec_hit_pass_name = ''

self.cluster_coll_name = 'ecalClusters'
self.cluster_pass_name = ''

# Need to mod for more than two electrons
self.build1DHistogram("ancestors", "Ancestors of particles", 4, 0, 3)

self.build1DHistogram("same_ancestor", "Percentage of hits in cluster coming from the electron that produced most hits", 21, 0, 105)
self.build1DHistogram("energy_percentage", "Percentage of energy in cluster coming from the electron that produced most of energy", 21, 0, 105)
self.build1DHistogram("mixed_hit_energy", "Percentage of total energy coming from hits with energy contributions from more than one electron", 21, 0, 105)
self.build1DHistogram("clusterless_hits", "Number of hits not in a cluster", 10, 0, 200)
self.build1DHistogram("clusterless_hits_percentage", "Percentage of hits not in a cluster", 21, 0, 105)
self.build1DHistogram("total_rechits_in_event", "Rechits per event", 20, 0, 500)
self.build1DHistogram("correctly_predicted_events", "Correctly predicted events", 3, 0, 3)

self.build2DHistogram("total_energy_vs_hits", "Total energy (edep)", 30, 0, 150, "Hits in cluster", 20, 0, 200)
self.build2DHistogram("total_energy_vs_purity", "Total energy (edep)", 30, 0, 150, "Energy purity %", 21, 0, 105)
self.build2DHistogram("distance_energy_purity", "Distance in xy-plane", 20, 0, 220, "Energy purity %", 21, 0, 105)

ecal_dqm = [
EcalDigiVerify(),
Expand Down Expand Up @@ -855,4 +887,4 @@ def __init__(self, name='SampleValidation') :
]


all_dqm = ecal_dqm + hcal_dqm + trigScint_dqm + trigger_dqm
all_dqm = ecal_dqm + hcal_dqm + trigScint_dqm + trigger_dqm
185 changes: 185 additions & 0 deletions DQM/src/DQM/EcalClusterAnalyzer.cxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/**
* @file EcalClusterAnalyzer.cxx
* @brief Analysis of cluster performance
* @author Ella Viirola, Lund University
*/

#include "DQM/EcalClusterAnalyzer.h"

#include <algorithm>
#include <fstream>
#include <iostream>

#include "DetDescr/SimSpecialID.h"
#include "Ecal/Event/EcalCluster.h"
#include "Ecal/Event/EcalHit.h"
#include "SimCore/Event/SimCalorimeterHit.h"
#include "SimCore/Event/SimTrackerHit.h"

namespace dqm {

void EcalClusterAnalyzer::configure(framework::config::Parameters& ps) {
nbr_of_electrons_ = ps.getParameter<int>("nbr_of_electrons");

ecal_sim_hit_coll_ = ps.getParameter<std::string>("ecal_sim_hit_coll");
ecal_sim_hit_pass_ = ps.getParameter<std::string>("ecal_sim_hit_pass");

rec_hit_coll_name_ = ps.getParameter<std::string>("rec_hit_coll_name");
rec_hit_pass_name_ = ps.getParameter<std::string>("rec_hit_pass_name");

cluster_coll_name_ = ps.getParameter<std::string>("cluster_coll_name");
cluster_pass_name_ = ps.getParameter<std::string>("cluster_pass_name");
return;
}

void EcalClusterAnalyzer::analyze(const framework::Event& event) {
const auto& ecal_rec_hits{event.getCollection<ldmx::EcalHit>(
rec_hit_coll_name_, rec_hit_pass_name_)};
const auto& ecal_sim_hits{event.getCollection<ldmx::SimCalorimeterHit>(
ecal_sim_hit_coll_, ecal_sim_hit_pass_)};
const auto& ecal_clusters{event.getCollection<ldmx::EcalCluster>(
cluster_coll_name_, cluster_pass_name_)};

if (ecal_clusters.size() == nbr_of_electrons_)
histograms_.fill("correctly_predicted_events", 1); // correct
else if (ecal_clusters.size() < nbr_of_electrons_)
histograms_.fill("correctly_predicted_events", 0); // undercounting
else if (ecal_clusters.size() > nbr_of_electrons_)
histograms_.fill("correctly_predicted_events", 2); // overcounting

std::unordered_map<int, std::pair<int, std::vector<double>>> hitInfo;
hitInfo.reserve(ecal_rec_hits.size());

double dist;
if (nbr_of_electrons_ == 2) {
// Measures distance between two electrons in the ECal scoring plane
// TODO: generalize for n electrons
std::vector<float> pos1;
std::vector<float> pos2;
bool p1 = false;
bool p2 = false;

const auto& ecal_sp_hits{
event.getCollection<ldmx::SimTrackerHit>("EcalScoringPlaneHits")};
for (const ldmx::SimTrackerHit& spHit : ecal_sp_hits) {
if (spHit.getTrackID() == 1) {
pos1 = spHit.getPosition();
p1 = true;
} else if (spHit.getTrackID() == 2) {
pos2 = spHit.getPosition();
p2 = true;
}
}
if (p1 && p2)
dist = std::sqrt(std::pow((pos1[0] - pos2[0]), 2) +
std::pow((pos1[1] - pos2[1]), 2));
}

for (const auto& hit : ecal_rec_hits) {
auto it = std::find_if(
ecal_sim_hits.begin(), ecal_sim_hits.end(),
[&hit](const auto& simHit) { return simHit.getID() == hit.getID(); });
if (it != ecal_sim_hits.end()) {
// if found a simhit matching this rechit
int ancestor = 0;
int prevAncestor = 0;
bool tagged = false;
int tag = 0;
std::vector<double> edep;
edep.resize(nbr_of_electrons_ + 1);
for (int i = 0; i < it->getNumberOfContribs(); i++) {
// for each contrib in this simhit
const auto& c = it->getContrib(i);
// get origin electron ID
ancestor = c.originID;
// store energy from this contrib at index = origin electron ID
if (ancestor <= nbr_of_electrons_) edep[ancestor] += c.edep;
if (!tagged && i != 0 && prevAncestor != ancestor) {
// if origin electron ID does not match previous origin electron ID
// this hit has contributions from several electrons, ie mixed case
tag = 0;
tagged = true;
}
prevAncestor = ancestor;
}
if (!tagged) {
// if not tagged, hit was from a single electron
tag = prevAncestor;
}
histograms_.fill("ancestors", tag);
hitInfo.insert({hit.getID(), std::make_pair(tag, edep)});
}
}

int clusteredHits = 0;

for (const auto& cl : ecal_clusters) {
// for each cluster
// total number of hits coming from electron, index = electron ID
std::vector<double> n;
n.resize(nbr_of_electrons_ + 1);
// total number of energy coming from electron, index = electron ID
std::vector<double> e;
e.resize(nbr_of_electrons_ + 1);
double eSum = 0.;
double nSum = 0.;

const auto& hitIDs = cl.getHitIDs();
for (const auto& id : hitIDs) {
// for each hit in cluster, find previously stored info
auto it = hitInfo.find(id);
if (it != hitInfo.end()) {
auto t = it->second;
auto eId = t.first; // origin electron ID (or 0 for mixed)
auto energies = t.second; // energy vector
n[eId]++; // increment number of hits coming from this electron
nSum++;

double hitESum = 0.;
for (int i = 1; i < nbr_of_electrons_ + 1; i++) {
// loop through energy vector
if (energies[i] > 0.) {
hitESum += energies[i];
// add energy from electron i in this hit to total energy from
// electron i in cluster
e[i] += energies[i];
}
}
// if mixed hit, add the total energy of this hit to mixed hit energy
// counter
if (eId == 0) e[0] += hitESum;
eSum += hitESum;

clusteredHits++;
}
}

if (eSum > 0) {
// get largest energy contribution
double eMax = *max_element(e.begin(), e.end());
// energy purity = largest contribution / all energy
histograms_.fill("energy_percentage", 100. * (eMax / eSum));
if (e[0] > 0.) histograms_.fill("mixed_hit_energy", 100. * (e[0] / eSum));

histograms_.fill("total_energy_vs_hits", eSum, cl.getHitIDs().size());
histograms_.fill("total_energy_vs_purity", eSum, 100. * (eMax / eSum));

if (nbr_of_electrons_ == 2)
histograms_.fill("distance_energy_purity", dist, 100. * (eMax / eSum));
}
if (nSum > 0) {
double nMax = *max_element(n.begin(), n.end());
histograms_.fill("same_ancestor", 100. * (nMax / nSum));
}
}

histograms_.fill("clusterless_hits", (ecal_rec_hits.size() - clusteredHits));
histograms_.fill("total_rechits_in_event", ecal_rec_hits.size());
histograms_.fill(
"clusterless_hits_percentage",
100. * (ecal_rec_hits.size() - clusteredHits) / ecal_rec_hits.size());
}

} // namespace dqm

DECLARE_ANALYZER(dqm::EcalClusterAnalyzer)
13 changes: 13 additions & 0 deletions Ecal/exampleConfigs/cluster.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from LDMX.Framework import ldmxcfg
p = ldmxcfg.Process('cluster')
import sys
p.inputFiles = sys.argv[1:]
p.outputFiles = [ 'clusters.root' ]
p.histogramFile = 'h_clusters.root'

from LDMX.Ecal.ecalClusters import *
p.sequence = [
EcalClusterProducer(),
EcalClusterAnalyzer()
]

Loading
Loading