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}