LDMX Software
tracking::digitization::SiStripDigitizer Class Reference

Realistic digitization of silicon strip sensor hits. More...

#include <SiStripDigitizer.h>

Classes

struct  SensorParams
 All parameters describing one silicon strip sensor layer. More...
 

Public Member Functions

 SiStripDigitizer (const SensorParams &params)
 
void seed (uint64_t s)
 Seed the internal random-number engine.
 
std::map< int, double > computeStripCharges (double edep, const Acts::Vector3 &local_pos, const Acts::Vector3 &local_dir, double path_length) const
 Simulate charge collection for a single hit.
 
void applyNoiseAndThreshold (std::map< int, double > &strip_charges)
 Add Gaussian electronic noise to signal strips and their immediate neighbours, then remove strips below the readout threshold.
 
const SensorParams & params () const
 
SensorParams & mutableParams ()
 
void setThickness (double thickness)
 Set the sensor thickness [mm] from the geometry before processing hits.
 

Private Member Functions

int adaptiveNSegments (const Acts::Vector3 &local_dir, double path_length) const
 Number of track sub-segments, chosen adaptively so that the U displacement per segment does not exceed deposition_granularity × sense_pitch.
 
double diffusionSigma (double d, bool is_minority) const
 Diffusion sigma [mm] for carriers drifting distance d [mm].
 
double stripFraction (double u0, double sigma, double strip_center, double pitch) const
 Fraction of a Gaussian charge cloud (centred at u0 with sigma sigma) collected by a strip of width pitch centred at strip_center.
 
std::map< int, double > computeCarrierCharges (double q_per_seg, int n_seg, const Acts::Vector3 &local_pos, const Acts::Vector3 &local_dir, double path_length, double w_collect, double lorentz_tan, bool is_minority) const
 Simulate one carrier type and return the sense-strip charge map.
 
std::map< int, double > senseToReadout (const std::map< int, double > &sense_charges) const
 Sum sense-strip charges into readout-strip charges according to the AC-coupling ratio ratio = round(readout_pitch / sense_pitch).
 

Private Attributes

SensorParams params_
 
std::default_random_engine generator_
 
std::normal_distribution< double > normal_ {0.0, 1.0}
 

Detailed Description

Realistic digitization of silicon strip sensor hits.

Follows the physics model of CDFSiSensorSim (itself a port of the LCSIM / HPS Java digitization). For each SimTrackerHit the track segment through the sensor is divided adaptively into sub-segments whose number is chosen so that the U displacement per segment is at most a configurable fraction of the sense pitch. For each sub-segment:

  1. Charge creation: N_ehp = Edep / E_pair (E_pair = 3.62 eV for Si).
  2. Charge trapping: linear collection-efficiency penalty proportional to drift distance (configurable; 0 for unirradiated sensors).
  3. Diffusion sigma: carrier-type-aware trapezoidal-field formula
    • minority carriers (e⁻ in p-type bulk, h⁺ in n-type): σ² = (kT/q)·t²/V_dep · ln(V_sum / (V_sum − 2·V_dep·d/t))
    • majority carriers (h⁺ in p-type, e⁻ in n-type): σ² = (kT/q)·t²/V_dep · ln((ΔV + 2·V_dep·d/t) / ΔV) where d is the drift distance, V_sum = V_bias+V_dep, ΔV = V_bias−V_dep. Falls back to uniform-field formula when V_bias ≤ V_dep.
  4. Lorentz drift: charge cloud centroid shifted by d·tan(θ_L) in U; effective 1-D sigma broadened by 1/cos²(θ_L) = 1+tan²(θ_L).
  5. Strip charge integration: Gaussian integrated over each sense strip via the error function.
  6. Sense→readout transfer: sense strips (fine pitch) summed into readout strips (coarser pitch) according to the AC-coupling ratio.

Both electron and hole sides can be simulated independently; by default only the electron (n-strip) side is read out, as for a single-sided p-type sensor.

Coordinate convention

local[0] = U (precision direction, perpendicular to strips) local[1] = V (strip direction) local[2] = W (surface normal)

