LDMX Software
ParticleFlow.cxx
2
3#include <algorithm>
4#include <array>
5#include <cmath>
6#include <map>
7#include <set>
8#include <tuple>
9#include <vector>
10
12#include "Framework/Exception/Exception.h"
14
15namespace recon {
16
18 // I/O
19 input_ecal_coll_name_ = ps.get<std::string>("input_ecal_coll_name");
20 input_hcal_coll_name_ = ps.get<std::string>("input_hcal_coll_name");
21 input_track_coll_name_ = ps.get<std::string>("input_track_coll_name");
22 output_coll_name_ = ps.get<std::string>("output_coll_name");
23 input_ecal_passname_ = ps.get<std::string>("input_ecal_passname");
24 input_hcal_passname_ = ps.get<std::string>("input_hcal_passname");
25 input_tracks_passname_ = ps.get<std::string>("input_tracks_passname");
26 truth_sp_coll_name_ = ps.get<std::string>("truth_sp_coll_name");
27 truth_sp_passname_ = ps.get<std::string>("truth_sp_passname");
28
29 // Algorithm configuration
30 single_particle_ = ps.get<bool>("single_particle");
31 use_existing_ecal_clusters_ = ps.get<bool>("use_existing_ecal_clusters");
32 input_mass_coll_name_ = ps.get<std::string>("input_mass_coll_name");
33 input_mass_passname_ = ps.get<std::string>("input_mass_passname");
34 link_max_dist_ = ps.get<double>("link_max_dist");
35 electron_ep_min_ = ps.get<double>("electron_ep_min");
36 link_min_rms_ = ps.get<double>("link_min_rms");
37 excess_ep_ = ps.get<double>("excess_ep");
38 proton_min_mass2_ = ps.get<double>("proton_min_mass2");
39 kaon_min_mass2_ = ps.get<double>("kaon_min_mass2");
40 proton_max_p_ = ps.get<double>("proton_max_p");
41 kaon_max_p_ = ps.get<double>("kaon_max_p");
42 recovery_radius_ = ps.get<double>("recovery_radius");
43 hcal_merge_angle_ = ps.get<double>("hcal_merge_angle");
44 hcal_track_angle_ = ps.get<double>("hcal_track_angle");
45 hadron_ecal_radius_ = ps.get<double>("hadron_ecal_radius");
46 hadron_absorb_ep_ = ps.get<double>("hadron_absorb_ep");
47 min_neutral_e_ = ps.get<double>("min_neutral_e");
48 ecal_front_z_ = ps.get<double>("ecal_front_z");
49 electron_max_depth_ = ps.get<double>("electron_max_depth");
50 electron_depth_slope_ = ps.get<double>("electron_depth_slope");
51 recovery_stop_ep_ = ps.get<double>("recovery_stop_ep");
52 ecal_link_radius_ = ps.get<double>("ecal_link_radius");
53 ecal_link_ratio_ = ps.get<double>("ecal_link_ratio");
54 ecal_link_min_layers_ = ps.get<int>("ecal_link_min_layers");
55 photon_keep_min_e_ = ps.get<double>("photon_keep_min_e");
56 photon_keep_min_dist_ = ps.get<double>("photon_keep_min_dist");
57 photon_keep_max_zstart_ = ps.get<double>("photon_keep_max_zstart");
58 dedx_min_hits_ = ps.get<int>("dedx_min_hits");
59
60 // energy correction tables, empty means no correction
61 ecal_corr_energies_ = ps.get<std::vector<double>>("ecal_corr_energies", {});
62 ecal_corr_factors_ = ps.get<std::vector<double>>("ecal_corr_factors", {});
63 hcal_corr_energies_ = ps.get<std::vector<double>>("hcal_corr_energies", {});
64 hcal_corr_factors_ = ps.get<std::vector<double>>("hcal_corr_factors", {});
65 for (const auto& [x, y] :
66 {std::pair{&ecal_corr_energies_, &ecal_corr_factors_},
67 std::pair{&hcal_corr_energies_, &hcal_corr_factors_}}) {
68 if (x->size() != y->size() || !std::is_sorted(x->begin(), x->end())) {
69 EXCEPTION_RAISE("InvalidArg",
70 "Correction energies must be sorted and match factors");
71 }
72 }
73}
74
75// linear interpolation, clamped to the end factors
76double ParticleFlow::correction(const std::vector<double>& energies,
77 const std::vector<double>& factors,
78 double energy) {
79 if (energies.empty()) return 1.;
80 if (energy <= energies.front()) return factors.front();
81 if (energy >= energies.back()) return factors.back();
82 auto hi = std::upper_bound(energies.begin(), energies.end(), energy);
83 size_t i = hi - energies.begin();
84 double t = (energy - energies[i - 1]) / (energies[i] - energies[i - 1]);
85 return factors[i - 1] + t * (factors[i] - factors[i - 1]);
86}
87
88// produce candidate track info
89void ParticleFlow::fillCandTrack(
90 ldmx::PFCandidate& cand, const ldmx::Track& tk,
91 const std::vector<ldmx::SimTrackerHit>& sp_hits) {
92 auto ts = tk.getTrackState(ldmx::AtECAL);
93 if (!ts || ts->pos_.size() < 3 || ts->mom_.size() < 3) return;
94 const std::vector<double>& xyz = ts->pos_;
95 const std::vector<double>& pxyz = ts->mom_;
96 float ecal_z = 248;
97 float ecal_x =
98 xyz[0] + pxyz[0] / pxyz[2] * (ecal_z - xyz[2]); // project onto ecal face
99 float ecal_y = xyz[1] + pxyz[1] / pxyz[2] * (ecal_z - xyz[2]);
100 cand.setEcalPositionXYZ(ecal_x, ecal_y, ecal_z);
101 cand.setTrackPxPyPz(pxyz[0], pxyz[1], pxyz[2]);
102 cand.setPID(cand.getPID() | 1); // OR with 001
103
104 // truth from the matched Ecal SP hit closest in momentum
105 const ldmx::SimTrackerHit* best = nullptr;
106 double best_dp = 9e9;
107 for (const auto& sp : sp_hits) {
108 if (sp.getTrackID() != tk.getTrackID()) continue;
109 if (fabs(240 - sp.getPosition()[2]) > 0.1) continue;
110 auto sp_p = sp.getMomentum();
111 double dp =
112 std::hypot(sp_p[0] - pxyz[0], sp_p[1] - pxyz[1], sp_p[2] - pxyz[2]);
113 if (dp < best_dp) {
114 best_dp = dp;
115 best = &sp;
116 }
117 }
118 if (!best) return;
119 std::vector<float> t_xyz = best->getPosition();
120 std::vector<double> t_pxyz = best->getMomentum();
121 cand.setTruthEcalXYZ(t_xyz[0] + t_pxyz[0] / t_pxyz[2] * (ecal_z - t_xyz[2]),
122 t_xyz[1] + t_pxyz[1] / t_pxyz[2] * (ecal_z - t_xyz[2]),
123 ecal_z);
124 cand.setTruthPxPyPz(t_pxyz[0], t_pxyz[1], t_pxyz[2]);
125 float m2 = pow(best->getEnergy(), 2) - pow(t_pxyz[0], 2) - pow(t_pxyz[1], 2) -
126 pow(t_pxyz[2], 2);
127 if (m2 < 0) m2 = 0;
128 cand.setTruthMass(sqrt(m2));
129 cand.setTruthEnergy(best->getEnergy());
130 cand.setTruthPdgId(best->getPdgID());
131 cand.setTruthTrackId(best->getTrackID());
132}
133// produce candidate ECal info
134void ParticleFlow::fillCandEMCalo(ldmx::PFCandidate& cand,
135 const ldmx::CaloCluster& em) {
136 float energy = em.getEnergy();
137 float corr = correction(ecal_corr_energies_, ecal_corr_factors_, energy);
138 cand.setEcalEnergy(energy * corr);
139 cand.setEcalRawEnergy(energy);
140 cand.setEcalClusterXYZ(em.getCentroidX(), em.getCentroidY(),
141 em.getCentroidZ());
142 cand.setEcalClusterEXYZ(em.getRMSX(), em.getRMSY(), em.getRMSZ());
143 cand.setEcalClusterDXDZ(em.getDXDZ());
144 cand.setEcalClusterDYDZ(em.getDYDZ());
145 cand.setEcalClusterEDXDZ(em.getEDXDZ());
146 cand.setEcalClusterEDYDZ(em.getEDYDZ());
147 cand.setEcalDepth(ecalDepth(em));
148 cand.clearEcalHits();
149 cand.addEcalHits(em.getHitIDs(), em.getHitE());
150 cand.setPID(cand.getPID() | 2); // OR with 010
151}
152// produce candidate HCal info
153void ParticleFlow::fillCandHadCalo(ldmx::PFCandidate& cand,
154 const ldmx::CaloCluster& had) {
155 float energy = had.getEnergy();
156 float corr = correction(hcal_corr_energies_, hcal_corr_factors_, energy);
157 cand.setHcalEnergy(energy * corr);
158 cand.setHcalRawEnergy(energy);
159 cand.clearHcalHits();
160 cand.addHcalHits(had.getHitIDs(), had.getHitE());
161 cand.setHcalClusterXYZ(had.getCentroidX(), had.getCentroidY(),
162 had.getCentroidZ());
163 cand.setHcalClusterEXYZ(had.getRMSX(), had.getRMSY(), had.getRMSZ());
164 cand.setHcalClusterDXDZ(had.getDXDZ());
165 cand.setHcalClusterDYDZ(had.getDYDZ());
166 cand.setHcalClusterEDXDZ(had.getEDXDZ());
167 cand.setHcalClusterEDYDZ(had.getEDYDZ());
168 cand.setPID(cand.getPID() | 4); // OR with 100
169}
170
171// RMS-normalized distance of the track to the cluster centroid
172double ParticleFlow::trackClusterDist(const std::vector<double>& xyz,
173 const std::vector<double>& pxyz,
174 const ldmx::CaloCluster& cl) const {
175 double z = cl.getCentroidZ();
176 double x = xyz[0] + pxyz[0] / pxyz[2] * (z - xyz[2]);
177 double y = xyz[1] + pxyz[1] / pxyz[2] * (z - xyz[2]);
178 // RMS floor covers scattering of soft tracks
179 return std::hypot(
180 (x - cl.getCentroidX()) / std::max(link_min_rms_, cl.getRMSX()),
181 (y - cl.getCentroidY()) / std::max(link_min_rms_, cl.getRMSY()));
182}
183
184// Ecal to Ecal linking: merge satellite clusters into their parent shower.
185// A satellite is short (few layers) or a small fraction of a nearby long
186// cluster. Clusters a track points to are never absorbed, and soft clusters
187// that are isolated and start early stay separate photons.
188std::vector<ldmx::CaloCluster> ParticleFlow::linkEcalClusters(
189 const std::vector<ldmx::CaloCluster>& clusters,
190 const std::vector<ldmx::Track>& tracks) const {
191 int n = clusters.size();
192 std::vector<int> nlay(n), parent(n, -1);
193 std::vector<double> zstart(n);
194 std::vector<bool> tk_linked(n, false);
195 for (int i = 0; i < n; i++) {
196 std::set<int> layers;
197 double zmin = 9e9;
198 for (float z : clusters[i].getHitZ()) {
199 layers.insert(std::lround(z));
200 zmin = std::min<double>(zmin, z);
201 }
202 nlay[i] = layers.size();
203 zstart[i] = layers.empty() ? clusters[i].getCentroidZ() : zmin;
204 for (const auto& tk : tracks) {
205 auto ts = tk.getTrackState(ldmx::AtECAL);
206 if (!ts || ts->mom_.size() < 3 || ts->mom_[2] <= 0) continue;
207 if (trackClusterDist(ts->pos_, ts->mom_, clusters[i]) < link_max_dist_) {
208 tk_linked[i] = true;
209 break;
210 }
211 }
212 }
213 for (int s = 0; s < n; s++) {
214 if (tk_linked[s]) continue;
215 double es = clusters[s].getEnergy();
216 int best = -1;
217 double best_d = ecal_link_radius_;
218 double nearest = 9e9; // to any more energetic cluster
219 for (int p = 0; p < n; p++) {
220 if (p == s || clusters[p].getEnergy() <= es) continue;
221 double d =
222 std::hypot(clusters[p].getCentroidX() - clusters[s].getCentroidX(),
223 clusters[p].getCentroidY() - clusters[s].getCentroidY());
224 nearest = std::min(nearest, d);
225 bool satellite = nlay[s] < ecal_link_min_layers_ ||
226 es < ecal_link_ratio_ * clusters[p].getEnergy();
227 if (nlay[p] >= ecal_link_min_layers_ && satellite && d < best_d) {
228 best_d = d;
229 best = p;
230 }
231 }
232 // isolated, early showers look like photons from the target
233 bool photon_like = es >= photon_keep_min_e_ &&
234 nearest > photon_keep_min_dist_ &&
235 zstart[s] < photon_keep_max_zstart_;
236 if (best >= 0 && !photon_like) parent[s] = best;
237 }
238
239 // follow chains to the final parent and merge
240 auto root = [&parent](int i) {
241 while (parent[i] >= 0) i = parent[i];
242 return i;
243 };
244 std::map<int, std::vector<int>> groups;
245 for (int i = 0; i < n; i++) groups[root(i)].push_back(i);
246 std::vector<ldmx::CaloCluster> merged;
247 for (const auto& [r, members] : groups) {
248 merged.push_back(mergeClusters(clusters, r, members));
249 }
250 std::sort(merged.begin(), merged.end(), [](const auto& a, const auto& b) {
251 return a.getEnergy() > b.getEnergy();
252 });
253 return merged;
254}
255
256// sum of member clusters: energy, energy-weighted centroid, all hits;
257// shape info is kept from the root cluster
258ldmx::CaloCluster ParticleFlow::mergeClusters(
259 const std::vector<ldmx::CaloCluster>& clusters, int root,
260 const std::vector<int>& members) {
261 if (members.size() == 1) return clusters[root];
262 ldmx::CaloCluster cl = clusters[root];
263 double e = 0, x = 0, y = 0, z = 0;
264 std::vector<unsigned int> ids;
265 std::vector<float> hx, hy, hz, he;
266 for (int m : members) {
267 const auto& c = clusters[m];
268 e += c.getEnergy();
269 x += c.getEnergy() * c.getCentroidX();
270 y += c.getEnergy() * c.getCentroidY();
271 z += c.getEnergy() * c.getCentroidZ();
272 ids.insert(ids.end(), c.getHitIDs().begin(), c.getHitIDs().end());
273 hx.insert(hx.end(), c.getHitX().begin(), c.getHitX().end());
274 hy.insert(hy.end(), c.getHitY().begin(), c.getHitY().end());
275 hz.insert(hz.end(), c.getHitZ().begin(), c.getHitZ().end());
276 he.insert(he.end(), c.getHitE().begin(), c.getHitE().end());
277 }
278 cl.setEnergy(e);
279 if (e > 0) cl.setCentroidXYZ(x / e, y / e, z / e);
280 cl.setNHits(ids.size());
281 cl.setIDs(ids);
282 cl.setHitValsX(hx);
283 cl.setHitValsY(hy);
284 cl.setHitValsZ(hz);
285 cl.setHitValsE(he);
286 return cl;
287}
288
289// Hcal to Hcal linking: hadron showers fragment into many clusters, merge
290// those within a cone seen from the target around the most energetic one
291std::vector<ldmx::CaloCluster> ParticleFlow::linkHcalClusters(
292 const std::vector<ldmx::CaloCluster>& clusters) const {
293 int n = clusters.size();
294 std::vector<int> order(n);
295 for (int i = 0; i < n; i++) order[i] = i;
296 std::sort(order.begin(), order.end(), [&clusters](int a, int b) {
297 return clusters[a].getEnergy() > clusters[b].getEnergy();
298 });
299 auto dir = [&clusters](int i) {
300 const auto& c = clusters[i];
301 double r = std::hypot(c.getCentroidX(), c.getCentroidY(), c.getCentroidZ());
302 return std::array<double, 3>{c.getCentroidX() / r, c.getCentroidY() / r,
303 c.getCentroidZ() / r};
304 };
305 double cos_max = std::cos(hcal_merge_angle_ * M_PI / 180.);
306 std::vector<bool> used(n, false);
307 std::vector<ldmx::CaloCluster> merged;
308 for (int s : order) {
309 if (used[s]) continue;
310 auto ds = dir(s);
311 std::vector<int> members;
312 for (int o : order) {
313 if (used[o]) continue;
314 auto d = dir(o);
315 if (ds[0] * d[0] + ds[1] * d[1] + ds[2] * d[2] >= cos_max) {
316 used[o] = true;
317 members.push_back(o);
318 }
319 }
320 merged.push_back(mergeClusters(clusters, s, members));
321 }
322 return merged;
323}
324
325// free Hcal cluster closest in angle to the track direction, seen from the
326// track at the Ecal face; -1 if none within hcal_track_angle
327int ParticleFlow::linkTrackHcal(const ldmx::Track& tk,
328 const std::vector<ldmx::CaloCluster>& clusters,
329 const std::vector<bool>& used) const {
330 auto ts = tk.getTrackState(ldmx::AtECAL);
331 if (hcal_track_angle_ <= 0 || !ts || ts->mom_.size() < 3) return -1;
332 const auto& pos = ts->pos_;
333 const auto& mom = ts->mom_;
334 double pm = std::hypot(mom[0], mom[1], mom[2]);
335 int best = -1;
336 double best_cos = std::cos(hcal_track_angle_ * M_PI / 180.);
337 for (int j = 0; j < static_cast<int>(clusters.size()); j++) {
338 if (used[j]) continue;
339 double dx = clusters[j].getCentroidX() - pos[0];
340 double dy = clusters[j].getCentroidY() - pos[1];
341 double dz = clusters[j].getCentroidZ() - pos[2];
342 double c = (dx * mom[0] + dy * mom[1] + dz * mom[2]) /
343 (pm * std::hypot(dx, dy, dz));
344 if (c > best_cos) {
345 best_cos = c;
346 best = j;
347 }
348 }
349 return best;
350}
351
352// energy subtraction: a charged hadron absorbs free Ecal clusters near its
353// track and free Hcal clusters in its cone, closest first, while the
354// calibrated calorimeter energy stays below hadron_absorb_ep * p; what is
355// left stays neutral
356void ParticleFlow::absorbHadronShower(
357 ldmx::PFCandidate& cand, const ldmx::Track& tk,
358 const std::vector<ldmx::CaloCluster>& ecal, std::vector<bool>& ecal_used,
359 const std::vector<ldmx::CaloCluster>& hcal,
360 std::vector<bool>& hcal_used) const {
361 auto ts = tk.getTrackState(ldmx::AtECAL);
362 if (hadron_absorb_ep_ <= 0 || !ts || ts->mom_.size() < 3 || ts->mom_[2] <= 0)
363 return;
364 const auto& pos = ts->pos_;
365 const auto& mom = ts->mom_;
366 double pm = std::hypot(mom[0], mom[1], mom[2]);
367 double budget = hadron_absorb_ep_ * pm;
368 double calo = cand.getEcalEnergy() + cand.getHcalEnergy();
369
370 // (distance key, is_hcal, index), Ecal in mm and Hcal in angle scaled to mm
371 std::vector<std::tuple<double, bool, int>> near;
372 for (int j = 0; j < static_cast<int>(ecal.size()); j++) {
373 if (ecal_used[j]) continue;
374 double z = ecal[j].getCentroidZ();
375 double d = std::hypot(
376 pos[0] + mom[0] / mom[2] * (z - pos[2]) - ecal[j].getCentroidX(),
377 pos[1] + mom[1] / mom[2] * (z - pos[2]) - ecal[j].getCentroidY());
378 if (d < hadron_ecal_radius_) near.emplace_back(d, false, j);
379 }
380 double cos_max = std::cos(hcal_track_angle_ * M_PI / 180.);
381 for (int j = 0; j < static_cast<int>(hcal.size()); j++) {
382 if (hcal_used[j]) continue;
383 double dx = hcal[j].getCentroidX() - pos[0];
384 double dy = hcal[j].getCentroidY() - pos[1];
385 double dz = hcal[j].getCentroidZ() - pos[2];
386 double cs = (dx * mom[0] + dy * mom[1] + dz * mom[2]) /
387 (pm * std::hypot(dx, dy, dz));
388 if (cs > cos_max) {
389 // rank Hcal after nearby Ecal pieces, by angle
390 double ang = std::acos(std::min(1., cs)) * 180. / M_PI;
391 near.emplace_back(hadron_ecal_radius_ + ang, true, j);
392 }
393 }
394 std::sort(near.begin(), near.end());
395 for (const auto& [d, is_hcal, j] : near) {
396 const auto& cl = is_hcal ? hcal[j] : ecal[j];
397 double raw = cl.getEnergy();
398 double e =
399 raw * (is_hcal
400 ? correction(hcal_corr_energies_, hcal_corr_factors_, raw)
401 : correction(ecal_corr_energies_, ecal_corr_factors_, raw));
402 if (calo + e > budget) continue;
403 calo += e;
404 if (is_hcal) {
405 cand.setHcalEnergy(cand.getHcalEnergy() + e);
406 cand.setHcalRawEnergy(cand.getHcalRawEnergy() + raw);
407 cand.addHcalHits(cl.getHitIDs(), cl.getHitE());
408 cand.setPID(cand.getPID() | 4);
409 hcal_used[j] = true;
410 } else {
411 cand.setEcalEnergy(cand.getEcalEnergy() + e);
412 cand.setEcalRawEnergy(cand.getEcalRawEnergy() + raw);
413 cand.addEcalHits(cl.getHitIDs(), cl.getHitE());
414 ecal_used[j] = true;
415 }
416 }
417}
418
419// energy-weighted RMS distance of the cluster hits from the straight track
420// line through the Ecal; EM showers are narrow, hadronic ones wide [mm]
421double ParticleFlow::ecalWidth(const ldmx::CaloCluster& cl,
422 const ldmx::Track& tk) const {
423 auto ts = tk.getTrackState(ldmx::AtECAL);
424 if (!ts || ts->mom_.size() < 3 || ts->mom_[2] <= 0) return 0.;
425 const auto& pos = ts->pos_;
426 const auto& mom = ts->mom_;
427 const auto& hx = cl.getHitX();
428 const auto& hy = cl.getHitY();
429 const auto& hz = cl.getHitZ();
430 const auto& he = cl.getHitE();
431 double e = 0, er2 = 0;
432 for (size_t k = 0; k < he.size() && k < hz.size(); k++) {
433 double x = pos[0] + mom[0] / mom[2] * (hz[k] - pos[2]);
434 double y = pos[1] + mom[1] / mom[2] * (hz[k] - pos[2]);
435 e += he[k];
436 er2 += he[k] * (std::pow(hx[k] - x, 2) + std::pow(hy[k] - y, 2));
437 }
438 return e > 0 ? std::sqrt(er2 / e) : 0.;
439}
440
441// energy-weighted mean hit depth behind the Ecal front [mm]
442double ParticleFlow::ecalDepth(const ldmx::CaloCluster& cl) const {
443 const auto& hz = cl.getHitZ();
444 const auto& he = cl.getHitE();
445 double e = 0, ez = 0;
446 for (size_t k = 0; k < hz.size() && k < he.size(); k++) {
447 e += he[k];
448 ez += he[k] * hz[k];
449 }
450 return (e > 0 ? ez / e : cl.getCentroidZ()) - ecal_front_z_;
451}
452
453// a linked track with low E/p absorbs nearby unlinked clusters, closest
454// first, to recover electron showers split by clustering
455void ParticleFlow::recoverEcal(ldmx::PFCandidate& cand, const ldmx::Track& tk,
456 const std::vector<ldmx::CaloCluster>& clusters,
457 std::vector<bool>& used) const {
458 auto ts = tk.getTrackState(ldmx::AtECAL);
459 if (recovery_radius_ <= 0 || !ts || ts->mom_.size() < 3 || ts->mom_[2] <= 0)
460 return;
461 const auto& xyz = ts->pos_;
462 const auto& pxyz = ts->mom_;
463 double trk_p = std::hypot(pxyz[0], pxyz[1], pxyz[2]);
464 if (cand.getEcalEnergy() >= electron_ep_min_ * trk_p) return;
465 std::vector<std::pair<double, int>> near;
466 for (int j = 0; j < static_cast<int>(clusters.size()); j++) {
467 if (used[j]) continue;
468 double z = clusters[j].getCentroidZ();
469 double d = std::hypot(
470 xyz[0] + pxyz[0] / pxyz[2] * (z - xyz[2]) - clusters[j].getCentroidX(),
471 xyz[1] + pxyz[1] / pxyz[2] * (z - xyz[2]) - clusters[j].getCentroidY());
472 if (d < recovery_radius_) near.emplace_back(d, j);
473 }
474 std::sort(near.begin(), near.end());
475 for (const auto& [d, j] : near) {
476 if (cand.getEcalEnergy() >= recovery_stop_ep_ * trk_p) break;
477 double e = clusters[j].getEnergy();
478 double corr = correction(ecal_corr_energies_, ecal_corr_factors_, e);
479 cand.setEcalEnergy(cand.getEcalEnergy() + e * corr);
480 cand.setEcalRawEnergy(cand.getEcalRawEnergy() + e);
481 cand.addEcalHits(clusters[j].getHitIDs(), clusters[j].getHitE());
482 used[j] = true;
483 }
484}
485
486// charged ID, types as in CMS PF: electron from E/p and shower depth, else
487// charged hadron (pdg 211 by convention); dE/dx at low p only sets the
488// hadron mass hypothesis (p, K or pi), used for the energy
489void ParticleFlow::identifyCharged(ldmx::PFCandidate& cand) const {
490 auto p = cand.getTrackPxPyPz();
491 double trk_p = std::hypot(p[0], p[1], p[2]);
492 double ep = trk_p > 0 ? cand.getEcalEnergy() / trk_p : 0;
493 int q = cand.getCharge() >= 0 ? 1 : -1;
494 // m2 resolution grows as p^2, so dE/dx only separates at low p
495 bool has_dedx = cand.getDeDxNhits() >= dedx_min_hits_;
496 double m2 = cand.getDeDxMassSquared();
497 bool is_proton = has_dedx && trk_p < proton_max_p_ && m2 > proton_min_mass2_;
498 bool is_kaon = has_dedx && trk_p < kaon_max_p_ && m2 > kaon_min_mass2_ &&
499 m2 <= proton_min_mass2_;
500 if (is_proton) {
501 cand.setPdgId(211 * q);
502 cand.setMass(938.27);
503 } else if (is_kaon) {
504 cand.setPdgId(211 * q);
505 cand.setMass(493.68);
506 } else if ((cand.getPID() & 2) && ep > electron_ep_min_ &&
507 cand.getEcalDepth() <
508 electron_max_depth_ +
509 electron_depth_slope_ *
510 std::log2(std::max(trk_p, 100.) / 1000.)) {
511 // EM showers stay shallow, hadronic ones start late and go deep
512 // no upper E/p, overlapping photons are split off as excess
513 cand.setPdgId(-11 * q);
514 cand.setMass(0.511);
515 } else {
516 cand.setPdgId(211 * q);
517 cand.setMass(139.57);
518 }
519}
520
521// produce track, ecal, and hcal linking
523 if (!event.exists(input_track_coll_name_, input_tracks_passname_)) {
524 ldmx_log(error) << "Unable to find (one) collection named "
525 << input_track_coll_name_ << "_" << input_tracks_passname_;
526 return;
527 }
528 if (!event.exists(input_ecal_coll_name_, input_ecal_passname_)) {
529 ldmx_log(error) << "Unable to find (one) collection named "
530 << input_ecal_coll_name_ << "_" << input_ecal_passname_;
531 return;
532 }
533 if (!event.exists(input_hcal_coll_name_, input_hcal_passname_)) {
534 ldmx_log(error) << "Unable to find (one) collection named "
535 << input_hcal_coll_name_ << "_" << input_hcal_passname_;
536 return;
537 }
538 // get the track and clustering info
539 auto hcal_clusters = event.getCollection<ldmx::CaloCluster>(
540 input_hcal_coll_name_, input_hcal_passname_);
541 if (hcal_merge_angle_ > 0) hcal_clusters = linkHcalClusters(hcal_clusters);
542 const auto tracks = event.getCollection<ldmx::Track>(input_track_coll_name_,
543 input_tracks_passname_);
544 // truth info is optional
545 std::vector<ldmx::SimTrackerHit> sp_hits;
546 if (event.exists(truth_sp_coll_name_, truth_sp_passname_))
547 sp_hits = event.getCollection<ldmx::SimTrackerHit>(truth_sp_coll_name_,
548 truth_sp_passname_);
549 // dE/dx estimates made on the input tracks, keyed by track index
550 std::map<int, ldmx::TrackDeDxMassEstimate> mass_estimates;
551 if (!input_mass_coll_name_.empty() &&
552 event.exists(input_mass_coll_name_, input_mass_passname_)) {
553 for (const auto& est : event.getCollection<ldmx::TrackDeDxMassEstimate>(
554 input_mass_coll_name_, input_mass_passname_)) {
555 mass_estimates[est.getTrackIndex()] = est;
556 }
557 }
558 // here allow for using existing clusters of different type (EcalCluster)
559 auto ecal_clusters =
560 use_existing_ecal_clusters_
561 ? getEcalClusters(event, input_ecal_coll_name_, input_ecal_passname_)
562 : event.getCollection<ldmx::CaloCluster>(input_ecal_coll_name_,
563 input_ecal_passname_);
564 if (ecal_link_radius_ > 0)
565 ecal_clusters = linkEcalClusters(ecal_clusters, tracks);
566
567 std::vector<ldmx::PFCandidate> pf_cands;
568 // multi-particle case
569 if (!single_particle_) {
570 /*
571 1. Build links maps at the Tk/Ecal and Hcal/Hcal interfaces
572 2. Categorize tracks as: Ecal-matched, (Side) Hcal-matched, unmatched
573 3. Categorize Ecal clusters as: Hcal-matched, unmatched
574 4a. (Upstream?) Categorize tracks with dedx?
575 4b. (Upstream?) Categorize ecal clusters as: EM/Had-like
576 4c. (Upstream?) Categorize hcal clusters as: EM/Had-like
577 5. Build candidates by category, moving from Tk-Ecal-Hcal
578 */
579
580 //
581 // track-calo linking
582 //
583 std::vector<bool> tk_usable(tracks.size(), false);
584 // linked Ecal clusters per track, closest first
585 std::map<int, std::vector<std::pair<float, int>>> tk_calo_map;
586 for (int i = 0; i < tracks.size(); i++) {
587 const auto& tk = tracks[i];
588 const std::vector<double> xyz = tk.getPosition(ldmx::AtECAL);
589 const std::vector<double> pxyz = tk.getMomentum(ldmx::AtECAL);
590 // need a forward Ecal face state to extrapolate
591 if (pxyz.empty() || pxyz[2] <= 0) {
592 ldmx_log(warn) << "Skipping track " << i << " without AtECAL state";
593 continue;
594 }
595 tk_usable[i] = true;
596 for (int j = 0; j < ecal_clusters.size(); j++) {
597 float dist = trackClusterDist(xyz, pxyz, ecal_clusters[j]);
598 // E/p is used for the ID, not the link
599 if (dist < link_max_dist_) tk_calo_map[i].emplace_back(dist, j);
600 }
601 std::sort(tk_calo_map[i].begin(), tk_calo_map[i].end());
602 }
603
604 // em-hadcalo linking
605 std::map<int, std::vector<int>> em_had_calo_map;
606 for (int i = 0; i < ecal_clusters.size(); i++) {
607 const auto& ecal = ecal_clusters[i];
608 for (int j = 0; j < hcal_clusters.size(); j++) {
609 const auto& hcal = hcal_clusters[j];
610 const float x_at_h_clus =
611 ecal.getCentroidX() +
612 ecal.getDXDZ() * (hcal.getCentroidZ() -
613 ecal.getCentroidZ()); // extrapolated position
614 const float y_at_h_clus =
615 ecal.getCentroidY() +
616 ecal.getDYDZ() * (hcal.getCentroidZ() - ecal.getCentroidZ());
617 float dist = sqrt(
618 pow(x_at_h_clus - hcal.getCentroidX(), 2) /
619 std::max(1.0, pow(hcal.getRMSX(), 2) + pow(ecal.getRMSX(), 2)) +
620 pow(y_at_h_clus - hcal.getCentroidY(), 2) /
621 std::max(1.0, pow(hcal.getRMSY(), 2) + pow(ecal.getRMSY(), 2)));
622 bool is_match = (dist < 5); // matching criteria, was 2
623 if (is_match) em_had_calo_map[i].push_back(j);
624 }
625 }
626
627 //
628 // track / ecal cluster arbitration
629 //
630 std::vector<bool> tk_is_em_linked(tracks.size(), false);
631 std::vector<bool> em_is_tk_linked(ecal_clusters.size(), false);
632 std::map<int, int> tk_em_pairs{};
633 for (int i = 0; i < tracks.size(); i++) {
634 if (tk_calo_map.count(i)) {
635 // pick the closest unused matching cluster
636 for (const auto& [dist, em_idx] : tk_calo_map[i]) {
637 if (!em_is_tk_linked[em_idx]) {
638 em_is_tk_linked[em_idx] = true;
639 tk_is_em_linked[i] = true;
640 tk_em_pairs[i] = em_idx;
641 break;
642 }
643 }
644 }
645 }
646
647 // ecal / hcal cluster arbitration
648 std::vector<bool> em_is_had_linked(ecal_clusters.size(), false);
649 std::vector<bool> had_is_em_linked(hcal_clusters.size(), false);
650 std::map<int, int> em_had_pairs{};
651 for (int i = 0; i < ecal_clusters.size(); i++) {
652 if (em_had_calo_map.count(i)) {
653 // pick first (highest-energy) unused matching cluster
654 for (int had_idx : em_had_calo_map[i]) {
655 if (!had_is_em_linked[had_idx]) {
656 had_is_em_linked[had_idx] = true;
657 em_is_had_linked[i] = true;
658 em_had_pairs[i] = had_idx;
659 break;
660 }
661 }
662 }
663 }
664
665 // can consider combining satellite clusters here...
666 // define some "primary cluster" ID criterion
667 // and can add fails to the primaries
668
669 //
670 // Begin building pf candidates from tracks
671 //
672 for (int i = 0; i < tracks.size(); i++) {
673 if (!tk_usable[i]) continue;
675 fillCandTrack(cand, tracks[i],
676 sp_hits); // append track info to candidate
677 cand.setTrackIndex(i);
678 if (tk_is_em_linked[i]) { // if track is linked with ECal cluster
679 int em_idx = tk_em_pairs[i];
680 fillCandEMCalo(cand, ecal_clusters[em_idx]);
681 cand.setEcalWidth(ecalWidth(ecal_clusters[em_idx], tracks[i]));
682 cand.setEcalIndex(em_idx);
683 if (em_is_had_linked[em_idx]) { // if ECal is linked with HCal
684 fillCandHadCalo(cand, hcal_clusters[em_had_pairs[em_idx]]);
685 cand.setHcalIndex(em_had_pairs[em_idx]);
686 }
687 recoverEcal(cand, tracks[i], ecal_clusters, em_is_tk_linked);
688 }
689 // charged hadron showers in the Hcal belong to the track, not to a
690 // neutral; takes the closest free cluster in angle
691 if (cand.getHcalIndex() < 0) {
692 int h = linkTrackHcal(tracks[i], hcal_clusters, had_is_em_linked);
693 if (h >= 0) {
694 fillCandHadCalo(cand, hcal_clusters[h]);
695 cand.setHcalIndex(h);
696 had_is_em_linked[h] = true;
697 }
698 }
699 if (auto it = mass_estimates.find(i); it != mass_estimates.end()) {
700 cand.setDeDxMassSquared(it->second.getMassSquared());
701 cand.setDeDxIh(it->second.getIh());
702 cand.setDeDxNhits(it->second.getNhits());
703 }
704 cand.setCharge(static_cast<int>(tracks[i].getCharge()));
705 identifyCharged(cand);
706 if (std::abs(cand.getPdgId()) == 211) {
707 absorbHadronShower(cand, tracks[i], ecal_clusters, em_is_tk_linked,
708 hcal_clusters, had_is_em_linked);
709 }
710 // charged energy from the track and the mass hypothesis
711 auto p = cand.getTrackPxPyPz();
712 double trk_p = std::hypot(p[0], p[1], p[2]);
713 cand.setEnergy(std::hypot(trk_p, cand.getMass()));
714 pf_cands.push_back(cand);
715
716 // Ecal energy well above the track momentum is a neutral overlap
717 double ecal_e = cand.getEcalEnergy();
718 if (tk_is_em_linked[i] && trk_p > 0 && ecal_e > excess_ep_ * trk_p) {
719 ldmx::PFCandidate excess;
720 fillCandEMCalo(excess, ecal_clusters[tk_em_pairs[i]]);
721 excess.setEcalIndex(tk_em_pairs[i]);
722 excess.setPdgId(22);
723 // hits stay with the charged candidate, one candidate per hit
724 excess.clearEcalHits();
725 excess.setEnergy(ecal_e - trk_p);
726 pf_cands.push_back(excess);
727 }
728 }
729
730 // neutral candidates from Ecal clusters not linked to a track
731 for (int i = 0; i < ecal_clusters.size(); i++) {
732 if (em_is_tk_linked[i]) continue;
734 fillCandEMCalo(cand, ecal_clusters[i]);
735 cand.setEcalIndex(i);
736 if (em_is_had_linked[i]) {
737 fillCandHadCalo(cand, hcal_clusters[em_had_pairs[i]]);
738 cand.setHcalIndex(em_had_pairs[i]);
739 }
740 // Hcal energy means a hadronic shower
741 // pdg 130 for neutral hadrons, as in CMS PF
742 cand.setPdgId(em_is_had_linked[i] ? 130 : 22);
743 cand.setEnergy(cand.getEcalEnergy() + cand.getHcalEnergy());
744 if (cand.getEnergy() >= min_neutral_e_) pf_cands.push_back(cand);
745 }
746
747 // neutral candidates from unlinked Hcal clusters
748 for (int i = 0; i < hcal_clusters.size(); i++) {
749 if (had_is_em_linked[i]) continue;
751 fillCandHadCalo(cand, hcal_clusters[i]);
752 cand.setHcalIndex(i);
753 cand.setPdgId(130);
754 cand.setEnergy(cand.getHcalEnergy());
755 if (cand.getEnergy() >= min_neutral_e_) pf_cands.push_back(cand);
756 }
757
758 } else {
759 // Single-particle builder
761 int pid = 0; // initialize pid to add
762 if (tracks.size() && !tracks[0].getMomentum(ldmx::AtECAL).empty()) {
763 fillCandTrack(pf, tracks[0], sp_hits);
764 pf.setTrackIndex(0);
765 pid += 1;
766 }
767 if (ecal_clusters.size()) {
768 fillCandEMCalo(pf, ecal_clusters[0]);
769 pf.setEcalIndex(0);
770 pid += 2;
771 }
772 if (hcal_clusters.size()) {
773 fillCandHadCalo(pf, hcal_clusters[0]);
774 pf.setHcalIndex(0);
775 pid += 4;
776 }
777 pf.setPID(pid);
778 pf.setEnergy(pf.getEcalEnergy() + pf.getHcalEnergy());
779 pf_cands.push_back(pf);
780 }
781
782 event.add(output_coll_name_, pf_cands);
783}
784// stupid function to type cast from ecal to calo cluster
785const std::vector<ldmx::CaloCluster> ParticleFlow::getEcalClusters(
786 framework::Event& event, std::string inputClusterCollName,
787 std::string inputClusterPassName) {
788 const auto tmp_clusters = event.getCollection<ldmx::EcalCluster>(
789 inputClusterCollName, inputClusterPassName);
790 std::string new_name = inputClusterCollName + "Cast";
791 std::vector<ldmx::CaloCluster> new_clusters;
792 for (auto cl : tmp_clusters) {
793 new_clusters.emplace_back(cl);
794 }
795 event.add(new_name, new_clusters);
796 const auto calo_clusters =
797 event.getCollection<ldmx::CaloCluster>(new_name, event.getPassName());
798 return calo_clusters;
799}
800
801void ParticleFlow::onProcessEnd() { ldmx_log(debug) << "Process ends!"; }
802
803} // namespace recon
804
Class that stores cluster information from the ECal.
#define DECLARE_PRODUCER(CLASS)
Macro which allows the framework to construct a producer given its name during configuration.
Simple PFlow algorithm.
Class that represents the estimated mass of a particle using tracker dE/dx information.
Implements an event buffer system for storing event data.
Definition Event.h:40
bool exists(const std::string &name, const std::string &passName, bool unique=true) const
Check for the existence of an object or collection with the given name and pass name in the event.
Definition Event.cxx:107
const std::vector< ContentType > & getCollection(const std::string &collectionName, const std::string &passName) const
Get a collection (std::vector) of objects from the event bus.
Definition Event.h:409
Class encapsulating parameters for configuring a processor.
Definition Parameters.h:26
const T & get(const std::string &name) const
Retrieve the parameter of the given name.
Definition Parameters.h:75
Stores cluster information from the ECal.
Definition CaloCluster.h:25
double getRMSX() const
rms in x
void setNHits(int nHits)
Sets total number of hits in the cluster.
Definition CaloCluster.h:64
double getDYDZ() const
Delta in y-z plane.
double getCentroidZ() const
centroid z-location
double getCentroidX() const
centroid x-location
void setCentroidXYZ(double centroid_x, double centroid_y, double centroid_z)
Sets the three coordinates of the cluster centroid.
Definition CaloCluster.h:84
double getRMSZ() const
rms in z
double getRMSY() const
rms in y
void setEnergy(double energy)
Sets total energy for the cluster.
Definition CaloCluster.h:58
void setIDs(std::vector< unsigned int > &hitIDs)
Sets a sorted vector for the IDs of the hits_ that make up the cluster.
Definition CaloCluster.h:71
double getCentroidY() const
centroid y-location
double getEDXDZ() const
Delta unc on unc in x-z plane.
double getDXDZ() const
Delta in x-z plane.
double getEDYDZ() const
Delta unc on unc in y-z plane.
Stores cluster information from the ECal.
Definition EcalCluster.h:20
Represents a reconstructed particle.
Definition PFCandidate.h:21
Represents a simulated tracker hit in the simulation.
int getPdgID() const
Get the Sim particle track ID of the hit.
std::vector< float > getPosition() const
Get the XYZ position of the hit [mm].
float getEnergy() const
Get the energy.
std::vector< double > getMomentum() const
Get the XYZ momentum of the particle at the position at which the hit took place [MeV].
int getTrackID() const
Get the Sim particle track ID of the hit.
Represents the estimated mass of a particle using tracker dE/dx information.
Implementation of a track object.
Definition Track.h:54
std::vector< double > getMomentum(TrackStateType tstype) const
Returns the momentum (px, py, pz) in MeV in the LDMX global frame for the requested TrackState type.
Definition Track.h:237
std::vector< double > getPosition(TrackStateType tstype) const
Returns the position (x, y, z) in mm in the LDMX global frame for the requested TrackState type.
Definition Track.h:246
virtual void produce(framework::Event &event)
Process the event and put new data products into it.
virtual void onProcessEnd()
Callback for the EventProcessor to take any necessary action when the processing of events finishes,...
virtual void configure(framework::config::Parameters &ps)
Callback for the EventProcessor to configure itself from the given set of parameters.