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

org: rdkit
repo: rdkit
pr_number: 6231
issue_title: Do not include dative bonds in ring finding by default
issue_body: Fixes #6058 

The fix here is to add a new `includeDativeBonds` option to the ring-finding code which is disabled by default (i.e. dative bonds are not included in ring finding by default).

This necessitates a small tweak to the 2D coordinate generation code, which should take dative bonds into account (at least for some systems, it might be worth coming back to this at some point and ignoring the three-rings formed by haptic systems in the depiction code).

resolved_issues: number: 6058
title: Bad drawing of ferrocene
body: 
-----------------------------------------

**Describe the bug**
Drawing a representation of ferrocene where the bonds to the Fe are represented by dative bonds from the relevant carbon atoms gives rise to double bonds in ugly places.

**To Reproduce**
```
from rdkit import Chem
from rdkit.Chem.Draw import rdMolDraw2D, rdDepictor

molblock = """ferrocene
     RDKit          2D

  0  0  0  0  0  0  0  0  0  0999 V3000
M  V30 BEGIN CTAB
M  V30 COUNTS 11 20 0 0 0
M  V30 BEGIN ATOM
M  V30 1 C -34.237900 13.261800 0.000000 0 CHG=-1
M  V30 2 C -35.057400 14.266900 0.000000 0
M  V30 3 C -37.855700 14.284400 0.000000 0
M  V30 4 C -38.759100 13.293300 0.000000 0
M  V30 5 C -36.552000 12.683200 0.000000 0
M  V30 6 C -34.152900 8.363300 0.000000 0 CHG=-1
M  V30 7 C -34.972400 9.368400 0.000000 0
M  V30 8 C -37.770600 9.385900 0.000000 0
M  V30 9 C -38.674100 8.394800 0.000000 0
M  V30 10 C -36.450300 7.767900 0.000000 0
M  V30 11 Fe -36.435100 10.904500 0.000000 0 CHG=2 VAL=10
M  V30 END ATOM
M  V30 BEGIN BOND
M  V30 1 1 1 5
M  V30 2 2 4 5
M  V30 3 1 4 3
M  V30 4 2 2 3
M  V30 5 1 1 2
M  V30 6 1 6 10
M  V30 7 2 9 10
M  V30 8 1 9 8
M  V30 9 2 7 8
M  V30 10 1 6 7
M  V30 11 9 2 11
M  V30 12 9 3 11
M  V30 13 9 4 11
M  V30 14 9 5 11
M  V30 15 9 1 11
M  V30 16 9 7 11
M  V30 17 9 8 11
M  V30 18 9 9 11
M  V30 19 9 10 11
M  V30 20 9 6 11
M  V30 END BOND
M  V30 END CTAB
M  END
"""

mol = Chem.MolFromMolBlock(molblock)
d = rdMolDraw2D.MolDraw2DSVG(500, 500)
rdMolDraw2D.PrepareAndDrawMolecule(d, mol)
d.FinishDrawing()
txt = d.GetDrawingText()
with open('bad_ferrocene.svg', 'w') as f:
    f.write(txt)
```

**Expected behavior**
The double bonds should be shown with the second line inside the cyclopentadiene.

