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