Skip to main content

lindhard/electron/data/
optical.rs

1use super::{
2    check_energy_grid, check_in_range, check_nonnegative, checked_text, invalid, lerp_on_grid,
3    parse_error, read_text, ElectronDataError, Result,
4};
5use serde::{Deserialize, Serialize};
6use std::path::Path;
7
8/// A tabulated optical energy-loss function `ELF(ħω) = Im[-1/ε(ω)]` of one
9/// material, at zero momentum transfer.
10///
11/// The ELF is the quantity a dielectric-function inelastic model extends into
12/// momentum transfer (Ritchie, Phys. Rev. 106, 874 (1957); Penn, Phys. Rev. B
13/// 35, 482 (1987)). It is dimensionless.
14///
15/// Invariants (checked by [`OpticalElf::new`] and by every loader):
16/// non-blank `material` and `provenance`; at least two samples; photon
17/// energies `ħω` finite, positive and strictly increasing; ELF values finite
18/// and non-negative (zero is allowed, e.g. inside a band gap), one per energy.
19///
20/// **Interpolation** is piecewise linear in `(ħω, ELF)`. It reproduces the
21/// tabulated values at the knots, never goes negative, and handles zero-valued
22/// ELF (where log-log interpolation would fail). Queries outside
23/// `[ħω_min, ħω_max]` are an [`ElectronDataError::OutOfRange`] error: nothing
24/// is extrapolated here. Extrapolation to high energy (for example by a sum
25/// rule or atomic photoabsorption data) belongs to a later physical model.
26///
27/// TOML form (all four keys required, no others accepted):
28///
29/// ```toml
30/// material = "Si"
31/// provenance = "Author, Journal vol, page (year), Table N"
32/// energy_ev = [1.0, 10.0, 16.7, 100.0]
33/// elf = [0.0, 0.5, 4.0, 0.1]
34/// ```
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
36#[serde(try_from = "RawOpticalElf", into = "RawOpticalElf")]
37pub struct OpticalElf {
38    material: String,
39    provenance: String,
40    energy_ev: Vec<f64>,
41    elf: Vec<f64>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45#[serde(deny_unknown_fields)]
46struct RawOpticalElf {
47    material: Option<String>,
48    provenance: Option<String>,
49    energy_ev: Vec<f64>,
50    elf: Vec<f64>,
51}
52
53impl TryFrom<RawOpticalElf> for OpticalElf {
54    type Error = ElectronDataError;
55    fn try_from(raw: RawOpticalElf) -> Result<Self> {
56        let provenance = checked_text("provenance", raw.provenance.as_deref())?;
57        let material = checked_text("material", raw.material.as_deref())?;
58        Self::new(material, provenance, raw.energy_ev, raw.elf)
59    }
60}
61
62impl From<OpticalElf> for RawOpticalElf {
63    fn from(e: OpticalElf) -> Self {
64        Self {
65            material: Some(e.material),
66            provenance: Some(e.provenance),
67            energy_ev: e.energy_ev,
68            elf: e.elf,
69        }
70    }
71}
72
73impl OpticalElf {
74    /// Build a validated ELF table. See the type docs for the invariants.
75    pub fn new(
76        material: impl Into<String>,
77        provenance: impl Into<String>,
78        energy_ev: Vec<f64>,
79        elf: Vec<f64>,
80    ) -> Result<Self> {
81        let provenance = checked_text("provenance", Some(&provenance.into()))?;
82        let material = checked_text("material", Some(&material.into()))?;
83        check_energy_grid("photon energy grid", &energy_ev)?;
84        if elf.len() != energy_ev.len() {
85            return Err(invalid(
86                "ELF values",
87                format!(
88                    "need one value per energy: {} energies, {} values",
89                    energy_ev.len(),
90                    elf.len()
91                ),
92            ));
93        }
94        check_nonnegative("ELF values", &elf)?;
95        Ok(Self {
96            material,
97            provenance,
98            energy_ev,
99            elf,
100        })
101    }
102
103    /// Parse and validate the TOML form (see the type docs).
104    pub fn from_toml_str(text: &str) -> Result<Self> {
105        let raw: RawOpticalElf = toml::from_str(text).map_err(parse_error)?;
106        Self::try_from(raw)
107    }
108
109    /// Load the TOML form from a file.
110    pub fn from_toml_file(path: impl AsRef<Path>) -> Result<Self> {
111        Self::from_toml_str(&read_text(path.as_ref())?)
112    }
113
114    /// Material identity, as declared by the data.
115    pub fn material(&self) -> &str {
116        &self.material
117    }
118
119    /// Provenance string (to be copied into run metadata).
120    pub fn provenance(&self) -> &str {
121        &self.provenance
122    }
123
124    /// Tabulated photon energies `ħω`, eV.
125    pub fn energy_ev(&self) -> &[f64] {
126        &self.energy_ev
127    }
128
129    /// Tabulated ELF values (dimensionless), one per energy.
130    pub fn elf_values(&self) -> &[f64] {
131        &self.elf
132    }
133
134    /// The tabulated range `(min, max)` of `ħω`, eV.
135    pub fn energy_range_ev(&self) -> (f64, f64) {
136        (self.energy_ev[0], self.energy_ev[self.energy_ev.len() - 1])
137    }
138
139    /// `ELF(ħω)` by piecewise-linear interpolation; an error outside the
140    /// tabulated range.
141    pub fn elf(&self, energy_ev: f64) -> Result<f64> {
142        let (lo, hi) = self.energy_range_ev();
143        check_in_range("photon energy (eV)", energy_ev, lo, hi)?;
144        Ok(lerp_on_grid(&self.energy_ev, &self.elf, energy_ev))
145    }
146}