LDMX Software
LinearSeedFinder.cxx
1#include "Tracking/Reco/LinearSeedFinder.h"
2
3#include "Eigen/Dense"
4
5namespace tracking {
6namespace reco {
7
8LinearSeedFinder::LinearSeedFinder(const std::string& name,
9 framework::Process& process)
10 : TrackingGeometryUser(name, process) {}
11
13 truth_matching_tool_ = std::make_shared<tracking::sim::TruthMatchingTool>();
14}
15
17 // Output seed name
18 out_seed_collection_ = parameters.getParameter<std::string>(
19 "out_seed_collection", getName() + "LinearRecoilSeedTracks");
20
21 // Input strip hits
22 input_hits_collection_ = parameters.getParameter<std::string>(
23 "input_hits_collection", "DigiRecoilSimHits");
24 input_rec_hits_collection_ = parameters.getParameter<std::string>(
25 "input_rec_hits_collection", "EcalRecHits");
26
27 input_pass_name_ =
28 parameters.getParameter<std::string>("input_pass_name", "");
29
30 // the uncertainty is sigma_x = 6 microns and sigma_y = 20./sqrt(12)
31 recoil_uncertainty_ = parameters.getParameter<std::vector<double>>(
32 "recoil_uncertainty", {0.006, 5.7735});
33 ecal_uncertainty_ =
34 parameters.getParameter<double>("ecal_uncertainty", {3.87});
35 ecal_distance_threshold_ =
36 parameters.getParameter<double>("ecal_distance_threshold");
37 ecal_first_layer_z_threshold_ =
38 parameters.getParameter<double>("ecal_first_layer_z_threshold");
39
40 layer12_midpoint_ = parameters.getParameter<double>("layer12_midpoint");
41 layer23_midpoint_ = parameters.getParameter<double>("layer23_midpoint");
42 layer34_midpoint_ = parameters.getParameter<double>("layer34_midpoint");
43}
44
46 auto start = std::chrono::high_resolution_clock::now();
47 std::vector<ldmx::StraightTrack> straight_seed_tracks;
48 n_events_++;
49
50 const std::vector<ldmx::Measurement> recoil_hits =
51 event.getCollection<ldmx::Measurement>(input_hits_collection_,
52 input_pass_name_);
53 const std::vector<ldmx::EcalHit> ecal_rec_hit =
54 event.getCollection<ldmx::EcalHit>(input_rec_hits_collection_,
55 input_pass_name_);
56
57 std::vector<std::array<double, 3>> first_layer_ecal_rec_hits;
58
59 // Find RecHits at first layer of ECal
60 for (const auto& x_ecal : ecal_rec_hit) {
61 if (x_ecal.getZPos() < ecal_first_layer_z_threshold_) {
62 first_layer_ecal_rec_hits.push_back(
63 {x_ecal.getZPos(), x_ecal.getXPos(), x_ecal.getYPos()});
64 } // if first layer of Ecal
65 } // for positions in ecalRecHit
66
67 // Check if we would fit empty seeds, if so: end tracking
68 if ((recoil_hits.size() < 2) || (first_layer_ecal_rec_hits.empty()) ||
69 (uniqueLayersHit(recoil_hits) < 2)) {
70 n_missing_++;
71 n_seeds_ += straight_seed_tracks.size();
72 event.add(out_seed_collection_, straight_seed_tracks);
73 return;
74 }
75
76 // Setup truth map
77 std::map<int, ldmx::SimParticle> particle_map;
78 if (event.exists("SimParticles")) {
79 particle_map = event.getMap<int, ldmx::SimParticle>("SimParticles");
80 truth_matching_tool_->setup(particle_map, recoil_hits);
81 }
82
83 // weighted averaging: layer1+layer2 = sensor1, layer3+layer4 = sensor2
84 // this gives all possible combinations of two tracker points for fitting
85 auto [first_sensor, second_sensor] = combineMultiGlobalHits(recoil_hits);
86
87 for (const auto& first_point : first_sensor) {
88 for (const auto& second_point : second_sensor) {
89 for (const auto& rec_hit : first_layer_ecal_rec_hits) {
90 // Do fitting on 2 sensor + 1 recHit combinations = 1 degree of freedom
91 // for linear fit
92 ldmx::StraightTrack seed_track =
93 SeedTracker(first_point, second_point, rec_hit);
94
95 // Seed passed RecHit distance check
96 if (seed_track.getChi2() > 0.0) {
97 straight_seed_tracks.push_back(seed_track);
98 } // if chi2 > 0
99 } // for rec_hits
100 } // for second recoil tracker
101 } // for first recoil tracker
102
103 n_seeds_ += straight_seed_tracks.size();
104 event.add(out_seed_collection_, straight_seed_tracks);
105
106 auto end = std::chrono::high_resolution_clock::now();
107
108 auto diff = end - start;
109 processing_time_ += std::chrono::duration<double, std::milli>(diff).count();
110
111 first_layer_ecal_rec_hits.clear();
112 straight_seed_tracks.clear();
113
114} // produce
115
116ldmx::StraightTrack LinearSeedFinder::SeedTracker(
117 const std::tuple<std::array<double, 3>, ldmx::Measurement,
118 std::optional<ldmx::Measurement>>
119 recoil_one,
120 const std::tuple<std::array<double, 3>, ldmx::Measurement,
121 std::optional<ldmx::Measurement>>
122 recoil_two,
123 const std::array<double, 3> ecal_one) {
124 auto [sensor1, layer1, layer2] = recoil_one;
125 auto [sensor2, layer3, layer4] = recoil_two;
126 std::vector<ldmx::Measurement> all_points;
127
128 // TODO: in the case where we don't have all 4 hits, we will be fitting a
129 // sensor (weighted average of two layers) + single layer
130 // TODO: or fitting two single layers. Currently, the single layer point has
131 // the uncertainty of a sensor assigned to it,
132 // TODO: but this is not a realistic uncertainty for a single layer...
133 // IF all layers are well-defined, this sequence will add layer1, 2, 3, 4 to
134 // the allPoints vector
135 all_points.push_back(layer1);
136
137 // if layer2 doesn't exist (has_value == False), then the layer1 we added
138 // is either layer1 or 2, depending on which one has_value
139 if (layer2.has_value()) {
140 all_points.push_back(*layer2);
141 }
142
143 all_points.push_back(layer3);
144
145 // if layer4 doesn't exist (has_value == False), then the layer3 we added
146 // is either layer3 or 4, depending on which one has_value
147 if (layer4.has_value()) {
148 all_points.push_back(*layer4);
149 }
150
151 // Fit the 3 points to a 3D straight line, find track location at first layer
152 // of Ecal, check distance to recHit used in fitting
153 // m = slope ; b = intercept
154 auto [m_x, b_x, m_y, b_y, seed_cov] = fit3DLine(sensor1, sensor2, ecal_one);
155 std::array<double, 3> temp_extrapolated_point = {
156 ecal_one[0], m_x * ecal_one[0] + b_x, m_y * ecal_one[0] + b_y};
157 double temp_distance = calculateDistance(temp_extrapolated_point, ecal_one);
158
160
161 if (temp_distance < ecal_distance_threshold_) {
162 trk.setSlopeX(m_x);
163 trk.setInterceptX(b_x);
164 trk.setSlopeY(m_y);
165 trk.setInterceptY(b_y);
166 trk.setTheta(std::atan2(m_y, std::sqrt(1 + m_x * m_x)));
167 trk.setPhi(std::atan2(m_x, 1.0));
168
169 trk.setAllSensorPoints(all_points);
170 trk.setFirstSensorPosition(sensor1);
171 trk.setSecondSensorPosition(sensor2);
172 trk.setFirstLayerEcalRecHit(ecal_one);
173 trk.setDistancetoRecHit(temp_distance);
174
175 trk.setTargetLocation(0.0, b_x, b_y);
176 trk.setEcalLayer1Location(temp_extrapolated_point);
177 trk.setChi2(
178 globalChiSquare(sensor1, sensor2, ecal_one, m_x, m_y, b_x, b_y));
179 trk.setNhits(3);
180 trk.setNdf(1);
181
182 trk.setCov(seed_cov);
183
184 // truth matching
185 if (truth_matching_tool_->configured()) {
186 auto truth_info = truth_matching_tool_->TruthMatch(all_points);
187 trk.setTrackID(truth_info.trackID);
188 trk.setPdgID(truth_info.pdgID);
189 trk.setTruthProb(truth_info.truthProb);
190 }
191
192 return trk;
193
194 } // if (track is close enough to EcalRecHit)
195 else {
196 trk.setChi2(-1);
197 return trk;
198 } // else (does not pass the threshold)
199} // SeedTracker
200
202 ldmx_log(info) << "AVG Time/Event: " << std::fixed << std::setprecision(1)
203 << processing_time_ / n_events_ << " ms";
204 ldmx_log(info) << "Total Seeds/Events: " << n_seeds_ << "/" << n_events_;
205 ldmx_log(info) << "not enough seed points " << n_missing_;
206
207} // onProcessEnd
208
209std::pair<std::vector<std::tuple<std::array<double, 3>, ldmx::Measurement,
210 std::optional<ldmx::Measurement>>>,
211 std::vector<std::tuple<std::array<double, 3>, ldmx::Measurement,
212 std::optional<ldmx::Measurement>>>>
213LinearSeedFinder::combineMultiGlobalHits(
214 const std::vector<ldmx::Measurement>& hit_collection) {
215 std::vector<ldmx::Measurement> layer1, layer2, layer3, layer4;
216
217 // Split hits into layers based on z position
218 // TODO: can we access layer information directly from ldmx::Measurement??
219 for (const auto& point : hit_collection) {
220 if (point.getGlobalPosition()[0] < layer12_midpoint_)
221 layer1.push_back(point);
222 else if (point.getGlobalPosition()[0] < layer23_midpoint_)
223 layer2.push_back(point);
224 else if (point.getGlobalPosition()[0] < layer34_midpoint_)
225 layer3.push_back(point);
226 else
227 layer4.push_back(point);
228 }
229
230 std::vector<std::tuple<std::array<double, 3>, ldmx::Measurement,
231 std::optional<ldmx::Measurement>>>
232 first_sensor_merged_hits, second_sensor_merged_hits;
233
234 if (layer1.empty()) {
235 for (const auto& point : layer2) {
236 first_sensor_merged_hits.push_back(
237 std::make_tuple(std::array<double, 3>{point.getGlobalPosition()[0],
238 point.getGlobalPosition()[1],
239 point.getGlobalPosition()[2]},
240 point, std::nullopt));
241 } // only look at layer2
242 } // if layer1 empty
243 else if (layer2.empty()) {
244 for (const auto& point : layer1) {
245 first_sensor_merged_hits.push_back(
246 std::make_tuple(std::array<double, 3>{point.getGlobalPosition()[0],
247 point.getGlobalPosition()[1],
248 point.getGlobalPosition()[2]},
249 point, std::nullopt));
250 } // only look at layer1
251 } // if layer2 empty
252 else {
253 first_sensor_merged_hits = midPointCalculation(layer1, layer2);
254 } // do weighted average of two layers
255
256 if (layer3.empty()) {
257 for (const auto& point : layer4) {
258 second_sensor_merged_hits.push_back(
259 std::make_tuple(std::array<double, 3>{point.getGlobalPosition()[0],
260 point.getGlobalPosition()[1],
261 point.getGlobalPosition()[2]},
262 point, std::nullopt));
263 } // only look at layer4
264 } // if layer3 empty
265 else if (layer4.empty()) {
266 for (const auto& point : layer3) {
267 second_sensor_merged_hits.push_back(
268 std::make_tuple(std::array<double, 3>{point.getGlobalPosition()[0],
269 point.getGlobalPosition()[1],
270 point.getGlobalPosition()[2]},
271 point, std::nullopt));
272 }
273 } // if layer4 empty
274 // do weighted average of two layers
275 else {
276 second_sensor_merged_hits = midPointCalculation(layer3, layer4);
277 }
278
279 return {first_sensor_merged_hits, second_sensor_merged_hits};
280} // combineMultiGlobalHits
281
282std::vector<std::tuple<std::array<double, 3>, ldmx::Measurement,
283 std::optional<ldmx::Measurement>>>
284LinearSeedFinder::midPointCalculation(
285 const std::vector<ldmx::Measurement>& layer1,
286 const std::vector<ldmx::Measurement>& layer2) {
287 std::vector<std::tuple<std::array<double, 3>, ldmx::Measurement,
288 std::optional<ldmx::Measurement>>>
289 merged_hits;
290
291 for (const auto& point1 : layer1) {
292 for (const auto& point2 : layer2) {
293 double z_avg =
294 (point1.getGlobalPosition()[0] + point2.getGlobalPosition()[0]) /
295 (2.0);
296 double x_avg =
297 (point1.getGlobalPosition()[1] + point2.getGlobalPosition()[1]) /
298 (2.0);
299 // Until we make axial/stereo combinations, we don't know anything about
300 // the y value
301 double y_avg = 0.0;
302
303 merged_hits.push_back(std::make_tuple(
304 std::array<double, 3>{z_avg, x_avg, y_avg}, point1, point2));
305
306 } // for layer2
307 } // for layer1
308 return merged_hits;
309} // midPointCalculation
310
311std::tuple<double, double, double, double, std::vector<double>>
312LinearSeedFinder::fit3DLine(const std::array<double, 3>& first_recoil,
313 const std::array<double, 3>& second_recoil,
314 const std::array<double, 3>& ecal) {
315 double z_pos1 = first_recoil[0], x_pos1 = first_recoil[1],
316 y_pos1 = first_recoil[2];
317 double z_pos2 = second_recoil[0], x_pos2 = second_recoil[1],
318 y_pos2 = second_recoil[2];
319 double z_pos3 = ecal[0], x_pos3 = ecal[1], y_pos3 = ecal[2];
320
321 std::array<double, 6> weights = {
322 1 / pow(recoil_uncertainty_[0], 2), 1 / pow(recoil_uncertainty_[1], 2),
323 1 / pow(recoil_uncertainty_[0], 2), 1 / pow(recoil_uncertainty_[1], 2),
324 1 / pow(ecal_uncertainty_, 2), 1 / pow(ecal_uncertainty_, 2)};
325
326 Eigen::Matrix<double, 6, 4> A_mat;
327 Eigen::Matrix<double, 6, 1> d_vec, w_vec;
328
329 // Fill the A matrix (z, 1, 0, 0) for x and (0, 0, z, 1) for y
330 A_mat << z_pos1, 1, 0, 0, 0, 0, z_pos1, 1, z_pos2, 1, 0, 0, 0, 0, z_pos2, 1,
331 z_pos3, 1, 0, 0, 0, 0, z_pos3, 1;
332
333 // Fill the d vector with x and y values
334 d_vec << x_pos1, y_pos1, x_pos2, y_pos2, x_pos3, y_pos3;
335
336 // Fill the weights vector
337 w_vec = Eigen::Matrix<double, 6, 1>(weights.data());
338
339 // Solve the weighted least squares system
340 Eigen::MatrixXd At_W_A = A_mat.transpose() * w_vec.asDiagonal() * A_mat;
341 Eigen::MatrixXd At_W_d = A_mat.transpose() * w_vec.asDiagonal() * d_vec;
342 Eigen::VectorXd param_vec = At_W_A.ldlt().solve(At_W_d);
343
344 Eigen::Matrix4d covariance_matrix = At_W_A.inverse();
345
346 // Store only the upper triangular part of the covariance matrix since it is
347 // symmetric
348 std::vector<double> covariance_vector = {
349 covariance_matrix(0, 0), covariance_matrix(0, 1), covariance_matrix(0, 2),
350 covariance_matrix(0, 3), covariance_matrix(1, 1), covariance_matrix(1, 2),
351 covariance_matrix(1, 3), covariance_matrix(2, 2), covariance_matrix(2, 3),
352 covariance_matrix(3, 3)};
353
354 // return {slope_x, intercept_x, slope_y, intercept_y, covariance}
355 return {param_vec(0), param_vec(1), param_vec(2), param_vec(3),
356 covariance_vector};
357} // fit3DLine
358
359double LinearSeedFinder::calculateDistance(
360 const std::array<double, 3>& point1, const std::array<double, 3>& point2) {
361 return sqrt(pow(point1[1] - point2[1], 2) + pow(point1[2] - point2[2], 2));
362} // calculateDistance in xy
363
364double LinearSeedFinder::globalChiSquare(
365 const std::array<double, 3>& first_sensor,
366 const std::array<double, 3>& second_sensor,
367 const std::array<double, 3>& ecal_hit, double m_x, double m_y, double b_x,
368 double b_y) {
369 double chi2_x = 0, chi2_y = 0;
370 chi2_x += pow(
371 (m_x * first_sensor[0] + b_x - first_sensor[1]) / recoil_uncertainty_[0],
372 2);
373 chi2_y += pow(
374 (m_y * first_sensor[0] + b_y - first_sensor[2]) / recoil_uncertainty_[1],
375 2);
376
377 chi2_x += pow((m_x * second_sensor[0] + b_x - second_sensor[1]) /
378 recoil_uncertainty_[0],
379 2);
380 chi2_y += pow((m_y * second_sensor[0] + b_y - second_sensor[2]) /
381 recoil_uncertainty_[1],
382 2);
383
384 chi2_x += pow((m_x * ecal_hit[0] + b_x - ecal_hit[1]) / ecal_uncertainty_, 2);
385 chi2_y += pow((m_y * ecal_hit[0] + b_y - ecal_hit[2]) / ecal_uncertainty_, 2);
386
387 return chi2_x + chi2_y;
388} // globalChiSquare
389
390int LinearSeedFinder::uniqueLayersHit(
391 const std::vector<ldmx::Measurement>& digi_points) {
392 std::vector<ldmx::Measurement> sorted_points = digi_points;
393
394 // Sort by z position in the Recoil
395 std::sort(sorted_points.begin(), sorted_points.end(),
396 [](const ldmx::Measurement& meas1, const ldmx::Measurement& meas2) {
397 return meas1.getGlobalPosition()[0] <
398 meas2.getGlobalPosition()[0];
399 });
400
401 // Remove duplicates to ensure we only keep unique z positions
402 auto last = std::unique(
403 sorted_points.begin(), sorted_points.end(),
404 [](const ldmx::Measurement& meas1, const ldmx::Measurement& meas2) {
405 return meas1.getGlobalPosition()[0] == meas2.getGlobalPosition()[0];
406 });
407
408 // return the number of unique layer hits
409 return std::distance(sorted_points.begin(), last);
410} // uniqueLayersHit
411
412} // namespace reco
413} // namespace tracking
414
415DECLARE_PRODUCER_NS(tracking::reco, LinearSeedFinder);
#define DECLARE_PRODUCER_NS(NS, CLASS)
Macro which allows the framework to construct a producer given its name during configuration.
std::string getName() const
Get the processor name.
Implements an event buffer system for storing event data.
Definition Event.h:42
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:92
Class which represents the process under execution.
Definition Process.h:36
Class encapsulating parameters for configuring a processor.
Definition Parameters.h:29
Stores reconstructed hit information from the ECAL.
Definition EcalHit.h:19
Class representing a simulated particle.
Definition SimParticle.h:23
std::string out_seed_collection_
The name of the output collection of seeds to be stored.
LinearSeedFinder(const std::string &name, framework::Process &process)
Constructor.
void configure(framework::config::Parameters &parameters) override
Configure the processor using the given user specified parameters.
void produce(framework::Event &event) override
Run the processor and create a collection of results which indicate if a charge particle can be found...
void onProcessEnd() override
Output event statistics.
std::string input_hits_collection_
The name of the input hits collection to use in finding seeds..
void onProcessStart() override
Setup the truth matching.
std::string input_rec_hits_collection_
The name of the tagger Tracks (only for Recoil Seeding)
a helper base class providing some methods to shorten access to common conditions used within the tra...
The measurement calibrator can be a function or a class/struct able to retrieve the sim hits containe...