LDMX Software
SiStripDigitizer.cxx
1#include "Tracking/Digitization/SiStripDigitizer.h"
2
3#include <cmath>
4#include <set>
5
6#include "Tracking/Digitization/SiStripConstants.h"
7
8namespace tracking::digitization {
9
10// ---------------------------------------------------------------------------
11// Physical constants
12// ---------------------------------------------------------------------------
13
15static constexpr double KT_Q_300K = 0.025852;
16
17// ---------------------------------------------------------------------------
18// Constructor
19// ---------------------------------------------------------------------------
20
21// ---------------------------------------------------------------------------
22// Private helpers
23// ---------------------------------------------------------------------------
24
25int SiStripDigitizer::adaptiveNSegments(const Acts::Vector3& local_dir,
26 double path_length) const {
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}
36
37double SiStripDigitizer::diffusionSigma(double d, bool is_minority) const {
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}
77
78double SiStripDigitizer::stripFraction(double u0, double sigma,
79 double strip_center,
80 double pitch) const {
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}
88
90 double q_per_seg, int n_seg, const Acts::Vector3& local_pos,
91 const Acts::Vector3& local_dir, double path_length, double w_collect,
92 double lorentz_tan, bool is_minority) const {
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}
149
151 const std::map<int, double>& sense_charges) const {
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}
196
197// ---------------------------------------------------------------------------
198// Public interface
199// ---------------------------------------------------------------------------
200
202 double edep, const Acts::Vector3& local_pos, const Acts::Vector3& local_dir,
203 double path_length) const {
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}
241
243 std::map<int, double>& strip_charges) {
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}
267
268} // namespace tracking::digitization
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.
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...
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...
void applyNoiseAndThreshold(std::map< int, double > &strip_charges)
Add Gaussian electronic noise to signal strips and their immediate neighbours, then remove strips bel...
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.
double threshold_electrons
Readout threshold [electrons]. Strips below this are suppressed.
double readout_pitch
Readout strip pitch [mm]. Must be an integer multiple of sense_pitch.
double sense_pitch
Sense (inner) electrode pitch [mm].
double noise_electrons
Electronic noise sigma [electrons ENC].
double thickness
Sensor thickness [mm]. Must be set from the geometry before use.
double bias_voltage
Applied reverse-bias voltage [V].
double deposition_granularity
Adaptive segmentation granularity: max U-step as fraction of sense_pitch.
double trapping
Charge-trapping fraction lost per 100 µm of drift.
bool hole_side_readout
Simulate and read out the hole-collection side (p-strips / backplane).
double readout_transfer_efficiency
AC-coupling transfer efficiency from a paired sense strip (physically under a readout strip,...
int n_segments_min
Minimum number of track sub-segments (used when the track is close to normal incidence so the adaptiv...
double sense_transfer_efficiency
AC-coupling transfer efficiency from an unpaired sense strip (between two readout strips,...
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.