CMS 3D CMS Logo

TracksterLinkingbySuperClusteringDNN.cc
Go to the documentation of this file.
1 /*
2 TICL plugin for electron superclustering in HGCAL using a DNN.
3 DNN designed by Alessandro Tarabini.
4 
5 Inputs are CLUE3D EM tracksters. Outputs are superclusters (as vectors of IDs of trackster)
6 "Seed trackster" : seed of supercluster, always highest pT trackster of supercluster, normally should be an electron
7 "Candidate trackster" : trackster that is considered for superclustering with a seed
8 
9 Algorithm description :
10 1) Tracksters are ordered by decreasing pT.
11 2) We iterate over candidate tracksters, then over seed tracksters with higher pT than the candidate.
12  If the pair seed-candidate is in a compatible eta-phi window and passes some selections (seed pT, energy, etc), then we add the DNN features of the pair to a tensor for later inference.
13 3) We run the inference with the DNN on the pairs (in minibatches to reduce memory usage)
14 4) We iterate over candidate and seed pairs inference results. For each candidate, we take the seed for which the DNN score for the seed-candidate score is best.
15  If the score is also above a working point, then we add the candidate to the supercluster of the seed, and mask the candidate so it cannot be considered as a seed further
16 
17 The loop is first on candidate, then on seeds as it is more efficient for step 4 to find the best seed for each candidate.
18 
19 Authors : Theo Cuisset <theo.cuisset@cern.ch>, Shamik Ghosh <shamik.ghosh@cern.ch>
20 Date : 11/2023
21 */
22 
23 #include <string>
24 #include <memory>
25 #include <algorithm>
26 #include <vector>
27 
34 
38 
39 using namespace ticl;
40 
43  cms::Ort::ONNXRuntime const* onnxRuntime)
44  : TracksterLinkingAlgoBase(ps, iC, onnxRuntime),
45  dnnInputs_(makeSuperclusteringDNNInputFromString(ps.getParameter<std::string>("dnnInputsVersion"))),
46  inferenceBatchSize_(ps.getParameter<unsigned int>("inferenceBatchSize")),
47  nnWorkingPoint_(ps.getParameter<double>("nnWorkingPoint")),
48  deltaEtaWindow_(ps.getParameter<double>("deltaEtaWindow")),
49  deltaPhiWindow_(ps.getParameter<double>("deltaPhiWindow")),
50  seedPtThreshold_(ps.getParameter<double>("seedPtThreshold")),
51  candidateEnergyThreshold_(ps.getParameter<double>("candidateEnergyThreshold")),
52  explVarRatioCut_energyBoundary_(ps.getParameter<double>("candidateEnergyThreshold")),
53  explVarRatioMinimum_lowEnergy_(ps.getParameter<double>("explVarRatioMinimum_lowEnergy")),
54  explVarRatioMinimum_highEnergy_(ps.getParameter<double>("explVarRatioMinimum_highEnergy")),
55  filterByTracksterPID_(ps.getParameter<bool>("filterByTracksterPID")),
56  tracksterPIDCategoriesToFilter_(ps.getParameter<std::vector<int>>("tracksterPIDCategoriesToFilter")),
57  PIDThreshold_(ps.getParameter<double>("PIDThreshold")) {
59  "TracksterLinkingbySuperClusteringDNN : ONNXRuntime was not provided, the model should have been set in "
60  "onnxModelPath in the plugin config");
61 }
62 
64  const hgcal::RecHitTools rhtools,
65  const edm::ESHandle<MagneticField> bfieldH,
66  const edm::ESHandle<Propagator> propH) {}
67 
73  float explVar_denominator =
74  std::accumulate(std::begin(ts.eigenvalues()), std::end(ts.eigenvalues()), 0.f, std::plus<float>());
75  if (explVar_denominator != 0.) {
76  float explVarRatio = ts.eigenvalues()[0] / explVar_denominator;
78  return explVarRatio >= explVarRatioMinimum_highEnergy_;
79  else
80  return explVarRatio >= explVarRatioMinimum_lowEnergy_;
81  } else
82  return false;
83 }
84 
87  float probTotal = 0.0f;
89  probTotal += tst.id_probabilities(cat);
90  }
91  return probTotal >= PIDThreshold_;
92  } else
93  return true;
94 }
95 
103  const Inputs& input,
104  std::vector<Trackster>& resultTracksters,
105  std::vector<std::vector<unsigned int>>& outputSuperclusters,
106  std::vector<std::vector<unsigned int>>& linkedTracksterIdToInputTracksterId) {
107  // For now we use all input tracksters for superclustering. At some point there might be a filter here for EM tracksters (electromagnetic identification with DNN ?)
108  auto const& inputTracksters = input.tracksters;
109  const unsigned int tracksterCount = inputTracksters.size();
110 
111  /* Sorting tracksters by decreasing order of pT (out-of-place sort).
112  inputTracksters[trackstersIndicesPt[0]], ..., inputTracksters[trackstersIndicesPt[N]] makes a list of tracksters sorted by decreasing pT
113  Indices into this pT sorted collection will have the suffix _pt. Thus inputTracksters[index] and inputTracksters[trackstersIndicesPt[index_pt]] are correct
114  */
115  std::vector<unsigned int> trackstersIndicesPt(inputTracksters.size());
116  std::iota(trackstersIndicesPt.begin(), trackstersIndicesPt.end(), 0);
117  std::stable_sort(
118  trackstersIndicesPt.begin(), trackstersIndicesPt.end(), [&inputTracksters](unsigned int i1, unsigned int i2) {
119  return inputTracksters[i1].raw_pt() > inputTracksters[i2].raw_pt();
120  });
121 
122  /* Evaluate in minibatches since running with trackster count = 3000 leads to a short-lived ~15GB memory allocation
123  Also we do not know in advance how many superclustering candidate pairs there are going to be
124  The batch size needs to be rounded to featureCount
125  */
126  const unsigned int miniBatchSize =
127  static_cast<unsigned int>(inferenceBatchSize_) / dnnInputs_->featureCount() * dnnInputs_->featureCount();
128 
129  std::vector<std::vector<float>>
130  inputTensorBatches; // DNN input features tensors, in minibatches. Outer array : minibatches, inner array : 2D (flattened) array of features (indexed by batchIndex, featureId)
131  // How far along in the latest tensor of inputTensorBatches are we. Set to miniBatchSize to trigger the creation of the tensor batch on first run
132  unsigned int candidateIndexInCurrentBatch = miniBatchSize;
133  // List of all (ts_seed_id; ts_cand_id) selected for DNN inference (same layout as inputTensorBatches)
134  // Index is in global trackster collection (not pt ordered collection)
135  std::vector<std::vector<std::pair<unsigned int, unsigned int>>> tracksterIndicesUsedInDNN;
136 
137  // Use TracksterTiles to speed up search of tracksters in eta-phi window. One per endcap
138  std::array<TICLLayerTile, 2> tracksterTilesBothEndcaps_pt; // one per endcap
139  for (unsigned int i_pt = 0; i_pt < trackstersIndicesPt.size(); ++i_pt) {
140  Trackster const& ts = inputTracksters[trackstersIndicesPt[i_pt]];
141  tracksterTilesBothEndcaps_pt[ts.barycenter().eta() > 0.].fill(ts.barycenter().eta(), ts.barycenter().phi(), i_pt);
142  }
143 
144  // First loop on candidate tracksters (start at 1 since the highest pt trackster can only be a seed, not a candidate)
145  for (unsigned int ts_cand_idx_pt = 1; ts_cand_idx_pt < tracksterCount; ts_cand_idx_pt++) {
146  Trackster const& ts_cand = inputTracksters[trackstersIndicesPt[ts_cand_idx_pt]];
147 
148  if (ts_cand.raw_energy() < candidateEnergyThreshold_ ||
149  !checkExplainedVarianceRatioCut(ts_cand)) // || !trackstersPassesPIDCut(ts_cand)
150  continue;
151 
152  auto& tracksterTiles = tracksterTilesBothEndcaps_pt[ts_cand.barycenter().eta() > 0];
153  std::array<int, 4> search_box = tracksterTiles.searchBoxEtaPhi(ts_cand.barycenter().Eta() - deltaEtaWindow_,
154  ts_cand.barycenter().Eta() + deltaEtaWindow_,
155  ts_cand.barycenter().Phi() - deltaPhiWindow_,
156  ts_cand.barycenter().Phi() + deltaPhiWindow_);
157  // Look for seed trackster
158  for (int eta_i = search_box[0]; eta_i <= search_box[1]; ++eta_i) {
159  for (int phi_i = search_box[2]; phi_i <= search_box[3]; ++phi_i) {
160  for (unsigned int ts_seed_idx_pt :
161  tracksterTiles[tracksterTiles.globalBin(eta_i, (phi_i % TileConstants::nPhiBins))]) {
162  if (ts_seed_idx_pt >= ts_cand_idx_pt)
163  continue; // Look only at seed tracksters with higher pT than the candidate
164 
165  Trackster const& ts_seed = inputTracksters[trackstersIndicesPt[ts_seed_idx_pt]];
166 
167  if (ts_seed.raw_pt() < seedPtThreshold_)
168  break; // All further seeds will have lower pT than threshold (due to pT sorting)
169 
170  if (!checkExplainedVarianceRatioCut(ts_seed) || !trackstersPassesPIDCut(ts_seed))
171  continue;
172 
173  // Check that the two tracksters are geometrically compatible for superclustering
174  if (std::abs(ts_seed.barycenter().Eta() - ts_cand.barycenter().Eta()) < deltaEtaWindow_ &&
175  std::abs(deltaPhi(ts_seed.barycenter().Phi(), ts_cand.barycenter().Phi())) < deltaPhiWindow_) {
176  if (candidateIndexInCurrentBatch >= miniBatchSize) {
177  // Create new minibatch
178  assert(candidateIndexInCurrentBatch == miniBatchSize);
179 
180  /* Estimate how many seed-candidate pairs are remaining and don't allocate a full batch in this case. Use worst-case scenario of all pairs passing geometrical window
181  Also assume ts_seed_idx_pt=0 (worst case)
182  The last tensor of inputTensorBatches will be larger than necessary. The end of it will be uninitialized, then passed to the DNN.
183  We do not look at the output of the DNN on this section, so it will have no consequences.
184  */
185  inputTensorBatches.emplace_back(
186  std::min(miniBatchSize,
187  (tracksterCount * (tracksterCount - 1) - ts_cand_idx_pt * (ts_cand_idx_pt - 1)) / 2) *
188  dnnInputs_->featureCount());
189 
190  candidateIndexInCurrentBatch = 0;
191  tracksterIndicesUsedInDNN.emplace_back();
192  }
193 
194  std::vector<float> features = dnnInputs_->computeVector(ts_seed, ts_cand); // Compute DNN features
195  assert(features.size() == dnnInputs_->featureCount());
196  assert((candidateIndexInCurrentBatch + 1) * dnnInputs_->featureCount() <= inputTensorBatches.back().size());
197  // Copy the features into the batch (TODO : could probably avoid the copy and fill straight in the batch vector)
198  std::copy(features.begin(),
199  features.end(),
200  inputTensorBatches.back().begin() + candidateIndexInCurrentBatch * dnnInputs_->featureCount());
201  candidateIndexInCurrentBatch++;
202  tracksterIndicesUsedInDNN.back().emplace_back(trackstersIndicesPt[ts_seed_idx_pt],
203  trackstersIndicesPt[ts_cand_idx_pt]);
204  }
205  }
206  }
207  }
208  }
209 
210  if (inputTensorBatches.empty()) {
211  LogDebug("HGCalTICLSuperclustering")
212  << "No superclustering candidate pairs passed preselection before DNN. There are " << tracksterCount
213  << " tracksters in this event.";
214  }
215 
216 #ifdef EDM_ML_DEBUG
217  if (!inputTensorBatches.empty()) {
218  std::ostringstream s;
219  // Print the first 20 seed-cndidate pairs sent for inference
220  for (unsigned int i = 0;
221  i < std::min(dnnInputs_->featureCount() * 20, static_cast<unsigned int>(inputTensorBatches[0].size()));
222  i++) {
223  s << inputTensorBatches[0][i] << " ";
224  if (i != 0 && i % dnnInputs_->featureCount() == 0)
225  s << "],\t[";
226  }
227  LogDebug("HGCalTICLSuperclustering") << inputTensorBatches.size()
228  << " batches were created. First batch starts as follows : [" << s.str()
229  << "]";
230  }
231 #endif
232 
233  // Run the DNN inference
234  std::vector<std::vector<float>>
235  batchOutputs; // Outer index : minibatch, inner index : inference index in minibatch, value : DNN score
236  for (std::vector<float>& singleBatch : inputTensorBatches) {
237  // ONNXRuntime takes std::vector<std::vector<float>>& as input (non-const reference) so we have to make a new vector
238  std::vector<std::vector<float>> inputs_for_onnx{{std::move(singleBatch)}};
239  std::vector<float> outputs = onnxRuntime_->run(
240  {"input"}, inputs_for_onnx, {}, {}, inputs_for_onnx[0].size() / dnnInputs_->featureCount())[0];
241  batchOutputs.push_back(std::move(outputs));
242  }
243 
244  /* Build mask of tracksters already superclustered as candidates (to avoid using a trackster superclustered as candidate as a seed in further iterations).
245  Also mask seeds (only needed to add tracksters not in a supercluster to the output). */
246  std::vector<bool> tracksterMask(tracksterCount, false);
247 
248  /* Index of the seed trackster of the previous iteration
249  Initialized with an id that cannot be obtained in input */
250  unsigned int previousCandTrackster_idx = std::numeric_limits<unsigned int>::max();
251  unsigned int bestSeedForCurrentCandidate_idx = std::numeric_limits<unsigned int>::max();
252  float bestSeedForCurrentCandidate_dnnScore = nnWorkingPoint_;
253 
254  // Lambda to be called when there is a transition from one candidate to the next (as well as after the last iteration)
255  // Does the actual supercluster creation
256  auto onCandidateTransition = [&](unsigned ts_cand_idx) {
257  if (bestSeedForCurrentCandidate_idx <
258  std::numeric_limits<unsigned int>::max()) { // At least one seed can be superclustered with the candidate
259  tracksterMask[ts_cand_idx] = true; // Mask the candidate so it is not considered as seed in later iterations
260 
261  // Look for a supercluster of the seed
262  std::vector<std::vector<unsigned int>>::iterator seed_supercluster_it =
263  std::find_if(outputSuperclusters.begin(),
264  outputSuperclusters.end(),
265  [bestSeedForCurrentCandidate_idx](std::vector<unsigned int> const& sc) {
266  return sc[0] == bestSeedForCurrentCandidate_idx;
267  });
268 
269  if (seed_supercluster_it == outputSuperclusters.end()) { // No supercluster exists yet for the seed. Create one.
270  outputSuperclusters.emplace_back(std::initializer_list<unsigned int>{bestSeedForCurrentCandidate_idx});
271  resultTracksters.emplace_back(inputTracksters[bestSeedForCurrentCandidate_idx]);
272  linkedTracksterIdToInputTracksterId.emplace_back(
273  std::initializer_list<unsigned int>{bestSeedForCurrentCandidate_idx});
274  seed_supercluster_it = outputSuperclusters.end() - 1;
275  tracksterMask[bestSeedForCurrentCandidate_idx] =
276  true; // mask the seed as well (needed to find tracksters not in any supercluster)
277  }
278  // Index of the supercluster into resultTracksters, outputSuperclusters and linkedTracksterIdToInputTracksterId collections (the indices are the same)
279  unsigned int indexIntoOutputTracksters = seed_supercluster_it - outputSuperclusters.begin();
280  seed_supercluster_it->push_back(ts_cand_idx);
281  resultTracksters[indexIntoOutputTracksters].mergeTracksters(inputTracksters[ts_cand_idx]);
282  linkedTracksterIdToInputTracksterId[indexIntoOutputTracksters].push_back(ts_cand_idx);
283 
284  assert(outputSuperclusters.size() == resultTracksters.size() &&
285  outputSuperclusters.size() == linkedTracksterIdToInputTracksterId.size());
286  assert(seed_supercluster_it->size() == linkedTracksterIdToInputTracksterId[indexIntoOutputTracksters].size());
287 
288  bestSeedForCurrentCandidate_idx = std::numeric_limits<unsigned int>::max();
289  bestSeedForCurrentCandidate_dnnScore = nnWorkingPoint_;
290  }
291  };
292 
293  //Iterate over minibatches
294  for (unsigned int batchIndex = 0; batchIndex < batchOutputs.size(); batchIndex++) {
295  std::vector<float> const& currentBatchOutputs = batchOutputs[batchIndex]; // DNN score outputs
296  // Iterate over seed-candidate pairs inside current minibatch
297  for (unsigned int indexInBatch = 0; indexInBatch < tracksterIndicesUsedInDNN[batchIndex].size(); indexInBatch++) {
298  assert(indexInBatch < static_cast<unsigned int>(batchOutputs[batchIndex].size()));
299 
300  const unsigned int ts_seed_idx = tracksterIndicesUsedInDNN[batchIndex][indexInBatch].first;
301  const unsigned int ts_cand_idx = tracksterIndicesUsedInDNN[batchIndex][indexInBatch].second;
302  const float currentDnnScore = currentBatchOutputs[indexInBatch];
303 
304  if (previousCandTrackster_idx != std::numeric_limits<unsigned int>::max() &&
305  ts_cand_idx != previousCandTrackster_idx) {
306  // There is a transition from one seed to the next (don't make a transition for the first iteration)
307  onCandidateTransition(previousCandTrackster_idx);
308  }
309 
310  if (currentDnnScore > bestSeedForCurrentCandidate_dnnScore && !tracksterMask[ts_seed_idx]) {
311  // Check that the DNN suggests superclustering, that this seed-candidate assoc is better than previous ones, and that the seed is not already in a supercluster as candidate
312  bestSeedForCurrentCandidate_idx = ts_seed_idx;
313  bestSeedForCurrentCandidate_dnnScore = currentDnnScore;
314  }
315  previousCandTrackster_idx = ts_cand_idx;
316  }
317  }
318  onCandidateTransition(previousCandTrackster_idx);
319 
320  // Adding one-trackster superclusters for all tracksters not in a supercluster already that pass the seed threshold
321  for (unsigned int ts_id = 0; ts_id < tracksterCount; ts_id++) {
322  if (!tracksterMask[ts_id] && inputTracksters[ts_id].raw_pt() >= seedPtThreshold_) {
323  outputSuperclusters.emplace_back(std::initializer_list<unsigned int>{ts_id});
324  resultTracksters.emplace_back(inputTracksters[ts_id]);
325  linkedTracksterIdToInputTracksterId.emplace_back(std::initializer_list<unsigned int>{ts_id});
326  }
327  }
328 
329 #ifdef EDM_ML_DEBUG
330  for (std::vector<unsigned int> const& sc : outputSuperclusters) {
331  std::ostringstream s;
332  for (unsigned int trackster_id : sc)
333  s << trackster_id << " ";
334  LogDebug("HGCalTICLSuperclustering") << "Created supercluster of size " << sc.size()
335  << " holding tracksters (first one is seed) " << s.str();
336  }
337 #endif
338 }
339 
341  TracksterLinkingAlgoBase::fillPSetDescription(desc); // adds algo_verbosity
342  desc.add<edm::FileInPath>("onnxModelPath")->setComment("Path to DNN (as ONNX model)");
343  desc.ifValue(edm::ParameterDescription<std::string>("dnnInputsVersion", "v2", true),
344  edm::allowedValues<std::string>("v1", "v2"))
345  ->setComment(
346  "DNN inputs version tag. Defines which set of features is fed to the DNN. Must match with the actual DNN.");
347  desc.add<unsigned int>("inferenceBatchSize", 1e5)
348  ->setComment(
349  "Size of inference batches fed to DNN. Increasing it should produce faster inference but higher memory "
350  "usage. "
351  "Has no physics impact.");
352  desc.add<double>("nnWorkingPoint")
353  ->setComment("Working point of DNN (in [0, 1]). DNN score above WP will attempt to supercluster.");
354  desc.add<double>("deltaEtaWindow", 0.1)
355  ->setComment(
356  "Size of delta eta window to consider for superclustering. Seed-candidate pairs outside this window "
357  "are not considered for DNN inference.");
358  desc.add<double>("deltaPhiWindow", 0.5)
359  ->setComment(
360  "Size of delta phi window to consider for superclustering. Seed-candidate pairs outside this window "
361  "are not considered for DNN inference.");
362  desc.add<double>("seedPtThreshold", 4.)
363  ->setComment("Minimum transverse energy of trackster to be considered as seed of a supercluster");
364  desc.add<double>("candidateEnergyThreshold", 2.)
365  ->setComment("Minimum energy of trackster to be considered as candidate for superclustering");
366  desc.add<double>("explVarRatioCut_energyBoundary", 50.)
367  ->setComment("Boundary energy between low and high energy explVarRatio cut threshold");
368  desc.add<double>("explVarRatioMinimum_lowEnergy", 0.92)
369  ->setComment(
370  "Cut on explained variance ratio of tracksters to be considered as candidate, "
371  "for trackster raw_energy < explVarRatioCut_energyBoundary");
372  desc.add<double>("explVarRatioMinimum_highEnergy", 0.95)
373  ->setComment(
374  "Cut on explained variance ratio of tracksters to be considered as candidate, "
375  "for trackster raw_energy > explVarRatioCut_energyBoundary");
376  desc.add<bool>("filterByTracksterPID", true)->setComment("Filter tracksters before superclustering by PID score");
377  desc.add<std::vector<int>>(
378  "tracksterPIDCategoriesToFilter",
379  {static_cast<int>(Trackster::ParticleType::photon), static_cast<int>(Trackster::ParticleType::electron)})
380  ->setComment("List of PID particle types (ticl::Trackster::ParticleType enum) to consider for PID filtering");
381  desc.add<double>("PIDThreshold", 0.8)->setComment("PID score threshold");
382 }
void initialize(const HGCalDDDConstants *hgcons, const hgcal::RecHitTools rhtools, const edm::ESHandle< MagneticField > bfieldH, const edm::ESHandle< Propagator > propH) override
const float raw_pt() const
Definition: Trackster.h:156
std::unique_ptr< AbstractSuperclusteringDNNInput > dnnInputs_
static void fillPSetDescription(edm::ParameterSetDescription &iDesc)
static constexpr int nPhiBins
Definition: Common.h:15
const Vector & barycenter() const
Definition: Trackster.h:159
TracksterLinkingbySuperClusteringDNN(const edm::ParameterSet &ps, edm::ConsumesCollector iC, cms::Ort::ONNXRuntime const *onnxRuntime=nullptr)
assert(be >=bs)
static std::string const input
Definition: EdmProvDump.cc:50
void linkTracksters(const Inputs &input, std::vector< Trackster > &resultTracksters, std::vector< std::vector< unsigned int >> &linkedResultTracksters, std::vector< std::vector< unsigned int >> &linkedTracksterIdToInputTracksterId) override
def cat(path)
Definition: eostools.py:401
cms::Ort::ONNXRuntime const * onnxRuntime_
std::vector< float > features(const reco::PreId &ecal, const reco::PreId &hcal, double rho, const reco::BeamSpot &spot, noZS::EcalClusterLazyTools &ecalTools)
const float raw_energy() const
Definition: Trackster.h:154
Abs< T >::type abs(const T &t)
Definition: Abs.h:22
double f[11][100]
bool checkExplainedVarianceRatioCut(ticl::Trackster const &ts) const
static void fillPSetDescription(edm::ParameterSetDescription &desc)
std::unique_ptr< AbstractSuperclusteringDNNInput > makeSuperclusteringDNNInputFromString(std::string dnnVersion)
Definition: Common.h:10
const std::array< float, 8 > & id_probabilities() const
Definition: Trackster.h:165
def move(src, dest)
Definition: eostools.py:511
const std::array< float, 3 > & eigenvalues() const
Definition: Trackster.h:160
#define LogDebug(id)
FloatArrays run(const std::vector< std::string > &input_names, FloatArrays &input_values, const std::vector< std::vector< int64_t >> &input_shapes={}, const std::vector< std::string > &output_names={}, int64_t batch_size=1) const
Definition: ONNXRuntime.cc:87