1use std::collections::{BTreeMap, BTreeSet};
47
48use omgkit_core::{
49 AtomData, BondData, BondDirection, BondOrder, BondStereo, ChiralTag, MolBuilder,
50};
51use omgkit_io::smarts::{
52 map_number, required_chirality, AtomExpr, AtomPrim, BondExpr, BondPrim, QueryMol, Reaction,
53};
54
55use crate::matcher::{substructure_matches, MatchOptions};
56use crate::props::MolProps;
57
58pub type ProductSet = Vec<MolBuilder>;
64
65#[derive(Debug, Clone)]
67pub struct Outcome {
68 pub products: ProductSet,
70 pub reactants: Vec<MolBuilder>,
76 pub discarded: Vec<Vec<u32>>,
86}
87
88#[must_use]
201pub fn run_reactants(
202 reaction: &Reaction,
203 reactants: &[(MolBuilder, MolProps)],
204 max_products: usize,
205 atom_mapping: bool,
206) -> Vec<Outcome> {
207 debug_assert!(
208 !reactants
209 .iter()
210 .any(|(m, _)| omgkit_io::stereo::directions_not_perceived(m)),
211 "反应物里有双键的几何**方向键已经写明**、却没有感知过顺反 —— \
212 漏了 omgkit_io::stereo::perceive_bond_stereo。这样跑不会报错,\
213 但反应一旦删掉承载方向的那根单键,几何会静默丢失"
214 );
215 if reactants.len() != reaction.reactants.len() || reaction.products.is_empty() {
216 return Vec::new();
217 }
218
219 let opts = MatchOptions {
221 max_matches: 0,
222 uniquify: false,
223 use_chirality: false,
229 };
230 let n = reaction.reactants.len();
231 let per_template: Vec<Vec<Vec<u32>>> = reaction
232 .reactants
233 .iter()
234 .zip(reactants)
235 .map(|(t, (mol, props))| substructure_matches(t, mol, props, opts))
236 .collect();
237 let comps: Vec<Vec<u32>> = reactants.iter().map(|(m, _)| components(m)).collect();
239
240 if per_template.iter().all(|m| !m.is_empty()) {
243 let identity: Vec<usize> = (0..n).collect();
244 let out = outcomes_under(
245 reaction,
246 reactants,
247 &per_template,
248 &identity,
249 &comps,
250 max_products,
251 atom_mapping,
252 );
253 if !out.is_empty() {
254 return out;
255 }
256 }
257
258 let mut table: Vec<Vec<Vec<Vec<u32>>>> = Vec::with_capacity(n);
261 for (t, tpl) in reaction.reactants.iter().enumerate() {
262 let mut row = Vec::with_capacity(n);
263 for (m, (mol, props)) in reactants.iter().enumerate() {
264 row.push(if m == t {
265 per_template[t].clone()
266 } else {
267 substructure_matches(tpl, mol, props, opts)
268 });
269 }
270 table.push(row);
271 }
272 let mut assign = vec![0usize; n];
273 let mut used = vec![false; n];
274 search_assignment(
275 reaction,
276 reactants,
277 &table,
278 &comps,
279 max_products,
280 atom_mapping,
281 0,
282 &mut assign,
283 &mut used,
284 )
285 .unwrap_or_default()
286}
287
288fn outcomes_under(
293 reaction: &Reaction,
294 reactants: &[(MolBuilder, MolProps)],
295 per_template: &[Vec<Vec<u32>>],
296 assign: &[usize],
297 comps: &[Vec<u32>],
298 max_products: usize,
299 atom_mapping: bool,
300) -> Vec<Outcome> {
301 let mut out = Vec::new();
302 let mut combo: Vec<usize> = vec![0; per_template.len()];
303 loop {
304 let mapping: Vec<&Vec<u32>> = combo
305 .iter()
306 .enumerate()
307 .map(|(i, &j)| &per_template[i][j])
308 .collect();
309 let built = build_products(reaction, reactants, &mapping, assign, comps);
310 out.push(stamp_atom_maps(reactants, built, atom_mapping));
311 if max_products != 0 && out.len() >= max_products {
312 return out;
313 }
314 let mut i = 0;
316 loop {
317 if i == combo.len() {
318 return out;
319 }
320 combo[i] += 1;
321 if combo[i] < per_template[i].len() {
322 break;
323 }
324 combo[i] = 0;
325 i += 1;
326 }
327 }
328}
329
330#[allow(clippy::too_many_arguments)]
335fn search_assignment(
336 reaction: &Reaction,
337 reactants: &[(MolBuilder, MolProps)],
338 table: &[Vec<Vec<Vec<u32>>>],
339 comps: &[Vec<u32>],
340 max_products: usize,
341 atom_mapping: bool,
342 depth: usize,
343 assign: &mut Vec<usize>,
344 used: &mut Vec<bool>,
345) -> Option<Vec<Outcome>> {
346 if depth == assign.len() {
347 let per: Vec<Vec<Vec<u32>>> = assign
348 .iter()
349 .enumerate()
350 .map(|(t, &m)| table[t][m].clone())
351 .collect();
352 let out = outcomes_under(
353 reaction,
354 reactants,
355 &per,
356 assign,
357 comps,
358 max_products,
359 atom_mapping,
360 );
361 return if out.is_empty() { None } else { Some(out) };
362 }
363 for m in 0..used.len() {
364 if used[m] || table[depth][m].is_empty() {
365 continue;
366 }
367 used[m] = true;
368 assign[depth] = m;
369 if let Some(out) = search_assignment(
370 reaction,
371 reactants,
372 table,
373 comps,
374 max_products,
375 atom_mapping,
376 depth + 1,
377 assign,
378 used,
379 ) {
380 return Some(out);
381 }
382 used[m] = false;
383 }
384 None
385}
386
387fn concat(mols: &[(MolBuilder, MolProps)]) -> MolBuilder {
394 let n_atoms = mols.iter().map(|(m, _)| m.num_atoms()).sum();
395 let n_bonds = mols.iter().map(|(m, _)| m.num_bonds()).sum();
396 let mut out = MolBuilder::with_capacity(n_atoms, n_bonds);
397 for (m, _) in mols {
398 let base = u32::try_from(out.num_atoms()).unwrap_or(u32::MAX);
399 for a in m.atoms() {
400 out.add_atom_data(*a);
401 }
402 for b in m.bonds() {
403 let mut nb = *b;
404 nb.begin += base;
405 nb.end += base;
406 for s in &mut nb.stereo_atoms {
407 if *s != BondData::NO_STEREO_ATOM {
408 *s += base;
409 }
410 }
411 let _ = out.add_bond_data(nb);
412 }
413 }
414 out
415}
416
417#[must_use]
447pub fn run_on_substrate(
448 reaction: &Reaction,
449 substrate: &[(MolBuilder, MolProps)],
450 max_products: usize,
451 atom_mapping: bool,
452) -> Vec<Outcome> {
453 debug_assert!(
454 !substrate
455 .iter()
456 .any(|(m, _)| omgkit_io::stereo::directions_not_perceived(m)),
457 "底物里有双键的几何**方向键已经写明**、却没有感知过顺反 —— \
458 漏了 omgkit_io::stereo::perceive_bond_stereo。理由见 run_reactants"
459 );
460 if substrate.is_empty() || reaction.reactants.is_empty() || reaction.products.is_empty() {
461 return Vec::new();
462 }
463
464 let sizes: Vec<usize> = substrate.iter().map(|(m, _)| m.num_atoms()).collect();
467 let mol = concat(substrate);
468 let props = MolProps::compute(&mol);
469 let inputs = [(mol, props)];
470
471 let opts = MatchOptions {
473 max_matches: 0,
474 uniquify: false,
475 use_chirality: false,
476 };
477 let per_template: Vec<Vec<Vec<u32>>> = reaction
478 .reactants
479 .iter()
480 .map(|t| substructure_matches(t, &inputs[0].0, &inputs[0].1, opts))
481 .collect();
482 if per_template.iter().any(Vec::is_empty) {
483 return Vec::new();
484 }
485
486 let home = vec![0usize; reaction.reactants.len()];
488 let n_atoms = inputs[0].0.num_atoms();
489 let comps: Vec<Vec<u32>> = inputs.iter().map(|(m, _)| components(m)).collect();
490
491 let mut out = Vec::new();
492 let mut combo: Vec<usize> = vec![0; per_template.len()];
493 let mut used = vec![false; n_atoms];
494 loop {
495 let mapping: Vec<&Vec<u32>> = combo
496 .iter()
497 .enumerate()
498 .map(|(i, &j)| &per_template[i][j])
499 .collect();
500 used.iter_mut().for_each(|u| *u = false);
503 let disjoint = mapping.iter().all(|m| {
504 m.iter().all(|&a| {
505 let fresh = !used[a as usize];
506 used[a as usize] = true;
507 fresh
508 })
509 });
510 if disjoint {
511 let built = build_products(reaction, &inputs, &mapping, &home, &comps);
512 let mut outcome = stamp_atom_maps(&inputs, built, atom_mapping);
513 outcome.discarded = regroup_discarded(&outcome.discarded, &sizes);
514 out.push(outcome);
515 if max_products != 0 && out.len() >= max_products {
516 break;
517 }
518 }
519 let mut i = 0;
521 loop {
522 if i == combo.len() {
523 return out;
524 }
525 combo[i] += 1;
526 if combo[i] < per_template[i].len() {
527 break;
528 }
529 combo[i] = 0;
530 i += 1;
531 }
532 }
533 out
534}
535
536type Anchor = (usize, u32);
538
539struct ReactantFacts {
541 anchors: BTreeMap<u16, Anchor>,
543 degree: BTreeMap<u16, usize>,
546 chirality: BTreeMap<u16, Option<ChiralTag>>,
549 neighbors: BTreeMap<u16, Vec<Option<u16>>>,
553}
554
555fn neighbor_maps(template: &QueryMol, qi: u32) -> Vec<Option<u16>> {
557 template
558 .topology
559 .neighbors(qi)
560 .map(|(other, _)| map_number(&template.atoms[other as usize]))
561 .collect()
562}
563
564fn template_order_is_odd(react: &[Option<u16>], prod: &[Option<u16>]) -> Option<bool> {
583 if react.len() < 3 || prod.len() < 3 || react.len().abs_diff(prod.len()) > 1 {
585 return None;
586 }
587 let mut r: Vec<Option<u16>> = react.to_vec();
589 let mut p: Vec<Option<u16>> = prod.to_vec();
590 if r.len() < p.len() {
591 r.push(None);
592 } else if p.len() < r.len() {
593 p.push(None);
594 }
595 if r.iter().filter(|x| x.is_none()).count() > 1 || p.iter().filter(|x| x.is_none()).count() > 1
596 {
597 return None;
598 }
599 fill_missing(&mut r, &p)?;
600 fill_missing(&mut p, &r)?;
601 let enc = |v: &[Option<u16>]| -> Vec<u32> {
602 v.iter().map(|x| x.map_or(u32::MAX, u32::from)).collect()
603 };
604 omgkit_core::permutation_is_odd(&enc(&r), &enc(&p))
605}
606
607fn fill_missing(have: &mut [Option<u16>], want: &[Option<u16>]) -> Option<()> {
611 for &elem in want.iter().flatten() {
612 if have.contains(&Some(elem)) {
613 continue;
614 }
615 let slot = have.iter().position(Option::is_none)?;
616 have[slot] = Some(elem);
617 }
618 Some(())
619}
620
621#[derive(Clone, Copy, PartialEq, Eq)]
637enum ChiralityPlan {
638 Inherit,
640 Drop,
642 Set,
644 Retain,
646 Invert,
648}
649
650impl ChiralityPlan {
651 fn decide(
654 reactant: Option<ChiralTag>,
655 product: Option<ChiralTag>,
656 order_is_odd: Option<bool>,
657 ) -> Self {
658 match (reactant, product) {
659 (None, None) => Self::Inherit,
660 (Some(_), None) => Self::Drop,
661 (None, Some(_)) => Self::Set,
662 (Some(r), Some(p)) => {
665 if (r == p) != order_is_odd.unwrap_or(false) {
666 Self::Retain
667 } else {
668 Self::Invert
669 }
670 }
671 }
672 }
673}
674
675type BuiltProduct = (MolBuilder, Vec<BTreeMap<u32, u32>>);
680
681pub(crate) fn components(mol: &MolBuilder) -> Vec<u32> {
692 let n = mol.num_atoms();
693 let mut comp = vec![u32::MAX; n];
694 let mut stack: Vec<u32> = Vec::new();
695 let mut next = 0u32;
696 for s in 0..n as u32 {
697 if comp[s as usize] != u32::MAX {
698 continue;
699 }
700 comp[s as usize] = next;
701 stack.push(s);
702 while let Some(a) = stack.pop() {
703 for (other, _) in mol.neighbors(a) {
704 if comp[other as usize] == u32::MAX {
705 comp[other as usize] = next;
706 stack.push(other);
707 }
708 }
709 }
710 next += 1;
711 }
712 comp
713}
714
715fn build_products(
716 reaction: &Reaction,
717 reactants: &[(MolBuilder, MolProps)],
718 matches: &[&Vec<u32>],
719 home: &[usize],
720 comps: &[Vec<u32>],
721) -> Vec<BuiltProduct> {
722 let mut matched: Vec<Vec<bool>> = reactants
724 .iter()
725 .map(|(m, _)| vec![false; m.num_atoms()])
726 .collect();
727 let mut template_bonds: Vec<Vec<bool>> = reactants
731 .iter()
732 .map(|(m, _)| vec![false; m.num_bonds()])
733 .collect();
734
735 let mut facts = ReactantFacts {
736 anchors: BTreeMap::new(),
737 degree: BTreeMap::new(),
738 chirality: BTreeMap::new(),
739 neighbors: BTreeMap::new(),
740 };
741
742 for (ti, template) in reaction.reactants.iter().enumerate() {
743 let ri = home[ti];
746 for qb in template.topology.bonds() {
747 let (a, b) = (matches[ti][qb.begin as usize], matches[ti][qb.end as usize]);
748 if let Some(bi) = reactants[ri].0.bond_between(a, b) {
749 template_bonds[ri][bi as usize] = true;
750 }
751 }
752 for (qi, &target) in matches[ti].iter().enumerate() {
753 matched[ri][target as usize] = true;
754 if let Some(n) = map_number(&template.atoms[qi]) {
755 facts.anchors.entry(n).or_insert((ri, target));
756 facts
757 .degree
758 .entry(n)
759 .or_insert_with(|| template.topology.degree(qi as u32));
760 facts
761 .chirality
762 .entry(n)
763 .or_insert_with(|| required_chirality(&template.atoms[qi]));
764 facts
765 .neighbors
766 .entry(n)
767 .or_insert_with(|| neighbor_maps(template, qi as u32));
768 }
769 }
770 }
771
772 let mut out = MolBuilder::new();
778 let mut from_reactant: Vec<BTreeMap<u32, u32>> =
779 reactants.iter().map(|_| BTreeMap::new()).collect();
780 let mut settled_chirality: BTreeSet<u32> = BTreeSet::new();
782
783 for pt in &reaction.products {
784 emit_template(
785 pt,
786 reactants,
787 &facts,
788 &mut out,
789 &mut from_reactant,
790 &mut settled_chirality,
791 );
792 }
793
794 for (ti, (mol, _)) in reactants.iter().enumerate() {
799 seed_spectators(
800 mol,
801 &comps[ti],
802 &matched[ti],
803 &mut from_reactant[ti],
804 &mut out,
805 );
806 carry_over(
807 mol,
808 &matched[ti],
809 &template_bonds[ti],
810 &mut from_reactant[ti],
811 &mut out,
812 );
813 }
814 for (ti, (mol, _)) in reactants.iter().enumerate() {
816 rebase_chirality(mol, &from_reactant[ti], &settled_chirality, &mut out);
817 rebase_bond_stereo(mol, &from_reactant[ti], &mut out);
818 }
819
820 split_components(&out, &from_reactant)
821}
822
823fn honoured_directions(template: &QueryMol) -> Vec<bool> {
849 let bonds = template.topology.bonds();
850 let has_dir: Vec<bool> = template
851 .bonds
852 .iter()
853 .map(|e| bond_direction_from(e) != BondDirection::None)
854 .collect();
855 let flanked = |atom: u32, skip: usize| {
857 template
858 .topology
859 .neighbors(atom)
860 .any(|(_, bi)| bi as usize != skip && has_dir[bi as usize])
861 };
862 let determined: Vec<bool> = (0..bonds.len())
863 .map(|bi| {
864 product_bond_from(&template.bonds[bi]) == ProductBond::Fixed(BondOrder::Double)
865 && flanked(bonds[bi].begin, bi)
866 && flanked(bonds[bi].end, bi)
867 })
868 .collect();
869 (0..bonds.len())
870 .map(|bi| {
871 has_dir[bi]
872 && [bonds[bi].begin, bonds[bi].end].iter().any(|&a| {
873 template
874 .topology
875 .neighbors(a)
876 .any(|(_, ob)| ob as usize != bi && determined[ob as usize])
877 })
878 })
879 .collect()
880}
881
882fn emit_template(
884 template: &QueryMol,
885 reactants: &[(MolBuilder, MolProps)],
886 facts: &ReactantFacts,
887 out: &mut MolBuilder,
888 from_reactant: &mut [BTreeMap<u32, u32>],
889 settled_chirality: &mut BTreeSet<u32>,
890) {
891 let mut from_template: Vec<u32> = Vec::with_capacity(template.num_atoms());
892 let mut anchor_of: Vec<Option<Anchor>> = Vec::with_capacity(template.num_atoms());
893
894 for (qi, expr) in template.atoms.iter().enumerate() {
896 let anchor = map_number(expr)
897 .and_then(|n| facts.anchors.get(&n))
898 .copied();
899 anchor_of.push(anchor);
900 let base = match anchor {
901 Some((ti, ai)) => reactants[ti].0.atoms()[ai as usize],
903 None => AtomData::new(0),
905 };
906 let degree_kept = map_number(expr)
908 .and_then(|n| facts.degree.get(&n).copied())
909 .is_some_and(|d| d == template.topology.degree(qi as u32));
910 let plan = ChiralityPlan::decide(
911 map_number(expr)
912 .and_then(|n| facts.chirality.get(&n).copied())
913 .flatten(),
914 required_chirality(expr),
915 map_number(expr)
916 .and_then(|n| facts.neighbors.get(&n))
917 .and_then(|r| template_order_is_odd(r, &neighbor_maps(template, qi as u32))),
918 );
919 let idx = out.add_atom_data(apply_template(base, expr, degree_kept, plan));
920 if plan == ChiralityPlan::Set {
926 settled_chirality.insert(idx);
927 }
928 from_template.push(idx);
929 if let Some((ti, ai)) = anchor {
930 from_reactant[ti].insert(ai, idx);
931 }
932 }
933
934 let honoured = honoured_directions(template);
936 for (bi, expr) in template.bonds.iter().enumerate() {
937 let b = template.topology.bonds()[bi];
938 let order = match product_bond_from(expr) {
941 ProductBond::Fixed(o) => o,
942 ProductBond::FollowAromaticity => {
943 let aromatic = |ti: u32| {
944 out.atoms()[from_template[ti as usize] as usize]
945 .flags
946 .contains(omgkit_core::AtomFlags::AROMATIC)
947 };
948 if aromatic(b.begin) && aromatic(b.end) {
949 BondOrder::Aromatic
950 } else {
951 BondOrder::Single
952 }
953 }
954 ProductBond::Inherit => {
955 match (anchor_of[b.begin as usize], anchor_of[b.end as usize]) {
956 (Some((t1, a1)), Some((t2, a2))) if t1 == t2 => {
957 inherited_order(&reactants[t1].0, a1, a2)
958 }
959 _ => BondOrder::Unspecified,
961 }
962 }
963 };
964 let (tb, te) = if is_dative_reversed(expr) {
971 (b.end, b.begin)
972 } else {
973 (b.begin, b.end)
974 };
975 let mut bd = BondData::new(
976 from_template[tb as usize],
977 from_template[te as usize],
978 order,
979 );
980 bd.flags.set(
982 omgkit_core::BondFlags::AROMATIC,
983 order == BondOrder::Aromatic,
984 );
985 let from_template_dir = if honoured[bi] {
1002 bond_direction_from(expr)
1003 } else {
1004 BondDirection::None
1005 };
1006 bd.direction = if from_template_dir != BondDirection::None {
1007 from_template_dir
1008 } else if let (Some((t1, a1)), Some((t2, a2))) =
1009 (anchor_of[b.begin as usize], anchor_of[b.end as usize])
1010 {
1011 if t1 == t2 {
1012 inherited_direction(&reactants[t1].0, a1, a2)
1013 } else {
1014 BondDirection::None
1015 }
1016 } else {
1017 BondDirection::None
1018 };
1019 let _ = out.add_bond_data(bd);
1020 }
1021}
1022
1023fn split_components(
1036 shared: &MolBuilder,
1037 from_reactant: &[BTreeMap<u32, u32>],
1038) -> Vec<BuiltProduct> {
1039 let n = shared.num_atoms();
1040 let mut comp = vec![usize::MAX; n];
1041 let mut n_comp = 0usize;
1042 let mut stack: Vec<u32> = Vec::new();
1043 for s in 0..n as u32 {
1044 if comp[s as usize] != usize::MAX {
1045 continue;
1046 }
1047 comp[s as usize] = n_comp;
1048 stack.push(s);
1049 while let Some(a) = stack.pop() {
1050 for (other, _) in shared.neighbors(a) {
1051 if comp[other as usize] == usize::MAX {
1052 comp[other as usize] = n_comp;
1053 stack.push(other);
1054 }
1055 }
1056 }
1057 n_comp += 1;
1058 }
1059
1060 let mut mols: Vec<MolBuilder> = (0..n_comp).map(|_| MolBuilder::new()).collect();
1061 let mut local = vec![u32::MAX; n];
1063 for a in 0..n as u32 {
1064 let c = comp[a as usize];
1065 local[a as usize] = mols[c].add_atom_data(shared.atoms()[a as usize]);
1066 }
1067 for b in shared.bonds() {
1068 let c = comp[b.begin as usize];
1069 let mut nb = *b;
1070 nb.begin = local[b.begin as usize];
1071 nb.end = local[b.end as usize];
1072 nb.stereo_atoms = [
1073 translate_stereo_atom(b.stereo_atoms[0], &local),
1074 translate_stereo_atom(b.stereo_atoms[1], &local),
1075 ];
1076 let _ = mols[c].add_bond_data(nb);
1077 }
1078
1079 let mut tables: Vec<Vec<BTreeMap<u32, u32>>> = (0..n_comp)
1081 .map(|_| from_reactant.iter().map(|_| BTreeMap::new()).collect())
1082 .collect();
1083 for (ti, table) in from_reactant.iter().enumerate() {
1084 for (&src, &dst) in table {
1085 let c = comp[dst as usize];
1086 tables[c][ti].insert(src, local[dst as usize]);
1087 }
1088 }
1089
1090 mols.into_iter().zip(tables).collect()
1091}
1092
1093fn translate_stereo_atom(idx: u32, local: &[u32]) -> u32 {
1095 if idx == BondData::NO_STEREO_ATOM {
1096 return BondData::NO_STEREO_ATOM;
1097 }
1098 local
1099 .get(idx as usize)
1100 .copied()
1101 .filter(|&v| v != u32::MAX)
1102 .unwrap_or(BondData::NO_STEREO_ATOM)
1103}
1104
1105fn stamp_atom_maps(
1117 reactants: &[(MolBuilder, MolProps)],
1118 built: Vec<BuiltProduct>,
1119 atom_mapping: bool,
1120) -> Outcome {
1121 let discarded = discarded_atoms(reactants, &built);
1122 if !atom_mapping {
1123 return Outcome {
1124 products: built.into_iter().map(|(m, _)| m).collect(),
1125 reactants: Vec::new(),
1126 discarded,
1127 };
1128 }
1129
1130 let mut products: ProductSet = Vec::with_capacity(built.len());
1131 let mut first_home: BTreeMap<(usize, u32), (usize, u32)> = BTreeMap::new();
1134 for (pi, (mol, per_reactant)) in built.into_iter().enumerate() {
1135 for (ti, table) in per_reactant.iter().enumerate() {
1136 for (&src, &dst) in table {
1137 first_home.entry((ti, src)).or_insert((pi, dst));
1138 }
1139 }
1140 products.push(mol);
1141 }
1142
1143 let mut mapped: Vec<MolBuilder> = reactants.iter().map(|(m, _)| m.clone()).collect();
1144 for m in &mut mapped {
1145 for i in 0..m.num_atoms() as u32 {
1146 if let Some(a) = m.atom_mut(i) {
1147 a.atom_map = 0;
1148 }
1149 }
1150 }
1151
1152 let mut next: u32 = 1;
1153 for (&(ti, src), &(pi, dst)) in &first_home {
1154 let Ok(n) = u16::try_from(next) else { break };
1157 if mapped[ti].atoms().get(src as usize).is_none()
1159 || products[pi].atoms().get(dst as usize).is_none()
1160 {
1161 continue;
1162 }
1163 if let Some(a) = mapped[ti].atom_mut(src) {
1164 a.atom_map = n;
1165 }
1166 if let Some(a) = products[pi].atom_mut(dst) {
1167 a.atom_map = n;
1168 }
1169 next += 1;
1170 }
1171
1172 Outcome {
1173 products,
1174 reactants: mapped,
1175 discarded,
1176 }
1177}
1178
1179fn regroup_discarded(flat: &[Vec<u32>], sizes: &[usize]) -> Vec<Vec<u32>> {
1189 let mut out: Vec<Vec<u32>> = sizes.iter().map(|_| Vec::new()).collect();
1190 for a in flat.iter().flatten() {
1191 let mut rest = *a as usize;
1192 for (i, &n) in sizes.iter().enumerate() {
1193 if rest < n {
1194 out[i].push(u32::try_from(rest).unwrap_or(u32::MAX));
1195 break;
1196 }
1197 rest -= n;
1198 }
1199 }
1200 out
1201}
1202
1203fn discarded_atoms(reactants: &[(MolBuilder, MolProps)], built: &[BuiltProduct]) -> Vec<Vec<u32>> {
1208 let mut kept: Vec<Vec<bool>> = reactants
1209 .iter()
1210 .map(|(m, _)| vec![false; m.num_atoms()])
1211 .collect();
1212 for (_, per_reactant) in built {
1213 for (ti, table) in per_reactant.iter().enumerate() {
1214 for &src in table.keys() {
1215 if let Some(slot) = kept[ti].get_mut(src as usize) {
1216 *slot = true;
1217 }
1218 }
1219 }
1220 }
1221 kept.iter()
1222 .map(|flags| {
1223 flags
1224 .iter()
1225 .enumerate()
1226 .filter(|&(_, &k)| !k)
1227 .map(|(i, _)| u32::try_from(i).unwrap_or(u32::MAX))
1228 .collect()
1229 })
1230 .collect()
1231}
1232
1233fn rebase_chirality(
1274 mol: &MolBuilder,
1275 kept: &BTreeMap<u32, u32>,
1276 settled_chirality: &BTreeSet<u32>,
1277 out: &mut MolBuilder,
1278) {
1279 for (&src, &dst) in kept {
1280 if settled_chirality.contains(&dst) {
1281 continue;
1282 }
1283 let tag = out.atoms()[dst as usize].chiral_tag;
1284 if tag == ChiralTag::Unspecified {
1285 continue;
1286 }
1287 let after: Vec<u32> = out.neighbors(dst).map(|(other, _)| other).collect();
1288 if !tag.is_tetrahedral() {
1289 rebase_coordination(mol, src, dst, kept, &after, out);
1290 continue;
1291 }
1292 let slots: Vec<Option<u32>> = mol
1301 .neighbors(src)
1302 .map(|(other, _)| kept.get(&other).copied().filter(|p| after.contains(p)))
1303 .collect();
1304 let Some((before, after)) = align_for_rebase(&slots, &after) else {
1305 continue;
1306 };
1307 if omgkit_core::permutation_is_odd(&before, &after) == Some(true) {
1308 if let Some(a) = out.atom_mut(dst) {
1309 a.chiral_tag = tag.inverted();
1310 }
1311 }
1312 }
1313}
1314
1315fn rebase_coordination(
1320 mol: &MolBuilder,
1321 src: u32,
1322 dst: u32,
1323 kept: &BTreeMap<u32, u32>,
1324 after: &[u32],
1325 out: &mut MolBuilder,
1326) {
1327 let tag = out.atoms()[dst as usize].chiral_tag;
1328 let perm = out.atoms()[dst as usize].stereo_perm;
1329 let before: Vec<u32> = mol
1330 .neighbors(src)
1331 .filter_map(|(other, _)| kept.get(&other).copied())
1332 .filter(|p| after.contains(p))
1333 .collect();
1334 let renumbered = if perm == 0 || before.len() != after.len() {
1335 None
1336 } else {
1337 omgkit_core::polyhedron::renumber(tag, perm, &before, after)
1338 };
1339 if let Some(a) = out.atom_mut(dst) {
1340 match renumbered {
1341 Some(p) => a.stereo_perm = p,
1342 None => {
1343 a.stereo_perm = 0;
1344 a.chiral_tag = ChiralTag::Unspecified;
1345 }
1346 }
1347 }
1348}
1349
1350pub(crate) const IMPLICIT_H: u32 = u32::MAX;
1353
1354pub(crate) fn align_for_rebase(
1384 slots: &[Option<u32>],
1385 after: &[u32],
1386) -> Option<(Vec<u32>, Vec<u32>)> {
1387 if let Some(before) = fill_replaced_slots(slots, after) {
1388 if before.len() == after.len() {
1389 return Some((before, after.to_vec()));
1390 }
1391 }
1392 let vacated = slots.iter().filter(|s| s.is_none()).count();
1393 let occupied = slots.len() - vacated;
1394 if vacated == 1 && occupied == after.len() && slots.len() == 4 {
1395 let before: Vec<u32> = slots.iter().map(|s| s.unwrap_or(IMPLICIT_H)).collect();
1397 let mut aligned = after.to_vec();
1398 aligned.insert(1, IMPLICIT_H);
1399 return Some((before, aligned));
1400 }
1401 if vacated == 0 && after.len() == slots.len() + 1 && after.len() == 4 {
1402 let taken: BTreeSet<u32> = slots.iter().flatten().copied().collect();
1404 let mut fresh = after.iter().filter(|a| !taken.contains(a));
1405 let new = *fresh.next()?;
1406 if fresh.next().is_some() {
1407 return None;
1408 }
1409 let mut before: Vec<u32> = slots.iter().flatten().copied().collect();
1410 before.insert(1, new);
1411 return Some((before, after.to_vec()));
1412 }
1413 None
1414}
1415
1416fn fill_replaced_slots(slots: &[Option<u32>], after: &[u32]) -> Option<Vec<u32>> {
1446 if slots.iter().all(Option::is_some) {
1447 return Some(slots.iter().flatten().copied().collect());
1448 }
1449 let taken: BTreeSet<u32> = slots.iter().flatten().copied().collect();
1454 let mut fresh = after.iter().filter(|a| !taken.contains(a));
1455 let filled: Option<Vec<u32>> = slots
1456 .iter()
1457 .map(|s| match s {
1458 Some(x) => Some(*x),
1459 None => fresh.next().copied(),
1460 })
1461 .collect();
1462 let filled = filled?;
1463 if fresh.next().is_some() {
1465 return None;
1466 }
1467 Some(filled)
1468}
1469
1470fn inherited_order(mol: &MolBuilder, a: u32, b: u32) -> BondOrder {
1476 mol.neighbors(a)
1477 .find(|&(other, _)| other == b)
1478 .map_or(BondOrder::Unspecified, |(_, bi)| {
1479 mol.bonds()[bi as usize].order
1480 })
1481}
1482
1483fn inherited_direction(mol: &MolBuilder, a: u32, b: u32) -> BondDirection {
1484 let Some((_, bi)) = mol.neighbors(a).find(|&(other, _)| other == b) else {
1485 return BondDirection::None;
1486 };
1487 let src = mol.bonds()[bi as usize];
1488 if src.begin == a {
1489 src.direction
1490 } else {
1491 src.direction.flipped()
1492 }
1493}
1494
1495fn seed_spectators(
1518 mol: &MolBuilder,
1519 comp: &[u32],
1520 matched: &[bool],
1521 kept: &mut BTreeMap<u32, u32>,
1522 out: &mut MolBuilder,
1523) {
1524 let Some(&n_comp) = comp.iter().max() else {
1527 return;
1528 };
1529 if n_comp == 0 {
1530 return;
1531 }
1532 let n_comp = n_comp as usize + 1;
1533
1534 let mut has_match = vec![false; n_comp];
1536 for (a, &hit) in matched.iter().enumerate() {
1537 if hit {
1538 has_match[comp[a] as usize] = true;
1539 }
1540 }
1541 let mut seeded = vec![false; n_comp];
1544 for (a, &c) in comp.iter().enumerate() {
1545 let c = c as usize;
1546 if has_match[c] || seeded[c] {
1547 continue;
1548 }
1549 seeded[c] = true;
1550 let mut carried = mol.atoms()[a];
1551 carried.atom_map = 0;
1552 let idx = out.add_atom_data(carried);
1553 kept.insert(a as u32, idx);
1554 }
1555}
1556
1557fn carry_over(
1558 mol: &MolBuilder,
1559 matched: &[bool],
1560 template_bonds: &[bool],
1561 kept: &mut BTreeMap<u32, u32>,
1562 out: &mut MolBuilder,
1563) {
1564 let mut stack: Vec<u32> = kept.keys().copied().collect();
1566 let mut seen: Vec<bool> = vec![false; mol.num_atoms()];
1567 for &a in kept.keys() {
1568 seen[a as usize] = true;
1569 }
1570 let mut edges: Vec<(u32, u32, u32, BondData)> = Vec::new();
1578
1579 while let Some(a) = stack.pop() {
1580 for (other, bi) in mol.neighbors(a) {
1581 let b = mol.bonds()[bi as usize];
1582 if matched[other as usize] && !kept.contains_key(&other) {
1590 continue;
1591 }
1592 if template_bonds[bi as usize] {
1607 continue;
1608 }
1609 if !seen[other as usize] {
1610 seen[other as usize] = true;
1611 let mut carried = mol.atoms()[other as usize];
1613 carried.atom_map = 0;
1614 let idx = out.add_atom_data(carried);
1615 kept.insert(other, idx);
1616 stack.push(other);
1617 }
1618 edges.push((bi, a, other, b));
1619 }
1620 }
1621 edges.sort_by_key(|&(bi, ..)| bi);
1622
1623 let mut done: std::collections::HashSet<(u32, u32)> = std::collections::HashSet::new();
1624 for (_, a, b, src) in edges {
1625 let key = if a <= b { (a, b) } else { (b, a) };
1626 if !done.insert(key) {
1627 continue;
1628 }
1629 let (Some(&na), Some(&nb)) = (kept.get(&src.begin), kept.get(&src.end)) else {
1640 continue;
1641 };
1642 if out.bond_between(na, nb).is_some() {
1646 continue;
1647 }
1648 let mut nb_data = BondData::new(na, nb, src.order);
1652 nb_data.direction = src.direction;
1653 nb_data.stereo = src.stereo;
1656 nb_data.stereo_atoms = [BondData::NO_STEREO_ATOM; 2];
1657 nb_data.flags = src.flags;
1658 let _ = out.add_bond_data(nb_data);
1659 }
1660}
1661
1662fn rebase_bond_stereo(mol: &MolBuilder, kept: &BTreeMap<u32, u32>, out: &mut MolBuilder) {
1689 for src in mol.bonds() {
1690 if src.stereo == BondStereo::None
1691 || src.stereo_atoms[0] == BondData::NO_STEREO_ATOM
1692 || src.stereo_atoms[1] == BondData::NO_STEREO_ATOM
1693 {
1694 continue;
1695 }
1696 let (Some(&pb), Some(&pe)) = (kept.get(&src.begin), kept.get(&src.end)) else {
1697 continue;
1698 };
1699 let Some(bi) = out.bond_between(pb, pe) else {
1700 continue;
1701 };
1702 let cur = out.bonds()[bi as usize];
1704 if cur.stereo == BondStereo::None || cur.stereo_atoms[0] != BondData::NO_STEREO_ATOM {
1705 continue;
1706 }
1707
1708 let mut refs = [BondData::NO_STEREO_ATOM; 2];
1709 let mut flips = 0usize;
1711 for (i, (end, other, p_end, p_other)) in
1712 [(src.begin, src.end, pb, pe), (src.end, src.begin, pe, pb)]
1713 .into_iter()
1714 .enumerate()
1715 {
1716 let want = src.stereo_atoms[i];
1717 let p_subs: Vec<u32> = out
1719 .neighbors(p_end)
1720 .map(|(o, _)| o)
1721 .filter(|&o| o != p_other)
1722 .collect();
1723 if let Some(&p) = kept.get(&want) {
1725 if p_subs.contains(&p) {
1726 refs[i] = p;
1727 continue;
1728 }
1729 }
1730 let subs: Vec<u32> = mol
1732 .neighbors(end)
1733 .map(|(o, _)| o)
1734 .filter(|&o| o != other)
1735 .collect();
1736 let Some(pos) = subs.iter().position(|&o| o == want) else {
1737 break;
1738 };
1739 let slots: Vec<Option<u32>> = subs
1740 .iter()
1741 .map(|o| kept.get(o).copied().filter(|p| p_subs.contains(p)))
1742 .collect();
1743 let Some(filled) = fill_replaced_slots(&slots, &p_subs) else {
1744 let Some(&alt) = subs.iter().find(|&&o| o != want) else {
1757 break;
1760 };
1761 let Some(&p_alt) = kept.get(&alt) else {
1762 break;
1763 };
1764 if !p_subs.contains(&p_alt) {
1765 break;
1766 }
1767 refs[i] = p_alt;
1768 flips += 1;
1769 continue;
1770 };
1771 refs[i] = filled[pos];
1772 }
1773
1774 if let Some(mut b) = out.bond_mut(bi) {
1775 if refs[0] == BondData::NO_STEREO_ATOM || refs[1] == BondData::NO_STEREO_ATOM {
1776 b.set_stereo(BondStereo::None);
1778 } else if flips % 2 == 0 {
1779 b.set_stereo_atoms(refs);
1780 } else {
1781 match src.stereo {
1782 BondStereo::Cis => {
1783 b.set_stereo(BondStereo::Trans);
1784 b.set_stereo_atoms(refs);
1785 }
1786 BondStereo::Trans => {
1787 b.set_stereo(BondStereo::Cis);
1788 b.set_stereo_atoms(refs);
1789 }
1790 _ => b.set_stereo(BondStereo::None),
1794 }
1795 }
1796 }
1797 }
1798}
1799
1800fn apply_template(
1812 mut base: AtomData,
1813 expr: &AtomExpr,
1814 degree_kept: bool,
1815 plan: ChiralityPlan,
1816) -> AtomData {
1817 base.num_radical_electrons = 0;
1830
1831 let element_changed = template_element(expr).is_some_and(|z| z != base.atomic_num);
1834
1835 if element_changed || !degree_kept {
1839 base.num_explicit_hs = 0;
1840 base.num_implicit_hs = 0;
1841 base.flags.remove(omgkit_core::AtomFlags::NO_IMPLICIT);
1842 }
1843 if element_changed {
1844 base.formal_charge = 0;
1845 base.isotope = 0;
1846 base.chiral_tag = ChiralTag::Unspecified;
1847 }
1848 let inherited = base.chiral_tag;
1850 apply_expr(&mut base, expr);
1851 base.chiral_tag = match plan {
1852 ChiralityPlan::Inherit | ChiralityPlan::Set => base.chiral_tag,
1853 ChiralityPlan::Drop => ChiralTag::Unspecified,
1854 ChiralityPlan::Retain => inherited,
1855 ChiralityPlan::Invert => inherited.inverted(),
1856 };
1857 base.atom_map = 0;
1858 base
1859}
1860
1861fn template_element(expr: &AtomExpr) -> Option<u8> {
1864 match expr {
1865 AtomExpr::Prim(AtomPrim::Element { z, .. }) => Some(*z),
1866 AtomExpr::And(parts) => parts.iter().find_map(template_element),
1867 _ => None,
1868 }
1869}
1870
1871fn apply_expr(a: &mut AtomData, expr: &AtomExpr) {
1872 match expr {
1873 AtomExpr::Prim(p) => apply_prim(a, p),
1874 AtomExpr::And(parts) => {
1875 for p in parts {
1876 apply_expr(a, p);
1877 }
1878 }
1879 AtomExpr::Or(_) | AtomExpr::Not(_) => {}
1882 }
1883}
1884
1885fn apply_prim(a: &mut AtomData, p: &AtomPrim) {
1886 match p {
1887 AtomPrim::Element { z, aromatic } => {
1888 a.atomic_num = *z;
1889 if let Some(arom) = aromatic {
1890 a.flags.set(omgkit_core::AtomFlags::AROMATIC, *arom);
1891 }
1892 }
1893 AtomPrim::Charge(c) => a.formal_charge = i8::try_from(*c).unwrap_or(0),
1894 AtomPrim::Isotope(i) => a.isotope = *i,
1895 AtomPrim::TotalHs(n) => {
1896 a.num_explicit_hs = u8::try_from(*n).unwrap_or(0);
1897 a.num_implicit_hs = 0;
1898 a.flags.insert(omgkit_core::AtomFlags::NO_IMPLICIT);
1899 }
1900 AtomPrim::Chirality(t) => a.chiral_tag = *t,
1901 _ => {}
1904 }
1905}
1906
1907fn is_dative_reversed(expr: &BondExpr) -> bool {
1912 match expr {
1913 BondExpr::Prim(BondPrim::DativeReversed) => true,
1914 BondExpr::And(parts) => parts.iter().any(is_dative_reversed),
1915 _ => false,
1917 }
1918}
1919
1920fn bond_direction_from(expr: &BondExpr) -> BondDirection {
1926 match expr {
1927 BondExpr::Prim(BondPrim::UpRight) => BondDirection::UpRight,
1928 BondExpr::Prim(BondPrim::DownRight) => BondDirection::DownRight,
1929 BondExpr::And(parts) => parts
1931 .iter()
1932 .map(bond_direction_from)
1933 .find(|d| *d != BondDirection::None)
1934 .unwrap_or(BondDirection::None),
1935 _ => BondDirection::None,
1937 }
1938}
1939
1940#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1942enum ProductBond {
1943 Fixed(BondOrder),
1945 FollowAromaticity,
1947 Inherit,
1949}
1950
1951fn product_bond_from(expr: &BondExpr) -> ProductBond {
1965 match expr {
1966 BondExpr::Prim(BondPrim::Any) => ProductBond::Inherit,
1967 BondExpr::Prim(p) => ProductBond::Fixed(match p {
1968 BondPrim::Double => BondOrder::Double,
1969 BondPrim::Triple => BondOrder::Triple,
1970 BondPrim::Quadruple => BondOrder::Quadruple,
1971 BondPrim::Aromatic => BondOrder::Aromatic,
1972 BondPrim::Dative | BondPrim::DativeReversed => BondOrder::Dative,
1973 _ => BondOrder::Single,
1974 }),
1975 BondExpr::And(parts) => parts
1976 .iter()
1977 .map(product_bond_from)
1978 .find(|o| !matches!(o, ProductBond::Fixed(BondOrder::Single)))
1979 .unwrap_or(ProductBond::Fixed(BondOrder::Single)),
1980 BondExpr::Or(_) | BondExpr::Not(_) => {
1981 if *expr == BondExpr::default_bond() {
1982 return ProductBond::FollowAromaticity;
1983 }
1984 let parts = match expr {
1985 BondExpr::Or(parts) => parts.as_slice(),
1986 _ => &[],
1987 };
1988 parts
1989 .iter()
1990 .map(product_bond_from)
1991 .find(|o| matches!(o, ProductBond::Fixed(_)))
1992 .unwrap_or(ProductBond::FollowAromaticity)
1993 }
1994 }
1995}
1996
1997#[cfg(test)]
1998mod tests {
1999 use super::*;
2000
2001 #[test]
2008 fn template_order_parity_gives_up_when_the_correspondence_is_not_unique() {
2009 let n = |v: &[u16]| -> Vec<Option<u16>> { v.iter().map(|&x| Some(x)).collect() };
2010
2011 assert_eq!(
2013 template_order_is_odd(&n(&[2, 3, 4]), &n(&[2, 3, 4])),
2014 Some(false)
2015 );
2016 assert_eq!(
2018 template_order_is_odd(&n(&[2, 3, 4]), &n(&[4, 3, 2])),
2019 Some(true)
2020 );
2021 assert_eq!(
2023 template_order_is_odd(&n(&[2, 3, 4]), &n(&[3, 4, 2])),
2024 Some(false)
2025 );
2026
2027 let mut react = n(&[2, 3, 4]);
2029 react[0] = None;
2030 let mut prod = n(&[2, 3, 4]);
2031 prod[2] = None;
2032 assert!(
2033 template_order_is_odd(&react, &prod).is_some(),
2034 "各有一个对不上时该顶替得起来"
2035 );
2036
2037 assert_eq!(
2039 template_order_is_odd(&n(&[2, 3, 4]), &n(&[5, 6, 4])),
2040 None,
2041 "产物侧有两个邻居在反应物侧找不到,对应关系不唯一"
2042 );
2043 assert_eq!(template_order_is_odd(&n(&[2, 3]), &n(&[2, 3])), None);
2045 assert_eq!(
2047 template_order_is_odd(&n(&[2, 3, 4]), &n(&[2, 3, 4, 5, 6])),
2048 None
2049 );
2050 }
2051}