#![allow(clippy::indexing_slicing, clippy::needless_range_loop)]
use serde::{Deserialize, Serialize};
use crate::{Error, Lattice, Structure};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Alignment {
pub rotation: [[f64; 3]; 3],
pub translation: [f64; 3],
pub rmsd: f64,
pub aligned: Vec<[f64; 3]>,
}
pub fn kabsch_alignment(points: &[[f64; 3]], reference: &[[f64; 3]]) -> Result<Alignment, Error> {
if points.len() != reference.len() {
return Err(Error::InvalidInput(format!(
"alignment needs the same number of points, got {} and {}",
points.len(),
reference.len()
)));
}
if points.len() < 2 {
return Err(Error::InvalidInput(
"alignment needs at least two points".to_owned(),
));
}
if !points
.iter()
.chain(reference)
.flatten()
.all(|value| value.is_finite())
{
return Err(Error::InvalidInput(
"alignment points must be finite".to_owned(),
));
}
let first_centre = centroid(points);
let second_centre = centroid(reference);
let mut covariance = [[0.0; 3]; 3];
for (point, target) in points.iter().zip(reference) {
let a = subtract(*point, first_centre);
let b = subtract(*target, second_centre);
for row in 0..3 {
for column in 0..3 {
if let (Some(value), Some(other)) = (a.get(row), b.get(column))
&& let Some(entry) = covariance.get_mut(row).and_then(|r| r.get_mut(column))
{
*entry += value * other;
}
}
}
}
let rotation = rotation_from_covariance(covariance);
let rotated_centre = apply(rotation, first_centre);
let translation = [
second_centre[0] - rotated_centre[0],
second_centre[1] - rotated_centre[1],
second_centre[2] - rotated_centre[2],
];
let mut aligned = Vec::with_capacity(points.len());
let mut squared = 0.0;
for (point, target) in points.iter().zip(reference) {
let moved = apply(rotation, *point);
let moved = [
moved[0] + translation[0],
moved[1] + translation[1],
moved[2] + translation[2],
];
let delta = subtract(moved, *target);
squared += dot(delta, delta);
aligned.push(moved);
}
Ok(Alignment {
rotation,
translation,
rmsd: (squared / points.len() as f64).sqrt(),
aligned,
})
}
pub fn periodic_rmsd(
first: &Structure,
second: &Structure,
mapping: &[usize],
) -> Result<f64, Error> {
if mapping.is_empty() {
return Err(Error::InvalidInput(
"periodic RMSD needs at least one mapped atom".to_owned(),
));
}
if !first.lattice().almost_equal(second.lattice(), 1e-6) {
return Err(Error::InvalidInput(
"periodic RMSD needs the same cell; use the structure matcher first".to_owned(),
));
}
let first_cell = first.p1()?;
let second_cell = second.p1()?;
if mapping.len() != first_cell.atoms.len() {
return Err(Error::InvalidInput(format!(
"mapping has {} entries for {} atoms",
mapping.len(),
first_cell.atoms.len()
)));
}
let mut squared = 0.0;
for (atom, &target) in first_cell.atoms.iter().zip(mapping) {
let Some(other) = second_cell.atoms.get(target) else {
return Err(Error::InvalidInput(format!(
"mapping index {target} is out of range"
)));
};
let delta = subtract(other.frac, atom.frac);
let (_, distance) = first.lattice().minimum_image(delta);
squared += distance * distance;
}
Ok((squared / mapping.len() as f64).sqrt())
}
pub fn interpolate(
first: &Structure,
second: &Structure,
images: u32,
) -> Result<Vec<Structure>, Error> {
if images == 0 {
return Err(Error::InvalidInput(
"interpolation needs at least one image".to_owned(),
));
}
let start = first.p1()?;
let end = second.p1()?;
if start.atoms.len() != end.atoms.len() {
return Err(Error::InvalidInput(format!(
"interpolation needs the same atom count, got {} and {}",
start.atoms.len(),
end.atoms.len()
)));
}
for (a, b) in start.atoms.iter().zip(&end.atoms) {
if a.element != b.element {
return Err(Error::InvalidInput(format!(
"atom {} is {} in the first structure and {} in the second",
a.label, a.element, b.element
)));
}
}
let matrix = start.lattice.matrix();
let end_matrix = end.lattice.matrix();
let mut frames = Vec::with_capacity(images as usize);
for frame in 1..=images {
let t = f64::from(frame) / f64::from(images + 1);
let mut lattice_rows = [[0.0; 3]; 3];
for row in 0..3 {
for column in 0..3 {
let a = matrix[row][column];
let b = end_matrix[row][column];
lattice_rows[row][column] = a + (b - a) * t;
}
}
let lattice = Lattice::new(lattice_rows)?;
let mut builder = crate::StructureBuilder::new(first.name(), lattice);
for (a, b) in start.atoms.iter().zip(&end.atoms) {
let (delta, _) = start.lattice.minimum_image(subtract(b.frac, a.frac));
let frac = [
a.frac[0] + delta[0] * t,
a.frac[1] + delta[1] * t,
a.frac[2] + delta[2] * t,
];
let site = crate::Site::new(a.label.clone(), &a.element, Lattice::wrap(frac))?;
builder = builder.site(site.with_occupancy(a.occupancy)?);
}
frames.push(builder.build()?);
}
Ok(frames)
}
fn rotation_from_covariance(s: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
let [[sxx, sxy, sxz], [syx, syy, syz], [szx, szy, szz]] = s;
let matrix = [
[sxx + syy + szz, syz - szy, szx - sxz, sxy - syx],
[syz - szy, sxx - syy - szz, sxy + syx, szx + sxz],
[szx - sxz, sxy + syx, -sxx + syy - szz, syz + szy],
[sxy - syx, szx + sxz, syz + szy, -sxx - syy + szz],
];
let (values, vectors) = jacobi4(matrix);
let mut best = 0;
for index in 1..4 {
if values.get(index).copied().unwrap_or(f64::NEG_INFINITY)
> values.get(best).copied().unwrap_or(f64::NEG_INFINITY)
{
best = index;
}
}
let quaternion = [
vectors[0][best],
vectors[1][best],
vectors[2][best],
vectors[3][best],
];
quaternion_matrix(quaternion)
}
fn jacobi4(mut matrix: [[f64; 4]; 4]) -> ([f64; 4], [[f64; 4]; 4]) {
let mut vectors = [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
];
for _ in 0..64 {
let mut p = 0;
let mut q = 1;
let mut largest = 0.0;
for row in 0..4 {
for column in (row + 1)..4 {
let value = matrix[row][column].abs();
if value > largest {
largest = value;
p = row;
q = column;
}
}
}
if largest < 1e-14 {
break;
}
let theta = (matrix[q][q] - matrix[p][p]) / (2.0 * matrix[p][q]);
let t = theta.signum() / (theta.abs() + (theta * theta + 1.0).sqrt());
let c = 1.0 / (t * t + 1.0).sqrt();
let s = t * c;
let mut next = matrix;
for k in 0..4 {
next[p][k] = c * matrix[p][k] - s * matrix[q][k];
next[q][k] = s * matrix[p][k] + c * matrix[q][k];
}
matrix = next;
let mut transposed = matrix;
for k in 0..4 {
transposed[k][p] = c * matrix[k][p] - s * matrix[k][q];
transposed[k][q] = s * matrix[k][p] + c * matrix[k][q];
}
matrix = transposed;
for k in 0..4 {
let a = vectors[k][p];
let b = vectors[k][q];
vectors[k][p] = c * a - s * b;
vectors[k][q] = s * a + c * b;
}
}
(
[matrix[0][0], matrix[1][1], matrix[2][2], matrix[3][3]],
vectors,
)
}
fn quaternion_matrix(quaternion: [f64; 4]) -> [[f64; 3]; 3] {
let norm = (quaternion[0] * quaternion[0]
+ quaternion[1] * quaternion[1]
+ quaternion[2] * quaternion[2]
+ quaternion[3] * quaternion[3])
.sqrt();
if norm < 1e-12 {
return [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
}
let [w, x, y, z] = quaternion.map(|value| value / norm);
[
[
1.0 - 2.0 * (y * y + z * z),
2.0 * (x * y - z * w),
2.0 * (x * z + y * w),
],
[
2.0 * (x * y + z * w),
1.0 - 2.0 * (x * x + z * z),
2.0 * (y * z - x * w),
],
[
2.0 * (x * z - y * w),
2.0 * (y * z + x * w),
1.0 - 2.0 * (x * x + y * y),
],
]
}
fn centroid(points: &[[f64; 3]]) -> [f64; 3] {
let mut total = [0.0; 3];
for point in points {
total[0] += point[0];
total[1] += point[1];
total[2] += point[2];
}
let count = points.len().max(1) as f64;
[total[0] / count, total[1] / count, total[2] / count]
}
fn apply(matrix: [[f64; 3]; 3], point: [f64; 3]) -> [f64; 3] {
[
matrix[0][0] * point[0] + matrix[0][1] * point[1] + matrix[0][2] * point[2],
matrix[1][0] * point[0] + matrix[1][1] * point[1] + matrix[1][2] * point[2],
matrix[2][0] * point[0] + matrix[2][1] * point[1] + matrix[2][2] * point[2],
]
}
fn subtract(first: [f64; 3], second: [f64; 3]) -> [f64; 3] {
[
first[0] - second[0],
first[1] - second[1],
first[2] - second[2],
]
}
fn dot(first: [f64; 3], second: [f64; 3]) -> f64 {
first[0] * second[0] + first[1] * second[1] + first[2] * second[2]
}