gchemol_geometry/
alignment.rs

1// [[file:../gchemol-geometry.note::040d137b][040d137b]]
2use super::*;
3use vecfx::*;
4
5type Point3 = [f64; 3];
6// 040d137b ends here
7
8// [[file:../gchemol-geometry.note::*mods][mods:1]]
9mod qcprot;
10mod quaternion;
11// mods:1 ends here
12
13// [[file:../gchemol-geometry.note::*base][base:1]]
14#[derive(Clone, Copy, Debug)]
15pub enum SuperpositionAlgo {
16    QCP,
17    Quaternion,
18}
19
20impl Default for SuperpositionAlgo {
21    fn default() -> Self {
22        SuperpositionAlgo::QCP
23        // SuperpositionAlgo::Quaternion
24    }
25}
26
27/// The result of alignment defining how to superimpose.
28#[derive(Clone, Debug)]
29pub struct Superposition {
30    /// superpostion rmsd
31    pub rmsd: f64,
32
33    /// translation vector
34    pub translation: Vector3f,
35
36    /// rotation matrix
37    pub rotation_matrix: Matrix3f,
38}
39
40impl Superposition {
41    /// Apply superposition to other structure `conf`.
42    pub fn apply(&self, conf: &[[f64; 3]]) -> Vec<[f64; 3]> {
43        let mut res = Vec::with_capacity(conf.len());
44        for &v in conf {
45            let v = Vector3f::from(v);
46            let v = self.rotation_matrix * v + self.translation;
47            res.push(v.into());
48        }
49
50        // detect NaN floats
51        if res.as_flat().iter().any(|x| x.is_nan()) {
52            dbg!(&self);
53            panic!("found invalid float numbers!");
54        }
55
56        res
57    }
58
59    /// Apply translation to other structure `conf`.
60    pub fn apply_translation(&self, conf: &[Point3]) -> Vec<Point3> {
61        let mut res = Vec::with_capacity(conf.len());
62        for &v in conf {
63            let v = Vector3f::from(v);
64            let v = v + self.translation;
65            res.push(v.into());
66        }
67
68        res
69    }
70
71    /// Apply rotation to other structure `conf`.
72    pub fn apply_rotation(&self, conf: &[Point3]) -> Vec<Point3> {
73        let mut res = Vec::with_capacity(conf.len());
74        for &v in conf {
75            let v = Vector3f::from(v);
76            let v = v + self.rotation_matrix * v;
77            res.push(v.into());
78        }
79
80        res
81    }
82}
83// base:1 ends here
84
85// [[file:../gchemol-geometry.note::*alignment/deprecated][alignment/deprecated:1]]
86#[deprecated(note = "use Superpose instead")]
87/// Alignment of candidate structure onto the reference
88#[derive(Clone, Debug)]
89pub struct Alignment<'a> {
90    /// The positions of the candidate structure
91    positions: &'a [[f64; 3]],
92
93    /// Select algo
94    pub algorithm: SuperpositionAlgo,
95}
96
97impl<'a> Alignment<'a> {
98    /// Construct from positions of the candidate to be aligned
99    pub fn new(positions: &'a [[f64; 3]]) -> Self {
100        Alignment {
101            positions,
102            algorithm: SuperpositionAlgo::default(),
103        }
104    }
105
106    /// Calculate Root-mean-square deviation of self with the reference coordinates
107    ///
108    /// Parameters
109    /// ----------
110    /// * reference: reference coordinates
111    /// * weights  : weight of each point
112    pub fn rmsd(&self, reference: &[[f64; 3]], weights: Option<&[f64]>) -> Result<f64> {
113        // sanity check
114        let npts = self.positions.len();
115        if reference.len() != npts {
116            bail!("points size mismatch!");
117        }
118        if weights.is_some() && weights.unwrap().len() != npts {
119            bail!("weights size mismatch!");
120        }
121
122        // calculate rmsd
123        let mut ws = 0.0f64;
124        for i in 0..npts {
125            // take the weight if any, or set it to 1.0
126            let wi = weights.map_or_else(|| 1.0, |w| w[i]);
127            let dx = wi * (self.positions[i][0] - reference[i][0]);
128            let dy = wi * (self.positions[i][1] - reference[i][1]);
129            let dz = wi * (self.positions[i][2] - reference[i][2]);
130
131            ws += dx.powi(2) + dy.powi(2) + dz.powi(2);
132        }
133        let ws = ws.sqrt();
134
135        Ok(ws)
136    }
137
138    /// Superpose candidate structure onto reference structure which will be held fixed
139    /// Return superposition struct
140    ///
141    /// Parameters
142    /// ----------
143    /// * reference: reference coordinates
144    /// * weights  : weight of each point
145    pub fn superpose(&mut self, reference: &[[f64; 3]], weights: Option<&[f64]>) -> Result<Superposition> {
146        // calculate the RMSD & rotational matrix
147        let (rmsd, trans, rot) = match self.algorithm {
148            SuperpositionAlgo::QCP => self::qcprot::calc_rmsd_rotational_matrix(&reference, &self.positions, weights),
149            SuperpositionAlgo::Quaternion => self::quaternion::calc_rmsd_rotational_matrix(&reference, &self.positions, weights),
150        };
151
152        // return unit matrix if two structures are already close enough
153        let rotation_matrix = if let Some(rot) = rot {
154            Matrix3f::from_row_slice(&rot)
155        } else {
156            Matrix3f::identity()
157        };
158
159        // return superimposition result
160        let sp = Superposition {
161            rmsd,
162            translation: trans.into(),
163            rotation_matrix,
164        };
165
166        Ok(sp)
167    }
168}
169// alignment/deprecated:1 ends here
170
171// [[file:../gchemol-geometry.note::62a7ce8f][62a7ce8f]]
172/// Alias name of `Superpose`
173pub type Superimpose<'a> = Superpose<'a>;
174
175/// Superpose of candidate structure onto the reference
176#[derive(Clone, Debug)]
177pub struct Superpose<'a> {
178    /// The positions of the candidate structure
179    positions: &'a [[f64; 3]],
180
181    /// Select algo
182    pub algorithm: SuperpositionAlgo,
183}
184
185impl<'a> Superpose<'a> {
186    /// Construct from positions of the candidate to be aligned
187    pub fn new(positions: &'a [[f64; 3]]) -> Self {
188        Self {
189            positions,
190            algorithm: SuperpositionAlgo::default(),
191        }
192    }
193
194    /// Calculate Root-mean-square deviation of self with the reference coordinates
195    ///
196    /// Parameters
197    /// ----------
198    /// * reference: reference coordinates
199    /// * weights  : weight of each point
200    pub fn rmsd(&self, reference: &[[f64; 3]], weights: Option<&[f64]>) -> Result<f64> {
201        // sanity check
202        let npts = self.positions.len();
203        if reference.len() != npts {
204            bail!("points size mismatch!");
205        }
206        if weights.is_some() && weights.unwrap().len() != npts {
207            bail!("weights size mismatch!");
208        }
209
210        // calculate rmsd
211        let mut ws = 0.0f64;
212        for i in 0..npts {
213            // take the weight if any, or set it to 1.0
214            let wi = weights.map_or_else(|| 1.0, |w| w[i]);
215            let dx = wi * (self.positions[i][0] - reference[i][0]);
216            let dy = wi * (self.positions[i][1] - reference[i][1]);
217            let dz = wi * (self.positions[i][2] - reference[i][2]);
218
219            ws += dx.powi(2) + dy.powi(2) + dz.powi(2);
220        }
221        let ws = ws.sqrt();
222
223        Ok(ws)
224    }
225
226    /// Superpose candidate structure onto reference structure which will be held fixed
227    /// Return superposition struct
228    ///
229    /// Parameters
230    /// ----------
231    /// * reference: reference coordinates
232    /// * weights  : weight of each point
233    pub fn onto(&mut self, reference: &[[f64; 3]], weights: Option<&[f64]>) -> Superposition {
234        // calculate the RMSD & rotational matrix
235        let (rmsd, trans, rot) = match self.algorithm {
236            SuperpositionAlgo::QCP => self::qcprot::calc_rmsd_rotational_matrix(&reference, &self.positions, weights),
237            SuperpositionAlgo::Quaternion => self::quaternion::calc_rmsd_rotational_matrix(&reference, &self.positions, weights),
238        };
239
240        // return unit matrix if two structures are already close enough
241        let rotation_matrix = if let Some(rot) = rot {
242            Matrix3f::from_row_slice(&rot)
243        } else {
244            Matrix3f::identity()
245        };
246
247        // return superimposition result
248        Superposition {
249            rmsd,
250            translation: trans.into(),
251            rotation_matrix,
252        }
253    }
254}
255// 62a7ce8f ends here
256
257// [[file:../gchemol-geometry.note::*test][test:1]]
258#[test]
259fn test_alignment() {
260    use vecfx::*;
261
262    // fragment a
263    let (reference, candidate, weights) = qcprot::prepare_test_data();
264
265    // construct alignment for superimposition
266    let sp = Superpose::new(&candidate).onto(&reference, Some(&weights));
267    let rot = sp.rotation_matrix;
268
269    // validation
270    let rot_expected = Matrix3f::from_row_slice(&[
271        0.77227551,
272        0.63510272,
273        -0.01533190,
274        -0.44544846,
275        0.52413614,
276        -0.72584914,
277        -0.45295276,
278        0.56738509,
279        0.68768304,
280    ]);
281    approx::assert_relative_eq!(rot_expected, rot, epsilon = 1e-4);
282}
283// test:1 ends here
284
285// [[file:../gchemol-geometry.note::*test][test:2]]
286#[test]
287fn test_alignment_hcn() {
288    use vecfx::*;
289
290    let positions_ref = [
291        [0.83334699, 0.716865, 0.0],
292        [-0.5486581, -0.35588, 0.0],
293        [-0.2855828, 1.036928, 0.0],
294    ];
295
296    let positions_can = [[-0.634504, -0.199638, -0.0], [0.970676, 0.670662, 0.0], [-0.337065, 0.926883, 0.0]];
297
298    let weights = vec![0.0001; 3];
299    let sp = Superpose::new(&positions_can).onto(&positions_ref, Some(&weights));
300    approx::assert_relative_eq!(sp.rmsd, 0.0614615, epsilon = 1e-4);
301
302    let t = Vector3f::from([0.423160235, 0.2715202, 0.0]);
303    let r = Matrix3f::from_column_slice(&[-0.4167190, -0.9090353, 0.0, -0.909035, 0.4167190, 0.0, 0.0, 0.0, -1.0]);
304    approx::assert_relative_eq!(sp.rotation_matrix, r, epsilon = 1e-4);
305}
306// test:2 ends here