bio_forge/ops/
transform.rs

1//! Geometric transformations for molecular structures.
2//!
3//! This module provides utilities for translating, centering, and rotating structures.
4
5use crate::model::structure::Structure;
6use crate::model::types::Point;
7use crate::utils::parallel::*;
8use nalgebra::{Rotation3, Vector3};
9
10/// Collection of geometric transformation operations for structures.
11///
12/// The `Transform` type groups static methods that mutate structure coordinates in place.
13/// These operations include translations, centering (by geometry or mass), and rotations
14/// about principal axes or via Euler angles.
15pub struct Transform;
16
17impl Transform {
18    /// Translates all atoms by the specified displacement vector.
19    ///
20    /// # Arguments
21    ///
22    /// * `structure` - Mutable structure whose atoms will be displaced.
23    /// * `x` - Translation along the x-axis in ångströms.
24    /// * `y` - Translation along the y-axis in ångströms.
25    /// * `z` - Translation along the z-axis in ångströms.
26    pub fn translate(structure: &mut Structure, x: f64, y: f64, z: f64) {
27        let translation = Vector3::new(x, y, z);
28        structure.par_residues_mut().for_each(|residue| {
29            for atom in residue.iter_atoms_mut() {
30                atom.translate_by(&translation);
31            }
32        });
33    }
34
35    /// Centers the structure's geometric centroid at the target point.
36    ///
37    /// When `target` is `None`, the structure is centered at the origin.
38    ///
39    /// # Arguments
40    ///
41    /// * `structure` - Mutable structure to be centered.
42    /// * `target` - Optional target point; defaults to the origin.
43    pub fn center_geometry(structure: &mut Structure, target: Option<Point>) {
44        let current_center = structure.geometric_center();
45        let target_point = target.unwrap_or(Point::origin());
46        let translation = target_point - current_center;
47
48        structure.par_residues_mut().for_each(|residue| {
49            for atom in residue.iter_atoms_mut() {
50                atom.translate_by(&translation);
51            }
52        });
53    }
54
55    /// Centers the structure's center of mass at the target point.
56    ///
57    /// Mass weighting uses atomic masses from element definitions. When `target` is
58    /// `None`, the structure is centered at the origin.
59    ///
60    /// # Arguments
61    ///
62    /// * `structure` - Mutable structure to be centered.
63    /// * `target` - Optional target point; defaults to the origin.
64    pub fn center_mass(structure: &mut Structure, target: Option<Point>) {
65        let current_com = structure.center_of_mass();
66        let target_point = target.unwrap_or(Point::origin());
67        let translation = target_point - current_com;
68
69        structure.par_residues_mut().for_each(|residue| {
70            for atom in residue.iter_atoms_mut() {
71                atom.translate_by(&translation);
72            }
73        });
74    }
75
76    /// Rotates the structure about the x-axis by the specified angle.
77    ///
78    /// # Arguments
79    ///
80    /// * `structure` - Mutable structure to be rotated.
81    /// * `radians` - Rotation angle in radians.
82    pub fn rotate_x(structure: &mut Structure, radians: f64) {
83        let rotation = Rotation3::from_axis_angle(&Vector3::x_axis(), radians);
84        Self::apply_rotation(structure, rotation);
85    }
86
87    /// Rotates the structure about the y-axis by the specified angle.
88    ///
89    /// # Arguments
90    ///
91    /// * `structure` - Mutable structure to be rotated.
92    /// * `radians` - Rotation angle in radians.
93    pub fn rotate_y(structure: &mut Structure, radians: f64) {
94        let rotation = Rotation3::from_axis_angle(&Vector3::y_axis(), radians);
95        Self::apply_rotation(structure, rotation);
96    }
97
98    /// Rotates the structure about the z-axis by the specified angle.
99    ///
100    /// # Arguments
101    ///
102    /// * `structure` - Mutable structure to be rotated.
103    /// * `radians` - Rotation angle in radians.
104    pub fn rotate_z(structure: &mut Structure, radians: f64) {
105        let rotation = Rotation3::from_axis_angle(&Vector3::z_axis(), radians);
106        Self::apply_rotation(structure, rotation);
107    }
108
109    /// Rotates the structure using Euler angles (XYZ convention).
110    ///
111    /// # Arguments
112    ///
113    /// * `structure` - Mutable structure to be rotated.
114    /// * `x_rad` - Rotation about x-axis in radians.
115    /// * `y_rad` - Rotation about y-axis in radians.
116    /// * `z_rad` - Rotation about z-axis in radians.
117    pub fn rotate_euler(structure: &mut Structure, x_rad: f64, y_rad: f64, z_rad: f64) {
118        let rotation = Rotation3::from_euler_angles(x_rad, y_rad, z_rad);
119        Self::apply_rotation(structure, rotation);
120    }
121
122    /// Applies a rotation matrix to all atoms and box vectors.
123    fn apply_rotation(structure: &mut Structure, rotation: Rotation3<f64>) {
124        structure.par_residues_mut().for_each(|residue| {
125            for atom in residue.iter_atoms_mut() {
126                atom.pos = rotation * atom.pos;
127            }
128        });
129
130        if let Some(box_vecs) = structure.box_vectors {
131            let v1 = Vector3::from(box_vecs[0]);
132            let v2 = Vector3::from(box_vecs[1]);
133            let v3 = Vector3::from(box_vecs[2]);
134
135            let v1_rot = rotation * v1;
136            let v2_rot = rotation * v2;
137            let v3_rot = rotation * v3;
138
139            structure.box_vectors = Some([v1_rot.into(), v2_rot.into(), v3_rot.into()]);
140        }
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::Transform;
147    use crate::model::{
148        atom::Atom,
149        chain::Chain,
150        residue::Residue,
151        structure::Structure,
152        types::{Element, Point, ResidueCategory, StandardResidue},
153    };
154
155    fn structure_with_points(points: &[Point]) -> Structure {
156        let mut chain = Chain::new("A");
157        let mut residue = Residue::new(
158            1,
159            None,
160            "GLY",
161            Some(StandardResidue::GLY),
162            ResidueCategory::Standard,
163        );
164
165        for (idx, point) in points.iter().enumerate() {
166            let name = format!("C{}", idx);
167            residue.add_atom(Atom::new(&name, Element::C, *point));
168        }
169
170        chain.add_residue(residue);
171        let mut structure = Structure::new();
172        structure.add_chain(chain);
173        structure
174    }
175
176    fn assert_point_close(actual: &Point, expected: &Point) {
177        assert!((actual.x - expected.x).abs() < 1e-6);
178        assert!((actual.y - expected.y).abs() < 1e-6);
179        assert!((actual.z - expected.z).abs() < 1e-6);
180    }
181
182    #[test]
183    fn translate_moves_all_atoms_by_vector() {
184        let mut structure =
185            structure_with_points(&[Point::new(0.0, 0.0, 0.0), Point::new(1.0, 2.0, 3.0)]);
186
187        Transform::translate(&mut structure, 5.0, -2.0, 1.5);
188
189        let mut atoms = structure.iter_atoms();
190        assert_point_close(&atoms.next().unwrap().pos, &Point::new(5.0, -2.0, 1.5));
191        assert_point_close(&atoms.next().unwrap().pos, &Point::new(6.0, 0.0, 4.5));
192    }
193
194    #[test]
195    fn center_geometry_moves_geometric_center_to_target() {
196        let mut structure =
197            structure_with_points(&[Point::new(2.0, 0.0, 0.0), Point::new(4.0, 0.0, 0.0)]);
198
199        Transform::center_geometry(&mut structure, Some(Point::new(10.0, 0.0, 0.0)));
200
201        let center = structure.geometric_center();
202        assert_point_close(&center, &Point::new(10.0, 0.0, 0.0));
203    }
204
205    #[test]
206    fn center_mass_moves_center_of_mass_to_origin_by_default() {
207        let mut structure =
208            structure_with_points(&[Point::new(2.0, 0.0, 0.0), Point::new(4.0, 0.0, 0.0)]);
209
210        {
211            let chain = structure.chain_mut("A").unwrap();
212            let residue = chain.residue_mut(1, None).unwrap();
213            residue
214                .iter_atoms_mut()
215                .enumerate()
216                .for_each(|(idx, atom)| {
217                    atom.element = if idx == 0 { Element::H } else { Element::O };
218                });
219        }
220
221        Transform::center_mass(&mut structure, None);
222
223        let com = structure.center_of_mass();
224        assert_point_close(&com, &Point::origin());
225    }
226
227    #[test]
228    fn rotate_z_rotates_atoms_about_origin() {
229        let mut structure =
230            structure_with_points(&[Point::new(1.0, 0.0, 0.0), Point::new(0.0, 2.0, 0.0)]);
231
232        Transform::rotate_z(&mut structure, std::f64::consts::FRAC_PI_2);
233
234        let mut atoms = structure.iter_atoms();
235        assert_point_close(&atoms.next().unwrap().pos, &Point::new(0.0, 1.0, 0.0));
236        assert_point_close(&atoms.next().unwrap().pos, &Point::new(-2.0, 0.0, 0.0));
237    }
238
239    #[test]
240    fn rotate_euler_updates_box_vectors() {
241        let mut structure = structure_with_points(&[Point::new(1.0, 0.0, 0.0)]);
242        structure.box_vectors = Some([[1.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 3.0]]);
243
244        Transform::rotate_euler(&mut structure, 0.0, 0.0, std::f64::consts::FRAC_PI_2);
245
246        let box_vectors = structure.box_vectors.unwrap();
247        assert_point_close(&Point::from(box_vectors[0]), &Point::new(0.0, 1.0, 0.0));
248        assert_point_close(&Point::from(box_vectors[1]), &Point::new(-2.0, 0.0, 0.0));
249        assert_point_close(&Point::from(box_vectors[2]), &Point::new(0.0, 0.0, 3.0));
250    }
251}