use crate::{Error, Structure};
pub fn distance(structure: &Structure, i: usize, j: usize) -> Result<f64, Error> {
let p1 = structure.p1()?;
let first = p1
.atoms
.get(i)
.ok_or_else(|| index_error(i, p1.atoms.len()))?;
let second = p1
.atoms
.get(j)
.ok_or_else(|| index_error(j, p1.atoms.len()))?;
Ok(p1.lattice.distance(first.frac, second.frac))
}
pub fn angle(structure: &Structure, i: usize, j: usize, k: usize) -> Result<f64, Error> {
let p1 = structure.p1()?;
let pivot = p1
.atoms
.get(j)
.ok_or_else(|| index_error(j, p1.atoms.len()))?;
let first = p1
.atoms
.get(i)
.ok_or_else(|| index_error(i, p1.atoms.len()))?;
let second = p1
.atoms
.get(k)
.ok_or_else(|| index_error(k, p1.atoms.len()))?;
let first_vector = p1
.lattice
.to_cart(p1.lattice.minimum_image(subtract(first.frac, pivot.frac)).0);
let second_vector = p1.lattice.to_cart(
p1.lattice
.minimum_image(subtract(second.frac, pivot.frac))
.0,
);
let first_norm = norm(first_vector);
let second_norm = norm(second_vector);
if first_norm < 1e-12 || second_norm < 1e-12 {
return Err(Error::InvalidInput(
"angle has a zero-length arm".to_owned(),
));
}
let cosine = dot(first_vector, second_vector) / (first_norm * second_norm);
Ok(cosine.clamp(-1.0, 1.0).acos().to_degrees())
}
pub fn torsion(
structure: &Structure,
i: usize,
j: usize,
k: usize,
l: usize,
) -> Result<f64, Error> {
let p1 = structure.p1()?;
let point = |index: usize| {
p1.atoms
.get(index)
.map(|atom| atom.frac)
.ok_or_else(|| index_error(index, p1.atoms.len()))
};
let fi = point(i)?;
let fj = point(j)?;
let fk = point(k)?;
let fl = point(l)?;
let b1 = p1
.lattice
.to_cart(p1.lattice.minimum_image(subtract(fj, fi)).0);
let b2 = p1
.lattice
.to_cart(p1.lattice.minimum_image(subtract(fk, fj)).0);
let b3 = p1
.lattice
.to_cart(p1.lattice.minimum_image(subtract(fl, fk)).0);
let n1 = cross(b1, b2);
let n2 = cross(b2, b3);
let b2_norm = norm(b2);
if norm(n1) < 1e-12 || norm(n2) < 1e-12 || b2_norm < 1e-12 {
return Err(Error::InvalidInput(
"torsion has a collinear segment".to_owned(),
));
}
let m = cross(n1, scale(b2, 1.0 / b2_norm));
let x = dot(n1, n2);
let y = dot(m, n2);
Ok(y.atan2(x).to_degrees())
}
pub fn centroid(points: &[[f64; 3]]) -> Result<[f64; 3], Error> {
if points.is_empty() {
return Err(Error::InvalidInput(
"a centroid needs at least one point".to_owned(),
));
}
let mut sum = [0.0; 3];
for point in points {
sum[0] += point[0];
sum[1] += point[1];
sum[2] += point[2];
}
let count = points.len() as f64;
Ok([sum[0] / count, sum[1] / count, sum[2] / count])
}
pub fn interplanar_angle(first_normal: [f64; 3], second_normal: [f64; 3]) -> Result<f64, Error> {
let first_norm = norm(first_normal);
let second_norm = norm(second_normal);
if first_norm < 1e-12 || second_norm < 1e-12 {
return Err(Error::InvalidInput(
"a plane normal has zero length".to_owned(),
));
}
let cosine = dot(first_normal, second_normal) / (first_norm * second_norm);
Ok(cosine.clamp(-1.0, 1.0).acos().to_degrees())
}
fn index_error(index: usize, length: usize) -> Error {
Error::InvalidInput(format!(
"atom index {index} is outside the P1 cell (length {length})"
))
}
fn subtract(first: [f64; 3], second: [f64; 3]) -> [f64; 3] {
[
first[0] - second[0],
first[1] - second[1],
first[2] - second[2],
]
}
fn scale(vector: [f64; 3], factor: f64) -> [f64; 3] {
[vector[0] * factor, vector[1] * factor, vector[2] * factor]
}
fn cross(first: [f64; 3], second: [f64; 3]) -> [f64; 3] {
[
first[1] * second[2] - first[2] * second[1],
first[2] * second[0] - first[0] * second[2],
first[0] * second[1] - first[1] * second[0],
]
}
fn dot(first: [f64; 3], second: [f64; 3]) -> f64 {
first[0] * second[0] + first[1] * second[1] + first[2] * second[2]
}
fn norm(vector: [f64; 3]) -> f64 {
dot(vector, vector).sqrt()
}