Repository: rdkit/rdkit — pull request #6785 (cpp)
Title: Fixes #6773

This PR fixes #6773.
There was a flaw in the logic to generate the ring queries when `BondCompareParameters.MatchFusedRingsStrict` is `true`.
Upon further testing I realized that `Rn` queries with _n_>1 should rather be `R&!R1` as different molecules may have different fusions and the query needs to capture all of them.

Now the test molecules in #6773 yield the expected result (note that by default `CompleteRingsOnly` is set to `False`):
```
from rdkit import Chem
from rdkit.Chem.Draw import IPythonConsole, MolsToGridImage
from rdkit.Chem import rdFMCS

IPythonConsole.ipython_useSVG = True
IPythonConsole.drawOptions.addAtomIndices = True

def get_bond_matches(atom_idx_matches, mol, query):
    inverse_match = [None] * mol.GetNumAtoms()
    for i, atom_idx in enumerate(atom_idx_matches):
        inverse_match[atom_idx] = i
    res = []
    for b in mol.GetBonds():
        bi = b.GetBeginAtomIdx()
        ei = b.GetEndAtomIdx()
        if (bi in atom_idx_matches and ei in atom_idx_matches
            and query.GetBondBetweenAtoms(inverse_match[bi], inverse_match[ei])):
            res.append(b.GetIdx())
    return tuple(res)

mol1 = Chem.MolFromSmiles('OC1CCC2CCCCC2C1')
mol2 = Chem.MolFromSmiles('OC1CCCC2CCCCC12')
mols = [mol1, mol2]

print(f"mcs.numAtoms {mcs.numAtoms} mcs.numBonds {mcs.numBonds}")
print(mcs.smartsString)
atom_matches = [m.GetSubstructMatch(mcs.queryMol) for m in mols]
bond_matches = [get_bond_matches(am, mols[i], mcs.queryMol) for i, am in enumerate(atom_matches)]
for i, m in enumerate(mols):
    print(f"mol{i+1} atom_matches {atom_matches[i]} bond_matches {bond_matches[i]}")
MolsToGridImage(mols, highlightAtomLists=atom_matches, highlightBondLists=bond_matches)

mcs.numAtoms 11 mcs.numBonds 11
[#8&!R]-&!@[#6&R]1-&@[#6&R]-&@[#6&R]-&@[#6&R]-&@[#6&R](-&@[#6&R]-&@1)-&@[#6&R]-&@[#6&R]-&@[#6&R]-&@[#6&R]
mol1 atom_matches (0, 1, 2, 10, 3, 9, 4, 8, 7, 6, 5) bond_matches (0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 11)
mol2 atom_matches (0, 1, 2, 10, 3, 5, 4, 6, 7, 8, 9) bond_matches (0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11)
params = rdFMCS.MCSParameters()
params.BondCompareParameters.RingMatchesRingOnly = True
params.AtomCompareParameters.RingMatchesRingOnly = True
params.BondCompareParameters.MatchFusedRingsStrict = True
```
![image](https://github.com/rdkit/rdkit/assets/5244385/7e76698b-6cf5-4a03-825b-78e702f78708)
```
mcs.queryMol
```
![image](https://github.com/rdkit/rdkit/assets/5244385/90905598-2194-49b8-9a5e-90edf9031fe8)
This what you get when you add a third molecule with different ring sizes:
```
mol3 = Chem.MolFromSmiles('OC1CCC2CCCCCC12')
mols = [mol1, mol2, mol3]
mcs = rdFMCS.FindMCS(mols, params)
print(f"mcs.numAtoms {mcs.numAtoms} mcs.numBonds {mcs.numBonds}")
print(mcs.smartsString)
atom_matches = [m.GetSubstructMatch(mcs.queryMol) for m in mols]
bond_matches = [get_bond_matches(am, mols[i], mcs.queryMol) for i, am in enumerate(atom_matches)]
for i, m in enumerate(mols):
    print(f"mol{i+1} atom_matches {atom_matches[i]} bond_matches {bond_matches[i]}")
MolsToGridImage(mols, highlightAtomLists=atom_matches, highlightBondLists=bond_matches)

mcs.numAtoms 10 mcs.numBonds 9
[#8&!R]-&!@[#6&R](-&@[#6&R]-&@[#6&R])-&@[#6&R]-&@[#6&R]-&@[#6&R]-&@[#6&R]-&@[#6&R]-&@[#6&R]
mol1 atom_matches (0, 1, 2, 10, 3, 9, 8, 7, 6, 5) bond_matches (0, 1, 2, 5, 6, 7, 8, 9, 10)
mol2 atom_matches (0, 1, 2, 10, 3, 9, 8, 7, 6, 5) bond_matches (0, 1, 2, 5, 6, 7, 8, 9, 10)
mol3 atom_matches (0, 1, 2, 10, 3, 9, 8, 7, 6, 5) bond_matches (0, 1, 2, 5, 6, 7, 8, 9, 10)
```
![image](https://github.com/rdkit/rdkit/assets/5244385/9a1d36f4-2b64-4c8c-8723-540d3cca091e)
If we set `CompleteRingsOnly` to `True`, the MCS is decalin with the first two molecules;
```
mols = [mol1, mol2]
params.BondCompareParameters.CompleteRingsOnly = True
params.AtomCompareParameters.CompleteRingsOnly = True
mcs = rdFMCS.FindMCS(mols, params)
print(f"mcs.numAtoms {mcs.numAtoms} mcs.numBonds {mcs.numBonds}")
print(mcs.smartsString)
atom_matches = [m.GetSubstructMatch(mcs.queryMol) for m in mols]
bond_matches = [get_bond_matches(am, mols[i], mcs.queryMol) for i, am in enumerate(atom_matches)]
for i, m in enumerate(mols):
    print(f"mol{i+1} atom_matches {atom_matches[i]} bond_matches {bond_matches[i]}")
MolsToGridImage(mols, highlightAtomLists=atom_matches, highlightBondLists=bond_matches)

mcs.numAtoms 10 mcs.numBonds 11
[#6&R]1-&@[#6&R]-&@[#6&R]-&@[#6&R&!R1]2-&@[#6&R&!R1](-&@[#6&R]-&@1)-&@[#6&R]-&@[#6&R]-&@[#6&R]-&@[#6&R]-&@2
mol1 atom_matches (1, 2, 10, 3, 9, 8, 4, 7, 5, 6) bond_matches (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
mol2 atom_matches (2, 3, 1, 4, 10, 9, 5, 8, 6, 7) bond_matches (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
```
![image](https://github.com/rdkit/rdkit/assets/5244385/85e04b9a-440c-46aa-8afb-ee91f8c0ef1c)
When we add the third molecule, MCS is methanol:
```
mols = [mol1, mol2, mol3]
mcs = rdFMCS.FindMCS(mols, params)
print(f"mcs.numAtoms {mcs.numAtoms} mcs.numBonds {mcs.numBonds}")
print(mcs.smartsString)
atom_matches = [m.GetSubstructMatch(mcs.queryMol) for m in mols]
bond_matches = [get_bond_matches(am, mols[i], mcs.queryMol) for i, am in enumerate(atom_matches)]
for i, m in enumerate(mols):
    print(f"mol{i+1} atom_matches {atom_matches[i]} bond_matches {bond_matches[i]}")
MolsToGridImage(mols, highlightAtomLists=atom_matches, highlightBondLists=bond_matches)

mcs.numAtoms 2 mcs.numBonds 1
[#8&!R]-&!@[#6&R]
mol1 atom_matches (0, 1) bond_matches (0,)
mol2 atom_matches (0, 1) bond_matches (0,)
mol3 atom_matches (0, 1) bond_matches (0,)
```
![image](https://github.com/rdkit/rdkit/assets/5244385/29aff400-53f6-48c2-b74a-9b3b4e8b8cf1)


While writing unit tests for #6773 I also noticed a bug in `checkIfRingsAreClosed` when `noLoneRingAtoms` is `true`, and I fixed that as well; now I get the expected result with this test case:
```
smi = ["C1CC2CCC3CC2C(C1)CCCCCC3", "C1CC2CCC3CCCC4CCC(C1)C2C34"]
mols = [Chem.MolFromSmiles(s) for s in smi]
params = rdFMCS.MCSParameters()
params.BondCompareParameters.MatchFusedRingsStrict = True
mcs = rdFMCS.FindMCS([mols[0], mols[1]], params)
print(f"mcs.numAtoms {mcs.numAtoms} mcs.numBonds {mcs.numBonds}")
print(mcs.smartsString)
atom_matches = [m.GetSubstructMatch(mcs.queryMol) for m in mols]
bond_matches = [get_bond_matches(am, mols[i], mcs.queryMol) for i, am in enumerate(atom_matches)]
for i, m in enumerate(mols):
    print(f"mol{i+1} atom_matches {tuple(enumerate(atom_matches[i]))} bond_matches {tuple(enumerate(bond_matches[i]))}")
MolsToGridImage(mols, highlightAtomLists=atom_matches, highlightBondLists=bond_matches)

mcs.numAtoms 15 mcs.numBonds 16
[#6]1-&@[#6]-&@[#6&R&!R1]2-&@[#6]-&@[#6]-&@[#6](-&@[#6]-&@[#6&R&!R1]-&@2-&@[#6](-&@[#6]-&@1)-&@[#6]-&@[#6])-&@[#6]-&@[#6]-&@[#6]
mol1 atom_matches ((0, 0), (1, 1), (2, 9), (3, 2), (4, 3), (5, 7), (6, 4), (7, 6), (8, 8), (9, 5), (10, 10), (11, 15), (12, 11), (13, 14), (14, 13)) bond_matches ((0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9), (10, 10), (11, 13), (12, 14), (13, 15), (14, 16), (15, 17))
mol2 atom_matches ((0, 0), (1, 1), (2, 13), (3, 2), (4, 3), (5, 14), (6, 4), (7, 15), (8, 12), (9, 5), (10, 11), (11, 6), (12, 10), (13, 7), (14, 8)) bond_matches ((0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 10), (9, 11), (10, 12), (11, 13), (12, 14), (13, 15), (14, 16), (15, 17))
```
![image](https://github.com/rdkit/rdkit/assets/5244385/bfde6008-5e35-407a-a778-65df44f49bea)
```
params.BondCompareParameters.CompleteRingsOnly = True
params.AtomCompareParameters.CompleteRingsOnly = True
mcs = rdFMCS.FindMCS([mols[0], mols[1]], params)
print(f"mcs.numAtoms {mcs.numAtoms} mcs.numBonds {mcs.numBonds}")
print(mcs.smartsString)
atom_matches = [m.GetSubstructMatch(mcs.queryMol) for m in mols]
bond_matches = [get_bond_matches(am, mols[i], mcs.queryMol) for i, am in enumerate(atom_matches)]
for i, m in enumerate(mols):
    print(f"mol{i+1} atom_matches {tuple(enumerate(atom_matches[i]))} bond_matches {tuple(enumerate(bond_matches[i]))}")
MolsToGridImage(mols, highlightAtomLists=atom_matches, highlightBondLists=bond_matches)

mcs.numAtoms 10 mcs.numBonds 11
[#6&R]1-&@[#6&R]-&@[#6&R&!R1]2-&@[#6&R]-&@[#6&R]-&@[#6&R]-&@[#6&R]-&@[#6&R&!R1]-&@2-&@[#6&R]-&@[#6&R]-&@1
mol1 atom_matches ((0, 0), (1, 1), (2, 9), (3, 2), (4, 3), (5, 7), (6, 4), (7, 6), (8, 8), (9, 5)) bond_matches ((0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 15), (10, 16))
mol2 atom_matches ((0, 0), (1, 1), (2, 13), (3, 2), (4, 3), (5, 14), (6, 4), (7, 15), (8, 12), (9, 5)) bond_matches ((0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 12), (6, 13), (7, 14), (8, 15), (9, 16), (10, 17))
```
![image](https://github.com/rdkit/rdkit/assets/5244385/b8f8dcfa-7412-4454-8c74-8c35ec6a1fab)

Finally, I created the `RingBondCountVect` class as it is much tidier than passing around the same `std::vector<BondCount>&` and `consrt ROMol&` across multiple functions (which are now member functions of `RingBondCountVect`).

@d-b-w It would be great if you could check this against your additional test cases. Thanks in advance!

Resolved issues:
title: ConnectTheDots can segfault if all atoms do not have residue info
body: I do not have a minimal example, I was throwing random atoms and coordinates at the function and it crashed on me.  The point is that an atom's monomer info is initialized to `nullptr` but [this code](https://github.com/rdkit/rdkit/blob/master/Code/GraphMol/FileParsers/ProximityBonds.cpp#L218-L231):

```
        auto *n_info =
            (AtomPDBResidueInfo *)(mol->getAtomWithIdx(*nbr)->getMonomerInfo());
        if (d < best &&
            atom_info->getResidueNumber() == n_info->getResidueNumber()) {
          best = d;
          best_idx = *nbr;
        }
```
does not check before dereferencing either atom_info or n_info.  
title: MCS query incorrect when ringCompare=RingCompare.StrictRingFusion
body: **Describe the bug**
The MCS returned when `ringCompare=RingCompare.StrictRingFusion` may not match all molecules used to generate it. This is new since https://github.com/rdkit/rdkit/pull/6646 . I'm currently using 0a3fe925288c.

**To Reproduce**

```
from rdkit import Chem
from rdkit.Chem.Draw import IPythonConsole
from rdkit.Chem import rdFMCS

IPythonConsole.ipython_useSVG = True

from rdkit import Chem
from rdkit.Chem.Draw import IPythonConsole
from rdkit.Chem import rdFMCS

IPythonConsole.ipython_useSVG = True

mol1 = Chem.MolFromSmiles('Oc1ccc2ccccc2c1')
mol2 = Chem.MolFromSmiles('Oc1cccc2ccccc12')
mcs = rdFMCS.FindMCS([mol1, mol2],
    ringMatchesRingOnly=True,
    # The problem is the ring fusion.
    ringCompare=Chem.rdFMCS.RingCompare.StrictRingFusion)

print('I think this is wrong, the ring fusion seems to have "slipped"')
print('I think the right MCS if ring fusion count is strict is maybe `[#8!R]`')
print(mcs.smartsString)
print(mol1.GetSubstructMatch(mcs.queryMol))
print(mol2.GetSubstructMatch(mcs.queryMol), '<<<< this should match!!')
IPythonConsole.ShowMols((mol1, mol2))

# Previous to https://github.com/rdkit/rdkit/pull/6646, the MCS here was
old_mcs = '[#8&!R]-&!@[#6&R]1:&@[#6&R]:&@[#6&R]:&@[#6&R]:&@[#6&R](:&@[#6&R]:&@1):&@[#6&R]:&@[#6&R]:&@[#6&R]'
# which seems wrong because it doesn't require ring fusion matching.
# but at least it matches both mols??
old_mcs_mol = Chem.MolFromSmarts(old_mcs)
print(mol1.GetSubstructMatch(old_mcs_mol))
print(mol2.GetSubstructMatch(old_mcs_mol))
```

Here's the above in a Jupyter notebook:

<img width="1008" alt="mcs_bug" src="https://github.com/rdkit/rdkit/assets/3013277/25ad1c47-dcd0-44ef-be78-2e20e68f86f5">

Other context:
This was reported deep inside some internal Schrödinger validation code (PRIME-5144). Definitely let me know if I'm using StrictRingFusion incorrectly.

Hints:

Fix patch:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b7a63699b10..99d627a9e5d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -137,7 +137,7 @@ set(RDK_PYTHON_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/rdkit")
 set(RDKit_Year "2023")
 set(RDKit_Month "09")
 set(RDKit_Revision "1")
-set(RDKit_RevisionModifier "pre")
+set(RDKit_RevisionModifier "")
 set(RDKit_ABI "1")
 
 # we need an integer version of the month later, so remove the zero padding
diff --git a/Code/GraphMol/FMCS/FMCS.cpp b/Code/GraphMol/FMCS/FMCS.cpp
index 159dca41c7c..d456121b996 100644
--- a/Code/GraphMol/FMCS/FMCS.cpp
+++ b/Code/GraphMol/FMCS/FMCS.cpp
@@ -30,12 +30,6 @@ struct cmpCStr {
     return std::strcmp(a, b) < 0;
   }
 };
-
-struct BondCount {
-  boost::dynamic_bitset<> isMCSRingBond;
-  boost::dynamic_bitset<> isMCSRingBondNonFused;
-  boost::dynamic_bitset<> isMCSRingBondFused;
-};
 }  // namespace
 
 void MCSParameters::setMCSAtomTyperFromEnum(AtomComparator atomComp) {
@@ -498,111 +492,131 @@ bool MCSBondCompareOrderExact(const MCSBondCompareParameters& p,
 
 //=== RING COMPARE ========================================================
 namespace {
-std::vector<BondCount> initRingBondCountVect(const ROMol& mol) {
-  std::vector<BondCount> res(mol.getRingInfo()->bondRings().size());
-  for (auto& ringBondCount : res) {
-    ringBondCount.isMCSRingBond.resize(mol.getNumBonds());
-    ringBondCount.isMCSRingBondNonFused.resize(mol.getNumBonds());
-    ringBondCount.isMCSRingBondFused.resize(mol.getNumBonds());
-  }
-  return res;
-}
-
 // there are 3 bitsets for each ring
 // isMCSRingBond: bits are set in correspondence of ring bond indices which are
-// part of MCS isMCSRingBondFused: bits are set in correspondence of ring bond
-// indices which are part of MCS and are fused isMCSRingBondNonFused: bits are
-// set in correspondence of ring bond indices which are part of MCS and are not
-// fused in the 1st pass, we set fused/non-fused bits simply based on
-// numBondRings in the 2nd pass, we refine the above as certain bonds originally
-// marked as fused can be relabelled as non-fused
-void setMCSBondBitsPass1(unsigned int beginAtomIdx, unsigned int endAtomIdx,
-                         const ROMol& mol, const std::uint32_t c[],
-                         const FMCS::Graph& graph,
-                         std::vector<BondCount>& ringBondCountVect) {
-  const auto ri = mol.getRingInfo();
-  const auto bond =
-      mol.getBondBetweenAtoms(graph[c[beginAtomIdx]], graph[c[endAtomIdx]]);
-  CHECK_INVARIANT(bond, "");
-  const auto bi = bond->getIdx();
-  if (!ri->numBondRings(bi)) {
-    return;
-  }
-  if (ri->numBondRings(bi) == 1) {
-    const auto ringIdx = ri->bondMembers(bi).front();
-    ringBondCountVect[ringIdx].isMCSRingBond.set(bi);
-    ringBondCountVect[ringIdx].isMCSRingBondNonFused.set(bi);
-  } else {
-    for (const auto& ringIdx : ri->bondMembers(bi)) {
-      ringBondCountVect[ringIdx].isMCSRingBond.set(bi);
-      ringBondCountVect[ringIdx].isMCSRingBondFused.set(bi);
+// part of MCS.
+// isMCSRingBondFused: bits are set in correspondence of ring bond
+// indices which are part of MCS and are fused.
+// isMCSRingBondNonFused: bits are set in correspondence of ring bond indices
+// which are part of MCS and are not fused.
+class RingBondCountVect {
+ public:
+  RingBondCountVect(const ROMol& mol) :
+    d_mol(mol),
+    d_ringInfo(mol.getRingInfo()) {
+    d_ringBondCountVect.resize(d_ringInfo->numRings());
+    d_isMCSBond.resize(mol.getNumBonds());
+    for (auto& ringBondCount : d_ringBondCountVect) {
+      ringBondCount.isMCSRingBond.resize(mol.getNumBonds());
+      ringBondCount.isMCSRingBondNonFused.resize(mol.getNumBonds());
+      ringBondCount.isMCSRingBondFused.resize(mol.getNumBonds());
+    }
+  }
+  // In the 1st pass, we set fused/non-fused bits simply based on
+  // numBondRings.
+  void setMCSBondBitsPass1(unsigned int beginAtomIdx, unsigned int endAtomIdx,
+                          const std::uint32_t c[],
+                          const FMCS::Graph& graph) {
+    const auto bond =
+        d_mol.getBondBetweenAtoms(graph[c[beginAtomIdx]], graph[c[endAtomIdx]]);
+    CHECK_INVARIANT(bond, "");
+    const auto bi = bond->getIdx();
+    d_isMCSBond.set(bi);
+    if (!d_ringInfo->numBondRings(bi)) {
+      return;
+    }
+    if (d_ringInfo->numBondRings(bi) == 1) {
+      const auto ringIdx = d_ringInfo->bondMembers(bi).front();
+      d_ringBondCountVect[ringIdx].isMCSRingBond.set(bi);
+      d_ringBondCountVect[ringIdx].isMCSRingBondNonFused.set(bi);
+    } else {
+      for (const auto& ringIdx : d_ringInfo->bondMembers(bi)) {
+        d_ringBondCountVect[ringIdx].isMCSRingBond.set(bi);
+        d_ringBondCountVect[ringIdx].isMCSRingBondFused.set(bi);
+      }
     }
   }
-}
-
-void setMCSBondBitsPass2(const ROMol& mol,
-                         std::vector<BondCount>& ringBondCountVect) {
-  const auto ri = mol.getRingInfo();
-  for (unsigned int bi = 0; bi < mol.getNumBonds(); ++bi) {
-    int fusedBondRingIdx = -1;
-    unsigned int fusedBondCount = 0;
-    for (auto& ringBondCount : ringBondCountVect) {
-      auto ringIdx = &ringBondCount - &ringBondCountVect.front();
-      if (ringBondCount.isMCSRingBondFused.test(bi)) {
-        if (ringBondCount.isMCSRingBondNonFused.count() == 0 &&
-            ringBondCount.isMCSRingBondFused.count() <
-                ri->bondRings().at(ringIdx).size()) {
+  // In the 2nd pass, we refine the above as certain bonds originally
+  // marked as fused can be relabelled as non-fused
+  void setMCSBondBitsPass2() {
+    for (auto& ringBondCount : d_ringBondCountVect) {
+      ringBondCount.nonFusedCountPass1 = ringBondCount.isMCSRingBondNonFused.count();
+      ringBondCount.fusedCountPass1 = ringBondCount.isMCSRingBondFused.count();
+    }
+    for (unsigned int bi = 0; bi < d_mol.getNumBonds(); ++bi) {
+      if (!d_isMCSBond.test(bi)) {
+        continue;
+      }
+      int fusedBondRingIdx = -1;
+      unsigned int fusedBondCount = 0;
+      for (auto& ringBondCount : d_ringBondCountVect) {
+        if (!ringBondCount.isMCSRingBondFused.test(bi)) {
+          continue;
+        }
+        auto ringIdx = &ringBondCount - &d_ringBondCountVect.front();
+        if (ringBondCount.nonFusedCountPass1 == 0 &&
+            ringBondCount.fusedCountPass1 <
+                d_ringInfo->bondRings().at(ringIdx).size()) {
           ringBondCount.isMCSRingBondFused.set(bi, false);
         } else {
           ++fusedBondCount;
           fusedBondRingIdx = ringIdx;
         }
       }
-    }
-    if (fusedBondCount == 1) {
-      ringBondCountVect[fusedBondRingIdx].isMCSRingBondNonFused.set(bi);
-      ringBondCountVect[fusedBondRingIdx].isMCSRingBondFused.set(bi, false);
+      if (fusedBondCount == 1) {
+        d_ringBondCountVect[fusedBondRingIdx].isMCSRingBondNonFused.set(bi);
+        d_ringBondCountVect[fusedBondRingIdx].isMCSRingBondFused.set(bi, false);
+      }
     }
   }
-}
-
-bool isRingFusionHonored(const ROMol& mol,
-                         const std::vector<BondCount>& ringBondCountVect) {
-  const auto ri = mol.getRingInfo();
-  for (const auto& ringBondCount : ringBondCountVect) {
-    unsigned int ringIdx = &ringBondCount - &ringBondCountVect.front();
-    const auto& bondRings = ri->bondRings().at(ringIdx);
-    // if all or no bonds of this ring are part of MCS, no need to do further
-    // checks
-    const auto numRingBondsInMCS = ringBondCount.isMCSRingBond.count();
-    if (!numRingBondsInMCS || numRingBondsInMCS == bondRings.size()) {
-      continue;
-    }
-    // check ring bonds:
-    // if they are non-fused, it's OK
-    // if they are fused but classified as non-fused, it's OK
-    // otherwise count a missing fused bond
-    // if the sum of missing fused bonds + fused bonds in MCS + non-fused bonds
-    // in MCS equals to the ring size, then we have failed the check
-    const auto numNonFusedRingBondsInMCS =
-        ringBondCount.isMCSRingBondNonFused.count();
-    const auto numFusedRingBondsInMCS =
-        ringBondCount.isMCSRingBondFused.count();
-    const auto numMissingFusedBonds =
-        std::count_if(bondRings.begin(), bondRings.end(),
-                      [ri, &ringBondCount](const auto bi) {
-                        return (ri->numBondRings(bi) > 1 &&
-                                !ringBondCount.isMCSRingBondNonFused.test(bi) &&
-                                !ringBondCount.isMCSRingBondFused.test(bi));
-                      });
-    if (numMissingFusedBonds + numFusedRingBondsInMCS +
-            numNonFusedRingBondsInMCS ==
-        bondRings.size()) {
-      return false;
+  bool isRingFusionHonored() {
+    for (const auto& ringBondCount : d_ringBondCountVect) {
+      unsigned int ringIdx = &ringBondCount - &d_ringBondCountVect.front();
+      const auto& bondRings = d_ringInfo->bondRings().at(ringIdx);
+      // if all or no bonds of this ring are part of MCS, no need to do further
+      // checks
+      const auto numRingBondsInMCS = ringBondCount.isMCSRingBond.count();
+      if (!numRingBondsInMCS || numRingBondsInMCS == bondRings.size()) {
+        continue;
+      }
+      // check ring bonds:
+      // if they are non-fused, it's OK
+      // if they are fused but classified as non-fused, it's OK
+      // otherwise count a missing fused bond
+      // if the sum of missing fused bonds + fused bonds in MCS + non-fused bonds
+      // in MCS equals to the ring size, then we have failed the check
+      const auto numNonFusedRingBondsInMCS =
+          ringBondCount.isMCSRingBondNonFused.count();
+      const auto numFusedRingBondsInMCS =
+          ringBondCount.isMCSRingBondFused.count();
+      const auto numMissingFusedBonds =
+          std::count_if(bondRings.begin(), bondRings.end(),
+                        [this, &ringBondCount](const auto bi) {
+                          return (d_ringInfo->numBondRings(bi) > 1 &&
+                                  !ringBondCount.isMCSRingBondNonFused.test(bi) &&
+                                  !ringBondCount.isMCSRingBondFused.test(bi));
+                        });
+      if (numMissingFusedBonds + numFusedRingBondsInMCS +
+              numNonFusedRingBondsInMCS ==
+          bondRings.size()) {
+        return false;
+      }
     }
+    return true;
   }
-  return true;
-}
+ private:
+  struct BondCount {
+    boost::dynamic_bitset<> isMCSRingBond;
+    boost::dynamic_bitset<> isMCSRingBondNonFused;
+    unsigned int nonFusedCountPass1 = 0;
+    boost::dynamic_bitset<> isMCSRingBondFused;
+    unsigned int fusedCountPass1 = 0;
+  };
+  const ROMol& d_mol;
+  const RingInfo *d_ringInfo;
+  std::vector<BondCount> d_ringBondCountVect;
+  boost::dynamic_bitset<> d_isMCSBond;
+};
 }  // end of anonymous namespace
 
 inline bool ringFusionCheck(const std::uint32_t c1[], const std::uint32_t c2[],
@@ -631,31 +645,29 @@ inline bool ringFusionCheck(const std::uint32_t c1[], const std::uint32_t c2[],
   there is no missing fused bond. This is OK for permissive mode.
   In strict mode, we also need to check against 1-methylbicyclo[3.1.0]hexane,
   where there is indeed a missing fused bond.
-  Basically, in permissive mode one of two pairs is allowed to fail the
-  match. In strict mode, none is.
+  Basically, in permissive mode one of two molecules is allowed to fail the
+  match, but not both. In strict mode, none is.
   */
   bool res = true;
   if (boost::num_edges(target) < boost::num_edges(query)) {
     return true;
   }
-  auto mol1RingBondCountVect = initRingBondCountVect(mol1);
-  auto mol2RingBondCountVect = initRingBondCountVect(mol2);
+  RingBondCountVect mol1RingBondCountVect(mol1);
+  RingBondCountVect mol2RingBondCountVect(mol2);
   auto queryEdges = boost::edges(query);
   std::for_each(
       queryEdges.first, queryEdges.second,
-      [&c1, &c2, &mol1, &query, &mol2, &target, &mol1RingBondCountVect,
+      [&c1, &c2, &query, &target, &mol1RingBondCountVect,
        &mol2RingBondCountVect](const auto& edge) {
         const auto beginAtomIdx = boost::source(edge, query);
         const auto endAtomIdx = boost::target(edge, query);
-        setMCSBondBitsPass1(beginAtomIdx, endAtomIdx, mol1, c1, query,
-                            mol1RingBondCountVect);
-        setMCSBondBitsPass1(beginAtomIdx, endAtomIdx, mol2, c2, target,
-                            mol2RingBondCountVect);
+        mol1RingBondCountVect.setMCSBondBitsPass1(beginAtomIdx, endAtomIdx, c1, query);
+        mol2RingBondCountVect.setMCSBondBitsPass1(beginAtomIdx, endAtomIdx, c2, target);
       });
-  setMCSBondBitsPass2(mol1, mol1RingBondCountVect);
-  setMCSBondBitsPass2(mol2, mol2RingBondCountVect);
-  bool mol1Honored = isRingFusionHonored(mol1, mol1RingBondCountVect);
-  bool mol2Honored = isRingFusionHonored(mol2, mol2RingBondCountVect);
+  mol1RingBondCountVect.setMCSBondBitsPass2();
+  mol2RingBondCountVect.setMCSBondBitsPass2();
+  bool mol1Honored = mol1RingBondCountVect.isRingFusionHonored();
+  bool mol2Honored = mol2RingBondCountVect.isRingFusionHonored();
   if (p.BondCompareParameters.MatchFusedRingsStrict) {
     res = mol1Honored && mol2Honored;
   } else {
diff --git a/Code/GraphMol/FMCS/MaximumCommonSubgraph.cpp b/Code/GraphMol/FMCS/MaximumCommonSubgraph.cpp
index 6bede444152..6976e880602 100644
--- a/Code/GraphMol/FMCS/MaximumCommonSubgraph.cpp
+++ b/Code/GraphMol/FMCS/MaximumCommonSubgraph.cpp
@@ -446,17 +446,18 @@ bool checkIfRingsAreClosed(const Seed& fs, bool noLoneRingAtoms) {
   if (!ri->numRings()) {
     return true;
   }
-  boost::dynamic_bitset<> fragmentBonds(om.getNumBonds());
-  boost::dynamic_bitset<> fragmentRings(ri->numRings());
+  boost::dynamic_bitset<> mcsBonds(om.getNumBonds());
+  boost::dynamic_bitset<> mcsNonFusedRings(ri->numRings());
+  boost::dynamic_bitset<> mcsFusedRings(ri->numRings());
   for (const auto& bond : fs.MoleculeFragment.Bonds) {
     auto bi = bond->getIdx();
-    fragmentBonds.set(bi);
+    mcsBonds.set(bi);
     if (ri->numBondRings(bi) == 1) {
-      fragmentRings.set(ri->bondMembers(bi).front());
+      mcsNonFusedRings.set(ri->bondMembers(bi).front());
     }
   }
-  for (unsigned int ringIdx = 0; ringIdx < fragmentRings.size(); ++ringIdx) {
-    if (!fragmentRings.test(ringIdx)) {
+  for (unsigned int ringIdx = 0; ringIdx < mcsNonFusedRings.size(); ++ringIdx) {
+    if (!mcsNonFusedRings.test(ringIdx)) {
       continue;
     }
     for (const auto& bi : ri->bondRings().at(ringIdx)) {
@@ -464,12 +465,12 @@ bool checkIfRingsAreClosed(const Seed& fs, bool noLoneRingAtoms) {
       for (unsigned int memberOf : ri->bondMembers(bi)) {
         if (memberOf == ringIdx) {
           keepBond = true;
-        } else if (fragmentRings.test(memberOf)) {
+        } else if (mcsNonFusedRings.test(memberOf)) {
           keepBond = false;
           break;
         }
       }
-      if (keepBond && !fragmentBonds.test(bi)) {
+      if (keepBond && !mcsBonds.test(bi)) {
         return false;
       }
     }
@@ -477,13 +478,40 @@ bool checkIfRingsAreClosed(const Seed& fs, bool noLoneRingAtoms) {
   if (noLoneRingAtoms) {
     for (const auto& atom : fs.MoleculeFragment.Atoms) {
       auto ai = atom->getIdx();
-      for (const auto& memberOf : ri->atomMembers(ai)) {
-        if (!fragmentRings.test(memberOf)) {
-          return false;
+      const auto& ringIndices = ri->atomMembers(ai);
+      if (!ringIndices.empty() &&
+          !std::any_of(ringIndices.begin(), ringIndices.end(),
+                       [&mcsNonFusedRings](const auto& ringIdx) {
+                         return mcsNonFusedRings.test(ringIdx);
+                       })) {
+        return false;
+      }
+    }
+  }
+  if (mcsNonFusedRings.none()) {
+    for (const auto& bond : fs.MoleculeFragment.Bonds) {
+      auto bi = bond->getIdx();
+      if (ri->numBondRings(bi) > 1) {
+        for (auto ringIdx : ri->bondMembers(bi)) {
+          mcsFusedRings.set(ringIdx);
         }
       }
     }
   }
+  if (mcsFusedRings.any()) {
+    for (unsigned int ringIdx = 0; ringIdx < mcsFusedRings.size(); ++ringIdx) {
+      if (!mcsFusedRings.test(ringIdx)) {
+        continue;
+      }
+      const auto &ringBondIndices = ri->bondRings().at(ringIdx);
+      if (std::all_of(ringBondIndices.begin(), ringBondIndices.end(), [&mcsBonds](const auto &bi) {
+        return mcsBonds.test(bi);
+      })) {
+        return true;
+      }
+    }
+    return false;
+  }
   return true;
 }
 
@@ -725,7 +753,30 @@ MaximumCommonSubgraph::generateResultSMARTSAndQueryMol(
   auto mol = new RWMol();
   ROMOL_SPTR molSptr(mol);
   const auto ri = mcsIdx.QueryMolecule->getRingInfo();
+  boost::dynamic_bitset<> mcsRingIsComplete;
+  bool needAtomRingQueries =
+      (Parameters.AtomCompareParameters.RingMatchesRingOnly ||
+       Parameters.BondCompareParameters.MatchFusedRingsStrict);
+  if (needAtomRingQueries) {
+    mcsRingIsComplete.resize(ri->numRings(), true);
+    boost::dynamic_bitset<> queryBondInMcs(mcsIdx.QueryMolecule->getNumBonds());
+    for (const auto& bond : mcsIdx.Bonds) {
+      queryBondInMcs.set(bond->getIdx());
+    }
+    const auto& bondRings = ri->bondRings();
+    for (const auto& bondRing : bondRings) {
+      auto ringIdx = &bondRing - &bondRings.front();
+      for (const auto& bondIdx : bondRing) {
+        if (!queryBondInMcs.test(bondIdx)) {
+          mcsRingIsComplete.reset(ringIdx);
+          break;
+        }
+      }
+    }
+  }
   for (const auto& atom : mcsIdx.Atoms) {
+    auto queryAtomIdx = atom->getIdx();
+    auto numAtomRings = ri->numAtomRings(queryAtomIdx);
     QueryAtom a;
     const auto ai = &atom - &mcsIdx.Atoms.front();
     if (Parameters.AtomTyper == MCSAtomCompareIsotopes ||
@@ -747,30 +798,29 @@ MaximumCommonSubgraph::generateResultSMARTSAndQueryMol(
         }
       }
     }
-    if (Parameters.AtomCompareParameters.RingMatchesRingOnly ||
-        Parameters.BondCompareParameters.MatchFusedRingsStrict) {
-      const auto& targets = mcsIdx.Targets;
-      const auto numAtomRings = ri->numAtomRings(atom->getIdx());
-      const auto isAtomFusedAcrossAllTargets =
-          [ai, numAtomRings, &targets, &atomMatchResult](unsigned int itarget) {
-            const auto& tag = targets.at(itarget);
-            const auto tagRingInfo = tag.Molecule->getRingInfo();
-            const auto ti = atomMatchResult.at(itarget).at(ai).TargetAtomIdx;
-            return tagRingInfo->numAtomRings(ti) == numAtomRings;
-          };
-      if (Parameters.AtomCompareParameters.RingMatchesRingOnly ||
-          (Parameters.BondCompareParameters.MatchFusedRingsStrict &&
-           numAtomRings > 1 &&
-           std::all_of(matchedTargetIndices.begin(), matchedTargetIndices.end(),
-                       isAtomFusedAcrossAllTargets))) {
-        QueryAtom::QUERYATOM_QUERY* q = nullptr;
-        if (Parameters.BondCompareParameters.MatchFusedRingsStrict &&
-            numAtomRings > 1) {
-          q = makeAtomInNRingsQuery(numAtomRings);
-        } else {
-          q = makeAtomInRingQuery();
-          q->setNegation(!numAtomRings);
-        }
+    if (needAtomRingQueries) {
+      const auto& ringIndicesAtomIsMemberOf = ri->atomMembers(queryAtomIdx);
+      auto numCompleteRings = std::count_if(
+          ringIndicesAtomIsMemberOf.begin(), ringIndicesAtomIsMemberOf.end(),
+          [&mcsRingIsComplete](const auto& ringIdx) {
+            return mcsRingIsComplete.test(ringIdx);
+          });
+      if (Parameters.AtomCompareParameters.RingMatchesRingOnly &&
+          !numCompleteRings) {
+        auto q = makeAtomInRingQuery();
+        q->setNegation(!numAtomRings);
+        a.expandQuery(q, Queries::COMPOSITE_AND, true);
+      } else if (Parameters.BondCompareParameters.MatchFusedRingsStrict &&
+                 numAtomRings == 1 && numCompleteRings == 1) {
+        auto ringSize =
+            ri->atomRings().at(ringIndicesAtomIsMemberOf.front()).size();
+        auto q = new ATOM_OR_QUERY;
+        q->setDescription("AtomOr");
+        q->addChild(QueryAtom::QUERYATOM_QUERY::CHILD_TYPE(
+            makeAtomMinRingSizeQuery(ringSize)));
+        auto q2 = makeAtomInNRingsQuery(1);
+        q2->setNegation(true);
+        q->addChild(QueryAtom::QUERYATOM_QUERY::CHILD_TYPE(q2));
         a.expandQuery(q, Queries::COMPOSITE_AND, true);
       }
     }
diff --git a/Code/GraphMol/MolDraw2D/DrawMolMCHLasso.cpp b/Code/GraphMol/MolDraw2D/DrawMolMCHLasso.cpp
index b3de0ee534c..fb650ea7654 100644
--- a/Code/GraphMol/MolDraw2D/DrawMolMCHLasso.cpp
+++ b/Code/GraphMol/MolDraw2D/DrawMolMCHLasso.cpp
@@ -61,14 +61,23 @@ void DrawMolMCHLasso::extractAtomColourLists(
     std::vector<DrawColour> &colours,
     std::vector<std::vector<int>> &colourAtoms,
     std::vector<std::vector<int>> &colourLists) const {
+  std::vector<boost::dynamic_bitset<>> inColourAtoms;
   for (const auto &cm : mcHighlightAtomMap_) {
     for (const auto &col : cm.second) {
       auto cpos = std::find(colours.begin(), colours.end(), col);
       if (cpos == colours.end()) {
         colours.push_back(col);
         colourAtoms.push_back(std::vector<int>(1, cm.first));
+        inColourAtoms.push_back(
+            boost::dynamic_bitset<>(drawMol_->getNumAtoms()));
+        inColourAtoms.back().set(cm.first);
       } else {
-        colourAtoms[std::distance(colours.begin(), cpos)].push_back(cm.first);
+        auto ln = std::distance(colours.begin(), cpos);
+        // it's important that each atom is only in the list once - Github6749
+        if (!inColourAtoms[ln][cm.first]) {
+          colourAtoms[ln].push_back(cm.first);
+          inColourAtoms[ln].set(cm.first);
+        }
       }
     }
   }
@@ -137,6 +146,7 @@ void DrawMolMCHLasso::drawLasso(size_t lassoNum, const RDKit::DrawColour &col,
   fixIntersectingLines(arcs, lines);
   fixIntersectingArcsAndLines(arcs, lines);
   fixProtrudingLines(lines);
+  fixOrphanLines(arcs, lines);
 
   for (auto &it : arcs) {
     highlights_.push_back(std::move(it));
@@ -146,23 +156,40 @@ void DrawMolMCHLasso::drawLasso(size_t lassoNum, const RDKit::DrawColour &col,
   }
 }
 
+namespace {
+double getLassoWidth(const DrawMolMCH *dm, int atNum, int lassoNum) {
+  PRECONDITION(dm, "Needs valid DrawMolMCH")
+  double xrad, yrad;
+  dm->getAtomRadius(atNum, xrad, yrad);
+  // Double the area of the circles for successive lassos.
+  const static double rats[] = {1.0, 1.414, 2, 2.828, 4};
+  if (lassoNum > 4) {
+    // It's going to look horrible, probably, but it's a lot of lassos.
+    return xrad * (1 + lassoNum) * 0.75;
+  } else {
+    return xrad * rats[lassoNum];
+  }
+}
+}  // namespace
+
 // ****************************************************************************
 void DrawMolMCHLasso::extractAtomArcs(
     size_t lassoNum, const RDKit::DrawColour &col,
     const std::vector<int> &colAtoms,
     std::vector<std::unique_ptr<DrawShapeArc>> &arcs) const {
-  double xradius, yradius;
+  // an empirically derived lineWidth.
+  int lineWidth = 3;
+  bool scaleLineWidth = true;
   for (auto ca : colAtoms) {
     if (ca < 0 || static_cast<unsigned int>(ca) >= drawMol_->getNumAtoms()) {
       // there's an error in the colour map
       continue;
     }
-    getAtomRadius(ca, xradius, yradius);
-    xradius += xradius * lassoNum * 0.5;
-    Point2D radii(xradius, xradius);
+    double lassoWidth = getLassoWidth(this, ca, lassoNum);
+    Point2D radii(lassoWidth, lassoWidth);
     std::vector<Point2D> pts{atCds_[ca], radii};
-    DrawShapeArc *ell = new DrawShapeArc(
-        pts, 0.0, 360.0, drawOptions_.bondLineWidth, true, col, false, ca);
+    DrawShapeArc *ell = new DrawShapeArc(pts, 0.0, 360.0, lineWidth,
+                                         scaleLineWidth, col, false, ca);
     arcs.emplace_back(ell);
   }
 }
@@ -172,6 +199,8 @@ void DrawMolMCHLasso::extractBondLines(
     size_t lassoNum, const RDKit::DrawColour &col,
     const std::vector<int> &colAtoms,
     std::vector<std::unique_ptr<DrawShapeSimpleLine>> &lines) const {
+  int lineWidth = 3;
+  bool scaleLineWidth = true;
   if (colAtoms.size() > 1) {
     for (size_t i = 0U; i < colAtoms.size() - 1; ++i) {
       if (colAtoms[i] < 0 ||
@@ -179,20 +208,16 @@ void DrawMolMCHLasso::extractBondLines(
         // there's an error in the colour map.
         continue;
       }
-      double xradiusI, yradiusI;
-      getAtomRadius(colAtoms[i], xradiusI, yradiusI);
-      xradiusI += xradiusI * lassoNum * 0.5;
-      auto dispI = xradiusI * 0.75;
+      auto lassoWidthI = getLassoWidth(this, colAtoms[i], lassoNum);
+      auto dispI = lassoWidthI * 0.75;
       for (size_t j = i + 1; j < colAtoms.size(); ++j) {
         if (colAtoms[j] < 0 ||
             static_cast<unsigned int>(colAtoms[j]) >= drawMol_->getNumAtoms()) {
           // there's an error in the colour map.
           continue;
         }
-        double xradiusJ, yradiusJ;
-        getAtomRadius(colAtoms[j], xradiusJ, yradiusJ);
-        xradiusJ += xradiusJ * lassoNum * 0.5;
-        auto dispJ = xradiusJ * 0.75;
+        auto lassoWidthJ = getLassoWidth(this, colAtoms[j], lassoNum);
+        auto dispJ = lassoWidthJ * 0.75;
         auto bond = drawMol_->getBondBetweenAtoms(colAtoms[i], colAtoms[j]);
         if (bond) {
           if (!mcHighlightBondMap_.empty()) {
@@ -213,14 +238,13 @@ void DrawMolMCHLasso::extractBondLines(
             // less than the radii of the circles (just less, so that they still
             // intersect rather than hitting on the tangent)
             auto mid = (p1 + p2) / 2.0;
-            if ((atCdsI - mid).lengthSq() < xradiusI * xradiusI) {
-              p1 = atCdsI + perp * xradiusI * 0.99 * m;
-              p2 = atCdsJ + perp * xradiusJ * 0.99 * m;
+            if ((atCdsI - mid).lengthSq() < lassoWidthI * lassoWidthI) {
+              p1 = atCdsI + perp * lassoWidthI * 0.99 * m;
+              p2 = atCdsJ + perp * lassoWidthJ * 0.99 * m;
             }
             DrawShapeSimpleLine *pl = new DrawShapeSimpleLine(
-                {p1, p2}, drawOptions_.bondLineWidth,
-                drawOptions_.scaleBondWidth, col, colAtoms[i], colAtoms[j],
-                bond->getIdx(), noDash);
+                {p1, p2}, lineWidth, scaleLineWidth, col, colAtoms[i],
+                colAtoms[j], bond->getIdx(), noDash);
             lines.emplace_back(pl);
           }
         }
@@ -488,7 +512,8 @@ void DrawMolMCHLasso::fixIntersectingArcsAndLines(
 void DrawMolMCHLasso::fixProtrudingLines(
     std::vector<std::unique_ptr<DrawShapeSimpleLine>> &lines) const {
   // lasso_highlight_7.svg also had the problem where two lines didn't
-  // intersect, but one protruded beyond the end of the other inside the lasso.
+  // intersect, but one protruded beyond the end of the other inside the
+  // lasso.
   for (auto &line1 : lines) {
     for (auto &line2 : lines) {
       auto d1_0 = (line1->points_[0] - line2->points_[0]).length();
@@ -514,5 +539,88 @@ void DrawMolMCHLasso::fixProtrudingLines(
     }
   }
 }
+
+namespace {
+std::pair<Point2D, Point2D> getArcEnds(const DrawShapeArc &arc) {
+  std::pair<Point2D, Point2D> retVal;
+  // for these purposes, it's always a circle, so just use the x
+  // radius
+  retVal.first.x =
+      arc.points_[0].x + arc.points_[1].x * cos(arc.ang1_ * M_PI / 180.0);
+  retVal.first.y =
+      arc.points_[0].y + arc.points_[1].x * sin(arc.ang1_ * M_PI / 180.0);
+  retVal.second.x =
+      arc.points_[0].x + arc.points_[1].x * cos(arc.ang2_ * M_PI / 180.0);
+  retVal.second.y =
+      arc.points_[0].y + arc.points_[1].x * sin(arc.ang2_ * M_PI / 180.0);
+  return retVal;
+}
+}  // namespace
+void DrawMolMCHLasso::fixOrphanLines(
+    std::vector<std::unique_ptr<DrawShapeArc>> &arcs,
+    std::vector<std::unique_ptr<DrawShapeSimpleLine>> &lines) {
+  // lasso_highlights_7.svg had a line close to an arc at
+  // one end, but not at the other.  Such lines are clearly
+  // artifacts that need to be removed.  Takes out all lines
+  // that aren't within a tolerance of something at both ends.
+  std::vector<std::pair<Point2D, Point2D>> arcEnds;
+  for (const auto &arc : arcs) {
+    arcEnds.push_back(getArcEnds(*arc));
+  }
+  // This tolerance was arrived at empirically.  It's enough
+  // to fix lasso_highlights_7.svg without breaking it and
+  // lasso_highlights_6.svg.  A slightly tighter tolerance
+  // (e.g. 0.003) causes green lines associated with bonds 9
+  // and 12 to be removed incorrectly in both of these.
+  static const double tol = 0.005;
+  for (auto &line1 : lines) {
+    bool attached0 = false, attached1 = false;
+    for (const auto &arcEnd : arcEnds) {
+      if ((line1->points_[0] - arcEnd.first).lengthSq() < tol) {
+        attached0 = true;
+      }
+      if ((line1->points_[1] - arcEnd.first).lengthSq() < tol) {
+        attached1 = true;
+      }
+      if ((line1->points_[0] - arcEnd.second).lengthSq() < tol) {
+        attached0 = true;
+      }
+      if ((line1->points_[1] - arcEnd.second).lengthSq() < tol) {
+        attached1 = true;
+      }
+    }
+    if (!attached0 || !attached1) {
+      for (auto &line2 : lines) {
+        if (line1 == line2 || !line1 || !line2) {
+          continue;
+        }
+        if ((line1->points_[0] - line2->points_[0]).lengthSq() < tol) {
+          attached0 = true;
+        }
+        if ((line1->points_[0] - line2->points_[1]).lengthSq() < tol) {
+          attached0 = true;
+        }
+        if ((line1->points_[1] - line2->points_[0]).lengthSq() < tol) {
+          attached1 = true;
+        }
+        if ((line1->points_[1] - line2->points_[1]).lengthSq() < tol) {
+          attached1 = true;
+        }
+        if (attached0 && attached1) {
+          break;
+        }
+      }
+    }
+    if (!attached0 || !attached1) {
+      line1.reset();
+    }
+  }
+  lines.erase(std::remove_if(
+                  lines.begin(), lines.end(),
+                  [](const std::unique_ptr<DrawShapeSimpleLine> &line) -> bool {
+                    return !line;
+                  }),
+              lines.end());
+}
 }  // namespace MolDraw2D_detail
 }  // namespace RDKit
diff --git a/Code/GraphMol/MolDraw2D/DrawMolMCHLasso.h b/Code/GraphMol/MolDraw2D/DrawMolMCHLasso.h
index 55bad32b1a7..69aa135f3c6 100644
--- a/Code/GraphMol/MolDraw2D/DrawMolMCHLasso.h
+++ b/Code/GraphMol/MolDraw2D/DrawMolMCHLasso.h
@@ -84,6 +84,10 @@ class DrawMolMCHLasso : public DrawMolMCH {
       std::vector<std::unique_ptr<DrawShapeSimpleLine>> &lines) const;
   void fixProtrudingLines(
       std::vector<std::unique_ptr<DrawShapeSimpleLine>> &lines) const;
+  // Orphan lines are ones where at least one end isn't close to the end
+  // of any line or arc.
+  void fixOrphanLines(std::vector<std::unique_ptr<DrawShapeArc>> &arcs,
+                      std::vector<std::unique_ptr<DrawShapeSimpleLine>> &lines);
 };
 }  // namespace MolDraw2D_detail
 }  // namespace RDKit
diff --git a/Docs/Book/BackwardsIncompatibleChanges.md b/Docs/Book/BackwardsIncompatibleChanges.md
index 952a37c61f6..17defc8ef51 100644
--- a/Docs/Book/BackwardsIncompatibleChanges.md
+++ b/Docs/Book/BackwardsIncompatibleChanges.md
@@ -8,6 +8,51 @@ generally include changes in results that arise due to bug fixes; we try to call
 those out in the release notes. The release notes, in general, contain a more
 comprehensive version of this list.
 
+## Release 2023.09
+
+### Changes in atomic stereochemistry perception
+
+The algorithm for perception of atomic stereochemistry from 2D structures has
+been rewritten. The new algorithm is more accurate, which results in some
+differences in perceived stereo between this release and the previous ones.
+
+### Changes in the MCS code
+
+- The user-configurable `MCSParameters::FinalMatchChecker` function is now
+called after the built-in `FinalMatchChecker` function, rather as
+alternatively to the built-in `FinalMatchChecker` function. This was a
+design flaw which is worth correcting.
+- Setting `MCSParameters::Timeout` to 0 means no timeout, rather than 0s
+timeout, which is rather pointless as it would cause MCS to be canceled
+immediately.
+- Result SMARTS strings generated by `FindMCS` when
+`MCSParameters::MatchRingFusionStrict` is `true` now include ring membership
+queries where appropriate in order to ensure more specific substructure
+matches.
+- In MCS Verbose statistics, `SingleBondExcluded` was renamed to
+`IndividualBondExcluded` to avoid confusion, since single bond has a
+different meaning in chemistry.
+
+### Changes to canonicalization
+
+Information about stereo groups is no longer used in the SMILES canonicalization
+process if CXSMILES are not being generated.
+
+
+## Release 2023.03
+
+### Changes to ring finding
+
+- The ring-finding functions will now run even if the molecule already has ring information. Older versions of the RDKit would return whatever ring information was present, even if it had been generated using a different algorithm.
+- The ring-finding functions now no longer consider dative bonds as possible ring bonds by default. All of the ring-finding functions have a new optional argument `includeDativeBonds` which can be used to change this behavior
+
+### Changes to canonicalization
+
+The canonical SMILES and CXSMILES generated for molecules with enhanced stereochemistry (stereo groups) is different than in previous releases. The enhanced stereochemistry information and the stereo groups themselves are now canonical. This does *not* affect molecules which do not have enhanced stereo and will not have any effect if you generate non-isomeric SMILES. This change also affects the output of the MolHash and RegistrationHash code when applied to molecules with enhanced stereo.
+
+
+
+
 ## Release 2022.09
 
 ### Changes in canonicalization
diff --git a/Docs/Book/GettingStartedInPython.rst b/Docs/Book/GettingStartedInPython.rst
index 3b439c2c44b..e930d3fc19c 100644
--- a/Docs/Book/GettingStartedInPython.rst
+++ b/Docs/Book/GettingStartedInPython.rst
@@ -1553,7 +1553,7 @@ requirement.
   >>> rdFMCS.FindMCS(mols).smartsString
   '[#6]1-[#6]-[#6](-[#6]-1-[#6])-[#6]'
   >>> rdFMCS.FindMCS(mols, ringMatchesRingOnly=True).smartsString
-  '[#6&R]1-&@[#6&R]-&@[#6&R](-&@[#6&R]-&@1)-&@[#6&R]'
+  '[#6]1-&@[#6]-&@[#6](-&@[#6]-&@1)-&@[#6&R]'
   >>> rdFMCS.FindMCS(mols, completeRingsOnly=True).smartsString
   '[#6]1-&@[#6]-&@[#6]-&@[#6]-&@1'
 
diff --git a/ReleaseNotes.md b/ReleaseNotes.md
index bd95bafbd5f..5a1708c521c 100644
--- a/ReleaseNotes.md
+++ b/ReleaseNotes.md
@@ -2,8 +2,27 @@
 (Changes relative to Release_2023.03.1)
 
 ## Acknowledgements
+(Note: I'm no longer attempting to manually curate names. If you would like to
+see your contribution acknowledged with your name, please set your name in
+GitHub)
+
+Jason Biggs, Jonathan Bisson, David Cosgrove, Andrew Dalke, Christian W.
+Feldmann, Eloy Félix, Richard Gowers, Tadd Hurst, Gareth Jones, Eisuke
+Kawashima, Brian Kelley, Joos Kiener, Juuso Lehtivarjo, John Mayfield, Vedran
+Miletić, Jeremy Monat, Dan Nealschneider, Timothy Ngotiaoco, Axel Pahl, Rachael
+Pirie, Ricardo Rodriguez-Schmidt, Ernst-Georg Schmid, Paolo Tosco, Ivan
+Tubert-Brohman, Riccardo Vianello, Rachel Walker, Maciej Wójcikowski, pierred5,
+lhyuen, paconius, BartlomiejF, thomp-j, wangyingxie, teltim, Meteor-han,
+abefrandsen, 
 
 ## Highlights
+- The new RascalMCES code adds a very fast maximum common substructure
+  implementation for pairs of molecules.
+- The RDKit core now supports "generalized substructure searching", making it
+  easier to take link nodes, variable attachment points, and tautomer queries
+  into account when doing substructure searches. This is now also supported in
+  the PostgreSQL cartridge.
+- The RDKit now has support for reading and writing MRV files.
 
 ## Backwards incompatible changes
 - The CDXML parser now returns mols with reasonable coordinates and in
@@ -43,13 +62,267 @@ cast", they now need to be matched with the regex `[B,b]ad any[\ ,_]cast`
 - The functions for determining connectivity in DetermineBonds now use a more
 efficient method by default. To go back to the old behavior, set the useVdw argument
 to True.
+- The algorithm for perception of atomic stereochemistry from 2D structures has
+been rewritten. The new algorithm is more accurate, which results in some
+differences in perceived stereo between this release and the previous ones.
+- Information about stereo groups is no longer used in the SMILES
+canonicalization process if CXSMILES are not being generated.
+
+## New Features and Enhancements:
+  - Mols matrix to grid image
+ (github pull #6080 from bertiewooster)
+  - Reduce space overhead of enabling the inclusion of properties when serializing molecules
+ (github issue #6312 from rvianello)
+  - Add optional sortsupport methods to the PostgreSQL GiST indices
+ (github pull #6313 from rvianello)
+  - Add a new parameter to mol_adjust_query_properties for generic query parameters
+ (github pull #6332 from bjonnh-work)
+  - add DebugDraw() function
+ (github pull #6340 from greglandrum)
+  - Optimize GetPropsFromDict: use tags for conversion
+ (github pull #6355 from bp-kelley)
+  - Fix cleanupOrganometallics and reinstate to sanitisation
+ (github pull #6357 from DavidACosgrove)
+  - postgres cartridge: cleanup a few obsolete build options
+ (github pull #6363 from rvianello)
+  - Fixes some issues in the SubstructLibrary JS implementation
+ (github pull #6385 from ptosco)
+  - Support TautomerQuery and MolBundle queries in the cartridge
+ (github pull #6393 from greglandrum)
+  - JS: Implement in-place aromatisation/kekulisation and avoid undesired exception
+ (github pull #6407 from ptosco)
+  - Optionally expose MCS to JS and extend optional compilation to JSReaction and JSSubstructLibrary
+ (github pull #6409 from ptosco)
+  - Add a method "HasQuery()" to Mol class
+ (github issue #6411 from kienerj)
+  - Enable using JSSubstructLibrary without pattern fps
+ (github pull #6431 from ptosco)
+  - Add support for generalized substructure searching
+ (github pull #6443 from greglandrum)
+  - Add atom and bond property parameters to substruct matching
+ (github pull #6453 from rachelnwalker)
+  - Replace a try..catch block with an if clause
+ (github pull #6488 from ptosco)
+  - Add an in place version of most of the MolStandardize functionality
+ (github pull #6491 from greglandrum)
+  - Exposed partial sanitization options to MinimalLib (both JS and CFFI)
+ (github pull #6519 from ptosco)
+  - Optionally forward Enhanced Stereo Group ids
+ (github pull #6560 from ricrogz)
+  - Add support for dative bonds in MOL files
+ (github pull #6566 from bjonnh-work)
+  - RASCAL MCES
+ (github pull #6568 from DavidACosgrove)
+  - Support additional generic groups
+ (github pull #6570 from bjonnh-work)
+  - Add support for Marvin files
+ (github pull #6575 from bjonnh-work)
+  - Add a "rootedAtAtom" to MolToSmarts
+ (github pull #6581 from ricrogz)
+  - RGD to support tautomers of core
+ (github issue #6609 from jones-gareth)
+  - Fix some build warnings
+ (github pull #6618 from ricrogz)
+  - Exposed log capture functionality to MinimalLib
+ (github pull #6628 from ptosco)
+  - add option to use sequential random seeds in the conformer generator
+ (github pull #6639 from greglandrum)
+  - Major MCS refactoring: new features and bug fixes
+ (github pull #6646 from ptosco)
+  - Lasso highlights
+ (github pull #6653 from DavidACosgrove)
+  - extract continuous lines from the conrec code
+ (github pull #6676 from greglandrum)
+  - Allow some tolerance in flatness determination
+ (github pull #6696 from ricrogz)
+  - Do not trust the 2D/3D tag in ctab mol files
+ (github pull #6697 from ricrogz)
+  - expose the CDXML reaction parsers to python
+ (github pull #6700 from greglandrum)
+  - Add hasQueryHs
+ (github pull #6702 from bp-kelley)
+  - Exporting to mol marks imine bonds EITHERDOUBLE when imine H is implicit
+ (github issue #6703 from ricrogz) 
+  - Use the connect-the-dots algorithm by default in DetermineBonds
+ (github pull #6740 from greglandrum)
+  - Add function to calculate all 3D descriptors
+ (github pull #6741 from RPirie96)
+  - Add SpacialScore
+ (github pull #6742 from apahl)
+  - Extract the core matching logic into a separate function
+ (github pull #6754 from ptosco)
 
 
 ## Bug Fixes:
+  - rdFMCS.FindMCS uses huge amounts of memory for this pair of molecules when CompleteRingsOnly is True
+ (github issue #3965 from i-tub)
+  - PF6- still can not get Bad Conformer Id after the #510 issue fix 
+ (github issue #5145 from wangyingxie)
+  - Order dependence for rdFMCS.FindMCS with MatchFusedRingsStrict
+ (github issue #5411 from pierred5)
+  - Failed FMCS results with certain seed SMARTS and MatchFusedRings* parameters on
+ (github issue #5440 from pierred5)
+  - Seed SMARTS to FindMCS() produces incorrect MCS
+ (github issue #5457 from pierred5)
+  - FindMCS returns wrong result with monoatomic molecules
+ (github issue #5510 from ptosco)
+  - queryMol from FindMCS doesn't match mols used to generate MCS
+ (github issue #6082 from paconius)
+  - Crash when parsing InChI
+ (github issue #6172 from eloyfelix)
+  - Iteration over Python GetAtoms is 10-20x slower than need be
+ (github issue #6208 from d-b-w)
+  - refactor(python): replace deprecated unittest methods
+ (github pull #6304 from e-kwsm)
+  - generateDepictionMatching2DStructure: bonds to R groups should be generic when matching
+ (github pull #6306 from ptosco)
+  - MolToSmiles(canonical=False) creates the wrong _smilesBondOutputOrder property
+ (github issue #6315 from adalke)
+  - MolToMolBlock ignores unspecified information for double bonds in rings
+ (github issue #6316 from mwojcikowski)
+  - bump yaehmop version
+ (github pull #6330 from greglandrum)
+  - rdMolDraw2D.MolDraw2DCairo produces Pre-condition Violation: no draw context when SetColour, DrawRect or DrawLine was called.
+ (github issue #6336 from lhyuen)
+  - Added cstdint include
+ (github pull #6338 from vedranmiletic)
+  - remove the dependency from python distutils in the top CMakeLists.txt file
+ (github pull #6339 from rvianello)
+  - take drawOptions into account when exporting structure to xlsx format
+ (github pull #6341 from ptosco)
+  - Fix swig memory leak
+ (github pull #6346 from jones-gareth)
+  - Add inlines to ForceFieldHelpers header functions
+ (github pull #6356 from JLVarjo)
+  - Bug relating to this PF6- still can not get Bad Conformer Id
+ (github issue #6365 from teltim)
+  - straightenDepiction should not consider 0-degree rotations as multiples of 60
+ (github pull #6367 from ptosco)
+  - expose two missing EmbedFailureCauses tags to python
+ (github pull #6372 from greglandrum)
+  - Molfile Unsaturation Query Not Parsed Correctly
+ (github issue #6395 from timothyngo)
+  - MolDraw2D: chiral tag overlapping atom label
+ (github issue #6397 from greglandrum)
+  - MolDraw2D: increasing padding results in the legend not being displayed
+ (github issue #6400 from greglandrum)
+  - expose some missing CXSmiles flags to python
+ (github pull #6415 from greglandrum)
+  - V3000 structure segfaults when converting to SVG
+ (github issue #6416 from ergo70)
+  - WedgeMolBonds won't wedge/dash a 2nd bond when input already has a wedge/dash around the same chiral atom
+ (github issue #6423 from ricrogz)
+  - MolEnumerate should clear the reaction properties on its results
+ (github issue #6432 from greglandrum)
+  - RDKit hangs indefinitely when parsing not so big molblock
+ (github issue #6434 from eloyfelix)
+  - Removing Hs on a pyrrol-like structure throws kekulization error
+ (github issue #6437 from ricrogz)
+  - Molecules from CDXML Parser have inverted, unrealistic atomic coordinates
+ (github issue #6461 from greglandrum)
+  - CDXML Parser does not preserve information about bond wedging
+ (github issue #6462 from greglandrum)
+  - boost::bad_any_cast error when calling getProp<string> on properties set by applyMolListPropsToAtoms<int64_t>
+ (github issue #6465 from rachelnwalker)
+  - Allow systems like C/C=N/[H] to be stereogenic with the new chirality code
+ (github pull #6473 from greglandrum)
+  - Fix RWMol::addAtom docstring
+ (github pull #6477 from d-b-w)
+  - StereoGroup information should not impact canonicalization when CXSMILES isn't being generated
+ (github issue #6479 from greglandrum)
+  - Fix a few broken docstrings
+ (github pull #6480 from ptosco)
+  - pin numpy to 1.24.3
+ (github pull #6483 from bp-kelley)
+  - CMAKE_INSTALL_PREFIX not honored for Python files installation on Windows
+ (github pull #6485 from ricrogz)
+  - Fixed tests that weren't being run in testDepictor.py
+ (github pull #6486 from rachelnwalker)
+  - Get tests to work when building without exception support (i.e., legacy pure JS library)
+ (github pull #6487 from ptosco)
+  - Fixes rdkit-js/issues/347
+ (github pull #6490 from ptosco)
+  - Make sure that molecules are shown as images by PandasTools also when DataFrames are truncated horizontally
+ (github pull #6496 from ptosco)
+  - MolToMolBlock writes "either" stereo for double bonds which shouldn't be stereo
+ (github issue #6502 from ricrogz)
+  - Double bonds are not correctly drawn on sulfoximines
+ (github issue #6504 from ptosco)
+  - RegistrationHash.GetMolLayers() with v2 tautomer hash does not filter CX extensions
+ (github issue #6505 from ricrogz)
+  - Drop the s_m_color_rgb property from MaeWriter
+ (github pull #6511 from ricrogz)
+  - update avalontools version to incorporate bug fixes
+ (github pull #6513 from ptosco)
+  - update windows DLL CI build config
+ (github pull #6535 from greglandrum)
+  - Add MolEnumerator to C#
+ (github pull #6542 from jones-gareth)
+  - MolDraw2D: placement of bond or atom labels gets confused when atoms overlap
+ (github issue #6569 from greglandrum)
+  - partial MCS failure
+ (github issue #6578 from greglandrum)
+  - Fix vulnerabilities found by fuzzer.
+ (github pull #6579 from thomp-j)
+  - allow building the cartridge against PostgreSQL 16
+ (github pull #6580 from ptosco)
+  - SIGSEGV while calculating molecular descriptors after using salt remover.
+ (github issue #6592 from BartlomiejF)
+  - Add newline to ConstrainedEmbed docstring.
+ (github pull #6596 from DavidACosgrove)
+  - Avoid leaking memory in case exceptions are thrown while generating FPs
+ (github pull #6630 from ptosco)
+  - Pre-condition violation in canonicalization of dative bond adjacent to double bond
+ (github issue #6633 from ricrogz)
+  - Incorrect most abundant isotope for Vanadium
+ (github issue #6638 from abefrandsen)
+  - Simple imine-containing molecule causes an infinite loop in FindStereo.cpp
+ (github issue #6640 from ptosco)
+  - Mol file parser strips stereogenic H from imine bonds
+ (github issue #6664 from ricrogz)
+  - ROMol move constructor and assignment do not update SubstanceGroup ownership
+ (github issue #6681 from ricrogz)
+  - Flexicanvas cuts off bottom of reaction
+ (github issue #6685 from DavidACosgrove)
+  - make sure bond attachpt info is pickled
+ (github pull #6698 from greglandrum)
+  - Catch meanBondLen of 0.0
+ (github pull #6699 from DavidACosgrove)
+  - Add trans layout for double bonds in rings
+ (github pull #6709 from d-b-w)
+  - Segmentation fault in MMFF
+ (github issue #6728 from Meteor-han)
+  - Fix chirality handling when the chiral atom is the first one in a SMARTS
+ (github pull #6730 from johnmay)
+  - ConnectTheDots can segfault if all atoms do not have residue info
+ (github issue #6756 from jasondbiggs)
+  - _moltoimg() should honor drawOptions.prepareMolsBeforeDrawing
+ (github issue #6792 from ptosco)
 
 ## Cleanup work:
+ - adjustQueryProperties cleanup
+ (github pull #6361 from ptosco)
+  - Fix identical for-loop variable names
+ (github pull #6391 from JLVarjo)
+  - Deprecate JSMol::is_valid() and JSReaction::is_valid() and return nullptr instead
+ (github pull #6392 from ptosco)
+  - misc jswrapper.cpp cleanup
+ (github pull #6449 from ptosco)
+  - Deprecate the pure python MolStandardize implementations.
+ (github pull #6548 from greglandrum)
+  - clear up some compiler warnings
+ (github pull #6627 from greglandrum)
+  - switch from boost::any to std::any
+ (github pull #6662 from greglandrum)
+  - Fail CI builds on compiler warnings + some fixes
+ (github pull #6675 from ricrogz)
+  - fix some more leaks
+ (github pull #6684 from ricrogz)
+  - Some small cleanups from the UGM Hackathon
+ (github pull #6744 from greglandrum)
+  - some modernization of the memory handing in the canonicalization code
+ (github pull #6763 from greglandrum)
 
-## New Features and Enhancements:
 
 ## Code removed in this release:
 - The `compare` and `callback` methods (deprecated since release 2021.01)
diff --git a/rdkit/Chem/Descriptors3D.py b/rdkit/Chem/Descriptors3D.py
index 479514980e5..0b86a37c2cb 100644
--- a/rdkit/Chem/Descriptors3D.py
+++ b/rdkit/Chem/Descriptors3D.py
@@ -196,3 +196,55 @@
       - confId: (optional) the conformation ID to use
 
     """
+
+  PBF = lambda *x, **y: rdMolDescriptors.CalcPBF(*x, **y)
+  PBF.version = rdMolDescriptors._CalcPBF_version
+  PBF.__doc__ = """ Plane of best fit
+      from: https://doi.org/10.1021/ci300293f
+
+    **Arguments**
+
+      - inMol: a molecule
+
+      - confId: (optional) the conformation ID to use
+
+    """
+
+
+
+from rdkit.Chem.Descriptors import _isCallable
+descList = []
+def _setupDescriptors(namespace):
+  global descList
+  descList.clear()
+
+  for nm, thing in tuple(namespace.items()):
+    if nm[0] != '_' and nm != 'CalcMolDescriptors3D' and _isCallable(thing):
+      descList.append((nm, thing))
+
+
+_setupDescriptors(locals())
+
+def CalcMolDescriptors3D(mol, confId=None):
+    """
+    Compute all 3D descriptors of a molecule
+    
+    Arguments:
+    - mol: the molecule to work with
+    - confId: conformer ID to work with. If not specified the default (-1) is used
+    
+    Return:
+    
+    dict
+        A dictionary with decriptor names as keys and the descriptor values as values
+
+    raises a ValueError 
+        If the molecule does not have conformers
+    """
+    if mol.GetNumConformers() == 0:
+        raise ValueError('Computing 3D Descriptors requires a structure with at least 1 conformer')
+    else:
+        vals_3D = {}
+        for nm,fn in descList:
+           vals_3D[nm] = fn(mol)
+        return vals_3D
diff --git a/rdkit/Chem/Draw/__init__.py b/rdkit/Chem/Draw/__init__.py
index 38e0c4e9ae5..910d707beda 100644
--- a/rdkit/Chem/Draw/__init__.py
+++ b/rdkit/Chem/Draw/__init__.py
@@ -462,13 +462,14 @@ def _moltoimg(mol, sz, highlights, legend, returnPNG=False, drawOptions=None, **
   kekulize = shouldKekulize(mol, kwargs.get('kekulize', True))
   wedge = kwargs.get('wedgeBonds', True)
 
-  try:
-    with rdBase.BlockLogs():
-      mc = rdMolDraw2D.PrepareMolForDrawing(mol, kekulize=kekulize, wedgeBonds=wedge)
-  except ValueError:  # <- can happen on a kekulization failure
-    mc = rdMolDraw2D.PrepareMolForDrawing(mol, kekulize=False, wedgeBonds=wedge)
+  if not drawOptions or drawOptions.prepareMolsBeforeDrawing:
+    try:
+      with rdBase.BlockLogs():
+        mol = rdMolDraw2D.PrepareMolForDrawing(mol, kekulize=kekulize, wedgeBonds=wedge)
+    except ValueError:  # <- can happen on a kekulization failure
+      mol = rdMolDraw2D.PrepareMolForDrawing(mol, kekulize=False, wedgeBonds=wedge)
   if not hasattr(rdMolDraw2D, 'MolDraw2DCairo'):
-    img = MolToImage(mc, sz, legend=legend, highlightAtoms=highlights, **kwargs)
+    img = MolToImage(mol, sz, legend=legend, highlightAtoms=highlights, **kwargs)
     if returnPNG:
       bio = BytesIO()
       img.save(bio, format='PNG')
@@ -483,10 +484,10 @@ def _moltoimg(mol, sz, highlights, legend, returnPNG=False, drawOptions=None, **
     d2d.drawOptions().prepareMolsBeforeDrawing = False
     bondHighlights = kwargs.get('highlightBonds', None)
     if bondHighlights is not None:
-      d2d.DrawMolecule(mc, legend=legend or "", highlightAtoms=highlights or [],
+      d2d.DrawMolecule(mol, legend=legend or "", highlightAtoms=highlights or [],
                        highlightBonds=bondHighlights)
     else:
-      d2d.DrawMolecule(mc, legend=legend or "", highlightAtoms=highlights or [])
+      d2d.DrawMolecule(mol, legend=legend or "", highlightAtoms=highlights or [])
     d2d.FinishDrawing()
     if returnPNG:
       img = d2d.GetDrawingText()
@@ -504,11 +505,12 @@ def _moltoSVG(mol, sz, highlights, legend, kekulize, drawOptions=None, **kwargs)
 
   kekulize = shouldKekulize(mol, kekulize)
 
-  try:
-    with rdBase.BlockLogs():
-      mc = rdMolDraw2D.PrepareMolForDrawing(mol, kekulize=kekulize)
-  except ValueError:  # <- can happen on a kekulization failure
-    mc = rdMolDraw2D.PrepareMolForDrawing(mol, kekulize=False)
+  if not drawOptions or drawOptions.prepareMolsBeforeDrawing:
+    try:
+      with rdBase.BlockLogs():
+        mol = rdMolDraw2D.PrepareMolForDrawing(mol, kekulize=kekulize)
+    except ValueError:  # <- can happen on a kekulization failure
+      mol = rdMolDraw2D.PrepareMolForDrawing(mol, kekulize=False)
   d2d = rdMolDraw2D.MolDraw2DSVG(sz[0], sz[1])
   if drawOptions is not None:
     d2d.SetDrawOptions(drawOptions)
@@ -516,10 +518,10 @@ def _moltoSVG(mol, sz, highlights, legend, kekulize, drawOptions=None, **kwargs)
   d2d.drawOptions().prepareMolsBeforeDrawing = False
   bondHighlights = kwargs.get('highlightBonds', None)
   if bondHighlights is not None:
-    d2d.DrawMolecule(mc, legend=legend or "", highlightAtoms=highlights or [],
+    d2d.DrawMolecule(mol, legend=legend or "", highlightAtoms=highlights or [],
                      highlightBonds=bondHighlights)
   else:
-    d2d.DrawMolecule(mc, legend=legend or "", highlightAtoms=highlights or [])
+    d2d.DrawMolecule(mol, legend=legend or "", highlightAtoms=highlights or [])
   d2d.FinishDrawing()
   svg = d2d.GetDrawingText()
   return svg


Test patch:
diff --git a/Code/GraphMol/FMCS/Wrap/testFMCS.py b/Code/GraphMol/FMCS/Wrap/testFMCS.py
index f9bfceafcb4..99437bcb7b2 100644
--- a/Code/GraphMol/FMCS/Wrap/testFMCS.py
+++ b/Code/GraphMol/FMCS/Wrap/testFMCS.py
@@ -707,7 +707,7 @@ def test7Seed(self, **kwargs):
       r = rdFMCS.FindMCS(ms, params)
     else:
       r = rdFMCS.FindMCS(ms, seedSmarts='C1OC1', ringMatchesRingOnly=True)
-    self.assertEqual(r.smartsString, "[#6&R]1-&@[#6&R]-&@[#6&R]-&@[#6&R]-&@1")
+    self.assertEqual(r.smartsString, "[#6]1-&@[#6]-&@[#6]-&@[#6]-&@1")
     self.assertEqual(r.numAtoms, 4)
     self.assertEqual(r.numBonds, 4)
     if kwargs:
@@ -1164,7 +1164,7 @@ def test18GitHub3693(self):
     self.assertEqual(res.numBonds, 11)
     self.assertEqual(
       res.smartsString,
-      "[#6&R]1:&@[#6&R]:&@[#6&R]:&@[#6&R]2:&@[#6&R](:&@[#6&R]:&@1):&@[#6&R]:&@[#6&R]:&@[#6&R]:&@[#6&R]:&@2"
+      "[#6]1:&@[#6]:&@[#6]:&@[#6]2:&@[#6](:&@[#6]:&@1):&@[#6]:&@[#6]:&@[#6]:&@[#6]:&@2"
     )
 
     params = rdFMCS.MCSParameters()
@@ -1176,7 +1176,7 @@ def test18GitHub3693(self):
     self.assertEqual(res.numBonds, 11)
     self.assertEqual(
       res.smartsString,
-      "[#6&R]1:&@[#6&R]:&@[#6&R]:&@[#6&R]2:&@[#6&R](:&@[#6&R]:&@1):&@[#6&R]:&@[#6&R]:&@[#6&R]:&@[#6&R]:&@2"
+      "[#6]1:&@[#6]:&@[#6]:&@[#6]2:&@[#6](:&@[#6]:&@1):&@[#6]:&@[#6]:&@[#6]:&@[#6]:&@2"
     )
 
   def test19MCS3d(self):
@@ -1241,7 +1241,7 @@ def test21ShouldAcceptMCS(self):
     mcs = rdFMCS.FindMCS(mols2, params)
     self.assertEqual(mcs.numAtoms, 4)
     self.assertEqual(mcs.numBonds, 4)
-    self.assertEqual(mcs.smartsString, "[#6&R]1-&@[#6&R]-&@[#6&R]-&@1-&!@[#0,#6;!R]")
+    self.assertEqual(mcs.smartsString, "[#6]1-&@[#6]-&@[#6]-&@1-&!@[#0,#6;!R]")
     self.assertGreater(params.ProgressCallback.callCount, 0)
     self.assertGreater(params.FinalMatchChecker.callCount, 0)
 
diff --git a/Code/GraphMol/FMCS/testFMCS_Unit.cpp b/Code/GraphMol/FMCS/testFMCS_Unit.cpp
index 60832bcf55f..ea6a29705f2 100644
--- a/Code/GraphMol/FMCS/testFMCS_Unit.cpp
+++ b/Code/GraphMol/FMCS/testFMCS_Unit.cpp
@@ -2035,7 +2035,7 @@ void testGitHub2731_comment546175466() {
       TEST_ASSERT(res.NumBonds == 14);
       TEST_ASSERT(
           res.SmartsString ==
-          "[#6]1:&@[#6]:&@[#6&R2]2:&@[#6]:&@[#6&R2](:&@[#6]:&@[#6]):&@[#6&R2](:&@[#7]:&@[#6&R2]:&@2:&@[#6]:&@[#6]:&@1):&@[#6]");
+          "[#6;r6,!R1]1:&@[#6;r6,!R1]:&@[#6]2:&@[#6;r6,!R1]:&@[#6](:&@[#6]:&@[#6]):&@[#6](:&@[#7;r6,!R1]:&@[#6]:&@2:&@[#6;r6,!R1]:&@[#6;r6,!R1]:&@1):&@[#6]");
     }
     {
       MCSParameters p;
@@ -2049,7 +2049,7 @@ void testGitHub2731_comment546175466() {
       TEST_ASSERT(res.NumBonds == 11);
       TEST_ASSERT(
           res.SmartsString ==
-          "[#6]1:&@[#6]:&@[#6&R2]2:&@[#6]:&@[#6&R2]:&@[#6&R2]:&@[#7]:&@[#6&R2]:&@2:&@[#6]:&@[#6]:&@1");
+          "[#6;r6,!R1]1:&@[#6;r6,!R1]:&@[#6]2:&@[#6;r6,!R1]:&@[#6]:&@[#6]:&@[#7;r6,!R1]:&@[#6]:&@2:&@[#6;r6,!R1]:&@[#6;r6,!R1]:&@1");
     }
   }
   {
@@ -2096,7 +2096,7 @@ void testGitHub2731_comment546175466() {
       TEST_ASSERT(res.NumBonds == 9);
       TEST_ASSERT(
           res.SmartsString ==
-          "[#6&R2](-&@[#6]-&@[#6])(-&@[#6])-&@[#6](-&@[#6]-&@[#6]-&@[#6])-&@[#6&R2]-&@[#6]");
+          "[#6](-&@[#6]-&@[#6])(-&@[#6])-&@[#6](-&@[#6]-&@[#6]-&@[#6])-&@[#6]-&@[#6]");
     }
   }
 
@@ -2134,9 +2134,9 @@ void testQueryMolVsSmarts() {
   TEST_ASSERT(SubstructMatch(*mols[0], *smartsMol, matchVectFromSmartsMol) ==
               1);
   TEST_ASSERT(SubstructMatch(*mols[1], *res.QueryMol, matchVectFromQueryMol) ==
-              2);
+              1);
   TEST_ASSERT(SubstructMatch(*mols[1], *smartsMol, matchVectFromSmartsMol) ==
-              2);
+              1);
 
   BOOST_LOG(rdInfoLog) << "============================================"
                        << std::endl;
@@ -2327,10 +2327,9 @@ void testGitHub3458() {
       MCSResult res = findMCS(mols, &p);
       TEST_ASSERT(res.NumAtoms == 17);
       TEST_ASSERT(res.NumBonds == 18);
-      TEST_ASSERT(res.SmartsString ==
-                  "[#6&R]:&@[#6&R]:&@[#6&R]1:&@[#6&R](-&!@[#7&!R]-&!@[#6&R]2:&@"
-                  "[#6&R]:&@[#6&R]:&@[#6&R]:&@[#6&R](:&@[#6&R]:&@2)-&!@[#35&!R]"
-                  "):&@[#7&R]:&@[#6&R]:&@[#7&R]:&@[#6&R]:&@1:&@[#6&R]");
+      TEST_ASSERT(
+          res.SmartsString ==
+          "[#6&R]:&@[#6&R]:&@[#6]1:&@[#6](-&!@[#7&!R]-&!@[#6]2:&@[#6]:&@[#6]:&@[#6]:&@[#6](:&@[#6]:&@2)-&!@[#35&!R]):&@[#7]:&@[#6]:&@[#7]:&@[#6]:&@1:&@[#6&R]");
     }
     {
       MCSParameters p;
@@ -2340,10 +2339,9 @@ void testGitHub3458() {
       MCSResult res = findMCS(mols, &p);
       TEST_ASSERT(res.NumAtoms == 15);
       TEST_ASSERT(res.NumBonds == 15);
-      TEST_ASSERT(res.SmartsString ==
-                  "[#6&R]:&@[#6&R]:&@[#6&R](:&@[#6&R]-&!@[#7&!R]-&!@[#6&R]1:&@["
-                  "#6&R]:&@[#6&R]:&@[#6&R]:&@[#6&R]:&@[#6&R]:&@1):&@[#6&R](:&@["
-                  "#7&R]:&@[#6&R]):&@[#6&R]");
+      TEST_ASSERT(
+          res.SmartsString ==
+          "[#6&R]:&@[#6&R]:&@[#6&R](:&@[#6&R]-&!@[#7&!R]-&!@[#6]1:&@[#6]:&@[#6]:&@[#6]:&@[#6]:&@[#6]:&@1):&@[#6&R](:&@[#7&R]:&@[#6&R]):&@[#6&R]");
     }
   }
 
@@ -2386,9 +2384,9 @@ void testGitHub3693() {
       MCSResult res = findMCS(mols, &p);
       TEST_ASSERT(res.NumAtoms == 10);
       TEST_ASSERT(res.NumBonds == 11);
-      TEST_ASSERT(res.SmartsString ==
-                  "[#6&R]1:&@[#6&R]:&@[#6&R]:&@[#6&R]2:&@[#6&R](:&@[#6&R]:&@1):"
-                  "&@[#6&R]:&@[#6&R]:&@[#6&R]:&@[#6&R]:&@2");
+      TEST_ASSERT(
+          res.SmartsString ==
+          "[#6]1:&@[#6]:&@[#6]:&@[#6]2:&@[#6](:&@[#6]:&@1):&@[#6]:&@[#6]:&@[#6]:&@[#6]:&@2");
     }
     {
       MCSParameters p;
@@ -2397,9 +2395,9 @@ void testGitHub3693() {
       MCSResult res = findMCS(mols, &p);
       TEST_ASSERT(res.NumAtoms == 10);
       TEST_ASSERT(res.NumBonds == 11);
-      TEST_ASSERT(res.SmartsString ==
-                  "[#6&R]1:&@[#6&R]:&@[#6&R]:&@[#6&R]2:&@[#6&R](:&@[#6&R]:&@1):"
-                  "&@[#6&R]:&@[#6&R]:&@[#6&R]:&@[#6&R]:&@2");
+      TEST_ASSERT(
+          res.SmartsString ==
+          "[#6]1:&@[#6]:&@[#6]:&@[#6]2:&@[#6](:&@[#6]:&@1):&@[#6]:&@[#6]:&@[#6]:&@[#6]:&@2");
     }
     {
       MCSParameters p;
@@ -2409,9 +2407,9 @@ void testGitHub3693() {
       MCSResult res = findMCS(mols, &p);
       TEST_ASSERT(res.NumAtoms == 10);
       TEST_ASSERT(res.NumBonds == 11);
-      TEST_ASSERT(res.SmartsString ==
-                  "[#6&R]1:&@[#6&R]:&@[#6&R]:&@[#6&R]2:&@[#6&R](:&@[#6&R]:&@1):"
-                  "&@[#6&R]:&@[#6&R]:&@[#6&R]:&@[#6&R]:&@2");
+      TEST_ASSERT(
+          res.SmartsString ==
+          "[#6]1:&@[#6]:&@[#6]:&@[#6]2:&@[#6](:&@[#6]:&@1):&@[#6]:&@[#6]:&@[#6]:&@[#6]:&@2");
     }
   }
   BOOST_LOG(rdInfoLog) << "============================================"
@@ -2455,8 +2453,7 @@ void testAtomCompareCompleteRingsOnly() {
     MCSResult res = findMCS(mols, &p);
     TEST_ASSERT(res.NumAtoms == 5);
     TEST_ASSERT(res.NumBonds == 5);
-    TEST_ASSERT(res.SmartsString ==
-                "[#6&R]1-&@[#6&R]-&@[#6&R]-&@[#6&R]-&@[#6&R]-&@1");
+    TEST_ASSERT(res.SmartsString == "[#6]1-&@[#6]-&@[#6]-&@[#6]-&@[#6]-&@1");
   }
   {
     MCSParameters p;
@@ -2466,8 +2463,7 @@ void testAtomCompareCompleteRingsOnly() {
     MCSResult res = findMCS(mols, &p);
     TEST_ASSERT(res.NumAtoms == 5);
     TEST_ASSERT(res.NumBonds == 5);
-    TEST_ASSERT(res.SmartsString ==
-                "[#6&R]1-&@[#6&R]-&@[#6&R]-&@[#6&R]-&@[#6&R]-&@1");
+    TEST_ASSERT(res.SmartsString == "[#6]1-&@[#6]-&@[#6]-&@[#6]-&@[#6]-&@1");
   }
   BOOST_LOG(rdInfoLog) << "============================================"
                        << std::endl;
@@ -2803,7 +2799,7 @@ void testGitHub5411() {
   ROMOL_SPTR molb(
       SmilesToMol("[NH3+]CCCCn1c(SCCc2c[nH]c3ccccc23)nnc1-c1ccc2ccccc2n1"));
   std::string expectedSmarts(
-      "[#7]-&!@[#6]-&!@[#6]-&!@[#6]-&!@[#6]-&!@[#7]1:&@[#6](-&!@[#16]-&!@[#6]-&!@[#6]-&!@[#6]2:&@[#6]:&@[#7]:&@[#6&R2]3:&@[#6&R2]:&@2:&@[#6]:&@[#6]:&@[#6]:&@[#6]:&@3):&@[#7]:&@[#7]:&@[#6]:&@1-&!@[#6]:&@[#6]:&@[#6]:&@[#6]:&@[#6&R2]:&@[#6]:&@[#6]:&@[#6]:&@[#6]");
+      "[#7]-&!@[#6]-&!@[#6]-&!@[#6]-&!@[#6]-&!@[#7;r5,!R1]1:&@[#6;r5,!R1](-&!@[#16]-&!@[#6]-&!@[#6]-&!@[#6;r5,!R1]2:&@[#6;r5,!R1]:&@[#7;r5,!R1]:&@[#6]3:&@[#6]:&@2:&@[#6;r6,!R1]:&@[#6;r6,!R1]:&@[#6;r6,!R1]:&@[#6;r6,!R1]:&@3):&@[#7;r5,!R1]:&@[#7;r5,!R1]:&@[#6;r5,!R1]:&@1-&!@[#6]:&@[#6]:&@[#6]:&@[#6]:&@[#6]:&@[#6]:&@[#6]:&@[#6]:&@[#6]");
   MCSParameters p;
   p.BondCompareParameters.MatchFusedRings = true;
   p.BondCompareParameters.MatchFusedRingsStrict = true;
@@ -2901,7 +2897,7 @@ void testGitHub5457() {
   TEST_ASSERT(mcs.NumBonds == 23);
   TEST_ASSERT(
       mcs.SmartsString ==
-      "[#6&!R](-&!@[#7,#6;!R])-&!@[#6&!R]-&!@[#6&!R]-&!@[#7&R]1:&@[#6&R](-&!@[#16&!R]-&!@[#6&!R]-&!@[#6&!R]-&!@[#6&R]2:&@[#6&R]:&@[#7&R]:&@[#6&R]3:&@[#6&R]:&@2:&@[#6&R]:&@[#6&R]:&@[#6&R]:&@[#6&R]:&@3):&@[#7&R]:&@[#7&R]:&@[#6&R]:&@1");
+      "[#6&!R](-&!@[#7,#6;!R])-&!@[#6&!R]-&!@[#6&!R]-&!@[#7]1:&@[#6](-&!@[#16&!R]-&!@[#6&!R]-&!@[#6&!R]-&!@[#6]2:&@[#6]:&@[#7]:&@[#6]3:&@[#6]:&@2:&@[#6]:&@[#6]:&@[#6]:&@[#6]:&@3):&@[#7]:&@[#7]:&@[#6]:&@1");
   TEST_ASSERT(!SubstructMatch(*mola, *mcs.QueryMol).empty());
   TEST_ASSERT(!SubstructMatch(*molb, *mcs.QueryMol).empty());
   ;
@@ -2989,6 +2985,145 @@ void testGitHub6578() {
   }
 }
 
+void testGitHub6773() {
+  BOOST_LOG(rdInfoLog) << "-------------------------------------" << std::endl;
+  BOOST_LOG(rdInfoLog) << "test GitHub issue #6773" << std::endl;
+  {
+    // these are 2- and 1-hydroxydecalin
+    std::vector<ROMOL_SPTR> mols{"OC1CCC2CCCCC2C1"_smiles,
+                                 "OC1CCCC2CCCCC12"_smiles};
+    {
+      MCSParameters p;
+      p.BondCompareParameters.RingMatchesRingOnly = true;
+      p.AtomCompareParameters.RingMatchesRingOnly = true;
+      p.BondCompareParameters.MatchFusedRingsStrict = true;
+      auto mcs = findMCS(mols, &p);
+      TEST_ASSERT(mcs.NumAtoms == 11);
+      TEST_ASSERT(mcs.NumBonds == 11);
+      TEST_ASSERT(
+          mcs.SmartsString ==
+          "[#8&!R]-&!@[#6;r6,!R1]1-&@[#6;r6,!R1]-&@[#6;r6,!R1]-&@[#6]-&@[#6](-&@[#6;r6,!R1]-&@1)-&@[#6&R]-&@[#6&R]-&@[#6&R]-&@[#6&R]");
+    }
+    {
+      MCSParameters p;
+      p.BondCompareParameters.RingMatchesRingOnly = true;
+      p.AtomCompareParameters.RingMatchesRingOnly = true;
+      p.BondCompareParameters.CompleteRingsOnly = true;
+      p.AtomCompareParameters.CompleteRingsOnly = true;
+      p.BondCompareParameters.MatchFusedRingsStrict = true;
+      auto mcs = findMCS(mols, &p);
+      TEST_ASSERT(mcs.NumAtoms == 10);
+      TEST_ASSERT(mcs.NumBonds == 11);
+      TEST_ASSERT(
+          mcs.SmartsString ==
+          "[#6;r6,!R1]1-&@[#6;r6,!R1]-&@[#6;r6,!R1]-&@[#6]2-&@[#6](-&@[#6;r6,!R1]-&@1)-&@[#6;r6,!R1]-&@[#6;r6,!R1]-&@[#6;r6,!R1]-&@[#6;r6,!R1]-&@2");
+    }
+    // this is 1-hydroxyperhydroazulene
+    mols.push_back("OC1CCC2CCCCCC12"_smiles);
+    {
+      MCSParameters p;
+      p.BondCompareParameters.RingMatchesRingOnly = true;
+      p.AtomCompareParameters.RingMatchesRingOnly = true;
+      p.BondCompareParameters.MatchFusedRingsStrict = true;
+      auto mcs = findMCS(mols, &p);
+      TEST_ASSERT(mcs.NumAtoms == 10);
+      TEST_ASSERT(mcs.NumBonds == 9);
+      TEST_ASSERT(
+          mcs.SmartsString ==
+          "[#8&!R]-&!@[#6&R](-&@[#6&R]-&@[#6&R])-&@[#6&R]-&@[#6&R]-&@[#6&R]-&@[#6&R]-&@[#6&R]-&@[#6&R]");
+    }
+    {
+      MCSParameters p;
+      p.BondCompareParameters.RingMatchesRingOnly = true;
+      p.AtomCompareParameters.RingMatchesRingOnly = true;
+      p.BondCompareParameters.CompleteRingsOnly = true;
+      p.AtomCompareParameters.CompleteRingsOnly = true;
+      p.BondCompareParameters.MatchFusedRingsStrict = true;
+      auto mcs = findMCS(mols, &p);
+      TEST_ASSERT(mcs.NumAtoms == 2);
+      TEST_ASSERT(mcs.NumBonds == 1);
+      TEST_ASSERT(mcs.SmartsString == "[#8&!R]-&!@[#6&R]");
+    }
+    mols = std::vector<ROMOL_SPTR>{"C1CC2CCC3CC2C(C1)CCCCCC3"_smiles, "C1CC2CCC3CCCC4CCC(C1)C2C34"_smiles};
+    {
+      MCSParameters p;
+      p.BondCompareParameters.MatchFusedRingsStrict = true;
+      auto mcs = findMCS(mols, &p);
+      TEST_ASSERT(mcs.NumAtoms == 15);
+      TEST_ASSERT(mcs.NumBonds == 16);
+      TEST_ASSERT(
+          mcs.SmartsString ==
+          "[#6;r6,!R1]1-&@[#6;r6,!R1]-&@[#6]2-&@[#6;r6,!R1]-&@[#6;r6,!R1]-&@[#6](-&@[#6]-&@[#6]-&@2-&@[#6](-&@[#6;r6,!R1]-&@1)-&@[#6]-&@[#6])-&@[#6]-&@[#6]-&@[#6]");
+    }
+    {
+      MCSParameters p;
+      p.BondCompareParameters.MatchFusedRingsStrict = true;
+      p.BondCompareParameters.CompleteRingsOnly = true;
+      auto mcs = findMCS(mols, &p);
+      TEST_ASSERT(mcs.NumAtoms == 10);
+      TEST_ASSERT(mcs.NumBonds == 11);
+      TEST_ASSERT(
+          mcs.SmartsString ==
+          "[#6;r6,!R1]1-&@[#6;r6,!R1]-&@[#6]2-&@[#6;r6,!R1]-&@[#6;r6,!R1]-&@[#6]-&@[#6]-&@[#6]-&@2-&@[#6]-&@[#6;r6,!R1]-&@1");
+      p.AtomCompareParameters.CompleteRingsOnly = true;
+      mcs = findMCS(mols, &p);
+      TEST_ASSERT(mcs.NumAtoms == 10);
+      TEST_ASSERT(mcs.NumBonds == 11);
+      TEST_ASSERT(
+          mcs.SmartsString ==
+          "[#6;r6,!R1]1-&@[#6;r6,!R1]-&@[#6]2-&@[#6;r6,!R1]-&@[#6;r6,!R1]-&@[#6]-&@[#6]-&@[#6]-&@2-&@[#6]-&@[#6;r6,!R1]-&@1");
+    }
+  }
+}
+
+void testBondCompareCompleteRingsOnly() {
+  BOOST_LOG(rdInfoLog) << "-------------------------------------" << std::endl;
+  BOOST_LOG(rdInfoLog) << "test CompleteRingsOnly should work also on unsubstituted fused systems" << std::endl;
+  std::vector<ROMOL_SPTR> mols{"C1CCCC2CCCCC12"_smiles,
+                                "C1CCC2CCCC12"_smiles};
+  MCSParameters p;
+  p.BondCompareParameters.CompleteRingsOnly = true;
+  auto mcs = findMCS(mols, &p);
+  TEST_ASSERT(mcs.NumAtoms == 0);
+  TEST_ASSERT(mcs.NumBonds == 0);
+}
+
+void testAtomRingQueries() {
+  BOOST_LOG(rdInfoLog) << "-------------------------------------" << std::endl;
+  BOOST_LOG(rdInfoLog) << "test atom ring queries to enforce strict ring fusion"
+                       << std::endl;
+  std::vector<ROMOL_SPTR> mols{"NC(=O)CC1C2CC3CCCCC3C12"_smiles,
+                               "NC(=O)CC1CCCC2CCCCC12"_smiles};
+  MCSParameters p;
+  p.BondCompareParameters.CompleteRingsOnly = true;
+  auto mcs = findMCS(mols, &p);
+  TEST_ASSERT(mcs.NumAtoms == 14);
+  TEST_ASSERT(mcs.NumBonds == 14);
+  TEST_ASSERT(
+      mcs.SmartsString ==
+      "[#7]-&!@[#6](=&!@[#8])-&!@[#6]-&!@[#6]1-&@[#6]-&@[#6]-&@[#6]-&@[#6]-&@[#6]-&@[#6]-&@[#6]-&@[#6]-&@[#6]-&@1");
+  p.BondCompareParameters.MatchFusedRings = true;
+  mcs = findMCS(mols, &p);
+  TEST_ASSERT(mcs.NumAtoms == 10);
+  TEST_ASSERT(mcs.NumBonds == 11);
+  TEST_ASSERT(
+      mcs.SmartsString ==
+      "[#6]1-&@[#6]-&@[#6]-&@[#6]-&@[#6]2-&@[#6]-&@1-&@[#6]-&@[#6]-&@[#6]-&@[#6]-&@2");
+  p.BondCompareParameters.MatchFusedRingsStrict = true;
+  mcs = findMCS(mols, &p);
+  TEST_ASSERT(mcs.NumAtoms == 6);
+  TEST_ASSERT(mcs.NumBonds == 6);
+  TEST_ASSERT(
+      mcs.SmartsString ==
+      "[#6;r6,!R1]1-&@[#6;r6,!R1]-&@[#6;r6,!R1]-&@[#6;r6,!R1]-&@[#6]-&@[#6]-&@1");
+  ROMOL_SPTR smartsMol(SmartsToMol(mcs.SmartsString));
+  std::vector<MatchVectType> matches;
+  TEST_ASSERT(SubstructMatch(*mols[0], *mcs.QueryMol, matches) == 1);
+  TEST_ASSERT(SubstructMatch(*mols[1], *mcs.QueryMol, matches) == 2);
+  TEST_ASSERT(SubstructMatch(*mols[0], *smartsMol, matches) == 1);
+  TEST_ASSERT(SubstructMatch(*mols[1], *smartsMol, matches) == 2);
+}
+
 //====================================================================================================
 //====================================================================================================
 
@@ -3082,6 +3217,9 @@ int main(int argc, const char* argv[]) {
   testGitHub6082();
   testGitHub3965();
   testGitHub6578();
+  testGitHub6773();
+  testBondCompareCompleteRingsOnly();
+  testAtomRingQueries();
 #endif
 
   unsigned long long t1 = nanoClock();
diff --git a/Code/GraphMol/MolDraw2D/catch_tests.cpp b/Code/GraphMol/MolDraw2D/catch_tests.cpp
index 21b77abe8b1..3291cab52ee 100644
--- a/Code/GraphMol/MolDraw2D/catch_tests.cpp
+++ b/Code/GraphMol/MolDraw2D/catch_tests.cpp
@@ -307,17 +307,18 @@ static const std::map<std::string, std::hash_result_t> SVG_HASHES = {
     {"github6504_2.svg", 2871662880U},
     {"github6569_1.svg", 116573839U},
     {"github6569_2.svg", 2367779037U},
-    {"lasso_highlights_1.svg", 1259621911U},
-    {"lasso_highlights_2.svg", 39607542U},
-    {"lasso_highlights_3.svg", 2744227900U},
-    {"lasso_highlights_4.svg", 307369790U},
-    {"lasso_highlights_5.svg", 1103195299U},
-    {"lasso_highlights_6.svg", 3501858565U},
-    {"lasso_highlights_7.svg", 4002684466U},
+    {"lasso_highlights_1.svg", 689837467U},
+    {"lasso_highlights_2.svg", 348394942U},
+    {"lasso_highlights_3.svg", 2174136207U},
+    {"lasso_highlights_4.svg", 1265047504U},
+    {"lasso_highlights_5.svg", 35778943U},
+    {"lasso_highlights_6.svg", 1359376880U},
+    {"lasso_highlights_7.svg", 773081917U},
     {"testGithub6685_1.svg", 1835717197U},
     {"testGithub6685_2.svg", 116380465U},
     {"testGithub6685_3.svg", 409385402U},
-    {"testGithub6685_4.svg", 1239628830U}};
+    {"testGithub6685_4.svg", 1239628830U},
+    {"bad_lasso_1.svg", 1183031575U}};
 
 // These PNG hashes aren't completely reliable due to floating point cruft,
 // but they can still reduce the number of drawings that need visual
@@ -8692,8 +8693,9 @@ M  END)RXN";
     MolDraw2DSVG drawer(-1, -1);
     auto reactIter = rxn->beginReactantTemplates();
     REQUIRE_THROWS_AS(MolDraw2DUtils::setACS1996Options(
-        drawer.drawOptions(),
-        MolDraw2DUtils::meanBondLength(*(*reactIter)) * 0.7), ValueErrorException);
+                          drawer.drawOptions(),
+                          MolDraw2DUtils::meanBondLength(*(*reactIter)) * 0.7),
+                      ValueErrorException);
   }
 }
 
@@ -9042,4 +9044,96 @@ M  END
     checkImage(text);
     check_file_hash(baseName + "4.svg");
   }
-}
\ No newline at end of file
+}
+
+TEST_CASE("Github 6749 : various bad things in the lasso highlighting") {
+  std::string baseName = "bad_lasso_";
+  auto mol =
+      "CCCS(=O)(=O)Nc1ccc(F)c(c1F)C(=O)c2c[nH]c3c2cc(cn3)c4ccc(Cl)cc4"_smiles;
+  REQUIRE(mol);
+
+  auto update_colour_map =
+      [](const std::vector<int> &ats, DrawColour col,
+         std::map<int, std::vector<DrawColour>> &ha_map) -> void {
+    for (auto h : ats) {
+      auto ex = ha_map.find(h);
+      if (ex == ha_map.end()) {
+        std::vector<DrawColour> cvec(1, col);
+        ha_map.insert(make_pair(h, cvec));
+      } else {
+        if (ex->second.end() ==
+            find(ex->second.begin(), ex->second.end(), col)) {
+          ex->second.push_back(col);
+        }
+      }
+    }
+  };
+  auto update_bond_map =
+      [](const std::vector<int> &ats, DrawColour col, const ROMol &mol,
+         std::map<int, std::vector<DrawColour>> &hb_map) -> void {
+    for (auto at1 : ats) {
+      for (auto at2 : ats) {
+        if (at1 > at2) {
+          auto b = mol.getBondBetweenAtoms(at1, at2);
+          if (b) {
+            auto ex = hb_map.find(b->getIdx());
+            if (ex == hb_map.end()) {
+              std::vector<DrawColour> cvec(1, col);
+              hb_map.insert(make_pair(b->getIdx(), cvec));
+            } else {
+              ex->second.push_back(col);
+            }
+          }
+        }
+      }
+    }
+  };
+  std::vector<DrawColour> colours = {
+      DrawColour(1.0, 0.2, 1.0), DrawColour(0.2, 1.0, 1.0),
+      DrawColour(0.8, 0.8, 0.2), DrawColour(0.4, 0.4, 0.2)};
+  std::map<int, std::vector<DrawColour>> ha_map;
+  std::map<int, std::vector<DrawColour>> hb_map;
+  update_colour_map({6, 19, 25}, colours[0], ha_map);
+  update_bond_map({6, 19, 25}, colours[0], *mol, hb_map);
+
+  update_colour_map({25, 4, 5, 11, 14, 16}, colours[1], ha_map);
+  update_bond_map({25, 4, 5, 11, 14, 16}, colours[1], *mol, hb_map);
+
+  update_colour_map({19, 25, 17, 18, 20, 21, 7,  8,  9,  10, 12,
+                     13, 22, 23, 24, 26, 27, 28, 29, 31, 32},
+                    colours[2], ha_map);
+  update_bond_map({19, 25, 17, 18, 20, 21, 7,  8,  9,  10, 12,
+                   13, 22, 23, 24, 26, 27, 28, 29, 31, 32},
+                  colours[2], *mol, hb_map);
+  // If there are duplicate colours in the list, there was a bug where
+  // arcs weren't removed correctly (Github 6749)
+  ha_map[20].push_back(colours[2]);
+  ha_map[21].push_back(colours[2]);
+
+  update_colour_map({7, 8, 9, 10, 12, 13, 26, 27, 28, 29, 31, 32}, colours[3],
+                    ha_map);
+  update_bond_map({7, 8, 9, 10, 12, 13, 26, 27, 28, 29, 31, 32}, colours[3],
+                  *mol, hb_map);
+
+  std::map<int, double> h_rads;
+  std::map<int, int> h_lw_mult;
+  MolDraw2DSVG drawer(600, 400);
+  drawer.drawOptions().multiColourHighlightStyle =
+      RDKit::MultiColourHighlightStyle::LASSO;
+  drawer.drawOptions().fillHighlights = false;
+  drawer.drawMoleculeWithHighlights(*mol, "Bad Lasso", ha_map, hb_map, h_rads,
+                                    h_lw_mult);
+  drawer.finishDrawing();
+  std::string text = drawer.getDrawingText();
+  std::ofstream outs(baseName + "1.svg");
+  outs << text;
+  outs.flush();
+  outs.close();
+  std::regex atom20("<path class='atom-20'");
+  // there should be 3 matches for "class='atom-20'" - the buggy version gave 5
+  std::ptrdiff_t const match_count(
+      std::distance(std::sregex_iterator(text.begin(), text.end(), atom20),
+                    std::sregex_iterator()));
+  REQUIRE(match_count == 3);
+  check_file_hash(baseName + "1.svg");
+}
diff --git a/rdkit/Chem/UnitTestDescriptors.py b/rdkit/Chem/UnitTestDescriptors.py
index f53cadfedea..c5a3ab8fb0a 100644
--- a/rdkit/Chem/UnitTestDescriptors.py
+++ b/rdkit/Chem/UnitTestDescriptors.py
@@ -20,7 +20,7 @@
 import numpy as np
 
 from rdkit import Chem, RDConfig
-from rdkit.Chem import AllChem, Descriptors, Lipinski, rdMolDescriptors
+from rdkit.Chem import AllChem, Descriptors, Lipinski, rdMolDescriptors, Descriptors3D
 
 
 def load_tests(loader, tests, ignore):
@@ -202,6 +202,18 @@ def testGetMolDescriptors(self):
     self.assertTrue('MolLogP' in descs)
     self.assertEqual(descs['NumHDonors'], 1)
 
+  def testGet3DMolDescriptors(self):
+    mol = Chem.MolFromSmiles('CCCO')
+
+    # check ValueError raised when no 3D coordinates supplied
+    with self.assertRaises(ValueError):
+      Descriptors3D.CalcMolDescriptors3D(mol)
+
+    # test function returns expected outputs
+    AllChem.EmbedMolecule(mol, randomSeed=0xf00d)
+    descs = Descriptors3D.CalcMolDescriptors3D(mol)
+    self.assertTrue('InertialShapeFactor' in descs)
+    self.assertEqual(descs['PMI1'], 20.954531335493417)
 
 if __name__ == '__main__':
   unittest.main()