use std::collections::{BTreeMap, BTreeSet};
use omgkit_core::{
AtomData, BondData, BondDirection, BondOrder, BondStereo, ChiralTag, MolBuilder,
};
use omgkit_io::smarts::{
map_number, required_chirality, AtomExpr, AtomPrim, BondExpr, BondPrim, QueryMol, Reaction,
};
use crate::matcher::{substructure_matches, MatchOptions};
use crate::props::MolProps;
pub type ProductSet = Vec<MolBuilder>;
#[derive(Debug, Clone)]
pub struct Outcome {
pub products: ProductSet,
pub reactants: Vec<MolBuilder>,
pub discarded: Vec<Vec<u32>>,
}
#[must_use]
pub fn run_reactants(
reaction: &Reaction,
reactants: &[(MolBuilder, MolProps)],
max_products: usize,
atom_mapping: bool,
) -> Vec<Outcome> {
debug_assert!(
!reactants
.iter()
.any(|(m, _)| omgkit_io::stereo::directions_not_perceived(m)),
"反应物里有双键的几何**方向键已经写明**、却没有感知过顺反 —— \
漏了 omgkit_io::stereo::perceive_bond_stereo。这样跑不会报错,\
但反应一旦删掉承载方向的那根单键,几何会静默丢失"
);
if reactants.len() != reaction.reactants.len() || reaction.products.is_empty() {
return Vec::new();
}
let opts = MatchOptions {
max_matches: 0,
uniquify: false,
use_chirality: false,
};
let n = reaction.reactants.len();
let per_template: Vec<Vec<Vec<u32>>> = reaction
.reactants
.iter()
.zip(reactants)
.map(|(t, (mol, props))| substructure_matches(t, mol, props, opts))
.collect();
let comps: Vec<Vec<u32>> = reactants.iter().map(|(m, _)| components(m)).collect();
if per_template.iter().all(|m| !m.is_empty()) {
let identity: Vec<usize> = (0..n).collect();
let out = outcomes_under(
reaction,
reactants,
&per_template,
&identity,
&comps,
max_products,
atom_mapping,
);
if !out.is_empty() {
return out;
}
}
let mut table: Vec<Vec<Vec<Vec<u32>>>> = Vec::with_capacity(n);
for (t, tpl) in reaction.reactants.iter().enumerate() {
let mut row = Vec::with_capacity(n);
for (m, (mol, props)) in reactants.iter().enumerate() {
row.push(if m == t {
per_template[t].clone()
} else {
substructure_matches(tpl, mol, props, opts)
});
}
table.push(row);
}
let mut assign = vec![0usize; n];
let mut used = vec![false; n];
search_assignment(
reaction,
reactants,
&table,
&comps,
max_products,
atom_mapping,
0,
&mut assign,
&mut used,
)
.unwrap_or_default()
}
fn outcomes_under(
reaction: &Reaction,
reactants: &[(MolBuilder, MolProps)],
per_template: &[Vec<Vec<u32>>],
assign: &[usize],
comps: &[Vec<u32>],
max_products: usize,
atom_mapping: bool,
) -> Vec<Outcome> {
let mut out = Vec::new();
let mut combo: Vec<usize> = vec![0; per_template.len()];
loop {
let mapping: Vec<&Vec<u32>> = combo
.iter()
.enumerate()
.map(|(i, &j)| &per_template[i][j])
.collect();
let built = build_products(reaction, reactants, &mapping, assign, comps);
out.push(stamp_atom_maps(reactants, built, atom_mapping));
if max_products != 0 && out.len() >= max_products {
return out;
}
let mut i = 0;
loop {
if i == combo.len() {
return out;
}
combo[i] += 1;
if combo[i] < per_template[i].len() {
break;
}
combo[i] = 0;
i += 1;
}
}
}
#[allow(clippy::too_many_arguments)]
fn search_assignment(
reaction: &Reaction,
reactants: &[(MolBuilder, MolProps)],
table: &[Vec<Vec<Vec<u32>>>],
comps: &[Vec<u32>],
max_products: usize,
atom_mapping: bool,
depth: usize,
assign: &mut Vec<usize>,
used: &mut Vec<bool>,
) -> Option<Vec<Outcome>> {
if depth == assign.len() {
let per: Vec<Vec<Vec<u32>>> = assign
.iter()
.enumerate()
.map(|(t, &m)| table[t][m].clone())
.collect();
let out = outcomes_under(
reaction,
reactants,
&per,
assign,
comps,
max_products,
atom_mapping,
);
return if out.is_empty() { None } else { Some(out) };
}
for m in 0..used.len() {
if used[m] || table[depth][m].is_empty() {
continue;
}
used[m] = true;
assign[depth] = m;
if let Some(out) = search_assignment(
reaction,
reactants,
table,
comps,
max_products,
atom_mapping,
depth + 1,
assign,
used,
) {
return Some(out);
}
used[m] = false;
}
None
}
fn concat(mols: &[(MolBuilder, MolProps)]) -> MolBuilder {
let n_atoms = mols.iter().map(|(m, _)| m.num_atoms()).sum();
let n_bonds = mols.iter().map(|(m, _)| m.num_bonds()).sum();
let mut out = MolBuilder::with_capacity(n_atoms, n_bonds);
for (m, _) in mols {
let base = u32::try_from(out.num_atoms()).unwrap_or(u32::MAX);
for a in m.atoms() {
out.add_atom_data(*a);
}
for b in m.bonds() {
let mut nb = *b;
nb.begin += base;
nb.end += base;
for s in &mut nb.stereo_atoms {
if *s != BondData::NO_STEREO_ATOM {
*s += base;
}
}
let _ = out.add_bond_data(nb);
}
}
out
}
#[must_use]
pub fn run_on_substrate(
reaction: &Reaction,
substrate: &[(MolBuilder, MolProps)],
max_products: usize,
atom_mapping: bool,
) -> Vec<Outcome> {
debug_assert!(
!substrate
.iter()
.any(|(m, _)| omgkit_io::stereo::directions_not_perceived(m)),
"底物里有双键的几何**方向键已经写明**、却没有感知过顺反 —— \
漏了 omgkit_io::stereo::perceive_bond_stereo。理由见 run_reactants"
);
if substrate.is_empty() || reaction.reactants.is_empty() || reaction.products.is_empty() {
return Vec::new();
}
let sizes: Vec<usize> = substrate.iter().map(|(m, _)| m.num_atoms()).collect();
let mol = concat(substrate);
let props = MolProps::compute(&mol);
let inputs = [(mol, props)];
let opts = MatchOptions {
max_matches: 0,
uniquify: false,
use_chirality: false,
};
let per_template: Vec<Vec<Vec<u32>>> = reaction
.reactants
.iter()
.map(|t| substructure_matches(t, &inputs[0].0, &inputs[0].1, opts))
.collect();
if per_template.iter().any(Vec::is_empty) {
return Vec::new();
}
let home = vec![0usize; reaction.reactants.len()];
let n_atoms = inputs[0].0.num_atoms();
let comps: Vec<Vec<u32>> = inputs.iter().map(|(m, _)| components(m)).collect();
let mut out = Vec::new();
let mut combo: Vec<usize> = vec![0; per_template.len()];
let mut used = vec![false; n_atoms];
loop {
let mapping: Vec<&Vec<u32>> = combo
.iter()
.enumerate()
.map(|(i, &j)| &per_template[i][j])
.collect();
used.iter_mut().for_each(|u| *u = false);
let disjoint = mapping.iter().all(|m| {
m.iter().all(|&a| {
let fresh = !used[a as usize];
used[a as usize] = true;
fresh
})
});
if disjoint {
let built = build_products(reaction, &inputs, &mapping, &home, &comps);
let mut outcome = stamp_atom_maps(&inputs, built, atom_mapping);
outcome.discarded = regroup_discarded(&outcome.discarded, &sizes);
out.push(outcome);
if max_products != 0 && out.len() >= max_products {
break;
}
}
let mut i = 0;
loop {
if i == combo.len() {
return out;
}
combo[i] += 1;
if combo[i] < per_template[i].len() {
break;
}
combo[i] = 0;
i += 1;
}
}
out
}
type Anchor = (usize, u32);
struct ReactantFacts {
anchors: BTreeMap<u16, Anchor>,
degree: BTreeMap<u16, usize>,
chirality: BTreeMap<u16, Option<ChiralTag>>,
neighbors: BTreeMap<u16, Vec<Option<u16>>>,
}
fn neighbor_maps(template: &QueryMol, qi: u32) -> Vec<Option<u16>> {
template
.topology
.neighbors(qi)
.map(|(other, _)| map_number(&template.atoms[other as usize]))
.collect()
}
fn template_order_is_odd(react: &[Option<u16>], prod: &[Option<u16>]) -> Option<bool> {
if react.len() < 3 || prod.len() < 3 || react.len().abs_diff(prod.len()) > 1 {
return None;
}
let mut r: Vec<Option<u16>> = react.to_vec();
let mut p: Vec<Option<u16>> = prod.to_vec();
if r.len() < p.len() {
r.push(None);
} else if p.len() < r.len() {
p.push(None);
}
if r.iter().filter(|x| x.is_none()).count() > 1 || p.iter().filter(|x| x.is_none()).count() > 1
{
return None;
}
fill_missing(&mut r, &p)?;
fill_missing(&mut p, &r)?;
let enc = |v: &[Option<u16>]| -> Vec<u32> {
v.iter().map(|x| x.map_or(u32::MAX, u32::from)).collect()
};
omgkit_core::permutation_is_odd(&enc(&r), &enc(&p))
}
fn fill_missing(have: &mut [Option<u16>], want: &[Option<u16>]) -> Option<()> {
for &elem in want.iter().flatten() {
if have.contains(&Some(elem)) {
continue;
}
let slot = have.iter().position(Option::is_none)?;
have[slot] = Some(elem);
}
Some(())
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum ChiralityPlan {
Inherit,
Drop,
Set,
Retain,
Invert,
}
impl ChiralityPlan {
fn decide(
reactant: Option<ChiralTag>,
product: Option<ChiralTag>,
order_is_odd: Option<bool>,
) -> Self {
match (reactant, product) {
(None, None) => Self::Inherit,
(Some(_), None) => Self::Drop,
(None, Some(_)) => Self::Set,
(Some(r), Some(p)) => {
if (r == p) != order_is_odd.unwrap_or(false) {
Self::Retain
} else {
Self::Invert
}
}
}
}
}
type BuiltProduct = (MolBuilder, Vec<BTreeMap<u32, u32>>);
pub(crate) fn components(mol: &MolBuilder) -> Vec<u32> {
let n = mol.num_atoms();
let mut comp = vec![u32::MAX; n];
let mut stack: Vec<u32> = Vec::new();
let mut next = 0u32;
for s in 0..n as u32 {
if comp[s as usize] != u32::MAX {
continue;
}
comp[s as usize] = next;
stack.push(s);
while let Some(a) = stack.pop() {
for (other, _) in mol.neighbors(a) {
if comp[other as usize] == u32::MAX {
comp[other as usize] = next;
stack.push(other);
}
}
}
next += 1;
}
comp
}
fn build_products(
reaction: &Reaction,
reactants: &[(MolBuilder, MolProps)],
matches: &[&Vec<u32>],
home: &[usize],
comps: &[Vec<u32>],
) -> Vec<BuiltProduct> {
let mut matched: Vec<Vec<bool>> = reactants
.iter()
.map(|(m, _)| vec![false; m.num_atoms()])
.collect();
let mut template_bonds: Vec<Vec<bool>> = reactants
.iter()
.map(|(m, _)| vec![false; m.num_bonds()])
.collect();
let mut facts = ReactantFacts {
anchors: BTreeMap::new(),
degree: BTreeMap::new(),
chirality: BTreeMap::new(),
neighbors: BTreeMap::new(),
};
for (ti, template) in reaction.reactants.iter().enumerate() {
let ri = home[ti];
for qb in template.topology.bonds() {
let (a, b) = (matches[ti][qb.begin as usize], matches[ti][qb.end as usize]);
if let Some(bi) = reactants[ri].0.bond_between(a, b) {
template_bonds[ri][bi as usize] = true;
}
}
for (qi, &target) in matches[ti].iter().enumerate() {
matched[ri][target as usize] = true;
if let Some(n) = map_number(&template.atoms[qi]) {
facts.anchors.entry(n).or_insert((ri, target));
facts
.degree
.entry(n)
.or_insert_with(|| template.topology.degree(qi as u32));
facts
.chirality
.entry(n)
.or_insert_with(|| required_chirality(&template.atoms[qi]));
facts
.neighbors
.entry(n)
.or_insert_with(|| neighbor_maps(template, qi as u32));
}
}
}
let mut out = MolBuilder::new();
let mut from_reactant: Vec<BTreeMap<u32, u32>> =
reactants.iter().map(|_| BTreeMap::new()).collect();
let mut settled_chirality: BTreeSet<u32> = BTreeSet::new();
for pt in &reaction.products {
emit_template(
pt,
reactants,
&facts,
&mut out,
&mut from_reactant,
&mut settled_chirality,
);
}
for (ti, (mol, _)) in reactants.iter().enumerate() {
seed_spectators(
mol,
&comps[ti],
&matched[ti],
&mut from_reactant[ti],
&mut out,
);
carry_over(
mol,
&matched[ti],
&template_bonds[ti],
&mut from_reactant[ti],
&mut out,
);
}
for (ti, (mol, _)) in reactants.iter().enumerate() {
rebase_chirality(mol, &from_reactant[ti], &settled_chirality, &mut out);
rebase_bond_stereo(mol, &from_reactant[ti], &mut out);
}
split_components(&out, &from_reactant)
}
fn honoured_directions(template: &QueryMol) -> Vec<bool> {
let bonds = template.topology.bonds();
let has_dir: Vec<bool> = template
.bonds
.iter()
.map(|e| bond_direction_from(e) != BondDirection::None)
.collect();
let flanked = |atom: u32, skip: usize| {
template
.topology
.neighbors(atom)
.any(|(_, bi)| bi as usize != skip && has_dir[bi as usize])
};
let determined: Vec<bool> = (0..bonds.len())
.map(|bi| {
product_bond_from(&template.bonds[bi]) == ProductBond::Fixed(BondOrder::Double)
&& flanked(bonds[bi].begin, bi)
&& flanked(bonds[bi].end, bi)
})
.collect();
(0..bonds.len())
.map(|bi| {
has_dir[bi]
&& [bonds[bi].begin, bonds[bi].end].iter().any(|&a| {
template
.topology
.neighbors(a)
.any(|(_, ob)| ob as usize != bi && determined[ob as usize])
})
})
.collect()
}
fn emit_template(
template: &QueryMol,
reactants: &[(MolBuilder, MolProps)],
facts: &ReactantFacts,
out: &mut MolBuilder,
from_reactant: &mut [BTreeMap<u32, u32>],
settled_chirality: &mut BTreeSet<u32>,
) {
let mut from_template: Vec<u32> = Vec::with_capacity(template.num_atoms());
let mut anchor_of: Vec<Option<Anchor>> = Vec::with_capacity(template.num_atoms());
for (qi, expr) in template.atoms.iter().enumerate() {
let anchor = map_number(expr)
.and_then(|n| facts.anchors.get(&n))
.copied();
anchor_of.push(anchor);
let base = match anchor {
Some((ti, ai)) => reactants[ti].0.atoms()[ai as usize],
None => AtomData::new(0),
};
let degree_kept = map_number(expr)
.and_then(|n| facts.degree.get(&n).copied())
.is_some_and(|d| d == template.topology.degree(qi as u32));
let plan = ChiralityPlan::decide(
map_number(expr)
.and_then(|n| facts.chirality.get(&n).copied())
.flatten(),
required_chirality(expr),
map_number(expr)
.and_then(|n| facts.neighbors.get(&n))
.and_then(|r| template_order_is_odd(r, &neighbor_maps(template, qi as u32))),
);
let idx = out.add_atom_data(apply_template(base, expr, degree_kept, plan));
if plan == ChiralityPlan::Set {
settled_chirality.insert(idx);
}
from_template.push(idx);
if let Some((ti, ai)) = anchor {
from_reactant[ti].insert(ai, idx);
}
}
let honoured = honoured_directions(template);
for (bi, expr) in template.bonds.iter().enumerate() {
let b = template.topology.bonds()[bi];
let order = match product_bond_from(expr) {
ProductBond::Fixed(o) => o,
ProductBond::FollowAromaticity => {
let aromatic = |ti: u32| {
out.atoms()[from_template[ti as usize] as usize]
.flags
.contains(omgkit_core::AtomFlags::AROMATIC)
};
if aromatic(b.begin) && aromatic(b.end) {
BondOrder::Aromatic
} else {
BondOrder::Single
}
}
ProductBond::Inherit => {
match (anchor_of[b.begin as usize], anchor_of[b.end as usize]) {
(Some((t1, a1)), Some((t2, a2))) if t1 == t2 => {
inherited_order(&reactants[t1].0, a1, a2)
}
_ => BondOrder::Unspecified,
}
}
};
let (tb, te) = if is_dative_reversed(expr) {
(b.end, b.begin)
} else {
(b.begin, b.end)
};
let mut bd = BondData::new(
from_template[tb as usize],
from_template[te as usize],
order,
);
bd.flags.set(
omgkit_core::BondFlags::AROMATIC,
order == BondOrder::Aromatic,
);
let from_template_dir = if honoured[bi] {
bond_direction_from(expr)
} else {
BondDirection::None
};
bd.direction = if from_template_dir != BondDirection::None {
from_template_dir
} else if let (Some((t1, a1)), Some((t2, a2))) =
(anchor_of[b.begin as usize], anchor_of[b.end as usize])
{
if t1 == t2 {
inherited_direction(&reactants[t1].0, a1, a2)
} else {
BondDirection::None
}
} else {
BondDirection::None
};
let _ = out.add_bond_data(bd);
}
}
fn split_components(
shared: &MolBuilder,
from_reactant: &[BTreeMap<u32, u32>],
) -> Vec<BuiltProduct> {
let n = shared.num_atoms();
let mut comp = vec![usize::MAX; n];
let mut n_comp = 0usize;
let mut stack: Vec<u32> = Vec::new();
for s in 0..n as u32 {
if comp[s as usize] != usize::MAX {
continue;
}
comp[s as usize] = n_comp;
stack.push(s);
while let Some(a) = stack.pop() {
for (other, _) in shared.neighbors(a) {
if comp[other as usize] == usize::MAX {
comp[other as usize] = n_comp;
stack.push(other);
}
}
}
n_comp += 1;
}
let mut mols: Vec<MolBuilder> = (0..n_comp).map(|_| MolBuilder::new()).collect();
let mut local = vec![u32::MAX; n];
for a in 0..n as u32 {
let c = comp[a as usize];
local[a as usize] = mols[c].add_atom_data(shared.atoms()[a as usize]);
}
for b in shared.bonds() {
let c = comp[b.begin as usize];
let mut nb = *b;
nb.begin = local[b.begin as usize];
nb.end = local[b.end as usize];
nb.stereo_atoms = [
translate_stereo_atom(b.stereo_atoms[0], &local),
translate_stereo_atom(b.stereo_atoms[1], &local),
];
let _ = mols[c].add_bond_data(nb);
}
let mut tables: Vec<Vec<BTreeMap<u32, u32>>> = (0..n_comp)
.map(|_| from_reactant.iter().map(|_| BTreeMap::new()).collect())
.collect();
for (ti, table) in from_reactant.iter().enumerate() {
for (&src, &dst) in table {
let c = comp[dst as usize];
tables[c][ti].insert(src, local[dst as usize]);
}
}
mols.into_iter().zip(tables).collect()
}
fn translate_stereo_atom(idx: u32, local: &[u32]) -> u32 {
if idx == BondData::NO_STEREO_ATOM {
return BondData::NO_STEREO_ATOM;
}
local
.get(idx as usize)
.copied()
.filter(|&v| v != u32::MAX)
.unwrap_or(BondData::NO_STEREO_ATOM)
}
fn stamp_atom_maps(
reactants: &[(MolBuilder, MolProps)],
built: Vec<BuiltProduct>,
atom_mapping: bool,
) -> Outcome {
let discarded = discarded_atoms(reactants, &built);
if !atom_mapping {
return Outcome {
products: built.into_iter().map(|(m, _)| m).collect(),
reactants: Vec::new(),
discarded,
};
}
let mut products: ProductSet = Vec::with_capacity(built.len());
let mut first_home: BTreeMap<(usize, u32), (usize, u32)> = BTreeMap::new();
for (pi, (mol, per_reactant)) in built.into_iter().enumerate() {
for (ti, table) in per_reactant.iter().enumerate() {
for (&src, &dst) in table {
first_home.entry((ti, src)).or_insert((pi, dst));
}
}
products.push(mol);
}
let mut mapped: Vec<MolBuilder> = reactants.iter().map(|(m, _)| m.clone()).collect();
for m in &mut mapped {
for i in 0..m.num_atoms() as u32 {
if let Some(a) = m.atom_mut(i) {
a.atom_map = 0;
}
}
}
let mut next: u32 = 1;
for (&(ti, src), &(pi, dst)) in &first_home {
let Ok(n) = u16::try_from(next) else { break };
if mapped[ti].atoms().get(src as usize).is_none()
|| products[pi].atoms().get(dst as usize).is_none()
{
continue;
}
if let Some(a) = mapped[ti].atom_mut(src) {
a.atom_map = n;
}
if let Some(a) = products[pi].atom_mut(dst) {
a.atom_map = n;
}
next += 1;
}
Outcome {
products,
reactants: mapped,
discarded,
}
}
fn regroup_discarded(flat: &[Vec<u32>], sizes: &[usize]) -> Vec<Vec<u32>> {
let mut out: Vec<Vec<u32>> = sizes.iter().map(|_| Vec::new()).collect();
for a in flat.iter().flatten() {
let mut rest = *a as usize;
for (i, &n) in sizes.iter().enumerate() {
if rest < n {
out[i].push(u32::try_from(rest).unwrap_or(u32::MAX));
break;
}
rest -= n;
}
}
out
}
fn discarded_atoms(reactants: &[(MolBuilder, MolProps)], built: &[BuiltProduct]) -> Vec<Vec<u32>> {
let mut kept: Vec<Vec<bool>> = reactants
.iter()
.map(|(m, _)| vec![false; m.num_atoms()])
.collect();
for (_, per_reactant) in built {
for (ti, table) in per_reactant.iter().enumerate() {
for &src in table.keys() {
if let Some(slot) = kept[ti].get_mut(src as usize) {
*slot = true;
}
}
}
}
kept.iter()
.map(|flags| {
flags
.iter()
.enumerate()
.filter(|&(_, &k)| !k)
.map(|(i, _)| u32::try_from(i).unwrap_or(u32::MAX))
.collect()
})
.collect()
}
fn rebase_chirality(
mol: &MolBuilder,
kept: &BTreeMap<u32, u32>,
settled_chirality: &BTreeSet<u32>,
out: &mut MolBuilder,
) {
for (&src, &dst) in kept {
if settled_chirality.contains(&dst) {
continue;
}
let tag = out.atoms()[dst as usize].chiral_tag;
if tag == ChiralTag::Unspecified {
continue;
}
let after: Vec<u32> = out.neighbors(dst).map(|(other, _)| other).collect();
if !tag.is_tetrahedral() {
rebase_coordination(mol, src, dst, kept, &after, out);
continue;
}
let slots: Vec<Option<u32>> = mol
.neighbors(src)
.map(|(other, _)| kept.get(&other).copied().filter(|p| after.contains(p)))
.collect();
let Some((before, after)) = align_for_rebase(&slots, &after) else {
continue;
};
if omgkit_core::permutation_is_odd(&before, &after) == Some(true) {
if let Some(a) = out.atom_mut(dst) {
a.chiral_tag = tag.inverted();
}
}
}
}
fn rebase_coordination(
mol: &MolBuilder,
src: u32,
dst: u32,
kept: &BTreeMap<u32, u32>,
after: &[u32],
out: &mut MolBuilder,
) {
let tag = out.atoms()[dst as usize].chiral_tag;
let perm = out.atoms()[dst as usize].stereo_perm;
let before: Vec<u32> = mol
.neighbors(src)
.filter_map(|(other, _)| kept.get(&other).copied())
.filter(|p| after.contains(p))
.collect();
let renumbered = if perm == 0 || before.len() != after.len() {
None
} else {
omgkit_core::polyhedron::renumber(tag, perm, &before, after)
};
if let Some(a) = out.atom_mut(dst) {
match renumbered {
Some(p) => a.stereo_perm = p,
None => {
a.stereo_perm = 0;
a.chiral_tag = ChiralTag::Unspecified;
}
}
}
}
pub(crate) const IMPLICIT_H: u32 = u32::MAX;
pub(crate) fn align_for_rebase(
slots: &[Option<u32>],
after: &[u32],
) -> Option<(Vec<u32>, Vec<u32>)> {
if let Some(before) = fill_replaced_slots(slots, after) {
if before.len() == after.len() {
return Some((before, after.to_vec()));
}
}
let vacated = slots.iter().filter(|s| s.is_none()).count();
let occupied = slots.len() - vacated;
if vacated == 1 && occupied == after.len() && slots.len() == 4 {
let before: Vec<u32> = slots.iter().map(|s| s.unwrap_or(IMPLICIT_H)).collect();
let mut aligned = after.to_vec();
aligned.insert(1, IMPLICIT_H);
return Some((before, aligned));
}
if vacated == 0 && after.len() == slots.len() + 1 && after.len() == 4 {
let taken: BTreeSet<u32> = slots.iter().flatten().copied().collect();
let mut fresh = after.iter().filter(|a| !taken.contains(a));
let new = *fresh.next()?;
if fresh.next().is_some() {
return None;
}
let mut before: Vec<u32> = slots.iter().flatten().copied().collect();
before.insert(1, new);
return Some((before, after.to_vec()));
}
None
}
fn fill_replaced_slots(slots: &[Option<u32>], after: &[u32]) -> Option<Vec<u32>> {
if slots.iter().all(Option::is_some) {
return Some(slots.iter().flatten().copied().collect());
}
let taken: BTreeSet<u32> = slots.iter().flatten().copied().collect();
let mut fresh = after.iter().filter(|a| !taken.contains(a));
let filled: Option<Vec<u32>> = slots
.iter()
.map(|s| match s {
Some(x) => Some(*x),
None => fresh.next().copied(),
})
.collect();
let filled = filled?;
if fresh.next().is_some() {
return None;
}
Some(filled)
}
fn inherited_order(mol: &MolBuilder, a: u32, b: u32) -> BondOrder {
mol.neighbors(a)
.find(|&(other, _)| other == b)
.map_or(BondOrder::Unspecified, |(_, bi)| {
mol.bonds()[bi as usize].order
})
}
fn inherited_direction(mol: &MolBuilder, a: u32, b: u32) -> BondDirection {
let Some((_, bi)) = mol.neighbors(a).find(|&(other, _)| other == b) else {
return BondDirection::None;
};
let src = mol.bonds()[bi as usize];
if src.begin == a {
src.direction
} else {
src.direction.flipped()
}
}
fn seed_spectators(
mol: &MolBuilder,
comp: &[u32],
matched: &[bool],
kept: &mut BTreeMap<u32, u32>,
out: &mut MolBuilder,
) {
let Some(&n_comp) = comp.iter().max() else {
return;
};
if n_comp == 0 {
return;
}
let n_comp = n_comp as usize + 1;
let mut has_match = vec![false; n_comp];
for (a, &hit) in matched.iter().enumerate() {
if hit {
has_match[comp[a] as usize] = true;
}
}
let mut seeded = vec![false; n_comp];
for (a, &c) in comp.iter().enumerate() {
let c = c as usize;
if has_match[c] || seeded[c] {
continue;
}
seeded[c] = true;
let mut carried = mol.atoms()[a];
carried.atom_map = 0;
let idx = out.add_atom_data(carried);
kept.insert(a as u32, idx);
}
}
fn carry_over(
mol: &MolBuilder,
matched: &[bool],
template_bonds: &[bool],
kept: &mut BTreeMap<u32, u32>,
out: &mut MolBuilder,
) {
let mut stack: Vec<u32> = kept.keys().copied().collect();
let mut seen: Vec<bool> = vec![false; mol.num_atoms()];
for &a in kept.keys() {
seen[a as usize] = true;
}
let mut edges: Vec<(u32, u32, u32, BondData)> = Vec::new();
while let Some(a) = stack.pop() {
for (other, bi) in mol.neighbors(a) {
let b = mol.bonds()[bi as usize];
if matched[other as usize] && !kept.contains_key(&other) {
continue;
}
if template_bonds[bi as usize] {
continue;
}
if !seen[other as usize] {
seen[other as usize] = true;
let mut carried = mol.atoms()[other as usize];
carried.atom_map = 0;
let idx = out.add_atom_data(carried);
kept.insert(other, idx);
stack.push(other);
}
edges.push((bi, a, other, b));
}
}
edges.sort_by_key(|&(bi, ..)| bi);
let mut done: std::collections::HashSet<(u32, u32)> = std::collections::HashSet::new();
for (_, a, b, src) in edges {
let key = if a <= b { (a, b) } else { (b, a) };
if !done.insert(key) {
continue;
}
let (Some(&na), Some(&nb)) = (kept.get(&src.begin), kept.get(&src.end)) else {
continue;
};
if out.bond_between(na, nb).is_some() {
continue;
}
let mut nb_data = BondData::new(na, nb, src.order);
nb_data.direction = src.direction;
nb_data.stereo = src.stereo;
nb_data.stereo_atoms = [BondData::NO_STEREO_ATOM; 2];
nb_data.flags = src.flags;
let _ = out.add_bond_data(nb_data);
}
}
fn rebase_bond_stereo(mol: &MolBuilder, kept: &BTreeMap<u32, u32>, out: &mut MolBuilder) {
for src in mol.bonds() {
if src.stereo == BondStereo::None
|| src.stereo_atoms[0] == BondData::NO_STEREO_ATOM
|| src.stereo_atoms[1] == BondData::NO_STEREO_ATOM
{
continue;
}
let (Some(&pb), Some(&pe)) = (kept.get(&src.begin), kept.get(&src.end)) else {
continue;
};
let Some(bi) = out.bond_between(pb, pe) else {
continue;
};
let cur = out.bonds()[bi as usize];
if cur.stereo == BondStereo::None || cur.stereo_atoms[0] != BondData::NO_STEREO_ATOM {
continue;
}
let mut refs = [BondData::NO_STEREO_ATOM; 2];
let mut flips = 0usize;
for (i, (end, other, p_end, p_other)) in
[(src.begin, src.end, pb, pe), (src.end, src.begin, pe, pb)]
.into_iter()
.enumerate()
{
let want = src.stereo_atoms[i];
let p_subs: Vec<u32> = out
.neighbors(p_end)
.map(|(o, _)| o)
.filter(|&o| o != p_other)
.collect();
if let Some(&p) = kept.get(&want) {
if p_subs.contains(&p) {
refs[i] = p;
continue;
}
}
let subs: Vec<u32> = mol
.neighbors(end)
.map(|(o, _)| o)
.filter(|&o| o != other)
.collect();
let Some(pos) = subs.iter().position(|&o| o == want) else {
break;
};
let slots: Vec<Option<u32>> = subs
.iter()
.map(|o| kept.get(o).copied().filter(|p| p_subs.contains(p)))
.collect();
let Some(filled) = fill_replaced_slots(&slots, &p_subs) else {
let Some(&alt) = subs.iter().find(|&&o| o != want) else {
break;
};
let Some(&p_alt) = kept.get(&alt) else {
break;
};
if !p_subs.contains(&p_alt) {
break;
}
refs[i] = p_alt;
flips += 1;
continue;
};
refs[i] = filled[pos];
}
if let Some(mut b) = out.bond_mut(bi) {
if refs[0] == BondData::NO_STEREO_ATOM || refs[1] == BondData::NO_STEREO_ATOM {
b.set_stereo(BondStereo::None);
} else if flips % 2 == 0 {
b.set_stereo_atoms(refs);
} else {
match src.stereo {
BondStereo::Cis => {
b.set_stereo(BondStereo::Trans);
b.set_stereo_atoms(refs);
}
BondStereo::Trans => {
b.set_stereo(BondStereo::Cis);
b.set_stereo_atoms(refs);
}
_ => b.set_stereo(BondStereo::None),
}
}
}
}
}
fn apply_template(
mut base: AtomData,
expr: &AtomExpr,
degree_kept: bool,
plan: ChiralityPlan,
) -> AtomData {
base.num_radical_electrons = 0;
let element_changed = template_element(expr).is_some_and(|z| z != base.atomic_num);
if element_changed || !degree_kept {
base.num_explicit_hs = 0;
base.num_implicit_hs = 0;
base.flags.remove(omgkit_core::AtomFlags::NO_IMPLICIT);
}
if element_changed {
base.formal_charge = 0;
base.isotope = 0;
base.chiral_tag = ChiralTag::Unspecified;
}
let inherited = base.chiral_tag;
apply_expr(&mut base, expr);
base.chiral_tag = match plan {
ChiralityPlan::Inherit | ChiralityPlan::Set => base.chiral_tag,
ChiralityPlan::Drop => ChiralTag::Unspecified,
ChiralityPlan::Retain => inherited,
ChiralityPlan::Invert => inherited.inverted(),
};
base.atom_map = 0;
base
}
fn template_element(expr: &AtomExpr) -> Option<u8> {
match expr {
AtomExpr::Prim(AtomPrim::Element { z, .. }) => Some(*z),
AtomExpr::And(parts) => parts.iter().find_map(template_element),
_ => None,
}
}
fn apply_expr(a: &mut AtomData, expr: &AtomExpr) {
match expr {
AtomExpr::Prim(p) => apply_prim(a, p),
AtomExpr::And(parts) => {
for p in parts {
apply_expr(a, p);
}
}
AtomExpr::Or(_) | AtomExpr::Not(_) => {}
}
}
fn apply_prim(a: &mut AtomData, p: &AtomPrim) {
match p {
AtomPrim::Element { z, aromatic } => {
a.atomic_num = *z;
if let Some(arom) = aromatic {
a.flags.set(omgkit_core::AtomFlags::AROMATIC, *arom);
}
}
AtomPrim::Charge(c) => a.formal_charge = i8::try_from(*c).unwrap_or(0),
AtomPrim::Isotope(i) => a.isotope = *i,
AtomPrim::TotalHs(n) => {
a.num_explicit_hs = u8::try_from(*n).unwrap_or(0);
a.num_implicit_hs = 0;
a.flags.insert(omgkit_core::AtomFlags::NO_IMPLICIT);
}
AtomPrim::Chirality(t) => a.chiral_tag = *t,
_ => {}
}
}
fn is_dative_reversed(expr: &BondExpr) -> bool {
match expr {
BondExpr::Prim(BondPrim::DativeReversed) => true,
BondExpr::And(parts) => parts.iter().any(is_dative_reversed),
_ => false,
}
}
fn bond_direction_from(expr: &BondExpr) -> BondDirection {
match expr {
BondExpr::Prim(BondPrim::UpRight) => BondDirection::UpRight,
BondExpr::Prim(BondPrim::DownRight) => BondDirection::DownRight,
BondExpr::And(parts) => parts
.iter()
.map(bond_direction_from)
.find(|d| *d != BondDirection::None)
.unwrap_or(BondDirection::None),
_ => BondDirection::None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ProductBond {
Fixed(BondOrder),
FollowAromaticity,
Inherit,
}
fn product_bond_from(expr: &BondExpr) -> ProductBond {
match expr {
BondExpr::Prim(BondPrim::Any) => ProductBond::Inherit,
BondExpr::Prim(p) => ProductBond::Fixed(match p {
BondPrim::Double => BondOrder::Double,
BondPrim::Triple => BondOrder::Triple,
BondPrim::Quadruple => BondOrder::Quadruple,
BondPrim::Aromatic => BondOrder::Aromatic,
BondPrim::Dative | BondPrim::DativeReversed => BondOrder::Dative,
_ => BondOrder::Single,
}),
BondExpr::And(parts) => parts
.iter()
.map(product_bond_from)
.find(|o| !matches!(o, ProductBond::Fixed(BondOrder::Single)))
.unwrap_or(ProductBond::Fixed(BondOrder::Single)),
BondExpr::Or(_) | BondExpr::Not(_) => {
if *expr == BondExpr::default_bond() {
return ProductBond::FollowAromaticity;
}
let parts = match expr {
BondExpr::Or(parts) => parts.as_slice(),
_ => &[],
};
parts
.iter()
.map(product_bond_from)
.find(|o| matches!(o, ProductBond::Fixed(_)))
.unwrap_or(ProductBond::FollowAromaticity)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn template_order_parity_gives_up_when_the_correspondence_is_not_unique() {
let n = |v: &[u16]| -> Vec<Option<u16>> { v.iter().map(|&x| Some(x)).collect() };
assert_eq!(
template_order_is_odd(&n(&[2, 3, 4]), &n(&[2, 3, 4])),
Some(false)
);
assert_eq!(
template_order_is_odd(&n(&[2, 3, 4]), &n(&[4, 3, 2])),
Some(true)
);
assert_eq!(
template_order_is_odd(&n(&[2, 3, 4]), &n(&[3, 4, 2])),
Some(false)
);
let mut react = n(&[2, 3, 4]);
react[0] = None;
let mut prod = n(&[2, 3, 4]);
prod[2] = None;
assert!(
template_order_is_odd(&react, &prod).is_some(),
"各有一个对不上时该顶替得起来"
);
assert_eq!(
template_order_is_odd(&n(&[2, 3, 4]), &n(&[5, 6, 4])),
None,
"产物侧有两个邻居在反应物侧找不到,对应关系不唯一"
);
assert_eq!(template_order_is_odd(&n(&[2, 3]), &n(&[2, 3])), None);
assert_eq!(
template_order_is_odd(&n(&[2, 3, 4]), &n(&[2, 3, 4, 5, 6])),
None
);
}
}