Skip to main content

lindhard/electron/
secondary.rs

1//! Secondary-electron generation at inelastic events.
2//!
3//! The model is Kieft and Bosch's, J. Phys. D: Appl. Phys. 41, 215310 (2008),
4//! doi:10.1088/0022-3727/41/21/215310, as written out by T. Verduin,
5//! *Quantum Noise Effects in e-Beam Lithography and Metrology*, PhD thesis,
6//! Delft University of Technology (2017),
7//! doi:10.4233/uuid:f214f594-a21f-4318-9f29-9776d60ab06c ("Verduin" below,
8//! with the thesis's printed page and equation numbers). The kinematics are
9//! ported from Nebula, `source/physics/kieft/inelastic.h` at commit
10//! `a50a8e83207980ed221ac83cfe874751d39b86a9`
11//! (<https://github.com/Nebula-simulator/nebula>), BSD-3-Clause,
12//! Copyright (c) 2020, Nebula-simulator; the notice is in
13//! `THIRD_PARTY_LICENSES.md`.
14//!
15//! # Energy
16//!
17//! An inelastic event takes `W` from the primary (from the inelastic
18//! [`CrossSectionTable`](crate::electron::data::CrossSectionTable)) and
19//! lifts an electron of binding energy `B` (measured below the Fermi level)
20//! into the conduction band with kinetic energy
21//! `E_SE = E_F + W - B` (Verduin Eq. 3.86, p. 78), measured, like every
22//! kinetic energy inside a material, from the band bottom
23//! ([`crate::electron::boundary`]). For a valence event (no shell channel
24//! supplies `B`):
25//!
26//! - in a metal, `B = 0`: the electron comes from the Fermi level (Nebula's
27//!   handling when no binding energy applies), and every event with `W > 0`
28//!   makes one. A band built with
29//!   [`BandStructure::with_valence_binding_ev`] sets `B` to that value
30//!   instead (the rule of Azzolini et al., arXiv:1809.00859, p. 6; cited
31//!   there), and then only an event with `W > B` makes one, a loss
32//!   `W <= B` staying in the solid as below; `B = 0` is the default
33//!   unchanged;
34//! - in a semiconductor or insulator, `B = E_g` when `W > E_g`, an excitation
35//!   across the gap (Verduin p. 78; Nebula sets the binding energy to the
36//!   band gap); an event with `W <= E_g` makes no secondary and its energy
37//!   stays in the solid (Verduin p. 78 and section 3.4 assign it to optical
38//!   phonons).
39//!
40//! The bookkeeping of one event is a [`SecondaryEvent`], which satisfies
41//! `loss = secondary + binding + deposited` with `binding = B - E_F`, the
42//! energy that the liberated electron's initial state lies below the band
43//! bottom (negative for a conduction electron, which already had `E_F`).
44//!
45//! # Inner-shell events
46//!
47//! A loss `ω` drawn from the channel of inner shell `j`
48//! ([`crate::electron::transport::Transport::with_inner_shells`]) uses the
49//! same Eq. 3.86 with `B = B_j`, the shell's binding energy:
50//! `E_SE = E_F + ω - B_j`. This is the shell-resolved form of de Vera et al.,
51//! Int. J. Mol. Sci. 23, 6121 (2022), eqs. (32) and (35) (the reference of
52//! [`crate::electron::inelastic::inner_shell`]): a loss `ω = W + B_j` in shell
53//! `j` gives the emitted electron `W = ω - B_j`, which here is its kinetic
54//! energy above the Fermi level, so `E_SE = E_F + W` on the band-bottom axis.
55//!
56//! **Binding reference.** Eq. 3.86 measures `B` below the Fermi level, and so
57//! does this module for a shell: `B_j` is used as given, as a binding energy
58//! below `E_F`. The subshell energies of a
59//! [`crate::electron::data::SubshellBindingTable`] (EADL) are free-atom
60//! values; they are not converted to a solid-state, Fermi-referenced edge
61//! (no work-function or chemical shift is applied). The event's
62//! [`SecondaryEvent::binding_ev`] is `B_j - E_F`, positive for every shell
63//! that lies below the Fermi level, and the tallies keep it in the solid at
64//! the event (`tally::electron`): atomic relaxation (Auger electrons,
65//! fluorescence photons) is not transported.
66//!
67//! **Limits.** The transport draws `ω` within `[B_j, E - E_F]` (`E` the
68//! primary's band-bottom energy), so `E_SE` lies in `[E_F, E - B_j]` and is
69//! never negative; a shell channel is closed when `E - E_F <= B_j`. Every
70//! shell event liberates an electron (no gap test); the electron is followed
71//! only if `E_SE` reaches the stopping threshold, else its energy is
72//! deposited, as for a valence event.
73//!
74//! **Direction.** The Ivanchenko transformation below with `B = B_j`
75//! (Verduin Eqs. 3.105-3.111 are written for a bound electron of any
76//! binding).
77//!
78//! # Direction
79//!
80//! The method of Ivanchenko, as used by Kieft and Bosch (Verduin pp. 82-85):
81//! the binding is transformed away, `E -> E - E_F + 2B` (Eq. 3.105) and
82//! `ΔE -> W + B` (Eq. 3.106), and the free-electron binary-collision angle of
83//! the secondary, `cos β = sqrt(ΔE / E)` (Eq. 3.100, non-relativistic), gives
84//! its direction about the primary's with a uniform azimuth (Eq. 3.107).
85//! Optionally a randomly oriented instantaneous momentum of relative size
86//! `sqrt(B / ΔE)` is added (Eq. 3.108), and the primary is deflected to
87//! conserve momentum, `p̂'_i ∝ p̂_i - cos β p̂''_t` (Eq. 3.111). Both options
88//! are on in Nebula's defaults and in [`SecondaryModel::KIEFT_BOSCH`]. The
89//! secondary starts at the primary's position (no delocalisation, Verduin
90//! p. 85).
91
92use rand_core::Rng;
93use serde::Serialize;
94
95use crate::electron::boundary::BandStructure;
96use crate::electron::transport::deflect_cs;
97use crate::rng::uniform;
98use crate::vec3::normalize;
99
100/// Whether and how inelastic events make secondary electrons.
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
102#[serde(tag = "model", rename_all = "kebab-case")]
103pub enum SecondaryModel {
104    /// No secondaries; the inelastic event only takes `W` from the primary
105    /// and keeps its direction.
106    Off,
107    /// Kieft and Bosch (module docs).
108    KieftBosch {
109        /// Add the instantaneous momentum of the bound electron to the
110        /// secondary's direction (Verduin Eq. 3.108).
111        instantaneous_momentum: bool,
112        /// Deflect the primary to conserve momentum (Verduin Eq. 3.111).
113        momentum_conservation: bool,
114    },
115}
116
117impl SecondaryModel {
118    /// Kieft and Bosch with both options on, Nebula's defaults
119    /// (`kieft_inelastic` template arguments at the commit in the module
120    /// docs).
121    pub const KIEFT_BOSCH: Self = Self::KieftBosch {
122        instantaneous_momentum: true,
123        momentum_conservation: true,
124    };
125}
126
127/// The energy bookkeeping of one inelastic event under a secondary model, all
128/// in eV. `loss_ev = secondary_ev + binding_ev + deposited_ev` up to
129/// rounding.
130#[derive(Debug, Clone, Copy, PartialEq)]
131pub struct SecondaryEvent {
132    /// `W`, the energy the primary lost.
133    pub loss_ev: f64,
134    /// Whether an electron was lifted into the conduction band (`false` for a
135    /// sub-gap loss in an insulator or a zero loss).
136    pub liberated: bool,
137    /// Kinetic energy of the secondary that was created and will be
138    /// transported, `E_F + W - B`; zero if none was (nothing liberated, or
139    /// the liberated electron was below the stopping threshold, in which case
140    /// its energy is in `deposited_ev`).
141    pub secondary_ev: f64,
142    /// `B - E_F` if an electron was liberated, else zero: how far its initial
143    /// state lay below the band bottom.
144    pub binding_ev: f64,
145    /// Energy left in the solid at the event: `W` for a sub-gap loss, the
146    /// liberated electron's energy if it was not followed, else zero.
147    pub deposited_ev: f64,
148}
149
150/// What [`kieft_bosch`] decided for one event.
151pub(crate) struct Outcome {
152    pub event: SecondaryEvent,
153    /// The new primary direction.
154    pub primary_dir: [f64; 3],
155    /// The secondary's direction and energy, if one is to be followed.
156    pub secondary: Option<([f64; 3], f64)>,
157}
158
159/// Apply the Kieft-Bosch secondary model to one valence inelastic event,
160/// ported from Nebula's `kieft_inelastic::execute` (file and commit in the
161/// module docs).
162///
163/// `dir` is the primary's unit direction and `e_ev` its energy before the
164/// loss `w_ev`; `threshold_ev` is the stopping threshold of the layer (a
165/// secondary below it is not followed). The binding energy comes from the
166/// band (module docs, "Energy"). Draws, in order, only when an electron is
167/// liberated: the azimuth of the secondary, then (with
168/// `instantaneous_momentum`) `U1` and `U2` of Verduin Eq. 3.108.
169#[allow(clippy::too_many_arguments)]
170pub(crate) fn kieft_bosch<R: Rng>(
171    instantaneous_momentum: bool,
172    momentum_conservation: bool,
173    band: &BandStructure,
174    dir: [f64; 3],
175    e_ev: f64,
176    w_ev: f64,
177    threshold_ev: f64,
178    rng: &mut R,
179) -> Outcome {
180    let binding = match band.band_gap_ev() {
181        None => {
182            // The Fermi level (0) unless the band sets a valence binding.
183            let b = band.valence_binding_ev().unwrap_or(0.0);
184            (w_ev > b).then_some(b)
185        }
186        Some(gap) if w_ev > gap => Some(gap),
187        Some(_) => None,
188    };
189    let Some(b) = binding else {
190        return Outcome {
191            event: SecondaryEvent {
192                loss_ev: w_ev,
193                liberated: false,
194                secondary_ev: 0.0,
195                binding_ev: 0.0,
196                deposited_ev: w_ev,
197            },
198            primary_dir: dir,
199            secondary: None,
200        };
201    };
202    liberate(
203        instantaneous_momentum,
204        momentum_conservation,
205        band.fermi_ev(),
206        b,
207        dir,
208        e_ev,
209        w_ev,
210        threshold_ev,
211        rng,
212    )
213}
214
215/// Apply the Kieft-Bosch secondary model to one **inner-shell** ionisation:
216/// a loss `w_ev` in the channel of a shell with binding energy
217/// `shell_binding_ev` (module docs, "Inner-shell events").
218///
219/// The caller guarantees `shell_binding_ev <= w_ev <= e_ev - E_F` (the
220/// channel's kinematic limits on the band-bottom axis), so the secondary
221/// energy `E_F + w - B` is at least `E_F` and at most `e_ev - B`. The event
222/// always liberates an electron; it is followed only if its energy reaches
223/// `threshold_ev`. Draws as [`kieft_bosch`] does for a liberated electron.
224#[allow(clippy::too_many_arguments)]
225pub(crate) fn kieft_bosch_shell<R: Rng>(
226    instantaneous_momentum: bool,
227    momentum_conservation: bool,
228    band: &BandStructure,
229    shell_binding_ev: f64,
230    dir: [f64; 3],
231    e_ev: f64,
232    w_ev: f64,
233    threshold_ev: f64,
234    rng: &mut R,
235) -> Outcome {
236    debug_assert!(w_ev >= shell_binding_ev && shell_binding_ev > 0.0);
237    liberate(
238        instantaneous_momentum,
239        momentum_conservation,
240        band.fermi_ev(),
241        shell_binding_ev,
242        dir,
243        e_ev,
244        w_ev,
245        threshold_ev,
246        rng,
247    )
248}
249
250/// The event of a loss `w_ev` that lifts an electron of binding energy `b`
251/// (below the Fermi level `fermi`) into the conduction band: Verduin
252/// Eqs. 3.86 and 3.100-3.111 as ported from Nebula (module docs).
253#[allow(clippy::too_many_arguments)]
254fn liberate<R: Rng>(
255    instantaneous_momentum: bool,
256    momentum_conservation: bool,
257    fermi: f64,
258    b: f64,
259    dir: [f64; 3],
260    e_ev: f64,
261    w_ev: f64,
262    threshold_ev: f64,
263    rng: &mut R,
264) -> Outcome {
265    // Verduin Eqs. 3.105, 3.106 and 3.100. `dk > 0` because `w > 0` and
266    // `b >= 0`; the ratio is clamped to [0, 1] (a loss near the full energy,
267    // or an energy below the Fermi level, would otherwise leave it).
268    let k = e_ev - fermi + 2.0 * b;
269    let dk = b + w_ev;
270    let cos2 = if k > 0.0 {
271        (dk / k).clamp(0.0, 1.0)
272    } else {
273        1.0
274    };
275    let cos_b = cos2.sqrt();
276    let sin_b = (1.0 - cos2).sqrt();
277
278    // Eq. 3.107: about the primary, uniform azimuth.
279    let phi = std::f64::consts::TAU * uniform(rng);
280    let mut sdir = deflect_cs(dir, cos_b, sin_b, phi);
281    if instantaneous_momentum {
282        // Eq. 3.108: cos(theta) = 2 U1 - 1, azimuth 2 pi U2.
283        let ct = 2.0 * uniform(rng) - 1.0;
284        let st = (1.0 - ct * ct).max(0.0).sqrt();
285        let (sp, cp) = (std::f64::consts::TAU * uniform(rng)).sin_cos();
286        let a = (b / dk).sqrt();
287        sdir = normalize([
288            sdir[0] + a * st * cp,
289            sdir[1] + a * st * sp,
290            sdir[2] + a * ct,
291        ]);
292    }
293
294    let primary_dir = if momentum_conservation {
295        // Eq. 3.111, up to normalisation.
296        let p = [
297            dir[0] - cos_b * sdir[0],
298            dir[1] - cos_b * sdir[1],
299            dir[2] - cos_b * sdir[2],
300        ];
301        let n2 = p[0] * p[0] + p[1] * p[1] + p[2] * p[2];
302        if n2 > 1e-24 && n2.is_finite() {
303            normalize(p)
304        } else {
305            // A head-on transfer of all the momentum leaves no direction;
306            // keep the old one.
307            dir
308        }
309    } else {
310        dir
311    };
312
313    // Eq. 3.86.
314    let e_se = fermi + w_ev - b;
315    let binding_ev = b - fermi;
316    let followed = e_se >= threshold_ev;
317    Outcome {
318        event: SecondaryEvent {
319            loss_ev: w_ev,
320            liberated: true,
321            secondary_ev: if followed { e_se } else { 0.0 },
322            binding_ev,
323            deposited_ev: if followed { 0.0 } else { e_se },
324        },
325        primary_dir,
326        secondary: followed.then_some((sdir, e_se)),
327    }
328}