Fixes github #8205 (rdkit/rdkit#8368)

Recent changes to the v2 tautomer hash (likely #8173) had introduced an order-dependence (ordering of the bonds, not the atoms) in the resutlts.

This removes that order dependence as well as expanding the classes of systems which are explicitly not considered for tautomerism.

In addition to the tests I added here, I tested this by running through ~1.4 million compounds from ChEMBL35 and ensuring that the two hashes here are the same:
```
    m = Chem.MolFromMolBlock(mb)
    if not m:
        continue
    smi = Chem.MolToSmiles(m)
    m2 = Chem.MolFromSmiles(smi)
    if not m2:
        continue
    if Chem.MolToSmiles(m2) != smi:
        # if we don't get the canonical SMILES right, we can give up here.
        continue
    h1 = rdMolHash.MolHash(m,rdMolHash.HashFunction.HetAtomTautomerv2)
    m2 = Chem.MolFromSmiles(smi)
    h2 = rdMolHash.MolHash(m2,rdMolHash.HashFunction.HetAtomTautomerv2)
```

order dependence in tautomer hash

**Describe the bug**
Here's the reproducible:
```
In [35]: print(b2)

     RDKit          2D

  0  0  0  0  0  0  0  0  0  0999 V3000
M  V30 BEGIN CTAB
M  V30 COUNTS 15 16 0 0 0
M  V30 BEGIN ATOM
M  V30 1 C -0.006857 -1.225143 0.000000 0
M  V30 2 C -1.242571 -0.508286 0.000000 0
M  V30 3 C -2.481143 -1.220000 0.000000 0
M  V30 4 C -2.484000 -2.648571 0.000000 0
M  V30 5 N -1.248286 -3.365429 0.000000 0
M  V30 6 C -0.009714 -2.653714 0.000000 0
M  V30 7 O -3.722571 -3.360572 0.000000 0
M  V30 8 N -1.239714 0.920286 0.000000 0
M  V30 9 C 1.549143 3.346571 0.000000 0
M  V30 10 C 2.260857 2.108000 0.000000 0
M  V30 11 C 1.302857 1.048286 0.000000 0
M  V30 12 C -0.001143 1.632000 0.000000 0
M  V30 13 C 0.151143 3.052571 0.000000 0
M  V30 14 C -1.251227 -4.793997 0.000000 0
M  V30 15 C -0.908467 4.010717 0.000000 0
M  V30 END ATOM
M  V30 BEGIN BOND
M  V30 1 1 1 2
M  V30 2 2 2 3
M  V30 3 1 3 4
M  V30 4 1 4 5
M  V30 5 1 5 6
M  V30 6 2 6 1
M  V30 7 2 4 7
M  V30 8 1 2 8
M  V30 9 1 9 10
M  V30 10 1 10 11
M  V30 11 1 11 12
M  V30 12 1 12 13
M  V30 13 1 13 9
M  V30 14 1 12 8 CFG=1
M  V30 15 1 5 14
M  V30 16 1 13 15
M  V30 END BOND
M  V30 END CTAB
M  END
$$$$

In [36]: m = Chem.MolFromMolBlock(b2)

In [37]: m2 = Chem.MolFromSmiles(Chem.MolToSmiles(m))

In [38]: rdMolHash.MolHash(m,rdMolHash.HashFunction.HetAtomTautomerv2)
Out[38]: '[CH3]-[CH]1-[CH2]-[CH2]-[CH2]-[C]-1:[N]:[C]1:[C]:[C]:[N](-[CH3]):[C](:[O]):[C]:1_5_0'

In [39]: rdMolHash.MolHash(m2,rdMolHash.HashFunction.HetAtomTautomerv2)
Out[39]: '[CH3]-[CH]1-[CH2]-[CH2]-[CH2]-[C@H]-1-[N]:[C]1:[C]:[C]:[N](-[CH3]):[C](:[O]):[C]:1_4_0'
```

Renumbering the atoms in m2 to match the numbering in m does not change the results for m2, so this seems to be related to the ordering of the bonds.

**Configuration (please complete the following information):**
 - RDKit version: master and current release
 - OS: all

diff --git a/Code/GraphMol/MolHash/hashfunctions.cpp b/Code/GraphMol/MolHash/hashfunctions.cpp
index c687eb6402f..f27e02cceeb 100644
--- a/Code/GraphMol/MolHash/hashfunctions.cpp
+++ b/Code/GraphMol/MolHash/hashfunctions.cpp
@@ -387,11 +387,20 @@ constexpr std::uint64_t bondFlagCarboxyl =
 std::vector<std::uint64_t> getBondFlags(const ROMol &mol) {
   // FIX: oversimplified, but should work for now
   static std::vector<std::string> patterns{
-      "[C;!$(C-C(=[NH])-[NH2])]-[C;!$(C(-C)(=[NH])-[NH2])](=[O,N,S])-[O,N,S]",  //< one side of the "amide", with an ugly exclusion for amidine
-      "[C;!$(C=[O,N,S])]-[O,N,S]-C=[O,N,S]",  //< the other side
+      "[C;!$(C-C(=[NH])-[NH2])]-[C;!$(C(-C)(=[NH])-[NH2])](=[O,N,S])-[O,N,S]",
+      //< one side of the "amide", with an ugly exclusion for amidine
+      "[A;!$(C=[O,N,S])]-[O,N,S]-C=[O,N,S]",  //< the other side
       "[OH0,SH0]-C=[O,N,S]",                  //< "esters" and "carboxyls"
+      "C-[N;$([N+]=C(N)(N)),$(N-C(N)=N)]",    //< quanidine like
       "[C]-[c](:[o,n,s]):[o,n,s]",  //< a limited version of handling this for
                                     // aromatic systems
+      "*[SD4](=*)=*",               // sulfates, sulfonyls, etc.
+      "*=[SD4](=*)*",               // the other side
+      "*-[H0]=,#[C,N]=,#*",         // isocyanates, azides, et al
+      "*[#7+]-[O-]",                // nitro groups and aromatic n-oxides
+      "[O,S;H]-P=O",                // phosphoric acid, etc.
+      "[#6]-P=O",                   // phosphoric acid, etc.
+      "[#6]-N=[SD4]=*"              // don't know what this one is called
   };
   static std::vector<std::unique_ptr<RDKit::RWMol>> queries;
   if (queries.empty()) {
@@ -504,6 +513,41 @@ bool skipNeighborBond(const Atom *atom, const Atom *otherAtom,
            (!isUnsaturatedBond(nbrBond) && !nbrBond->getIsConjugated() &&
             !hasStartBond(atom, startBonds))));
 }
+
+// counts the number of neighboring atoms that have conjugated bonds.
+unsigned int getNumConjugatedNeighbors(
+    const Atom *atom, const boost::dynamic_bitset<> &startBonds) {
+  unsigned int res = 0;
+  for (auto nbr : atom->getOwningMol().atomNeighbors(atom)) {
+    for (auto nbrBond : atom->getOwningMol().atomBonds(nbr)) {
+      if (nbrBond->getIsConjugated() || startBonds[nbrBond->getIdx()]) {
+        ++res;
+        break;
+      }
+    }
+  }
+  return res;
+}
+// special case to prevent "overreach" with things like enamines.
+// the logic here prevents the first bond in CNC=C from being
+// included in the tautomeric system. So we get: [CH3]-[N]:[C]:[C]
+// instead of [C]:[N]:[C]:[C]
+// returns true if the bond is a candidate for overreach.
+// The arguments are, with atom indices corresponding to the example above:
+//   atom: atom 1
+//   oatom: atom 0
+//   bond: bond between atom 1 and atom 2
+//   nbrBond: bond between atom 1 and atom 0
+bool checkForOverreach(
+    const Atom *atom, const Atom *oatom, const Bond *bond, const Bond *nbrBond,
+    const boost::dynamic_bitset<> &startBonds,
+    const std::vector<unsigned int> &numConjugatedNeighbors) {
+  return (startBonds[bond->getIdx()] || hasStartBond(atom, startBonds)) &&
+         !startBonds[nbrBond->getIdx()] && isHeteroAtom(atom) &&
+         !isUnsaturatedBond(nbrBond) &&
+         numConjugatedNeighbors[oatom->getIdx()] < 2;
+}
+
 }  // namespace

 std::string TautomerHashv2(RWMol *mol, bool proto, bool useCXSmiles,
@@ -523,9 +567,15 @@ std::string TautomerHashv2(RWMol *mol, bool proto, bool useCXSmiles,

   boost::dynamic_bitset<> startBonds(mol->getNumBonds());
   for (const auto bnd : mol->bonds()) {
-    startBonds.set(bnd->getIdx(),
-                   isPossibleStartingBond(bnd, atomFlags, bondFlags));
+    auto isStartBond = isPossibleStartingBond(bnd, atomFlags, bondFlags);
+    startBonds.set(bnd->getIdx(), isStartBond);
   }
+  std::vector<unsigned int> numConjugatedNeighbors(mol->getNumAtoms(), 0);
+  for (const auto atm : mol->atoms()) {
+    numConjugatedNeighbors[atm->getIdx()] =
+        getNumConjugatedNeighbors(atm, startBonds);
+  }
+
 #ifdef VERBOSE_HASH
   std::cerr << " START BONDS: " << startBonds << std::endl;
 #endif
@@ -536,7 +586,11 @@ std::string TautomerHashv2(RWMol *mol, bool proto, bool useCXSmiles,
         !startBonds[bptr->getIdx()]) {
       continue;
     }
+#ifdef VERBOSE_HASH
+    std::cerr << "START BOND: " << bptr->getIdx() << std::endl;
+#endif
     boost::dynamic_bitset<> conjSystem(mol->getNumBonds());
+    boost::dynamic_bitset<> conjAtoms(mol->getNumAtoms());
     boost::dynamic_bitset<> atomsInSystem(mol->getNumAtoms());
     unsigned int numDonorCs = 0;
     unsigned int activeHeteroHs = 0;
@@ -557,31 +611,24 @@ std::string TautomerHashv2(RWMol *mol, bool proto, bool useCXSmiles,
         }
         auto oatom = nbrBond->getOtherAtom(atm);

-        // if the bond is unsaturated or to an atom with free Hs, include it:
-        // if (isUnsaturatedBond(nbrBond) || oatom->getTotalNumHs()) {
-
 #ifdef VERBOSE_HASH
         std::cerr << "  check neighbor1 " << nbrBond->getIdx() << " from "
                   << atm->getIdx() << "-" << oatom->getIdx() << std::endl;
         std::cerr << "    " << bondsConsidered[nbrBond->getIdx()] << " icao "
-                  << isCandidateAtom(oatom) << " hsbo "
-                  << hasStartBond(oatom, startBonds) << " atomunsato "
-                  << queryAtomUnsaturated(oatom) << " atomunsat "
-                  << queryAtomUnsaturated(atm) << " bondunsat "
+                  << isCandidateAtom(oatom, atomFlags) << " hsbo "
+                  << hasStartBond(oatom, startBonds) << " unsat "
                   << isUnsaturatedBond(nbrBond) << " icaa "
-                  << isCandidateAtom(atm) << " hsba "
-                  << hasStartBond(atm, startBonds) << std::endl;
+                  << isCandidateAtom(atm, atomFlags) << " hsba "
+                  << hasStartBond(atm, startBonds)
+                  << " ncno: " << numConjugatedNeighbors[oatom->getIdx()]
+                  << std::endl;
 #endif
-        // special case to prevent "overreach" with things like enamines.
-        // the logic here prevents the first bond in CNC=C from being included
-        // in the tautomeric system. So we get: [CH3]-[N]:[C] instead of
-        // [C]:[N]:[C]
-        if (startBonds[bptr->getIdx()] && isHeteroAtom(atm) &&
-            !isUnsaturatedBond(nbrBond)) {
+        if (checkForOverreach(atm, oatom, bptr, nbrBond, startBonds,
+                              numConjugatedNeighbors)) {
           continue;
         }

-        // if both bonds are not eligible, then we can skip this neighbor
+        // if the bond is not eligible, then we can skip this neighbor
         if (skipNeighborBond(atm, oatom, nbrBond, startBonds, atomFlags,
                              bondFlags) &&
             skipNeighborBond(oatom, atm, nbrBond, startBonds, atomFlags,
@@ -589,18 +636,21 @@ std::string TautomerHashv2(RWMol *mol, bool proto, bool useCXSmiles,
           continue;
         }

+        if (std::find(bq.begin(), bq.end(), nbrBond) == bq.end()) {
 #ifdef VERBOSE_HASH
-        std::cerr << "    push " << nbrBond->getIdx() << " "
-                  << nbrBond->getBeginAtomIdx() << "-"
-                  << nbrBond->getEndAtomIdx() << std::endl;
-        std::cerr << " #### SET1 " << bptr->getIdx() << std::endl;
+          std::cerr << "    push " << nbrBond->getIdx() << " "
+                    << nbrBond->getBeginAtomIdx() << "-"
+                    << nbrBond->getEndAtomIdx() << std::endl;
+          std::cerr << " #### SET1 " << bptr->getIdx() << std::endl;
 #endif
-        bq.push_back(nbrBond);
+          bq.push_back(nbrBond);
+        }

         // now we know that we should consider this bond
-
         bondsConsidered.set(bptr->getIdx());
         conjSystem.set(bptr->getIdx());
+        conjAtoms.set(bptr->getBeginAtomIdx());
+        conjAtoms.set(bptr->getEndAtomIdx());
       }
     }

@@ -618,6 +668,9 @@ std::string TautomerHashv2(RWMol *mol, bool proto, bool useCXSmiles,
 #endif
       bondsConsidered.set(bnd->getIdx());
       conjSystem.set(bnd->getIdx());
+      conjAtoms.set(bnd->getBeginAtomIdx());
+      conjAtoms.set(bnd->getEndAtomIdx());
+
       for (const auto atm :
            std::vector<const Atom *>{bnd->getBeginAtom(), bnd->getEndAtom()}) {
         if (atomsInSystem[atm->getIdx()]) {
@@ -633,7 +686,8 @@ std::string TautomerHashv2(RWMol *mol, bool proto, bool useCXSmiles,
           atomsInSystem.set(atm->getIdx());
         }
         for (auto nbrBnd : mol->atomBonds(atm)) {
-          if (nbrBnd == bnd) {
+          if (bondsConsidered[nbrBnd->getIdx()] ||
+              std::find(bq.begin(), bq.end(), nbrBnd) != bq.end()) {
             continue;
           }
           auto oatom = nbrBnd->getOtherAtom(atm);
@@ -642,23 +696,22 @@ std::string TautomerHashv2(RWMol *mol, bool proto, bool useCXSmiles,
           std::cerr << "  check neighbor " << nbrBnd->getIdx() << " from "
                     << atm->getIdx() << "-" << oatom->getIdx() << std::endl;
           std::cerr << "    " << bondsConsidered[nbrBnd->getIdx()] << " icao "
-                    << isCandidateAtom(oatom) << " hsbo "
+                    << isCandidateAtom(oatom, atomFlags) << " hsbo "
                     << hasStartBond(oatom, startBonds) << " unsat "
                     << isUnsaturatedBond(nbrBnd) << " icaa "
-                    << isCandidateAtom(atm) << " hsba "
-                    << hasStartBond(atm, startBonds) << std::endl;
+                    << isCandidateAtom(atm, atomFlags) << " hsba "
+                    << hasStartBond(atm, startBonds)
+                    << " ncno: " << numConjugatedNeighbors[oatom->getIdx()]
+                    << std::endl;
 #endif
-          if (bondsConsidered[nbrBnd->getIdx()] ||
-              (skipNeighborBond(atm, oatom, nbrBnd, startBonds, atomFlags,
+          if (checkForOverreach(atm, oatom, bnd, nbrBnd, startBonds,
+                                numConjugatedNeighbors)) {
+            continue;
+          }
+          if ((skipNeighborBond(atm, oatom, nbrBnd, startBonds, atomFlags,
                                 bondFlags) &&
                skipNeighborBond(oatom, atm, nbrBnd, startBonds, atomFlags,
                                 bondFlags))) {
-            // we won't add this bond for further traversal, but if both atoms
-            // are already in this system, then we should add the bond to the
-            // system
-            if (atomsInSystem[oatom->getIdx()]) {
-              conjSystem.set(nbrBnd->getIdx());
-            }
             continue;
           }
           bq.push_back(nbrBnd);
@@ -674,8 +727,30 @@ std::string TautomerHashv2(RWMol *mol, bool proto, bool useCXSmiles,
       std::cerr << "CONJ: " << conjSystem << " hetero " << activeHeteroHs
                 << " donor " << std::endl;
 #endif
-      bondsToModify |= conjSystem;
+      // all bonds between conjugated atoms in the system are considered to be
+      // in the system. There are situations where the traverse doesn't find the
+      // closure bond, so we explicitly check:
+      for (auto i = 0U; i < conjAtoms.size(); i++) {
+        if (conjAtoms[i]) {
+          for (const auto bnd : mol->atomBonds(mol->getAtomWithIdx(i))) {
+            if (conjAtoms[bnd->getOtherAtomIdx(i)]) {
+#ifdef VERBOSE_HASH
+              if (!conjSystem[bnd->getIdx()]) {
+                std::cerr << "  BACKFILL BOND: " << bnd->getIdx() << std::endl;
+              }
+#endif
+              bondsToModify.set(bnd->getIdx());
+            }
+          }
+        }
+      }
     } else {
+      // undo all the bonds we marked as considered in this conjugated system
+      for (unsigned int i = 0; i < mol->getNumBonds(); ++i) {
+        if (conjSystem[i]) {
+          bondsConsidered.reset(i);
+        }
+      }
 #ifdef VERBOSE_HASH
       std::cerr << "REJECT CONJ: " << conjSystem << " hetero " << activeHeteroHs
                 << " donor " << std::endl;

diff --git a/Code/GraphMol/MolHash/catch_tests.cpp b/Code/GraphMol/MolHash/catch_tests.cpp
index 4ce0046b456..d56436cfb70 100644
--- a/Code/GraphMol/MolHash/catch_tests.cpp
+++ b/Code/GraphMol/MolHash/catch_tests.cpp
@@ -411,6 +411,9 @@ TEST_CASE("tautomer v2") {
         data = {
             {{"CC=O", "C=CO"}, {}},
             {{"CCC=O", "CC=CO"}, {"C=CCO"}},
+            // this next one is the specific test to ensure that amino acid
+            // stereochemistry is not lost
+            {{"CCC(=O)NC", "CCC(O)=NC"}, {"CC=C(O)NC"}},
             {{"CC(=O)CC(=O)C", "C=C(O)CC(=O)C", "CC(=O)C=C(O)C",
               "C=C(O)C=C(O)C", "C=C(O)CC(O)=C"},
              {}},
@@ -435,10 +438,9 @@ TEST_CASE("tautomer v2") {
             {{
                  "S=C1N=CN=C2NC=NC12",
                  "S=C2C1N=CN=C1NC=N2",
-
+                 "S=C1NC=NC2N=CNC1=2",
              },
              {
-                 "S=C1NC=NC2N=CNC1=2",
                  "S=C1N=CN=C2N=CNC12",
                  "S=C2C1NC=NC1=NC=N2",
              }},
@@ -449,8 +451,9 @@ TEST_CASE("tautomer v2") {
                  "N1C(=O)NC(=O)C2C=NNC=21",
                  "N1C(=O)NC(=O)C2=CNNC2=1",
                  "N1C(=O)NC(=O)C2=CNNC2=1",
+                 "N1C(=O)NC(=O)C2CN=NC=21",
              },
-             {"N1C(=O)NC(=O)C2CN=NC=21", "N1C(=O)NC(=O)C2CN=NC2=1"}},
+             {"N1C(=O)NC(=O)C2CN=NC2=1"}},
             // ---------------------------
             // more stereochemistry
             // ---------------------------
@@ -932,4 +935,123 @@ TEST_CASE("v2 tautomers, carboxylic acids, amids, and related structures") {
       CHECK(hsh == ref);
     }
   }
+}
+
+TEST_CASE("github #8205: order dependence in tautomer hash") {
+  SECTION("as reported") {
+    auto mol1 = R"CTAB(
+     RDKit          2D
+
+  0  0  0  0  0  0  0  0  0  0999 V3000
+M  V30 BEGIN CTAB
+M  V30 COUNTS 15 16 0 0 0
+M  V30 BEGIN ATOM
+M  V30 1 C -0.006857 -1.225143 0.000000 0
+M  V30 2 C -1.242571 -0.508286 0.000000 0
+M  V30 3 C -2.481143 -1.220000 0.000000 0
+M  V30 4 C -2.484000 -2.648571 0.000000 0
+M  V30 5 N -1.248286 -3.365429 0.000000 0
+M  V30 6 C -0.009714 -2.653714 0.000000 0
+M  V30 7 O -3.722571 -3.360572 0.000000 0
+M  V30 8 N -1.239714 0.920286 0.000000 0
+M  V30 9 C 1.549143 3.346571 0.000000 0
+M  V30 10 C 2.260857 2.108000 0.000000 0
+M  V30 11 C 1.302857 1.048286 0.000000 0
+M  V30 12 C -0.001143 1.632000 0.000000 0
+M  V30 13 C 0.151143 3.052571 0.000000 0
+M  V30 14 C -1.251227 -4.793997 0.000000 0
+M  V30 15 C -0.908467 4.010717 0.000000 0
+M  V30 END ATOM
+M  V30 BEGIN BOND
+M  V30 1 1 1 2
+M  V30 2 2 2 3
+M  V30 3 1 3 4
+M  V30 4 1 4 5
+M  V30 5 1 5 6
+M  V30 6 2 6 1
+M  V30 7 2 4 7
+M  V30 8 1 2 8
+M  V30 9 1 9 10
+M  V30 10 1 10 11
+M  V30 11 1 11 12
+M  V30 12 1 12 13
+M  V30 13 1 13 9
+M  V30 14 1 12 8 CFG=1
+M  V30 15 1 5 14
+M  V30 16 1 13 15
+M  V30 END BOND
+M  V30 END CTAB
+M  END)CTAB"_ctab;
+    REQUIRE(mol1);
+    auto mol2 = v2::SmilesParse::MolFromSmiles(MolToSmiles(*mol1));
+    REQUIRE(mol2);
+    auto hsh1 =
+        MolHash::MolHash(mol1.get(), MolHash::HashFunction::HetAtomTautomerv2);
+    auto hsh2 =
+        MolHash::MolHash(mol2.get(), MolHash::HashFunction::HetAtomTautomerv2);
+    CHECK(hsh1 == hsh2);
+    // make sure the chirality wasn't destroyed
+    CHECK(hsh1.find("@") != std::string::npos);
+  }
+  SECTION("tests for the same issue from #8320 report") {
+    std::vector<std::string> smileses = {
+        "CC(=O)N[C@H](C)S(N)(=O)=O",
+        "O=P(O)(O)[C@@H](O)c1cccc2ccccc12",
+    };
+    for (const auto &smiles : smileses) {
+      INFO(smiles);
+      auto m = v2::SmilesParse::MolFromSmiles(smiles);
+      REQUIRE(m);
+      auto hsh =
+          MolHash::MolHash(m.get(), MolHash::HashFunction::HetAtomTautomerv2);
+      CHECK(hsh.find("@") != std::string::npos);
+    }
+  }
+}
+
+TEST_CASE("examples found in ChEMBL") {
+  SECTION("things that should match") {
+    std::vector<std::pair<std::string, std::string>> smileses = {
+        {"S1(Nc2ccccc2N1)(=O)(=O)", "O=S1(=O)Nc2ccccc2N1"},
+        {"c1ccccc1CN=C=S", "S=C=NCc1ccccc1"},
+        {"O=C1NCCC1", "OC1=NCCC1"},
+        {"c1no[n+]([O-])c1", "n1o[n+](cc1)[O-]"},
+    };
+    for (const auto &[smi1, smi2] : smileses) {
+      INFO(smi1 + " " + smi2);
+      auto m1 = v2::SmilesParse::MolFromSmiles(smi1);
+      REQUIRE(m1);
+      auto hsh1 =
+          MolHash::MolHash(m1.get(), MolHash::HashFunction::HetAtomTautomerv2);
+      auto m2 = v2::SmilesParse::MolFromSmiles(smi2);
+      REQUIRE(m2);
+      auto hsh2 =
+          MolHash::MolHash(m2.get(), MolHash::HashFunction::HetAtomTautomerv2);
+      CHECK(hsh1 == hsh2);
+    }
+  }
+
+  SECTION("specific problems") {
+    std::vector<std::pair<std::string, std::string>> data = {
+        {"NNC(=O)CC1=NNC(=O)C1",
+         "[NH2]-[N]:[C](:[O])-[C]:[C]1:[C]-[C](:[O]):[N]-[N]:1_6_0"},
+
+        {"Cc1ncn2c1NC=NC2N",
+         "[C]:[C]1:[N]:[C]:[N]2:[C](-[NH2]):[N]:[C]:[N]:[C]:1:2_7_0"},
+        {"Nc1nc2c(c(=O)[nH]1)CC=N2",
+         "[N]:[C]1:[N]:[C](:[O]):[C]2:[C]:[C]:[N]:[C]:2:[N]:1_6_0"},
+        {"NC(N)=[N+]1CCc2ccccc2C1",
+         "[NH2]-[C](-[NH2])=[N+]1-[CH2]-[CH2]-[c]2:[cH]:[cH]:[cH]:[cH]:[c]:2-[CH2]-1_0_0"},
+        {"O=S1(=Nc2ccncc2)CCCCC1",
+         "[O]=[S]1(=[N]-[C]2:[C]:[C]:[N]:[C]:[C]:2)-[CH2]-[CH2]-[CH2]-[CH2]-[CH2]-1_4_0"},
+    };
+    for (const auto &[smiles, ref] : data) {
+      INFO(smiles);
+      auto m = v2::SmilesParse::MolFromSmiles(smiles);
+      REQUIRE(m);
+      auto hsh =
+          MolHash::MolHash(m.get(), MolHash::HashFunction::HetAtomTautomerv2);
+      CHECK(hsh == ref);
+    }
+  }
 }
\ No newline at end of file


Fixes #8405 (rdkit/rdkit#8406)

There's another special case that we need to cover. This adds it, along with some documentation.

Some tautomer pairs generating different HetAtomTautomerv2 hashes

I mentioned this issue in https://github.com/rdkit/rdkit/pull/8368.

Since that PR was merged, these two pairs of known tautomers generate different hashes:

```
In [5]: from rdkit import  Chem
   ...: from rdkit.Chem import rdMolHash

In [6]: pair1 = ("OC1=CC(C)N=C(C1)O", "O=C1CC(C)NC(=O)C1")

In [7]: pair2 = (r'Cc2cc(C)nc(N/C(N)=N\c1ccc(Cl)c(C(F)(F)F)c1)n2', r'Cc2cc(C)nc(/N=C(N)/Nc1ccc(Cl)c(C(F)(F)F)c1)n2')

In [8]: # we'd expect both tautomers to produce the same hash, which should be deduplicated by the set
   ...: set(rdMolHash.MolHash(Chem.MolFromSmiles(s), rdMolHash.HashFunction.HetAtomTautomerv2) for s in pair1)
Out[8]:
{'[CH3]-[CH]1-[C]:[C](:[O]):[C]-[C](:[O]):[N]-1_5_0',
 '[CH3]-[C]1:[N]:[C](:[O])-[C]:[C](:[O]):[C]-1_6_0'}

In [9]: # these should also produce a single hash
   ...: set(rdMolHash.MolHash(Chem.MolFromSmiles(s), rdMolHash.HashFunction.HetAtomTautomerv2) for s in pair2)
Out[9]:
{'[C]:[C]1:[C]:[C](:[C]):[N]:[C](:[N]-[C](-[NH2]):[N]:[C]2:[C]:[C]:[C](-[Cl]):[C](-[C](-[F])(-[F])-[F]):[C]:2):[N]:1_11_0',
 '[C]:[C]1:[C]:[C](:[C]):[N]:[C](:[N]:[C](-[NH2])-[N]:[C]2:[C]:[C]:[C](-[Cl]):[C](-[C](-[F])(-[F])-[F]):[C]:2):[N]:1_11_0'}
```

Also:
```
In [3]: rdMolHash.MolHash(Chem.MolFromSmiles('O=C1CC(C)NC(=O)C1'),rdMolHash.HashFunction.HetAtomTautomerv2)
Out[3]: '[CH3]-[CH]1-[C]:[C](:[O]):[C]-[C](:[O]):[N]-1_5_0'

In [4]: rdMolHash.MolHash(Chem.MolFromSmiles('OC1=CC(C)N=C(C1)O'),rdMolHash.HashFunction.HetAtomTautomerv2)
Out[4]: '[CH3]-[C]1:[N]:[C](:[O])-[C]:[C](:[O]):[C]-1_6_0'

In [5]: rdMolHash.MolHash(Chem.MolFromSmiles('O=C1CC(C)N=C(C1)O'),rdMolHash.HashFunction.HetAtomTautomerv2)
Out[5]: '[CH3]-[CH]1-[C]:[C](:[O]):[C]-[C](:[O]):[N]-1_5_0'

In [6]: _5 == _3
Out[6]: True

In [7]: rdMolHash.MolHash(Chem.MolFromSmiles('OC1=CC(C)NC(C1)=O'),rdMolHash.HashFunction.HetAtomTautomerv2)
Out[7]: '[CH3]-[CH]1-[C]:[C](:[O]):[C]-[C](:[O]):[N]-1_5_0'

In [8]: _7 == _3
Out[8]: True
```

diff --git a/Code/GraphMol/MolHash/hashfunctions.cpp b/Code/GraphMol/MolHash/hashfunctions.cpp
index f27e02cceeb..43a2209b4b3 100644
--- a/Code/GraphMol/MolHash/hashfunctions.cpp
+++ b/Code/GraphMol/MolHash/hashfunctions.cpp
@@ -395,7 +395,7 @@ std::vector<std::uint64_t> getBondFlags(const ROMol &mol) {
       "[C]-[c](:[o,n,s]):[o,n,s]",  //< a limited version of handling this for
                                     // aromatic systems
       "*[SD4](=*)=*",               // sulfates, sulfonyls, etc.
-      "*=[SD4](=*)*",               // the other side
+      "*=[SD4;$(S(=*)=*)](*)*",     // the other side
       "*-[H0]=,#[C,N]=,#*",         // isocyanates, azides, et al
       "*[#7+]-[O-]",                // nitro groups and aromatic n-oxides
       "[O,S;H]-P=O",                // phosphoric acid, etc.
@@ -757,6 +757,73 @@ std::string TautomerHashv2(RWMol *mol, bool proto, bool useCXSmiles,
 #endif
     }
   }
+
+  /*  Another situation we need to correct for is the following:
+
+   These two tautomers should be recognized as equivalent:
+
+      O8          O9
+      ||          |
+   C1-C2-C3-C4-N5=C6-C7
+
+      O8          O9
+      |           |
+   C1-C2=C3-C4-N5=C6-C7
+
+   In the first case neither the N5-C4 nor the C3-C4 bond is recognized as being
+   tautomeric since C4 does not have two conjugated neighbors, in the second
+   case the N5-C4 bond is recognized since C4 does have two conjugated
+   neighbors.
+
+
+   In order to fix this and other similar cases we need to check the neighboring
+   atoms of each start bond and check to see they are connected to other atoms
+   which are involved in bonds which are either in a tautomeric system already
+   (i.e. bondsToModify is set for them) or are start bonds.
+
+   So in the case of the first tautomer above, here we're going to check N5-C4,
+   and recognize that C4 has a neighbor (C3) with an adjacent tautomeric bond
+   (C3-C2). So we'll flag N5-C4 as being part of the tautomeric system.
+
+  */
+  for (const auto bptr : mol->bonds()) {
+    // If this is not a possible starting bond,
+    // then skip it
+    if (!startBonds[bptr->getIdx()]) {
+      continue;
+    }
+
+    for (const auto atm :
+         std::vector<const Atom *>{bptr->getBeginAtom(), bptr->getEndAtom()}) {
+      for (const auto nbrBond : mol->atomBonds(atm)) {
+        if (nbrBond == bptr || bondsToModify[nbrBond->getIdx()]) {
+          continue;
+        }
+        const auto oatom = nbrBond->getOtherAtom(atm);
+        if (!oatom->getTotalNumHs()) {
+          continue;
+        }
+        unsigned int numModifiedNeighbors = 0;
+        for (const auto nbr : mol->atomNeighbors(oatom)) {
+          if (nbr == atm) {
+            continue;
+          }
+          for (const auto nbnd : mol->atomBonds(nbr)) {
+            if (bondsToModify[nbnd->getIdx()] || startBonds[nbnd->getIdx()]) {
+              ++numModifiedNeighbors;
+              break;
+            }
+          }
+          if (numModifiedNeighbors) {
+            break;
+          }
+        }
+        if (numModifiedNeighbors) {
+          bondsToModify.set(nbrBond->getIdx());
+        }
+      }
+    }
+  }
 #ifdef VERBOSE_HASH
   std::cerr << "FINAL: " << bondsToModify << std::endl;
 #endif


Fix indexing of heavy atoms in PubChemShape (rdkit/rdkit#8417)

#### Reference Issue
Fixes #8416

#### What does this implement/fix? Explain your changes.
The loop was going over all atoms, but only updating coords for non-H atoms so gaps could be left in the array and values written over the end of the vector.

#### Any other comments?
I couldn't find a simple test that crashed it, so haven't added one.  However the test "Github #8096" now gives Tanimoto scores of 1.0 for shape and colour which I think is correct since the deuterium shouldn't be affecting things at all.  Previously, it caused an extra coordinate in the array which was left at its default value of 0.0, 0.0, 0.0 (atom index 4).
It is not impossible that whatever was done to fix #8096 didn't get to the root of the problem, or there were 2 problems.

Overwrite end of array in PubChemShape.cpp

**Describe the bug**
Core dump when passing molecule with hydrogens to PrepareConformer in PubChemShape.cpp

**To Reproduce**
I can't make a short reproducible that causes a crash.

**Expected behavior**
No crash in my code!

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Configuration (please complete the following information):**
 - RDKit version: built from master
 - OS: [e.g. Ubuntu 20.04] Ubuntu 22.04 and macOS latest
 - Python version (if relevant):
 - Are you using conda?
 - If you are using conda, which channel did you install the rdkit from?
 - If you are not using conda: how did you install the RDKit?

**Additional context**
Fix imminent.

Tanimoto score exceeds 1 for deuterium-containing molecules in rdShapeAlign.AlignMol()

**Issue**
rdShapeAlign.AlignMol() returns Tanimoto scores above 1 for deuterium-containing molecules, which is problematic because ChEMBL includes deuterated drugs, so others are also likely to encounter this issue. Tanimoto scores should always lie between 0 and 1.

**Root Cause**
In `PubChemShape.cpp` (function `PrepareConformer`), the code indexes atom-related arrays using the full atom index `i` instead of using a dedicated counter for heavy atoms. This results in out-of-range memory writes ("memory overwrites") and corrupts data used to calculate Tanimoto scores.

**To Reproduce**
```python
from rdkit import Chem
from rdkit.Chem import rdShapeAlign, rdDistGeom

query_smi_2H = '[2H]c1ccccc1'
query_smi    = 'c1ccccc1'
ref_smi      = 'c1ccccc1'

query_2H = Chem.AddHs(Chem.MolFromSmiles(query_smi_2H), addCoords=True)
query    = Chem.AddHs(Chem.MolFromSmiles(query_smi), addCoords=True)
ref_mol  = Chem.AddHs(Chem.MolFromSmiles(ref_smi), addCoords=True)

params = rdDistGeom.ETKDGv3()
params.randomSeed = 0xa100f
for mol in [query_2H, query, ref_mol]:
    rdDistGeom.EmbedMultipleConfs(mol, numConfs=1, params=params)

print("With [2H]:", rdShapeAlign.AlignMol(query_2H, ref_mol))
print("Without [2H]:", rdShapeAlign.AlignMol(query, ref_mol))
```

```
With [2H]: (1.2137843687421104, 0.8742135141493095)
Without [2H]: (0.9999984471607044, 0.9999999704411953)
```

**Expected Behavior**
Tanimoto scores should remain within the range [0,1].

**Actual Behavior**
Scores exceed 1 when 2H atoms are present.

**Proposed Fix**
Introduce a separate heavy-atom index counter to prevent out-of-bound writes:

```cpp
unsigned int heavyAtomIdx = 0; // Dedicated counter for heavy atoms
if (Z > 1) {
    rad_vector[heavyAtomIdx] = vdw_radii.at(Z); // Use heavyAtomIdx for indexing
    heavyAtomIdx++;
}
```

**Environment**
* RDKit version: 2024.09.3
* Build: py313hc8d2389_0 (conda-forge)
* OS: Ubuntu 24.04.1 LTS
* Python: 3.13.1
* Conda: Yes

**Additional Info**
After applying this fix, the Tanimoto scores remain within the expected range. The updated [PubChemShape.cpp.txt](https://github.com/user-attachments/files/18132106/PubChemShape.cpp.txt) is attached for reference.
I am not a C++ coder, so I am not fully certain this is the optimal solution, but it resolved the issue for me.

diff --git a/External/pubchem_shape/PubChemShape.cpp b/External/pubchem_shape/PubChemShape.cpp
index 014fa36767b..e25b44c188c 100644
--- a/External/pubchem_shape/PubChemShape.cpp
+++ b/External/pubchem_shape/PubChemShape.cpp
@@ -283,15 +283,16 @@ ShapeInput PrepareConformer(const ROMol &mol, int confId, bool useColors) {
   res.shift = {-ave.x, -ave.y, -ave.z};
   res.coord.resize(nAlignmentAtoms * 3);

-  for (unsigned i = 0; i < nAtoms; ++i) {
+  for (unsigned i = 0, j = 0; i < nAtoms; ++i) {
     // use only non-H for alignment
     if (mol.getAtomWithIdx(i)->getAtomicNum() > 1) {
       RDGeom::Point3D pos = conformer.getAtomPos(i);
       pos -= ave;

-      res.coord[i * 3] = pos.x;
-      res.coord[(i * 3) + 1] = pos.y;
-      res.coord[(i * 3) + 2] = pos.z;
+      res.coord[j * 3] = pos.x;
+      res.coord[(j * 3) + 1] = pos.y;
+      res.coord[(j * 3) + 2] = pos.z;
+      ++j;
     }
   }

diff --git a/External/pubchem_shape/PubChemShape.hpp b/External/pubchem_shape/PubChemShape.hpp
index 245dba9d0e5..a55064f9d7f 100644
--- a/External/pubchem_shape/PubChemShape.hpp
+++ b/External/pubchem_shape/PubChemShape.hpp
@@ -2,6 +2,9 @@
 #include <map>
 #include <vector>

+#ifndef RDKIT_PUBCHEMSHAPE_GUARD
+#define RDKIT_PUBCHEMSHAPE_GUARD
+
 //! The input for the pubchem shape alignment code
 struct RDKIT_PUBCHEMSHAPE_EXPORT ShapeInput {
   std::vector<float> coord;
@@ -66,3 +69,5 @@ RDKIT_PUBCHEMSHAPE_EXPORT std::pair<double, double> AlignMolecule(
     int refConfId = -1, int fitConfId = -1, bool useColors = true,
     double opt_param = 1.0, unsigned int max_preiters = 10u,
     unsigned int max_postiters = 30u);
+
+#endif
\ No newline at end of file

diff --git a/External/pubchem_shape/test.cpp b/External/pubchem_shape/test.cpp
index 9dd34e2909b..737b0941508 100644
--- a/External/pubchem_shape/test.cpp
+++ b/External/pubchem_shape/test.cpp
@@ -278,7 +278,7 @@ TEST_CASE("Github #8096") {
     REQUIRE(m2);
     std::vector<float> matrix(12, 0.0);
     auto [nbr_st, nbr_ct] = AlignMolecule(*m1, *m2, matrix);
-    CHECK_THAT(nbr_st, Catch::Matchers::WithinAbs(0.940, 0.005));
-    CHECK_THAT(nbr_ct, Catch::Matchers::WithinAbs(0.902, 0.005));
+    CHECK_THAT(nbr_st, Catch::Matchers::WithinAbs(1.0, 0.005));
+    CHECK_THAT(nbr_ct, Catch::Matchers::WithinAbs(1.0, 0.005));
   }
 }


Synthon space bad chiral count (rdkit/rdkit#8550)

#### Reference Issue
Fixes #8549

#### What does this implement/fix? Explain your changes.

#### Any other comments?

SynthonSpaceSearch bad chiral atom count

**Describe the bug**
When counting the atoms that are chiral or potentially chiral, it also counts stereo bonds.  That's clearly wrong, but if the bond idx is greater than the number of atoms it also throws an exception.

**To Reproduce**
```
  SynthonSpace space;
  std::istringstream iss(R"(SMILES	synton_id	synton#	reaction_id	release
C[U]	200011483129	1	4a	2024-09
c1c/c2n3/c1=C\C1=N/C(=C\c4c(C)c5c(n4[Mg]3)/C(=C3\N=C(\C=2)[C@@H](C)[C@@H]3C)[C@@H](C)C5=[U])C=C1	bad	2	4a	2024-09
)");
  bool cancelled = false;
  space.readStream(iss, cancelled);
```
This is a bit of FreedomSpace with a cut-down piece of an offending synthon.

**Expected behavior**
This molecule shouldn't throw an exception, although that's a side-effect of the bug itself.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Configuration (please complete the following information):**
 - RDKit version: current master branch.
 - OS: [e.g. Ubuntu 20.04]. macOS, but this is not OS dependent
 - Python version (if relevant):
 - Are you using conda?
 - If you are using conda, which channel did you install the rdkit from?
 - If you are not using conda: how did you install the RDKit? Built from source.

**Additional context**
Fix incoming.

diff --git a/Code/GraphMol/Descriptors/DCLV.cpp b/Code/GraphMol/Descriptors/DCLV.cpp
index 6a38e794d88..472a00a193a 100644
--- a/Code/GraphMol/Descriptors/DCLV.cpp
+++ b/Code/GraphMol/Descriptors/DCLV.cpp
@@ -288,7 +288,7 @@ struct State {
     elem->radius2 = rad * rad;

     if (dotDensity) {
-      long count = ((4.0 * M_PI) * rad * rad * dotDensity);
+      unsigned long count = ((4.0 * M_PI) * rad * rad * dotDensity);
       std::vector<DotStruct> dots(count);

       unsigned int equat = sqrt(M_PI * count);
diff --git a/Code/GraphMol/Kekulize.cpp b/Code/GraphMol/Kekulize.cpp
index 8805222cfc1..8a77330574c 100644
--- a/Code/GraphMol/Kekulize.cpp
+++ b/Code/GraphMol/Kekulize.cpp
@@ -140,7 +140,7 @@ void markDbondCands(RWMol &mol, const INT_VECT &allAtms,

     auto numAtomRings = mol.getRingInfo()->numAtomRings(at->getIdx());
     const auto &riVect = mol.getRingInfo()->atomMembers(at->getIdx());
-    auto numNonCandRings = std::count_if(
+    size_t numNonCandRings = std::count_if(
         riVect.begin(), riVect.end(),
         [&isRingNotCand](int ri) { return isRingNotCand.test(ri); });
     if (!at->getAtomicNum() && nonArNonDummyNbr < numAtomRings &&
diff --git a/Code/GraphMol/SynthonSpaceSearch/SynthonSpaceSearch_details.cpp b/Code/GraphMol/SynthonSpaceSearch/SynthonSpaceSearch_details.cpp
index 55b3b3b7d8e..0dedbaf64fe 100644
--- a/Code/GraphMol/SynthonSpaceSearch/SynthonSpaceSearch_details.cpp
+++ b/Code/GraphMol/SynthonSpaceSearch/SynthonSpaceSearch_details.cpp
@@ -873,6 +873,9 @@ unsigned int countChiralAtoms(ROMol &mol, unsigned int *numExcDummies) {
     *numExcDummies = 0;
   }
   for (auto &si : sis) {
+    if (si.type != Chirality::StereoType::Atom_Tetrahedral) {
+      continue;
+    }
     ++numChiralAtoms;
     if (numExcDummies) {
       auto atom = mol.getAtomWithIdx(si.centeredOn);

diff --git a/Code/GraphMol/SynthonSpaceSearch/substructure_search_catch_tests.cpp b/Code/GraphMol/SynthonSpaceSearch/substructure_search_catch_tests.cpp
index 0e4afb7e994..8a50027aaca 100644
--- a/Code/GraphMol/SynthonSpaceSearch/substructure_search_catch_tests.cpp
+++ b/Code/GraphMol/SynthonSpaceSearch/substructure_search_catch_tests.cpp
@@ -726,6 +726,7 @@ F[2*]	277310376-742385dd	2	fake-chiral
   auto res2 = space.substructureSearch(*mol2);
   CHECK(res2.getHitMolecules().size() == 2);
 }
+
 TEST_CASE("Chiral substructure search") {
   SynthonSpace space;
   std::istringstream iss(R"(SMILES	synton_id	synton#	reaction_id
@@ -759,4 +760,14 @@ F[2*]	277310376-742385dd	2	fake-chiral
   sparams.maxHitChiralAtoms = 0;
   auto res4 = space.substructureSearch(*qmol, mparams, sparams);
   CHECK(res4.getHitMolecules().size() == 0);
-}
\ No newline at end of file
+}
+
+TEST_CASE("Bad Chiral Atom Count") {
+  SynthonSpace space;
+  std::istringstream iss(R"(SMILES	synton_id	synton#	reaction_id	release
+C[U]	200011483129	1	4a	2024-09
+c1c/c2n3/c1=C\C1=N/C(=C\c4c(C)c5c(n4[Mg]3)/C(=C3\N=C(\C=2)[C@@H](C)[C@@H]3C)[C@@H](C)C5=[U])C=C1	bad	2	4a	2024-09
+)");
+  bool cancelled = false;
+  CHECK_NOTHROW(space.readStream(iss, cancelled));
+}


Fixes #8569 (rdkit/rdkit#8588)

While fixing #8569 I could not restrain myself from refactoring the MMPA code, which is of the same C-style/reinvent the wheel multiple times/add as much useless boilerplate as possible/add irrelevant micro-optimizations and waste a lot of time on std::find or nested loops quality, or worse, than the MCS code.

* Fix #8569
* Remove duplicate `#include`
* Replace explicit types with `auto` where appropriate
* Replace C-style casts with `static_cast`
* Replace C-style `for` loops with iterators or STL functions where appropriate
* Avoid unnecessary loops when copying vectors
* Move duplicate code into functions
* Use `fragmentOnBonds` to simplify code and retain stereochemistry
* Replace inefficient nested `for` loop on atom neighbors with single `for` loop on bonds
* Replace expensive `std::find` calls with more efficient `boost::dynamic_bitset::test`
* Do not tamper with pre-existing atom map numbers on atoms other than dummy atoms, as they have nothing to do with the MMPA code and no assumptions should be made

RDKit minilib loses stereochemistry when breaking molecules to fragments

Rdkit loses stereochemistry information when breaking into fragments.

For instance, when breaking C(\C=C\c1ccccc1)N2CCN(CC2)C(c3ccccc3)c4ccccc4 into fragments with mol.get_mmpa_frags() we get the result without stereochemistry: C(=C[*:1])c1ccccc1.c1ccc(C(c2ccccc2)N2CCN(C[*:1])CC2)cc1. Though we expect to have C(=C/[*:1])/c1ccccc1 as a fragment.

Is it possible to keep this stereochemistry information when breaking into fragments?

diff --git a/Code/GraphMol/MMPA/MMPA.cpp b/Code/GraphMol/MMPA/MMPA.cpp
index dc029c08f23..e63a3cc55f1 100644
--- a/Code/GraphMol/MMPA/MMPA.cpp
+++ b/Code/GraphMol/MMPA/MMPA.cpp
@@ -17,7 +17,7 @@
 #include "../SmilesParse/SmilesWrite.h"
 #include "../Substruct/SubstructMatch.h"
 #include <GraphMol/new_canon.h>
-#include <GraphMol/MolOps.h>
+#include <GraphMol/ChemTransforms/MolFragmenter.h>
 #include "MMPA.h"

 // #define MMPA_DEBUG // enable debug info output
@@ -30,36 +30,32 @@ typedef std::vector<std::pair<unsigned, unsigned>>

 namespace detail {
 unsigned long long computeMorganCodeHash(const ROMol &mol) {
-  size_t nv = mol.getNumAtoms();
-  size_t ne = mol.getNumBonds();
-  std::vector<unsigned long> currCodes(nv);
-  std::vector<unsigned long> prevCodes(nv);
-  size_t nIterations = mol.getNumBonds();
+  auto nv = mol.getNumAtoms();
+  std::vector<unsigned long> currCodes;
+  currCodes.reserve(nv);
+  std::vector<unsigned long> prevCodes;
+  auto nIterations = mol.getNumBonds();
   if (nIterations > 5) {
     nIterations = 5;
   }

-  for (unsigned ai = 0; ai < nv; ai++) {
-    const Atom &a = *mol.getAtomWithIdx(ai);
-    unsigned atomCode = a.getAtomicNum();
-    atomCode |= a.getIsotope() << 8;
+  for (const auto a : mol.atoms()) {
+    auto atomCode = a->getAtomicNum();
+    atomCode |= a->getIsotope() << 8;

-    auto charge = a.getFormalCharge();
+    auto charge = a->getFormalCharge();
     atomCode |= std::abs(charge) << 16;
     if (charge < 0) {
       atomCode |= 1 << 29;
     }
-    atomCode |= (a.getIsAromatic() ? 1 : 0) << 30;
-    currCodes[ai] = atomCode;
+    atomCode |= (a->getIsAromatic() ? 1 : 0) << 30;
+    currCodes.push_back(atomCode);
   }

   for (size_t iter = 0; iter < nIterations; iter++) {
-    for (size_t i = 0; i < nv; i++) {
-      prevCodes[i] = currCodes[i];
-    }
+    prevCodes = currCodes;

-    for (size_t bi = 0; bi < ne; bi++) {
-      const Bond *bond = mol.getBondWithIdx(bi);
+    for (const auto bond : mol.bonds()) {
       unsigned order = bond->getBondType();
       unsigned atom1 = bond->getBeginAtomIdx();
       unsigned atom2 = bond->getEndAtomIdx();
@@ -71,12 +67,56 @@ unsigned long long computeMorganCodeHash(const ROMol &mol) {
     }
   }

-  unsigned long long result = 0;
-  for (unsigned ai = 0; ai < nv; ai++) {
-    unsigned long code = currCodes[ai];
-    result += code * (code + 6849) + 29;
+  return std::accumulate(currCodes.begin(), currCodes.end(), 0ULL,
+                         [](unsigned long long acc, unsigned long code) {
+                           return acc + code * (code + 6849) + 29;
+                         });
+}
+
+// if skipDoubleBonds is true, double bonds are skipped
+// if doubleBondFlag is false, non-double bonds are skipped
+void addBondsFromTemplate(const RWMol &templateMol, RWMol &newMol,
+                          const std::map<unsigned, unsigned> &newAtomMap,
+                          const boost::dynamic_bitset<> &isAtomInFragment,
+                          bool skipDoubleBonds) {
+  for (const auto templateBond : templateMol.bonds()) {
+    bool isDoubleBond = (templateBond->getBondType() == Bond::DOUBLE);
+    bool shouldProcessBond = (isDoubleBond ^ skipDoubleBonds);
+    if (!shouldProcessBond ||
+        !isAtomInFragment.test(templateBond->getBeginAtomIdx()) ||
+        !isAtomInFragment.test(templateBond->getEndAtomIdx())) {
+      continue;
+    }
+    auto ai1 = newAtomMap.at(templateBond->getBeginAtomIdx());
+    auto ai2 = newAtomMap.at(templateBond->getEndAtomIdx());
+    auto newBondIdx = newMol.addBond(ai1, ai2, templateBond->getBondType()) - 1;
+    auto newBond = newMol.getBondWithIdx(newBondIdx);
+    newBond->setBondDir(templateBond->getBondDir());
+    if (isDoubleBond) {
+      const auto &stereoAtoms = templateBond->getStereoAtoms();
+      if (stereoAtoms.size() == 2) {
+        newBond->setStereoAtoms(newAtomMap.at(stereoAtoms[0]),
+                                newAtomMap.at(stereoAtoms[1]));
+      }
+    }
   }
-  return result;
+}
+
+void extractAtoms(const RWMol &templateMol, RWMol &newMol,
+                  const INT_VECT &atomIndices) {
+  boost::dynamic_bitset<> isAtomInFragment(templateMol.getNumAtoms());
+  // key is atom index in template molecule
+  std::map<unsigned int, unsigned int> newAtomMap;
+  for (int ai : atomIndices) {
+    isAtomInFragment.set(ai);
+    auto a = templateMol.getAtomWithIdx(ai);
+    newAtomMap[ai] = newMol.addAtom(a->copy(), true, true);
+  }
+  // add bonds from this fragment skipping double bonds
+  addBondsFromTemplate(templateMol, newMol, newAtomMap, isAtomInFragment, true);
+  // add bonds from this fragment skipping non-double bonds
+  addBondsFromTemplate(templateMol, newMol, newAtomMap, isAtomInFragment,
+                       false);
 }
 }  // namespace detail

@@ -85,11 +125,13 @@ static inline void convertMatchingToBondVect(
     const std::vector<MatchVectType> &matching_atoms) {
   for (const auto &matching_atom : matching_atoms) {
     matching_bonds.emplace_back();
-    BondVector_t &mb = matching_bonds.back();  // current match
+    auto &mb = matching_bonds.back();  // current match
     // assume pattern is only one bond pattern
-    auto a1 = (unsigned)matching_atom[0].second;  // mol atom 1 index
-    auto a2 = (unsigned)matching_atom[1].second;  // mol atom 2 index
-    mb.push_back(std::pair<unsigned, unsigned>(a1, a2));
+    auto a1 =
+        static_cast<unsigned>(matching_atom[0].second);  // mol atom 1 index
+    auto a2 =
+        static_cast<unsigned>(matching_atom[1].second);  // mol atom 2 index
+    mb.emplace_back(a1, a2);
   }
 }

@@ -100,26 +142,29 @@ static void addResult(std::vector<std::pair<ROMOL_SPTR, ROMOL_SPTR>>
 #ifdef MMPA_DEBUG
   std::cout << res.size() + 1 << ": ";
 #endif
-  RWMol em(mol);
   // loop through the bonds to delete. == deleteBonds()
   unsigned isotope = 0;
   std::map<unsigned, unsigned> isotope_track;
+  std::vector<unsigned int> bondIndices;
+  bondIndices.reserve(bonds_selected.size());
+  std::vector<std::pair<unsigned int, unsigned int>> dummyLabels;
+  dummyLabels.reserve(bonds_selected.size());
   for (const auto &bi : bonds_selected) {
 #ifdef MMPA_DEBUG
     {
       std::string symbol =
-          em.getAtomWithIdx(bonds_selected[bi].first)->getSymbol();
+          mol.getAtomWithIdx(bonds_selected[bi].first)->getSymbol();
       int label = 0;
-      em.getAtomWithIdx(bonds_selected[bi].first)
+      mol.getAtomWithIdx(bonds_selected[bi].first)
           ->getPropIfPresent(common_properties::molAtomMapNumber, label);
       char a1[32];
       if (0 == label)
         sprintf(a1, "\'%s\'", symbol.c_str(), label);
       else
         sprintf(a1, "\'%s:%u\'", symbol.c_str(), label);
-      symbol = em.getAtomWithIdx(bonds_selected[bi].second)->getSymbol();
+      symbol = mol.getAtomWithIdx(bonds_selected[bi].second)->getSymbol();
       label = 0;
-      em.getAtomWithIdx(bonds_selected[bi].second)
+      mol.getAtomWithIdx(bonds_selected[bi].second)
           ->getPropIfPresent(common_properties::molAtomMapNumber, label);
       char a2[32];
       if (0 == label)
@@ -131,59 +176,52 @@ static void addResult(std::vector<std::pair<ROMOL_SPTR, ROMOL_SPTR>>
                 << bonds_selected[bi].second << a2 << ") ";
     }
 #endif
-    isotope += 1;
-    // remove the bond
-    em.removeBond(bi.first, bi.second);
-
-    // now add attachment points and set attachment point labels
-    auto *a = new Atom(0);
-    a->setProp(common_properties::molAtomMapNumber, (int)isotope);
-    unsigned newAtomA = em.addAtom(a, true, true);
-    em.addBond(bi.first, newAtomA, Bond::SINGLE);
-    a = new Atom(0);
-    a->setProp(common_properties::molAtomMapNumber, (int)isotope);
-    unsigned newAtomB = em.addAtom(a, true, true);
-    em.addBond(bi.second, newAtomB, Bond::SINGLE);
-
-    // keep track of where to put isotopes
-    isotope_track[newAtomA] = isotope;
-    isotope_track[newAtomB] = isotope;
+    ++isotope;
+    const auto oldBond = mol.getBondBetweenAtoms(bi.first, bi.second);
+    CHECK_INVARIANT(oldBond, "bond not found");
+    bondIndices.push_back(oldBond->getIdx());
+    dummyLabels.emplace_back(isotope, isotope);
+  }
+  std::unique_ptr<ROMol> fragmentedMol(
+      MolFragmenter::fragmentOnBonds(mol, bondIndices, true, &dummyLabels));
+  for (auto ai = mol.getNumAtoms(); ai < fragmentedMol->getNumAtoms(); ++ai) {
+    auto a = fragmentedMol->getAtomWithIdx(ai);
+    isotope = a->getIsotope();
+    CHECK_INVARIANT(isotope, "isotope should be >0");
+    a->setProp(common_properties::molAtomMapNumber, static_cast<int>(isotope));
+    a->setIsotope(0);
+    isotope_track[ai] = isotope;
   }
 #ifdef MMPA_DEBUG
   std::cout << "\n";
 #endif
   RWMOL_SPTR core, side_chains;  // core & side_chains output molecules

-  if (isotope == 1) {
-    side_chains = RWMOL_SPTR(new RWMol(em));  // output = '%s,%s,,%s.%s'
+  if (bondIndices.size() == 1) {
+    side_chains =
+        RWMOL_SPTR(new RWMol(*fragmentedMol));  // output = '%s,%s,,%s.%s'
 // DEBUG PRINT
 #ifdef MMPA_DEBUG
 // OK: std::cout<<res.size()+1<<" isotope="<< isotope <<","<<
 // MolToSmiles(*side_chains, true) <<"\n";
 #endif
-  } else if (isotope >= 2) {
-    std::vector<std::vector<int>> frags;
-    unsigned int nFrags = MolOps::getMolFrags(em, frags);
+  } else if (bondIndices.size() >= 2) {
+    VECT_INT_VECT frags;
+    MolOps::getMolFrags(*fragmentedMol, frags);

     // #check if its a valid triple or bigger cut.  matchObj = re.search(
     //'\*.*\*.*\*', f)
     //  check if exists a fragment with maxCut connection points (*.. *.. *)
     if (isotope >= 3) {
-      bool valid = false;
-      for (size_t i = 0; i < nFrags; i++) {
-        unsigned nLabels = 0;
-        for (int ai : frags[i]) {
-          if (isotope_track.end() !=
-              isotope_track.find(ai)) {  // new added atom
-            ++nLabels;                   // found connection point
-          }
-        }
-        if (nLabels >=
-            maxCuts) {  // looks like it should be selected as core !  ??????
-          valid = true;
-          break;
-        }
-      }
+      bool valid = std::any_of(
+          frags.begin(), frags.end(),
+          [&isotope_track, &maxCuts](const INT_VECT &frag) {
+            size_t nLabels = std::count_if(
+                frag.begin(), frag.end(), [&isotope_track](int ai) {
+                  return isotope_track.end() != isotope_track.find(ai);
+                });
+            return (nLabels >= maxCuts);
+          });
       if (!valid) {
 #ifdef MMPA_DEBUG
         std::cout << "isotope>=3: invalid fragments. fragment with maxCut "
@@ -194,47 +232,20 @@ static void addResult(std::vector<std::pair<ROMOL_SPTR, ROMOL_SPTR>>
       }
     }

-    size_t iCore = std::numeric_limits<size_t>::max();
+    auto iCore = std::numeric_limits<size_t>::max();
     side_chains = RWMOL_SPTR(new RWMol);
-    std::map<unsigned, unsigned>
-        visitedBonds;  // key is bond index in source molecule
-    unsigned maxAttachments = 0;
+    size_t maxAttachments = 0;
     for (size_t i = 0; i < frags.size(); i++) {
-      unsigned nAttachments = 0;
-      for (int ai : frags[i]) {
-        if (isotope_track.end() !=
-            isotope_track.find(ai)) {  // == if(a->hasProp("molAtomMapNumber"))
-          ++nAttachments;
-        }
-      }
+      size_t nAttachments = std::count_if(
+          frags[i].begin(), frags[i].end(), [&isotope_track](int ai) {
+            return isotope_track.end() != isotope_track.find(ai);
+          });
       if (maxAttachments < nAttachments) {
         maxAttachments = nAttachments;
       }
       if (1 == nAttachments) {  // build side-chain set of molecules from
                                 // selected fragment
-        std::map<unsigned, unsigned>
-            newAtomMap;  // key is atom index in source molecule
-        for (int ai : frags[i]) {
-          Atom *a = em.getAtomWithIdx(ai);
-          newAtomMap[ai] = side_chains->addAtom(a->copy(), true, true);
-        }
-        // add all bonds from this fragment
-        for (int ai : frags[i]) {
-          Atom *a = em.getAtomWithIdx(ai);
-          ROMol::OEDGE_ITER beg, end;
-          for (boost::tie(beg, end) = em.getAtomBonds(a); beg != end; ++beg) {
-            const Bond *bond = em[*beg];
-            if (newAtomMap.end() == newAtomMap.find(bond->getBeginAtomIdx()) ||
-                newAtomMap.end() == newAtomMap.find(bond->getEndAtomIdx()) ||
-                visitedBonds.end() != visitedBonds.find(bond->getIdx())) {
-              continue;
-            }
-            unsigned ai1 = newAtomMap[bond->getBeginAtomIdx()];
-            unsigned ai2 = newAtomMap[bond->getEndAtomIdx()];
-            unsigned bi = side_chains->addBond(ai1, ai2, bond->getBondType());
-            visitedBonds[bond->getIdx()] = bi;
-          }
-        }
+        detail::extractAtoms(*fragmentedMol, *side_chains, frags[i]);
       } else {  // select the core fragment
 // DEBUG PRINT
 #ifdef MMPA_DEBUG
@@ -252,30 +263,7 @@ static void addResult(std::vector<std::pair<ROMOL_SPTR, ROMOL_SPTR>>
     // build core molecule from selected fragment
     if (iCore != std::numeric_limits<size_t>::max()) {
       core = RWMOL_SPTR(new RWMol);
-      visitedBonds.clear();
-      std::map<unsigned, unsigned>
-          newAtomMap;  // key is atom index in source molecule
-      for (int ai : frags[iCore]) {
-        Atom *a = em.getAtomWithIdx(ai);
-        newAtomMap[ai] = core->addAtom(a->copy(), true, true);
-      }
-      // add all bonds from this fragment
-      for (int ai : frags[iCore]) {
-        Atom *a = em.getAtomWithIdx(ai);
-        ROMol::OEDGE_ITER beg, end;
-        for (boost::tie(beg, end) = em.getAtomBonds(a); beg != end; ++beg) {
-          const Bond *bond = em[*beg];
-          if (newAtomMap.end() == newAtomMap.find(bond->getBeginAtomIdx()) ||
-              newAtomMap.end() == newAtomMap.find(bond->getEndAtomIdx()) ||
-              visitedBonds.end() != visitedBonds.find(bond->getIdx())) {
-            continue;
-          }
-          unsigned ai1 = newAtomMap[bond->getBeginAtomIdx()];
-          unsigned ai2 = newAtomMap[bond->getEndAtomIdx()];
-          unsigned bi = core->addBond(ai1, ai2, bond->getBondType());
-          visitedBonds[bond->getIdx()] = bi;
-        }
-      }
+      detail::extractAtoms(*fragmentedMol, *core, frags[iCore]);
 // DEBUG PRINT
 #ifdef MMPA_DEBUG
 // std::cout<<res.size()+1<<" isotope="<< isotope <<" "<< MolToSmiles(*core,
@@ -285,9 +273,7 @@ static void addResult(std::vector<std::pair<ROMOL_SPTR, ROMOL_SPTR>>
   }
   // check for duplicates:
   bool resFound = false;
-  size_t ri = 0;
-  for (ri = 0; ri < res.size(); ri++) {
-    const std::pair<ROMOL_SPTR, ROMOL_SPTR> &r = res[ri];
+  for (const auto &r : res) {
     if (side_chains->getNumAtoms() == r.second->getNumAtoms() &&
         side_chains->getNumBonds() == r.second->getNumBonds() &&
         ((nullptr == core.get() && nullptr == r.first.get()) ||
@@ -322,26 +308,24 @@ static void addResult(std::vector<std::pair<ROMOL_SPTR, ROMOL_SPTR>>

       // clear atom labels (they are used in canonicalization)
       //  and move them to dummy storage
-      for (ROMol::AtomIterator at = tmp_side_chain.beginAtoms();
-           at != tmp_side_chain.endAtoms(); ++at) {
+      for (auto at : tmp_side_chain.atoms()) {
         int label = 0;
-        if ((*at)->getPropIfPresent(common_properties::molAtomMapNumber,
-                                    label)) {
-          (*at)->clearProp(common_properties::molAtomMapNumber);
-          oldMaps[(*at)->getIdx()] = label;
+        if (at->getAtomicNum() == 0 &&
+            at->getPropIfPresent(common_properties::molAtomMapNumber, label)) {
+          at->clearProp(common_properties::molAtomMapNumber);
+          oldMaps[at->getIdx()] = label;
         }
       }

       const bool doIsomericSmiles = true;  // should this be false???
-      std::string smiles = MolToSmiles(tmp_side_chain, doIsomericSmiles);
+      auto smiles = MolToSmiles(tmp_side_chain, doIsomericSmiles);
       // std::cerr << "smiles: " << smiles << std::endl;

       // Get the canonical output order and use it to remap
-      //  the atom maps int the side chains
-      //  these will get reapplied to the core (if there is a core)
-      const std::vector<unsigned int> &ranks =
-          tmp_side_chain.getProp<std::vector<unsigned int>>(
-              common_properties::_smilesAtomOutputOrder);
+      // the atom maps in the side chains
+      // these will get reapplied to the core (if there is a core)
+      const auto &ranks = tmp_side_chain.getProp<std::vector<unsigned int>>(
+          common_properties::_smilesAtomOutputOrder);

       std::vector<std::pair<unsigned int, int>> rankedAtoms;

@@ -360,9 +344,6 @@ static void addResult(std::vector<std::pair<ROMOL_SPTR, ROMOL_SPTR>>
       for (auto &rankedAtom : rankedAtoms) {
         if (canonicalAtomMaps.find(rankedAtom.second) ==
             canonicalAtomMaps.end()) {
-          // std::cerr << "Remapping: " << rankedAtoms[i].second << " " << " to
-          // " << (i+1) <<
-          //    std::endl;
           canonicalAtomMaps[rankedAtom.second] = ++nextMap;
         }
       }
@@ -370,29 +351,28 @@ static void addResult(std::vector<std::pair<ROMOL_SPTR, ROMOL_SPTR>>

     // std::cerr << "======== Remap core " << std::endl;
     if (core.get()) {  // remap core if it exists
-      for (ROMol::AtomIterator at = core->beginAtoms(); at != core->endAtoms();
-           ++at) {
+      for (auto at : core->atoms()) {
         int label = 0;
-        if ((*at)->getPropIfPresent(common_properties::molAtomMapNumber,
-                                    label)) {
+        if (at->getAtomicNum() == 0 &&
+            at->getPropIfPresent(common_properties::molAtomMapNumber, label)) {
           // std::cerr << "remapping core: " << label << " :" <<
           // canonicalAtomMaps[label] <<
           //    std::endl;
-          (*at)->setProp(common_properties::molAtomMapNumber,
-                         canonicalAtomMaps[label]);
+          at->setProp(common_properties::molAtomMapNumber,
+                      canonicalAtomMaps.at(label));
         }
       }
     }

     // std::cerr << "======== Remap side-chain " << std::endl;
-    for (ROMol::AtomIterator at = side_chains->beginAtoms();
-         at != side_chains->endAtoms(); ++at) {
+    for (auto at : side_chains->atoms()) {
       int label = 0;
-      if ((*at)->getPropIfPresent(common_properties::molAtomMapNumber, label)) {
+      if (at->getAtomicNum() == 0 &&
+          at->getPropIfPresent(common_properties::molAtomMapNumber, label)) {
         // std::cerr << "remapping side chain: " << label << " :" <<
         // canonicalAtomMaps[label] << std::endl;
-        (*at)->setProp(common_properties::molAtomMapNumber,
-                       canonicalAtomMaps[label]);
+        at->setProp(common_properties::molAtomMapNumber,
+                    canonicalAtomMaps.at(label));
       }
     }

@@ -408,13 +388,13 @@ static void addResult(std::vector<std::pair<ROMOL_SPTR, ROMOL_SPTR>>
 //=====================================================================
 static inline void appendBonds(BondVector_t &bonds,
                                const BondVector_t &matching_bonds) {
-  for (const auto &matching_bond : matching_bonds) {
-    bonds.push_back(matching_bond);
-  }
+  bonds.reserve(bonds.size() + matching_bonds.size());
+  bonds.insert(bonds.end(), matching_bonds.begin(), matching_bonds.end());
 }

 static inline void processCuts(
-    size_t i, size_t minCuts, size_t maxCuts, BondVector_t &bonds_selected,
+    size_t i, size_t minCuts, size_t maxCuts,
+    BondVector_t &bonds_selected,
     const std::vector<BondVector_t> &matching_bonds, const ROMol &mol,
     std::vector<std::pair<ROMOL_SPTR, ROMOL_SPTR>> &res) {
   if (maxCuts < minCuts) {
@@ -471,10 +451,11 @@ bool fragmentMol(const ROMol &mol,
 #endif

   res.clear();
-  std::unique_ptr<const ROMol> smarts((const ROMol *)SmartsToMol(pattern));
+  std::unique_ptr<const ROMol> smarts(
+      static_cast<ROMol *>(SmartsToMol(pattern)));
   std::vector<MatchVectType>
       matching_atoms;  // one bond per match ! with default pattern
-  unsigned int total = SubstructMatch(mol, *smarts, matching_atoms);
+  auto total = SubstructMatch(mol, *smarts, matching_atoms);
 #ifdef MMPA_DEBUG
   std::cout << "total substructs =" << total
             << "\nmatching bonds (atom1, atom2):\n";
@@ -531,12 +512,12 @@ bool fragmentMol(const ROMol &mol,
   std::vector<BondVector_t> matching_bonds;  // List of matched query's bonds

   for (auto i : bondsToCut) {
-    const Bond *bond = mol.getBondWithIdx(i);
+    const auto bond = mol.getBondWithIdx(i);
     BondVector_t bonds;
-    unsigned int a1 = bond->getBeginAtomIdx();
-    unsigned int a2 = bond->getEndAtomIdx();
-    bonds.push_back(std::make_pair(a1, a2));
-    matching_bonds.push_back(bonds);
+    auto a1 = bond->getBeginAtomIdx();
+    auto a2 = bond->getEndAtomIdx();
+    bonds.emplace_back(a1, a2);
+    matching_bonds.push_back(std::move(bonds));
   }

   // loop to generate every cut in the molecule

diff --git a/Code/GraphMol/MMPA/MMPA_UnitTest.cpp b/Code/GraphMol/MMPA/MMPA_UnitTest.cpp
index 11d772c7292..5606f9f4d97 100644
--- a/Code/GraphMol/MMPA/MMPA_UnitTest.cpp
+++ b/Code/GraphMol/MMPA/MMPA_UnitTest.cpp
@@ -718,6 +718,29 @@ void testMorganCodeHashChargeShift() {
   }
 }

+void testGithub8569() {
+  auto mol = "C\\C=C\\C=C\\C"_smiles;
+  std::vector<std::pair<ROMOL_SPTR, ROMOL_SPTR>> res;
+  TEST_ASSERT(RDKit::MMPA::fragmentMol(*mol, res, 3));
+  TEST_ASSERT(res.size() == 4);
+  std::vector<std::pair<std::string, std::string>> resAsSmiles(res.size());
+  std::transform(
+      res.begin(), res.end(), resAsSmiles.begin(), [](const auto &pair) {
+        return std::make_pair(pair.first ? MolToSmiles(*pair.first) : "null",
+                              pair.second ? MolToSmiles(*pair.second) : "null");
+      });
+  std::vector<std::pair<std::string, std::string>> expected = {
+      {"null", "C/C=C/C=C/[*:1].C[*:1]"},
+      {"C(=C/[*:2])\\[*:1]", "C/C=C/[*:1].C[*:2]"},
+      {"C(/C=C/[*:2])=C\\[*:1]", "C[*:1].C[*:2]"},
+      {"null", "C/C=C/[*:1].C/C=C/[*:1]"},
+  };
+  for (unsigned int i = 0; i < resAsSmiles.size(); ++i) {
+    TEST_ASSERT(resAsSmiles[i].first == expected[i].first);
+    TEST_ASSERT(resAsSmiles[i].second == expected[i].second);
+  }
+}
+
 int main() {
   BOOST_LOG(rdInfoLog)
       << "*******************************************************\n";
@@ -741,6 +764,7 @@ int main() {
   test3();

   testMorganCodeHashChargeShift();
+  testGithub8569();

   //    test4();
   // */