1use super::cell::UnitCell;
11use super::sbu::Sbu;
12use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct CrystalAtom {
17 pub element: u8,
19 pub frac_coords: [f64; 3],
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct CrystalStructure {
26 pub cell: UnitCell,
28 pub atoms: Vec<CrystalAtom>,
30 pub labels: Vec<String>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct Topology {
37 pub name: String,
39 pub nodes: Vec<[f64; 3]>,
41 pub edges: Vec<(usize, usize, [i32; 3])>,
44}
45
46impl Topology {
47 pub fn pcu() -> Self {
50 Self {
51 name: "pcu".to_string(),
52 nodes: vec![[0.0, 0.0, 0.0]],
53 edges: vec![
54 (0, 0, [1, 0, 0]), (0, 0, [0, 1, 0]), (0, 0, [0, 0, 1]), ],
58 }
59 }
60
61 pub fn dia() -> Self {
63 Self {
64 name: "dia".to_string(),
65 nodes: vec![[0.0, 0.0, 0.0], [0.25, 0.25, 0.25]],
66 edges: vec![
67 (0, 1, [0, 0, 0]),
68 (0, 1, [-1, 0, 0]),
69 (0, 1, [0, -1, 0]),
70 (0, 1, [0, 0, -1]),
71 ],
72 }
73 }
74
75 pub fn sql() -> Self {
77 Self {
78 name: "sql".to_string(),
79 nodes: vec![[0.0, 0.0, 0.0]],
80 edges: vec![(0, 0, [1, 0, 0]), (0, 0, [0, 1, 0])],
81 }
82 }
83}
84
85impl CrystalStructure {
86 pub fn num_atoms(&self) -> usize {
88 self.atoms.len()
89 }
90
91 pub fn cartesian_coords(&self) -> Vec<[f64; 3]> {
93 self.atoms
94 .iter()
95 .map(|a| self.cell.frac_to_cart(a.frac_coords))
96 .collect()
97 }
98
99 pub fn elements(&self) -> Vec<u8> {
101 self.atoms.iter().map(|a| a.element).collect()
102 }
103
104 pub fn make_supercell(&self, na: usize, nb: usize, nc: usize) -> CrystalStructure {
106 let (new_cell, offsets) = self.cell.supercell(na, nb, nc);
107 let mut new_atoms = Vec::new();
108 let mut new_labels = Vec::new();
109
110 let na_f = na as f64;
111 let nb_f = nb as f64;
112 let nc_f = nc as f64;
113
114 for offset in &offsets {
115 for (i, atom) in self.atoms.iter().enumerate() {
116 new_atoms.push(CrystalAtom {
117 element: atom.element,
118 frac_coords: [
119 (atom.frac_coords[0] + offset[0]) / na_f,
120 (atom.frac_coords[1] + offset[1]) / nb_f,
121 (atom.frac_coords[2] + offset[2]) / nc_f,
122 ],
123 });
124 new_labels.push(self.labels[i].clone());
125 }
126 }
127
128 CrystalStructure {
129 cell: new_cell,
130 atoms: new_atoms,
131 labels: new_labels,
132 }
133 }
134}
135
136pub fn assemble_framework(
145 node_sbu: &Sbu,
146 linker_sbu: &Sbu,
147 topology: &Topology,
148 cell: &UnitCell,
149) -> CrystalStructure {
150 let mut atoms = Vec::new();
151 let mut labels = Vec::new();
152
153 for (ni, node_frac) in topology.nodes.iter().enumerate() {
155 let node_cart = cell.frac_to_cart(*node_frac);
156
157 for (ai, &elem) in node_sbu.elements.iter().enumerate() {
158 let atom_cart = [
159 node_cart[0] + node_sbu.positions[ai][0],
160 node_cart[1] + node_sbu.positions[ai][1],
161 node_cart[2] + node_sbu.positions[ai][2],
162 ];
163 let frac = cell.cart_to_frac(atom_cart);
164 atoms.push(CrystalAtom {
165 element: elem,
166 frac_coords: frac,
167 });
168 labels.push(format!("node_{}", ni));
169 }
170 }
171
172 for (ei, &(ni, nj, offset)) in topology.edges.iter().enumerate() {
174 let node_i_frac = topology.nodes[ni];
175 let node_j_frac = [
176 topology.nodes[nj][0] + offset[0] as f64,
177 topology.nodes[nj][1] + offset[1] as f64,
178 topology.nodes[nj][2] + offset[2] as f64,
179 ];
180
181 let node_i_cart = cell.frac_to_cart(node_i_frac);
182 let node_j_cart = cell.frac_to_cart(node_j_frac);
183
184 let mid = [
186 (node_i_cart[0] + node_j_cart[0]) / 2.0,
187 (node_i_cart[1] + node_j_cart[1]) / 2.0,
188 (node_i_cart[2] + node_j_cart[2]) / 2.0,
189 ];
190
191 let dx = node_j_cart[0] - node_i_cart[0];
193 let dy = node_j_cart[1] - node_i_cart[1];
194 let dz = node_j_cart[2] - node_i_cart[2];
195 let dist = (dx * dx + dy * dy + dz * dz).sqrt();
196
197 if dist < 1e-12 {
198 continue;
199 }
200
201 let dir = [dx / dist, dy / dist, dz / dist];
202
203 let rot = rotation_from_x_to(dir);
206
207 for (ai, &elem) in linker_sbu.elements.iter().enumerate() {
208 let p = linker_sbu.positions[ai];
209 let rotated = apply_rotation(&rot, p);
211 let atom_cart = [
212 mid[0] + rotated[0],
213 mid[1] + rotated[1],
214 mid[2] + rotated[2],
215 ];
216 let frac = cell.cart_to_frac(atom_cart);
217 atoms.push(CrystalAtom {
218 element: elem,
219 frac_coords: frac,
220 });
221 labels.push(format!("linker_{}", ei));
222 }
223 }
224
225 CrystalStructure {
226 cell: cell.clone(),
227 atoms,
228 labels,
229 }
230}
231
232fn rotation_from_x_to(target: [f64; 3]) -> [[f64; 3]; 3] {
234 let x = [1.0, 0.0, 0.0];
235 let dot = x[0] * target[0] + x[1] * target[1] + x[2] * target[2];
236
237 if dot > 1.0 - 1e-12 {
239 return [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
240 }
241 if dot < -1.0 + 1e-12 {
243 return [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0]];
244 }
245
246 let cross = [
248 x[1] * target[2] - x[2] * target[1],
249 x[2] * target[0] - x[0] * target[2],
250 x[0] * target[1] - x[1] * target[0],
251 ];
252 let s = (cross[0].powi(2) + cross[1].powi(2) + cross[2].powi(2)).sqrt();
253 let c = dot;
254
255 let v = cross;
260 let k = [[0.0, -v[2], v[1]], [v[2], 0.0, -v[0]], [-v[1], v[0], 0.0]];
261
262 let k2 = mat_mul_3x3(k, k);
264
265 let factor = (1.0 - c) / (s * s);
267 [
268 [
269 1.0 + k[0][0] + k2[0][0] * factor,
270 k[0][1] + k2[0][1] * factor,
271 k[0][2] + k2[0][2] * factor,
272 ],
273 [
274 k[1][0] + k2[1][0] * factor,
275 1.0 + k[1][1] + k2[1][1] * factor,
276 k[1][2] + k2[1][2] * factor,
277 ],
278 [
279 k[2][0] + k2[2][0] * factor,
280 k[2][1] + k2[2][1] * factor,
281 1.0 + k[2][2] + k2[2][2] * factor,
282 ],
283 ]
284}
285
286fn mat_mul_3x3(a: [[f64; 3]; 3], b: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
287 let mut r = [[0.0; 3]; 3];
288 for i in 0..3 {
289 for j in 0..3 {
290 r[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j];
291 }
292 }
293 r
294}
295
296fn apply_rotation(rot: &[[f64; 3]; 3], p: [f64; 3]) -> [f64; 3] {
297 [
298 rot[0][0] * p[0] + rot[0][1] * p[1] + rot[0][2] * p[2],
299 rot[1][0] * p[0] + rot[1][1] * p[1] + rot[1][2] * p[2],
300 rot[2][0] * p[0] + rot[2][1] * p[1] + rot[2][2] * p[2],
301 ]
302}
303
304#[cfg(test)]
305mod tests {
306 use super::super::sbu::{CoordinationGeometry, Sbu};
307 use super::*;
308
309 #[test]
310 fn test_assemble_simple_cubic_framework() {
311 let node = Sbu::metal_node(30, 0.0, CoordinationGeometry::Octahedral); let linker = Sbu::linear_linker(&[6, 6], 1.4, "carboxylate");
313 let topo = Topology::pcu();
314 let cell = UnitCell::cubic(10.0);
315
316 let structure = assemble_framework(&node, &linker, &topo, &cell);
317
318 assert_eq!(structure.num_atoms(), 7);
320 assert!(structure
321 .atoms
322 .iter()
323 .all(|a| a.element == 30 || a.element == 6));
324 }
325
326 #[test]
327 fn test_assemble_diamond_topology() {
328 let node = Sbu::metal_node(14, 0.0, CoordinationGeometry::Tetrahedral); let linker = Sbu::linear_linker(&[8], 1.0, "bridge"); let topo = Topology::dia();
331 let cell = UnitCell::cubic(5.43); let structure = assemble_framework(&node, &linker, &topo, &cell);
334
335 assert_eq!(structure.num_atoms(), 6);
337 }
338
339 #[test]
340 fn test_supercell_atom_count() {
341 let node = Sbu::metal_node(30, 0.0, CoordinationGeometry::Octahedral);
342 let linker = Sbu::linear_linker(&[6, 6], 1.4, "carboxylate");
343 let topo = Topology::pcu();
344 let cell = UnitCell::cubic(10.0);
345
346 let structure = assemble_framework(&node, &linker, &topo, &cell);
347 let n_base = structure.num_atoms();
348
349 let super_struct = structure.make_supercell(2, 2, 2);
350 assert_eq!(super_struct.num_atoms(), n_base * 8);
351 }
352
353 #[test]
354 fn test_topology_pcu() {
355 let t = Topology::pcu();
356 assert_eq!(t.nodes.len(), 1);
357 assert_eq!(t.edges.len(), 3);
358 assert_eq!(t.name, "pcu");
359 }
360
361 #[test]
362 fn test_topology_dia() {
363 let t = Topology::dia();
364 assert_eq!(t.nodes.len(), 2);
365 assert_eq!(t.edges.len(), 4);
366 }
367
368 #[test]
369 fn test_rotation_identity() {
370 let rot = rotation_from_x_to([1.0, 0.0, 0.0]);
371 let p = [1.0, 2.0, 3.0];
372 let rp = apply_rotation(&rot, p);
373 for i in 0..3 {
374 assert!((rp[i] - p[i]).abs() < 1e-10);
375 }
376 }
377
378 #[test]
379 fn test_rotation_to_y() {
380 let rot = rotation_from_x_to([0.0, 1.0, 0.0]);
381 let p = [1.0, 0.0, 0.0];
382 let rp = apply_rotation(&rot, p);
383 assert!((rp[0]).abs() < 1e-10, "x should be ~0, got {:.6}", rp[0]);
384 assert!(
385 (rp[1] - 1.0).abs() < 1e-10,
386 "y should be ~1, got {:.6}",
387 rp[1]
388 );
389 assert!((rp[2]).abs() < 1e-10, "z should be ~0, got {:.6}", rp[2]);
390 }
391
392 #[test]
393 fn test_crystal_cartesian_coords() {
394 let cell = UnitCell::cubic(10.0);
395 let structure = CrystalStructure {
396 cell,
397 atoms: vec![
398 CrystalAtom {
399 element: 6,
400 frac_coords: [0.5, 0.0, 0.0],
401 },
402 CrystalAtom {
403 element: 8,
404 frac_coords: [0.0, 0.5, 0.0],
405 },
406 ],
407 labels: vec!["a".into(), "b".into()],
408 };
409
410 let coords = structure.cartesian_coords();
411 assert!((coords[0][0] - 5.0).abs() < 1e-10);
412 assert!((coords[1][1] - 5.0).abs() < 1e-10);
413 }
414}