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