LDMX Software
DigitizationProcessor.cxx
1#include "Tracking/Reco/DigitizationProcessor.h"
2
3#include <algorithm>
4#include <fstream>
5
6#include "Acts/Definitions/Units.hpp"
7#include "Acts/Surfaces/Surface.hpp"
9#include "Tracking/Digitization/ChargeCarrier.h"
10#include "Tracking/Digitization/SiStripConstants.h"
11#include "Tracking/Sim/TrackingUtils.h"
12#include "Tracking/geo/DetectorElement.h"
13
14using namespace framework;
15
16namespace tracking::reco {
17
18DigitizationProcessor::DigitizationProcessor(const std::string& name,
19 framework::Process& process)
20 : TrackingGeometryUser(name, process) {}
21
22void DigitizationProcessor::onProcessStart() {
23 normal_ = std::make_shared<std::normal_distribution<float>>(0., 1.);
24
25 if (use_charge_digitization_) {
26 strip_digitizer_ =
27 std::make_unique<tracking::digitization::SiStripDigitizer>(
28 sensor_params_);
29 ldmx_log(info) << "Charge digitization enabled."
30 << " thickness=from geometry"
31 << " sense_pitch=" << tracking::digitization::SENSE_PITCH_MM
32 << " mm" << " readout_pitch="
33 << tracking::digitization::READOUT_PITCH_MM << " mm"
34 << " Vbias=" << sensor_params_.bias_voltage << " V"
35 << " Vdep=" << sensor_params_.depletion_voltage << " V"
36 << " bulk=" << (sensor_params_.is_n_type ? "n" : "p")
37 << "-type" << " e_lorentz_tan="
38 << sensor_params_.electron_lorentz_tangent
39 << " h_lorentz_tan=" << sensor_params_.hole_lorentz_tangent
40 << " trapping=" << sensor_params_.trapping
41 << " noise=" << sensor_params_.noise_electrons << " e-"
42 << " threshold=" << sensor_params_.threshold_electrons
43 << " e-"
44 << " n_segments_min=" << sensor_params_.n_segments_min
45 << " granularity=" << sensor_params_.deposition_granularity;
46
48 std::string(tracking::digitization::PULSE_SHAPE_NAME),
49 tracking::digitization::PEAKING_TIME_NS,
50 tracking::digitization::SECOND_TIME_CONST_NS);
51 ldmx_log(info) << "Pulse shaping: shape="
52 << tracking::digitization::PULSE_SHAPE_NAME
53 << " tp=" << tracking::digitization::PEAKING_TIME_NS
54 << " ns"
55 << " n_samples=" << tracking::digitization::N_SAMPLES
56 << " sampling_interval="
57 << tracking::digitization::SAMPLING_INTERVAL_NS << " ns"
58 << " t0_offset=" << tracking::digitization::T0_OFFSET_NS
59 << " ns";
60
61 if (field_map_.empty()) {
62 ldmx_log(debug) << "field_map not set; will auto-load from GDML";
63 }
64 if (use_lorentz_)
65 buildLorentzCache();
66 else
67 ldmx_log(info)
68 << "Lorentz angle correction disabled (use_lorentz=false).";
69 }
70
71 // Dump all ACTS surfaces to CSV for geometry verification.
72 if (!dump_geo_csv_.empty()) {
73 std::ofstream csv(dump_geo_csv_);
74 csv << "layer_id,cx,cy,cz,Ux,Uy,Uz,Vx,Vy,Vz,Wx,Wy,Wz\n";
75 for (const auto& [layer_id, surface] : geometry().layer_surface_map_) {
76 const auto& xf = surface->localToGlobalTransform(geometryContext());
77 const auto ctr = xf.translation(); // centre [mm in Acts units]
78 const auto r = xf.rotation();
79 const auto u = r.col(0);
80 const auto v = r.col(1);
81 const auto w = r.col(2);
82 csv << layer_id << "," << ctr.x() << "," << ctr.y() << "," << ctr.z()
83 << "," << u.x() << "," << u.y() << "," << u.z() << "," << v.x() << ","
84 << v.y() << "," << v.z() << "," << w.x() << "," << w.y() << ","
85 << w.z() << "\n";
86 }
87 ldmx_log(info) << "Surface geometry written to " << dump_geo_csv_ << " ("
88 << geometry().layer_surface_map_.size() << " surfaces)";
89 }
90}
91
92void DigitizationProcessor::configure(
94 hit_collection_ =
95 parameters.get<std::string>("hit_collection", "TaggerSimHits");
96
97 tracker_hit_passname_ = parameters.get<std::string>("tracker_hit_passname");
98 out_collection_ =
99 parameters.get<std::string>("out_collection", "OutputMeasuements");
100 min_e_dep_ = parameters.get<double>("min_e_dep", 0.05);
101 track_id_ = parameters.get<int>("track_id", -1);
102 do_smearing_ = parameters.get<bool>("do_smearing", true);
103 sigma_u_ = parameters.get<double>("sigma_u", 0.01);
104 sigma_v_ = parameters.get<double>("sigma_v", 0.);
105 merge_hits_ = parameters.get<bool>("merge_hits", false);
106
107 // Mode 1: charge digitization parameters
108 use_charge_digitization_ =
109 parameters.get<bool>("use_charge_digitization", false);
110
111 if (use_charge_digitization_) {
112 sensor_params_.bias_voltage = parameters.get<double>("bias_voltage", 200.0);
113 sensor_params_.depletion_voltage =
114 parameters.get<double>("depletion_voltage", 70.0);
115 sensor_params_.temperature = parameters.get<double>("temperature", 300.0);
116 sensor_params_.noise_electrons =
117 parameters.get<double>("noise_electrons", 1000.0);
118 sensor_params_.threshold_electrons =
119 parameters.get<double>("threshold_electrons", 3000.0);
120 // Fixed sensor properties — not user-configurable.
121 // LDMX (and HPS) use n-type bulk with hole-side readout.
122 sensor_params_.is_n_type = true;
123 sensor_params_.electron_side_readout = false;
124 sensor_params_.hole_side_readout = true;
125 use_lorentz_ = parameters.get<bool>("use_lorentz", true);
126 sensor_params_.electron_lorentz_tangent =
127 parameters.get<double>("electron_lorentz_tangent", 0.0);
128 sensor_params_.hole_lorentz_tangent =
129 parameters.get<double>("hole_lorentz_tangent", 0.0);
130 sensor_params_.trapping = parameters.get<double>("trapping", 0.0);
131 sensor_params_.deposition_granularity =
132 parameters.get<double>("deposition_granularity", 0.10);
133 sensor_params_.n_segments_min = parameters.get<int>("n_segments_min", 5);
134 // n_readout_strips is fixed by the sensor geometry constant
135 // N_READOUT_STRIPS.
136
137 out_raw_collection_ = parameters.get<std::string>("out_raw_collection", "");
138 field_map_ = parameters.get<std::string>("field_map", "");
139 }
140
141 dump_geo_csv_ = parameters.get<std::string>("dump_geo_csv", "");
142}
143
144void DigitizationProcessor::buildLorentzCache() {
145 if (!use_charge_digitization_) return;
146
147 if (field_map_.empty())
148 loadBField();
149 else
150 loadBField(field_map_);
151
152 // Low-field (Hall) mobility from the Canali model [cm²/(V·s)] → [m²/(V·s)]
153 const double t = sensor_params_.temperature;
154 auto carrier_e = tracking::digitization::getCarrier(-1);
155 auto carrier_h = tracking::digitization::getCarrier(1);
156 const double mu_e = carrier_e.mu0(t) * 1.0e-4; // m²/(V·s)
157 const double mu_h = carrier_h.mu0(t) * 1.0e-4;
158
159 auto bfield_cache = bField()->makeCache(magneticFieldContext());
160
161 for (const auto& [layer_id, surface] : geometry().layer_surface_map_) {
162 const Acts::Vector3 center_mm = surface->center(geometryContext());
163 const auto b_result = bField()->getField(center_mm, bfield_cache);
164 if (!b_result.ok()) continue;
165
166 // B in Tesla (ACTS field providers return values in Acts internal units)
167 const Acts::Vector3 b_t = b_result.value() / Acts::UnitConstants::T;
168
169 // Sensor W-normal = 3rd column of the rotation matrix
170 const Acts::Vector3 w_hat =
171 surface->localToGlobalTransform(geometryContext()).rotation().col(2);
172
173 const double bw = b_t.dot(w_hat); // [T]
174
175 // tan(θ_L) = charge_sign · μ · Bw
176 // electrons: charge = −1, holes: charge = +1
177 const double tan_e = -mu_e * bw;
178 const double tan_h = +mu_h * bw;
179
180 lorentz_tan_cache_[layer_id] = {tan_e, tan_h};
181
182 ldmx_log(debug) << "Lorentz cache: layer=" << layer_id << " Bw=" << bw
183 << " T" << " tan_e=" << tan_e << " tan_h=" << tan_h;
184 }
185
186 ldmx_log(info) << "Lorentz tangents computed for "
187 << lorentz_tan_cache_.size() << " layers from field map "
188 << (field_map_.empty() ? geometry().fieldMapFile()
189 : field_map_);
190}
191
192void DigitizationProcessor::onNewRun(const ldmx::RunHeader& runHeader) {
193 const auto& rseed = getCondition<framework::RandomNumberSeedService>(
195 const uint64_t seed = rseed.getSeed("Tracking::DigitizationProcessor");
196 generator_.seed(seed);
197 if (strip_digitizer_) strip_digitizer_->seed(seed);
198}
199
200void DigitizationProcessor::produce(framework::Event& event) {
201 ldmx_log(trace) << " Getting the tracking geometry:" << geometry().getTG();
202
203 const auto& sim_hits = event.getCollection<ldmx::SimTrackerHit>(
204 hit_collection_, tracker_hit_passname_);
205
206 std::vector<ldmx::SimTrackerHit> merged_hits;
207 std::vector<ldmx::Measurement> measurements;
208 std::vector<ldmx::SimSiStripHit> raw_hits;
209
210 const bool save_raw =
211 use_charge_digitization_ && !out_raw_collection_.empty();
212 auto* raw_ptr = save_raw ? &raw_hits : nullptr;
213
214 if (merge_hits_) {
215 mergeSimHits(sim_hits, merged_hits);
216 measurements = digitizeHits(merged_hits, raw_ptr);
217 } else {
218 measurements = digitizeHits(sim_hits, raw_ptr);
219 }
220
221 event.add(out_collection_, measurements);
222 if (save_raw) {
223 event.add(out_raw_collection_, raw_hits);
224 }
225}
226
227// ---------------------------------------------------------------------------
228// mergeHits / mergeSimHits
229// ---------------------------------------------------------------------------
230
231bool DigitizationProcessor::mergeHits(
232 const std::vector<ldmx::SimTrackerHit>& sihits,
233 std::vector<ldmx::SimTrackerHit>& mergedHits) {
234 if (sihits.size() < 1) return false;
235
236 if (sihits.size() == 1) {
237 mergedHits.push_back(sihits[0]);
238 return true;
239 }
240
241 ldmx::SimTrackerHit merged_hit;
242 merged_hit.setLayerID(sihits[0].getLayerID());
243 merged_hit.setModuleID(sihits[0].getModuleID());
244 merged_hit.setID(sihits[0].getID());
245 merged_hit.setTrackID(sihits[0].getTrackID());
246
247 double x{0}, y{0}, z{0}, px{0}, py{0}, pz{0};
248 double t{0}, e{0}, edep{0}, path{0};
249 int pdg_id = sihits[0].getPdgID();
250
251 for (auto hit : sihits) {
252 double edep_hit = hit.getEdep();
253 edep += edep_hit;
254 e += hit.getEnergy();
255 t += edep_hit * hit.getTime();
256 x += edep_hit * hit.getPosition()[0];
257 y += edep_hit * hit.getPosition()[1];
258 z += edep_hit * hit.getPosition()[2];
259 px += edep_hit * hit.getMomentum()[0];
260 py += edep_hit * hit.getMomentum()[1];
261 pz += edep_hit * hit.getMomentum()[2];
262 path += edep_hit * hit.getPathLength();
263
264 if (hit.getPdgID() != pdg_id) {
265 ldmx_log(error)
266 << "ERROR:: Found hits with compatible sensorID and track_id "
267 "but different PDGID";
268 ldmx_log(error) << "TRACKID ==" << hit.getTrackID() << " vs "
269 << sihits[0].getTrackID();
270 ldmx_log(error) << "PDGID== " << hit.getPdgID() << " vs " << pdg_id;
271 return false;
272 }
273 }
274
275 merged_hit.setTime(t / edep);
276 merged_hit.setPosition(x / edep, y / edep, z / edep);
277 merged_hit.setMomentum(px / edep, py / edep, pz / edep);
278 merged_hit.setPathLength(path / edep);
279 merged_hit.setEnergy(e);
280 merged_hit.setEdep(edep);
281 merged_hit.setPdgID(pdg_id);
282
283 mergedHits.push_back(merged_hit);
284 return true;
285}
286
287bool DigitizationProcessor::mergeSimHits(
288 const std::vector<ldmx::SimTrackerHit>& sim_hits,
289 std::vector<ldmx::SimTrackerHit>& merged_hits) {
290 // Key: [sensor_id][track_id] → list of hits to merge
291 std::map<int, std::map<int, std::vector<ldmx::SimTrackerHit>>> hitmap;
292
293 for (const auto& hit : sim_hits) {
294 unsigned int index = tracking::sim::utils::getSensorID(hit);
295 unsigned int trackid = hit.getTrackID();
296 hitmap[index][trackid].push_back(hit);
297
298 ldmx_log(trace) << "hitmap being filled, size::[" << index << "]["
299 << trackid << "] size " << hitmap[index][trackid].size();
300 }
301
302 typedef std::map<int,
303 std::map<int, std::vector<ldmx::SimTrackerHit>>>::iterator
304 hitmap_it1;
305 typedef std::map<int, std::vector<ldmx::SimTrackerHit>>::iterator hitmap_it2;
306
307 for (hitmap_it1 it = hitmap.begin(); it != hitmap.end(); it++) {
308 for (hitmap_it2 it2 = it->second.begin(); it2 != it->second.end(); it2++) {
309 mergeHits(it2->second, merged_hits);
310 }
311 }
312
313 ldmx_log(debug) << "Sim_hits Size = " << sim_hits.size()
314 << " Merged_hits Size = " << merged_hits.size();
315
316 for (const auto& hit : sim_hits) {
317 ldmx_log(trace) << hit;
318 }
319 for (const auto& mhit : merged_hits) {
320 ldmx_log(trace) << mhit;
321 }
322
323 return true;
324}
325
326// ---------------------------------------------------------------------------
327// digitizeHits — Mode 0 (smearing) and Mode 1 (charge digitization)
328// ---------------------------------------------------------------------------
329
330std::vector<ldmx::Measurement> DigitizationProcessor::digitizeHits(
331 const std::vector<ldmx::SimTrackerHit>& sim_hits,
332 std::vector<ldmx::SimSiStripHit>* raw_hits) {
333 ldmx_log(debug) << "Found: " << sim_hits.size() << " sim hits in '"
334 << hit_collection_ << "' with passname '"
335 << tracker_hit_passname_ << "'";
336
337 std::vector<ldmx::Measurement> measurements;
338
339 struct StripContrib {
340 double charge_electrons_;
341 double hit_time_ns_;
342 int track_id_;
343 int pdg_id_;
344 int sim_hit_id_;
345 float edep_;
346 };
347 // layer_id -> strip_idx -> per-hit contributions (populated in Phase 1,
348 // consumed in Phase 2 after the loop to apply noise once per strip)
349 std::map<int, std::map<int, std::vector<StripContrib>>> layer_strip_contribs;
350
351 for (auto& sim_hit : sim_hits) {
352 // Energy deposition cut
353 if (sim_hit.getEdep() <= min_e_dep_) continue;
354 if (track_id_ > 0 && sim_hit.getTrackID() != track_id_) continue;
355
356 ldmx::Measurement measurement(sim_hit);
357
358 // Sensor identification
359 auto layer_id = tracking::sim::utils::getSensorID(sim_hit);
360 measurement.setLayerID(layer_id);
361
362 auto hit_surface{geometry().getSurface(layer_id)};
363 if (!hit_surface) continue;
364
365 ldmx_log(trace)
366 << "Local to global\n"
367 << hit_surface->localToGlobalTransform(geometryContext()).rotation()
368 << "\n"
369 << hit_surface->localToGlobalTransform(geometryContext()).translation();
370
371 // -----------------------------------------------------------------------
372 // Project global hit position onto the surface (2D local coords)
373 // -----------------------------------------------------------------------
374 Acts::Vector3 dummy_momentum;
375 Acts::Vector2 local_pos_2d;
376
377 // TODO: clarify / derive the 0.320 mm surface tolerance from the geometry
378 constexpr double surface_thickness = 0.320 * Acts::UnitConstants::mm;
379
380 Acts::Vector3 global_pos(measurement.getGlobalPosition()[0],
381 measurement.getGlobalPosition()[1],
382 measurement.getGlobalPosition()[2]);
383
384 try {
385 local_pos_2d = hit_surface
386 ->globalToLocal(geometryContext(), global_pos,
387 dummy_momentum, surface_thickness)
388 .value();
389 } catch (const std::exception& e) {
390 ldmx_log(warn) << "hit not on surface... Skipping.";
391 continue;
392 }
393
394 // Store the projected truth U before any smearing or charge digitization.
395 measurement.setTruthU(static_cast<float>(local_pos_2d[0]));
396
397 // -----------------------------------------------------------------------
398 // Mode 1: realistic charge digitization
399 // -----------------------------------------------------------------------
400 if (use_charge_digitization_) {
401 // Read sensor thickness from the geometry.
402 const auto* placement = hit_surface->surfacePlacement();
403 if (!placement) {
404 ldmx_log(warn) << "No detector element for layer_id=" << layer_id
405 << " — skipping hit";
406 continue;
407 }
408 const double thickness =
409 static_cast<const tracking::geo::DetectorElement*>(placement)
410 ->thickness();
411 strip_digitizer_->setThickness(thickness);
412
413 // Build the full 3D local position and direction for charge simulation.
414 const Acts::Transform3 surf_transform =
415 hit_surface->localToGlobalTransform(geometryContext());
416
417 // 3D local position: apply the inverse surface transform to the global
418 // hit position so that we know the depth (W) coordinate.
419 const Acts::Vector3 local_pos_3d = surf_transform.inverse() * global_pos;
420
421 // 3D local direction: rotate the global unit momentum into local frame.
422 // Apply the same LDMX→ACTS frame permutation as Measurement.cxx:
423 // ACTS-X = LDMX-z [2], ACTS-Y = LDMX-x [0], ACTS-Z = LDMX-y [1].
424 Acts::Vector3 global_mom(sim_hit.getMomentum()[2],
425 sim_hit.getMomentum()[0],
426 sim_hit.getMomentum()[1]);
427 const double mom_mag = global_mom.norm();
428
429 Acts::Vector3 local_dir_3d;
430 if (mom_mag > 0.0) {
431 local_dir_3d =
432 surf_transform.rotation().transpose() * (global_mom / mom_mag);
433 } else {
434 // Degenerate case: treat as normal incidence
435 local_dir_3d = Acts::Vector3(0.0, 0.0, 1.0);
436 }
437
438 // Path length through the sensor; fall back to thickness / |cos θ|
439 // if the stored value is not set.
440 double path_length = sim_hit.getPathLength();
441 if (path_length <= 0.0) {
442 const double cos_theta = std::abs(local_dir_3d[2]);
443 path_length = (cos_theta > 1e-3) ? thickness / cos_theta : thickness;
444 }
445
446 // Apply per-layer Lorentz tangents from the B-field cache (if available).
447 if (use_lorentz_) {
448 auto lorentz_it = lorentz_tan_cache_.find(layer_id);
449 if (lorentz_it != lorentz_tan_cache_.end()) {
450 strip_digitizer_->mutableParams().electron_lorentz_tangent =
451 lorentz_it->second.first;
452 strip_digitizer_->mutableParams().hole_lorentz_tangent =
453 lorentz_it->second.second;
454 }
455 }
456
457 // Compute charge deposited on each strip
458 auto strip_charges = strip_digitizer_->computeStripCharges(
459 sim_hit.getEdep(), local_pos_3d, local_dir_3d, path_length);
460
461 ldmx_log(trace) << "Charge digi: " << strip_charges.size()
462 << " strips from computeStripCharges (pre-noise)";
463
464 // Phase 1: accumulate this hit's strip charges into the per-layer map.
465 // Noise is applied once per strip in Phase 2 (after the sim-hit loop)
466 // so that overlapping contributions from different SimParticles are
467 // summed before threshold is applied.
468 if (raw_hits && pulse_shape_) {
469 const double hit_time_ns = sim_hit.getTime();
470 for (const auto& [strip_idx, charge] : strip_charges) {
471 layer_strip_contribs[layer_id][strip_idx].push_back(StripContrib{
472 charge, hit_time_ns, sim_hit.getTrackID(), sim_hit.getPdgID(),
473 sim_hit.getID(), sim_hit.getEdep()});
474 }
475 }
476
477 // Measurements are produced downstream by StripFitProcessor +
478 // StripClusterProcessor for the reconstructed position, but we still
479 // emit a truth-position Measurement here so that DigiDQM can build a
480 // per-layer truth-U lookup for the sim_cluster_du residual.
481 // Global position, time, edep, ID, and track ID are already populated
482 // by the Measurement(sim_hit) constructor above; set local coords and
483 // zero the covariance (this is a truth hit, not a smeared measurement).
484 measurement.setLocalPosition(local_pos_2d(0), local_pos_2d(1));
485 measurement.setLocalCovariance(0., 0.);
486 measurements.push_back(measurement);
487
488 // -----------------------------------------------------------------------
489 // Mode 0: simple Gaussian smearing
490 // -----------------------------------------------------------------------
491 } else {
492 if (do_smearing_) {
493 float smear_factor{(*normal_)(generator_)};
494 local_pos_2d[0] += smear_factor * sigma_u_;
495 smear_factor = (*normal_)(generator_);
496 local_pos_2d[1] += smear_factor * sigma_v_;
497
498 measurement.setLocalCovariance(
499 static_cast<float>(sigma_u_ * sigma_u_),
500 static_cast<float>(tracking::digitization::SIGMA_V_MM *
501 tracking::digitization::SIGMA_V_MM));
502
503 auto transf_global_pos{hit_surface->localToGlobal(
504 geometryContext(), local_pos_2d, dummy_momentum)};
505 measurement.setGlobalPosition(measurement.getGlobalPosition()[0],
506 transf_global_pos(1),
507 transf_global_pos(2));
508 }
509
510 measurement.setLocalPosition(local_pos_2d(0), local_pos_2d(1));
511 measurements.push_back(measurement);
512 }
513 } // loop over sim hits
514
515 // Phase 2: apply noise once per strip across all sim-hit contributions,
516 // then build SimSiStripHits with correctly superimposed pulse shapes.
517 if (raw_hits && pulse_shape_) {
518 const int adc_max = (1 << tracking::digitization::ADC_BITS) - 1;
519
520 for (auto& [lyr_id, strip_contribs_map] : layer_strip_contribs) {
521 // Sum all contributions to get the total pre-noise charge per strip.
522 std::map<int, double> total_charges;
523 for (const auto& [strip_idx, contribs] : strip_contribs_map) {
524 double total = 0.0;
525 for (const auto& c : contribs) total += c.charge_electrons_;
526 total_charges[strip_idx] = total;
527 }
528
529 // Add noise to every strip (and its ±1 neighbours) then apply threshold.
530 strip_digitizer_->applyNoiseAndThreshold(total_charges);
531 if (total_charges.empty()) continue;
532
533 for (const auto& [strip_idx, final_charge] : total_charges) {
534 const auto contrib_it = strip_contribs_map.find(strip_idx);
535 const bool has_signal = (contrib_it != strip_contribs_map.end());
536
537 int track_id_out = -1;
538 int pdg_id_out = 0;
539 int sim_hit_id_out = -1;
540 float edep_out = 0.f;
541 double ref_time_ns = 0.0;
542 std::vector<short> samples(tracking::digitization::N_SAMPLES);
543
544 if (has_signal) {
545 const auto& contribs = contrib_it->second;
546
547 // Dominant contributor = strip's largest single charge deposit.
548 const StripContrib* dom = &contribs.front();
549 for (const auto& c : contribs)
550 if (c.charge_electrons_ > dom->charge_electrons_) dom = &c;
551
552 ref_time_ns = dom->hit_time_ns_;
553 track_id_out = dom->track_id_;
554 pdg_id_out = dom->pdg_id_;
555 sim_hit_id_out = dom->sim_hit_id_;
556 for (const auto& c : contribs) edep_out += c.edep_;
557
558 // ADC = pedestal + superposition of each contributor's shaped pulse.
559 for (int isamp = 0; isamp < tracking::digitization::N_SAMPLES;
560 ++isamp) {
561 const double t_samp =
562 tracking::digitization::T0_OFFSET_NS +
563 isamp * tracking::digitization::SAMPLING_INTERVAL_NS;
564 double val =
565 static_cast<double>(tracking::digitization::ADC_PEDESTAL);
566 for (const auto& c : contribs)
567 val += (c.charge_electrons_ /
568 tracking::digitization::ADC_ELECTRONS_PER_COUNT) *
569 pulse_shape_->eval(t_samp - c.hit_time_ns_);
570 samples[isamp] = static_cast<short>(
571 std::clamp(static_cast<int>(std::round(val)), 0, adc_max));
572 }
573 } else {
574 // Noise-only strip: added as a ±1 neighbour by
575 // applyNoiseAndThreshold. Borrow the nearest signal strip's dominant
576 // hit time for pulse shaping.
577 for (int delta : {-1, +1}) {
578 const auto nb = strip_contribs_map.find(strip_idx + delta);
579 if (nb != strip_contribs_map.end() && !nb->second.empty()) {
580 const StripContrib* dom = &nb->second.front();
581 for (const auto& c : nb->second)
582 if (c.charge_electrons_ > dom->charge_electrons_) dom = &c;
583 ref_time_ns = dom->hit_time_ns_;
584 break;
585 }
586 }
587 const double peak_adc =
588 final_charge / tracking::digitization::ADC_ELECTRONS_PER_COUNT;
589 for (int isamp = 0; isamp < tracking::digitization::N_SAMPLES;
590 ++isamp) {
591 const double t_samp =
592 tracking::digitization::T0_OFFSET_NS +
593 isamp * tracking::digitization::SAMPLING_INTERVAL_NS;
594 const double val =
595 static_cast<double>(tracking::digitization::ADC_PEDESTAL) +
596 peak_adc * pulse_shape_->eval(t_samp - ref_time_ns);
597 samples[isamp] = static_cast<short>(
598 std::clamp(static_cast<int>(std::round(val)), 0, adc_max));
599 }
600 }
601
602 raw_hits->emplace_back(lyr_id, strip_idx, std::move(samples),
603 static_cast<long>(ref_time_ns), track_id_out,
604 pdg_id_out, sim_hit_id_out, edep_out);
605 }
606 }
607 } // Phase 2
608
609 return measurements;
610} // digitizeHits
611
612} // namespace tracking::reco
613
#define DECLARE_PRODUCER(CLASS)
Macro which allows the framework to construct a producer given its name during configuration.
Conditions object for random number seeds.
Implements an event buffer system for storing event data.
Definition Event.h:40
Class which represents the process under execution.
Definition Process.h:34
static const std::string CONDITIONS_OBJECT_NAME
Conditions object name.
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
void setTruthU(float u)
Set the truth local U [mm]: the sim-hit global position projected onto the sensor surface,...
std::array< float, 3 > getGlobalPosition() const
Definition Measurement.h:50
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
Run-specific configuration and data stored in its own output TTree alongside the event TTree in the o...
Definition RunHeader.h:68
Represents a simulated tracker hit in the simulation.
void setEdep(const float edep)
Set the energy deposited on the hit [MeV].
void setModuleID(const int moduleID)
Set the module ID associated with a hit.
void setPosition(const float x_, const float y_, const float z_)
Set the position of the hit [mm].
void setTime(const float time)
Set the global time of the hit [ns].
void setID(const long id)
Set the detector ID of the hit.
void setLayerID(const int layerID)
Set the geometric layer ID of the hit.
void setPathLength(const float pathLength)
Set the path length of the hit [mm].
void setEnergy(const float energy)
Set the energy of the hit.
void setPdgID(const int simPdgID)
Set the Sim particle track ID of the hit.
void setTrackID(const int simTrackID)
Set the Sim particle track ID of the hit.
void setMomentum(const float px, const float py, const float pz)
Set the momentum of the particle at the position at which the hit took place [GeV].
static std::unique_ptr< PulseShape > make(const std::string &name, double tp, double tp2=0.0)
Factory: construct a pulse shape by name.
Digitization processor for the silicon strip tracker.
All classes in the ldmx-sw project use this namespace.