Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ jobs:
with:
components: rustfmt, clippy

- name: Cache APT packages
uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: >-
libxkbcommon-dev libwayland-dev libxrandr-dev libxi-dev
libx11-dev libx11-xcb-dev libxcb-randr0-dev libxcb-xfixes0-dev
- name: Install Linux dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libxkbcommon-dev libwayland-dev libxrandr-dev libxi-dev \
libx11-dev libx11-xcb-dev libxcb-randr0-dev libxcb-xfixes0-dev \
libasound2-dev libfontconfig1-dev libgtk-3-dev libxdo-dev
version: 1.0
pkg-config --exists fontconfig

- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
Expand Down
115 changes: 110 additions & 5 deletions crates/patinae-io/src/bcif/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//!
//! Parses BinaryCIF format files into ObjectMolecule.

use std::borrow::Cow;
use std::collections::{BTreeMap, HashMap};
use std::io::Read;

Expand All @@ -10,8 +11,9 @@ use patinae_mol::{Element, ObjectMolecule, SecondaryStructure};

use crate::assembly::{AssemblyRow, AssemblyRows, CATEGORIES};
use crate::cif::common::{apply_secondary_structure, SecondaryStructureRange, SsCategory};
use crate::entity::{EntityCategory, EntityRows};
use crate::error::{IoError, IoResult};
use crate::logical_models::{build_molecules, ParsedAtom, ParsedModel};
use crate::logical_models::{build_molecules, ParsedAtom, ParsedLabels, ParsedModel};
use crate::traits::MoleculeReader;

use super::decode::{decode_column, decode_mask, ColumnMask, DecodedColumn};
Expand Down Expand Up @@ -103,6 +105,14 @@ impl CategoryColumns {
col.str_at(i).filter(|s| !s.is_empty())
}

/// Text of a column that encoders store either as strings or as integers.
fn code_at(&self, name: &str, i: usize) -> Option<Cow<'_, str>> {
self.str_at(name, i).map(Cow::Borrowed).or_else(|| {
self.int_at(name, i)
.map(|value| Cow::Owned(value.to_string()))
})
}

