Repository: rdkit/rdkit — pull request #6193 (cpp)
Title: Fix/github6138

Fixed #6138 

There are a few atom properties that are not pickled properly after ChemReaction::validate has been called.

These are 

 _QueryIsotope
 _QueryFormalCharge

and the like.

This ensures they are pickled if the reaction has been initialized.

Resolved issues:
title: Round-tripping a reaction through pickle changes the outputs from RunReactants
body: **Describe the bug**

RDKit's reaction objects can be pickled/unpickled, but in some cases running the unpickled reaction can produce slightly different results than it did before the pickling. Interestingly, in my example it only happens if the reaction was ran at least once _before_ pickling, otherwise there are no issues.

**To Reproduce**
```python
import pickle
from rdkit import Chem
from rdkit.Chem import AllChem

mol = Chem.MolFromSmiles("COc1ccccc1Oc1nc(Nc2cc(C)[nH]n2)cc2ccccc12")
rxn = AllChem.ReactionFromSmarts("([c:1]:[n&H1&+0&D2:3]:[n:2])>>([c:1]:[n&H0&+0&D3:3](:[n:2])-C1-C-C-C-C-O-1)")

def run(r):
  return Chem.MolToSmiles(r.RunReactants((mol,))[0][0])

pickle.dump(rxn, open("tmp.pkl", "wb"))
rxn_reloaded = pickle.load(open("tmp.pkl", "rb"))

print(run(rxn))
print(run(rxn_reloaded))

pickle.dump(rxn, open("tmp_after.pkl", "wb"))
rxn_reloaded_after_use = pickle.load(open("tmp_after.pkl", "rb"))

print(run(rxn_reloaded_after_use))
```

Outputs:
```
COc1ccccc1Oc1nc(Nc2cc(C)n(C3CCCCO3)n2)cc2ccccc12
COc1ccccc1Oc1nc(Nc2cc(C)n(C3CCCCO3)n2)cc2ccccc12
COc1ccccc1Oc1nc(Nc2cc(C)[nH](C3CCCCO3)n2)cc2ccccc12
```

While the original SMILES can be correctly parsed by RDKit, the one obtained with the unpickled reaction can no longer be parsed.

**Expected behavior**

Result after round-tripping through pickle should be the same.

**Configuration (please complete the following information):**
 - RDKit version: 2021.09.4
 - OS: Ubuntu 16.04.6 LTS
 - Python version: 3.9.7
 - Are you using conda? Yes
 - If you are using conda, which channel did you install the rdkit from? conda-forge


Hints:

