Skip to main content

lindhard/input/electron/
schema.rs

1//! The serde schema of the electron input: the `*Spec` and `*Choice` types
2//! and their defaults.
3
4use std::collections::BTreeMap;
5
6use serde::{Deserialize, Serialize};
7
8use crate::electron::elastic::table::{DEFAULT_MIN_ENERGY_EV, DEFAULT_POINTS_PER_DECADE};
9use crate::electron::inelastic::{FitWeighting, MerminFitOptions, PennAlgorithm};
10use crate::electron::transport::TransportConfig;
11use crate::input::{DivergenceSpec, TargetSpec, VoxelTallySpec};
12use crate::material::MaterialSpec;
13use crate::tally::{PsfModel, PsfNormalization, SE_BSE_SPLIT_EV};
14
15// Named only by intra-doc links in this module.
16#[cfg(doc)]
17use crate::electron::{
18    boundary::{BandModel, BandStructure},
19    data::OpticalElf,
20    phonon::{FrohlichPhonon, PolaronTrapping},
21    secondary::SecondaryModel,
22    transport::{BoundaryModel, CutoffReference, EscapeRule},
23};
24
25/// The whole electron run description.
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
27#[serde(deny_unknown_fields)]
28pub struct ElectronInput {
29    /// Everything electron-specific.
30    pub electron: ElectronSpec,
31    /// Named materials (as in an ion run).
32    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
33    pub materials: BTreeMap<String, MaterialSpec>,
34    /// The layered target (as in an ion run).
35    pub target: TargetSpec,
36    /// History count, seed and threads.
37    pub run: ElectronRunSpec,
38}
39
40/// `[electron]`.
41#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
42#[serde(deny_unknown_fields)]
43pub struct ElectronSpec {
44    /// The primary electrons.
45    pub beam: ElectronBeamSpec,
46    /// Cutoff, faces, secondaries.
47    pub transport: TransportSpec,
48    /// Elastic model.
49    pub elastic: ElasticSpec,
50    /// Inelastic model.
51    #[serde(default)]
52    pub inelastic: InelasticSpec,
53    /// Energy grid of the cross-section tables.
54    #[serde(default)]
55    pub tables: TableGridSpec,
56    /// Electron data of each material the target uses, by the name the
57    /// target gives it (a `[materials]` key or an element symbol).
58    pub materials: BTreeMap<String, ElectronMaterialSpec>,
59    /// What the run records.
60    #[serde(default)]
61    pub tally: ElectronTallySpec,
62}
63
64/// `[electron.beam]`.
65#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
66#[serde(deny_unknown_fields)]
67pub struct ElectronBeamSpec {
68    /// Kinetic energy of the primaries, eV: the vacuum energy with
69    /// `boundary = "step-barrier"`, the energy inside the first layer
70    /// otherwise.
71    pub energy_ev: f64,
72    /// Polar angle from the surface normal, degrees, in `[0, 90)`.
73    #[serde(default)]
74    pub tilt_deg: f64,
75    /// Azimuth of the incidence plane, degrees.
76    #[serde(default)]
77    pub azimuth_deg: f64,
78    /// Landing point `[y, z]` of the beam centre on the front face, nm
79    /// (default `[0, 0]`).
80    #[serde(default, skip_serializing_if = "is_origin")]
81    pub landing_nm: [f64; 2],
82    /// Lateral profile about the landing point (default: a point).
83    #[serde(default, skip_serializing_if = "Option::is_none")]
84    pub profile: Option<BeamProfileSpec>,
85    /// Angular spread about the nominal direction; the ion beam's
86    /// `[beam.divergence]` laws ([`DivergenceSpec`]), with the same inward
87    /// conditioning and width limit (default: none).
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    pub divergence: Option<DivergenceSpec>,
90}
91
92fn is_origin(v: &[f64; 2]) -> bool {
93    *v == [0.0; 2]
94}
95
96/// `[electron.beam.profile]`: the lateral profile of the beam on the front
97/// face, selected by `model`; lengths in nm.
98///
99/// ```toml
100/// [electron.beam.profile]
101/// model = "gaussian"   # sigma_nm: standard deviation of each of y and z
102/// sigma_nm = 5.0
103/// # model = "disc"      # radius_nm: uniform in area
104/// # model = "rectangle" # width_y_nm, width_z_nm: full widths, uniform
105/// ```
106///
107/// See [`BeamProfile`](crate::electron::transport::BeamProfile) for the laws.
108#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
109#[serde(tag = "model", rename_all = "kebab-case", deny_unknown_fields)]
110pub enum BeamProfileSpec {
111    /// Every primary lands at the landing point.
112    Point,
113    /// Gaussian spot.
114    Gaussian {
115        /// Standard deviation of each of `y` and `z`, nm, `> 0`.
116        sigma_nm: f64,
117    },
118    /// Uniform disc.
119    Disc {
120        /// Radius, nm, `> 0`.
121        radius_nm: f64,
122    },
123    /// Uniform rectangle centred on the landing point.
124    Rectangle {
125        /// Full width along `y`, nm, `> 0`.
126        width_y_nm: f64,
127        /// Full width along `z`, nm, `> 0`.
128        width_z_nm: f64,
129    },
130}
131
132/// Where the cutoff is measured from.
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
134#[serde(rename_all = "kebab-case")]
135pub enum CutoffReferenceChoice {
136    /// [`CutoffReference::BandBottom`].
137    #[default]
138    BandBottom,
139    /// [`CutoffReference::VacuumLevel`].
140    VacuumLevel,
141}
142
143/// Escape rule at the faces.
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
145#[serde(rename_all = "kebab-case")]
146pub enum EscapeRuleChoice {
147    /// [`EscapeRule::BothFaces`].
148    #[default]
149    BothFaces,
150    /// [`EscapeRule::FrontOnly`].
151    FrontOnly,
152}
153
154/// Secondary-electron model.
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
156#[serde(rename_all = "kebab-case")]
157pub enum SecondaryChoice {
158    /// [`SecondaryModel::Off`].
159    #[default]
160    Off,
161    /// [`SecondaryModel::KieftBosch`].
162    KieftBosch,
163}
164
165/// Face model.
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
167#[serde(rename_all = "kebab-case")]
168pub enum BoundaryChoice {
169    /// [`BoundaryModel::Transparent`].
170    #[default]
171    Transparent,
172    /// [`BoundaryModel::StepBarrier`].
173    StepBarrier,
174}
175
176fn default_max_events() -> u64 {
177    TransportConfig::new(1.0).max_events
178}
179
180/// `[electron.transport]`: the [`TransportConfig`].
181#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
182#[serde(deny_unknown_fields)]
183pub struct TransportSpec {
184    /// Stopping cutoff, eV.
185    pub cutoff_ev: f64,
186    /// Where the cutoff is measured from.
187    #[serde(default)]
188    pub cutoff_reference: CutoffReferenceChoice,
189    /// What the faces of the target do.
190    #[serde(default)]
191    pub escape_rule: EscapeRuleChoice,
192    /// Collision and reflection cap per electron.
193    #[serde(default = "default_max_events")]
194    pub max_events: u64,
195    /// Secondary-electron generation.
196    #[serde(default)]
197    pub secondaries: SecondaryChoice,
198    /// Kieft-Bosch option: instantaneous momentum of the bound electron.
199    /// Default `true` with `"kieft-bosch"`; not allowed with `"off"`.
200    #[serde(default, skip_serializing_if = "Option::is_none")]
201    pub instantaneous_momentum: Option<bool>,
202    /// Kieft-Bosch option: deflect the primary to conserve momentum.
203    #[serde(default, skip_serializing_if = "Option::is_none")]
204    pub momentum_conservation: Option<bool>,
205    /// Face model.
206    #[serde(default)]
207    pub boundary: BoundaryChoice,
208    /// Step-barrier option: quantum-mechanical transmission. Default `true`
209    /// with `"step-barrier"`; not allowed with `"transparent"`.
210    #[serde(default, skip_serializing_if = "Option::is_none")]
211    pub quantum_transmission: Option<bool>,
212    /// Step-barrier option: refraction on transmission.
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub refraction: Option<bool>,
215}
216
217/// Elastic model. Only the Mott cross section from the radial-Dirac partial
218/// waves exists.
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
220#[serde(rename_all = "kebab-case")]
221pub enum ElasticModelChoice {
222    /// [`crate::electron::elastic`] and [`crate::electron::elastic::table`].
223    #[default]
224    Mott,
225}
226
227impl ElasticModelChoice {
228    /// The stable label (the input spelling).
229    pub fn label(self) -> &'static str {
230        match self {
231            Self::Mott => "mott",
232        }
233    }
234}
235
236/// Atomic potential of the elastic model.
237#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
238#[serde(rename_all = "kebab-case")]
239pub enum PotentialChoice {
240    /// The Thomas-Fermi-length Yukawa **stand-in**
241    /// ([`crate::electron::elastic::table::ThomasFermiYukawa`]).
242    ThomasFermiYukawa,
243    /// Salvat et al. (1987) DHFS
244    /// ([`crate::electron::elastic::table::SalvatDhfsTable`]), Table I
245    /// coefficients for Z = 1..92.
246    SalvatDhfs,
247}
248
249impl PotentialChoice {
250    /// The stable label.
251    pub fn label(self) -> &'static str {
252        match self {
253            Self::ThomasFermiYukawa => "thomas-fermi-yukawa",
254            Self::SalvatDhfs => "salvat-dhfs",
255        }
256    }
257}
258
259/// `[electron.elastic]`.
260#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
261#[serde(deny_unknown_fields)]
262pub struct ElasticSpec {
263    /// The elastic model.
264    #[serde(default)]
265    pub model: ElasticModelChoice,
266    /// The atomic potential (required: the only one that runs is a
267    /// stand-in, and the input must say so).
268    pub potential: PotentialChoice,
269    /// Furness-McCarthy exchange correction.
270    #[serde(default)]
271    pub exchange: bool,
272    /// Correlation-polarization correction, absent for off.
273    #[serde(default, skip_serializing_if = "Option::is_none")]
274    pub correlation_polarization: Option<CorrelationPolarizationSpec>,
275}
276
277/// One element's dipole polarizability and its source.
278#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
279#[serde(deny_unknown_fields)]
280pub struct PolarizabilitySpec {
281    /// Static dipole polarizability, bohr³.
282    pub bohr3: f64,
283    /// Citation of the value (required, non-blank).
284    pub source: String,
285}
286
287fn default_outer_radius() -> f64 {
288    crate::electron::elastic::corrections::DEFAULT_OUTER_RADIUS
289}
290
291/// `[electron.elastic.correlation_polarization]`.
292#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
293#[serde(deny_unknown_fields)]
294pub struct CorrelationPolarizationSpec {
295    /// `b_pol²` of the polarization cutoff; absent: Seltzer's
296    /// `(E - 50 eV)/(16 eV)`, which needs every table energy above 50 eV.
297    #[serde(default, skip_serializing_if = "Option::is_none")]
298    pub b_pol_squared: Option<f64>,
299    /// Radius beyond which the polarization tail is dropped, bohr.
300    #[serde(default = "default_outer_radius")]
301    pub outer_radius_bohr: f64,
302    /// Polarizability of every element of the target, by symbol.
303    pub polarizability: BTreeMap<String, PolarizabilitySpec>,
304}
305
306/// `[electron.inelastic]`.
307#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
308#[serde(deny_unknown_fields)]
309pub struct InelasticSpec {
310    /// `"penn-single-pole"`, `"penn-full"` or `"mermin-melf"`.
311    #[serde(default = "default_inelastic_model")]
312    pub model: String,
313    /// Fermi energy of the model, eV, for the inelastic tables of materials
314    /// without a band. Must be 0 if a material has a band: the table of a
315    /// material with a band takes the band's (#241).
316    #[serde(default)]
317    pub fermi_energy_ev: f64,
318}
319
320fn default_inelastic_model() -> String {
321    PennAlgorithm::SinglePole.label().to_string()
322}
323
324impl Default for InelasticSpec {
325    fn default() -> Self {
326        Self {
327            model: default_inelastic_model(),
328            fermi_energy_ev: 0.0,
329        }
330    }
331}
332
333fn default_min_energy() -> f64 {
334    DEFAULT_MIN_ENERGY_EV
335}
336
337fn default_points_per_decade() -> f64 {
338    DEFAULT_POINTS_PER_DECADE
339}
340
341/// `[electron.tables]`: the energy grid both tables of every layer share.
342#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
343#[serde(deny_unknown_fields)]
344pub struct TableGridSpec {
345    /// Lowest grid energy, eV.
346    #[serde(default = "default_min_energy")]
347    pub min_energy_ev: f64,
348    /// Highest grid energy, eV. Default: the beam energy (plus the largest
349    /// inner potential with the step barrier).
350    #[serde(default, skip_serializing_if = "Option::is_none")]
351    pub max_energy_ev: Option<f64>,
352    /// Minimum points per decade.
353    #[serde(default = "default_points_per_decade")]
354    pub points_per_decade: f64,
355}
356
357impl Default for TableGridSpec {
358    fn default() -> Self {
359        Self {
360            min_energy_ev: default_min_energy(),
361            max_energy_ev: None,
362            points_per_decade: default_points_per_decade(),
363        }
364    }
365}
366
367/// Band parameters of a material ([`BandStructure`]).
368#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
369#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
370pub enum BandSpec {
371    /// [`BandModel::Metal`].
372    Metal {
373        /// Fermi energy above the band bottom, eV.
374        fermi_ev: f64,
375        /// Work function, eV.
376        work_function_ev: f64,
377        /// Binding energy below the Fermi level of the electron a valence
378        /// event liberates, eV ([`BandStructure::with_valence_binding_ev`]);
379        /// absent: the Fermi level, the default.
380        #[serde(default, skip_serializing_if = "Option::is_none")]
381        valence_binding_ev: Option<f64>,
382        /// Source of the values.
383        provenance: String,
384    },
385    /// [`BandModel::Insulator`].
386    Insulator {
387        /// Valence band width, eV.
388        valence_band_width_ev: f64,
389        /// Band gap, eV.
390        band_gap_ev: f64,
391        /// Electron affinity, eV.
392        affinity_ev: f64,
393        /// Source of the values.
394        provenance: String,
395    },
396    /// [`BandStructure::free_electron_metal`]: the Fermi energy from the
397    /// free-electron relation and the material's atom density.
398    FreeElectronMetal {
399        /// Valence electrons per atom (a model choice).
400        valence_electrons_per_atom: f64,
401        /// Work function, eV.
402        work_function_ev: f64,
403        /// As for [`BandSpec::Metal`].
404        #[serde(default, skip_serializing_if = "Option::is_none")]
405        valence_binding_ev: Option<f64>,
406        /// Source of the work function and of the valence count (and of
407        /// `valence_binding_ev`, if given).
408        provenance: String,
409    },
410}
411
412/// A Fröhlich LO-phonon channel: explicit parameters, or a preset the library
413/// carries with its citation.
414#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
415#[serde(deny_unknown_fields)]
416pub struct PhononSpec {
417    /// `"sio2-63mev"` or `"sio2-153mev"` ([`FrohlichPhonon::sio2`]); then
418    /// only `temperature_k` may be given.
419    #[serde(default, skip_serializing_if = "Option::is_none")]
420    pub preset: Option<String>,
421    /// LO-phonon energy, eV.
422    #[serde(default, skip_serializing_if = "Option::is_none")]
423    pub hbar_omega_ev: Option<f64>,
424    /// Static relative permittivity.
425    #[serde(default, skip_serializing_if = "Option::is_none")]
426    pub eps_static: Option<f64>,
427    /// High-frequency relative permittivity.
428    #[serde(default, skip_serializing_if = "Option::is_none")]
429    pub eps_high_frequency: Option<f64>,
430    /// Lattice temperature, K.
431    pub temperature_k: f64,
432    /// Source of the values (required without a preset).
433    #[serde(default, skip_serializing_if = "Option::is_none")]
434    pub provenance: Option<String>,
435}
436
437/// A polaron-trapping channel ([`PolaronTrapping`]).
438#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
439#[serde(deny_unknown_fields)]
440pub struct PolaronSpec {
441    /// `C`, nm⁻¹.
442    pub c_per_nm: f64,
443    /// `γ`, eV⁻¹.
444    pub gamma_per_ev: f64,
445    /// Source of the values.
446    pub provenance: String,
447}
448
449fn default_mermin_oscillators() -> usize {
450    MerminFitOptions::default().n_oscillators
451}
452
453/// The `floor` of the default relative weighting of
454/// [`MerminFitOptions::default`].
455pub(super) fn default_relative_floor() -> f64 {
456    match MerminFitOptions::default().weighting {
457        FitWeighting::Relative { floor } => floor,
458        FitWeighting::Uniform => unreachable!("the default fit weighting is relative"),
459    }
460}
461
462/// Weighting of the residuals of the Mermin oscillator fit
463/// ([`FitWeighting`]).
464#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
465#[serde(rename_all = "kebab-case")]
466pub enum MerminWeightingChoice {
467    /// [`FitWeighting::Relative`] (the default).
468    #[default]
469    Relative,
470    /// [`FitWeighting::Uniform`].
471    Uniform,
472}
473
474/// `[electron.materials.<name>.mermin_fit]`: the options of the oscillator
475/// fit of the `mermin-melf` model to this material's optical ELF
476/// ([`MerminFitOptions`],
477/// [`crate::electron::inelastic::fit_mermin_oscillators`]). Absent: exactly
478/// [`MerminFitOptions::default`]. Only `n_oscillators` and `weighting` are
479/// exposed; the starting point and the iteration cap keep their defaults.
480#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
481#[serde(deny_unknown_fields)]
482pub struct MerminFitSpec {
483    /// Number of oscillators, `1..=MAX_OSCILLATORS` (16); default 3.
484    #[serde(default = "default_mermin_oscillators")]
485    pub oscillators: usize,
486    /// `"relative"` (default) or `"uniform"`.
487    #[serde(default)]
488    pub weighting: MerminWeightingChoice,
489    /// `floor` of the relative weighting, in `(0, 1]`; default 0.01. Not
490    /// allowed with `"uniform"`.
491    #[serde(default, skip_serializing_if = "Option::is_none")]
492    pub relative_floor: Option<f64>,
493}
494
495/// `[electron.materials.<name>.inner_shells]`: inner-shell ionisation
496/// channels of this material (single-pole Penn only). Each listed subshell of
497/// every element of the material becomes a channel with a hydrogenic optical
498/// ELF ([`crate::electron::inelastic::hydrogenic_shell_elfs`]) built from a
499/// binding-energy table ([`crate::electron::data::SubshellBindingTable`]).
500#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
501#[serde(deny_unknown_fields)]
502pub struct InnerShellsSpec {
503    /// Subshell labels: `"K"`, `"L1"`, `"L2"`, `"L3"` (the only ones with a
504    /// hydrogenic formula). Not empty, no repeats.
505    pub subshells: Vec<String>,
506    /// Path of a binding-energy table in the TOML form of
507    /// [`crate::electron::data::SubshellBindingTable::from_toml_str`],
508    /// relative to the input file's directory; its `provenance` is
509    /// required. Absent: the embedded EADL2017 table.
510    #[serde(default, skip_serializing_if = "Option::is_none")]
511    pub binding: Option<String>,
512}
513
514/// `[electron.materials.<name>]`: the electron data of one material.
515#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
516#[serde(deny_unknown_fields)]
517pub struct ElectronMaterialSpec {
518    /// Path of the optical ELF file ([`OpticalElf`] TOML form), relative to
519    /// the input file's directory.
520    pub optical_elf: String,
521    /// Band parameters; required with secondaries, the step barrier or the
522    /// vacuum-level cutoff.
523    #[serde(default, skip_serializing_if = "Option::is_none")]
524    pub band: Option<BandSpec>,
525    /// Fröhlich LO-phonon channel (polar insulators only).
526    #[serde(default, skip_serializing_if = "Option::is_none")]
527    pub phonon: Option<PhononSpec>,
528    /// Polaron trapping (polar insulators only).
529    #[serde(default, skip_serializing_if = "Option::is_none")]
530    pub polaron: Option<PolaronSpec>,
531    /// Options of the Mermin oscillator fit (`mermin-melf` only); absent:
532    /// [`MerminFitOptions::default`].
533    #[serde(default, skip_serializing_if = "Option::is_none")]
534    pub mermin_fit: Option<MerminFitSpec>,
535    /// Inner-shell ionisation channels (`single-pole` only); absent: none.
536    #[serde(default, skip_serializing_if = "Option::is_none")]
537    pub inner_shells: Option<InnerShellsSpec>,
538    /// Mean energy per electron-hole pair `E_pair`, with its source. Absent:
539    /// the report gives the ionisation energy of the material's layers only
540    /// and no pair count; no default is supplied.
541    #[serde(default, skip_serializing_if = "Option::is_none")]
542    pub e_pair: Option<EPairSpec>,
543}
544
545/// `[electron.materials.<name>.e_pair]`: the energy needed per electron-hole
546/// pair, a per-material input that must carry its citation (for example the
547/// Klein 1968 rule for a semiconductor, or a measured value for SiO2). Pairs
548/// per incident electron are the ionisation energy divided by it, a
549/// post-processing step; nothing in transport depends on it.
550#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
551#[serde(deny_unknown_fields)]
552pub struct EPairSpec {
553    /// `E_pair`, eV (positive).
554    pub ev: f64,
555    /// Where the value comes from (required, non-blank).
556    pub source: String,
557}
558
559/// A uniform binning in nm.
560#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
561#[serde(deny_unknown_fields)]
562pub struct BinsNm {
563    /// Lower edge, nm.
564    pub lo_nm: f64,
565    /// Upper edge, nm.
566    pub hi_nm: f64,
567    /// Number of bins.
568    pub bins: usize,
569}
570
571/// `[electron.tally.cartesian]`.
572#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
573#[serde(deny_unknown_fields)]
574pub struct CartesianSpec {
575    /// Depth bins.
576    pub x: BinsNm,
577    /// Lateral `y` bins.
578    pub y: BinsNm,
579    /// Lateral `z` bins.
580    pub z: BinsNm,
581}
582
583/// `[electron.tally.cylindrical]`.
584#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
585#[serde(deny_unknown_fields)]
586pub struct CylindricalSpec {
587    /// Radial bins (from `r >= 0`).
588    pub r: BinsNm,
589    /// Depth bins.
590    pub depth: BinsNm,
591}
592
593/// A PSF form to fit (`fits` of `[electron.tally.psf]`).
594#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
595#[serde(rename_all = "kebab-case")]
596pub enum PsfFitSpec {
597    /// Double Gaussian ([`PsfModel::DoubleGaussian`]).
598    Double,
599    /// Triple Gaussian ([`PsfModel::TripleGaussian`]).
600    Triple,
601}
602
603impl From<PsfFitSpec> for PsfModel {
604    fn from(f: PsfFitSpec) -> Self {
605        match f {
606            PsfFitSpec::Double => PsfModel::DoubleGaussian,
607            PsfFitSpec::Triple => PsfModel::TripleGaussian,
608        }
609    }
610}
611
612/// How the energy scale of a PSF fit is set (`normalization`).
613#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
614#[serde(rename_all = "kebab-case")]
615pub enum PsfNormalizationSpec {
616    /// Fixed to the slab energy ([`PsfNormalization::SlabTotal`]).
617    #[default]
618    SlabTotal,
619    /// A free parameter ([`PsfNormalization::Free`]).
620    Free,
621}
622
623impl From<PsfNormalizationSpec> for PsfNormalization {
624    fn from(n: PsfNormalizationSpec) -> Self {
625        match n {
626            PsfNormalizationSpec::SlabTotal => PsfNormalization::SlabTotal,
627            PsfNormalizationSpec::Free => PsfNormalization::Free,
628        }
629    }
630}
631
632pub(super) fn default_psf_fits() -> Vec<PsfFitSpec> {
633    vec![PsfFitSpec::Double, PsfFitSpec::Triple]
634}
635
636/// `[electron.tally.psf]`: the radial profile of the energy deposited in a
637/// depth slab, on log radial bins, and the PSF fits to it
638/// ([`crate::tally::psf`]).
639#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
640#[serde(deny_unknown_fields)]
641pub struct PsfSpec {
642    /// Lower depth of the slab, nm.
643    pub depth_lo_nm: f64,
644    /// Upper depth of the slab (excluded), nm.
645    pub depth_hi_nm: f64,
646    /// Outer edge of the central disc and inner edge of the first log bin, nm.
647    pub r_min_nm: f64,
648    /// Outer edge of the last bin, nm.
649    pub r_max_nm: f64,
650    /// Log bins between `r_min_nm` and `r_max_nm`.
651    pub bins: usize,
652    /// Forms to fit (`"double"`, `"triple"`); may be empty.
653    #[serde(default = "default_psf_fits")]
654    pub fits: Vec<PsfFitSpec>,
655    /// How the energy scale of the fits is set.
656    #[serde(default)]
657    pub normalization: PsfNormalizationSpec,
658}
659
660fn default_exposure_bins() -> usize {
661    200
662}
663fn default_exposure_quantiles() -> Vec<f64> {
664    vec![0.05, 0.5, 0.95]
665}
666fn default_exposure_tail() -> f64 {
667    1e-12
668}
669
670/// Beam profile of `[electron.tally.exposure]`.
671#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
672#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
673pub enum ExposureBeamSpec {
674    /// All electrons land on the axis.
675    Point,
676    /// Gaussian landing offset, `sigma_nm` per lateral axis.
677    Gaussian {
678        /// Standard deviation per axis, nm.
679        sigma_nm: f64,
680    },
681    /// Uniform disc.
682    UniformDisc {
683        /// Radius, nm.
684        radius_nm: f64,
685    },
686}
687
688/// Shape of a volume of `[electron.tally.exposure]`.
689#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
690#[serde(rename_all = "kebab-case")]
691pub enum ExposureShapeSpec {
692    /// Cylinder along the depth axis (needs `radius_nm`).
693    Cylinder,
694    /// Box centred on the offset (needs `half_y_nm` and `half_z_nm`).
695    Box,
696}
697
698/// One `[[electron.tally.exposure.volumes]]` entry.
699#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
700#[serde(deny_unknown_fields)]
701pub struct ExposureVolumeSpec {
702    /// Label in the outputs (unique).
703    pub name: String,
704    /// Shape.
705    pub shape: ExposureShapeSpec,
706    /// Cylinder radius, nm.
707    #[serde(default, skip_serializing_if = "Option::is_none")]
708    pub radius_nm: Option<f64>,
709    /// Box half width along `y`, nm.
710    #[serde(default, skip_serializing_if = "Option::is_none")]
711    pub half_y_nm: Option<f64>,
712    /// Box half width along `z`, nm.
713    #[serde(default, skip_serializing_if = "Option::is_none")]
714    pub half_z_nm: Option<f64>,
715    /// Lower depth, nm.
716    pub depth_lo_nm: f64,
717    /// Upper depth (excluded), nm.
718    pub depth_hi_nm: f64,
719    /// Lateral centre `y`, nm.
720    #[serde(default)]
721    pub offset_y_nm: f64,
722    /// Lateral centre `z`, nm.
723    #[serde(default)]
724    pub offset_z_nm: f64,
725}
726
727/// `[electron.tally.exposure]`: the per-electron deposit in target volumes
728/// and the compound-Poisson distribution of a finite exposure
729/// ([`crate::tally::exposure`]).
730#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
731#[serde(deny_unknown_fields)]
732pub struct ExposureSpec {
733    /// Mean number of incident electrons.
734    pub lambda: f64,
735    /// Beam profile.
736    pub beam: ExposureBeamSpec,
737    /// Target volumes (a neighbour is another volume at its offset).
738    pub volumes: Vec<ExposureVolumeSpec>,
739    /// Histogram bins of the per-electron deposit.
740    #[serde(default = "default_exposure_bins")]
741    pub bins: usize,
742    /// Upper edge of the histogram, eV. Default: the beam energy.
743    #[serde(default, skip_serializing_if = "Option::is_none")]
744    pub x_max_ev: Option<f64>,
745    /// Probabilities whose quantiles of the exposure energy are reported.
746    #[serde(default = "default_exposure_quantiles")]
747    pub quantiles: Vec<f64>,
748    /// Thresholds `a`, eV, for `P(S < a)`.
749    #[serde(default)]
750    pub below_ev: Vec<f64>,
751    /// Thresholds `b`, eV, for `P(S > b)`.
752    #[serde(default)]
753    pub above_ev: Vec<f64>,
754    /// Poisson tail mass dropped from the convolution.
755    #[serde(default = "default_exposure_tail")]
756    pub tail_tolerance: f64,
757}
758
759fn default_split() -> f64 {
760    SE_BSE_SPLIT_EV
761}
762fn default_energy_bins() -> usize {
763    100
764}
765fn default_polar_max() -> f64 {
766    90.0
767}
768fn default_polar_bins() -> usize {
769    18
770}
771
772/// `[electron.tally]`.
773#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
774#[serde(deny_unknown_fields)]
775pub struct ElectronTallySpec {
776    /// SE/BSE split, eV.
777    #[serde(default = "default_split")]
778    pub se_bse_split_ev: f64,
779    /// Upper edge of the escape-energy spectra, eV (from 0). Default: the
780    /// beam energy.
781    #[serde(default, skip_serializing_if = "Option::is_none")]
782    pub escape_energy_max_ev: Option<f64>,
783    /// Escape-energy bins.
784    #[serde(default = "default_energy_bins")]
785    pub escape_energy_bins: usize,
786    /// Upper edge of the escape polar-angle spectra, degrees from the
787    /// outward normal (from 0, at most 180).
788    #[serde(default = "default_polar_max")]
789    pub escape_polar_max_deg: f64,
790    /// Escape polar-angle bins.
791    #[serde(default = "default_polar_bins")]
792    pub escape_polar_bins: usize,
793    /// Cartesian deposition grid.
794    #[serde(default, skip_serializing_if = "Option::is_none")]
795    pub cartesian: Option<CartesianSpec>,
796    /// Cylindrical deposition grid.
797    #[serde(default, skip_serializing_if = "Option::is_none")]
798    pub cylindrical: Option<CylindricalSpec>,
799    /// Radial profile of a depth slab and its PSF fits.
800    #[serde(default, skip_serializing_if = "Option::is_none")]
801    pub psf: Option<PsfSpec>,
802    /// Dose per layer, depth-dose profile and interface transmission.
803    #[serde(default, skip_serializing_if = "Option::is_none")]
804    pub dose: Option<DoseSpec>,
805    /// Exposure statistics of small volumes.
806    #[serde(default, skip_serializing_if = "Option::is_none")]
807    pub exposure: Option<ExposureSpec>,
808    /// Voxel energy-deposition grid, written as `damage_export.csv` (the
809    /// ion `[tally.voxel]` table, same fields). The electron tally has no
810    /// species axis, so the per-species defect columns are empty.
811    #[serde(default, skip_serializing_if = "Option::is_none")]
812    pub voxel: Option<VoxelTallySpec>,
813}
814
815/// `[electron.tally.dose]`: dose in each layer per unit incident fluence
816/// (always on once the table is present), optional depth-dose profile and
817/// interface transmission.
818#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
819#[serde(deny_unknown_fields)]
820pub struct DoseSpec {
821    /// Uniform depth bins per layer; omit for no depth-dose profile.
822    #[serde(default, skip_serializing_if = "Option::is_none")]
823    pub depth_bins_per_layer: Option<usize>,
824    /// Width of the refined depth bands next to each interface, nm.
825    #[serde(default, skip_serializing_if = "Option::is_none")]
826    pub refine_width_nm: Option<f64>,
827    /// Bins in each refined band (default 10 when a width is given).
828    #[serde(default, skip_serializing_if = "Option::is_none")]
829    pub refine_bins: Option<usize>,
830    /// Depth binned in a semi-infinite last layer, nm.
831    #[serde(default, skip_serializing_if = "Option::is_none")]
832    pub substrate_depth_nm: Option<f64>,
833    /// Count crossings of each interface, with energy spectra.
834    #[serde(default)]
835    pub transmission: bool,
836}
837
838impl Default for ElectronTallySpec {
839    fn default() -> Self {
840        Self {
841            se_bse_split_ev: default_split(),
842            escape_energy_max_ev: None,
843            escape_energy_bins: default_energy_bins(),
844            escape_polar_max_deg: default_polar_max(),
845            escape_polar_bins: default_polar_bins(),
846            cartesian: None,
847            cylindrical: None,
848            psf: None,
849            dose: None,
850            exposure: None,
851            voxel: None,
852        }
853    }
854}
855
856/// `[run]` of an electron run.
857#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
858#[serde(deny_unknown_fields)]
859pub struct ElectronRunSpec {
860    /// Number of primary electrons (histories).
861    pub histories: u64,
862    /// Run seed.
863    pub seed: u64,
864    /// Worker threads (not echoed; never changes results).
865    #[serde(default, skip_serializing_if = "Option::is_none")]
866    pub threads: Option<usize>,
867}