use std::collections::HashMap;
use omgkit_core::{BondDirection, BondOrder, BondStereo, MolBuilder};
use omgkit_io::smarts::{
allowed_elements, atom_matches, bond_matches, required_chirality, BondExpr, BondPrim,
BondProps, QueryMol,
};
use omgkit_io::stereo;
use crate::props::MolProps;
type OrderKey = (usize, std::cmp::Reverse<usize>, usize);
#[derive(Debug, Clone, Copy)]
pub struct MatchOptions {
pub max_matches: usize,
pub uniquify: bool,
pub use_chirality: bool,
}
impl Default for MatchOptions {
fn default() -> Self {
Self {
max_matches: 0,
uniquify: true,
use_chirality: true,
}
}
}
pub type Mapping = Vec<u32>;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct SearchStats {
pub candidate_tests: u64,
}
#[must_use]
pub fn substructure_matches_counted(
query: &QueryMol,
mol: &MolBuilder,
props: &MolProps,
opts: MatchOptions,
) -> (Vec<Mapping>, SearchStats) {
if query.num_atoms() == 0 || query.num_atoms() > mol.num_atoms() {
return (Vec::new(), SearchStats::default());
}
let mut ctx = Ctx {
mol,
props,
candidate_tests: 0,
recursive_cache: HashMap::new(),
};
let counts = candidate_counts(query, props);
let order = search_order(query, &counts);
let mut out = Vec::new();
let mut seen: std::collections::HashSet<Vec<u32>> = std::collections::HashSet::new();
let mut mapping = vec![u32::MAX; query.num_atoms()];
let mut used = vec![false; mol.num_atoms()];
extend(
query,
&order,
0,
&mut mapping,
&mut used,
&mut ctx,
&opts,
&mut seen,
&mut out,
);
(
out,
SearchStats {
candidate_tests: ctx.candidate_tests,
},
)
}
#[must_use]
pub fn substructure_matches(
query: &QueryMol,
mol: &MolBuilder,
props: &MolProps,
opts: MatchOptions,
) -> Vec<Mapping> {
substructure_matches_counted(query, mol, props, opts).0
}
fn matches_rooted(query: &QueryMol, root: u32, ctx: &mut Ctx) -> bool {
if query.num_atoms() == 0 || query.num_atoms() > ctx.mol.num_atoms() {
return false;
}
let counts = candidate_counts(query, ctx.props);
let order = search_order(query, &counts);
let order = if order.first() == Some(&0) {
order
} else {
let mut o = vec![0u32];
o.extend(order.into_iter().filter(|&x| x != 0));
o
};
let mut mapping = vec![u32::MAX; query.num_atoms()];
let mut used = vec![false; ctx.mol.num_atoms()];
let opts = MatchOptions {
max_matches: 1,
uniquify: false,
use_chirality: true,
};
let mut seen = std::collections::HashSet::new();
let mut out = Vec::new();
if !atom_feasible(query, order[0], root, &mapping, ctx) {
return false;
}
mapping[order[0] as usize] = root;
used[root as usize] = true;
extend(
query,
&order,
1,
&mut mapping,
&mut used,
ctx,
&opts,
&mut seen,
&mut out,
);
!out.is_empty()
}
struct Ctx<'a> {
mol: &'a MolBuilder,
props: &'a MolProps,
candidate_tests: u64,
recursive_cache: HashMap<(usize, u32), bool>,
}
fn candidate_counts(query: &QueryMol, props: &MolProps) -> Vec<usize> {
let mut by_element = [0usize; 256];
for a in &props.atoms {
by_element[a.atomic_num as usize] += 1;
}
let total = props.atoms.len();
(0..query.num_atoms())
.map(|i| match allowed_elements(&query.atoms[i]) {
Some(set) => set.iter().map(|&z| by_element[z as usize]).sum(),
None => total,
})
.collect()
}
fn search_order(query: &QueryMol, counts: &[usize]) -> Vec<u32> {
let n = query.num_atoms();
let topo = &query.topology;
let mut order = Vec::with_capacity(n);
let mut placed = vec![false; n];
while order.len() < n {
let start = (0..n as u32)
.filter(|&a| !placed[a as usize])
.min_by_key(|&a| (counts[a as usize], std::cmp::Reverse(topo.degree(a))))
.expect("还有未放置的原子");
order.push(start);
placed[start as usize] = true;
loop {
let mut best: Option<(u32, OrderKey)> = None;
for a in 0..n as u32 {
if placed[a as usize] {
continue;
}
let links = topo
.neighbors(a)
.filter(|&(o, _)| placed[o as usize])
.count();
if links == 0 {
continue;
}
let key: OrderKey = (links, std::cmp::Reverse(counts[a as usize]), topo.degree(a));
if best.map_or(true, |(_, k)| key > k) {
best = Some((a, key));
}
}
match best {
Some((a, _)) => {
order.push(a);
placed[a as usize] = true;
}
None => break,
}
}
}
order
}
fn atom_feasible(query: &QueryMol, q: u32, t: u32, _mapping: &[u32], ctx: &mut Ctx) -> bool {
let props = ctx.props.atoms[t as usize];
let mut resolve = |sub: &QueryMol| {
let key = (sub as *const QueryMol as usize, t);
if let Some(&v) = ctx.recursive_cache.get(&key) {
return v;
}
ctx.recursive_cache.insert(key, false);
let v = matches_rooted(sub, t, ctx);
ctx.recursive_cache.insert(key, v);
v
};
atom_matches(&query.atoms[q as usize], &props, &mut resolve)
}
fn bonds_feasible(query: &QueryMol, q: u32, t: u32, mapping: &[u32], ctx: &Ctx) -> bool {
for (other_q, qbond) in query.topology.neighbors(q) {
let mapped = mapping[other_q as usize];
if mapped == u32::MAX {
continue; }
let Some(tbond) = ctx.mol.bond_between(t, mapped) else {
return false; };
if !bond_ok(query, qbond, tbond, mapping, ctx) {
return false;
}
}
true
}
fn bond_ok(query: &QueryMol, qbond: u32, tbond: u32, mapping: &[u32], ctx: &Ctx) -> bool {
let qb = query.topology.bonds()[qbond as usize];
let tb = ctx.mol.bonds()[tbond as usize];
let mut props: BondProps = ctx.props.bonds[tbond as usize];
if tb.order == BondOrder::Dative {
let want_donor = mapping[qb.begin as usize];
props.dative_forward = want_donor != u32::MAX && tb.begin == want_donor;
}
bond_matches(&query.bonds[qbond as usize], &props)
}
fn query_cis_trans(query: &QueryMol, bond: u32) -> Option<(BondStereo, [u32; 2])> {
if !expr_has(&query.bonds[bond as usize], BondPrim::Double) {
return None;
}
let b = query.topology.bonds()[bond as usize];
let (ra, da) = query_outward(query, b.begin, b.end)?;
let (rb, dbi) = query_outward(query, b.end, b.begin)?;
Some((
if da == dbi {
BondStereo::Cis
} else {
BondStereo::Trans
},
[ra, rb],
))
}
fn query_outward(query: &QueryMol, end: u32, other: u32) -> Option<(u32, BondDirection)> {
query
.topology
.neighbors(end)
.filter(|&(o, _)| o != other)
.find_map(|(o, bi)| {
let e = &query.bonds[bi as usize];
let raw = if expr_has(e, BondPrim::UpRight) {
BondDirection::UpRight
} else if expr_has(e, BondPrim::DownRight) {
BondDirection::DownRight
} else {
return None;
};
let tb = query.topology.bonds()[bi as usize];
Some((o, if tb.begin == end { raw } else { raw.flipped() }))
})
}
fn expr_has(e: &BondExpr, want: BondPrim) -> bool {
match e {
BondExpr::Prim(p) => *p == want,
BondExpr::And(parts) => parts.iter().any(|x| expr_has(x, want)),
BondExpr::Or(_) | BondExpr::Not(_) => false,
}
}
fn cis_trans_ok(query: &QueryMol, mapping: &Mapping, mol: &MolBuilder) -> bool {
for qb in 0..query.topology.num_bonds() as u32 {
let Some((want, qrefs)) = query_cis_trans(query, qb) else {
continue;
};
let b = query.topology.bonds()[qb as usize];
let (tb0, tb1) = (mapping[b.begin as usize], mapping[b.end as usize]);
let Some((_, ti)) = mol.neighbors(tb0).find(|&(o, _)| o == tb1) else {
return false;
};
let Some((got, trefs)) = stereo::raw_cis_trans(mol, ti) else {
return false;
};
let tb = mol.bonds()[ti as usize];
let trefs = if tb.begin == tb0 {
trefs
} else {
[trefs[1], trefs[0]]
};
let mut flips = 0;
for (i, &qr) in qrefs.iter().enumerate() {
if mapping[qr as usize] != trefs[i] {
flips += 1;
}
}
let effective = if flips % 2 == 1 { flipped(got) } else { got };
if effective != want {
return false;
}
}
true
}
fn flipped(s: BondStereo) -> BondStereo {
match s {
BondStereo::Cis => BondStereo::Trans,
BondStereo::Trans => BondStereo::Cis,
other => other,
}
}
fn chirality_ok(query: &QueryMol, mapping: &Mapping, mol: &MolBuilder) -> bool {
for (qi, expr) in query.atoms.iter().enumerate() {
let Some(want) = required_chirality(expr) else {
continue;
};
if !want.is_tetrahedral() {
continue;
}
let t = mapping[qi];
let got = mol.atoms()[t as usize].chiral_tag;
if !got.is_tetrahedral() {
return false;
}
let qn: Vec<u32> = query
.topology
.neighbors(qi as u32)
.map(|(o, _)| o)
.collect();
if qn.len() < 3 {
continue;
}
let imaged: Vec<u32> = qn.iter().map(|&o| mapping[o as usize]).collect();
let stored: Vec<u32> = mol.neighbors(t).map(|(o, _)| o).collect();
let extra: Vec<u32> = stored
.iter()
.copied()
.filter(|o| !imaged.contains(o))
.collect();
if stored.len() != imaged.len() + extra.len() {
continue;
}
let mut query_side = imaged.clone();
query_side.extend(extra);
let Some(odd) = omgkit_core::permutation_is_odd(&query_side, &stored) else {
continue;
};
let effective = if odd { got.inverted() } else { got };
if effective != want {
return false;
}
}
true
}
#[allow(clippy::too_many_arguments)]
fn extend(
query: &QueryMol,
order: &[u32],
depth: usize,
mapping: &mut Mapping,
used: &mut [bool],
ctx: &mut Ctx,
opts: &MatchOptions,
seen: &mut std::collections::HashSet<Vec<u32>>,
out: &mut Vec<Mapping>,
) {
if opts.max_matches != 0 && out.len() >= opts.max_matches {
return;
}
if depth == order.len() {
if opts.use_chirality
&& (!chirality_ok(query, mapping, ctx.mol) || !cis_trans_ok(query, mapping, ctx.mol))
{
return;
}
if opts.uniquify {
let mut key = mapping.clone();
key.sort_unstable();
if !seen.insert(key) {
return;
}
}
out.push(mapping.clone());
return;
}
let q = order[depth];
let anchor = query
.topology
.neighbors(q)
.map(|(o, _)| o)
.find(|&o| mapping[o as usize] != u32::MAX);
let candidates: Vec<u32> = match anchor {
Some(o) => ctx
.mol
.neighbors(mapping[o as usize])
.map(|(other, _)| other)
.collect(),
None => (0..ctx.mol.num_atoms() as u32).collect(),
};
for t in candidates {
ctx.candidate_tests += 1;
if used[t as usize] {
continue;
}
if !atom_feasible(query, q, t, mapping, ctx) {
continue;
}
mapping[q as usize] = t;
if bonds_feasible(query, q, t, mapping, ctx) {
used[t as usize] = true;
extend(query, order, depth + 1, mapping, used, ctx, opts, seen, out);
used[t as usize] = false;
}
mapping[q as usize] = u32::MAX;
if opts.max_matches != 0 && out.len() >= opts.max_matches {
return;
}
}
}