molframe-pdb 0.6.1

Legacy PDB reading and writing for molframe
Documentation
use super::*;
use molframe_core::io::InputBuffer;
use molframe_core::structure::AtomRef;
use std::fmt::Write as _;

#[test]
fn common_records_do_not_retain_atom_lines() {
    let text = "HEADER    EXAMPLE\nATOM      1  N   GLY A   1      1.000   1.000   1.000  1.00  0.00           N\nCONECT    1    2\n";
    let mut common = CommonRecords::new();
    for line in Lines::new(text) {
        common.observe(&line.expect("line"));
    }
    assert_eq!(common.lines.len(), 2);
    assert_eq!(fixed::record(common.lines[0].text), "HEADER");
    assert_eq!(fixed::record(common.lines[1].text), "CONECT");
}

#[test]
fn many_ragged_models_are_materialised_in_deposition_order() {
    let mut text = String::new();
    for model in 1..=128 {
        let atom = if model == 128 { " O  " } else { " N  " };
        let written = writeln!(
            text,
            "MODEL     {model:4}\nATOM      1 {atom} GLY A   1      1.000   1.000   1.000  1.00  0.00           N\nENDMDL"
        );
        assert!(written.is_ok());
    }
    let input = InputBuffer::from_bytes(text.into_bytes());
    let (structure, _) = crate::read(&input, &ReadOptions::new())
        .unwrap_or_else(|findings| panic!("read failed: {findings:?}"));
    let Some(models) = structure.ragged_models() else {
        panic!("the final identity change must select ragged storage");
    };
    assert_eq!(models.len(), 128);
    let numbers: Vec<_> = structure
        .data()
        .models()
        .filter_map(molframe_core::structure::ModelRef::number)
        .collect();
    assert_eq!(numbers.first(), Some(&1));
    assert_eq!(numbers.last(), Some(&128));
}

#[test]
fn per_model_occupancy_and_b_factor_stay_dense_and_are_reported_once() {
    let text = "MODEL        1\nATOM      1  N   GLY A   1      1.000   1.000   1.000  1.00 10.00           N\nENDMDL\nMODEL        2\nATOM      1  N   GLY A   1      2.000   2.000   2.000  0.50 20.00           N\nENDMDL\nMODEL        3\nATOM      1  N   GLY A   1      3.000   3.000   3.000  0.25 30.00           N\nENDMDL\n";
    let input = InputBuffer::from_bytes(text.as_bytes().to_vec());
    let (structure, findings) = crate::read(&input, &ReadOptions::new())
        .unwrap_or_else(|findings| panic!("read failed: {findings:?}"));
    assert!(structure.ragged_models().is_none());
    assert_eq!(structure.model_count(), 3);
    let atom = structure.data().atoms().next().expect("one atom");
    assert_eq!(atom.occupancy(), Some(1.0));
    assert_eq!(atom.b_factor(), Some(10.0));
    let noted = findings
        .iter()
        .filter(|finding| finding.code() == molframe_core::diagnostic::Code::W3015)
        .count();
    assert_eq!(noted, 1);
}

#[test]
fn a_non_coordinate_identity_change_selects_independent_annotations() {
    let text = "MODEL        1\nATOM      1  N   GLY A   1      1.000   1.000   1.000  1.00 10.00           N\nENDMDL\nMODEL        2\nATOM      1  O   GLY A   1      2.000   2.000   2.000  0.50 20.00           O\nENDMDL\n";
    let input = InputBuffer::from_bytes(text.as_bytes().to_vec());
    let (structure, _) = crate::read(&input, &ReadOptions::new())
        .unwrap_or_else(|findings| panic!("read failed: {findings:?}"));
    let Some(models) = structure.ragged_models() else {
        panic!("an atom identity change must be ragged");
    };
    let occupancies: Vec<_> = models
        .iter()
        .filter_map(|model| model.data().atoms().next().and_then(AtomRef::occupancy))
        .collect();
    assert_eq!(occupancies, [1.0, 0.5]);
}

#[test]
fn ragged_models_receive_shared_metadata_and_connectivity() {
    use crate::PdbHeadersExt as _;

    let text = "HEADER    TEST                                      01-JAN-00   9XYZ\nCRYST1   10.000   11.000   12.000  90.00  90.00  90.00 P 1\nMODEL        1\nATOM      1  N   GLY A   1      1.000   1.000   1.000  1.00 10.00           N\nATOM      2  C   GLY A   1      2.000   2.000   2.000  1.00 10.00           C\nENDMDL\nMODEL        2\nATOM      1  O   GLY A   1      1.000   1.000   1.000  1.00 10.00           O\nATOM      2  C   GLY A   1      2.000   2.000   2.000  1.00 10.00           C\nENDMDL\nCONECT    1    2\n";
    let input = InputBuffer::from_bytes(text.as_bytes().to_vec());
    let (structure, _) = crate::read(&input, &ReadOptions::new())
        .unwrap_or_else(|findings| panic!("read failed: {findings:?}"));
    let Some(models) = structure.ragged_models() else {
        panic!("identity-changing models must be ragged");
    };
    assert!(models.iter().all(|model| model.data().bonds.len() == 1));
    assert!(models.iter().all(|model| model.data().cell.is_some()));
    assert!(structure.pdb_headers().is_some());
}

#[test]
fn a_ragged_pdb_ensemble_is_refused_by_the_model_guard() {
    let text = "MODEL        1\nATOM      1  N   GLY A   1      1.000   1.000   1.000  1.00 10.00           N\nENDMDL\nMODEL        2\nATOM      1  O   GLY A   1      2.000   2.000   2.000  1.00 20.00           O\nENDMDL\n";
    let input = InputBuffer::from_bytes(text.as_bytes().to_vec());
    let (structure, _) = crate::read(&input, &ReadOptions::new())
        .unwrap_or_else(|findings| panic!("read failed: {findings:?}"));
    assert!(structure.is_ragged_ensemble());
    // The outer shell has no atoms of its own, so an unguarded analysis sees an
    // empty molecule; the guard turns that into an explicit refusal.
    assert_eq!(structure.data().atoms().count(), 0);
    let Err(finding) = structure.require_resolved_model() else {
        panic!("a ragged ensemble must be refused");
    };
    assert_eq!(finding.code(), molframe_core::diagnostic::Code::E6003);
    let Some((model, _)) = structure.model_snapshot(molframe_core::index::ModelIndex::new(1))
    else {
        panic!("model 2 exists");
    };
    assert!(model.require_resolved_model().is_ok());
    assert_eq!(model.data().atoms().count(), 1);
}