molframe-cif 0.6.4

PDBx/mmCIF reading and writing for molframe
Documentation
//! Canonical `struct_conn` output with complete endpoint preflight.

use super::options::{CifWriteError, CifWriteOptions};
use super::value::{Quoted, quoted};
use molframe_core::bond::{BondOrder, BondProvenance, BondRecord};
use molframe_core::structure::{AtomRef, ResidueRef, Structure};
use std::fmt::{self, Display, Formatter};

const HEADER: &str = "loop_\n\
_struct_conn.id\n_struct_conn.conn_type_id\n\
_struct_conn.ptnr1_label_asym_id\n_struct_conn.ptnr1_label_seq_id\n\
_struct_conn.ptnr1_label_comp_id\n_struct_conn.ptnr1_label_atom_id\n\
_struct_conn.pdbx_ptnr1_label_alt_id\n_struct_conn.ptnr1_auth_seq_id\n\
_struct_conn.ptnr2_label_asym_id\n_struct_conn.ptnr2_label_seq_id\n\
_struct_conn.ptnr2_label_comp_id\n_struct_conn.ptnr2_label_atom_id\n\
_struct_conn.pdbx_ptnr2_label_alt_id\n_struct_conn.ptnr2_auth_seq_id\n\
_struct_conn.pdbx_value_order\n_struct_conn.details\n";

fn connection_bonds(structure: &Structure) -> impl Iterator<Item = BondRecord> + '_ {
    structure
        .data()
        .bonds
        .iter()
        .filter(|bond| bond.provenance != BondProvenance::InferredDistance)
}

pub(super) fn preflight(
    structure: &Structure,
    options: &CifWriteOptions,
) -> Result<(), CifWriteError> {
    let mut bonds = connection_bonds(structure).peekable();
    if bonds.peek().is_none() {
        return Ok(());
    }
    if !options.generates_connection_ids() {
        return Err(CifWriteError::ConnectionIdsNotEnabled);
    }
    for (position, bond) in bonds.enumerate() {
        let atom_a = atom(structure, position, bond.atom_a.get())?;
        let atom_b = atom(structure, position, bond.atom_b.get())?;
        validate_endpoint(structure, atom_a, position, 1)?;
        validate_endpoint(structure, atom_b, position, 2)?;
    }
    Ok(())
}

pub(super) fn write(out: &mut impl fmt::Write, structure: &Structure) -> Result<(), CifWriteError> {
    let mut bonds = connection_bonds(structure).peekable();
    if bonds.peek().is_none() {
        return Ok(());
    }
    let _ = out.write_str(HEADER);
    for (position, bond) in bonds.enumerate() {
        let atom_a = atom(structure, position, bond.atom_a.get())?;
        let atom_b = atom(structure, position, bond.atom_b.get())?;
        let a = endpoint(structure, atom_a, position, 1)?;
        let b = endpoint(structure, atom_b, position, 2)?;
        // The bond's own classification, as deposited. A bond whose source
        // recorded none is written as unknown: asserting a class the input never
        // stated would turn a metal coordination or a disulfide into a covalent
        // bond the moment the file is read back.
        let kind = ConnectionKind {
            structure,
            kind: bond.kind,
        };
        // The trailing `?` is the `details` item the header declares; this writer
        // does not record a provenance note there.
        let _ = writeln!(
            out,
            "{} {kind} {a} {b} {} ?",
            position + 1,
            order(bond.order),
        );
    }
    let _ = out.write_str("#\n");
    Ok(())
}

/// The `conn_type_id` of one bond, or the dictionary's unknown marker.
struct ConnectionKind<'a> {
    structure: &'a Structure,
    kind: molframe_core::OptionalSymbol,
}

impl Display for ConnectionKind<'_> {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
        match self.kind.get() {
            Some(symbol) => match self.structure.resolve(symbol) {
                Some(text) if !text.is_empty() => Display::fmt(&quoted(text), formatter),
                Some(_) => formatter.write_str("?"),
                None => Err(fmt::Error),
            },
            None => formatter.write_str("?"),
        }
    }
}

