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}