Fixes #5008: Add H and X specification to P tautomerization rules (rdkit/rdkit#5077)

The change is simple: make sure that the P involved in the tautomer shifts only has three neighbors and doesn't have an H (this is to avoid hypervalent P)

Bad tautomers produced for phosphorous compounds

**Describe the bug**
rdMolStandardize.TautomerEnumerator().Enumerate() gives tautomers that chemically are not possible and rdMolStandardize.TautomerEnumerator().Canonicalize() assigns the chemically non-plausible tautomer as the canonical tautomer.

**To Reproduce**
```
from rdkit import Chem
from rdkit.Chem.MolStandardize import rdMolStandardize

lig = Chem.MolFromSmiles("NP(=O)(O)N(CCCl)CCCl")
canonicalizer = rdMolStandardize.TautomerEnumerator()
tautomers = canonicalizer.Enumerate(lig)

"""
Tautomers generated:
Tautomer 1: N=P(O)(O)N(CCCl)CCCl
Tautomer 2: N=[PH](=O)(O)N(CCCl)CCCl
Tautomer 3: NP(=O)(O)N(CCCl)CCCl
Tautomer 4: N[PH](=O)(=O)N(CCCl)CCCl
"""

canonical_tautomer = canonicalizer.Canonicalize(lig)

# Canonical tautomer: N[PH](=O)(=O)N(CCCl)CCCl
```

**Expected behavior**
Tautomers 2 and 4 should not be returned and tautomer 4 should not be picked as the canonical tautomer.

**Configuration:**
 - RDKit version: 2021.09.4
 - OS: CentOS Linux 7, GNOME Version 3.28.2
 - Python version (if relevant): 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/MolStandardize/TautomerCatalog/tautomerTransforms.in b/Code/GraphMol/MolStandardize/TautomerCatalog/tautomerTransforms.in
index b8328122aae..ba4a030e9fb 100644
--- a/Code/GraphMol/MolStandardize/TautomerCatalog/tautomerTransforms.in
+++ b/Code/GraphMol/MolStandardize/TautomerCatalog/tautomerTransforms.in
@@ -49,7 +49,7 @@ const std::vector<
         std::make_tuple(
             std::string("1,3 heteroatom H shift"),
             std::string(
-                "[#7+0,S,O,Se,Te;!H0]-[#7X2,#6,#15]=[#7+0,#16,#8,Se,Te]"),
+                "[#7+0,S,O,Se,Te;!H0]-[#7X2,#6,#15X3H0]=[#7+0,#16,#8,Se,Te]"),
             std::string(""), std::string("")),
         std::make_tuple(
             std::string("1,5 aromatic heteroatom H shift"),
@@ -135,9 +135,9 @@ const std::vector<
                         std::string("[N+!H0]#[C-]"), std::string("#"),
                         std::string("-+")),
         std::make_tuple(std::string("phosphonic acid f"),
-                        std::string("[OH]-[PH0]"), std::string("="),
+                        std::string("[OH]-[PX3H0]"), std::string("="),
                         std::string("")),
         std::make_tuple(std::string("phosphonic acid r"),
-                        std::string("[PH]=[O]"), std::string("-"),
+                        std::string("[PX4H]=[O]"), std::string("-"),
                         std::string(""))};
 }  // namespace defaults
\ No newline at end of file

diff --git a/Code/GraphMol/MolStandardize/catch_tests.cpp b/Code/GraphMol/MolStandardize/catch_tests.cpp
index 2e2b157e028..3067192bdee 100644
--- a/Code/GraphMol/MolStandardize/catch_tests.cpp
+++ b/Code/GraphMol/MolStandardize/catch_tests.cpp
@@ -843,4 +843,28 @@ TEST_CASE(
     CHECK(m2->getAtomWithIdx(0)->getFormalCharge() == 2);
     CHECK(m2->getAtomWithIdx(1)->getFormalCharge() == -1);
   }
+}
+
+TEST_CASE("Github #5008: bad tautomers for phosphorous compounds") {
+  SECTION("as reported") {
+    auto m = "NP(=O)(O)N(CCCl)CCCl"_smiles;
+    REQUIRE(m);
+    MolStandardize::TautomerEnumerator tenum;
+    auto tauts = tenum.enumerate(*m);
+    CHECK(tauts.size() == 1);
+  }
+  SECTION("P which should tautomerize") {
+    auto m = "CP(O)C"_smiles;
+    REQUIRE(m);
+    MolStandardize::TautomerEnumerator tenum;
+    auto tauts = tenum.enumerate(*m);
+    CHECK(tauts.size() == 2);
+  }
+  SECTION("Canonical version") {
+    auto m = "CP(O)C"_smiles;
+    REQUIRE(m);
+    std::unique_ptr<RWMol> ct(MolStandardize::canonicalTautomer(m.get()));
+    REQUIRE(ct);
+    CHECK(MolToSmiles(*ct) == "C[PH](C)=O");
+  }
 }