fn atom(structure: &Structure, bond: usize, atom: u32) -> Result<AtomRef<'_>, CifWriteError> {
    match structure.atom(molframe_core::index::AtomIndex::new(atom)) {
        Some(value) => Ok(value),
        None => Err(CifWriteError::InvalidBondAtom { bond, atom }),
    }
}

fn validate_endpoint(
    structure: &Structure,
    atom: AtomRef<'_>,
    bond: usize,
    endpoint: u8,
) -> Result<(), CifWriteError> {
    let Some(residue) = atom.residue() else {
        return Err(missing(bond, endpoint, "residue"));
    };
    let chain = chain_of(structure, residue).and_then(molframe_core::structure::ChainRef::label);
    if chain.is_none_or(str::is_empty) {
        return Err(missing(bond, endpoint, "label_asym_id"));
    }
    if atom.component_name().is_none_or(str::is_empty) {
        return Err(missing(bond, endpoint, "label_comp_id"));
    }
    if atom.name().is_none_or(str::is_empty) {
        return Err(missing(bond, endpoint, "label_atom_id"));
    }
    Ok(())
}

const fn missing(bond: usize, endpoint: u8, field: &'static str) -> CifWriteError {
    CifWriteError::MissingBondField {
        bond,
        endpoint,
        field,
    }
}

struct Endpoint<'a> {
    chain: Quoted<'a>,
    seq: Option<i32>,
    /// A partner with no label sequence — any non-polymer — is identified
    /// by its author sequence number, which the reader falls back to.
    auth_seq: Option<i32>,
    component: Quoted<'a>,
    atom: Quoted<'a>,
    alt: Option<Quoted<'a>>,
}

fn endpoint<'a>(
    structure: &'a Structure,
    atom: AtomRef<'a>,
    bond: usize,
    side: u8,
) -> Result<Endpoint<'a>, CifWriteError> {
    let Some(residue) = atom.residue() else {
        return Err(missing(bond, side, "residue"));
    };
    let Some(chain) =
        chain_of(structure, residue).and_then(molframe_core::structure::ChainRef::label)
    else {
        return Err(missing(bond, side, "label_asym_id"));
    };
    let Some(component) = atom.component_name() else {
        return Err(missing(bond, side, "label_comp_id"));
    };
    let Some(name) = atom.name() else {
        return Err(missing(bond, side, "label_atom_id"));
    };
    Ok(Endpoint {
        chain: quoted(chain),
        seq: residue.label_seq_id(),
        auth_seq: residue.auth_seq_id(),
        component: quoted(component),
        atom: quoted(name),
        alt: atom.alt_label().map(quoted),
    })
}

impl Display for Endpoint<'_> {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
        write!(
            formatter,
            "{} {} {} {} {} {}",
            self.chain,
            OptionalInteger(self.seq),
            self.component,
            self.atom,
            OptionalQuoted(self.alt.as_ref()),
            OptionalInteger(self.auth_seq),
        )
    }
}

struct OptionalInteger(Option<i32>);

impl Display for OptionalInteger {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
        match self.0 {
            Some(value) => Display::fmt(&value, formatter),
            None => formatter.write_str("."),
        }
    }
}

struct OptionalQuoted<'borrow, 'text>(Option<&'borrow Quoted<'text>>);

impl Display for OptionalQuoted<'_, '_> {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
        match self.0 {
            Some(value) => Display::fmt(value, formatter),
            None => formatter.write_str("."),
        }
    }
}

fn chain_of<'a>(
    structure: &'a Structure,
    residue: ResidueRef<'_>,
) -> Option<molframe_core::structure::ChainRef<'a>> {
    let index = structure
        .data()
        .topology
        .chains
        .containing(residue.index().get())?;
    structure.chain(index)
}

const fn order(order: BondOrder) -> &'static str {
    match order {
        BondOrder::Single => "SING",
        BondOrder::Double => "DOUB",
        BondOrder::Triple => "TRIP",
        BondOrder::Quadruple => "QUAD",
        BondOrder::Aromatic => "AROM",
        BondOrder::Polymeric | BondOrder::Unknown => "?",
    }
}