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] += v_final[r][k] * u[c][k];
141 }
142 }
143 }
144
145 let det = det3(rot);
147 if det < 0.0 {
148 for r in 0..3 {
149 v_final[r][0] *= -1.0;
150 }
151 rot = [[0.0; 3]; 3];
152 for r in 0..3 {
153 for c in 0..3 {
154 for k in 0..3 {
155 rot[r][c] += v_final[r][k] * u[c][k];
156 }
157 }
158 }
159 }
160
161 let mut sum_sq = 0.0f64;
163 for i in 0..n {
164 for row in 0..3 {
165 let rotated = (0..3).map(|k| rot[row][k] * q[i][k]).sum::<f64>();
166 let diff = p[i][row] - rotated;
167 sum_sq += diff * diff;
168 }
169 }
170 let rmsd = (sum_sq / nf).sqrt();
171
172 let translation = [cr[0] - cm[0], cr[1] - cm[1], cr[2] - cm[2]];
174
175 AlignResult {
176 rmsd,
177 rotation: rot,
178 translation,
179 }
180}
181
182pub fn apply_alignment(mobile: &[[f64; 3]], result: &AlignResult) -> Vec<[f64; 3]> {
186 let n = mobile.len();
188 if n == 0 {
189 return Vec::new();
190 }
191 let mut cm = [0.0f64; 3];
192 for v in mobile {
193 for k in 0..3 {
194 cm[k] += v[k];
195 }
196 }
197 for k in 0..3 {
198 cm[k] /= n as f64;
199 }
200
201 let cr = [
206 cm[0] + result.translation[0],
207 cm[1] + result.translation[1],
208 cm[2] + result.translation[2],
209 ];
210
211 mobile
212 .iter()
213 .map(|v| {
214 let centered = [v[0] - cm[0], v[1] - cm[1], v[2] - cm[2]];
215 let mut out = [0.0f64; 3];
216 for row in 0..3 {
217 out[row] = (0..3)
218 .map(|k| result.rotation[row][k] * centered[k])
219 .sum::<f64>()
220 + cr[row];
221 }
222 out
223 })
224 .collect()
225}
226
227fn det3(m: [[f64; 3]; 3]) -> f64 {
228 m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
229 - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
230 + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
231}
232
233#[cfg(test)]
238mod tests {
239 use super::*;
240
241 fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
242 (a - b).abs() < tol
243 }
244
245 #[test]
246 fn test_rmsd_no_align_identical() {
247 let coords = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
248 assert!(approx_eq(rmsd_no_align(&coords, &coords), 0.0, 1e-10));
249 }
250
251 #[test]
252 fn test_rmsd_no_align_empty() {
253 assert_eq!(rmsd_no_align(&[], &[]), 0.0);
254 }
255
256 #[test]
257 fn test_rmsd_no_align_translated() {
258 let a = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
259 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));
262 }
263
264 #[test]
265 fn test_align_identical() {
266 let coords = vec![[0.0, 0.0, 0.0], [1.5, 0.0, 0.0], [0.75, 1.3, 0.0]];
267 let result = align_coords(&coords, &coords);
268 assert!(
269 approx_eq(result.rmsd, 0.0, 1e-9),
270 "identical coords → RMSD 0"
271 );
272 }
273
274 #[test]
275 fn test_align_pure_translation() {
276 let reference = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0]];
278 let mobile: Vec<[f64; 3]> = reference
279 .iter()
280 .map(|v| [v[0] + 3.0, v[1] - 2.0, v[2] + 1.0])
281 .collect();
282 let result = align_coords(&reference, &mobile);
283 assert!(
284 approx_eq(result.rmsd, 0.0, 1e-6),
285 "pure translation → RMSD 0 after Kabsch"
286 );
287 }
288
289 #[test]
290 fn test_align_different_shapes_nonzero_rmsd() {
291 let reference = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0]];
292 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);
294 assert!(result.rmsd > 0.0, "different shapes → RMSD > 0");
295 }
296
297 #[test]
298 fn test_apply_alignment_reduces_rmsd() {
299 let reference = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0]];
300 let mobile: Vec<[f64; 3]> = reference
301 .iter()
302 .map(|v| [v[0] + 2.0, v[1] + 2.0, v[2]])
303 .collect();
304 let result = align_coords(&reference, &mobile);
305 let aligned = apply_alignment(&mobile, &result);
306 let rmsd_after = rmsd_no_align(&reference, &aligned);
307 assert!(
308 approx_eq(rmsd_after, result.rmsd, 1e-6),
309 "apply_alignment should match reported RMSD"
310 );
311 }
312}