Fix patch:
diff --git a/Code/GraphMol/ChemReactions/ReactionPickler.cpp b/Code/GraphMol/ChemReactions/ReactionPickler.cpp
index 9450fbebfc3..9a4822e43a6 100644
--- a/Code/GraphMol/ChemReactions/ReactionPickler.cpp
+++ b/Code/GraphMol/ChemReactions/ReactionPickler.cpp
@@ -146,15 +146,18 @@ void ReactionPickler::_pickle(const ChemicalReaction *rxn, std::ostream &ss,
   streamWrite(ss, BEGINREACTANTS);
   for (auto tmpl = rxn->beginReactantTemplates();
        tmpl != rxn->endReactantTemplates(); ++tmpl) {
-    MolPickler::pickleMol(tmpl->get(), ss);
+    auto props = rxn->df_needsInit ? PicklerOps::PropertyPickleOptions::NoProps :
+      PicklerOps::PropertyPickleOptions::AllProps;
+    MolPickler::pickleMol(tmpl->get(), ss, props);
   }
   streamWrite(ss, ENDREACTANTS);
 
   streamWrite(ss, BEGINPRODUCTS);
   for (auto tmpl = rxn->beginProductTemplates();
        tmpl != rxn->endProductTemplates(); ++tmpl) {
-    MolPickler::pickleMol(tmpl->get(), ss,
-                          PicklerOps::PropertyPickleOptions::AtomProps);
+    auto props = rxn->df_needsInit ? PicklerOps::PropertyPickleOptions::AtomProps :
+      PicklerOps::PropertyPickleOptions::AllProps;
+    MolPickler::pickleMol(tmpl->get(), ss, props);
   }
   streamWrite(ss, ENDPRODUCTS);
 
@@ -162,7 +165,8 @@ void ReactionPickler::_pickle(const ChemicalReaction *rxn, std::ostream &ss,
     streamWrite(ss, BEGINAGENTS);
     for (auto tmpl = rxn->beginAgentTemplates();
          tmpl != rxn->endAgentTemplates(); ++tmpl) {
-      MolPickler::pickleMol(tmpl->get(), ss);
+      // reagents don't have private properties set during initialization
+      MolPickler::pickleMol(tmpl->get(), ss, PicklerOps::PropertyPickleOptions::AtomProps);
     }
     streamWrite(ss, ENDAGENTS);
   }


Test patch:
diff --git a/Code/GraphMol/ChemReactions/Wrap/testReactionWrapper.py b/Code/GraphMol/ChemReactions/Wrap/testReactionWrapper.py
index 4879a96869c..87011e6b296 100644
--- a/Code/GraphMol/ChemReactions/Wrap/testReactionWrapper.py
+++ b/Code/GraphMol/ChemReactions/Wrap/testReactionWrapper.py
@@ -38,7 +38,7 @@
 
 from rdkit import rdBase
 from rdkit import Chem
-from rdkit.Chem import rdChemReactions
+from rdkit.Chem import AllChem, rdChemReactions
 from rdkit import Geometry
 from rdkit import RDConfig
 from rdkit.Chem.SimpleEnum import Enumerator
@@ -1082,6 +1082,21 @@ def testV3000Reactions(self):
 
     self.assertEqual(reaction.GetNumReactantTemplates(), reaction2.GetNumReactantTemplates())
 
+  def testGithub6138(self):
+    mol = Chem.MolFromSmiles("COc1ccccc1Oc1nc(Nc2cc(C)[nH]n2)cc2ccccc12")
+    rxn = AllChem.ReactionFromSmarts("([c:1]:[n&H1&+0&D2:3]:[n:2])>>([c:1]:[n&H0&+0&D3:3](:[n:2])-C1-C-C-C-C-O-1)")
+
+    def run(r):
+      return Chem.MolToSmiles(r.RunReactants((mol,))[0][0])
+
+    rxn_reloaded = pickle.loads(pickle.dumps(rxn))
+
+    res1 = Chem.MolToSmiles(rxn.RunReactants((mol,))[0][0])
+    res2 = Chem.MolToSmiles(rxn_reloaded.RunReactants((mol,))[0][0])
+    rxn_reloaded_after_use = pickle.loads(pickle.dumps(rxn))
+    res3 = Chem.MolToSmiles(rxn_reloaded_after_use.RunReactants((mol,))[0][0])
+    self.assertEqual(res1, res2)
+    self.assertEqual(res1, res3)
 
 if __name__ == '__main__':
   unittest.main(verbosity=True)
diff --git a/Code/GraphMol/ChemReactions/testReaction.cpp b/Code/GraphMol/ChemReactions/testReaction.cpp
index 6c33c23eab6..3466cc3e0a2 100644
--- a/Code/GraphMol/ChemReactions/testReaction.cpp
+++ b/Code/GraphMol/ChemReactions/testReaction.cpp
@@ -7775,6 +7775,26 @@ void testChemicalReactionCopyAssignment() {
   delete rxn2;
 }
 
+void testGithub6138() {
+  // Pickling reactions removed some of their properties set after reaction
+    // initialization
+  auto rxn_smarts = "[c:1]:[n&H1&+0&D2:3]:[n:2]>>[c:1]:[3n&H0&+0&D3:3]:[2n:2]";
+  std::unique_ptr<ChemicalReaction> rxn(RxnSmartsToChemicalReaction(rxn_smarts));
+  ROMOL_SPTR mol("c1cn[nH]c1"_smiles);
+  rxn->initReactantMatchers();
+  MOL_SPTR_VECT reacts;
+  reacts.push_back(mol);
+  auto prods = rxn->runReactants(reacts);
+  std::string pkl;
+  ReactionPickler::pickleReaction(*rxn, pkl);
+  std::unique_ptr<ChemicalReaction> lrxn(new ChemicalReaction());
+  ReactionPickler::reactionFromPickle(pkl, lrxn.get());
+  auto prods2 = lrxn->runReactants(reacts);
+  auto s1 =MolToSmiles(*prods[0][0]);
+  auto s2 =MolToSmiles(*prods2[0][0]);
+  TEST_ASSERT(s1 == s2);
+}
+
 int main() {
   RDLog::InitLogs();
 
@@ -7874,6 +7894,7 @@ int main() {
   testGithub4410();
   testMultiTemplateRxnQueries();
   testChemicalReactionCopyAssignment();
+  testGithub6138();
 
   BOOST_LOG(rdInfoLog)
       << "*******************************************************\n";