Skip to main content

lindhard/electron/
boundary.rs

1//! Surfaces and interfaces for the electron loop: per-material band
2//! parameters (Fermi energy and inner potential) and the quantum-mechanical
3//! potential step.
4//!
5//! # Energies
6//!
7//! Inside a material, the electron's kinetic energy is measured from the
8//! bottom of that material's band, the convention of Kieft and Bosch,
9//! J. Phys. D: Appl. Phys. 41, 215310 (2008), doi:10.1088/0022-3727/41/21/215310,
10//! as written out by T. Verduin, *Quantum Noise Effects in e-Beam Lithography
11//! and Metrology*, PhD thesis, Delft University of Technology (2017),
12//! doi:10.4233/uuid:f214f594-a21f-4318-9f29-9776d60ab06c (cited below as
13//! "Verduin" with the thesis's printed page and equation numbers). The
14//! **inner potential** `U` is the depth of that band bottom below the vacuum
15//! level, so an electron of kinetic energy `E` inside has `E - U` in vacuum.
16//! For a metal `U = E_F + Φ` (Fermi energy above the band bottom plus work
17//! function; Verduin p. 98 and Fig. 3.28). For a semiconductor or insulator
18//! the band bottom is the bottom of the valence band and
19//! `U = W_v + E_g + χ` (valence band width, band gap, electron affinity), with
20//! the Fermi level placed mid-gap, `E_F = W_v + E_g / 2`. That band model is
21//! cstool's (`cstool/input_data/band_structure.py`, see [`BandStructure`]).
22//!
23//! # The step
24//!
25//! An electron crossing from a region of inner potential `U` into one of `U'`
26//! sees the step `ΔU = U' - U` (Verduin Eq. 3.136, p. 99; vacuum has `U = 0`).
27//! With `θ` the angle between its direction and the face normal and
28//! `E_n = E cos²θ` its normal energy:
29//!
30//! - if `E_n + ΔU <= 0` it is always reflected (specularly);
31//! - otherwise it is transmitted with probability
32//!   `T = 4 s / (1 + s)²`, `s = sqrt(1 + ΔU / E_n)` (Verduin Eq. 3.145, p. 102,
33//!   the plane-wave step of Eqs. 3.140-3.144), and reflected otherwise;
34//! - on transmission its kinetic energy becomes `E + ΔU` and it is refracted,
35//!   conserving the momentum parallel to the face:
36//!   `sin θ' = sin θ / sqrt(1 + ΔU / E)` (Verduin Eq. 3.139, p. 100).
37//!
38//! The step and the refraction are ported from Nebula,
39//! `source/physics/boundary_intersect.h` at commit
40//! `a50a8e83207980ed221ac83cfe874751d39b86a9`
41//! (<https://github.com/Nebula-simulator/nebula>), BSD-3-Clause,
42//! Copyright (c) 2020, Nebula-simulator; the notice is in
43//! `THIRD_PARTY_LICENSES.md`. Nebula's optional empirical interface absorption
44//! (off by default there) is not ported.
45//!
46//! # Defaults
47//!
48//! A [`BandStructure`] is made from values the caller supplies, and it
49//! refuses an empty provenance. The one computed helper is
50//! [`BandStructure::free_electron_metal`], which takes the Fermi energy from
51//! the free-electron relation (Verduin Eq. 3.133) and the layer's atom
52//! density.
53//!
54//! A small table of **cited** per-material parameters, [`BAND_DEFAULTS`],
55//! covers Al, Cu, Au, W, Si and SiO2. Look an entry up with
56//! [`band_defaults`]. Each value in it names the document, table or figure,
57//! page, and the date the document was opened. A parameter with no source
58//! that could be opened has **no number**: the field is `None` and the
59//! entry's [`BandDefaults::gap`] note says what was tried. As of 2026-10-08
60//! the table holds:
61//!
62//! - Si: band gap 1.1 eV and electron affinity 4.05 eV (Robertson and
63//!   Wallace 2015). No valence band width.
64//! - SiO2: band gap 9 eV (Robertson and Wallace 2015). No electron affinity
65//!   and no valence band width.
66//! - Al, Cu, Au, W: nothing. No work function and no measured Fermi energy
67//!   could be cited to a source that was opened.
68//!
69//! So no entry is complete yet. [`BandStructure::from_defaults`] builds a
70//! band from an entry alone and says which parameters are missing.
71//! [`BandDefaults::complete`] takes the missing ones from the caller (with
72//! their provenance) and keeps the cited ones, and its provenance string
73//! names both. The full list of sources tried, opened or not, is in
74//! `docs/data-provenance.md` (rows "Per-material barrier parameters" and
75//! "Cited band defaults").
76
77use serde::Serialize;
78
79use crate::constants::{ELECTRON_MASS, ELEMENTARY_CHARGE, HBAR};
80use crate::material::Material;
81
82/// Errors from building a [`BandStructure`].
83#[derive(Debug, Clone, PartialEq, thiserror::Error)]
84pub enum BandError {
85    /// A parameter is out of range.
86    #[error("invalid {what}: {why}")]
87    Invalid {
88        /// The parameter.
89        what: &'static str,
90        /// Why it was rejected.
91        why: String,
92    },
93}
94
95fn invalid<T>(what: &'static str, why: impl Into<String>) -> Result<T, BandError> {
96    Err(BandError::Invalid {
97        what,
98        why: why.into(),
99    })
100}
101
102/// The band model of one material, after cstool's `band_structure` class
103/// (`cstool/input_data/band_structure.py` at commit
104/// `0c739eb3fcc3fe5297e74c601ac4a9546db596cf`,
105/// <https://github.com/Nebula-simulator/cstool>, BSD-3-Clause, Copyright (c)
106/// 2020, Nebula-simulator; notice in `THIRD_PARTY_LICENSES.md`). cstool treats
107/// semiconductors and insulators alike, and so does this type.
108#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
109#[serde(tag = "kind", rename_all = "kebab-case")]
110pub enum BandModel {
111    /// A metal: the conduction band is filled up to the Fermi level.
112    Metal {
113        /// Fermi energy above the bottom of the band, eV.
114        fermi_ev: f64,
115        /// Work function, eV.
116        work_function_ev: f64,
117    },
118    /// A semiconductor or insulator.
119    Insulator {
120        /// Width of the valence band, eV (its bottom is the energy origin).
121        valence_band_width_ev: f64,
122        /// Band gap, eV.
123        band_gap_ev: f64,
124        /// Electron affinity (vacuum level above the conduction band
125        /// bottom), eV. May be negative, as long as the inner potential is
126        /// not.
127        affinity_ev: f64,
128    },
129}
130
131/// The band parameters of one layer, with where they came from.
132#[derive(Debug, Clone, PartialEq, Serialize)]
133pub struct BandStructure {
134    model: BandModel,
135    /// Binding energy below the Fermi level of the electron that a valence
136    /// event liberates in a metal, eV; `None` is the default, the Fermi
137    /// level (see [`BandStructure::with_valence_binding_ev`]).
138    #[serde(skip_serializing_if = "Option::is_none")]
139    valence_binding_ev: Option<f64>,
140    provenance: String,
141}
142
143impl BandStructure {
144    /// Check and wrap. Every parameter must be finite; the Fermi energy, the
145    /// work function, the valence band width and the inner potential must be
146    /// non-negative and the band gap positive. `provenance` (the source of
147    /// the numbers) must not be blank.
148    pub fn new(model: BandModel, provenance: impl Into<String>) -> Result<Self, BandError> {
149        let provenance = provenance.into();
150        if provenance.trim().is_empty() {
151            return invalid("provenance", "must name the source of the parameters");
152        }
153        let nonneg = |what: &'static str, v: f64| -> Result<(), BandError> {
154            if v.is_finite() && v >= 0.0 {
155                Ok(())
156            } else {
157                invalid(what, format!("{v} must be finite and non-negative"))
158            }
159        };
160        match model {
161            BandModel::Metal {
162                fermi_ev,
163                work_function_ev,
164            } => {
165                nonneg("Fermi energy", fermi_ev)?;
166                nonneg("work function", work_function_ev)?;
167            }
168            BandModel::Insulator {
169                valence_band_width_ev,
170                band_gap_ev,
171                affinity_ev,
172            } => {
173                nonneg("valence band width", valence_band_width_ev)?;
174                if !(band_gap_ev.is_finite() && band_gap_ev > 0.0) {
175                    return invalid(
176                        "band gap",
177                        format!("{band_gap_ev} must be finite and positive"),
178                    );
179                }
180                if !affinity_ev.is_finite() {
181                    return invalid("electron affinity", "must be finite");
182                }
183                nonneg(
184                    "inner potential",
185                    valence_band_width_ev + band_gap_ev + affinity_ev,
186                )?;
187            }
188        }
189        Ok(Self {
190            model,
191            valence_binding_ev: None,
192            provenance,
193        })
194    }
195
196    /// The same metal band, with the electron that a **valence** inelastic
197    /// event liberates bound `binding_ev` below the Fermi level instead of
198    /// at it (the default, binding 0; [`crate::electron::secondary`],
199    /// "Energy"). A valence loss `W > B` then lifts it to `E_F + W - B`
200    /// (Verduin Eq. 3.86, p. 78, with this `B`), and a loss `W <= B`
201    /// liberates nothing and leaves `W` in the solid.
202    ///
203    /// This is the rule of M. Azzolini et al., "Secondary electron emission
204    /// and yield spectra of metals from Monte Carlo simulations and
205    /// experiments", arXiv:1809.00859v1 (2018), p. 6: "should the energy
206    /// loss be larger than the first ionization energy B ..., a secondary
207    /// electron is emitted with kinetic energy equal to W - B", with `B` of
208    /// their Table I, p. 7 ("the mean ionization energy characteristic of
209    /// each sample"). Their kinetic energies inside the solid are counted
210    /// from the Fermi level (p. 3: the incident energy is increased by the
211    /// work function χ, and an electron escapes if `E cos²θ >= χ`), so their
212    /// `W - B` is `E_F + W - B` on the band-bottom axis of this module.
213    ///
214    /// It is a switch for a sensitivity run, off by default; inner-shell
215    /// events keep their own shell binding. The source of `binding_ev`
216    /// belongs in the band's provenance. Refused: an insulator (its valence
217    /// events already use the band gap) and a binding that is not finite and
218    /// non-negative.
219    pub fn with_valence_binding_ev(mut self, binding_ev: f64) -> Result<Self, BandError> {
220        if !matches!(self.model, BandModel::Metal { .. }) {
221            return invalid(
222                "valence binding energy",
223                "only a metal takes one; an insulator's valence events use its band gap",
224            );
225        }
226        if !(binding_ev.is_finite() && binding_ev >= 0.0) {
227            return invalid(
228                "valence binding energy",
229                format!("{binding_ev} must be finite and non-negative"),
230            );
231        }
232        self.valence_binding_ev = Some(binding_ev);
233        Ok(self)
234    }
235
236    /// The binding energy below the Fermi level of the electron that a
237    /// valence event liberates in a metal, eV: the value set by
238    /// [`BandStructure::with_valence_binding_ev`], or `None` (the Fermi
239    /// level, binding 0).
240    pub fn valence_binding_ev(&self) -> Option<f64> {
241        self.valence_binding_ev
242    }
243
244    /// A free-electron metal: the Fermi energy is computed from the
245    /// conduction-electron density `n = z N` (`z` valence electrons per atom,
246    /// `N` the material's atom density) by [`free_electron_fermi_energy_ev`];
247    /// the work function is the caller's. `z` is a model choice the caller
248    /// makes and should name in `provenance` together with the source of the
249    /// work function.
250    pub fn free_electron_metal(
251        material: &Material,
252        valence_electrons_per_atom: f64,
253        work_function_ev: f64,
254        provenance: impl Into<String>,
255    ) -> Result<Self, BandError> {
256        let z = valence_electrons_per_atom;
257        if !(z.is_finite() && z > 0.0) {
258            return invalid(
259                "valence electrons per atom",
260                format!("{z} must be finite and positive"),
261            );
262        }
263        let n = z * material.atom_number_density();
264        Self::new(
265            BandModel::Metal {
266                fermi_ev: free_electron_fermi_energy_ev(n),
267                work_function_ev,
268            },
269            provenance,
270        )
271    }
272
273    /// The band model.
274    pub fn model(&self) -> BandModel {
275        self.model
276    }
277
278    /// Where the parameters came from.
279    pub fn provenance(&self) -> &str {
280        &self.provenance
281    }
282
283    /// Fermi energy above the band bottom, eV: the metal's own, or mid-gap
284    /// (`W_v + E_g / 2`) for an insulator (cstool `get_fermi`).
285    pub fn fermi_ev(&self) -> f64 {
286        match self.model {
287            BandModel::Metal { fermi_ev, .. } => fermi_ev,
288            BandModel::Insulator {
289                valence_band_width_ev,
290                band_gap_ev,
291                ..
292            } => valence_band_width_ev + 0.5 * band_gap_ev,
293        }
294    }
295
296    /// The lowest energy above the band bottom at which an electron can
297    /// excite a secondary, eV: the Fermi energy of a metal, the
298    /// conduction-band bottom `W_v + E_g` of an insulator, where both the
299    /// primary and the secondary must end in the conduction band (cstool
300    /// `get_min_excitation`, `cstool/input_data/band_structure.py` at commit
301    /// `0c739eb3fcc3fe5297e74c601ac4a9546db596cf`). cstool's table compiler caps the loss of an electron of
302    /// energy `K` at `K` minus this energy
303    /// ([`crate::electron::inelastic::table`], "Energy axis").
304    pub fn min_excitation_ev(&self) -> f64 {
305        match self.model {
306            BandModel::Metal { fermi_ev, .. } => fermi_ev,
307            BandModel::Insulator {
308                valence_band_width_ev,
309                band_gap_ev,
310                ..
311            } => valence_band_width_ev + band_gap_ev,
312        }
313    }
314
315    /// Inner potential `U` (vacuum level above the band bottom), eV:
316    /// `E_F + Φ` for a metal, `W_v + E_g + χ` for an insulator (cstool
317    /// `get_barrier`; Verduin p. 98 and Eq. 3.136).
318    pub fn inner_potential_ev(&self) -> f64 {
319        match self.model {
320            BandModel::Metal {
321                fermi_ev,
322                work_function_ev,
323            } => fermi_ev + work_function_ev,
324            BandModel::Insulator {
325                valence_band_width_ev,
326                band_gap_ev,
327                affinity_ev,
328            } => valence_band_width_ev + band_gap_ev + affinity_ev,
329        }
330    }
331
332    /// The band gap, eV, or `None` for a metal.
333    pub fn band_gap_ev(&self) -> Option<f64> {
334        match self.model {
335            BandModel::Metal { .. } => None,
336            BandModel::Insulator { band_gap_ev, .. } => Some(band_gap_ev),
337        }
338    }
339}
340
341/// Whether a [`BandDefaults`] entry describes a metal or an insulator, i.e.
342/// which [`BandModel`] it builds.
343#[derive(Debug, Clone, Copy, PartialEq, Eq)]
344pub enum BandKind {
345    /// [`BandModel::Metal`]: Fermi energy and work function.
346    Metal,
347    /// [`BandModel::Insulator`]: valence band width, band gap and electron
348    /// affinity.
349    Insulator,
350}
351
352/// One cited band parameter.
353#[derive(Debug, Clone, Copy, PartialEq)]
354pub struct CitedValue {
355    /// The value, eV.
356    pub value_ev: f64,
357    /// Where it was read: document, table or figure, page, and the date the
358    /// document was opened.
359    pub source: &'static str,
360}
361
362/// The cited band parameters of one material. A field is `None` where no
363/// source that could be opened gives the value; [`BandDefaults::gap`] says
364/// what was tried. Fields that do not belong to the entry's [`BandKind`] are
365/// always `None`.
366#[derive(Debug, Clone, Copy, PartialEq)]
367pub struct BandDefaults {
368    /// The material, as a chemical formula (`"Si"`, `"SiO2"`, ...).
369    pub material: &'static str,
370    /// Metal or insulator.
371    pub kind: BandKind,
372    /// Fermi energy above the band bottom, eV (metals).
373    pub fermi_ev: Option<CitedValue>,
374    /// Work function, eV (metals).
375    pub work_function_ev: Option<CitedValue>,
376    /// Valence band width, eV (insulators).
377    pub valence_band_width_ev: Option<CitedValue>,
378    /// Band gap, eV (insulators).
379    pub band_gap_ev: Option<CitedValue>,
380    /// Electron affinity, eV (insulators).
381    pub affinity_ev: Option<CitedValue>,
382    /// Why the missing parameters are missing.
383    pub gap: &'static str,
384}
385
386/// Caller-supplied values for the parameters a [`BandDefaults`] entry lacks.
387/// Leave a field `None` to use the cited value; a field set for a parameter
388/// the entry already cites is refused (build with [`BandStructure::new`] to
389/// replace a cited value).
390#[derive(Debug, Clone, Copy, Default, PartialEq)]
391pub struct BandFill {
392    /// Fermi energy above the band bottom, eV (metals).
393    pub fermi_ev: Option<f64>,
394    /// Work function, eV (metals).
395    pub work_function_ev: Option<f64>,
396    /// Valence band width, eV (insulators).
397    pub valence_band_width_ev: Option<f64>,
398    /// Band gap, eV (insulators).
399    pub band_gap_ev: Option<f64>,
400    /// Electron affinity, eV (insulators).
401    pub affinity_ev: Option<f64>,
402}
403
404/// J. Robertson and R. M. Wallace, read in the authors' manuscript.
405macro_rules! robertson_wallace {
406    () => {
407        "J. Robertson and R. M. Wallace, \"High-K materials and metal gates for CMOS \
408         applications\", Mater. Sci. Eng. R 88, 1-41 (2015), doi:10.1016/j.mser.2014.11.001, \
409         read in the authors' manuscript in the University of Cambridge repository \
410         (hdl:1810/246441; page numbers are the manuscript's), opened 2026-10-08"
411    };
412}
413
414/// What was tried for the metals' parameters (the full list is in
415/// `docs/data-provenance.md`).
416macro_rules! metal_gap {
417    () => {
418        "no work function and no measured Fermi energy could be cited to a source that was \
419         opened (searched 2026-10-07 and 2026-10-08): the compilations of Michaelson (1977) and \
420         Derry et al. (2015) and the primary photoemission papers are closed access; the open \
421         NASA report of J. R. Smith (1969) only repeats Fomenko's 1966 recommended values; see \
422         docs/data-provenance.md, 'Per-material barrier parameters'. \
423         BandStructure::free_electron_metal computes a Fermi energy from a valence count"
424    };
425}
426
427const fn metal(material: &'static str) -> BandDefaults {
428    BandDefaults {
429        material,
430        kind: BandKind::Metal,
431        fermi_ev: None,
432        work_function_ev: None,
433        valence_band_width_ev: None,
434        band_gap_ev: None,
435        affinity_ev: None,
436        gap: metal_gap!(),
437    }
438}
439
440/// The cited band parameters, one entry per material (see the module docs,
441/// "Defaults"). Every value cites its table or figure and page.
442pub const BAND_DEFAULTS: &[BandDefaults] = &[
443    metal("Al"),
444    metal("Cu"),
445    metal("Au"),
446    metal("W"),
447    BandDefaults {
448        material: "Si",
449        kind: BandKind::Insulator,
450        fermi_ev: None,
451        work_function_ev: None,
452        valence_band_width_ev: None,
453        // Robertson and Wallace, Table 1, manuscript p. 43: "experimental
454        // band gap", Si 1.1 eV.
455        band_gap_ev: Some(CitedValue {
456            value_ev: 1.1,
457            source: concat!(
458                robertson_wallace!(),
459                ", Table 1, p. 43 (experimental band gap of Si, 1.1 eV; Table 3, p. 45, also \
460                 prints 1.1 eV); cross-check: S. Tanuma, C. J. Powell and D. R. Penn, Surf. \
461                 Interface Anal. 43, 689 (2011), doi:10.1002/sia.3522, Table 1 (authors' \
462                 manuscript at NIMS MDR, p. 32), Eg = 1.1 eV, opened 2026-10-08"
463            ),
464        }),
465        // Robertson and Wallace, Fig. 42, manuscript p. 63: chi = 4.05 eV
466        // drawn from the vacuum level to the Si conduction band; Section 6.1,
467        // p. 20: n+ and p+ poly-Si gates have work functions of 4.05 and
468        // 5.15 eV (the two band edges), a difference of the 1.1 eV gap.
469        affinity_ev: Some(CitedValue {
470            value_ev: 4.05,
471            source: concat!(
472                robertson_wallace!(),
473                ", Fig. 42, p. 63 (electron affinity of Si, chi = 4.05 eV, vacuum level to \
474                 conduction band edge; the valence band edge is at 5.15 eV), and Section 6.1, \
475                 p. 20 (n+ and p+ poly-Si gate work functions 4.05 and 5.15 eV)"
476            ),
477        }),
478        gap: "valence band width: no source that was opened gives the bottom of the Si valence \
479              band relative to its top (L. Ley et al., LBL-688 (1972), the preprint of Phys. Rev. \
480              Lett. 29, 1088, Table 1, place the Gamma1 feature at 14.7 eV below the Fermi level \
481              only); see docs/data-provenance.md",
482    },
483    BandDefaults {
484        material: "SiO2",
485        kind: BandKind::Insulator,
486        fermi_ev: None,
487        work_function_ev: None,
488        valence_band_width_ev: None,
489        // Robertson and Wallace, Table 1, manuscript p. 43: SiO2 gap 9 eV.
490        band_gap_ev: Some(CitedValue {
491            value_ev: 9.0,
492            source: concat!(
493                robertson_wallace!(),
494                ", Table 1, p. 43 (experimental band gap of SiO2, 9 eV; Table 4, p. 45, prints \
495                 9.0 eV); cross-check: S. Tanuma, C. J. Powell and D. R. Penn, Surf. Interface \
496                 Anal. 17, 927 (1991), doi:10.1002/sia.740171305, Table 5 (authors' manuscript \
497                 at NIMS MDR, p. 21), Eg = 9.1 eV, opened 2026-10-08"
498            ),
499        }),
500        affinity_ev: None,
501        gap: "electron affinity and valence band width: no source that was opened states them. \
502              Robertson and Wallace (2015), Table 1, give only the Si/SiO2 conduction band offset \
503              (3.2 eV), which equals the affinity difference only without an interface dipole \
504              (their Section 4.4), so no affinity is derived from it; see docs/data-provenance.md",
505    },
506];
507
508/// The cited band parameters of `material` (a chemical formula such as
509/// `"Si"` or `"SiO2"`, matched exactly), or `None` if the table has no entry
510/// for it.
511pub fn band_defaults(material: &str) -> Option<&'static BandDefaults> {
512    BAND_DEFAULTS.iter().find(|d| d.material == material)
513}
514
515impl BandDefaults {
516    /// Build a [`BandStructure`] from the cited values, taking each missing
517    /// parameter from `fill`. `provenance` names the source of the filled
518    /// values and must not be blank when any is filled. Refused: a parameter
519    /// with neither a cited value nor a fill (the error quotes
520    /// [`BandDefaults::gap`]), a fill for a parameter that is already cited,
521    /// and a fill for a parameter of the other [`BandKind`]. The returned
522    /// band's provenance lists every cited value with its source, then the
523    /// filled values with `provenance`.
524    pub fn complete(&self, fill: BandFill, provenance: &str) -> Result<BandStructure, BandError> {
525        let (needed, foreign): (&[_], &[_]) = match self.kind {
526            BandKind::Metal => (
527                &[
528                    ("Fermi energy", self.fermi_ev, fill.fermi_ev),
529                    (
530                        "work function",
531                        self.work_function_ev,
532                        fill.work_function_ev,
533                    ),
534                ],
535                &[
536                    ("valence band width", fill.valence_band_width_ev),
537                    ("band gap", fill.band_gap_ev),
538                    ("electron affinity", fill.affinity_ev),
539                ],
540            ),
541            BandKind::Insulator => (
542                &[
543                    (
544                        "valence band width",
545                        self.valence_band_width_ev,
546                        fill.valence_band_width_ev,
547                    ),
548                    ("band gap", self.band_gap_ev, fill.band_gap_ev),
549                    ("electron affinity", self.affinity_ev, fill.affinity_ev),
550                ],
551                &[
552                    ("Fermi energy", fill.fermi_ev),
553                    ("work function", fill.work_function_ev),
554                ],
555            ),
556        };
557        let material = self.material;
558        if let Some((what, _)) = foreign.iter().find(|(_, v)| v.is_some()) {
559            let kind = match self.kind {
560                BandKind::Metal => "a metal",
561                BandKind::Insulator => "a semiconductor or insulator",
562            };
563            return invalid(what, format!("{material} is {kind}; it has no {what}"));
564        }
565        let mut values = Vec::with_capacity(needed.len());
566        let mut cited = Vec::new();
567        let mut filled = Vec::new();
568        for &(what, default, given) in needed {
569            match (default, given) {
570                (Some(c), None) => {
571                    values.push(c.value_ev);
572                    cited.push(format!("{what} {} eV from {}", c.value_ev, c.source));
573                }
574                (Some(c), Some(_)) => {
575                    return invalid(
576                        what,
577                        format!(
578                            "{material} has a cited default ({} eV); build with \
579                             BandStructure::new to replace it",
580                            c.value_ev
581                        ),
582                    )
583                }
584                (None, Some(v)) => {
585                    values.push(v);
586                    filled.push(format!("{what} {v} eV"));
587                }
588                (None, None) => {
589                    return invalid(
590                        what,
591                        format!(
592                            "no cited default for {material} ({}); supply it with \
593                             BandDefaults::complete",
594                            self.gap
595                        ),
596                    )
597                }
598            }
599        }
600        if !filled.is_empty() && provenance.trim().is_empty() {
601            return invalid(
602                "provenance",
603                "must name the source of the caller-supplied parameters",
604            );
605        }
606        let mut text = format!(
607            "{material}: cited defaults (lindhard::electron::boundary::BAND_DEFAULTS): {}",
608            if cited.is_empty() {
609                "none".to_string()
610            } else {
611                cited.join("; ")
612            }
613        );
614        if !filled.is_empty() {
615            text.push_str(&format!(
616                ". Caller-supplied: {}, from: {}",
617                filled.join(", "),
618                provenance.trim()
619            ));
620        }
621        let model = match self.kind {
622            BandKind::Metal => BandModel::Metal {
623                fermi_ev: values[0],
624                work_function_ev: values[1],
625            },
626            BandKind::Insulator => BandModel::Insulator {
627                valence_band_width_ev: values[0],
628                band_gap_ev: values[1],
629                affinity_ev: values[2],
630            },
631        };
632        BandStructure::new(model, text)
633    }
634}
635
636impl BandStructure {
637    /// The band of `material` from [`BAND_DEFAULTS`] alone. Fails if the
638    /// table has no entry for `material` or the entry lacks a parameter (the
639    /// error names the first missing one and why it is missing); as of
640    /// 2026-10-08 every entry lacks at least one, so use
641    /// [`BandDefaults::complete`] to supply the rest.
642    pub fn from_defaults(material: &str) -> Result<Self, BandError> {
643        let Some(d) = band_defaults(material) else {
644            let known: Vec<&str> = BAND_DEFAULTS.iter().map(|d| d.material).collect();
645            return invalid(
646                "material",
647                format!(
648                    "no band defaults for {material:?}; the table covers {}",
649                    known.join(", ")
650                ),
651            );
652        };
653        d.complete(BandFill::default(), "")
654    }
655}
656
657/// Free-electron Fermi energy, eV, for `n` conduction electrons per m³:
658/// Verduin Eq. 3.133 (p. 95), `n = (2 sqrt 2 / 3π²) (m_e E_F / ħ²)^(3/2)`,
659/// solved for `E_F`, which gives `E_F = (ħ² / 2 m_e) (3π² n)^(2/3)`. CODATA
660/// constants from [`crate::constants`].
661pub fn free_electron_fermi_energy_ev(n_per_m3: f64) -> f64 {
662    let k_f = (3.0 * std::f64::consts::PI.powi(2) * n_per_m3).cbrt();
663    HBAR * HBAR * k_f * k_f / (2.0 * ELECTRON_MASS) / ELEMENTARY_CHARGE
664}
665
666/// Conduction-electron density, per m³, at Fermi energy `fermi_ev`: Verduin
667/// Eq. 3.133 (p. 95) as printed. The inverse of
668/// [`free_electron_fermi_energy_ev`].
669pub fn free_electron_density_per_m3(fermi_ev: f64) -> f64 {
670    let e = fermi_ev * ELEMENTARY_CHARGE;
671    2.0 * std::f64::consts::SQRT_2 / (3.0 * std::f64::consts::PI.powi(2))
672        * (ELECTRON_MASS * e / (HBAR * HBAR)).powf(1.5)
673}
674
675/// Transmission probability through a potential step `delta_u_ev` for an
676/// electron of normal energy `normal_energy_ev = E cos²θ` (Verduin Eq. 3.145,
677/// p. 102): zero when `E_n + ΔU <= 0` (the electron cannot get over), else
678/// `4 s / (1 + s)²` with `s = sqrt(1 + ΔU / E_n)`. Symmetric under exchanging
679/// the two sides (`s -> 1/s`).
680pub fn step_transmission(normal_energy_ev: f64, delta_u_ev: f64) -> f64 {
681    if normal_energy_ev.is_nan() || normal_energy_ev <= 0.0 || normal_energy_ev + delta_u_ev <= 0.0
682    {
683        return 0.0;
684    }
685    let s = (1.0 + delta_u_ev / normal_energy_ev).sqrt();
686    4.0 * s / ((1.0 + s) * (1.0 + s))
687}
688
689/// The outcome of [`cross_step`].
690#[derive(Debug, Clone, Copy, PartialEq)]
691pub enum StepOutcome {
692    /// The electron got through: its new direction and kinetic energy.
693    Transmitted {
694        /// Unit direction after the face.
695        dir: [f64; 3],
696        /// Kinetic energy after the face, eV (`E + ΔU`).
697        energy_ev: f64,
698    },
699    /// The electron was reflected specularly: its new direction.
700    Reflected {
701        /// Unit direction after the reflection (the normal component
702        /// reversed).
703        dir: [f64; 3],
704    },
705}
706
707/// One electron at a face normal to `x`, ported from Nebula's
708/// `boundary_intersect::execute` (file and commit in the module docs).
709///
710/// `dir` is the unit direction (its `x` component must be nonzero; its sign
711/// says which way the electron crosses), `energy_ev` the kinetic energy on
712/// the near side, `delta_u_ev` the step `U' - U`. `u` is a uniform draw on
713/// `[0, 1)`; it is only consulted when the electron has the normal energy to
714/// get over, and then transmission happens when `u < T`. With
715/// `quantum_transmission` false, `T = 1` whenever the electron can get over;
716/// with `refraction` false the direction is kept on transmission.
717pub fn cross_step(
718    dir: [f64; 3],
719    energy_ev: f64,
720    delta_u_ev: f64,
721    quantum_transmission: bool,
722    refraction: bool,
723    u: f64,
724) -> StepOutcome {
725    let mu = dir[0];
726    let normal = energy_ev * mu * mu;
727    if normal + delta_u_ev > 0.0 {
728        let s = (1.0 + delta_u_ev / normal).sqrt();
729        let t = if quantum_transmission {
730            4.0 * s / ((1.0 + s) * (1.0 + s))
731        } else {
732            1.0
733        };
734        if u < t {
735            // Verduin Eq. 3.139 in Nebula's vector form: the normal component
736            // is scaled by `s`, the parallel ones are kept.
737            let dir = if refraction {
738                let v = [s * mu, dir[1], dir[2]];
739                let n = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
740                [v[0] / n, v[1] / n, v[2] / n]
741            } else {
742                dir
743            };
744            return StepOutcome::Transmitted {
745                dir,
746                energy_ev: energy_ev + delta_u_ev,
747            };
748        }
749    }
750    StepOutcome::Reflected {
751        dir: [-mu, dir[1], dir[2]],
752    }
753}
754
755#[cfg(test)]
756mod tests {
757    use super::*;
758
759    #[test]
760    fn transmission_is_one_without_a_step_and_symmetric() {
761        assert_eq!(step_transmission(3.0, 0.0), 1.0);
762        // Crossing up by dU at normal energy E_n is the reverse of crossing
763        // down by -dU at E_n + dU: the same s, inverted.
764        for (en, du) in [(2.0, 5.0), (10.0, 0.5), (0.3, 12.0)] {
765            let up = step_transmission(en, du);
766            let down = step_transmission(en + du, -du);
767            assert!((up - down).abs() < 1e-14, "{up} vs {down}");
768        }
769    }
770
771    #[test]
772    fn below_the_barrier_is_reflected_for_every_draw() {
773        for u in [0.0, 1e-12, 0.5, 0.999_999] {
774            let d = [-0.6, 0.8, 0.0];
775            // E cos^2 = 10 * 0.36 = 3.6 < 4.
776            match cross_step(d, 10.0, -4.0, true, true, u) {
777                StepOutcome::Reflected { dir } => assert_eq!(dir, [0.6, 0.8, 0.0]),
778                o => panic!("transmitted below the barrier: {o:?}"),
779            }
780        }
781    }
782
783    #[test]
784    fn refraction_conserves_parallel_momentum() {
785        let d = {
786            let v: [f64; 3] = [0.5, 0.7, -0.3];
787            let n = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
788            [v[0] / n, v[1] / n, v[2] / n]
789        };
790        for du in [-1.5, 2.0, 7.0] {
791            let e = 9.0;
792            match cross_step(d, e, du, false, true, 0.0) {
793                StepOutcome::Transmitted { dir, energy_ev } => {
794                    assert_eq!(energy_ev, e + du);
795                    let len = (dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]).sqrt();
796                    assert!((len - 1.0).abs() < 1e-14);
797                    assert!(dir[0] * d[0] > 0.0, "kept its side of travel");
798                    // Verduin Eq. 3.138: sqrt(E) sin(theta) = sqrt(E') sin(theta').
799                    let sin = (1.0 - d[0] * d[0]).sqrt();
800                    let sin2 = (1.0 - dir[0] * dir[0]).sqrt();
801                    assert!((e.sqrt() * sin - energy_ev.sqrt() * sin2).abs() < 1e-13);
802                    // The azimuth is unchanged.
803                    assert!((dir[1] * d[2] - dir[2] * d[1]).abs() < 1e-14);
804                }
805                o => panic!("expected transmission: {o:?}"),
806            }
807        }
808    }
809
810    #[test]
811    fn fermi_energy_inverts_eq_3_133() {
812        // Arbitrary densities: a round trip, not a material check.
813        for n in [1e27, 4e28, 2e29] {
814            let ef = free_electron_fermi_energy_ev(n);
815            let back = free_electron_density_per_m3(ef);
816            assert!((back - n).abs() <= 1e-12 * n, "{n} -> {ef} eV -> {back}");
817        }
818        // E_F scales as n^(2/3).
819        let r = free_electron_fermi_energy_ev(8e28) / free_electron_fermi_energy_ev(1e28);
820        assert!((r - 4.0).abs() < 1e-12);
821    }
822
823    #[test]
824    fn band_structure_checks_its_inputs() {
825        let metal = |f, w| {
826            BandStructure::new(
827                BandModel::Metal {
828                    fermi_ev: f,
829                    work_function_ev: w,
830                },
831                "test",
832            )
833        };
834        assert!(metal(5.0, 4.0).is_ok());
835        assert!(metal(-1.0, 4.0).is_err());
836        assert!(metal(5.0, f64::NAN).is_err());
837        assert!(BandStructure::new(
838            BandModel::Metal {
839                fermi_ev: 5.0,
840                work_function_ev: 4.0
841            },
842            "  "
843        )
844        .is_err());
845        let ins = |v, g, a| {
846            BandStructure::new(
847                BandModel::Insulator {
848                    valence_band_width_ev: v,
849                    band_gap_ev: g,
850                    affinity_ev: a,
851                },
852                "test",
853            )
854        };
855        let b = ins(6.0, 2.0, 1.0).unwrap();
856        assert_eq!(b.fermi_ev(), 7.0);
857        assert_eq!(b.inner_potential_ev(), 9.0);
858        assert_eq!(b.band_gap_ev(), Some(2.0));
859        assert!(ins(6.0, 0.0, 1.0).is_err());
860        assert!(ins(0.0, 1.0, -2.0).is_err(), "negative inner potential");
861        assert!(ins(6.0, 1.0, -0.5).is_ok(), "negative affinity is allowed");
862    }
863
864    /// Valence band width used only to complete the Si and SiO2 entries in
865    /// these tests: a synthetic round number, not a material value.
866    const SYNTHETIC_WV: f64 = 12.0;
867    const SYNTHETIC: &str = "synthetic test value, not material data";
868
869    #[test]
870    fn silicon_defaults_match_robertson_wallace() {
871        let si = band_defaults("Si").expect("Si entry");
872        assert_eq!(si.kind, BandKind::Insulator);
873        let gap = si.band_gap_ev.expect("Si band gap");
874        assert_eq!(gap.value_ev, 1.1);
875        for s in [
876            "Robertson and R. M. Wallace",
877            "Table 1, p. 43",
878            "2026-10-08",
879        ] {
880            assert!(gap.source.contains(s), "{s:?} missing from {}", gap.source);
881        }
882        let chi = si.affinity_ev.expect("Si electron affinity");
883        assert_eq!(chi.value_ev, 4.05);
884        for s in ["Fig. 42, p. 63", "Section 6.1", "2026-10-08"] {
885            assert!(chi.source.contains(s), "{s:?} missing from {}", chi.source);
886        }
887        assert!(si.valence_band_width_ev.is_none(), "no cited W_v");
888        assert!(si.gap.contains("valence band width"));
889    }
890
891    #[test]
892    fn silicon_dioxide_defaults_match_robertson_wallace() {
893        let d = band_defaults("SiO2").expect("SiO2 entry");
894        assert_eq!(d.kind, BandKind::Insulator);
895        let gap = d.band_gap_ev.expect("SiO2 band gap");
896        assert_eq!(gap.value_ev, 9.0);
897        for s in ["Table 1, p. 43", "Table 5", "2026-10-08"] {
898            assert!(gap.source.contains(s), "{s:?} missing from {}", gap.source);
899        }
900        assert!(d.affinity_ev.is_none() && d.valence_band_width_ev.is_none());
901        assert!(d.gap.contains("electron affinity"));
902    }
903
904    #[test]
905    fn metals_have_no_numbers() {
906        for m in ["Al", "Cu", "Au", "W"] {
907            let d = band_defaults(m).expect("metal entry");
908            assert_eq!(d.kind, BandKind::Metal);
909            assert!(d.fermi_ev.is_none() && d.work_function_ev.is_none(), "{m}");
910            assert!(d.gap.contains("work function"), "{m}");
911        }
912    }
913
914    #[test]
915    fn entries_are_unique_and_hold_only_their_kind() {
916        for (i, d) in BAND_DEFAULTS.iter().enumerate() {
917            assert!(
918                BAND_DEFAULTS[..i].iter().all(|e| e.material != d.material),
919                "duplicate {}",
920                d.material
921            );
922            assert!(!d.gap.trim().is_empty());
923            let (own, other) = match d.kind {
924                BandKind::Metal => (
925                    vec![d.fermi_ev, d.work_function_ev],
926                    vec![d.valence_band_width_ev, d.band_gap_ev, d.affinity_ev],
927                ),
928                BandKind::Insulator => (
929                    vec![d.valence_band_width_ev, d.band_gap_ev, d.affinity_ev],
930                    vec![d.fermi_ev, d.work_function_ev],
931                ),
932            };
933            assert!(other.iter().all(Option::is_none), "{}", d.material);
934            for c in own.into_iter().flatten() {
935                assert!(c.value_ev.is_finite(), "{}", d.material);
936                assert!(c.source.contains("opened 2026-"), "{}", c.source);
937            }
938        }
939        assert!(band_defaults("si").is_none(), "matched exactly");
940        assert!(band_defaults("Ge").is_none());
941    }
942
943    #[test]
944    fn no_entry_is_complete_on_its_own() {
945        for d in BAND_DEFAULTS {
946            let e = BandStructure::from_defaults(d.material).unwrap_err();
947            assert!(e.to_string().contains("no cited default"), "{e}");
948        }
949        let e = BandStructure::from_defaults("Ge").unwrap_err();
950        assert!(e.to_string().contains("Si, SiO2"), "{e}");
951    }
952
953    #[test]
954    fn completed_silicon_passes_the_checks_and_cites_both_sources() {
955        let fill = BandFill {
956            valence_band_width_ev: Some(SYNTHETIC_WV),
957            ..BandFill::default()
958        };
959        let b = band_defaults("Si")
960            .unwrap()
961            .complete(fill, SYNTHETIC)
962            .unwrap();
963        assert_eq!(
964            b.model(),
965            BandModel::Insulator {
966                valence_band_width_ev: SYNTHETIC_WV,
967                band_gap_ev: 1.1,
968                affinity_ev: 4.05,
969            }
970        );
971        assert_eq!(b.band_gap_ev(), Some(1.1));
972        assert!((b.inner_potential_ev() - (SYNTHETIC_WV + 1.1 + 4.05)).abs() < 1e-12);
973        // The same numbers pass `BandStructure::new` directly.
974        assert!(BandStructure::new(b.model(), SYNTHETIC).is_ok());
975        let p = b.provenance();
976        for s in [
977            "Fig. 42, p. 63",
978            "Table 1, p. 43",
979            "valence band width 12 eV",
980            SYNTHETIC,
981        ] {
982            assert!(p.contains(s), "{s:?} missing from {p}");
983        }
984    }
985
986    #[test]
987    fn completion_refuses_bad_fills() {
988        let si = band_defaults("Si").unwrap();
989        let wv = BandFill {
990            valence_band_width_ev: Some(SYNTHETIC_WV),
991            ..BandFill::default()
992        };
993        // Replacing a cited value.
994        let e = si
995            .complete(
996                BandFill {
997                    band_gap_ev: Some(1.2),
998                    ..wv
999                },
1000                SYNTHETIC,
1001            )
1002            .unwrap_err();
1003        assert!(e.to_string().contains("cited default"), "{e}");
1004        // A metal parameter on an insulator.
1005        let e = si
1006            .complete(
1007                BandFill {
1008                    work_function_ev: Some(4.0),
1009                    ..wv
1010                },
1011                SYNTHETIC,
1012            )
1013            .unwrap_err();
1014        assert!(e.to_string().contains("work function"), "{e}");
1015        // Filled values need a provenance.
1016        assert!(si.complete(wv, "  ").is_err());
1017        // Out-of-range fills still go through `BandStructure::new`.
1018        let neg = BandFill {
1019            valence_band_width_ev: Some(-1.0),
1020            ..BandFill::default()
1021        };
1022        assert!(si.complete(neg, SYNTHETIC).is_err());
1023        // SiO2 still lacks its affinity with W_v filled.
1024        let e = band_defaults("SiO2")
1025            .unwrap()
1026            .complete(wv, SYNTHETIC)
1027            .unwrap_err();
1028        assert!(e.to_string().contains("electron affinity"), "{e}");
1029        // A metal completed entirely from the caller.
1030        let cu = band_defaults("Cu").unwrap();
1031        let b = cu
1032            .complete(
1033                BandFill {
1034                    fermi_ev: Some(5.0),
1035                    work_function_ev: Some(4.0),
1036                    ..BandFill::default()
1037                },
1038                SYNTHETIC,
1039            )
1040            .unwrap();
1041        assert_eq!(b.inner_potential_ev(), 9.0);
1042        assert!(b.provenance().contains("cited defaults"));
1043        assert!(cu
1044            .complete(
1045                BandFill {
1046                    fermi_ev: Some(5.0),
1047                    work_function_ev: Some(4.0),
1048                    band_gap_ev: Some(1.0),
1049                    ..BandFill::default()
1050                },
1051                SYNTHETIC,
1052            )
1053            .is_err());
1054    }
1055}