use super::bonds_symmetry::{OperatorTable, Unrepresentable, unrepresentable};
use crate::document::{Category, CifValue, DataBlock};
use crate::lower::diagnostics::at_source_row;
use molframe_core::bond::{BondOrder, BondProvenance, BondRecord, BondTableBuilder};
use molframe_core::diagnostic::{Code, Diagnostic, Diagnostics};
use molframe_core::hashing::{IdentityBuildHasher, IdentityHashMap, IdentityHashSet};
use molframe_core::index::AtomIndex;
use molframe_core::optional::OptionalSymbol;
use molframe_core::structure::{AtomRef, ChainRef, ResidueRef, StructureData};
use molframe_core::symbol::SymbolId;
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
struct AtomKey {
asym: SymbolId,
sequence: Option<i32>,
component: SymbolId,
atom: SymbolId,
alternate: Option<SymbolId>,
}
#[derive(Clone, Copy, Default)]
enum Match {
#[default]
Missing,
Unique(AtomIndex),
Ambiguous,
}
impl Match {
fn observe(&mut self, atom: AtomIndex) {
*self = match self {
Self::Missing => Self::Unique(atom),
Self::Unique(_) | Self::Ambiguous => Self::Ambiguous,
};
}
const fn unique(self) -> Option<AtomIndex> {
match self {
Self::Unique(atom) => Some(atom),
Self::Missing | Self::Ambiguous => None,
}
}
}
type AtomMap = IdentityHashMap<AtomKey, Match>;
#[derive(Clone, Copy)]
struct Endpoint {
label: Option<AtomKey>,
auth: Option<AtomKey>,
}
impl Endpoint {
fn resolve(self, label: &AtomMap, auth: &AtomMap) -> Option<AtomIndex> {
self.label
.and_then(|key| label.get(&key).copied().and_then(Match::unique))
.or_else(|| {
self.auth
.and_then(|key| auth.get(&key).copied().and_then(Match::unique))
})
}
}
struct Connection<'a> {
row: usize,
atom_a: Endpoint,
atom_b: Endpoint,
order: BondOrder,
provenance: BondProvenance,
kind: OptionalSymbol,
symmetry: Option<Unrepresentable<'a>>,
}
pub(super) fn read(block: &DataBlock, data: &mut StructureData, findings: &mut Diagnostics) {
let Some(category) = block.category("struct_conn") else {
return;
};
let operators = OperatorTable::of(block);
let connections = connections(category, data, findings, &operators);
let (mut label, mut auth) = requested_maps(&connections);
match_atoms(data, &mut label, &mut auth);
let mut bonds = BondTableBuilder::new();
for connection in connections {
if let Some(found) = connection.symmetry {
findings.push(at_source_row(
Diagnostic::new(Code::W3013)
.in_category("struct_conn")
.with_context("partner", found.partner)
.with_context("symmetry", found.value),
connection.row,
));
continue;
}
let atom_a = connection.atom_a.resolve(&label, &auth);
let atom_b = connection.atom_b.resolve(&label, &auth);
let (Some(atom_a), Some(atom_b)) = (atom_a, atom_b) else {
findings.push(at_source_row(
Diagnostic::new(Code::E3006).in_category("struct_conn"),
connection.row,
));
continue;
};
bonds.push(BondRecord {
atom_a,
atom_b,
order: connection.order,
provenance: connection.provenance,
kind: connection.kind,
});
}
data.bonds = bonds.finish();
}
fn connections<'a>(
category: &'a Category,
data: &mut StructureData,
findings: &mut Diagnostics,
operators: &OperatorTable,
) -> Vec<Connection<'a>> {
let mut output = Vec::with_capacity(category.row_count());
for row in 0..category.row_count() {
let recorded = category.text("conn_type_id", row);
if !is_connectivity(recorded) {
continue;
}
let kind = match recorded {
Some(text) => {
if let Ok(symbol) = data.dictionary.intern(text) {
OptionalSymbol::some(symbol)
} else {
findings.push(
Diagnostic::new(Code::E1901)
.with_message("the dictionary is full")
.in_category("struct_conn"),
);
OptionalSymbol::NONE
}
}
None => OptionalSymbol::NONE,
};
let symmetry = unrepresentable(
category.text("ptnr1_symmetry", row),
category.text("ptnr2_symmetry", row),
operators,
);
let (label_a, auth_a) = row_keys(category, row, 1, data);
let (label_b, auth_b) = row_keys(category, row, 2, data);
output.push(Connection {
row,
atom_a: Endpoint {
label: label_a,
auth: auth_a,
},
atom_b: Endpoint {
label: label_b,
auth: auth_b,
},
order: order(category.text("pdbx_value_order", row)),
provenance: provenance(category.text("details", row)),
kind,
symmetry,
});
}
output
}
fn row_keys(
category: &Category,
row: usize,
partner: u8,
data: &StructureData,
) -> (Option<AtomKey>, Option<AtomKey>) {
(
row_key(category, row, partner, Namespace::Label, data),
row_key(category, row, partner, Namespace::Author, data),
)
}
#[derive(Clone, Copy)]
enum Namespace {
Label,
Author,
}
fn row_key(
category: &Category,
row: usize,
partner: u8,
namespace: Namespace,
data: &StructureData,
) -> Option<AtomKey> {
let items = endpoint_items(partner, namespace)?;
Some(AtomKey {
asym: symbol(category, items.asym, row, data)?,
sequence: sequence(category, items.sequence, items.fallback_sequence, row),
component: symbol(category, items.component, row, data)?,
atom: symbol(category, items.atom, row, data)?,
alternate: category
.identifier(items.alternate, row)
.and_then(|value| data.dictionary.get(&value)),
})
}
fn sequence(category: &Category, primary: &str, fallback: Option<&str>, row: usize) -> Option<i32> {
integer(category, primary, row)
.or_else(|| fallback.and_then(|item| integer(category, item, row)))
}
fn integer(category: &Category, item: &str, row: usize) -> Option<i32> {
category
.value(item, row)
.and_then(CifValue::as_integer)
.and_then(|value| i32::try_from(value).ok())
}
fn symbol(category: &Category, item: &str, row: usize, data: &StructureData) -> Option<SymbolId> {
category
.identifier(item, row)
.and_then(|value| data.dictionary.get(&value))
}
struct EndpointItems {
asym: &'static str,
sequence: &'static str,
fallback_sequence: Option<&'static str>,
component: &'static str,
atom: &'static str,
alternate: &'static str,
}
fn endpoint_items(partner: u8, namespace: Namespace) -> Option<EndpointItems> {
match (partner, namespace) {
(1, Namespace::Label) => Some(EndpointItems {
asym: "ptnr1_label_asym_id",
sequence: "ptnr1_label_seq_id",
fallback_sequence: Some("ptnr1_auth_seq_id"),
component: "ptnr1_label_comp_id",
atom: "ptnr1_label_atom_id",
alternate: "pdbx_ptnr1_label_alt_id",
}),
(2, Namespace::Label) => Some(EndpointItems {
asym: "ptnr2_label_asym_id",
sequence: "ptnr2_label_seq_id",
fallback_sequence: Some("ptnr2_auth_seq_id"),
component: "ptnr2_label_comp_id",
atom: "ptnr2_label_atom_id",
alternate: "pdbx_ptnr2_label_alt_id",
}),
(1, Namespace::Author) => Some(EndpointItems {
asym: "ptnr1_auth_asym_id",
sequence: "ptnr1_auth_seq_id",
fallback_sequence: None,
component: "ptnr1_auth_comp_id",
atom: "ptnr1_auth_atom_id",
alternate: "pdbx_ptnr1_label_alt_id",
}),
(2, Namespace::Author) => Some(EndpointItems {
asym: "ptnr2_auth_asym_id",
sequence: "ptnr2_auth_seq_id",
fallback_sequence: None,
component: "ptnr2_auth_comp_id",
atom: "ptnr2_auth_atom_id",
alternate: "pdbx_ptnr2_label_alt_id",
}),
_ => None,
}
}
fn requested_maps(connections: &[Connection<'_>]) -> (AtomMap, AtomMap) {
let capacity = connections.len().saturating_mul(2);
let mut label = AtomMap::with_capacity_and_hasher(capacity, IdentityBuildHasher::default());
let mut auth = AtomMap::with_capacity_and_hasher(capacity, IdentityBuildHasher::default());
for connection in connections {
request(&mut label, connection.atom_a.label);
request(&mut label, connection.atom_b.label);
request(&mut auth, connection.atom_a.auth);
request(&mut auth, connection.atom_b.auth);
}
(label, auth)
}
fn request(map: &mut AtomMap, key: Option<AtomKey>) {
if let Some(key) = key {
map.entry(key).or_default();
}
}
struct Wanted {
chains: IdentityHashSet<SymbolId>,
residues: IdentityHashSet<(SymbolId, Option<i32>)>,
}
impl Wanted {
fn of(map: &AtomMap) -> Self {
let mut chains = IdentityHashSet::default();
let mut residues = IdentityHashSet::default();
for key in map.keys() {
chains.insert(key.asym);
residues.insert((key.asym, key.sequence));
}
Self { chains, residues }
}
fn residue(&self, chain: Option<SymbolId>, sequence: Option<i32>) -> bool {
chain.is_some_and(|chain| {
self.chains.contains(&chain) && self.residues.contains(&(chain, sequence))
})
}
}
fn match_atoms(data: &StructureData, label: &mut AtomMap, auth: &mut AtomMap) {
let wanted_label = Wanted::of(label);
let wanted_auth = Wanted::of(auth);
for chain in data.chains() {
for residue in chain.residues() {
let by_label = wanted_label.residue(
chain.label_asym_id(),
residue.label_seq_id().or_else(|| residue.auth_seq_id()),
);
let by_author = wanted_auth.residue(chain.auth_asym_id(), residue.auth_seq_id());
if !by_label && !by_author {
continue;
}
for atom in residue.atoms() {
if by_label {
observe(label, label_key(chain, residue, atom), atom.index());
}
if by_author {
observe(auth, auth_key(chain, residue, atom), atom.index());
}
}
}
}
}
fn observe(map: &mut AtomMap, key: Option<AtomKey>, atom: AtomIndex) {
if let Some(key) = key
&& let Some(found) = map.get_mut(&key)
{
found.observe(atom);
}
}
fn label_key(chain: ChainRef<'_>, residue: ResidueRef<'_>, atom: AtomRef<'_>) -> Option<AtomKey> {
Some(AtomKey {
asym: chain.label_asym_id()?,
sequence: residue.label_seq_id().or_else(|| residue.auth_seq_id()),
component: atom.component_id()?,
atom: atom.name_symbol()?,
alternate: atom.alt_id()?.symbol(),
})
}
fn auth_key(chain: ChainRef<'_>, residue: ResidueRef<'_>, atom: AtomRef<'_>) -> Option<AtomKey> {
Some(AtomKey {
asym: chain.auth_asym_id()?,
sequence: residue.auth_seq_id(),
component: match residue.auth_comp_id() {
Some(component) => component,
None => atom.component_id()?,
},
atom: match atom.auth_name_symbol() {
Some(name) => name,
None => atom.name_symbol()?,
},
alternate: atom.alt_id()?.symbol(),
})
}
fn is_connectivity(kind: Option<&str>) -> bool {
let Some(kind) = kind else {
return true;
};
["covale", "disulf", "metalc", "modres"]
.iter()
.any(|prefix| starts_with_ignore_ascii_case(kind, prefix))
}
fn starts_with_ignore_ascii_case(value: &str, prefix: &str) -> bool {
value
.get(..prefix.len())
.is_some_and(|start| start.eq_ignore_ascii_case(prefix))
}
fn order(value: Option<&str>) -> BondOrder {
let Some(value) = value else {
return BondOrder::Unknown;
};
if value.eq_ignore_ascii_case("SING") || value.eq_ignore_ascii_case("SINGLE") {
BondOrder::Single
} else if value.eq_ignore_ascii_case("DOUB") || value.eq_ignore_ascii_case("DOUBLE") {
BondOrder::Double
} else if value.eq_ignore_ascii_case("TRIP") || value.eq_ignore_ascii_case("TRIPLE") {
BondOrder::Triple
} else if value.eq_ignore_ascii_case("QUAD") || value.eq_ignore_ascii_case("QUADRUPLE") {
BondOrder::Quadruple
} else if value.eq_ignore_ascii_case("AROM") || value.eq_ignore_ascii_case("AROMATIC") {
BondOrder::Aromatic
} else {
BondOrder::Unknown
}
}
fn provenance(value: Option<&str>) -> BondProvenance {
match value {
Some("molframe:ccd") => BondProvenance::ChemicalComponentDictionary,
Some("molframe:inferred-distance") => BondProvenance::InferredDistance,
Some("molframe:user") => BondProvenance::User,
_ => BondProvenance::File,
}
}
#[cfg(test)]
#[path = "bonds_tests.rs"]
mod tests;