mod lddt;
mod rmsd;
mod superposition;
mod transform;
pub use lddt::{LddtOptions, LddtResult, PerResidueLddt, calculate_lddt, per_residue_lddt};
pub use rmsd::{calculate_rmsd, calculate_rmsd_chain, rmsd_from_coords};
pub(crate) use superposition::superpose_coords;
pub use superposition::{
AlignmentResult, PerResidueRmsd, align_structures, calculate_alignment, per_residue_rmsd,
};
pub use transform::{
AtomSelection, apply_transform, apply_transform_to_coords, center_coords, compute_centroid,
extract_coords_by_selection, extract_coords_with_residue_info, translate_structure,
};
use crate::core::PdbStructure;
use crate::error::PdbError;
impl PdbStructure {
pub fn rmsd_to(&self, other: &PdbStructure) -> Result<f64, PdbError> {
calculate_rmsd(self, other, AtomSelection::CaOnly)
}
pub fn rmsd_to_with_selection(
&self,
other: &PdbStructure,
selection: AtomSelection,
) -> Result<f64, PdbError> {
calculate_rmsd(self, other, selection)
}
pub fn align_to(&self, target: &PdbStructure) -> Result<(Self, AlignmentResult), PdbError> {
align_structures(self, target, AtomSelection::CaOnly)
}
pub fn align_to_with_selection(
&self,
target: &PdbStructure,
selection: AtomSelection,
) -> Result<(Self, AlignmentResult), PdbError> {
align_structures(self, target, selection)
}
pub fn per_residue_rmsd_to(
&self,
target: &PdbStructure,
) -> Result<Vec<PerResidueRmsd>, PdbError> {
per_residue_rmsd(self, target, AtomSelection::CaOnly)
}
pub fn per_residue_rmsd_to_with_selection(
&self,
target: &PdbStructure,
selection: AtomSelection,
) -> Result<Vec<PerResidueRmsd>, PdbError> {
per_residue_rmsd(self, target, selection)
}
pub fn lddt_to(&self, reference: &PdbStructure) -> Result<LddtResult, PdbError> {
calculate_lddt(
self,
reference,
AtomSelection::CaOnly,
LddtOptions::default(),
)
}
pub fn lddt_to_with_options(
&self,
reference: &PdbStructure,
selection: AtomSelection,
options: LddtOptions,
) -> Result<LddtResult, PdbError> {
calculate_lddt(self, reference, selection, options)
}
pub fn per_residue_lddt_to(
&self,
reference: &PdbStructure,
) -> Result<Vec<PerResidueLddt>, PdbError> {
per_residue_lddt(
self,
reference,
AtomSelection::CaOnly,
LddtOptions::default(),
)
}
pub fn per_residue_lddt_to_with_options(
&self,
reference: &PdbStructure,
selection: AtomSelection,
options: LddtOptions,
) -> Result<Vec<PerResidueLddt>, PdbError> {
per_residue_lddt(self, reference, selection, options)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::records::Atom;
fn create_atom(x: f64, y: f64, z: f64, residue_seq: i32) -> Atom {
Atom {
serial: residue_seq,
name: "CA".to_string(),
alt_loc: None,
residue_name: "ALA".to_string(),
chain_id: "A".to_string(),
residue_seq,
x,
y,
z,
occupancy: 1.0,
temp_factor: 0.0,
element: "C".to_string(),
ins_code: None,
is_hetatm: false,
}
}
fn create_test_structure() -> PdbStructure {
let mut structure = PdbStructure::new();
structure.atoms = vec![
create_atom(0.0, 0.0, 0.0, 1),
create_atom(3.8, 0.0, 0.0, 2),
create_atom(7.6, 0.0, 0.0, 3),
create_atom(11.4, 0.0, 0.0, 4),
];
structure
}
#[test]
fn test_pdbstructure_rmsd_to_self() {
let structure = create_test_structure();
let rmsd = structure.rmsd_to(&structure).unwrap();
assert!(rmsd < 1e-10);
}
#[test]
fn test_pdbstructure_align_to_self() {
let structure = create_test_structure();
let (aligned, result) = structure.align_to(&structure).unwrap();
assert!(result.rmsd < 1e-10);
assert_eq!(result.num_atoms, 4);
assert_eq!(aligned.atoms.len(), structure.atoms.len());
}
#[test]
fn test_pdbstructure_align_translated() {
let target = create_test_structure();
let mut mobile = target.clone();
for atom in &mut mobile.atoms {
atom.x += 100.0;
atom.y += 100.0;
atom.z += 100.0;
}
let (aligned, result) = mobile.align_to(&target).unwrap();
assert!(result.rmsd < 1e-6);
for (a, t) in aligned.atoms.iter().zip(target.atoms.iter()) {
assert!((a.x - t.x).abs() < 1e-6);
assert!((a.y - t.y).abs() < 1e-6);
assert!((a.z - t.z).abs() < 1e-6);
}
}
#[test]
fn test_pdbstructure_per_residue_rmsd() {
let target = create_test_structure();
let mut mobile = target.clone();
for atom in &mut mobile.atoms {
atom.x += 10.0;
}
let per_res = mobile.per_residue_rmsd_to(&target).unwrap();
assert_eq!(per_res.len(), 4);
for r in &per_res {
assert!(r.rmsd < 1e-6);
}
}
#[test]
fn test_pdbstructure_lddt_to_self() {
let structure = create_test_structure();
let result = structure.lddt_to(&structure).unwrap();
assert!(
(result.score - 1.0).abs() < 1e-10,
"Self-LDDT should be 1.0, got {}",
result.score
);
}
#[test]
fn test_pdbstructure_lddt_translation_invariant() {
let reference = create_test_structure();
let mut model = reference.clone();
for atom in &mut model.atoms {
atom.x += 100.0;
atom.y += 50.0;
atom.z += 25.0;
}
let result = model.lddt_to(&reference).unwrap();
assert!(
(result.score - 1.0).abs() < 1e-10,
"LDDT should be translation invariant, got {}",
result.score
);
}
#[test]
fn test_pdbstructure_lddt_rotation_invariant() {
let reference = create_test_structure();
let mut model = reference.clone();
for atom in &mut model.atoms {
let x = atom.x;
let y = atom.y;
atom.x = -y;
atom.y = x;
}
let result = model.lddt_to(&reference).unwrap();
assert!(
(result.score - 1.0).abs() < 1e-10,
"LDDT should be rotation invariant, got {}",
result.score
);
}
#[test]
fn test_pdbstructure_lddt_perturbed() {
let reference = create_test_structure();
let mut model = reference.clone();
model.atoms[1].y += 5.0;
let result = model.lddt_to(&reference).unwrap();
assert!(
result.score < 1.0,
"Perturbed LDDT should be < 1.0, got {}",
result.score
);
assert!(
result.score > 0.0,
"Perturbed LDDT should be > 0.0, got {}",
result.score
);
}
#[test]
fn test_pdbstructure_per_residue_lddt() {
let reference = create_test_structure();
let model = reference.clone();
let per_res = model.per_residue_lddt_to(&reference).unwrap();
assert_eq!(per_res.len(), 4, "Should have 4 residues");
for r in &per_res {
assert!(
(r.score - 1.0).abs() < 1e-10,
"Self per-residue LDDT should be 1.0"
);
}
}
}