#![allow(clippy::indexing_slicing, clippy::needless_range_loop)]
use serde::{Deserialize, Serialize};
use crate::polyhedron::{HalfSpace, Polyhedron};
use crate::{Error, Structure, neighbors};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct VoronoiNeighbor {
pub index: usize,
pub image: [i32; 3],
pub distance: f64,
pub area: f64,
pub solid_angle: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct VoronoiCell {
pub index: usize,
pub volume: f64,
pub neighbors: Vec<VoronoiNeighbor>,
pub vertices: Vec<[f64; 3]>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct VoronoiReport {
pub cells: Vec<VoronoiCell>,
pub total_volume: f64,
}
pub fn voronoi(structure: &Structure, cutoff: f64) -> Result<VoronoiReport, Error> {
if !cutoff.is_finite() || cutoff <= 0.0 {
return Err(Error::InvalidInput(format!(
"Voronoi cutoff must be positive and finite, got {cutoff}"
)));
}
let p1 = structure.p1()?;
if p1.atoms.len() < 2 {
return Err(Error::InvalidInput(
"Voronoi tessellation needs at least two atoms".to_owned(),
));
}
let matrix = p1.lattice.matrix();
let cart: Vec<[f64; 3]> = p1
.atoms
.iter()
.map(|atom| p1.lattice.to_cart(atom.frac))
.collect();
let pairs = neighbors(&p1_to_structure(&p1)?, cutoff)?;
let mut adjacency: Vec<Vec<(usize, [i32; 3], f64)>> = vec![Vec::new(); p1.atoms.len()];
for pair in pairs {
let Some(first) = adjacency.get_mut(pair.i) else {
continue;
};
first.push((pair.j, pair.image, pair.distance));
let Some(second) = adjacency.get_mut(pair.j) else {
continue;
};
second.push((
pair.i,
[-pair.image[0], -pair.image[1], -pair.image[2]],
pair.distance,
));
}
let mut cells = Vec::with_capacity(p1.atoms.len());
let mut total_volume = 0.0;
for index in 0..p1.atoms.len() {
let Some(¢re) = cart.get(index) else {
continue;
};
let Some(entries) = adjacency.get(index) else {
continue;
};
if entries.is_empty() {
return Err(Error::InvalidInput(format!(
"atom {index} has no neighbor within {cutoff} Angstrom"
)));
}
let mut planes = Vec::with_capacity(entries.len());
let mut vectors = Vec::with_capacity(entries.len());
for &(other, image, distance) in entries {
let Some(&target) = cart.get(other) else {
continue;
};
let shifted = [
target[0]
+ f64::from(image[0]) * matrix[0][0]
+ f64::from(image[1]) * matrix[1][0]
+ f64::from(image[2]) * matrix[2][0],
target[1]
+ f64::from(image[0]) * matrix[0][1]
+ f64::from(image[1]) * matrix[1][1]
+ f64::from(image[2]) * matrix[2][1],
target[2]
+ f64::from(image[0]) * matrix[0][2]
+ f64::from(image[1]) * matrix[1][2]
+ f64::from(image[2]) * matrix[2][2],
];
let normal = [
shifted[0] - centre[0],
shifted[1] - centre[1],
shifted[2] - centre[2],
];
let plane = HalfSpace::new(normal, 0.5 * distance * distance)?;
planes.push(plane);
vectors.push((
HalfSpace::new(
[
normal[0] / distance,
normal[1] / distance,
normal[2] / distance,
],
0.5 * distance,
)?,
other,
image,
distance,
));
}
let polyhedron = Polyhedron::from_half_spaces(&planes)?;
let volume = polyhedron.volume();
total_volume += volume;
let mut faces = Vec::new();
for (face, _) in polyhedron.faces.iter().enumerate() {
let Some(normal) = polyhedron.face_normal(face) else {
continue;
};
let mut best: Option<(f64, usize)> = None;
for (position, (plane, _, _, _)) in vectors.iter().enumerate() {
let cosine = normal[0] * plane.normal[0]
+ normal[1] * plane.normal[1]
+ normal[2] * plane.normal[2];
if cosine > best.map_or(0.999, |(value, _)| value) {
best = Some((cosine, position));
}
}
let Some((cosine, position)) = best else {
continue;
};
if cosine < 0.9 {
continue;
}
let Some((_, other, image, distance)) = vectors.get(position) else {
continue;
};
faces.push(VoronoiNeighbor {
index: *other,
image: *image,
distance: *distance,
area: polyhedron.face_area(face),
solid_angle: polyhedron.face_solid_angle(face, centre),
});
}
faces.sort_by(|first, second| {
second
.solid_angle
.partial_cmp(&first.solid_angle)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| (first.index, first.image).cmp(&(second.index, second.image)))
});
cells.push(VoronoiCell {
index,
volume,
neighbors: faces,
vertices: polyhedron.vertices.clone(),
});
}
Ok(VoronoiReport {
cells,
total_volume,
})
}
fn p1_to_structure(p1: &crate::P1Cell) -> Result<Structure, Error> {
let mut builder = crate::StructureBuilder::new("p1", p1.lattice);
for atom in &p1.atoms {
builder = builder.site(
crate::Site::new(atom.label.clone(), &atom.element, atom.frac)?
.with_occupancy(atom.occupancy)?,
);
}
builder.build()
}