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/BoundTrackParameters.hpp"
18#include "Acts/EventData/MultiTrajectory.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/ActorList.hpp"
27#include "Acts/Propagator/MaterialInteractor.hpp"
28#include "Acts/Propagator/StandardAborters.hpp"
29#include "Acts/Propagator/detail/SteppingLogger.hpp"
30#include "Acts/Surfaces/PerigeeSurface.hpp"
31#include "Acts/TrackFinding/MeasurementSelector.hpp"
32#include "Acts/Utilities/Logger.hpp"
33#include "Acts/Utilities/TrackHelpers.hpp"
34
35// C++
36#include <algorithm>
37#include <chrono>
38#include <cmath>
39#include <fstream>
40#include <sstream>
41
42namespace ecal {
43
45 framework::Process& process)
46 : Producer(name, process) {
47 // Setup surface rotation: u=+Y, v=+Z, w=+X (beam direction)
48 surf_rotation_ = Acts::RotationMatrix3::Zero();
49 surf_rotation_(1, 0) = 1; // u along Y
50 surf_rotation_(2, 1) = 1; // v along Z
51 surf_rotation_(0, 2) = 1; // w along X (beam/normal)
52}
53
56 rec_coll_name_ = parameters.get<std::string>("rec_coll_name");
57 rec_pass_name_ = parameters.get<std::string>("rec_pass_name");
58 out_track_collection_ = parameters.get<std::string>("out_track_collection");
59
60 min_hits_ = parameters.get<int>("min_hits");
61 max_chi2_ = parameters.get<double>("max_chi2");
62 cell_resolution_ = parameters.get<double>("cell_resolution");
63 debug_ = parameters.get<bool>("debug");
64
65 max_seed_rms_ = parameters.get<double>("max_seed_rms");
66 min_momentum_ = parameters.get<double>("min_momentum");
67 max_momentum_ = parameters.get<double>("max_momentum");
68
69 use_roc_energy_ = parameters.get<bool>("use_roc_energy");
70 if (use_roc_energy_) {
71 roc_file_name_ = parameters.get<std::string>("roc_file");
72 std::ifstream rocfile(roc_file_name_);
73 if (!rocfile.good()) {
74 EXCEPTION_RAISE("EcalTrackFinderProcessor",
75 "ROC file '" + roc_file_name_ + "' does not exist!");
76 }
77 std::string line, value;
78 // Skip header line
79 std::getline(rocfile, line);
80 while (std::getline(rocfile, line)) {
81 std::stringstream ss(line);
82 std::vector<float> values;
83 while (std::getline(ss, value, ',')) {
84 values.push_back(value.empty() ? -1.0f : std::stof(value));
85 }
86 roc_range_values_.push_back(values);
87 }
88 ldmx_log(info) << "Loaded ROC file with " << roc_range_values_.size()
89 << " bins";
90 }
91}
92
94 // Geometry is available here: EcalGeometryProvider::onNewRun has already
95 // populated detector_geometry_ before processors receive onNewRun.
97 ldmx::EcalGeometry::CONDITIONS_OBJECT_NAME);
98
99 // Create ECAL layer surfaces
100 layer_surfaces_.clear();
102
103 // Setup zero magnetic field
104 Acts::Vector3 b_field(0., 0., 0.);
105 auto zero_b_field = std::make_shared<Acts::ConstantBField>(b_field);
106
107 // Build ACTS tracking geometry for the ECAL using CuboidVolumeBuilder
108 // Each ECAL layer becomes a sensitive layer in the tracking geometry
109
112 .get();
113
114 // Get ECAL extent in ACTS coordinates
115 double ecal_front_z = geometry_->getEcalFrontZ();
116 double ecal_back_z =
117 geometry_->getZPosition(geometry_->getNumLayers() - 1) + 50.0;
118 Acts::Vector3 front_acts =
119 tracking::sim::utils::ldmx2Acts(Acts::Vector3(0.0, 0.0, ecal_front_z));
120 Acts::Vector3 back_acts =
121 tracking::sim::utils::ldmx2Acts(Acts::Vector3(0.0, 0.0, ecal_back_z));
122
123 // Create layer configurations - one per ECAL layer.
124 // IMPORTANT: layer_configs must be in ascending x-order so LayerArrayCreator
125 // gets a monotone sequence for BinningType::arbitrary along AxisX.
126 std::vector<Acts::CuboidVolumeBuilder::LayerConfig> layer_configs;
127 double clearance = 1.0; // mm envelope around each layer surface
128
129 for (auto& [layer, surface] : layer_surfaces_) {
130 Acts::CuboidVolumeBuilder::LayerConfig lcfg;
131 lcfg.surfaces = {surface};
132 lcfg.envelopeX = std::array<double, 2>{clearance, clearance};
133 lcfg.active = true;
134 layer_configs.push_back(lcfg);
135 }
136
137 // Volume config
138 Acts::Vector3 volume_center = 0.5 * (front_acts + back_acts);
139 double x_length = std::abs(back_acts.x() - front_acts.x()) + 20.0;
140
141 Acts::CuboidVolumeBuilder::VolumeConfig ecal_vol_cfg;
142 ecal_vol_cfg.position = volume_center;
143 ecal_vol_cfg.length = {x_length, 1000.0, 1000.0}; // generous transverse size
144 ecal_vol_cfg.name = "EcalVolume";
145 ecal_vol_cfg.layerCfg = layer_configs;
146 ecal_vol_cfg.volumeMaterial =
147 std::make_shared<Acts::HomogeneousVolumeMaterial>(
148 Acts::Material::Vacuum());
149
150 // Build the tracking geometry
151 Acts::CuboidVolumeBuilder cvb;
152 Acts::CuboidVolumeBuilder::Config cvb_cfg;
153 cvb_cfg.position = volume_center;
154 cvb_cfg.length = {x_length + 20.0, 1020.0, 1020.0};
155 cvb_cfg.volumeCfg = {ecal_vol_cfg};
156 cvb.setConfig(cvb_cfg);
157
158 Acts::TrackingGeometryBuilder::Config tgb_cfg;
159 tgb_cfg.trackingVolumeBuilders.push_back(
160 [=](const auto& cxt, const auto& inner, const auto&) {
161 return cvb.trackingVolume(cxt, inner, nullptr);
162 });
163
164 Acts::TrackingGeometryBuilder tgb(tgb_cfg);
165 tracking_geometry_ = tgb.trackingGeometry(gctx);
166
167 // Extract the geometry IDs that the builder assigned to our surfaces.
168 // CuboidVolumeBuilder/TrackingGeometryBuilder reassign GeometryIdentifiers
169 // based on the volume/layer/sensitive hierarchy, overwriting our manual IDs.
170 // We must use these builder-assigned IDs when creating the source link map,
171 // because the CKF Navigator will see these IDs when it reaches a surface.
172 layer_geo_ids_.clear();
173 tracking_geometry_->visitSurfaces([&](const Acts::Surface* surface) {
174 if (!surface) return;
175 // Only care about sensitive surfaces (those with a sensitive ID)
176 if (surface->geometryId().sensitive() == 0) return;
177
178 // CuboidVolumeBuilder creates new PlaneSurface objects inside the geometry
179 // rather than using our originals. Mark them sensitive here so the CKF
180 // actor doesn't skip them as passive surfaces.
181 const_cast<Acts::Surface*>(surface)->assignIsSensitive(true);
182
183 // Match to ECAL layer by z position (LDMX frame)
184 Acts::Vector3 center_ldmx =
185 tracking::sim::utils::acts2Ldmx(surface->center(gctx));
186 double z_ldmx = center_ldmx[2];
187
188 for (int layer = 0; layer < geometry_->getNumLayers(); ++layer) {
189 double layer_z = geometry_->getZPosition(layer);
190 if (std::abs(z_ldmx - layer_z) < 0.1) { // 0.1 mm tolerance
191 layer_geo_ids_[layer] = surface->geometryId();
192 ldmx_log(debug) << "ECAL layer " << layer << " -> builder geo_id: vol="
193 << surface->geometryId().volume()
194 << " lay=" << surface->geometryId().layer()
195 << " sen=" << surface->geometryId().sensitive();
196 break;
197 }
198 }
199 });
200
201 ldmx_log(info) << "Mapped " << layer_geo_ids_.size()
202 << " ECAL layers to builder-assigned geometry IDs";
203
204 // Setup stepper with zero B-field
205 const auto stepper = Acts::EigenStepper<>{zero_b_field};
206
207 auto acts_logging_level =
208 debug_ ? Acts::Logging::VERBOSE : Acts::Logging::FATAL;
209
210 // Setup navigator with tracking geometry
211 Acts::Navigator::Config nav_cfg{tracking_geometry_};
212 nav_cfg.resolveSensitive = true;
213 nav_cfg.resolvePassive = false;
214 nav_cfg.resolveMaterial = false;
215 const Acts::Navigator navigator(
216 nav_cfg,
217 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 = Acts::transformFreeToBoundParameters(
443 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()
553 << " measurements";
554
555 // Find seed tracks
556 auto seed_tracks = findSeeds(measurements);
557 ldmx_log(info) << "Found " << seed_tracks.size() << " seed tracks";
558 nseeds_ += seed_tracks.size();
559
560 if (seed_tracks.empty()) {
561 event.add(out_track_collection_, tracks);
562 return;
563 }
564
565 // Get ACTS contexts
568 .get();
571 .get();
574 .get();
575
576 // Setup propagator options
577 Acts::PropagatorPlainOptions propagator_options(gctx, mctx);
578 propagator_options.pathLimit = std::numeric_limits<double>::max();
579 propagator_options.maxSteps = 1000;
580 propagator_options.stepping.maxStepSize = 100.0 * Acts::UnitConstants::mm;
581
582 // Setup CKF extensions
583 Acts::GainMatrixUpdater kf_updater;
584 Acts::MeasurementSelector::Config meas_sel_cfg = {
585 {Acts::GeometryIdentifier(), {{}, {max_chi2_}, {1u}}}};
586 Acts::MeasurementSelector meas_sel{meas_sel_cfg};
587
588 tracking::sim::LdmxMeasurementCalibrator calibrator{measurements};
589
590 // Setup source link accessor iterator type and lambda
591 struct SourceLinkAccIt {
592 using BaseIt = decltype(geo_id_sl_map.begin());
593 BaseIt it_;
594
595#pragma GCC diagnostic push
596#pragma GCC diagnostic ignored "-Wunused-local-typedefs"
597
598 using difference_type = typename BaseIt::difference_type;
599 using iterator_category = std::input_iterator_tag;
600 using value_type = Acts::SourceLink;
601 using pointer = value_type*;
602 using reference = value_type&;
603#pragma GCC diagnostic pop
604
605 SourceLinkAccIt& operator++() {
606 ++it_;
607 return *this;
608 }
609 bool operator==(const SourceLinkAccIt& other) const {
610 return it_ == other.it_;
611 }
612 bool operator!=(const SourceLinkAccIt& other) const {
613 return !(*this == other);
614 }
615 value_type operator*() const { return value_type{it_->second}; }
616 };
617
618 auto source_link_accessor = [&](const Acts::Surface& surface)
619 -> std::pair<SourceLinkAccIt, SourceLinkAccIt> {
620 auto [begin, end] = geo_id_sl_map.equal_range(surface.geometryId());
621 return {SourceLinkAccIt{begin}, SourceLinkAccIt{end}};
622 };
623
624 // v46: calibrator and measurementSelector moved to TrackStateCreator
625 Acts::TrackStateCreator<SourceLinkAccIt, TrackContainer> track_state_creator;
626 track_state_creator.sourceLinkAccessor
627 .connect<&decltype(source_link_accessor)::operator(),
628 decltype(source_link_accessor)>(&source_link_accessor);
629 track_state_creator.calibrator
631 Acts::VectorMultiTrajectory>>(&calibrator);
632 track_state_creator.measurementSelector
633 .connect<&Acts::MeasurementSelector::select<Acts::VectorMultiTrajectory>>(
634 &meas_sel);
635
636 Acts::CombinatorialKalmanFilterExtensions<TrackContainer> ckf_extensions;
637 ckf_extensions.updater.connect<
638 &Acts::GainMatrixUpdater::operator()<Acts::VectorMultiTrajectory>>(
639 &kf_updater);
640 ckf_extensions.createTrackStates.connect<&Acts::TrackStateCreator<
641 SourceLinkAccIt, TrackContainer>::createTrackStates>(
642 &track_state_creator);
643
644 // Create track container
645 Acts::VectorTrackContainer vtc;
646 Acts::VectorMultiTrajectory mtj;
647 Acts::TrackContainer tc{vtc, mtj};
648
649 // Process each seed
650 for (size_t seed_idx = 0; seed_idx < seed_tracks.size(); ++seed_idx) {
651 const auto& seed = seed_tracks[seed_idx];
652
653 // Convert seed to BoundTrackParameters.
654 // Start from layer_surfaces_[0] which is part of the tracking geometry and
655 // has associatedLayer() set — this lets the Navigator use the fast
656 // initialization path and correctly traverse all 32 ECAL layers.
657 Acts::BoundVector param_vec;
658 param_vec << seed.getD0(), seed.getZ0(), seed.getPhi(), seed.getTheta(),
659 seed.getQoP(), seed.getT();
660
661 ldmx_log(debug) << "Seed " << seed_idx << ": loc0=" << param_vec[0]
662 << " loc1=" << param_vec[1] << " phi=" << param_vec[2]
663 << " theta=" << param_vec[3] << " qop=" << param_vec[4];
664
665 Acts::BoundMatrix cov_mat =
666 tracking::sim::utils::unpackCov(seed.getPerigeeCov());
667
668 auto part_hypo{Acts::ParticleHypothesis::electron()};
669 auto& layer0_surface = layer_surfaces_.begin()->second;
670 Acts::BoundTrackParameters start_params(layer0_surface, param_vec,
671 cov_mat, part_hypo);
672
673 // Setup CKF options
674 const Acts::CombinatorialKalmanFilterOptions<TrackContainer> ckf_options(
675 gctx, mctx, cctx, ckf_extensions, propagator_options);
676
677 // Run CKF
678 auto results = ckf_->findTracks(start_params, ckf_options, tc);
679
680 if (!results.ok()) {
681 ldmx_log(debug) << "CKF failed for seed " << seed_idx << ": "
682 << results.error().message();
683 continue;
684 }
685
686 auto& tracks_from_seed = results.value();
687 ldmx_log(info) << "CKF returned " << tracks_from_seed.size()
688 << " tracks from seed " << seed_idx;
689 for (auto& track : tracks_from_seed) {
690 // Count track state types before smoothing
691 int n_meas = 0, n_holes = 0, n_outliers = 0, n_total = 0;
692 for (const auto& ts : track.trackStatesReversed()) {
693 ++n_total;
694 if (ts.typeFlags().isMeasurement()) ++n_meas;
695 if (ts.typeFlags().isHole()) ++n_holes;
696 if (ts.typeFlags().isOutlier()) ++n_outliers;
697 }
698 ldmx_log(info) << "Track states: total=" << n_total
699 << " meas=" << n_meas << " holes=" << n_holes
700 << " outliers=" << n_outliers;
701
702 // Smooth the track
703 auto smooth_result = Acts::smoothTrack(gctx, track);
704 if (!smooth_result.ok()) {
705 ldmx_log(warn) << "smoothTrack failed: "
706 << smooth_result.error().message();
707 continue;
708 }
709
710 // Create output track
711 ldmx::Track trk;
712
713 // Get parameters from first smoothed state
714 // (setReferenceSurface doesn't actually transform params, need actual
715 // state)
716 Acts::BoundVector smoothed_params;
717 std::shared_ptr<const Acts::Surface> smoothed_surface;
718 bool found_smoothed = false;
719
720 for (const auto& ts : track.trackStatesReversed()) {
721 if (ts.hasSmoothed()) {
722 smoothed_params = ts.smoothed();
723 smoothed_surface = ts.referenceSurface().getSharedPtr();
724 found_smoothed = true;
725 break;
726 }
727 }
728
729 if (!found_smoothed) {
730 ldmx_log(warn) << "No smoothed track state found after smoothing";
731 continue;
732 }
733
734 ldmx_log(debug) << "Smoothed params: loc0=" << smoothed_params[0]
735 << " loc1=" << smoothed_params[1]
736 << " phi=" << smoothed_params[2]
737 << " theta=" << smoothed_params[3]
738 << " qop=" << smoothed_params[4];
739
740 // Convert to free parameters (in ACTS global coords)
741 Acts::FreeVector free_params = Acts::transformBoundToFreeParameters(
742 *smoothed_surface, gctx, smoothed_params);
743
744 // Convert ACTS position and momentum to LDMX coordinates
745 Acts::Vector3 pos_acts(free_params[Acts::eFreePos0],
746 free_params[Acts::eFreePos1],
747 free_params[Acts::eFreePos2]);
748 Acts::Vector3 mom_acts(free_params[Acts::eFreeDir0],
749 free_params[Acts::eFreeDir1],
750 free_params[Acts::eFreeDir2]);
751
752 Acts::Vector3 pos_ldmx = tracking::sim::utils::acts2Ldmx(pos_acts);
753 Acts::Vector3 mom_ldmx = tracking::sim::utils::acts2Ldmx(mom_acts);
754
755 double x = pos_ldmx[0];
756 double y = pos_ldmx[1];
757 double z = pos_ldmx[2];
758 double px = mom_ldmx[0];
759 double py = mom_ldmx[1];
760 double pz = mom_ldmx[2];
761
762 ldmx_log(debug) << "LDMX momentum: px=" << px << " py=" << py
763 << " pz=" << pz;
764
765 // Compute theta and phi same way as SP electron (atan2(pt, pz))
766 double pt = std::sqrt(px * px + py * py);
767 double theta = std::atan2(pt, pz);
768 double phi = std::atan2(py, px);
769 if (phi < 0)
770 phi += 2.0 * M_PI; // Shift to [0, 2π] to avoid boundary artifacts
771 double qop = free_params[Acts::eFreeQOverP];
772
773 // Compute perigee parameters from smoothed track state position.
774 // PCA-based z0 is ill-defined for forward tracks (pz >> pt), so
775 // we just use the z of the smoothed state directly.
776 double d0 = -(x * std::sin(phi) - y * std::cos(phi));
777 double z0 = z;
778 double time = free_params[Acts::eFreeTime];
779
780 Acts::BoundVector perigee_params;
781 perigee_params << d0, z0, phi, theta, qop, time;
782
783 ldmx_log(debug) << "Perigee params: d0=" << d0 << " z0=" << z0
784 << " phi=" << phi << " theta=" << theta;
785
786 // Store perigee parameters
787 trk.setPerigeeParameters(
788 tracking::sim::utils::convertActsToLdmxPars(perigee_params));
789
790 // Covariance is always available for TrackProxy
791 std::vector<double> cov_vec;
792 tracking::sim::utils::flatCov(track.covariance(), cov_vec);
793 trk.setPerigeeCov(cov_vec);
794
795 Acts::Vector3 ref_loc_ldmx =
796 tracking::sim::utils::acts2Ldmx(layer_surfaces_.begin()->second->center(gctx));
797 trk.setPerigeeLocation(ref_loc_ldmx[0], ref_loc_ldmx[1], ref_loc_ldmx[2]);
798
799 trk.setChi2(track.chi2());
800 trk.setNhits(track.nMeasurements());
801 trk.setNdf(track.nMeasurements() - 5);
802 trk.setNsharedHits(0);
803 trk.setCharge(qop > 0 ? 1 : -1);
804
805 // Add measurement indices
806 for (const auto ts : track.trackStatesReversed()) {
807 if (ts.typeFlags().isMeasurement() && ts.hasUncalibratedSourceLink()) {
808 Acts::SourceLink usl = ts.getUncalibratedSourceLink();
811 trk.addMeasurementIndex(sl.index());
812 }
813 }
814
815 // Compute track energy from RecHits
816 double track_energy = 0.0;
817
818 if (use_roc_energy_ && !roc_range_values_.empty()) {
819 // Use 68% containment cone: project track through each layer,
820 // sum energies of all RecHits within the ROC radius
821
822 // Track momentum magnitude and angle for ROC bin selection
823 double trk_p_mag = 1.0 / std::abs(smoothed_params[Acts::eBoundQOverP]);
824 double trk_theta_deg = theta * 180.0 / M_PI;
825
826 // Select ROC bin based on momentum and angle
827 std::vector<float> ele_radii(roc_range_values_[0].begin() + 4,
828 roc_range_values_[0].end());
829 for (const auto& row : roc_range_values_) {
830 float theta_min = row[0], theta_max = row[1];
831 float p_min = row[2], p_max = row[3];
832 bool inrange = true;
833 if (theta_min != -1.0f)
834 inrange = inrange && (trk_theta_deg >= theta_min);
835 if (theta_max != -1.0f)
836 inrange = inrange && (trk_theta_deg < theta_max);
837 if (p_min != -1.0f) inrange = inrange && (trk_p_mag >= p_min);
838 if (p_max != -1.0f) inrange = inrange && (trk_p_mag < p_max);
839 if (inrange) {
840 ele_radii.assign(row.begin() + 4, row.end());
841 }
842 }
843
844 // Project track through each ECAL layer (straight line in LDMX coords)
845 // Track at (x, y, z) with direction (px, py, pz) normalized
846 for (const auto& hit : ecal_hits) {
847 if (hit.isNoise()) continue;
848 ldmx::EcalID ecal_id(hit.getID());
849 int layer = ecal_id.layer();
850 if (layer < 0 || layer >= static_cast<int>(ele_radii.size()))
851 continue;
852
853 auto [hx, hy, hz] = geometry_->getPosition(ecal_id);
854
855 // Project track to this layer's z
856 double dz = hz - z;
857 double proj_x = x + (px / pz) * dz;
858 double proj_y = y + (py / pz) * dz;
859
860 // Distance from projected track to hit
861 double dx = hx - proj_x;
862 double dy = hy - proj_y;
863 double dist = std::sqrt(dx * dx + dy * dy);
864
865 if (dist < ele_radii[layer]) {
866 track_energy += hit.getEnergy();
867 }
868 }
869 } else {
870 // Fallback: sum on-track hit energies only
871 for (const auto ts : track.trackStatesReversed()) {
872 if (ts.typeFlags().isMeasurement() &&
873 ts.hasUncalibratedSourceLink()) {
874 Acts::SourceLink usl = ts.getUncalibratedSourceLink();
877 track_energy += measurement_energies[sl.index()];
878 }
879 }
880 }
881
882 // Use energy as track momentum (E ≈ p for relativistic electrons)
883 double track_p = track_energy;
884 qop = (track_p > 0) ? -1.0 / track_p : 0.0;
885 perigee_params[Acts::eBoundQOverP] = qop;
886 trk.setPerigeeParameters(
887 tracking::sim::utils::convertActsToLdmxPars(perigee_params));
888
889 ldmx_log(debug) << "Track energy from RecHits: " << track_energy
890 << " MeV, q/p=" << qop;
891
892 tracks.push_back(trk);
893 ntracks_++;
894 }
895 }
896
897 ldmx_log(info) << "Found " << tracks.size() << " fitted tracks";
898
899 // Add to event
900 event.add(out_track_collection_, tracks);
901
902 auto end = std::chrono::high_resolution_clock::now();
903 auto diff = end - start;
904 processing_time_ += std::chrono::duration<double, std::milli>(diff).count();
905}
906
908 ldmx_log(info) << "EcalTrackFinderProcessor Statistics:";
909 ldmx_log(info) << " Events processed: " << nevents_;
910 ldmx_log(info) << " Total seeds: " << nseeds_;
911 ldmx_log(info) << " Total tracks: " << ntracks_;
912 ldmx_log(info) << " Avg tracks/event: "
913 << (nevents_ > 0 ? (double)ntracks_ / nevents_ : 0);
914 ldmx_log(info) << " Avg time/event: "
915 << (nevents_ > 0 ? processing_time_ / nevents_ : 0) << " ms";
916}
917
918} // namespace ecal
919
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.