Skip to main content

lindhard/electron/
knock_on.rs

1//! Knock-on (displacement) threshold report for an electron beam.
2//!
3//! Pure kinematics on resolved input; it needs no transport. For a beam
4//! electron of kinetic energy `E` hitting a free target atom of mass `M` at
5//! rest head on, conservation of energy and momentum gives the largest energy
6//! the atom can take (relativistic, electron rest energy `mc²`):
7//!
8//! ```text
9//! T_max = 2 E (E + 2 m c²) / (M c²)
10//! ```
11//!
12//! This follows from conservation of energy and momentum for a head-on
13//! elastic collision with a heavy atom at rest (electron momentum
14//! `p²c² = E (E + 2 m c²)`, recoil `T = 2 p²c² / (M c²)`); it is the form
15//! used in electron-irradiation damage work, for example F. Seitz and J. S.
16//! Koehler, Solid State Physics 2, 305 (1956) (not opened for this change;
17//! the derivation above stands on its own). It treats the atom as free and at
18//! rest and the recoil as non-relativistic (`T_max << Mc²`), so lattice
19//! binding, thermal motion and the crystal direction are ignored: `E_d` is
20//! the threshold displacement energy of the input, a damage-accounting
21//! convention, not a measured property of one direction.
22//!
23//! An atom can be displaced when `T_max >= E_d`. Setting `T_max = E_d` and
24//! solving the quadratic in `E` gives the threshold beam energy:
25//!
26//! ```text
27//! E_th = -m c² + sqrt((m c²)² + E_d M c² / 2)
28//! ```
29//!
30//! evaluated as `(E_d M c² / 2) / (m c² + sqrt(...))` so that it does not
31//! cancel for small `E_d / Mc²`.
32//!
33//! The report says nothing about a displacement cross section or a dpa: it
34//! is a threshold test only. An element with no `E_d` in the input is
35//! reported with `e_d_ev = None` (printed "not set"); no default is invented
36//! here.
37//!
38//! `M` is the atomic weight of the element (relative atomic mass) times the
39//! atomic mass unit; the electron mass difference of the atom is neglected.
40
41use serde::{Deserialize, Serialize};
42
43use crate::constants::{ATOMIC_MASS_UNIT, ELECTRON_REST_ENERGY, ELEMENTARY_CHARGE, SPEED_OF_LIGHT};
44use crate::elements::element;
45use crate::material::Material;
46
47/// Electron rest energy `m c²`, eV.
48pub fn electron_rest_energy_ev() -> f64 {
49    ELECTRON_REST_ENERGY / ELEMENTARY_CHARGE
50}
51
52/// Rest energy `M c²`, eV, of an atom of `atomic_weight` (relative atomic
53/// mass, i.e. g/mol).
54pub fn atom_rest_energy_ev(atomic_weight: f64) -> f64 {
55    atomic_weight * ATOMIC_MASS_UNIT * SPEED_OF_LIGHT * SPEED_OF_LIGHT / ELEMENTARY_CHARGE
56}
57
58/// Largest energy, eV, an electron of kinetic energy `beam_ev` can transfer
59/// to a free atom at rest of rest energy `atom_rest_ev` (see the module docs).
60pub fn t_max_ev(beam_ev: f64, atom_rest_ev: f64) -> f64 {
61    2.0 * beam_ev * (beam_ev + 2.0 * electron_rest_energy_ev()) / atom_rest_ev
62}
63
64/// Beam energy, eV, at which `T_max` equals `e_d_ev` (see the module docs).
65pub fn threshold_beam_energy_ev(e_d_ev: f64, atom_rest_ev: f64) -> f64 {
66    let mc2 = electron_rest_energy_ev();
67    let c = e_d_ev * atom_rest_ev / 2.0;
68    c / (mc2 + (mc2 * mc2 + c).sqrt())
69}
70
71/// The knock-on result for one element of a layer.
72#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
73pub struct KnockOnElement {
74    /// Atomic number.
75    pub z: u8,
76    /// Element symbol.
77    pub symbol: String,
78    /// Atomic weight used for `M`, g/mol.
79    pub atomic_weight: f64,
80    /// `T_max` at the beam energy, eV.
81    pub t_max_ev: f64,
82    /// The element's displacement energy `E_d`, eV; `None` if not set.
83    pub e_d_ev: Option<f64>,
84    /// Whether `T_max >= E_d`; `None` if `E_d` is not set.
85    pub can_displace: Option<bool>,
86    /// Beam energy at which `T_max = E_d`, eV; `None` if `E_d` is not set.
87    pub threshold_beam_ev: Option<f64>,
88}
89
90/// The knock-on result for every element of one layer.
91#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92pub struct KnockOnLayer {
93    /// Layer index in stack order.
94    pub layer: usize,
95    /// The beam energy the test was made at, eV.
96    pub beam_ev: f64,
97    /// One entry per element, in the material's order.
98    pub elements: Vec<KnockOnElement>,
99}
100
101/// Knock-on report of layer `layer`, made of `material`, for a beam of
102/// `beam_ev`. `E_d` is read from the material; an element without one is
103/// reported as not set.
104pub fn layer_report(layer: usize, material: &Material, beam_ev: f64) -> KnockOnLayer {
105    let elements = material
106        .components()
107        .iter()
108        .map(|c| {
109            let z = c.z();
110            let el = element(z).expect("material elements are validated");
111            let rest = atom_rest_energy_ev(el.atomic_weight);
112            let t = t_max_ev(beam_ev, rest);
113            let e_d = material.displacement_energy_ev(z).ok();
114            KnockOnElement {
115                z,
116                symbol: el.symbol.to_string(),
117                atomic_weight: el.atomic_weight,
118                t_max_ev: t,
119                e_d_ev: e_d,
120                can_displace: e_d.map(|d| t >= d),
121                threshold_beam_ev: e_d.map(|d| threshold_beam_energy_ev(d, rest)),
122            }
123        })
124        .collect();
125    KnockOnLayer {
126        layer,
127        beam_ev,
128        elements,
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    /// Closed form at the threshold, written out independently: with
137    /// `x = E_th`, `2 x (x + 2 mc²) = E_d Mc²`.
138    fn check(z: u8, e_d: f64) {
139        let el = element(z).unwrap();
140        let m = atom_rest_energy_ev(el.atomic_weight);
141        let mc2 = 510_998.950_0; // electron rest energy, eV (CODATA 2022)
142        let th = threshold_beam_energy_ev(e_d, m);
143        let direct = -mc2 + (mc2 * mc2 + e_d * m / 2.0).sqrt();
144        assert!(
145            (th - direct).abs() < 1e-6 * direct,
146            "Z={z}: {th} vs {direct}"
147        );
148        // T_max at the threshold is E_d, and straddles it either side.
149        assert!((t_max_ev(th, m) - e_d).abs() < 1e-9 * e_d);
150        assert!(t_max_ev(th * 0.99, m) < e_d);
151        assert!(t_max_ev(th * 1.01, m) > e_d);
152    }
153
154    #[test]
155    fn threshold_matches_closed_form_for_three_elements() {
156        check(6, 35.0); // C
157        check(14, 21.0); // Si
158        check(29, 25.0); // Cu
159    }
160
161    #[test]
162    fn layer_report_flags_and_unset_ed() {
163        let mut m = Material::from_atom_fractions(&[(14, 1.0)], None).unwrap();
164        m.set_displacement_energy_ev(14, 21.0).unwrap();
165        let el = element(14).unwrap();
166        let th = threshold_beam_energy_ev(21.0, atom_rest_energy_ev(el.atomic_weight));
167        let above = layer_report(0, &m, th * 1.1);
168        assert_eq!(above.elements[0].can_displace, Some(true));
169        let below = layer_report(0, &m, th * 0.9);
170        assert_eq!(below.elements[0].can_displace, Some(false));
171        assert!(below.elements[0].t_max_ev < 21.0);
172        assert!((below.elements[0].threshold_beam_ev.unwrap() - th).abs() < 1e-9 * th);
173    }
174
175    #[test]
176    fn element_without_ed_is_not_set_not_an_error() {
177        // Silicon has no E_d convention value (src/elements.rs), so it is unset.
178        let m = Material::from_atom_fractions(&[(14, 1.0)], None).unwrap();
179        let r = layer_report(2, &m, 1.0e6);
180        assert_eq!(r.layer, 2);
181        let e = &r.elements[0];
182        assert_eq!(
183            (e.e_d_ev, e.can_displace, e.threshold_beam_ev),
184            (None, None, None)
185        );
186        assert!(e.t_max_ev > 0.0);
187    }
188}