use std::collections::BTreeMap;
use std::fmt::Write as _;
use omgkit_core::{element, AtomFlags, BondData, BondDirection, BondOrder, ChiralTag, MolBuilder};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Written {
pub smiles: String,
pub atom_order: Vec<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WriteStyle {
Faithful,
Canonical,
}
#[must_use]
pub fn write(mol: &MolBuilder) -> Written {
let priority: Vec<u32> = (0..mol.num_atoms() as u32).collect();
write_with_priority(mol, &priority)
}
#[must_use]
pub fn write_with_priority(mol: &MolBuilder, priority: &[u32]) -> Written {
write_with_priority_styled(mol, priority, WriteStyle::Faithful)
}
#[must_use]
pub fn write_with_priority_styled(
mol: &MolBuilder,
priority: &[u32],
style: WriteStyle,
) -> Written {
let n = mol.num_atoms();
assert_eq!(
priority.len(),
n,
"优先级数组长度 {} 与原子数 {n} 不符",
priority.len()
);
if n == 0 {
return Written {
smiles: String::new(),
atom_order: Vec::new(),
};
}
let normalized = if style == WriteStyle::Canonical {
crate::stereo::normalized_stereo_refs(mol, priority)
} else {
None
};
let mol = normalized.as_ref().unwrap_or(mol);
let tree = build_tree(mol, priority);
emit(mol, &tree, style)
}
struct Dfs {
roots: Vec<u32>,
children: Vec<Vec<u32>>,
ring_closures: Vec<Vec<u32>>,
}
fn build_tree(mol: &MolBuilder, priority: &[u32]) -> Dfs {
let n = mol.num_atoms();
let mut nbrs: Vec<Vec<(u32, u32)>> = Vec::with_capacity(n);
for a in 0..n as u32 {
let mut v: Vec<(u32, u32)> = mol
.neighbors(a)
.map(|(other, bond)| (bond, other))
.collect();
v.sort_unstable_by_key(|&(bond, other)| (priority[other as usize], bond));
nbrs.push(v);
}
let mut order: Vec<u32> = (0..n as u32).collect();
order.sort_unstable_by_key(|&a| priority[a as usize]);
let mut visited = vec![false; n];
let mut edge_used = vec![false; mol.num_bonds()];
let mut dfs = Dfs {
roots: Vec::new(),
children: vec![Vec::new(); n],
ring_closures: vec![Vec::new(); n],
};
let mut stack: Vec<(u32, usize)> = Vec::new();
for &root in &order {
if visited[root as usize] {
continue;
}
visited[root as usize] = true;
dfs.roots.push(root);
stack.push((root, 0));
while let Some(&mut (a, ref mut cursor)) = stack.last_mut() {
let Some(&(bond, other)) = nbrs[a as usize].get(*cursor) else {
stack.pop();
continue;
};
*cursor += 1;
if edge_used[bond as usize] {
continue; }
edge_used[bond as usize] = true;
if visited[other as usize] {
dfs.ring_closures[a as usize].push(bond);
dfs.ring_closures[other as usize].push(bond);
} else {
visited[other as usize] = true;
dfs.children[a as usize].push(bond);
stack.push((other, 0));
}
}
}
dfs
}
enum Step {
Atom { atom: u32, via: Option<u32> },
Literal(&'static str),
}
fn emit(mol: &MolBuilder, dfs: &Dfs, style: WriteStyle) -> Written {
let written = crate::stereo::directions_for_writing(mol);
let (dirs, comps) = (written.dirs, written.component);
let mut gauge: BTreeMap<u32, bool> = BTreeMap::new();
let mut out = String::new();
let mut atom_order = Vec::with_capacity(mol.num_atoms());
let mut open_label: Vec<Option<u32>> = vec![None; mol.num_bonds()];
let mut label_in_use: Vec<bool> = Vec::new();
let mut stack: Vec<Step> = Vec::new();
for (i, &root) in dfs.roots.iter().enumerate() {
if i > 0 {
stack.push(Step::Literal("."));
}
stack.push(Step::Atom {
atom: root,
via: None,
});
}
stack.reverse();
while let Some(step) = stack.pop() {
let (atom, via) = match step {
Step::Literal(s) => {
out.push_str(s);
continue;
}
Step::Atom { atom, via } => (atom, via),
};
if let Some(bond) = via {
let parent = other_end(mol, bond, atom);
out.push_str(gauged_symbol(
bond, parent, mol, &dirs, &comps, style, &mut gauge,
));
}
let mut written_bonds: Vec<u32> = Vec::with_capacity(mol.degree(atom));
written_bonds.extend(via);
written_bonds.extend(dfs.ring_closures[atom as usize].iter().copied());
written_bonds.extend(dfs.children[atom as usize].iter().copied());
let tag = output_chiral_tag(
mol,
atom,
&written_bonds,
via.is_none(),
dfs.ring_closures[atom as usize].len(),
);
write_atom(&mut out, mol, atom, tag, style);
atom_order.push(atom);
for &bond in &dfs.ring_closures[atom as usize] {
match open_label[bond as usize] {
Some(label) => {
out.push_str(&ring_label(label));
open_label[bond as usize] = None;
label_in_use[label as usize - 1] = false;
}
None => {
let label = alloc_label(&mut label_in_use);
open_label[bond as usize] = Some(label);
out.push_str(gauged_symbol(
bond, atom, mol, &dirs, &comps, style, &mut gauge,
));
out.push_str(&ring_label(label));
}
}
}
let kids = &dfs.children[atom as usize];
if let Some((&last, rest)) = kids.split_last() {
stack.push(Step::Atom {
atom: other_end(mol, last, atom),
via: Some(last),
});
for &bond in rest.iter().rev() {
stack.push(Step::Literal(")"));
stack.push(Step::Atom {
atom: other_end(mol, bond, atom),
via: Some(bond),
});
stack.push(Step::Literal("("));
}
}
}
Written {
smiles: out,
atom_order,
}
}
fn output_chiral_tag(
mol: &MolBuilder,
atom: u32,
written_bonds: &[u32],
is_fragment_start: bool,
ring_closures: usize,
) -> ChiralTag {
let a = mol.atoms()[atom as usize];
if !a.chiral_tag.is_tetrahedral() {
return ChiralTag::Unspecified;
}
let stored: Vec<u32> = mol.neighbors(atom).map(|(_, bond)| bond).collect();
let Some(mut odd) = super::permutation_is_odd(written_bonds, &stored) else {
debug_assert!(false, "输出的邻居顺序与存储顺序不是同一组键");
return ChiralTag::Unspecified;
};
if stored.len() == 3 {
let hs = total_hs(&a);
let unsaturated = mol
.neighbors(atom)
.any(|(_, bond)| mol.bonds()[bond as usize].order.as_double() > 1.0);
if (is_fragment_start && hs == 1) || (hs != 1 && ring_closures == 1 && !unsaturated) {
odd = !odd;
}
}
if odd {
a.chiral_tag.inverted()
} else {
a.chiral_tag
}
}
fn total_hs(a: &omgkit_core::AtomData) -> u8 {
a.num_explicit_hs.saturating_add(a.num_implicit_hs)
}
fn other_end(mol: &MolBuilder, bond: u32, from: u32) -> u32 {
mol.bonds()[bond as usize]
.other_end(from)
.expect("遍历产生的键必以当前原子为端点")
}
fn alloc_label(in_use: &mut Vec<bool>) -> u32 {
match in_use.iter().position(|&used| !used) {
Some(i) => {
in_use[i] = true;
i as u32 + 1
}
None => {
in_use.push(true);
in_use.len() as u32
}
}
}
fn ring_label(label: u32) -> String {
if label < 10 {
label.to_string()
} else if label < 100 {
format!("%{label}")
} else {
format!("%({label})")
}
}
fn gauged_symbol(
bond: u32,
from: u32,
mol: &MolBuilder,
dirs: &[BondDirection],
comps: &[Option<u32>],
style: WriteStyle,
gauge: &mut BTreeMap<u32, bool>,
) -> &'static str {
let sym = bond_symbol(bond, from, mol, dirs);
if style != WriteStyle::Canonical || (sym != "/" && sym != "\\") {
return sym;
}
let Some(comp) = comps.get(bond as usize).copied().flatten() else {
return sym;
};
let flip = *gauge.entry(comp).or_insert(sym == "\\");
match (flip, sym) {
(true, "/") => "\\",
(true, _) => "/",
(false, s) => s,
}
}
fn bond_symbol(bond: u32, from: u32, mol: &MolBuilder, dirs: &[BondDirection]) -> &'static str {
let b = mol.bonds()[bond as usize];
match b.order {
BondOrder::Double => "=",
BondOrder::Triple => "#",
BondOrder::Quadruple => "$",
BondOrder::Dative => {
if b.begin == from {
"->"
} else {
"<-"
}
}
BondOrder::Aromatic => match direction_from(b, from, dirs[bond as usize]) {
BondDirection::UpRight => "/",
BondDirection::DownRight => "\\",
BondDirection::None => {
if both_aromatic(mol, b.begin, b.end) {
""
} else {
":"
}
}
},
BondOrder::Single | BondOrder::Unspecified => {
match direction_from(b, from, dirs[bond as usize]) {
BondDirection::UpRight => "/",
BondDirection::DownRight => "\\",
BondDirection::None => {
if both_aromatic(mol, b.begin, b.end) {
"-"
} else {
""
}
}
}
}
}
}
fn direction_from(b: BondData, from: u32, stored: BondDirection) -> BondDirection {
if b.begin == from {
stored
} else {
stored.flipped()
}
}
fn both_aromatic(mol: &MolBuilder, a: u32, b: u32) -> bool {
let at = mol.atoms();
at[a as usize].flags.contains(AtomFlags::AROMATIC)
&& at[b as usize].flags.contains(AtomFlags::AROMATIC)
}
fn write_atom(out: &mut String, mol: &MolBuilder, idx: u32, tag: ChiralTag, style: WriteStyle) {
let a = mol.atoms()[idx as usize];
let aromatic = a.flags.contains(AtomFlags::AROMATIC);
if a.atomic_num == 0 && !needs_brackets(mol, idx, style) {
out.push('*');
return;
}
if !needs_brackets(mol, idx, style) {
let sym = element::by_atomic_num(a.atomic_num).map_or("*", |e| e.symbol);
if aromatic {
out.push_str(&sym.to_ascii_lowercase());
} else {
out.push_str(sym);
}
return;
}
out.push('[');
if a.isotope != 0 {
let _ = write!(out, "{}", a.isotope);
}
if a.atomic_num == 0 {
out.push('*');
} else {
let sym = element::by_atomic_num(a.atomic_num).map_or("*", |e| e.symbol);
if aromatic {
out.push_str(&sym.to_ascii_lowercase());
} else {
out.push_str(sym);
}
}
match tag {
ChiralTag::Ccw => out.push('@'),
ChiralTag::Cw => out.push_str("@@"),
_ => {}
}
match total_hs(&a) {
0 => {}
1 => out.push('H'),
k => {
let _ = write!(out, "H{k}");
}
}
match a.formal_charge.cmp(&0) {
std::cmp::Ordering::Greater => {
out.push('+');
if a.formal_charge > 1 {
let _ = write!(out, "{}", a.formal_charge);
}
}
std::cmp::Ordering::Less => {
out.push('-');
if a.formal_charge < -1 {
let _ = write!(out, "{}", -i32::from(a.formal_charge));
}
}
std::cmp::Ordering::Equal => {}
}
if a.atom_map != 0 {
let _ = write!(out, ":{}", a.atom_map);
}
out.push(']');
}
fn needs_brackets(mol: &MolBuilder, idx: u32, style: WriteStyle) -> bool {
if hard_bracket(mol, idx) {
return true;
}
let a = mol.atoms()[idx as usize];
let author_fixed_hs = a.flags.contains(AtomFlags::NO_IMPLICIT) || a.num_explicit_hs != 0;
match style {
WriteStyle::Faithful => author_fixed_hs,
WriteStyle::Canonical => author_fixed_hs && !hs_survive_without_brackets(mol, idx),
}
}
fn hard_bracket(mol: &MolBuilder, idx: u32) -> bool {
let a = mol.atoms()[idx as usize];
a.isotope != 0
|| a.formal_charge != 0
|| a.atom_map != 0
|| a.num_radical_electrons != 0
|| a.chiral_tag != ChiralTag::Unspecified
|| (a.atomic_num != 0 && !element::is_organic_subset(a.atomic_num))
|| (a.flags.contains(AtomFlags::AROMATIC)
&& !element::can_be_aromatic_lowercase(a.atomic_num))
}
fn hs_survive_without_brackets(mol: &MolBuilder, idx: u32) -> bool {
let a = mol.atoms()[idx as usize];
let Some(e) = element::by_atomic_num(a.atomic_num) else {
return false;
};
if !e.has_valence_constraint() {
return false;
}
if mol
.neighbors(idx)
.any(|(_, bi)| mol.bonds()[bi as usize].order == BondOrder::Dative)
{
return false;
}
let default_valence = i32::from(e.valences[0]);
let bonds: f32 = mol
.neighbors(idx)
.map(|(_, bi)| mol.bonds()[bi as usize].valence_contribution_to(idx))
.sum();
#[allow(clippy::cast_possible_truncation)]
let bonds = (bonds + 0.1).round() as i32;
let total_hs = i32::from(a.num_explicit_hs) + i32::from(a.num_implicit_hs);
if total_hs == 0 {
bonds >= default_valence
} else {
bonds + total_hs == default_valence
}
}