use crate::prelude::*;
use std::collections::BTreeMap;
const MAX_ANGLE: Float = 63.0; const MAX_DIST_AT_MAX_ANGLE: Float = 0.32; const MAX_DIST_AT_ZERO: Float = 0.40; const POWER_A: Float = 1.6;
const POWER_B: Float = 5.0;
const H_BOND_LEN: Float = 0.1;
const HELIX_PHI_TARGET: Float = -57.0;
const HELIX_PSI_TARGET: Float = -48.0;
const HELIX_INCLUDE: Float = 55.0;
const HELIX_EXCLUDE: Float = 85.0;
const STRAND_PHI_TARGET: Float = -129.0;
const STRAND_PSI_TARGET: Float = 124.0;
const STRAND_INCLUDE: Float = 40.0;
const STRAND_PHI_EXCLUDE: Float = 100.0;
const STRAND_PSI_EXCLUDE: Float = 90.0;
const HELIX3: u32 = 0x0001;
const HELIX4: u32 = 0x0002;
const HELIX5: u32 = 0x0004;
const HELIX_HB: u32 = HELIX3 | HELIX4 | HELIX5;
const PHIPSI_HELIX: u32 = 0x0010;
const PHIPSI_NOT_HELIX: u32 = 0x0020;
const PHIPSI_STRAND: u32 = 0x0040;
const PHIPSI_NOT_STRAND: u32 = 0x0080;
const ANTI_SINGLE: u32 = 0x0100;
const ANTI_DOUBLE: u32 = 0x0200;
const ANTI_BULGE: u32 = 0x0400;
const ANTI_SKIP: u32 = 0x0800;
const PARA_SINGLE: u32 = 0x1000;
const PARA_DOUBLE: u32 = 0x2000;
const PARA_SKIP: u32 = 0x4000;
const PAD: usize = 5;
pub struct Dss {
ss: Vec<SS>,
}
impl Dss {
pub fn new(sel: &impl ParticleIterProvider) -> Self {
Dss { ss: assign(sel) }
}
pub fn ss(&self) -> &[SS] {
&self.ss
}
pub fn ss_string(&self) -> String {
self.ss.iter().map(|s| s.to_char()).collect()
}
pub fn len(&self) -> usize {
self.ss.len()
}
pub fn is_empty(&self) -> bool {
self.ss.is_empty()
}
}
#[derive(Clone)]
struct R {
real: bool,
resindex: usize,
n: Pos,
ca: Pos,
c: Pos,
o: Pos,
flags: u32,
ss: u8, acc: Vec<usize>, don: Vec<usize>, }
impl R {
fn blank() -> Self {
R {
real: false,
resindex: 0,
n: Pos::origin(),
ca: Pos::origin(),
c: Pos::origin(),
o: Pos::origin(),
flags: 0,
ss: b'L',
acc: Vec::new(),
don: Vec::new(),
}
}
}
fn assign(sel: &impl ParticleIterProvider) -> Vec<SS> {
struct Bb {
n: Option<Pos>,
ca: Option<Pos>,
c: Option<Pos>,
o: Option<Pos>,
chain: char,
}
let mut by_res: BTreeMap<usize, Bb> = BTreeMap::new();
for p in sel.iter_particle() {
let e = by_res.entry(p.atom.resindex).or_insert(Bb {
n: None,
ca: None,
c: None,
o: None,
chain: p.atom.chain,
});
match p.atom.name.as_str() {
"N" => e.n = Some(*p.pos),
"CA" => e.ca = Some(*p.pos),
"C" => e.c = Some(*p.pos),
"O" | "OT1" | "OXT" => {
if e.o.is_none() {
e.o = Some(*p.pos);
}
}
_ => {}
}
}
let all_resindices: Vec<usize> = by_res.keys().copied().collect();
let mut res: Vec<R> = vec![R::blank(); PAD];
let mut prev: Option<(char, usize)> = None;
for &ri in &all_resindices {
let bb = &by_res[&ri];
let complete = bb.n.is_some() && bb.ca.is_some() && bb.c.is_some() && bb.o.is_some();
if !complete {
if prev.is_some() {
res.extend(std::iter::repeat_with(R::blank).take(PAD));
prev = None;
}
continue;
}
let break_here = matches!(prev, Some((pc, pr)) if bb.chain != pc || ri != pr + 1);
if break_here {
res.extend(std::iter::repeat_with(R::blank).take(PAD));
}
res.push(R {
real: true,
resindex: ri,
n: bb.n.unwrap(),
ca: bb.ca.unwrap(),
c: bb.c.unwrap(),
o: bb.o.unwrap(),
flags: 0,
ss: b'L',
acc: Vec::new(),
don: Vec::new(),
});
prev = Some((bb.chain, ri));
}
res.extend(std::iter::repeat_with(R::blank).take(PAD));
let n = res.len();
if n > 2 * PAD {
detect_hbonds(&mut res);
classify_phi_psi(&mut res);
pattern_flags(&mut res);
assign_helices(&mut res);
assign_strands(&mut res);
cleanup(&mut res);
}
let mut ss_by_ri: BTreeMap<usize, SS> = BTreeMap::new();
for r in &res {
if r.real {
ss_by_ri.insert(
r.resindex,
match r.ss {
b'H' => SS::AlphaHelix,
b'S' => SS::BetaSheet,
_ => SS::Coil,
},
);
}
}
all_resindices
.iter()
.map(|ri| ss_by_ri.get(ri).copied().unwrap_or(SS::Coil))
.collect()
}
fn detect_hbonds(res: &mut [R]) {
let n = res.len();
let mut bonds: Vec<(usize, usize)> = Vec::new(); for i in PAD..n - PAD {
if !res[i].real {
continue;
}
for j in PAD..n - PAD {
if i == j || !res[j].real {
continue;
}
if (i as isize - j as isize).abs() <= 2 {
continue;
}
let c_prev = if res[j - 1].real { Some(res[j - 1].c) } else { None };
if is_hbond(&res[j].n, &res[j].ca, c_prev.as_ref(), &res[i].o, &res[i].c) {
bonds.push((i, j));
}
}
}
for (i, j) in bonds {
if res[i].acc.len() < 5 {
res[i].acc.push(j);
}
if res[j].don.len() < 5 {
res[j].don.push(i);
}
}
}
fn is_hbond(n: &Pos, ca: &Pos, c_prev: Option<&Pos>, o: &Pos, c_acc: &Pos) -> bool {
let don_to_acc = o - n;
let dist = don_to_acc.norm();
if dist > MAX_DIST_AT_ZERO {
return false; }
let h_dir = match c_prev {
Some(cp) => {
let u_ca = (ca - n).normalize();
let u_cp = (cp - n).normalize();
let s = u_ca + u_cp;
if s.norm() < 1e-6 {
(n - ca).normalize()
} else {
-s.normalize()
}
}
None => (n - ca).normalize(),
};
let h = n + h_dir * H_BOND_LEN;
let plane = (o - c_acc).normalize();
let h_to_acc = (o - h).normalize();
if h_to_acc.dot(&plane) > 0.0 {
return false;
}
let dangle = h_dir.dot(&don_to_acc.normalize());
let angle = if dangle < 1.0 && dangle > 0.0 {
dangle.clamp(-1.0, 1.0).acos().to_degrees()
} else if dangle > 0.0 {
0.0
} else {
90.0
};
if angle > MAX_ANGLE {
return false;
}
let factor_a = 0.5 / MAX_ANGLE.powf(POWER_A);
let factor_b = 0.5 / MAX_ANGLE.powf(POWER_B);
let curve = angle.powf(POWER_A) * factor_a + angle.powf(POWER_B) * factor_b;
let cutoff = MAX_DIST_AT_MAX_ANGLE * curve + MAX_DIST_AT_ZERO * (1.0 - curve);
dist <= cutoff
}
fn classify_phi_psi(res: &mut [R]) {
let n = res.len();
for a in PAD..n - PAD {
if !(res[a].real && res[a - 1].real && res[a + 1].real) {
continue;
}
let phi = dihedral(&res[a - 1].c, &res[a].n, &res[a].ca, &res[a].c);
let psi = dihedral(&res[a].n, &res[a].ca, &res[a].c, &res[a + 1].n);
let h_phi = circ_delta(phi, HELIX_PHI_TARGET);
let h_psi = circ_delta(psi, HELIX_PSI_TARGET);
let s_phi = circ_delta(phi, STRAND_PHI_TARGET);
let s_psi = circ_delta(psi, STRAND_PSI_TARGET);
let f = &mut res[a].flags;
if h_psi > HELIX_EXCLUDE || h_phi > HELIX_EXCLUDE {
*f |= PHIPSI_NOT_HELIX;
} else if h_psi < HELIX_INCLUDE && h_phi < HELIX_INCLUDE {
*f |= PHIPSI_HELIX;
}
if s_psi > STRAND_PSI_EXCLUDE || s_phi > STRAND_PHI_EXCLUDE {
*f |= PHIPSI_NOT_STRAND;
} else if s_psi < STRAND_INCLUDE && s_phi < STRAND_INCLUDE {
*f |= PHIPSI_STRAND;
}
}
}
fn pattern_flags(res: &mut [R]) {
let n = res.len();
let mut add: Vec<(usize, u32)> = Vec::new();
for a in PAD..n - PAD {
if !res[a].real {
continue;
}
for &acc in &res[a].acc {
if acc == a + 3 {
add.push((a, HELIX3));
} else if acc == a + 4 {
add.push((a, HELIX4));
} else if acc == a + 5 {
add.push((a, HELIX5));
}
}
for &don in &res[a].don {
if don + 3 == a {
add.push((a, HELIX3));
} else if don + 4 == a {
add.push((a, HELIX4));
} else if don + 5 == a {
add.push((a, HELIX5));
}
}
for &r2 in &res[a].acc {
if res[r2].real && res[r2].acc.contains(&a) {
add.push((a, ANTI_DOUBLE));
add.push((r2, ANTI_DOUBLE));
}
}
for &acc in &res[a].acc {
let r2 = acc + 1;
if r2 < n && res[r2].real && res[r2].acc.contains(&a) {
add.push((a, ANTI_DOUBLE));
add.push((r2, ANTI_BULGE));
add.push((r2 - 1, ANTI_BULGE));
}
}
if res[a + 1].real && res[a + 2].real {
for &acc in &res[a].acc {
if acc < 2 {
continue;
}
let r2 = acc - 2;
if res[r2].real && res[r2].acc.contains(&(a + 2)) {
add.push((a, ANTI_SINGLE));
add.push((a + 1, ANTI_SKIP));
add.push((a + 2, ANTI_SINGLE));
add.push((r2, ANTI_SINGLE));
add.push((r2 + 1, ANTI_SKIP));
add.push((r2 + 2, ANTI_SINGLE));
}
}
}
if res[a + 1].real && res[a + 2].real {
for &acc in &res[a].acc {
let r2 = acc;
if res[r2].real && res[r2].acc.contains(&(a + 2)) {
add.push((a, PARA_SINGLE));
add.push((a + 1, PARA_SKIP));
add.push((a + 2, PARA_SINGLE));
add.push((r2, PARA_DOUBLE));
}
}
}
}
for (i, f) in add {
res[i].flags |= f;
}
}
fn assign_helices(res: &mut [R]) {
let n = res.len();
for a in PAD..n - PAD {
if res[a].real
&& res[a - 1].flags & HELIX_HB != 0
&& res[a].flags & HELIX_HB != 0
&& res[a + 1].flags & HELIX_HB != 0
&& res[a].flags & PHIPSI_NOT_HELIX == 0
{
res[a].ss = b'H';
}
}
for a in PAD..n - PAD {
if res[a].real
&& res[a - 2].flags & HELIX_HB != 0
&& res[a - 1].flags & HELIX_HB != 0
&& res[a - 1].flags & PHIPSI_HELIX != 0
&& res[a].flags & PHIPSI_HELIX != 0
&& res[a + 1].flags & HELIX_HB != 0
&& res[a + 1].flags & PHIPSI_HELIX != 0
&& res[a + 2].flags & HELIX_HB != 0
{
res[a].ss = b'h';
}
}
for a in PAD..n - PAD {
if res[a].real && res[a].ss == b'h' {
res[a].flags |= HELIX_HB;
res[a].ss = b'H';
}
}
for a in PAD..n - PAD {
if !res[a].real {
continue;
}
let (f, fp1, fp2, fm1, fm2) = (
res[a].flags,
res[a + 1].flags,
res[a + 2].flags,
res[a - 1].flags,
res[a - 2].flags,
);
let ss_next = res[a + 1].ss;
let ss_prev = res[a - 1].ss;
let geo = |x: u32| x & HELIX_HB != 0 && x & PHIPSI_HELIX != 0;
if geo(f) && geo(fp1) && geo(fp2) && ss_next == b'H' {
res[a].ss = b'H';
}
if geo(f) && geo(fm1) && geo(fm2) && ss_prev == b'H' {
res[a].ss = b'H';
}
}
}
fn assign_strands(res: &mut [R]) {
let n = res.len();
for a in PAD..n - PAD {
if !res[a].real {
continue;
}
let fm1 = res[a - 1].flags;
let f = res[a].flags;
let fp1 = res[a + 1].flags;
if f & ANTI_DOUBLE != 0 && f & PHIPSI_NOT_STRAND == 0 {
res[a].ss = b'S';
}
if f & ANTI_BULGE != 0 && fp1 & ANTI_BULGE != 0 {
res[a].ss = b'S';
res[a + 1].ss = b'S';
}
if fm1 & ANTI_DOUBLE != 0
&& f & ANTI_SKIP != 0
&& f & PHIPSI_NOT_STRAND == 0
&& fp1 & (ANTI_SINGLE | ANTI_DOUBLE) != 0
{
res[a].ss = b'S';
}
if fm1 & (ANTI_SINGLE | ANTI_DOUBLE) != 0
&& f & ANTI_SKIP != 0
&& f & PHIPSI_NOT_STRAND == 0
&& fp1 & ANTI_DOUBLE != 0
{
res[a].ss = b'S';
}
if fm1 & (ANTI_SINGLE | ANTI_DOUBLE) != 0
&& fm1 & PHIPSI_STRAND != 0
&& f & PHIPSI_STRAND != 0
&& fp1 & (ANTI_SINGLE | ANTI_DOUBLE) != 0
&& fp1 & PHIPSI_STRAND != 0
{
res[a - 1].ss = b'S';
res[a].ss = b'S';
res[a + 1].ss = b'S';
}
if f & PARA_DOUBLE != 0 && f & PHIPSI_NOT_STRAND == 0 {
res[a].ss = b'S';
}
if fm1 & PARA_DOUBLE != 0
&& f & PARA_SKIP != 0
&& f & PHIPSI_NOT_STRAND == 0
&& fp1 & (PARA_SINGLE | PARA_DOUBLE) != 0
{
res[a].ss = b'S';
}
if fm1 & (PARA_SINGLE | PARA_DOUBLE) != 0
&& f & PARA_SKIP != 0
&& f & PHIPSI_NOT_STRAND == 0
&& fp1 & PARA_DOUBLE != 0
{
res[a].ss = b'S';
}
if fm1 & (PARA_SINGLE | PARA_DOUBLE) != 0
&& fm1 & PHIPSI_STRAND != 0
&& f & PARA_SKIP != 0
&& f & PHIPSI_STRAND != 0
&& fp1 & (PARA_SINGLE | PARA_DOUBLE) != 0
&& fp1 & PHIPSI_STRAND != 0
{
res[a - 1].ss = b'S';
res[a].ss = b'S';
res[a + 1].ss = b'S';
}
}
}
fn cleanup(res: &mut [R]) {
let n = res.len();
let mut repeat = true;
while repeat {
repeat = false;
for a in PAD..n - PAD {
if !res[a].real {
continue;
}
let ss = res[a].ss;
let ssm1 = res[a - 1].ss;
let ssp1 = res[a + 1].ss;
let ssp2 = res[a + 2].ss;
if (ss == b'S' && ssp1 == b'S' && ssm1 != b'S' && ssp2 != b'S')
|| (ss == b'H' && ssp1 == b'H' && ssm1 != b'H' && ssp2 != b'H')
{
res[a].ss = b'L';
res[a + 1].ss = b'L';
repeat = true;
continue;
}
if (ss == b'S' && ssm1 != b'S' && ssp1 != b'S')
|| (ss == b'H' && ssm1 != b'H' && ssp1 != b'H')
{
res[a].ss = b'L';
repeat = true;
continue;
}
if ss == b'S' && (ssm1 != b'S' || ssp1 != b'S') {
let mut found = res[a].acc.iter().any(|&p| res[p].ss == b'S')
|| res[a].don.iter().any(|&p| res[p].ss == b'S');
if !found && res[a].flags & (ANTI_SKIP | PARA_SKIP) != 0 {
if ssp1 == b'S' {
found = res[a + 1].acc.iter().any(|&p| res[p].ss == b'S');
}
if !found && ssm1 == b'S' {
found = res[a - 1].don.iter().any(|&p| res[p].ss == b'S');
}
}
if !found {
res[a].ss = b'L';
repeat = true;
}
}
}
}
}
fn dihedral(a: &Pos, b: &Pos, c: &Pos, d: &Pos) -> Float {
let ba = a - b;
let cd = d - c;
let cb = b - c;
let cbxba = cb.cross(&ba);
let cbxcd = cb.cross(&cd);
let cbxcbxcd = cb.cross(&cbxcd);
let vdot1 = cbxcd.dot(&cbxcd);
let vdot2 = cbxcbxcd.dot(&cbxcbxcd);
if vdot1 > 0.0 && vdot2 > 0.0 {
let x = cbxba.dot(&cbxcd) / vdot1.sqrt();
let y = cbxba.dot(&cbxcbxcd) / vdot2.sqrt();
y.atan2(x).to_degrees()
} else {
360.0
}
}
fn circ_delta(a: Float, target: Float) -> Float {
let d = (a - target).abs();
if d > 180.0 {
360.0 - d
} else {
d
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dss_2lao() {
const PYMOL: &str = "~~~~EEEEEE~~~~~~~~EEE~~EEEE~HHHHHHHHHHHHH~~EEEEEE~~HHHHHHHHHH~~~~EEE~~~~~~HHHHHH~~~~~~~~~~~EEEEEE~~~~~~~~HHHH~~~EEEEE~~~HHHHHHHHHHHHH~~EEEEE~~HHHHHHHHHH~~~~EEEEEHHHHHHH~~~~HHHH~EEE~~~~~~~HHHH~~~~~~~~~~~~HHHHHHHHHHHHHHHHH~HHHHHHHHH~~~~~~~~";
let sys = System::from_file("tests/2lao.pdb").expect("load 2lao.pdb");
let sel = sys.select_bound("protein").expect("select protein");
let s = Dss::new(&sel).ss_string();
assert_eq!(s.len(), PYMOL.len(), "residue count");
let agree = s.bytes().zip(PYMOL.bytes()).filter(|(a, b)| a == b).count();
let pct = 100.0 * agree as f32 / PYMOL.len() as f32;
println!("PyMOL-port: {s}");
println!("agreement with PyMOL dss: {agree}/{} = {pct:.1}%", PYMOL.len());
assert!(pct >= 98.0, "only {pct:.1}% agreement with PyMOL dss");
assert_eq!(&s[177..188], "EEE~~~~~~~H");
}
}