Repository: rdkit/rdkit — pull request #7564 (cpp)
Title: Fixes #7552

This adds code to recognize tetrahedral centers which are involved in more than one ring with < members and be more tolerant when checking their geometries.

Resolved issues:
title: AllChem.EmbedMolecule returns -1
body: Howdy,

I want to get the 3D conformation information for any given molecule; however, some molecules fail to produce the conformer. Following is the code that reproduces the issue.

`from rdkit import Chem`
`from rdkit.Chem import AllChem`
`sample_smiles = 'O=CCC1OC2COC12'`
`mol = Chem.AddHs(Chem.MolFromSmiles(sample_smiles))`
`AllChem.EmbedMolecule(mol) # -1`

If the molecule can generate conformation information, `AllChem.EmbedMolecule()` would return 0; I have identified many smiles/molecules that result in -1 when calling `AllChem.EmbedMolecule(mol)` , you may find some of those smiles in the picture below.

![image](https://github.com/rdkit/rdkit/assets/40327643/bab1a067-2e7d-492e-b4fc-782ab9260f5e)

Appreciate any suggestions! 



Hints:

Fix patch:
diff --git a/Code/DistGeom/ChiralSet.h b/Code/DistGeom/ChiralSet.h
index 6b9a077dfe6..a1b26035cb0 100644
--- a/Code/DistGeom/ChiralSet.h
+++ b/Code/DistGeom/ChiralSet.h
@@ -17,6 +17,11 @@
 
 namespace DistGeom {
 
+enum class ChiralSetStructureFlags : std::uint64_t {
+  IN_FUSED_SMALL_RINGS =
+      1 << 0,  // a chiral center involved in fusing 2 or more small rings
+};
+
 /*! \brief Class used to store a quartet of points and chiral volume bounds on
  *them
  *
@@ -30,17 +35,20 @@ class RDKIT_DISTGEOMETRY_EXPORT ChiralSet {
   unsigned int d_idx4;
   double d_volumeLowerBound;
   double d_volumeUpperBound;
+  std::uint64_t d_structureFlags =
+      0;  // used to track special cases that may make embedding more difficult
 
   ChiralSet(unsigned int pid0, unsigned int pid1, unsigned int pid2,
             unsigned int pid3, unsigned int pid4, double lowerVolBound,
-            double upperVolBound)
+            double upperVolBound, std::uint64_t structureFlags = 0)
       : d_idx0(pid0),
         d_idx1(pid1),
         d_idx2(pid2),
         d_idx3(pid3),
         d_idx4(pid4),
         d_volumeLowerBound(lowerVolBound),
-        d_volumeUpperBound(upperVolBound) {
+        d_volumeUpperBound(upperVolBound),
+        d_structureFlags(structureFlags) {
     CHECK_INVARIANT(lowerVolBound <= upperVolBound, "Inconsistent bounds\n");
     d_volumeLowerBound = lowerVolBound;
     d_volumeUpperBound = upperVolBound;
diff --git a/Code/GraphMol/DistGeomHelpers/Embedder.cpp b/Code/GraphMol/DistGeomHelpers/Embedder.cpp
index 1ce159a81df..ee97ccde868 100644
--- a/Code/GraphMol/DistGeomHelpers/Embedder.cpp
+++ b/Code/GraphMol/DistGeomHelpers/Embedder.cpp
@@ -308,12 +308,21 @@ bool _volumeTest(const DistGeom::ChiralSetPtr &chiralSet,
   RDGeom::Point3D v4 = p0 - p4;
   v4.normalize();
 
+  // be more tolerant of tethrahedral centers that are involved in multiple
+  // small rings
+  double volScale = 1;
+  if (chiralSet->d_structureFlags &
+      static_cast<std::uint64_t>(
+          DistGeom::ChiralSetStructureFlags::IN_FUSED_SMALL_RINGS)) {
+    volScale = 0.25;
+  }
+
   RDGeom::Point3D crossp = v1.crossProduct(v2);
   double vol = crossp.dotProduct(v3);
   if (verbose) {
     std::cerr << "   " << fabs(vol) << std::endl;
   }
-  if (fabs(vol) < MIN_TETRAHEDRAL_CHIRAL_VOL) {
+  if (fabs(vol) < volScale * MIN_TETRAHEDRAL_CHIRAL_VOL) {
     return false;
   }
   crossp = v1.crossProduct(v2);
@@ -321,7 +330,7 @@ bool _volumeTest(const DistGeom::ChiralSetPtr &chiralSet,
   if (verbose) {
     std::cerr << "   " << fabs(vol) << std::endl;
   }
-  if (fabs(vol) < MIN_TETRAHEDRAL_CHIRAL_VOL) {
+  if (fabs(vol) < volScale * MIN_TETRAHEDRAL_CHIRAL_VOL) {
     return false;
   }
   crossp = v1.crossProduct(v3);
@@ -329,7 +338,7 @@ bool _volumeTest(const DistGeom::ChiralSetPtr &chiralSet,
   if (verbose) {
     std::cerr << "   " << fabs(vol) << std::endl;
   }
-  if (fabs(vol) < MIN_TETRAHEDRAL_CHIRAL_VOL) {
+  if (fabs(vol) < volScale * MIN_TETRAHEDRAL_CHIRAL_VOL) {
     return false;
   }
   crossp = v2.crossProduct(v3);
@@ -337,7 +346,7 @@ bool _volumeTest(const DistGeom::ChiralSetPtr &chiralSet,
   if (verbose) {
     std::cerr << "   " << fabs(vol) << std::endl;
   }
-  return fabs(vol) >= MIN_TETRAHEDRAL_CHIRAL_VOL;
+  return fabs(vol) >= volScale * MIN_TETRAHEDRAL_CHIRAL_VOL;
 }
 
 bool _sameSide(const RDGeom::Point3D &v1, const RDGeom::Point3D &v2,
@@ -1035,19 +1044,33 @@ void findChiralSets(const ROMol &mol, DistGeom::VECT_CHIRALSET &chiralCenters,
           nbrs.insert(nbrs.end(), atom->getIdx());
         }
 
+        // set a flag for tetrahedral centers that are in multiple small rings
+        auto numSmallRings = 0u;
+        constexpr int smallRingSize = 5;
+        for (const auto sz : mol.getRingInfo()->atomRingSizes(atom->getIdx())) {
+          if (sz < smallRingSize) {
+            ++numSmallRings;
+          }
+        }
+        std::uint64_t structureFlags = 0;
+        if (numSmallRings > 1) {
+          structureFlags = static_cast<std::uint64_t>(
+              DistGeom::ChiralSetStructureFlags::IN_FUSED_SMALL_RINGS);
+        }
+
         // now create a chiral set and set the upper and lower bound on the
         // volume
         if (chiralType == Atom::CHI_TETRAHEDRAL_CCW) {
           // positive chiral volume
-          auto *cset =
-              new DistGeom::ChiralSet(atom->getIdx(), nbrs[0], nbrs[1], nbrs[2],
-                                      nbrs[3], volLowerBound, volUpperBound);
+          auto *cset = new DistGeom::ChiralSet(atom->getIdx(), nbrs[0], nbrs[1],
+                                               nbrs[2], nbrs[3], volLowerBound,
+                                               volUpperBound, structureFlags);
           DistGeom::ChiralSetPtr cptr(cset);
           chiralCenters.push_back(cptr);
         } else if (chiralType == Atom::CHI_TETRAHEDRAL_CW) {
-          auto *cset =
-              new DistGeom::ChiralSet(atom->getIdx(), nbrs[0], nbrs[1], nbrs[2],
-                                      nbrs[3], -volUpperBound, -volLowerBound);
+          auto *cset = new DistGeom::ChiralSet(atom->getIdx(), nbrs[0], nbrs[1],
+                                               nbrs[2], nbrs[3], -volUpperBound,
+                                               -volLowerBound, structureFlags);
           DistGeom::ChiralSetPtr cptr(cset);
           chiralCenters.push_back(cptr);
         } else {
@@ -1059,8 +1082,9 @@ void findChiralSets(const ROMol &mol, DistGeom::VECT_CHIRALSET &chiralCenters,
             // the coordMap
             // there's no sense doing 3-rings because those are a nightmare
           } else {
-            auto *cset = new DistGeom::ChiralSet(
-                atom->getIdx(), nbrs[0], nbrs[1], nbrs[2], nbrs[3], 0.0, 0.0);
+            auto *cset = new DistGeom::ChiralSet(atom->getIdx(), nbrs[0],
+                                                 nbrs[1], nbrs[2], nbrs[3], 0.0,
+                                                 0.0, structureFlags);
             DistGeom::ChiralSetPtr cptr(cset);
             tetrahedralCenters.push_back(cptr);
           }


Test patch:
diff --git a/Code/GraphMol/DistGeomHelpers/catch_tests.cpp b/Code/GraphMol/DistGeomHelpers/catch_tests.cpp
index 530c4a6f282..3eae46fa321 100644
--- a/Code/GraphMol/DistGeomHelpers/catch_tests.cpp
+++ b/Code/GraphMol/DistGeomHelpers/catch_tests.cpp
@@ -985,4 +985,34 @@ TEST_CASE("terminal groups in pruning") {
       CHECK(cids.size() >= 2);
     }
   }
+}
+
+TEST_CASE("github #7552") {
+  DGeomHelpers::EmbedParameters ps = DGeomHelpers::ETKDGv3;
+  ps.randomSeed = 0xf00d;
+  SECTION("as reported") {
+    auto mol = "O=CCC1OC2COC12"_smiles;
+    REQUIRE(mol);
+    MolOps::addHs(*mol);
+    CHECK(DGeomHelpers::EmbedMolecule(*mol, ps) == 0);
+  }
+  SECTION("as reported, bulk") {
+    std::vector<std::string> smileses{
+        "O=CCC1OC2COC12",      "O=C1OC2CCC12C#N",    "CC1C2CC3OC2C13O",
+        "CC12CC1C3(C)OCC23",   "OC1C2COC13COC23",    "OC1C2C3C2N4C3CC14",
+        "CC1OC12C3CC2(O)C3",   "OC1C2CC3C2CCC13",    "CN1CC2(O)C3CC3C12",
+        "C1OC2C3C4C5C4C12N35", "C1OC2CC3OC12C=C3",   "C1C2OC3C1OC23",
+        "CC1(O)CC2CCC12",      "CC12NC(=O)C1C3OC23", "OC1CC2(NCCC12)C#N",
+        "CC12C3C1C(=O)C3C2O",  "C1C=C2C3OC4C3N1C24", "CC12C3C1C4=NC3C2O4",
+        "C1OC23C=CC4C2N4C13",  "OCC12CNC1C(=O)N2",   "CC1C2C3C1C(C#C)n23",
+
+    };
+    for (const auto &smiles : smileses) {
+      INFO(smiles);
+      auto mol = v2::SmilesParse::MolFromSmiles(smileses[0]);
+      REQUIRE(mol);
+      MolOps::addHs(*mol);
+      CHECK(DGeomHelpers::EmbedMolecule(*mol, ps) == 0);
+    }
+  }
 }
\ No newline at end of file