1use cyanea_core::{CyaneaError, Result};
4
5use crate::types::{Atom, Point3D};
6
7use alloc::format;
8
9pub fn distance(a1: &Atom, a2: &Atom) -> f64 {
11 distance_points(&a1.coords, &a2.coords)
12}
13
14pub fn distance_points(p1: &Point3D, p2: &Point3D) -> f64 {
16 p1.distance_to(p2)
17}
18
19pub fn angle(a1: &Atom, a2: &Atom, a3: &Atom) -> f64 {
21 angle_points(&a1.coords, &a2.coords, &a3.coords)
22}
23
24pub 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 cos_angle.clamp(-1.0, 1.0).acos().to_degrees()
31}
32
33pub fn dihedral(a1: &Atom, a2: &Atom, a3: &Atom, a4: &Atom) -> f64 {
35 dihedral_points(&a1.coords, &a2.coords, &a3.coords, &a4.coords)
36}
37
38pub 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
55pub 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
61pub 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
73pub 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
92pub 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 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}