use super::normalize_symbol;
use crate::atom::{ATOMIC_SYMBOLS, Atom, Bond};
use nalgebra::Matrix4;
use std::{
collections::HashMap,
f32::consts::PI,
io::{self, BufRead, BufReader, Read},
};
#[derive(Default, PartialEq)]
enum CIFDialect {
#[default]
Undefined,
#[allow(clippy::upper_case_acronyms)]
CCDC,
#[allow(non_camel_case_types)]
mmCIF,
#[allow(non_camel_case_types)]
compCIF,
}
#[derive(Default)]
struct CIFAtomHeader {
symbol: usize,
x: usize,
y: usize,
z: usize,
id: usize,
disorder: usize,
residue: usize,
chain: usize,
seq_id: usize,
occupancy: usize,
}
fn parse_atom_line(
line: &str,
header: &CIFAtomHeader,
atom_count: &mut usize,
label_map: &mut HashMap<String, usize>,
dialect: &CIFDialect,
fract_matrix: &Matrix4<f32>,
) -> Option<Atom> {
let vec = line.split_whitespace().collect::<Vec<_>>();
if vec.len() <= header.z {
return None;
}
let symbol = vec[header.symbol];
let x = vec[header.x].split('(').next()?.parse().ok()?;
let y = vec[header.y].split('(').next()?.parse().ok()?;
let z = vec[header.z].split('(').next()?.parse().ok()?;
let id = vec[header.id];
let disorder_group = if header.disorder != 0 && header.disorder < vec.len() {
vec[header.disorder].parse::<usize>().unwrap_or(0)
} else {
0
};
let residue = if header.residue != 0 {
vec[header.residue]
} else {
"UNK"
};
let chain_id = if header.chain != 0 {
vec[header.chain].chars().next().unwrap_or_default()
} else {
char::default()
};
let seq_id = if header.seq_id != 0 {
vec[header.seq_id].parse::<i32>().unwrap_or_default()
} else {
0
};
let occ = if header.occupancy != 0 {
vec[header.occupancy].parse::<f32>().unwrap_or(1.0)
} else {
1.0
};
let atomic_number = ATOMIC_SYMBOLS
.iter()
.position(|&s| s == normalize_symbol(symbol))?
+ 1;
*atom_count += 1;
label_map.insert(id.to_owned(), *atom_count);
let mut atom = Atom::new(*atom_count, atomic_number as u8, x, y, z);
if *dialect == CIFDialect::CCDC {
atom.coord = fract_matrix.transform_vector(&atom.coord.coords).into();
}
atom.disorder_group = disorder_group;
atom.name = id.to_string();
atom.resname = residue.to_string();
atom.chain = chain_id;
atom.resid = seq_id;
atom.occupancy = occ;
Some(atom)
}
fn parse_bond_line(line: &str, map: &HashMap<String, usize>, dialect: &CIFDialect) -> Option<Bond> {
let mut iter = line.split_whitespace();
if *dialect == CIFDialect::compCIF {
iter.next()?;
}
let atom1 = iter.next()?;
let atom2 = iter.next()?;
Some(Bond {
atom1: *map.get(atom1)?,
atom2: *map.get(atom2)?,
order: 1,
is_aromatic: false,
})
}
pub fn parse<P: Read>(reader: BufReader<P>) -> io::Result<(Vec<Atom>, Vec<Bond>)> {
let mut dialect = CIFDialect::default();
let mut atoms = Vec::new();
let mut bonds = Vec::new();
let mut pick_atoms = false;
let mut pick_bonds = false;
let mut headers = CIFAtomHeader::default();
let mut header_idx = 0;
let mut atom_count = 0;
let mut param_idx = 0;
let mut cell_params: [f32; 6] = Default::default();
let mut fract_mtrx: Matrix4<f32> = Default::default();
let mut map: HashMap<String, usize> = HashMap::new();
for line in reader.lines() {
let line = line?;
let line_trimmed = line.trim();
if line_trimmed.is_empty() {
continue;
}
if dialect == CIFDialect::Undefined {
dialect = set_dialect(line_trimmed);
} else if dialect == CIFDialect::CCDC {
if line_trimmed.starts_with("_cell_length_") || line_trimmed.starts_with("_cell_angle_")
{
param_idx = parse_cell_params(line_trimmed, param_idx, &mut cell_params);
if param_idx == 6 {
fract_mtrx = conversion_matrix_arr(cell_params);
}
}
}
if line_trimmed.starts_with("loop_") {
pick_atoms = false;
pick_bonds = false;
}
if line_trimmed.starts_with("_atom_site_label")
|| line_trimmed.starts_with("_atom_site.")
|| line_trimmed.starts_with("_chem_comp_atom.")
{
pick_atoms = true;
pick_bonds = false;
}
if line_trimmed.starts_with("_geom_bond") || line_trimmed.starts_with("_chem_comp_bond") {
pick_atoms = false;
pick_bonds = true;
}
if pick_atoms {
if line_trimmed.starts_with("_") {
set_header_indices(line_trimmed, header_idx, &mut headers);
header_idx += 1;
} else if let Some(atom) = parse_atom_line(
line_trimmed,
&headers,
&mut atom_count,
&mut map,
&dialect,
&fract_mtrx,
) {
atoms.push(atom);
}
} else if pick_bonds {
if let Some(bond) = parse_bond_line(line_trimmed, &map, &dialect) {
bonds.push(bond);
}
}
}
Ok((atoms, bonds))
}
fn set_dialect(line: &str) -> CIFDialect {
if line.starts_with("_chem_comp") {
CIFDialect::compCIF
} else if line.starts_with("_atom_type_symbol") || line.starts_with("_symmetry") {
CIFDialect::CCDC
} else if line.starts_with("_pdbx") {
CIFDialect::mmCIF
} else {
CIFDialect::Undefined
}
}
fn set_header_indices(header: &str, index: usize, headers: &mut CIFAtomHeader) {
match header {
h if h.contains("symbol") => headers.symbol = index,
h if h.contains("fract_x") || h.contains("Cartn_x") => headers.x = index,
h if h.contains("fract_y") || h.contains("Cartn_y") => headers.y = index,
h if h.contains("fract_z") || h.contains("Cartn_z") => headers.z = index,
h if h.contains("label_atom_id")
|| h.contains("atom.atom_id")
|| h.contains("_site_label") =>
{
headers.id = index
}
h if h.contains("disorder_group") => headers.disorder = index,
h if h.contains("auth_comp_id") || h.contains("comp_id") => headers.residue = index,
h if h.contains("auth_asym_id") => headers.chain = index,
h if h.contains("auth_seq_id") => headers.seq_id = index,
h if h.contains("occupancy") => headers.occupancy = index,
_ => {}
}
}
fn parse_cell_params(line: &str, param_idx: usize, cell_params: &mut [f32; 6]) -> usize {
let mut iter = line.split_whitespace();
iter.next();
cell_params[param_idx] =
get_value_from_uncertainity(iter.next().unwrap_or_default()).unwrap_or_default();
param_idx + 1
}
fn get_value_from_uncertainity(input: &str) -> Option<f32> {
input.split('(').next()?.parse().ok()
}
fn conversion_matrix_arr(array: [f32; 6]) -> Matrix4<f32> {
conversion_matrix(array[0], array[1], array[2], array[3], array[4], array[5])
}
fn conversion_matrix(a: f32, b: f32, c: f32, alpha: f32, beta: f32, gamma: f32) -> Matrix4<f32> {
let cos_alpha = (alpha * PI / 180.0).cos();
let cos_beta = (beta * PI / 180.0).cos();
let cos_gamma = (gamma * PI / 180.0).cos();
let sin_gamma = (gamma * PI / 180.0).sin();
Matrix4::new(
a,
b * cos_gamma,
c * cos_beta,
0.0,
0.0,
b * sin_gamma,
c * (cos_alpha - cos_beta * cos_gamma) / sin_gamma,
0.0,
0.0,
0.0,
c * ((1.0 - cos_alpha.powi(2) - cos_beta.powi(2) - cos_gamma.powi(2)
+ 2.0 * cos_alpha * cos_beta * cos_gamma)
.sqrt())
/ sin_gamma,
0.0,
0.0,
0.0,
0.0,
1.0,
)
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
use std::fs::File;
#[rstest]
#[case("data/4n4n.cif", 15450, 0)] #[case("data/4r21.cif", 6752, 0)] #[case("data/147288.cif", 206, 230)]
#[case("data/1484829.cif", 466, 528)]
#[case("data/cif_noTrim.cif", 79, 89)]
#[case("data/cif.cif", 79, 89)]
#[case("data/CuHETMP.cif", 85, 0)] #[case("data/ligand.cif", 44, 46)]
#[case("data/mmcif.cif", 1291, 0)] fn test_cif_files(#[case] filename: &str, #[case] atom_len: usize, #[case] bond_len: usize) {
let file = File::open(filename).unwrap();
let reader = BufReader::new(file);
let (atoms, bonds) = parse(reader).unwrap();
assert_eq!(
atoms.iter().filter(|a| a.disorder_group != 2).count(),
atom_len
);
assert_eq!(
bonds
.iter()
.filter(|b| atoms[b.atom1 - 1].disorder_group != 2
&& atoms[b.atom2 - 1].disorder_group != 2)
.count(),
bond_len
);
}
}