Skip to main content

cyanea_struct/
geometry.rs

1//! Coordinate geometry: distances, angles, dihedrals, RMSD.
2
3use cyanea_core::{CyaneaError, Result};
4
5use crate::types::{Atom, Point3D};
6
7use alloc::format;
8
9/// Euclidean distance between two atoms.
10pub fn distance(a1: &Atom, a2: &Atom) -> f64 {
11    distance_points(&a1.coords, &a2.coords)
12}
13
14/// Euclidean distance between two points.
15pub fn distance_points(p1: &Point3D, p2: &Point3D) -> f64 {
16    p1.distance_to(p2)
17}
18
19/// Bond angle in degrees at the central atom `a2`.
20pub fn angle(a1: &Atom, a2: &Atom, a3: &Atom) -> f64 {
21    angle_points(&a1.coords, &a2.coords, &a3.coords)
22}
23
24/// Bond angle in degrees at the central point `p2`.
25pub fn angle_points(p1: &Point3D, p2: &Point3D, p3: &Point3D) -> f64 {
26    let v1 = p1.sub(p2);
27    let v2 = p3.sub(p2);
28    let cos_angle = v1.dot(&v2) / (v1.norm() * v2.norm());
29    // Clamp for numerical safety
30    cos_angle.clamp(-1.0, 1.0).acos().to_degrees()
31}
32
33/// Dihedral (torsion) angle in degrees defined by four atoms.
34pub fn dihedral(a1: &Atom, a2: &Atom, a3: &Atom, a4: &Atom) -> f64 {
35    dihedral_points(&a1.coords, &a2.coords, &a3.coords, &a4.coords)
36}
37
38/// Dihedral (torsion) angle in degrees defined by four points.
39pub fn dihedral_points(p1: &Point3D, p2: &Point3D, p3: &Point3D, p4: &Point3D) -> f64 {
40    let b1 = p2.sub(p1);
41    let b2 = p3.sub(p2);
42    let b3 = p4.sub(p3);
43
44    let n1 = b1.cross(&b2);
45    let n2 = b2.cross(&b3);
46
47    let m1 = n1.cross(&b2.normalize());
48
49    let x = n1.dot(&n2);
50    let y = m1.dot(&n2);
51
52    (-y).atan2(x).to_degrees()
53}
54
55/// Geometric center of mass (unweighted) of a slice of atoms.
56pub fn center_of_mass(atoms: &[&Atom]) -> Point3D {
57    let points: alloc::vec::Vec<Point3D> = atoms.iter().map(|a| a.coords).collect();
58    center_of_mass_points(&points)
59}
60
61/// Geometric center of mass (unweighted) of a slice of points.
62pub fn center_of_mass_points(points: &[Point3D]) -> Point3D {
63    if points.is_empty() {
64        return Point3D::zero();
65    }
66    let mut sum = Point3D::zero();
67    for p in points {
68        sum = sum.add(p);
69    }
70    sum.scale(1.0 / points.len() as f64)
71}
72
73/// RMSD between two equal-length sets of atoms (no alignment, direct comparison).
74pub fn rmsd(atoms1: &[&Atom], atoms2: &[&Atom]) -> Result<f64> {
75    if atoms1.len() != atoms2.len() {
76        return Err(CyaneaError::InvalidInput(format!(
77            "atom set sizes differ: {} vs {}",
78            atoms1.len(),
79            atoms2.len()
80        )));
81    }
82    if atoms1.is_empty() {
83        return Err(CyaneaError::InvalidInput(
84            "cannot compute RMSD of empty atom sets".into(),
85        ));
86    }
87    let p1: alloc::vec::Vec<Point3D> = atoms1.iter().map(|a| a.coords).collect();
88    let p2: alloc::vec::Vec<Point3D> = atoms2.iter().map(|a| a.coords).collect();
89    rmsd_points(&p1, &p2)
90}
91
92/// RMSD between two equal-length slices of points.
93pub fn rmsd_points(points1: &[Point3D], points2: &[Point3D]) -> Result<f64> {
94    if points1.len() != points2.len() {
95        return Err(CyaneaError::InvalidInput(format!(
96            "point set sizes differ: {} vs {}",
97            points1.len(),
98            points2.len()
99        )));
100    }
101    if points1.is_empty() {
102        return Err(CyaneaError::InvalidInput(
103            "cannot compute RMSD of empty point sets".into(),
104        ));
105    }
106    let sum: f64 = points1
107        .iter()
108        .zip(points2.iter())
109        .map(|(a, b)| {
110            let d = a.sub(b);
111            d.dot(&d)
112        })
113        .sum();
114    Ok((sum / points1.len() as f64).sqrt())
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use alloc::vec;
121    use crate::types::Atom;
122
123    fn make_atom(x: f64, y: f64, z: f64) -> Atom {
124        Atom {
125            serial: 1,
126            name: "CA".into(),
127            alt_loc: None,
128            coords: Point3D::new(x, y, z),
129            occupancy: 1.0,
130            temp_factor: 0.0,
131            element: None,
132            charge: None,
133            is_hetatm: false,
134        }
135    }
136
137    #[test]
138    fn test_distance() {
139        let a = make_atom(0.0, 0.0, 0.0);
140        let b = make_atom(3.0, 4.0, 0.0);
141        assert!((distance(&a, &b) - 5.0).abs() < 1e-10);
142    }
143
144    #[test]
145    fn test_angle_90() {
146        let p1 = Point3D::new(1.0, 0.0, 0.0);
147        let p2 = Point3D::new(0.0, 0.0, 0.0);
148        let p3 = Point3D::new(0.0, 1.0, 0.0);
149        assert!((angle_points(&p1, &p2, &p3) - 90.0).abs() < 1e-10);
150    }
151
152    #[test]
153    fn test_angle_180() {
154        let p1 = Point3D::new(-1.0, 0.0, 0.0);
155        let p2 = Point3D::new(0.0, 0.0, 0.0);
156        let p3 = Point3D::new(1.0, 0.0, 0.0);
157        assert!((angle_points(&p1, &p2, &p3) - 180.0).abs() < 1e-10);
158    }
159
160    #[test]
161    fn test_dihedral() {
162        // Trans conformation: ~180 degrees
163        let p1 = Point3D::new(1.0, 0.0, 0.0);
164        let p2 = Point3D::new(0.0, 0.0, 0.0);
165        let p3 = Point3D::new(0.0, 1.0, 0.0);
166        let p4 = Point3D::new(-1.0, 1.0, 0.0);
167        let d = dihedral_points(&p1, &p2, &p3, &p4);
168        assert!((d.abs() - 180.0).abs() < 1e-10);
169    }
170
171    #[test]
172    fn test_center_of_mass() {
173        let points = vec![
174            Point3D::new(0.0, 0.0, 0.0),
175            Point3D::new(2.0, 0.0, 0.0),
176            Point3D::new(0.0, 2.0, 0.0),
177        ];
178        let com = center_of_mass_points(&points);
179        assert!((com.x - 2.0 / 3.0).abs() < 1e-10);
180        assert!((com.y - 2.0 / 3.0).abs() < 1e-10);
181        assert!((com.z).abs() < 1e-10);
182    }
183
184    #[test]
185    fn test_rmsd_identical() {
186        let points = vec![
187            Point3D::new(1.0, 0.0, 0.0),
188            Point3D::new(0.0, 1.0, 0.0),
189            Point3D::new(0.0, 0.0, 1.0),
190        ];
191        assert!((rmsd_points(&points, &points).unwrap()).abs() < 1e-10);
192    }
193
194    #[test]
195    fn test_rmsd_mismatch_error() {
196        let p1 = vec![Point3D::new(0.0, 0.0, 0.0)];
197        let p2 = vec![
198            Point3D::new(0.0, 0.0, 0.0),
199            Point3D::new(1.0, 0.0, 0.0),
200        ];
201        assert!(rmsd_points(&p1, &p2).is_err());
202    }
203}