Repository: rdkit/rdkit — pull request #5542 (cpp)
Title: Fixes #5402

The problem here was a transformation (the "special imine r") which applied to this Kekule form of an aromatic substructure:
![image](https://user-images.githubusercontent.com/540511/188902044-f68acfc1-f92f-4aa2-bd85-d9738b29a1ff.png)
but not this one:
![image](https://user-images.githubusercontent.com/540511/188902091-2f518741-84ce-42ab-a193-4264ad8e5c5d.png)

This fix adds a new rule to detect and handle the second case. Along the way I also had to modify a bit of the logic for applying transforms to support transform rules with branched queries, but this is a simple change.

I wouldn't be horribly surprised to discover that there are more of this type of error in the default transformations, but it's non-trivial to identify them directly.

Resolved issues:
title: Tautomer enumeration removes stereochemistry depending on how aromatic rings are defined in SMILES
body: Here's a compact demonstration:
```
In [12]: m = Chem.MolFromSmiles("c1ccc([C@@H](CC2=NCCN2)c2ccccn2)cc1")

In [13]: nm = Chem.Mol(m)

In [14]: Chem.Kekulize(nm, True)

In [15]: Chem.MolToSmiles(enum.Canonicalize(m))
Out[15]: 'c1ccc([C@@H](CC2=NCCN2)c2ccccn2)cc1'

In [16]: Chem.MolToSmiles(enum.Canonicalize(nm))
Out[16]: 'c1ccc(C(CC2=NCCN2)c2ccccn2)cc1'
```

### Discussed in https://github.com/rdkit/rdkit/discussions/5397

<div type='discussions-op-text'>

<sup>Originally posted by **gedeck** July  1, 2022</sup>
Before I file an issue, I would like to make sure that it is one. The Tautomer enumerator behaves differently if the structure is loaded from a SMILES where the aromatic bonds are defined using kekule format or not.
```
from rdkit import Chem
from rdkit.Chem.MolStandardize import rdMolStandardize
from rdkit.Chem import Draw

enumerator = rdMolStandardize.TautomerEnumerator()

smiles = 'c1ccc([C@@H](CC2=NCCN2)c2ccccn2)cc1'
mol = Chem.MolFromSmiles(smiles)
Draw.MolsToGridImage([mol, enumerator.Canonicalize(mol)])
```
In this case, the stereochemistry of the carbon is preserved:

![image](https://user-images.githubusercontent.com/8720575/176784310-ec5c88fa-156a-4dbd-a6df-59ecf9c7ad2b.png)

If on the other hand, the aromatic rings are described with single and double bonds
```
smiles = 'C([C@H](C1=CC=CC=C1)C2=NC=CC=C2)C3=NCCN3'
mol = Chem.MolFromSmiles(smiles)
Draw.MolsToGridImage([mol, enumerator.Canonicalize(mol)])
```
the stereochemistry is removed.
![image](https://user-images.githubusercontent.com/8720575/176784328-44bdaa56-d3fd-4ace-a85a-0ce64f679aab.png)
</div>

Hints:

Fix patch:
diff --git a/Code/GraphMol/MolStandardize/Tautomer.cpp b/Code/GraphMol/MolStandardize/Tautomer.cpp
index 247f5fa7826..901a50f7abb 100644
--- a/Code/GraphMol/MolStandardize/Tautomer.cpp
+++ b/Code/GraphMol/MolStandardize/Tautomer.cpp
@@ -28,8 +28,6 @@
 #include <GraphMol/SmilesParse/SmartsWrite.h>
 #endif
 
-using namespace RDKit;
-
 namespace RDKit {
 
 namespace MolStandardize {
@@ -342,8 +340,10 @@ TautomerEnumeratorResult TautomerEnumerator::enumerate(const ROMol &mol) const {
 #ifdef VERBOSE_ENUMERATION
         std::string name;
         (transform.Mol)->getProp(common_properties::_Name, name);
+        SmilesWriteParams smilesWriteParams;
+        smilesWriteParams.allBondsExplicit = true;
         std::cout << "kmol for " << smilesTautomerPair.first << " : "
-                  << MolToSmiles(*kmol) << std::endl;
+                  << MolToSmiles(*kmol, smilesWriteParams) << std::endl;
         std::cout << "transform mol: " << MolToSmarts(*(transform.Mol))
                   << std::endl;
 
@@ -385,9 +385,11 @@ TautomerEnumeratorResult TautomerEnumerator::enumerate(const ROMol &mol) const {
           last->setNoImplicit(true);
           // Adjust bond orders
           unsigned int bi = 0;
-          for (size_t i = 0; i < match.size() - 1; ++i) {
-            Bond *bond = product->getBondBetweenAtoms(match[i].second,
-                                                      match[i + 1].second);
+          for (size_t i = 0; i < transform.Mol->getNumBonds(); ++i) {
+            const auto tbond = transform.Mol->getBondWithIdx(i);
+            Bond *bond = product->getBondBetweenAtoms(
+                match[tbond->getBeginAtomIdx()].second,
+                match[tbond->getEndAtomIdx()].second);
             ASSERT_INVARIANT(bond, "required bond not found");
             // check if bonds is specified in tautomer.in file
             if (!transform.BondTypes.empty()) {
@@ -424,6 +426,14 @@ TautomerEnumeratorResult TautomerEnumerator::enumerate(const ROMol &mol) const {
                                     transform.Charges[ci++]);
             }
           }
+#ifdef VERBOSE_ENUMERATION
+          {
+            SmilesWriteParams smilesWriteParams;
+            smilesWriteParams.allBondsExplicit = true;
+            std::cout << "pre-sanitize: "
+                      << MolToSmiles(*product, smilesWriteParams) << std::endl;
+          }
+#endif
 
           unsigned int failedOp;
           MolOps::sanitizeMol(*product, failedOp,
@@ -433,8 +443,10 @@ TautomerEnumeratorResult TautomerEnumerator::enumerate(const ROMol &mol) const {
                                   MolOps::SANITIZE_SETHYBRIDIZATION |
                                   MolOps::SANITIZE_ADJUSTHS);
 #ifdef VERBOSE_ENUMERATION
-          std::cout << "pre-setTautomerStereo: " << MolToSmiles(*product, true)
-                    << std::endl;
+          SmilesWriteParams smilesWriteParams;
+          smilesWriteParams.allBondsExplicit = true;
+          std::cout << "pre-setTautomerStereo: "
+                    << MolToSmiles(*product, smilesWriteParams) << std::endl;
 #endif
           setTautomerStereoAndIsoHs(mol, *product, res);
           tsmiles = MolToSmiles(*product, true);
@@ -472,7 +484,8 @@ TautomerEnumeratorResult TautomerEnumerator::enumerate(const ROMol &mol) const {
             std::cout << "New tautomer replaced for ";
           }
           std::cout << tsmiles << ", taut: " << MolToSmiles(*product)
-                    << ", kek: " << MolToSmiles(*kekulized_product, true, true)
+                    << ", kek: "
+                    << MolToSmiles(*kekulized_product, smilesWriteParams)
                     << std::endl;
 #endif
           // BOOST_LOG(rdInfoLog)
diff --git a/Code/GraphMol/MolStandardize/TautomerCatalog/tautomerTransforms.in b/Code/GraphMol/MolStandardize/TautomerCatalog/tautomerTransforms.in
index 2ac09548b6f..4ea7850b305 100644
--- a/Code/GraphMol/MolStandardize/TautomerCatalog/tautomerTransforms.in
+++ b/Code/GraphMol/MolStandardize/TautomerCatalog/tautomerTransforms.in
@@ -40,6 +40,9 @@ const std::vector<
         std::make_tuple(std::string("special imine r"),
                         std::string("[Cz0R0X4!H0]-[c]=[nz0]"), std::string(""),
                         std::string("")),
+        std::make_tuple(std::string("special imine r2"),
+                        std::string("[Cz0R0X4!H0]-[cr6](=[cr6])-[nr6H0z0]"),
+                        std::string("==-"), std::string("")),
         std::make_tuple(std::string("1,3 aromatic heteroatom H shift f"),
                         std::string("[#7+0!H0]-[#6R1]=[O,#7X2+0]"),
                         std::string(""), std::string("")),


Test patch:
diff --git a/Code/GraphMol/MolStandardize/catch_tests.cpp b/Code/GraphMol/MolStandardize/catch_tests.cpp
index 1307c0e678a..19331afd7c2 100644
--- a/Code/GraphMol/MolStandardize/catch_tests.cpp
+++ b/Code/GraphMol/MolStandardize/catch_tests.cpp
@@ -978,3 +978,50 @@ TEST_CASE("Github #5320: cleanup() and stereochemistry") {
     CHECK(MolToSmiles(*m2) == "O=C([O-])C(O)(O)Cl");
   }
 }
+
+TEST_CASE("Github #5402: order dependence of tautomer transforms") {
+  SECTION("as-reported") {
+    MolStandardize::TautomerEnumerator te;
+
+    auto m1 = "c1ccc([C@@H](CC2=NCCN2)c2ccccn2)cc1"_smiles;
+    REQUIRE(m1);
+    auto m2 = "C([C@H](C1=CC=CC=C1)C2=NC=CC=C2)C3=NCCN3"_smiles;
+    REQUIRE(m2);
+    std::cerr << " * - * - * - * m1" << std::endl;
+    std::unique_ptr<ROMol> res1{te.canonicalize(*m1)};
+    REQUIRE(res1);
+    std::cerr << " * - * - * - * m2" << std::endl;
+    std::unique_ptr<ROMol> res2{te.canonicalize(*m2)};
+    REQUIRE(res2);
+    CHECK(MolToSmiles(*res1) == MolToSmiles(*res2));
+  }
+  SECTION("zoom") {
+    MolStandardize::CleanupParameters params;
+    const std::vector<
+        std::tuple<std::string, std::string, std::string, std::string>>
+        tTransforms{
+            std::make_tuple(std::string("special imine r1"),
+                            std::string("[Cz0R0X4!H0]-[c]=[nz0]"),
+                            std::string(""), std::string("")),
+            std::make_tuple(std::string("special imine r2"),
+                            std::string("[Cz0R0X4!H0]-[c](=c)-[nz0]"),
+                            std::string("==-"), std::string("")),
+        };
+
+    params.tautomerTransformData = tTransforms;
+    MolStandardize::TautomerEnumerator te(params);
+
+    auto m1 = "c1ccc([C@@H](CC2=NCCN2)c2ccccn2)cc1"_smiles;
+    REQUIRE(m1);
+    auto m2 = "C([C@H](C1=CC=CC=C1)C2=NC=CC=C2)C3=NCCN3"_smiles;
+    REQUIRE(m2);
+
+    std::cerr << " * - * - * - * m1" << std::endl;
+    std::unique_ptr<ROMol> res1{te.canonicalize(*m1)};
+    REQUIRE(res1);
+    std::cerr << " * - * - * - * m2" << std::endl;
+    std::unique_ptr<ROMol> res2{te.canonicalize(*m2)};
+    REQUIRE(res2);
+    CHECK(MolToSmiles(*res1) == MolToSmiles(*res2));
+  }
+}
diff --git a/Code/GraphMol/MolStandardize/testTautomer.cpp b/Code/GraphMol/MolStandardize/testTautomer.cpp
index c07030085d9..b596d148b9a 100644
--- a/Code/GraphMol/MolStandardize/testTautomer.cpp
+++ b/Code/GraphMol/MolStandardize/testTautomer.cpp
@@ -712,7 +712,7 @@ void testCanonicalize() {
       std::unique_ptr<TautomerCatalogParams>(new TautomerCatalogParams(""));
 
   unsigned int ntransforms = tautparams->getTransforms().size();
-  TEST_ASSERT(ntransforms == 36);
+  TEST_ASSERT(ntransforms == 37);
 
   TautomerEnumerator te(new TautomerCatalog(tautparams.get()));
 
@@ -734,7 +734,7 @@ void testPickCanonical() {
       std::unique_ptr<TautomerCatalogParams>(new TautomerCatalogParams(""));
 
   unsigned int ntransforms = tautparams->getTransforms().size();
-  TEST_ASSERT(ntransforms == 36);
+  TEST_ASSERT(ntransforms == 37);
 
   TautomerEnumerator te(new TautomerCatalog(tautparams.get()));
 
@@ -759,7 +759,7 @@ void testCustomScoreFunc() {
       std::unique_ptr<TautomerCatalogParams>(new TautomerCatalogParams(""));
 
   unsigned int ntransforms = tautparams->getTransforms().size();
-  TEST_ASSERT(ntransforms == 36);
+  TEST_ASSERT(ntransforms == 37);
 
   TautomerEnumerator te(new TautomerCatalog(tautparams.get()));
 
@@ -828,7 +828,7 @@ void testEnumerationProblems() {
       std::unique_ptr<TautomerCatalogParams>(new TautomerCatalogParams(""));
 
   unsigned int ntransforms = tautparams->getTransforms().size();
-  TEST_ASSERT(ntransforms == 36);
+  TEST_ASSERT(ntransforms == 37);
 
   TautomerEnumerator te(new TautomerCatalog(tautparams.get()));
 #if 1
@@ -860,7 +860,7 @@ void testPickCanonical2() {
   auto tautparams =
       std::unique_ptr<TautomerCatalogParams>(new TautomerCatalogParams(""));
   unsigned int ntransforms = tautparams->getTransforms().size();
-  TEST_ASSERT(ntransforms == 36);
+  TEST_ASSERT(ntransforms == 37);
 
   TautomerEnumerator te(new TautomerCatalog(tautparams.get()));
   {
@@ -894,7 +894,7 @@ void testEnumerateDetails() {
   auto tautparams =
       std::unique_ptr<TautomerCatalogParams>(new TautomerCatalogParams(""));
   unsigned int ntransforms = tautparams->getTransforms().size();
-  TEST_ASSERT(ntransforms == 36);
+  TEST_ASSERT(ntransforms == 37);
   TautomerEnumerator te(new TautomerCatalog(tautparams.get()));
   {
     auto mol = "c1ccccc1CN=c1[nH]cccc1"_smiles;
@@ -950,7 +950,7 @@ void testGithub2990() {
       std::unique_ptr<TautomerCatalogParams>(new TautomerCatalogParams(""));
 
   unsigned int ntransforms = tautparams->getTransforms().size();
-  TEST_ASSERT(ntransforms == 36);
+  TEST_ASSERT(ntransforms == 37);
   TautomerEnumerator te(new TautomerCatalog(tautparams.get()));
   {
     // atom stereo