Skip to main content

lindhard/electron/inelastic/
shell_elf.rs

1//! Inner-shell optical ELFs built from the hydrogenic K-shell (1s) and
2//! L-subshell (2s, 2p) photoionization formulas, for
3//! [`ShellResolvedChannels`].
4//!
5//! # Sources (opened 2026-10-08; Karzas and Latter opened 2026-10-09)
6//!
7//! * **The shell ELF.** P. de Vera, S. Taioli, P. E. Trevisanutto, M. Dapor,
8//!   I. Abril, S. Simonucci and R. Garcia-Molina, Int. J. Mol. Sci. 23, 6121
9//!   (2022), doi:10.3390/ijms23116121 (open access, PMC9181504; "dV2022", as
10//!   in [`super::inner_shell`]), eq. (2): the inner-shell part of the ELF of a
11//!   target with `N` molecules per unit volume, `ν_j` atoms of element `j`
12//!   per molecule, is
13//!
14//!   ```text
15//!   Im[-1/ε(k, E)]_inner = (2π² ħ² e² N / E) Σ_j ν_j Σ_nℓ df_nℓ^j(k, E)/dE Θ(E - B_nℓ^j),
16//!   ```
17//!
18//!   written in atomic units (the electron mass is set to 1). The prefactor
19//!   with the mass restored is fixed by the f-sum rule of H. Shinotsuka et
20//!   al., Surf. Interface Anal. 49, 238 (2017), doi:10.1002/sia.6123 (open
21//!   copy PMC5524379), eq. (18), `N_eff = (2 / (π (ħΩ_a)²)) ∫ W ELF dW` with
22//!   `(ħΩ_a)² = ħ² n_a e² / (ε₀ m_e)` (SI, the form [`super::sum_rules`]
23//!   uses): requiring `N_eff = ∫ df` for one shell of atoms at density `n_a`
24//!   gives, in the optical limit `k -> 0`,
25//!
26//!   ```text
27//!   ELF_nℓ(W) = (π/2) (ħΩ_a)² (1/W) df_nℓ/dW,   W >= B_nℓ,   zero below,
28//!   ```
29//!
30//!   which is dV2022 eq. (2) for one shell (`(π/2) 4π = 2π²` with the
31//!   Gaussian `Ω² = 4π n e²/m`). The shell's oscillator strength is then
32//!   exactly the shell's `N_eff` (tested).
33//!
34//! * **The oscillator-strength density.** F. Salvat, "Inelastic collisions of
35//!   fast charged particles with atoms. Relativistic plane-wave Born
36//!   approximation", arXiv:2605.22442 (2026), eq. (6.2): the optical
37//!   oscillator strength is proportional to the dipole photoabsorption cross
38//!   section, `df/dW = (m_e c / (2π² e² ħ)) σ_ph(W)` (Gaussian `e²`); and eq.
39//!   (6.1a): a subshell with `q_a` of its `2|κ_a|` places filled contributes
40//!   `q_a / 2|κ_a|` times the closed-subshell value, i.e. the oscillator
41//!   strength scales with the occupancy.
42//!
43//! * **The hydrogenic K-shell cross section.** M. Stobbe, Ann. Phys. (Leipzig)
44//!   7, 661 (1930) (not read), as printed by A. I. Mikhailov, A. V. Nefiodov
45//!   and G. Plunien, Phys. Lett. A 368, 391 (2007),
46//!   doi:10.1016/j.physleta.2007.04.027 (open copy arXiv:0704.2180), eq. (1):
47//!   for one electron in the 1s state of a hydrogen-like ion of charge `Z`,
48//!
49//!   ```text
50//!   σ_K = α a₀² (2⁹ π² / (3 Z²)) exp(-4ξ cot⁻¹ ξ) / ((1 + ξ⁻²)⁴ [1 - exp(-2πξ)]),
51//!   ```
52//!
53//!   with `ξ = 1/sqrt(ε_γ - 1)`, `ε_γ = W/I` the photon energy over the
54//!   ionization energy `I = Z² E_h / 2`, valid for `1 <= ε_γ << 2 (αZ)⁻²`
55//!   (dipole approximation, nonrelativistic). Its eq. (3) is the threshold
56//!   value (`ξ -> ∞`, `exp(-4)`), its eq. (2) the high-energy limit
57//!   `α a₀² (2⁸ π / (3 Z²)) ε_γ^(-7/2)`, and its eq. (6) says that for two
58//!   independent K electrons the cross section is `2 σ_K`.
59//!
60//! * **The hydrogenic L-subshell cross sections.** W. J. Karzas and R.
61//!   Latter, "Electron Radiative Transitions in a Coulomb Field", Astrophys.
62//!   J. Suppl. 6, 167 (1961) ("KL1961"; open scan at
63//!   <https://articles.adsabs.harvard.edu/pdf/1961ApJS....6..167K>, read
64//!   from the page images, the text layer of the scan being unreliable in
65//!   the equations). For a dipole transition from the bound state `(n, ℓ)`
66//!   of a hydrogen-like ion to the continuum, averaged over the magnetic
67//!   substates (eq. (27), one electron), the cross section is the sum, eq.
68//!   (38), of the `ℓ -> ℓ-1` part, eq. (36),
69//!
70//!   ```text
71//!   σ_(nℓ -> E,ℓ-1) = (π e² / (m c ν)) (2^(4ℓ) / 3)
72//!       ℓ² (n+ℓ)! {[1² + η²][2² + η²] ... [(ℓ-1)² + η²]} / ((2ℓ+1)! (2ℓ-1)! (n-ℓ-1)!)
73//!       × (exp[-4η cot⁻¹ρ] / (1 - e^(-2πη))) (ρ^(2ℓ+2) / (1 + ρ²)^(2n-2))
74//!       × [G_ℓ(ℓ+1-n; η; ρ) - (1 + ρ²)⁻² G_ℓ(ℓ-1-n; η; ρ)]²
75//!   ```
76//!
77//!   (the expression in braces is 1 for `ℓ = 1`), and the `ℓ -> ℓ+1` part,
78//!   eq. (37),
79//!
80//!   ```text
81//!   σ_(nℓ -> E,ℓ+1) = (π e² / (m c ν)) (2^(4ℓ+6) / 3)
82//!       (ℓ+1)² (n+ℓ)! (1² + η²)(2² + η²) ... [(ℓ+1)² + η²]
83//!         / ((2ℓ+1) (2ℓ+1)! (2ℓ+2)! (n-ℓ-1)! [(ℓ+1)² + η²]²)
84//!       × (exp[-4η cot⁻¹ρ] / (1 - e^(-2πη))) (ρ^(2ℓ+4) η² / (1 + ρ²)^(2n))
85//!       × [(ℓ+1-n) G_(ℓ+1)(ℓ+1-n; η; ρ) + ((ℓ+1+n) / (1 + ρ²)) G_(ℓ+1)(ℓ-n; η; ρ)]²,
86//!   ```
87//!
88//!   with `η = (Z² Ry / E)^(1/2)` for a free electron of energy `E` (eq.
89//!   (33)), `ρ = η/n`, and the real polynomials of its Appendix C,
90//!   `G_ℓ(-m; η; ρ) = Σ_(s=0)^(2m) b_s ρ^s` (eq. (C.5)) with, eq. (C.8),
91//!
92//!   ```text
93//!   b_0 = 1,   b_1 = 2mη/ℓ,
94//!   b_s = -(1 / (s (s + 2ℓ - 1))) [4η (s-1-m) b_(s-1) + (2m+2-s)(2m+2ℓ+1-s) b_(s-2)].
95//!   ```
96//!
97//!   Kramers' cross section is its eq. (39),
98//!   `σ^K = (2⁴ / (3√3)) (e² / (m c ν)) (1/n) (ρ² / (1 + ρ²))²`, and the
99//!   Gaunt factor its eq. (40), `g = σ / σ^K`, tabulated in its Table 1 for
100//!   `n = 1 ... 6` and every `ℓ` against `E / Z² Ry`.
101//!
102//! # The formulas used
103//!
104//! Inserting eq. (1) into eq. (6.2) with `e² = α ħ c`, `a₀ = ħ / (m_e c α)`
105//! and `I = Z² E_h / 2`, `E_h = ħ² / (m_e a₀²)` (our algebra; the test
106//! `oscillator_strength_density_is_eq_6_2_of_the_stobbe_cross_section`
107//! redoes it numerically with CODATA constants), the oscillator-strength
108//! density per K electron is
109//!
110//! ```text
111//! df_K/dW = (2⁷ / (3 I)) exp(-4 atan(t) / t) / ((1 + t²)⁴ [1 - exp(-2π/t)]),   t = 1/ξ = sqrt(W/I - 1),
112//! ```
113//!
114//! equal to `(2⁷ / (3 I)) e⁻⁴` at the edge. Integrated from the edge to
115//! infinity it gives about 0.435 per electron (computed here, by quadrature
116//! of the formula; the rest of the hydrogenic 1s oscillator strength goes to
117//! discrete excitations below the edge, which dV2022 eq. (2) leaves out with
118//! its step `Θ(E - B)`).
119//!
120//! **The L subshells.** The photon energy is `W = hν = E + I` with
121//! `I = Z² Ry / n²`, so `W/I = 1 + 1/ρ²` and `ρ = 1/t` with the same
122//! `t = sqrt(W/I - 1)`. Salvat's eq. (6.2) turns the prefactor
123//! `π e² / (m c ν)` of KL1961 eqs. (36) and (37) into `1/W`: `df/dW` is the
124//! rest of the right-hand side over `W`. For `n = 2` (`η = 2ρ`) the
125//! polynomials of eq. (C.8) are (our algebra)
126//!
127//! ```text
128//! G_1(0) = G_2(0) = 1,
129//! G_1(-1; η; ρ) = 1 + 2ηρ - ρ²                              = 1 + 3ρ²,
130//! G_1(-2; η; ρ) = 1 + 4ηρ + ((8η² - 10)/3) ρ² - 4ηρ³ + ρ⁴ = (1 + ρ²)(1 + (11/3) ρ²),
131//! G_2(-1; η; ρ) = 1 + ηρ - ρ²                               = 1 + ρ²,
132//! ```
133//!
134//! the bracket of eq. (37) is `2 (1 + 4ρ²)` for 2s and `4` for 2p, and that
135//! of eq. (36) is `-(8/3) ρ² / (1 + ρ²)` for 2p. Per electron, with
136//! `ε = W/I = 1 + t²`,
137//!
138//! ```text
139//! df_2s/dW = (2¹⁰ / (3 I)) ((ε + 3) / ε⁵)    exp(-8 atan(t) / t) / [1 - exp(-4π/t)],
140//! df_2p/dW = (2¹⁰ / (9 I)) ((3ε + 8) / ε⁶)   exp(-8 atan(t) / t) / [1 - exp(-4π/t)],
141//! ```
142//!
143//! equal to `(2¹² / (3 I)) e⁻⁸` and `(11 · 2¹⁰ / (9 I)) e⁻⁸` at the edge.
144//! Of the 2p value, `(2¹³ / (27 I)) (ε + 3) / ε⁶` times the same last factor
145//! is the transition to the d continuum, eq. (37), and
146//! `(2¹⁰ / (27 I)) / ε⁵` times it the transition to the s continuum, eq.
147//! (36). The same
148//! reduction for `n = 1` gives back the K-shell formula above, an
149//! independent check of the transcription. The unit tests of this module
150//! evaluate eqs. (36), (37) and (C.8) as printed and compare them with these
151//! closed forms; `lindhard/tests/penn_shell_elf.rs` compares the closed
152//! forms with the 2s and 2p Gaunt factors of KL1961 Table 1. Integrated from
153//! the edge to infinity they give about 0.351 (2s) and 0.191 (2p) per
154//! electron (computed here, by quadrature of the formulas).
155//!
156//! **Our choices, not from the sources:**
157//!
158//! * The ionization energy `I` of the formula is set to the shell's binding
159//!   energy `B` from the [`SubshellBindingTable`] (for example EADL2017),
160//!   so the edge of the ELF is the tabulated edge. For a hydrogen-like ion
161//!   this is the formula itself; for a many-electron atom it amounts to a
162//!   hydrogenic shell with the effective charge `Z_eff = sqrt(2B/E_h)` that
163//!   reproduces `B`. No screening rule is applied beyond that.
164//! * The oscillator strength is the per-electron value times the subshell
165//!   occupancy from the same table (Salvat eq. (6.1a), Mikhailov et al. eq.
166//!   (6)). L1 is the 2s formula. L2 (2p1/2) and L3 (2p3/2) are both the
167//!   nonrelativistic 2p formula, each with its own tabulated binding energy
168//!   and occupancy, so the 2p oscillator strength is shared between them in
169//!   the ratio of their occupancies.
170//! * The ELF is sampled on a log-spaced grid from `B` to `B` times
171//!   [`ShellElfGrid::max_energy_factor`] and interpolated linearly between
172//!   the samples, like every [`OpticalElf`]. The formula is nonrelativistic;
173//!   the caller keeps the top of the grid well below `2 B (αZ)⁻²`.
174//!
175//! # Scope
176//!
177//! Only the K, L1, L2 and L3 subshells are built; any other subshell (M and
178//! above) is an error. KL1961 eqs. (36) and (37) hold for every `(n, ℓ)`,
179//! but their reduction for `n >= 3` has not been done or checked here. The
180//! valence remainder (total ELF minus the inner shells) is not built here.
181//!
182//! Nothing here is random: the tables are bit-identical whatever the number
183//! of threads.
184//!
185//! [`ShellResolvedChannels`]: super::ShellResolvedChannels
186
187use super::inner_shell::InnerShell;
188use crate::constants::{ELECTRON_MASS, ELEMENTARY_CHARGE, HBAR, VACUUM_PERMITTIVITY};
189use crate::electron::data::{ElectronDataError, OpticalElf, Subshell, SubshellBindingTable};
190
191type Result<T> = std::result::Result<T, ElectronDataError>;
192
193fn invalid(what: &'static str, reason: String) -> ElectronDataError {
194    ElectronDataError::Invalid { what, reason }
195}
196
197/// The citation recorded in the provenance of every built K-shell ELF.
198const SOURCE: &str = "hydrogenic K-shell optical ELF: Stobbe (1930) cross section as printed \
199     in Mikhailov, Nefiodov and Plunien, Phys. Lett. A 368, 391 (2007), arXiv:0704.2180, eq. (1); \
200     df/dW from Salvat, arXiv:2605.22442 (2026), eqs. (6.1a), (6.2); ELF from de Vera et al., \
201     Int. J. Mol. Sci. 23, 6121 (2022), eq. (2), prefactor from Shinotsuka et al., Surf. Interface \
202     Anal. 49, 238 (2017), eq. (18); ionization energy set to the tabulated binding energy";
203
204/// The hydrogenic K-shell optical oscillator-strength density per electron,
205/// `df/dW` in eV⁻¹, at energy loss `energy_ev` for a shell of binding energy
206/// `binding_energy_ev` (module docs, "The formula used"). Zero below the
207/// edge; at the edge, its limit `(2⁷/(3B)) e⁻⁴`.
208pub fn hydrogenic_k_oscillator_strength_density_per_ev(
209    binding_energy_ev: f64,
210    energy_ev: f64,
211) -> f64 {
212    // Also zero for a NaN loss.
213    if energy_ev.is_nan() || energy_ev < binding_energy_ev {
214        return 0.0;
215    }
216    let prefactor = 128.0 / (3.0 * binding_energy_ev);
217    let eps = energy_ev / binding_energy_ev;
218    let t2 = eps - 1.0;
219    if t2 <= 0.0 {
220        return prefactor * (-4.0f64).exp();
221    }
222    let t = t2.sqrt();
223    // exp(-4 ξ cot⁻¹ξ) with ξ = 1/t: ξ cot⁻¹ ξ = atan(t)/t.
224    let num = (-4.0 * t.atan() / t).exp();
225    // (1 + ξ⁻²)⁴ = (1 + t²)⁴ = ε⁴; 1 - exp(-2πξ) = -expm1(-2π/t).
226    let den = eps.powi(4) * -(-std::f64::consts::TAU / t).exp_m1();
227    prefactor * num / den
228}
229
230/// The citation recorded in the provenance of every built L-subshell ELF.
231const SOURCE_L: &str = "hydrogenic L-subshell optical ELF: 2s and 2p cross sections from \
232     Karzas and Latter, Astrophys. J. Suppl. 6, 167 (1961), eqs. (36), (37), (38) and (C.8), \
233     reduced for n = 2; df/dW from Salvat, arXiv:2605.22442 (2026), eqs. (6.1a), (6.2); ELF from \
234     de Vera et al., Int. J. Mol. Sci. 23, 6121 (2022), eq. (2), prefactor from Shinotsuka et al., \
235     Surf. Interface Anal. 49, 238 (2017), eq. (18); ionization energy set to the tabulated \
236     binding energy";
237
238/// The factor `exp(-8 atan(t)/t) / [1 - exp(-4π/t)]` common to the two
239/// `n = 2` formulas (`exp[-4η cot⁻¹ρ] / (1 - e^(-2πη))` of Karzas and Latter
240/// 1961, eqs. (36) and (37), with `η = 2ρ = 2/t`), for `t² = W/B - 1`; its
241/// limit `e⁻⁸` at the edge.
242fn n2_coulomb_factor(t2: f64) -> f64 {
243    if t2 <= 0.0 {
244        return (-8.0f64).exp();
245    }
246    let t = t2.sqrt();
247    (-8.0 * t.atan() / t).exp() / -(-2.0 * std::f64::consts::TAU / t).exp_m1()
248}
249
250/// The hydrogenic 2s (L1) optical oscillator-strength density per electron,
251/// `df/dW` in eV⁻¹, at energy loss `energy_ev` for a subshell of binding
252/// energy `binding_energy_ev`: Karzas and Latter, Astrophys. J. Suppl. 6,
253/// 167 (1961), eq. (37) for `n = 2`, `ℓ = 0`, converted by Salvat,
254/// arXiv:2605.22442, eq. (6.2) (module docs, "The formulas used"). Zero
255/// below the edge; at the edge, its limit `(2¹²/(3B)) e⁻⁸`.
256pub fn hydrogenic_2s_oscillator_strength_density_per_ev(
257    binding_energy_ev: f64,
258    energy_ev: f64,
259) -> f64 {
260    // Also zero for a NaN loss.
261    if energy_ev.is_nan() || energy_ev < binding_energy_ev {
262        return 0.0;
263    }
264    let eps = energy_ev / binding_energy_ev;
265    1024.0 / (3.0 * binding_energy_ev) * (eps + 3.0) / eps.powi(5) * n2_coulomb_factor(eps - 1.0)
266}
267
268/// The hydrogenic 2p (L2, L3) optical oscillator-strength density per
269/// electron, `df/dW` in eV⁻¹, at energy loss `energy_ev` for a subshell of
270/// binding energy `binding_energy_ev`: Karzas and Latter, Astrophys. J.
271/// Suppl. 6, 167 (1961), eqs. (36), (37) and (38) for `n = 2`, `ℓ = 1`
272/// (the s and d continua together), converted by Salvat, arXiv:2605.22442,
273/// eq. (6.2) (module docs, "The formulas used"). Zero below the edge; at
274/// the edge, its limit `(11 · 2¹⁰/(9B)) e⁻⁸`.
275pub fn hydrogenic_2p_oscillator_strength_density_per_ev(
276    binding_energy_ev: f64,
277    energy_ev: f64,
278) -> f64 {
279    // Also zero for a NaN loss.
280    if energy_ev.is_nan() || energy_ev < binding_energy_ev {
281        return 0.0;
282    }
283    let eps = energy_ev / binding_energy_ev;
284    1024.0 / (9.0 * binding_energy_ev) * (3.0 * eps + 8.0) / eps.powi(6)
285        * n2_coulomb_factor(eps - 1.0)
286}
287
288/// The hydrogenic orbital a subshell is built from.
289#[derive(Debug, Clone, Copy, PartialEq, Eq)]
290enum Orbital {
291    /// K.
292    S1,
293    /// L1.
294    S2,
295    /// L2 and L3.
296    P2,
297}
298
299impl Orbital {
300    /// K, L1, L2 and L3 (ENDF designators 1 to 4); nothing else.
301    fn of(subshell: Subshell) -> Option<Self> {
302        match subshell.designator() {
303            1 => Some(Self::S1),
304            2 => Some(Self::S2),
305            3 | 4 => Some(Self::P2),
306            _ => None,
307        }
308    }
309
310    fn source(self) -> &'static str {
311        match self {
312            Self::S1 => SOURCE,
313            Self::S2 | Self::P2 => SOURCE_L,
314        }
315    }
316
317    /// `df/dW` per electron, eV⁻¹.
318    fn oscillator_strength_density_per_ev(self, binding_energy_ev: f64, energy_ev: f64) -> f64 {
319        match self {
320            Self::S1 => {
321                hydrogenic_k_oscillator_strength_density_per_ev(binding_energy_ev, energy_ev)
322            }
323            Self::S2 => {
324                hydrogenic_2s_oscillator_strength_density_per_ev(binding_energy_ev, energy_ev)
325            }
326            Self::P2 => {
327                hydrogenic_2p_oscillator_strength_density_per_ev(binding_energy_ev, energy_ev)
328            }
329        }
330    }
331}
332
333/// The sampling grid of a built shell ELF.
334#[derive(Debug, Clone, Copy, PartialEq)]
335pub struct ShellElfGrid {
336    /// Log-spaced samples per decade of energy (at least 2).
337    pub points_per_decade: usize,
338    /// The top of the grid over the binding energy, `W_max / B` (above 1).
339    pub max_energy_factor: f64,
340}
341
342impl Default for ShellElfGrid {
343    /// 300 points per decade up to `10³ B`. The oscillator strength above
344    /// `10³ B` is below 1e-6 of the shell's (K, L1, L2 and L3), and the
345    /// linear interpolation raises the f-sum by about 1e-4 (both tested; the
346    /// error falls as the square of the spacing).
347    fn default() -> Self {
348        Self {
349            points_per_decade: 300,
350            max_energy_factor: 1.0e3,
351        }
352    }
353}
354
355impl ShellElfGrid {
356    fn energies_ev(&self, binding_energy_ev: f64) -> Result<Vec<f64>> {
357        let f = self.max_energy_factor;
358        if self.points_per_decade < 2 || !(f.is_finite() && f > 1.0) {
359            return Err(invalid(
360                "shell ELF grid",
361                format!(
362                    "need at least 2 points per decade and a finite top factor above 1, got {} \
363                     and {f}",
364                    self.points_per_decade
365                ),
366            ));
367        }
368        let decades = f.log10();
369        let n = ((decades * self.points_per_decade as f64).ceil() as usize).max(1);
370        let mut e: Vec<f64> = (0..=n)
371            .map(|i| binding_energy_ev * 10f64.powf(decades * i as f64 / n as f64))
372            .collect();
373        // The first sample is the edge itself, bit for bit.
374        e[0] = binding_energy_ev;
375        Ok(e)
376    }
377}
378
379/// `(ħΩ_a)² = ħ² n_a e² / (ε₀ m_e)` in eV² for `n_a` per m³ (Shinotsuka et
380/// al. 2017, eq. (18)).
381fn plasma_energy_sq_ev2(density_per_m3: f64) -> f64 {
382    let j2 = HBAR * HBAR * density_per_m3 * ELEMENTARY_CHARGE * ELEMENTARY_CHARGE
383        / (VACUUM_PERMITTIVITY * ELECTRON_MASS);
384    j2 / (ELEMENTARY_CHARGE * ELEMENTARY_CHARGE)
385}
386
387/// The hydrogenic optical ELF of one inner shell (module docs): the shell
388/// `subshell` of element `z`, with binding energy and occupancy from
389/// `table`, for `atoms_per_m3` atoms of that element per m³ in the target
390/// `material`. Returns the [`InnerShell`] and its [`OpticalElf`], ready for
391/// [`super::ShellResolvedChannels::new`]: the ELF starts at the binding
392/// energy and is zero below it.
393///
394/// K is built from the 1s formula, L1 from the 2s formula, and L2 and L3
395/// each from the 2p formula with its own binding energy and occupancy.
396///
397/// Errors if the subshell is not K, L1, L2 or L3 (no hydrogenic formula is
398/// implemented for the M shell and above), if `(z, subshell)` is not in
399/// `table`, if `atoms_per_m3` is
400/// not finite and positive, or if the grid is invalid.
401pub fn hydrogenic_shell_elf(
402    material: &str,
403    table: &SubshellBindingTable,
404    z: u8,
405    subshell: Subshell,
406    atoms_per_m3: f64,
407    grid: ShellElfGrid,
408) -> Result<(InnerShell, OpticalElf)> {
409    let Some(orbital) = Orbital::of(subshell) else {
410        return Err(invalid(
411            "hydrogenic shell ELF",
412            format!(
413                "Z = {z} {}: only the K, L1, L2 and L3 subshells are implemented (the \
414                 hydrogenic formulas for the M shell and above are not reduced or checked here)",
415                subshell.label()
416            ),
417        ));
418    };
419    if !(atoms_per_m3.is_finite() && atoms_per_m3 > 0.0) {
420        return Err(invalid(
421            "hydrogenic shell ELF",
422            format!("atom density must be finite and positive, got {atoms_per_m3} m^-3"),
423        ));
424    }
425    let binding = table
426        .atom(z)
427        .and_then(|a| a.shell(subshell))
428        .ok_or_else(|| {
429            invalid(
430                "hydrogenic shell ELF",
431                format!("Z = {z} {} is not in the binding table", subshell.label()),
432            )
433        })?;
434    let b = binding.binding_energy_ev();
435    let occupancy = binding.occupancy();
436    let shell = InnerShell {
437        z,
438        subshell,
439        binding_energy_ev: b,
440    };
441    let energy_ev = grid.energies_ev(b)?;
442    let scale = std::f64::consts::FRAC_PI_2 * plasma_energy_sq_ev2(atoms_per_m3) * occupancy;
443    let elf = energy_ev
444        .iter()
445        .map(|&w| scale * orbital.oscillator_strength_density_per_ev(b, w) / w)
446        .collect();
447    let provenance = format!(
448        "{}; Z = {z} {}, B = {b} eV and occupancy {occupancy} from: {}; {atoms_per_m3:e} \
449         atoms/m^3; {} points per decade up to {} B",
450        orbital.source(),
451        subshell.label(),
452        table.provenance(),
453        grid.points_per_decade,
454        grid.max_energy_factor,
455    );
456    let elf = OpticalElf::new(material, provenance, energy_ev, elf)?;
457    Ok((shell, elf))
458}
459
460/// [`hydrogenic_shell_elf`] for several shells of a target `material`, each
461/// given as `(Z, subshell, atoms of that element per m³)` and kept in the
462/// given order. For a compound with `N` molecules per m³ and `ν_j` atoms of
463/// element `j` per molecule, the density of element `j` is `ν_j N` (dV2022
464/// eq. (2)).
465pub fn hydrogenic_shell_elfs(
466    material: &str,
467    table: &SubshellBindingTable,
468    shells: &[(u8, Subshell, f64)],
469    grid: ShellElfGrid,
470) -> Result<Vec<(InnerShell, OpticalElf)>> {
471    shells
472        .iter()
473        .map(|&(z, s, n)| hydrogenic_shell_elf(material, table, z, s, n, grid))
474        .collect()
475}
476
477#[cfg(test)]
478mod tests {
479    use super::*;
480
481    #[test]
482    fn density_is_zero_below_the_edge_and_continuous_above_it() {
483        let b = 284.0;
484        assert_eq!(
485            hydrogenic_k_oscillator_strength_density_per_ev(b, 283.9),
486            0.0
487        );
488        assert_eq!(
489            hydrogenic_k_oscillator_strength_density_per_ev(b, f64::NAN),
490            0.0
491        );
492        let at = hydrogenic_k_oscillator_strength_density_per_ev(b, b);
493        let just_above = hydrogenic_k_oscillator_strength_density_per_ev(b, b * (1.0 + 1e-10));
494        assert!((just_above / at - 1.0).abs() < 1e-4, "{at} {just_above}");
495        // Monotonically decreasing above the edge.
496        let mut prev = at;
497        for k in 1..200 {
498            let w = b * (1.0 + 0.05 * k as f64);
499            let x = hydrogenic_k_oscillator_strength_density_per_ev(b, w);
500            assert!(x < prev && x > 0.0, "{w}");
501            prev = x;
502        }
503    }
504
505    /// `G_ℓ(-m; η; ρ)`, Karzas and Latter 1961 eqs. (C.5) and (C.8), by the
506    /// recurrence as printed.
507    fn kl_g(l: u32, m: u32, eta: f64, rho: f64) -> f64 {
508        if m == 0 {
509            return 1.0;
510        }
511        let (l, m) = (f64::from(l), f64::from(m));
512        let mut b = vec![1.0, 2.0 * m * eta / l];
513        for s in 2..=(2.0 * m) as usize {
514            let sf = s as f64;
515            let x = 4.0 * eta * (sf - 1.0 - m) * b[s - 1]
516                + (2.0 * m + 2.0 - sf) * (2.0 * m + 2.0 * l + 1.0 - sf) * b[s - 2];
517            b.push(-x / (sf * (sf + 2.0 * l - 1.0)));
518        }
519        b.iter().rev().fold(0.0, |acc, &c| acc * rho + c)
520    }
521
522    fn factorial(k: u32) -> f64 {
523        (1..=k).map(f64::from).product()
524    }
525
526    /// Karzas and Latter 1961 eqs. (36) and (37) as printed, without the
527    /// prefactor `π e² / (m c ν)`: the `ℓ -> ℓ-1` and `ℓ -> ℓ+1` parts.
528    fn kl_eqs_36_37(n: u32, l: u32, eta: f64) -> (f64, f64) {
529        let nf = f64::from(n);
530        let lf = f64::from(l);
531        let rho = eta / nf;
532        let r2 = 1.0 + rho * rho;
533        // cot⁻¹ρ = atan(1/ρ).
534        let coulomb =
535            (-4.0 * eta * (1.0 / rho).atan()).exp() / (1.0 - (-std::f64::consts::TAU * eta).exp());
536        let product =
537            |top: u32| -> f64 { (1..=top).map(|j| f64::from(j * j) + eta * eta).product() };
538        let down = if l == 0 {
539            0.0
540        } else {
541            let bracket = kl_g(l, n - l - 1, eta, rho) - kl_g(l, n - l + 1, eta, rho) / (r2 * r2);
542            2f64.powi(4 * l as i32) / 3.0 * lf * lf * factorial(n + l) * product(l - 1)
543                / (factorial(2 * l + 1) * factorial(2 * l - 1) * factorial(n - l - 1))
544                * coulomb
545                * rho.powi(2 * l as i32 + 2)
546                / r2.powi(2 * n as i32 - 2)
547                * bracket
548                * bracket
549        };
550        let bracket = (lf + 1.0 - nf) * kl_g(l + 1, n - l - 1, eta, rho)
551            + (lf + 1.0 + nf) / r2 * kl_g(l + 1, n - l, eta, rho);
552        let up = 2f64.powi(4 * l as i32 + 6) / 3.0
553            * (lf + 1.0).powi(2)
554            * factorial(n + l)
555            * product(l + 1)
556            / ((2.0 * lf + 1.0)
557                * factorial(2 * l + 1)
558                * factorial(2 * l + 2)
559                * factorial(n - l - 1)
560                * ((lf + 1.0).powi(2) + eta * eta).powi(2))
561            * coulomb
562            * rho.powi(2 * l as i32 + 4)
563            * eta
564            * eta
565            / r2.powi(2 * n as i32)
566            * bracket
567            * bracket;
568        (down, up)
569    }
570
571    /// The closed forms are eqs. (36) + (37) over `W` (Salvat eq. (6.2)
572    /// cancels the prefactor against `1/W`), with `W/B = 1 + 1/ρ²`. For
573    /// `n = 1` this is the Stobbe formula, from a different source.
574    #[test]
575    fn closed_forms_are_karzas_latter_eqs_36_and_37() {
576        let b = 99.2;
577        type Density = fn(f64, f64) -> f64;
578        let cases: [(u32, u32, Density); 3] = [
579            (1, 0, hydrogenic_k_oscillator_strength_density_per_ev),
580            (2, 0, hydrogenic_2s_oscillator_strength_density_per_ev),
581            (2, 1, hydrogenic_2p_oscillator_strength_density_per_ev),
582        ];
583        for (n, l, closed) in cases {
584            for rho in [30.0, 3.0, 1.0, 0.5, 0.1, 0.03] {
585                let w = b * (1.0 + 1.0 / (rho * rho));
586                let (down, up) = kl_eqs_36_37(n, l, rho * f64::from(n));
587                let want = (down + up) / w;
588                let got = closed(b, w);
589                // The bracket of eq. (36) for 2p cancels to O(ρ²).
590                assert!(
591                    (got / want - 1.0).abs() < 1e-9,
592                    "n = {n}, l = {l}, rho = {rho}: {got:e} vs {want:e}"
593                );
594            }
595        }
596        // The two parts of 2p: (2¹⁰/(27B)) / ε⁵ to the s continuum, eq. (36),
597        // and (2¹³/(27B)) (ε + 3)/ε⁶ to the d continuum, eq. (37), each times
598        // the Coulomb factor.
599        let eps: f64 = 5.0;
600        let (down, up) = kl_eqs_36_37(2, 1, 2.0 / (eps - 1.0).sqrt());
601        let c = n2_coulomb_factor(eps - 1.0);
602        let want_s = 1024.0 / (27.0 * b) / eps.powi(5) * c;
603        let want_d = 8192.0 / (27.0 * b) * (eps + 3.0) / eps.powi(6) * c;
604        assert!((down / (eps * b) / want_s - 1.0).abs() < 1e-12);
605        assert!((up / (eps * b) / want_d - 1.0).abs() < 1e-12);
606    }
607
608    #[test]
609    fn l_densities_are_zero_below_the_edge_and_continuous_above_it() {
610        let b = 99.2;
611        type Density = fn(f64, f64) -> f64;
612        let cases: [(Density, f64); 2] = [
613            (
614                hydrogenic_2s_oscillator_strength_density_per_ev,
615                4096.0 / 3.0,
616            ),
617            (
618                hydrogenic_2p_oscillator_strength_density_per_ev,
619                11264.0 / 9.0,
620            ),
621        ];
622        for (f, edge) in cases {
623            assert_eq!(f(b, 99.1), 0.0);
624            assert_eq!(f(b, f64::NAN), 0.0);
625            let at = f(b, b);
626            assert!((at / (edge / b * (-8.0f64).exp()) - 1.0).abs() < 1e-14);
627            let just_above = f(b, b * (1.0 + 1e-10));
628            assert!((just_above / at - 1.0).abs() < 1e-4, "{at} {just_above}");
629            let mut prev = at;
630            for k in 1..200 {
631                let w = b * (1.0 + 0.05 * f64::from(k));
632                let x = f(b, w);
633                assert!(x < prev && x > 0.0, "{w}");
634                prev = x;
635            }
636        }
637    }
638
639    #[test]
640    fn orbitals_are_k_and_the_three_l_subshells() {
641        let of = |label: &str| Orbital::of(Subshell::from_label(label).unwrap());
642        assert_eq!(of("K"), Some(Orbital::S1));
643        assert_eq!(of("L1"), Some(Orbital::S2));
644        assert_eq!(of("L2"), Some(Orbital::P2));
645        assert_eq!(of("L3"), Some(Orbital::P2));
646        assert_eq!(of("M1"), None);
647        assert_eq!(of("M5"), None);
648    }
649
650    #[test]
651    fn grid_starts_at_the_edge() {
652        let e = ShellElfGrid::default().energies_ev(1838.9).unwrap();
653        assert_eq!(e[0], 1838.9);
654        assert_eq!(e.len(), 901);
655        assert!((e[900] / 1.8389e6 - 1.0).abs() < 1e-12);
656        assert!(e.windows(2).all(|w| w[1] > w[0]));
657        for bad in [
658            ShellElfGrid {
659                points_per_decade: 1,
660                max_energy_factor: 10.0,
661            },
662            ShellElfGrid {
663                points_per_decade: 10,
664                max_energy_factor: 1.0,
665            },
666            ShellElfGrid {
667                points_per_decade: 10,
668                max_energy_factor: f64::INFINITY,
669            },
670        ] {
671            assert!(bad.energies_ev(100.0).is_err());
672        }
673    }
674}