Electrons are collected at W = +thickness/2 (the n-strip side). Holes are collected at W = −thickness/2 (the p-bulk/backplane side).

Strip indexing: readout strip m is centred at U = (m − N/2) · readout_pitch, where N = n_readout_strips (default 512). Strip 0 is at the most-negative U edge; strip N−1 at the most-positive U edge. The sensor centre is between strips N/2−1 and N/2. Mapping: sense strip n → readout strip ⌊n / ratio⌋ + N/2 where ratio = round(readout_pitch / sense_pitch).

Definition at line 58 of file SiStripDigitizer.h.

Constructor & Destructor Documentation

◆ SiStripDigitizer()

tracking::digitization::SiStripDigitizer::SiStripDigitizer ( const SensorParams & params)
inlineexplicit

Definition at line 120 of file SiStripDigitizer.h.

120: params_(params) {}

Member Function Documentation

◆ adaptiveNSegments()

int tracking::digitization::SiStripDigitizer::adaptiveNSegments ( const Acts::Vector3 & local_dir,
double path_length ) const
private

Number of track sub-segments, chosen adaptively so that the U displacement per segment does not exceed deposition_granularity × sense_pitch.

Definition at line 26 of file SiStripDigitizer.cxx.

27 {
28 // Ensure U-displacement per segment <= granularity * sense_pitch.
29 // For near-normal-incidence tracks (dir_u ≈ 0) the adaptive count can be
30 // very small; clamp to n_segments_min.
31 const double max_step = params_.deposition_granularity * params_.sense_pitch;
32 const double total_u = std::abs(path_length * local_dir[0]);
33 const int adaptive =
34 (max_step > 0.0) ? static_cast<int>(std::ceil(total_u / max_step)) : 1;
35 return std::max(params_.n_segments_min, adaptive);
36}
double sense_pitch
Sense (inner) electrode pitch [mm].
double deposition_granularity
Adaptive segmentation granularity: max U-step as fraction of sense_pitch.
int n_segments_min
Minimum number of track sub-segments (used when the track is close to normal incidence so the adaptiv...

References tracking::digitization::SiStripDigitizer::SensorParams::deposition_granularity, tracking::digitization::SiStripDigitizer::SensorParams::n_segments_min, and tracking::digitization::SiStripDigitizer::SensorParams::sense_pitch.

Referenced by computeStripCharges().

◆ applyNoiseAndThreshold()

void tracking::digitization::SiStripDigitizer::applyNoiseAndThreshold ( std::map< int, double > & strip_charges)

Add Gaussian electronic noise to signal strips and their immediate neighbours, then remove strips below the readout threshold.

Parameters
strip_chargesReadout-strip charge map. Modified in place.

Definition at line 243 of file SiStripDigitizer.cxx.

244 {
245 // Add the immediate neighbours of every signal strip so that noise alone
246 // can promote them above threshold (as in a real detector where every
247 // strip has readout noise).
248 std::set<int> extra;
249 for (const auto& [strip, charge] : strip_charges) {
250 extra.insert(strip - 1);
251 extra.insert(strip + 1);
252 }
253 for (int s : extra) {
254 strip_charges.emplace(s, 0.0); // does not overwrite existing entries
255 }
256
257 // Add Gaussian noise to all strips.
258 for (auto& [strip, charge] : strip_charges) {
259 charge += params_.noise_electrons * normal_(generator_);
260 }
261
262 // Remove strips below the readout threshold.
263 for (auto it = strip_charges.begin(); it != strip_charges.end();) {
264 it = (it->second < params_.threshold_electrons) ? strip_charges.erase(it)
265 : std::next(it);
266 }
267}
double threshold_electrons
Readout threshold [electrons]. Strips below this are suppressed.
double noise_electrons
Electronic noise sigma [electrons ENC].

References tracking::digitization::SiStripDigitizer::SensorParams::noise_electrons, and tracking::digitization::SiStripDigitizer::SensorParams::threshold_electrons.

◆ computeCarrierCharges()

std::map< int, double > tracking::digitization::SiStripDigitizer::computeCarrierCharges ( double q_per_seg,
int n_seg,
const Acts::Vector3 & local_pos,
const Acts::Vector3 & local_dir,
double path_length,
double w_collect,
double lorentz_tan,
bool is_minority ) const
private

Simulate one carrier type and return the sense-strip charge map.

Parameters
q_per_segCharge per sub-segment [electrons].
n_segNumber of sub-segments.
local_pos3D hit midpoint in local coordinates [mm].
local_dirUnit track direction in local coordinates.
path_lengthPath length through sensor [mm].
w_collectW coordinate of collection electrode [mm].
lorentz_tantan(θ_Lorentz) for this carrier (may be 0).
is_minorityTrue if this carrier is the minority species.

Definition at line 90 of file SiStripDigitizer.cxx.

93 {
94 // 1/cos²(θ_L) = 1 + tan²(θ_L): Lorentz broadening of the diffusion sigma.
95 const double inv_cos2 = 1.0 + lorentz_tan * lorentz_tan;
96
97 std::map<int, double> sense_charges;
98
99 for (int iseg = 0; iseg < n_seg; ++iseg) {
100 // Fractional position along the track: f = 0 (entry) → 1 (exit).
101 const double f = (iseg + 0.5) / n_seg;
102
103 // 3D segment centre in sensor-local coordinates [mm].
104 const Acts::Vector3 seg_pos =
105 local_pos + (f - 0.5) * path_length * local_dir;
106
107 const double u_seg = seg_pos[0];
108 const double w_seg = seg_pos[2];
109
110 // Drift distance to collection electrode, clamped to sensor thickness.
111 const double drift =
112 std::max(0.0, std::min(params_.thickness, std::abs(w_collect - w_seg)));
113
114 // Charge trapping: linear model from CDFSiSensorSim.
115 // trapping_ = fraction lost per 100 µm (= 0.1 mm) of drift.
116 // collection_efficiency = 1 − 10·trapping·drift_mm
117 double q_seg = q_per_seg;
118 if (params_.trapping > 0.0) {
119 const double efficiency =
120 std::max(0.0, std::min(1.0, 1.0 - 10.0 * params_.trapping * drift));
121 q_seg *= efficiency;
122 }
123
124 // Diffusion sigma [mm] with 1/cos²(θ_L) broadening from Lorentz angle.
125 const double sigma_1d = diffusionSigma(drift, is_minority);
126 const double sigma =
127 std::max(sigma_1d * std::sqrt(inv_cos2), 1.0e-4); // ≥ 0.1 µm
128
129 // Lorentz shift: carriers arrive at U = u_seg + drift · tan(θ_L).
130 const double u_dest = u_seg + drift * lorentz_tan;
131
132 // Deposit charge on sense strips within 5 sigma of the charge centroid.
133 const int strip_lo = static_cast<int>(
134 std::floor((u_dest - 5.0 * sigma) / params_.sense_pitch));
135 const int strip_hi = static_cast<int>(
136 std::ceil((u_dest + 5.0 * sigma) / params_.sense_pitch));
137
138 for (int istrip = strip_lo; istrip <= strip_hi; ++istrip) {
139 const double strip_center = istrip * params_.sense_pitch;
140 const double frac =
141 stripFraction(u_dest, sigma, strip_center, params_.sense_pitch);
142 if (frac > 1.0e-7) {
143 sense_charges[istrip] += q_seg * frac;
144 }
145 }
146 }
147
148 return sense_charges;
149}
double diffusionSigma(double d, bool is_minority) const
Diffusion sigma [mm] for carriers drifting distance d [mm].
double stripFraction(double u0, double sigma, double strip_center, double pitch) const
Fraction of a Gaussian charge cloud (centred at u0 with sigma sigma) collected by a strip of width pi...
double thickness
Sensor thickness [mm]. Must be set from the geometry before use.
double trapping
Charge-trapping fraction lost per 100 µm of drift.

References diffusionSigma(), tracking::digitization::SiStripDigitizer::SensorParams::sense_pitch, stripFraction(), tracking::digitization::SiStripDigitizer::SensorParams::thickness, and tracking::digitization::SiStripDigitizer::SensorParams::trapping.

Referenced by computeStripCharges().

◆ computeStripCharges()

std::map< int, double > tracking::digitization::SiStripDigitizer::computeStripCharges ( double edep,
const Acts::Vector3 & local_pos,
const Acts::Vector3 & local_dir,
double path_length ) const

Simulate charge collection for a single hit.

Runs both electron and hole sides as configured, performs the sense-to-readout charge transfer, and returns the final readout-strip charge map.

Parameters
edepEnergy deposited in the sensitive volume [MeV].
local_pos3D hit midpoint in sensor-local coordinates [mm].
local_dirUnit track direction in sensor-local coordinates.
path_lengthTrack path length through the sensor volume [mm].
Returns
Map of readout_strip_index → collected charge [electrons]. Indices are in [0, n_readout_strips); strip 0 is at the most-negative U edge of the sensor.

Definition at line 202 of file SiStripDigitizer.cxx.

204 {
205 const double total_electrons = edep / ENERGY_PER_EHP_MEV;
206 const int n_seg = adaptiveNSegments(local_dir, path_length);
207 const double q_per_seg = total_electrons / n_seg;
208
209 // Minority-carrier flags for the two bulk types.
210 // p-type bulk (is_n_type = false): electrons = minority, holes = majority.
211 // n-type bulk (is_n_type = true): holes = minority, electrons =
212 // majority.
213 const bool electron_is_minority = !params_.is_n_type;
214 const bool hole_is_minority = params_.is_n_type;
215
216 std::map<int, double> readout_charges;
217
218 // Electron side: collection at W = +thickness/2 (n-strip side).
219 if (params_.electron_side_readout) {
220 const double w_electron = +0.5 * params_.thickness;
221 auto sense = computeCarrierCharges(
222 q_per_seg, n_seg, local_pos, local_dir, path_length, w_electron,
223 params_.electron_lorentz_tangent, electron_is_minority);
224 for (const auto& [strip, charge] : senseToReadout(sense)) {
225 readout_charges[strip] += charge;
226 }
227 }
228
229 // Hole side: collection at W = −thickness/2 (p-bulk / backplane side).
230 if (params_.hole_side_readout) {
231 const double w_hole = -0.5 * params_.thickness;
232 auto sense = computeCarrierCharges(
233 q_per_seg, n_seg, local_pos, local_dir, path_length, w_hole,
234 params_.hole_lorentz_tangent, hole_is_minority);
235 for (const auto& [strip, charge] : senseToReadout(sense)) {
236 readout_charges[strip] += charge;
237 }
238 }
239
240 return readout_charges;
241}
int adaptiveNSegments(const Acts::Vector3 &local_dir, double path_length) const
Number of track sub-segments, chosen adaptively so that the U displacement per segment does not excee...
std::map< int, double > senseToReadout(const std::map< int, double > &sense_charges) const
Sum sense-strip charges into readout-strip charges according to the AC-coupling ratio ratio = round(r...
std::map< int, double > computeCarrierCharges(double q_per_seg, int n_seg, const Acts::Vector3 &local_pos, const Acts::Vector3 &local_dir, double path_length, double w_collect, double lorentz_tan, bool is_minority) const
Simulate one carrier type and return the sense-strip charge map.
double electron_lorentz_tangent
tan(θ_Lorentz) for electrons. Sign encodes U-shift direction.
bool hole_side_readout
Simulate and read out the hole-collection side (p-strips / backplane).
bool electron_side_readout
Simulate and read out the electron-collection side (n-strips).
bool is_n_type
true = n-type bulk; false = p-type bulk. LDMX (and HPS) use n-type bulk.

References adaptiveNSegments(), computeCarrierCharges(), tracking::digitization::SiStripDigitizer::SensorParams::electron_lorentz_tangent, tracking::digitization::SiStripDigitizer::SensorParams::electron_side_readout, tracking::digitization::SiStripDigitizer::SensorParams::hole_lorentz_tangent, tracking::digitization::SiStripDigitizer::SensorParams::hole_side_readout, tracking::digitization::SiStripDigitizer::SensorParams::is_n_type, senseToReadout(), and tracking::digitization::SiStripDigitizer::SensorParams::thickness.

◆ diffusionSigma()

double tracking::digitization::SiStripDigitizer::diffusionSigma ( double d,
bool is_minority ) const
private

Diffusion sigma [mm] for carriers drifting distance d [mm].

Parameters
dDrift distance [mm].
is_minorityTrue for minority carriers (e⁻ in p-type, h⁺ in n-type); false for majority carriers.

Definition at line 38 of file SiStripDigitizer.cxx.

38 {
39 const double kt_q = KT_Q_300K * params_.temperature / 300.0;
40 const double t = params_.thickness;
41 const double vb = params_.bias_voltage;
42 const double vd = params_.depletion_voltage;
43
44 double sigma_sq = 0.0;
45
46 if (vb > vd && vd > 0.0) {
47 const double cf = 2.0 * vd * d / t;
48 const double base = kt_q * t * t / vd;
49
50 if (is_minority) {
51 // Minority carriers (e⁻ in p-type, h⁺ in n-type):
52 // σ² = (kT/q)·t²/Vd · ln( V_sum / (V_sum − cf) )
53 // These carriers drift quickly through the high-field region →
54 // smaller diffusion sigma.
55 const double v_sum = vb + vd;
56 const double denom = v_sum - cf;
57 if (denom > 0.0) {
58 sigma_sq = base * std::log(v_sum / denom);
59 }
60 } else {
61 // Majority carriers (h⁺ in p-type, e⁻ in n-type):
62 // σ² = (kT/q)·t²/Vd · ln( (ΔV + cf) / ΔV )
63 // These carriers drift through the low-field region →
64 // larger diffusion sigma.
65 const double delta_v = vb - vd;
66 if (delta_v > 0.0) {
67 sigma_sq = base * std::log(1.0 + cf / delta_v);
68 }
69 }
70 } else if (vb > 0.0) {
71 // Uniform-field fallback (V_bias ≤ V_dep or V_dep = 0):
72 // σ² = 2·(kT/q)·d·t / V_bias
73 sigma_sq = 2.0 * kt_q * d * t / vb;
74 }
75
76 return std::sqrt(std::max(sigma_sq, 0.0));
77}
double bias_voltage
Applied reverse-bias voltage [V].

References tracking::digitization::SiStripDigitizer::SensorParams::bias_voltage, tracking::digitization::SiStripDigitizer::SensorParams::depletion_voltage, tracking::digitization::SiStripDigitizer::SensorParams::temperature, and tracking::digitization::SiStripDigitizer::SensorParams::thickness.

Referenced by computeCarrierCharges().

◆ mutableParams()

SensorParams & tracking::digitization::SiStripDigitizer::mutableParams ( )
inline

Definition at line 156 of file SiStripDigitizer.h.

156{ return params_; }

◆ params()

const SensorParams & tracking::digitization::SiStripDigitizer::params ( ) const
inline

Definition at line 155 of file SiStripDigitizer.h.

155{ return params_; }

◆ seed()

void tracking::digitization::SiStripDigitizer::seed ( uint64_t s)
inline

Seed the internal random-number engine.

Call from onNewRun once a seed is available from the framework seed service.

Definition at line 124 of file SiStripDigitizer.h.

124{ generator_.seed(s); }

◆ senseToReadout()

std::map< int, double > tracking::digitization::SiStripDigitizer::senseToReadout ( const std::map< int, double > & sense_charges) const
private

Sum sense-strip charges into readout-strip charges according to the AC-coupling ratio ratio = round(readout_pitch / sense_pitch).

Sense strip n maps to readout strip floor(n / ratio).

Definition at line 151 of file SiStripDigitizer.cxx.

152 {
153 const int ratio =
154 std::max(1, static_cast<int>(
155 std::round(params_.readout_pitch / params_.sense_pitch)));
156
157 const int offset = params_.n_readout_strips / 2;
158 std::map<int, double> readout_charges;
159
160 if (ratio == 1) {
161 // No interleaving: paired strip only, apply readout transfer efficiency.
162 for (const auto& [sense_strip, charge] : sense_charges) {
163 readout_charges[sense_strip + offset] +=
164 charge * params_.readout_transfer_efficiency;
165 }
166 return readout_charges;
167 }
168
169 // AC-coupled sense→readout transfer following HPS CDFSiSensorSim.
170 //
171 // For each sense strip n, compute its position within its readout group:
172 // k = floor(n / ratio) — group index
173 // position_in_group = n − ratio × k — 0 … ratio−1
174 //
175 // position_in_group == 0: "paired" strip, physically under readout strip r.
176 // → transfers readout_transfer_efficiency × charge to readout r.
177 //
178 // position_in_group > 0: "unpaired" strip, between readout strips r and r+1.
179 // → transfers sense_transfer_efficiency × charge to EACH of r and r+1.
180 // (total ≈ 2 × 0.419 = 0.838; ~16% lost to capacitive cross-talk)
181
182 for (const auto& [sense_strip, charge] : sense_charges) {
183 const int k =
184 static_cast<int>(std::floor(static_cast<double>(sense_strip) / ratio));
185 const int position_in_group = sense_strip - ratio * k;
186 const int r = k + offset;
187
188 if (position_in_group == 0) {
189 readout_charges[r] += charge * params_.readout_transfer_efficiency;
190 } else {
191 readout_charges[r] += charge * params_.sense_transfer_efficiency;
192 readout_charges[r + 1] += charge * params_.sense_transfer_efficiency;
193 }
194 }
195 return readout_charges;
196}
double readout_pitch
Readout strip pitch [mm]. Must be an integer multiple of sense_pitch.
double readout_transfer_efficiency
AC-coupling transfer efficiency from a paired sense strip (physically under a readout strip,...
double sense_transfer_efficiency
AC-coupling transfer efficiency from an unpaired sense strip (between two readout strips,...

References tracking::digitization::SiStripDigitizer::SensorParams::n_readout_strips, tracking::digitization::SiStripDigitizer::SensorParams::readout_pitch, tracking::digitization::SiStripDigitizer::SensorParams::readout_transfer_efficiency, tracking::digitization::SiStripDigitizer::SensorParams::sense_pitch, and tracking::digitization::SiStripDigitizer::SensorParams::sense_transfer_efficiency.

Referenced by computeStripCharges().

◆ setThickness()

void tracking::digitization::SiStripDigitizer::setThickness ( double thickness)
inline

Set the sensor thickness [mm] from the geometry before processing hits.

Definition at line 159 of file SiStripDigitizer.h.

159{ params_.thickness = thickness; }

References tracking::digitization::SiStripDigitizer::SensorParams::thickness.

◆ stripFraction()

double tracking::digitization::SiStripDigitizer::stripFraction ( double u0,
double sigma,
double strip_center,
double pitch ) const
private

Fraction of a Gaussian charge cloud (centred at u0 with sigma sigma) collected by a strip of width pitch centred at strip_center.

Definition at line 79 of file SiStripDigitizer.cxx.

81 {
82 // Integral of N(u0, sigma) over [strip_center − pitch/2, strip_center +
83 // pitch/2]
84 const double inv_sqrt2_sigma = 1.0 / (std::sqrt(2.0) * sigma);
85 const double lo = (strip_center - 0.5 * pitch - u0) * inv_sqrt2_sigma;
86 const double hi = (strip_center + 0.5 * pitch - u0) * inv_sqrt2_sigma;
87 return 0.5 * (std::erf(hi) - std::erf(lo));
88}

Referenced by computeCarrierCharges().

Member Data Documentation

◆ generator_

std::default_random_engine tracking::digitization::SiStripDigitizer::generator_
private

Definition at line 213 of file SiStripDigitizer.h.

◆ normal_

std::normal_distribution<double> tracking::digitization::SiStripDigitizer::normal_ {0.0, 1.0}
private

Definition at line 214 of file SiStripDigitizer.h.

214{0.0, 1.0};

◆ params_

SensorParams tracking::digitization::SiStripDigitizer::params_
private

Definition at line 212 of file SiStripDigitizer.h.


The documentation for this class was generated from the following files: