1use std::collections::BTreeMap;
5use std::path::Path;
6
7use sha2::{Digest, Sha256};
8
9use crate::electron::boundary::{BandModel, BandStructure};
10use crate::electron::data::{OpticalElf, Subshell, SubshellBindingTable};
11use crate::electron::elastic::corrections::{CorrelationPolarization, PolarizationCutoff};
12use crate::electron::elastic::table::DEFAULT_MAX_ENERGY_EV;
13use crate::electron::inelastic::{FitWeighting, MerminFitOptions, PennAlgorithm, MAX_OSCILLATORS};
14use crate::electron::phonon::{FrohlichPhonon, InsulatorChannels, PolaronTrapping, Sio2LoMode};
15use crate::electron::secondary::SecondaryModel;
16use crate::electron::transport::{
17 BeamProfile, BeamSpread, BoundaryModel, CutoffReference, EscapeRule, Primary, TransportConfig,
18};
19use crate::elements::element_by_symbol;
20use crate::geometry::Stack;
21use crate::input::{divergence_from_spec, invalid, resolve_material, InputError, ResolvedLayer};
22use crate::ion::crystal::Divergence;
23use crate::material::Material;
24use crate::tally::{
25 BeamProfile as ExposureBeamProfile, Binning, CartesianGrid, CylindricalGrid, DepthDoseConfig,
26 DoseConfig, ElectronTallyConfig, ExposureConfig, ExposureVolume, InterfaceRefine,
27 LogRadialBinning, PsfConfig, PsfModel, PsfNormalization, VolumeShape, VoxelTallyGrid,
28};
29
30use super::resolved::*;
31use super::schema::*;
32
33const NM: f64 = 1e-9;
34
35const EXPOSURE_SEED_SALT: u64 = 0x6578_706f_7375_7265;
37
38fn finite_pos(v: f64) -> bool {
39 v.is_finite() && v > 0.0
40}
41
42fn resolve_beam_spread(b: &ElectronBeamSpec) -> Result<BeamSpread, InputError> {
44 if !b.landing_nm.iter().all(|v| v.is_finite()) {
45 return Err(invalid("electron.beam.landing_nm", "must be finite"));
46 }
47 let size = |field: &str, v: f64| {
48 if finite_pos(v) {
49 Ok(v * NM)
50 } else {
51 Err(invalid(
52 format!("electron.beam.profile.{field}"),
53 "must be finite and positive",
54 ))
55 }
56 };
57 let profile = match b.profile {
58 None | Some(BeamProfileSpec::Point) => BeamProfile::Point,
59 Some(BeamProfileSpec::Gaussian { sigma_nm }) => BeamProfile::Gaussian {
60 sigma_m: size("sigma_nm", sigma_nm)?,
61 },
62 Some(BeamProfileSpec::Disc { radius_nm }) => BeamProfile::Disc {
63 radius_m: size("radius_nm", radius_nm)?,
64 },
65 Some(BeamProfileSpec::Rectangle {
66 width_y_nm,
67 width_z_nm,
68 }) => BeamProfile::Rectangle {
69 width_y_m: size("width_y_nm", width_y_nm)?,
70 width_z_m: size("width_z_nm", width_z_nm)?,
71 },
72 };
73 let divergence = match &b.divergence {
74 None => Divergence::None,
75 Some(d) => divergence_from_spec(d, "electron.beam.divergence")?,
76 };
77 Ok(BeamSpread {
78 landing_m: [b.landing_nm[0] * NM, b.landing_nm[1] * NM],
79 profile,
80 divergence,
81 })
82}
83
84fn with_valence_binding(
86 band: BandStructure,
87 binding_ev: Option<f64>,
88) -> Result<BandStructure, crate::electron::boundary::BandError> {
89 match binding_ev {
90 None => Ok(band),
91 Some(b) => band.with_valence_binding_ev(b),
92 }
93}
94
95fn read_data_file(
96 base_dir: &Path,
97 field: &str,
98 path: &str,
99) -> Result<(String, DataFile), InputError> {
100 let joined = base_dir.join(path);
101 let bytes = std::fs::read(&joined)
102 .map_err(|e| invalid(field, format!("cannot read {}: {e}", joined.display())))?;
103 let text = String::from_utf8(bytes.clone()).map_err(|_| {
104 invalid(
105 field,
106 format!("{} is not valid UTF-8 text", joined.display()),
107 )
108 })?;
109 let sha256 = Sha256::digest(&bytes)
110 .iter()
111 .map(|b| format!("{b:02x}"))
112 .collect();
113 Ok((
114 text,
115 DataFile {
116 path: path.to_string(),
117 resolved_path: std::fs::canonicalize(&joined).unwrap_or(joined),
118 sha256,
119 },
120 ))
121}
122
123fn binning_nm(field: &str, b: &BinsNm) -> Result<Binning, InputError> {
124 Binning::new(b.lo_nm * NM, b.hi_nm * NM, b.bins)
125 .map_err(|e| invalid(field, format!("{e} (edges in nm)")))
126}
127
128fn lowest_threshold(
131 materials: &[ResolvedElectronMaterial],
132 layer_material: &[usize],
133 reference: CutoffReference,
134 cutoff_ev: f64,
135) -> f64 {
136 layer_material
137 .iter()
138 .map(|&i| match reference {
139 CutoffReference::BandBottom => cutoff_ev,
140 CutoffReference::VacuumLevel => {
141 materials[i]
142 .band
143 .as_ref()
144 .map_or(0.0, BandStructure::inner_potential_ev)
145 + cutoff_ev
146 }
147 })
148 .fold(f64::INFINITY, f64::min)
149}
150
151struct ResolvedTally {
153 tally: ElectronTallyConfig,
154 voxel_grid: Option<VoxelTallyGrid>,
155 psf_fits: Vec<PsfModel>,
156 psf_normalization: PsfNormalization,
157}
158
159fn reject_unless_selected(
163 fields: [(&str, Option<bool>); 2],
164 applies_with: &str,
165) -> Result<(), InputError> {
166 for (k, v) in fields {
167 if v.is_some() {
168 return Err(invalid(
169 format!("electron.transport.{k}"),
170 format!("only applies with {applies_with}"),
171 ));
172 }
173 }
174 Ok(())
175}
176
177fn resolve_primary(b: &ElectronBeamSpec) -> Result<Primary, InputError> {
179 if !finite_pos(b.energy_ev) {
180 return Err(invalid(
181 "electron.beam.energy_ev",
182 "must be finite and positive",
183 ));
184 }
185 if !(b.tilt_deg.is_finite() && (0.0..90.0).contains(&b.tilt_deg)) {
186 return Err(invalid(
187 "electron.beam.tilt_deg",
188 "must be in [0, 90) degrees",
189 ));
190 }
191 if !b.azimuth_deg.is_finite() {
192 return Err(invalid("electron.beam.azimuth_deg", "must be finite"));
193 }
194 let (s, c) = b.tilt_deg.to_radians().sin_cos();
195 let (sa, ca) = b.azimuth_deg.to_radians().sin_cos();
196 Ok(Primary {
197 energy_ev: b.energy_ev,
198 direction: [c, s * ca, s * sa],
199 })
200}
201
202fn resolve_transport(
206 t: &TransportSpec,
207 echo: &mut ElectronInput,
208) -> Result<(TransportConfig, bool), InputError> {
209 if !finite_pos(t.cutoff_ev) {
210 return Err(invalid(
211 "electron.transport.cutoff_ev",
212 "must be finite and positive",
213 ));
214 }
215 if t.max_events == 0 {
216 return Err(invalid(
217 "electron.transport.max_events",
218 "must be at least 1",
219 ));
220 }
221 let secondaries = match t.secondaries {
222 SecondaryChoice::Off => {
223 reject_unless_selected(
224 [
225 ("instantaneous_momentum", t.instantaneous_momentum),
226 ("momentum_conservation", t.momentum_conservation),
227 ],
228 "secondaries = \"kieft-bosch\"",
229 )?;
230 SecondaryModel::Off
231 }
232 SecondaryChoice::KieftBosch => {
233 let im = t.instantaneous_momentum.unwrap_or(true);
234 let mc = t.momentum_conservation.unwrap_or(true);
235 echo.electron.transport.instantaneous_momentum = Some(im);
236 echo.electron.transport.momentum_conservation = Some(mc);
237 SecondaryModel::KieftBosch {
238 instantaneous_momentum: im,
239 momentum_conservation: mc,
240 }
241 }
242 };
243 let boundary = match t.boundary {
244 BoundaryChoice::Transparent => {
245 reject_unless_selected(
246 [
247 ("quantum_transmission", t.quantum_transmission),
248 ("refraction", t.refraction),
249 ],
250 "boundary = \"step-barrier\"",
251 )?;
252 BoundaryModel::Transparent
253 }
254 BoundaryChoice::StepBarrier => {
255 let qt = t.quantum_transmission.unwrap_or(true);
256 let rf = t.refraction.unwrap_or(true);
257 echo.electron.transport.quantum_transmission = Some(qt);
258 echo.electron.transport.refraction = Some(rf);
259 BoundaryModel::StepBarrier {
260 quantum_transmission: qt,
261 refraction: rf,
262 }
263 }
264 };
265 let cutoff_reference = match t.cutoff_reference {
266 CutoffReferenceChoice::BandBottom => CutoffReference::BandBottom,
267 CutoffReferenceChoice::VacuumLevel => CutoffReference::VacuumLevel,
268 };
269 let config = TransportConfig {
270 cutoff_ev: t.cutoff_ev,
271 escape_rule: match t.escape_rule {
272 EscapeRuleChoice::BothFaces => EscapeRule::BothFaces,
273 EscapeRuleChoice::FrontOnly => EscapeRule::FrontOnly,
274 },
275 max_events: t.max_events,
276 secondaries,
277 boundary,
278 cutoff_reference,
279 };
280 let needs_bands = secondaries != SecondaryModel::Off
281 || boundary != BoundaryModel::Transparent
282 || cutoff_reference == CutoffReference::VacuumLevel;
283 Ok((config, needs_bands))
284}
285
286fn resolve_elastic(
289 el: &ElasticSpec,
290 layers: &[ResolvedLayer],
291) -> Result<ElasticChoice, InputError> {
292 let mut cp = BTreeMap::new();
293 if let Some(c) = &el.correlation_polarization {
294 let f = "electron.elastic.correlation_polarization";
295 let cutoff = match c.b_pol_squared {
296 None => PolarizationCutoff::Seltzer,
297 Some(b) if b.is_finite() && b >= 0.0 => PolarizationCutoff::BPolSquared(b),
298 Some(b) => {
299 return Err(invalid(
300 format!("{f}.b_pol_squared"),
301 format!("{b} must be finite and non-negative"),
302 ))
303 }
304 };
305 if !finite_pos(c.outer_radius_bohr) {
306 return Err(invalid(
307 format!("{f}.outer_radius_bohr"),
308 "must be finite and positive",
309 ));
310 }
311 let mut zs: Vec<u8> = Vec::new();
312 for l in layers {
313 for comp in l.material.components() {
314 if comp.atom_fraction() > 0.0 && !zs.contains(&comp.z()) {
315 zs.push(comp.z());
316 }
317 }
318 }
319 for (sym, p) in &c.polarizability {
320 let pf = format!("{f}.polarizability.{sym}");
321 let elm = element_by_symbol(sym)
322 .ok_or_else(|| invalid(&pf, format!("unknown element symbol {sym:?}")))?;
323 if !zs.contains(&elm.z) {
324 return Err(invalid(&pf, format!("{sym} is not in any target layer")));
325 }
326 if !finite_pos(p.bohr3) {
327 return Err(invalid(
328 format!("{pf}.bohr3"),
329 "must be finite and positive",
330 ));
331 }
332 if p.source.trim().is_empty() {
333 return Err(invalid(
334 format!("{pf}.source"),
335 "a polarizability needs a source (a citation of the value)",
336 ));
337 }
338 cp.insert(
339 elm.z,
340 CorrelationPolarization {
341 polarizability: p.bohr3,
342 polarizability_source: p.source.trim().to_string(),
343 cutoff,
344 outer_radius: c.outer_radius_bohr,
345 },
346 );
347 }
348 for z in zs {
349 if !cp.contains_key(&z) {
350 let sym = crate::elements::element(z).expect("valid").symbol;
351 return Err(invalid(
352 format!("{f}.polarizability"),
353 format!("no polarizability for {sym}; give every target element one"),
354 ));
355 }
356 }
357 }
358 Ok(ElasticChoice {
359 model: el.model,
360 potential: el.potential,
361 exchange: el.exchange,
362 correlation_polarization: cp,
363 })
364}
365
366fn resolve_inelastic(
369 e: &ElectronSpec,
370 materials: &[ResolvedElectronMaterial],
371 echo: &mut ElectronInput,
372) -> Result<PennAlgorithm, InputError> {
373 let inelastic: PennAlgorithm =
374 e.inelastic
375 .model
376 .parse()
377 .map_err(|err: crate::electron::data::ElectronDataError| {
378 invalid("electron.inelastic.model", err.to_string())
379 })?;
380 if !(e.inelastic.fermi_energy_ev.is_finite() && e.inelastic.fermi_energy_ev >= 0.0) {
381 return Err(invalid(
382 "electron.inelastic.fermi_energy_ev",
383 "must be finite and non-negative",
384 ));
385 }
386 for (name, spec) in &e.materials {
389 if spec.mermin_fit.is_some() && inelastic != PennAlgorithm::Mermin {
390 return Err(invalid(
391 format!("electron.materials.{name}.mermin_fit"),
392 format!(
393 "only applies with electron.inelastic.model = \"{}\" (the model is \"{}\")",
394 PennAlgorithm::Mermin.label(),
395 inelastic.label()
396 ),
397 ));
398 }
399 }
400 for (name, spec) in &e.materials {
403 if spec.inner_shells.is_some() && inelastic != PennAlgorithm::SinglePole {
404 return Err(invalid(
405 format!("electron.materials.{name}.inner_shells"),
406 format!(
407 "only applies with electron.inelastic.model = \"{}\" (the model is \"{}\")",
408 PennAlgorithm::SinglePole.label(),
409 inelastic.label()
410 ),
411 ));
412 }
413 }
414 for m in materials {
415 if let FitWeighting::Relative { floor } = m.mermin_fit.weighting {
416 if let Some(f) = echo
417 .electron
418 .materials
419 .get_mut(&m.name)
420 .and_then(|s| s.mermin_fit.as_mut())
421 {
422 f.relative_floor = Some(floor);
423 }
424 }
425 }
426 if e.inelastic.fermi_energy_ev != 0.0 {
429 if let Some(m) = materials.iter().find(|m| m.band.is_some()) {
430 return Err(invalid(
431 "electron.inelastic.fermi_energy_ev",
432 format!(
433 "must be 0 when a material has a band (material {} does): the \
434 inelastic table of a material with a band takes its Fermi energy \
435 from the band",
436 m.name
437 ),
438 ));
439 }
440 }
441 Ok(inelastic)
442}
443
444fn resolve_table_grid(
447 e: &ElectronSpec,
448 config: &TransportConfig,
449 materials: &[ResolvedElectronMaterial],
450 layer_material: &[usize],
451 echo: &mut ElectronInput,
452 warnings: &mut Vec<String>,
453) -> Result<Vec<f64>, InputError> {
454 let g = &e.tables;
455 let max_inner = materials
456 .iter()
457 .filter_map(|m| m.band.as_ref().map(BandStructure::inner_potential_ev))
458 .fold(0.0, f64::max);
459 let top = match config.boundary {
460 BoundaryModel::StepBarrier { .. } => e.beam.energy_ev + max_inner,
461 BoundaryModel::Transparent => e.beam.energy_ev,
462 };
463 let max_energy = g.max_energy_ev.unwrap_or(top);
464 echo.electron.tables.max_energy_ev = Some(max_energy);
465 if !(finite_pos(g.min_energy_ev) && max_energy.is_finite() && max_energy > g.min_energy_ev) {
466 return Err(invalid(
467 "electron.tables",
468 format!(
469 "need 0 < min_energy_ev < max_energy_ev, got {} and {max_energy} eV",
470 g.min_energy_ev
471 ),
472 ));
473 }
474 let table_energy_ev = crate::electron::elastic::table::log_energy_grid(
475 g.min_energy_ev,
476 max_energy,
477 g.points_per_decade,
478 )
479 .map_err(|err| invalid("electron.tables", err.to_string()))?;
480 if max_energy < top {
481 warnings.push(format!(
482 "electron.tables.max_energy_ev: {max_energy} eV is below the largest energy \
483 an electron can have ({top} eV); rates above it are held at the last row \
484 (results.table_coverage counts how often)"
485 ));
486 }
487 let lowest = lowest_threshold(
490 materials,
491 layer_material,
492 config.cutoff_reference,
493 e.transport.cutoff_ev,
494 );
495 if g.min_energy_ev > lowest {
496 warnings.push(format!(
497 "electron.tables.min_energy_ev: {} eV is above the lowest stopping threshold \
498 ({lowest} eV); rates below it are held at the first row \
499 (results.table_coverage counts how often)",
500 g.min_energy_ev
501 ));
502 }
503 if max_energy > DEFAULT_MAX_ENERGY_EV {
504 warnings.push(format!(
505 "electron.tables.max_energy_ev: {max_energy} eV is above the {DEFAULT_MAX_ENERGY_EV} eV \
506 the elastic tables are tested to; the inelastic models are nonrelativistic"
507 ));
508 }
509 Ok(table_energy_ev)
510}
511
512impl ElectronInput {
513 pub fn is_electron_toml(text: &str) -> bool {
516 text.parse::<toml::Table>()
517 .map(|t| t.contains_key("electron"))
518 .unwrap_or(false)
519 }
520
521 pub fn from_toml_str(text: &str) -> Result<Self, InputError> {
523 if text
524 .parse::<toml::Table>()
525 .is_ok_and(|t| t.contains_key("crystal"))
526 {
527 return Err(invalid(
528 "crystal",
529 "not supported with an [electron] run (crystal transport is for ion runs)",
530 ));
531 }
532 toml::from_str(text).map_err(|e| InputError::Parse(e.to_string()))
533 }
534
535 pub fn to_toml_string(&self) -> Result<String, InputError> {
537 toml::to_string(self).map_err(|e| InputError::Parse(e.to_string()))
538 }
539
540 pub fn resolve(&self) -> Result<ResolvedElectron, InputError> {
543 self.resolve_in(Path::new("."))
544 }
545
546 pub fn resolve_in(&self, base_dir: &Path) -> Result<ResolvedElectron, InputError> {
550 let mut warnings = Vec::new();
551 let e = &self.electron;
552 let mut echo = self.clone();
553 echo.run.threads = None;
554
555 self.resolve_run()?;
556
557 let primary = resolve_primary(&e.beam)?;
558 let beam = resolve_beam_spread(&e.beam)?;
559
560 let (config, needs_bands) = resolve_transport(&e.transport, &mut echo)?;
561
562 for (name, spec) in &self.materials {
564 Material::try_from(spec.clone())
565 .map_err(|err| invalid(format!("materials.{name}"), err.to_string()))?;
566 }
567
568 let (stack, layers, fields) = self.resolve_target()?;
569
570 let (materials, layer_material) =
571 self.resolve_layer_materials(base_dir, &layers, &fields, needs_bands, &mut warnings)?;
572
573 let elastic = resolve_elastic(&e.elastic, &layers)?;
574
575 let inelastic = resolve_inelastic(e, &materials, &mut echo)?;
576
577 let table_energy_ev = resolve_table_grid(
578 e,
579 &config,
580 &materials,
581 &layer_material,
582 &mut echo,
583 &mut warnings,
584 )?;
585
586 let ResolvedTally {
587 tally,
588 voxel_grid,
589 psf_fits,
590 psf_normalization,
591 } = self.resolve_tally(&mut echo)?;
592
593 Ok(ResolvedElectron {
594 input: echo,
595 stack,
596 layers,
597 layer_material,
598 materials,
599 config,
600 primary,
601 beam,
602 elastic,
603 inelastic,
604 inelastic_fermi_ev: e.inelastic.fermi_energy_ev,
605 table_energy_ev,
606 tally,
607 voxel_grid,
608 psf_fits,
609 psf_normalization,
610 warnings,
611 })
612 }
613
614 fn resolve_run(&self) -> Result<(), InputError> {
616 if self.run.histories == 0 {
617 return Err(invalid("run.histories", "must be at least 1"));
618 }
619 if self.run.threads == Some(0) {
620 return Err(invalid(
621 "run.threads",
622 "must be at least 1 (omit for all cores)",
623 ));
624 }
625 Ok(())
626 }
627
628 fn resolve_target(&self) -> Result<(Stack, Vec<ResolvedLayer>, Vec<String>), InputError> {
632 let tg = &self.target;
633 if tg.layers.is_empty() && tg.substrate.is_none() {
634 return Err(invalid(
635 "target",
636 "needs at least one [[target.layers]] entry or a substrate",
637 ));
638 }
639 let mut layers = Vec::new();
640 let mut fields = Vec::new();
641 let mut thick = Vec::new();
642 for (i, l) in tg.layers.iter().enumerate() {
643 let field = format!("target.layers[{i}]");
644 if !finite_pos(l.thickness_nm) {
645 return Err(invalid(
646 format!("{field}.thickness_nm"),
647 format!("{} nm must be finite and positive", l.thickness_nm),
648 ));
649 }
650 let f = format!("{field}.material");
651 layers.push(resolve_material(&self.materials, &f, &l.material)?);
652 fields.push(f);
653 thick.push(l.thickness_nm * NM);
654 }
655 if let Some(sub) = &tg.substrate {
656 layers.push(resolve_material(&self.materials, "target.substrate", sub)?);
657 fields.push("target.substrate".to_string());
658 }
659 let substrate = tg.substrate.as_ref().map(|_| {
660 layers
661 .last()
662 .expect("substrate pushed last")
663 .material
664 .clone()
665 });
666 let finite: Vec<(Material, f64)> = layers
667 .iter()
668 .zip(&thick)
669 .map(|(l, &t)| (l.material.clone(), t))
670 .collect();
671 let stack =
672 Stack::new(finite, substrate).map_err(|err| invalid("target", err.to_string()))?;
673 Ok((stack, layers, fields))
674 }
675
676 fn resolve_layer_materials(
679 &self,
680 base_dir: &Path,
681 layers: &[ResolvedLayer],
682 fields: &[String],
683 needs_bands: bool,
684 warnings: &mut Vec<String>,
685 ) -> Result<(Vec<ResolvedElectronMaterial>, Vec<usize>), InputError> {
686 let e = &self.electron;
687 let mut materials: Vec<ResolvedElectronMaterial> = Vec::new();
688 let mut layer_material = Vec::with_capacity(layers.len());
689 for (l, field) in layers.iter().zip(fields) {
690 if l.source == "inline" {
691 return Err(invalid(
692 field,
693 "an electron run needs a named material (a [materials] key or an \
694 element symbol), so that [electron.materials.<name>] can give its \
695 electron data",
696 ));
697 }
698 if let Some(i) = materials.iter().position(|m| m.name == l.source) {
699 layer_material.push(i);
700 continue;
701 }
702 let name = &l.source;
703 let mfield = format!("electron.materials.{name}");
704 let spec = e.materials.get(name).ok_or_else(|| {
705 invalid(
706 field,
707 format!(
708 "material {name:?} has no electron data; add [{mfield}] with at \
709 least optical_elf"
710 ),
711 )
712 })?;
713 materials.push(self.resolve_electron_material(
714 base_dir,
715 &mfield,
716 name,
717 &l.material,
718 spec,
719 needs_bands,
720 )?);
721 layer_material.push(materials.len() - 1);
722 }
723 for name in e.materials.keys() {
724 if !materials.iter().any(|m| &m.name == name) {
725 warnings.push(format!(
726 "electron.materials.{name}: not used by any target layer"
727 ));
728 }
729 }
730 Ok((materials, layer_material))
731 }
732
733 fn resolve_tally(&self, echo: &mut ElectronInput) -> Result<ResolvedTally, InputError> {
736 let e = &self.electron;
737 let b = &e.beam;
738 let ts = &e.tally;
739 let f = "electron.tally";
740 if !(ts.se_bse_split_ev.is_finite() && ts.se_bse_split_ev >= 0.0) {
741 return Err(invalid(
742 format!("{f}.se_bse_split_ev"),
743 "must be finite and non-negative",
744 ));
745 }
746 let emax = ts.escape_energy_max_ev.unwrap_or(b.energy_ev);
747 echo.electron.tally.escape_energy_max_ev = Some(emax);
748 let escape_energy = Binning::new(0.0, emax, ts.escape_energy_bins).map_err(|err| {
749 invalid(
750 format!("{f}.escape_energy_max_ev / escape_energy_bins"),
751 err.to_string(),
752 )
753 })?;
754 if !(finite_pos(ts.escape_polar_max_deg) && ts.escape_polar_max_deg <= 180.0) {
755 return Err(invalid(
756 format!("{f}.escape_polar_max_deg"),
757 "must be in (0, 180] degrees",
758 ));
759 }
760 let escape_polar = Binning::new(
761 0.0,
762 ts.escape_polar_max_deg
763 .to_radians()
764 .min(std::f64::consts::PI),
765 ts.escape_polar_bins,
766 )
767 .map_err(|err| invalid(format!("{f}.escape_polar_bins"), err.to_string()))?;
768 let mut tally = ElectronTallyConfig::new(escape_energy, escape_polar);
769 tally.se_bse_split_ev = ts.se_bse_split_ev;
770 if let Some(c) = &ts.cartesian {
771 tally.cartesian = Some(CartesianGrid {
772 x: binning_nm(&format!("{f}.cartesian.x"), &c.x)?,
773 y: binning_nm(&format!("{f}.cartesian.y"), &c.y)?,
774 z: binning_nm(&format!("{f}.cartesian.z"), &c.z)?,
775 });
776 }
777 if let Some(c) = &ts.cylindrical {
778 if c.r.lo_nm < 0.0 {
779 return Err(invalid(format!("{f}.cylindrical.r.lo_nm"), "must be >= 0"));
780 }
781 tally.cylindrical = Some(CylindricalGrid {
782 r: binning_nm(&format!("{f}.cylindrical.r"), &c.r)?,
783 depth: binning_nm(&format!("{f}.cylindrical.depth"), &c.depth)?,
784 });
785 }
786 if let Some(d) = &ts.dose {
787 let depth = match d.depth_bins_per_layer {
788 None => {
789 if d.refine_width_nm.is_some()
790 || d.refine_bins.is_some()
791 || d.substrate_depth_nm.is_some()
792 {
793 return Err(invalid(
794 format!("{f}.dose.depth_bins_per_layer"),
795 "required when a depth-profile key is given",
796 ));
797 }
798 None
799 }
800 Some(bins_per_layer) => {
801 if bins_per_layer == 0 {
802 return Err(invalid(
803 format!("{f}.dose.depth_bins_per_layer"),
804 "must be at least 1",
805 ));
806 }
807 let refine = match d.refine_width_nm {
808 None => None,
809 Some(w) => {
810 if !(w.is_finite() && w >= 0.0) {
811 return Err(invalid(
812 format!("{f}.dose.refine_width_nm"),
813 "must be finite and non-negative",
814 ));
815 }
816 let bins = d.refine_bins.unwrap_or(10);
817 if bins == 0 {
818 return Err(invalid(
819 format!("{f}.dose.refine_bins"),
820 "must be at least 1",
821 ));
822 }
823 Some(InterfaceRefine {
824 width_m: w * NM,
825 bins,
826 })
827 }
828 };
829 Some(DepthDoseConfig {
830 bins_per_layer,
831 refine,
832 substrate_depth_m: d.substrate_depth_nm.unwrap_or(0.0) * NM,
833 })
834 }
835 };
836 tally.dose = Some(DoseConfig {
837 depth,
838 transmission: d.transmission,
839 });
840 }
841 let mut voxel_grid = None;
842 if let Some(v) = &ts.voxel {
843 for a in 0..3 {
844 if !v.origin_nm[a].is_finite() {
845 return Err(invalid(format!("{f}.voxel.origin_nm"), "must be finite"));
846 }
847 if !finite_pos(v.spacing_nm[a]) {
848 return Err(invalid(
849 format!("{f}.voxel.spacing_nm"),
850 "must be finite and positive",
851 ));
852 }
853 if v.dims[a] == 0 {
854 return Err(invalid(format!("{f}.voxel.dims"), "must be at least 1"));
855 }
856 }
857 voxel_grid = Some(
858 VoxelTallyGrid::new(
859 v.origin_nm.map(|x| x * NM),
860 v.spacing_nm.map(|x| x * NM),
861 v.dims,
862 )
863 .map_err(|e| invalid(format!("{f}.voxel"), e.to_string()))?,
864 );
865 }
866 let mut psf_fits: Vec<PsfModel> = Vec::new();
867 let mut psf_normalization = PsfNormalization::default();
868 if let Some(p) = &ts.psf {
869 let nm = |key: &str, v: f64| -> Result<f64, InputError> {
870 if v.is_finite() {
871 Ok(v * NM)
872 } else {
873 Err(invalid(format!("{f}.psf.{key}"), "must be finite"))
874 }
875 };
876 let depth_lo = nm("depth_lo_nm", p.depth_lo_nm)?;
877 let depth_hi = nm("depth_hi_nm", p.depth_hi_nm)?;
878 if depth_hi <= depth_lo {
879 return Err(invalid(
880 format!("{f}.psf.depth_hi_nm"),
881 "must be greater than depth_lo_nm",
882 ));
883 }
884 let r_min = nm("r_min_nm", p.r_min_nm)?;
885 let r_max = nm("r_max_nm", p.r_max_nm)?;
886 if r_min <= 0.0 {
887 return Err(invalid(format!("{f}.psf.r_min_nm"), "must be positive"));
888 }
889 if r_max <= r_min {
890 return Err(invalid(
891 format!("{f}.psf.r_max_nm"),
892 "must be greater than r_min_nm",
893 ));
894 }
895 if p.bins == 0 {
896 return Err(invalid(format!("{f}.psf.bins"), "must be at least 1"));
897 }
898 let radial = LogRadialBinning::new(r_min, r_max, p.bins)
899 .map_err(|e| invalid(format!("{f}.psf.bins"), e.to_string()))?;
900 tally.psf = Some(
901 PsfConfig::new(radial, depth_lo, depth_hi)
902 .map_err(|e| invalid(format!("{f}.psf"), e.to_string()))?,
903 );
904 for m in p.fits.iter().copied().map(PsfModel::from) {
905 if !psf_fits.contains(&m) {
906 psf_fits.push(m);
907 }
908 }
909 psf_normalization = p.normalization.into();
910 }
911
912 if let Some(x) = &ts.exposure {
913 tally.exposure = Some(self.resolve_exposure(x, echo)?);
914 }
915 Ok(ResolvedTally {
916 tally,
917 voxel_grid,
918 psf_fits,
919 psf_normalization,
920 })
921 }
922
923 fn resolve_exposure(
925 &self,
926 x: &ExposureSpec,
927 echo: &mut ElectronInput,
928 ) -> Result<ExposureConfig, InputError> {
929 let k = "electron.tally.exposure".to_string();
930 let nm = |key: &str, v: f64| -> Result<f64, InputError> {
931 if v.is_finite() {
932 Ok(v * NM)
933 } else {
934 Err(invalid(format!("{k}.{key}"), "must be finite"))
935 }
936 };
937 let beam = match x.beam {
938 ExposureBeamSpec::Point => ExposureBeamProfile::Point,
939 ExposureBeamSpec::Gaussian { sigma_nm } => ExposureBeamProfile::Gaussian {
940 sigma_m: nm("beam.sigma_nm", sigma_nm)?,
941 },
942 ExposureBeamSpec::UniformDisc { radius_nm } => ExposureBeamProfile::UniformDisc {
943 radius_m: nm("beam.radius_nm", radius_nm)?,
944 },
945 };
946 let mut volumes = Vec::new();
947 for (i, v) in x.volumes.iter().enumerate() {
948 let key = format!("volumes[{i}]");
949 if v.name.is_empty() || x.volumes[..i].iter().any(|o| o.name == v.name) {
950 return Err(invalid(
951 format!("{k}.{key}.name"),
952 "must be non-empty and unique",
953 ));
954 }
955 let need = |name: &str, v: Option<f64>| -> Result<f64, InputError> {
956 let v = v.ok_or_else(|| {
957 invalid(format!("{k}.{key}.{name}"), "required for this shape")
958 })?;
959 nm(&format!("{key}.{name}"), v)
960 };
961 let shape = match v.shape {
962 ExposureShapeSpec::Cylinder => VolumeShape::Cylinder {
963 radius_m: need("radius_nm", v.radius_nm)?,
964 },
965 ExposureShapeSpec::Box => VolumeShape::Box {
966 half_y_m: need("half_y_nm", v.half_y_nm)?,
967 half_z_m: need("half_z_nm", v.half_z_nm)?,
968 },
969 };
970 volumes.push(ExposureVolume {
971 name: v.name.clone(),
972 shape,
973 depth_lo_m: nm(&format!("{key}.depth_lo_nm"), v.depth_lo_nm)?,
974 depth_hi_m: nm(&format!("{key}.depth_hi_nm"), v.depth_hi_nm)?,
975 offset_y_m: nm(&format!("{key}.offset_y_nm"), v.offset_y_nm)?,
976 offset_z_m: nm(&format!("{key}.offset_z_nm"), v.offset_z_nm)?,
977 });
978 }
979 let x_max_ev = x.x_max_ev.unwrap_or(self.electron.beam.energy_ev);
980 echo.electron.tally.exposure.as_mut().unwrap().x_max_ev = Some(x_max_ev);
981 let cfg = ExposureConfig {
982 volumes,
983 beam,
984 seed: self.run.seed ^ EXPOSURE_SEED_SALT,
986 bins: x.bins,
987 x_max_ev,
988 lambda: x.lambda,
989 quantiles: x.quantiles.clone(),
990 below_ev: x.below_ev.clone(),
991 above_ev: x.above_ev.clone(),
992 tail_tolerance: x.tail_tolerance,
993 };
994 cfg.validate()
995 .map_err(|e| invalid(k.clone(), e.to_string()))?;
996 Ok(cfg)
997 }
998
999 fn resolve_electron_material(
1000 &self,
1001 base_dir: &Path,
1002 field: &str,
1003 name: &str,
1004 material: &Material,
1005 spec: &ElectronMaterialSpec,
1006 needs_bands: bool,
1007 ) -> Result<ResolvedElectronMaterial, InputError> {
1008 let ef = format!("{field}.optical_elf");
1009 let (text, file) = read_data_file(base_dir, &ef, &spec.optical_elf)?;
1010 let optical_elf = OpticalElf::from_toml_str(&text)
1012 .map_err(|err| invalid(&ef, format!("{}: {err}", spec.optical_elf)))?;
1013
1014 let band = match &spec.band {
1015 None if needs_bands => {
1016 return Err(invalid(
1017 format!("{field}.band"),
1018 "required with secondaries, the step barrier or the vacuum-level \
1019 cutoff (they need the material's Fermi energy and inner potential)",
1020 ))
1021 }
1022 None => None,
1023 Some(bs) => {
1024 let r = match bs {
1025 BandSpec::Metal {
1026 fermi_ev,
1027 work_function_ev,
1028 valence_binding_ev,
1029 provenance,
1030 } => BandStructure::new(
1031 BandModel::Metal {
1032 fermi_ev: *fermi_ev,
1033 work_function_ev: *work_function_ev,
1034 },
1035 provenance.trim(),
1036 )
1037 .and_then(|b| with_valence_binding(b, *valence_binding_ev)),
1038 BandSpec::Insulator {
1039 valence_band_width_ev,
1040 band_gap_ev,
1041 affinity_ev,
1042 provenance,
1043 } => BandStructure::new(
1044 BandModel::Insulator {
1045 valence_band_width_ev: *valence_band_width_ev,
1046 band_gap_ev: *band_gap_ev,
1047 affinity_ev: *affinity_ev,
1048 },
1049 provenance.trim(),
1050 ),
1051 BandSpec::FreeElectronMetal {
1052 valence_electrons_per_atom,
1053 work_function_ev,
1054 valence_binding_ev,
1055 provenance,
1056 } => BandStructure::free_electron_metal(
1057 material,
1058 *valence_electrons_per_atom,
1059 *work_function_ev,
1060 provenance.trim(),
1061 )
1062 .and_then(|b| with_valence_binding(b, *valence_binding_ev)),
1063 };
1064 Some(r.map_err(|err| invalid(format!("{field}.band"), err.to_string()))?)
1065 }
1066 };
1067
1068 let mut channels = InsulatorChannels::none();
1069 if let Some(p) = &spec.phonon {
1070 let pf = format!("{field}.phonon");
1071 let ph = match p.preset.as_deref() {
1072 Some(preset) => {
1073 if p.hbar_omega_ev.is_some()
1074 || p.eps_static.is_some()
1075 || p.eps_high_frequency.is_some()
1076 || p.provenance.is_some()
1077 {
1078 return Err(invalid(
1079 &pf,
1080 "a preset fixes the parameters and their source; give only \
1081 preset and temperature_k",
1082 ));
1083 }
1084 let mode = match preset {
1085 "sio2-63mev" => Sio2LoMode::Mev63,
1086 "sio2-153mev" => Sio2LoMode::Mev153,
1087 other => {
1088 return Err(invalid(
1089 format!("{pf}.preset"),
1090 format!(
1091 "unknown preset {other:?}; expected \"sio2-63mev\" or \
1092 \"sio2-153mev\""
1093 ),
1094 ))
1095 }
1096 };
1097 FrohlichPhonon::sio2(mode, p.temperature_k)
1098 }
1099 None => {
1100 let need = |k: &str, v: Option<f64>| {
1101 v.ok_or_else(|| invalid(format!("{pf}.{k}"), "required without a preset"))
1102 };
1103 FrohlichPhonon::new(
1104 need("hbar_omega_ev", p.hbar_omega_ev)?,
1105 need("eps_static", p.eps_static)?,
1106 need("eps_high_frequency", p.eps_high_frequency)?,
1107 p.temperature_k,
1108 p.provenance.as_deref().unwrap_or("").trim(),
1109 )
1110 }
1111 };
1112 channels.phonon = Some(ph.map_err(|err| invalid(&pf, err.to_string()))?);
1113 }
1114 if let Some(p) = &spec.polaron {
1115 channels.polaron = Some(
1116 PolaronTrapping::new(p.c_per_nm / NM, p.gamma_per_ev, p.provenance.trim())
1117 .map_err(|err| invalid(format!("{field}.polaron"), err.to_string()))?,
1118 );
1119 }
1120
1121 let mermin_fit = match &spec.mermin_fit {
1122 None => MerminFitOptions::default(),
1123 Some(f) => mermin_fit_options(&format!("{field}.mermin_fit"), f)?,
1124 };
1125
1126 let inner_shells = match &spec.inner_shells {
1127 None => None,
1128 Some(i) => Some(resolve_inner_shells(
1129 base_dir,
1130 &format!("{field}.inner_shells"),
1131 material,
1132 i,
1133 )?),
1134 };
1135
1136 let e_pair = match &spec.e_pair {
1137 None => None,
1138 Some(p) => {
1139 if !(p.ev.is_finite() && p.ev > 0.0) {
1140 return Err(invalid(
1141 format!("{field}.e_pair.ev"),
1142 format!("must be finite and positive, got {}", p.ev),
1143 ));
1144 }
1145 if p.source.trim().is_empty() {
1146 return Err(invalid(
1147 format!("{field}.e_pair.source"),
1148 "a citation is required: E_pair has no default",
1149 ));
1150 }
1151 Some(ResolvedEPair {
1152 ev: p.ev,
1153 source: p.source.trim().to_string(),
1154 })
1155 }
1156 };
1157
1158 Ok(ResolvedElectronMaterial {
1159 name: name.to_string(),
1160 material: material.clone(),
1161 optical_elf,
1162 optical_elf_file: file,
1163 band,
1164 channels,
1165 mermin_fit,
1166 inner_shells,
1167 e_pair,
1168 })
1169 }
1170}
1171
1172fn resolve_inner_shells(
1176 base_dir: &Path,
1177 field: &str,
1178 material: &Material,
1179 spec: &InnerShellsSpec,
1180) -> Result<ResolvedInnerShells, InputError> {
1181 let sf = format!("{field}.subshells");
1182 if spec.subshells.is_empty() {
1183 return Err(invalid(&sf, "must list at least one subshell"));
1184 }
1185 let mut subshells: Vec<Subshell> = Vec::new();
1186 for label in &spec.subshells {
1187 let s = match Subshell::from_label(label) {
1188 Some(s) if SUPPORTED_SUBSHELLS.contains(&s.label()) => s,
1189 _ => {
1190 return Err(invalid(
1191 &sf,
1192 format!(
1193 "unknown or unsupported subshell {label:?}; expected one of \"K\", \
1194 \"L1\", \"L2\", \"L3\" (no hydrogenic formula is implemented for the M \
1195 shell and above)"
1196 ),
1197 ))
1198 }
1199 };
1200 if subshells.contains(&s) {
1201 return Err(invalid(&sf, format!("{label:?} is listed twice")));
1202 }
1203 subshells.push(s);
1204 }
1205 let (binding, binding_file) = match &spec.binding {
1206 None => (SubshellBindingTable::eadl2017(), None),
1207 Some(path) => {
1208 let bf = format!("{field}.binding");
1209 let (text, file) = read_data_file(base_dir, &bf, path)?;
1210 let table = SubshellBindingTable::from_toml_str(&text)
1212 .map_err(|err| invalid(&bf, format!("{path}: {err}")))?;
1213 (table, Some(file))
1214 }
1215 };
1216 for c in material.components() {
1217 if c.atom_fraction() <= 0.0 {
1218 continue;
1219 }
1220 let z = c.z();
1221 let sym = crate::elements::element(z).map_or("?", |e| e.symbol);
1222 if binding.atom(z).is_none() {
1223 return Err(invalid(
1224 format!("{field}.binding"),
1225 format!("the table has no entry for {sym} (Z = {z}), an element of the material"),
1226 ));
1227 }
1228 for s in &subshells {
1229 if binding.atom(z).and_then(|a| a.shell(*s)).is_none() {
1230 return Err(invalid(
1231 &sf,
1232 format!(
1233 "{} is not in the binding table for {sym} (Z = {z}), an element of \
1234 the material",
1235 s.label()
1236 ),
1237 ));
1238 }
1239 }
1240 }
1241 Ok(ResolvedInnerShells {
1242 subshells,
1243 binding,
1244 binding_file,
1245 })
1246}
1247
1248const SUPPORTED_SUBSHELLS: [&str; 4] = ["K", "L1", "L2", "L3"];
1250
1251fn mermin_fit_options(field: &str, f: &MerminFitSpec) -> Result<MerminFitOptions, InputError> {
1253 if !(1..=MAX_OSCILLATORS).contains(&f.oscillators) {
1254 return Err(invalid(
1255 format!("{field}.oscillators"),
1256 format!("must be in 1..={MAX_OSCILLATORS}, got {}", f.oscillators),
1257 ));
1258 }
1259 let weighting = match f.weighting {
1260 MerminWeightingChoice::Uniform => {
1261 if f.relative_floor.is_some() {
1262 return Err(invalid(
1263 format!("{field}.relative_floor"),
1264 "only applies with weighting = \"relative\"",
1265 ));
1266 }
1267 FitWeighting::Uniform
1268 }
1269 MerminWeightingChoice::Relative => {
1270 let floor = f.relative_floor.unwrap_or_else(default_relative_floor);
1271 if !(floor.is_finite() && floor > 0.0 && floor <= 1.0) {
1272 return Err(invalid(
1273 format!("{field}.relative_floor"),
1274 format!("must be in (0, 1], got {floor}"),
1275 ));
1276 }
1277 FitWeighting::Relative { floor }
1278 }
1279 };
1280 Ok(MerminFitOptions {
1281 n_oscillators: f.oscillators,
1282 weighting,
1283 ..MerminFitOptions::default()
1284 })
1285}