Skip to main content

lindhard/electron/inelastic/
inner_shell.rs

1//! Inner-shell ionization channels from shell-resolved loss functions.
2//!
3//! # Source
4//!
5//! P. de Vera, S. Taioli, P. E. Trevisanutto, M. Dapor, I. Abril, S.
6//! Simonucci and R. Garcia-Molina, "Energy Deposition around Swift Carbon-Ion
7//! Tracks in Liquid Water", Int. J. Mol. Sci. 23, 6121 (2022),
8//! doi:10.3390/ijms23116121 (open access, PMC9181504; "dV2022", as in
9//! [`super::penn`]):
10//!
11//! * eq. (1) splits the ELF into an outer-shell and an inner-shell part, and
12//!   eq. (2) writes the inner part as a sum over elements `j` (weighted by the
13//!   atoms per molecule `ν_j`) and subshells `nℓ` of atomic generalised
14//!   oscillator strengths, each multiplied by the step `Θ(E - B_nℓ^j)`: an
15//!   inner shell contributes only to losses above its binding energy;
16//! * eqs. (32) and (35) write the secondary-electron spectrum and the
17//!   ionization cross section as a sum of one outer-shell (valence) term and
18//!   one term per inner shell `j`, each built from **that shell's own loss
19//!   function** `Im[-1/ε(k, W + B_j)]_j`, where `W` is the kinetic energy of
20//!   the emitted electron. So a loss `ω = W + B_j` in shell `j` hands the
21//!   secondary `W = ω - B_j` ([`ShellResolvedChannels::secondary_energy_ev`]).
22//!
23//! # The model here
24//!
25//! [`ShellResolvedChannels`] takes what those equations take: a valence
26//! optical ELF and **one optical ELF per inner shell**. The shell ELFs are
27//! either supplied by the caller or built from atomic oscillator strengths
28//! as in dV2022 eq. (2) by [`super::shell_elf`] (the hydrogenic K-, L1-,
29//! L2- and L3-subshell formulas, with binding energies and occupancies from
30//! a [`SubshellBindingTable`]; other subshells are not built there). A single
31//! total ELF is never split: no partition rule of ours is applied. Each shell's ELF must
32//! start at or above its binding energy (the step of eq. (2)). Each channel's
33//! DIIMFP is the single-pole Penn DIIMFP ([`SinglePolePenn`]) of its own ELF;
34//! with the exchange correction applying, shell `j` uses its binding energy
35//! in the Born-Ochkur denominator `T' - W = T' - ω + B_j` and is limited to
36//! `ω <= (T' + B_j)/2`, and the valence channel has `B = 0` (dV2022 eqs. (32),
37//! (34), (35); see the `super::penn` module docs for the terms of eq. (32)
38//! not followed: the Coulomb-field correction of the direct term, and the
39//! energy-dependent valence binding energy `B(T)`).
40//!
41//! **Our choices, not from dV2022:** the momentum dependence of every
42//! channel's loss function is the Penn SPA of S2017 applied to that
43//! channel's optical ELF (dV2022 uses Mermin functions for the outer shells
44//! and hydrogenic GOS for the inner ones); and the channel models share the
45//! valence model's Fermi energy, tolerance and exchange setting. The SPA is
46//! linear in the optical ELF, so without exchange the channel inverse mean
47//! free paths add up to that of one model built from the sum of the ELFs
48//! (when the sum is representable on one grid; tested).
49//!
50//! The binding energies come from a [`SubshellBindingTable`]
51//! ([`InnerShell::from_table`]) or are given directly.
52//!
53//! # In the transport
54//!
55//! [`super::table::build_shell_channel_tables`] tabulates every channel
56//! (one [`ShellChannelTable`] per shell, beside the valence table), and
57//! [`crate::electron::transport::Transport::with_inner_shells`] samples them:
58//! a channel is chosen first, with probability proportional to its inverse
59//! IMFP, and the loss is then drawn from that channel's own table. That is a
60//! different construction from [`ShellResolvedChannels::sample_channel`]
61//! (channel conditional on a loss already drawn from the total DIIMFP), which
62//! the transport does not use. The secondary-energy convention on the
63//! transport's band-bottom axis is in [`crate::electron::secondary`],
64//! "Inner-shell events".
65
66use super::penn::SinglePolePenn;
67use crate::electron::data::{
68    ElectronDataError, OpticalElf, ShellChannelTable, Subshell, SubshellBindingTable,
69};
70
71type Result<T> = std::result::Result<T, ElectronDataError>;
72
73fn invalid(what: &'static str, reason: String) -> ElectronDataError {
74    ElectronDataError::Invalid { what, reason }
75}
76
77/// An inner-shell channel: one subshell of one element.
78#[derive(Debug, Clone, Copy, PartialEq)]
79pub struct InnerShell {
80    /// Atomic number.
81    pub z: u8,
82    /// The subshell.
83    pub subshell: Subshell,
84    /// Binding energy `B`, eV.
85    pub binding_energy_ev: f64,
86}
87
88impl InnerShell {
89    /// The shell `subshell` of element `z`, with its binding energy from
90    /// `table`.
91    pub fn from_table(table: &SubshellBindingTable, z: u8, subshell: Subshell) -> Result<Self> {
92        let binding_energy_ev = table
93            .atom(z)
94            .and_then(|a| a.binding_energy_ev(subshell))
95            .ok_or_else(|| {
96                invalid(
97                    "inner shell",
98                    format!("Z = {z} {} is not in the binding table", subshell.label()),
99                )
100            })?;
101        Ok(Self {
102            z,
103            subshell,
104            binding_energy_ev,
105        })
106    }
107}
108
109impl From<&ShellChannelTable> for InnerShell {
110    /// The shell a stored channel table belongs to.
111    fn from(t: &ShellChannelTable) -> Self {
112        Self {
113            z: t.z(),
114            subshell: t.subshell(),
115            binding_energy_ev: t.binding_energy_ev(),
116        }
117    }
118}
119
120/// A loss channel.
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum Channel {
123    /// Valence excitation and ionization.
124    Valence,
125    /// Ionization of inner shell `i` (an index into
126    /// [`ShellResolvedChannels::inner_shells`]).
127    InnerShell(usize),
128}
129
130/// Inverse mean free path by channel, m⁻¹.
131#[derive(Debug, Clone, PartialEq)]
132pub struct ChannelInverseImfp {
133    /// Valence channel.
134    pub valence_per_m: f64,
135    /// Inner-shell channels, in the order of
136    /// [`ShellResolvedChannels::inner_shells`].
137    pub shells_per_m: Vec<f64>,
138}
139
140impl ChannelInverseImfp {
141    /// The sum over channels.
142    pub fn total_per_m(&self) -> f64 {
143        self.valence_per_m + self.shells_per_m.iter().sum::<f64>()
144    }
145}
146
147/// DIIMFP by channel at one loss, m⁻¹ eV⁻¹.
148#[derive(Debug, Clone, PartialEq)]
149pub struct ChannelDiimfp {
150    /// Valence channel.
151    pub valence_per_m_ev: f64,
152    /// Inner-shell channels, in the order of
153    /// [`ShellResolvedChannels::inner_shells`].
154    pub shells_per_m_ev: Vec<f64>,
155}
156
157impl ChannelDiimfp {
158    /// The sum over channels.
159    pub fn total_per_m_ev(&self) -> f64 {
160        self.valence_per_m_ev + self.shells_per_m_ev.iter().sum::<f64>()
161    }
162}
163
164/// A valence channel and inner-shell channels, each with its own optical ELF
165/// (module docs).
166#[derive(Debug, Clone)]
167pub struct ShellResolvedChannels {
168    valence: SinglePolePenn,
169    shells: Vec<InnerShell>,
170    models: Vec<SinglePolePenn>,
171}
172
173impl ShellResolvedChannels {
174    /// Channels from a `valence` model and one optical ELF per inner shell,
175    /// kept in the given order. The shell models take the valence model's
176    /// Fermi energy, tolerance and exchange setting. Each binding energy must
177    /// be finite and positive, each shell's ELF must start at or above its
178    /// binding energy, and a `(Z, subshell)` may not repeat.
179    pub fn new(valence: SinglePolePenn, shells: Vec<(InnerShell, OpticalElf)>) -> Result<Self> {
180        let mut inner = Vec::with_capacity(shells.len());
181        let mut models = Vec::with_capacity(shells.len());
182        for (shell, elf) in shells {
183            let b = shell.binding_energy_ev;
184            let name = format!("Z = {} {}", shell.z, shell.subshell.label());
185            if !(b.is_finite() && b > 0.0) {
186                return Err(invalid(
187                    "inner shell",
188                    format!("{name}: binding energy must be finite and positive, got {b} eV"),
189                ));
190            }
191            let first = elf.energy_range_ev().0;
192            if first < b {
193                return Err(invalid(
194                    "inner-shell ELF",
195                    format!(
196                        "{name}: the ELF starts at {first} eV, below the binding energy {b} eV \
197                         (a shell contributes only above its edge)"
198                    ),
199                ));
200            }
201            if inner
202                .iter()
203                .any(|s: &InnerShell| s.z == shell.z && s.subshell == shell.subshell)
204            {
205                return Err(invalid("inner shell", format!("{name} listed twice")));
206            }
207            models.push(valence.same_settings_for(elf));
208            inner.push(shell);
209        }
210        Ok(Self {
211            valence,
212            shells: inner,
213            models,
214        })
215    }
216
217    /// The valence model.
218    pub fn valence(&self) -> &SinglePolePenn {
219        &self.valence
220    }
221
222    /// The inner-shell channels, in the order given.
223    pub fn inner_shells(&self) -> &[InnerShell] {
224        &self.shells
225    }
226
227    /// The model of inner shell `i` (its own ELF, the valence settings).
228    pub fn shell_model(&self, i: usize) -> &SinglePolePenn {
229        &self.models[i]
230    }
231
232    /// The channel-resolved inverse IMFP at kinetic energy `energy_ev` above
233    /// the Fermi level: the valence model's `λ⁻¹`, and for shell `i`
234    /// [`SinglePolePenn::imfp_and_stopping_with_binding`] of its model with
235    /// its binding energy (module docs).
236    pub fn inverse_imfps(&self, energy_ev: f64) -> Result<ChannelInverseImfp> {
237        let valence_per_m = self
238            .valence
239            .imfp_and_stopping(energy_ev)?
240            .inverse_imfp_per_m;
241        let shells_per_m = self
242            .shells
243            .iter()
244            .zip(&self.models)
245            .map(|(s, m)| {
246                m.imfp_and_stopping_with_binding(energy_ev, s.binding_energy_ev)
247                    .map(|p| p.inverse_imfp_per_m)
248            })
249            .collect::<Result<Vec<_>>>()?;
250        Ok(ChannelInverseImfp {
251            valence_per_m,
252            shells_per_m,
253        })
254    }
255
256    /// The channel-resolved DIIMFP at kinetic energy `energy_ev` and loss
257    /// `loss_ev` ([`SinglePolePenn::diimfp_with_binding_per_m_ev`] per
258    /// channel).
259    pub fn diimfps_per_m_ev(&self, energy_ev: f64, loss_ev: f64) -> Result<ChannelDiimfp> {
260        let valence_per_m_ev = self.valence.diimfp_per_m_ev(energy_ev, loss_ev)?;
261        let shells_per_m_ev = self
262            .shells
263            .iter()
264            .zip(&self.models)
265            .map(|(s, m)| m.diimfp_with_binding_per_m_ev(energy_ev, loss_ev, s.binding_energy_ev))
266            .collect::<Result<Vec<_>>>()?;
267        Ok(ChannelDiimfp {
268            valence_per_m_ev,
269            shells_per_m_ev,
270        })
271    }
272
273    /// The channel of a loss `loss_ev` at kinetic energy `energy_ev`, for a
274    /// uniform variate `u` in `[0, 1)`: each channel with probability
275    /// proportional to its DIIMFP there, the inner shells first in order,
276    /// then valence. `None` if every channel's DIIMFP is zero there.
277    /// Deterministic in its arguments.
278    pub fn sample_channel(&self, energy_ev: f64, loss_ev: f64, u: f64) -> Result<Option<Channel>> {
279        let d = self.diimfps_per_m_ev(energy_ev, loss_ev)?;
280        let total = d.total_per_m_ev();
281        if total <= 0.0 {
282            return Ok(None);
283        }
284        let mut acc = 0.0;
285        for (i, p) in d.shells_per_m_ev.iter().enumerate() {
286            acc += p / total;
287            if *p > 0.0 && u < acc {
288                return Ok(Some(Channel::InnerShell(i)));
289            }
290        }
291        Ok(Some(Channel::Valence))
292    }
293
294    /// The kinetic energy `W = ω - B` handed to the secondary electron by an
295    /// inner-shell loss `loss_ev`, eV (dV2022 eqs. (32), (34)); `None` for the
296    /// valence channel (its secondary energy depends on the band structure,
297    /// see `electron::secondary`) and below the edge.
298    pub fn secondary_energy_ev(&self, channel: Channel, loss_ev: f64) -> Option<f64> {
299        match channel {
300            Channel::Valence => None,
301            Channel::InnerShell(i) => {
302                let w = loss_ev - self.shells.get(i)?.binding_energy_ev;
303                (w >= 0.0).then_some(w)
304            }
305        }
306    }
307}