fn int_at(&self, name: &str, i: usize) -> Option<i32> {
let (col, mask) = self.columns.get(name)?;
if let Some(m) = mask {
Expand Down Expand Up @@ -184,11 +194,15 @@ fn parse_data_block(block: BcifDataBlock, bond_tolerance: f32) -> IoResult<Vec<O
let mut cell = CellInfo::default();
let mut title = String::new();
let mut assemblies = AssemblyRows::default();
let mut entities = EntityRows::default();

for category in &block.categories {
if CATEGORIES.contains(&category.name.as_str()) {
parse_assembly_category(category, &mut assemblies)?;
}
if let Some(entity_category) = EntityCategory::from_name(&category.name) {
parse_entity_category(category, entity_category, &mut entities)?;
}
match category.name.as_str() {
"_atom_site" => parse_atom_site(category, &mut models)?,
"_cell" => parse_cell(category, &mut cell)?,
Expand All @@ -207,6 +221,7 @@ fn parse_data_block(block: BcifDataBlock, bond_tolerance: f32) -> IoResult<Vec<O
}

let definitions = assemblies.resolve()?;
let entities = entities.resolve()?;
if definitions.is_empty() {
for model in models.values_mut() {
model.source_chains.clear();
Expand All @@ -217,6 +232,7 @@ fn parse_data_block(block: BcifDataBlock, bond_tolerance: f32) -> IoResult<Vec<O

for mol in &mut molecules {
mol.assembly.definitions = definitions.clone();
mol.entities = entities.clone();
cell.apply_to(mol, space_group.as_deref());
apply_secondary_structure(mol, &ss_ranges);
mol.classify_atoms();
Expand Down Expand Up @@ -273,6 +289,14 @@ fn parse_atom_site(
// become "A2", which is valid in the selection language.
let chain_id = chain.replace('-', "");

// mmCIF label residue ids, kept alongside the auth ids above
let label_asym_id = cols.str_at("label_asym_id", i);
let label_entity_id = cols.code_at("label_entity_id", i);
let label_seq_id = cols
.int_at("label_seq_id", i)
.and_then(|seq_id| u32::try_from(seq_id).ok())
.filter(|&seq_id| seq_id > 0);

// Alt loc
let alt = cols
.str_at("label_alt_id", i)
Expand Down Expand Up @@ -310,9 +334,13 @@ fn parse_atom_site(
let model = models
.entry(model_num)
.or_insert_with(|| ParsedModel::new(model_num));
model
.source_chains
.push(cols.str_at("label_asym_id", i).unwrap_or(""));
model.source_chains.push(label_asym_id.unwrap_or(""));
let labels = ParsedLabels::new(
label_asym_id,
label_entity_id.as_deref(),
label_seq_id,
model.atoms.last(),
);
model.atoms.push(ParsedAtom {
name: atom_name.to_string(),
element,
Expand All @@ -327,6 +355,7 @@ fn parse_atom_site(
occupancy,
b_factor,
segi: String::new(),
labels,
});
model.coords.push(Vec3::new(x, y, z));
}
Expand All @@ -353,6 +382,21 @@ fn parse_assembly_category(category: &BcifCategory, assemblies: &mut AssemblyRow
Ok(())
}

fn parse_entity_category(
category: &BcifCategory,
entity_category: EntityCategory,
entities: &mut EntityRows,
) -> IoResult<()> {
let cols =
CategoryColumns::decode_selected(category, &["id", "type", "entity_id", "num", "mon_id"])?;
for i in 0..category.row_count as usize {
entities.push(entity_category, |name| {
cols.code_at(name, i).map(Cow::into_owned)
});
}
Ok(())
}

fn parse_cell(category: &BcifCategory, cell: &mut CellInfo) -> IoResult<()> {
let cols = CategoryColumns::decode_selected(
category,
Expand Down Expand Up @@ -493,7 +537,9 @@ fn parse_entry(category: &BcifCategory, title: &mut String) -> IoResult<()> {
#[cfg(test)]
mod tests {
use super::*;
use crate::bcif::test_support::{atom_row, atom_site_block, bcif_file, encode_bcif_file};
use crate::bcif::test_support::{
atom_row, atom_site_block, bcif_file, encode_bcif_file, int_column, string_column,
};
use std::collections::HashMap;

/// Verify that CategoryColumns::str_at filters out empty strings,
Expand Down Expand Up @@ -613,4 +659,63 @@ mod tests {
assert_eq!(molecule.state_count(), 1);
assert_eq!(molecule.atoms_slice()[0].residue.chain, "A");
}

#[test]
fn label_fields_are_kept_alongside_auth_fields() {
let rows = [
atom_row(1, "N", "ALA", "B", 0.0, 1),
atom_row(2, "CA", "ALA", "B", 1.5, 1),
];
let mut block = atom_site_block("AF3", &rows);
block.categories[0].columns.extend([
string_column("auth_asym_id", ["H", "H"].into_iter()),
int_column("auth_seq_id", [101, 101].into_iter()),
int_column("label_entity_id", [2, 2].into_iter()),
]);

let molecule =
crate::bcif::read_bcif_bytes_with_bond_tolerance(&encode_bcif_file(&[block]), 0.1)
.unwrap();
let atoms = molecule.atoms_slice();
let residue = &atoms[0].residue;

assert_eq!((residue.chain.as_str(), residue.resv), ("H", 101));
assert_eq!(residue.label_asym_id.as_deref(), Some("B"));
assert_eq!(residue.label_entity_id.as_deref(), Some("2"));
assert_eq!(residue.label_seq_id, Some(1));
}

#[test]
fn entity_tables_are_read_from_typed_columns() {
let mut block = atom_site_block("ENT", &[atom_row(1, "CA", "GLY", "A", 0.0, 1)]);
block.categories.extend([
BcifCategory {
name: "_entity".to_string(),
row_count: 1,
columns: vec![
int_column("id", [1].into_iter()),
string_column("type", ["polymer"].into_iter()),
],
},
BcifCategory {
name: "_entity_poly_seq".to_string(),
row_count: 2,
columns: vec![
string_column("entity_id", ["1", "1"].into_iter()),
int_column("num", [1, 2].into_iter()),
string_column("mon_id", ["MET", "GLY"].into_iter()),
],
},
]);

let molecule =
crate::bcif::read_bcif_bytes_with_bond_tolerance(&encode_bcif_file(&[block]), 0.1)
.unwrap();

assert_eq!(molecule.entities.len(), 1);
let entity = &molecule.entities[0];
assert_eq!(entity.id, "1");
assert_eq!(entity.kind, Some(patinae_mol::EntityKind::Polymer));
assert_eq!(entity.polymer.as_ref().unwrap().sequence, ["MET", "GLY"]);
}
}
2 changes: 1 addition & 1 deletion crates/patinae-io/src/bcif/test_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ pub(crate) fn encode_bcif_file(blocks: &[BcifDataBlock]) -> Vec<u8> {
rmp_serde::to_vec_named(&file).expect("test bCIF fixture should serialize")
}

fn int_column(name: &str, values: impl Iterator<Item = i32>) -> BcifColumn {
pub(crate) fn int_column(name: &str, values: impl Iterator<Item = i32>) -> BcifColumn {
BcifColumn {
name: name.to_string(),
data: BcifData {
Expand Down
Loading
Loading