Repository: rdkit/rdkit — pull request #4207 (cpp)
Title: Fixes #4114

This closes #4114 

Besides the bond properties, also EITHERDOUBLE Bond Dir was not being copied over to reaction products.

This enables copying bond properties and EITHERDOUBLE for reactant bonds that are not specified in the product template, or are specified in the product template as "Null Bond".


Resolved issues:
title: Reactions don't propagate bond properties
body: I'm using a custom build of RDKit 2021.03 on Mac.

It looks like RDKit reactions don't preserve bond properties from the reactants. Is this intentional?


```
from rdkit import Chem
from rdkit.Chem import rdChemReactions

rxn = rdChemReactions.ReactionFromSmarts('[O:1]>>[N:1]')

mol = Chem.MolFromSmiles('CCO')

mol.GetAtomWithIdx(0).SetIntProp('demo', 1)
reactant_bond = mol.GetBondBetweenAtoms(0, 1)
reactant_bond.SetIntProp('demo', 1)

product_groups = rxn.RunReactants([mol])
product = product_groups[0][0]

mapping = {int(a.GetProp('react_atom_idx')): a.GetIdx() for a in product.GetAtoms()}
product_bond = product.GetBondBetweenAtoms(mapping[0], mapping[1])
print(product_bond.GetPropsAsDict())
```

This shows no properties on the bond, and only the mapping properties on the atoms.

Hints:

Fix patch:
diff --git a/Code/GraphMol/ChemReactions/ReactionRunner.cpp b/Code/GraphMol/ChemReactions/ReactionRunner.cpp
index 72abd1f2a30..c7a09a8006e 100644
--- a/Code/GraphMol/ChemReactions/ReactionRunner.cpp
+++ b/Code/GraphMol/ChemReactions/ReactionRunner.cpp
@@ -718,8 +718,14 @@ void setReactantBondPropertiesToProduct(RWMOL_SPTR product,
     }
 
     pBond->setBondType(rBond->getBondType());
+    if (rBond->getBondType() == Bond::DOUBLE &&
+        rBond->getBondDir() == Bond::EITHERDOUBLE) {
+      pBond->setBondDir(Bond::EITHERDOUBLE);
+    }
+
     pBond->setIsAromatic(rBond->getIsAromatic());
 
+    pBond->updateProps(*rBond);
     if (pBond->hasProp(common_properties::NullBond)) {
       pBond->clearProp(common_properties::NullBond);
     }
@@ -858,6 +864,14 @@ void addMissingProductAtom(const Atom &reactAtom, unsigned reactNeighborIdx,
   } else {
     product->addBond(productIdx, prodNeighborIdx, origB->getBondType());
   }
+
+  auto prodB = product->getBondBetweenAtoms(prodNeighborIdx, productIdx);
+  if (origB->getBondType() == Bond::DOUBLE &&
+      origB->getBondDir() == Bond::EITHERDOUBLE) {
+    prodB->setBondDir(Bond::EITHERDOUBLE);
+  }
+  bool preserveExisting = true;
+  prodB->updateProps(*origB, preserveExisting);
 }
 
 void addReactantNeighborsToProduct(


Test patch:
diff --git a/Code/GraphMol/ChemReactions/testReaction.cpp b/Code/GraphMol/ChemReactions/testReaction.cpp
index 4b8af214ed9..05529b92325 100644
--- a/Code/GraphMol/ChemReactions/testReaction.cpp
+++ b/Code/GraphMol/ChemReactions/testReaction.cpp
@@ -7539,6 +7539,73 @@ void testGithub4162() {
   RxnOps::sanitizeRxn(*rxnFromPickle);
 }
 
+void testGithub4114() {
+  BOOST_LOG(rdInfoLog) << "--------------------------------------" << std::endl;
+  BOOST_LOG(rdInfoLog) << "Reactions don't propagate bond properties"
+                       << std::endl;
+
+  const std::string DUMMY_PROP{"dummy_prop"};
+  std::vector<ROMOL_SPTR> mol{"ClC(N=O)=C=C=C=C(Br)N=S"_smiles};
+
+  // React the left side of the mol
+  const std::string reaction(
+      R"([Cl:1][C:2]([N:3]=[O:4])=[C:5]=[C:6]>>[F:1][Si:2]([P:3]~[S:4])(~[Si:5]=[Si:6]))");
+  std::unique_ptr<ChemicalReaction> rxn(RxnSmartsToChemicalReaction(reaction));
+  TEST_ASSERT(rxn);
+  rxn->initReactantMatchers();
+
+  // Set dummy properties on all bonds; mark the central double bonds
+  // as EITHERDOUBLE
+  for (unsigned int i = 0; i < mol[0]->getNumBonds(); ++i) {
+    auto bnd = mol[0]->getBondWithIdx(i);
+    bnd->setProp(DUMMY_PROP, i);
+
+    if (i >= 3 && i <= 6) {
+      TEST_ASSERT(bnd->getBondType() == Bond::DOUBLE);
+      bnd->setBondDir(Bond::EITHERDOUBLE);
+    }
+  }
+
+  auto prods = rxn->runReactants(mol);
+  TEST_ASSERT(prods.size() == 1);
+  TEST_ASSERT(prods[0].size() == 1);
+
+  // Make sure the reaction did not alter the graph
+  for (const auto &at : prods[0][0]->atoms()) {
+    auto mapno = at->getProp<unsigned int>(common_properties::reactantAtomIdx);
+    TEST_ASSERT(mapno == at->getIdx());
+  }
+
+  for (const auto &rBnd : mol[0]->bonds()) {
+    auto pBnd = prods[0][0]->getBondBetweenAtoms(rBnd->getBeginAtomIdx(),
+                                                 rBnd->getEndAtomIdx());
+
+    // Bonds 0, 1 and 4 are overridden in the product template, and should
+    // therefore override the properties.
+    // Bonds 2 and 3 are "null bonds", and should get properties copied over.
+    // Bonds > 4 are reactant bonds, and should keep their properties.
+    auto rBndIdx = rBnd->getIdx();
+    if (rBndIdx == 0 || rBndIdx == 1 || rBndIdx == 4) {
+      TEST_ASSERT(!pBnd->hasProp(DUMMY_PROP));
+    } else {
+      unsigned int dummy_prop;
+      TEST_ASSERT(pBnd->getPropIfPresent(DUMMY_PROP, dummy_prop));
+      TEST_ASSERT(dummy_prop == rBndIdx);
+    }
+
+    // Bond 3 is a "null bond", and should preserve EITHERDOUBLE.
+    // Bond 4 is specified in the reaction, and should not preserve
+    // EITHERDOUBLE. Bonds 5 & 6 are not included in the reaction, so they
+    // should keep EITHERDOUBLE dir.
+    if (rBndIdx == 3 || rBndIdx == 5 || rBndIdx == 6) {
+      TEST_ASSERT(pBnd->getBondType() == Bond::DOUBLE);
+      TEST_ASSERT(pBnd->getBondDir() == Bond::EITHERDOUBLE);
+    } else {
+      TEST_ASSERT(pBnd->getBondDir() == Bond::NONE);
+    }
+  }
+}
+
 int main() {
   RDLog::InitLogs();
 
@@ -7633,6 +7700,7 @@ int main() {
   testRxnBlockRemoveHs();
   testGithub3078();
   testGithub4162();
+  testGithub4114();
 
   BOOST_LOG(rdInfoLog)
       << "*******************************************************\n";