Skip to main content

xtalforge_core/
supercell.rs

1//! Supercells and origin shifts (Task 1.7).
2
3use crate::limits::{Limits, check_supercell};
4use crate::spatial::{FractionalGrid, perpendicular_widths};
5use crate::{Error, Lattice, Site, Structure, StructureBuilder};
6
7impl Structure {
8    /// Builds an `na x nb x nc` supercell as a P1 structure.
9    ///
10    /// # Errors
11    ///
12    /// Returns [`Error::InvalidInput`] for zero repeats, or
13    /// [`Error::LimitExceeded`] when the estimate exceeds the hard limit.
14    pub fn supercell(&self, na: u32, nb: u32, nc: u32) -> Result<Self, Error> {
15        self.supercell_with_limits(na, nb, nc, &Limits::default())
16    }
17
18    /// Builds an `na x nb x nc` supercell with an explicit limits profile.
19    ///
20    /// # Errors
21    ///
22    /// Returns [`Error::InvalidInput`] for zero repeats, or
23    /// [`Error::LimitExceeded`] when the estimate exceeds the profile's hard
24    /// limit.
25    pub fn supercell_with_limits(
26        &self,
27        na: u32,
28        nb: u32,
29        nc: u32,
30        limits: &Limits,
31    ) -> Result<Self, Error> {
32        if na == 0 || nb == 0 || nc == 0 {
33            return Err(Error::InvalidInput(
34                "supercell repeats must be positive".to_owned(),
35            ));
36        }
37        self.supercell_matrix_with_limits(
38            [
39                [f64::from(na), 0.0, 0.0],
40                [0.0, f64::from(nb), 0.0],
41                [0.0, 0.0, f64::from(nc)],
42            ],
43            limits,
44        )
45    }
46
47    /// Builds a supercell from a positive integer transformation matrix.
48    ///
49    /// Rows of `p` are the new vectors in units of the old ones.
50    ///
51    /// # Errors
52    ///
53    /// Returns [`Error::InvalidInput`] for non-integer, singular or negative
54    /// determinant matrices, or [`Error::LimitExceeded`] for a refused size.
55    pub fn supercell_matrix(&self, p: [[f64; 3]; 3]) -> Result<Self, Error> {
56        self.supercell_matrix_with_limits(p, &Limits::default())
57    }
58
59    /// Builds a supercell with an explicit limits profile.
60    ///
61    /// # Errors
62    ///
63    /// Returns [`Error::InvalidInput`] for non-integer, singular or negative
64    /// determinant matrices, or [`Error::LimitExceeded`] for a refused size.
65    pub fn supercell_matrix_with_limits(
66        &self,
67        p: [[f64; 3]; 3],
68        limits: &Limits,
69    ) -> Result<Self, Error> {
70        for value in p.iter().flatten() {
71            if !value.is_finite() || (value - value.round()).abs() > 1e-9 {
72                return Err(Error::InvalidInput(
73                    "supercell matrix must contain integers".to_owned(),
74                ));
75            }
76        }
77        let expanded = if self.is_p1() {
78            None
79        } else {
80            Some(self.as_p1()?)
81        };
82        let source = expanded.as_ref().unwrap_or(self);
83        let verdict = check_supercell(limits, source.sites().len() as u64, p)?;
84        if verdict.refused() {
85            return Err(Error::LimitExceeded(verdict));
86        }
87        let lattice = source.lattice().transform(p)?;
88        let inverse = Lattice::new(p)?.inverse();
89        let max_inverse = inverse
90            .iter()
91            .flatten()
92            .map(|value| value.abs())
93            .fold(0.0, f64::max);
94        let range = (max_inverse * 4.0).ceil() as i64 + 2;
95        let widths = perpendicular_widths(&lattice)?;
96        let mut grid = FractionalGrid::new(widths, 1e-6);
97        let mut sites: Vec<Site> = Vec::new();
98        for site in source.sites() {
99            for i in -range..=range {
100                for j in -range..=range {
101                    for k in -range..=range {
102                        let candidate = [
103                            site.frac()[0] + i as f64,
104                            site.frac()[1] + j as f64,
105                            site.frac()[2] + k as f64,
106                        ];
107                        let frac = multiply_row(candidate, inverse);
108                        if !frac.iter().all(|value| (-1e-9..1.0).contains(value)) {
109                            continue;
110                        }
111                        let mut duplicate = false;
112                        grid.for_each_candidate(site.element(), frac, |existing_index| {
113                            let Some(existing) = sites.get(existing_index) else {
114                                return true;
115                            };
116                            if lattice.distance(existing.frac(), frac) <= 1e-6 {
117                                duplicate = true;
118                                return false;
119                            }
120                            true
121                        });
122                        if duplicate {
123                            continue;
124                        }
125                        let index = sites.len();
126                        sites.push(site.clone().with_frac(frac)?);
127                        grid.insert(site.element(), frac, index);
128                    }
129                }
130            }
131        }
132        let mut builder = StructureBuilder::new(source.name(), lattice);
133        for site in sites {
134            builder = builder.site(site);
135        }
136        for (key, value) in source.metadata() {
137            builder = builder.metadata(key.clone(), value.clone());
138        }
139        builder.build()
140    }
141
142    /// Shifts the origin by a fractional vector and rewraps the sites.
143    ///
144    /// # Errors
145    ///
146    /// Returns [`Error::InvalidInput`] for non-finite shifts.
147    pub fn origin_shift(&self, delta: [f64; 3]) -> Result<Self, Error> {
148        if !delta.iter().all(|value| value.is_finite()) {
149            return Err(Error::InvalidInput(
150                "origin shift must be finite".to_owned(),
151            ));
152        }
153        let mut builder = StructureBuilder::new(self.name(), *self.lattice());
154        for site in self.sites() {
155            let frac = [
156                site.frac()[0] + delta[0],
157                site.frac()[1] + delta[1],
158                site.frac()[2] + delta[2],
159            ];
160            builder = builder.site(site.clone().with_frac(Lattice::wrap(frac))?);
161        }
162        for (key, value) in self.metadata() {
163            builder = builder.metadata(key.clone(), value.clone());
164        }
165        builder.build()
166    }
167}
168
169fn multiply_row(vector: [f64; 3], matrix: [[f64; 3]; 3]) -> [f64; 3] {
170    let [x, y, z] = vector;
171    let [[a, b, c], [d, e, f], [g, h, i]] = matrix;
172    [
173        x * a + y * d + z * g,
174        x * b + y * e + z * h,
175        x * c + y * f + z * i,
176    ]
177}