molframe-pdb 0.6.0

Legacy PDB reading and writing for molframe
Documentation
use super::super::pdb::{PdbOptions, write};
use molframe_core::io::{InputBuffer, ReadOptions};
use molframe_core::{SecondaryStructure as Ss, Structure};
use std::fmt::Write as _;

#[test]
fn author_numbering_need_not_increase_along_a_secondary_range() {
    let structure = identity_fixture(
        &[("A", "20", ""), ("A", "10", ""), ("A", "15", "")],
        &[Ss::PolyProline, Ss::PolyProline, Ss::Unknown],
    );
    let output = write(
        &structure,
        &PdbOptions::new().namespace(super::super::pdb::PdbIdentifierNamespace::Auth),
    )
    .expect("author projection");
    let (roundtrip, _) = crate::read(
        &InputBuffer::from_bytes(output.into_bytes()),
        &ReadOptions::new(),
    )
    .expect("read output");
    assert_eq!(
        roundtrip.secondary_structure(),
        structure.secondary_structure()
    );
}

#[test]
fn selecting_out_a_residue_splits_secondary_records_without_extending_endpoints() {
    struct WithoutMiddle;
    impl molframe_core::io::Select for WithoutMiddle {
        fn accept_residue(&self, residue: molframe_core::index::ResidueIndex) -> bool {
            residue.get() != 1
        }
    }
    let structure = identity_fixture(
        &[("A", "10", "A"), ("A", "10", "B"), ("A", "10", "C")],
        &[Ss::PolyProline; 3],
    );
    let output = crate::write_selected(&structure, &PdbOptions::new(), &WithoutMiddle)
        .expect("selected writer");
    assert_eq!(
        output
            .lines()
            .filter(|line| line.starts_with("HELIX"))
            .count(),
        2
    );
    let (roundtrip, _) = crate::read(
        &InputBuffer::from_bytes(output.into_bytes()),
        &ReadOptions::new(),
    )
    .expect("read selected output");
    assert_eq!(roundtrip.secondary_structure(), &[Ss::PolyProline; 2]);
    assert_eq!(
        roundtrip
            .data()
            .residues()
            .filter_map(molframe_core::structure::ResidueRef::ins_code)
            .collect::<Vec<_>>(),
        ["A", "C"]
    );
}

#[test]
fn a_missing_secondary_component_refuses_before_streaming() {
    let source = write(&fixture(), &PdbOptions::new())
        .expect("fixture writer")
        .replace("GLY", "   ");
    let (structure, _) = crate::read(
        &InputBuffer::from_bytes(source.into_bytes()),
        &ReadOptions::new(),
    )
    .expect("blank component fixture");
    let mut output = Vec::new();
    assert!(crate::write_to(&structure, &PdbOptions::new(), &mut output).is_err());
    assert!(output.is_empty());
}

fn fixture() -> Structure {
    let mut source = String::new();
    for residue in 1..=7 {
        writeln!(source, "ATOM  {residue:>5}  CA  GLY A{residue:>4}    {:>8.3}   0.000   0.000  1.00 10.00           C", f64::from(residue) * 3.8).expect("write into String");
    }
    source.push_str(
        "END
",
    );
    let input = InputBuffer::from_bytes(source.into_bytes());
    let (raw, _) = crate::read(&input, &ReadOptions::new()).expect("fixture reads");
    let mut data = raw.data().clone();
    data.secondary_structure = vec![
        Ss::AlphaHelix,
        Ss::ThreeTenHelix,
        Ss::PiHelix,
        Ss::PolyProline,
        Ss::OtherHelix,
        Ss::Strand,
        Ss::Unknown,
    ]
    .into();
    Structure::new(data)
}

#[test]
fn generated_helix_classes_and_sheets_round_trip() {
    let structure = fixture();
    let text = write(&structure, &PdbOptions::new()).expect("write");
    let input = InputBuffer::from_bytes(text.into_bytes());
    let (roundtrip, _) = crate::read(&input, &ReadOptions::new()).expect("read output");
    assert_eq!(
        structure.secondary_structure(),
        roundtrip.secondary_structure()
    );
}

#[test]
fn renamed_chains_are_used_in_secondary_records_too() {
    let structure = fixture();
    let text = write(&structure, &PdbOptions::new().chain_map("A", "Z")).expect("write");
    for record in text.lines().filter(|line| line.starts_with("HELIX")) {
        assert_eq!(&record[19..20], "Z");
        assert_eq!(&record[31..32], "Z");
    }
    let input = InputBuffer::from_bytes(text.into_bytes());
    let (roundtrip, _) = crate::read(&input, &ReadOptions::new()).expect("read output");
    assert_eq!(
        structure.secondary_structure(),
        roundtrip.secondary_structure()
    );
}

fn identity_fixture(rows: &[(&str, &str, &str)], states: &[Ss]) -> Structure {
    let mut source = String::new();
    for (serial, &(chain, sequence, insertion)) in (1..).zip(rows) {
        writeln!(source, "ATOM  {serial:>5}  CA  GLY {chain:1}{sequence:>4}{insertion:1}      0.000   0.000   0.000  1.00 10.00           C").expect("String");
    }
    let (raw, _) = crate::read(
        &InputBuffer::from_bytes(source.into_bytes()),
        &ReadOptions::new(),
    )
    .expect("read fixture");
    let mut data = raw.data().clone();
    data.secondary_structure = states.to_vec().into();
    Structure::new(data)
}

