use super::options::{CifWriteError, CifWriteOptions, CifWriteToError};
use super::projection::{
CanonicalAtomRow, CanonicalProjection, CanonicalValue, canonical_projection,
};
use super::value::quoted;
use molframe_core::io::TextOutput;
use molframe_core::structure::Structure;
use molframe_core::topology::EntityKind;
use std::fmt::{self, Display, Formatter, Write as _};
use std::io::Write as IoWrite;
pub fn write_canonical(structure: &Structure) -> Result<String, CifWriteError> {
write_canonical_with_options(structure, &CifWriteOptions::new())
}
pub fn write_canonical_with_options(
structure: &Structure,
options: &CifWriteOptions,
) -> Result<String, CifWriteError> {
let mut out = String::with_capacity(structure.atom_count() as usize * 100);
render_canonical(&mut out, structure, options)?;
Ok(out)
}
pub fn write_canonical_to<W: IoWrite>(
structure: &Structure,
options: &CifWriteOptions,
output: &mut W,
) -> Result<(), CifWriteToError> {
let mut output = TextOutput::new(output);
render_canonical(&mut output, structure, options)?;
output.finish().map_err(CifWriteToError::Output)
}
fn render_canonical(
out: &mut impl fmt::Write,
structure: &Structure,
options: &CifWriteOptions,
) -> Result<(), CifWriteError> {
let distinct = structure.with_distinct_labels();
let structure = match distinct.as_ref() {
Some(distinct) => distinct,
None => structure,
};
let projection = canonical_projection(structure, options)?;
let _ = writeln!(out, "data_{}", projection.block_id());
let _ = out.write_str("#\n");
write_entry_metadata(out, structure);
write_cell(out, structure);
write_entities(out, structure);
super::references::write(out, structure);
super::supplement::write(out, structure, options);
write_atoms(out, projection)?;
super::bonds::write(out, structure)?;
write_anisotropy(out, structure);
Ok(())
}
fn write_entry_metadata(out: &mut impl fmt::Write, structure: &Structure) {
let entry = &structure.data().entry;
if let Some(id) = &entry.id {
let _ = writeln!(out, "_entry.id {}\n#", quoted(id));
}
if let Some(title) = &entry.title {
let _ = writeln!(out, "_struct.title {}\n#", quoted(title));
}
if let Some(method) = &entry.method {
let _ = writeln!(out, "_exptl.method {}\n#", quoted(method));
}
}
fn write_cell(out: &mut impl fmt::Write, structure: &Structure) {
let Some(cell) = structure.data().cell else {
return;
};
let _ = writeln!(
out,
"_cell.length_a {:.4}\n_cell.length_b {:.4}\n_cell.length_c {:.4}\n\
_cell.angle_alpha {:.4}\n_cell.angle_beta {:.4}\n_cell.angle_gamma {:.4}\n#",
cell.lengths[0],
cell.lengths[1],
cell.lengths[2],
cell.angles[0],
cell.angles[1],
cell.angles[2],
);
}
fn write_entities(out: &mut impl fmt::Write, structure: &Structure) {
let entities = &structure.data().topology.entities;
if entities.is_empty() {
return;
}
let _ = out.write_str("loop_\n_entity.id\n_entity.type\n_entity.pdbx_description\n");
for entity in entities.iter() {
let id = symbol_or_dot(structure, entities.id(entity));
let kind = match entities.kind(entity) {
Some(EntityKind::Polymer) => "polymer",
Some(EntityKind::NonPolymer) => "non-polymer",
Some(EntityKind::Water) => "water",
Some(EntityKind::Branched) => "branched",
_ => "?",
};
let description = symbol_or_dot(structure, entities.description(entity));
let _ = writeln!(out, "{id} {kind} {description}");
}
let _ = out.write_str("#\n");
write_entity_polymers(out, structure);
write_entity_sequences(out, structure);
}
fn write_entity_polymers(out: &mut impl fmt::Write, structure: &Structure) {
let declared = super::declared_polymer_types(structure);
if declared.is_empty() {
return;
}
let entities = &structure.data().topology.entities;
let _ = out.write_str("loop_\n_entity_poly.entity_id\n_entity_poly.type\n");
for (entity, kind) in declared {
let id = symbol_or_dot(structure, entities.id(entity));
let _ = writeln!(out, "{id} {}", quoted(kind));
}
let _ = out.write_str("#\n");
}
fn write_entity_sequences(out: &mut impl fmt::Write, structure: &Structure) {
let entities = &structure.data().topology.entities;
if !entities
.iter()
.any(|entity| !entities.canonical_sequence(entity).is_empty())
{
return;
}
let _ = out.write_str(
"loop_\n_entity_poly_seq.entity_id\n_entity_poly_seq.num\n_entity_poly_seq.mon_id\n",
);
for entity in entities.iter() {
let id = symbol_or_dot(structure, entities.id(entity));
for (position, component) in entities.canonical_sequence(entity).iter().enumerate() {
let component = symbol_or_dot(structure, Some(*component));
let _ = writeln!(out, "{id} {} {component}", position + 1);
}
}
let _ = out.write_str("#\n");
}
const ATOM_SITE_HEADER: &str = "loop_\n\
_atom_site.group_PDB\n_atom_site.id\n_atom_site.type_symbol\n\
_atom_site.label_atom_id\n_atom_site.label_alt_id\n_atom_site.label_comp_id\n\
_atom_site.label_asym_id\n_atom_site.label_entity_id\n_atom_site.label_seq_id\n\
_atom_site.pdbx_PDB_ins_code\n_atom_site.Cartn_x\n_atom_site.Cartn_y\n\
_atom_site.Cartn_z\n_atom_site.occupancy\n_atom_site.B_iso_or_equiv\n\
_atom_site.auth_seq_id\n_atom_site.auth_comp_id\n_atom_site.auth_asym_id\n\
_atom_site.auth_atom_id\n";
const MODEL_NUMBER_HEADER: &str = "_atom_site.pdbx_PDB_model_num\n";
const FORMAL_CHARGE_HEADER: &str = "_atom_site.pdbx_formal_charge\n";
fn write_atoms(
out: &mut impl fmt::Write,
projection: CanonicalProjection<'_>,
) -> Result<(), CifWriteError> {
let charged = projection.carries_formal_charge();
let _ = out.write_str(ATOM_SITE_HEADER);
if charged {
let _ = out.write_str(FORMAL_CHARGE_HEADER);
}
let _ = out.write_str(MODEL_NUMBER_HEADER);
projection.visit_atom_rows(|row| {
let _ = write_atom(out, row, charged);
})?;
let _ = out.write_str("#\n");
Ok(())
}
fn write_atom(out: &mut impl fmt::Write, row: CanonicalAtomRow<'_>, charged: bool) -> fmt::Result {
let element = match row.element() {
CanonicalValue::Present(item) => ElementValue::Present(item.symbol()),
CanonicalValue::Inapplicable => ElementValue::Inapplicable,
CanonicalValue::Unknown => ElementValue::Unknown,
};
let charge = ChargeCell { row, charged };
writeln!(
out,
"{} {} {element} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {}{charge} {}",
row.group(),
row.atom_site_id(),
quoted(row.label_atom_id()),
text_value(row.label_alt_id()),
quoted(row.label_comp_id()),
quoted(row.label_asym_id()),
text_value(row.label_entity_id()),
integer_value(row.label_seq_id()),
text_value(row.insertion_code()),
float_value(row.coordinate(0), 3),
float_value(row.coordinate(1), 3),
float_value(row.coordinate(2), 3),
float_value(row.occupancy(), 2),
float_value(row.b_factor(), 2),
integer_value(row.auth_seq_id()),
text_value(row.auth_comp_id()),
text_value(row.auth_asym_id()),
text_value(row.auth_atom_id()),
row.model_number(),
)
}
fn write_anisotropy(out: &mut impl fmt::Write, structure: &Structure) {
let anisotropy = &structure.data().anisotropy;
if anisotropy.iter().next().is_none() {
return;
}
let _ = out.write_str(
"loop_\n\
_atom_site_anisotrop.id\n\
_atom_site_anisotrop.type_symbol\n\
_atom_site_anisotrop.U[1][1]\n\
_atom_site_anisotrop.U[2][2]\n\
_atom_site_anisotrop.U[3][3]\n\
_atom_site_anisotrop.U[1][2]\n\
_atom_site_anisotrop.U[1][3]\n\
_atom_site_anisotrop.U[2][3]\n",
);
for record in anisotropy.iter() {
let Some(atom) = structure.atom(record.atom) else {
continue;
};
let Some(id) = atom.atom_site_id().filter(|value| *value != 0) else {
continue;
};
let element = match atom.element() {
Some(element) => element.symbol(),
None => "",
};
let _ = writeln!(
out,
"{} {} {u11} {u22} {u33} {u12} {u13} {u23}",
id,
element,
u11 = u_field(record.u[0]),
u22 = u_field(record.u[1]),
u33 = u_field(record.u[2]),
u12 = u_field(record.u[3]),
u13 = u_field(record.u[4]),
u23 = u_field(record.u[5]),
);
}
let _ = out.write_str("#\n");
}
fn u_field(value: f32) -> String {
format!("{value:.5}")
}
fn text_value(value: CanonicalValue<&str>) -> TextValue<'_> {
TextValue(value)
}
fn integer_value(value: CanonicalValue<i64>) -> IntegerValue {
IntegerValue(value)
}
fn float_value(value: CanonicalValue<f64>, precision: usize) -> FloatValue {
FloatValue { value, precision }
}
fn symbol_or_dot(
structure: &Structure,
symbol: Option<molframe_core::symbol::SymbolId>,
) -> TextValue<'_> {
match symbol.and_then(|id| structure.resolve(id)) {
Some(text) if !text.is_empty() => TextValue(CanonicalValue::Present(text)),
Some(_) | None => TextValue(CanonicalValue::Inapplicable),
}
}
struct TextValue<'a>(CanonicalValue<&'a str>);
impl Display for TextValue<'_> {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
match self.0 {
CanonicalValue::Present(text) => Display::fmt("ed(text), formatter),
CanonicalValue::Inapplicable => formatter.write_str("."),
CanonicalValue::Unknown => formatter.write_str("?"),
}
}
}
struct IntegerValue(CanonicalValue<i64>);
impl Display for IntegerValue {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
match self.0 {
CanonicalValue::Present(number) => Display::fmt(&number, formatter),
CanonicalValue::Inapplicable => formatter.write_str("."),
CanonicalValue::Unknown => formatter.write_str("?"),
}
}
}
struct FloatValue {
value: CanonicalValue<f64>,
precision: usize,
}
impl Display for FloatValue {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
match self.value {
CanonicalValue::Present(number) => write_shortest(formatter, number, self.precision),
CanonicalValue::Inapplicable => formatter.write_str("."),
CanonicalValue::Unknown => formatter.write_str("?"),
}
}
}
struct ChargeCell<'a> {
row: CanonicalAtomRow<'a>,
charged: bool,
}
impl Display for ChargeCell<'_> {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
if !self.charged {
return Ok(());
}
write!(formatter, " {}", integer_value(self.row.formal_charge()))
}
}
fn write_shortest(formatter: &mut Formatter<'_>, number: f64, minimum: usize) -> fmt::Result {
let stored = molframe_core::numeric::f64_to_f32(number);
if !stored.is_finite() {
return write!(formatter, "{number:.minimum$}");
}
let mut decimals = minimum;
while decimals < 9 {
let mut probe = Probe {
bytes: [0; 48],
length: 0,
};
let _ = write!(probe, "{stored:.decimals$}");
if probe
.text()
.parse::<f32>()
.is_ok_and(|back| back.to_bits() == stored.to_bits())
{
break;
}
decimals += 1;
}
write!(formatter, "{stored:.decimals$}")
}
struct Probe {
bytes: [u8; 48],
length: usize,
}
impl Probe {
fn text(&self) -> &str {
match std::str::from_utf8(&self.bytes[..self.length]) {
Ok(text) => text,
Err(_) => "",
}
}
}
impl fmt::Write for Probe {
fn write_str(&mut self, text: &str) -> fmt::Result {
let end = self.length + text.len();
let slot = self.bytes.get_mut(self.length..end).ok_or(fmt::Error)?;
slot.copy_from_slice(text.as_bytes());
self.length = end;
Ok(())
}
}
enum ElementValue<'a> {
Present(&'a str),
Inapplicable,
Unknown,
}
impl Display for ElementValue<'_> {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Present(symbol) => {
for byte in symbol.bytes() {
formatter.write_char(char::from(byte.to_ascii_uppercase()))?;
}
Ok(())
}
Self::Inapplicable => formatter.write_str("."),
Self::Unknown => formatter.write_str("?"),
}
}
}
#[cfg(test)]
#[path = "canonical_tests.rs"]
mod tests;