Repository: rdkit/rdkit — pull request #6051 (cpp)
Title: Add canonicalization of stereo groups (enhanced stereo)

# What's changed?
In previous releases the SMILES generated for molecules with stereo groups was not canonical: the information from the stereo groups was not taken into account when determining the atom ordering and there was no handling of the fact that `C[C@H](F)O |o1:1|` and `C[C@@H](F)O |o1:1|` are the same thing. This resolves that.

# Changed results
SMILES (and SMILES-derived things like some of the mol hashes) for molecules with stereo groups will now be different if canonicalization and isomeric SMILES are enabled (the default).

# Questions for review
- We could theoretically not do the canonicalization of stereogroups if we're just generating SMILES (instead of CXSMILES), but this would have the consequence that the SMILES part of a CXSMILES wouldn't match the SMILES for the molecule. That feels ugly and wrong to me.


Resolved issues:
title: EnumerateStereoisomers should remove non-abs enhanced stereo from results
body: **Describe the bug**

```
In [7]: m = Chem.MolFromSmiles('O[C@H](Br)[C@H](F)C |&1:1,3|')

In [8]: set(Chem.MolToCXSmiles(x) for x in EnumerateStereoisomers.EnumerateStereoisomers(m))
Out[8]: {'C[C@@H](F)[C@H](O)Br |&1:1,3|', 'C[C@H](F)[C@@H](O)Br |&1:1,3|'}
```
Given that one of the purposes of `EnumerateStereoisomers()` is to give all the individual stereoisomers which are described by a set of StereoGropus, I believe that the results should no longer have the enhanced stereo.

**Configuration (please complete the following information):**
 - RDKit version: release and master
 - OS: all


Hints:

Fix patch:
diff --git a/Code/GraphMol/Canon.cpp b/Code/GraphMol/Canon.cpp
index b1c665fe844..55a80ec73c6 100644
--- a/Code/GraphMol/Canon.cpp
+++ b/Code/GraphMol/Canon.cpp
@@ -1187,6 +1187,7 @@ void canonicalizeFragment(ROMol &mol, int atomIdx,
           msI.obj.atom->getChiralTag() != Atom::CHI_UNSPECIFIED &&
           !msI.obj.atom->hasProp(common_properties::_brokenChirality)) {
         if (msI.obj.atom->hasProp(common_properties::_ringStereoAtoms)) {
+          // FIX: handle stereogroups here too
           if (!ringStereoChemAdjusted[msI.obj.atom->getIdx()]) {
             msI.obj.atom->setChiralTag(Atom::CHI_TETRAHEDRAL_CCW);
             ringStereoChemAdjusted.set(msI.obj.atom->getIdx());
@@ -1226,6 +1227,24 @@ void canonicalizeFragment(ROMol &mol, int atomIdx,
               ringStereoChemAdjusted.set(nbrIdx);
             }
           }
+        } else if (size_t sgidx;
+                   msI.obj.atom->getPropIfPresent("_stereoGroup", sgidx)) {
+          // make sure that the reference atom in the stereogroup is CCW
+          auto &sg = mol.getStereoGroups()[sgidx];
+          bool swapIt =
+              msI.obj.atom->getChiralTag() == Atom::CHI_TETRAHEDRAL_CW;
+          if (swapIt) {
+            msI.obj.atom->invertChirality();
+          }
+          if (swapIt || numSwapsChiralAtoms[msI.obj.atom->getIdx()]) {
+            for (auto at : sg.getAtoms()) {
+              if (at == msI.obj.atom) {
+                continue;
+              }
+              at->invertChirality();
+            }
+          }
+
         } else {
           if (msI.obj.atom->getChiralTag() == Atom::CHI_TETRAHEDRAL_CW ||
               msI.obj.atom->getChiralTag() == Atom::CHI_TETRAHEDRAL_CCW) {
@@ -1255,5 +1274,71 @@ void canonicalizeFragment(ROMol &mol, int atomIdx,
     std::cerr<<"----------------------------------------->"<<std::endl;
 #endif
 }
+void rankMolAtoms(const ROMol &mol, std::vector<unsigned int> &res,
+                  bool breakTies = true, bool includeChirality = true,
+                  bool includeIsotopes = true);
+void canonicalizeEnhancedStereo(ROMol &mol,
+                                const std::vector<unsigned int> *atomRanks) {
+  const auto &sgs = mol.getStereoGroups();
+  if (sgs.empty()) {
+    return;
+  }
+
+  std::vector<unsigned int> lranks;
+  if (!atomRanks) {
+    bool breakTies = true;
+    rankMolAtoms(mol, lranks, breakTies);
+    atomRanks = &lranks;
+  }
+  // one thing that makes this all easier is that the stereogroups are
+  // independent of each other
+  std::vector<StereoGroup> newSgs;
+  for (auto &sg : sgs) {
+    // we don't do anything to ABS groups
+    if (sg.getGroupType() == StereoGroupType::STEREO_ABSOLUTE) {
+      newSgs.push_back(sg);
+      continue;
+    }
+
+    // sort the atoms by rank:
+    auto getAtomRank = [&atomRanks](const Atom *at1, const Atom *at2) {
+      return atomRanks->at(at1->getIdx()) < atomRanks->at(at2->getIdx());
+    };
+    auto sgAtoms = sg.getAtoms();
+    std::sort(sgAtoms.begin(), sgAtoms.end(), getAtomRank);
+    // find the reference (lowest-ranked) atom
+    auto refAtom = sgAtoms.front();
+
+    // we will use CCW as the "canonical" state for chirality, so if the
+    // referenceAtom is already CCW then we don't need to do anything more with
+    // this stereogroup
+    auto refState = Atom::ChiralType::CHI_TETRAHEDRAL_CCW;
+#if 0
+    INT_LIST nbrs;
+    auto [beg, end] = mol.getAtomBonds(*refAtom);
+    while (beg != end) {
+      nbrs.push_back(mol[*beg++]->getIdx());
+    }
+    std::cerr << "!!!! " << (*refAtom)->getDegree() << " " << nbrs.size()
+              << std::endl;
+    // sort the neighbor indices by atom ranks
+    nbrs.sort([&atomRanks](auto i, auto j) {
+      return atomRanks->at(i) < atomRanks->at(j);
+    });
+    if ((*refAtom)->getPerturbationOrder(nbrs) % 1) {
+      refState = Atom::ChiralType::CHI_TETRAHEDRAL_CW;
+    }
+#endif
+    if (refAtom->getChiralTag() != refState) {
+      // we need to flip everyone... so loop over the other atoms and flip them
+      // all:
+      std::for_each(sgAtoms.begin(), sgAtoms.end(),
+                    [](auto atom) { atom->invertChirality(); });
+    }
+    newSgs.emplace_back(StereoGroup(sg.getGroupType(), std::move(sgAtoms)));
+    refAtom->setProp("_stereoGroup", newSgs.size() - 1, true);
+  }
+  mol.setStereoGroups(newSgs);
+}
 }  // namespace Canon
 }  // namespace RDKit
diff --git a/Code/GraphMol/Canon.h b/Code/GraphMol/Canon.h
index a51320fad86..5ec1ba50d21 100644
--- a/Code/GraphMol/Canon.h
+++ b/Code/GraphMol/Canon.h
@@ -125,6 +125,17 @@ RDKIT_GRAPHMOL_EXPORT bool chiralAtomNeedsTagInversion(const RDKit::ROMol &mol,
                                                        bool isAtomFirst,
                                                        size_t numClosures);
 
+//! Canonicalizes the atom stereo labels in enhanced stereo groups
+/*!
+
+  For example, after calling this function the chiral centers in the
+  molecules `C[C@H](F)Cl |&1:1|` and `C[C@@H](F)Cl |&1:1|` will have the same
+  chiral tags.
+
+*/
+RDKIT_GRAPHMOL_EXPORT void canonicalizeEnhancedStereo(
+    ROMol &mol, const std::vector<unsigned int> *atomRanks = nullptr);
+
 }  // end of namespace Canon
 }  // end of namespace RDKit
 #endif
diff --git a/Code/GraphMol/FileParsers/cdxml_parser_catch.cpp b/Code/GraphMol/FileParsers/cdxml_parser_catch.cpp
index e4f108ae2e0..ea1c930661b 100644
--- a/Code/GraphMol/FileParsers/cdxml_parser_catch.cpp
+++ b/Code/GraphMol/FileParsers/cdxml_parser_catch.cpp
@@ -411,9 +411,9 @@ TEST_CASE("CDXML") {
   SECTION("Enhanced Stereo") {
     auto fname = cdxmlbase + "beta-cypermethrin.cdxml";
     std::vector<std::string> expected = {
-        "CC1(C)[C@H](C=C(Cl)Cl)[C@H]1C(=O)O[C@@H](C#N)c1cccc(Oc2ccccc2)c1"};
+        "CC1(C)[C@H](C=C(Cl)Cl)[C@@H]1C(=O)O[C@H](C#N)c1cccc(Oc2ccccc2)c1"};
     std::vector<std::string> expected_cx = {
-        "CC1(C)[C@H](C=C(Cl)Cl)[C@H]1C(=O)O[C@@H](C#N)c1cccc(Oc2ccccc2)c1 |&1:3,&2:8,12|"};
+        "CC1(C)[C@H](C=C(Cl)Cl)[C@@H]1C(=O)O[C@H](C#N)c1cccc(Oc2ccccc2)c1 |&1:3,&2:8,12|"};
     auto mols = CDXMLFileToMols(fname);
     CHECK(mols.size() == expected.size());
     int i = 0;
