molframe 0.5.1

A batteries-included structural bioinformatics engine
Documentation
use super::*;
use crate::validation_inputs::ValidationInputError;
use crate::{AnalysisPolicy, ReadOptions, read_bytes};

const PDB: &str = "\
ATOM      1  N   ALA A   1      11.104   6.134  -6.504  1.00  0.00           N
ATOM      2  CA  ALA A   1      12.560   6.195  -6.504  1.00  0.00           C
ATOM      3  C   ALA A   1      13.100   7.600  -6.504  1.00  0.00           C
ATOM      4  N   GLY B   1       1.000   2.000   3.000  1.00  0.00           N
ATOM      5  CA  GLY B   1       2.000   2.500   3.200  1.00  0.00           C
END
";

fn structure() -> Structure {
    match read_bytes(PDB.into(), Some("t.pdb"), &ReadOptions::new()) {
        Ok((structure, _)) => structure,
        Err(error) => panic!("fixture failed: {error}"),
    }
}

fn write(text: &str) -> tempfile::NamedTempFile {
    let file = match tempfile::Builder::new().suffix(".toml").tempfile() {
        Ok(file) => file,
        Err(error) => panic!("temporary file failed: {error}"),
    };
    if let Err(error) = std::fs::write(file.path(), text) {
        panic!("write failed: {error}")
    }
    file
}

const ZERO: &str = "[[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]";

fn group(id: &str, selection: &str) -> String {
    format!(
        "[[group]]\nid = '{id}'\nselection = '{selection}'\norigin = [1.0, 2.0, 3.0]\nt = [[0.1, 0.0, 0.0], [0.0, 0.2, 0.0], [0.0, 0.0, 0.3]]\nl = {ZERO}\ns = {ZERO}\n"
    )
}

#[test]
fn planes_resolve_their_selections_in_the_structure() {
    let file = write(
        "[[plane]]\nid = 'a'\nselection = 'chain A'\n[[plane]]\nid = 'b'\nselection = 'chain B'\n",
    );
    let planes = match read_plane_restraints(file.path(), &structure(), &AnalysisPolicy::default())
    {
        Ok(planes) => planes,
        Err(error) => panic!("planes failed: {error}"),
    };
    assert_eq!(planes.len(), 2);
    assert_eq!(planes[0].id, "a");
    assert_eq!(planes[0].atoms.len(), 3);
    assert_eq!(planes[1].atoms.len(), 2);
}

#[test]
fn a_selection_matching_nothing_is_refused_rather_than_covering_nothing() {
    let file = write("[[plane]]\nid = 'a'\nselection = 'chain Z'\n");
    assert!(matches!(
        read_plane_restraints(file.path(), &structure(), &AnalysisPolicy::default()),
        Err(ValidationInputError::InvalidValue { .. })
    ));
}

#[test]
fn a_selection_that_does_not_parse_names_its_plane() {
    let file = write("[[plane]]\nid = 'broken'\nselection = 'chain ((('\n");
    match read_plane_restraints(file.path(), &structure(), &AnalysisPolicy::default()) {
        Err(ValidationInputError::Selection { id, .. }) => assert_eq!(id, "broken"),
        other => panic!("expected a selection error, got {other:?}"),
    }
}

#[test]
fn a_repeated_plane_id_is_refused() {
    let file = write(
        "[[plane]]\nid = 'a'\nselection = 'chain A'\n[[plane]]\nid = 'a'\nselection = 'chain B'\n",
    );
    assert!(matches!(
        read_plane_restraints(file.path(), &structure(), &AnalysisPolicy::default()),
        Err(ValidationInputError::InvalidValue { .. })
    ));
}

#[test]
fn tls_groups_carry_the_declared_model() {
    let file = write(&format!(
        "{}{}",
        group("A", "chain A"),
        group("B", "chain B")
    ));
    let groups = match read_tls_groups(file.path(), &structure(), &AnalysisPolicy::default()) {
        Ok(groups) => groups,
        Err(error) => panic!("groups failed: {error}"),
    };
    assert_eq!(groups.len(), 2);
    assert_eq!(groups[0].id, "A");
    assert_eq!(groups[0].atoms.len(), 3);
    for (read, expected) in groups[0].model.origin.iter().zip([1.0, 2.0, 3.0]) {
        assert!((read - expected).abs() < f64::EPSILON);
    }
    assert!((groups[0].model.translation[1][1] - 0.2).abs() < f64::EPSILON);
}

#[test]
fn a_tensor_that_is_not_three_by_three_is_a_schema_error() {
    let file = write(&group("A", "chain A").replace(
        "t = [[0.1, 0.0, 0.0], [0.0, 0.2, 0.0], [0.0, 0.0, 0.3]]",
        "t = [[0.1, 0.0], [0.0, 0.2]]",
    ));
    assert!(matches!(
        read_tls_groups(file.path(), &structure(), &AnalysisPolicy::default()),
        Err(ValidationInputError::Toml(_))
    ));
}

#[test]
fn a_nonfinite_tensor_entry_is_refused() {
    let file = write(&group("A", "chain A").replace("0.1,", "nan,"));
    assert!(matches!(
        read_tls_groups(file.path(), &structure(), &AnalysisPolicy::default()),
        Err(ValidationInputError::InvalidValue { .. })
    ));
}

#[test]
fn read_planes_drive_the_planarity_kernel_end_to_end() {
    use crate::ValidationExt;
    use molframe_validate::PlanarityOptions;
    const FLAT_AND_BENT: &str = "\
ATOM      1  C1  LIG A   1       0.000   0.000   0.000  1.00  0.00           C
ATOM      2  C2  LIG A   1       1.000   0.000   0.000  1.00  0.00           C
ATOM      3  C3  LIG A   1       0.000   1.000   0.000  1.00  0.00           C
ATOM      4  C4  LIG A   1       1.000   1.000   0.000  1.00  0.00           C
ATOM      5  C5  LIG B   1       0.000   0.000   0.000  1.00  0.00           C
ATOM      6  C6  LIG B   1       1.000   0.000   0.000  1.00  0.00           C
ATOM      7  C7  LIG B   1       0.000   1.000   0.000  1.00  0.00           C
ATOM      8  C8  LIG B   1       1.000   1.000   2.000  1.00  0.00           C
END
";
    let structure = match read_bytes(FLAT_AND_BENT.into(), Some("t.pdb"), &ReadOptions::new()) {
        Ok((structure, _)) => structure,
        Err(error) => panic!("fixture failed: {error}"),
    };
    let file = write(
        "[[plane]]\nid = 'flat'\nselection = 'chain A'\n[[plane]]\nid = 'bent'\nselection = 'chain B'\n",
    );
    let restraints =
        match read_plane_restraints(file.path(), &structure, &AnalysisPolicy::default()) {
            Ok(restraints) => restraints,
            Err(error) => panic!("planes failed: {error}"),
        };
    let report = match structure.plane_restraint_outliers(
        &restraints,
        PlanarityOptions {
            maximum_deviation: 0.1,
            plane_fit: molframe_geom::EigenOptions::standard(),
        },
    ) {
        Ok(report) => report,
        Err(error) => panic!("planarity failed: {error}"),
    };
    assert_eq!(report.assessed, 2);
    assert_eq!(report.flags.len(), 1);
    assert_eq!(report.flags[0].id, "bent");
}