Add canonicalization of stereo groups (enhanced stereo) (rdkit/rdkit#6051)

# 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.

EnumerateStereoisomers should remove non-abs enhanced stereo from results

**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

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)

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:


Fixes #5883 (rdkit/rdkit#6189)

The fix here is to recognize that the expected chiral volume for three-coordinate atoms is smaller than that for four-coordinate atoms.
The corrected value here is empirical

Conformer Generation Fails for three-coordinate Ns with specified stereo

**Describe the bug**
RDKit doesn't seem to be able to generate conformers for a three-membered ring with two nitrogen atoms if stereocenters are specified.

**To Reproduce**

```python
from rdkit.Chem import AllChem

mol = AllChem.AddHs(AllChem.MolFromSmiles("N1NC1"))

c1 = AllChem.Mol(mol, quickCopy=False)
AllChem.AssignStereochemistry(c1, flagPossibleStereoCenters=False)

for isomer in AllChem.EnumerateStereoisomers(c1):
    AllChem.EmbedMolecule(isomer)
    assert isomer.GetNumConformers() == 1

c2 = AllChem.Mol(mol, quickCopy=False)
AllChem.AssignStereochemistry(c2, flagPossibleStereoCenters=True)  # flagPossibleStereoCenters

for isomer in AllChem.EnumerateStereoisomers(c2):
    AllChem.EmbedMolecule(isomer)
    assert isomer.GetNumConformers() == 1  # Fails
```

**Configuration**
 - RDKit version: 2022.09.1, py38h5acb366_1, conda-forge
 - OS: Ubuntu 20.04

diff --git a/Code/GraphMol/DistGeomHelpers/Embedder.cpp b/Code/GraphMol/DistGeomHelpers/Embedder.cpp
index d1bf33610ab..6613466a846 100644
--- a/Code/GraphMol/DistGeomHelpers/Embedder.cpp
+++ b/Code/GraphMol/DistGeomHelpers/Embedder.cpp
@@ -34,13 +34,14 @@
 #include <boost/dynamic_bitset.hpp>
 #include <iomanip>
 #include <RDGeneral/RDThreads.h>
+#include <typeinfo>

 #ifdef RDK_BUILD_THREADSAFE_SSS
 #include <future>
 #include <mutex>
 #endif

-//#define DEBUG_EMBEDDING 1
+// #define DEBUG_EMBEDDING 1

 #ifdef M_PI_2
 #undef M_PI_2