\ No newline at end of file
diff --git a/Code/GraphMol/MolStandardize/test_data/tautomerTransforms.in b/Code/GraphMol/MolStandardize/test_data/tautomerTransforms.in
index 3559f73345d..74509ded661 100644
--- a/Code/GraphMol/MolStandardize/test_data/tautomerTransforms.in
+++ b/Code/GraphMol/MolStandardize/test_data/tautomerTransforms.in
@@ -9,7 +9,7 @@ special imine f	[N!H0]-[C]=[CX3R0]
 special imine r	[CX4!H0]-[c]=[n]
 1,3 aromatic heteroatom H shift f	[#7+0!H0]-[#6R1]=[O,#7X2+0]
 1,3 aromatic heteroatom H shift 	[O,#7+0;!H0]-[#6R1]=[#7+0X2]
-1,3 heteroatom H shift	[#7+0,S,O,Se,Te;!H0]-[#7X2,#6,#15]=[#7+0,#16,#8,Se,Te]
+1,3 heteroatom H shift	[#7+0,S,O,Se,Te;!H0]-[#7X2,#6,#15X3H0]=[#7+0,#16,#8,Se,Te]
 1,5 aromatic heteroatom H shift	[#7+0,#16,#8;!H0]-[#6,#7]=[#6]-[#6,#7]=[#7+0,#16,#8;H0]
 1,5 aromatic heteroatom H shift 	[#7+0,#16,#8,Se,Te;!H0]-[#6,nX2]=[#6,nX2]-[#6,#7X2]=[#7X2+0,S,O,Se,Te]
 1,5 aromatic heteroatom H shift r	[#7+0,S,O,Se,Te;!H0]-[#6,#7X2]=[#6,nX2]-[#6,nX2]=[#7+0,#16,#8,Se,Te]
@@ -33,5 +33,5 @@ formamidinesulfinic acid f	[O,N;!H0]-[C]=[S,Se,Te;v6]=[O]	=--
 formamidinesulfinic acid r	[O!H0]-[S,Se,Te;v4]-[C]=[O,N]	==-
 isocyanide f	[C-0!H0]#[N+0]	#	-+
 isocyanide r	[N+!H0]#[C-]	#	-+
-phosphonic acid f	[OH]-[PH0]	=
-phosphonic acid r	[PH]=[O]	-
+phosphonic acid f	[OH]-[PX3H0]	=
+phosphonic acid r	[PX4H]=[O]	-


Fixes #5134 (rdkit/rdkit#5136)

This is one of those "can't believe it survived that long" bugs.

The fix is reasonably straightforward. Here's a description of the change (the part after the OR):
```
    // conditions for a ring/fused ring to be considered for aromaticity here:
    //   all atoms are candidates to be aromatic
    //     OR
    //   at least two members are candidates to be aromatic AND all of the
    //     non-candidates are in at least two fused rings
    //   This last condition is there to handle the molecule
    //      C1=CC2=CC3=CC=C4C=C1C2C34
    //   where the outer envelope is aromatic, but all of the sub-rings have SP3
    //   carbons in them. (This was github #5134)
```

In the process of fixing this, I also ended up fixing #5078

Can't roundtrip CHEMBL4080644 through SMILES

**Describe the bug**
The SMILES generated from the molblock for CHEMBL4080644 cannot be sanitized:
```
In [2]: chembl4080644 = """
     RDKit          2D

 22 25  0  0  0  0  0  0  0  0999 V2000
    6.1174   -5.5956    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
    5.5355   -6.1734    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    5.9052   -6.9054    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    6.7157   -6.7800    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    6.8468   -5.9705    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    3.9896   -4.3666    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    3.9885   -5.1861    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    4.6965   -5.5951    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    4.6947   -3.9577    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    5.4033   -4.3630    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    5.4067   -5.1882    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    6.1123   -3.9452    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
    6.8293   -4.3571    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    6.8321   -5.1820    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    7.5468   -5.5902    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    8.2590   -5.1746    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    8.2522   -4.3466    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    7.5370   -3.9422    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    3.2804   -5.5941    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    8.9563   -3.9319    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    7.6601   -5.9680    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
    4.7196   -6.1277    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
  2  3  1  0
  3  4  2  0
  4  5  1  0
  5  1  1  0
  6  7  2  0
  7  8  1  0
  8 11  2  0
 10  9  2  0
  9  6  1  0
 10 11  1  0
 10 12  1  0
 11  1  1  0
  1 14  1  0
 13 12  1  0
 13 14  2  0
 14 15  1  0
 15 16  2  0
 16 17  1  0
 17 18  2  0
 18 13  1  0
  7 19  1  0
 17 20  1  0
  5 21  2  0
  2 22  2  0
  2 12  1  0
M  END
"""

In [3]: Chem.MolToSmiles(Chem.MolFromMolBlock(chembl4080644))
Out[3]: 'Cc1ccc2c(c1)-n1-c(=O)/c=c\\c(=O)-n-2-c2cc(C)ccc2-1'

In [4]: Chem.MolFromSmiles(_3)
[05:52:40] Can't kekulize mol.  Unkekulized atoms: 1 2 3 4 5 6 10 11 15 16 17 19 20 21
```
This came up in discussion #5073

The bridgehead Ns in this molecule are somehow being marked as aromatic and that's what's causing the problem.
Additional info: when parsing the SMILES corresponding to the mol block above we don't have that problem:
```
Chem.CanonSmiles('CC1=CC=C2C(=C1)N1C(=O)/C=C\\C(=O)N2C2=C1C=CC(C)=C2')
  --> Cc1ccc2c(c1)N1C(=O)/C=C\\C(=O)N2c2cc(C)ccc21
```

The problem is that after processing the mol block we have aromatic atoms (the two Ns) which have no aromatic bonds to them. The aromaticity code should detect this case and unset the aromaticity on the atom.

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

diff --git a/Code/GraphMol/Aromaticity.cpp b/Code/GraphMol/Aromaticity.cpp
index a8b4fc67253..95a5615e890 100644
--- a/Code/GraphMol/Aromaticity.cpp
+++ b/Code/GraphMol/Aromaticity.cpp
@@ -150,25 +150,24 @@ static void applyHuckelToFused(
     ROMol &mol,                   // molecule of interest
     const VECT_INT_VECT &srings,  // list of all ring as atom IDS
     const VECT_INT_VECT &brings,  // list of all rings as bond ids
-    const INT_VECT &fused,       // list of ring ids in the current fused system
+    const INT_VECT &fused,  // list of ring ids in the current fused system
+    const boost::dynamic_bitset<>
+        &acands,  // whether or not an atom is a candidate for aromaticity
     const VECT_EDON_TYPE &edon,  // electron donor state for each atom
     INT_INT_VECT_MAP &ringNeighs,
     int &narom,  // number of aromatic ring so far
     unsigned int maxNumFusedRings, const std::vector<Bond *> &bondsByIdx,
     unsigned int minRingSize = 0);

-void markAtomsBondsArom(ROMol &mol, const VECT_INT_VECT &srings,
+void markAtomsBondsArom(ROMol &mol, const INT_VECT &unon,
                         const VECT_INT_VECT &brings, const INT_VECT &ringIds,
                         std::set<unsigned int> &doneBonds,
                         const std::vector<Bond *> &bondsByIdx) {
   // first mark the atoms in the rings
-  for (auto ri : ringIds) {
-    const auto &aring = srings[ri];
-
-    // first mark the atoms in the ring
-    for (auto ai : aring) {
-      mol.getAtomWithIdx(ai)->setIsAromatic(true);
-    }
+  boost::dynamic_bitset<> activeAtoms(mol.getNumAtoms());
+  for (auto ai : unon) {
+    activeAtoms.set(ai);
+    mol.getAtomWithIdx(ai)->setIsAromatic(true);
   }

   // mark the bonds
@@ -191,6 +190,10 @@ void markAtomsBondsArom(ROMol &mol, const VECT_INT_VECT &srings,
     // std::cerr << " " << bci->first << "(" << bci->second << ")";
     if (bci.second == 1) {
       auto bond = bondsByIdx[bci.first];
+      if (!activeAtoms[bond->getBeginAtomIdx()] ||
+          !activeAtoms[bond->getEndAtomIdx()]) {
+        continue;
+      }
       // Bond *bond = mol.get BondWithIdx(bci->first);
       bond->setIsAromatic(true);
       switch (bond->getBondType()) {
@@ -273,27 +276,60 @@ bool incidentMultipleBond(const Atom *at) {
   return at->getValence(Atom::ValenceType::EXPLICIT) != deg;
 }

+bool ringCanBeAromatic(ROMol &, const INT_VECT &ring,
+                       const VECT_EDON_TYPE &edon, unsigned int minRingSize) {
+  if (ring.size() < minRingSize) {
+    return false;
+  }
+  unsigned int nAnyElectronDonorType = 0;
+  for (auto idx : ring) {
+    ElectronDonorType edonType = edon[idx];
+    if (edonType == AnyElectronDonorType) {
+      ++nAnyElectronDonorType;
+      if (nAnyElectronDonorType > 1) {
+        return false;
+      }
+    } else if (edonType == NoElectronDonorType) {
+      // if the ring includes atoms which can't be aromatic we can just skip it
+      return false;
+    }
+  }
+  return true;
+}
+
 bool applyHuckel(ROMol &, const INT_VECT &ring, const VECT_EDON_TYPE &edon,
-                 unsigned int minRingSize) {
+                 unsigned int minRingSize,
+                 const boost::dynamic_bitset<> &acands) {
   if (ring.size() < minRingSize) {
     return false;
   }
+
   int atlw, atup, rlw, rup, rie;
   bool aromatic = false;
   rlw = 0;
   rup = 0;
   unsigned int nAnyElectronDonorType = 0;
   for (auto idx : ring) {
+    // we shouldn't be here if there are any atoms in the ring that can't be
+    // aromatic, but check anyway
+    if (!acands[idx]) {
+      return false;
+    }
     ElectronDonorType edonType = edon[idx];
     if (edonType == AnyElectronDonorType) {
       ++nAnyElectronDonorType;
       if (nAnyElectronDonorType > 1) {
         return false;
       }
+    } else if (edonType == NoElectronDonorType) {
+      // if the ring includes atoms which can't be aromatic we can just skip it
+      return false;
     }
     getMinMaxAtomElecs(edonType, atlw, atup);
     rlw += atlw;
     rup += atup;
+    // std::cerr << "  atom: " << idx << ": " << atlw << "-" << atup <<
+    // std::endl;
   }

   if (rup >= 6) {
@@ -313,8 +349,10 @@ void applyHuckelToFused(
     ROMol &mol,                   // molecule of interest
     const VECT_INT_VECT &srings,  // list of all ring as atom IDS
     const VECT_INT_VECT &brings,  // list of all rings as bond ids
-    const INT_VECT &fused,       // list of ring ids in the current fused system
-    const VECT_EDON_TYPE &edon,  // electron donor state for each atom
+    const INT_VECT &fused,  // list of ring ids in the current fused system
+    const boost::dynamic_bitset<>
+        &acands,  // whether or not an atom is a candidate for aromaticity
+    const VECT_EDON_TYPE &edon,    // electron donor state for each atom
     INT_INT_VECT_MAP &ringNeighs,  // list of neighbors for each candidate ring
     int &narom,                    // number of aromatic ring so far
     unsigned int maxNumFusedRings, const std::vector<Bond *> &bondsByIdx,
@@ -344,6 +382,7 @@ void applyHuckelToFused(
     }
     nRingBonds = rdcast<unsigned int>(fusedBonds.count());
   }
+
   std::set<unsigned int> doneBonds;
   while (1) {
     if (pos == -1) {
@@ -388,27 +427,65 @@ void applyHuckelToFused(
     if (ringNeighs.size() && !RingUtils::checkFused(curRs, ringNeighs)) {
       continue;
     }
-
+    std::vector<unsigned int> ringAdjacencies(
+        mol.getNumAtoms() * mol.getNumAtoms(), 0);
     // check aromaticity on the current fused system
     INT_VECT atsInRingSystem(mol.getNumAtoms(), 0);
     for (auto ridx : curRs) {
-      for (auto rid : srings[ridx]) {
+      const auto &sring = srings[ridx];
+      for (auto ri = 0u; ri < sring.size(); ++ri) {
+        auto rid = sring[ri];
         ++atsInRingSystem[rid];
+        unsigned nextAt;
+        if (ri < sring.size() - 1) {
+          nextAt = sring[ri + 1];
+        } else {
+          nextAt = sring[0];
+        }
+        // keep track of which ring atoms are adjacent to each other
+        ringAdjacencies[rid * mol.getNumAtoms() + nextAt]++;
+        ringAdjacencies[nextAt * mol.getNumAtoms() + rid]++;
       }
     }
     INT_VECT unon;
+    bool nonAromaticAtomsInRing = false;
     for (i = 0; i < atsInRingSystem.size(); ++i) {
       // condition for inclusion of an atom in the aromaticity of a fused ring
       // system is that it's present in one or two of the rings. this was #2895:
       // the central atom in acepentalene was being included in the count of
       // aromatic atoms
       if (atsInRingSystem[i] == 1 || atsInRingSystem[i] == 2) {
+        if (!acands[i]) {
+          // std::cerr << " atom " << i << " not a candidate" << std::endl;
+          nonAromaticAtomsInRing = true;
+          break;
+        }
         unon.push_back(i);
       }
     }
-    if (applyHuckel(mol, unon, edon, minRingSize)) {
+    if (nonAromaticAtomsInRing) {
+      continue;
+    }
+    // make sure that we still actually have a ring: each atom which is left
+    // should have at least(?) two connections to other atoms in the ring
+    std::vector<unsigned int> nbrCounts(unon.size(), 0);
+    for (auto i = 0u; i < unon.size(); ++i) {
+      for (auto j = i; j < unon.size(); ++j) {
+        if (ringAdjacencies[unon[i] * mol.getNumAtoms() + unon[j]]) {
+          nbrCounts[i] += 1;
+          nbrCounts[j] += 1;
+          // TODO: can we stop this if we exceed 2? There's probably some edge
+          // case where that happens
+        }
+      }
+    }
+    if (*std::min_element(nbrCounts.begin(), nbrCounts.end()) < 2) {
+      continue;
+    }
+
+    if (applyHuckel(mol, unon, edon, minRingSize, acands)) {
       // mark the atoms and bonds in these rings to be aromatic
-      markAtomsBondsArom(mol, srings, brings, curRs, doneBonds, bondsByIdx);
+      markAtomsBondsArom(mol, unon, brings, curRs, doneBonds, bondsByIdx);

       // add the ring IDs to the aromatic rings found so far
       // avoid duplicates
@@ -516,7 +593,28 @@ bool isAtomCandForArom(const Atom *at, const ElectronDonorType edon,
     }
   }

-  return (true);
+  // It seems like we'd want to exclude atoms for being bridgeheads since they
+  // are going to be 3D and not actually conjugated
+  // But we need to be careful about that since rings fused onto macrocycles
+  // have "bridgeheads" according to our definition and we don't want to
+  // exclude things like:
+  //   c1cc2ccc1CCCCCCCCCCCCCCCC2
+  // where atoms 2 and 5 are "bridgeheads" but in a macrocycle
+  if (queryIsAtomBridgehead(at) && !nUnsaturations) {
+    bool inMacrocycle = false;
+    for (const auto &aring : at->getOwningMol().getRingInfo()->atomRings()) {
+      if (aring.size() > 10 &&
+          std::find(aring.begin(), aring.end(), at->getIdx()) != aring.end()) {
+        inMacrocycle = true;
+        break;
+      }
+    }
+    if (!inMacrocycle) {
+      return false;
+    }
+  }
+
+  return true;
 }

 ElectronDonorType getAtomDonorTypeArom(
@@ -749,8 +847,8 @@ int mdlAromaticityHelper(RWMol &mol, const VECT_INT_VECT &srings) {
     RingUtils::pickFusedRings(curr, neighMap, fused, fusDone);
     const unsigned int maxFused = 6;
     const unsigned int minRingSize = 6;
-    applyHuckelToFused(mol, cRings, brings, fused, edon, neighMap, narom,
-                       maxFused, bondsByIdx, minRingSize);
+    applyHuckelToFused(mol, cRings, brings, fused, acands, edon, neighMap,
+                       narom, maxFused, bondsByIdx, minRingSize);

     int rix;
     for (rix = 0; rix < cnrs; ++rix) {
@@ -827,7 +925,17 @@ int aromaticityHelper(RWMol &mol, const VECT_INT_VECT &srings,
       continue;
     }

-    bool allAromatic = true;
+    // conditions for a ring/fused ring to be considered for aromaticity here:
+    //   all atoms are candidates to be aromatic
+    //     OR
+    //   at least two members are candidates to be aromatic AND all of the
+    //     non-candidates are in at least two fused rings
+    //   This last condition is there to handle the molecule
+    //      C1=CC2=CC3=CC=C4C=C1C2C34
+    //   where the outer envelope is aromatic, but all of the sub-rings have SP3
+    //   carbons in them. (This was github #5134)
+    unsigned int numNonAromatic = 0;
+    bool nonCandidatesInFusedRings = true;
     bool allDummy = true;
     for (auto firstIdx : sring) {
       const auto at = mol.getAtomWithIdx(firstIdx);
@@ -838,7 +946,10 @@ int aromaticityHelper(RWMol &mol, const VECT_INT_VECT &srings,

       if (aseen[firstIdx]) {
         if (!acands[firstIdx]) {
-          allAromatic = false;
+          ++numNonAromatic;
+          nonCandidatesInFusedRings =
+              nonCandidatesInFusedRings &&
+              (mol.getRingInfo()->numAtomRings(firstIdx) >= 2);
         }
         continue;
       }
@@ -851,14 +962,18 @@ int aromaticityHelper(RWMol &mol, const VECT_INT_VECT &srings,
       edon[firstIdx] = getAtomDonorTypeArom(at);
       acands[firstIdx] = isAtomCandForArom(at, edon[firstIdx]);
       if (!acands[firstIdx]) {
-        allAromatic = false;
+        ++numNonAromatic;
+        nonCandidatesInFusedRings =
+            nonCandidatesInFusedRings &&
+            (mol.getRingInfo()->numAtomRings(firstIdx) >= 2);
       }
     }
-    if (allAromatic && !allDummy) {
+    if (!allDummy &&
+        (!numNonAromatic ||
+         ((sring.size() - numNonAromatic) >= 2 && nonCandidatesInFusedRings))) {
       cRings.push_back(sring);
     }
   }
-
   // first convert all rings to bonds ids
   VECT_INT_VECT brings;
   RingUtils::convertToBonds(cRings, brings, mol);
@@ -878,8 +993,8 @@ int aromaticityHelper(RWMol &mol, const VECT_INT_VECT &srings,
       fused.push_back(ri);
       const unsigned int maxFused = 6;
       const unsigned int minRingSize = 0;
-      applyHuckelToFused(mol, cRings, brings, fused, edon, neighMap, narom,
-                         maxFused, bondsByIdx, minRingSize);
+      applyHuckelToFused(mol, cRings, brings, fused, acands, edon, neighMap,
+                         narom, maxFused, bondsByIdx, minRingSize);
     }
   } else {
     // make the neighbor map for the rings
@@ -900,8 +1015,8 @@ int aromaticityHelper(RWMol &mol, const VECT_INT_VECT &srings,
     while (curr < cnrs) {
       fused.clear();
       RingUtils::pickFusedRings(curr, neighMap, fused, fusDone);
-      applyHuckelToFused(mol, cRings, brings, fused, edon, neighMap, narom, 6,
-                         bondsByIdx);
+      applyHuckelToFused(mol, cRings, brings, fused, acands, edon, neighMap,
+                         narom, 6, bondsByIdx);

       int rix;
       for (rix = 0; rix < cnrs; ++rix) {
diff --git a/Code/GraphMol/catch_graphmol.cpp b/Code/GraphMol/catch_graphmol.cpp
index 8fd8ba0a729..3edc31fdfed 100644
--- a/Code/GraphMol/catch_graphmol.cpp
+++ b/Code/GraphMol/catch_graphmol.cpp
@@ -2662,6 +2662,145 @@ TEST_CASE("Github #5055") {
     REQUIRE(m);
   }
 }
+
+TEST_CASE("Github #5134: Fused ring system not perceived as aromatic") {
+#if 1
+  SECTION("as reported") {
+    auto m = "C1=CC2=CC3=CC=C4C=C1C2C34"_smiles;
+    REQUIRE(m);
+    for (auto i = 0u; i < 10; ++i) {
+      CHECK(m->getAtomWithIdx(i)->getIsAromatic());
+    }
+  }
+  SECTION("Related") {
+    auto m = "C1=CC2=CC=C3C=CC=C4C=CC(=C1)C2C34"_smiles;
+    REQUIRE(m);
+    for (auto i = 0u; i < 14; ++i) {
+      CHECK(m->getAtomWithIdx(i)->getIsAromatic());
+    }
+  }
+  SECTION("some edge cases where the new rule does not apply") {
+    std::vector<std::string> smis = {"C1CC2C=C3C=CC4=CC1C2C34",
+                                     "C1C=C2C=CC3=CC=C1C23"};
+    for (const auto &smi : smis) {
+      std::unique_ptr<ROMol> mol(SmilesToMol(smi));
+      REQUIRE(mol);
+      for (auto atom : mol->atoms()) {
+        CHECK(!atom->getIsAromatic());
+      }
+    }
+  }
+  SECTION("no bridgeheads!") {
+    auto m = "O=C1C2=C(C=CC=C2)C(=O)C3=C1C4C5=C(C=CC=C5)C3C6=C4C=CC=C6"_smiles;
+    REQUIRE(m);
+    CHECK(!m->getAtomWithIdx(12)->getIsAromatic());
+    CHECK(!m->getAtomWithIdx(19)->getIsAromatic());
+  }
+  SECTION("Github #5078: Can't roundtrip CHEMBL4080644 through SMILES") {
+    auto m = R"CTAB(
+     RDKit          2D
+
+ 22 25  0  0  0  0  0  0  0  0999 V2000
+    6.1174   -5.5956    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
+    5.5355   -6.1734    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    5.9052   -6.9054    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    6.7157   -6.7800    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    6.8468   -5.9705    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    3.9896   -4.3666    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    3.9885   -5.1861    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    4.6965   -5.5951    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    4.6947   -3.9577    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    5.4033   -4.3630    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    5.4067   -5.1882    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    6.1123   -3.9452    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
+    6.8293   -4.3571    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    6.8321   -5.1820    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    7.5468   -5.5902    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    8.2590   -5.1746    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    8.2522   -4.3466    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    7.5370   -3.9422    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    3.2804   -5.5941    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    8.9563   -3.9319    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    7.6601   -5.9680    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
+    4.7196   -6.1277    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
+  2  3  1  0
+  3  4  2  0
+  4  5  1  0
+  5  1  1  0
+  6  7  2  0
+  7  8  1  0
+  8 11  2  0
+ 10  9  2  0
+  9  6  1  0
+ 10 11  1  0
+ 10 12  1  0
+ 11  1  1  0
+  1 14  1  0
+ 13 12  1  0
+ 13 14  2  0
+ 14 15  1  0
+ 15 16  2  0
+ 16 17  1  0
+ 17 18  2  0
+ 18 13  1  0
+  7 19  1  0
+ 17 20  1  0
+  5 21  2  0
+  2 22  2  0
+  2 12  1  0
+M  END)CTAB"_ctab;
+    REQUIRE(m);
+    CHECK(!m->getAtomWithIdx(11)->getIsAromatic());
+    CHECK(!m->getAtomWithIdx(0)->getIsAromatic());
+    auto smiles = MolToSmiles(*m);
+    std::unique_ptr<RWMol> m2(SmilesToMol(smiles));
+    REQUIRE(m2);
+  }
+  SECTION("trickier") {
+    auto m = "C1=CC2=CC=C3C=CC4=CC=C5C=CC=C6C5C4C3C2C6=C1"_smiles;
+    REQUIRE(m);
+    for (auto i = 0u; i < 16; ++i) {
+      CHECK(m->getAtomWithIdx(i)->getIsAromatic());
+    }
+    CHECK(m->getAtomWithIdx(20)->getIsAromatic());
+  }
+  SECTION("another odd edge case") {
+    auto m = "C2=CC1=S(S2)SC=C1"_smiles;
+    REQUIRE(m);
+    for (auto atom : m->atoms()) {
+      CHECK(!atom->getIsAromatic());
+    }
+  }
+  SECTION("macrocycle bridgehead edge case") {
+    auto m = "C2CCCCCCCCCN1C=C(CC2)C=CC1=O"_smiles;
+    REQUIRE(m);
+    CHECK(m->getAtomWithIdx(10)->getIsAromatic());
+    CHECK(m->getAtomWithIdx(11)->getIsAromatic());
+    CHECK(m->getAtomWithIdx(12)->getIsAromatic());
+  }
+  SECTION("found during testing") {
+    auto m =
+        "c1ccc2c(c1)c1cc[n+]2Cc2ccc(cc2)C[n+]2ccc(c3ccccc32)NCc2ccc(cc2)CN1"_smiles;
+    REQUIRE(m);
+    CHECK(m->getAtomWithIdx(6)->getIsAromatic());
+    CHECK(m->getAtomWithIdx(11)->getIsAromatic());
+    CHECK(m->getAtomWithIdx(18)->getIsAromatic());
+    CHECK(m->getAtomWithIdx(30)->getIsAromatic());
+
+    CHECK(!m->getAtomWithIdx(28)->getIsAromatic());
+    CHECK(!m->getAtomWithIdx(29)->getIsAromatic());
+  }
+#endif
+  SECTION("more testing fun") {
+    auto m = "CC1C2c3ccccc3C(c3ccccc32)C1CN"_smiles;
+    REQUIRE(m);
+    CHECK(m->getAtomWithIdx(4)->getIsAromatic());
+    CHECK(m->getAtomWithIdx(14)->getIsAromatic());
+    CHECK(!m->getAtomWithIdx(2)->getIsAromatic());
+    CHECK(!m->getAtomWithIdx(9)->getIsAromatic());
+  }
+}
+
 TEST_CASE("Iterators") {
   auto m = "CCCCCCC=N"_smiles;
   REQUIRE(m);
@@ -2964,26 +3103,45 @@ TEST_CASE("molecules with single bond to metal atom use dative instead") {
       {"CC1=C(CCC(O)=O)C2=[N]3C1=Cc1c(C)c(C=C)c4C=C5C(C)=C(C=C)C6=[N]5[Fe]3(n14)n1c(=C6)c(C)c(CCC(O)=O)c1=C2",
        "C=CC1=C(C)C2=Cc3c(C=C)c(C)c4n3[Fe]35<-N2=C1C=c1c(C)c(CCC(=O)O)c(n13)=CC1=N->5C(=C4)C(C)=C1CCC(=O)O"},
       {"CC1=C(CCC([O-])=O)C2=[N+]3C1=Cc1c(C)c(C=C)c4C=C5C(C)=C(C=C)C6=[N+]5[Fe--]3(n14)n1c(=C6)c(C)c(CCC([O-])=O)c1=C2",
-       "C=CC1=C(C)C2=Cc3c(C=C)c(C)c4n3[Fe-2]35n6c(c(C)c(CCC(=O)[O-])c6=CC6=[N+]3C(=C4)C(C)=C6CCC(=O)[O-])=CC1=[N+]25"},
+       "C=Cc1c(C)c2cc3c(C)c(CCC(=O)[O-])c4cc5c(CCC(=O)[O-])c(C)c6cc7c(C=C)c(C)c8cc1n2[Fe-2](n65)([n+]87)[n+]34"},
       {"CC1=C(CCC(O)=O)C2=[N+]3C1=Cc1c(C)c(C=C)c4C=C5C(C)=C(C=C)C6=[N+]5[Fe--]3(n14)n1c(=C6)c(C)c(CCC(O)=O)c1=C2",
-       "C=CC1=C(C)C2=Cc3c(C=C)c(C)c4n3[Fe-2]35n6c(c(C)c(CCC(=O)O)c6=CC6=[N+]3C(=C4)C(C)=C6CCC(=O)O)=CC1=[N+]25"},
+       "C=Cc1c(C)c2cc3c(C)c(CCC(=O)O)c4cc5c(CCC(=O)O)c(C)c6cc7c(C=C)c(C)c8cc1n2[Fe-2](n65)([n+]87)[n+]34"},
       {"CCC1=[O+][Cu]2([O+]=C(CC)C1)[O+]=C(CC)CC(CC)=[O+]2",
        "CCC1=[O+][Cu]2([O+]=C(CC)C1)[O+]=C(CC)CC(CC)=[O+]2"}};
   for (size_t i = 0; i < test_vals.size(); ++i) {
     INFO(test_vals[i].first);
     RWMOL_SPTR m(RDKit::SmilesToMol(test_vals[i].first));
     CHECK(MolToSmiles(*m) == test_vals[i].second);
+    // make sure the metal atoms are not aromatic and have no aromatic bonds:
+    for (auto atom : m->atoms()) {
+      if (atom->getAtomicNum() == 26 || atom->getAtomicNum() == 29) {
+        CHECK(!atom->getIsAromatic());
+        for (auto bond : m->atomBonds(atom)) {
+          CHECK(!bond->getIsAromatic());
+        }
+      }
+    }
   }
+
   // make sure we can call cleanupOrganometallics() on non-sanitized molecules
   for (size_t i = 0; i < test_vals.size(); ++i) {
     INFO(test_vals[i].first);
     bool sanitize = false;
     RWMOL_SPTR m(RDKit::SmilesToMol(test_vals[i].first, 0, sanitize));
     MolOps::cleanUpOrganometallics(*m);
+    MolOps::sanitizeMol(*m);
     CHECK(MolToSmiles(*m) == test_vals[i].second);
+    // make sure the metal atoms are not aromatic and have no aromatic bonds:
+    for (auto atom : m->atoms()) {
+      if (atom->getAtomicNum() == 26 || atom->getAtomicNum() == 29) {
+        CHECK(!atom->getIsAromatic());
+        for (auto bond : m->atomBonds(atom)) {
+          CHECK(!bond->getIsAromatic());
+        }
+      }
+    }
   }
 }
-
 TEST_CASE(
     "cleanUpOrganometallics should produce canonical output.  cf PR6292") {
   std::vector<std::pair<std::string, std::string>> test_vals{
@@ -3207,8 +3365,8 @@ M  END
     CHECK(distToTarget.lengthSq() < 0.1);
   }

-  SECTION("Counterclockwise") {  // Chiral Tag (CCW) Different from CIPCode (R)
-                                 // due to different ordering of atoms
+  SECTION("Counterclockwise") {  // Chiral Tag (CCW) Different from CIPCode
+                                 // (R) due to different ordering of atoms
     std::string mb = R"CTAB(testmol
   CT1066645023

@@ -4529,8 +4687,8 @@ M  END
           }
         }
       }
-      // if we aren't in a five-ring (where the results of kekulize are normally
-      // fixed), wedge the double bond and flatten the other one
+      // if we aren't in a five-ring (where the results of kekulize are
+      // normally fixed), wedge the double bond and flatten the other one
       if (wedgedBond && dblBond &&
           !m->getRingInfo()->isBondInRingOfSize(dblBond->getIdx(), 5) &&
           !m->getRingInfo()->isBondInRingOfSize(wedgedBond->getIdx(), 5)) {
@@ -4548,8 +4706,8 @@ M  END
           // }
           // std::cerr << "\n\nbefore kekulize:" << std::endl;
           // m->debugMol(std::cerr);
-          // kekulize again now that we wedged the bonds that were set to double
-          // before
+          // kekulize again now that we wedged the bonds that were set to
+          // double before
           MolOps::Kekulize(*m, clearAromaticFlags);
           // and make sure that those didn't end up double again:
           for (auto atm : {bnd->getBeginAtom(), bnd->getEndAtom()}) {

diff --git a/Code/GraphMol/molopstest.cpp b/Code/GraphMol/molopstest.cpp
index 813f5aac1e7..64ed1a38278 100644
--- a/Code/GraphMol/molopstest.cpp
+++ b/Code/GraphMol/molopstest.cpp
@@ -536,7 +536,6 @@ void test5() {
   delete m;
 }

-
 void test8() {
   BOOST_LOG(rdInfoLog) << "-----------------------\n Testing Hydrogen Ops"
                        << std::endl;
@@ -3499,7 +3498,13 @@ void testSFNetIssue2196817() {
   {
     ROMOL_SPTR m = "C1=CC2=CC=C3C=CC4=CC=C5C=CN1*1*2*3*4N51"_smiles;
     for (const auto a : m->atoms()) {
-      TEST_ASSERT(a->getIsAromatic());
+      if (a->getAtomicNum() > 0 || a->getIdx() == 15 || a->getIdx() == 18) {
+        TEST_ASSERT(a->getIsAromatic());
+      } else if (a->getIdx() == 16 || a->getIdx() == 17) {
+        TEST_ASSERT(!a->getIsAromatic());
+      } else {
+        TEST_ASSERT(0);
+      }
     }
     unsigned int nNonAromaticBonds = 0;
     for (const auto b : m->bonds()) {
@@ -4111,8 +4116,8 @@ void testSFNetIssue3525076() {
 void testBasicCanon() {
   BOOST_LOG(rdInfoLog) << "-------------------------------------" << std::endl;
   BOOST_LOG(rdInfoLog) << "Testing canonicalization basics" << std::endl;
-// these are all cases that were problematic at one time or another during
-// the canonicalization rewrite.
+  // these are all cases that were problematic at one time or another during
+  // the canonicalization rewrite.
   {
     std::string smi = "FC1C(=C/Cl)\\C1";
     RWMol *m = SmilesToMol(smi);
@@ -7129,21 +7134,22 @@ void testGithubIssue868() {
   }

   // test atom type query merging
-  for (int aromatic =0; aromatic<2; ++aromatic) {
+  for (int aromatic = 0; aromatic < 2; ++aromatic) {
     sstrm.str("");
     TEST_ASSERT(sstrm.str() == "");
     RWMol m;
     QueryAtom *qa = new QueryAtom();
     qa->setQuery(makeAtomTypeQuery(1, aromatic));
-    qa->expandQuery(makeAtomNumQuery(6), Queries::CompositeQueryType::COMPOSITE_OR);
+    qa->expandQuery(makeAtomNumQuery(6),
+                    Queries::CompositeQueryType::COMPOSITE_OR);
     m.addAtom(qa, true, true);
     MolOps::mergeQueryHs(m);
     TEST_ASSERT(sstrm.str().find("merging explicit H queries involved in "
-				 "ORs is not supported") != std::string::npos);
+                                 "ORs is not supported") != std::string::npos);
     TEST_ASSERT(sstrm.str().find("This query will not be merged") !=
-		std::string::npos);
+                std::string::npos);
   }
-
+
   {
     sstrm.str("");
     TEST_ASSERT(sstrm.str() == "");
@@ -8497,11 +8503,11 @@ void testHasQueryHs() {
               has_only_query_hs);

   auto github7687 = "[#1,#6,#7]"_smarts;
-  TEST_ASSERT(RDKit::MolOps::hasQueryHs(*github7687) == has_unmergeable_hs );
+  TEST_ASSERT(RDKit::MolOps::hasQueryHs(*github7687) == has_unmergeable_hs);
   auto github7687b = "[1;#7,#1,#6]"_smarts;
-  TEST_ASSERT(RDKit::MolOps::hasQueryHs(*github7687b) == has_unmergeable_hs );
+  TEST_ASSERT(RDKit::MolOps::hasQueryHs(*github7687b) == has_unmergeable_hs);
   auto github7687c = "[1&#7,#1,#6]"_smarts;
-  TEST_ASSERT(RDKit::MolOps::hasQueryHs(*github7687c) == has_unmergeable_hs );
+  TEST_ASSERT(RDKit::MolOps::hasQueryHs(*github7687c) == has_unmergeable_hs);
   BOOST_LOG(rdInfoLog) << "\tdone" << std::endl;
 }


Molzip: save bond dir when zipping (rdkit/rdkit#5445)

Fixes #5450

Simple fix to allow mol zip to save the bond direction when zipping molecules.

This is necessary for the CDXML parser.

MolZip doesn't preserve bond directions when zipping

This is more esoteric, but required for cdxml parsing.

diff --git a/Code/GraphMol/ChemTransforms/MolFragmenter.cpp b/Code/GraphMol/ChemTransforms/MolFragmenter.cpp
index 80b5d7ee6d7..762b8f05aa4 100644
--- a/Code/GraphMol/ChemTransforms/MolFragmenter.cpp
+++ b/Code/GraphMol/ChemTransforms/MolFragmenter.cpp
@@ -746,18 +746,72 @@ struct ZipBond {
                       "molzip: begin atom and specified dummy atom connection "
                       "are not bonded.")
       auto bond_type_a = bnd->getBondType();
+      auto bond_dir_a = bnd->getBondDir();
+      auto a_is_start = bnd->getBeginAtom() == a;
+
       bnd = newmol.getBondBetweenAtoms(b->getIdx(), b_dummy->getIdx());
       CHECK_INVARIANT(bnd != nullptr,
                       "molzip: end atom and specified dummy connection atom "
                       "are not bonded.")
       auto bond_type_b = bnd->getBondType();
+
+      auto bond_dir_b = bnd->getBondDir();
+      auto b_is_start = bnd->getBeginAtom() == b;
+
+      unsigned int bnd_idx = 0;
+      // Fusion bond-dir logic table
+      // a-* b-* => a-b
+      //  < = wedge
+
+      //  a<* b-* => a<b
+      //  a>* b-* => a>b
+      //  a-* b>* => a<b
+      //  a-* b<* => a>b
+      Bond::BondDir bond_dir;
+      auto start = a;
+      auto end = b;
+      if(bond_dir_a != Bond::BondDir::NONE && bond_dir_b != Bond::BondDir::NONE) {
+          // are we consistent between the two bond orders
+          // check for the case of fragment on bonds where a<* and b>* or a>* and b<*
+          //  when < is either a hash or wedge bond but not both.
+          bool consistent_directions = false;
+          if(bond_dir_a == bond_dir_b) {
+              if((a_is_start != b_is_start)){
+                  consistent_directions = true;
+              }
+          }
+          if(!consistent_directions) {
+              BOOST_LOG(rdWarningLog) << "inconsistent bond directions when merging fragments, ignoring..." << std::endl;
+              bond_dir_a = bond_dir_b = Bond::BondDir::NONE;
+          } else {
+              bond_dir_b = Bond::BondDir::NONE;
+          }
+      }
+
+      if (bond_dir_a != Bond::BondDir::NONE) {
+          if(!a_is_start) {
+              start = b;
+              end = a;
+          }
+          bond_dir = bond_dir_a;
+      } else if (bond_dir_b != Bond::BondDir::NONE) {
+          if(b_is_start) {
+            start = b;
+            end = a;
+          }
+          bond_dir = bond_dir_b;
+      }
+
       if (bond_type_a != Bond::BondType::SINGLE) {
-        newmol.addBond(a, b, bond_type_a);
+        bnd_idx = newmol.addBond(start, end, bond_type_a);
       } else if (bond_type_b != Bond::BondType::SINGLE) {
-        newmol.addBond(a, b, bond_type_b);
+        bnd_idx = newmol.addBond(start, end, bond_type_b);
       } else {
-        newmol.addBond(a, b, Bond::BondType::SINGLE);
+        bnd_idx = newmol.addBond(start, end, Bond::BondType::SINGLE);
       }
+
+      newmol.getBondWithIdx(bnd_idx-1)->setBondDir(bond_dir);
+
     }
     a_dummy->setProp("__molzip_used", true);
     b_dummy->setProp("__molzip_used", true);

diff --git a/Code/GraphMol/ChemTransforms/catch_tests.cpp b/Code/GraphMol/ChemTransforms/catch_tests.cpp
index d06c126d3d6..1d020a5436c 100644
--- a/Code/GraphMol/ChemTransforms/catch_tests.cpp
+++ b/Code/GraphMol/ChemTransforms/catch_tests.cpp
@@ -398,6 +398,54 @@ TEST_CASE("molzip", "[]") {
     auto b = "[*:1]-N=C"_smiles;
     auto mol = molzip(*a, *b, p);
   }
+
+  SECTION("MolZip saves bonddir") {
+      { // a-* b<*
+         auto a = "CC[*:1]"_smiles;
+         auto b = "N[*:1]"_smiles;
+         b->getBondWithIdx(0)->setBondDir(Bond::BondDir::BEGINWEDGE);
+         auto mol = molzip(*a, *b);
+         CHECK(MolToSmiles(*mol) == "CCN");
+         CHECK(mol->getBondWithIdx(1)->getBondDir() == Bond::BondDir::BEGINWEDGE);
+         CHECK(mol->getBondWithIdx(1)->getBeginAtom()->getIdx() == 2);
+       }
+       { // a>* b-*
+         auto a = "[*:1]CC"_smiles;
+         auto b = "N[*:1]"_smiles;
+         a->getBondWithIdx(0)->setBondDir(Bond::BondDir::BEGINWEDGE);
+         auto mol = molzip(*a, *b);
+         CHECK(MolToSmiles(*mol) == "CCN");
+         CHECK(mol->getBondWithIdx(1)->getBondDir() == Bond::BondDir::BEGINWEDGE);
+         CHECK(mol->getBondWithIdx(1)->getBeginAtom()->getIdx() == 2);
+       }
+       { // a<* b-*
+         auto a = "CC[*:1]"_smiles;
+         auto b = "N[*:1]"_smiles;
+         a->getBondWithIdx(1)->setBondDir(Bond::BondDir::BEGINWEDGE);
+         auto mol = molzip(*a, *b);
+         CHECK(MolToSmiles(*mol) == "CCN");
+         CHECK(mol->getBondWithIdx(1)->getBondDir() == Bond::BondDir::BEGINWEDGE);
+         CHECK(mol->getBondWithIdx(1)->getBeginAtom()->getIdx() == 1);
+       }
+       { // a>* b-*
+         auto a = "[*:1]CC"_smiles;
+         auto b = "N[*:1]"_smiles;
+         a->getBondWithIdx(0)->setBondDir(Bond::BondDir::BEGINWEDGE);
+         auto mol = molzip(*a, *b);
+         CHECK(MolToSmiles(*mol) == "CCN");
+         CHECK(mol->getBondWithIdx(1)->getBondDir() == Bond::BondDir::BEGINWEDGE);
+         CHECK(mol->getBondWithIdx(1)->getBeginAtom()->getIdx() == 2);
+       }
+       { // a-* b<*
+         auto a = "CC[*:1]"_smiles;
+         auto b = "[*:1]N"_smiles;
+         b->getBondWithIdx(0)->setBondDir(Bond::BondDir::BEGINWEDGE);
+         auto mol = molzip(*a, *b);
+         CHECK(MolToSmiles(*mol) == "CCN");
+         CHECK(mol->getBondWithIdx(1)->getBondDir() == Bond::BondDir::BEGINWEDGE);
+         CHECK(mol->getBondWithIdx(1)->getBeginAtom()->getIdx() == 1);
+       }
+   }
 }

 TEST_CASE(


Fixes #5402 (rdkit/rdkit#5542)

The problem here was a transformation (the "special imine r") which applied to this Kekule form of an aromatic substructure:
![image](https://user-images.githubusercontent.com/540511/188902044-f68acfc1-f92f-4aa2-bd85-d9738b29a1ff.png)
but not this one:
![image](https://user-images.githubusercontent.com/540511/188902091-2f518741-84ce-42ab-a193-4264ad8e5c5d.png)

This fix adds a new rule to detect and handle the second case. Along the way I also had to modify a bit of the logic for applying transforms to support transform rules with branched queries, but this is a simple change.

I wouldn't be horribly surprised to discover that there are more of this type of error in the default transformations, but it's non-trivial to identify them directly.

Tautomer enumeration removes stereochemistry depending on how aromatic rings are defined in SMILES

Here's a compact demonstration:
```
In [12]: m = Chem.MolFromSmiles("c1ccc([C@@H](CC2=NCCN2)c2ccccn2)cc1")

In [13]: nm = Chem.Mol(m)

In [14]: Chem.Kekulize(nm, True)

In [15]: Chem.MolToSmiles(enum.Canonicalize(m))
Out[15]: 'c1ccc([C@@H](CC2=NCCN2)c2ccccn2)cc1'

In [16]: Chem.MolToSmiles(enum.Canonicalize(nm))
Out[16]: 'c1ccc(C(CC2=NCCN2)c2ccccn2)cc1'
```

### Discussed in https://github.com/rdkit/rdkit/discussions/5397

Originally posted by **gedeck** July  1, 2022
Before I file an issue, I would like to make sure that it is one. The Tautomer enumerator behaves differently if the structure is loaded from a SMILES where the aromatic bonds are defined using kekule format or not.
```
from rdkit import Chem
from rdkit.Chem.MolStandardize import rdMolStandardize
from rdkit.Chem import Draw

enumerator = rdMolStandardize.TautomerEnumerator()

smiles = 'c1ccc([C@@H](CC2=NCCN2)c2ccccn2)cc1'
mol = Chem.MolFromSmiles(smiles)
Draw.MolsToGridImage([mol, enumerator.Canonicalize(mol)])
```
In this case, the stereochemistry of the carbon is preserved:

![image](https://user-images.githubusercontent.com/8720575/176784310-ec5c88fa-156a-4dbd-a6df-59ecf9c7ad2b.png)

If on the other hand, the aromatic rings are described with single and double bonds
```
smiles = 'C([C@H](C1=CC=CC=C1)C2=NC=CC=C2)C3=NCCN3'
mol = Chem.MolFromSmiles(smiles)
Draw.MolsToGridImage([mol, enumerator.Canonicalize(mol)])
```
the stereochemistry is removed.
![image](https://user-images.githubusercontent.com/8720575/176784328-44bdaa56-d3fd-4ace-a85a-0ce64f679aab.png)

diff --git a/Code/GraphMol/MolStandardize/Tautomer.cpp b/Code/GraphMol/MolStandardize/Tautomer.cpp
index 247f5fa7826..901a50f7abb 100644
--- a/Code/GraphMol/MolStandardize/Tautomer.cpp
+++ b/Code/GraphMol/MolStandardize/Tautomer.cpp
@@ -28,8 +28,6 @@
 #include <GraphMol/SmilesParse/SmartsWrite.h>
 #endif

-using namespace RDKit;
-
 namespace RDKit {

 namespace MolStandardize {
@@ -342,8 +340,10 @@ TautomerEnumeratorResult TautomerEnumerator::enumerate(const ROMol &mol) const {
 #ifdef VERBOSE_ENUMERATION
         std::string name;
         (transform.Mol)->getProp(common_properties::_Name, name);
+        SmilesWriteParams smilesWriteParams;
+        smilesWriteParams.allBondsExplicit = true;
         std::cout << "kmol for " << smilesTautomerPair.first << " : "
-                  << MolToSmiles(*kmol) << std::endl;
+                  << MolToSmiles(*kmol, smilesWriteParams) << std::endl;
         std::cout << "transform mol: " << MolToSmarts(*(transform.Mol))
                   << std::endl;

@@ -385,9 +385,11 @@ TautomerEnumeratorResult TautomerEnumerator::enumerate(const ROMol &mol) const {
           last->setNoImplicit(true);
           // Adjust bond orders
           unsigned int bi = 0;
-          for (size_t i = 0; i < match.size() - 1; ++i) {
-            Bond *bond = product->getBondBetweenAtoms(match[i].second,
-                                                      match[i + 1].second);
+          for (size_t i = 0; i < transform.Mol->getNumBonds(); ++i) {
+            const auto tbond = transform.Mol->getBondWithIdx(i);
+            Bond *bond = product->getBondBetweenAtoms(
+                match[tbond->getBeginAtomIdx()].second,
+                match[tbond->getEndAtomIdx()].second);
             ASSERT_INVARIANT(bond, "required bond not found");
             // check if bonds is specified in tautomer.in file
             if (!transform.BondTypes.empty()) {
@@ -424,6 +426,14 @@ TautomerEnumeratorResult TautomerEnumerator::enumerate(const ROMol &mol) const {
                                     transform.Charges[ci++]);
             }
           }
+#ifdef VERBOSE_ENUMERATION
+          {
+            SmilesWriteParams smilesWriteParams;
+            smilesWriteParams.allBondsExplicit = true;
+            std::cout << "pre-sanitize: "
+                      << MolToSmiles(*product, smilesWriteParams) << std::endl;
+          }
+#endif

           unsigned int failedOp;
           MolOps::sanitizeMol(*product, failedOp,
@@ -433,8 +443,10 @@ TautomerEnumeratorResult TautomerEnumerator::enumerate(const ROMol &mol) const {
                                   MolOps::SANITIZE_SETHYBRIDIZATION |
                                   MolOps::SANITIZE_ADJUSTHS);
 #ifdef VERBOSE_ENUMERATION
-          std::cout << "pre-setTautomerStereo: " << MolToSmiles(*product, true)
-                    << std::endl;
+          SmilesWriteParams smilesWriteParams;
+          smilesWriteParams.allBondsExplicit = true;
+          std::cout << "pre-setTautomerStereo: "
+                    << MolToSmiles(*product, smilesWriteParams) << std::endl;
 #endif
           setTautomerStereoAndIsoHs(mol, *product, res);
           tsmiles = MolToSmiles(*product, true);
@@ -472,7 +484,8 @@ TautomerEnumeratorResult TautomerEnumerator::enumerate(const ROMol &mol) const {
             std::cout << "New tautomer replaced for ";
           }
           std::cout << tsmiles << ", taut: " << MolToSmiles(*product)
-                    << ", kek: " << MolToSmiles(*kekulized_product, true, true)
+                    << ", kek: "
+                    << MolToSmiles(*kekulized_product, smilesWriteParams)
                     << std::endl;
 #endif
           // BOOST_LOG(rdInfoLog)
diff --git a/Code/GraphMol/MolStandardize/TautomerCatalog/tautomerTransforms.in b/Code/GraphMol/MolStandardize/TautomerCatalog/tautomerTransforms.in
index 2ac09548b6f..4ea7850b305 100644
--- a/Code/GraphMol/MolStandardize/TautomerCatalog/tautomerTransforms.in
+++ b/Code/GraphMol/MolStandardize/TautomerCatalog/tautomerTransforms.in
@@ -40,6 +40,9 @@ const std::vector<
         std::make_tuple(std::string("special imine r"),
                         std::string("[Cz0R0X4!H0]-[c]=[nz0]"), std::string(""),
                         std::string("")),
+        std::make_tuple(std::string("special imine r2"),
+                        std::string("[Cz0R0X4!H0]-[cr6](=[cr6])-[nr6H0z0]"),
+                        std::string("==-"), std::string("")),
         std::make_tuple(std::string("1,3 aromatic heteroatom H shift f"),
                         std::string("[#7+0!H0]-[#6R1]=[O,#7X2+0]"),
                         std::string(""), std::string("")),

diff --git a/Code/GraphMol/MolStandardize/catch_tests.cpp b/Code/GraphMol/MolStandardize/catch_tests.cpp
index 1307c0e678a..19331afd7c2 100644
--- a/Code/GraphMol/MolStandardize/catch_tests.cpp
+++ b/Code/GraphMol/MolStandardize/catch_tests.cpp
@@ -978,3 +978,50 @@ TEST_CASE("Github #5320: cleanup() and stereochemistry") {
     CHECK(MolToSmiles(*m2) == "O=C([O-])C(O)(O)Cl");
   }
 }
+
+TEST_CASE("Github #5402: order dependence of tautomer transforms") {
+  SECTION("as-reported") {
+    MolStandardize::TautomerEnumerator te;
+
+    auto m1 = "c1ccc([C@@H](CC2=NCCN2)c2ccccn2)cc1"_smiles;
+    REQUIRE(m1);
+    auto m2 = "C([C@H](C1=CC=CC=C1)C2=NC=CC=C2)C3=NCCN3"_smiles;
+    REQUIRE(m2);
+    std::cerr << " * - * - * - * m1" << std::endl;
+    std::unique_ptr<ROMol> res1{te.canonicalize(*m1)};
+    REQUIRE(res1);
+    std::cerr << " * - * - * - * m2" << std::endl;
+    std::unique_ptr<ROMol> res2{te.canonicalize(*m2)};
+    REQUIRE(res2);
+    CHECK(MolToSmiles(*res1) == MolToSmiles(*res2));
+  }
+  SECTION("zoom") {
+    MolStandardize::CleanupParameters params;
+    const std::vector<
+        std::tuple<std::string, std::string, std::string, std::string>>
+        tTransforms{
+            std::make_tuple(std::string("special imine r1"),
+                            std::string("[Cz0R0X4!H0]-[c]=[nz0]"),
+                            std::string(""), std::string("")),
+            std::make_tuple(std::string("special imine r2"),
+                            std::string("[Cz0R0X4!H0]-[c](=c)-[nz0]"),
+                            std::string("==-"), std::string("")),
+        };
+
+    params.tautomerTransformData = tTransforms;
+    MolStandardize::TautomerEnumerator te(params);
+
+    auto m1 = "c1ccc([C@@H](CC2=NCCN2)c2ccccn2)cc1"_smiles;
+    REQUIRE(m1);
+    auto m2 = "C([C@H](C1=CC=CC=C1)C2=NC=CC=C2)C3=NCCN3"_smiles;
+    REQUIRE(m2);
+
+    std::cerr << " * - * - * - * m1" << std::endl;
+    std::unique_ptr<ROMol> res1{te.canonicalize(*m1)};
+    REQUIRE(res1);
+    std::cerr << " * - * - * - * m2" << std::endl;
+    std::unique_ptr<ROMol> res2{te.canonicalize(*m2)};
+    REQUIRE(res2);
+    CHECK(MolToSmiles(*res1) == MolToSmiles(*res2));
+  }
+}
diff --git a/Code/GraphMol/MolStandardize/testTautomer.cpp b/Code/GraphMol/MolStandardize/testTautomer.cpp
index c07030085d9..b596d148b9a 100644
--- a/Code/GraphMol/MolStandardize/testTautomer.cpp
+++ b/Code/GraphMol/MolStandardize/testTautomer.cpp
@@ -712,7 +712,7 @@ void testCanonicalize() {
       std::unique_ptr<TautomerCatalogParams>(new TautomerCatalogParams(""));

   unsigned int ntransforms = tautparams->getTransforms().size();
-  TEST_ASSERT(ntransforms == 36);
+  TEST_ASSERT(ntransforms == 37);

   TautomerEnumerator te(new TautomerCatalog(tautparams.get()));

@@ -734,7 +734,7 @@ void testPickCanonical() {
       std::unique_ptr<TautomerCatalogParams>(new TautomerCatalogParams(""));

   unsigned int ntransforms = tautparams->getTransforms().size();
-  TEST_ASSERT(ntransforms == 36);
+  TEST_ASSERT(ntransforms == 37);

   TautomerEnumerator te(new TautomerCatalog(tautparams.get()));

@@ -759,7 +759,7 @@ void testCustomScoreFunc() {
       std::unique_ptr<TautomerCatalogParams>(new TautomerCatalogParams(""));

   unsigned int ntransforms = tautparams->getTransforms().size();
-  TEST_ASSERT(ntransforms == 36);
+  TEST_ASSERT(ntransforms == 37);

   TautomerEnumerator te(new TautomerCatalog(tautparams.get()));

@@ -828,7 +828,7 @@ void testEnumerationProblems() {
       std::unique_ptr<TautomerCatalogParams>(new TautomerCatalogParams(""));

   unsigned int ntransforms = tautparams->getTransforms().size();
-  TEST_ASSERT(ntransforms == 36);
+  TEST_ASSERT(ntransforms == 37);

   TautomerEnumerator te(new TautomerCatalog(tautparams.get()));
 #if 1
@@ -860,7 +860,7 @@ void testPickCanonical2() {
   auto tautparams =
       std::unique_ptr<TautomerCatalogParams>(new TautomerCatalogParams(""));
   unsigned int ntransforms = tautparams->getTransforms().size();
-  TEST_ASSERT(ntransforms == 36);
+  TEST_ASSERT(ntransforms == 37);

   TautomerEnumerator te(new TautomerCatalog(tautparams.get()));
   {
@@ -894,7 +894,7 @@ void testEnumerateDetails() {
   auto tautparams =
       std::unique_ptr<TautomerCatalogParams>(new TautomerCatalogParams(""));
   unsigned int ntransforms = tautparams->getTransforms().size();
-  TEST_ASSERT(ntransforms == 36);
+  TEST_ASSERT(ntransforms == 37);
   TautomerEnumerator te(new TautomerCatalog(tautparams.get()));
   {
     auto mol = "c1ccccc1CN=c1[nH]cccc1"_smiles;
@@ -950,7 +950,7 @@ void testGithub2990() {
       std::unique_ptr<TautomerCatalogParams>(new TautomerCatalogParams(""));

   unsigned int ntransforms = tautparams->getTransforms().size();
-  TEST_ASSERT(ntransforms == 36);
+  TEST_ASSERT(ntransforms == 37);
   TautomerEnumerator te(new TautomerCatalog(tautparams.get()));
   {
     // atom stereo


Support ring-bond stereo flags in CXSMILES (rdkit/rdkit#5595)

This adds `c:`, `t:`, and `ctu:` to the CXSMILES readers and writers. These only apply to ring bonds.

It also:
- updates the documentation to include the newly supported CXSMILES features and to document where stereo information comes from.
- Fixes #5585

incorrect _smilesBondOutputOrder for ring closures

**Describe the bug**
Here's the demo:
```
In [26]: m = Chem.MolFromSmiles("OC1CCCN1")

In [27]: Chem.MolToSmiles(m)
Out[27]: 'OC1CCCN1'

In [28]: m.GetProp("_smilesBondOutputOrder")
Out[28]: '[0,1,5,2,3,4,]'

In [29]: m.GetProp("_smilesAtomOutputOrder")
Out[29]: '[0,1,2,3,4,5,]'
```
Given that the output order is the same as the input order, the `_smilesBondOutputOrder` should be:
`[0,1,2,3,4,5]`

**Configuration (please complete the following information):**
 - RDKit version: all
 - OS: all

diff --git a/Code/GraphMol/FindStereo.cpp b/Code/GraphMol/FindStereo.cpp
index 0ecadfb5a79..1d3caf24d9c 100644
--- a/Code/GraphMol/FindStereo.cpp
+++ b/Code/GraphMol/FindStereo.cpp
@@ -339,8 +339,9 @@ bool isBondPotentialStereoBond(const Bond *bond) {
     // check rings
     const auto ri = bond->getOwningMol().getRingInfo();
     for (const auto &bring : ri->bondRings()) {
-      if (bring.size() < 8 && std::find(bring.begin(), bring.end(),
-                                        bond->getIdx()) != bring.end()) {
+      if (bring.size() < minRingSizeForDoubleBondStereo &&
+          std::find(bring.begin(), bring.end(), bond->getIdx()) !=
+              bring.end()) {
         return false;
       }
     }
diff --git a/Code/GraphMol/SmilesParse/CXSmilesOps.cpp b/Code/GraphMol/SmilesParse/CXSmilesOps.cpp
index a75626b3edb..0cc249f347b 100644
--- a/Code/GraphMol/SmilesParse/CXSmilesOps.cpp
+++ b/Code/GraphMol/SmilesParse/CXSmilesOps.cpp
@@ -20,6 +20,7 @@
 #include "SmilesParse.h"
 #include "SmilesParseOps.h"
 #include <GraphMol/MolEnumerator/LinkNode.h>
+#include <GraphMol/Chirality.h>

 namespace SmilesParseOps {
 using namespace RDKit;
@@ -980,15 +981,14 @@ bool parse_variable_attachments(Iterator &first, Iterator last,
         return false;
       }
       if (VALID_ATIDX(aidx)) {
-        others.push_back(
-            (boost::format("%d") % (aidx - startAtomIdx + 1)).str());
+        others.push_back(std::to_string(aidx - startAtomIdx + 1));
       }
       if (first < last && *first == '.') {
         ++first;
       }
     }
     if (VALID_ATIDX(at1idx)) {
-      std::string endPts = (boost::format("(%d") % others.size()).str();
+      std::string endPts = "(" + std::to_string(others.size());
       for (auto idx : others) {
         endPts += " " + idx;
       }
@@ -1062,49 +1062,102 @@ bool parse_wedged_bonds(Iterator &first, Iterator last, RDKit::RWMol &mol,
       return false;
     }

-    if (!VALID_ATIDX(atomIdx)) {
-      BOOST_LOG(rdWarningLog)
-          << "bad atom index, " << atomIdx << ", in w block" << std::endl;
-      return false;
-    }
-    if (!VALID_BNDIDX(bondIdx)) {
-      BOOST_LOG(rdWarningLog)
-          << "bad bond index, " << bondIdx << ", in w block" << std::endl;
-      return false;
-    }
-    auto atom = mol.getAtomWithIdx(atomIdx + startAtomIdx);
-    auto bond = mol.getBondWithIdx(bondIdx + startBondIdx);
+    if (VALID_ATIDX(atomIdx) && VALID_BNDIDX(bondIdx)) {
+      auto atom = mol.getAtomWithIdx(atomIdx - startAtomIdx);
+      auto bond = mol.getBondWithIdx(bondIdx - startBondIdx);

-    // we can't set wedging twice:
-    if (bond->hasProp(common_properties::_MolFileBondCfg)) {
-      BOOST_LOG(rdWarningLog)
-          << "w block attempts to set wedging on bond " << bond->getIdx()
-          << " more than once." << std::endl;
-      return false;
-    }
-
-    // first things first, the atom needs to be the start atom of the bond for
-    // any of this to make sense
-    if (atom->getIdx() != bond->getBeginAtomIdx()) {
-      if (atom->getIdx() != bond->getEndAtomIdx()) {
+      // we can't set wedging twice:
+      if (bond->hasProp(common_properties::_MolFileBondCfg)) {
         BOOST_LOG(rdWarningLog)
-            << "atom " << atomIdx << " is not associated with bond " << bondIdx
-            << " in w block" << std::endl;
+            << "w block attempts to set wedging on bond " << bond->getIdx()
+            << " more than once." << std::endl;
         return false;
       }
-      auto eidx = bond->getBeginAtomIdx();
-      bond->setBeginAtomIdx(atom->getIdx());
-      bond->setEndAtomIdx(eidx);
+
+      // first things first, the atom needs to be the start atom of the bond for
+      // any of this to make sense
+      if (atom->getIdx() != bond->getBeginAtomIdx()) {
+        if (atom->getIdx() != bond->getEndAtomIdx()) {
+          BOOST_LOG(rdWarningLog)
+              << "atom " << atomIdx << " is not associated with bond "
+              << bondIdx << " in w block" << std::endl;
+          return false;
+        }
+        auto eidx = bond->getBeginAtomIdx();
+        bond->setBeginAtomIdx(atom->getIdx());
+        bond->setEndAtomIdx(eidx);
+      }
+      bond->setProp(common_properties::_MolFileBondCfg, cfg);
+      bond->setBondDir(state);
+      if (cfg == 2 && bond->getBondType() == Bond::BondType::SINGLE) {
+        bond->getBeginAtom()->setChiralTag(Atom::ChiralType::CHI_UNSPECIFIED);
+        mol.setProp(detail::_needsDetectBondStereo, 1);
+      }
+      if ((cfg == 1 || cfg == 3) &&
+          bond->getBondType() == Bond::BondType::SINGLE) {
+        mol.setProp(detail::_needsDetectAtomStereo, 1);
+      }
     }
-    bond->setProp(common_properties::_MolFileBondCfg, cfg);
-    bond->setBondDir(state);
-    if (cfg == 2 && bond->getBondType() == Bond::BondType::SINGLE) {
-      bond->getBeginAtom()->setChiralTag(Atom::ChiralType::CHI_UNSPECIFIED);
-      mol.setProp(detail::_needsDetectBondStereo, 1);
+    if (first < last && *first == ',') {
+      ++first;
     }
-    if ((cfg == 1 || cfg == 3) &&
-        bond->getBondType() == Bond::BondType::SINGLE) {
-      mol.setProp(detail::_needsDetectAtomStereo, 1);
+  }
+  return true;
+}
+
+template <typename Iterator>
+bool parse_doublebond_stereo(Iterator &first, Iterator last, RDKit::RWMol &mol,
+                             unsigned int, unsigned int startBondIdx,
+                             Bond::BondStereo stereo) {
+  // these look like: C1CCCC/C=C/CCC1 |ctu:5|
+  // also c and t for cis or trans
+  //
+  while (first < last && *first != ':') {
+    ++first;
+  }
+  if (first >= last || *first != ':') {
+    return false;
+  }
+  ++first;
+
+  while (first < last && *first >= '0' && *first <= '9') {
+    unsigned int bondIdx;
+    if (!read_int(first, last, bondIdx)) {
+      return false;
+    }
+    if (VALID_BNDIDX(bondIdx)) {
+      auto bond = mol.getBondWithIdx(bondIdx - startBondIdx);
+
+      // the cis/trans/unknown marker is relative to the lowest numbered atom
+      // connected to the lowest numbered double bond atom and the
+      // highest-numbered atom connected to the highest-numbered double bond
+      // atom find those
+      auto begAtom = bond->getBeginAtom();
+      auto endAtom = bond->getEndAtom();
+      if (begAtom->getIdx() > endAtom->getIdx()) {
+        std::swap(begAtom, endAtom);
+      }
+      if (begAtom->getDegree() > 1 && endAtom->getDegree() > 1) {
+        unsigned int begControl = mol.getNumAtoms();
+        for (auto nbr : mol.atomNeighbors(begAtom)) {
+          if (nbr == endAtom) {
+            continue;
+          }
+          begControl = std::min(nbr->getIdx(), begControl);
+        }
+        unsigned int endControl = 0;
+        for (auto nbr : mol.atomNeighbors(endAtom)) {
+          if (nbr == begAtom) {
+            continue;
+          }
+          endControl = std::max(nbr->getIdx(), endControl);
+        }
+        if (begAtom != bond->getBeginAtom()) {
+          std::swap(begControl, endControl);
+        }
+        bond->setStereoAtoms(begControl, endControl);
+        bond->setStereo(stereo);
+      }
     }
     if (first < last && *first == ',') {
       ++first;
@@ -1378,6 +1431,22 @@ bool parse_it(Iterator &first, Iterator last, RDKit::RWMol &mol,
       if (!parse_wedged_bonds(first, last, mol, startAtomIdx, startBondIdx)) {
         return false;
       }
+    } else if (*first == 'c' && first + 2 < last && first[1] == 't' &&
+               first[2] == 'u') {
+      if (!parse_doublebond_stereo(first, last, mol, startAtomIdx, startBondIdx,
+                                   Bond::BondStereo::STEREOANY)) {
+        return false;
+      }
+    } else if (*first == 'c') {
+      if (!parse_doublebond_stereo(first, last, mol, startAtomIdx, startBondIdx,
+                                   Bond::BondStereo::STEREOCIS)) {
+        return false;
+      }
+    } else if (*first == 't') {
+      if (!parse_doublebond_stereo(first, last, mol, startAtomIdx, startBondIdx,
+                                   Bond::BondStereo::STEREOTRANS)) {
+        return false;
+      }
     } else {
       ++first;
     }
@@ -1938,6 +2007,97 @@ std::string get_bond_config_block(const ROMol &mol,
   return w + wU + wD;
 }

+std::string get_ringbond_cistrans_block(
+    const ROMol &mol, const std::vector<unsigned int> &atomOrder,
+    const std::vector<unsigned int> &bondOrder) {
+  if (!mol.getRingInfo()->isInitialized()) {
+    return "";
+  }
+
+  const auto rinfo = mol.getRingInfo();
+  std::string c = "", t = "", ctu = "";
+  for (unsigned int i = 0; i < bondOrder.size(); ++i) {
+    auto idx = bondOrder[i];
+    if (!rinfo->numBondRings(idx) ||
+        rinfo->minBondRingSize(idx) <
+            Chirality::minRingSizeForDoubleBondStereo) {
+      // we only do ring bonds of a minimum size
+      continue;
+    }
+    const auto bond = mol.getBondWithIdx(idx);
+    if (bond->getBondType() != Bond::BondType::DOUBLE &&
+        bond->getBondType() != Bond::BondType::AROMATIC) {
+      continue;
+    }
+    Bond::BondStereo bstereo = bond->getStereo();
+    if (bstereo != Bond::BondStereo::STEREOANY &&
+        bstereo != Bond::BondStereo::STEREOCIS &&
+        bstereo != Bond::BondStereo::STEREOTRANS) {
+      continue;
+    }
+
+    auto label = std::to_string(i);
+
+    if (bstereo == Bond::BondStereo::STEREOANY) {
+      // this one's easy because we don't care about the atom order.
+      if (ctu.empty()) {
+        ctu += "ctu:";
+      } else {
+        ctu += ",";
+      }
+      ctu += label;
+    } else {
+      Atom *begAtom = bond->getBeginAtom();
+      Atom *endAtom = bond->getEndAtom();
+      bool needSwap = false;
+      if (begAtom->getDegree() > 2) {
+        unsigned int o1 = atomOrder[bond->getStereoAtoms()[0]];
+        for (const auto nbr : mol.atomNeighbors(begAtom)) {
+          if (nbr == endAtom ||
+              nbr->getIdx() ==
+                  static_cast<unsigned>(bond->getStereoAtoms()[0])) {
+            continue;
+          }
+          if (atomOrder[nbr->getIdx() < o1]) {
+            // this neighbor came first, we need to swap:
+            needSwap = !needSwap;
+          }
+        }
+      }
+      if (endAtom->getDegree() > 2) {
+        unsigned int o1 = atomOrder[bond->getStereoAtoms()[1]];
+        for (const auto nbr : mol.atomNeighbors(endAtom)) {
+          if (nbr == begAtom ||
+              nbr->getIdx() ==
+                  static_cast<unsigned>(bond->getStereoAtoms()[1])) {
+            continue;
+          }
+          if (atomOrder[nbr->getIdx() < o1]) {
+            // this neighbor came first, we need to swap:
+            needSwap = !needSwap;
+          }
+        }
+      }
+      if (bstereo == Bond::BondStereo::STEREOCIS || needSwap) {
+        if (c.empty()) {
+          c += "c:";
+        } else {
+          c += ",";
+        }
+        c += label;
+      } else {
+        if (t.empty()) {
+          t += "t:";
+        } else {
+          t += ",";
+        }
+        t += label;
+      }
+    }
+  }
+  return c + t + ctu;
+}
+
 std::string get_linknodes_block(const ROMol &mol,
                                 const std::vector<unsigned int> &atomOrder) {
   bool strict = false;
@@ -2046,6 +2206,9 @@ std::string getCXExtensions(const ROMol &mol, std::uint32_t flags) {
     const auto cfgblock =
         get_bond_config_block(mol, atomOrder, bondOrder, includeCoords);
     appendToCXExtension(cfgblock, res);
+    const auto cistransblock =
+        get_ringbond_cistrans_block(mol, atomOrder, bondOrder);
+    appendToCXExtension(cistransblock, res);
   }

   if (flags & SmilesWrite::CXSmilesFields::CX_LINKNODES) {
diff --git a/Code/GraphMol/SmilesParse/SmilesWrite.cpp b/Code/GraphMol/SmilesParse/SmilesWrite.cpp
index c670a37afc9..21e5e0b2bb9 100644
--- a/Code/GraphMol/SmilesParse/SmilesWrite.cpp
+++ b/Code/GraphMol/SmilesParse/SmilesWrite.cpp
@@ -360,7 +360,7 @@ std::string FragmentSmilesConstruct(
           ringClosureMap.erase(rclosure);
         }
         ringClosuresToErase.clear();
-        // std::cout<<"\t\tAtom: "<<mSE.obj.atom->getIdx()<<std::endl;
+        // std::cout << "\t\tAtom: " << mSE.obj.atom->getIdx() << std::endl;
         if (!atomSymbols) {
           res << GetAtomSmiles(mSE.obj.atom, params.doKekule, bond,
                                params.allHsExplicit, params.doIsomericSmiles);
@@ -371,7 +371,7 @@ std::string FragmentSmilesConstruct(
         break;
       case Canon::MOL_STACK_BOND:
         bond = mSE.obj.bond;
-        // std::cout<<"\t\tBond: "<<bond->getIdx()<<std::endl;
+        // std::cout << "\t\tBond: " << bond->getIdx() << std::endl;
         if (!bondSymbols) {
           res << GetBondSmiles(bond, mSE.number, params.doKekule,
                                params.allBondsExplicit);
@@ -382,7 +382,7 @@ std::string FragmentSmilesConstruct(
         break;
       case Canon::MOL_STACK_RING:
         ringIdx = mSE.number;
-        // std::cout<<"\t\tRing: "<<ringIdx;
+        // std::cout << "\t\tRing: " << ringIdx << std::endl;
         if (ringClosureMap.count(ringIdx)) {
           // the index is already in the map ->
           //   we're closing a ring, so grab
@@ -453,17 +453,19 @@ std::string MolToSmiles(const ROMol &mol, const SmilesWriteParams &params) {
       MolOps::getMolFrags(mol, false, nullptr, &fragsMolAtomMapping, false);
   // we got the mapping between fragments and atoms; repeat that for bonds
   std::vector<std::vector<int>> fragsMolBondMapping;
+  boost::dynamic_bitset<> atsPresent(mol.getNumAtoms());
+  std::vector<int> bondsInFrag;
+  bondsInFrag.reserve(mol.getNumBonds());
   for (const auto &atsInFrag : fragsMolAtomMapping) {
-    std::vector<int> bondsInFrag;
+    atsPresent.reset();
+    bondsInFrag.clear();
     for (auto aidx : atsInFrag) {
-      const auto atm = mol.getAtomWithIdx(aidx);
-      for (const auto bndIter :
-           boost::make_iterator_range(mol.getAtomBonds(atm))) {
-        const auto bnd = mol[bndIter];
-        if (std::find(bondsInFrag.begin(), bondsInFrag.end(), bnd->getIdx()) ==
-            bondsInFrag.end()) {
-          bondsInFrag.push_back(bnd->getIdx());
-        }
+      atsPresent.set(aidx);
+    }
+    for (const auto bnd : mol.bonds()) {
+      if (atsPresent[bnd->getBeginAtomIdx()] &&
+          atsPresent[bnd->getEndAtomIdx()]) {
+        bondsInFrag.push_back(bnd->getIdx());
       }
     }
     fragsMolBondMapping.push_back(bondsInFrag);
@@ -623,7 +625,7 @@ std::string MolToSmiles(const ROMol &mol, const SmilesWriteParams &params) {
   mol.setProp(common_properties::_smilesBondOutputOrder, flattenedBondOrdering,
               true);
   return result;
-}  // end of MolToSmiles()
+}

 std::string MolToCXSmiles(const ROMol &mol, const SmilesWriteParams &params,
                           std::uint32_t flags) {
diff --git a/Docs/Book/RDKit_Book.rst b/Docs/Book/RDKit_Book.rst
index 1a62184318e..bea560adedc 100644
--- a/Docs/Book/RDKit_Book.rst
+++ b/Docs/Book/RDKit_Book.rst
@@ -255,10 +255,13 @@ The features which are parsed include:
 - radicals ``^``
 - enhanced stereo (these are converted into ``StereoGroups``)
 - linknodes ``LN``
-- multi-center attachments ``m``
+- variable/multi-center attachments ``m``
 - ring bond count specifications ``rb``
 - non-hydrogen substitution count specifications ``s``
 - unsaturation specification ``u``
+- wedged bonds (only when atomic coordinates are present): ``wU``, ``wD``
+- wiggly bonds ``w``
+- double bond stereo (only for ring bonds) ``c``, ``t``, ``ctu``
 - SGroup Data ``SgD``
 - polymer SGroups ``Sg``
 - SGroup Hierarchy ``SgH``
@@ -273,7 +276,10 @@ The features which are written by :py:func:`rdkit.Chem.rdmolfiles.MolToCXSmiles`
 - atomic properties
 - radicals
 - enhanced stereo
-- linknodes
+- linknodes
+- wedged bonds (only when atomic coordinates are also written)
+- wiggly bonds
+- double bond stereo (only for ring bonds)
 - SGroup Data
 - polymer SGroups
 - SGroup Hierarchy
@@ -531,14 +537,15 @@ Stereochemistry
 Types of stereochemistry supported
 ----------------------------------

-The RDKit currently supports tetrahedral atomic stereochemistry and cis/trans
-stereochemistry at double bonds. We plan to add support for additional types of
-stereochemistry in the future.
+The RDKit currently fully supports tetrahedral atomic stereochemistry and
+cis/trans stereochemistry at double bonds. There is partial support for
+non-tetrahedral stereochemistry, see the section :ref:`Support for non-tetrahedral atomic stereochemistry`.

 Identification of potential stereoatoms/stereobonds
 ---------------------------------------------------

-As of the 2020.09 release the RDKit has two different ways of identifying potential stereoatoms/stereobonds:
+As of the 2020.09 release the RDKit has two different ways of identifying
+potential stereoatoms/stereobonds:

    1. The legacy approach: ``AssignStereochemistry()``.
       This approach does a reasonable job of recognizing potential
@@ -655,6 +662,54 @@ Brief description of the ``findPotentialStereo()`` algorithm
       ranked atoms attached to either end [#eitherend]_ to the results
    10. Return the results

+Sources of information about stereochemistry
+--------------------------------------------
+
+From SMILES
+^^^^^^^^^^^
+
+Atomic stereochemistry can be specified using ``@``, ``@@``, ``@SP``, etc.
+Potential stereocenters with no information provided are
+``ChiralType::CHI_UNSPECIFIED``.
+
+Double-bond stereochemistry is specfied using ``/`` and ``\`` to indicate the
+directionality of the neighboring single bonds. Double bonds with no stereo
+information provided are ``BondStereo::STEREONONE``.
+
+
+From Mol
+^^^^^^^^
+
+Atomic stereochemistry can be specified using wedged bonds if 2D coordinates are
+present. If 3D coordinates are present, they are used to set the stereochemistry
+for stereogenic atoms. Wiggly bonds (``CFG=2`` in V3000 mol blocks) set the
+chiral tag of stereogenic start atom to ``ChiralType::CHI_UNSPECIFIED``.
+
+Double-bond stereochemistry is automatically set using the atomic coordinates;
+this is true for both 2D and 3D coordinates. If a stereogenic double bound is
+crossed (``CFG=2`` in V3000 mol blocks) or has an adjacent wiggly single bond
+(``CFG=2`` in V3000 mol blocks), then it will be ``BondStereo::STEREOANY``.
+
+
+From CXSMILES
+^^^^^^^^^^^^^
+
+An initial stereochemistry assignment is done following the SMILES rules (see above).
+
+A ``w:`` (wiggly bond) specification will set the stereochemistry of the start
+atom to ``ChiralType::CHI_UNSPECIFIED`` and double bonds to
+``BondStereo::STEREOANY``. Stereochemistry of ring bonds can be set using ``t``,
+``c``, or ``ctu``.
+
+If 2D coordinates are present in the CXSMILES, atomic stereo can be set using
+```wU``` or ```wD``` to create wedged bonds.
+
+If 3D coordinates are present in the CXSMILES, they are used to set the
+stereochemistry for stereogenic atoms and bonds. This supersedes other
+specifications in the CXSMILES except for ``ctu`` and ``w``.
+
+
+
 Support for non-tetrahedral atomic stereochemistry
 ==================================================

diff --git a/Docs/Book/conf.py b/Docs/Book/conf.py
index 053d07a6428..ba08c6a03a3 100644
--- a/Docs/Book/conf.py
+++ b/Docs/Book/conf.py
@@ -27,7 +27,9 @@

 # Add any Sphinx extension module names here, as strings. They can be extensions
 # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'recommonmark']  # , 'extapi']
+extensions = [
+  'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'recommonmark', 'sphinx.ext.autosectionlabel'
+]  # , 'extapi']
 #autosummary_generate = True
 doctest_test_doctest_blocks = ""

diff --git a/Code/GraphMol/ChemReactions/catch_tests.cpp b/Code/GraphMol/ChemReactions/catch_tests.cpp
index 2e5a8bf6aba..a935333114d 100644
--- a/Code/GraphMol/ChemReactions/catch_tests.cpp
+++ b/Code/GraphMol/ChemReactions/catch_tests.cpp
@@ -1180,10 +1180,33 @@ TEST_CASE("CXSMILES for reactions", "[cxsmiles]") {
           "(3 6 8 7)");
   }
 #endif
+  SECTION("cis/trans markers") {
+    auto rxn =
+        "C1C=CC=CCC=CC=C1>>C1C=CC=CNC=CC=C1 |c:1,3,6,8,11,13,16,18|"_rxnsmiles;
+    REQUIRE(rxn);
+    CHECK(rxn->getReactants().size() == 1);
+    CHECK(rxn->getProducts().size() == 1);
+    CHECK(rxn->getReactants()[0]->getBondWithIdx(1)->getStereo() ==
+          Bond::BondStereo::STEREOCIS);
+    CHECK(rxn->getProducts()[0]->getBondWithIdx(1)->getStereo() ==
+          Bond::BondStereo::STEREOCIS);
+  }
+  SECTION("wedged bonds") {
+    auto rxn = "CC(O)(F)Cl>>CC(N)(F)Cl |w:1.0,6.5|"_rxnsmiles;
+    REQUIRE(rxn);
+    CHECK(rxn->getReactants().size() == 1);
+    CHECK(rxn->getProducts().size() == 1);
+    unsigned int bondcfg;
+    CHECK(rxn->getReactants()[0]->getBondWithIdx(0)->getPropIfPresent(
+        "_MolFileBondCfg", bondcfg));
+    CHECK(bondcfg == 2);
+    CHECK(rxn->getProducts()[0]->getBondWithIdx(1)->getPropIfPresent(
+        "_MolFileBondCfg", bondcfg));
+    CHECK(bondcfg == 2);
+  }
 }

 TEST_CASE("V3K rxn blocks") {
-
   SECTION("writing basics") {
     // clang-format off
     auto rxn =
diff --git a/Code/GraphMol/SmilesParse/catch_tests.cpp b/Code/GraphMol/SmilesParse/catch_tests.cpp
index a9a2e85a46d..f5f7946d50c 100644
--- a/Code/GraphMol/SmilesParse/catch_tests.cpp
+++ b/Code/GraphMol/SmilesParse/catch_tests.cpp
@@ -1317,6 +1317,16 @@ TEST_CASE("smilesBondOutputOrder") {
     m->getProp(common_properties::_smilesBondOutputOrder, order);
     CHECK(order == std::vector<unsigned int>{3, 4, 2, 1, 0});
   }
+  SECTION("Github #5585: incorrect ordering for ring closures") {
+    auto m = "OC1CCCN1"_smiles;
+    REQUIRE(m);
+    CHECK(MolToSmiles(*m) == "OC1CCCN1");
+    std::vector<unsigned int> order;
+    m->getProp(common_properties::_smilesAtomOutputOrder, order);
+    CHECK(order == std::vector<unsigned int>{0, 1, 2, 3, 4, 5});
+    m->getProp(common_properties::_smilesBondOutputOrder, order);
+    CHECK(order == std::vector<unsigned int>{0, 1, 2, 3, 4, 5});
+  }
 }

 TEST_CASE("Github #4320: Support toggling components of CXSMILES output") {
@@ -2204,3 +2214,71 @@ M  END
     }
   }
 }
+
+TEST_CASE("ring bond stereochemistry in CXSMILES") {
+  SECTION("basic reading") {
+    std::vector<std::pair<std::string, Bond::BondStereo>> tests = {
+        {"C1CCCC/C=C/CCC1 |t:5|", Bond::BondStereo::STEREOTRANS},
+        {"C1CCCCC=CCCC1 |t:5|", Bond::BondStereo::STEREOTRANS},
+        {"C1CCCCC=CCCC1 |c:5|", Bond::BondStereo::STEREOCIS},
+        {"C1CCCC/C=C/CCC1 |c:5|", Bond::BondStereo::STEREOCIS},
+        {"C1CCCCC=CCCC1 |ctu:5|", Bond::BondStereo::STEREOANY},
+        {"C1CCCC/C=C/CCC1 |ctu:5|", Bond::BondStereo::STEREOANY},
+    };
+    for (const auto [smi, val] : tests) {
+      SmilesParserParams ps;
+      ps.useLegacyStereo = false;
+      std::unique_ptr<RWMol> m{SmilesToMol(smi, ps)};
+      INFO(smi);
+      REQUIRE(m);
+      CHECK(m->getBondWithIdx(5)->getBondType() == Bond::BondType::DOUBLE);
+      CHECK(m->getBondWithIdx(5)->getStereo() == val);
+    }
+  }
+  SECTION("read multiple values") {
+    SmilesParserParams ps;
+    ps.useLegacyStereo = false;
+    std::unique_ptr<RWMol> m{SmilesToMol("C1CCCCC=CCCC=1 |t:5,9|", ps)};
+    REQUIRE(m);
+    CHECK(m->getBondWithIdx(5)->getBondType() == Bond::BondType::DOUBLE);
+    CHECK(m->getBondWithIdx(5)->getStereo() == Bond::BondStereo::STEREOTRANS);
+    CHECK(m->getBondWithIdx(9)->getBondType() == Bond::BondType::DOUBLE);
+    CHECK(m->getBondWithIdx(9)->getStereo() == Bond::BondStereo::STEREOTRANS);
+  }
+  SECTION("skip small rings") {
+    std::vector<std::pair<std::string, Bond::BondStereo>> tests = {
+        {"C1=CCCCC1 |ctu:0|", Bond::BondStereo::STEREONONE},
+        {"C1=CCCCC1 |c:0|", Bond::BondStereo::STEREONONE}};
+    for (const auto [smi, val] : tests) {
+      SmilesParserParams ps;
+      ps.useLegacyStereo = false;
+      std::unique_ptr<RWMol> m{SmilesToMol(smi, ps)};
+      INFO(smi);
+      REQUIRE(m);
+      CHECK(m->getBondWithIdx(0)->getBondType() == Bond::BondType::DOUBLE);
+      CHECK(m->getBondWithIdx(0)->getStereo() == val);
+    }
+  }
+  SECTION("basic writing") {
+    std::vector<std::pair<std::string, std::string>> tests = {
+        {"C1CCCC/C=C/CCC1 |t:5|", "C1=C/CCCCCCCC/1 |t:0|"},
+        {"C1CCCCC=CCCC1 |t:5|", "C1=CCCCCCCCC1 |t:0|"},
+        {"C1CCCCC=CCCC1 |c:5|", "C1=CCCCCCCCC1 |c:0|"},
+        {"C1CCCC/C=C/CCC1 |c:5|", "C1=C/CCCCCCCC/1 |c:0|"},
+        {"C1=CCCCCCCCC1 |ctu:0|", "C1=CCCCCCCCC1 |ctu:0|"},
+        {"C=CCCCCCCCC |ctu:0|",
+         "C=CCCCCCCCC"}  // we don't write the markers for non-ring bonds
+    };
+    for (const auto [smi, val] : tests) {
+      SmilesParserParams ps;
+      ps.useLegacyStereo = false;
+      std::unique_ptr<RWMol> m{SmilesToMol(smi, ps)};
+      INFO(smi);
+      REQUIRE(m);
+      SmilesWriteParams ops;
+      auto cxsmi = MolToCXSmiles(
+          *m, ops, SmilesWrite::CXSmilesFields::CX_ALL_BUT_COORDS);
+      CHECK(cxsmi == val);
+    }
+  }
+}


Fix R Goups in matching core are poorly positioned (rdkit/rdkit#5684)

<!--
Thanks for contributing a pull request!
-->
#### Reference Issue
<!-- Example: Fixes #1234 -->

Fixes #5631

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

Issue #5631 has 2 structures.  When the first is decomposed the position of R2 on the core is poor.  This is fixed by recalculating the dummy position on core output if the  R group is hydrogen only.
When the second structure is decomposed the poor position of R2 is retained.  This is because it matches the heavy atom co-ordinates in the input structure.  While I don't like the position it doesn't make sense to override the user's input (the position would not look so bad if the explicit hydrogens in the input structure were retained).

![image](https://user-images.githubusercontent.com/24233741/197587972-6392b0c6-786c-4580-b095-4de746ac04e2.png)

#### Any other comments?

The core for the second structure is missing the stereo chemistry information (see #5613)

RDKit R Goups in matching core are poorly positioned

**Describe the bug**

When decomposing structures with coordinates the generated coordinates of a core R group may be poor.

**To Reproduce**

```python
# %%

from rdkit import Chem
from rdkit.Chem import AllChem
from rdkit.Chem.rdRGroupDecomposition import RGroupDecomposition, RGroupDecompositionParameters, \
    RGroupMatching, RGroupScore, RGroupLabels, RGroupCoreAlignment, RGroupLabelling
from rdkit.Chem.Draw import IPythonConsole

IPythonConsole.ipython_useSVG = True

from rdkit.Chem import PandasTools
from rdkit.Chem import Draw
from IPython.display import HTML

import pandas as pd

# %%

def do_decomp(options, core, mols):
  decomp = RGroupDecomposition([core], options)
  display_mols = []
  for mol in mols:
    if decomp.Add(mol) > -1:
      display_mols.append(mol)
    else:
      print("Failed to add mol")
  decomp.Process()
  groups = decomp.GetRGroupsAsColumns()
  df = PandasTools.RGroupDecompositionToFrame(groups, display_mols, include_core=True)
  PandasTools.ChangeMoleculeRendering(df)
  return HTML(df.to_html())

# %%
query_block = """
     RDKit          2D

 12 12  0  0  0  0  0  0  0  0999 V2000
   -3.4154    3.2137    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -2.5903    3.2137    0.0000 A   0  0  0  0  0  0  0  0  0  0  0  0
   -2.1778    3.9282    0.0000 A   0  0  0  0  0  0  0  0  0  0  0  0
   -2.5903    4.6427    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -3.4154    4.6427    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -3.8278    3.9282    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -4.6528    3.9282    0.0000 A   0  0  0  0  0  0  0  0  0  0  0  0
   -3.8278    2.4993    0.0000 R#  0  0  0  0  0  0  0  0  0  0  0  0
   -1.3528    3.9282    0.0000 R#  0  0  0  0  0  0  0  0  0  0  0  0
   -5.0654    4.6427    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -4.6528    5.3572    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
   -5.8904    4.6427    0.0000 R#  0  0  0  0  0  0  0  0  0  0  0  0
  5  6  8  0
  4  5  8  0
  3  4  8  0
  2  3  8  0
  1  2  8  0
  1  6  8  0
  6  7  1  0
  1  8  1  0
  3  9  1  0
  7 10  1  0
 10 11  2  0
 10 12  1  0
M  ISO  3   8   2   9   3  12   1
M  RGP  3   8   2   9   3  12   1
M  END
"""
core = Chem.MolFromMolBlock(query_block)
core

# %%
mol_block = """
  -OEChem-02051811442D

 25 27  0     0  0  0  0  0  0999 V2000
   -0.8675    0.4975    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    0.8675    0.4975    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    0.8675    1.5027    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -0.0000    2.0104    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
   -0.8675    1.5027    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    1.7350    2.0001    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
    2.6070    1.5001    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    3.4790    2.0002    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    3.4790    3.0002    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    2.6159    3.5053    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    1.7439    3.0052    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    4.3465    3.4976    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
    4.3495    4.4976    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    3.4849    5.0002    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
    5.2170    4.9951    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    6.1281    4.5828    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    6.8007    5.3248    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    6.3049    6.1952    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    5.3214    5.9901    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
    6.7160    7.1067    0.0000 Cl  0  0  0  0  0  0  0  0  0  0  0  0
    7.7947    5.2157    0.0000 Cl  0  0  0  0  0  0  0  0  0  0  0  0
    1.7328   -0.0038    0.0000 N   0  3  0  0  0  0  0  0  0  0  0  0
    1.7313   -1.0038    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
    2.5995    0.4950    0.0000 O   0  5  0  0  0  0  0  0  0  0  0  0
  1  6  2  0  0  0  0
  1  2  1  0  0  0  0
  2  3  2  0  0  0  0
  3  4  1  0  0  0  0
  4  5  2  0  0  0  0
  5  6  1  0  0  0  0
  4  7  1  0  0  0  0
  7 12  1  0  0  0  0
  7  8  1  0  0  0  0
  8  9  1  0  0  0  0
  9 10  1  0  0  0  0
 10 11  1  0  0  0  0
 11 12  1  0  0  0  0
 10 13  1  0  0  0  0
 13 14  1  0  0  0  0
 14 15  2  0  0  0  0
 14 16  1  0  0  0  0
 16 20  1  0  0  0  0
 16 17  2  0  0  0  0
 17 18  1  0  0  0  0
 18 19  2  0  0  0  0
 19 20  1  0  0  0  0
 19 21  1  0  0  0  0
 18 22  1  0  0  0  0
  3 23  1  0  0  0  0
 23 24  2  0  0  0  0
 23 25  1  0  0  0  0
M  CHG  2  23   1  25  -1
M  END
$$$$
"""
mol = Chem.MolFromMolBlock(mol_block)
mol

# %%
mol_block2 = """
  -OEChem-02051811442D

 45 49  0     1  0  0  0  0  0999 V2000
   -2.9472   -3.8597    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -2.8765   -2.8622    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -3.6440   -2.2211    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -3.2684   -1.2927    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -2.2691   -1.3606    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -2.0260   -2.3354    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
   -1.6270   -0.5940    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -1.9700    0.3454    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
   -0.6420   -0.7667    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
    0.0000    0.0000    0.0000 C   0  0  2  0  0  0  0  0  0  0  0  0
    1.1236   -1.3417    0.0000 H   0  0  0  0  0  0  0  0  0  0  0  0
   -0.8675    0.4975    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -0.8675    1.5027    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    0.0000    2.0104    0.0000 N   0  0  3  0  0  0  0  0  0  0  0  0
    0.8675    1.5027    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    0.8675    0.4975    0.0000 C   0  0  2  0  0  0  0  0  0  0  0  0
    1.2077   -0.4429    0.0000 H   0  0  0  0  0  0  0  0  0  0  0  0
    1.8525    0.6702    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
    2.1954    1.6096    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    0.0000    3.0104    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -0.8098    3.5999    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
   -0.5017    4.5529    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    0.4983    4.5516    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    0.8122    3.6017    0.0000 S   0  0  0  0  0  0  0  0  0  0  0  0
    1.0846    5.3617    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    2.0793    5.2591    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
    0.6762    6.2745    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
   -1.0895    5.3618    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -2.0889    5.2537    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -2.6768    6.0627    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
   -2.2755    6.9786    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -1.2762    7.0868    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -0.6781    6.2789    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
   -2.8669    7.7850    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
   -3.8610    7.6760    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -4.4524    8.4824    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -5.4465    8.3734    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
   -6.0457    9.1803    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -7.0449    9.0708    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -7.4449    8.1542    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
   -6.8558    7.3461    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -5.8566    7.4557    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -8.4389    8.0452    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -3.7990   -0.4450    0.0000 Cl  0  0  0  0  0  0  0  0  0  0  0  0
   -4.6141   -2.4639    0.0000 Cl  0  0  0  0  0  0  0  0  0  0  0  0
  1  2  1  0  0  0  0
  2  6  1  0  0  0  0
  2  3  2  0  0  0  0
  3  4  1  0  0  0  0
  4  5  2  0  0  0  0
  5  6  1  0  0  0  0
  5  7  1  0  0  0  0
  7  8  2  0  0  0  0
  7  9  1  0  0  0  0
  9 10  1  0  0  0  0
 10 11  1  1  0  0  0
 10 16  1  0  0  0  0
 10 12  1  0  0  0  0
 12 13  1  0  0  0  0
 13 14  1  0  0  0  0
 14 15  1  0  0  0  0
 15 16  1  0  0  0  0
 16 17  1  1  0  0  0
 16 18  1  0  0  0  0
 18 19  1  0  0  0  0
 14 20  1  0  0  0  0
 20 24  1  0  0  0  0
 20 21  2  0  0  0  0
 21 22  1  0  0  0  0
 22 23  2  0  0  0  0
 23 24  1  0  0  0  0
 23 25  1  0  0  0  0
 25 26  2  0  0  0  0
 25 27  1  0  0  0  0
 22 28  1  0  0  0  0
 28 33  2  0  0  0  0
 28 29  1  0  0  0  0
 29 30  2  0  0  0  0
 30 31  1  0  0  0  0
 31 32  2  0  0  0  0
 32 33  1  0  0  0  0
 31 34  1  0  0  0  0
 34 35  1  0  0  0  0
 35 36  1  0  0  0  0
 36 37  1  0  0  0  0
 37 42  1  0  0  0  0
 37 38  1  0  0  0  0
 38 39  1  0  0  0  0
 39 40  1  0  0  0  0
 40 41  1  0  0  0  0
 41 42  1  0  0  0  0
 40 43  1  0  0  0  0
  4 44  1  0  0  0  0
  3 45  1  0  0  0  0
M  END
$$$$
"""
mol2 = Chem.MolFromMolBlock(mol_block2)
mol2

# %%
options = RGroupDecompositionParameters()
options.matchingStrategy = RGroupMatching.GreedyChunks
options.matchingStrategy = RGroupMatching.Exhaustive
options.onlyMatchAtRGroups = False
options.scoreMethod = RGroupScore.FingerprintVariance
options.allowMultipleRGroupsOnUnlabelled = True
options.removeHydrogensPostMatch = True
options.allowMultipleRGroupsOnUnlabelled = False
options.removeAllHydrogenRGroups = False
do_decomp(options, core, [mol, mol2])

```

**Expected behavior**

I expect to see the core R2 for both structures in a better geometry.

**Screenshots**

But I see this:

![image](https://user-images.githubusercontent.com/24233741/197569558-6f31014e-19ec-4371-8c2c-e55ba7987c05.png)

**Configuration (please complete the following information):**
 - RDKit version:  master as o 10/9/22
 - OS: [e.g. Ubuntu 20.04] Ubuntu 20.04
 - Python version (if relevant):
 - Are you using conda?
 - If you are using conda, which channel did you install the rdkit from?
 - If you are not using conda: how did you install the RDKit?

**Additional context**

I think what is happening is that the R group is added while there are explicit hydrogens on the core.  I will look at it once [PR 5618](https://github.com/rdkit/rdkit/pull/5618) is out of the way.

Edited and added an additional example n 10/24/22.  In the first example R2 is positioned as if there are 2 explicit hydrogens on the core, while in the second example R2 uses the co-ordinates of the chiral methoxy in the input structure.  Both look rubbish in the output core, though there may be an argument for retaining the R2 group position of the second structure as that is specified in the input structure.

This behavior is not present in the 2022_03 release.  I think it comes from changes in `RDKit::replaceCore` where atom positions are now set from matching atoms in the parent molecule rather than from `MolOps::setTerminalAtomCoords`.

diff --git a/Code/GraphMol/RGroupDecomposition/RGroupDecomp.cpp b/Code/GraphMol/RGroupDecomposition/RGroupDecomp.cpp
index 8ad35951683..b8a2c72cec9 100644
--- a/Code/GraphMol/RGroupDecomposition/RGroupDecomp.cpp
+++ b/Code/GraphMol/RGroupDecomposition/RGroupDecomp.cpp
@@ -456,6 +456,7 @@ RWMOL_SPTR RGroupDecomposition::outputCoreMolecule(
     }
     auto label = data->getRlabel(atom);
     auto missingRGroup = atom->hasProp(MISSING_RGROUP);
+    bool atomRemoved = false;
     Atom *nbrAtom = nullptr;
     for (const auto &nbri :
          boost::make_iterator_range(coreWithMatches->getAtomNeighbors(atom))) {
@@ -473,6 +474,7 @@ RWMOL_SPTR RGroupDecomposition::outputCoreMolecule(
       } else if (!isUserDefinedLabel ||
                  data->params.removeAllHydrogenRGroupsAndLabels) {
         coreWithMatches->removeAtom(atomIdx);
+        atomRemoved = true;
         // if we remove an unused label from an aromatic atom,
         // we need to check whether we need to adjust its explicit
         // H count, or it will fail to kekulize
@@ -485,6 +487,13 @@ RWMOL_SPTR RGroupDecomposition::outputCoreMolecule(
         }
       }
       nbrAtom->updatePropertyCache(false);
+      if (!atomRemoved && coreWithMatches->getNumConformers() > 0) {
+        auto iter = match.rgroups.find(label);
+        if (iter != match.rgroups.end() && iter->second->is_hydrogen) {
+          MolOps::setTerminalAtomCoords(*coreWithMatches, atomIdx,
+                                        nbrAtom->getIdx());
+        }
+      }
     }
   }
   return coreWithMatches;
diff --git a/Code/GraphMol/RGroupDecomposition/RGroupDecompData.h b/Code/GraphMol/RGroupDecomposition/RGroupDecompData.h
index b96017fcd25..3130576592f 100644
--- a/Code/GraphMol/RGroupDecomposition/RGroupDecompData.h
+++ b/Code/GraphMol/RGroupDecomposition/RGroupDecompData.h
@@ -71,7 +71,7 @@ struct RGroupDecompData {
   }

   void addCore(const ROMol &inputCore) {
-    if (params.allowMultipleRGroupsOnUnlabelled) {
+    if (params.allowMultipleRGroupsOnUnlabelled && !params.onlyMatchAtRGroups) {
       RWMol core(inputCore);
       params.addDummyAtomsToUnlabelledCoreAtoms(core);
       cores[cores.size()] = RCore(core);