Repository: rdkit/rdkit — pull request #8192 (cpp)
Title: Simple fix to allow aromatic CIS/TRANS SMARTS to match molecules

Fixes #7944

This converts smarts / and \ to match single or aromatic bonds with the appropriate bond direction.

Resolved issues:
title: rxn.RunReactants mismatch directional bonds in a heterocycle ring
body: **Describe the bug**
rxn.RunReactants mismatch directional bonds in a heterocycle ring when two aromatic atoms are connected by directional bonds.

**To Reproduce**
from rdkit import Chem
from rdkit.Chem import rdChemReactions

`rxn = rdChemReactions.ReactionFromSmarts("[O:1]=[c:2]1/[c:3](=[C:4]/[c:5]2:[c:10]:[c:9]:[c:8]:[c:7]:[c:6]:2):[s:11]:[c:12]2:[n:13]:1-[N:14]-[C:15]-[N:20]-[N:21]=2>>[S:11]=[C:12]1-[N:13]-[N:14]-[C:15]-[N:20]-[N:21]-1.[O]=[C:4]-[c:5]1:[c:10]:[c:9]:[c:8]:[c:7]:[c:6]:1.[C]-[C]-[O]-[C:2](=[O:1])-[C:3]-[Cl]")`

`reacts = [Chem.MolFromSmiles('[O:1]=[c:2]1/[c:3](=[CH:4]/[c:5]2[cH:6][cH:7][cH:8][cH:9][cH:10]2)[s:11][c:12]2[n:13]1[NH:14][C:15]1([CH2:16][CH2:17][CH2:18][CH2:19]1)[NH:20][N:21]=2')]`

`products = rxn.RunReactants(reacts)`
`len(products)`
 0

`rxn.GetReactants()[0].GetBondWithIdx(1).GetBondType()`
 rdkit.Chem.rdchem.BondType.SINGLE
`reacts[0].GetBondWithIdx(1).GetBondType()`
 rdkit.Chem.rdchem.BondType.AROMATIC


**Expected behavior**
The rxn is supposed to work for reactants and yield products. Bond 2-3 is the same directional bond by aromatic atoms ([c:2]1/[c:3]) for both rxn and reacts.


**Configuration (please complete the following information):**
 - RDKit version: 2024.03.4
 - OS: Windows
 - Python version (if relevant): 3.12.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? pip install


Hints:

Fix patch:
diff --git a/Code/GraphMol/SmilesParse/SmartsWrite.cpp b/Code/GraphMol/SmilesParse/SmartsWrite.cpp
index 77065da7a96..6446a99a103 100644
--- a/Code/GraphMol/SmilesParse/SmartsWrite.cpp
+++ b/Code/GraphMol/SmilesParse/SmartsWrite.cpp
@@ -410,7 +410,19 @@ std::string getBondSmartsSimple(const Bond *bond,
   } else if (descrip == "BondInRing") {
     res += "@";
   } else if (descrip == "SingleOrAromaticBond") {
-    // don't need to do anything here... :-)
+    auto dir = bond->getBondDir();
+    switch(dir) {
+      case Bond::ENDDOWNRIGHT: {
+        res += "\\";
+        break;
+      }
+      case Bond::ENDUPRIGHT: {
+        res += "/";
+        break;
+      }
+      default:
+        break;
+    }
   } else if (descrip == "SingleOrDoubleBond") {
     res += "-,=";
   } else if (descrip == "DoubleOrAromaticBond") {
@@ -1040,4 +1052,4 @@ std::string MolFragmentToCXSmarts(const ROMol &mol,
   return res;
 }
 
-}  // namespace RDKit
\ No newline at end of file
+}  // namespace RDKit
diff --git a/Code/GraphMol/SmilesParse/lex.yysmarts.cpp.cmake b/Code/GraphMol/SmilesParse/lex.yysmarts.cpp.cmake
index bec2ec0a604..c6ff0e64ef6 100644
--- a/Code/GraphMol/SmilesParse/lex.yysmarts.cpp.cmake
+++ b/Code/GraphMol/SmilesParse/lex.yysmarts.cpp.cmake
@@ -1733,6 +1733,7 @@ YY_RULE_SETUP
 #line 362 "smarts.ll"
 { yylval->bond = new QueryBond(Bond::SINGLE);
 	yylval->bond->setBondDir(Bond::ENDDOWNRIGHT);
+	yylval->bond->setQuery(makeSingleOrAromaticBondQuery());
 	return BOND_TOKEN;  }
 	YY_BREAK
 case 153:
@@ -1740,6 +1741,7 @@ YY_RULE_SETUP
 #line 366 "smarts.ll"
 { yylval->bond = new QueryBond(Bond::SINGLE);
 	yylval->bond->setBondDir(Bond::ENDUPRIGHT);
+	yylval->bond->setQuery(makeSingleOrAromaticBondQuery());
 	return BOND_TOKEN;  }
 	YY_BREAK
 case 154:
