1use serde::Serialize;
78
79use crate::constants::{ELECTRON_MASS, ELEMENTARY_CHARGE, HBAR};
80use crate::material::Material;
81
82#[derive(Debug, Clone, PartialEq, thiserror::Error)]
84pub enum BandError {
85 #[error("invalid {what}: {why}")]
87 Invalid {
88 what: &'static str,
90 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#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
109#[serde(tag = "kind", rename_all = "kebab-case")]
110pub enum BandModel {
111 Metal {
113 fermi_ev: f64,
115 work_function_ev: f64,
117 },
118 Insulator {
120 valence_band_width_ev: f64,
122 band_gap_ev: f64,
124 affinity_ev: f64,
128 },
129}
130
131#[derive(Debug, Clone, PartialEq, Serialize)]
133pub struct BandStructure {
134 model: BandModel,
135 #[serde(skip_serializing_if = "Option::is_none")]
139 valence_binding_ev: Option<f64>,
140 provenance: String,
141}
142
143impl BandStructure {
144 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 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 pub fn valence_binding_ev(&self) -> Option<f64> {
241 self.valence_binding_ev
242 }
243
244 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 pub fn model(&self) -> BandModel {
275 self.model
276 }
277
278 pub fn provenance(&self) -> &str {
280 &self.provenance
281 }
282
283 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
344pub enum BandKind {
345 Metal,
347 Insulator,
350}
351
352#[derive(Debug, Clone, Copy, PartialEq)]
354pub struct CitedValue {
355 pub value_ev: f64,
357 pub source: &'static str,
360}
361
362#[derive(Debug, Clone, Copy, PartialEq)]
367pub struct BandDefaults {
368 pub material: &'static str,
370 pub kind: BandKind,
372 pub fermi_ev: Option<CitedValue>,
374 pub work_function_ev: Option<CitedValue>,
376 pub valence_band_width_ev: Option<CitedValue>,
378 pub band_gap_ev: Option<CitedValue>,
380 pub affinity_ev: Option<CitedValue>,
382 pub gap: &'static str,
384}
385
386#[derive(Debug, Clone, Copy, Default, PartialEq)]
391pub struct BandFill {
392 pub fermi_ev: Option<f64>,
394 pub work_function_ev: Option<f64>,
396 pub valence_band_width_ev: Option<f64>,
398 pub band_gap_ev: Option<f64>,
400 pub affinity_ev: Option<f64>,
402}
403
404macro_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
414macro_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
440pub 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 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 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 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
508pub fn band_defaults(material: &str) -> Option<&'static BandDefaults> {
512 BAND_DEFAULTS.iter().find(|d| d.material == material)
513}
514
515impl BandDefaults {
516 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 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
657pub 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
666pub 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
675pub 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#[derive(Debug, Clone, Copy, PartialEq)]
691pub enum StepOutcome {
692 Transmitted {
694 dir: [f64; 3],
696 energy_ev: f64,
698 },
699 Reflected {
701 dir: [f64; 3],
704 },
705}
706
707pub 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 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 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 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 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 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 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 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 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 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 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 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 assert!(si.complete(wv, " ").is_err());
1017 let neg = BandFill {
1019 valence_band_width_ev: Some(-1.0),
1020 ..BandFill::default()
1021 };
1022 assert!(si.complete(neg, SYNTHETIC).is_err());
1023 let e = band_defaults("SiO2")
1025 .unwrap()
1026 .complete(wv, SYNTHETIC)
1027 .unwrap_err();
1028 assert!(e.to_string().contains("electron affinity"), "{e}");
1029 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}