xtalforge_core/structure/properties.rs
1//! Properties of the whole cell: contents, formula, mass, density, Z and charge.
2
3use super::{Error, Site, Structure, element_by_symbol};
4
5impl Structure {
6 /// Atoms of the cell as `(element, occupancy, charge)`: the P1 expansion for
7 /// a structure with symmetry operations, the sites themselves for a P1
8 /// structure or when the expansion fails.
9 fn cell_atoms(&self) -> Vec<(String, f64, f64)> {
10 if !self.is_p1()
11 && let Ok(p1) = self.p1()
12 {
13 return p1
14 .atoms
15 .iter()
16 .map(|atom| {
17 let charge = self
18 .sites
19 .get(atom.site_idx)
20 .and_then(Site::charge)
21 .unwrap_or(0.0);
22 (atom.element.clone(), atom.occupancy, charge)
23 })
24 .collect();
25 }
26 self.sites
27 .iter()
28 .map(|site| {
29 (
30 site.element().to_owned(),
31 site.occupancy(),
32 site.charge().unwrap_or(0.0),
33 )
34 })
35 .collect()
36 }
37
38 /// Cell contents as `(element, occupancy sum)` in first-appearance order,
39 /// counted on the whole cell (the P1 expansion when the structure has
40 /// symmetry operations).
41 #[must_use]
42 pub fn cell_contents(&self) -> Vec<(String, f64)> {
43 let mut contents: Vec<(String, f64)> = Vec::new();
44 for (element, occupancy, _) in self.cell_atoms() {
45 if let Some(entry) = contents.iter_mut().find(|(name, _)| *name == element) {
46 entry.1 += occupancy;
47 } else {
48 contents.push((element, occupancy));
49 }
50 }
51 contents
52 }
53
54 /// Formula string, for example `Fe2 O3` or `C0.5`.
55 #[must_use]
56 pub fn formula(&self) -> String {
57 self.cell_contents()
58 .iter()
59 .map(|(element, count)| {
60 if (*count - count.round()).abs() < 1e-9 {
61 let count = count.round() as u64;
62 if count == 1 {
63 element.clone()
64 } else {
65 format!("{element}{count}")
66 }
67 } else {
68 format!("{element}{count:.3}")
69 }
70 })
71 .collect::<Vec<_>>()
72 .join(" ")
73 }
74
75 /// Mass of the whole cell contents in unified atomic mass units.
76 ///
77 /// # Errors
78 ///
79 /// Returns an error if a site element is missing from the element table.
80 pub fn cell_mass(&self) -> Result<f64, Error> {
81 let mut mass = 0.0;
82 for (element, occupancy, _) in self.cell_atoms() {
83 mass += occupancy * element_by_symbol(&element)?.mass;
84 }
85 Ok(mass)
86 }
87
88 /// Molar mass of the cell contents in g/mol.
89 ///
90 /// # Errors
91 ///
92 /// Returns the errors of [`Structure::cell_mass`].
93 pub fn molar_mass(&self) -> Result<f64, Error> {
94 self.cell_mass()
95 }
96
97 /// Density in g/cm3 of the whole cell (the P1 expansion when the structure
98 /// has symmetry operations).
99 ///
100 /// # Errors
101 ///
102 /// Returns the errors of [`Structure::cell_mass`].
103 pub fn density(&self) -> Result<f64, Error> {
104 Ok(self.cell_mass()? * 1.660_539_066_60 / self.lattice.volume())
105 }
106
107 /// Number of formula units in the cell: the greatest common divisor of the
108 /// element counts of the whole cell, or 1 when a count is not an integer.
109 #[must_use]
110 pub fn z(&self) -> u32 {
111 let mut divisor: u64 = 0;
112 for (_, count) in self.cell_contents() {
113 if count < 0.5 || (count - count.round()).abs() > 1e-6 {
114 return 1;
115 }
116 let mut a = divisor;
117 let mut b = count.round() as u64;
118 while b != 0 {
119 (a, b) = (b, a % b);
120 }
121 divisor = a;
122 }
123 u32::try_from(divisor.max(1)).unwrap_or(1)
124 }
125
126 /// Sum of `occupancy * charge` over the atoms of the whole cell.
127 #[must_use]
128 pub fn total_charge(&self) -> f64 {
129 self.cell_atoms()
130 .iter()
131 .map(|(_, occupancy, charge)| occupancy * charge)
132 .sum()
133 }
134
135 /// Charge balance, the same as [`Structure::total_charge`].
136 #[must_use]
137 pub fn charge_balance(&self) -> f64 {
138 self.total_charge()
139 }
140}