use crate::structure::Structure;
use crate::{BondOrder, compare::ligand_symmetry_rmsd};
use molframe_chem::{Component, ComponentAtom, ComponentBond, ComponentKind};
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Clone, Debug)]
pub struct HeavyLigand {
pub component: Component,
pub atoms: Vec<usize>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LigandPairing {
ByName,
ByOrder,
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum LigandCompareError {
#[error("the reference ligand has no heavy atoms")]
NoHeavyAtoms,
#[error("{reference} heavy atoms in the reference ligand, {model} in the prediction's")]
AtomCount {
reference: usize,
model: usize,
},
#[error("the prediction's ligand atoms do not match the reference's")]
Unpaired,
#[error("{0}")]
Symmetry(String),
}
fn heavy_indices(ligand: &Structure) -> Vec<usize> {
ligand
.atoms()
.into_iter()
.filter(|a| a.element().is_some_and(|e| e.atomic_number() != 1))
.map(|a| a.index().as_usize())
.collect()
}
fn slots(heavy: &[usize]) -> HashMap<usize, usize> {
heavy.iter().enumerate().map(|(s, a)| (*a, s)).collect()
}
#[must_use]
pub fn heavy_atom_component(ligand: &Structure) -> Option<HeavyLigand> {
let heavy = heavy_indices(ligand);
if heavy.is_empty() {
return None;
}
let slot = slots(&heavy);
let name = |s: usize| -> Box<str> { format!("a{s}").into() };
let atoms: Vec<ComponentAtom> = heavy
.iter()
.enumerate()
.filter_map(|(s, a)| {
let element = ligand.atom_at(*a)?.element()?;
Some(ComponentAtom {
name: name(s),
alternate_name: None,
element,
charge: 0,
aromatic: false,
leaving: false,
stereo: None,
})
})
.collect();
let bonds: Vec<ComponentBond> = ligand
.bonds()
.iter()
.filter_map(|b| {
let (x, y) = (
*slot.get(&b.atom_a.as_usize())?,
*slot.get(&b.atom_b.as_usize())?,
);
Some(ComponentBond {
atom_a: name(x),
atom_b: name(y),
order: b.order,
aromatic: matches!(b.order, BondOrder::Aromatic),
stereo: None,
})
})
.collect();
Some(HeavyLigand {
component: Component {
id: "ligand".into(),
name: "ligand".into(),
kind: ComponentKind::NonPolymer,
parent: None,
one_letter_code: None,
formula: None,
atoms: Arc::from(atoms),
bonds: Arc::from(bonds),
ideal_coordinates: None,
model_coordinates: None,
},
atoms: heavy,
})
}
#[must_use]
pub fn heavy_graph_key(ligand: &Structure) -> (Vec<u8>, Vec<(usize, usize)>) {
let heavy = heavy_indices(ligand);
let slot = slots(&heavy);
let elements = heavy
.iter()
.filter_map(|i| {
ligand
.atom_at(*i)
.and_then(molframe_core::structure::AtomRef::element)
.map(molframe_core::Element::atomic_number)
})
.collect();
let mut bonds: Vec<(usize, usize)> = ligand
.bonds()
.iter()
.filter_map(|b| {
let (x, y) = (
*slot.get(&b.atom_a.as_usize())?,
*slot.get(&b.atom_b.as_usize())?,
);
Some((x.min(y), x.max(y)))
})
.collect();
bonds.sort_unstable();
(elements, bonds)
}
fn coordinates_of(ligand: &Structure, heavy: &[usize]) -> Vec<[f32; 3]> {
let all = ligand.coordinates();
heavy.iter().filter_map(|i| all.get(*i).copied()).collect()
}
#[must_use]
pub fn pose_rmsd(a: &Structure, b: &Structure, mapping_limit: usize) -> Option<f64> {
if heavy_graph_key(a) != heavy_graph_key(b) {
return None;
}
let first = heavy_atom_component(a)?;
let heavy_b = heavy_indices(b);
if first.atoms.len() != heavy_b.len() {
return None;
}
let (ra, rb) = (coordinates_of(a, &first.atoms), coordinates_of(b, &heavy_b));
ligand_symmetry_rmsd(&ra, &rb, &first.component, mapping_limit)
.ok()
.map(|r| r.rmsd)
}
fn named_heavy_atoms(ligand: &Structure) -> Vec<(String, u8, [f32; 3])> {
ligand
.atoms()
.into_iter()
.filter_map(|a| {
let e = a.element().filter(|e| e.atomic_number() > 1)?;
Some((
a.name()?.trim().to_owned(),
e.atomic_number(),
a.position()?,
))
})
.collect()
}
fn place(m: &[f32; 16], p: [f32; 3]) -> [f32; 3] {
std::array::from_fn(|i| m[i] * p[0] + m[4 + i] * p[1] + m[8 + i] * p[2] + m[12 + i])
}
pub fn placed_rmsd(
reference: &Structure,
model: &Structure,
placement: &[f32; 16],
mapping_limit: usize,
) -> Result<(f64, LigandPairing), LigandCompareError> {
let heavy = heavy_atom_component(reference).ok_or(LigandCompareError::NoHeavyAtoms)?;
let refs = coordinates_of(reference, &heavy.atoms);
let ref_atoms = named_heavy_atoms(reference);
let mod_atoms = named_heavy_atoms(model);
if ref_atoms.len() != mod_atoms.len() || ref_atoms.len() != refs.len() {
return Err(LigandCompareError::AtomCount {
reference: ref_atoms.len(),
model: mod_atoms.len(),
});
}
let unique = |v: &[(String, u8, [f32; 3])]| {
let mut n: Vec<&str> = v.iter().map(|a| a.0.as_str()).collect();
n.sort_unstable();
n.windows(2).all(|w| w[0] != w[1])
};
let by_name: Option<Vec<[f32; 3]>> = (unique(&ref_atoms) && unique(&mod_atoms))
.then(|| {
ref_atoms
.iter()
.map(|r| {
mod_atoms
.iter()
.find(|m| m.0 == r.0 && m.1 == r.1)
.map(|m| m.2)
})
.collect()
})
.flatten();
let (coords, pairing) = match by_name {
Some(c) => (c, LigandPairing::ByName),
None if ref_atoms.iter().zip(&mod_atoms).all(|(r, m)| r.1 == m.1) => (
mod_atoms.iter().map(|m| m.2).collect(),
LigandPairing::ByOrder,
),
None => return Err(LigandCompareError::Unpaired),
};
let moved: Vec<[f32; 3]> = coords.iter().map(|p| place(placement, *p)).collect();
ligand_symmetry_rmsd(&refs, &moved, &heavy.component, mapping_limit)
.map(|r| (r.rmsd, pairing))
.map_err(|e| LigandCompareError::Symmetry(e.to_string()))
}
#[cfg(test)]
#[path = "ligand_tests.rs"]
mod tests;