use super::cell::UnitCell;
use super::sbu::Sbu;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrystalAtom {
pub element: u8,
pub frac_coords: [f64; 3],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrystalStructure {
pub cell: UnitCell,
pub atoms: Vec<CrystalAtom>,
pub labels: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Topology {
pub name: String,
pub nodes: Vec<[f64; 3]>,
pub edges: Vec<(usize, usize, [i32; 3])>,
}
impl Topology {
pub fn pcu() -> Self {
Self {
name: "pcu".to_string(),
nodes: vec![[0.0, 0.0, 0.0]],
edges: vec![
(0, 0, [1, 0, 0]), (0, 0, [0, 1, 0]), (0, 0, [0, 0, 1]), ],
}
}
pub fn dia() -> Self {
Self {
name: "dia".to_string(),
nodes: vec![[0.0, 0.0, 0.0], [0.25, 0.25, 0.25]],
edges: vec![
(0, 1, [0, 0, 0]),
(0, 1, [-1, 0, 0]),
(0, 1, [0, -1, 0]),
(0, 1, [0, 0, -1]),
],
}
}
pub fn sql() -> Self {
Self {
name: "sql".to_string(),
nodes: vec![[0.0, 0.0, 0.0]],
edges: vec![(0, 0, [1, 0, 0]), (0, 0, [0, 1, 0])],
}
}
pub fn bcu() -> Self {
Self {
name: "bcu".to_string(),
nodes: vec![[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]],
edges: vec![
(0, 1, [0, 0, 0]),
(0, 1, [-1, 0, 0]),
(0, 1, [0, -1, 0]),
(0, 1, [0, 0, -1]),
],
}
}
pub fn fcu() -> Self {
Self {
name: "fcu".to_string(),
nodes: vec![
[0.0, 0.0, 0.0],
[0.5, 0.5, 0.0],
[0.5, 0.0, 0.5],
[0.0, 0.5, 0.5],
],
edges: vec![
(0, 1, [0, 0, 0]),
(0, 1, [0, -1, 0]),
(0, 1, [-1, 0, 0]),
(0, 2, [0, 0, 0]),
(0, 2, [0, 0, -1]),
(0, 2, [-1, 0, 0]),
(0, 3, [0, 0, 0]),
(0, 3, [0, -1, 0]),
(0, 3, [0, 0, -1]),
(1, 2, [0, 0, 0]),
(1, 2, [0, 0, -1]),
(1, 3, [0, 0, 0]),
(1, 3, [-1, 0, 0]),
(2, 3, [0, 0, 0]),
(2, 3, [0, -1, 0]),
],
}
}
pub fn nbo() -> Self {
Self {
name: "nbo".to_string(),
nodes: vec![[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]],
edges: vec![
(0, 1, [0, 0, 0]),
(0, 1, [-1, 0, 0]),
(0, 1, [0, -1, 0]),
(0, 1, [0, 0, -1]),
],
}
}
pub fn pts() -> Self {
Self {
name: "pts".to_string(),
nodes: vec![
[0.0, 0.0, 0.0], [0.5, 0.5, 0.0], ],
edges: vec![
(0, 1, [0, 0, 0]),
(0, 1, [0, -1, 0]),
(0, 1, [0, 0, 1]),
(0, 1, [0, -1, 1]),
],
}
}
pub fn kgm() -> Self {
Self {
name: "kgm".to_string(),
nodes: vec![[0.5, 0.0, 0.0], [0.0, 0.5, 0.0], [0.5, 0.5, 0.0]],
edges: vec![
(0, 2, [0, 0, 0]),
(1, 2, [0, 0, 0]),
(0, 1, [1, 0, 0]),
(0, 1, [0, -1, 0]),
(1, 2, [-1, 1, 0]),
(0, 2, [0, -1, 0]),
],
}
}
pub fn hxl() -> Self {
Self {
name: "hxl".to_string(),
nodes: vec![[0.0, 0.0, 0.0]],
edges: vec![(0, 0, [1, 0, 0]), (0, 0, [0, 1, 0]), (0, 0, [1, -1, 0])],
}
}
pub fn pillared_sql() -> Self {
Self {
name: "pillared_sql".to_string(),
nodes: vec![[0.0, 0.0, 0.0]],
edges: vec![(0, 0, [1, 0, 0]), (0, 0, [0, 1, 0]), (0, 0, [0, 0, 1])],
}
}
pub fn from_name(name: &str) -> Option<Self> {
match name {
"pcu" => Some(Self::pcu()),
"dia" => Some(Self::dia()),
"sql" => Some(Self::sql()),
"bcu" => Some(Self::bcu()),
"fcu" => Some(Self::fcu()),
"nbo" => Some(Self::nbo()),
"pts" => Some(Self::pts()),
"kgm" => Some(Self::kgm()),
"hxl" => Some(Self::hxl()),
"pillared_sql" => Some(Self::pillared_sql()),
_ => None,
}
}
}
impl CrystalStructure {
pub fn num_atoms(&self) -> usize {
self.atoms.len()
}
pub fn cartesian_coords(&self) -> Vec<[f64; 3]> {
self.atoms
.iter()
.map(|a| self.cell.frac_to_cart(a.frac_coords))
.collect()
}
pub fn elements(&self) -> Vec<u8> {
self.atoms.iter().map(|a| a.element).collect()
}
pub fn make_supercell(&self, na: usize, nb: usize, nc: usize) -> CrystalStructure {
let (new_cell, offsets) = self.cell.supercell(na, nb, nc);
let mut new_atoms = Vec::new();
let mut new_labels = Vec::new();
let na_f = na as f64;
let nb_f = nb as f64;
let nc_f = nc as f64;
for offset in &offsets {
for (i, atom) in self.atoms.iter().enumerate() {
new_atoms.push(CrystalAtom {
element: atom.element,
frac_coords: [
(atom.frac_coords[0] + offset[0]) / na_f,
(atom.frac_coords[1] + offset[1]) / nb_f,
(atom.frac_coords[2] + offset[2]) / nc_f,
],
});
new_labels.push(self.labels[i].clone());
}
}
CrystalStructure {
cell: new_cell,
atoms: new_atoms,
labels: new_labels,
}
}
}
pub fn assemble_framework(
node_sbu: &Sbu,
linker_sbu: &Sbu,
topology: &Topology,
cell: &UnitCell,
) -> CrystalStructure {
let mut atoms = Vec::new();
let mut labels = Vec::new();
for (ni, node_frac) in topology.nodes.iter().enumerate() {
let node_cart = cell.frac_to_cart(*node_frac);
for (ai, &elem) in node_sbu.elements.iter().enumerate() {
let atom_cart = [
node_cart[0] + node_sbu.positions[ai][0],
node_cart[1] + node_sbu.positions[ai][1],
node_cart[2] + node_sbu.positions[ai][2],
];
let frac = cell.cart_to_frac(atom_cart);
atoms.push(CrystalAtom {
element: elem,
frac_coords: frac,
});
labels.push(format!("node_{}", ni));
}
}
for (ei, &(ni, nj, offset)) in topology.edges.iter().enumerate() {
let node_i_frac = topology.nodes[ni];
let node_j_frac = [
topology.nodes[nj][0] + offset[0] as f64,
topology.nodes[nj][1] + offset[1] as f64,
topology.nodes[nj][2] + offset[2] as f64,
];
let node_i_cart = cell.frac_to_cart(node_i_frac);
let node_j_cart = cell.frac_to_cart(node_j_frac);
let mid = [
(node_i_cart[0] + node_j_cart[0]) / 2.0,
(node_i_cart[1] + node_j_cart[1]) / 2.0,
(node_i_cart[2] + node_j_cart[2]) / 2.0,
];
let dx = node_j_cart[0] - node_i_cart[0];
let dy = node_j_cart[1] - node_i_cart[1];
let dz = node_j_cart[2] - node_i_cart[2];
let dist = (dx * dx + dy * dy + dz * dz).sqrt();
if dist < 1e-12 {
continue;
}
let dir = [dx / dist, dy / dist, dz / dist];
let rot = rotation_from_x_to(dir);
for (ai, &elem) in linker_sbu.elements.iter().enumerate() {
let p = linker_sbu.positions[ai];
let rotated = apply_rotation(&rot, p);
let atom_cart = [
mid[0] + rotated[0],
mid[1] + rotated[1],
mid[2] + rotated[2],
];
let frac = cell.cart_to_frac(atom_cart);
atoms.push(CrystalAtom {
element: elem,
frac_coords: frac,
});
labels.push(format!("linker_{}", ei));
}
}
CrystalStructure {
cell: cell.clone(),
atoms,
labels,
}
}
fn rotation_from_x_to(target: [f64; 3]) -> [[f64; 3]; 3] {
let x = [1.0, 0.0, 0.0];
let dot = x[0] * target[0] + x[1] * target[1] + x[2] * target[2];
if dot > 1.0 - 1e-12 {
return [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
}
if dot < -1.0 + 1e-12 {
return [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0]];
}
let cross = [
x[1] * target[2] - x[2] * target[1],
x[2] * target[0] - x[0] * target[2],
x[0] * target[1] - x[1] * target[0],
];
let s = (cross[0].powi(2) + cross[1].powi(2) + cross[2].powi(2)).sqrt();
let c = dot;
let v = cross;
let k = [[0.0, -v[2], v[1]], [v[2], 0.0, -v[0]], [-v[1], v[0], 0.0]];
let k2 = mat_mul_3x3(k, k);
let factor = (1.0 - c) / (s * s);
[
[
1.0 + k[0][0] + k2[0][0] * factor,
k[0][1] + k2[0][1] * factor,
k[0][2] + k2[0][2] * factor,
],
[
k[1][0] + k2[1][0] * factor,
1.0 + k[1][1] + k2[1][1] * factor,
k[1][2] + k2[1][2] * factor,
],
[
k[2][0] + k2[2][0] * factor,
k[2][1] + k2[2][1] * factor,
1.0 + k[2][2] + k2[2][2] * factor,
],
]
}
fn mat_mul_3x3(a: [[f64; 3]; 3], b: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
let mut r = [[0.0; 3]; 3];
for i in 0..3 {
for j in 0..3 {
r[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j];
}
}
r
}
fn apply_rotation(rot: &[[f64; 3]; 3], p: [f64; 3]) -> [f64; 3] {
[
rot[0][0] * p[0] + rot[0][1] * p[1] + rot[0][2] * p[2],
rot[1][0] * p[0] + rot[1][1] * p[1] + rot[1][2] * p[2],
rot[2][0] * p[0] + rot[2][1] * p[1] + rot[2][2] * p[2],
]
}
#[cfg(test)]
mod tests {
use super::super::sbu::{CoordinationGeometry, Sbu};
use super::*;
#[test]
fn test_assemble_simple_cubic_framework() {
let node = Sbu::metal_node(30, 0.0, CoordinationGeometry::Octahedral); let linker = Sbu::linear_linker(&[6, 6], 1.4, "carboxylate");
let topo = Topology::pcu();
let cell = UnitCell::cubic(10.0);
let structure = assemble_framework(&node, &linker, &topo, &cell);
assert_eq!(structure.num_atoms(), 7);
assert!(structure
.atoms
.iter()
.all(|a| a.element == 30 || a.element == 6));
}
#[test]
fn test_assemble_diamond_topology() {
let node = Sbu::metal_node(14, 0.0, CoordinationGeometry::Tetrahedral); let linker = Sbu::linear_linker(&[8], 1.0, "bridge"); let topo = Topology::dia();
let cell = UnitCell::cubic(5.43);
let structure = assemble_framework(&node, &linker, &topo, &cell);
assert_eq!(structure.num_atoms(), 6);
}
#[test]
fn test_supercell_atom_count() {
let node = Sbu::metal_node(30, 0.0, CoordinationGeometry::Octahedral);
let linker = Sbu::linear_linker(&[6, 6], 1.4, "carboxylate");
let topo = Topology::pcu();
let cell = UnitCell::cubic(10.0);
let structure = assemble_framework(&node, &linker, &topo, &cell);
let n_base = structure.num_atoms();
let super_struct = structure.make_supercell(2, 2, 2);
assert_eq!(super_struct.num_atoms(), n_base * 8);
}
#[test]
fn test_topology_pcu() {
let t = Topology::pcu();
assert_eq!(t.nodes.len(), 1);
assert_eq!(t.edges.len(), 3);
assert_eq!(t.name, "pcu");
}
#[test]
fn test_topology_dia() {
let t = Topology::dia();
assert_eq!(t.nodes.len(), 2);
assert_eq!(t.edges.len(), 4);
}
#[test]
fn test_rotation_identity() {
let rot = rotation_from_x_to([1.0, 0.0, 0.0]);
let p = [1.0, 2.0, 3.0];
let rp = apply_rotation(&rot, p);
for i in 0..3 {
assert!((rp[i] - p[i]).abs() < 1e-10);
}
}
#[test]
fn test_rotation_to_y() {
let rot = rotation_from_x_to([0.0, 1.0, 0.0]);
let p = [1.0, 0.0, 0.0];
let rp = apply_rotation(&rot, p);
assert!((rp[0]).abs() < 1e-10, "x should be ~0, got {:.6}", rp[0]);
assert!(
(rp[1] - 1.0).abs() < 1e-10,
"y should be ~1, got {:.6}",
rp[1]
);
assert!((rp[2]).abs() < 1e-10, "z should be ~0, got {:.6}", rp[2]);
}
#[test]
fn test_crystal_cartesian_coords() {
let cell = UnitCell::cubic(10.0);
let structure = CrystalStructure {
cell,
atoms: vec![
CrystalAtom {
element: 6,
frac_coords: [0.5, 0.0, 0.0],
},
CrystalAtom {
element: 8,
frac_coords: [0.0, 0.5, 0.0],
},
],
labels: vec!["a".into(), "b".into()],
};
let coords = structure.cartesian_coords();
assert!((coords[0][0] - 5.0).abs() < 1e-10);
assert!((coords[1][1] - 5.0).abs() < 1e-10);
}
#[test]
fn test_topology_bcu() {
let t = Topology::bcu();
assert_eq!(t.nodes.len(), 2);
assert_eq!(t.edges.len(), 4);
assert_eq!(t.name, "bcu");
}
#[test]
fn test_topology_fcu() {
let t = Topology::fcu();
assert_eq!(t.nodes.len(), 4);
assert_eq!(t.edges.len(), 15);
assert_eq!(t.name, "fcu");
}
#[test]
fn test_topology_nbo() {
let t = Topology::nbo();
assert_eq!(t.nodes.len(), 2);
assert_eq!(t.edges.len(), 4);
assert_eq!(t.name, "nbo");
}
#[test]
fn test_topology_pts() {
let t = Topology::pts();
assert_eq!(t.nodes.len(), 2);
assert_eq!(t.edges.len(), 4);
assert_eq!(t.name, "pts");
}
#[test]
fn test_topology_kgm() {
let t = Topology::kgm();
assert_eq!(t.nodes.len(), 3);
assert_eq!(t.edges.len(), 6);
assert_eq!(t.name, "kgm");
}
#[test]
fn test_topology_from_name() {
assert!(Topology::from_name("pcu").is_some());
assert!(Topology::from_name("dia").is_some());
assert!(Topology::from_name("fcu").is_some());
assert!(Topology::from_name("bcu").is_some());
assert!(Topology::from_name("nbo").is_some());
assert!(Topology::from_name("pts").is_some());
assert!(Topology::from_name("kgm").is_some());
assert!(Topology::from_name("hxl").is_some());
assert!(Topology::from_name("pillared_sql").is_some());
assert!(Topology::from_name("unknown").is_none());
}
#[test]
fn test_assemble_fcu_framework() {
let node = Sbu::metal_node(40, 0.0, CoordinationGeometry::Octahedral); let linker = Sbu::linear_linker(&[6, 6], 1.4, "bdc");
let topo = Topology::fcu();
let cell = UnitCell::cubic(20.0);
let structure = assemble_framework(&node, &linker, &topo, &cell);
assert_eq!(structure.num_atoms(), 34);
}
#[test]
fn test_assemble_bcu_framework() {
let node = Sbu::metal_node(40, 0.0, CoordinationGeometry::Octahedral);
let linker = Sbu::linear_linker(&[6], 1.0, "bridge");
let topo = Topology::bcu();
let cell = UnitCell::cubic(15.0);
let structure = assemble_framework(&node, &linker, &topo, &cell);
assert_eq!(structure.num_atoms(), 6);
}
}