LDMX Software
SiStripDigitizer.cxx
1#include "Tracking/Digitization/SiStripDigitizer.h"
2
3#include <cmath>
4#include <set>
5#include <utility>
6
7#include "Tracking/Digitization/SiStripConstants.h"
8
9namespace tracking::digitization {
10
11// ---------------------------------------------------------------------------
12// Physical constants
13// ---------------------------------------------------------------------------
14
16static constexpr double KT_Q_300K = 0.025852;
17
18// ---------------------------------------------------------------------------
19// Constructor
20// ---------------------------------------------------------------------------
21
22// ---------------------------------------------------------------------------
23// Private helpers
24// ---------------------------------------------------------------------------
25
26int SiStripDigitizer::adaptiveNSegments(const Acts::Vector3& local_dir,
27 double path_length) const {
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}
37
38double SiStripDigitizer::diffusionSigma(double d, bool is_minority) const {
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}
78
79double SiStripDigitizer::stripFraction(double u0, double sigma,
80 double strip_center,
81 double pitch) const {
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}
89
91 double q_per_seg, int n_seg, const Acts::Vector3& local_pos,
92 const Acts::Vector3& local_dir, double path_length, double w_collect,
93 double lorentz_tan, bool is_minority) const {
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}
150
152 const std::map<int, double>& sense_charges) const {
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}
197
198// ---------------------------------------------------------------------------
199// Public interface
200// ---------------------------------------------------------------------------
201
203 double edep, const Acts::Vector3& local_pos, const Acts::Vector3& local_dir,
204 double path_length) const {
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}
242
244 std::map<int, double>& strip_charges) {
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}
268
269} // 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.