LDMX Software
EcalTrackFinderProcessor.cxx
Go to the documentation of this file.
1
7
8// LDMX
9#include "Tracking/Sim/MeasurementCalibrator.h"
10#include "Tracking/Sim/TrackingUtils.h"
11#include "Tracking/geo/CalibrationContext.h"
12#include "Tracking/geo/GeometryContext.h"
13#include "Tracking/geo/MagneticFieldContext.h"
14
15// ACTS
16#include "Acts/Definitions/Units.hpp"
17#include "Acts/EventData/MultiTrajectory.hpp"
18#include "Acts/EventData/TrackParameters.hpp"
19#include "Acts/EventData/TransformationHelpers.hpp"
20#include "Acts/Geometry/CuboidVolumeBuilder.hpp"
21#include "Acts/Geometry/GeometryContext.hpp"
22#include "Acts/Geometry/TrackingGeometry.hpp"
23#include "Acts/Geometry/TrackingGeometryBuilder.hpp"
24#include "Acts/Geometry/TrackingVolume.hpp"
25#include "Acts/MagneticField/MagneticFieldContext.hpp"
26#include "Acts/Propagator/AbortList.hpp"
27#include "Acts/Propagator/ActionList.hpp"
28#include "Acts/Propagator/MaterialInteractor.hpp"
29#include "Acts/Propagator/StandardAborters.hpp"
30#include "Acts/Propagator/detail/SteppingLogger.hpp"
31#include "Acts/Surfaces/PerigeeSurface.hpp"
32#include "Acts/TrackFinding/MeasurementSelector.hpp"
33#include "Acts/Utilities/Logger.hpp"
34#include "Acts/Utilities/TrackHelpers.hpp"
35
36// C++
37#include <algorithm>
38#include <chrono>
39#include <cmath>
40#include <fstream>
41#include <sstream>
42
43namespace ecal {
44
46 framework::Process& process)
47 : Producer(name, process) {
48 // Setup surface rotation: u=+Y, v=+Z, w=+X (beam direction)
49 surf_rotation_ = Acts::RotationMatrix3::Zero();
50 surf_rotation_(1, 0) = 1; // u along Y
51 surf_rotation_(2, 1) = 1; // v along Z
52 surf_rotation_(0, 2) = 1; // w along X (beam/normal)
53}
54
57 rec_coll_name_ = parameters.get<std::string>("rec_coll_name");
58 rec_pass_name_ = parameters.get<std::string>("rec_pass_name");
59 out_track_collection_ = parameters.get<std::string>("out_track_collection");
60
61 min_hits_ = parameters.get<int>("min_hits");
62 max_chi2_ = parameters.get<double>("max_chi2");
63 cell_resolution_ = parameters.get<double>("cell_resolution");
64 debug_ = parameters.get<bool>("debug");
65
66 max_seed_rms_ = parameters.get<double>("max_seed_rms");
67 min_momentum_ = parameters.get<double>("min_momentum");
68 max_momentum_ = parameters.get<double>("max_momentum");
69
70 use_roc_energy_ = parameters.get<bool>("use_roc_energy");
71 if (use_roc_energy_) {
72 roc_file_name_ = parameters.get<std::string>("roc_file");
73 std::ifstream rocfile(roc_file_name_);
74 if (!rocfile.good()) {
75 EXCEPTION_RAISE("EcalTrackFinderProcessor",
76 "ROC file '" + roc_file_name_ + "' does not exist!");
77 }
78 std::string line, value;
79 // Skip header line
80 std::getline(rocfile, line);
81 while (std::getline(rocfile, line)) {
82 std::stringstream ss(line);
83 std::vector<float> values;
84 while (std::getline(ss, value, ',')) {
85 values.push_back(value.empty() ? -1.0f : std::stof(value));
86 }
87 roc_range_values_.push_back(values);
88 }
89 ldmx_log(info) << "Loaded ROC file with " << roc_range_values_.size()
90 << " bins";
91 }
92}
93
95 // Geometry is available here: EcalGeometryProvider::onNewRun has already
96 // populated detector_geometry_ before processors receive onNewRun.
98 ldmx::EcalGeometry::CONDITIONS_OBJECT_NAME);
99
100 // Create ECAL layer surfaces
101 layer_surfaces_.clear();
103
104 // Setup zero magnetic field
105 Acts::Vector3 b_field(0., 0., 0.);
106 auto zero_b_field = std::make_shared<Acts::ConstantBField>(b_field);
107
108 // Build ACTS tracking geometry for the ECAL using CuboidVolumeBuilder
109 // Each ECAL layer becomes a sensitive layer in the tracking geometry
110
113 .get();
114
115 // Get ECAL extent in ACTS coordinates
116 double ecal_front_z = geometry_->getEcalFrontZ();
117 double ecal_back_z =
118 geometry_->getZPosition(geometry_->getNumLayers() - 1) + 50.0;
119 Acts::Vector3 front_acts =
120 tracking::sim::utils::ldmx2Acts(Acts::Vector3(0.0, 0.0, ecal_front_z));
121 Acts::Vector3 back_acts =
122 tracking::sim::utils::ldmx2Acts(Acts::Vector3(0.0, 0.0, ecal_back_z));
123
124 // Create layer configurations - one per ECAL layer
125 std::vector<Acts::CuboidVolumeBuilder::LayerConfig> layer_configs;
126 double clearance = 1.0; // mm envelope around each layer surface
127
128 for (auto& [layer, surface] : layer_surfaces_) {
129 Acts::CuboidVolumeBuilder::LayerConfig lcfg;
130 lcfg.surfaces = {surface};
131 lcfg.envelopeX = std::array<double, 2>{clearance, clearance};
132 lcfg.active = true;
133 layer_configs.push_back(lcfg);
134 }
135
136 // Volume config
137 Acts::Vector3 volume_center = 0.5 * (front_acts + back_acts);
138 double x_length = std::abs(back_acts.x() - front_acts.x()) + 20.0;
139
140 Acts::CuboidVolumeBuilder::VolumeConfig ecal_vol_cfg;
141 ecal_vol_cfg.position = volume_center;
142 ecal_vol_cfg.length = {x_length, 1000.0, 1000.0}; // generous transverse size
143 ecal_vol_cfg.name = "EcalVolume";
144 ecal_vol_cfg.layerCfg = layer_configs;
145 ecal_vol_cfg.volumeMaterial =
146 std::make_shared<Acts::HomogeneousVolumeMaterial>(Acts::Material());
147
148 // Build the tracking geometry
149 Acts::CuboidVolumeBuilder cvb;
150 Acts::CuboidVolumeBuilder::Config cvb_cfg;
151 cvb_cfg.position = volume_center;
152 cvb_cfg.length = {x_length + 20.0, 1020.0, 1020.0};
153 cvb_cfg.volumeCfg = {ecal_vol_cfg};
154 cvb.setConfig(cvb_cfg);
155
156 Acts::TrackingGeometryBuilder::Config tgb_cfg;
157 tgb_cfg.trackingVolumeBuilders.push_back(
158 [=](const auto& cxt, const auto& inner, const auto&) {
159 return cvb.trackingVolume(cxt, inner, nullptr);
160 });
161
162 Acts::TrackingGeometryBuilder tgb(tgb_cfg);
163 tracking_geometry_ = tgb.trackingGeometry(gctx);
164
165 // Extract the geometry IDs that the builder assigned to our surfaces.
166 // CuboidVolumeBuilder/TrackingGeometryBuilder reassign GeometryIdentifiers
167 // based on the volume/layer/sensitive hierarchy, overwriting our manual IDs.
168 // We must use these builder-assigned IDs when creating the source link map,
169 // because the CKF Navigator will see these IDs when it reaches a surface.
170 layer_geo_ids_.clear();
171 tracking_geometry_->visitSurfaces([&](const Acts::Surface* surface) {
172 if (!surface) return;
173 // Only care about sensitive surfaces (those with a sensitive ID)
174 if (surface->geometryId().sensitive() == 0) return;
175
176 // Match to ECAL layer by z position (LDMX frame)
177 Acts::Vector3 center_ldmx =
178 tracking::sim::utils::acts2Ldmx(surface->center(gctx));
179 double z_ldmx = center_ldmx[2];
180
181 for (int layer = 0; layer < geometry_->getNumLayers(); ++layer) {
182 double layer_z = geometry_->getZPosition(layer);
183 if (std::abs(z_ldmx - layer_z) < 0.1) { // 0.1 mm tolerance
184 layer_geo_ids_[layer] = surface->geometryId();
185 ldmx_log(debug) << "ECAL layer " << layer << " -> builder geo_id: vol="
186 << surface->geometryId().volume()
187 << " lay=" << surface->geometryId().layer()
188 << " sen=" << surface->geometryId().sensitive();
189 break;
190 }
191 }
192 });
193
194 ldmx_log(info) << "Mapped " << layer_geo_ids_.size()
195 << " ECAL layers to builder-assigned geometry IDs";
196
197 // Setup stepper with zero B-field
198 const auto stepper = Acts::EigenStepper<>{zero_b_field};
199
200 // Setup navigator with tracking geometry
201 Acts::Navigator::Config nav_cfg{tracking_geometry_};
202 nav_cfg.resolveSensitive = true;
203 nav_cfg.resolvePassive = false;
204 nav_cfg.resolveMaterial = false;
205 const Acts::Navigator navigator(nav_cfg);
206
207 // Create propagator
208 auto acts_logging_level =
209 debug_ ? Acts::Logging::VERBOSE : Acts::Logging::WARNING;
210 propagator_ = std::make_unique<EcalPropagator>(
211 stepper, navigator,
212 Acts::getDefaultLogger("ECAL_PROP", acts_logging_level));
213
214 // Create CKF
215 ckf_ = std::make_unique<std::decay_t<decltype(*ckf_)>>(
216 *propagator_, Acts::getDefaultLogger("ECAL_CKF", acts_logging_level));
217
218 ldmx_log(info) << "EcalTrackFinderProcessor initialized with "
219 << layer_surfaces_.size() << " ECAL layer surfaces";
220}
221
223 int n_layers = geometry_->getNumLayers();
224
225 for (int layer = 0; layer < n_layers; ++layer) {
226 // Get z position of this layer
227 double z_pos = geometry_->getZPosition(layer);
228
229 // Create plane surface at this z position
230 // Position in ACTS frame (x=z_ldmx, y=x_ldmx, z=y_ldmx)
231 Acts::Vector3 acts_pos =
232 tracking::sim::utils::ldmx2Acts(Acts::Vector3(0.0, 0.0, z_pos));
233
234 Acts::Translation3 translation(acts_pos);
235 Acts::Transform3 transform(translation * surf_rotation_);
236
237 // Create bounded plane surface (500mm x 500mm, covers full ECAL)
238 auto bounds = std::make_shared<Acts::RectangleBounds>(500.0, 500.0);
239 auto surface =
240 Acts::Surface::makeShared<Acts::PlaneSurface>(transform, bounds);
241
242 // Assign a geometry ID (use layer as volume, 0 as layer in ACTS sense)
243 Acts::GeometryIdentifier geo_id;
244 geo_id.setVolume(layer);
245 geo_id.setLayer(0);
246 surface->assignGeometryId(geo_id);
247
248 layer_surfaces_[layer] = surface;
249 }
250
251 // Create reference surface at ECAL front face
252 double ecal_front_z = geometry_->getEcalFrontZ();
253 Acts::Vector3 ref_pos =
254 tracking::sim::utils::ldmx2Acts(Acts::Vector3(0.0, 0.0, ecal_front_z));
255 Acts::Translation3 ref_translation(ref_pos);
256 Acts::Transform3 ref_transform(ref_translation * surf_rotation_);
257 reference_surface_ =
258 Acts::Surface::makeShared<Acts::PlaneSurface>(ref_transform);
259}
260
261std::vector<ldmx::Measurement> EcalTrackFinderProcessor::createMeasurements(
262 const std::vector<ldmx::EcalHit>& hits, std::vector<double>& energies) {
263 std::vector<ldmx::Measurement> measurements;
264 measurements.reserve(hits.size());
265 energies.clear();
266 energies.reserve(hits.size());
267
268 for (const auto& hit : hits) {
269 // Skip noise hits
270 if (hit.isNoise()) continue;
271
272 // Get EcalID
273 ldmx::EcalID ecal_id(hit.getID());
274 int layer = ecal_id.layer();
275
276 // Get position from geometry
277 auto [x, y, z] = geometry_->getPosition(ecal_id);
278
279 // Create measurement
281 meas.setGlobalPosition(x, y, z);
282 meas.setLayerID(layer);
283 meas.setTime(hit.getTime());
284
285 // Local coordinates: use x, y as local u, v in the layer plane
286 meas.setLocalPosition(x, y);
287
288 // Covariance: use cell resolution
289 double cov = cell_resolution_ * cell_resolution_;
290 meas.setLocalCovariance(cov, cov);
291
292 measurements.push_back(meas);
293 energies.push_back(hit.getEnergy());
294 }
295
296 return measurements;
297}
298
299std::tuple<Acts::Vector3, Acts::Vector3, double>
301 const std::vector<Acts::Vector3>& points) {
302 if (points.size() < 2) {
303 return {Acts::Vector3::Zero(), Acts::Vector3::Zero(), 1e6};
304 }
305
306 // Calculate centroid
307 Acts::Vector3 centroid = Acts::Vector3::Zero();
308 for (const auto& p : points) {
309 centroid += p;
310 }
311 centroid /= points.size();
312
313 // Build covariance matrix
314 Eigen::Matrix3d cov = Eigen::Matrix3d::Zero();
315 for (const auto& p : points) {
316 Acts::Vector3 dp = p - centroid;
317 cov += dp * dp.transpose();
318 }
319 cov /= points.size();
320
321 // Find principal direction (eigenvector with largest eigenvalue)
322 Eigen::SelfAdjointEigenSolver<Eigen::Matrix3d> solver(cov);
323 Acts::Vector3 direction = solver.eigenvectors().col(2); // Largest eigenvalue
324 direction.normalize();
325
326 // Eigenvector has sign ambiguity — ensure it points forward along the beam.
327 // In ACTS coords, beam direction is +X (LDMX Z -> ACTS X).
328 if (direction.x() < 0) {
329 direction = -direction;
330 }
331
332 // Calculate RMS residual
333 double rms = 0.0;
334 for (const auto& p : points) {
335 Acts::Vector3 dp = p - centroid;
336 double residual = (dp - (dp.dot(direction)) * direction).norm();
337 rms += residual * residual;
338 }
339 rms = std::sqrt(rms / points.size());
340
341 return {centroid, direction, rms};
342}
343
344std::vector<ldmx::Track> EcalTrackFinderProcessor::findSeeds(
345 const std::vector<ldmx::Measurement>& measurements) {
346 std::vector<ldmx::Track> seeds;
347
348 if (measurements.size() < static_cast<size_t>(min_hits_)) {
349 ldmx_log(debug) << "Too few measurements for seed: " << measurements.size()
350 << " < " << min_hits_;
351 return seeds;
352 }
353
354 // Group measurements by layer
355 std::map<int, std::vector<const ldmx::Measurement*>> layer_map;
356 for (const auto& meas : measurements) {
357 layer_map[meas.getLayerID()].push_back(&meas);
358 }
359
360 ldmx_log(debug) << "Measurements span " << layer_map.size() << " layers";
361
362 // Simple strategy: use all hits for a single seed (can be extended later)
363 std::vector<Acts::Vector3> points;
364 std::vector<ldmx::Measurement> seed_measurements;
365
366 for (const auto& [layer, meas_vec] : layer_map) {
367 // For now, just take first hit in each layer
368 if (!meas_vec.empty()) {
369 const auto* meas = meas_vec[0];
370 auto gpos = meas->getGlobalPosition();
371
372 // Convert to ACTS frame
373 Acts::Vector3 pos_acts = tracking::sim::utils::ldmx2Acts(
374 Acts::Vector3(gpos[0], gpos[1], gpos[2]));
375 points.push_back(pos_acts);
376 seed_measurements.push_back(*meas);
377 }
378 }
379
380 if (points.size() < static_cast<size_t>(min_hits_)) {
381 ldmx_log(debug) << "Too few layers hit for seed: " << points.size() << " < "
382 << min_hits_;
383 return seeds;
384 }
385
386 // Fit straight line
387 auto [position, direction, rms] = fitStraightLine(points);
388
389 ldmx_log(debug) << "Seed line fit: rms=" << rms << " dir=(" << direction.x()
390 << "," << direction.y() << "," << direction.z() << ")";
391
392 if (rms > max_seed_rms_) {
393 ldmx_log(debug) << "Seed RMS too large: " << rms << " > " << max_seed_rms_
394 << " mm";
395 return seeds;
396 }
397
398 // Create seed track at reference surface (ECAL front)
399 // Intersect line with reference surface
402 .get();
403
404 // For a plane surface, normal is the third column of rotation (z-direction in
405 // local frame)
406 Acts::Vector3 ref_normal =
407 reference_surface_->transform(gctx).rotation().col(2);
408 Acts::Vector3 ref_center = reference_surface_->center(gctx);
409
410 double t =
411 (ref_center - position).dot(ref_normal) / direction.dot(ref_normal);
412 Acts::Vector3 seed_pos = position + t * direction;
413
414 // Estimate momentum (assume MIP ~200 MeV for now)
415 double p_estimate = 200.0; // MeV
416 Acts::Vector3 seed_mom = p_estimate * direction;
417
418 // Charge (assume positive)
419 Acts::ActsScalar q = Acts::UnitConstants::e;
420
421 // Convert to bound parameters at reference surface
422 Acts::FreeVector seed_free =
423 tracking::sim::utils::toFreeParameters(seed_pos, seed_mom, q);
424
425 auto bound_params_result = Acts::transformFreeToBoundParameters(
426 seed_free, *reference_surface_, gctx);
427
428 if (!bound_params_result.ok()) {
429 ldmx_log(warn) << "Failed to create bound parameters for seed";
430 return seeds;
431 }
432
433 Acts::BoundVector bound_params = bound_params_result.value();
434
435 // Create inflated covariance
436 Acts::BoundVector stddev;
437 stddev[Acts::eBoundLoc0] = 10.0 * Acts::UnitConstants::mm;
438 stddev[Acts::eBoundLoc1] = 10.0 * Acts::UnitConstants::mm;
439 stddev[Acts::eBoundPhi] = 0.1 * Acts::UnitConstants::rad;
440 stddev[Acts::eBoundTheta] = 0.1 * Acts::UnitConstants::rad;
441 stddev[Acts::eBoundQOverP] = 0.5 / p_estimate; // 50% uncertainty
442 stddev[Acts::eBoundTime] = 10.0 * Acts::UnitConstants::ns;
443
444 Acts::BoundSquareMatrix bound_cov = stddev.cwiseProduct(stddev).asDiagonal();
445
446 // Create ldmx::Track seed
447 ldmx::Track seed;
448
449 // Convert reference surface position to LDMX frame
450 Acts::Vector3 ref_ldmx = tracking::sim::utils::acts2Ldmx(ref_center);
451 seed.setPerigeeLocation(ref_ldmx[0], ref_ldmx[1], ref_ldmx[2]);
452
453 seed.setChi2(0.0);
454 seed.setNhits(seed_measurements.size());
455 seed.setNdf(0);
456 seed.setNsharedHits(0);
457 seed.setCharge(q > 0 ? 1 : -1);
458
459 // Convert to std::vector for storage
460 std::vector<double> v_seed_params(bound_params.data(),
461 bound_params.data() + bound_params.size());
462 std::vector<double> v_seed_cov;
463 tracking::sim::utils::flatCov(bound_cov, v_seed_cov);
464
465 seed.setPerigeeParameters(v_seed_params);
466 seed.setPerigeeCov(v_seed_cov);
467
468 seeds.push_back(seed);
469 return seeds;
470}
471
472std::unordered_multimap<Acts::GeometryIdentifier,
475 const std::vector<ldmx::Measurement>& measurements) {
476 std::unordered_multimap<Acts::GeometryIdentifier,
478 geo_id_sl_map;
479
480 for (size_t i = 0; i < measurements.size(); ++i) {
481 const auto& meas = measurements[i];
482 int layer = meas.getLayerID();
483
484 // Use the builder-assigned geometry ID for this layer (not the manually
485 // assigned one). This ensures the CKF Navigator's surface.geometryId()
486 // matches our source link map keys.
487 auto it = layer_geo_ids_.find(layer);
488 if (it == layer_geo_ids_.end()) {
489 ldmx_log(debug) << "No builder geometry ID for layer " << layer;
490 continue;
491 }
492
493 Acts::GeometryIdentifier geo_id = it->second;
494 acts_examples::IndexSourceLink idx_sl(geo_id, i);
495 geo_id_sl_map.insert(std::make_pair(geo_id, idx_sl));
496 }
497
498 ldmx_log(debug) << "Source link map has " << geo_id_sl_map.size()
499 << " entries from " << measurements.size() << " measurements";
500
501 return geo_id_sl_map;
502}
503
505 auto start = std::chrono::high_resolution_clock::now();
506 nevents_++;
507
508 std::vector<ldmx::Track> tracks;
509
510 // Get ECAL RecHits
511 if (!event.exists(rec_coll_name_, rec_pass_name_)) {
512 ldmx_log(debug) << "No ECAL RecHits collection found";
513 event.add(out_track_collection_, tracks);
514 return;
515 }
516
517 const std::vector<ldmx::EcalHit> ecal_hits =
518 event.getCollection<ldmx::EcalHit>(rec_coll_name_, rec_pass_name_);
519
520 ldmx_log(debug) << "Processing " << ecal_hits.size() << " ECAL hits";
521
522 // Convert hits to measurements (with parallel energy vector)
523 std::vector<double> measurement_energies;
524 auto measurements = createMeasurements(ecal_hits, measurement_energies);
525 ldmx_log(debug) << "Created " << measurements.size() << " measurements";
526
527 if (measurements.empty()) {
528 event.add(out_track_collection_, tracks);
529 return;
530 }
531
532 // Create source link map
533 auto geo_id_sl_map = makeGeoIdSourceLinkMap(measurements);
534 ldmx_log(debug) << "Source link map: " << geo_id_sl_map.size() << " entries";
535
536 // Find seed tracks
537 auto seed_tracks = findSeeds(measurements);
538 ldmx_log(info) << "Found " << seed_tracks.size() << " seed tracks";
539 nseeds_ += seed_tracks.size();
540
541 if (seed_tracks.empty()) {
542 event.add(out_track_collection_, tracks);
543 return;
544 }
545
546 // Get ACTS contexts
549 .get();
552 .get();
555 .get();
556
557 // Setup propagator options
558 Acts::PropagatorPlainOptions propagator_options(gctx, mctx);
559 propagator_options.pathLimit = std::numeric_limits<double>::max();
560 propagator_options.maxSteps = 1000;
561 propagator_options.stepping.maxStepSize = 100.0 * Acts::UnitConstants::mm;
562
563 // Setup CKF extensions
564 Acts::GainMatrixUpdater kf_updater;
565 Acts::MeasurementSelector::Config meas_sel_cfg = {
566 {Acts::GeometryIdentifier(), {{}, {max_chi2_}, {1u}}}};
567 Acts::MeasurementSelector meas_sel{meas_sel_cfg};
568
569 tracking::sim::LdmxMeasurementCalibrator calibrator{measurements};
570
571 Acts::CombinatorialKalmanFilterExtensions<TrackContainer> ckf_extensions;
572 ckf_extensions.calibrator
574 Acts::VectorMultiTrajectory>>(&calibrator);
575 ckf_extensions.updater.connect<
576 &Acts::GainMatrixUpdater::operator()<Acts::VectorMultiTrajectory>>(
577 &kf_updater);
578 ckf_extensions.measurementSelector
579 .connect<&Acts::MeasurementSelector::select<Acts::VectorMultiTrajectory>>(
580 &meas_sel);
581
582 // Setup source link accessor
583 struct SourceLinkAccIt {
584 using BaseIt = decltype(geo_id_sl_map.begin());
585 BaseIt it_;
586
587#pragma GCC diagnostic push
588#pragma GCC diagnostic ignored "-Wunused-local-typedefs"
589
590 using difference_type = typename BaseIt::difference_type;
591 using iterator_category = std::input_iterator_tag;
592 using value_type = Acts::SourceLink;
593 using pointer = value_type*;
594 using reference = value_type&;
595#pragma GCC diagnostic pop
596
597 SourceLinkAccIt& operator++() {
598 ++it_;
599 return *this;
600 }
601 bool operator==(const SourceLinkAccIt& other) const {
602 return it_ == other.it_;
603 }
604 bool operator!=(const SourceLinkAccIt& other) const {
605 return !(*this == other);
606 }
607 value_type operator*() const { return value_type{it_->second}; }
608 };
609
610 auto source_link_accessor = [&](const Acts::Surface& surface)
611 -> std::pair<SourceLinkAccIt, SourceLinkAccIt> {
612 auto [begin, end] = geo_id_sl_map.equal_range(surface.geometryId());
613 return {SourceLinkAccIt{begin}, SourceLinkAccIt{end}};
614 };
615
616 Acts::SourceLinkAccessorDelegate<SourceLinkAccIt>
617 source_link_accessor_delegate;
618 source_link_accessor_delegate
619 .connect<&decltype(source_link_accessor)::operator(),
620 decltype(source_link_accessor)>(&source_link_accessor);
621
622 // Create track container
623 Acts::VectorTrackContainer vtc;
624 Acts::VectorMultiTrajectory mtj;
625 Acts::TrackContainer tc{vtc, mtj};
626
627 // Process each seed
628 for (size_t seed_idx = 0; seed_idx < seed_tracks.size(); ++seed_idx) {
629 const auto& seed = seed_tracks[seed_idx];
630
631 // Convert seed to BoundTrackParameters
632 // The seed was created with bound parameters on the reference PlaneSurface,
633 // so we must start the CKF from that same surface (NOT a PerigeeSurface,
634 // which interprets loc0/loc1 as d0/z0 instead of plane-local coordinates).
635 Acts::BoundVector param_vec;
636 param_vec << seed.getD0(), seed.getZ0(), seed.getPhi(), seed.getTheta(),
637 seed.getQoP(), seed.getT();
638
639 ldmx_log(debug) << "Seed " << seed_idx << ": loc0=" << param_vec[0]
640 << " loc1=" << param_vec[1] << " phi=" << param_vec[2]
641 << " theta=" << param_vec[3] << " qop=" << param_vec[4];
642
643 Acts::BoundSquareMatrix cov_mat =
644 tracking::sim::utils::unpackCov(seed.getPerigeeCov());
645
646 auto part_hypo{Acts::SinglyChargedParticleHypothesis::electron()};
647 Acts::BoundTrackParameters start_params(reference_surface_, param_vec,
648 cov_mat, part_hypo);
649
650 // Setup CKF options
651 const Acts::CombinatorialKalmanFilterOptions<SourceLinkAccIt,
652 TrackContainer>
653 ckf_options(gctx, mctx, cctx, source_link_accessor_delegate,
654 ckf_extensions, propagator_options);
655
656 // Run CKF
657 auto results = ckf_->findTracks(start_params, ckf_options, tc);
658
659 if (!results.ok()) {
660 ldmx_log(debug) << "CKF failed for seed " << seed_idx << ": "
661 << results.error().message();
662 continue;
663 }
664
665 auto& tracks_from_seed = results.value();
666 ldmx_log(debug) << "CKF returned " << tracks_from_seed.size()
667 << " tracks from seed " << seed_idx;
668 for (auto& track : tracks_from_seed) {
669 // Smooth the track
670 Acts::smoothTrack(gctx, track);
671
672 // Create output track
673 ldmx::Track trk;
674
675 // Get parameters from first smoothed state
676 // (setReferenceSurface doesn't actually transform params, need actual
677 // state)
678 Acts::BoundVector smoothed_params;
679 std::shared_ptr<const Acts::Surface> smoothed_surface;
680 bool found_smoothed = false;
681
682 for (const auto& ts : track.trackStatesReversed()) {
683 if (ts.hasSmoothed()) {
684 smoothed_params = ts.smoothed();
685 smoothed_surface = ts.referenceSurface().getSharedPtr();
686 found_smoothed = true;
687 break;
688 }
689 }
690
691 if (!found_smoothed) {
692 ldmx_log(warn) << "No smoothed track state found";
693 continue;
694 }
695
696 ldmx_log(debug) << "Smoothed params: loc0=" << smoothed_params[0]
697 << " loc1=" << smoothed_params[1]
698 << " phi=" << smoothed_params[2]
699 << " theta=" << smoothed_params[3]
700 << " qop=" << smoothed_params[4];
701
702 // Convert to free parameters (in ACTS global coords)
703 Acts::FreeVector free_params = Acts::transformBoundToFreeParameters(
704 *smoothed_surface, gctx, smoothed_params);
705
706 // Convert ACTS position and momentum to LDMX coordinates
707 Acts::Vector3 pos_acts(free_params[Acts::eFreePos0],
708 free_params[Acts::eFreePos1],
709 free_params[Acts::eFreePos2]);
710 Acts::Vector3 mom_acts(free_params[Acts::eFreeDir0],
711 free_params[Acts::eFreeDir1],
712 free_params[Acts::eFreeDir2]);
713
714 Acts::Vector3 pos_ldmx = tracking::sim::utils::acts2Ldmx(pos_acts);
715 Acts::Vector3 mom_ldmx = tracking::sim::utils::acts2Ldmx(mom_acts);
716
717 double x = pos_ldmx[0];
718 double y = pos_ldmx[1];
719 double z = pos_ldmx[2];
720 double px = mom_ldmx[0];
721 double py = mom_ldmx[1];
722 double pz = mom_ldmx[2];
723
724 ldmx_log(debug) << "LDMX momentum: px=" << px << " py=" << py
725 << " pz=" << pz;
726
727 // Compute theta and phi same way as SP electron (atan2(pt, pz))
728 double pt = std::sqrt(px * px + py * py);
729 double theta = std::atan2(pt, pz);
730 double phi = std::atan2(py, px);
731 if (phi < 0)
732 phi += 2.0 * M_PI; // Shift to [0, 2π] to avoid boundary artifacts
733 double qop = free_params[Acts::eFreeQOverP];
734
735 // Compute perigee parameters from smoothed track state position.
736 // PCA-based z0 is ill-defined for forward tracks (pz >> pt), so
737 // we just use the z of the smoothed state directly.
738 double d0 = -(x * std::sin(phi) - y * std::cos(phi));
739 double z0 = z;
740 double time = free_params[Acts::eFreeTime];
741
742 Acts::BoundVector perigee_params;
743 perigee_params << d0, z0, phi, theta, qop, time;
744
745 ldmx_log(debug) << "Perigee params: d0=" << d0 << " z0=" << z0
746 << " phi=" << phi << " theta=" << theta;
747
748 // Store perigee parameters
749 trk.setPerigeeParameters(
750 tracking::sim::utils::convertActsToLdmxPars(perigee_params));
751
752 // Covariance is always available for TrackProxy
753 std::vector<double> cov_vec;
754 tracking::sim::utils::flatCov(track.covariance(), cov_vec);
755 trk.setPerigeeCov(cov_vec);
756
757 Acts::Vector3 ref_loc_ldmx =
758 tracking::sim::utils::acts2Ldmx(reference_surface_->center(gctx));
759 trk.setPerigeeLocation(ref_loc_ldmx[0], ref_loc_ldmx[1], ref_loc_ldmx[2]);
760
761 trk.setChi2(track.chi2());
762 trk.setNhits(track.nMeasurements());
763 trk.setNdf(track.nMeasurements() - 5);
764 trk.setNsharedHits(0);
765 trk.setCharge(qop > 0 ? 1 : -1);
766
767 // Add measurement indices
768 for (const auto ts : track.trackStatesReversed()) {
769 if (ts.typeFlags().test(Acts::TrackStateFlag::MeasurementFlag) &&
770 ts.hasUncalibratedSourceLink()) {
772 ts.getUncalibratedSourceLink()
773 .template get<acts_examples::IndexSourceLink>();
774 trk.addMeasurementIndex(sl.index());
775 }
776 }
777
778 // Compute track energy from RecHits
779 double track_energy = 0.0;
780
781 if (use_roc_energy_ && !roc_range_values_.empty()) {
782 // Use 68% containment cone: project track through each layer,
783 // sum energies of all RecHits within the ROC radius
784
785 // Track momentum magnitude and angle for ROC bin selection
786 double trk_p_mag = 1.0 / std::abs(smoothed_params[Acts::eBoundQOverP]);
787 double trk_theta_deg = theta * 180.0 / M_PI;
788
789 // Select ROC bin based on momentum and angle
790 std::vector<float> ele_radii(roc_range_values_[0].begin() + 4,
791 roc_range_values_[0].end());
792 for (const auto& row : roc_range_values_) {
793 float theta_min = row[0], theta_max = row[1];
794 float p_min = row[2], p_max = row[3];
795 bool inrange = true;
796 if (theta_min != -1.0f)
797 inrange = inrange && (trk_theta_deg >= theta_min);
798 if (theta_max != -1.0f)
799 inrange = inrange && (trk_theta_deg < theta_max);
800 if (p_min != -1.0f) inrange = inrange && (trk_p_mag >= p_min);
801 if (p_max != -1.0f) inrange = inrange && (trk_p_mag < p_max);
802 if (inrange) {
803 ele_radii.assign(row.begin() + 4, row.end());
804 }
805 }
806
807 // Project track through each ECAL layer (straight line in LDMX coords)
808 // Track at (x, y, z) with direction (px, py, pz) normalized
809 for (const auto& hit : ecal_hits) {
810 if (hit.isNoise()) continue;
811 ldmx::EcalID ecal_id(hit.getID());
812 int layer = ecal_id.layer();
813 if (layer < 0 || layer >= static_cast<int>(ele_radii.size()))
814 continue;
815
816 auto [hx, hy, hz] = geometry_->getPosition(ecal_id);
817
818 // Project track to this layer's z
819 double dz = hz - z;
820 double proj_x = x + (px / pz) * dz;
821 double proj_y = y + (py / pz) * dz;
822
823 // Distance from projected track to hit
824 double dx = hx - proj_x;
825 double dy = hy - proj_y;
826 double dist = std::sqrt(dx * dx + dy * dy);
827
828 if (dist < ele_radii[layer]) {
829 track_energy += hit.getEnergy();
830 }
831 }
832 } else {
833 // Fallback: sum on-track hit energies only
834 for (const auto ts : track.trackStatesReversed()) {
835 if (ts.typeFlags().test(Acts::TrackStateFlag::MeasurementFlag) &&
836 ts.hasUncalibratedSourceLink()) {
838 ts.getUncalibratedSourceLink()
839 .template get<acts_examples::IndexSourceLink>();
840 track_energy += measurement_energies[sl.index()];
841 }
842 }
843 }
844
845 // Use energy as track momentum (E ≈ p for relativistic electrons)
846 double track_p = track_energy;
847 qop = (track_p > 0) ? -1.0 / track_p : 0.0;
848 perigee_params[Acts::eBoundQOverP] = qop;
849 trk.setPerigeeParameters(
850 tracking::sim::utils::convertActsToLdmxPars(perigee_params));
851
852 ldmx_log(debug) << "Track energy from RecHits: " << track_energy
853 << " MeV, q/p=" << qop;
854
855 tracks.push_back(trk);
856 ntracks_++;
857 }
858 }
859
860 ldmx_log(info) << "Found " << tracks.size() << " fitted tracks";
861
862 // Add to event
863 event.add(out_track_collection_, tracks);
864
865 auto end = std::chrono::high_resolution_clock::now();
866 auto diff = end - start;
867 processing_time_ += std::chrono::duration<double, std::milli>(diff).count();
868}
869
871 ldmx_log(info) << "EcalTrackFinderProcessor Statistics:";
872 ldmx_log(info) << " Events processed: " << nevents_;
873 ldmx_log(info) << " Total seeds: " << nseeds_;
874 ldmx_log(info) << " Total tracks: " << ntracks_;
875 ldmx_log(info) << " Avg tracks/event: "
876 << (nevents_ > 0 ? (double)ntracks_ / nevents_ : 0);
877 ldmx_log(info) << " Avg time/event: "
878 << (nevents_ > 0 ? processing_time_ / nevents_ : 0) << " ms";
879}
880
881} // namespace ecal
882
Processor that uses ACTS to fit tracks through ECAL hits.
#define DECLARE_PRODUCER(CLASS)
Macro which allows the framework to construct a producer given its name during configuration.
Uses ACTS framework to fit tracks through ECAL hits with zero B-field.
EcalTrackFinderProcessor(const std::string &name, framework::Process &process)
Constructor.
void onNewRun(const ldmx::RunHeader &) override
Initialize ACTS tracking objects once the detector geometry is known.
std::vector< ldmx::Measurement > createMeasurements(const std::vector< ldmx::EcalHit > &hits, std::vector< double > &energies)
Create ACTS measurement objects from ECAL hits.
std::unordered_multimap< Acts::GeometryIdentifier, acts_examples::IndexSourceLink > makeGeoIdSourceLinkMap(const std::vector< ldmx::Measurement > &measurements)
Create geometry ID to source link map for CKF.
std::tuple< Acts::Vector3, Acts::Vector3, double > fitStraightLine(const std::vector< Acts::Vector3 > &points)
Fit straight line through 3D points Returns: (position, direction, RMS residual)
void produce(framework::Event &event) override
Process event to find ECAL tracks.
void configure(framework::config::Parameters &parameters) override
Configure the processor.
std::vector< ldmx::Track > findSeeds(const std::vector< ldmx::Measurement > &measurements)
Find seed tracks via straight-line fitting.
void createEcalSurfaces()
Create unbounded plane surfaces at each ECAL layer.
void onProcessEnd() override
Print statistics.
const T & getCondition(const std::string &condition_name)
Access a conditions object for the current event.
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:105
Class which represents the process under execution.
Definition Process.h:37
Class encapsulating parameters for configuring a processor.
Definition Parameters.h:29
const T & get(const std::string &name) const
Retrieve the parameter of the given name.
Definition Parameters.h:78
std::tuple< double, double, double > getPosition(EcalID id) const
Get a cell's position from its ID number.
double getEcalFrontZ() const
Get the z-coordinate of the Ecal face.
int getNumLayers() const
Get the number of layers in the Ecal Geometry.
double getZPosition(int layer) const
Get the z-coordinate given the layer id.
Stores reconstructed hit information from the ECAL.
Definition EcalHit.h:19
Extension of DetectorID providing access to ECal layers and cell numbers in a hex grid.
Definition EcalID.h:20
int layer() const
Get the value of the layer field from the ID.
Definition EcalID.h:99
void setLocalPosition(const float &meas_u, const float &meas_v)
Set the local position i.e.
Definition Measurement.h:60
void setLayerID(const int &layer_id)
Set the layer ID of the sensor where this measurement took place.
void setGlobalPosition(const float &meas_x, const float &meas_y, const float &meas_z)
Set the global position i.e.
Definition Measurement.h:41
void setLocalCovariance(const float &cov_uu, const float &cov_vv)
Set cov(U,U) and cov(V, V).
Definition Measurement.h:76
void setTime(const float &meas_t)
Set the measurement time in ns.
Definition Measurement.h:92
Run-specific configuration and data stored in its own output TTree alongside the event TTree in the o...
Definition RunHeader.h:57
Implementation of a track object.
Definition Track.h:53
static const std::string NAME
Conditions object name.
static const std::string NAME
Conditions object name.
static const std::string NAME
Conditions object name.
void calibrate(const Acts::GeometryContext &, const Acts::CalibrationContext &, const Acts::SourceLink &genericSourceLink, typename traj_t::TrackStateProxy trackState) const
Find the measurement corresponding to the source link.