1use crate::shape_descriptors::jacobi3;
10
11#[derive(Debug, Clone)]
17pub struct AlignResult {
18 pub rmsd: f64,
20 pub rotation: [[f64; 3]; 3],
22 pub translation: [f64; 3],
24}
25
26pub fn rmsd_no_align(a: &[[f64; 3]], b: &[[f64; 3]]) -> f64 {
35 let n = a.len().min(b.len());
36 if n == 0 {
37 return 0.0;
38 }
39 let sum_sq: f64 = a
40 .iter()
41 .zip(b.iter())
42 .map(|(pa, pb)| (0..3).map(|i| (pa[i] - pb[i]).powi(2)).sum::<f64>())
43 .sum();
44 (sum_sq / n as f64).sqrt()
45}
46
47pub fn align_coords(reference: &[[f64; 3]], mobile: &[[f64; 3]]) -> AlignResult {
61 let n = reference.len().min(mobile.len());
62 if n == 0 {
63 return AlignResult {
64 rmsd: 0.0,
65 rotation: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
66 translation: [0.0, 0.0, 0.0],
67 };
68 }
69
70 let nf = n as f64;
71
72 let mut cr = [0.0f64; 3];
74 let mut cm = [0.0f64; 3];
75 for i in 0..n {
76 for k in 0..3 {
77 cr[k] += reference[i][k];
78 cm[k] += mobile[i][k];
79 }
80 }
81 for k in 0..3 {
82 cr[k] /= nf;
83 cm[k] /= nf;
84 }
85
86 let p: Vec<[f64; 3]> = reference
88 .iter()
89 .map(|v| [v[0] - cr[0], v[1] - cr[1], v[2] - cr[2]])
90 .collect();
91 let q: Vec<[f64; 3]> = mobile
92 .iter()
93 .map(|v| [v[0] - cm[0], v[1] - cm[1], v[2] - cm[2]])
94 .collect();
95
96 let mut h = [[0.0f64; 3]; 3];
98 for i in 0..n {
99 for r in 0..3 {
100 for c in 0..3 {
101 h[r][c] += p[i][r] * q[i][c];
102 }
103 }
104 }
105
106 let mut hth = [[0.0f64; 3]; 3];
108 for r in 0..3 {
109 for c in 0..3 {
110 for k in 0..3 {
111 hth[r][c] += h[k][r] * h[k][c];
112 }
113 }
114 }
115 let (evals, v) = jacobi3(hth);
116
117 let mut hv = [[0.0f64; 3]; 3];
119 for r in 0..3 {
120 for c in 0..3 {
121 for k in 0..3 {
122 hv[r][c] += h[r][k] * v[k][c];
123 }
124 }
125 }
126 let mut u = [[0.0f64; 3]; 3];
127 for j in 0..3 {
128 let sigma = evals[j].max(0.0).sqrt();
129 for r in 0..3 {
130 u[r][j] = if sigma > 1e-10 { hv[r][j] / sigma } else { 0.0 };
131 }
132 }
133
134 let mut rot = [[0.0f64; 3]; 3];
136 let mut v_final = v;
137 for r in 0..3 {
138 for c in 0..3 {
139 for k in 0..3 {
140 rot[r][c] += u[r][k] * v_final[c][k];
141 }
142 }
143 }
144
145 let det = det3(rot);
148 if det < 0.0 {
149 for r in 0..3 {
150 v_final[r][0] *= -1.0;
151 }
152 rot = [[0.0; 3]; 3];
153 for r in 0..3 {
154 for c in 0..3 {
155 for k in 0..3 {
156 rot[r][c] += u[r][k] * v_final[c][k];
157 }
158 }
159 }
160 }
161
162 let mut sum_sq = 0.0f64;
164 for i in 0..n {
165 for row in 0..3 {
166 let rotated = (0..3).map(|k| rot[row][k] * q[i][k]).sum::<f64>();
167 let diff = p[i][row] - rotated;
168 sum_sq += diff * diff;
169 }
170 }
171 let rmsd = (sum_sq / nf).sqrt();
172
173 let translation = [cr[0] - cm[0], cr[1] - cm[1], cr[2] - cm[2]];
175
176 AlignResult {
177 rmsd,
178 rotation: rot,
179 translation,
180 }
181}
182
183pub fn apply_alignment(mobile: &[[f64; 3]], result: &AlignResult) -> Vec<[f64; 3]> {
187 let n = mobile.len();
189 if n == 0 {
190 return Vec::new();
191 }
192 let mut cm = [0.0f64; 3];
193 for v in mobile {
194 for k in 0..3 {
195 cm[k] += v[k];
196 }
197 }
198 for k in 0..3 {
199 cm[k] /= n as f64;
200 }
201
202 let cr = [
207 cm[0] + result.translation[0],
208 cm[1] + result.translation[1],
209 cm[2] + result.translation[2],
210 ];
211
212 mobile
213 .iter()
214 .map(|v| {
215 let centered = [v[0] - cm[0], v[1] - cm[1], v[2] - cm[2]];
216 let mut out = [0.0f64; 3];
217 for row in 0..3 {
218 out[row] = (0..3)
219 .map(|k| result.rotation[row][k] * centered[k])
220 .sum::<f64>()
221 + cr[row];
222 }
223 out
224 })
225 .collect()
226}
227
228fn det3(m: [[f64; 3]; 3]) -> f64 {
229 m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
230 - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
231 + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
232}
233
234#[cfg(test)]
239mod tests {
240 use super::*;
241
242 fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
243 (a - b).abs() < tol
244 }
245
246 #[test]
247 fn test_rmsd_no_align_identical() {
248 let coords = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
249 assert!(approx_eq(rmsd_no_align(&coords, &coords), 0.0, 1e-10));
250 }
251
252 #[test]
253 fn test_rmsd_no_align_empty() {
254 assert_eq!(rmsd_no_align(&[], &[]), 0.0);
255 }
256
257 #[test]
258 fn test_rmsd_no_align_translated() {
259 let a = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
260 let b = vec![[1.0, 0.0, 0.0], [2.0, 0.0, 0.0]]; assert!(approx_eq(rmsd_no_align(&a, &b), 1.0, 1e-9));
263 }
264
265 #[test]
266 fn test_align_identical() {
267 let coords = vec![[0.0, 0.0, 0.0], [1.5, 0.0, 0.0], [0.75, 1.3, 0.0]];
268 let result = align_coords(&coords, &coords);
269 assert!(
270 approx_eq(result.rmsd, 0.0, 1e-9),
271 "identical coords → RMSD 0"
272 );
273 }
274
275 #[test]
276 fn test_align_pure_translation() {
277 let reference = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0]];
279 let mobile: Vec<[f64; 3]> = reference
280 .iter()
281 .map(|v| [v[0] + 3.0, v[1] - 2.0, v[2] + 1.0])
282 .collect();
283 let result = align_coords(&reference, &mobile);
284 assert!(
285 approx_eq(result.rmsd, 0.0, 1e-6),
286 "pure translation → RMSD 0 after Kabsch"
287 );
288 }
289
290 #[test]
291 fn test_align_different_shapes_nonzero_rmsd() {
292 let reference = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0]];
293 let mobile = vec![[0.0, 0.0, 0.0], [1.0, 0.1, 0.0], [0.5, 1.1, 0.0]]; let result = align_coords(&reference, &mobile);
295 assert!(result.rmsd > 0.0, "different shapes → RMSD > 0");
296 }
297
298 #[test]
299 fn test_apply_alignment_reduces_rmsd() {
300 let reference = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0]];
301 let mobile: Vec<[f64; 3]> = reference
302 .iter()
303 .map(|v| [v[0] + 2.0, v[1] + 2.0, v[2]])
304 .collect();
305 let result = align_coords(&reference, &mobile);
306 let aligned = apply_alignment(&mobile, &result);
307 let rmsd_after = rmsd_no_align(&reference, &aligned);
308 assert!(
309 approx_eq(rmsd_after, result.rmsd, 1e-6),
310 "apply_alignment should match reported RMSD"
311 );
312 }
313
314 #[test]
320 fn test_align_pure_rotation_recovers_zero_rmsd() {
321 let reference = vec![
322 [0.0, 0.0, 0.0],
323 [2.0, 0.0, 0.0],
324 [0.0, 3.0, 0.0],
325 [1.0, 1.0, 4.0],
326 ];
327 let mobile: Vec<[f64; 3]> = reference
329 .iter()
330 .map(|p| [-p[1] + 5.0, p[0] - 3.0, p[2] + 2.0])
331 .collect();
332 let result = align_coords(&reference, &mobile);
333 assert!(
334 approx_eq(result.rmsd, 0.0, 1e-6),
335 "pure rotation + translation → RMSD ~0 after Kabsch, got {}",
336 result.rmsd
337 );
338 }
339
340 #[test]
341 fn test_apply_alignment_under_rotation_matches_reference_pointwise() {
342 let reference = vec![
343 [0.0, 0.0, 0.0],
344 [2.0, 0.0, 0.0],
345 [0.0, 3.0, 0.0],
346 [1.0, 1.0, 4.0],
347 ];
348 let mobile: Vec<[f64; 3]> = reference
349 .iter()
350 .map(|p| [-p[1] + 5.0, p[0] - 3.0, p[2] + 2.0])
351 .collect();
352 let result = align_coords(&reference, &mobile);
353 let aligned = apply_alignment(&mobile, &result);
354 for (r, a) in reference.iter().zip(aligned.iter()) {
355 for k in 0..3 {
356 assert!(
357 approx_eq(r[k], a[k], 1e-6),
358 "aligned point {a:?} should match reference point {r:?}"
359 );
360 }
361 }
362 }
363
364 #[test]
369 fn test_align_mirror_image_forces_reflection_correction() {
370 let reference = vec![
371 [0.0, 0.0, 0.0],
372 [2.0, 0.0, 0.0],
373 [0.0, 3.0, 0.0],
374 [1.0, 1.0, 4.0],
375 ];
376 let mobile: Vec<[f64; 3]> = reference.iter().map(|p| [p[0], p[1], -p[2]]).collect();
378 let result = align_coords(&reference, &mobile);
379 assert!(
380 approx_eq(det3(result.rotation), 1.0, 1e-6),
381 "corrected rotation must be proper (det=+1), got det={}",
382 det3(result.rotation)
383 );
384 assert!(
385 result.rmsd > 0.5,
386 "a chiral point set can't be superposed onto its mirror image by any rotation, rmsd={}",
387 result.rmsd
388 );
389 }
390}