mod valence;
mod write;
use std::collections::HashMap;
use std::fmt;
use thiserror::Error;
use crate::perception::KekulizeError;
use crate::prelude::*;
#[derive(Debug, Clone)]
pub struct Smiles {
string: String,
atom_order: Vec<usize>,
}
impl Smiles {
pub fn as_str(&self) -> &str {
&self.string
}
pub fn atom_order(&self) -> &[usize] {
&self.atom_order
}
pub fn into_string(self) -> String {
self.string
}
}
impl fmt::Display for Smiles {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.string)
}
}
impl AsRef<str> for Smiles {
fn as_ref(&self) -> &str {
&self.string
}
}
#[derive(Debug, Error)]
pub enum SmilesError {
#[error("nothing to write: no atoms in scope")]
Empty,
#[error(
"bond {0}-{1} has no known order; SMILES needs explicit bond orders (use an SDF/mol \
input, or install connectivity that has them via System::set_bonds)"
)]
UnspecifiedBondOrder(usize, usize),
#[error(
"selection is not bond-complete: atom {global} is bonded to non-selected atom \
{neighbor}, and SMILES cannot express a dangling bond"
)]
OpenSelection { global: usize, neighbor: usize },
#[error("atom {global} has no recognised element (atomic number {z})")]
UnknownElement { global: usize, z: u8 },
#[error("more than 99 ring-closure bonds are open at once, which SMILES cannot express")]
TooManyRingClosures,
#[error(transparent)]
Kekulize(KekulizeError),
}
struct MolView<'a> {
z: &'a [u8],
fc: &'a [i32],
orders: &'a [BondOrder],
adj: &'a BondAdjacency,
h_count: &'a [u8],
suppressed: &'a [bool],
global: &'a [usize],
rank: &'a [u32],
}
pub trait ToSmiles {
fn to_smiles(&self) -> Result<Smiles, SmilesError>;
}
impl<T: AtomProvider + BondProvider> ToSmiles for T {
fn to_smiles(&self) -> Result<Smiles, SmilesError> {
let global: Vec<usize> = self.iter_index().collect();
if global.is_empty() {
return Err(SmilesError::Empty);
}
let n = global.len();
let g2l: HashMap<usize, usize> =
global.iter().enumerate().map(|(l, &g)| (g, l)).collect();
let z: Vec<u8> = self.iter_atoms().map(|a| a.get_atomic_number()).collect();
let fc: Vec<i32> =
self.iter_atoms().map(|a| a.get_formal_charge().unwrap_or(0)).collect();
let mut pairs: Vec<[usize; 2]> = Vec::new();
let mut orders: Vec<BondOrder> = Vec::new();
for b in self.iter_bonds() {
let ([g1, g2], order) = (b.pair(), b.order());
match (g2l.get(&g1).copied(), g2l.get(&g2).copied()) {
(Some(i), Some(j)) => {
if order == BondOrder::Unspecified {
return Err(SmilesError::UnspecifiedBondOrder(g1, g2));
}
pairs.push([i, j]);
orders.push(order);
}
(Some(_), None) => {
return Err(SmilesError::OpenSelection { global: g1, neighbor: g2 })
}
(None, Some(_)) => {
return Err(SmilesError::OpenSelection { global: g2, neighbor: g1 })
}
(None, None) => {} }
}
let adj = BondAdjacency::build(n, pairs.iter().copied());
let orders = crate::perception::kekulize(&z, &fc, &orders, &adj)
.map_err(|e| SmilesError::Kekulize(e.remap_atoms(&global)))?;
debug_assert!(!orders.contains(&BondOrder::Aromatic));
let mut suppressed = vec![false; n];
let mut h_count = vec![0u8; n];
for i in 0..n {
if z[i] != 1 || fc[i] != 0 {
continue;
}
let nb = adj.neighbors(i);
if nb.len() != 1 {
continue;
}
let (partner, bond) = (nb[0].atom(), nb[0].bond());
if z[partner] == 1 || orders[bond] != BondOrder::Single {
continue;
}
suppressed[i] = true;
h_count[partner] += 1;
}
let rank: Vec<u32> = (0..n as u32).collect();
let (string, atom_order) = write::write(&MolView {
z: &z,
fc: &fc,
orders: &orders,
adj: &adj,
h_count: &h_count,
suppressed: &suppressed,
global: &global,
rank: &rank,
})?;
Ok(Smiles { string, atom_order })
}
}
#[cfg(test)]
mod tests {
use super::*;
use BondOrder::{Double as D, Single as S, Triple as T};
fn mol(z: &[u8], bonds: &[(usize, usize, BondOrder)]) -> Topology {
let mut t = Topology::default();
for &n in z {
t.atoms.push(&Atom::new().with_atomic_number(n).with_resid(1));
}
for &(i, j, o) in bonds {
t.bonds.push(&Bond::with_order(i, j, o));
}
t
}
fn add_h(z: &mut Vec<u8>, bonds: &mut Vec<(usize, usize, BondOrder)>, to: usize, count: usize) {
for _ in 0..count {
z.push(1);
bonds.push((to, z.len() - 1, S));
}
}
fn smiles_of(t: &Topology) -> String {
t.to_smiles().expect("should write").into_string()
}
#[test]
fn simple_acyclic_molecules() {
let mut z = vec![6];
let mut b = vec![];
add_h(&mut z, &mut b, 0, 4);
assert_eq!(smiles_of(&mol(&z, &b)), "C");
let mut z = vec![6, 6];
let mut b = vec![(0, 1, D)];
add_h(&mut z, &mut b, 0, 2);
add_h(&mut z, &mut b, 1, 2);
assert_eq!(smiles_of(&mol(&z, &b)), "C=C");
let mut z = vec![6, 6];
let mut b = vec![(0, 1, T)];
add_h(&mut z, &mut b, 0, 1);
add_h(&mut z, &mut b, 1, 1);
assert_eq!(smiles_of(&mol(&z, &b)), "C#C");
let mut z = vec![6, 6, 8];
let mut b = vec![(0, 1, S), (1, 2, S)];
add_h(&mut z, &mut b, 0, 3);
add_h(&mut z, &mut b, 1, 2);
add_h(&mut z, &mut b, 2, 1);
assert_eq!(smiles_of(&mol(&z, &b)), "CCO");
}
#[test]
fn branches_are_parenthesised() {
let mut z = vec![6, 6, 8, 8];
let mut b = vec![(0, 1, S), (1, 2, D), (1, 3, S)];
add_h(&mut z, &mut b, 0, 3);
add_h(&mut z, &mut b, 3, 1);
assert_eq!(smiles_of(&mol(&z, &b)), "CC(=O)O", "acetic acid");
}
#[test]
fn charged_atoms_use_the_bracket_form() {
let mut z = vec![6, 6, 8, 8];
let mut b = vec![(0, 1, S), (1, 2, D), (1, 3, S)];
add_h(&mut z, &mut b, 0, 3);
let mut t = mol(&z, &b);
t.atoms.get_mut(3).unwrap().set_formal_charge(-1);
assert_eq!(smiles_of(&t), "CC(=O)[O-]");
let mut z = vec![7];
let mut b = vec![];
add_h(&mut z, &mut b, 0, 4);
let mut t = mol(&z, &b);
t.atoms.get_mut(0).unwrap().set_formal_charge(1);
assert_eq!(smiles_of(&t), "[NH4+]");
}
#[test]
fn a_mismatched_hydrogen_count_forces_brackets() {
let mut z = vec![6];
let mut b = vec![];
add_h(&mut z, &mut b, 0, 3);
assert_eq!(smiles_of(&mol(&z, &b)), "[CH3]");
}
#[test]
fn unfoldable_hydrogens_are_written_as_bracket_atoms() {
assert_eq!(smiles_of(&mol(&[1, 1], &[(0, 1, S)])), "[H][H]", "H2 has no heavy atom");
assert_eq!(smiles_of(&mol(&[1], &[])), "[H]", "a lone hydrogen");
}
#[test]
fn non_subset_elements_bracket() {
let mut t = mol(&[11], &[]); t.atoms.get_mut(0).unwrap().set_formal_charge(1);
assert_eq!(smiles_of(&t), "[Na+]");
let mut z = vec![14]; let mut b = vec![];
add_h(&mut z, &mut b, 0, 4);
assert_eq!(smiles_of(&mol(&z, &b)), "[SiH4]");
}
fn benzene_kekule() -> Topology {
let mut z = vec![6; 6];
let mut b = vec![(0, 1, D), (1, 2, S), (2, 3, D), (3, 4, S), (4, 5, D), (5, 0, S)];
for i in 0..6 {
add_h(&mut z, &mut b, i, 1);
}
mol(&z, &b)
}
#[test]
fn ring_closure_numbering() {
assert_eq!(smiles_of(&benzene_kekule()), "C1=CC=CC=C1");
let mut z = vec![6; 6];
let mut b = vec![(0, 1, S), (1, 2, S), (2, 3, S), (3, 4, S), (4, 5, S), (5, 0, S)];
for i in 0..6 {
add_h(&mut z, &mut b, i, 2);
}
assert_eq!(smiles_of(&mol(&z, &b)), "C1CCCCC1");
}
#[test]
fn aromatic_input_is_kekulized_on_the_way_out() {
let mut t = benzene_kekule();
crate::perception::perceive(&mut t); let s = smiles_of(&t);
assert_eq!(s.matches('=').count(), 3, "benzene needs three double bonds: {s}");
assert!(!s.contains('c'), "v1 never writes lowercase aromatic atoms: {s}");
assert_eq!(s.len(), "C1=CC=CC=C1".len(), "{s}");
}
#[test]
fn ring_closure_numbers_past_nine_use_the_percent_form() {
let n = 15;
let mut b: Vec<(usize, usize, BondOrder)> = (0..n - 1).map(|i| (i, i + 1, S)).collect();
for far in 5..n {
b.push((0, far, S));
}
let s = smiles_of(&mol(&vec![6; n], &b));
assert!(s.contains("%10"), "a tenth simultaneous ring closure needs %10: {s}");
for k in 1..=9u32 {
let c = s.matches(&k.to_string()).count();
assert!(c % 2 == 0, "ring number {k} appears {c} times in {s}");
}
}
#[test]
fn disconnected_scopes_are_dot_separated() {
let mut z = vec![6];
let mut b = vec![];
add_h(&mut z, &mut b, 0, 4);
z.push(8);
let o = z.len() - 1;
add_h(&mut z, &mut b, o, 2);
assert_eq!(smiles_of(&mol(&z, &b)), "C.O");
}
#[test]
fn atom_order_maps_written_atoms_back_to_global_indices() {
let mut z = vec![6, 6, 8];
let mut b = vec![(0, 1, S), (1, 2, S)];
add_h(&mut z, &mut b, 0, 3);
add_h(&mut z, &mut b, 1, 2);
add_h(&mut z, &mut b, 2, 1);
let s = mol(&z, &b).to_smiles().unwrap();
assert_eq!(s.as_str(), "CCO");
assert_eq!(s.atom_order(), &[0, 1, 2], "folded hydrogens are absent");
}
#[test]
fn atom_order_is_global_for_a_selection() {
let mut z = vec![8];
let mut b = vec![];
add_h(&mut z, &mut b, 0, 2); let c0 = z.len();
z.push(6);
add_h(&mut z, &mut b, c0, 4); let mut t = mol(&z, &b);
t.assign_resindex();
let n = t.atoms.len();
let sys =
System::new(t, State { coords: vec![Pos::default(); n], ..Default::default() }).unwrap();
let sel = sys.select_bound(c0..n).unwrap();
let s = sel.to_smiles().unwrap();
assert_eq!(s.as_str(), "C");
assert_eq!(s.atom_order(), &[c0], "the methane carbon's global index");
}
#[test]
fn unspecified_bond_orders_are_rejected_not_guessed() {
let t = mol(&[6, 6], &[(0, 1, BondOrder::Unspecified)]);
assert!(matches!(
t.to_smiles(),
Err(SmilesError::UnspecifiedBondOrder(0, 1))
));
}
#[test]
fn a_selection_cutting_a_bond_is_rejected() {
let mut z = vec![6, 6, 8];
let mut b = vec![(0, 1, S), (1, 2, S)];
add_h(&mut z, &mut b, 0, 3);
add_h(&mut z, &mut b, 1, 2);
add_h(&mut z, &mut b, 2, 1);
let mut t = mol(&z, &b);
t.assign_resindex();
let n = t.atoms.len();
let sys =
System::new(t, State { coords: vec![Pos::default(); n], ..Default::default() }).unwrap();
let sel = sys.select_bound(0..2).unwrap();
assert!(matches!(
sel.to_smiles(),
Err(SmilesError::OpenSelection { global: 1, neighbor: 2 })
));
}
#[test]
fn an_empty_topology_has_nothing_to_write() {
assert!(matches!(Topology::default().to_smiles(), Err(SmilesError::Empty)));
}
#[test]
fn an_unknown_element_is_reported() {
let mut z = vec![0]; let mut b = vec![];
add_h(&mut z, &mut b, 0, 1);
assert!(matches!(
mol(&z, &b).to_smiles(),
Err(SmilesError::UnknownElement { global: 0, z: 0 })
));
}
}