use crate::PdbStructure;
#[cfg(feature = "dssp")]
use crate::dssp::{
BackboneAtoms, compute_all_virtual_hydrogens, detect_hydrogen_bonds, extract_backbone_atoms,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HBondType {
IntraHelical,
BetaSheet,
Turn,
LongRange,
InterChain,
}
impl HBondType {
pub fn is_secondary_structure(&self) -> bool {
matches!(
self,
HBondType::IntraHelical | HBondType::BetaSheet | HBondType::Turn
)
}
}
#[derive(Debug, Clone)]
pub struct MainchainHBond {
pub donor_chain: String,
pub donor_resid: i32,
pub donor_resname: String,
pub donor_ins_code: Option<char>,
pub acceptor_chain: String,
pub acceptor_resid: i32,
pub acceptor_resname: String,
pub acceptor_ins_code: Option<char>,
pub energy: f64,
pub n_o_distance: f64,
pub sequence_separation: i32,
pub hbond_type: HBondType,
}
impl MainchainHBond {
pub fn is_strong(&self) -> bool {
self.energy < -1.0
}
pub fn is_helical(&self) -> bool {
matches!(self.hbond_type, HBondType::IntraHelical)
}
pub fn is_beta_sheet(&self) -> bool {
matches!(self.hbond_type, HBondType::BetaSheet)
}
}
#[derive(Debug, Clone, Default)]
pub struct ResidueHBonds {
pub donated: Vec<MainchainHBond>,
pub accepted: Vec<MainchainHBond>,
}
impl ResidueHBonds {
pub fn total(&self) -> usize {
self.donated.len() + self.accepted.len()
}
pub fn has_hbonds(&self) -> bool {
!self.donated.is_empty() || !self.accepted.is_empty()
}
}
#[derive(Debug, Clone, Default)]
pub struct HBondStats {
pub total_hbonds: usize,
pub intra_helical: usize,
pub beta_sheet: usize,
pub turn: usize,
pub long_range: usize,
pub inter_chain: usize,
pub mean_energy: f64,
pub min_energy: f64,
pub max_energy: f64,
pub donor_residues: usize,
pub acceptor_residues: usize,
}
#[cfg(feature = "dssp")]
fn calculate_n_o_distance(donor: &BackboneAtoms, acceptor: &BackboneAtoms) -> Option<f64> {
let n = donor.n?;
let o = acceptor.o?;
let dx = n[0] - o[0];
let dy = n[1] - o[1];
let dz = n[2] - o[2];
Some((dx * dx + dy * dy + dz * dz).sqrt())
}
fn classify_hbond(
donor_chain: &str,
donor_resid: i32,
acceptor_chain: &str,
acceptor_resid: i32,
) -> HBondType {
if donor_chain != acceptor_chain {
return HBondType::InterChain;
}
let sep = (acceptor_resid - donor_resid).abs();
match sep {
2 => HBondType::Turn, 3..=5 => HBondType::IntraHelical, _ if sep > 5 => HBondType::BetaSheet, _ => HBondType::LongRange,
}
}
#[cfg(feature = "dssp")]
impl PdbStructure {
pub fn mainchain_hbonds(&self) -> Vec<MainchainHBond> {
let mut backbone_atoms = extract_backbone_atoms(&self.atoms);
if backbone_atoms.is_empty() {
return Vec::new();
}
compute_all_virtual_hydrogens(&mut backbone_atoms);
let raw_hbonds = detect_hydrogen_bonds(&backbone_atoms);
raw_hbonds
.iter()
.filter_map(|hb| {
let donor = &backbone_atoms[hb.donor_index];
let acceptor = &backbone_atoms[hb.acceptor_index];
let n_o_distance = calculate_n_o_distance(donor, acceptor)?;
let hbond_type = classify_hbond(
&donor.chain_id,
donor.residue_seq,
&acceptor.chain_id,
acceptor.residue_seq,
);
let sequence_separation = if donor.chain_id == acceptor.chain_id {
acceptor.residue_seq - donor.residue_seq
} else {
0
};
Some(MainchainHBond {
donor_chain: donor.chain_id.clone(),
donor_resid: donor.residue_seq,
donor_resname: donor.residue_name.clone(),
donor_ins_code: donor.ins_code,
acceptor_chain: acceptor.chain_id.clone(),
acceptor_resid: acceptor.residue_seq,
acceptor_resname: acceptor.residue_name.clone(),
acceptor_ins_code: acceptor.ins_code,
energy: hb.energy,
n_o_distance,
sequence_separation,
hbond_type,
})
})
.collect()
}
pub fn hbonds_for_residue(&self, chain: &str, resid: i32) -> ResidueHBonds {
let all_hbonds = self.mainchain_hbonds();
let mut result = ResidueHBonds::default();
for hb in all_hbonds {
if hb.donor_chain == chain && hb.donor_resid == resid {
result.donated.push(hb);
} else if hb.acceptor_chain == chain && hb.acceptor_resid == resid {
result.accepted.push(hb);
}
}
result
}
pub fn hbond_statistics(&self) -> HBondStats {
let hbonds = self.mainchain_hbonds();
let mut stats = HBondStats::default();
if hbonds.is_empty() {
return stats;
}
stats.total_hbonds = hbonds.len();
let mut sum_energy = 0.0;
stats.min_energy = f64::MAX;
stats.max_energy = f64::MIN;
let mut donor_set = std::collections::HashSet::new();
let mut acceptor_set = std::collections::HashSet::new();
for hb in &hbonds {
match hb.hbond_type {
HBondType::IntraHelical => stats.intra_helical += 1,
HBondType::BetaSheet => stats.beta_sheet += 1,
HBondType::Turn => stats.turn += 1,
HBondType::LongRange => stats.long_range += 1,
HBondType::InterChain => stats.inter_chain += 1,
}
sum_energy += hb.energy;
if hb.energy < stats.min_energy {
stats.min_energy = hb.energy;
}
if hb.energy > stats.max_energy {
stats.max_energy = hb.energy;
}
donor_set.insert((hb.donor_chain.clone(), hb.donor_resid));
acceptor_set.insert((hb.acceptor_chain.clone(), hb.acceptor_resid));
}
stats.mean_energy = sum_energy / stats.total_hbonds as f64;
stats.donor_residues = donor_set.len();
stats.acceptor_residues = acceptor_set.len();
stats
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hbond_type_methods() {
assert!(HBondType::IntraHelical.is_secondary_structure());
assert!(HBondType::BetaSheet.is_secondary_structure());
assert!(HBondType::Turn.is_secondary_structure());
assert!(!HBondType::LongRange.is_secondary_structure());
assert!(!HBondType::InterChain.is_secondary_structure());
}
#[test]
fn test_classify_hbond_helical() {
let hb_type = classify_hbond("A", 1, "A", 5);
assert_eq!(hb_type, HBondType::IntraHelical);
let hb_type = classify_hbond("A", 1, "A", 4);
assert_eq!(hb_type, HBondType::IntraHelical);
}
#[test]
fn test_classify_hbond_beta_sheet() {
let hb_type = classify_hbond("A", 1, "A", 20);
assert_eq!(hb_type, HBondType::BetaSheet);
}
#[test]
fn test_classify_hbond_turn() {
let hb_type = classify_hbond("A", 1, "A", 3);
assert_eq!(hb_type, HBondType::Turn);
}
#[test]
fn test_classify_hbond_inter_chain() {
let hb_type = classify_hbond("A", 1, "B", 5);
assert_eq!(hb_type, HBondType::InterChain);
}
#[test]
fn test_mainchain_hbond_methods() {
let hb = MainchainHBond {
donor_chain: "A".to_string(),
donor_resid: 1,
donor_resname: "ALA".to_string(),
donor_ins_code: None,
acceptor_chain: "A".to_string(),
acceptor_resid: 5,
acceptor_resname: "LEU".to_string(),
acceptor_ins_code: None,
energy: -1.5,
n_o_distance: 2.9,
sequence_separation: 4,
hbond_type: HBondType::IntraHelical,
};
assert!(hb.is_strong());
assert!(hb.is_helical());
assert!(!hb.is_beta_sheet());
}
#[test]
fn test_residue_hbonds() {
let mut res_hbonds = ResidueHBonds::default();
assert!(!res_hbonds.has_hbonds());
assert_eq!(res_hbonds.total(), 0);
res_hbonds.donated.push(MainchainHBond {
donor_chain: "A".to_string(),
donor_resid: 1,
donor_resname: "ALA".to_string(),
donor_ins_code: None,
acceptor_chain: "A".to_string(),
acceptor_resid: 5,
acceptor_resname: "LEU".to_string(),
acceptor_ins_code: None,
energy: -1.5,
n_o_distance: 2.9,
sequence_separation: 4,
hbond_type: HBondType::IntraHelical,
});
assert!(res_hbonds.has_hbonds());
assert_eq!(res_hbonds.total(), 1);
}
#[test]
fn test_hbond_stats_default() {
let stats = HBondStats::default();
assert_eq!(stats.total_hbonds, 0);
assert_eq!(stats.intra_helical, 0);
assert_eq!(stats.beta_sheet, 0);
}
}