use molrs::spatial::neighbors::NeighborQuery;
use molrs::store::frame_access::FrameAccess;
use molrs::types::F;
use super::criterion::{DistKind, HBondCriterion};
use crate::compute::error::ComputeError;
use crate::compute::result::ComputeResult;
use crate::compute::traits::Compute;
use crate::compute::util::{MicHelper, get_positions_ref};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct HBond {
pub donor: u32,
pub hydrogen: u32,
pub acceptor: u32,
pub distance: F,
pub angle: F,
}
#[derive(Debug, Clone)]
pub struct HBonds {
pub donors: Vec<(u32, u32)>,
pub acceptors: Vec<u32>,
pub criterion: HBondCriterion,
}
#[derive(Debug, Clone)]
pub struct HBondsResult {
pub per_frame: Vec<Vec<HBond>>,
pub counts: Vec<usize>,
}
impl ComputeResult for HBondsResult {}
#[inline]
fn norm(v: [F; 3]) -> F {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
#[inline]
fn dot(a: [F; 3], b: [F; 3]) -> F {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
impl HBonds {
pub fn new(donors: Vec<(u32, u32)>, acceptors: Vec<u32>, criterion: HBondCriterion) -> Self {
Self {
donors,
acceptors,
criterion,
}
}
fn detect_frame<FA: FrameAccess>(&self, frame: &FA) -> Result<Vec<HBond>, ComputeError> {
if self.donors.is_empty() || self.acceptors.is_empty() {
return Ok(Vec::new());
}
let (xp, yp, zp) = get_positions_ref(frame)?;
let (xs, ys, zs) = (xp.slice(), yp.slice(), zp.slice());
let n = xs.len();
let pos = |i: u32| -> [F; 3] {
let i = i as usize;
[xs[i], ys[i], zs[i]]
};
for &(d, h) in &self.donors {
if d as usize >= n || h as usize >= n {
return Err(ComputeError::OutOfRange {
field: "HBonds::donors atom index",
value: format!("{d}/{h} >= {n}"),
});
}
}
for &a in &self.acceptors {
if a as usize >= n {
return Err(ComputeError::OutOfRange {
field: "HBonds::acceptors atom index",
value: format!("{a} >= {n}"),
});
}
}
let mic = MicHelper::from_simbox(frame.simbox_ref());
let na = self.acceptors.len();
let (mut acc_x, mut acc_y, mut acc_z) = (
Vec::with_capacity(na),
Vec::with_capacity(na),
Vec::with_capacity(na),
);
for &a in &self.acceptors {
let p = pos(a);
acc_x.push(p[0]);
acc_y.push(p[1]);
acc_z.push(p[2]);
}
let nd = self.donors.len();
let (mut q_x, mut q_y, mut q_z) = (
Vec::with_capacity(nd),
Vec::with_capacity(nd),
Vec::with_capacity(nd),
);
for &(don, hyd) in &self.donors {
let src = match self.criterion.dist_kind {
DistKind::DonorAcceptor => don,
DistKind::HydrogenAcceptor => hyd,
};
let p = pos(src);
q_x.push(p[0]);
q_y.push(p[1]);
q_z.push(p[2]);
}
let cutoff = self.criterion.dist_cutoff;
let nlist = match frame.simbox_ref() {
Some(sb) => NeighborQuery::from_columns(sb, &acc_x, &acc_y, &acc_z, cutoff)
.query_columns(&q_x, &q_y, &q_z),
None => NeighborQuery::free_columns(&acc_x, &acc_y, &acc_z, cutoff)
.query_columns(&q_x, &q_y, &q_z),
};
let qi = nlist.query_point_indices();
let pj = nlist.point_indices();
let mut bonds = Vec::new();
for k in 0..nlist.n_pairs() {
let (donor, hydrogen) = self.donors[qi[k] as usize];
let acceptor = self.acceptors[pj[k] as usize];
if acceptor == donor || acceptor == hydrogen {
continue;
}
let dpos = pos(donor);
let hpos = pos(hydrogen);
let apos = pos(acceptor);
let v_da = mic.disp(dpos, apos);
let r_da = norm(v_da);
let dist_ok = match self.criterion.dist_kind {
DistKind::DonorAcceptor => true,
DistKind::HydrogenAcceptor => r_da <= self.criterion.dist_cutoff + 1.05,
};
let v_hd = mic.disp(hpos, dpos);
let v_ha = mic.disp(hpos, apos);
let n_hd = norm(v_hd);
let n_ha = norm(v_ha);
if n_hd == 0.0 || n_ha == 0.0 {
continue;
}
let cos = (dot(v_hd, v_ha) / (n_hd * n_ha)).clamp(-1.0, 1.0);
let angle = cos.acos().to_degrees();
if dist_ok && angle >= self.criterion.angle_cutoff {
bonds.push(HBond {
donor,
hydrogen,
acceptor,
distance: r_da,
angle,
});
}
}
Ok(bonds)
}
}
impl Compute for HBonds {
type Args<'a> = ();
type Output = HBondsResult;
fn compute<'a, FA: FrameAccess + Sync + 'a>(
&self,
frames: &[&'a FA],
_args: (),
) -> Result<HBondsResult, ComputeError> {
if frames.is_empty() {
return Err(ComputeError::EmptyInput);
}
#[cfg(feature = "rayon")]
const PAR_THRESHOLD: usize = 4;
#[cfg(feature = "rayon")]
let per_frame: Vec<Vec<HBond>> = if frames.len() >= PAR_THRESHOLD {
use rayon::prelude::*;
frames
.par_iter()
.map(|frame| self.detect_frame(*frame))
.collect::<Result<Vec<_>, _>>()?
} else {
frames
.iter()
.map(|frame| self.detect_frame(*frame))
.collect::<Result<Vec<_>, _>>()?
};
#[cfg(not(feature = "rayon"))]
let per_frame: Vec<Vec<HBond>> = frames
.iter()
.map(|frame| self.detect_frame(*frame))
.collect::<Result<Vec<_>, _>>()?;
let counts = per_frame.iter().map(Vec::len).collect();
Ok(HBondsResult { per_frame, counts })
}
}