diff --git a/Code/GraphMol/SmilesParse/smarts.ll b/Code/GraphMol/SmilesParse/smarts.ll
index 5de37432b14..874a5a8a43b 100644
--- a/Code/GraphMol/SmilesParse/smarts.ll
+++ b/Code/GraphMol/SmilesParse/smarts.ll
@@ -360,10 +360,12 @@ A			{
 
 [\\]{1,2}    { yylval->bond = new QueryBond(Bond::SINGLE);
 	yylval->bond->setBondDir(Bond::ENDDOWNRIGHT);
+	yylval->bond->setQuery(makeSingleOrAromaticBondQuery());
 	return BOND_TOKEN;  }
 
 [\/]    { yylval->bond = new QueryBond(Bond::SINGLE);
 	yylval->bond->setBondDir(Bond::ENDUPRIGHT);
+	yylval->bond->setQuery(makeSingleOrAromaticBondQuery());	
 	return BOND_TOKEN;  }
 
 \-\> {


Test patch:
diff --git a/Code/GraphMol/SmilesParse/smatest.cpp b/Code/GraphMol/SmilesParse/smatest.cpp
index 379e77978e7..54c4098869f 100644
--- a/Code/GraphMol/SmilesParse/smatest.cpp
+++ b/Code/GraphMol/SmilesParse/smatest.cpp
@@ -1167,6 +1167,16 @@ void testSmartsStereochem() {
   _checkMatches("C/C=C/C", "C/C=C/C", 1, 4);
   _checkMatches("C/C=C/C", "C\\C=C\\C", 1, 4);
   _checkMatches("C/C=C/C", "C/C=C\\C", 1, 4);
+  
+  // directional bonds are set to be a direction \ /
+  //  and a query - SingleOrAromatic, make sure that this
+  //  is their current representation
+  auto m1 = "C/C=C\\C"_smarts;
+  TEST_ASSERT(m1->getBondWithIdx(0)->hasQuery());
+  TEST_ASSERT(m1->getBondWithIdx(0)->getQuery()->getDescription() == "SingleOrAromaticBond");
+
+  auto m2 = "O=c1/c(=C/c2ccccc2)sc2n1-N-C-N-N=2"_smarts;
+  TEST_ASSERT(MolToSmarts(*m2) == "O=c1/c(=C/c2ccccc2)sc2n1-N-C-N-N=2");
 
   BOOST_LOG(rdInfoLog) << "\tdone" << std::endl;
 }
diff --git a/Code/GraphMol/Substruct/catch_tests.cpp b/Code/GraphMol/Substruct/catch_tests.cpp
index 0c007677a52..cbf014fded4 100644
--- a/Code/GraphMol/Substruct/catch_tests.cpp
+++ b/Code/GraphMol/Substruct/catch_tests.cpp
@@ -840,4 +840,18 @@ M  END)CTAB"_ctab;
     CHECK(SubstructMatch(*m_single, *m_aromatic, ps).size() == 1);
     CHECK(SubstructMatch(*m_aromatic, *m_single, ps).size() == 1);
   }
-}
\ No newline at end of file
+}
+
+TEST_CASE("Github #7295", "CIS/TRANS in aromatic ring") {
+    SECTION("Smart CIS/TRANS bonds should match single or aromatic"){
+      SubstructMatchParameters ps;
+
+      auto query = "[O:1]=[c:2]1/[c:3](=[C:4]/[c:5]2:[c:10]:[c:9]:[c:8]:[c:7]:[c:6]:2):[s:11]:[c:12]2:[n:13]:1-[N:14]-[C:15]-[N:20]-[N:21]=2"_smarts;
+      auto mol = "[O:1]=[c:2]1/[c:3](=[CH:4]/[c:5]2[cH:6][cH:7][cH:8][cH:9][cH:10]2)[s:11][c:12]2[n:13]1[NH:14][C:15]1([CH2:16][CH2:17][CH2:18][CH2:19]1)[NH:20][N:21]=2"_smiles;
+      CHECK(SubstructMatch(*mol, *query, ps).size() == 1);
+      ps.useChirality = true;
+      CHECK(SubstructMatch(*mol, *query, ps).size() == 1);
+      auto mol2 = "[O:1]=[c:2]1\\[c:3](=[CH:4]/[c:5]2[cH:6][cH:7][cH:8][cH:9][cH:10]2)[s:11][c:12]2[n:13]1[NH:14][C:15]1([CH2:16][CH2:17][CH2:18][CH2:19]1)[NH:20][N:21]=2"_smiles;
+      CHECK(SubstructMatch(*mol2, *query, ps).size() == 0);
+    }
+}