1use super::endf6;
2use super::{checked_text, invalid, parse_error, read_text, ElectronDataError, Result};
3use serde::{Deserialize, Serialize};
4use std::path::Path;
5
6const EADL2017_TOML: &str = include_str!("../eadl2017_binding.toml");
9
10const SUBSHELLS: [(&str, &str, u8); 39] = [
17 ("K", "1s1/2", 1),
18 ("L1", "2s1/2", 1),
19 ("L2", "2p1/2", 1),
20 ("L3", "2p3/2", 3),
21 ("M1", "3s1/2", 1),
22 ("M2", "3p1/2", 1),
23 ("M3", "3p3/2", 3),
24 ("M4", "3d3/2", 3),
25 ("M5", "3d5/2", 5),
26 ("N1", "4s1/2", 1),
27 ("N2", "4p1/2", 1),
28 ("N3", "4p3/2", 3),
29 ("N4", "4d3/2", 3),
30 ("N5", "4d5/2", 5),
31 ("N6", "4f5/2", 5),
32 ("N7", "4f7/2", 7),
33 ("O1", "5s1/2", 1),
34 ("O2", "5p1/2", 1),
35 ("O3", "5p3/2", 3),
36 ("O4", "5d3/2", 3),
37 ("O5", "5d5/2", 5),
38 ("O6", "5f5/2", 5),
39 ("O7", "5f7/2", 7),
40 ("O8", "5g7/2", 7),
41 ("O9", "5g9/2", 9),
42 ("P1", "6s1/2", 1),
43 ("P2", "6p1/2", 1),
44 ("P3", "6p3/2", 3),
45 ("P4", "6d3/2", 3),
46 ("P5", "6d5/2", 5),
47 ("P6", "6f5/2", 5),
48 ("P7", "6f7/2", 7),
49 ("P8", "6g7/2", 7),
50 ("P9", "6g9/2", 9),
51 ("P10", "6h9/2", 9),
52 ("P11", "6h11/2", 11),
53 ("Q1", "7s1/2", 1),
54 ("Q2", "7p1/2", 1),
55 ("Q3", "7p3/2", 3),
56];
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
64#[serde(try_from = "u8", into = "u8")]
65pub struct Subshell(u8);
66
67impl Subshell {
68 pub const K: Subshell = Subshell(1);
70
71 pub const MAX_DESIGNATOR: u8 = 39;
73
74 pub fn new(designator: u8) -> Result<Self> {
76 if (1..=Self::MAX_DESIGNATOR).contains(&designator) {
77 Ok(Self(designator))
78 } else {
79 Err(invalid(
80 "subshell designator",
81 format!("{designator} is not in 1..={}", Self::MAX_DESIGNATOR),
82 ))
83 }
84 }
85
86 pub fn from_label(label: &str) -> Option<Self> {
88 SUBSHELLS
89 .iter()
90 .position(|s| s.0 == label)
91 .map(|i| Self(i as u8 + 1))
92 }
93
94 pub fn designator(self) -> u8 {
96 self.0
97 }
98
99 pub fn label(self) -> &'static str {
101 SUBSHELLS[usize::from(self.0) - 1].0
102 }
103
104 pub fn orbital(self) -> &'static str {
106 SUBSHELLS[usize::from(self.0) - 1].1
107 }
108
109 pub fn capacity(self) -> u8 {
111 SUBSHELLS[usize::from(self.0) - 1].2 + 1
112 }
113}
114
115impl TryFrom<u8> for Subshell {
116 type Error = ElectronDataError;
117 fn try_from(d: u8) -> Result<Self> {
118 Self::new(d)
119 }
120}
121
122impl From<Subshell> for u8 {
123 fn from(s: Subshell) -> u8 {
124 s.0
125 }
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
130#[serde(try_from = "RawShellBinding", into = "RawShellBinding")]
131pub struct ShellBinding {
132 subshell: Subshell,
133 binding_energy_ev: f64,
134 occupancy: f64,
135}
136
137#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
138#[serde(deny_unknown_fields)]
139struct RawShellBinding {
140 subshell: Subshell,
141 binding_energy_ev: f64,
142 occupancy: f64,
143}
144
145impl TryFrom<RawShellBinding> for ShellBinding {
146 type Error = ElectronDataError;
147 fn try_from(r: RawShellBinding) -> Result<Self> {
148 Self::new(r.subshell, r.binding_energy_ev, r.occupancy)
149 }
150}
151
152impl From<ShellBinding> for RawShellBinding {
153 fn from(s: ShellBinding) -> Self {
154 Self {
155 subshell: s.subshell,
156 binding_energy_ev: s.binding_energy_ev,
157 occupancy: s.occupancy,
158 }
159 }
160}
161
162impl ShellBinding {
163 pub fn new(subshell: Subshell, binding_energy_ev: f64, occupancy: f64) -> Result<Self> {
168 if !(binding_energy_ev.is_finite() && binding_energy_ev > 0.0) {
169 return Err(invalid(
170 "binding energy",
171 format!(
172 "{} binding energy must be finite and positive, got {binding_energy_ev} eV",
173 subshell.label()
174 ),
175 ));
176 }
177 let cap = f64::from(subshell.capacity());
178 if !(occupancy.is_finite() && occupancy > 0.0 && occupancy <= cap) {
179 return Err(invalid(
180 "occupancy",
181 format!(
182 "{} occupancy must be in (0, {cap}], got {occupancy}",
183 subshell.label()
184 ),
185 ));
186 }
187 Ok(Self {
188 subshell,
189 binding_energy_ev,
190 occupancy,
191 })
192 }
193
194 pub fn subshell(&self) -> Subshell {
196 self.subshell
197 }
198
199 pub fn binding_energy_ev(&self) -> f64 {
201 self.binding_energy_ev
202 }
203
204 pub fn occupancy(&self) -> f64 {
206 self.occupancy
207 }
208}
209
210pub const OCCUPANCY_SUM_TOLERANCE: f64 = 1.0e-3;
213
214#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
225#[serde(try_from = "RawAtomBindings", into = "RawAtomBindings")]
226pub struct AtomBindings {
227 z: u8,
228 shells: Vec<ShellBinding>,
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize)]
232#[serde(deny_unknown_fields)]
233struct RawAtomBindings {
234 z: u8,
235 shells: Vec<ShellBinding>,
236}
237
238impl TryFrom<RawAtomBindings> for AtomBindings {
239 type Error = ElectronDataError;
240 fn try_from(r: RawAtomBindings) -> Result<Self> {
241 Self::new(r.z, r.shells)
242 }
243}
244
245impl From<AtomBindings> for RawAtomBindings {
246 fn from(a: AtomBindings) -> Self {
247 Self {
248 z: a.z,
249 shells: a.shells,
250 }
251 }
252}
253
254impl AtomBindings {
255 pub fn new(z: u8, shells: Vec<ShellBinding>) -> Result<Self> {
258 if crate::elements::element(z).is_none() {
259 return Err(invalid(
260 "atomic number",
261 format!("Z = {z} is not in 1..={}", crate::elements::NUM_ELEMENTS),
262 ));
263 }
264 if shells.is_empty() {
265 return Err(invalid("subshells", format!("Z = {z} has no subshells")));
266 }
267 for w in shells.windows(2) {
268 let (a, b) = (w[0].subshell, w[1].subshell);
269 if a == b {
270 return Err(invalid(
271 "subshells",
272 format!("Z = {z} lists subshell {} twice", a.label()),
273 ));
274 }
275 if b < a {
276 return Err(invalid(
277 "subshells",
278 format!(
279 "Z = {z}: subshells must be in increasing designator order, got {} after {}",
280 b.label(),
281 a.label()
282 ),
283 ));
284 }
285 }
286 let electrons: f64 = shells.iter().map(|s| s.occupancy).sum();
287 if (electrons - f64::from(z)).abs() > OCCUPANCY_SUM_TOLERANCE {
288 return Err(invalid(
289 "occupancy",
290 format!("Z = {z}: occupancies sum to {electrons}, not {z} (neutral atom)"),
291 ));
292 }
293 Ok(Self { z, shells })
294 }
295
296 pub fn z(&self) -> u8 {
298 self.z
299 }
300
301 pub fn shells(&self) -> &[ShellBinding] {
303 &self.shells
304 }
305
306 pub fn shell(&self, subshell: Subshell) -> Option<&ShellBinding> {
308 self.shells
309 .binary_search_by_key(&subshell, |s| s.subshell)
310 .ok()
311 .map(|i| &self.shells[i])
312 }
313
314 pub fn binding_energy_ev(&self, subshell: Subshell) -> Option<f64> {
316 self.shell(subshell).map(|s| s.binding_energy_ev)
317 }
318}
319
320#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
332#[serde(try_from = "RawSubshellBindingTable", into = "RawSubshellBindingTable")]
333pub struct SubshellBindingTable {
334 provenance: String,
335 atoms: Vec<AtomBindings>,
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize)]
339#[serde(deny_unknown_fields)]
340struct RawSubshellBindingTable {
341 provenance: Option<String>,
342 atoms: Vec<AtomBindings>,
343}
344
345impl TryFrom<RawSubshellBindingTable> for SubshellBindingTable {
346 type Error = ElectronDataError;
347 fn try_from(r: RawSubshellBindingTable) -> Result<Self> {
348 let provenance = checked_text("provenance", r.provenance.as_deref())?;
349 Self::new(provenance, r.atoms)
350 }
351}
352
353impl From<SubshellBindingTable> for RawSubshellBindingTable {
354 fn from(t: SubshellBindingTable) -> Self {
355 Self {
356 provenance: Some(t.provenance),
357 atoms: t.atoms,
358 }
359 }
360}
361
362impl SubshellBindingTable {
363 pub fn new(provenance: impl Into<String>, atoms: Vec<AtomBindings>) -> Result<Self> {
365 let provenance = checked_text("provenance", Some(&provenance.into()))?;
366 if atoms.is_empty() {
367 return Err(invalid("binding table", "has no atoms"));
368 }
369 if let Some(w) = atoms.windows(2).find(|w| w[1].z <= w[0].z) {
370 return Err(invalid(
371 "binding table",
372 format!(
373 "atoms must be in strictly increasing Z (no duplicates), got Z = {} after Z = {}",
374 w[1].z, w[0].z
375 ),
376 ));
377 }
378 Ok(Self { provenance, atoms })
379 }
380
381 pub fn provenance(&self) -> &str {
383 &self.provenance
384 }
385
386 pub fn atoms(&self) -> &[AtomBindings] {
388 &self.atoms
389 }
390
391 pub fn atom(&self, z: u8) -> Option<&AtomBindings> {
393 self.atoms
394 .binary_search_by_key(&z, |a| a.z)
395 .ok()
396 .map(|i| &self.atoms[i])
397 }
398
399 pub fn missing(&self, range: std::ops::RangeInclusive<u8>) -> Vec<u8> {
401 range.filter(|&z| self.atom(z).is_none()).collect()
402 }
403
404 pub fn require_coverage(&self, range: std::ops::RangeInclusive<u8>) -> Result<()> {
407 let (lo, hi) = (*range.start(), *range.end());
408 let missing = self.missing(range);
409 if missing.is_empty() {
410 Ok(())
411 } else {
412 Err(invalid(
413 "binding table coverage",
414 format!("Z = {lo}..={hi} required; missing {missing:?}"),
415 ))
416 }
417 }
418
419 pub fn from_toml_str(text: &str) -> Result<Self> {
428 let raw: RawSubshellBindingTable = toml::from_str(text).map_err(parse_error)?;
429 Self::try_from(raw)
430 }
431
432 pub fn to_toml_string(&self) -> Result<String> {
434 toml::to_string(self).map_err(|e| ElectronDataError::Parse(e.to_string()))
435 }
436
437 pub fn eadl2017() -> Self {
448 Self::from_toml_str(EADL2017_TOML).expect("the embedded EADL2017 table is valid")
449 }
450
451 pub fn from_endf6_mf28_str(text: &str, provenance: &str) -> Result<Self> {
468 let provenance = checked_text("provenance", Some(provenance))?;
469 let atoms = endf6::read_mf28(text)?;
470 Self::new(provenance, atoms)
471 }
472
473 pub fn from_endf6_mf28_file(path: impl AsRef<Path>, provenance: &str) -> Result<Self> {
475 Self::from_endf6_mf28_str(&read_text(path.as_ref())?, provenance)
476 }
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482
483 #[test]
484 fn subshell_table_matches_endf_mt_numbering() {
485 assert_eq!(Subshell::K.label(), "K");
487 assert_eq!(Subshell::new(4).unwrap().label(), "L3");
488 assert_eq!(Subshell::new(4).unwrap().orbital(), "2p3/2");
489 assert_eq!(Subshell::new(39).unwrap().label(), "Q3");
490 assert_eq!(Subshell::new(16).unwrap().capacity(), 8); assert_eq!(Subshell::from_label("P11").unwrap().designator(), 36);
492 assert!(Subshell::new(0).is_err());
493 assert!(Subshell::new(40).is_err());
494 for d in 1..=Subshell::MAX_DESIGNATOR {
496 let s = Subshell::new(d).unwrap();
497 let j2: u8 = s.orbital().split('/').next().unwrap()[2..].parse().unwrap();
498 assert_eq!(s.capacity(), j2 + 1, "{}", s.label());
499 }
500 }
501}