Skip to main content

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}