@@ -426,9 +426,9 @@ TEST_CASE("CDXML") {
   SECTION("Enhanced Stereo 2") {
     auto fname = cdxmlbase + "beta-cypermethrin-or-abs.cdxml";
     std::vector<std::string> expected = {
-        "CC1(C)[C@H](C=C(Cl)Cl)[C@H]1C(=O)O[C@@H](C#N)c1cccc(Oc2ccccc2)c1"};
+        "CC1(C)[C@H](C=C(Cl)Cl)[C@@H]1C(=O)O[C@H](C#N)c1cccc(Oc2ccccc2)c1"};
     std::vector<std::string> expected_cx = {
-        "CC1(C)[C@H](C=C(Cl)Cl)[C@H]1C(=O)O[C@@H](C#N)c1cccc(Oc2ccccc2)c1 |o1:8,12|"};
+        "CC1(C)[C@H](C=C(Cl)Cl)[C@@H]1C(=O)O[C@H](C#N)c1cccc(Oc2ccccc2)c1 |o1:8,12|"};
     auto mols = CDXMLFileToMols(fname);
     CHECK(mols.size() == expected.size());
     int i = 0;
@@ -704,44 +704,40 @@ TEST_CASE("CDXML") {
     {
       auto fname = cdxmlbase + "stereo.cdxml";
       std::vector<std::string> expected = {
-        "C[C@@H](Cl)[C@H](N)O.C[C@@H](F)[C@H](N)O.C[C@H](Br)[C@@H](N)O.C[C@H](I)[C@@H](N)O"};
+          "C[C@@H](Cl)[C@H](N)O.C[C@@H](F)[C@H](N)O.C[C@H](Br)[C@@H](N)O.C[C@H](I)[C@@H](N)O"};
       auto mols = CDXMLFileToMols(fname);
       CHECK(mols.size() == expected.size());
       int i = 0;
       for (auto &mol : mols) {
-	CHECK(MolToSmiles(*mol) == expected[i++]);
+        CHECK(MolToSmiles(*mol) == expected[i++]);
       }
     }
     {
       auto fname = cdxmlbase + "wavy.cdxml";
-      std::vector<std::string> expected = {
-        "Cc1cccc(C)c1NC(=O)N=C1CCCN1C",
-        "Cc1cccc(C)c1NC(=O)/N=C1\\CCCN1C"
-      };
+      std::vector<std::string> expected = {"Cc1cccc(C)c1NC(=O)N=C1CCCN1C",
+                                           "Cc1cccc(C)c1NC(=O)/N=C1\\CCCN1C"};
       auto mols = CDXMLFileToMols(fname);
       CHECK(mols.size() == expected.size());
       int i = 0;
       for (auto &mol : mols) {
-          if (i == 0) {
-              CHECK(mol->getBondWithIdx(11)->getBondDir() == Bond::BondDir::EITHERDOUBLE);
-          }
-          CHECK(MolToSmiles(*mol) == expected[i++]);
-          
+        if (i == 0) {
+          CHECK(mol->getBondWithIdx(11)->getBondDir() ==
+                Bond::BondDir::EITHERDOUBLE);
+        }
+        CHECK(MolToSmiles(*mol) == expected[i++]);
       }
     }
     {
       auto fname = cdxmlbase + "wavy-single.cdxml";
-      std::vector<std::string> expected = {
-          "CCCC"
-      };
+      std::vector<std::string> expected = {"CCCC"};
       auto mols = CDXMLFileToMols(fname);
       CHECK(mols.size() == expected.size());
       int i = 0;
       for (auto &mol : mols) {
-          CHECK(mol->getBondWithIdx(0)->getBondDir() == Bond::BondDir::NONE);
-          CHECK(mol->getBondWithIdx(1)->getBondDir() == Bond::BondDir::NONE);
-          CHECK(mol->getBondWithIdx(2)->getBondDir() == Bond::BondDir::NONE);
-          CHECK(MolToSmiles(*mol) == expected[i++]);
+        CHECK(mol->getBondWithIdx(0)->getBondDir() == Bond::BondDir::NONE);
+        CHECK(mol->getBondWithIdx(1)->getBondDir() == Bond::BondDir::NONE);
+        CHECK(mol->getBondWithIdx(2)->getBondDir() == Bond::BondDir::NONE);
+        CHECK(MolToSmiles(*mol) == expected[i++]);
       }
     }
   }
