Skip to main content

lindhard/
input.rs

1//! The run description: one TOML document describing the beam, the layered
2//! target and its materials, the physics choices and the run size.
3//!
4//! The same types are serialized back into the output metadata
5//! (`summary.json`), so a result carries the input that produced it. Every
6//! table uses `deny_unknown_fields`, so a misspelt key is an error naming the
7//! key rather than a silently ignored setting. Defaults are filled in on
8//! parsing, so the echoed input shows every choice explicitly.
9//!
10//! The schema is documented with an example in `docs/cli.md`. In short:
11//!
12//! ```toml
13//! [beam]
14//! ion = "B"              # element symbol; mass_amu optional
15//! energy_ev = 5000.0
16//! tilt_deg = 7.0          # polar angle from the surface normal
17//!
18//! [target]
19//! substrate = "Si"        # a name from [materials], or an element symbol
20//!
21//! [physics]
22//! primary_cutoff_ev = 5.0
23//! recoil_cutoff_ev = 2.0
24//! [physics.energies.Si]
25//! e_d_ev = 15.0
26//!
27//! [run]
28//! ions = 1000
29//! seed = 1
30//! ```
31//!
32//! [`Input::resolve`] validates the whole description and turns it into the
33//! engine's types ([`Resolved`]); [`Resolved::models`] lists every model in
34//! use with its citation, for the output metadata.
35//!
36//! An electron run is described by a different document, with an
37//! `[electron]` table and the same `[materials]` and `[target]`; its schema is
38//! [`electron`].
39
40use std::collections::BTreeMap;
41
42use serde::{Deserialize, Serialize};
43
44use crate::material::MaterialSpec;
45
46pub mod electron;
47mod resolve;
48mod resolved;
49mod schema;
50#[cfg(test)]
51mod tests;
52
53pub(crate) use resolve::divergence_from_spec;
54pub(crate) use resolved::resolve_material;
55pub use resolved::{
56    stopping_model, ModelInfo, Resolved, ResolvedCrystal, ResolvedLayer, TABLE_SPEC,
57};
58pub use schema::*;
59
60/// Errors from reading or validating an [`Input`].
61#[derive(Debug, Clone, PartialEq, thiserror::Error)]
62pub enum InputError {
63    /// The document is not valid TOML or does not match the schema (unknown
64    /// key, wrong type, missing required key). The message from the parser
65    /// names the key and the line.
66    #[error("{0}")]
67    Parse(String),
68    /// A value is out of range or inconsistent. `field` is the dotted path of
69    /// the offending key, e.g. `target.layers[1].thickness_nm`.
70    #[error("{field}: {message}")]
71    Invalid {
72        /// Dotted path of the key.
73        field: String,
74        /// What is wrong with it.
75        message: String,
76    },
77}
78
79fn invalid(field: impl Into<String>, message: impl Into<String>) -> InputError {
80    InputError::Invalid {
81        field: field.into(),
82        message: message.into(),
83    }
84}
85
86/// The whole run description.
87#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
88#[serde(deny_unknown_fields)]
89pub struct Input {
90    /// The incident beam.
91    pub beam: BeamSpec,
92    /// Named materials, referenced by name from the target layers.
93    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
94    pub materials: BTreeMap<String, MaterialSpec>,
95    /// The layered target.
96    pub target: TargetSpec,
97    /// Physics model choices and cutoffs.
98    pub physics: PhysicsSpec,
99    /// User-supplied electronic stopping tables (optional).
100    #[serde(default, skip_serializing_if = "StoppingSpec::is_empty")]
101    pub stopping: StoppingSpec,
102    /// Run size, seed and threads.
103    pub run: RunSpec,
104    /// What the run records.
105    #[serde(default)]
106    pub tally: TallySpec,
107    /// Fluence-dependent target (optional). Absent: the static run.
108    #[serde(default, skip_serializing_if = "Option::is_none")]
109    pub dynamic: Option<DynamicSpec>,
110    /// Cubic crystal assignments to stack layers (optional). Absent: every
111    /// layer is amorphous.
112    #[serde(default, skip_serializing_if = "Vec::is_empty")]
113    pub crystal: Vec<CrystalSpec>,
114}
115
116impl Input {
117    /// Parse a TOML document. Schema errors (unknown or missing keys, wrong
118    /// types) are reported with the key and line; values are checked by
119    /// [`Input::resolve`].
120    pub fn from_toml_str(text: &str) -> Result<Self, InputError> {
121        toml::from_str(text).map_err(|e| InputError::Parse(e.to_string()))
122    }
123
124    /// The input as a TOML document in the same schema [`Input::from_toml_str`]
125    /// reads: parsing the text gives back an equal [`Input`].
126    pub fn to_toml_string(&self) -> Result<String, InputError> {
127        toml::to_string(self).map_err(|e| InputError::Parse(e.to_string()))
128    }
129
130    /// The input as echoed into output metadata: defaults filled in, and
131    /// `run.threads` removed (it does not affect results).
132    pub fn echo(&self) -> Input {
133        let mut e = self.clone();
134        e.run.threads = None;
135        if e.physics.screening_length.is_none() {
136            e.physics.screening_length = Some(LengthChoice::from_length(
137                e.physics.potential.screening().default_length(),
138            ));
139        }
140        e
141    }
142}