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
}
fn parent_bonds(mol: &MolBuilder, dfs: &Dfs) -> Vec<Option<u32>> {
let mut parents = vec![None; mol.num_atoms()];
for (a, kids) in dfs.children.iter().enumerate() {
for &bond in kids {
let child = other_end(mol, bond, a as u32);
parents[child as usize] = Some(bond);
}
}
parents
}
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 parents = parent_bonds(mol, dfs);
let order_at = |a: u32| -> (Vec<u32>, usize) {
let via = parents[a as usize];
let mut v = Vec::with_capacity(mol.degree(a));
v.extend(via);
v.extend(dfs.ring_closures[a as usize].iter().copied());
v.extend(dfs.children[a as usize].iter().copied());
(v, usize::from(via.is_some()))
};
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 (written_bonds, _) = order_at(atom);
let tag = output_chiral_tag(
mol,
atom,
&written_bonds,
via.is_none(),
dfs.ring_closures[atom as usize].len(),
&order_at,
);
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,
order_at: &dyn Fn(u32) -> (Vec<u32>, usize),
) -> (ChiralTag, u8) {
let a = mol.atoms()[atom as usize];
if a.chiral_tag == ChiralTag::Allene {
let perm = super::allene_renumber(
mol,
atom,
a.stereo_perm,
&super::stored_order_at(mol),
order_at,
)
.unwrap_or(0);
return (a.chiral_tag, perm);
}
if omgkit_core::polyhedron::ligand_count(a.chiral_tag).is_some() {
let perm = super::coordination_ligands(mol, atom, a.chiral_tag)
.and_then(|stored| {
let mut written = written_bonds.to_vec();
if stored.len() == written.len() + 1 {
written.insert(usize::from(!is_fragment_start), super::VACANT_LIGAND);
}
omgkit_core::polyhedron::renumber(a.chiral_tag, a.stereo_perm, &stored, &written)
})
.unwrap_or(0);
return (a.chiral_tag, perm);
}
if !a.chiral_tag.is_tetrahedral() {
return (ChiralTag::Unspecified, 0);
}
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, 0);
};
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;
}
}
let tag = if odd {
a.chiral_tag.inverted()
} else {
a.chiral_tag
};
(tag, 0)
}
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,
stereo: (ChiralTag, u8),
style: WriteStyle,
) {
let (tag, perm) = stereo;
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("@@"),
ChiralTag::SquarePlanar if perm != 0 => {
let _ = write!(out, "@SP{perm}");
}
ChiralTag::TrigonalBipyramidal if perm != 0 => {
let _ = write!(out, "@TB{perm}");
}
ChiralTag::Octahedral if perm != 0 => {
let _ = write!(out, "@OH{perm}");
}
ChiralTag::Allene if perm != 0 => {
let _ = write!(out, "@AL{perm}");
}
_ => {}
}
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 {
debug_assert!(
!hard_bracket(mol, idx),
"hs_survive_without_brackets 的前置条件被破坏了:带电/自由基/同位素的原子\
必须由 hard_bracket 提前挡掉,本函数不做那几项调整 —— 见函数文档"
);
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;
}
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 Ok(used) = i8::try_from(bonds) else {
return false; };
let total_hs = i32::from(a.num_explicit_hs) + i32::from(a.num_implicit_hs);
if e.default_valence_for(used).is_none() {
return false;
}
let Some(bare) = omgkit_core::valence::implicit_hs_for_bare_form(mol, idx) else {
return false; };
i32::from(bare) == total_hs
}