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 pub fn bcu() -> Self {
87 Self {
88 name: "bcu".to_string(),
89 nodes: vec![[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]],
90 edges: vec![
91 (0, 1, [0, 0, 0]),
92 (0, 1, [-1, 0, 0]),
93 (0, 1, [0, -1, 0]),
94 (0, 1, [0, 0, -1]),
95 ],
96 }
97 }
98
99 pub fn fcu() -> Self {
102 Self {
103 name: "fcu".to_string(),
104 nodes: vec![
105 [0.0, 0.0, 0.0],
106 [0.5, 0.5, 0.0],
107 [0.5, 0.0, 0.5],
108 [0.0, 0.5, 0.5],
109 ],
110 edges: vec![
111 (0, 1, [0, 0, 0]),
113 (0, 1, [0, -1, 0]),
114 (0, 1, [-1, 0, 0]),
115 (0, 2, [0, 0, 0]),
117 (0, 2, [0, 0, -1]),
118 (0, 2, [-1, 0, 0]),
119 (0, 3, [0, 0, 0]),
121 (0, 3, [0, -1, 0]),
122 (0, 3, [0, 0, -1]),
123 (1, 2, [0, 0, 0]),
125 (1, 2, [0, 0, -1]),
126 (1, 3, [0, 0, 0]),
128 (1, 3, [-1, 0, 0]),
129 (2, 3, [0, 0, 0]),
131 (2, 3, [0, -1, 0]),
132 ],
133 }
134 }
135
136 pub fn nbo() -> Self {
139 Self {
140 name: "nbo".to_string(),
141 nodes: vec![[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]],
142 edges: vec![
143 (0, 1, [0, 0, 0]),
144 (0, 1, [-1, 0, 0]),
145 (0, 1, [0, -1, 0]),
146 (0, 1, [0, 0, -1]),
147 ],
148 }
149 }
150
151 pub fn pts() -> Self {
154 Self {
155 name: "pts".to_string(),
156 nodes: vec![
157 [0.0, 0.0, 0.0], [0.5, 0.5, 0.0], ],
160 edges: vec![
161 (0, 1, [0, 0, 0]),
162 (0, 1, [0, -1, 0]),
163 (0, 1, [0, 0, 1]),
164 (0, 1, [0, -1, 1]),
165 ],
166 }
167 }
168
169 pub fn kgm() -> Self {
172 Self {
173 name: "kgm".to_string(),
174 nodes: vec![[0.5, 0.0, 0.0], [0.0, 0.5, 0.0], [0.5, 0.5, 0.0]],
175 edges: vec![
176 (0, 2, [0, 0, 0]),
177 (1, 2, [0, 0, 0]),
178 (0, 1, [1, 0, 0]),
179 (0, 1, [0, -1, 0]),
180 (1, 2, [-1, 1, 0]),
181 (0, 2, [0, -1, 0]),
182 ],
183 }
184 }
185
186 pub fn hxl() -> Self {
189 Self {
190 name: "hxl".to_string(),
191 nodes: vec![[0.0, 0.0, 0.0]],
192 edges: vec![(0, 0, [1, 0, 0]), (0, 0, [0, 1, 0]), (0, 0, [1, -1, 0])],
193 }
194 }
195
196 pub fn pillared_sql() -> Self {
199 Self {
200 name: "pillared_sql".to_string(),
201 nodes: vec![[0.0, 0.0, 0.0]],
202 edges: vec![(0, 0, [1, 0, 0]), (0, 0, [0, 1, 0]), (0, 0, [0, 0, 1])],
203 }
204 }
205
206 pub fn from_name(name: &str) -> Option<Self> {
208 match name {
209 "pcu" => Some(Self::pcu()),
210 "dia" => Some(Self::dia()),
211 "sql" => Some(Self::sql()),
212 "bcu" => Some(Self::bcu()),
213 "fcu" => Some(Self::fcu()),
214 "nbo" => Some(Self::nbo()),
215 "pts" => Some(Self::pts()),
216 "kgm" => Some(Self::kgm()),
217 "hxl" => Some(Self::hxl()),
218 "pillared_sql" => Some(Self::pillared_sql()),
219 _ => None,
220 }
221 }
222}
223
224impl CrystalStructure {
225 pub fn num_atoms(&self) -> usize {
227 self.atoms.len()
228 }
229
230 pub fn cartesian_coords(&self) -> Vec<[f64; 3]> {
232 self.atoms
233 .iter()
234 .map(|a| self.cell.frac_to_cart(a.frac_coords))
235 .collect()
236 }
237
238 pub fn elements(&self) -> Vec<u8> {
240 self.atoms.iter().map(|a| a.element).collect()
241 }
242
243 pub fn make_supercell(&self, na: usize, nb: usize, nc: usize) -> CrystalStructure {
245 let (new_cell, offsets) = self.cell.supercell(na, nb, nc);
246 let mut new_atoms = Vec::new();
247 let mut new_labels = Vec::new();
248
249 let na_f = na as f64;
250 let nb_f = nb as f64;
251 let nc_f = nc as f64;
252
253 for offset in &offsets {
254 for (i, atom) in self.atoms.iter().enumerate() {
255 new_atoms.push(CrystalAtom {
256 element: atom.element,
257 frac_coords: [
258 (atom.frac_coords[0] + offset[0]) / na_f,
259 (atom.frac_coords[1] + offset[1]) / nb_f,
260 (atom.frac_coords[2] + offset[2]) / nc_f,
261 ],
262 });
263 new_labels.push(self.labels[i].clone());
264 }
265 }
266
267 CrystalStructure {
268 cell: new_cell,
269 atoms: new_atoms,
270 labels: new_labels,
271 }
272 }
273}
274
275pub fn assemble_framework(
284 node_sbu: &Sbu,
285 linker_sbu: &Sbu,
286 topology: &Topology,
287 cell: &UnitCell,
288) -> CrystalStructure {
289 let mut atoms = Vec::new();
290 let mut labels = Vec::new();
291
292 for (ni, node_frac) in topology.nodes.iter().enumerate() {
294 let node_cart = cell.frac_to_cart(*node_frac);
295
296 for (ai, &elem) in node_sbu.elements.iter().enumerate() {
297 let atom_cart = [
298 node_cart[0] + node_sbu.positions[ai][0],
299 node_cart[1] + node_sbu.positions[ai][1],
300 node_cart[2] + node_sbu.positions[ai][2],
301 ];
302 let frac = cell.cart_to_frac(atom_cart);
303 atoms.push(CrystalAtom {
304 element: elem,
305 frac_coords: frac,
306 });
307 labels.push(format!("node_{}", ni));
308 }
309 }
310
311 for (ei, &(ni, nj, offset)) in topology.edges.iter().enumerate() {
313 let node_i_frac = topology.nodes[ni];
314 let node_j_frac = [
315 topology.nodes[nj][0] + offset[0] as f64,
316 topology.nodes[nj][1] + offset[1] as f64,
317 topology.nodes[nj][2] + offset[2] as f64,
318 ];
319
320 let node_i_cart = cell.frac_to_cart(node_i_frac);
321 let node_j_cart = cell.frac_to_cart(node_j_frac);
322
323 let mid = [
325 (node_i_cart[0] + node_j_cart[0]) / 2.0,
326 (node_i_cart[1] + node_j_cart[1]) / 2.0,
327 (node_i_cart[2] + node_j_cart[2]) / 2.0,
328 ];
329
330 let dx = node_j_cart[0] - node_i_cart[0];
332 let dy = node_j_cart[1] - node_i_cart[1];
333 let dz = node_j_cart[2] - node_i_cart[2];
334 let dist = (dx * dx + dy * dy + dz * dz).sqrt();
335
336 if dist < 1e-12 {
337 continue;
338 }
339
340 let dir = [dx / dist, dy / dist, dz / dist];
341
342 let rot = rotation_from_x_to(dir);
345
346 for (ai, &elem) in linker_sbu.elements.iter().enumerate() {
347 let p = linker_sbu.positions[ai];
348 let rotated = apply_rotation(&rot, p);
350 let atom_cart = [
351 mid[0] + rotated[0],
352 mid[1] + rotated[1],
353 mid[2] + rotated[2],
354 ];
355 let frac = cell.cart_to_frac(atom_cart);
356 atoms.push(CrystalAtom {
357 element: elem,
358 frac_coords: frac,
359 });
360 labels.push(format!("linker_{}", ei));
361 }
362 }
363
364 CrystalStructure {
365 cell: cell.clone(),
366 atoms,
367 labels,
368 }
369}
370
371fn rotation_from_x_to(target: [f64; 3]) -> [[f64; 3]; 3] {
373 let x = [1.0, 0.0, 0.0];
374 let dot = x[0] * target[0] + x[1] * target[1] + x[2] * target[2];
375
376 if dot > 1.0 - 1e-12 {
378 return [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
379 }
380 if dot < -1.0 + 1e-12 {
382 return [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0]];
383 }
384
385 let cross = [
387 x[1] * target[2] - x[2] * target[1],
388 x[2] * target[0] - x[0] * target[2],
389 x[0] * target[1] - x[1] * target[0],
390 ];
391 let s = (cross[0].powi(2) + cross[1].powi(2) + cross[2].powi(2)).sqrt();
392 let c = dot;
393
394 let v = cross;
399 let k = [[0.0, -v[2], v[1]], [v[2], 0.0, -v[0]], [-v[1], v[0], 0.0]];
400
401 let k2 = mat_mul_3x3(k, k);
403
404 let factor = (1.0 - c) / (s * s);
406 [
407 [
408 1.0 + k[0][0] + k2[0][0] * factor,
409 k[0][1] + k2[0][1] * factor,
410 k[0][2] + k2[0][2] * factor,
411 ],
412 [
413 k[1][0] + k2[1][0] * factor,
414 1.0 + k[1][1] + k2[1][1] * factor,
415 k[1][2] + k2[1][2] * factor,
416 ],
417 [
418 k[2][0] + k2[2][0] * factor,
419 k[2][1] + k2[2][1] * factor,
420 1.0 + k[2][2] + k2[2][2] * factor,
421 ],
422 ]
423}
424
425fn mat_mul_3x3(a: [[f64; 3]; 3], b: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
426 let mut r = [[0.0; 3]; 3];
427 for i in 0..3 {
428 for j in 0..3 {
429 r[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j];
430 }
431 }
432 r
433}
434
435fn apply_rotation(rot: &[[f64; 3]; 3], p: [f64; 3]) -> [f64; 3] {
436 [
437 rot[0][0] * p[0] + rot[0][1] * p[1] + rot[0][2] * p[2],
438 rot[1][0] * p[0] + rot[1][1] * p[1] + rot[1][2] * p[2],
439 rot[2][0] * p[0] + rot[2][1] * p[1] + rot[2][2] * p[2],
440 ]
441}
442
443#[cfg(test)]
444mod tests {
445 use super::super::sbu::{CoordinationGeometry, Sbu};
446 use super::*;
447
448 #[test]
449 fn test_assemble_simple_cubic_framework() {
450 let node = Sbu::metal_node(30, 0.0, CoordinationGeometry::Octahedral); let linker = Sbu::linear_linker(&[6, 6], 1.4, "carboxylate");
452 let topo = Topology::pcu();
453 let cell = UnitCell::cubic(10.0);
454
455 let structure = assemble_framework(&node, &linker, &topo, &cell);
456
457 assert_eq!(structure.num_atoms(), 7);
459 assert!(structure
460 .atoms
461 .iter()
462 .all(|a| a.element == 30 || a.element == 6));
463 }
464
465 #[test]
466 fn test_assemble_diamond_topology() {
467 let node = Sbu::metal_node(14, 0.0, CoordinationGeometry::Tetrahedral); let linker = Sbu::linear_linker(&[8], 1.0, "bridge"); let topo = Topology::dia();
470 let cell = UnitCell::cubic(5.43); let structure = assemble_framework(&node, &linker, &topo, &cell);
473
474 assert_eq!(structure.num_atoms(), 6);
476 }
477
478 #[test]
479 fn test_supercell_atom_count() {
480 let node = Sbu::metal_node(30, 0.0, CoordinationGeometry::Octahedral);
481 let linker = Sbu::linear_linker(&[6, 6], 1.4, "carboxylate");
482 let topo = Topology::pcu();
483 let cell = UnitCell::cubic(10.0);
484
485 let structure = assemble_framework(&node, &linker, &topo, &cell);
486 let n_base = structure.num_atoms();
487
488 let super_struct = structure.make_supercell(2, 2, 2);
489 assert_eq!(super_struct.num_atoms(), n_base * 8);
490 }
491
492 #[test]
493 fn test_topology_pcu() {
494 let t = Topology::pcu();
495 assert_eq!(t.nodes.len(), 1);
496 assert_eq!(t.edges.len(), 3);
497 assert_eq!(t.name, "pcu");
498 }
499
500 #[test]
501 fn test_topology_dia() {
502 let t = Topology::dia();
503 assert_eq!(t.nodes.len(), 2);
504 assert_eq!(t.edges.len(), 4);
505 }
506
507 #[test]
508 fn test_rotation_identity() {
509 let rot = rotation_from_x_to([1.0, 0.0, 0.0]);
510 let p = [1.0, 2.0, 3.0];
511 let rp = apply_rotation(&rot, p);
512 for i in 0..3 {
513 assert!((rp[i] - p[i]).abs() < 1e-10);
514 }
515 }
516
517 #[test]
518 fn test_rotation_to_y() {
519 let rot = rotation_from_x_to([0.0, 1.0, 0.0]);
520 let p = [1.0, 0.0, 0.0];
521 let rp = apply_rotation(&rot, p);
522 assert!((rp[0]).abs() < 1e-10, "x should be ~0, got {:.6}", rp[0]);
523 assert!(
524 (rp[1] - 1.0).abs() < 1e-10,
525 "y should be ~1, got {:.6}",
526 rp[1]
527 );
528 assert!((rp[2]).abs() < 1e-10, "z should be ~0, got {:.6}", rp[2]);
529 }
530
531 #[test]
532 fn test_crystal_cartesian_coords() {
533 let cell = UnitCell::cubic(10.0);
534 let structure = CrystalStructure {
535 cell,
536 atoms: vec![
537 CrystalAtom {
538 element: 6,
539 frac_coords: [0.5, 0.0, 0.0],
540 },
541 CrystalAtom {
542 element: 8,
543 frac_coords: [0.0, 0.5, 0.0],
544 },
545 ],
546 labels: vec!["a".into(), "b".into()],
547 };
548
549 let coords = structure.cartesian_coords();
550 assert!((coords[0][0] - 5.0).abs() < 1e-10);
551 assert!((coords[1][1] - 5.0).abs() < 1e-10);
552 }
553
554 #[test]
555 fn test_topology_bcu() {
556 let t = Topology::bcu();
557 assert_eq!(t.nodes.len(), 2);
558 assert_eq!(t.edges.len(), 4);
559 assert_eq!(t.name, "bcu");
560 }
561
562 #[test]
563 fn test_topology_fcu() {
564 let t = Topology::fcu();
565 assert_eq!(t.nodes.len(), 4);
566 assert_eq!(t.edges.len(), 15);
567 assert_eq!(t.name, "fcu");
568 }
569
570 #[test]
571 fn test_topology_nbo() {
572 let t = Topology::nbo();
573 assert_eq!(t.nodes.len(), 2);
574 assert_eq!(t.edges.len(), 4);
575 assert_eq!(t.name, "nbo");
576 }
577
578 #[test]
579 fn test_topology_pts() {
580 let t = Topology::pts();
581 assert_eq!(t.nodes.len(), 2);
582 assert_eq!(t.edges.len(), 4);
583 assert_eq!(t.name, "pts");
584 }
585
586 #[test]
587 fn test_topology_kgm() {
588 let t = Topology::kgm();
589 assert_eq!(t.nodes.len(), 3);
590 assert_eq!(t.edges.len(), 6);
591 assert_eq!(t.name, "kgm");
592 }
593
594 #[test]
595 fn test_topology_from_name() {
596 assert!(Topology::from_name("pcu").is_some());
597 assert!(Topology::from_name("dia").is_some());
598 assert!(Topology::from_name("fcu").is_some());
599 assert!(Topology::from_name("bcu").is_some());
600 assert!(Topology::from_name("nbo").is_some());
601 assert!(Topology::from_name("pts").is_some());
602 assert!(Topology::from_name("kgm").is_some());
603 assert!(Topology::from_name("hxl").is_some());
604 assert!(Topology::from_name("pillared_sql").is_some());
605 assert!(Topology::from_name("unknown").is_none());
606 }
607
608 #[test]
609 fn test_assemble_fcu_framework() {
610 let node = Sbu::metal_node(40, 0.0, CoordinationGeometry::Octahedral); let linker = Sbu::linear_linker(&[6, 6], 1.4, "bdc");
612 let topo = Topology::fcu();
613 let cell = UnitCell::cubic(20.0);
614
615 let structure = assemble_framework(&node, &linker, &topo, &cell);
616 assert_eq!(structure.num_atoms(), 34);
618 }
619
620 #[test]
621 fn test_assemble_bcu_framework() {
622 let node = Sbu::metal_node(40, 0.0, CoordinationGeometry::Octahedral);
623 let linker = Sbu::linear_linker(&[6], 1.0, "bridge");
624 let topo = Topology::bcu();
625 let cell = UnitCell::cubic(15.0);
626
627 let structure = assemble_framework(&node, &linker, &topo, &cell);
628 assert_eq!(structure.num_atoms(), 6);
630 }
631}