use crate::core::PdbStructure;
use crate::error::PdbError;
use super::transform::{AtomSelection, extract_coords_by_selection};
pub fn rmsd_from_coords(
coords1: &[(f64, f64, f64)],
coords2: &[(f64, f64, f64)],
) -> Result<f64, PdbError> {
if coords1.is_empty() || coords2.is_empty() {
return Err(PdbError::NoAtomsSelected(
"Cannot compute RMSD with empty coordinate sets".to_string(),
));
}
if coords1.len() != coords2.len() {
return Err(PdbError::AtomCountMismatch {
expected: coords1.len(),
found: coords2.len(),
});
}
let n = coords1.len() as f64;
let sum_sq: f64 = coords1
.iter()
.zip(coords2.iter())
.map(|((x1, y1, z1), (x2, y2, z2))| {
let dx = x1 - x2;
let dy = y1 - y2;
let dz = z1 - z2;
dx * dx + dy * dy + dz * dz
})
.sum();
Ok((sum_sq / n).sqrt())
}
pub fn calculate_rmsd(
structure1: &PdbStructure,
structure2: &PdbStructure,
selection: AtomSelection,
) -> Result<f64, PdbError> {
let coords1 = extract_coords_by_selection(structure1, &selection, None);
let coords2 = extract_coords_by_selection(structure2, &selection, None);
if coords1.is_empty() {
return Err(PdbError::NoAtomsSelected(format!(
"No atoms matching {:?} selection in first structure",
selection
)));
}
if coords2.is_empty() {
return Err(PdbError::NoAtomsSelected(format!(
"No atoms matching {:?} selection in second structure",
selection
)));
}
rmsd_from_coords(&coords1, &coords2)
}
pub fn calculate_rmsd_chain(
structure1: &PdbStructure,
structure2: &PdbStructure,
selection: AtomSelection,
chain_id: &str,
) -> Result<f64, PdbError> {
let coords1 = extract_coords_by_selection(structure1, &selection, Some(chain_id));
let coords2 = extract_coords_by_selection(structure2, &selection, Some(chain_id));
if coords1.is_empty() {
return Err(PdbError::NoAtomsSelected(format!(
"No atoms matching {:?} selection in chain {} of first structure",
selection, chain_id
)));
}
if coords2.is_empty() {
return Err(PdbError::NoAtomsSelected(format!(
"No atoms matching {:?} selection in chain {} of second structure",
selection, chain_id
)));
}
rmsd_from_coords(&coords1, &coords2)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::records::Atom;
fn create_atom(x: f64, y: f64, z: f64, name: &str, residue_seq: i32) -> Atom {
Atom {
serial: residue_seq,
name: name.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(offset_x: f64) -> PdbStructure {
let mut structure = PdbStructure::new();
structure.atoms = vec![
create_atom(0.0 + offset_x, 0.0, 0.0, "CA", 1),
create_atom(3.8 + offset_x, 0.0, 0.0, "CA", 2),
create_atom(7.6 + offset_x, 0.0, 0.0, "CA", 3),
];
structure
}
#[test]
fn test_rmsd_identical_coords() {
let coords = vec![(0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0)];
let rmsd = rmsd_from_coords(&coords, &coords).unwrap();
assert!(rmsd < 1e-10);
}
#[test]
fn test_rmsd_known_displacement() {
let coords1 = vec![(0.0, 0.0, 0.0), (1.0, 0.0, 0.0)];
let coords2 = vec![(1.0, 0.0, 0.0), (2.0, 0.0, 0.0)];
let rmsd = rmsd_from_coords(&coords1, &coords2).unwrap();
assert!((rmsd - 1.0).abs() < 1e-10);
}
#[test]
fn test_rmsd_single_displaced() {
let coords1 = vec![(0.0, 0.0, 0.0), (0.0, 0.0, 0.0)];
let coords2 = vec![(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)];
let rmsd = rmsd_from_coords(&coords1, &coords2).unwrap();
assert!((rmsd - (1.5_f64).sqrt()).abs() < 1e-10);
}
#[test]
fn test_rmsd_empty_coords() {
let empty: Vec<(f64, f64, f64)> = vec![];
let coords = vec![(0.0, 0.0, 0.0)];
assert!(rmsd_from_coords(&empty, &coords).is_err());
assert!(rmsd_from_coords(&coords, &empty).is_err());
}
#[test]
fn test_rmsd_mismatched_length() {
let coords1 = vec![(0.0, 0.0, 0.0), (1.0, 0.0, 0.0)];
let coords2 = vec![(0.0, 0.0, 0.0)];
let result = rmsd_from_coords(&coords1, &coords2);
assert!(matches!(result, Err(PdbError::AtomCountMismatch { .. })));
}
#[test]
fn test_calculate_rmsd_identical_structures() {
let structure = create_test_structure(0.0);
let rmsd = calculate_rmsd(&structure, &structure, AtomSelection::CaOnly).unwrap();
assert!(rmsd < 1e-10);
}
#[test]
fn test_calculate_rmsd_translated_structure() {
let structure1 = create_test_structure(0.0);
let structure2 = create_test_structure(1.0);
let rmsd = calculate_rmsd(&structure1, &structure2, AtomSelection::CaOnly).unwrap();
assert!((rmsd - 1.0).abs() < 1e-10);
}
#[test]
fn test_calculate_rmsd_no_matching_atoms() {
let mut structure = PdbStructure::new();
structure.atoms = vec![create_atom(0.0, 0.0, 0.0, "CB", 1)];
let result = calculate_rmsd(&structure, &structure, AtomSelection::CaOnly);
assert!(matches!(result, Err(PdbError::NoAtomsSelected(_))));
}
}