Skip to content

Commit 5d056d7

Browse files
Add CLUE clustering (#1699)
* manually copy over CLUE clustering files from Ella's developments git switch ella-dev-clustering git rebase trunk git switch 1411-add-clue-clustering git checkout ella-dev-clustering -- Ecal/ Ella Viirola originally authored these files while doing summer research into ECal clustering at Lund University Co-authored-by: Lysarina * store the track ID of the 'origin' particle which may be more precise than the incident copied over from work done originally by Ella Viirola during Summer 2024 at Lund. Co-authored-by: Lysarina * bump ClassDef version after introducing new member * use variable that was determined earlier * move CLUE into logging, transition member variables to snake_case * dont override, that interacts poorly with ROOT's ClassDef macro * purge debug_ flag, drop debug messages to trace * Apply clang-format * parameter naming in python to snake_case * add example config to test-run clustering needed to patch python parameter naming and now it works * merge Working clusters and prefer pointers to save on copying * Apply clang-format * remove unused variables * rename WorkingCluster to IntermediateCluster for clarity * move cluster analyzer into DQM * move max origin track ID from hardcode to config parameter add a bit more documentation to the SD implementation * add ecal clustering to ecal_pn, inclusive, it_pileup samples * clang-format * fix typo in ecal_pn config * update declaration macro from Factory updates --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 3dce988 commit 5d056d7

File tree

26 files changed

+1362
-202
lines changed

26 files changed

+1362
-202
lines changed

.github/validation_samples/ecal_pn/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import LDMX.Ecal.ecal_hardcoded_conditions
3333
import LDMX.Ecal.digi as ecal_digi
3434
import LDMX.Ecal.vetos as ecal_vetos
35+
import LDMX.Ecal.ecalClusters as ecal_cluster
3536

3637
# Load the HCAL modules
3738
import LDMX.Hcal.HcalGeometry
@@ -83,6 +84,7 @@
8384
p.sequence.extend([
8485
ecal_digi.EcalDigiProducer(),
8586
ecal_digi.EcalRecProducer(),
87+
ecal_cluster.EcalClusterProducer(),
8688
ecal_veto,
8789
hcal_digi_reco,
8890
hcal_veto,
@@ -91,6 +93,7 @@
9193
trigScintTrack,
9294
count, TriggerProcessor('trigger', 8000.),
9395
dqm.PhotoNuclearDQM(),
96+
dqm.EcalClusterAnalyzer()
9497
])
9598

9699
p.sequence.extend(dqm.all_dqm)

.github/validation_samples/inclusive/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import LDMX.Ecal.ecal_hardcoded_conditions
3333
import LDMX.Ecal.digi as ecal_digi
3434
import LDMX.Ecal.vetos as ecal_vetos
35+
import LDMX.Ecal.ecalClusters as ecal_cluster
3536

3637
# Load the HCAL modules
3738
import LDMX.Hcal.HcalGeometry
@@ -83,6 +84,7 @@
8384
p.sequence.extend([
8485
ecal_digi.EcalDigiProducer(),
8586
ecal_digi.EcalRecProducer(),
87+
ecal_cluster.EcalClusterProducer(),
8688
ecalVeto,
8789
hcal_digi_reco,
8890
hcal_veto,
@@ -91,6 +93,7 @@
9193
trigScintTrack,
9294
count, TriggerProcessor('trigger', 8000.),
9395
dqm.PhotoNuclearDQM(),
96+
dqm.EcalClusterAnalyzer()
9497
])
9598

9699
p.sequence.extend(dqm.all_dqm)

.github/validation_samples/it_pileup/config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848

4949
from LDMX.Ecal import digi as eDigi
5050
from LDMX.Ecal import vetos
51+
import LDMX.Ecal.ecalClusters as ecal_cluster
5152
from LDMX.Hcal import digi as hDigi
5253

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

196197
p.sequence.extend([
197-
ecalDigi, ecalReco, ecalVeto,
198+
ecalDigi, ecalReco, ecalVeto, ecal_cluster.EcalClusterProducer(),
198199
hcal_digi_reco,
199200
hcal_veto,
200201
*ts_digis,
201202
*ts_clusters,
202203
trigScintTrack,
203204
count, TriggerProcessor('trigger', 8000.),
204205
dqm.PhotoNuclearDQM(),
206+
dqm.EcalClusterAnalyzer()
205207
])
206208

207209
p.sequence.extend(dqm_with_overlay)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* @file EcalClusterAnalyzer.h
3+
* @brief Analysis of cluster performance
4+
* @author Ella Viirola, Lund University
5+
*/
6+
7+
#ifndef DQM_ECALCLUSTERANALYZER_H
8+
#define DQM_ECALCLUSTERANALYZER_H
9+
10+
// LDMX Framework
11+
#include "Framework/Configure/Parameters.h"
12+
#include "Framework/EventProcessor.h"
13+
14+
namespace dqm {
15+
16+
/**
17+
* @class EcalClusterAnalyzer
18+
* @brief
19+
*/
20+
class EcalClusterAnalyzer : public framework::Analyzer {
21+
public:
22+
EcalClusterAnalyzer(const std::string& name, framework::Process& process)
23+
: Analyzer(name, process) {}
24+
~EcalClusterAnalyzer() override = default;
25+
void configure(framework::config::Parameters& ps) override;
26+
void analyze(const framework::Event& event) override;
27+
28+
private:
29+
int nbr_of_electrons_;
30+
31+
// Collection Name for SimHits
32+
std::string ecal_sim_hit_coll_;
33+
34+
// Pass Name for SimHits
35+
std::string ecal_sim_hit_pass_;
36+
37+
// Collection Name for RecHits
38+
std::string rec_hit_coll_name_;
39+
40+
// Pass Name for RecHits
41+
std::string rec_hit_pass_name_;
42+
43+
// Collection Name for clusters
44+
std::string cluster_coll_name_;
45+
46+
// Pass Name for clusters
47+
std::string cluster_pass_name_;
48+
};
49+
50+
} // namespace dqm
51+
52+
#endif

DQM/python/dqm.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,38 @@ def __init__(self, name='SampleValidation') :
804804
self.build1DHistogram("pdgid_hardbremdaughters", "ID of hard brem daughters", 20, 0, 20)
805805
self.build1DHistogram("startZ_hardbremdaughters", "Start z position of hard brem daughters [mm]", 200, -1000, 1000)
806806

807+
class EcalClusterAnalyzer(ldmxcfg.Analyzer) :
808+
"""Analyze clustering"""
809+
810+
def __init__(self,name='EcalClusterAnalyzer') :
811+
super().__init__(name, "dqm::EcalClusterAnalyzer", 'DQM')
812+
813+
self.nbr_of_electrons = 2
814+
815+
self.ecal_sim_hit_coll = "EcalSimHits"
816+
self.ecal_sim_hit_pass = "" #use whatever pass is available
817+
818+
# Pass name for ecal digis and rec hits
819+
self.rec_hit_coll_name = 'EcalRecHits'
820+
self.rec_hit_pass_name = ''
821+
822+
self.cluster_coll_name = 'ecalClusters'
823+
self.cluster_pass_name = ''
824+
825+
# Need to mod for more than two electrons
826+
self.build1DHistogram("ancestors", "Ancestors of particles", 4, 0, 3)
827+
828+
self.build1DHistogram("same_ancestor", "Percentage of hits in cluster coming from the electron that produced most hits", 21, 0, 105)
829+
self.build1DHistogram("energy_percentage", "Percentage of energy in cluster coming from the electron that produced most of energy", 21, 0, 105)
830+
self.build1DHistogram("mixed_hit_energy", "Percentage of total energy coming from hits with energy contributions from more than one electron", 21, 0, 105)
831+
self.build1DHistogram("clusterless_hits", "Number of hits not in a cluster", 10, 0, 200)
832+
self.build1DHistogram("clusterless_hits_percentage", "Percentage of hits not in a cluster", 21, 0, 105)
833+
self.build1DHistogram("total_rechits_in_event", "Rechits per event", 20, 0, 500)
834+
self.build1DHistogram("correctly_predicted_events", "Correctly predicted events", 3, 0, 3)
835+
836+
self.build2DHistogram("total_energy_vs_hits", "Total energy (edep)", 30, 0, 150, "Hits in cluster", 20, 0, 200)
837+
self.build2DHistogram("total_energy_vs_purity", "Total energy (edep)", 30, 0, 150, "Energy purity %", 21, 0, 105)
838+
self.build2DHistogram("distance_energy_purity", "Distance in xy-plane", 20, 0, 220, "Energy purity %", 21, 0, 105)
807839

808840
ecal_dqm = [
809841
EcalDigiVerify(),
@@ -855,4 +887,4 @@ def __init__(self, name='SampleValidation') :
855887
]
856888

857889

858-
all_dqm = ecal_dqm + hcal_dqm + trigScint_dqm + trigger_dqm
890+
all_dqm = ecal_dqm + hcal_dqm + trigScint_dqm + trigger_dqm
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/**
2+
* @file EcalClusterAnalyzer.cxx
3+
* @brief Analysis of cluster performance
4+
* @author Ella Viirola, Lund University
5+
*/
6+
7+
#include "DQM/EcalClusterAnalyzer.h"
8+
9+
#include <algorithm>
10+
#include <fstream>
11+
#include <iostream>
12+
13+
#include "DetDescr/SimSpecialID.h"
14+
#include "Ecal/Event/EcalCluster.h"
15+
#include "Ecal/Event/EcalHit.h"
16+
#include "SimCore/Event/SimCalorimeterHit.h"
17+
#include "SimCore/Event/SimTrackerHit.h"
18+
19+
namespace dqm {
20+
21+
void EcalClusterAnalyzer::configure(framework::config::Parameters& ps) {
22+
nbr_of_electrons_ = ps.getParameter<int>("nbr_of_electrons");
23+
24+
ecal_sim_hit_coll_ = ps.getParameter<std::string>("ecal_sim_hit_coll");
25+
ecal_sim_hit_pass_ = ps.getParameter<std::string>("ecal_sim_hit_pass");
26+
27+
rec_hit_coll_name_ = ps.getParameter<std::string>("rec_hit_coll_name");
28+
rec_hit_pass_name_ = ps.getParameter<std::string>("rec_hit_pass_name");
29+
30+
cluster_coll_name_ = ps.getParameter<std::string>("cluster_coll_name");
31+
cluster_pass_name_ = ps.getParameter<std::string>("cluster_pass_name");
32+
return;
33+
}
34+
35+
void EcalClusterAnalyzer::analyze(const framework::Event& event) {
36+
const auto& ecal_rec_hits{event.getCollection<ldmx::EcalHit>(
37+
rec_hit_coll_name_, rec_hit_pass_name_)};
38+
const auto& ecal_sim_hits{event.getCollection<ldmx::SimCalorimeterHit>(
39+
ecal_sim_hit_coll_, ecal_sim_hit_pass_)};
40+
const auto& ecal_clusters{event.getCollection<ldmx::EcalCluster>(
41+
cluster_coll_name_, cluster_pass_name_)};
42+
43+
if (ecal_clusters.size() == nbr_of_electrons_)
44+
histograms_.fill("correctly_predicted_events", 1); // correct
45+
else if (ecal_clusters.size() < nbr_of_electrons_)
46+
histograms_.fill("correctly_predicted_events", 0); // undercounting
47+
else if (ecal_clusters.size() > nbr_of_electrons_)
48+
histograms_.fill("correctly_predicted_events", 2); // overcounting
49+
50+
std::unordered_map<int, std::pair<int, std::vector<double>>> hitInfo;
51+
hitInfo.reserve(ecal_rec_hits.size());
52+
53+
double dist;
54+
if (nbr_of_electrons_ == 2) {
55+
// Measures distance between two electrons in the ECal scoring plane
56+
// TODO: generalize for n electrons
57+
std::vector<float> pos1;
58+
std::vector<float> pos2;
59+
bool p1 = false;
60+
bool p2 = false;
61+
62+
const auto& ecal_sp_hits{
63+
event.getCollection<ldmx::SimTrackerHit>("EcalScoringPlaneHits")};
64+
for (const ldmx::SimTrackerHit& spHit : ecal_sp_hits) {
65+
if (spHit.getTrackID() == 1) {
66+
pos1 = spHit.getPosition();
67+
p1 = true;
68+
} else if (spHit.getTrackID() == 2) {
69+
pos2 = spHit.getPosition();
70+
p2 = true;
71+
}
72+
}
73+
if (p1 && p2)
74+
dist = std::sqrt(std::pow((pos1[0] - pos2[0]), 2) +
75+
std::pow((pos1[1] - pos2[1]), 2));
76+
}
77+
78+
for (const auto& hit : ecal_rec_hits) {
79+
auto it = std::find_if(
80+
ecal_sim_hits.begin(), ecal_sim_hits.end(),
81+
[&hit](const auto& simHit) { return simHit.getID() == hit.getID(); });
82+
if (it != ecal_sim_hits.end()) {
83+
// if found a simhit matching this rechit
84+
int ancestor = 0;
85+
int prevAncestor = 0;
86+
bool tagged = false;
87+
int tag = 0;
88+
std::vector<double> edep;
89+
edep.resize(nbr_of_electrons_ + 1);
90+
for (int i = 0; i < it->getNumberOfContribs(); i++) {
91+
// for each contrib in this simhit
92+
const auto& c = it->getContrib(i);
93+
// get origin electron ID
94+
ancestor = c.originID;
95+
// store energy from this contrib at index = origin electron ID
96+
if (ancestor <= nbr_of_electrons_) edep[ancestor] += c.edep;
97+
if (!tagged && i != 0 && prevAncestor != ancestor) {
98+
// if origin electron ID does not match previous origin electron ID
99+
// this hit has contributions from several electrons, ie mixed case
100+
tag = 0;
101+
tagged = true;
102+
}
103+
prevAncestor = ancestor;
104+
}
105+
if (!tagged) {
106+
// if not tagged, hit was from a single electron
107+
tag = prevAncestor;
108+
}
109+
histograms_.fill("ancestors", tag);
110+
hitInfo.insert({hit.getID(), std::make_pair(tag, edep)});
111+
}
112+
}
113+
114+
int clusteredHits = 0;
115+
116+
for (const auto& cl : ecal_clusters) {
117+
// for each cluster
118+
// total number of hits coming from electron, index = electron ID
119+
std::vector<double> n;
120+
n.resize(nbr_of_electrons_ + 1);
121+
// total number of energy coming from electron, index = electron ID
122+
std::vector<double> e;
123+
e.resize(nbr_of_electrons_ + 1);
124+
double eSum = 0.;
125+
double nSum = 0.;
126+
127+
const auto& hitIDs = cl.getHitIDs();
128+
for (const auto& id : hitIDs) {
129+
// for each hit in cluster, find previously stored info
130+
auto it = hitInfo.find(id);
131+
if (it != hitInfo.end()) {
132+
auto t = it->second;
133+
auto eId = t.first; // origin electron ID (or 0 for mixed)
134+
auto energies = t.second; // energy vector
135+
n[eId]++; // increment number of hits coming from this electron
136+
nSum++;
137+
138+
double hitESum = 0.;
139+
for (int i = 1; i < nbr_of_electrons_ + 1; i++) {
140+
// loop through energy vector
141+
if (energies[i] > 0.) {
142+
hitESum += energies[i];
143+
// add energy from electron i in this hit to total energy from
144+
// electron i in cluster
145+
e[i] += energies[i];
146+
}
147+
}
148+
// if mixed hit, add the total energy of this hit to mixed hit energy
149+
// counter
150+
if (eId == 0) e[0] += hitESum;
151+
eSum += hitESum;
152+
153+
clusteredHits++;
154+
}
155+
}
156+
157+
if (eSum > 0) {
158+
// get largest energy contribution
159+
double eMax = *max_element(e.begin(), e.end());
160+
// energy purity = largest contribution / all energy
161+
histograms_.fill("energy_percentage", 100. * (eMax / eSum));
162+
if (e[0] > 0.) histograms_.fill("mixed_hit_energy", 100. * (e[0] / eSum));
163+
164+
histograms_.fill("total_energy_vs_hits", eSum, cl.getHitIDs().size());
165+
histograms_.fill("total_energy_vs_purity", eSum, 100. * (eMax / eSum));
166+
167+
if (nbr_of_electrons_ == 2)
168+
histograms_.fill("distance_energy_purity", dist, 100. * (eMax / eSum));
169+
}
170+
if (nSum > 0) {
171+
double nMax = *max_element(n.begin(), n.end());
172+
histograms_.fill("same_ancestor", 100. * (nMax / nSum));
173+
}
174+
}
175+
176+
histograms_.fill("clusterless_hits", (ecal_rec_hits.size() - clusteredHits));
177+
histograms_.fill("total_rechits_in_event", ecal_rec_hits.size());
178+
histograms_.fill(
179+
"clusterless_hits_percentage",
180+
100. * (ecal_rec_hits.size() - clusteredHits) / ecal_rec_hits.size());
181+
}
182+
183+
} // namespace dqm
184+
185+
DECLARE_ANALYZER(dqm::EcalClusterAnalyzer)

Ecal/exampleConfigs/cluster.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from LDMX.Framework import ldmxcfg
2+
p = ldmxcfg.Process('cluster')
3+
import sys
4+
p.inputFiles = sys.argv[1:]
5+
p.outputFiles = [ 'clusters.root' ]
6+
p.histogramFile = 'h_clusters.root'
7+
8+
from LDMX.Ecal.ecalClusters import *
9+
p.sequence = [
10+
EcalClusterProducer(),
11+
EcalClusterAnalyzer()
12+
]
13+

0 commit comments

Comments
 (0)