use std::collections::BTreeSet;
use omgkit_core::{AtomFlags, MolBuilder};
type Signature = Vec<u8>;
type Touched = Vec<(u32, Signature)>;
#[must_use]
pub fn canonical_ranks(mol: &MolBuilder) -> Vec<u32> {
if mol.num_atoms() == 0 {
return Vec::new();
}
let mut p = Partition::new(mol);
p.refine_with_stereo(mol);
p.break_all_ties(mol);
p.ranks()
}
#[must_use]
pub fn symmetry_classes(mol: &MolBuilder) -> Vec<u32> {
if mol.num_atoms() == 0 {
return Vec::new();
}
let mut p = Partition::new(mol);
p.refine_with_stereo(mol);
p.class_ids()
}
#[must_use]
pub fn classed_ranks(mol: &MolBuilder) -> Vec<u32> {
let classes = symmetry_classes(mol);
let w = canonical_smiles(mol);
debug_assert_eq!(
w.atom_order.len(),
mol.num_atoms(),
"规范 SMILES 没把所有原子写出来,类内平局会退回存储序"
);
let mut pos = vec![0u32; mol.num_atoms()];
for (i, a) in w.atom_order.iter().enumerate() {
pos[*a as usize] = u32::try_from(i).expect("原子数超出 u32");
}
let mut order: Vec<u32> =
(0..u32::try_from(mol.num_atoms()).expect("原子数超出 u32")).collect();
order.sort_by_key(|a| (classes[*a as usize], pos[*a as usize]));
let mut r = vec![0u32; mol.num_atoms()];
for (i, a) in order.iter().enumerate() {
r[*a as usize] = u32::try_from(i).expect("原子数超出 u32");
}
r
}
#[must_use]
pub fn canonical_smiles(mol: &MolBuilder) -> crate::smiles::Written {
if mol.num_atoms() == 0 {
return crate::smiles::write(mol);
}
let cleaned = drop_uninformative_stereo(mol);
let mol = &cleaned;
let mut base = Partition::new(mol);
base.refine_with_stereo(mol);
let Some(cell) = base.first_non_singleton() else {
return crate::smiles::write_with_priority_styled(
mol,
&base.ranks(),
crate::smiles::WriteStyle::Canonical,
);
};
let members: Vec<u32> = base.order[base.start[cell]..base.end[cell]].to_vec();
let n = mol.num_atoms();
let mut orbit = UnionFind::new(n);
let mut tried: Vec<u32> = Vec::new();
let mut best: Option<(crate::smiles::Written, Vec<u32>)> = None;
for &a in &members {
if tried.iter().any(|&t| orbit.same(t as usize, a as usize)) {
continue;
}
let mut p = base.clone();
p.split_off_atom(a, cell);
p.refine_with_stereo(mol);
p.break_all_ties(mol);
let written = crate::smiles::write_with_priority_styled(
mol,
&p.ranks(),
crate::smiles::WriteStyle::Canonical,
);
if let Some((prev, prev_order)) = &best {
if prev.smiles == written.smiles {
for (x, y) in prev_order.iter().zip(p.order.iter()) {
orbit.union(*x as usize, *y as usize);
}
}
}
let replace = best
.as_ref()
.map_or(true, |(b, _)| written.smiles < b.smiles);
if replace {
best = Some((written, p.order.clone()));
}
tried.push(a);
}
best.expect("格非空").0
}
fn drop_uninformative_stereo(mol: &MolBuilder) -> MolBuilder {
let genuine = crate::stereo::genuine_tetrahedral(mol);
let mut out = mol.clone();
for (i, &g) in genuine.iter().enumerate() {
if g {
continue;
}
if let Some(a) = out.atom_mut(i as u32) {
if a.chiral_tag.is_tetrahedral() {
a.chiral_tag = omgkit_core::ChiralTag::Unspecified;
}
}
}
out
}
#[must_use]
pub fn tie_break_matters(mol: &MolBuilder) -> bool {
if mol.num_atoms() == 0 {
return false;
}
let cleaned = drop_uninformative_stereo(mol);
let mol = &cleaned;
let mut base = Partition::new(mol);
base.refine_with_stereo(mol);
let Some(cell) = base.first_non_singleton() else {
return false;
};
let members: Vec<u32> = base.order[base.start[cell]..base.end[cell]].to_vec();
let mut first: Option<String> = None;
for &a in &members {
let mut p = base.clone();
p.split_off_atom(a, cell);
p.refine_with_stereo(mol);
p.break_all_ties(mol);
let s = crate::smiles::write_with_priority_styled(
mol,
&p.ranks(),
crate::smiles::WriteStyle::Canonical,
)
.smiles;
match &first {
None => first = Some(s),
Some(f) if *f != s => return true,
Some(_) => {}
}
}
false
}
struct UnionFind(Vec<usize>);
impl UnionFind {
fn new(n: usize) -> Self {
Self((0..n).collect())
}
fn find(&mut self, mut x: usize) -> usize {
while self.0[x] != x {
self.0[x] = self.0[self.0[x]]; x = self.0[x];
}
x
}
fn union(&mut self, a: usize, b: usize) {
let (ra, rb) = (self.find(a), self.find(b));
if ra != rb {
self.0[ra] = rb;
}
}
fn same(&mut self, a: usize, b: usize) -> bool {
self.find(a) == self.find(b)
}
}
fn stereo_descriptor(mol: &MolBuilder, a: u32, classes: &[u32]) -> u8 {
let at = mol.atoms()[a as usize];
if !at.chiral_tag.is_tetrahedral() {
return 0;
}
let nbrs: Vec<(u32, u32)> = mol
.neighbors(a)
.map(|(other, bond)| (classes[other as usize], bond))
.collect();
let mut cs: Vec<u32> = nbrs.iter().map(|&(c, _)| c).collect();
cs.sort_unstable();
if cs.windows(2).any(|w| w[0] == w[1]) {
return 0;
}
let storage: Vec<u32> = nbrs.iter().map(|&(_, b)| b).collect();
let mut by_class = nbrs;
by_class.sort_unstable_by_key(|&(c, _)| c);
let class_order: Vec<u32> = by_class.iter().map(|&(_, b)| b).collect();
let odd = crate::smiles::permutation_is_odd(&storage, &class_order).unwrap_or(false);
let tag = if odd {
at.chiral_tag.inverted()
} else {
at.chiral_tag
};
tag as u8
}
fn initial_invariant(mol: &MolBuilder, a: u32) -> (u8, u8, u32, i8, u8, u16, bool) {
let at = mol.atoms()[a as usize];
let bond_sum2: u32 = mol
.neighbors(a)
.map(|(_, b)| (mol.bonds()[b as usize].order.as_double() * 2.0) as u32)
.sum();
(
at.atomic_num,
mol.degree(a) as u8,
bond_sum2,
at.formal_charge,
at.num_explicit_hs.saturating_add(at.num_implicit_hs),
at.isotope,
at.flags.contains(AtomFlags::AROMATIC),
)
}
#[derive(Clone)]
struct Partition {
order: Vec<u32>,
pos: Vec<usize>,
cell_of: Vec<usize>,
start: Vec<usize>,
end: Vec<usize>,
pending: BTreeSet<(usize, usize)>,
}
impl Partition {
fn new(mol: &MolBuilder) -> Self {
let n = mol.num_atoms();
let mut order: Vec<u32> = (0..n as u32).collect();
order.sort_by_key(|&a| initial_invariant(mol, a));
let mut pos = vec![0usize; n];
let mut cell_of = vec![0usize; n];
let (mut start, mut end) = (Vec::new(), Vec::new());
let mut i = 0;
while i < n {
let key = initial_invariant(mol, order[i]);
let cell = start.len();
let lo = i;
while i < n && initial_invariant(mol, order[i]) == key {
pos[order[i] as usize] = i;
cell_of[order[i] as usize] = cell;
i += 1;
}
start.push(lo);
end.push(i);
}
let pending = start.iter().enumerate().map(|(c, &s)| (s, c)).collect();
Self {
order,
pos,
cell_of,
start,
end,
pending,
}
}
fn size(&self, c: usize) -> usize {
self.end[c] - self.start[c]
}
fn refine(&mut self, mol: &MolBuilder) {
let mut sig: Touched = Vec::new();
let mut mark: Vec<usize> = vec![usize::MAX; mol.num_atoms()];
while let Some(&(_, splitter)) = self.pending.iter().next() {
self.pending.remove(&(self.start[splitter], splitter));
sig.clear();
for i in self.start[splitter]..self.end[splitter] {
let x = self.order[i];
for (nbr, bond) in mol.neighbors(x) {
let code = mol.bonds()[bond as usize].order as u8;
let slot = mark[nbr as usize];
if slot == usize::MAX {
mark[nbr as usize] = sig.len();
sig.push((nbr, vec![code]));
} else {
sig[slot].1.push(code);
}
}
}
for (a, codes) in &mut sig {
codes.sort_unstable();
mark[*a as usize] = usize::MAX;
}
let mut by_cell: Vec<(usize, Touched)> = Vec::new();
let mut cell_slot: Vec<usize> = Vec::new();
for (a, codes) in sig.drain(..) {
let c = self.cell_of[a as usize];
if cell_slot.len() <= c {
cell_slot.resize(c + 1, usize::MAX);
}
if cell_slot[c] == usize::MAX {
cell_slot[c] = by_cell.len();
by_cell.push((c, Vec::new()));
}
by_cell[cell_slot[c]].1.push((a, codes));
}
for (c, touched) in by_cell {
self.split_cell(c, touched);
}
}
}
fn split_cell(&mut self, c: usize, mut touched: Touched) {
if touched.len() == self.size(c) && touched.iter().all(|(_, s)| *s == touched[0].1) {
return; }
touched.sort_by(|a, b| a.1.cmp(&b.1).then(a.0.cmp(&b.0)));
let mut boundary = self.end[c];
for &(a, _) in &touched {
boundary -= 1;
let pa = self.pos[a as usize];
let moved = self.order[boundary];
self.order[pa] = moved;
self.pos[moved as usize] = pa;
self.order[boundary] = a;
self.pos[a as usize] = boundary;
}
for (k, (a, _)) in touched.iter().enumerate() {
self.order[boundary + k] = *a;
self.pos[*a as usize] = boundary + k;
}
let old_end = self.end[c];
let was_pending = self.pending.remove(&(self.start[c], c));
let mut pieces: Vec<usize> = Vec::new();
if boundary > self.start[c] {
self.end[c] = boundary;
pieces.push(c);
}
let mut k = 0;
while k < touched.len() {
let mut j = k + 1;
while j < touched.len() && touched[j].1 == touched[k].1 {
j += 1;
}
let lo = boundary + k;
let hi = boundary + j;
let cell = if pieces.is_empty() && boundary == self.start[c] {
self.end[c] = hi;
c
} else {
self.start.push(lo);
self.end.push(hi);
self.start.len() - 1
};
for i in lo..hi {
self.cell_of[self.order[i] as usize] = cell;
}
pieces.push(cell);
k = j;
}
debug_assert_eq!(self.end[*pieces.last().expect("至少一块")], old_end);
let largest = pieces
.iter()
.copied()
.max_by_key(|&p| self.size(p))
.expect("至少一块");
for p in pieces {
if was_pending || p != largest {
self.pending.insert((self.start[p], p));
}
}
}
fn refine_with_stereo(&mut self, mol: &MolBuilder) {
loop {
self.refine(mol);
let classes = self.class_ids();
let cells: Vec<usize> = (0..self.start.len())
.filter(|&c| self.size(c) > 1)
.collect();
let before = self.start.len();
for c in cells {
if self.size(c) <= 1 {
continue;
}
let touched: Touched = self.order[self.start[c]..self.end[c]]
.iter()
.map(|&a| (a, vec![stereo_descriptor(mol, a, &classes)]))
.collect();
self.split_cell(c, touched);
}
if self.start.len() == before {
return; }
}
}
fn break_all_ties(&mut self, mol: &MolBuilder) {
while let Some(cell) = self.first_non_singleton() {
self.split_off_first(cell);
self.refine_with_stereo(mol);
}
}
fn ranks(&self) -> Vec<u32> {
let mut r = vec![0u32; self.order.len()];
for (i, &a) in self.order.iter().enumerate() {
r[a as usize] = i as u32;
}
r
}
fn class_ids(&self) -> Vec<u32> {
self.cell_of
.iter()
.map(|&cell| self.start[cell] as u32)
.collect()
}
fn first_non_singleton(&self) -> Option<usize> {
(0..self.start.len())
.filter(|&c| self.size(c) > 1)
.min_by_key(|&c| self.start[c])
}
fn split_off_first(&mut self, c: usize) {
self.split_off_atom(self.order[self.start[c]], c);
}
fn split_off_atom(&mut self, a: u32, c: usize) {
debug_assert_eq!(self.cell_of[a as usize], c, "原子不在该格里");
let lo = self.start[c];
let pa = self.pos[a as usize];
let head = self.order[lo];
self.order[lo] = a;
self.pos[a as usize] = lo;
self.order[pa] = head;
self.pos[head as usize] = pa;
let cell = self.start.len();
self.start.push(lo);
self.end.push(lo + 1);
self.cell_of[a as usize] = cell;
self.start[c] = lo + 1;
self.pending.insert((self.start[cell], cell));
self.pending.insert((self.start[c], c));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::smiles;
fn ranks_of(smi: &str) -> Vec<u32> {
let m = smiles::parse(smi).unwrap_or_else(|e| panic!("{smi}: {}", e.render()));
canonical_ranks(&m)
}
#[test]
fn ranks_are_a_permutation() {
for smi in [
"C",
"CCO",
"c1ccccc1",
"OC(=O)c1ccccc1N",
"CCO.CCN",
"C1CC2CCC1CC2",
"CC(C)(C)C",
] {
let r = ranks_of(smi);
let mut sorted = r.clone();
sorted.sort_unstable();
let expect: Vec<u32> = (0..r.len() as u32).collect();
assert_eq!(sorted, expect, "{smi} 的秩不是一个排列:{r:?}");
}
}
#[test]
fn symmetric_molecules_need_tie_breaking() {
let m = smiles::parse("c1ccccc1").unwrap();
let mut p = Partition::new(&m);
p.refine(&m);
assert!(
p.first_non_singleton().is_some(),
"苯细化之后应当仍有多原子的格"
);
assert_eq!(canonical_ranks(&m).len(), 6);
}
#[test]
fn refinement_separates_inequivalent_atoms() {
let m = smiles::parse("Cc1ccccc1").unwrap();
let mut p = Partition::new(&m);
p.refine(&m);
let cells: std::collections::BTreeSet<usize> =
(0..m.num_atoms()).map(|a| p.cell_of[a]).collect();
assert_eq!(cells.len(), 5, "甲苯应细分成 5 类,实际 {}", cells.len());
}
}