Repository: rdkit/rdkit — pull request #4190 (cpp)
Title: fixes #3967

Reference Issue
fixes #3967

What does this implement/fix? Explain your changes.

Seems like the two reported smiles issues are caused by two different bugs.

For "C=c1s/c2n(c1=O)CCCCCCC\\N=2" the bug seems like it's cause by the ring closure happening on the double bond. This condition is checked on line 384 in Canon.cpp

I struggled quite a bit figuring out exactly why "C1=C\\C/C=C2C3=C/C/C=C\\C=C/C\\3C\\2\\C=C/1" isn't working. It seems like it's a similar issue to the first one, but only because "secondFromAtom1" is not aromatic and is closing the ring. The condition for checking this is on lines 385-387 in Canon.cpp

I wanted to refactor this line into an inline function since it is use many times, but I didn't want to make this PR too big.
atom2Dir = (atom2Dir == Bond::ENDUPRIGHT) ? Bond::ENDDOWNRIGHT
                                                : Bond::ENDUPRIGHT;



Resolved issues:
title: Double bond stereo gets flipped by SMILES reader/writer
body: **Describe the bug**
Double bond stereo is flipped if you roundtrip a SMILES and double bond is located at the ring closure in canonical SMILES

**To Reproduce**
```python
print(Chem.CanonSmiles('C=c1s/c2n(c1=O)CCCCCCC\\N=2'))
```
Output: `C=c1s/c2n(c1=O)CCCCCCC/N=2`

**Expected behavior**
Double bond stereo should be retained

**Screenshots**
![image](https://user-images.githubusercontent.com/2325645/112114703-a695a380-8bb8-11eb-9b5c-85ec99315ad5.png)

**Configuration (please complete the following information):**
 - RDKit version: 2020.09.5
 - OS: MacOS
 - Python version (if relevant): 3.8
 - Are you using conda? Yes
 - If you are using conda, which channel did you install the rdkit from? conda-forge
 - If you are not using conda: how did you install the RDKit?


Hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index c4bcb9015dc..3c1ac56259b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,8 @@
 #- Eclipse files
 /.project
 /.pydevproject
+/.cproject
+/.settings/
 
 #- CLion files
 /cmake-build-*
diff --git a/Code/GraphMol/Canon.cpp b/Code/GraphMol/Canon.cpp
index 542a563e16e..6d83b20547b 100644
--- a/Code/GraphMol/Canon.cpp
+++ b/Code/GraphMol/Canon.cpp
@@ -350,10 +350,25 @@ void canonicalizeDoubleBond(Bond *dblBond, UINT_VECT &bondVisitOrders,
     // If we're not looking at the bonds used to determine the
     // stereochemistry, we need to flip the setting on the other bond:
     const INT_VECT &stereoAtoms = dblBond->getStereoAtoms();
+
+    auto isClosingRingBond = [](Bond *bond) {
+      if (bond == nullptr) {
+        return false;
+      }
+      auto beginIdx = bond->getBeginAtomIdx();
+      auto endIdx = bond->getEndAtomIdx();
+      return beginIdx > endIdx && 
+             beginIdx - endIdx > 1 &&
+             bond->hasProp(common_properties::_TraversalRingClosureBond);
+    };
+
+    auto isFlipped = false;
+
     if (atom1->getDegree() == 3 &&
         std::find(stereoAtoms.begin(), stereoAtoms.end(),
                   static_cast<int>(atom1ControllingBond->getOtherAtomIdx(
                       atom1->getIdx()))) == stereoAtoms.end()) {
+      isFlipped = true;
       atom2Dir = (atom2Dir == Bond::ENDUPRIGHT) ? Bond::ENDDOWNRIGHT
                                                 : Bond::ENDUPRIGHT;
     }
@@ -363,6 +378,17 @@ void canonicalizeDoubleBond(Bond *dblBond, UINT_VECT &bondVisitOrders,
         std::find(stereoAtoms.begin(), stereoAtoms.end(),
                   static_cast<int>(firstFromAtom2->getOtherAtomIdx(
                       atom2->getIdx()))) == stereoAtoms.end()) {
+      isFlipped = true;
+      atom2Dir = (atom2Dir == Bond::ENDUPRIGHT) ? Bond::ENDDOWNRIGHT
+                                                : Bond::ENDUPRIGHT;
+    } 
+    
+    if (!isFlipped && 
+          (isClosingRingBond(dblBond) ||
+          (isClosingRingBond(secondFromAtom1) && 
+            !secondFromAtom1->getIsAromatic() && 
+            secondFromAtom1->getBondDir() != Bond::NONE))
+        ) {
       atom2Dir = (atom2Dir == Bond::ENDUPRIGHT) ? Bond::ENDDOWNRIGHT
                                                 : Bond::ENDUPRIGHT;
     }


Test patch:
diff --git a/Code/GraphMol/SmilesParse/test.cpp b/Code/GraphMol/SmilesParse/test.cpp
index 4a69cda451c..4e272cb66b0 100644
--- a/Code/GraphMol/SmilesParse/test.cpp
+++ b/Code/GraphMol/SmilesParse/test.cpp
@@ -4339,6 +4339,27 @@ void testOSSFuzzFailures() {
   BOOST_LOG(rdInfoLog) << "done" << std::endl;
 }
 
+void testGithub3967() {
+  BOOST_LOG(rdInfoLog) << "-------------------------------------" << std::endl;
+  BOOST_LOG(rdInfoLog) << "Testing Github Issue 3967: Double bond stereo gets "
+                          "flipped by SMILES reader/writer"
+                       << std::endl;
+
+  {
+    auto mol = "C=c1s/c2n(c1=O)CCCCCCC\\N=2"_smiles;
+    TEST_ASSERT(mol);
+    auto smi = MolToSmiles(*mol);
+    TEST_ASSERT(smi == "C=c1s/c2n(c1=O)CCCCCCC\\N=2");
+  }
+  {
+    auto mol = "C1=C\\C/C=C2C3=C/C/C=C\\C=C/C\\3C\\2\\C=C/1"_smiles;
+    TEST_ASSERT(mol);
+    auto smi = MolToSmiles(*mol);
+    TEST_ASSERT(smi == "C1=C\\C/C=C2C3=C/C/C=C\\C=C/C\\3C\\2\\C=C/1");
+  }
+  BOOST_LOG(rdInfoLog) << "\tdone" << std::endl;
+}
+
 int main(int argc, char *argv[]) {
   (void)argc;
   (void)argv;
@@ -4417,6 +4438,7 @@ int main(int argc, char *argv[]) {
   testdoRandomSmileGeneration();
   testGithub1028();
   testGithub3139();
+  testGithub3967();
 #endif
   testOSSFuzzFailures();
 }