use crate::records::Atom;
use std::collections::HashMap;
pub const HBOND_THRESHOLD: f64 = -0.5;
const ELECTROSTATIC_CONSTANT: f64 = 27.888;
const NH_BOND_LENGTH: f64 = 1.020;
const MAX_CN_DISTANCE: f64 = 6.0;
#[derive(Debug, Clone)]
pub struct BackboneAtoms {
pub chain_id: String,
pub residue_seq: i32,
pub residue_name: String,
pub ins_code: Option<char>,
pub n: Option<[f64; 3]>,
pub ca: Option<[f64; 3]>,
pub c: Option<[f64; 3]>,
pub o: Option<[f64; 3]>,
pub h: Option<[f64; 3]>,
}
impl BackboneAtoms {
pub fn new(
chain_id: String,
residue_seq: i32,
residue_name: String,
ins_code: Option<char>,
) -> Self {
Self {
chain_id,
residue_seq,
residue_name,
ins_code,
n: None,
ca: None,
c: None,
o: None,
h: None,
}
}
pub fn is_complete_donor(&self) -> bool {
self.n.is_some() && self.h.is_some()
}
pub fn is_complete_acceptor(&self) -> bool {
self.c.is_some() && self.o.is_some()
}
pub fn is_complete(&self) -> bool {
self.n.is_some() && self.ca.is_some() && self.c.is_some() && self.o.is_some()
}
pub fn is_proline(&self) -> bool {
self.residue_name == "PRO"
}
}
#[derive(Debug, Clone)]
pub struct HydrogenBond {
pub donor_index: usize,
pub acceptor_index: usize,
pub energy: f64,
}
impl HydrogenBond {
pub fn new(donor_index: usize, acceptor_index: usize, energy: f64) -> Self {
Self {
donor_index,
acceptor_index,
energy,
}
}
}
#[inline]
fn distance(p1: &[f64; 3], p2: &[f64; 3]) -> f64 {
let dx = p2[0] - p1[0];
let dy = p2[1] - p1[1];
let dz = p2[2] - p1[2];
(dx * dx + dy * dy + dz * dz).sqrt()
}
#[inline]
fn distance_squared(p1: &[f64; 3], p2: &[f64; 3]) -> f64 {
let dx = p2[0] - p1[0];
let dy = p2[1] - p1[1];
let dz = p2[2] - p1[2];
dx * dx + dy * dy + dz * dz
}
#[inline]
fn normalize(v: &[f64; 3]) -> Option<[f64; 3]> {
let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
if len < 1e-10 {
return None;
}
Some([v[0] / len, v[1] / len, v[2] / len])
}
pub fn compute_virtual_hydrogen(
backbone: &BackboneAtoms,
prev_backbone: Option<&BackboneAtoms>,
) -> Option<[f64; 3]> {
if backbone.is_proline() {
return None;
}
let n = backbone.n?;
let prev_c = prev_backbone?.c?;
let cn_dir = [n[0] - prev_c[0], n[1] - prev_c[1], n[2] - prev_c[2]];
let cn_normalized = normalize(&cn_dir)?;
Some([
n[0] + NH_BOND_LENGTH * cn_normalized[0],
n[1] + NH_BOND_LENGTH * cn_normalized[1],
n[2] + NH_BOND_LENGTH * cn_normalized[2],
])
}
pub fn calculate_hbond_energy(donor: &BackboneAtoms, acceptor: &BackboneAtoms) -> Option<f64> {
let n = donor.n?;
let h = donor.h?;
let c = acceptor.c?;
let o = acceptor.o?;
let cn_dist_sq = distance_squared(&c, &n);
if cn_dist_sq > MAX_CN_DISTANCE * MAX_CN_DISTANCE {
return None;
}
let r_on = distance(&o, &n);
let r_ch = distance(&c, &h);
let r_oh = distance(&o, &h);
let r_cn = cn_dist_sq.sqrt();
if r_on < 0.5 || r_ch < 0.5 || r_oh < 0.5 || r_cn < 0.5 {
return None;
}
let energy = ELECTROSTATIC_CONSTANT * (1.0 / r_on + 1.0 / r_ch - 1.0 / r_oh - 1.0 / r_cn);
Some(energy)
}
pub fn extract_backbone_atoms(atoms: &[Atom]) -> Vec<BackboneAtoms> {
let mut residue_map: HashMap<(String, i32, Option<char>), BackboneAtoms> = HashMap::new();
for atom in atoms {
let name = atom.name.trim();
let key = (atom.chain_id.clone(), atom.residue_seq, atom.ins_code);
let backbone = residue_map.entry(key).or_insert_with(|| {
BackboneAtoms::new(
atom.chain_id.clone(),
atom.residue_seq,
atom.residue_name.clone(),
atom.ins_code,
)
});
let coords = [atom.x, atom.y, atom.z];
match name {
"N" => backbone.n = Some(coords),
"CA" => backbone.ca = Some(coords),
"C" => backbone.c = Some(coords),
"O" | "OXT" => {
if backbone.o.is_none() || name == "O" {
backbone.o = Some(coords);
}
}
"H" | "HN" | "H1" => {
if backbone.h.is_none() {
backbone.h = Some(coords);
}
}
_ => {}
}
}
let mut residues: Vec<BackboneAtoms> = residue_map.into_values().collect();
residues.sort_by(|a, b| {
a.chain_id
.cmp(&b.chain_id)
.then_with(|| a.residue_seq.cmp(&b.residue_seq))
.then_with(|| a.ins_code.cmp(&b.ins_code))
});
residues
}
pub fn compute_all_virtual_hydrogens(residues: &mut [BackboneAtoms]) {
for i in 1..residues.len() {
if residues[i].h.is_some() {
continue;
}
if residues[i].chain_id != residues[i - 1].chain_id {
continue;
}
let seq_diff = residues[i].residue_seq - residues[i - 1].residue_seq;
if seq_diff > 1 {
continue; }
let prev = &residues[i - 1];
if let Some(virtual_h) = compute_virtual_hydrogen(&residues[i], Some(prev)) {
residues[i].h = Some(virtual_h);
}
}
}
#[allow(clippy::needless_range_loop)]
pub fn detect_hydrogen_bonds(residues: &[BackboneAtoms]) -> Vec<HydrogenBond> {
let mut hbonds = Vec::new();
for i in 0..residues.len() {
let donor = &residues[i];
if !donor.is_complete_donor() || donor.is_proline() {
continue;
}
for j in 0..residues.len() {
if (i as isize - j as isize).abs() < 2 {
continue;
}
let acceptor = &residues[j];
if !acceptor.is_complete_acceptor() {
continue;
}
if let Some(energy) = calculate_hbond_energy(donor, acceptor) {
if energy < HBOND_THRESHOLD {
hbonds.push(HydrogenBond::new(i, j, energy));
}
}
}
}
hbonds
}
pub fn create_hbond_matrix(hbonds: &[HydrogenBond]) -> HashMap<(usize, usize), f64> {
hbonds
.iter()
.map(|hb| ((hb.donor_index, hb.acceptor_index), hb.energy))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_distance() {
let p1 = [0.0, 0.0, 0.0];
let p2 = [3.0, 4.0, 0.0];
assert!((distance(&p1, &p2) - 5.0).abs() < 1e-10);
}
#[test]
fn test_normalize() {
let v = [3.0, 4.0, 0.0];
let n = normalize(&v).unwrap();
assert!((n[0] - 0.6).abs() < 1e-10);
assert!((n[1] - 0.8).abs() < 1e-10);
assert!(n[2].abs() < 1e-10);
let zero = [0.0, 0.0, 0.0];
assert!(normalize(&zero).is_none());
}
#[test]
fn test_backbone_atoms() {
let backbone = BackboneAtoms::new("A".to_string(), 1, "ALA".to_string(), None);
assert!(!backbone.is_complete());
assert!(!backbone.is_proline());
let proline = BackboneAtoms::new("A".to_string(), 2, "PRO".to_string(), None);
assert!(proline.is_proline());
}
#[test]
fn test_hbond_energy_threshold() {
assert!((HBOND_THRESHOLD - (-0.5)).abs() < 1e-10);
}
}