1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
//! This crate provides functions for working with dihedral angles. Currently, there are two functions:
//!
//! - `dihedral` calculates the dihedral angle in the range -π to π in accordance with biochemistry textbooks (see also: https://en.wikipedia.org/wiki/Dihedral_angle#In_stereochemistry)
//! - `dihedral_unsigned` ignores the direction of rotation and outputs the angle within the range 0 to π. This function is faster than the above signed version.
//!
//! # References
//!
//! - https://math.stackexchange.com/a/47084
//! - https://en.wikipedia.org/wiki/Dihedral_angle#In_stereochemistry

type Float = f64;
type Vector3D = [Float; 3];
type Point3D = [Float; 3];

/// Calculates the dihedral angle, in the range -π to π, of the four ordered coordinates.
///
/// This is in accordance with the definition of dihedral angle in biochemistry textbooks.
///
/// # Examples
///
/// ```
/// use dihedral::dihedral;
///
/// const P0: [f64; 3] = [24.969, 13.428, 30.692]; // N
/// const P1: [f64; 3] = [24.044, 12.661, 29.808]; // CA
/// const P2: [f64; 3] = [22.785, 13.482, 29.543]; // C
/// const P3: [f64; 3] = [21.951, 13.670, 30.431]; // O
/// const P4: [f64; 3] = [23.672, 11.328, 30.466]; // CB
/// const P5: [f64; 3] = [22.881, 10.326, 29.620]; // CG
/// const P6: [f64; 3] = [23.691, 9.935, 28.389]; // CD1
/// const P7: [f64; 3] = [22.557, 9.096, 30.459]; // CD2
///
/// assert!((dihedral([P0, P1, P2, P3]).to_degrees() - (-71.21515)).abs() < 1E-4);
/// assert!((dihedral([P0, P1, P4, P5]).to_degrees() - (-171.94319)).abs() < 1E-4);
/// assert!((dihedral([P1, P4, P5, P6]).to_degrees() - (60.82226)).abs() < 1E-4);
/// assert!((dihedral([P1, P4, P5, P7]).to_degrees() - (-177.63641)).abs() < 1E-4);
/// ```
#[inline]
pub fn dihedral([a, b, c, d]: [Point3D; 4]) -> Float {
    let (a, b, c) = (v(a, b), v(b, c), v(c, d));
    let (r, s) = (u(cross(a, b)), u(cross(b, c)));
    let t = cross(r, u(b));
    let (x, y) = (dot(r, s), dot(s, t));
    -y.atan2(x)
}

/// Calculates the unsigned dihedral angle, in the range 0 to π, of the four ordered coordinates
///
/// # Examples
///
/// ```
/// use dihedral::dihedral_unsigned;
///
/// const P0: [f64; 3] = [24.969, 13.428, 30.692]; // N
/// const P1: [f64; 3] = [24.044, 12.661, 29.808]; // CA
/// const P2: [f64; 3] = [22.785, 13.482, 29.543]; // C
/// const P3: [f64; 3] = [21.951, 13.670, 30.431]; // O
/// const P4: [f64; 3] = [23.672, 11.328, 30.466]; // CB
/// const P5: [f64; 3] = [22.881, 10.326, 29.620]; // CG
/// const P6: [f64; 3] = [23.691, 9.935, 28.389]; // CD1
/// const P7: [f64; 3] = [22.557, 9.096, 30.459]; // CD2
///
/// assert!((dihedral_unsigned([P0, P1, P2, P3]).to_degrees() - (71.21515)).abs() < 1E-4);
/// assert!((dihedral_unsigned([P0, P1, P4, P5]).to_degrees() - (171.94319)).abs() < 1E-4);
/// assert!((dihedral_unsigned([P1, P4, P5, P6]).to_degrees() - (60.82226)).abs() < 1E-4);
/// assert!((dihedral_unsigned([P1, P4, P5, P7]).to_degrees() - (177.63641)).abs() < 1E-4);
/// ```
#[inline]
pub fn dihedral_unsigned([a, b, c, d]: [Point3D; 4]) -> Float {
    let (a, b, c) = (v(a, b), v(b, c), v(c, d));
    let (r, s) = (cross(a, b), cross(b, c));
    (dot(r, s) / (norm(r) * norm(s))).acos()
}

#[inline(always)]
fn norm(a: Vector3D) -> Float {
    dot(a, a).sqrt()
}

#[inline(always)]
fn u(v: Vector3D) -> Vector3D {
    let norm = norm(v);
    [v[0] / norm, v[1] / norm, v[2] / norm]
}

#[inline(always)]
fn v(p1: Point3D, p2: Point3D) -> Vector3D {
    [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2]]
}

#[inline(always)]
fn dot(m: Vector3D, n: Vector3D) -> Float {
    m[0] * n[0] + m[1] * n[1] + m[2] * n[2]
}

#[inline(always)]
fn cross(m: Vector3D, n: Vector3D) -> Vector3D {
    [
        m[1] * n[2] - m[2] * n[1],
        m[2] * n[0] - m[0] * n[2],
        m[0] * n[1] - m[1] * n[0],
    ]
}

#[cfg(test)]
mod tests {

    use super::*;

    const P0: Point3D = [24.969, 13.428, 30.692]; // N
    const P1: Point3D = [24.044, 12.661, 29.808]; // CA
    const P2: Point3D = [22.785, 13.482, 29.543]; // C
    const P3: Point3D = [21.951, 13.670, 30.431]; // O
    const P4: Point3D = [23.672, 11.328, 30.466]; // CB
    const P5: Point3D = [22.881, 10.326, 29.620]; // CG
    const P6: Point3D = [23.691, 9.935, 28.389]; // CD1
    const P7: Point3D = [22.557, 9.096, 30.459]; // CD2

    #[test]
    fn test_dihedral() {
        assert!((dihedral([P0, P1, P2, P3]).to_degrees() - (-71.21515)).abs() < 1E-4);
        assert!((dihedral([P0, P1, P4, P5]).to_degrees() - (-171.94319)).abs() < 1E-4);
        assert!((dihedral([P1, P4, P5, P6]).to_degrees() - (60.82226)).abs() < 1E-4);
        assert!((dihedral([P1, P4, P5, P7]).to_degrees() - (-177.63641)).abs() < 1E-4);
    }

    #[test]
    fn test_dihedral_unsigned() {
        assert!((dihedral_unsigned([P0, P1, P2, P3]).to_degrees() - (71.21515)).abs() < 1E-4);
        assert!((dihedral_unsigned([P0, P1, P4, P5]).to_degrees() - (171.94319)).abs() < 1E-4);
        assert!((dihedral_unsigned([P1, P4, P5, P6]).to_degrees() - (60.82226)).abs() < 1E-4);
        assert!((dihedral_unsigned([P1, P4, P5, P7]).to_degrees() - (177.63641)).abs() < 1E-4);
    }
}