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 SensorParamsparams () const
 
SensorParamsmutableParams ()
 
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 60 of file SiStripDigitizer.h.

Constructor & Destructor Documentation

◆ SiStripDigitizer()

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

Definition at line 118 of file SiStripDigitizer.h.

118: 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 25 of file SiStripDigitizer.cxx.

26 {
27 // Ensure U-displacement per segment <= granularity * sense_pitch.
28 // For near-normal-incidence tracks (dir_u ≈ 0) the adaptive count can be
29 // very small; clamp to n_segments_min.
30 const double max_step = params_.deposition_granularity * params_.sense_pitch;
31 const double total_u = std::abs(path_length * local_dir[0]);
32 const int adaptive =
33 (max_step > 0.0) ? static_cast<int>(std::ceil(total_u / max_step)) : 1;
34 return std::max(params_.n_segments_min, adaptive);
35}
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 242 of file SiStripDigitizer.cxx.

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

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

203 {
204 const double total_electrons = edep / ENERGY_PER_EHP_MEV;
205 const int n_seg = adaptiveNSegments(local_dir, path_length);
206 const double q_per_seg = total_electrons / n_seg;
207
208 // Minority-carrier flags for the two bulk types.
209 // p-type bulk (is_n_type = false): electrons = minority, holes = majority.
210 // n-type bulk (is_n_type = true): holes = minority, electrons =
211 // majority.
212 const bool electron_is_minority = !params_.is_n_type;
213 const bool hole_is_minority = params_.is_n_type;
214
215 std::map<int, double> readout_charges;
216
217 // Electron side: collection at W = +thickness/2 (n-strip side).
218 if (params_.electron_side_readout) {
219 const double w_electron = +0.5 * params_.thickness;
220 auto sense = computeCarrierCharges(
221 q_per_seg, n_seg, local_pos, local_dir, path_length, w_electron,
222 params_.electron_lorentz_tangent, electron_is_minority);
223 for (const auto& [strip, charge] : senseToReadout(sense)) {
224 readout_charges[strip] += charge;
225 }
226 }
227
228 // Hole side: collection at W = −thickness/2 (p-bulk / backplane side).
229 if (params_.hole_side_readout) {
230 const double w_hole = -0.5 * params_.thickness;
231 auto sense = computeCarrierCharges(
232 q_per_seg, n_seg, local_pos, local_dir, path_length, w_hole,
233 params_.hole_lorentz_tangent, hole_is_minority);
234 for (const auto& [strip, charge] : senseToReadout(sense)) {
235 readout_charges[strip] += charge;
236 }
237 }
238
239 return readout_charges;
240}
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 37 of file SiStripDigitizer.cxx.

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

154{ return params_; }

◆ params()

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

Definition at line 153 of file SiStripDigitizer.h.

153{ 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 122 of file SiStripDigitizer.h.

122{ 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 150 of file SiStripDigitizer.cxx.

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

157{ 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 78 of file SiStripDigitizer.cxx.

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

Referenced by computeCarrierCharges().

Member Data Documentation

◆ generator_

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

Definition at line 211 of file SiStripDigitizer.h.

◆ normal_

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

Definition at line 212 of file SiStripDigitizer.h.

212{0.0, 1.0};

◆ params_

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

Definition at line 210 of file SiStripDigitizer.h.


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