lindhard/electron/inelastic/table/
sampler.rs1use super::super::penn::{hartree_ev, SinglePolePenn};
5use super::density::{adaptive_density, LinearDensity};
6use super::{check_no_exchange, invalid, InelasticTableError, DEFAULT_Q_TOLERANCE};
7use crate::constants::BOHR_RADIUS;
8
9#[derive(Debug, Clone, PartialEq)]
12pub struct MomentumTransferSampler {
13 density: LinearDensity,
14 k: f64,
16 kp: f64,
17 tp: f64,
18}
19
20impl MomentumTransferSampler {
21 pub fn new(
23 penn: &SinglePolePenn,
24 energy_ev: f64,
25 loss_ev: f64,
26 ) -> Result<Self, InelasticTableError> {
27 Self::with_tolerance(penn, energy_ev, loss_ev, DEFAULT_Q_TOLERANCE)
28 }
29
30 pub fn with_tolerance(
34 penn: &SinglePolePenn,
35 energy_ev: f64,
36 loss_ev: f64,
37 tol: f64,
38 ) -> Result<Self, InelasticTableError> {
39 check_no_exchange(penn)?;
40 if !(energy_ev.is_finite() && energy_ev > 0.0) {
41 return Err(invalid("electron energy", format!("{energy_ev} eV")));
42 }
43 if !(loss_ev.is_finite() && loss_ev > 0.0 && loss_ev <= energy_ev) {
44 return Err(invalid(
45 "energy loss",
46 format!("{loss_ev} eV (need 0 < W <= E = {energy_ev} eV)"),
47 ));
48 }
49 if !(tol.is_finite() && (1.0e-9..=1.0e-2).contains(&tol)) {
50 return Err(invalid(
51 "q tolerance",
52 format!("{tol} (need 1e-9 ..= 1e-2)"),
53 ));
54 }
55 let h = hartree_ev();
56 let (t, w) = (energy_ev / h, loss_ev / h);
57 let breaks = penn.q_slice_breaks_au(t, w).ok_or_else(|| {
58 invalid(
59 "momentum-transfer slice",
60 format!("no allowed q at E = {energy_ev} eV, W = {loss_ev} eV"),
61 )
62 })?;
63 let f = |u: f64| penn.q_integrand_au(w, u);
64 let (density, _) = adaptive_density(&f, &breaks, 8, tol, false, &[], 0.0);
67 if !(density.total() > 0.0 && density.total().is_finite()) {
68 return Err(invalid(
69 "momentum-transfer slice",
70 format!("zero loss function at E = {energy_ev} eV, W = {loss_ev} eV"),
71 ));
72 }
73 let tp = t + penn.fermi_energy_ev() / h;
74 Ok(Self {
75 density,
76 k: (2.0 * tp).sqrt(),
77 kp: (2.0 * (tp - w)).max(0.0).sqrt(),
78 tp,
79 })
80 }
81
82 pub fn sample(&self, u: f64) -> f64 {
85 self.density.quantile(u.clamp(0.0, 1.0)).exp() / BOHR_RADIUS
86 }
87
88 pub fn q_range_per_m(&self) -> (f64, f64) {
91 let n = self.density.x.len();
92 (
93 self.density.x[0].exp() / BOHR_RADIUS,
94 self.density.x[n - 1].exp() / BOHR_RADIUS,
95 )
96 }
97
98 pub fn kinematic_limits_per_m(&self) -> (f64, f64) {
100 (
101 (self.k - self.kp) / BOHR_RADIUS,
102 (self.k + self.kp) / BOHR_RADIUS,
103 )
104 }
105
106 pub fn cos_theta(&self, q_per_m: f64) -> f64 {
109 let q = q_per_m * BOHR_RADIUS;
110 let c = (self.k * self.k + self.kp * self.kp - q * q) / (2.0 * self.k * self.kp);
111 c.clamp(-1.0, 1.0)
112 }
113
114 pub fn diimfp_per_m_ev(&self) -> f64 {
118 let h = hartree_ev();
119 self.density.total() / (std::f64::consts::PI * self.tp) / (BOHR_RADIUS * h)
120 }
121}