@@ -1016,7 +1017,13 @@ void findChiralSets(const ROMol &mol, DistGeom::VECT_CHIRALSET &chiralCenters,
         // we should at least have 3 though
         CHECK_INVARIANT(nbrs.size() >= 3, "Cannot be a chiral center");

+        double volLowerBound = 5.0;
+        double volUpperBound = 100.0;
+
         if (nbrs.size() < 4) {
+          // we get lower volumes if there are three neighbors,
+          //  this was github #5883
+          volLowerBound = 2.0;
           nbrs.insert(nbrs.end(), atom->getIdx());
         }

@@ -1024,13 +1031,15 @@ void findChiralSets(const ROMol &mol, DistGeom::VECT_CHIRALSET &chiralCenters,
         // volume
         if (chiralType == Atom::CHI_TETRAHEDRAL_CCW) {
           // positive chiral volume
-          auto *cset = new DistGeom::ChiralSet(atom->getIdx(), nbrs[0], nbrs[1],
-                                               nbrs[2], nbrs[3], 5.0, 100.0);
+          auto *cset =
+              new DistGeom::ChiralSet(atom->getIdx(), nbrs[0], nbrs[1], nbrs[2],
+                                      nbrs[3], volLowerBound, volUpperBound);
           DistGeom::ChiralSetPtr cptr(cset);
           chiralCenters.push_back(cptr);
         } else if (chiralType == Atom::CHI_TETRAHEDRAL_CW) {
-          auto *cset = new DistGeom::ChiralSet(atom->getIdx(), nbrs[0], nbrs[1],
-                                               nbrs[2], nbrs[3], -100.0, -5.0);
+          auto *cset =
+              new DistGeom::ChiralSet(atom->getIdx(), nbrs[0], nbrs[1], nbrs[2],
+                                      nbrs[3], -volUpperBound, -volLowerBound);
           DistGeom::ChiralSetPtr cptr(cset);
           chiralCenters.push_back(cptr);
         } else {
diff --git a/ReleaseNotes.md b/ReleaseNotes.md
index 7597e8342ec..ed8dcd520ab 100644
--- a/ReleaseNotes.md
+++ b/ReleaseNotes.md
@@ -11,6 +11,8 @@
 - 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.
 - The doIsomericSmiles parameter in Java and C# ROMol.MolToSmiles() now defaults to true (previously it was false), thus aligning to the C++ and Python behavior.
 - Double bonds which are marked as crossed (i.e. `bond.GetBondDir() == Bond.BondDir.EITHERDOUBLE`) now have their BondStereo set to `Bond.BondStereo.STEREOANY` and the BondDir information removed by default when molecules are parsed or `AssignStereochemistry()` is called with the `cleanIt` argument set to True.
+- The conformers generated for molecules with three-coordinate chiral centers will be somewhat different due to the fix for #5883.
+

 ## Bug Fixes:

diff --git a/Code/GraphMol/DistGeomHelpers/Wrap/testDistGeom.py b/Code/GraphMol/DistGeomHelpers/Wrap/testDistGeom.py
index 0af5cd38b23..7cce5a0827f 100644
--- a/Code/GraphMol/DistGeomHelpers/Wrap/testDistGeom.py
+++ b/Code/GraphMol/DistGeomHelpers/Wrap/testDistGeom.py
@@ -274,7 +274,7 @@ def test6Chirality(self):
     self.assertTrue(nPos > 0)
     self.assertTrue(nNeg > 0)

-    tgtVol = 5.0
+    tgtVol = 3.0
     for i in range(10):
       smiles = "Cl[C@H](F)Br"
       mol = Chem.MolFromSmiles(smiles)
diff --git a/Code/GraphMol/DistGeomHelpers/catch_tests.cpp b/Code/GraphMol/DistGeomHelpers/catch_tests.cpp
index 695a2d76981..2f3bb6df834 100644
--- a/Code/GraphMol/DistGeomHelpers/catch_tests.cpp
+++ b/Code/GraphMol/DistGeomHelpers/catch_tests.cpp
@@ -589,32 +589,29 @@ TEST_CASE("double bond stereo not honored in conformer generator") {
   }
 }

-TEST_CASE("tracking failure causes") {
-  SECTION("basics") {
-    // auto mol = "CCNS(=O)(=O)c1ccccc1"_smiles;
-    // auto mol = "c2cccc3c2OC2=CC=CC[C@H]23"_smiles;
+TEST_CASE("tracking failure causes"){SECTION("basics"){
     auto mol =
         "C=CC1=C(N)Oc2cc1c(-c1cc(C(C)O)cc(=O)cc1C1NCC(=O)N1)c(OC)c2OC"_smiles;
-    REQUIRE(mol);
-    MolOps::addHs(*mol);
-    DGeomHelpers::EmbedParameters ps = DGeomHelpers::ETKDGv3;
-    ps.randomSeed = 0xf00d;
-    ps.trackFailures = true;
-    ps.maxIterations = 50;
-    ps.randomSeed = 42;
-    auto cid = DGeomHelpers::EmbedMolecule(*mol, ps);
-    CHECK(cid < 0);
-
-    CHECK(ps.failures[DGeomHelpers::EmbedFailureCauses::INITIAL_COORDS] > 5);
-    CHECK(ps.failures[DGeomHelpers::EmbedFailureCauses::ETK_MINIMIZATION] > 10);
-
-    auto fail_cp = ps.failures;
-    // make sure we reset the counts each time
-    cid = DGeomHelpers::EmbedMolecule(*mol, ps);
-    CHECK(ps.failures == fail_cp);
-  }
-  SECTION("chirality") {
-    auto mol = R"CTAB(
+REQUIRE(mol);
+MolOps::addHs(*mol);
+DGeomHelpers::EmbedParameters ps = DGeomHelpers::ETKDGv3;
+ps.randomSeed = 0xf00d;
+ps.trackFailures = true;
+ps.maxIterations = 50;
+ps.randomSeed = 42;
+auto cid = DGeomHelpers::EmbedMolecule(*mol, ps);
+CHECK(cid < 0);
+
+CHECK(ps.failures[DGeomHelpers::EmbedFailureCauses::INITIAL_COORDS] > 5);
+CHECK(ps.failures[DGeomHelpers::EmbedFailureCauses::ETK_MINIMIZATION] > 10);
+
+auto fail_cp = ps.failures;
+// make sure we reset the counts each time
+cid = DGeomHelpers::EmbedMolecule(*mol, ps);
+CHECK(ps.failures == fail_cp);
+}
+SECTION("chirality") {
+  auto mol = R"CTAB(
   Ketcher  1102315302D 1   1.00000     0.00000     0

  10 11  0  0  1  0  0  0  0  0999 V2000
@@ -641,39 +638,74 @@ TEST_CASE("tracking failure causes") {
   4 10  1  1     0  0
 M  END
 )CTAB"_ctab;
+  REQUIRE(mol);
+  MolOps::addHs(*mol);
+  DGeomHelpers::EmbedParameters ps = DGeomHelpers::ETKDGv3;
+  ps.randomSeed = 0xf00d;
+  ps.trackFailures = true;
+  ps.maxIterations = 50;
+  auto cid = DGeomHelpers::EmbedMolecule(*mol, ps);
+  CHECK(cid < 0);
+  CHECK(ps.failures[DGeomHelpers::EmbedFailureCauses::INITIAL_COORDS] > 5);
+  CHECK(ps.failures[DGeomHelpers::EmbedFailureCauses::FINAL_CHIRAL_BOUNDS] > 5);
+}
+
+#ifdef RDK_TEST_MULTITHREADED
+SECTION("multithreaded") {
+  auto mol =
+      "C=CC1=C(N)Oc2cc1c(-c1cc(C(C)O)cc(=O)cc1C1NCC(=O)N1)c(OC)c2OC"_smiles;
+  REQUIRE(mol);
+  MolOps::addHs(*mol);
+  DGeomHelpers::EmbedParameters ps = DGeomHelpers::ETKDGv3;
+  ps.randomSeed = 0xf00d;
+  ps.trackFailures = true;
+  ps.maxIterations = 10;
+  ps.randomSeed = 42;
+  auto cids = DGeomHelpers::EmbedMultipleConfs(*mol, 20, ps);
+
+  DGeomHelpers::EmbedParameters ps2 = ps;
+  ps2.numThreads = 4;
+
+  auto cids2 = DGeomHelpers::EmbedMultipleConfs(*mol, 20, ps2);
+  CHECK(cids2 == cids);
+
+  CHECK(ps.failures == ps2.failures);
+}
+#endif
+}
+
+TEST_CASE("Github #5883: confgen failing for chiral N in a three ring") {
+  SECTION("basics1") {
+    auto mol = "N1[C@H-]C1"_smiles;
     REQUIRE(mol);
     MolOps::addHs(*mol);
+    mol->getAtomWithIdx(1)->setChiralTag(Atom::ChiralType::CHI_TETRAHEDRAL_CCW);
     DGeomHelpers::EmbedParameters ps = DGeomHelpers::ETKDGv3;
-    ps.randomSeed = 0xf00d;
-    ps.trackFailures = true;
-    ps.maxIterations = 50;
+    ps.randomSeed = 42;
+    ps.maxIterations = 1;
     auto cid = DGeomHelpers::EmbedMolecule(*mol, ps);
-    CHECK(cid < 0);
-    CHECK(ps.failures[DGeomHelpers::EmbedFailureCauses::INITIAL_COORDS] > 5);
-    CHECK(ps.failures[DGeomHelpers::EmbedFailureCauses::FINAL_CHIRAL_BOUNDS] >
-          5);
+    CHECK(cid >= 0);
   }
-
-#ifdef RDK_TEST_MULTITHREADED
-  SECTION("multithreaded") {
-    auto mol =
-        "C=CC1=C(N)Oc2cc1c(-c1cc(C(C)O)cc(=O)cc1C1NCC(=O)N1)c(OC)c2OC"_smiles;
+  SECTION("basics2") {
+    auto mol = "N1[N@H]C1"_smiles;
     REQUIRE(mol);
     MolOps::addHs(*mol);
+    mol->getAtomWithIdx(1)->setChiralTag(Atom::ChiralType::CHI_TETRAHEDRAL_CCW);
     DGeomHelpers::EmbedParameters ps = DGeomHelpers::ETKDGv3;
-    ps.randomSeed = 0xf00d;
-    ps.trackFailures = true;
-    ps.maxIterations = 10;
     ps.randomSeed = 42;
-    auto cids = DGeomHelpers::EmbedMultipleConfs(*mol, 20, ps);
-
-    DGeomHelpers::EmbedParameters ps2 = ps;
-    ps2.numThreads = 4;
-
-    auto cids2 = DGeomHelpers::EmbedMultipleConfs(*mol, 20, ps2);
-    CHECK(cids2 == cids);
-
-    CHECK(ps.failures == ps2.failures);
+    ps.maxIterations = 1;
+    auto cid = DGeomHelpers::EmbedMolecule(*mol, ps);
+    CHECK(cid >= 0);
+  }
+  SECTION("no ring") {
+    auto mol = "N[C@H-]C"_smiles;
+    REQUIRE(mol);
+    MolOps::addHs(*mol);
+    mol->getAtomWithIdx(1)->setChiralTag(Atom::ChiralType::CHI_TETRAHEDRAL_CCW);
+    DGeomHelpers::EmbedParameters ps = DGeomHelpers::ETKDGv3;
+    ps.randomSeed = 42;
+    ps.maxIterations = 1;
+    auto cid = DGeomHelpers::EmbedMolecule(*mol, ps);
+    CHECK(cid >= 0);
   }
-#endif
 }
\ No newline at end of file


Fix/github6138 (rdkit/rdkit#6193)

Fixed #6138

There are a few atom properties that are not pickled properly after ChemReaction::validate has been called.

These are

 _QueryIsotope
 _QueryFormalCharge

and the like.

This ensures they are pickled if the reaction has been initialized.

Round-tripping a reaction through pickle changes the outputs from RunReactants

**Describe the bug**

RDKit's reaction objects can be pickled/unpickled, but in some cases running the unpickled reaction can produce slightly different results than it did before the pickling. Interestingly, in my example it only happens if the reaction was ran at least once _before_ pickling, otherwise there are no issues.

**To Reproduce**
```python
import pickle
from rdkit import Chem
from rdkit.Chem import AllChem

mol = Chem.MolFromSmiles("COc1ccccc1Oc1nc(Nc2cc(C)[nH]n2)cc2ccccc12")
rxn = AllChem.ReactionFromSmarts("([c:1]:[n&H1&+0&D2:3]:[n:2])>>([c:1]:[n&H0&+0&D3:3](:[n:2])-C1-C-C-C-C-O-1)")

def run(r):
  return Chem.MolToSmiles(r.RunReactants((mol,))[0][0])

pickle.dump(rxn, open("tmp.pkl", "wb"))
rxn_reloaded = pickle.load(open("tmp.pkl", "rb"))

print(run(rxn))
print(run(rxn_reloaded))

pickle.dump(rxn, open("tmp_after.pkl", "wb"))
rxn_reloaded_after_use = pickle.load(open("tmp_after.pkl", "rb"))

print(run(rxn_reloaded_after_use))
```

Outputs:
```
COc1ccccc1Oc1nc(Nc2cc(C)n(C3CCCCO3)n2)cc2ccccc12
COc1ccccc1Oc1nc(Nc2cc(C)n(C3CCCCO3)n2)cc2ccccc12
COc1ccccc1Oc1nc(Nc2cc(C)[nH](C3CCCCO3)n2)cc2ccccc12
```

While the original SMILES can be correctly parsed by RDKit, the one obtained with the unpickled reaction can no longer be parsed.

**Expected behavior**

Result after round-tripping through pickle should be the same.

**Configuration (please complete the following information):**
 - RDKit version: 2021.09.4
 - OS: Ubuntu 16.04.6 LTS
 - Python version: 3.9.7
 - Are you using conda? Yes
 - If you are using conda, which channel did you install the rdkit from? conda-forge

diff --git a/Code/GraphMol/ChemReactions/ReactionPickler.cpp b/Code/GraphMol/ChemReactions/ReactionPickler.cpp
index 9450fbebfc3..9a4822e43a6 100644
--- a/Code/GraphMol/ChemReactions/ReactionPickler.cpp
+++ b/Code/GraphMol/ChemReactions/ReactionPickler.cpp
@@ -146,15 +146,18 @@ void ReactionPickler::_pickle(const ChemicalReaction *rxn, std::ostream &ss,
   streamWrite(ss, BEGINREACTANTS);
   for (auto tmpl = rxn->beginReactantTemplates();
        tmpl != rxn->endReactantTemplates(); ++tmpl) {
-    MolPickler::pickleMol(tmpl->get(), ss);
+    auto props = rxn->df_needsInit ? PicklerOps::PropertyPickleOptions::NoProps :
+      PicklerOps::PropertyPickleOptions::AllProps;
+    MolPickler::pickleMol(tmpl->get(), ss, props);
   }
   streamWrite(ss, ENDREACTANTS);

   streamWrite(ss, BEGINPRODUCTS);
   for (auto tmpl = rxn->beginProductTemplates();
        tmpl != rxn->endProductTemplates(); ++tmpl) {
-    MolPickler::pickleMol(tmpl->get(), ss,
-                          PicklerOps::PropertyPickleOptions::AtomProps);
+    auto props = rxn->df_needsInit ? PicklerOps::PropertyPickleOptions::AtomProps :
+      PicklerOps::PropertyPickleOptions::AllProps;
+    MolPickler::pickleMol(tmpl->get(), ss, props);
   }
   streamWrite(ss, ENDPRODUCTS);

@@ -162,7 +165,8 @@ void ReactionPickler::_pickle(const ChemicalReaction *rxn, std::ostream &ss,
     streamWrite(ss, BEGINAGENTS);
     for (auto tmpl = rxn->beginAgentTemplates();
          tmpl != rxn->endAgentTemplates(); ++tmpl) {
-      MolPickler::pickleMol(tmpl->get(), ss);
+      // reagents don't have private properties set during initialization
+      MolPickler::pickleMol(tmpl->get(), ss, PicklerOps::PropertyPickleOptions::AtomProps);
     }
     streamWrite(ss, ENDAGENTS);
   }

diff --git a/Code/GraphMol/ChemReactions/Wrap/testReactionWrapper.py b/Code/GraphMol/ChemReactions/Wrap/testReactionWrapper.py
index 4879a96869c..87011e6b296 100644
--- a/Code/GraphMol/ChemReactions/Wrap/testReactionWrapper.py
+++ b/Code/GraphMol/ChemReactions/Wrap/testReactionWrapper.py
@@ -38,7 +38,7 @@

 from rdkit import rdBase
 from rdkit import Chem
-from rdkit.Chem import rdChemReactions
+from rdkit.Chem import AllChem, rdChemReactions
 from rdkit import Geometry
 from rdkit import RDConfig
 from rdkit.Chem.SimpleEnum import Enumerator
@@ -1082,6 +1082,21 @@ def testV3000Reactions(self):

     self.assertEqual(reaction.GetNumReactantTemplates(), reaction2.GetNumReactantTemplates())

+  def testGithub6138(self):
+    mol = Chem.MolFromSmiles("COc1ccccc1Oc1nc(Nc2cc(C)[nH]n2)cc2ccccc12")
+    rxn = AllChem.ReactionFromSmarts("([c:1]:[n&H1&+0&D2:3]:[n:2])>>([c:1]:[n&H0&+0&D3:3](:[n:2])-C1-C-C-C-C-O-1)")
+
+    def run(r):
+      return Chem.MolToSmiles(r.RunReactants((mol,))[0][0])
+
+    rxn_reloaded = pickle.loads(pickle.dumps(rxn))
+
+    res1 = Chem.MolToSmiles(rxn.RunReactants((mol,))[0][0])
+    res2 = Chem.MolToSmiles(rxn_reloaded.RunReactants((mol,))[0][0])
+    rxn_reloaded_after_use = pickle.loads(pickle.dumps(rxn))
+    res3 = Chem.MolToSmiles(rxn_reloaded_after_use.RunReactants((mol,))[0][0])
+    self.assertEqual(res1, res2)
+    self.assertEqual(res1, res3)

 if __name__ == '__main__':
   unittest.main(verbosity=True)
diff --git a/Code/GraphMol/ChemReactions/testReaction.cpp b/Code/GraphMol/ChemReactions/testReaction.cpp
index 6c33c23eab6..3466cc3e0a2 100644
--- a/Code/GraphMol/ChemReactions/testReaction.cpp
+++ b/Code/GraphMol/ChemReactions/testReaction.cpp
@@ -7775,6 +7775,26 @@ void testChemicalReactionCopyAssignment() {
   delete rxn2;
 }

+void testGithub6138() {
+  // Pickling reactions removed some of their properties set after reaction
+    // initialization
+  auto rxn_smarts = "[c:1]:[n&H1&+0&D2:3]:[n:2]>>[c:1]:[3n&H0&+0&D3:3]:[2n:2]";
+  std::unique_ptr<ChemicalReaction> rxn(RxnSmartsToChemicalReaction(rxn_smarts));
+  ROMOL_SPTR mol("c1cn[nH]c1"_smiles);
+  rxn->initReactantMatchers();
+  MOL_SPTR_VECT reacts;
+  reacts.push_back(mol);
+  auto prods = rxn->runReactants(reacts);
+  std::string pkl;
+  ReactionPickler::pickleReaction(*rxn, pkl);
+  std::unique_ptr<ChemicalReaction> lrxn(new ChemicalReaction());
+  ReactionPickler::reactionFromPickle(pkl, lrxn.get());
+  auto prods2 = lrxn->runReactants(reacts);
+  auto s1 =MolToSmiles(*prods[0][0]);
+  auto s2 =MolToSmiles(*prods2[0][0]);
+  TEST_ASSERT(s1 == s2);
+}
+
 int main() {
   RDLog::InitLogs();

@@ -7874,6 +7894,7 @@ int main() {
   testGithub4410();
   testMultiTemplateRxnQueries();
   testChemicalReactionCopyAssignment();
+  testGithub6138();

   BOOST_LOG(rdInfoLog)
       << "*******************************************************\n";


Do not include dative bonds in ring finding by default (rdkit/rdkit#6231)

Fixes #6058

The fix here is to add a new `includeDativeBonds` option to the ring-finding code which is disabled by default (i.e. dative bonds are not included in ring finding by default).

This necessitates a small tweak to the 2D coordinate generation code, which should take dative bonds into account (at least for some systems, it might be worth coming back to this at some point and ignoring the three-rings formed by haptic systems in the depiction code).

Bad drawing of ferrocene

-----------------------------------------

**Describe the bug**
Drawing a representation of ferrocene where the bonds to the Fe are represented by dative bonds from the relevant carbon atoms gives rise to double bonds in ugly places.

**To Reproduce**
```
from rdkit import Chem
from rdkit.Chem.Draw import rdMolDraw2D, rdDepictor

molblock = """ferrocene
     RDKit          2D

  0  0  0  0  0  0  0  0  0  0999 V3000
M  V30 BEGIN CTAB
M  V30 COUNTS 11 20 0 0 0
M  V30 BEGIN ATOM
M  V30 1 C -34.237900 13.261800 0.000000 0 CHG=-1
M  V30 2 C -35.057400 14.266900 0.000000 0
M  V30 3 C -37.855700 14.284400 0.000000 0
M  V30 4 C -38.759100 13.293300 0.000000 0
M  V30 5 C -36.552000 12.683200 0.000000 0
M  V30 6 C -34.152900 8.363300 0.000000 0 CHG=-1
M  V30 7 C -34.972400 9.368400 0.000000 0
M  V30 8 C -37.770600 9.385900 0.000000 0
M  V30 9 C -38.674100 8.394800 0.000000 0
M  V30 10 C -36.450300 7.767900 0.000000 0
M  V30 11 Fe -36.435100 10.904500 0.000000 0 CHG=2 VAL=10
M  V30 END ATOM
M  V30 BEGIN BOND
M  V30 1 1 1 5
M  V30 2 2 4 5
M  V30 3 1 4 3
M  V30 4 2 2 3
M  V30 5 1 1 2
M  V30 6 1 6 10
M  V30 7 2 9 10
M  V30 8 1 9 8
M  V30 9 2 7 8
M  V30 10 1 6 7
M  V30 11 9 2 11
M  V30 12 9 3 11
M  V30 13 9 4 11
M  V30 14 9 5 11
M  V30 15 9 1 11
M  V30 16 9 7 11
M  V30 17 9 8 11
M  V30 18 9 9 11
M  V30 19 9 10 11
M  V30 20 9 6 11
M  V30 END BOND
M  V30 END CTAB
M  END
"""

mol = Chem.MolFromMolBlock(molblock)
d = rdMolDraw2D.MolDraw2DSVG(500, 500)
rdMolDraw2D.PrepareAndDrawMolecule(d, mol)
d.FinishDrawing()
txt = d.GetDrawingText()
with open('bad_ferrocene.svg', 'w') as f:
    f.write(txt)
```

**Expected behavior**
The double bonds should be shown with the second line inside the cyclopentadiene.

**Screenshots**
![image](https://user-images.githubusercontent.com/9198870/216962372-db77f0a2-a703-406b-aa94-1c3d4895a1cf.png)

**Configuration (please complete the following information):**
 - RDKit version: current master
 - OS: macOS Ventura 13.2 Apple silicon
 - Python version (if relevant): 3.10.4
 - Are you using conda? No
 - 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? build from scratch

**Additional context**
2022.09.4 gives a different, even more horrible result, but that's already been fixed, I think.
Fix in development.

diff --git a/Code/GraphMol/Depictor/RDDepictor.cpp b/Code/GraphMol/Depictor/RDDepictor.cpp
index cf9be5d3fad..32e9b68b582 100644
--- a/Code/GraphMol/Depictor/RDDepictor.cpp
+++ b/Code/GraphMol/Depictor/RDDepictor.cpp
@@ -269,8 +269,8 @@ void embedFusedSystems(const RDKit::ROMol &mol,
     bool allowRingTemplates = useRingTemplates;
     if (useRingTemplates && coordMap) {
       boost::dynamic_bitset<> coordMapAtoms(mol.getNumAtoms());
-      for (const auto& ring : frings) {
-        for (const auto& aid : ring) {
+      for (const auto &ring : frings) {
+        for (const auto &aid : ring) {
           if (coordMap->find(aid) != coordMap->end()) {
             coordMapAtoms.set(aid);
           }
@@ -420,7 +420,8 @@ void computeInitialCoords(RDKit::ROMol &mol,
   RDKit::VECT_INT_VECT arings;

   // first find all the rings
-  RDKit::MolOps::symmetrizeSSSR(mol, arings);
+  bool includeDativeBonds = true;
+  RDKit::MolOps::symmetrizeSSSR(mol, arings, includeDativeBonds);

   // do stereochemistry
   RDKit::MolOps::assignStereochemistry(mol, false);
@@ -439,7 +440,8 @@ void computeInitialCoords(RDKit::ROMol &mol,

   if (arings.size() > 0) {
     // first deal with the fused rings
-    DepictorLocal::embedFusedSystems(mol, arings, efrags, coordMap, useRingTemplates);
+    DepictorLocal::embedFusedSystems(mol, arings, efrags, coordMap,
+                                     useRingTemplates);
   }

   // do non-tetrahedral stereo
@@ -555,7 +557,7 @@ unsigned int compute2DCoords(RDKit::ROMol &mol,
 //
 //
 unsigned int compute2DCoords(RDKit::ROMol &mol,
-                             const Compute2DCoordParameters& params) {
+                             const Compute2DCoordParameters &params) {
   if (mol.needsUpdatePropertyCache()) {
     mol.updatePropertyCache(false);
   }
@@ -570,9 +572,11 @@ unsigned int compute2DCoords(RDKit::ROMol &mol,
     return cid;
   };
 #endif
+
+  RDKit::ROMol cp(mol);
   // storage for pieces of a molecule/s that are embedded in 2D
   std::list<EmbeddedFrag> efrags;
-  computeInitialCoords(mol, params.coordMap, efrags, params.useRingTemplates);
+  computeInitialCoords(cp, params.coordMap, efrags, params.useRingTemplates);

 #if 1
   // perform random sampling here to improve the density
@@ -581,9 +585,9 @@ unsigned int compute2DCoords(RDKit::ROMol &mol,
     // bonds in the structure or flip only bonds along the shortest
     // path between colliding atoms - don't do both
     if ((params.nSamples > 0) && (params.nFlipsPerSample > 0)) {
-      eri.randomSampleFlipsAndPermutations(params.nFlipsPerSample, params.nSamples,
-                                           params.sampleSeed, nullptr, 0.0,
-                                           params.permuteDeg4Nodes);
+      eri.randomSampleFlipsAndPermutations(
+          params.nFlipsPerSample, params.nSamples, params.sampleSeed, nullptr,
+          0.0, params.permuteDeg4Nodes);
     } else {
       eri.removeCollisionsBondFlip();
     }
diff --git a/Code/GraphMol/FindRings.cpp b/Code/GraphMol/FindRings.cpp
index 77282ea7527..20543537d8c 100644
--- a/Code/GraphMol/FindRings.cpp
+++ b/Code/GraphMol/FindRings.cpp
@@ -67,6 +67,15 @@ void convertToBonds(const VECT_INT_VECT &res, VECT_INT_VECT &brings,

 }  // end of namespace RingUtils

+namespace RDKit {
+namespace {
+bool bondIsDative(const RDKit::Bond &bond) {
+  auto bt = bond.getBondType();
+  return bt == Bond::BondType::DATIVE || bt == Bond::BondType::DATIVEL ||
+         bt == Bond::BondType::DATIVER || bt == Bond::BondType::DATIVEONE;
+}
+}  // namespace
+}  // namespace RDKit
 namespace FindRings {
 using namespace RDKit;

@@ -847,16 +856,16 @@ bool findRingConnectingAtoms(const ROMol &tMol, const Bond *bond,

 namespace RDKit {
 namespace MolOps {
-int findSSSR(const ROMol &mol, VECT_INT_VECT *res) {
+int findSSSR(const ROMol &mol, VECT_INT_VECT *res, bool includeDativeBonds) {
   if (!res) {
     VECT_INT_VECT rings;
-    return findSSSR(mol, rings);
+    return findSSSR(mol, rings, includeDativeBonds);
   } else {
-    return findSSSR(mol, (*res));
+    return findSSSR(mol, (*res), includeDativeBonds);
   }
 }

-int findSSSR(const ROMol &mol, VECT_INT_VECT &res) {
+int findSSSR(const ROMol &mol, VECT_INT_VECT &res, bool includeDativeBonds) {
   res.resize(0);
   if (mol.getRingInfo()->isInitialized()) {
     mol.getRingInfo()->reset();
@@ -871,12 +880,14 @@ int findSSSR(const ROMol &mol, VECT_INT_VECT &res) {
   boost::dynamic_bitset<> activeBonds(nbnds);
   activeBonds.set();

-  // Zero-order bonds are not candidates for rings
+  // Zero-order bonds are not candidates for rings, and dative bonds may also be
+  // out
   ROMol::EDGE_ITER firstB, lastB;
   boost::tie(firstB, lastB) = mol.getEdges();
   while (firstB != lastB) {
     const Bond *bond = mol[*firstB];
-    if (bond->getBondType() == Bond::ZERO) {
+    if (bond->getBondType() == Bond::ZERO ||
+        (!includeDativeBonds && bondIsDative(*bond))) {
       activeBonds[bond->getIdx()] = 0;
     }
     ++firstB;
@@ -892,14 +903,11 @@ int findSSSR(const ROMol &mol, VECT_INT_VECT &res) {
     int deg = atom->getDegree();
     atomDegrees[i] = deg;
     atomDegreesWithZeroOrderBonds[i] = deg;
-    ROMol::OEDGE_ITER beg, end;
-    boost::tie(beg, end) = mol.getAtomBonds(atom);
-    while (beg != end) {
-      const Bond *bond = mol[*beg];
-      if (bond->getBondType() == Bond::ZERO) {
+    for (const auto bond : mol.atomBonds(atom)) {
+      if (bond->getBondType() == Bond::ZERO ||
+          (!includeDativeBonds && bondIsDative(*bond))) {
         atomDegrees[i]--;
       }
-      ++beg;
     }
   }

@@ -1117,18 +1125,18 @@ int findSSSR(const ROMol &mol, VECT_INT_VECT &res) {
   return rdcast<int>(res.size());
 }

-int symmetrizeSSSR(ROMol &mol) {
+int symmetrizeSSSR(ROMol &mol, bool includeDativeBonds) {
   VECT_INT_VECT tmp;
-  return symmetrizeSSSR(mol, tmp);
+  return symmetrizeSSSR(mol, tmp, includeDativeBonds);
 };

-int symmetrizeSSSR(ROMol &mol, VECT_INT_VECT &res) {
+int symmetrizeSSSR(ROMol &mol, VECT_INT_VECT &res, bool includeDativeBonds) {
   res.clear();
   VECT_INT_VECT sssrs;

   // FIX: need to set flag here the symmetrization has been done in order to
   // avoid repeating this work
-  findSSSR(mol, sssrs);
+  findSSSR(mol, sssrs, includeDativeBonds);

   res.reserve(sssrs.size());
   for (const auto &r : sssrs) {
diff --git a/Code/GraphMol/MolDraw2D/MolDraw2D.cpp b/Code/GraphMol/MolDraw2D/MolDraw2D.cpp
index c53a9398bcf..640c3a2fbdc 100644
--- a/Code/GraphMol/MolDraw2D/MolDraw2D.cpp
+++ b/Code/GraphMol/MolDraw2D/MolDraw2D.cpp
@@ -81,8 +81,14 @@ void MolDraw2D::drawMolecule(const ROMol &mol, const std::string &legend,
                              const std::map<int, double> *highlight_radii,
                              int confId) {
   setupTextDrawer();
+  // we need ring info for drawing, so copy the molecule
+  // in order to add it
+  ROMol lmol(mol);
+  if (!lmol.getRingInfo()->isInitialized()) {
+    MolOps::symmetrizeSSSR(lmol);
+  }
   drawMols_.emplace_back(new MolDraw2D_detail::DrawMol(
-      mol, legend, panelWidth(), panelHeight(), drawOptions(), *text_drawer_,
+      lmol, legend, panelWidth(), panelHeight(), drawOptions(), *text_drawer_,
       highlight_atoms, highlight_bonds, highlight_atom_map, highlight_bond_map,
       nullptr, highlight_radii, supportsAnnotations(), confId));
   drawMols_.back()->setOffsets(x_offset_, y_offset_);
diff --git a/Code/GraphMol/MolOps.h b/Code/GraphMol/MolOps.h
index 6f66857d900..358244e30c7 100644
--- a/Code/GraphMol/MolOps.h
+++ b/Code/GraphMol/MolOps.h
@@ -688,6 +688,8 @@ RDKIT_GRAPHMOL_EXPORT void setHybridization(ROMol &mol);
   \param res used to return the vector of rings. Each entry is a vector with
       atom indices.  This information is also stored in the molecule's
       RingInfo structure, so this argument is optional (see overload)
+  \param includeDativeBonds - determines whether or not dative bonds are used in
+      the ring finding.

   \return number of smallest rings found

@@ -723,10 +725,12 @@ RDKIT_GRAPHMOL_EXPORT void setHybridization(ROMol &mol);
   done. The extra rings this process adds can be quite useful.
 */
 RDKIT_GRAPHMOL_EXPORT int findSSSR(const ROMol &mol,
-                                   std::vector<std::vector<int>> &res);
+                                   std::vector<std::vector<int>> &res,
+                                   bool includeDativeBonds = false);
 //! \overload
-RDKIT_GRAPHMOL_EXPORT int findSSSR(
-    const ROMol &mol, std::vector<std::vector<int>> *res = nullptr);
+RDKIT_GRAPHMOL_EXPORT int findSSSR(const ROMol &mol,
+                                   std::vector<std::vector<int>> *res = nullptr,
+                                   bool includeDativeBonds = false);

 //! use a DFS algorithm to identify ring bonds and atoms in a molecule
 /*!
@@ -756,6 +760,8 @@ RDKIT_GRAPHMOL_EXPORT void findRingFamilies(const ROMol &mol);
   \param res used to return the vector of rings. Each entry is a vector with
       atom indices.  This information is also stored in the molecule's
       RingInfo structure, so this argument is optional (see overload)
+  \param includeDativeBonds - determines whether or not dative bonds are used in
+      the ring finding.

   \return the total number of rings = (new rings + old SSSRs)

@@ -764,9 +770,11 @@ RDKIT_GRAPHMOL_EXPORT void findRingFamilies(const ROMol &mol);
   first
 */
 RDKIT_GRAPHMOL_EXPORT int symmetrizeSSSR(ROMol &mol,
-                                         std::vector<std::vector<int>> &res);
+                                         std::vector<std::vector<int>> &res,
+                                         bool includeDativeBonds = false);
 //! \overload
-RDKIT_GRAPHMOL_EXPORT int symmetrizeSSSR(ROMol &mol);
+RDKIT_GRAPHMOL_EXPORT int symmetrizeSSSR(ROMol &mol,
+                                         bool includeDativeBonds = false);

 //! @}

diff --git a/Code/GraphMol/Wrap/MolOps.cpp b/Code/GraphMol/Wrap/MolOps.cpp
index d199197ef9e..b592bda2887 100644
--- a/Code/GraphMol/Wrap/MolOps.cpp
+++ b/Code/GraphMol/Wrap/MolOps.cpp
@@ -306,9 +306,9 @@ ROMol *addHs(const ROMol &orig, bool explicitOnly, bool addCoords,
   return res;
 }

-VECT_INT_VECT getSSSR(ROMol &mol) {
+VECT_INT_VECT getSSSR(ROMol &mol, bool includeDativeBonds) {
   VECT_INT_VECT rings;
-  MolOps::findSSSR(mol, rings);
+  MolOps::findSSSR(mol, rings, includeDativeBonds);
   return rings;
 }

@@ -466,9 +466,9 @@ void setHybridizationMol(ROMol &mol) {
   MolOps::setHybridization(wmol);
 }

-VECT_INT_VECT getSymmSSSR(ROMol &mol) {
+VECT_INT_VECT getSymmSSSR(ROMol &mol, bool includeDativeBonds) {
   VECT_INT_VECT rings;
-  MolOps::symmetrizeSSSR(mol, rings);
+  MolOps::symmetrizeSSSR(mol, rings, includeDativeBonds);
   return rings;
 }
 PyObject *getDistanceMatrix(ROMol &mol, bool useBO = false,
@@ -1055,11 +1055,14 @@ struct molops_wrapper {
   ARGUMENTS:\n\
 \n\
     - mol: the molecule to use.\n\
+    - includeDativeBonds: whether or not dative bonds should be included in the ring finding.\n\
 \n\
   RETURNS: a sequence of sequences containing the rings found as atom ids\n\
          The length of this will be equal to NumBonds-NumAtoms+1 for single-fragment molecules.\n\
 \n";
-    python::def("GetSSSR", getSSSR, docString.c_str());
+    python::def("GetSSSR", getSSSR,
+                (python::arg("mol"), python::arg("includeDativeBonds") = false),
+                docString.c_str());

     // ------------------------------------------------------------------------
     docString =
@@ -1072,10 +1075,13 @@ struct molops_wrapper {
   ARGUMENTS:\n\
 \n\
     - mol: the molecule to use.\n\
+    - includeDativeBonds: whether or not dative bonds should be included in the ring finding.\n\
 \n\
   RETURNS: a sequence of sequences containing the rings found as atom ids\n\
 \n";
-    python::def("GetSymmSSSR", getSymmSSSR, docString.c_str());
+    python::def("GetSymmSSSR", getSymmSSSR,
+                (python::arg("mol"), python::arg("includeDativeBonds") = false),
+                docString.c_str());

     // ------------------------------------------------------------------------
     docString =
diff --git a/Code/GraphMol/catch_organometallics.cpp b/Code/GraphMol/catch_organometallics.cpp
index 98ca289bfc3..c6577ad9814 100644
--- a/Code/GraphMol/catch_organometallics.cpp
+++ b/Code/GraphMol/catch_organometallics.cpp
@@ -12,6 +12,7 @@

 #include <GraphMol/RWMol.h>
 #include <GraphMol/FileParsers/FileParsers.h>
+#include <GraphMol/SmilesParse/SmilesParse.h>
 #include <GraphMol/SmilesParse/SmilesWrite.h>

 using namespace RDKit;
@@ -128,3 +129,26 @@ TEST_CASE("get haptic bond end points") {
   endPts = MolOps::details::hapticBondEndpoints(bond);
   CHECK(std::vector<int>{} == endPts);
 }
+
+TEST_CASE("Rings and dative bonds") {
+  SECTION("ferrocene") {
+    auto m =
+        "C12->[Fe+2]3456789(<-C1=C->3[CH-]->4C->5=2)<-C1=C->6[CH-]->7C->8=C->91"_smiles;
+    REQUIRE(m);
+    std::vector<std::vector<int>> rings;
+    auto nrings = MolOps::symmetrizeSSSR(*m, rings);
+    CHECK(nrings == 2);
+    CHECK(rings[0].size() == 5);
+    CHECK(rings[1].size() == 5);
+  }
+}
+
+TEST_CASE("aromaticity and dative bonds") {
+  SECTION("ferrocene") {
+    auto m =
+        "C12->[Fe+2]3456789(<-C1=C->3[CH-]->4C->5=2)<-C1=C->6[CH-]->7C->8=C->91"_smiles;
+    REQUIRE(m);
+    CHECK(m->getAtomWithIdx(0)->getIsAromatic());
+    CHECK(!m->getAtomWithIdx(1)->getIsAromatic());
+  }
+}
\ No newline at end of file
diff --git a/ReleaseNotes.md b/ReleaseNotes.md
index ed8dcd520ab..e453374a97e 100644
--- a/ReleaseNotes.md
+++ b/ReleaseNotes.md
@@ -8,6 +8,8 @@
 ## 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 ring-finding functions now no longer consider dative bonds as possible ring bonds by default. All of the ring-finding functions have a new optional argument `includeDativeBonds` which can be used to change this behavior
+- Generating 2D coordinates no longer has the side effect of running ring finding on molecules.
 - 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.
 - The doIsomericSmiles parameter in Java and C# ROMol.MolToSmiles() now defaults to true (previously it was false), thus aligning to the C++ and Python behavior.
 - Double bonds which are marked as crossed (i.e. `bond.GetBondDir() == Bond.BondDir.EITHERDOUBLE`) now have their BondStereo set to `Bond.BondStereo.STEREOANY` and the BondDir information removed by default when molecules are parsed or `AssignStereochemistry()` is called with the `cleanIt` argument set to True.

diff --git a/Code/GraphMol/Depictor/catch_tests.cpp b/Code/GraphMol/Depictor/catch_tests.cpp
index 5481098be46..65431380f10 100644
--- a/Code/GraphMol/Depictor/catch_tests.cpp
+++ b/Code/GraphMol/Depictor/catch_tests.cpp
@@ -212,12 +212,27 @@ TEST_CASE("use ring system templates") {
   auto mol = "C1CCC2C(C1)C1CCN2NN1"_smiles;
   RDDepict::Compute2DCoordParameters params;
   RDDepict::compute2DCoords(*mol, params);
-  auto diff = mol->getConformer().getAtomPos(10) - mol->getConformer().getAtomPos(11);
+  auto diff =
+      mol->getConformer().getAtomPos(10) - mol->getConformer().getAtomPos(11);
   // when templates are not used, bond from 10-11 is very short
   TEST_ASSERT(RDKit::feq(diff.length(), 0.116, .1));

   params.useRingTemplates = true;
   RDDepict::compute2DCoords(*mol, params);
-  diff = mol->getConformer().getAtomPos(10) - mol->getConformer().getAtomPos(11);
+  diff =
+      mol->getConformer().getAtomPos(10) - mol->getConformer().getAtomPos(11);
   TEST_ASSERT(RDKit::feq(diff.length(), 1.0, .1))
+}
+
+TEST_CASE("dative bonds and rings") {
+  auto mol = "O->[Pt]1(<-O)<-NC2CCC2N->1"_smiles;
+  REQUIRE(mol);
+  auto rings = mol->getRingInfo();
+  CHECK(rings->numRings() == 1);  // the dative bonds are ignored
+  RDDepict::compute2DCoords(*mol);
+  CHECK(rings->numRings() == 1);  // ensure the ring count hasn't changed
+  auto conf = mol->getConformer();
+  auto v1 = conf.getAtomPos(1) - conf.getAtomPos(3);
+  auto v2 = conf.getAtomPos(1) - conf.getAtomPos(8);
+  CHECK_THAT(v1.length(), Catch::Matchers::WithinAbs(v2.length(), 0.01));
 }
\ No newline at end of file
diff --git a/Code/GraphMol/Depictor/testDepictor.cpp b/Code/GraphMol/Depictor/testDepictor.cpp
index fad5549c64e..08420863495 100644
--- a/Code/GraphMol/Depictor/testDepictor.cpp
+++ b/Code/GraphMol/Depictor/testDepictor.cpp
@@ -821,13 +821,12 @@ void testGitHubIssue1073() {
     RWMol *m = SmartsToMol(smarts);
     TEST_ASSERT(m);

+    // compute2DCoords does ring finding internally, so there's no error
     RDDepict::compute2DCoords(*m);

+    // but the molecule itself is not modified
     RingInfo *ri = m->getRingInfo();
-    TEST_ASSERT(ri->isInitialized());
-    TEST_ASSERT(ri->isAtomInRingOfSize(0, 6));
-    TEST_ASSERT(ri->isAtomInRingOfSize(0, 5));
-    TEST_ASSERT(!ri->isAtomInRingOfSize(0, 9));
+    TEST_ASSERT(!ri->isInitialized());

     delete m;
   }
diff --git a/Code/GraphMol/MolDraw2D/catch_tests.cpp b/Code/GraphMol/MolDraw2D/catch_tests.cpp
index aff69fec493..cce33a3d477 100755
--- a/Code/GraphMol/MolDraw2D/catch_tests.cpp
+++ b/Code/GraphMol/MolDraw2D/catch_tests.cpp
@@ -58,7 +58,7 @@ static const std::map<std::string, std::hash_result_t> SVG_HASHES = {
     {"contourMol_3.svg", 3493070184U},
     {"contourMol_4.svg", 764999893U},
     {"testDativeBonds_1.svg", 555607912U},
-    {"testDativeBonds_2.svg", 93109626U},
+    {"testDativeBonds_2.svg", 2355686690U},
     {"testDativeBonds_3.svg", 3944956974U},
     {"testDativeBonds_2a.svg", 1026259021U},
     {"testDativeBonds_2b.svg", 3842058701U},
@@ -718,7 +718,7 @@ TEST_CASE("dative bonds", "[drawing][organometallics]") {
     outs.close();
     check_file_hash("testDativeBonds_2.svg");

-    CHECK(text.find("-8' d='M 101.1,79.8 L 95.8,87.1' "
+    CHECK(text.find("-8' d='M 101.1,77.2 L 95.8,84.5' "
                     "style='fill:none;fill-rule:evenodd;stroke:#0000FF;") !=
           std::string::npos);
   }
@@ -7476,8 +7476,8 @@ TEST_CASE(
     auto &atomRings = rings->atomRings();
     std::map<int, std::vector<DrawColour>> atomCols;
     std::map<int, double> atomRads;
-    for (auto i = 0; i < atomRings.size(); ++i) {
-      for (auto j = 0; j < atomRings[i].size(); ++j) {
+    for (auto i = 0u; i < atomRings.size(); ++i) {
+      for (auto j = 0u; j < atomRings[i].size(); ++j) {
         auto ex = atomCols.find(atomRings[i][j]);
         if (ex == atomCols.end()) {
           std::vector<DrawColour> cvec(1, colours[i]);
@@ -7495,8 +7495,8 @@ TEST_CASE(
     auto &bondRings = rings->bondRings();
     std::map<int, int> bondMults;
     std::map<int, std::vector<DrawColour>> bondCols;
-    for (auto i = 0; i < bondRings.size(); ++i) {
-      for (auto j = 0; j < bondRings[i].size(); ++j) {
+    for (auto i = 0u; i < bondRings.size(); ++i) {
+      for (auto j = 0u; j < bondRings[i].size(); ++j) {
         auto ex = bondCols.find(bondRings[i][j]);
         if (ex == bondCols.end()) {
           std::vector<DrawColour> cvec(1, colours[i]);