1#![allow(clippy::indexing_slicing, clippy::needless_range_loop)] use serde::{Deserialize, Serialize};
10
11use crate::{Error, Lattice, Structure};
12
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15pub struct Alignment {
16 pub rotation: [[f64; 3]; 3],
18 pub translation: [f64; 3],
20 pub rmsd: f64,
22 pub aligned: Vec<[f64; 3]>,
24}
25
26pub fn kabsch_alignment(points: &[[f64; 3]], reference: &[[f64; 3]]) -> Result<Alignment, Error> {
36 if points.len() != reference.len() {
37 return Err(Error::InvalidInput(format!(
38 "alignment needs the same number of points, got {} and {}",
39 points.len(),
40 reference.len()
41 )));
42 }
43 if points.len() < 2 {
44 return Err(Error::InvalidInput(
45 "alignment needs at least two points".to_owned(),
46 ));
47 }
48 if !points
49 .iter()
50 .chain(reference)
51 .flatten()
52 .all(|value| value.is_finite())
53 {
54 return Err(Error::InvalidInput(
55 "alignment points must be finite".to_owned(),
56 ));
57 }
58 let first_centre = centroid(points);
59 let second_centre = centroid(reference);
60 let mut covariance = [[0.0; 3]; 3];
61 for (point, target) in points.iter().zip(reference) {
62 let a = subtract(*point, first_centre);
63 let b = subtract(*target, second_centre);
64 for row in 0..3 {
65 for column in 0..3 {
66 if let (Some(value), Some(other)) = (a.get(row), b.get(column))
67 && let Some(entry) = covariance.get_mut(row).and_then(|r| r.get_mut(column))
68 {
69 *entry += value * other;
70 }
71 }
72 }
73 }
74 let rotation = rotation_from_covariance(covariance);
75 let rotated_centre = apply(rotation, first_centre);
76 let translation = [
77 second_centre[0] - rotated_centre[0],
78 second_centre[1] - rotated_centre[1],
79 second_centre[2] - rotated_centre[2],
80 ];
81 let mut aligned = Vec::with_capacity(points.len());
82 let mut squared = 0.0;
83 for (point, target) in points.iter().zip(reference) {
84 let moved = apply(rotation, *point);
85 let moved = [
86 moved[0] + translation[0],
87 moved[1] + translation[1],
88 moved[2] + translation[2],
89 ];
90 let delta = subtract(moved, *target);
91 squared += dot(delta, delta);
92 aligned.push(moved);
93 }
94 Ok(Alignment {
95 rotation,
96 translation,
97 rmsd: (squared / points.len() as f64).sqrt(),
98 aligned,
99 })
100}
101
102pub fn periodic_rmsd(
112 first: &Structure,
113 second: &Structure,
114 mapping: &[usize],
115) -> Result<f64, Error> {
116 if mapping.is_empty() {
117 return Err(Error::InvalidInput(
118 "periodic RMSD needs at least one mapped atom".to_owned(),
119 ));
120 }
121 if !first.lattice().almost_equal(second.lattice(), 1e-6) {
122 return Err(Error::InvalidInput(
123 "periodic RMSD needs the same cell; use the structure matcher first".to_owned(),
124 ));
125 }
126 let first_cell = first.p1()?;
127 let second_cell = second.p1()?;
128 if mapping.len() != first_cell.atoms.len() {
129 return Err(Error::InvalidInput(format!(
130 "mapping has {} entries for {} atoms",
131 mapping.len(),
132 first_cell.atoms.len()
133 )));
134 }
135 let mut squared = 0.0;
136 for (atom, &target) in first_cell.atoms.iter().zip(mapping) {
137 let Some(other) = second_cell.atoms.get(target) else {
138 return Err(Error::InvalidInput(format!(
139 "mapping index {target} is out of range"
140 )));
141 };
142 let delta = subtract(other.frac, atom.frac);
143 let (_, distance) = first.lattice().minimum_image(delta);
144 squared += distance * distance;
145 }
146 Ok((squared / mapping.len() as f64).sqrt())
147}
148
149pub fn interpolate(
159 first: &Structure,
160 second: &Structure,
161 images: u32,
162) -> Result<Vec<Structure>, Error> {
163 if images == 0 {
164 return Err(Error::InvalidInput(
165 "interpolation needs at least one image".to_owned(),
166 ));
167 }
168 let start = first.p1()?;
169 let end = second.p1()?;
170 if start.atoms.len() != end.atoms.len() {
171 return Err(Error::InvalidInput(format!(
172 "interpolation needs the same atom count, got {} and {}",
173 start.atoms.len(),
174 end.atoms.len()
175 )));
176 }
177 for (a, b) in start.atoms.iter().zip(&end.atoms) {
178 if a.element != b.element {
179 return Err(Error::InvalidInput(format!(
180 "atom {} is {} in the first structure and {} in the second",
181 a.label, a.element, b.element
182 )));
183 }
184 }
185 let matrix = start.lattice.matrix();
186 let end_matrix = end.lattice.matrix();
187 let mut frames = Vec::with_capacity(images as usize);
188 for frame in 1..=images {
189 let t = f64::from(frame) / f64::from(images + 1);
190 let mut lattice_rows = [[0.0; 3]; 3];
191 for row in 0..3 {
192 for column in 0..3 {
193 let a = matrix[row][column];
194 let b = end_matrix[row][column];
195 lattice_rows[row][column] = a + (b - a) * t;
196 }
197 }
198 let lattice = Lattice::new(lattice_rows)?;
199 let mut builder = crate::StructureBuilder::new(first.name(), lattice);
200 for (a, b) in start.atoms.iter().zip(&end.atoms) {
201 let (delta, _) = start.lattice.minimum_image(subtract(b.frac, a.frac));
202 let frac = [
203 a.frac[0] + delta[0] * t,
204 a.frac[1] + delta[1] * t,
205 a.frac[2] + delta[2] * t,
206 ];
207 let site = crate::Site::new(a.label.clone(), &a.element, Lattice::wrap(frac))?;
208 builder = builder.site(site.with_occupancy(a.occupancy)?);
209 }
210 frames.push(builder.build()?);
211 }
212 Ok(frames)
213}
214
215fn rotation_from_covariance(s: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
217 let [[sxx, sxy, sxz], [syx, syy, syz], [szx, szy, szz]] = s;
218 let matrix = [
219 [sxx + syy + szz, syz - szy, szx - sxz, sxy - syx],
220 [syz - szy, sxx - syy - szz, sxy + syx, szx + sxz],
221 [szx - sxz, sxy + syx, -sxx + syy - szz, syz + szy],
222 [sxy - syx, szx + sxz, syz + szy, -sxx - syy + szz],
223 ];
224 let (values, vectors) = jacobi4(matrix);
225 let mut best = 0;
226 for index in 1..4 {
227 if values.get(index).copied().unwrap_or(f64::NEG_INFINITY)
228 > values.get(best).copied().unwrap_or(f64::NEG_INFINITY)
229 {
230 best = index;
231 }
232 }
233 let quaternion = [
234 vectors[0][best],
235 vectors[1][best],
236 vectors[2][best],
237 vectors[3][best],
238 ];
239 quaternion_matrix(quaternion)
240}
241
242fn jacobi4(mut matrix: [[f64; 4]; 4]) -> ([f64; 4], [[f64; 4]; 4]) {
243 let mut vectors = [
244 [1.0, 0.0, 0.0, 0.0],
245 [0.0, 1.0, 0.0, 0.0],
246 [0.0, 0.0, 1.0, 0.0],
247 [0.0, 0.0, 0.0, 1.0],
248 ];
249 for _ in 0..64 {
250 let mut p = 0;
251 let mut q = 1;
252 let mut largest = 0.0;
253 for row in 0..4 {
254 for column in (row + 1)..4 {
255 let value = matrix[row][column].abs();
256 if value > largest {
257 largest = value;
258 p = row;
259 q = column;
260 }
261 }
262 }
263 if largest < 1e-14 {
264 break;
265 }
266 let theta = (matrix[q][q] - matrix[p][p]) / (2.0 * matrix[p][q]);
267 let t = theta.signum() / (theta.abs() + (theta * theta + 1.0).sqrt());
268 let c = 1.0 / (t * t + 1.0).sqrt();
269 let s = t * c;
270 let mut next = matrix;
271 for k in 0..4 {
272 next[p][k] = c * matrix[p][k] - s * matrix[q][k];
273 next[q][k] = s * matrix[p][k] + c * matrix[q][k];
274 }
275 matrix = next;
276 let mut transposed = matrix;
277 for k in 0..4 {
278 transposed[k][p] = c * matrix[k][p] - s * matrix[k][q];
279 transposed[k][q] = s * matrix[k][p] + c * matrix[k][q];
280 }
281 matrix = transposed;
282 for k in 0..4 {
283 let a = vectors[k][p];
284 let b = vectors[k][q];
285 vectors[k][p] = c * a - s * b;
286 vectors[k][q] = s * a + c * b;
287 }
288 }
289 (
290 [matrix[0][0], matrix[1][1], matrix[2][2], matrix[3][3]],
291 vectors,
292 )
293}
294
295fn quaternion_matrix(quaternion: [f64; 4]) -> [[f64; 3]; 3] {
296 let norm = (quaternion[0] * quaternion[0]
297 + quaternion[1] * quaternion[1]
298 + quaternion[2] * quaternion[2]
299 + quaternion[3] * quaternion[3])
300 .sqrt();
301 if norm < 1e-12 {
302 return [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
303 }
304 let [w, x, y, z] = quaternion.map(|value| value / norm);
305 [
306 [
307 1.0 - 2.0 * (y * y + z * z),
308 2.0 * (x * y - z * w),
309 2.0 * (x * z + y * w),
310 ],
311 [
312 2.0 * (x * y + z * w),
313 1.0 - 2.0 * (x * x + z * z),
314 2.0 * (y * z - x * w),
315 ],
316 [
317 2.0 * (x * z - y * w),
318 2.0 * (y * z + x * w),
319 1.0 - 2.0 * (x * x + y * y),
320 ],
321 ]
322}
323
324fn centroid(points: &[[f64; 3]]) -> [f64; 3] {
325 let mut total = [0.0; 3];
326 for point in points {
327 total[0] += point[0];
328 total[1] += point[1];
329 total[2] += point[2];
330 }
331 let count = points.len().max(1) as f64;
332 [total[0] / count, total[1] / count, total[2] / count]
333}
334
335fn apply(matrix: [[f64; 3]; 3], point: [f64; 3]) -> [f64; 3] {
336 [
337 matrix[0][0] * point[0] + matrix[0][1] * point[1] + matrix[0][2] * point[2],
338 matrix[1][0] * point[0] + matrix[1][1] * point[1] + matrix[1][2] * point[2],
339 matrix[2][0] * point[0] + matrix[2][1] * point[1] + matrix[2][2] * point[2],
340 ]
341}
342
343fn subtract(first: [f64; 3], second: [f64; 3]) -> [f64; 3] {
344 [
345 first[0] - second[0],
346 first[1] - second[1],
347 first[2] - second[2],
348 ]
349}
350
351fn dot(first: [f64; 3], second: [f64; 3]) -> f64 {
352 first[0] * second[0] + first[1] * second[1] + first[2] * second[2]
353}