Task: RGD and Stereo Groups (rdkit/rdkit#6247)

org: rdkit
repo: rdkit
pr_number: 6247
issue_title: RGD and Stereo Groups
issue_body: <!--
Thanks for contributing a pull request! 
-->
#### Reference Issue
<!-- Example: Fixes #1234 -->

Fixes #6146 

#### What does this implement/fix? Explain your changes.

Modified R group decomposition to copy stereo groups from the input structure to the core and R groups as described in the issue.  In addition to RGD changes I had to modify `ChemTransforms::replaceCore`

The decomposition from Issue #6146 now looks good:

![image](https://user-images.githubusercontent.com/24233741/228696687-4c1273d5-efbb-4bc9-a798-dc7bf2a277ff.png)


#### Any other comments?


resolved_issues: number: 6146
title: RGD and enhanced stereochemistry
body: 

**Describe the bug**

Enhanced stereochemistry is not carried over from input structures to R groups during RGD.

**To Reproduce**

```python

# %%

import sys
from rdkit import Chem
from rdkit.Chem import AllChem
from rdkit.Chem.rdRGroupDecomposition import RGroupDecomposition, RGroupDecompositionParameters, \
    RGroupMatching, RGroupScore, RGroupLabels, RGroupCoreAlignment, RGroupLabelling
from rdkit.Chem.Draw import IPythonConsole

IPythonConsole.ipython_useSVG = True

from rdkit.Chem import PandasTools
from rdkit.Chem import Draw
from IPython.display import HTML

IPythonConsole.drawOptions.addAtomIndices = False
IPythonConsole.drawOptions.addStereoAnnotation = True

import pandas as pd

def do_decomp(options, core, mols):
  decomp = RGroupDecomposition([core], options)
  display_mols = []
  for mol in mols:
    if decomp.Add(mol) > -1:
      display_mols.append(mol)
    else:
      print("Failed to add mol")
  decomp.Process()
  groups = decomp.GetRGroupsAsColumns()
  df = PandasTools.RGroupDecompositionToFrame(groups, display_mols, include_core=True)
  PandasTools.ChangeMoleculeRendering(df)
  return HTML(df.to_html())
# %%

block1="""
  ChemDraw02032311272D

  0  0  0     0  0              0 V3000
M  V30 BEGIN CTAB
M  V30 COUNTS 16 17 0 0 1
M  V30 BEGIN ATOM
M  V30 1 C 0.348623 1.031250 0.000000 0
M  V30 2 C 0.348623 0.206250 0.000000 0
M  V30 3 C 1.063094 -0.206250 0.000000 0
M  V30 4 C 1.777565 0.206250 0.000000 0
M  V30 5 C 1.777565 1.031250 0.000000 0
M  V30 6 C 1.063094 1.443750 0.000000 0
M  V30 7 C 1.063094 -1.031250 0.000000 0
M  V30 8 N 1.777565 -1.443750 0.000000 0
M  V30 9 C 0.348623 -1.443750 0.000000 0
M  V30 10 C -0.365848 -1.031250 0.000000 0
M  V30 11 C -0.452084 -0.210769 0.000000 0
M  V30 12 C -1.259056 -0.039242 0.000000 0
M  V30 13 C -1.671556 -0.753713 0.000000 0
M  V30 14 C -1.119523 -1.366808 0.000000 0
M  V30 15 O -2.492036 -0.839949 0.000000 0
M  V30 16 Cl 2.492036 1.443750 0.000000 0
M  V30 END ATOM
M  V30 BEGIN BOND
M  V30 1 2 1 2
M  V30 2 1 2 3
M  V30 3 2 3 4
M  V30 4 1 4 5
M  V30 5 2 5 6
M  V30 6 1 6 1
M  V30 7 1 3 7
M  V30 8 1 7 8 CFG=1
M  V30 9 1 7 9
M  V30 10 1 9 10
M  V30 11 1 10 11
M  V30 12 1 11 12
M  V30 13 1 12 13
M  V30 14 1 13 14
M  V30 15 1 14 10
M  V30 16 1 13 15 CFG=3
M  V30 17 1 5 16
M  V30 END BOND
M  V30 BEGIN COLLECTION
M  V30 MDLV30/STEABS ATOMS=(2 7 13)
M  V30 END COLLECTION
M  V30 END CTAB
M  END"""

mol1 = Chem.MolFromMolBlock(block1)
mol1

# %%
block2 = """
  ChemDraw02032311382D

  0  0  0     0  0              0 V3000
M  V30 BEGIN CTAB
M  V30 COUNTS 18 19 0 0 0
M  V30 BEGIN ATOM
M  V30 1 C 0.348623 1.388485 0.000000 0
M  V30 2 C 0.348623 0.563485 0.000000 0
M  V30 3 C 1.063094 0.150985 0.000000 0
M  V30 4 C 1.777565 0.563485 0.000000 0
M  V30 5 C 1.777565 1.388485 0.000000 0
M  V30 6 C 1.063094 1.800985 0.000000 0
M  V30 7 C 1.063094 -0.674015 0.000000 0
M  V30 8 N 1.777565 -1.086515 0.000000 0
M  V30 9 C 0.348623 -1.086515 0.000000 0
M  V30 10 C -0.365848 -0.674015 0.000000 0
M  V30 11 C -0.452084 0.146466 0.000000 0
M  V30 12 C -1.259056 0.317993 0.000000 0
M  V30 13 C -1.671556 -0.396478 0.000000 0
M  V30 14 C -1.119523 -1.009572 0.000000 0
M  V30 15 O -2.492036 -0.482714 0.000000 0
M  V30 16 Cl 2.492036 1.800986 0.000000 0
M  V30 17 O -0.063877 -1.800986 0.000000 0
M  V30 18 C 0.761123 -1.800986 0.000000 0
M  V30 END ATOM
M  V30 BEGIN BOND
M  V30 1 2 1 2
M  V30 2 1 2 3
M  V30 3 2 3 4
M  V30 4 1 4 5
M  V30 5 2 5 6
M  V30 6 1 6 1
M  V30 7 1 3 7
M  V30 8 1 7 8 CFG=1
M  V30 9 1 7 9
M  V30 10 1 9 10
M  V30 11 1 10 11
M  V30 12 1 11 12
M  V30 13 1 12 13
M  V30 14 1 13 14
M  V30 15 1 14 10
M  V30 16 1 13 15 CFG=3
M  V30 17 1 5 16
M  V30 18 1 9 17 CFG=1
M  V30 19 1 9 18 CFG=3
M  V30 END BOND
M  V30 BEGIN COLLECTION
M  V30 MDLV30/STEABS ATOMS=(2 7 9)
M  V30 MDLV30/STEREL1 ATOMS=(1 13)
M  V30 END COLLECTION
M  V30 END CTAB
M  END"""
mol2 = Chem.MolFromMolBlock(block2)
mol2
# %%
query_block = """
  ChemDraw02132309392D

  0  0  0     0  0              0 V3000
M  V30 BEGIN CTAB
M  V30 COUNTS 10 10 0 0 1
M  V30 BEGIN ATOM
M  V30 1 C -1.071690 1.031297 0.000000 0
M  V30 2 C -1.071690 0.206260 0.000000 0
M  V30 3 C -0.357230 -0.206259 0.000000 0
M  V30 4 C 0.357230 0.206260 0.000000 0
M  V30 5 C 0.357230 1.031297 0.000000 0
M  V30 6 C -0.357230 1.443816 0.000000 0
M  V30 7 C -0.357230 -1.031297 0.000000 0
M  V30 8 N 0.357230 -1.443816 0.000000 0
M  V30 9 R1 -1.071690 -1.443816 0.000000 0
M  V30 10 Cl 1.071690 1.443816 0.000000 0
M  V30 END ATOM
M  V30 BEGIN BOND
M  V30 1 2 1 2
M  V30 2 1 2 3
M  V30 3 2 3 4
M  V30 4 1 4 5
M  V30 5 2 5 6
M  V30 6 1 6 1
M  V30 7 1 3 7
M  V30 8 1 7 8 CFG=1
M  V30 9 1 7 9
M  V30 10 1 5 10
M  V30 END BOND
M  V30 END CTAB
M  END
"""
query= Chem.MolFromMolBlock(query_block)
query

# %%
options = RGroupDecompositionParameters()
options.onlyMatchAtRGroups = False
options.allowMultipleRGroupsOnUnlabelled = True
do_decomp(options, query, [mol1, mol2])
# %%


```

**Expected behavior**

I like to see `abs`, `or` and `and` enhanced stereo chemistry labels being properly carried over to the R groups which is not happening.  I understand that a single `or1` or `and1` in a group make little sense but it would be nice to see when viewing the decomposition.  

The structures are examples created by a customer.  They have proprietary data with multiple `and` and `or` centers that get scrambled on decomposition.  Unfortunately, I can't share those.
 
**Screenshots**

This is what the current RGD looks like:

![image](https://user-images.githubusercontent.com/24233741/222020086-efe36b97-8540-467d-b3de-62f0c9f7b750.png)


**Configuration (please complete the following information):**
 - RDKit version:  Master as of 2/28/2023
 - OS: [e.g. Ubuntu 20.04] Ubuntu 20.04
 - Python version (if relevant):
 - Are you using conda?
 - If you are using conda, which channel did you install the rdkit from?
 - If you are not using conda: how did you install the RDKit?

**Additional context**

@bp-kelley This is an example of some of the RGD issues we've been seeing.



Reference solution:
fix_patch: diff --git a/Code/GraphMol/ChemTransforms/ChemTransforms.cpp b/Code/GraphMol/ChemTransforms/ChemTransforms.cpp
index 38d9882712c..a7b47469235 100644
--- a/Code/GraphMol/ChemTransforms/ChemTransforms.cpp
+++ b/Code/GraphMol/ChemTransforms/ChemTransforms.cpp
@@ -31,6 +31,7 @@
 #include <RDGeneral/FileParseException.h>
 #include <RDGeneral/BadFileException.h>
 #include <GraphMol/ChemTransforms/ChemTransforms.h>
+
 namespace RDKit {
 namespace details {
 void updateSubMolConfs(const ROMol &mol, RWMol &res,
@@ -52,6 +53,26 @@ void updateSubMolConfs(const ROMol &mol, RWMol &res,
     res.addConformer(newConf, false);
   }
 }
+void copyStereoGroups(const std::map<const Atom *, Atom*> &molAtomMap, const ROMol &mol, RWMol &newMol) {
+  // Copy over any stereo groups that lie in the new molecule
+  if (!mol.getStereoGroups().empty()) {
+    std::vector<StereoGroup> newStereoGroups;
+    for (auto &stereoGroup : mol.getStereoGroups()) {
+      std::vector<Atom *> newStereoAtoms;
+      for (const auto stereoGroupAtom : stereoGroup.getAtoms()) {
+        if (auto found = molAtomMap.find(stereoGroupAtom);
+            found != molAtomMap.end()) {
+          newStereoAtoms.push_back(found->second);
+        }
+      }
+      if (!newStereoAtoms.empty()) {
+        newStereoGroups.emplace_back(stereoGroup.getGroupType(),
+                                     newStereoAtoms);
+      }
+    }
+    newMol.setStereoGroups(std::move(newStereoGroups));
+  }
+}
 }  // namespace details
 
 namespace {
@@ -62,10 +83,10 @@ struct SideChainMapping {
 
   SideChainMapping(int molIndex)
       : molIndex(molIndex), coreIndex(-1), useMatch(false) {}
+
   SideChainMapping(int molIndex, int coreIndex, bool useMatch)
       : molIndex(molIndex), coreIndex(coreIndex), useMatch(useMatch) {}
 };
-
 }  // namespace
 
 ROMol *deleteSubstructs(const ROMol &mol, const ROMol &query, bool onlyFrags,
@@ -488,6 +509,7 @@ ROMol *replaceCore(const ROMol &mol, const ROMol &core,
   auto *newMol = new RWMol(mol);
   std::vector<Atom *> keepList;
   std::map<Atom *, int> dummyAtomMap;
+  std::map<const Atom *, Atom *> molAtomMap;
 
   // go through the matches in query order, not target molecule
   //  order
@@ -516,6 +538,7 @@ ROMol *replaceCore(const ROMol &mol, const ROMol &core,
       Atom *sidechainAtom = newMol->getAtomWithIdx(mappingInfo.molIndex);
       // we're keeping the sidechain atoms:
       keepList.push_back(sidechainAtom);
+      molAtomMap[mol.getAtomWithIdx(mappingInfo.molIndex)] = sidechainAtom;
 
       // loop over our neighbors and see if any are in the match:
       std::list<unsigned int> nbrList;
@@ -700,25 +723,27 @@ ROMol *replaceCore(const ROMol &mol, const ROMol &core,
                                       endAtom->getIdx());
       }
     }
+  }
 
-    // make a guess at the position of the dummy atoms showing the attachment
-    // point:
-    for (auto citer = mol.beginConformers(); citer != mol.endConformers();
-         ++citer) {
-      Conformer &newConf = newMol->getConformer((*citer)->getId());
-      for (auto iter = dummyAtomMap.begin(); iter != dummyAtomMap.end();
-           ++iter) {
-        newConf.setAtomPos(iter->first->getIdx(),
-                           (*citer)->getAtomPos(iter->second));
-      }
+  // make a guess at the position of the dummy atoms showing the attachment
+  // point:
+  for (auto citer = mol.beginConformers(); citer != mol.endConformers();
+       ++citer) {
+    Conformer &newConf = newMol->getConformer((*citer)->getId());
+    for (auto iter = dummyAtomMap.begin(); iter != dummyAtomMap.end(); ++iter) {
+      newConf.setAtomPos(iter->first->getIdx(),
+                         (*citer)->getAtomPos(iter->second));
     }
-
-    // clear computed props and do basic updates on
-    // the resulting molecule, but allow unhappiness:
-    newMol->clearComputedProps(true);
-    newMol->updatePropertyCache(false);
   }
 
+  // copy over stereo groups
+  details::copyStereoGroups(molAtomMap, mol, *newMol);
+
+  // clear computed props and do basic updates on
+  // the resulting molecule, but allow unhappiness:
+  newMol->clearComputedProps(true);
+  newMol->updatePropertyCache(false);
+
   return static_cast<ROMol *>(newMol);
 }
 
@@ -971,6 +996,7 @@ void parseQueryDefFile(std::istream *inStream,
     queryDefs[qname] = msptr;
   }
 }
+
 void parseQueryDefFile(const std::string &filename,
                        std::map<std::string, ROMOL_SPTR> &queryDefs,
                        bool standardize, const std::string &delimiter,
@@ -985,6 +1011,7 @@ void parseQueryDefFile(const std::string &filename,
   parseQueryDefFile(&inStream, queryDefs, standardize, delimiter, comment,
                     nameColumn, smartsColumn);
 }
+
 void parseQueryDefText(const std::string &queryDefText,
                        std::map<std::string, ROMOL_SPTR> &queryDefs,
                        bool standardize, const std::string &delimiter,
diff --git a/Code/GraphMol/ChemTransforms/ChemTransforms.h b/Code/GraphMol/ChemTransforms/ChemTransforms.h
index 978aeb64cfb..ef03b1c3679 100644
--- a/Code/GraphMol/ChemTransforms/ChemTransforms.h
+++ b/Code/GraphMol/ChemTransforms/ChemTransforms.h
@@ -270,6 +270,9 @@ namespace details {
 //! not recommended for use in other code
 RDKIT_CHEMTRANSFORMS_EXPORT void updateSubMolConfs(
     const ROMol &mol, RWMol &res, boost::dynamic_bitset<> &removedAtoms);
+RDKIT_CHEMTRANSFORMS_EXPORT void copyStereoGroups(
+    const std::map<const Atom *, Atom *> &molAtomMap, const ROMol &mol,
+    RWMol &newMol);
 }  // namespace details
 
 }  // namespace RDKit
diff --git a/Code/GraphMol/RGroupDecomposition/RGroupCore.cpp b/Code/GraphMol/RGroupDecomposition/RGroupCore.cpp
index 9017134d865..2b09f51af19 100644
--- a/Code/GraphMol/RGroupDecomposition/RGroupCore.cpp
+++ b/Code/GraphMol/RGroupDecomposition/RGroupCore.cpp
@@ -14,7 +14,6 @@
 #include "GraphMol/Substruct/SubstructUtils.h"
 
 namespace RDKit {
-
 namespace {
 // From answer 12 in
 // https://stackoverflow.com/questions/5279051/how-can-i-create-cartesian-product-of-vector-of-vectors
@@ -71,6 +70,7 @@ std::pair<RWMOL_SPTR, bool> RCore::extractCoreFromMolMatch(
   std::set<int> atomIndicesToKeep;
   std::vector<Bond *> newBonds;
   std::map<Atom *, int> dummyAtomMap;
+  std::map<const Atom *, Atom *> molAtomMap;
   bool hasCoreDummies = false;
   for (auto &pair : match) {
     const auto queryAtom = core->getAtomWithIdx(pair.first);
@@ -90,6 +90,7 @@ std::pair<RWMOL_SPTR, bool> RCore::extractCoreFromMolMatch(
       continue;
     } else {
       atomIndicesToKeep.insert(pair.second);
+      molAtomMap[mol.getAtomWithIdx(pair.second)] = targetAtom;
       int neighborNumber = -1;
 #ifdef VERBOSE
       std::cerr << "Atom Chirality In " << targetAtom->getChiralTag()
@@ -124,6 +125,8 @@ std::pair<RWMOL_SPTR, bool> RCore::extractCoreFromMolMatch(
             // Hydrogen needed to define chirality is present in target but
             // not mapped to core.  Copy it to the extracted core
             hydrogensToAdd.push_back(static_cast<int>(targetNeighborIndex));
+            molAtomMap[mol.getAtomWithIdx(targetNeighborIndex)] =
+                targetNeighborAtom;
           } else if (isChiral) {
             // There is a heavy sidechain in the decomp that is connected to
             // the core by an unknown bond (onlyMatchAtRGroups = False and
@@ -260,6 +263,9 @@ std::pair<RWMOL_SPTR, bool> RCore::extractCoreFromMolMatch(
     }
   }
 
+  // Copy over any stereo groups that lie in the extracted core
+  details::copyStereoGroups(molAtomMap, mol, *extractedCore);
+
   extractedCore->clearComputedProps(true);
   extractedCore->updatePropertyCache(false);
 
@@ -810,5 +816,4 @@ int RCore::matchingIndexToCoreIndex(int matchingIndex) const {
                   "Matched atom missing core index");
   return atom->getProp<int>(RLABEL_CORE_INDEX);
 }
-
 }  // namespace RDKit