use std::{
collections::{HashMap, hash_map::Entry},
io,
};
use bio_files::BondType;
use lin_alg::f64::Vec3;
use na_seq::Element;
use crate::molecules::{Atom, Bond, common::MoleculeCommon};
impl MoleculeCommon {
pub fn to_smiles(&self) -> String {
if self.atoms.is_empty() {
return String::new();
}
let n = self.atoms.len();
let h_mask: Vec<bool> = self
.atoms
.iter()
.map(|a| a.element == Element::Hydrogen)
.collect();
let mut detect_visited = h_mask.clone();
let mut detect_in_stack = vec![false; n];
let mut ring_bond_map: HashMap<(usize, usize), u8> = HashMap::new();
let mut next_ring: u8 = 1;
loop {
match pick_start(n, &self.adjacency_list, &self.atoms, &detect_visited) {
None => break,
Some(start) => collect_ring_bonds(
start,
usize::MAX,
&self.adjacency_list,
&mut detect_visited,
&mut detect_in_stack,
&mut ring_bond_map,
&mut next_ring,
),
}
}
let mut ring_closures: Vec<Vec<(usize, u8)>> = vec![Vec::new(); n];
for (&(lo, hi), &rnum) in &ring_bond_map {
ring_closures[lo].push((hi, rnum));
ring_closures[hi].push((lo, rnum));
}
let mut write_visited = h_mask;
let mut out = String::new();
let mut first_component = true;
loop {
match pick_start(n, &self.adjacency_list, &self.atoms, &write_visited) {
None => break,
Some(start) => {
if !first_component {
out.push('.');
}
first_component = false;
write_atom(self, start, &mut write_visited, &ring_closures, &mut out);
}
}
}
out
}
pub fn from_smiles(data: &str) -> io::Result<Self> {
let mut atoms: Vec<Atom> = Vec::new();
let mut bonds: Vec<Bond> = Vec::new();
let mut adjacency_list: Vec<Vec<usize>> = Vec::new();
let mut atom_posits: Vec<Vec3> = Vec::new();
let mut current: Option<usize> = None;
let mut current_aromatic: bool = false;
let mut last_bond: Option<BondType> = None;
let mut branch_stack: Vec<(Option<usize>, bool)> = Vec::new();
let mut ring_map: HashMap<u32, (usize, Option<BondType>, bool)> = HashMap::new();
let mut chars = data.chars().peekable();
let mut next_serial: u32 = 1;
while let Some(&ch) = chars.peek() {
match ch {
'-' => {
last_bond = Some(BondType::Single);
chars.next();
}
'=' => {
last_bond = Some(BondType::Double);
chars.next();
}
'#' => {
last_bond = Some(BondType::Triple);
chars.next();
}
':' => {
last_bond = Some(BondType::Aromatic);
chars.next();
}
'/' | '\\' => {
last_bond = Some(BondType::Single);
chars.next();
}
'(' => {
branch_stack.push((current, current_aromatic));
chars.next();
}
')' => {
let (prev, prev_ar) = branch_stack.pop().ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidData, "unmatched ')' in SMILES")
})?;
current = prev;
current_aromatic = prev_ar;
last_bond = None;
chars.next();
}
'.' => {
current = None;
current_aromatic = false;
last_bond = None;
chars.next();
}
'%' => {
chars.next(); let d1 = consume_digit(&mut chars)?;
let d2 = consume_digit(&mut chars)?;
handle_ring(
d1 * 10 + d2,
current,
current_aromatic,
last_bond.take(), &mut ring_map,
&mut bonds,
&mut adjacency_list,
&atoms,
)?;
}
'0'..='9' => {
let d = ch as u32 - '0' as u32;
chars.next();
handle_ring(
d,
current,
current_aromatic,
last_bond.take(), &mut ring_map,
&mut bonds,
&mut adjacency_list,
&atoms,
)?;
}
'[' => {
let (element, is_aromatic) = parse_bracket_atom(&mut chars)?;
let bt = last_bond
.take()
.unwrap_or_else(|| implicit_bt(current_aromatic, is_aromatic, current));
let idx = push_atom(
next_serial,
element,
current,
Some(bt),
&mut atoms,
&mut bonds,
&mut adjacency_list,
&mut atom_posits,
);
next_serial += 1;
current = Some(idx);
current_aromatic = is_aromatic;
}
_ => match parse_organic_atom(&mut chars)? {
Some((element, is_aromatic)) => {
let bt = last_bond
.take()
.unwrap_or_else(|| implicit_bt(current_aromatic, is_aromatic, current));
let idx = push_atom(
next_serial,
element,
current,
Some(bt),
&mut atoms,
&mut bonds,
&mut adjacency_list,
&mut atom_posits,
);
next_serial += 1;
current = Some(idx);
current_aromatic = is_aromatic;
}
None => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("unrecognized SMILES character: '{ch}'"),
));
}
},
}
}
if !ring_map.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"unclosed ring closure index in SMILES",
));
}
let mut mol = MoleculeCommon {
ident: data.trim().to_string(),
atoms,
bonds,
adjacency_list,
atom_posits,
filename: String::from("From SMILES"),
next_atom_sn: next_serial,
..Default::default()
};
mol.assign_posits();
Ok(mol)
}
fn find_bond(&self, a: usize, b: usize) -> Option<&Bond> {
self.bonds
.iter()
.find(|bb| (bb.atom_0 == a && bb.atom_1 == b) || (bb.atom_0 == b && bb.atom_1 == a))
}
}
fn pick_start(n: usize, adj: &[Vec<usize>], atoms: &[Atom], visited: &[bool]) -> Option<usize> {
let mut best: Option<usize> = None;
let mut best_score: i32 = -1;
for i in 0..n {
if visited[i] {
continue;
}
let heavy_deg = adj[i].iter().filter(|&&v| !visited[v]).count();
let score: i32 = match (heavy_deg, atoms[i].element == Element::Carbon) {
(1, true) => 3, (1, false) => 2, (0, _) => 1, _ => 0, };
if score > best_score || (score == best_score && i > best.unwrap_or(0)) {
best_score = score;
best = Some(i);
}
}
best
}
fn collect_ring_bonds(
u: usize,
parent: usize,
adj: &[Vec<usize>],
visited: &mut Vec<bool>,
in_stack: &mut Vec<bool>,
ring_bond_map: &mut HashMap<(usize, usize), u8>,
next_ring: &mut u8,
) {
visited[u] = true;
in_stack[u] = true;
for &v in &adj[u] {
if v == parent {
continue;
}
if in_stack[v] {
let key = (u.min(v), u.max(v));
if let Entry::Vacant(e) = ring_bond_map.entry(key) {
e.insert(*next_ring);
*next_ring += 1;
}
} else if !visited[v] {
collect_ring_bonds(v, u, adj, visited, in_stack, ring_bond_map, next_ring);
}
}
in_stack[u] = false;
}
fn write_atom(
mol: &MoleculeCommon,
u: usize,
visited: &mut Vec<bool>,
ring_closures: &[Vec<(usize, u8)>],
out: &mut String,
) {
visited[u] = true;
let el = mol.atoms[u].element;
if el == Element::Hydrogen {
return;
}
let u_aromatic = atom_is_aromatic(u, &mol.bonds);
out.push_str(&smiles_symbol_for(el, u_aromatic, u, mol));
for &(other, rnum) in &ring_closures[u] {
if let Some(b) = mol.find_bond(u, other) {
let other_aromatic = atom_is_aromatic(other, &mol.bonds);
push_bond_char_ctx(b.bond_type, u_aromatic, other_aromatic, out);
}
push_ring_num(rnum, out);
}
let children: Vec<usize> = mol.adjacency_list[u]
.iter()
.copied()
.filter(|&v| !visited[v] && ring_closures[u].iter().all(|&(rc, _)| rc != v))
.collect();
let last_i = children.len().wrapping_sub(1);
for (i, v) in children.into_iter().enumerate() {
let bt = mol.find_bond(u, v).map(|b| b.bond_type);
let v_aromatic = atom_is_aromatic(v, &mol.bonds);
if i == last_i {
if let Some(t) = bt {
push_bond_char_ctx(t, u_aromatic, v_aromatic, out);
}
write_atom(mol, v, visited, ring_closures, out);
} else {
out.push('(');
if let Some(t) = bt {
push_bond_char_ctx(t, u_aromatic, v_aromatic, out);
}
write_atom(mol, v, visited, ring_closures, out);
out.push(')');
}
}
}
fn push_bond_char_ctx(bt: BondType, u_aromatic: bool, v_aromatic: bool, out: &mut String) {
match bt {
BondType::Aromatic if u_aromatic && v_aromatic => {
}
BondType::Double => out.push('='),
BondType::Triple => out.push('#'),
BondType::Aromatic => out.push(':'), _ => {} }
}
fn atom_is_aromatic(u: usize, bonds: &[Bond]) -> bool {
bonds
.iter()
.any(|b| (b.atom_0 == u || b.atom_1 == u) && b.bond_type == BondType::Aromatic)
}
fn smiles_symbol_for(el: Element, aromatic: bool, u: usize, mol: &MoleculeCommon) -> String {
if !aromatic {
return smiles_symbol(el);
}
let h = mol.adjacency_list[u]
.iter()
.filter(|&&v| mol.atoms[v].element == Element::Hydrogen)
.count();
match el {
Element::Carbon => "c".into(),
Element::Nitrogen => match h {
0 => "n".into(),
1 => "[nH]".into(),
n => format!("[nH{n}]"),
},
Element::Oxygen => match h {
0 => "o".into(),
_ => "[oH]".into(),
},
Element::Sulfur => match h {
0 => "s".into(),
_ => "[sH]".into(),
},
Element::Phosphorus => "p".into(),
Element::Boron => "b".into(),
other => format!("[{}]", other.to_letter()), }
}
fn push_ring_num(rnum: u8, out: &mut String) {
if rnum < 10 {
out.push((b'0' + rnum) as char);
} else {
out.push('%');
out.push((b'0' + rnum / 10) as char);
out.push((b'0' + rnum % 10) as char);
}
}
fn smiles_symbol(el: Element) -> String {
match el {
Element::Boron => "B".into(),
Element::Carbon => "C".into(),
Element::Nitrogen => "N".into(),
Element::Oxygen => "O".into(),
Element::Phosphorus => "P".into(),
Element::Sulfur => "S".into(),
Element::Fluorine => "F".into(),
Element::Chlorine => "Cl".into(),
Element::Bromine => "Br".into(),
Element::Iodine => "I".into(),
Element::Hydrogen => "[H]".into(),
other => format!("[{}]", other.to_letter()),
}
}
#[inline]
fn implicit_bt(prev_aromatic: bool, new_aromatic: bool, prev: Option<usize>) -> BondType {
if prev.is_some() && prev_aromatic && new_aromatic {
BondType::Aromatic
} else {
BondType::Single
}
}
fn push_atom(
serial: u32,
element: Element,
prev: Option<usize>,
bond_type: Option<BondType>,
atoms: &mut Vec<Atom>,
bonds: &mut Vec<Bond>,
adj: &mut Vec<Vec<usize>>,
atom_posits: &mut Vec<Vec3>,
) -> usize {
let idx = atoms.len();
atoms.push(Atom {
serial_number: serial,
posit: Vec3::new(0.0, 0.0, 0.0),
element,
..Default::default()
});
adj.push(Vec::new());
atom_posits.push(Vec3::new(0.0, 0.0, 0.0));
if let Some(p) = prev {
let bt = bond_type.unwrap_or(BondType::Single);
add_bond(p, idx, bt, bonds, adj, atoms);
}
idx
}
fn handle_ring(
ring_idx: u32,
current: Option<usize>,
current_aromatic: bool,
explicit_bt: Option<BondType>,
ring_map: &mut HashMap<u32, (usize, Option<BondType>, bool)>,
bonds: &mut Vec<Bond>,
adj: &mut [Vec<usize>],
atoms: &[Atom],
) -> io::Result<()> {
let cur = current.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
"ring closure digit without a current atom",
)
})?;
match ring_map.remove(&ring_idx) {
Some((other, bt_open, open_aromatic)) => {
let bond_type = explicit_bt.or(bt_open).unwrap_or({
if open_aromatic && current_aromatic {
BondType::Aromatic
} else {
BondType::Single
}
});
add_bond(cur, other, bond_type, bonds, adj, atoms);
}
None => {
ring_map.insert(ring_idx, (cur, explicit_bt, current_aromatic));
}
}
Ok(())
}
fn parse_bracket_atom(
chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
) -> io::Result<(Element, bool)> {
chars.next();
while chars.peek().is_some_and(|c| c.is_ascii_digit()) {
chars.next();
}
let first = chars.next().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
"unexpected end of input inside bracket atom",
)
})?;
let aromatic = first.is_ascii_lowercase();
let mut sym = String::from(first.to_ascii_uppercase());
if chars.peek().is_some_and(|c| c.is_ascii_lowercase()) {
sym.push(chars.next().unwrap());
}
while chars.peek().copied() == Some('@') {
chars.next();
}
if chars.peek().copied() == Some('H') {
chars.next();
while chars.peek().is_some_and(|c| c.is_ascii_digit()) {
chars.next();
}
}
if chars.peek().is_some_and(|&c| c == '+' || c == '-') {
chars.next();
while chars
.peek()
.is_some_and(|&c| c.is_ascii_digit() || c == '+' || c == '-')
{
chars.next();
}
}
if chars.peek().copied() == Some(':') {
chars.next();
while chars.peek().is_some_and(|c| c.is_ascii_digit()) {
chars.next();
}
}
match chars.next() {
Some(']') => {}
other => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("expected ']' to close bracket atom, found {:?}", other),
));
}
}
let element = Element::from_letter(&sym)?;
Ok((element, aromatic))
}
fn parse_organic_atom(
chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
) -> io::Result<Option<(Element, bool)>> {
let ch = match chars.peek().copied() {
Some(c) => c,
None => return Ok(None),
};
match ch {
'C' => {
chars.next();
if chars.peek().copied() == Some('l') {
chars.next();
Ok(Some((Element::Chlorine, false)))
} else {
Ok(Some((Element::Carbon, false)))
}
}
'B' => {
chars.next();
if chars.peek().copied() == Some('r') {
chars.next();
Ok(Some((Element::Bromine, false)))
} else {
Ok(Some((Element::Boron, false)))
}
}
'N' => {
chars.next();
Ok(Some((Element::Nitrogen, false)))
}
'O' => {
chars.next();
Ok(Some((Element::Oxygen, false)))
}
'S' => {
chars.next();
Ok(Some((Element::Sulfur, false)))
}
'P' => {
chars.next();
Ok(Some((Element::Phosphorus, false)))
}
'F' => {
chars.next();
Ok(Some((Element::Fluorine, false)))
}
'I' => {
chars.next();
Ok(Some((Element::Iodine, false)))
}
'H' => {
chars.next();
Ok(Some((Element::Hydrogen, false)))
}
'c' => {
chars.next();
Ok(Some((Element::Carbon, true)))
}
'n' => {
chars.next();
Ok(Some((Element::Nitrogen, true)))
}
'o' => {
chars.next();
Ok(Some((Element::Oxygen, true)))
}
's' => {
chars.next();
Ok(Some((Element::Sulfur, true)))
}
'p' => {
chars.next();
Ok(Some((Element::Phosphorus, true)))
}
_ => Ok(None),
}
}
fn consume_digit(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) -> io::Result<u32> {
match chars.next() {
Some(c) if c.is_ascii_digit() => Ok(c as u32 - '0' as u32),
Some(c) => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("expected digit after '%', found '{c}'"),
)),
None => Err(io::Error::new(
io::ErrorKind::InvalidData,
"expected digit after '%', found end of input",
)),
}
}
fn add_bond(
a: usize,
b: usize,
bond_type: BondType,
bonds: &mut Vec<Bond>,
adj: &mut [Vec<usize>],
atoms: &[Atom],
) {
let (lo, hi) = if a < b { (a, b) } else { (b, a) };
bonds.push(Bond {
bond_type,
atom_0_sn: atoms[lo].serial_number,
atom_1_sn: atoms[hi].serial_number,
atom_0: lo,
atom_1: hi,
is_backbone: false,
});
adj[a].push(b);
adj[b].push(a);
}
pub fn is_smiles(s: &str) -> bool {
if s.len() < 5 {
return false;
}
if s.contains(' ') {
return false;
}
if s.starts_with(|c: char| c.is_ascii_digit()) {
return false;
}
if s.bytes()
.any(|b| matches!(b, b'=' | b'#' | b'(' | b')' | b'[' | b']' | b'.'))
{
return true;
}
s.bytes().all(|b| {
matches!(
b,
b'C' | b'N' | b'O' | b'S' | b'P' | b'F' | b'B' | b'I' |
b'c' | b'n' | b'o' | b's' | b'p' | b'f' | b'b' | b'i' |
b'l' | b'r' | b'0'..=b'9' | b'-' | b'/' | b'\\' | b':' | b'@'
)
})
}