Repository: rdkit/rdkit — pull request #7904 (cpp)
Title: Fixes #7699

Stereo groups in product templates of reactions are now propagated to the product molecules.

If the atoms involved in those stereo groups are present in any stereo groups in the reactant molecules, they will be removed from those stereogroups when they are propagated to the products.

Resolved issues:
title: Carry over enhanced stereochemistry groups from reaction product templates to products
body: **Is your feature request related to a problem? Please describe.**
The problem is discussed in https://github.com/rdkit/rdkit/discussions/7660
Basically, for reactions, while stereo groups from reactants are carried over after https://github.com/rdkit/rdkit/issues/2366, this does not happen for stereogroups in product templates, yet.

**Describe the solution you'd like**
I would like to see stereogroups from the product template to be copied over to the actual reaction products in the core RDKit code for reactions.

**Describe alternatives you've considered**
In the discussion linked above, a workaround is shown, which involves copying the stereogroups manually with the help of reaction-realted atom props annotated during reaction application.

**Additional context**
Copying over the example from the discussion.


```python
from rdkit import Chem
from rdkit.Chem import rdChemReactions

input = Chem.MolFromMolBlock(""""
  MJ231601                      

  5  4  0  0  1  0  0  0  0  0999 V2000
   -0.8258    1.4955    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -0.8258    0.6705    0.0000 F   0  0  0  0  0  0  0  0  0  0  0  0
   -1.6508    1.4955    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -0.8258    2.3205    0.0000 Cl  0  0  0  0  0  0  0  0  0  0  0  0
   -0.0008    1.4955    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
  1  2  1  0  0  0  0
  1  4  1  0  0  0  0
  1  3  1  0  0  0  0
  1  5  1  0  0  0  0
M  END
""")

# rxn molblock with enhanced stereochemistry
rxn = """$RXN V3000

      Mrv2401  072420241107

M  V30 COUNTS 1 1
M  V30 BEGIN REACTANT
M  V30 BEGIN CTAB
M  V30 COUNTS 5 4 0 0 1
M  V30 BEGIN ATOM
M  V30 1 C -1.5417 2.7917 0 1
M  V30 2 F -1.5417 1.2517 0 4
M  V30 3 C -3.0817 2.7917 0 3
M  V30 4 Cl -1.5417 4.3317 0 2
M  V30 5 C -0.0017 2.7917 0 0
M  V30 END ATOM
M  V30 BEGIN BOND
M  V30 1 1 1 2
M  V30 2 1 1 4
M  V30 3 1 1 3
M  V30 4 1 1 5
M  V30 END BOND
M  V30 END CTAB
M  V30 END REACTANT
M  V30 BEGIN PRODUCT
M  V30 BEGIN CTAB
M  V30 COUNTS 5 4 0 0 0
M  V30 BEGIN ATOM
M  V30 1 C 9.2083 2.9167 0 1
M  V30 2 C 7.6683 2.9167 0 3
M  V30 3 F 9.2083 1.3767 0 4
M  V30 4 Cl 9.2083 4.4567 0 2
M  V30 5 I 10.7483 2.9167 0 0
M  V30 END ATOM
M  V30 BEGIN BOND
M  V30 1 1 1 2
M  V30 2 1 1 3
M  V30 3 1 1 4
M  V30 4 1 1 5 CFG=1
M  V30 END BOND
M  V30 BEGIN COLLECTION
M  V30 MDLV30/STERAC1 ATOMS=(1 1)
M  V30 END COLLECTION
M  V30 END CTAB
M  V30 END PRODUCT
M  END
"""

rxn = rdChemReactions.ReactionFromRxnBlock(rxn)
rxn.Initialize()
p = rxn.RunReactants([input])
for prod in p:
    sgs = prod[0].GetStereoGroups()
    if len(sgs) <= 0:
        print("Stereochemistry missing :(")
```

```
Stereochemistry missing :(
Stereochemistry missing :(
```


Hints:

Fix patch:
diff --git a/Code/GraphMol/ChemReactions/ReactionRunner.cpp b/Code/GraphMol/ChemReactions/ReactionRunner.cpp
index 19fc31fc14f..26639feb67a 100644
--- a/Code/GraphMol/ChemReactions/ReactionRunner.cpp
+++ b/Code/GraphMol/ChemReactions/ReactionRunner.cpp
@@ -1255,15 +1255,14 @@ void copyEnhancedStereoGroups(const ROMol &reactant, RWMOL_SPTR product,
   for (const auto &sg : reactant.getStereoGroups()) {
     std::vector<Atom *> atoms;
     std::vector<Bond *> bonds;
-    for (auto &&reactantAtom : sg.getAtoms()) {
+    for (const auto &reactantAtom : sg.getAtoms()) {
       auto productAtoms = mapping.reactProdAtomMap.find(reactantAtom->getIdx());
       if (productAtoms == mapping.reactProdAtomMap.end()) {
         continue;
       }
 
-      for (auto &&productAtomIdx : productAtoms->second) {
+      for (auto &productAtomIdx : productAtoms->second) {
         auto productAtom = product->getAtomWithIdx(productAtomIdx);
-
         // If chirality destroyed by the reaction, skip the atom
         if (productAtom->getChiralTag() == Atom::CHI_UNSPECIFIED) {
           continue;
@@ -1409,6 +1408,76 @@ void addReactantAtomsAndBonds(const ChemicalReaction &rxn, RWMOL_SPTR product,
   delete (mapping);
 }  // end of addReactantAtomsAndBonds
 
+namespace {
+void copyTemplateStereoGroupsToMol(const ROMol &templateMol,
+                                   RWMOL_SPTR product) {
+  const auto &stereoGroups = templateMol.getStereoGroups();
+  if (stereoGroups.empty()) {
+    return;
+  }
+
+  boost::dynamic_bitset<> atomsInTemplateStereoGroups(product->getNumAtoms());
+  std::vector<StereoGroup> newStereoGroups;
+  for (const auto &sg : stereoGroups) {
+    bool keepIt = true;
+    std::vector<Atom *> atoms;
+    for (const auto &atom : sg.getAtoms()) {
+      if (auto mapNum = atom->getAtomMapNum()) {
+        for (auto productAtom : product->atoms()) {
+          int oldMapNum = 0;
+          if (productAtom->getPropIfPresent(common_properties::reactionMapNum,
+                                            oldMapNum) &&
+              oldMapNum == mapNum) {
+            atoms.push_back(productAtom);
+            atomsInTemplateStereoGroups.set(productAtom->getIdx());
+          }
+        }
+      } else {
+        keepIt = false;
+        break;
+      }
+    }
+    if (keepIt && !atoms.empty()) {
+      std::vector<Bond *> bonds;
+      newStereoGroups.emplace_back(sg.getGroupType(), std::move(atoms),
+                                   std::move(bonds), sg.getReadId());
+    }
+  }
+  if (!newStereoGroups.empty()) {
+    // remove any stereo groups that are already present in the product (these
+    // were copied over from the reactant in copyEnhancedStereoGroups()) and
+    // that overlap with the added ones
+    for (const auto &productSG : product->getStereoGroups()) {
+      unsigned int nOverlappingAtoms = 0;
+      for (const auto atom : productSG.getAtoms()) {
+        if (atomsInTemplateStereoGroups[atom->getIdx()]) {
+          ++nOverlappingAtoms;
+        }
+      }
+      if (!nOverlappingAtoms) {
+        // no overlapping atoms, we can just keep the stereogroup.
+        newStereoGroups.push_back(productSG);
+      } else if (nOverlappingAtoms < productSG.getAtoms().size()) {
+        // some of the atoms in the stereo group are not already there
+        // in the product, we need to split the stereo group
+        std::vector<Atom *> newAtoms;
+        for (const auto atom : productSG.getAtoms()) {
+          if (!atomsInTemplateStereoGroups[atom->getIdx()]) {
+            newAtoms.push_back(atom);
+          }
+        }
+        std::vector<Bond *> newBonds;
+        newStereoGroups.emplace_back(productSG.getGroupType(),
+                                     std::move(newAtoms), std::move(newBonds),
+                                     productSG.getReadId());
+      }
+      // else: all atoms in the stereo group are already there, we can skip it
+    }
+    product->setStereoGroups(std::move(newStereoGroups));
+  }
+}
+}  // namespace
+
 MOL_SPTR_VECT
 generateOneProductSet(const ChemicalReaction &rxn,
                       const MOL_SPTR_VECT &reactants,
@@ -1469,6 +1538,11 @@ generateOneProductSet(const ChemicalReaction &rxn,
       MolOps::setDoubleBondNeighborDirections(*product);
     }
 
+    // if the product template has stereo groups, copy them over now
+    if (!(*pTemplIt)->getStereoGroups().empty()) {
+      copyTemplateStereoGroupsToMol(**pTemplIt, product);
+    }
+
     res[prodId] = product;
     ++prodId;
   }


Test patch:
diff --git a/Code/GraphMol/ChemReactions/catch_tests.cpp b/Code/GraphMol/ChemReactions/catch_tests.cpp
index 4bcb71143d8..d1d00dbdbb4 100644
--- a/Code/GraphMol/ChemReactions/catch_tests.cpp
+++ b/Code/GraphMol/ChemReactions/catch_tests.cpp
@@ -1983,3 +1983,111 @@ M  END)RXN";
     CHECK(rxn2.getReactants()[0]->getAtomWithIdx(0)->hasProp("molFileValue"));
   }
 }
+
+TEST_CASE("Github #7669: propate stereo groups from product templates") {
+  auto rxnBlock = R"RXN($RXN V3000
+
+      Mrv2401  072420241107
+
+M  V30 COUNTS 1 1
+M  V30 BEGIN REACTANT
+M  V30 BEGIN CTAB
+M  V30 COUNTS 5 4 0 0 1
+M  V30 BEGIN ATOM
+M  V30 1 C -1.5417 2.7917 0 1
+M  V30 2 F -1.5417 1.2517 0 4
+M  V30 3 C -3.0817 2.7917 0 3
+M  V30 4 Cl -1.5417 4.3317 0 2
+M  V30 5 C -0.0017 2.7917 0 0
+M  V30 END ATOM
+M  V30 BEGIN BOND
+M  V30 1 1 1 2
+M  V30 2 1 1 4
+M  V30 3 1 1 3
+M  V30 4 1 1 5
+M  V30 END BOND
+M  V30 END CTAB
+M  V30 END REACTANT
+M  V30 BEGIN PRODUCT
+M  V30 BEGIN CTAB
+M  V30 COUNTS 5 4 0 0 0
+M  V30 BEGIN ATOM
+M  V30 1 C 9.2083 2.9167 0 1
+M  V30 2 C 7.6683 2.9167 0 3
+M  V30 3 F 9.2083 1.3767 0 4
+M  V30 4 Cl 9.2083 4.4567 0 2
+M  V30 5 I 10.7483 2.9167 0 0
+M  V30 END ATOM
+M  V30 BEGIN BOND
+M  V30 1 1 1 2
+M  V30 2 1 1 3
+M  V30 3 1 1 4
+M  V30 4 1 1 5 CFG=1
+M  V30 END BOND
+M  V30 BEGIN COLLECTION
+M  V30 MDLV30/STERAC1 ATOMS=(1 1)
+M  V30 END COLLECTION
+M  V30 END CTAB
+M  V30 END PRODUCT
+M  END)RXN";
+  auto rxn = v2::ReactionParser::ReactionFromRxnBlock(rxnBlock);
+  REQUIRE(rxn);
+  CHECK(rxn->getProducts()[0]->getStereoGroups().size() == 1);
+  rxn->initReactantMatchers();
+  SECTION("as reported") {
+    auto mol = R"CTAB(
+  MJ231601                      
+
+  5  4  0  0  1  0  0  0  0  0999 V2000
+   -0.8258    1.4955    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+   -0.8258    0.6705    0.0000 F   0  0  0  0  0  0  0  0  0  0  0  0
+   -1.6508    1.4955    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+   -0.8258    2.3205    0.0000 Cl  0  0  0  0  0  0  0  0  0  0  0  0
+   -0.0008    1.4955    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+  1  2  1  0  0  0  0
+  1  4  1  0  0  0  0
+  1  3  1  0  0  0  0
+  1  5  1  0  0  0  0
+M  END)CTAB"_ctab;
+    REQUIRE(mol);
+    ROMOL_SPTR reactant(std::move(mol));
+    auto ps = rxn->runReactant(reactant, 0);
+    REQUIRE(ps.size() == 2);
+    REQUIRE(ps[0].size() == 1);
+
+    CHECK(ps[0][0]->getStereoGroups().size() == 1);
+    CHECK(ps[0][0]->getStereoGroups()[0].getGroupType() ==
+          StereoGroupType::STEREO_AND);
+    CHECK(ps[1][0]->getStereoGroups().size() == 1);
+    CHECK(ps[0][0]->getStereoGroups()[0].getGroupType() ==
+          StereoGroupType::STEREO_AND);
+
+    auto sg = StereoGroup(StereoGroupType::STEREO_OR,
+                          {reactant->getAtomWithIdx(0)}, {});
+    reactant->setStereoGroups({sg});
+    ps = rxn->runReactant(reactant, 0);
+    REQUIRE(ps.size() == 2);
+    REQUIRE(ps[0].size() == 1);
+    CHECK(ps[0][0]->getStereoGroups().size() == 1);
+    CHECK(ps[0][0]->getStereoGroups()[0].getGroupType() ==
+          StereoGroupType::STEREO_AND);
+    CHECK(ps[1][0]->getStereoGroups().size() == 1);
+    CHECK(ps[0][0]->getStereoGroups()[0].getGroupType() ==
+          StereoGroupType::STEREO_AND);
+  }
+
+  SECTION(
+      "stereogroups in product template remove atoms from stereogroups in reactant") {
+    auto mol = "C[C@](F)(Cl)C[C@](C)(O)Br |o1:1,5|"_smiles;
+    ROMOL_SPTR reactant(std::move(mol));
+    auto ps = rxn->runReactant(reactant, 0);
+    REQUIRE(ps.size() == 2);
+    REQUIRE(ps[0].size() == 1);
+    CHECK(ps[0][0]->getStereoGroups().size() == 1);
+    CHECK(ps[0][0]->getStereoGroups()[0].getGroupType() ==
+          StereoGroupType::STEREO_AND);
+    CHECK(ps[1][0]->getStereoGroups().size() == 2);
+    CHECK(ps[0][0]->getStereoGroups()[0].getGroupType() ==
+          StereoGroupType::STEREO_AND);
+  }
+}
\ No newline at end of file
diff --git a/Code/GraphMol/ChemReactions/testData/github7669_1.rxn b/Code/GraphMol/ChemReactions/testData/github7669_1.rxn
new file mode 100644
index 00000000000..32c84cf12ae
--- /dev/null
+++ b/Code/GraphMol/ChemReactions/testData/github7669_1.rxn
@@ -0,0 +1,90 @@
+$RXN V3000
+
+      Mrv2401  100720240457
+
+M  V30 COUNTS 1 1
+M  V30 BEGIN REACTANT
+M  V30 BEGIN CTAB
+M  V30 COUNTS 16 17 0 0 0
+M  V30 BEGIN ATOM
+M  V30 1 C -17.5833 8.8333 0 1
+M  V30 2 C -16.2497 9.6033 0 2
+M  V30 3 C -18.917 9.6033 0 3
+M  V30 4 C -20.2507 8.8333 0 4
+M  V30 5 C -20.2507 7.2933 0 5
+M  V30 6 C -18.917 6.5233 0 6
+M  V30 7 C -17.5833 7.2933 0 7
+M  V30 8 C -14.916 11.9134 0 8
+M  V30 9 C -16.2497 11.1434 0 9
+M  V30 10 C -14.916 8.8333 0 10
+M  V30 11 C -13.5823 9.6033 0 11
+M  V30 12 C -13.5823 11.1434 0 12
+M  V30 13 C -18.917 11.1433 0 13
+M  V30 14 F -16.2496 6.5233 0 14
+M  V30 15 F -14.916 7.2933 0 15
+M  V30 16 O -14.916 13.4534 0 16
+M  V30 END ATOM
+M  V30 BEGIN BOND
+M  V30 1 1 1 2
+M  V30 2 1 7 1
+M  V30 3 2 3 1
+M  V30 4 2 9 2
+M  V30 5 1 3 4
+M  V30 6 1 3 13
+M  V30 7 2 4 5
+M  V30 8 1 5 6
+M  V30 9 2 6 7
+M  V30 10 1 7 14
+M  V30 11 1 8 9
+M  V30 12 2 8 12
+M  V30 13 1 8 16
+M  V30 14 2 10 11
+M  V30 15 1 10 15
+M  V30 16 1 11 12
+M  V30 17 1 2 10
+M  V30 END BOND
+M  V30 END CTAB
+M  V30 END REACTANT
+M  V30 BEGIN PRODUCT
+M  V30 BEGIN CTAB
+M  V30 COUNTS 16 17 0 0 0
+M  V30 BEGIN ATOM
+M  V30 1 C -4.7085 10.1437 0 1
+M  V30 2 C -3.3749 10.9137 0 2
+M  V30 3 C -6.0422 10.9137 0 3
+M  V30 4 C -7.3759 10.1437 0 4
+M  V30 5 C -7.3759 8.6037 0 5
+M  V30 6 C -6.0422 7.8337 0 6
+M  V30 7 C -4.7085 8.6037 0 7
+M  V30 8 C -2.0412 13.2238 0 8
+M  V30 9 C -3.3749 12.4538 0 9
+M  V30 10 C -2.0412 10.1437 0 10
+M  V30 11 C -0.7075 10.9137 0 11
+M  V30 12 C -0.7075 12.4538 0 12
+M  V30 13 C -6.0422 12.4537 0 13
+M  V30 14 F -3.3748 7.8337 0 14
+M  V30 15 F -2.0412 8.6037 0 15
+M  V30 16 N -2.0412 14.7638 0 16
+M  V30 END ATOM
+M  V30 BEGIN BOND
+M  V30 1 1 1 2
+M  V30 2 1 7 1
+M  V30 3 2 3 1
+M  V30 4 2 9 2
+M  V30 5 1 3 4
+M  V30 6 1 3 13
+M  V30 7 2 4 5
+M  V30 8 1 5 6
+M  V30 9 2 6 7
+M  V30 10 1 7 14
+M  V30 11 1 8 9
+M  V30 12 2 8 12
+M  V30 13 1 8 16
+M  V30 14 2 10 11
+M  V30 15 1 10 15
+M  V30 16 1 11 12
+M  V30 17 1 2 10 CFG=1
+M  V30 END BOND
+M  V30 END CTAB
+M  V30 END PRODUCT
+M  END