diff --git a/Code/GraphMol/MolInterchange/molinterchange_catch.cpp b/Code/GraphMol/MolInterchange/molinterchange_catch.cpp
index 5df108945f8..b8caf502703 100644
--- a/Code/GraphMol/MolInterchange/molinterchange_catch.cpp
+++ b/Code/GraphMol/MolInterchange/molinterchange_catch.cpp
@@ -173,7 +173,7 @@ TEST_CASE("StereoGroups") {
     auto smi = MolToCXSmiles(*mols[0]);
     CHECK(
         smi ==
-        "C[C@@H]1C([C@H](O)F)O[C@H](C)C([C@@H](O)F)[C@@H]1C |a:7,o1:3,10,&1:1,&2:13|");
+        "C[C@H]1OC([C@H](O)F)[C@H](C)[C@H](C)C1[C@@H](O)F |a:1,o1:4,12,&1:7,&2:9|");
   }
   SECTION("writing multiple mols") {
     std::vector<ROMol *> mols{ormol.get(), andmol.get()};
diff --git a/Code/GraphMol/RGroupDecomposition/catch_rgd.cpp b/Code/GraphMol/RGroupDecomposition/catch_rgd.cpp
index be248c5a147..9f6b099c444 100644
--- a/Code/GraphMol/RGroupDecomposition/catch_rgd.cpp
+++ b/Code/GraphMol/RGroupDecomposition/catch_rgd.cpp
@@ -462,22 +462,22 @@ TEST_CASE("substructure parameters and RGD: enhanced stereo") {
       CHECK(flatten_whitespace(toJSON(rows)) == flatten_whitespace(R"JSON(
 [
   {
-    "Core":"C1C[C@@]([*:1])([*:2])N1",
+    "Core":"C1C[C@]([*:1])([*:2])N1",
     "R1":"F[*:1]",
     "R2":"[H][*:2]"
   },
   {
-    "Core":"C1C[C@@]([*:1])([*:2])N1",
+    "Core":"C1C[C@]([*:1])([*:2])N1",
     "R1":"O[*:1]",
     "R2":"F[*:2]"
   },
   {
-    "Core":"C1C[C@@]([*:1])([*:2])N1",
+    "Core":"C1C[C@]([*:1])([*:2])N1",
     "R1":"[H][*:1]",
     "R2":"F[*:2]"
   },
   {
-    "Core":"C1C[C@@]([*:1])([*:2])N1",
+    "Core":"C1C[C@]([*:1])([*:2])N1",
     "R1":"Cl[*:1]",
     "R2":"[H][*:2]"
   }
@@ -496,22 +496,22 @@ TEST_CASE("substructure parameters and RGD: enhanced stereo") {
       CHECK(flatten_whitespace(toJSON(rows)) == flatten_whitespace(R"JSON(
 [
   {
-    "Core":"C1C[C@@]([*:1])([*:2])N1",
+    "Core":"C1C[C@]([*:1])([*:2])N1",
     "R1":"F[*:1]",
     "R2":"[H][*:2]"
   },
   {
-    "Core":"C1C[C@@]([*:1])([*:2])N1",
+    "Core":"C1C[C@]([*:1])([*:2])N1",
     "R1":"O[*:1]",
     "R2":"F[*:2]"
   },
   {
-    "Core":"C1C[C@@]([*:1])([*:2])N1",
+    "Core":"C1C[C@]([*:1])([*:2])N1",
     "R1":"[H][*:1]",
     "R2":"F[*:2]"
   },
   {
-    "Core":"C1C[C@@]([*:1])([*:2])N1",
+    "Core":"C1C[C@]([*:1])([*:2])N1",
     "R1":"Cl[*:1]",
     "R2":"[H][*:2]"
   }
@@ -537,7 +537,7 @@ TEST_CASE("substructure parameters and RGD: enhanced stereo") {
       CHECK(flatten_whitespace(toJSON(rows)) == flatten_whitespace(R"JSON(
 [
   {
-    "Core":"C1C[C@@]([*:1])([*:2])N1",
+    "Core":"C1C[C@]([*:1])([*:2])N1",
     "R1":"F[*:1]",
     "R2":"[H][*:2]"
   },
@@ -565,7 +565,7 @@ TEST_CASE("substructure parameters and RGD: enhanced stereo") {
       CHECK(flatten_whitespace(toJSON(rows)) == flatten_whitespace(R"JSON(
 [
   {
-    "Core":"C1C[C@@]([*:1])([*:2])N1",
+    "Core":"C1C[C@]([*:1])([*:2])N1",
     "R1":"F[*:1]",
     "R2":"[H][*:2]"
   },
@@ -580,7 +580,7 @@ TEST_CASE("substructure parameters and RGD: enhanced stereo") {
     "R2":"[H][*:2]"
   },
   {
-    "Core":"C1C[C@@]([*:1])([*:2])N1",
+    "Core":"C1C[C@]([*:1])([*:2])N1",
     "R1":"Cl[*:1]",
     "R2":"[H][*:2]"
   }
diff --git a/Code/GraphMol/SmilesParse/SmilesWrite.cpp b/Code/GraphMol/SmilesParse/SmilesWrite.cpp
index 0e544d001b8..7da43dc0f1e 100644
--- a/Code/GraphMol/SmilesParse/SmilesWrite.cpp
+++ b/Code/GraphMol/SmilesParse/SmilesWrite.cpp
@@ -366,6 +366,10 @@ std::string FragmentSmilesConstruct(
   }
   std::list<unsigned int> ringClosuresToErase;
 
+  if (params.canonical && params.doIsomericSmiles) {
+    Canon::canonicalizeEnhancedStereo(mol, &ranks);
+  }
+
   Canon::canonicalizeFragment(mol, atomIdx, colors, ranks, molStack,
                               bondsInPlay, bondSymbols, params.doIsomericSmiles,
                               params.doRandom);
diff --git a/Code/GraphMol/Wrap/rdmolfiles.cpp b/Code/GraphMol/Wrap/rdmolfiles.cpp
index bc56b785c54..d89e571cd9f 100644
--- a/Code/GraphMol/Wrap/rdmolfiles.cpp
+++ b/Code/GraphMol/Wrap/rdmolfiles.cpp
@@ -18,6 +18,7 @@
 #include <GraphMol/RDKitBase.h>
 #include <GraphMol/MolOps.h>
 #include <GraphMol/new_canon.h>
+#include <GraphMol/Canon.h>
 #include <GraphMol/SmilesParse/SmilesParse.h>
 #include <GraphMol/SmilesParse/SmilesWrite.h>
 #include <GraphMol/SmilesParse/SmartsWrite.h>
@@ -657,6 +658,10 @@ python::dict MetadataFromPNGString(python::object png) {
   return translateMetadata(metadata);
 }
 
+void CanonicalizeEnhancedStereo(ROMol &mol) {
+  Canon::canonicalizeEnhancedStereo(mol);
+}
+
 }  // namespace RDKit
 
 // MolSupplier stuff
@@ -1908,6 +1913,9 @@ BOOST_PYTHON_MODULE(rdmolfiles) {
        python::arg("includeIsotopes") = true),
       docString.c_str());
 
+  python::def("CanonicalizeEnhancedStereo", CanonicalizeEnhancedStereo,
+              (python::arg("mol")));
+
   python::def(
       "CreateAtomIntPropertyList", FileParserUtils::createAtomIntPropertyList,
       (python::arg("mol"), python::arg("propName"),
diff --git a/Code/GraphMol/catch_canon.cpp b/Code/GraphMol/catch_canon.cpp
index a24c67d71a5..c468a6fcf3a 100644
--- a/Code/GraphMol/catch_canon.cpp
+++ b/Code/GraphMol/catch_canon.cpp
@@ -15,6 +15,7 @@
 #include <GraphMol/Chirality.h>
 #include <GraphMol/new_canon.h>
 #include <GraphMol/MolOps.h>
+#include <GraphMol/Canon.h>
 
 #include <GraphMol/FileParsers/FileParsers.h>
 #include <GraphMol/FileParsers/MolFileStereochem.h>
@@ -153,16 +154,10 @@ TEST_CASE("double bond stereo and canonicalization") {
     bool breakTies = false;
     std::vector<unsigned int> ranks;
     Canon::rankMolAtoms(*mol, ranks, breakTies);
-    std::copy(ranks.begin(), ranks.end(),
-              std::ostream_iterator<unsigned int>(std::cerr, " "));
-    std::cerr << std::endl;
     mol->getBondWithIdx(1)->setStereo(Bond::BondStereo::STEREOCIS);
     mol->getBondWithIdx(7)->setStereo(Bond::BondStereo::STEREOTRANS);
     std::vector<unsigned int> ranks2;
     Canon::rankMolAtoms(*mol, ranks2, breakTies);
-    std::copy(ranks2.begin(), ranks2.end(),
-              std::ostream_iterator<unsigned int>(std::cerr, " "));
-    std::cerr << std::endl;
     CHECK(ranks[0] == ranks2[9]);
     CHECK(ranks[1] == ranks2[8]);
     CHECK(ranks[2] == ranks2[6]);
@@ -175,9 +170,6 @@ TEST_CASE("double bond stereo and canonicalization") {
     mol->getBondWithIdx(7)->setStereo(Bond::BondStereo::STEREOCIS);
     std::vector<unsigned int> ranks3;
     Canon::rankMolAtoms(*mol, ranks3, breakTies);
-    std::copy(ranks3.begin(), ranks3.end(),
-              std::ostream_iterator<unsigned int>(std::cerr, " "));
-    std::cerr << std::endl;
     CHECK(ranks2 == ranks3);
   }
   SECTION("STEREOANY is higher priority than STEREONONE") {
@@ -188,9 +180,171 @@ TEST_CASE("double bond stereo and canonicalization") {
     bool breakTies = false;
     std::vector<unsigned int> ranks;
     Canon::rankMolAtoms(*mol, ranks, breakTies);
-    std::copy(ranks.begin(), ranks.end(),
-              std::ostream_iterator<unsigned int>(std::cerr, " "));
-    std::cerr << std::endl;
     CHECK(ranks[0] < ranks[9]);
   }
-}
\ No newline at end of file
+}
+
+TEST_CASE("enhanced stereo canonicalization") {
+  SECTION("simple chiral tags") {
+    std::vector<std::pair<std::string, std::string>> tests = {
+        {"C[C@H](F)Cl |&1:1|", "C[C@@H](F)Cl |&1:1|"},
+        {"C[C@H](F)Cl |o1:1|", "C[C@@H](F)Cl |o1:1|"},
+    };
+    for (const auto& [smi1, smi2] : tests) {
+      INFO(smi1 + " : " + smi2);
+      std::unique_ptr<RWMol> mol1{SmilesToMol(smi1)};
+      REQUIRE(mol1);
+      std::unique_ptr<RWMol> mol2{SmilesToMol(smi2)};
+      REQUIRE(mol2);
+
+      Canon::canonicalizeEnhancedStereo(*mol1);
+      Canon::canonicalizeEnhancedStereo(*mol2);
+
+      CHECK(mol1->getAtomWithIdx(1)->getChiralTag() ==
+            mol2->getAtomWithIdx(1)->getChiralTag());
+    }
+  }
+  SECTION("abs groups are not modified") {
+    std::vector<std::pair<std::string, std::string>> tests = {
+        {"C[C@H](F)Cl |a:1|", "C[C@@H](F)Cl |a:1|"},
+    };
+    for (const auto& [smi1, smi2] : tests) {
+      INFO(smi1 + " : " + smi2);
+      std::unique_ptr<RWMol> mol1{SmilesToMol(smi1)};
+      REQUIRE(mol1);
+      std::unique_ptr<RWMol> mol2{SmilesToMol(smi2)};
+      REQUIRE(mol2);
+
+      Canon::canonicalizeEnhancedStereo(*mol1);
+      Canon::canonicalizeEnhancedStereo(*mol2);
+
+      CHECK(mol1->getAtomWithIdx(1)->getChiralTag() !=
+            mol2->getAtomWithIdx(1)->getChiralTag());
+    }
+  }
+  SECTION("relative chiral tags") {
+    std::vector<std::pair<std::string, std::string>> tests = {
+        {"C[C@H](F)[C@H](Br)O |&1:1,3|", "C[C@@H](F)[C@@H](Br)O |&1:1,3|"},
+        {"C[C@H](F)[C@@H](Br)O |&1:1,3|", "C[C@@H](F)[C@H](Br)O |&1:1,3|"},
+        {"O[C@H](Br)[C@H](F)C |&1:1,3|", "O[C@@H](Br)[C@@H](F)C |&1:1,3|"},
+        {"O[C@H](Br)[C@@H](F)C |&1:1,3|", "O[C@@H](Br)[C@H](F)C |&1:1,3|"},
+    };
+    for (const auto& [smi1, smi2] : tests) {
+      INFO(smi1 + " : " + smi2);
+      std::unique_ptr<RWMol> mol1{SmilesToMol(smi1)};
+      REQUIRE(mol1);
+      std::unique_ptr<RWMol> mol2{SmilesToMol(smi2)};
+      REQUIRE(mol2);
+
+      Canon::canonicalizeEnhancedStereo(*mol1);
+      Canon::canonicalizeEnhancedStereo(*mol2);
+
+      CHECK(mol1->getAtomWithIdx(1)->getChiralTag() ==
+            mol2->getAtomWithIdx(1)->getChiralTag());
+      CHECK(mol1->getAtomWithIdx(3)->getChiralTag() ==
+            mol2->getAtomWithIdx(3)->getChiralTag());
+    }
+  }
+  SECTION("multiple groups") {
+    std::vector<std::pair<std::string, std::string>> tests = {
+        {"C[C@H](F)[C@H](Br)O |&1:1,o2:3|",
+         "C[C@@H](F)[C@@H](Br)O |&1:1,o2:3|"},
+        {"C[C@H](F)[C@H](Br)O |&1:1,o2:3|",
+         "C[C@@H](F)[C@@H](Br)O |o1:3,&1:1|"},
+    };
+    for (const auto& [smi1, smi2] : tests) {
+      INFO(smi1 + " : " + smi2);
+      std::unique_ptr<RWMol> mol1{SmilesToMol(smi1)};
+      REQUIRE(mol1);
+      std::unique_ptr<RWMol> mol2{SmilesToMol(smi2)};
+      REQUIRE(mol2);
+
+      Canon::canonicalizeEnhancedStereo(*mol1);
+      Canon::canonicalizeEnhancedStereo(*mol2);
+
+      CHECK(mol1->getAtomWithIdx(1)->getChiralTag() ==
+            mol2->getAtomWithIdx(1)->getChiralTag());
+      CHECK(mol1->getAtomWithIdx(3)->getChiralTag() ==
+            mol2->getAtomWithIdx(3)->getChiralTag());
+    }
+  }
+}
+
+TEST_CASE("using enhanced stereo in rankMolAtoms") {
+  SECTION("basics: different ranks") {
+    std::vector<std::string> smis{
+        "C[C@H](F)[C@@H](F)C |a:1|",
+        "C[C@H](F)[C@@H](F)C |&1:1|"
+        "C[C@H](F)[C@@H](F)C |o1:1|",
+        "C[C@H](F)[C@@H](F)C |o1:1,a:3|",
+        "C[C@H](F)[C@@H](F)C |o1:1,&:3|",
+        "C[C@H](F)[C@@H](F)C |a:1,&2:3|",
+    };
+    for (auto& smi : smis) {
+      INFO(smi);
+      std::unique_ptr<RWMol> mol{SmilesToMol(smi)};
+      REQUIRE(mol);
+      bool breakTies = false;
+      std::vector<unsigned int> atomRanks;
+      Canon::rankMolAtoms(*mol, atomRanks, breakTies);
+      CHECK(atomRanks[1] != atomRanks[3]);
+    }
+  }
+  SECTION("basics: same ranks") {
+    std::vector<std::string> smis{
+        "C[C@H](F)[C@@H](F)C |o1:1,o2:3|",
+        "C[C@H](F)[C@@H](F)C |&1:1,&2:3|",
+        "C[C@H](F)[C@@H](F)C |a:1,a:3|",
+    };
+    for (auto& smi : smis) {
+      INFO(smi);
+      std::unique_ptr<RWMol> mol{SmilesToMol(smi)};
+      REQUIRE(mol);
+      bool breakTies = false;
+      std::vector<unsigned int> atomRanks;
+      Canon::rankMolAtoms(*mol, atomRanks, breakTies);
+      CHECK(atomRanks[1] == atomRanks[3]);
+    }
+  }
+  SECTION("more complex, include group membership") {
+    auto m1 =
+        "C[C@@H]1CC(C[C@@H](C)[C@@H](C)O)C[C@H](C)C1 |o1:1,7,o2:5,11|"_smiles;
+    REQUIRE(m1);
+    std::vector<unsigned int> atomRanks;
+    bool breakTies = false;
+    Canon::rankMolAtoms(*m1, atomRanks, breakTies);
+    CHECK(atomRanks[11] > atomRanks[1]);
+
+    auto m2 =
+        "C[C@H](O)[C@H](C)CC1C[C@H](C)C[C@@H](C)C1 |o1:1,8;o2:3,11|"_smiles;
+    REQUIRE(m2);
+    Canon::rankMolAtoms(*m2, atomRanks, breakTies);
+    CHECK(atomRanks[11] > atomRanks[8]);
+  }
+}
+
+TEST_CASE("more enhanced stereo canonicalization") {
+  // FIX: add tests for ring stereo in an s group
+  SECTION("case 1") {
+    auto m1 =
+        "C[C@@H](O)[C@H](C)[C@@H](C)[C@@H](C)F |a:3,&1:1,7,&2:5,r|"_smiles;
+    REQUIRE(m1);
+    auto m2 = "C[C@H](O)[C@H](C)[C@@H](C)[C@H](C)F |a:3,&1:1,7,&2:5,r|"_smiles;
+    REQUIRE(m2);
+    Canon::canonicalizeEnhancedStereo(*m1);
+    Canon::canonicalizeEnhancedStereo(*m2);
+    CHECK(MolToCXSmiles(*m1) == MolToCXSmiles(*m2));
+  }
+  SECTION("case 2") {
+    auto m1 =
+        "C[C@@H](O)[C@H](C)[C@@H](C)[C@@H](C)O |&3:3,5,o1:7,&2:1,r|"_smiles;
+    REQUIRE(m1);
+    auto m2 =
+        "C[C@@H](O)[C@H](C)[C@@H](C)[C@@H](C)O |&3:3,5,&2:7,o1:1,r|"_smiles;
+    REQUIRE(m2);
+    Canon::canonicalizeEnhancedStereo(*m1);
+    Canon::canonicalizeEnhancedStereo(*m2);
+
+    CHECK(MolToCXSmiles(*m1) == MolToCXSmiles(*m2));
+  }
+}
diff --git a/Code/GraphMol/catch_graphmol.cpp b/Code/GraphMol/catch_graphmol.cpp
index 7608f800e81..6dbbbed327b 100644
--- a/Code/GraphMol/catch_graphmol.cpp
+++ b/Code/GraphMol/catch_graphmol.cpp
@@ -2018,7 +2018,7 @@ TEST_CASE("github #4071: StereoGroups not preserved by RenumberAtoms()",
             mol->getStereoGroups()[i].getGroupType());
     }
     CHECK(MolToCXSmiles(*nmol) ==
-          "C[C@H]([C@@H](C)[C@@H](C)O)[C@@H](C)O |o1:7,&1:1,&2:2,&3:4|");
+          "C[C@H](O)[C@H](C)[C@H](C)[C@H](C)O |o1:1,&1:3,&2:5,&3:7|");
   }
 }
 
diff --git a/Code/GraphMol/new_canon.cpp b/Code/GraphMol/new_canon.cpp
index 0286384f1dc..c489bfa8081 100644
--- a/Code/GraphMol/new_canon.cpp
+++ b/Code/GraphMol/new_canon.cpp
@@ -553,6 +553,16 @@ void initCanonAtoms(const ROMol &mol, std::vector<Canon::canon_atom> &atoms,
     atoms[i].bonds.reserve(atoms[i].degree);
     getBonds(mol, atoms[i].atom, atoms[i].bonds, includeChirality, atoms);
   }
+  if (includeChirality) {
+    unsigned int sgidx = 1;
+    for (auto &sg : mol.getStereoGroups()) {
+      for (auto atom : sg.getAtoms()) {
+        atoms[atom->getIdx()].whichStereoGroup = sgidx;
+        atoms[atom->getIdx()].typeOfStereoGroup = sg.getGroupType();
+      }
+      ++sgidx;
+    }
+  }
 }
 namespace detail {
 
diff --git a/Code/GraphMol/new_canon.h b/Code/GraphMol/new_canon.h
index 53853ed9062..1e7e6fead7f 100644
--- a/Code/GraphMol/new_canon.h
+++ b/Code/GraphMol/new_canon.h
@@ -13,6 +13,7 @@
 #include <RDGeneral/hanoiSort.h>
 #include <GraphMol/ROMol.h>
 #include <GraphMol/RingInfo.h>
+#include <GraphMol/StereoGroup.h>
 #include <RDGeneral/BoostStartInclude.h>
 #include <cstdint>
 #include <boost/dynamic_bitset.hpp>
@@ -96,14 +97,15 @@ struct RDKIT_GRAPHMOL_EXPORT bondholder {
     return 0;
   }
 };
-class RDKIT_GRAPHMOL_EXPORT canon_atom {
- public:
+struct RDKIT_GRAPHMOL_EXPORT canon_atom {
   const Atom *atom{nullptr};
   int index{-1};
   unsigned int degree{0};
   unsigned int totalNumHs{0};
   bool hasRingNbr{false};
   bool isRingStereoAtom{false};
+  unsigned int whichStereoGroup{0};
+  StereoGroupType typeOfStereoGroup{StereoGroupType::STEREO_ABSOLUTE};
   int *nbrIds{nullptr};
   const std::string *p_symbol{
       nullptr};  // if provided, this is used to order atoms
@@ -380,30 +382,67 @@ class RDKIT_GRAPHMOL_EXPORT AtomCompareFunctor {
     }
     // chirality if we're using it
     if (df_useChirality) {
-      // first atom stereochem:
-      ivi = 0;
-      ivj = 0;
-      // can't actually use values here, because they are arbitrary
-      ivi = dp_atoms[i].atom->getChiralTag() != 0;
-      ivj = dp_atoms[j].atom->getChiralTag() != 0;
-      if (ivi < ivj) {
-        return -1;
-      } else if (ivi > ivj) {
-        return 1;
-      }
-      // stereo set
-      if (ivi && ivj) {
-        if (ivi) {
-          ivi = getChiralRank(dp_mol, dp_atoms, i);
-        }
-        if (ivj) {
-          ivj = getChiralRank(dp_mol, dp_atoms, j);
+      // look at enhanced stereo
+      ivi = dp_atoms[i].whichStereoGroup;  // can't use the index itself, but if
+                                           // it's set then we're in an SG
+      ivj = dp_atoms[j].whichStereoGroup;
+      if (ivi || ivj) {
+        if (ivi && !ivj) {
+          return 1;
+        } else if (ivj && !ivi) {
+          return -1;
+        } else if (ivi && ivj) {
+          ivi = static_cast<unsigned int>(dp_atoms[i].typeOfStereoGroup);
+          ivj = static_cast<unsigned int>(dp_atoms[j].typeOfStereoGroup);
+          if (ivi < ivj) {
+            return -1;
+          } else if (ivi > ivj) {
+            return 1;
+          }
+          ivi = dp_atoms[i].whichStereoGroup - 1;
+          ivj = dp_atoms[j].whichStereoGroup - 1;
+          // now check the current classes of the other members of the SG
+          std::set<unsigned int> sgi;
+          for (auto sgat : dp_mol->getStereoGroups()[ivi].getAtoms()) {
+            sgi.insert(dp_atoms[sgat->getIdx()].index);
+          }
+          std::set<unsigned int> sgj;
+          for (auto sgat : dp_mol->getStereoGroups()[ivj].getAtoms()) {
+            sgj.insert(dp_atoms[sgat->getIdx()].index);
+          }
+          if (sgi < sgj) {
+            return -1;
+          } else if (sgi > sgj) {
+            return 1;
+          }
         }
+      } else {
+        // if there's no stereogroup, then use whatever atom stereochem is
+        // specfied:
+        ivi = 0;
+        ivj = 0;
+        // can't actually use values here, because they are arbitrary
+        ivi = dp_atoms[i].atom->getChiralTag() != 0;
+        ivj = dp_atoms[j].atom->getChiralTag() != 0;
         if (ivi < ivj) {
           return -1;
         } else if (ivi > ivj) {
           return 1;
         }
+        // stereo set
+        if (ivi && ivj) {
+          if (ivi) {
+            ivi = getChiralRank(dp_mol, dp_atoms, i);
+          }
+          if (ivj) {
+            ivj = getChiralRank(dp_mol, dp_atoms, j);
+          }
+          if (ivi < ivj) {
+            return -1;
+          } else if (ivi > ivj) {
+            return 1;
+          }
+        }
       }
     }
 
diff --git a/Code/PgSQL/rdkit/expected/rdkit-91.out b/Code/PgSQL/rdkit/expected/rdkit-91.out
index 0c148561aaa..88fe41ff2ad 100644
--- a/Code/PgSQL/rdkit/expected/rdkit-91.out
+++ b/Code/PgSQL/rdkit/expected/rdkit-91.out
@@ -1340,15 +1340,15 @@ select mol_to_smarts(mol_adjust_query_properties('*c1ncc(*)cc1'::qmol));
 
 -- CXSmiles
 SELECT mol_to_smiles(mol_from_smiles('C[C@H](F)[C@H](C)[C@@H](C)Br |a:1,o1:3,5|'));
-         mol_to_smiles         
--------------------------------
- C[C@@H]([C@H](C)F)[C@@H](C)Br
+        mol_to_smiles         
+------------------------------
+ C[C@H](F)[C@@H](C)[C@H](C)Br
 (1 row)
 
 SELECT mol_to_cxsmiles(mol_from_smiles('C[C@H](F)[C@H](C)[C@@H](C)Br |a:1,o1:3,5|'));
-              mol_to_cxsmiles               
---------------------------------------------
- C[C@@H]([C@H](C)F)[C@@H](C)Br |a:2,o1:1,5|
+              mol_to_cxsmiles              
+-------------------------------------------
+ C[C@H](F)[C@@H](C)[C@H](C)Br |a:1,o1:3,5|
 (1 row)
 
 SELECT mol_to_cxsmarts(mol_from_smiles('C[C@H](F)[C@H](C)[C@@H](C)Br |a:1,o1:3,5|'));
@@ -1365,9 +1365,9 @@ SELECT mol_to_cxsmarts(qmol_from_smarts('C[C@H]([F,Cl,Br])[C@H](C)[C@@H](C)Br |a
 
 -- CXSmiles from mol_out
 SELECT mol_out(mol_from_smiles('C[C@H](F)[C@H](C)[C@@H](C)Br |a:1,o1:3,5|'));
-                  mol_out                   
---------------------------------------------
- C[C@@H]([C@H](C)F)[C@@H](C)Br |a:2,o1:1,5|
+                  mol_out                  
+-------------------------------------------
+ C[C@H](F)[C@@H](C)[C@H](C)Br |a:1,o1:3,5|
 (1 row)
 
 -- github #3688: bad input to qmol_from_ctab() crashes db
diff --git a/Docs/Book/RDKit_Book.rst b/Docs/Book/RDKit_Book.rst
index cf47872b7e0..dd74cfe52b1 100644
--- a/Docs/Book/RDKit_Book.rst
+++ b/Docs/Book/RDKit_Book.rst
@@ -2028,8 +2028,10 @@ Use cases
 
 The initial target is to not lose data on an ``V3k mol -> RDKit -> V3k mol`` round trip. Manipulation and depiction are future goals.
 
-It is possible to enumerate the elements of a ``StereoGroup`` using the function :py:func:`rdkit.Chem.EnumerateStereoisomers.EumerateStereoisomers`, which also
-preserves membership in the original ``StereoGroup``.
+It is possible to enumerate the elements of a ``StereoGroup`` using the function
+:py:func:`rdkit.Chem.EnumerateStereoisomers.EumerateStereoisomers`. Note that
+this removes the ``StereoGroup`` information from the products since they now
+correspond to specific molecules:
 
 .. doctest ::
 
@@ -2040,7 +2042,7 @@ preserves membership in the original ``StereoGroup``.
   [1]
   >>> from rdkit.Chem.EnumerateStereoisomers import EnumerateStereoisomers
   >>> [Chem.MolToCXSmiles(x) for x in EnumerateStereoisomers(m)]
-  ['C[C@@H](F)C[C@H](O)Cl |&1:1|', 'C[C@H](F)C[C@H](O)Cl |&1:1|']
+  ['C[C@@H](F)C[C@H](O)Cl', 'C[C@H](F)C[C@H](O)Cl']
 
 Reactions also preserve ``StereoGroup``s. Product atoms are included in the ``StereoGroup`` as long as the reaction doesn't create or destroy chirality at the atom.
 
diff --git a/ReleaseNotes.md b/ReleaseNotes.md
index 28bd502512e..38fcec68117 100644
--- a/ReleaseNotes.md
+++ b/ReleaseNotes.md
@@ -8,6 +8,7 @@
 ## Backwards incompatible changes
 
 - The ring-finding functions will now run even if the molecule already has ring information. Older versions of the RDKit would return whatever ring information was present, even if it had been generated using a different algorithm.
+- The canonical SMILES and CXSMILES generated for molecules with enhanced stereochemistry (stereo groups) is different than in previous releases. The enhanced stereochemistry information and the stereo groups themselves are now canonical. This does *not* affect molecules which do not have enhanced stereo and will not have any effect if you generate non-isomeric SMILES. This change also affects the output of the MolHash and RegistrationHash code when applied to molecules with enhanced stereo.
 
 ## Bug Fixes:
 
diff --git a/rdkit/Chem/RegistrationHash.py b/rdkit/Chem/RegistrationHash.py
index 3c69d4ec522..d8ee27da39c 100644
--- a/rdkit/Chem/RegistrationHash.py
+++ b/rdkit/Chem/RegistrationHash.py
@@ -127,6 +127,8 @@ def GetMolLayers(original_molecule: Chem.rdchem.Mol, data_field_names: Optional[
   mol = _RemoveUnnecessaryHs(original_molecule, preserve_stereogenic_hs=True)
   _StripAtomMapLabels(mol)
 
+  Chem.CanonicalizeEnhancedStereo(mol)
+
   formula = rdMolHash.MolHash(mol, rdMolHash.HashFunction.MolFormula)
   cxsmiles, canonical_mol = _CanonicalizeStereoGroups(mol)
   tautomer_hash = GetStereoTautomerHash(canonical_mol)
@@ -437,8 +439,9 @@ def _CanonicalizeStereoGroups(mol):
     compare the CXSMILES of those.
     """
 
-  if not len(mol.GetStereoGroups()):
-    return Chem.MolToCXSmiles(mol), mol
+  # if not len(mol.GetStereoGroups()):
+  Chem.CanonicalizeEnhancedStereo(mol)
+  return Chem.MolToCXSmiles(mol), mol
 
   mol = Chem.Mol(mol)
 


Test patch:
diff --git a/Code/GraphMol/ChemReactions/Wrap/testReactionWrapper.py b/Code/GraphMol/ChemReactions/Wrap/testReactionWrapper.py
index f6d76078ea5..4879a96869c 100644
--- a/Code/GraphMol/ChemReactions/Wrap/testReactionWrapper.py
+++ b/Code/GraphMol/ChemReactions/Wrap/testReactionWrapper.py
@@ -819,7 +819,7 @@ def test_reaction_preserves_stereo(self):
     -> preserve stereo group
     """
     reaction = '[C@:1]>>[C@:1]'
-    reactants = ['F[C@H](Cl)Br |o1:1|', 'F[C@@H](Cl)Br |&1:1|', 'FC(Cl)Br']
+    reactants = ['F[C@H](Cl)Br |o1:1|', 'F[C@H](Cl)Br |&1:1|', 'FC(Cl)Br']
     for reactant in reactants:
       products = _reactAndSummarize(reaction, reactant)
       self.assertEqual(products, reactant)
@@ -831,7 +831,7 @@ def test_reaction_ignores_stereo(self):
     -> preserve stereo group
     """
     reaction = '[C:1]>>[C:1]'
-    reactants = ['F[C@H](Cl)Br |o1:1|', 'F[C@@H](Cl)Br |&1:1|', 'FC(Cl)Br']
+    reactants = ['F[C@H](Cl)Br |o1:1|', 'F[C@H](Cl)Br |&1:1|', 'FC(Cl)Br']
     for reactant in reactants:
       products = _reactAndSummarize(reaction, reactant)
       self.assertEqual(products, reactant)
@@ -845,7 +845,7 @@ def test_reaction_inverts_stereo(self):
     reaction = '[C@:1]>>[C@@:1]'
 
     products = _reactAndSummarize(reaction, 'F[C@H](Cl)Br |o1:1|')
-    self.assertEqual(products, 'F[C@@H](Cl)Br |o1:1|')
+    self.assertEqual(products, 'F[C@H](Cl)Br |o1:1|')
     products = _reactAndSummarize(reaction, 'F[C@@H](Cl)Br |&1:1|')
     self.assertEqual(products, 'F[C@H](Cl)Br |&1:1|')
     products = _reactAndSummarize(reaction, 'FC(Cl)Br')
@@ -868,10 +868,10 @@ def test_reaction_destroys_stereo(self):
     reaction = '[C@:1]F>>[C:1]F'
     # Reaction destroys stereo (but preserves unaffected group
     products = _reactAndSummarize(reaction, 'F[C@H](Cl)[C@@H](Cl)Br |o1:1,&2:3|')
-    self.assertEqual(products, 'FC(Cl)[C@@H](Cl)Br |&1:3|')
+    self.assertEqual(products, 'FC(Cl)[C@H](Cl)Br |&1:3|')
     # Reaction destroys stereo (but preserves the rest of the group
     products = _reactAndSummarize(reaction, 'F[C@H](Cl)[C@@H](Cl)Br |&1:1,3|')
-    self.assertEqual(products, 'FC(Cl)[C@@H](Cl)Br |&1:3|')
+    self.assertEqual(products, 'FC(Cl)[C@H](Cl)Br |&1:3|')
 
   def test_reaction_defines_stereo(self):
     """
@@ -889,11 +889,11 @@ def test_reaction_defines_stereo(self):
 
     # Remove group with defined stereo
     products = _reactAndSummarize('[C:1]F>>[C@@:1]F', 'F[C@H](Cl)[C@@H](Cl)Br |o1:1,&2:3|')
-    self.assertEqual(products, 'F[C@@H](Cl)[C@@H](Cl)Br |&1:3|')
+    self.assertEqual(products, 'F[C@@H](Cl)[C@H](Cl)Br |&1:3|')
 
     # Remove atoms with defined stereo from group
     products = _reactAndSummarize('[C:1]F>>[C@@:1]F', 'F[C@H](Cl)[C@@H](Cl)Br |o1:1,3|')
-    self.assertEqual(products, 'F[C@@H](Cl)[C@@H](Cl)Br |o1:3|')
+    self.assertEqual(products, 'F[C@@H](Cl)[C@H](Cl)Br |o1:3|')
 
   def test_stereogroup_is_spectator_to_reaction(self):
     """
@@ -902,19 +902,19 @@ def test_stereogroup_is_spectator_to_reaction(self):
     """
     # 5a. Reaction preserves unrelated stereo
     products = _reactAndSummarize('[C@:1]F>>[C@:1]F', 'F[C@H](Cl)[C@@H](Cl)Br |o1:3|')
-    self.assertEqual(products, 'F[C@H](Cl)[C@@H](Cl)Br |o1:3|')
+    self.assertEqual(products, 'F[C@H](Cl)[C@H](Cl)Br |o1:3|')
     # 5b. Reaction ignores unrelated stereo'
     products = _reactAndSummarize('[C:1]F>>[C:1]F', 'F[C@H](Cl)[C@@H](Cl)Br |o1:3|')
-    self.assertEqual(products, 'F[C@H](Cl)[C@@H](Cl)Br |o1:3|')
+    self.assertEqual(products, 'F[C@H](Cl)[C@H](Cl)Br |o1:3|')
     # 5c. Reaction inverts unrelated stereo'
     products = _reactAndSummarize('[C@:1]F>>[C@@:1]F', 'F[C@H](Cl)[C@@H](Cl)Br |o1:3|')
-    self.assertEqual(products, 'F[C@@H](Cl)[C@@H](Cl)Br |o1:3|')
+    self.assertEqual(products, 'F[C@@H](Cl)[C@H](Cl)Br |o1:3|')
     # 5d. Reaction destroys unrelated stereo' 1:3|
     products = _reactAndSummarize('[C@:1]F>>[C:1]F', 'F[C@H](Cl)[C@@H](Cl)Br |o1:3|')
-    self.assertEqual(products, 'FC(Cl)[C@@H](Cl)Br |o1:3|')
+    self.assertEqual(products, 'FC(Cl)[C@H](Cl)Br |o1:3|')
     # 5e. Reaction assigns unrelated stereo'
     products = _reactAndSummarize('[C:1]F>>[C@@:1]F', 'F[C@H](Cl)[C@@H](Cl)Br |o1:3|')
-    self.assertEqual(products, 'F[C@@H](Cl)[C@@H](Cl)Br |o1:3|')
+    self.assertEqual(products, 'F[C@@H](Cl)[C@H](Cl)Br |o1:3|')
 
   def test_reaction_splits_stereogroup(self):
     """
@@ -935,8 +935,9 @@ def test_reaction_copies_stereogroup(self):
     products = _reactAndSummarize('[O:1].[C:2]=O>>[O:1][C:2][O:1]',
                                   'Cl[C@@H](Br)C[C@H](Br)CCO |&1:1,4|', 'CC(=O)C')
     # stereogroup manually checked, product SMILES assumed correct.
-    self.assertEqual(products,
-                     'CC(C)(OCC[C@@H](Br)C[C@@H](Cl)Br)OCC[C@@H](Br)C[C@@H](Cl)Br |&1:6,9,15,18|')
+    self.assertEqual(
+      products,
+      'CC(C)(OCC[C@H](Br)C[C@H](Cl)Br)OCC[C@H](Br)C[C@H](Cl)Br |&1:6,9,15,18|')
 
     # Stereogroup atoms are not in the reaction, but have multiple copies in the
     # product.
@@ -944,7 +945,7 @@ def test_reaction_copies_stereogroup(self):
                                   'Cl[C@@H](Br)C[C@H](Br)CCO |&1:1,4|', 'CC(=O)C')
     # stereogroup manually checked, product SMILES assumed correct.
     self.assertEqual(products,
-                     'CC(C)(OCC[C@@H](Br)C[C@@H](Cl)Br)OCC[C@@H](Br)C[C@@H](Cl)Br |&1:6,9,15,18|')
+                     'CC(C)(OCC[C@H](Br)C[C@H](Cl)Br)OCC[C@H](Br)C[C@H](Cl)Br |&1:6,9,15,18|')
 
   def test_github(self):
     rxn = rdChemReactions.ReactionFromSmarts('[C:2][C:3]>>[C:2][C][*:3]')
diff --git a/Code/GraphMol/ChemReactions/catch_tests.cpp b/Code/GraphMol/ChemReactions/catch_tests.cpp
index d3c219975f7..ffd9fecaee4 100644
--- a/Code/GraphMol/ChemReactions/catch_tests.cpp
+++ b/Code/GraphMol/ChemReactions/catch_tests.cpp
@@ -99,7 +99,7 @@ TEST_CASE("Github #2366 Enhanced Stereo", "[Reaction][StereoGroup][bug]") {
     auto p = prods[0][0];
 
     clearAtomMappingProps(*p);
-    CHECK(MolToCXSmiles(*p) == "FC(Cl)[C@@H](Cl)Br |&1:3|");
+    CHECK(MolToCXSmiles(*p) == "FC(Cl)[C@H](Cl)Br |&1:3|");
   }
   SECTION("Reaction splits StereoGroup") {
     ROMOL_SPTR mol("F[C@H](Cl)[C@@H](Cl)Br |&1:1,3|"_smiles);
@@ -120,7 +120,7 @@ TEST_CASE("Github #2366 Enhanced Stereo", "[Reaction][StereoGroup][bug]") {
     clearAtomMappingProps(*p0);
     clearAtomMappingProps(*p1);
     CHECK(MolToCXSmiles(*p0) == "O[C@H](F)Cl |&1:1|");
-    CHECK(MolToCXSmiles(*p1) == "O[C@@H](Cl)Br |&1:1|");
+    CHECK(MolToCXSmiles(*p1) == "O[C@H](Cl)Br |&1:1|");
   }
   SECTION("Reaction combines StereoGroups") {
     ROMOL_SPTR mol1("F[C@H](Cl)O |&1:1|"_smiles);
diff --git a/Code/GraphMol/MolHash/Wrap/testMolHash.py b/Code/GraphMol/MolHash/Wrap/testMolHash.py
index b7a3d22a96a..1d27c70a25e 100644
--- a/Code/GraphMol/MolHash/Wrap/testMolHash.py
+++ b/Code/GraphMol/MolHash/Wrap/testMolHash.py
@@ -40,11 +40,11 @@ def testCxSmiles(self):
       'C[C@@H](O)[C@@H](C)[C@@H](C)C[C@H](C1=CN=CN1)C1=CNC=N1 |o1:8,5,&1:1,3,r,c:11,18,t:9,15|')
 
     self.assertEqual(rdMolHash.MolHash(m, rdMolHash.HashFunction.HetAtomTautomer),
-                     'C[C@H]([C@@H](C)[O])[C@@H](C)CC([C]1[CH][N][CH][N]1)[C]1[CH][N][CH][N]1_3_0')
+                     'C[C@H](CC([C]1[CH][N][CH][N]1)[C]1[CH][N][CH][N]1)[C@@H](C)[C@H](C)[O]_3_0')
 
     self.assertEqual(
       rdMolHash.MolHash(m, rdMolHash.HashFunction.HetAtomTautomer, True),
-      'C[C@H]([C@@H](C)[O])[C@@H](C)CC([C]1[CH][N][CH][N]1)[C]1[CH][N][CH][N]1_3_0 |o1:5,&1:1,2|')
+      'C[C@H](CC([C]1[CH][N][CH][N]1)[C]1[CH][N][CH][N]1)[C@@H](C)[C@H](C)[O]_3_0 |o1:1,&1:14,16|')
 
 
 if __name__ == "__main__":
diff --git a/Code/GraphMol/MolHash/catch_tests.cpp b/Code/GraphMol/MolHash/catch_tests.cpp
index 227d92215ed..2e9b805978e 100644
--- a/Code/GraphMol/MolHash/catch_tests.cpp
+++ b/Code/GraphMol/MolHash/catch_tests.cpp
@@ -233,8 +233,7 @@ TEST_CASE("Github issues", "[molhash]") {
 TEST_CASE("MolHash with CX extensions", "[molhash]") {
   SECTION("Tautomer") {
     auto mol =
-        "C[C@@H](O)[C@@H](C)[C@@H](C)C[C@H](C1=CN=CN1)C1=CNC=N1 "
-        "|o1:8,5,&1:1,3,r,c:11,18,t:9,15|"_smiles;
+        "C[C@@H](O)[C@@H](C)[C@@H](C)C[C@H](C1=CN=CN1)C1=CNC=N1 |o1:8,5,&1:1,3,r,c:11,18,t:9,15|"_smiles;
     REQUIRE(mol);
 
     {
@@ -242,7 +241,7 @@ TEST_CASE("MolHash with CX extensions", "[molhash]") {
       auto hsh = MolHash::MolHash(&cp, MolHash::HashFunction::HetAtomTautomer);
       CHECK(
           hsh ==
-          "C[C@H]([C@@H](C)[O])[C@@H](C)CC([C]1[CH][N][CH][N]1)[C]1[CH][N][CH][N]1_3_0");
+          "C[C@H](CC([C]1[CH][N][CH][N]1)[C]1[CH][N][CH][N]1)[C@@H](C)[C@H](C)[O]_3_0");
     }
     {
       RWMol cp(*mol);
@@ -251,7 +250,7 @@ TEST_CASE("MolHash with CX extensions", "[molhash]") {
           MolHash::MolHash(&cp, MolHash::HashFunction::HetAtomTautomer, true);
       CHECK(
           hsh ==
-          "C[C@H]([C@@H](C)[O])[C@@H](C)CC([C]1[CH][N][CH][N]1)[C]1[CH][N][CH][N]1_3_0 |o1:5,&1:1,2|");
+          "C[C@H](CC([C]1[CH][N][CH][N]1)[C]1[CH][N][CH][N]1)[C@@H](C)[C@H](C)[O]_3_0 |o1:1,&1:14,16|");
     }
   }
   SECTION("no coordinates please") {
diff --git a/Code/GraphMol/SmilesParse/catch_tests.cpp b/Code/GraphMol/SmilesParse/catch_tests.cpp
index 2b3f384ae39..9c34ed7ed73 100644
--- a/Code/GraphMol/SmilesParse/catch_tests.cpp
+++ b/Code/GraphMol/SmilesParse/catch_tests.cpp
@@ -264,7 +264,7 @@ TEST_CASE("github #2257: writing cxsmiles", "[smiles][cxsmiles]") {
     auto mol = "C[C@H](F)[C@H](C)[C@@H](C)Br |a:1,o1:3,5|"_smiles;
     REQUIRE(mol);
     auto smi = MolToCXSmiles(*mol);
-    CHECK(smi == "C[C@@H]([C@H](C)F)[C@@H](C)Br |a:2,o1:1,5|");
+    CHECK(smi == "C[C@H](F)[C@@H](C)[C@H](C)Br |a:1,o1:3,5|");
   }
 
   SECTION("enhanced stereo 2") {
@@ -281,7 +281,7 @@ TEST_CASE("github #2257: writing cxsmiles", "[smiles][cxsmiles]") {
     auto smi = MolToCXSmiles(*mol);
     CHECK(
         smi ==
-        "C[C@@H]1[C@H](C)[C@H](C)N[C@H](C)[C@@H]1C1[C@@H](C)O[C@@H](C)[C@@H](C)[C@H]1C |a:9,o1:2,4,o2:14,16,&1:1,7,&2:11,18|");
+        "C[C@H]1N[C@H](C)[C@H](C2[C@H](C)O[C@H](C)[C@H](C)[C@@H]2C)[C@H](C)[C@H]1C |a:5,o1:1,18,o2:10,12,&1:3,16,&2:7,14|");
   }
 
   SECTION("enhanced stereo 4") {
@@ -2357,13 +2357,12 @@ TEST_CASE("Github #5683: SMARTS bond ordering should be the same as SMILES") {
   }
 }
 
-TEST_CASE("SMARTS for molecule with zero order bond should match itself")
-{
-    auto m = "CN(<-[Li+])C"_smiles;
-    m->getBondBetweenAtoms(1, 2)->setBondType(Bond::BondType::ZERO);
-    const auto sma = MolToSmarts(*m);
-    std::unique_ptr<ROMol> q{SmartsToMol(sma)};
-    SubstructMatchParameters ps;
-    ps.maxMatches = 1;
-    CHECK(SubstructMatch(*m, *q, ps).size() == 1);
+TEST_CASE("SMARTS for molecule with zero order bond should match itself") {
+  auto m = "CN(<-[Li+])C"_smiles;
+  m->getBondBetweenAtoms(1, 2)->setBondType(Bond::BondType::ZERO);
+  const auto sma = MolToSmarts(*m);
+  std::unique_ptr<ROMol> q{SmartsToMol(sma)};
+  SubstructMatchParameters ps;
+  ps.maxMatches = 1;
+  CHECK(SubstructMatch(*m, *q, ps).size() == 1);
 }
\ No newline at end of file
diff --git a/Code/JavaWrappers/gmwrapper/src-test/org/RDKit/WrapperTests.java b/Code/JavaWrappers/gmwrapper/src-test/org/RDKit/WrapperTests.java
index 93755c8c603..8402c6129a7 100644
--- a/Code/JavaWrappers/gmwrapper/src-test/org/RDKit/WrapperTests.java
+++ b/Code/JavaWrappers/gmwrapper/src-test/org/RDKit/WrapperTests.java
@@ -342,7 +342,7 @@ public void cdxmlReader() {
 	assertEquals(prods.size(), 1);
 	for(int idx = 0; idx < prods.size(); idx++) {
 	    if(idx == 0) {
-		assertEquals(prods.get(idx).MolToSmiles(true), "CC1(C)[C@H](C=C(Cl)Cl)[C@H]1C(=O)O[C@@H](C#N)c1cccc(Oc2ccccc2)c1");
+		assertEquals(prods.get(idx).MolToSmiles(true), "CC1(C)[C@H](C=C(Cl)Cl)[C@@H]1C(=O)O[C@H](C#N)c1cccc(Oc2ccccc2)c1");
 	    }
 	}
 
diff --git a/rdkit/Chem/EnumerateStereoisomers.py b/rdkit/Chem/EnumerateStereoisomers.py
index 6d56035bcad..3ea7e6164ba 100644
--- a/rdkit/Chem/EnumerateStereoisomers.py
+++ b/rdkit/Chem/EnumerateStereoisomers.py
@@ -325,7 +325,14 @@ def EnumerateStereoisomers(m, options=StereoEnumerationOptions(), verbose=False)
     for i in range(nCenters):
       flag = bool(bitflag & (1 << i))
       flippers[i].flip(flag)
-    isomer = Chem.Mol(tm)
+
+    # from this point on we no longer need the stereogroups (if any are there), so
+    # remove them:
+    if tm.GetStereoGroups():
+      isomer = Chem.RWMol(tm)
+      isomer.SetStereoGroups([])
+    else:
+      isomer = Chem.Mol(tm)
     Chem.SetDoubleBondNeighborDirections(isomer)
     isomer.ClearComputedProps(includeRings=False)
 
@@ -355,3 +362,18 @@ def EnumerateStereoisomers(m, options=StereoEnumerationOptions(), verbose=False)
         break
     elif verbose:
       print("%s    failed to embed" % (Chem.MolToSmiles(isomer, isomericSmiles=True)))
+
+
+#------------------------------------
+#
+#  doctest boilerplate
+#
+def _test():
+  import doctest, sys
+  return doctest.testmod(sys.modules["__main__"])
+
+
+if __name__ == '__main__':
+  import sys
+  failed, tried = _test()
+  sys.exit(failed)
diff --git a/rdkit/Chem/UnitTestMol3D.py b/rdkit/Chem/UnitTestMol3D.py
index e191f737ead..4070cc948be 100755
--- a/rdkit/Chem/UnitTestMol3D.py
+++ b/rdkit/Chem/UnitTestMol3D.py
@@ -101,9 +101,8 @@ def testTorsionFingerprints(self):
     self.assertAlmostEqual(tfd, 0.0691, 4)
 
     # exactly equivalent to the above since the mols each only have one conformer
-    tfd = TorsionFingerprints.GetBestTFDBetweenMolecules(mol2,mol)
-    self.assertAlmostEqual(tfd,0.0691,4)
-
+    tfd = TorsionFingerprints.GetBestTFDBetweenMolecules(mol2, mol)
+    self.assertAlmostEqual(tfd, 0.0691, 4)
 
     mol.AddConformer(mol2.GetConformer(), assignId=True)
     mol.AddConformer(mol2.GetConformer(), assignId=True)
@@ -114,9 +113,8 @@ def testTorsionFingerprints(self):
     tfdmat = TorsionFingerprints.GetTFDMatrix(mol)
     self.assertEqual(len(tfdmat), 3)
 
-    tfd = TorsionFingerprints.GetBestTFDBetweenMolecules(mol2,mol)
-    self.assertAlmostEqual(tfd,0,4)
-
+    tfd = TorsionFingerprints.GetBestTFDBetweenMolecules(mol2, mol)
+    self.assertAlmostEqual(tfd, 0, 4)
 
   def testTorsionFingerprintsAtomReordering(self):
     # we use the xray structure from the paper (JCIM, 52, 1499, 2012): 1DWD
@@ -392,6 +390,9 @@ def testEnumerateStereoisomersOnlyEnhancedStereo(self):
     # switches the centers linked by an "OR", but not the absolute group
     self.assertEqual(smiles, {r'C[C@H]([C@@H](C)F)[C@@H](C)Br', r'C[C@@H]([C@H](C)F)[C@@H](C)Br'})
 
+    # we need the SMILES without the influence of the stereo groups:
+    mol = Chem.RWMol(mol)
+    mol.SetStereoGroups([])
     original_smiles = Chem.MolToSmiles(mol)
     self.assertIn(original_smiles, smiles)
 
@@ -406,6 +407,7 @@ def testNoExtrasIfEnumeratingAllWithEnhancedStereo(self):
 
     opts = AllChem.StereoEnumerationOptions(onlyUnassigned=False, unique=False)
     smiles = [Chem.MolToSmiles(m) for m in AllChem.EnumerateStereoisomers(mol, opts)]
+    print('!!!!', smiles)
     self.assertEqual(len(smiles), len(set(smiles)))
     self.assertEqual(len(smiles), 2**3)
 
@@ -461,6 +463,13 @@ def testTryEmbeddingManyChirals(self):
     opts = AllChem.StereoEnumerationOptions(tryEmbedding=True, maxIsomers=2)
     self.assertEqual(len(list(AllChem.EnumerateStereoisomers(mol, options=opts))), 2)
 
+  def testGithub6045(self):
+    mol = Chem.MolFromSmiles('O[C@H](Br)[C@H](F)C |&1:1,3|')
+    prods = list(AllChem.EnumerateStereoisomers(mol))
+    self.assertEqual(len(prods), 2)
+    for prod in prods:
+      self.assertEqual(len(prod.GetStereoGroups()), 0)
+
 
 if __name__ == '__main__':
   unittest.main()
diff --git a/rdkit/Chem/UnitTestRegistrationHash.py b/rdkit/Chem/UnitTestRegistrationHash.py
index 479c9956b5f..9edb6b3b50a 100644
--- a/rdkit/Chem/UnitTestRegistrationHash.py
+++ b/rdkit/Chem/UnitTestRegistrationHash.py
@@ -100,13 +100,13 @@ def test_example_structure(self):
 
         layers = hash_sdf(structure)
         expected_layers = {
-                HashLayer.CANONICAL_SMILES: "C[C@@H]1C[C@H](C)Nc2cc3nc(O)cc(C(F)(F)F)c3cc21 |o1:1,3|",
+                HashLayer.CANONICAL_SMILES: "C[C@H]1C[C@@H](C)c2cc3c(C(F)(F)F)cc(O)nc3cc2N1 |o1:1,3|",
                 HashLayer.ESCAPE: "",
                 HashLayer.FORMULA: "C15H15F3N2O",
                 HashLayer.NO_STEREO_SMILES: "CC1CC(C)c2cc3c(C(F)(F)F)cc(O)nc3cc2N1",
                 HashLayer.NO_STEREO_TAUTOMER_HASH: "CC1CC(C)[C]2[CH][C]3[C]([CH][C]2[N]1)[N][C]([O])[CH][C]3C(F)(F)F_2_0",
                 HashLayer.SGROUP_DATA: "[]",
-                HashLayer.TAUTOMER_HASH: "C[C@@H]1C[C@H](C)[N][C]2[CH][C]3[N][C]([O])[CH][C](C(F)(F)F)[C]3[CH][C]21_2_0 |o1:1,3|",
+                HashLayer.TAUTOMER_HASH: "C[C@H]1C[C@@H](C)[C]2[CH][C]3[C]([CH][C]2[N]1)[N][C]([O])[CH][C]3C(F)(F)F_2_0 |o1:1,3|",
         }
         self.assertEqual(layers, expected_layers)
 
@@ -276,6 +276,7 @@ def test_enhanced_stereo_canonicalizer(self):
             ),
         )
         for mols_smis in groups:
+            print()
             csmis = set()
             for smi in mols_smis:
                 mol = Chem.MolFromSmiles(smi)
@@ -631,11 +632,7 @@ def testIodine(self):
         """Does stereo group canonicalization mess up isotopes?"""
         mol = Chem.MolFromSmiles('CC[C@H](C)[999C@H](C)O  |o1:2,4|')
         layers = RegistrationHash.GetMolLayers(mol)
-        self.assertEqual(layers[HashLayer.CANONICAL_SMILES], 'CC[C@@H](C)[999C@@H](C)O |o1:2,4|')
-
-        mol = Chem.MolFromSmiles('CC[C@H](C)[1000C@H](C)O  |o1:2,4|')
-        with self.assertRaisesRegex(ValueError, expected_regex=r'does not support isotopes above 999'):
-            layers = RegistrationHash.GetMolLayers(mol)
+        self.assertEqual(layers[HashLayer.CANONICAL_SMILES], 'CC[C@H](C)[999C@H](C)O |o1:2,4|')
 
     def testBadBondDir(self):
         """"""
diff --git a/rdkit/Chem/test_list.py b/rdkit/Chem/test_list.py
index ef18e0a2e29..7121e573cc5 100755
--- a/rdkit/Chem/test_list.py
+++ b/rdkit/Chem/test_list.py
@@ -10,6 +10,7 @@
          ("python", "UnitTestFunctionalGroups.py", {}), ("python", "UnitTestCrippen.py", {}),
          ("python", "UnitTestPandasTools.py", {}), ("python", "UnitTestDocTestsChem.py", {}),
          ("python", "UnitTestFeatFinderCLI.py", {}), ("python", "UnitTestQED.py", {}),
+         ("python", "BRICS.py", {}), ("python", "EnumerateStereoisomers.py", {}),
          ("python", "UnitTestSaltRemover.py", {}), ("python", "test_list.py", {
            'dir': 'AtomPairs'
          }), ("python", "test_list.py", {
@@ -40,8 +41,7 @@
            'dir': 'Features'
          }), ("python", "test_list.py", {
            'dir': 'MolStandardize'
-         }), 
-         ("python", "UnitTestRegistrationHash.py", {})]
+         }), ("python", "UnitTestRegistrationHash.py", {})]
 
 # only attempt the MolKey tests if we have the pre-reqs:
 try: