use molframe_chem::PolymerAtomRole;
use molframe_core::index::{AtomIndex, ChainIndex, ModelIndex, ResidueIndex};
use molframe_core::structure::{AtomRef, ResidueRef, Structure};
use molframe_core::{Diagnostic, Findings};
use molframe_geom::{BackboneResidue, BackboneTorsions};
use super::roles::{require_polymer_roles, residue_has_role, role_atom};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BackboneTorsionRecord {
pub residue: ResidueIndex,
pub torsions: BackboneTorsions,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ProteinAlphaTrace {
pub chain: ChainIndex,
pub positions: Vec<Option<[f32; 3]>>,
}
pub fn structure_protein_alpha_traces(
structure: &Structure,
) -> Result<Vec<ProteinAlphaTrace>, Findings> {
require_polymer_roles(structure)?;
let mut traces = Vec::new();
for chain in structure.data().chains() {
let mut positions = Vec::new();
for residue in chain.residues().filter(|residue| {
residue_has_role(structure, *residue, PolymerAtomRole::PROTEIN_BACKBONE)
}) {
positions.push(
role_atom(structure, residue, PolymerAtomRole::PROTEIN_ALPHA_CARBON)?
.and_then(AtomRef::position),
);
}
if !positions.is_empty() {
traces.push(ProteinAlphaTrace {
chain: chain.index(),
positions,
});
}
}
Ok(traces)
}
pub fn structure_backbone_torsions(
structure: &Structure,
) -> Result<Vec<BackboneTorsionRecord>, Findings> {
structure_backbone_torsions_model(structure, ModelIndex::new(0))
}
pub fn structure_backbone_torsions_model(
structure: &Structure,
model: ModelIndex,
) -> Result<Vec<BackboneTorsionRecord>, Findings> {
require_polymer_roles(structure)?;
let Some(positions) = structure.model_positions(model) else {
return Ok(Vec::new());
};
let mut output = Vec::with_capacity(structure.residue_count());
for chain in structure.data().chains() {
let residues: Vec<ResidueRef<'_>> = chain
.residues()
.filter(|residue| {
residue_has_role(structure, *residue, PolymerAtomRole::PROTEIN_BACKBONE)
})
.collect();
let mut backbone = Vec::with_capacity(residues.len());
for (position, residue) in residues.iter().enumerate() {
let next = residues.get(position + 1).copied();
backbone.push(project(structure, positions, *residue, next)?);
}
let torsions = molframe_geom::backbone_torsions(&backbone);
output.extend(
residues
.into_iter()
.zip(torsions)
.map(|(residue, torsions)| BackboneTorsionRecord {
residue: residue.index(),
torsions,
}),
);
}
Ok(output)
}
fn project(
structure: &Structure,
positions: &[[f32; 3]],
residue: ResidueRef<'_>,
next: Option<ResidueRef<'_>>,
) -> Result<BackboneResidue, Diagnostic> {
let nitrogen = role_atom(structure, residue, PolymerAtomRole::PROTEIN_NITROGEN)?;
let alpha_carbon = role_atom(structure, residue, PolymerAtomRole::PROTEIN_ALPHA_CARBON)?;
let carbonyl_carbon = role_atom(structure, residue, PolymerAtomRole::PROTEIN_CARBONYL_CARBON)?;
Ok(BackboneResidue {
nitrogen: nitrogen.and_then(|atom| position(positions, atom)),
alpha_carbon: alpha_carbon.and_then(|atom| position(positions, atom)),
carbonyl_carbon: carbonyl_carbon.and_then(|atom| position(positions, atom)),
connected_to_next: next
.map(|next| role_atom(structure, next, PolymerAtomRole::PROTEIN_NITROGEN))
.transpose()?
.flatten()
.is_some_and(|next_nitrogen| {
connected(structure, carbonyl_carbon, Some(next_nitrogen))
}),
})
}
fn connected(
structure: &Structure,
carbon: Option<AtomRef<'_>>,
nitrogen: Option<AtomRef<'_>>,
) -> bool {
let (Some(carbon), Some(nitrogen)) = (carbon, nitrogen) else {
return false;
};
structure.data().bonds.is_available() && bonded(structure, carbon.index(), nitrogen.index())
}
fn position(positions: &[[f32; 3]], atom: AtomRef<'_>) -> Option<[f32; 3]> {
positions.get(atom.index().as_usize()).copied()
}
fn bonded(structure: &Structure, carbon: AtomIndex, nitrogen: AtomIndex) -> bool {
structure
.data()
.bonds
.adjacency(structure.atom_count())
.neighbours(carbon)
.binary_search(&nitrogen)
.is_ok()
}
#[cfg(test)]
#[path = "backbone_tests.rs"]
mod tests;