#[test]
fn secondary_writer_round_trips_insertions_negative_and_both_hybrid36_cases() {
    let rows = [
        ("A", "-2", ""),
        ("A", "-1", ""),
        ("A", "10", ""),
        ("A", "10", "A"),
        ("A", "10", "B"),
        ("A", "10", "C"),
        ("A", "A000", ""),
        ("A", "A001", ""),
        ("A", "a000", ""),
        ("A", "a001", ""),
        ("B", "10", "A"),
    ];
    let states = [
        Ss::OtherHelix,
        Ss::OtherHelix,
        Ss::Unknown,
        Ss::PolyProline,
        Ss::PolyProline,
        Ss::Unknown,
        Ss::Strand,
        Ss::Strand,
        Ss::ThreeTenHelix,
        Ss::ThreeTenHelix,
        Ss::PiHelix,
    ];
    let structure = identity_fixture(&rows, &states);
    let options = PdbOptions::new().hybrid36(true);
    let mut output = Vec::new();
    crate::write_to(&structure, &options, &mut output).expect("stream real writer");
    let text = str::from_utf8(&output).expect("ASCII");
    let pp = text
        .lines()
        .find(|line| line.starts_with("HELIX") && &line[38..40] == "10")
        .expect("PPII record");
    assert_eq!(&pp[21..26], "  10A");
    assert_eq!(&pp[33..38], "  10B");
    let (roundtrip, _) = crate::read(&InputBuffer::from_bytes(output), &ReadOptions::new())
        .expect("read actual writer");
    assert_eq!(roundtrip.secondary_structure(), states);
    let ids: Vec<_> = roundtrip
        .data()
        .residues()
        .map(|residue| (residue.auth_seq_id(), residue.ins_code()))
        .collect();
    let original: Vec<_> = structure
        .data()
        .residues()
        .map(|residue| (residue.auth_seq_id(), residue.ins_code()))
        .collect();
    assert_eq!(ids, original);
}

#[test]
fn a_chain_map_collision_refuses_secondary_output_before_streaming() {
    let structure = identity_fixture(
        &[("A", "1", ""), ("B", "1", "")],
        &[Ss::PolyProline, Ss::OtherHelix],
    );
    let mut output = Vec::new();
    let result = crate::write_to(
        &structure,
        &PdbOptions::new().chain_map("B", "A"),
        &mut output,
    );
    assert!(result.is_err());
    assert!(output.is_empty());
}

#[test]
fn secondary_sequence_overflow_refuses_even_with_hybrid36_enabled() {
    let raw = fixture();
    let mut data = raw.data().clone();
    data.topology.residues = molframe_core::topology::ResidueTable::default();
    for residue in raw.data().residues() {
        let record = molframe_core::topology::ResidueRecord {
            label_comp_id: raw
                .data()
                .topology
                .residues
                .label_comp_id(residue.index())
                .expect("component"),
            auth_comp_id: molframe_core::optional::OptionalSymbol::NONE,
            label_seq_id: molframe_core::optional::OptionalI32::some(i32::MAX),
            auth_seq_id: molframe_core::optional::OptionalI32::NONE,
            ins_code: molframe_core::optional::OptionalSymbol::NONE,
            het: false,
        };
        data.topology
            .residues
            .push(record, residue.index().get()..residue.index().get() + 1)
            .expect("residue");
    }
    let mut output = Vec::new();
    assert!(
        crate::write_to(
            &Structure::new(data),
            &PdbOptions::new().hybrid36(true),
            &mut output
        )
        .is_err()
    );
    assert!(output.is_empty());
}

#[test]
fn unrepresentable_secondary_states_are_not_relabelled_as_helix_or_strand() {
    let structure = identity_fixture(
        &[
            ("A", "1", ""),
            ("A", "2", ""),
            ("A", "3", ""),
            ("A", "4", ""),
        ],
        &[Ss::Coil, Ss::Bend, Ss::BetaBridge, Ss::Turn],
    );
    let text = write(&structure, &PdbOptions::new()).expect("write atoms");
    assert!(
        !text
            .lines()
            .any(|line| line.starts_with("HELIX") || line.starts_with("SHEET"))
    );
}

#[test]
fn states_a_reader_derived_are_not_written_as_deposited_records() {
    let mut data = identity_fixture(
        &[("A", "1", ""), ("A", "2", ""), ("A", "3", "")],
        &[Ss::AlphaHelix; 3],
    )
    .data()
    .clone();
    data.secondary_source = std::sync::Arc::new(vec![molframe_core::SecondarySource::Dssp; 3]);
    let text = write(&Structure::new(data), &PdbOptions::new()).expect("write atoms");
    assert!(
        !text.lines().any(|line| line.starts_with("HELIX")),
        "a DSSP assignment is not a deposited HELIX record"
    );
}