**Screenshots**
![image](https://user-images.githubusercontent.com/9198870/216962372-db77f0a2-a703-406b-aa94-1c3d4895a1cf.png)



**Configuration (please complete the following information):**
 - RDKit version: current master
 - OS: macOS Ventura 13.2 Apple silicon
 - Python version (if relevant): 3.10.4
 - Are you using conda? No
 - If you are using conda, which channel did you install the rdkit from?
 - If you are not using conda: how did you install the RDKit? build from scratch

**Additional context**
2022.09.4 gives a different, even more horrible result, but that's already been fixed, I think.
Fix in development.



Reference solution:
fix_patch: diff --git a/Code/GraphMol/Depictor/RDDepictor.cpp b/Code/GraphMol/Depictor/RDDepictor.cpp
index cf9be5d3fad..32e9b68b582 100644
--- a/Code/GraphMol/Depictor/RDDepictor.cpp
+++ b/Code/GraphMol/Depictor/RDDepictor.cpp
@@ -269,8 +269,8 @@ void embedFusedSystems(const RDKit::ROMol &mol,
     bool allowRingTemplates = useRingTemplates;
     if (useRingTemplates && coordMap) {
       boost::dynamic_bitset<> coordMapAtoms(mol.getNumAtoms());
-      for (const auto& ring : frings) {
-        for (const auto& aid : ring) {
+      for (const auto &ring : frings) {
+        for (const auto &aid : ring) {
           if (coordMap->find(aid) != coordMap->end()) {
             coordMapAtoms.set(aid);
           }
@@ -420,7 +420,8 @@ void computeInitialCoords(RDKit::ROMol &mol,
   RDKit::VECT_INT_VECT arings;
 
   // first find all the rings
-  RDKit::MolOps::symmetrizeSSSR(mol, arings);
+  bool includeDativeBonds = true;
+  RDKit::MolOps::symmetrizeSSSR(mol, arings, includeDativeBonds);
 
   // do stereochemistry
   RDKit::MolOps::assignStereochemistry(mol, false);
@@ -439,7 +440,8 @@ void computeInitialCoords(RDKit::ROMol &mol,
 
   if (arings.size() > 0) {
     // first deal with the fused rings
-    DepictorLocal::embedFusedSystems(mol, arings, efrags, coordMap, useRingTemplates);
+    DepictorLocal::embedFusedSystems(mol, arings, efrags, coordMap,
+                                     useRingTemplates);
   }
 
   // do non-tetrahedral stereo
@@ -555,7 +557,7 @@ unsigned int compute2DCoords(RDKit::ROMol &mol,
 //
 //
 unsigned int compute2DCoords(RDKit::ROMol &mol,
-                             const Compute2DCoordParameters& params) {
+                             const Compute2DCoordParameters &params) {
   if (mol.needsUpdatePropertyCache()) {
     mol.updatePropertyCache(false);
   }
@@ -570,9 +572,11 @@ unsigned int compute2DCoords(RDKit::ROMol &mol,
     return cid;
   };
 #endif
+
+  RDKit::ROMol cp(mol);
   // storage for pieces of a molecule/s that are embedded in 2D
   std::list<EmbeddedFrag> efrags;
-  computeInitialCoords(mol, params.coordMap, efrags, params.useRingTemplates);
+  computeInitialCoords(cp, params.coordMap, efrags, params.useRingTemplates);
 
 #if 1
   // perform random sampling here to improve the density
@@ -581,9 +585,9 @@ unsigned int compute2DCoords(RDKit::ROMol &mol,
     // bonds in the structure or flip only bonds along the shortest
     // path between colliding atoms - don't do both
     if ((params.nSamples > 0) && (params.nFlipsPerSample > 0)) {
-      eri.randomSampleFlipsAndPermutations(params.nFlipsPerSample, params.nSamples,
-                                           params.sampleSeed, nullptr, 0.0,
-                                           params.permuteDeg4Nodes);
+      eri.randomSampleFlipsAndPermutations(
+          params.nFlipsPerSample, params.nSamples, params.sampleSeed, nullptr,
+          0.0, params.permuteDeg4Nodes);
     } else {
       eri.removeCollisionsBondFlip();
     }
diff --git a/Code/GraphMol/FindRings.cpp b/Code/GraphMol/FindRings.cpp
index 77282ea7527..20543537d8c 100644
--- a/Code/GraphMol/FindRings.cpp
+++ b/Code/GraphMol/FindRings.cpp
@@ -67,6 +67,15 @@ void convertToBonds(const VECT_INT_VECT &res, VECT_INT_VECT &brings,
 
 }  // end of namespace RingUtils
 
+namespace RDKit {
+namespace {
+bool bondIsDative(const RDKit::Bond &bond) {
+  auto bt = bond.getBondType();
+  return bt == Bond::BondType::DATIVE || bt == Bond::BondType::DATIVEL ||
+         bt == Bond::BondType::DATIVER || bt == Bond::BondType::DATIVEONE;
+}
+}  // namespace
+}  // namespace RDKit
 namespace FindRings {
 using namespace RDKit;
 
@@ -847,16 +856,16 @@ bool findRingConnectingAtoms(const ROMol &tMol, const Bond *bond,
 
 namespace RDKit {
 namespace MolOps {
-int findSSSR(const ROMol &mol, VECT_INT_VECT *res) {
+int findSSSR(const ROMol &mol, VECT_INT_VECT *res, bool includeDativeBonds) {
   if (!res) {
     VECT_INT_VECT rings;
-    return findSSSR(mol, rings);
+    return findSSSR(mol, rings, includeDativeBonds);
   } else {
-    return findSSSR(mol, (*res));
+    return findSSSR(mol, (*res), includeDativeBonds);
   }
 }
 
-int findSSSR(const ROMol &mol, VECT_INT_VECT &res) {
+int findSSSR(const ROMol &mol, VECT_INT_VECT &res, bool includeDativeBonds) {
   res.resize(0);
   if (mol.getRingInfo()->isInitialized()) {
     mol.getRingInfo()->reset();
@@ -871,12 +880,14 @@ int findSSSR(const ROMol &mol, VECT_INT_VECT &res) {
   boost::dynamic_bitset<> activeBonds(nbnds);
   activeBonds.set();
 
-  // Zero-order bonds are not candidates for rings
+  // Zero-order bonds are not candidates for rings, and dative bonds may also be
+  // out
   ROMol::EDGE_ITER firstB, lastB;
   boost::tie(firstB, lastB) = mol.getEdges();
   while (firstB != lastB) {
     const Bond *bond = mol[*firstB];
-    if (bond->getBondType() == Bond::ZERO) {
+    if (bond->getBondType() == Bond::ZERO ||
+        (!includeDativeBonds && bondIsDative(*bond))) {
       activeBonds[bond->getIdx()] = 0;
     }
     ++firstB;
@@ -892,14 +903,11 @@ int findSSSR(const ROMol &mol, VECT_INT_VECT &res) {
     int deg = atom->getDegree();
     atomDegrees[i] = deg;
     atomDegreesWithZeroOrderBonds[i] = deg;
-    ROMol::OEDGE_ITER beg, end;
-    boost::tie(beg, end) = mol.getAtomBonds(atom);
-    while (beg != end) {
-      const Bond *bond = mol[*beg];
-      if (bond->getBondType() == Bond::ZERO) {
+    for (const auto bond : mol.atomBonds(atom)) {
+      if (bond->getBondType() == Bond::ZERO ||
+          (!includeDativeBonds && bondIsDative(*bond))) {
         atomDegrees[i]--;
       }
-      ++beg;
     }
   }
 
@@ -1117,18 +1125,18 @@ int findSSSR(const ROMol &mol, VECT_INT_VECT &res) {
   return rdcast<int>(res.size());
 }
 
-int symmetrizeSSSR(ROMol &mol) {
+int symmetrizeSSSR(ROMol &mol, bool includeDativeBonds) {
   VECT_INT_VECT tmp;
-  return symmetrizeSSSR(mol, tmp);
+  return symmetrizeSSSR(mol, tmp, includeDativeBonds);
 };
 
-int symmetrizeSSSR(ROMol &mol, VECT_INT_VECT &res) {
+int symmetrizeSSSR(ROMol &mol, VECT_INT_VECT &res, bool includeDativeBonds) {
   res.clear();
   VECT_INT_VECT sssrs;
 
   // FIX: need to set flag here the symmetrization has been done in order to
   // avoid repeating this work
-  findSSSR(mol, sssrs);
+  findSSSR(mol, sssrs, includeDativeBonds);
 
   res.reserve(sssrs.size());
   for (const auto &r : sssrs) {
diff --git a/Code/GraphMol/MolDraw2D/MolDraw2D.cpp b/Code/GraphMol/MolDraw2D/MolDraw2D.cpp
index c53a9398bcf..640c3a2fbdc 100644
--- a/Code/GraphMol/MolDraw2D/MolDraw2D.cpp
+++ b/Code/GraphMol/MolDraw2D/MolDraw2D.cpp
@@ -81,8 +81,14 @@ void MolDraw2D::drawMolecule(const ROMol &mol, const std::string &legend,
                              const std::map<int, double> *highlight_radii,
                              int confId) {
   setupTextDrawer();
+  // we need ring info for drawing, so copy the molecule
+  // in order to add it
+  ROMol lmol(mol);
+  if (!lmol.getRingInfo()->isInitialized()) {
+    MolOps::symmetrizeSSSR(lmol);
+  }
   drawMols_.emplace_back(new MolDraw2D_detail::DrawMol(
-      mol, legend, panelWidth(), panelHeight(), drawOptions(), *text_drawer_,
+      lmol, legend, panelWidth(), panelHeight(), drawOptions(), *text_drawer_,
       highlight_atoms, highlight_bonds, highlight_atom_map, highlight_bond_map,
       nullptr, highlight_radii, supportsAnnotations(), confId));
   drawMols_.back()->setOffsets(x_offset_, y_offset_);
diff --git a/Code/GraphMol/MolOps.h b/Code/GraphMol/MolOps.h
index 6f66857d900..358244e30c7 100644
--- a/Code/GraphMol/MolOps.h
+++ b/Code/GraphMol/MolOps.h
@@ -688,6 +688,8 @@ RDKIT_GRAPHMOL_EXPORT void setHybridization(ROMol &mol);
   \param res used to return the vector of rings. Each entry is a vector with
       atom indices.  This information is also stored in the molecule's
       RingInfo structure, so this argument is optional (see overload)
+  \param includeDativeBonds - determines whether or not dative bonds are used in
+      the ring finding.
 
   \return number of smallest rings found
 
@@ -723,10 +725,12 @@ RDKIT_GRAPHMOL_EXPORT void setHybridization(ROMol &mol);
   done. The extra rings this process adds can be quite useful.
 */
 RDKIT_GRAPHMOL_EXPORT int findSSSR(const ROMol &mol,
-                                   std::vector<std::vector<int>> &res);
+                                   std::vector<std::vector<int>> &res,
+                                   bool includeDativeBonds = false);
 //! \overload
-RDKIT_GRAPHMOL_EXPORT int findSSSR(
-    const ROMol &mol, std::vector<std::vector<int>> *res = nullptr);
+RDKIT_GRAPHMOL_EXPORT int findSSSR(const ROMol &mol,
+                                   std::vector<std::vector<int>> *res = nullptr,
+                                   bool includeDativeBonds = false);
 
 //! use a DFS algorithm to identify ring bonds and atoms in a molecule
 /*!
@@ -756,6 +760,8 @@ RDKIT_GRAPHMOL_EXPORT void findRingFamilies(const ROMol &mol);
   \param res used to return the vector of rings. Each entry is a vector with
       atom indices.  This information is also stored in the molecule's
       RingInfo structure, so this argument is optional (see overload)
+  \param includeDativeBonds - determines whether or not dative bonds are used in
+      the ring finding.
 
   \return the total number of rings = (new rings + old SSSRs)
 
@@ -764,9 +770,11 @@ RDKIT_GRAPHMOL_EXPORT void findRingFamilies(const ROMol &mol);
   first
 */
 RDKIT_GRAPHMOL_EXPORT int symmetrizeSSSR(ROMol &mol,
-                                         std::vector<std::vector<int>> &res);
+                                         std::vector<std::vector<int>> &res,
+                                         bool includeDativeBonds = false);
 //! \overload
-RDKIT_GRAPHMOL_EXPORT int symmetrizeSSSR(ROMol &mol);
+RDKIT_GRAPHMOL_EXPORT int symmetrizeSSSR(ROMol &mol,
+                                         bool includeDativeBonds = false);
 
 //! @}
 
diff --git a/Code/GraphMol/Wrap/MolOps.cpp b/Code/GraphMol/Wrap/MolOps.cpp
index d199197ef9e..b592bda2887 100644
--- a/Code/GraphMol/Wrap/MolOps.cpp
+++ b/Code/GraphMol/Wrap/MolOps.cpp
@@ -306,9 +306,9 @@ ROMol *addHs(const ROMol &orig, bool explicitOnly, bool addCoords,
   return res;
 }
 
-VECT_INT_VECT getSSSR(ROMol &mol) {
+VECT_INT_VECT getSSSR(ROMol &mol, bool includeDativeBonds) {
   VECT_INT_VECT rings;
-  MolOps::findSSSR(mol, rings);
+  MolOps::findSSSR(mol, rings, includeDativeBonds);
   return rings;
 }
 
@@ -466,9 +466,9 @@ void setHybridizationMol(ROMol &mol) {
   MolOps::setHybridization(wmol);
 }
 
-VECT_INT_VECT getSymmSSSR(ROMol &mol) {
+VECT_INT_VECT getSymmSSSR(ROMol &mol, bool includeDativeBonds) {
   VECT_INT_VECT rings;
-  MolOps::symmetrizeSSSR(mol, rings);
+  MolOps::symmetrizeSSSR(mol, rings, includeDativeBonds);
   return rings;
 }
 PyObject *getDistanceMatrix(ROMol &mol, bool useBO = false,
@@ -1055,11 +1055,14 @@ struct molops_wrapper {
   ARGUMENTS:\n\
 \n\
     - mol: the molecule to use.\n\
+    - includeDativeBonds: whether or not dative bonds should be included in the ring finding.\n\
 \n\
   RETURNS: a sequence of sequences containing the rings found as atom ids\n\
          The length of this will be equal to NumBonds-NumAtoms+1 for single-fragment molecules.\n\
 \n";
-    python::def("GetSSSR", getSSSR, docString.c_str());
+    python::def("GetSSSR", getSSSR,
+                (python::arg("mol"), python::arg("includeDativeBonds") = false),
+                docString.c_str());
 
     // ------------------------------------------------------------------------
     docString =
@@ -1072,10 +1075,13 @@ struct molops_wrapper {
   ARGUMENTS:\n\
 \n\
     - mol: the molecule to use.\n\
+    - includeDativeBonds: whether or not dative bonds should be included in the ring finding.\n\
 \n\
   RETURNS: a sequence of sequences containing the rings found as atom ids\n\
 \n";
-    python::def("GetSymmSSSR", getSymmSSSR, docString.c_str());
+    python::def("GetSymmSSSR", getSymmSSSR,
+                (python::arg("mol"), python::arg("includeDativeBonds") = false),
+                docString.c_str());
 
     // ------------------------------------------------------------------------
     docString =
diff --git a/Code/GraphMol/catch_organometallics.cpp b/Code/GraphMol/catch_organometallics.cpp
index 98ca289bfc3..c6577ad9814 100644
--- a/Code/GraphMol/catch_organometallics.cpp
+++ b/Code/GraphMol/catch_organometallics.cpp
@@ -12,6 +12,7 @@
 
 #include <GraphMol/RWMol.h>
 #include <GraphMol/FileParsers/FileParsers.h>
+#include <GraphMol/SmilesParse/SmilesParse.h>
 #include <GraphMol/SmilesParse/SmilesWrite.h>
 
 using namespace RDKit;
@@ -128,3 +129,26 @@ TEST_CASE("get haptic bond end points") {
   endPts = MolOps::details::hapticBondEndpoints(bond);
   CHECK(std::vector<int>{} == endPts);
 }
+
+TEST_CASE("Rings and dative bonds") {
+  SECTION("ferrocene") {
+    auto m =
+        "C12->[Fe+2]3456789(<-C1=C->3[CH-]->4C->5=2)<-C1=C->6[CH-]->7C->8=C->91"_smiles;
+    REQUIRE(m);
+    std::vector<std::vector<int>> rings;
+    auto nrings = MolOps::symmetrizeSSSR(*m, rings);
+    CHECK(nrings == 2);
+    CHECK(rings[0].size() == 5);
+    CHECK(rings[1].size() == 5);
+  }
+}
+
+TEST_CASE("aromaticity and dative bonds") {
+  SECTION("ferrocene") {
+    auto m =
+        "C12->[Fe+2]3456789(<-C1=C->3[CH-]->4C->5=2)<-C1=C->6[CH-]->7C->8=C->91"_smiles;
+    REQUIRE(m);
+    CHECK(m->getAtomWithIdx(0)->getIsAromatic());
+    CHECK(!m->getAtomWithIdx(1)->getIsAromatic());
+  }
+}
\ No newline at end of file
diff --git a/ReleaseNotes.md b/ReleaseNotes.md
index ed8dcd520ab..e453374a97e 100644
--- a/ReleaseNotes.md
+++ b/ReleaseNotes.md
@@ -8,6 +8,8 @@
 ## Backwards incompatible changes
 
 - The ring-finding functions will now run even if the molecule already has ring information. Older versions of the RDKit would return whatever ring information was present, even if it had been generated using a different algorithm.
+- The ring-finding functions now no longer consider dative bonds as possible ring bonds by default. All of the ring-finding functions have a new optional argument `includeDativeBonds` which can be used to change this behavior
+- Generating 2D coordinates no longer has the side effect of running ring finding on molecules.
 - The canonical SMILES and CXSMILES generated for molecules with enhanced stereochemistry (stereo groups) is different than in previous releases. The enhanced stereochemistry information and the stereo groups themselves are now canonical. This does *not* affect molecules which do not have enhanced stereo and will not have any effect if you generate non-isomeric SMILES. This change also affects the output of the MolHash and RegistrationHash code when applied to molecules with enhanced stereo.
 - The doIsomericSmiles parameter in Java and C# ROMol.MolToSmiles() now defaults to true (previously it was false), thus aligning to the C++ and Python behavior.
 - Double bonds which are marked as crossed (i.e. `bond.GetBondDir() == Bond.BondDir.EITHERDOUBLE`) now have their BondStereo set to `Bond.BondStereo.STEREOANY` and the BondDir information removed by default when molecules are parsed or `AssignStereochemistry()` is called with the `cleanIt` argument set to True.