1use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct UnitCell {
15 pub lattice: [[f64; 3]; 3],
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct CellParameters {
22 pub a: f64,
24 pub b: f64,
25 pub c: f64,
26 pub alpha: f64,
28 pub beta: f64,
29 pub gamma: f64,
30}
31
32impl UnitCell {
33 pub fn new(lattice: [[f64; 3]; 3]) -> Self {
35 Self { lattice }
36 }
37
38 pub fn cubic(a: f64) -> Self {
40 Self {
41 lattice: [[a, 0.0, 0.0], [0.0, a, 0.0], [0.0, 0.0, a]],
42 }
43 }
44
45 pub fn from_parameters(params: &CellParameters) -> Self {
47 let alpha = params.alpha.to_radians();
48 let beta = params.beta.to_radians();
49 let gamma = params.gamma.to_radians();
50
51 let cos_alpha = alpha.cos();
52 let cos_beta = beta.cos();
53 let cos_gamma = gamma.cos();
54 let sin_gamma = gamma.sin();
55
56 let ax = params.a;
58 let bx = params.b * cos_gamma;
59 let by = params.b * sin_gamma;
60 let cx = params.c * cos_beta;
61 let cy = params.c * (cos_alpha - cos_beta * cos_gamma) / sin_gamma;
62 let cz = (params.c * params.c - cx * cx - cy * cy).sqrt();
63
64 Self {
65 lattice: [[ax, 0.0, 0.0], [bx, by, 0.0], [cx, cy, cz]],
66 }
67 }
68
69 pub fn parameters(&self) -> CellParameters {
71 let a_vec = self.lattice[0];
72 let b_vec = self.lattice[1];
73 let c_vec = self.lattice[2];
74
75 let a = norm3(a_vec);
76 let b = norm3(b_vec);
77 let c = norm3(c_vec);
78
79 let alpha = angle_between(b_vec, c_vec).to_degrees();
80 let beta = angle_between(a_vec, c_vec).to_degrees();
81 let gamma = angle_between(a_vec, b_vec).to_degrees();
82
83 CellParameters {
84 a,
85 b,
86 c,
87 alpha,
88 beta,
89 gamma,
90 }
91 }
92
93 pub fn volume(&self) -> f64 {
95 let a = self.lattice[0];
96 let b = self.lattice[1];
97 let c = self.lattice[2];
98 let bxc = cross3(b, c);
100 dot3(a, bxc).abs()
101 }
102
103 pub fn frac_to_cart(&self, frac: [f64; 3]) -> [f64; 3] {
106 let a = self.lattice[0];
107 let b = self.lattice[1];
108 let c = self.lattice[2];
109 [
110 frac[0] * a[0] + frac[1] * b[0] + frac[2] * c[0],
111 frac[0] * a[1] + frac[1] * b[1] + frac[2] * c[1],
112 frac[0] * a[2] + frac[1] * b[2] + frac[2] * c[2],
113 ]
114 }
115
116 pub fn cart_to_frac(&self, cart: [f64; 3]) -> [f64; 3] {
120 let inv = self.inverse_matrix();
121 [
123 inv[0][0] * cart[0] + inv[1][0] * cart[1] + inv[2][0] * cart[2],
124 inv[0][1] * cart[0] + inv[1][1] * cart[1] + inv[2][1] * cart[2],
125 inv[0][2] * cart[0] + inv[1][2] * cart[1] + inv[2][2] * cart[2],
126 ]
127 }
128
129 pub fn wrap_frac(frac: [f64; 3]) -> [f64; 3] {
131 [
132 frac[0] - frac[0].floor(),
133 frac[1] - frac[1].floor(),
134 frac[2] - frac[2].floor(),
135 ]
136 }
137
138 pub fn wrap_cart(&self, cart: [f64; 3]) -> [f64; 3] {
140 let frac = self.cart_to_frac(cart);
141 let wrapped = Self::wrap_frac(frac);
142 self.frac_to_cart(wrapped)
143 }
144
145 pub fn minimum_image_distance(&self, a: [f64; 3], b: [f64; 3]) -> f64 {
147 let fa = self.cart_to_frac(a);
148 let fb = self.cart_to_frac(b);
149 let mut df = [fa[0] - fb[0], fa[1] - fb[1], fa[2] - fb[2]];
150 for d in &mut df {
152 *d -= d.round();
153 }
154 let dc = self.frac_to_cart(df);
155 norm3(dc)
156 }
157
158 pub fn supercell(&self, na: usize, nb: usize, nc: usize) -> (UnitCell, Vec<[f64; 3]>) {
161 let new_lattice = [
162 [
163 self.lattice[0][0] * na as f64,
164 self.lattice[0][1] * na as f64,
165 self.lattice[0][2] * na as f64,
166 ],
167 [
168 self.lattice[1][0] * nb as f64,
169 self.lattice[1][1] * nb as f64,
170 self.lattice[1][2] * nb as f64,
171 ],
172 [
173 self.lattice[2][0] * nc as f64,
174 self.lattice[2][1] * nc as f64,
175 self.lattice[2][2] * nc as f64,
176 ],
177 ];
178
179 let mut offsets = Vec::with_capacity(na * nb * nc);
180 for ia in 0..na {
181 for ib in 0..nb {
182 for ic in 0..nc {
183 offsets.push([ia as f64, ib as f64, ic as f64]);
184 }
185 }
186 }
187
188 (UnitCell::new(new_lattice), offsets)
189 }
190
191 fn inverse_matrix(&self) -> [[f64; 3]; 3] {
193 let m = self.lattice;
194 let det = m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
195 - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
196 + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
197
198 let inv_det = 1.0 / det;
199
200 [
201 [
202 (m[1][1] * m[2][2] - m[1][2] * m[2][1]) * inv_det,
203 (m[0][2] * m[2][1] - m[0][1] * m[2][2]) * inv_det,
204 (m[0][1] * m[1][2] - m[0][2] * m[1][1]) * inv_det,
205 ],
206 [
207 (m[1][2] * m[2][0] - m[1][0] * m[2][2]) * inv_det,
208 (m[0][0] * m[2][2] - m[0][2] * m[2][0]) * inv_det,
209 (m[0][2] * m[1][0] - m[0][0] * m[1][2]) * inv_det,
210 ],
211 [
212 (m[1][0] * m[2][1] - m[1][1] * m[2][0]) * inv_det,
213 (m[0][1] * m[2][0] - m[0][0] * m[2][1]) * inv_det,
214 (m[0][0] * m[1][1] - m[0][1] * m[1][0]) * inv_det,
215 ],
216 ]
217 }
218}
219
220fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
221 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
222}
223
224fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
225 [
226 a[1] * b[2] - a[2] * b[1],
227 a[2] * b[0] - a[0] * b[2],
228 a[0] * b[1] - a[1] * b[0],
229 ]
230}
231
232fn norm3(v: [f64; 3]) -> f64 {
233 (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
234}
235
236fn angle_between(a: [f64; 3], b: [f64; 3]) -> f64 {
237 let cos_angle = dot3(a, b) / (norm3(a) * norm3(b));
238 cos_angle.clamp(-1.0, 1.0).acos()
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn test_cubic_cell_volume() {
247 let cell = UnitCell::cubic(10.0);
248 assert!((cell.volume() - 1000.0).abs() < 1e-10);
249 }
250
251 #[test]
252 fn test_cubic_parameters() {
253 let cell = UnitCell::cubic(5.0);
254 let p = cell.parameters();
255 assert!((p.a - 5.0).abs() < 1e-10);
256 assert!((p.b - 5.0).abs() < 1e-10);
257 assert!((p.c - 5.0).abs() < 1e-10);
258 assert!((p.alpha - 90.0).abs() < 1e-10);
259 assert!((p.beta - 90.0).abs() < 1e-10);
260 assert!((p.gamma - 90.0).abs() < 1e-10);
261 }
262
263 #[test]
264 fn test_frac_cart_roundtrip() {
265 let cell = UnitCell::from_parameters(&CellParameters {
266 a: 10.0,
267 b: 12.0,
268 c: 8.0,
269 alpha: 90.0,
270 beta: 90.0,
271 gamma: 120.0,
272 });
273 let frac = [0.3, 0.4, 0.7];
274 let cart = cell.frac_to_cart(frac);
275 let back = cell.cart_to_frac(cart);
276 for i in 0..3 {
277 assert!(
278 (frac[i] - back[i]).abs() < 1e-10,
279 "Roundtrip failed at {i}: {:.6} vs {:.6}",
280 frac[i],
281 back[i]
282 );
283 }
284 }
285
286 #[test]
287 fn test_wrap_frac() {
288 let wrapped = UnitCell::wrap_frac([1.3, -0.2, 2.7]);
289 assert!((wrapped[0] - 0.3).abs() < 1e-10);
290 assert!((wrapped[1] - 0.8).abs() < 1e-10);
291 assert!((wrapped[2] - 0.7).abs() < 1e-10);
292 }
293
294 #[test]
295 fn test_minimum_image_distance_cubic() {
296 let cell = UnitCell::cubic(10.0);
297 let dist = cell.minimum_image_distance([1.0, 0.0, 0.0], [9.0, 0.0, 0.0]);
300 assert!(
301 (dist - 2.0).abs() < 1e-10,
302 "Minimum image distance should be 2.0, got {dist:.6}"
303 );
304 }
305
306 #[test]
307 fn test_supercell_offsets() {
308 let cell = UnitCell::cubic(5.0);
309 let (super_cell, offsets) = cell.supercell(2, 2, 2);
310 assert_eq!(offsets.len(), 8);
311 assert!((super_cell.lattice[0][0] - 10.0).abs() < 1e-10);
312 assert!((super_cell.volume() - 5.0f64.powi(3) * 8.0).abs() < 1e-6);
313 }
314
315 #[test]
316 fn test_from_parameters_orthorhombic() {
317 let p = CellParameters {
318 a: 5.0,
319 b: 7.0,
320 c: 9.0,
321 alpha: 90.0,
322 beta: 90.0,
323 gamma: 90.0,
324 };
325 let cell = UnitCell::from_parameters(&p);
326 assert!((cell.volume() - 315.0).abs() < 1e-6);
327 let back = cell.parameters();
328 assert!((back.a - 5.0).abs() < 1e-6);
329 assert!((back.b - 7.0).abs() < 1e-6);
330 assert!((back.c - 9.0).abs() < 1e-6);
331 }
332}