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}