use crate::PdbStructure;
#[cfg(feature = "dssp")]
use crate::dssp::{calculate_all_dihedrals, extract_backbone_atoms};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RamachandranRegion {
Core,
Allowed,
Generous,
Outlier,
Glycine,
Proline,
PrePro,
Unknown,
}
impl RamachandranRegion {
pub fn is_favorable(&self) -> bool {
matches!(
self,
RamachandranRegion::Core
| RamachandranRegion::Allowed
| RamachandranRegion::Glycine
| RamachandranRegion::Proline
| RamachandranRegion::PrePro
)
}
pub fn is_outlier(&self) -> bool {
matches!(self, RamachandranRegion::Outlier)
}
}
#[derive(Debug, Clone)]
pub struct ResidueDihedrals {
pub chain_id: String,
pub residue_seq: i32,
pub residue_name: String,
pub ins_code: Option<char>,
pub phi: Option<f64>,
pub psi: Option<f64>,
pub omega: Option<f64>,
pub ramachandran_region: RamachandranRegion,
}
impl ResidueDihedrals {
pub fn has_phi_psi(&self) -> bool {
self.phi.is_some() && self.psi.is_some()
}
pub fn is_cis_peptide(&self) -> bool {
if let Some(omega) = self.omega {
omega.abs() < 30.0
} else {
false
}
}
pub fn is_trans_peptide(&self) -> bool {
if let Some(omega) = self.omega {
(omega.abs() - 180.0).abs() < 30.0
} else {
false
}
}
}
#[derive(Debug, Clone)]
pub struct ResidueRef {
pub chain_id: String,
pub residue_seq: i32,
pub residue_name: String,
pub ins_code: Option<char>,
}
#[derive(Debug, Clone)]
pub struct RamachandranStats {
pub total_residues: usize,
pub favored_count: usize,
pub allowed_count: usize,
pub outlier_count: usize,
pub glycine_count: usize,
pub proline_count: usize,
pub prepro_count: usize,
pub favored_fraction: f64,
pub allowed_fraction: f64,
pub outlier_fraction: f64,
pub cis_peptide_count: usize,
pub cis_nonpro_count: usize,
}
impl Default for RamachandranStats {
fn default() -> Self {
Self {
total_residues: 0,
favored_count: 0,
allowed_count: 0,
outlier_count: 0,
glycine_count: 0,
proline_count: 0,
prepro_count: 0,
favored_fraction: 0.0,
allowed_fraction: 0.0,
outlier_fraction: 0.0,
cis_peptide_count: 0,
cis_nonpro_count: 0,
}
}
}
fn is_alpha_helix_core(phi: f64, psi: f64) -> bool {
let phi_diff = (phi - (-62.0)).abs();
let psi_diff = (psi - (-41.0)).abs();
phi_diff < 25.0 && psi_diff < 25.0
}
fn is_beta_sheet_core(phi: f64, psi: f64) -> bool {
let phi_diff = (phi - (-119.0)).abs();
let psi_in_range = psi > 100.0 && psi < 180.0;
phi_diff < 35.0 && psi_in_range
}
fn is_left_helix(phi: f64, psi: f64) -> bool {
let phi_diff = (phi - 60.0).abs();
let psi_diff = (psi - 45.0).abs();
phi_diff < 30.0 && psi_diff < 30.0
}
fn is_ppii_region(phi: f64, psi: f64) -> bool {
let phi_diff = (phi - (-75.0)).abs();
let psi_diff = (psi - 145.0).abs();
phi_diff < 29.0 && psi_diff < 29.0
}
fn is_proline_phi(phi: f64) -> bool {
phi > -80.0 && phi < -40.0
}
fn classify_ramachandran(
phi: f64,
psi: f64,
residue_name: &str,
next_residue_name: Option<&str>,
) -> RamachandranRegion {
if residue_name == "GLY" {
if is_alpha_helix_core(phi, psi) || is_beta_sheet_core(phi, psi) || is_left_helix(phi, psi)
{
return RamachandranRegion::Glycine;
}
if phi > -180.0 && phi < 180.0 && psi > -180.0 && psi < 180.0 {
if (phi.abs() < 20.0 && psi.abs() > 150.0) || (phi > 100.0 && psi < -100.0) {
return RamachandranRegion::Outlier;
}
return RamachandranRegion::Glycine;
}
}
if residue_name == "PRO" {
if is_proline_phi(phi) {
if is_alpha_helix_core(phi, psi) || is_ppii_region(phi, psi) {
return RamachandranRegion::Proline;
}
if psi > -60.0 && psi < 180.0 {
return RamachandranRegion::Proline;
}
}
return RamachandranRegion::Outlier;
}
if let Some(next) = next_residue_name {
if next == "PRO" {
if is_beta_sheet_core(phi, psi) || is_ppii_region(phi, psi) {
return RamachandranRegion::PrePro;
}
let phi_diff = (phi - (-60.0)).abs();
let psi_diff = (psi - (-30.0)).abs();
if phi_diff < 30.0 && psi_diff < 30.0 {
return RamachandranRegion::PrePro;
}
}
}
if is_alpha_helix_core(phi, psi) || is_beta_sheet_core(phi, psi) {
return RamachandranRegion::Core;
}
let phi_diff_alpha = (phi - (-62.0)).abs();
let psi_diff_alpha = (psi - (-41.0)).abs();
if phi_diff_alpha < 40.0 && psi_diff_alpha < 40.0 {
return RamachandranRegion::Allowed;
}
if phi < -60.0 && phi > -180.0 && psi > 80.0 && psi < 180.0 {
return RamachandranRegion::Allowed;
}
if is_ppii_region(phi, psi) {
return RamachandranRegion::Allowed;
}
if phi > -150.0 && phi < 0.0 && psi > -100.0 && psi < 50.0 {
return RamachandranRegion::Generous;
}
if phi < -60.0 && psi > 60.0 {
return RamachandranRegion::Generous;
}
RamachandranRegion::Outlier
}
#[cfg(feature = "dssp")]
impl PdbStructure {
pub fn phi_psi_angles(&self) -> Vec<ResidueDihedrals> {
let backbone_atoms = extract_backbone_atoms(&self.atoms);
if backbone_atoms.is_empty() {
return Vec::new();
}
let dihedrals = calculate_all_dihedrals(&backbone_atoms);
backbone_atoms
.iter()
.zip(dihedrals.iter())
.enumerate()
.map(|(i, (backbone, dihedral))| {
let next_res_name = if i + 1 < backbone_atoms.len()
&& backbone_atoms[i + 1].chain_id == backbone.chain_id
{
Some(backbone_atoms[i + 1].residue_name.as_str())
} else {
None
};
let phi_std = dihedral.phi.map(|p| -p);
let psi_std = dihedral.psi.map(|p| -p);
let region = match (phi_std, psi_std) {
(Some(phi), Some(psi)) => {
classify_ramachandran(phi, psi, &backbone.residue_name, next_res_name)
}
_ => RamachandranRegion::Unknown,
};
ResidueDihedrals {
chain_id: backbone.chain_id.clone(),
residue_seq: backbone.residue_seq,
residue_name: backbone.residue_name.clone(),
ins_code: backbone.ins_code,
phi: phi_std,
psi: psi_std,
omega: dihedral.omega,
ramachandran_region: region,
}
})
.collect()
}
pub fn ramachandran_outliers(&self) -> Vec<ResidueDihedrals> {
self.phi_psi_angles()
.into_iter()
.filter(|d| d.ramachandran_region == RamachandranRegion::Outlier)
.collect()
}
pub fn cis_peptide_bonds(&self) -> Vec<(ResidueRef, ResidueRef)> {
let dihedrals = self.phi_psi_angles();
let mut cis_bonds = Vec::new();
for i in 1..dihedrals.len() {
if dihedrals[i].is_cis_peptide() && dihedrals[i].chain_id == dihedrals[i - 1].chain_id {
let res1 = ResidueRef {
chain_id: dihedrals[i - 1].chain_id.clone(),
residue_seq: dihedrals[i - 1].residue_seq,
residue_name: dihedrals[i - 1].residue_name.clone(),
ins_code: dihedrals[i - 1].ins_code,
};
let res2 = ResidueRef {
chain_id: dihedrals[i].chain_id.clone(),
residue_seq: dihedrals[i].residue_seq,
residue_name: dihedrals[i].residue_name.clone(),
ins_code: dihedrals[i].ins_code,
};
cis_bonds.push((res1, res2));
}
}
cis_bonds
}
pub fn ramachandran_statistics(&self) -> RamachandranStats {
let dihedrals = self.phi_psi_angles();
let mut stats = RamachandranStats::default();
let valid_residues: Vec<_> = dihedrals.iter().filter(|d| d.has_phi_psi()).collect();
stats.total_residues = valid_residues.len();
if stats.total_residues == 0 {
return stats;
}
for d in &valid_residues {
match d.ramachandran_region {
RamachandranRegion::Core => stats.favored_count += 1,
RamachandranRegion::Allowed | RamachandranRegion::Generous => {
stats.allowed_count += 1
}
RamachandranRegion::Outlier => stats.outlier_count += 1,
RamachandranRegion::Glycine => {
stats.glycine_count += 1;
stats.favored_count += 1; }
RamachandranRegion::Proline => {
stats.proline_count += 1;
stats.favored_count += 1;
}
RamachandranRegion::PrePro => {
stats.prepro_count += 1;
stats.favored_count += 1;
}
RamachandranRegion::Unknown => {}
}
}
for i in 1..dihedrals.len() {
if dihedrals[i].is_cis_peptide() && dihedrals[i].chain_id == dihedrals[i - 1].chain_id {
stats.cis_peptide_count += 1;
if dihedrals[i].residue_name != "PRO" {
stats.cis_nonpro_count += 1;
}
}
}
let total = stats.total_residues as f64;
stats.favored_fraction = stats.favored_count as f64 / total;
stats.allowed_fraction = stats.allowed_count as f64 / total;
stats.outlier_fraction = stats.outlier_count as f64 / total;
stats
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ramachandran_region_methods() {
assert!(RamachandranRegion::Core.is_favorable());
assert!(RamachandranRegion::Allowed.is_favorable());
assert!(RamachandranRegion::Glycine.is_favorable());
assert!(RamachandranRegion::Proline.is_favorable());
assert!(!RamachandranRegion::Outlier.is_favorable());
assert!(!RamachandranRegion::Generous.is_favorable());
assert!(RamachandranRegion::Outlier.is_outlier());
assert!(!RamachandranRegion::Core.is_outlier());
}
#[test]
fn test_classify_alpha_helix() {
let region = classify_ramachandran(-62.0, -41.0, "ALA", None);
assert_eq!(region, RamachandranRegion::Core);
}
#[test]
fn test_classify_beta_sheet() {
let region = classify_ramachandran(-120.0, 130.0, "ALA", None);
assert_eq!(region, RamachandranRegion::Core);
}
#[test]
fn test_classify_glycine() {
let region = classify_ramachandran(60.0, 45.0, "GLY", None);
assert_eq!(region, RamachandranRegion::Glycine);
let region = classify_ramachandran(-62.0, -41.0, "GLY", None);
assert_eq!(region, RamachandranRegion::Glycine);
}
#[test]
fn test_classify_proline() {
let region = classify_ramachandran(-60.0, 145.0, "PRO", None);
assert_eq!(region, RamachandranRegion::Proline);
}
#[test]
fn test_classify_outlier() {
let region = classify_ramachandran(60.0, 60.0, "ALA", None);
assert_eq!(region, RamachandranRegion::Outlier);
}
#[test]
fn test_residue_dihedrals_cis_trans() {
let mut d = ResidueDihedrals {
chain_id: "A".to_string(),
residue_seq: 1,
residue_name: "ALA".to_string(),
ins_code: None,
phi: Some(-60.0),
psi: Some(-45.0),
omega: Some(180.0),
ramachandran_region: RamachandranRegion::Core,
};
assert!(d.is_trans_peptide());
assert!(!d.is_cis_peptide());
d.omega = Some(0.0);
assert!(d.is_cis_peptide());
assert!(!d.is_trans_peptide());
}
#[test]
fn test_ramachandran_stats_default() {
let stats = RamachandranStats::default();
assert_eq!(stats.total_residues, 0);
assert_eq!(stats.favored_count, 0);
assert_eq!(stats.outlier_count, 0);
}
}