Repository: rdkit/rdkit — pull request #6842 (cpp)
Title: Sanitize number of radical electrons in case of invalid value assigned by CXSMILES

#### Reference Issue
Fixes #6370 

#### What does this implement/fix? Explain your changes.
Number of radical electrons are first assigned by the regular SMILES, and then get overridden by the CXSMILES extension. For example,

```python
Chem.MolFromSmiles('[CH4]').Debug()
#	0 6 C chg: 0  deg: 0 exp: 4 imp: 0 hyb: 4 arom?: 0 chi: 0
Chem.MolFromSmiles('[CH4] |^1:0|').Debug()
#	0 6 C chg: 0  deg: 0 exp: 4 imp: 0 hyb: 4 arom?: 0 chi: 0 rad: 1
Chem.MolFromSmiles('[CH4] |^2:0|').Debug()
#	0 6 C chg: 0  deg: 0 exp: 4 imp: 0 hyb: 4 arom?: 0 chi: 0 rad: 2
```

The radical info from CXSMILES may be invalid and therefore should be sanitized. However, current implementation of `assignRadicals()` within `sanitizeMol()` will do nothing if the preset number of radical electrons is non-zero. The fix is then to allow such sanitization by removing the condition `if (atom->getNumRadicalElectrons())`.

Unittests are added for a set of CXSMILES:
1. `[XHn] |^m:0|` where X is the element symbol, n = 1, ..., valence, m = 1, ..., 7 denotes the radical type.
2. `[NH4+] |^m:0|` where m = 1, ..., 7 denotes the radical type.

I also fix a minor issue in `calcImplicitValence()` where `d_implicitValence` is not overwritten by the return value.

Resolved issues:
title: non-physical radical counts being preserved
body: 
**Describe the bug**
Sanitization is leaving radicals on molecules where they don't make sense.

**To Reproduce**
```
In [8]: Chem.MolFromSmiles('[NH4+] |^1:0|').Debug()
Atoms:
	0 7 N chg: 1  deg: 0 exp: 4 imp: 0 hyb: 4 arom?: 0 chi: 0 rad: 1
Bonds:

In [9]: Chem.MolFromSmiles('[CH4] |^1:0|').Debug()
Atoms:
	0 6 C chg: 0  deg: 0 exp: 4 imp: 0 hyb: 4 arom?: 0 chi: 0 rad: 1
Bonds:

```
Neither of those should have a radical on it.

**Configuration (please complete the following information):**
 - RDKit version: all
 - OS: all


Hints:

Fix patch:
diff --git a/Code/GraphMol/MolOps.cpp b/Code/GraphMol/MolOps.cpp
index 4ba259f3808..971ee8c7217 100644
--- a/Code/GraphMol/MolOps.cpp
+++ b/Code/GraphMol/MolOps.cpp
@@ -388,9 +388,8 @@ void adjustHs(RWMol &mol) {
 void assignRadicals(RWMol &mol) {
   for (auto atom : mol.atoms()) {
     // we only put automatically assign radicals to things that
-    // don't have them already and don't have implicit Hs:
-    if (!atom->getNoImplicit() || atom->getNumRadicalElectrons() ||
-        !atom->getAtomicNum()) {
+    // don't have implicit Hs:
+    if (!atom->getNoImplicit() || !atom->getAtomicNum()) {
       continue;
     }
     const auto &valens =
diff --git a/ReleaseNotes.md b/ReleaseNotes.md
index f392dd3e07b..7f3fce596cf 100644
--- a/ReleaseNotes.md
+++ b/ReleaseNotes.md
@@ -10,7 +10,7 @@ GitHub)
 
 ## Backwards incompatible changes
 - Two changes to improve the defaults for conformer generation: the functions EmbedMolecule() and EmbedMultipleConfis() now use ETKDGv3 by default (previously they were using ETKDGV1) and only consider heavy atoms when calculating RMSD for conformer pruning (previously Hs were alos considered).
-
+- The way that the number of radical electrons is calculated for atoms coming from mol blocks has been changed. Systems like a `[CH]` marked as a `DOUBLET` will now have three radical electrons assigned. This is consistent with the value from SMILES.
 
 ## New Features and Enhancements:
 


Test patch:
diff --git a/Code/GraphMol/FileParsers/test1.cpp b/Code/GraphMol/FileParsers/test1.cpp
index 6abe90cb05a..509bcb59554 100644
--- a/Code/GraphMol/FileParsers/test1.cpp
+++ b/Code/GraphMol/FileParsers/test1.cpp
@@ -2475,12 +2475,12 @@ void test3V3K() {
   {
     // radical and valence
     fName = rdbase + "CH.v3k.mol";
-    RWMol *m = MolFileToMol(fName);
+    RWMol *m = MolFileToMol(fName);  // cxsmiles = "[CH] |(0,0,),^5:0|"
     TEST_ASSERT(m);
     TEST_ASSERT(m->getNumAtoms() == 1);
     TEST_ASSERT(m->getAtomWithIdx(0)->getNoImplicit());
     TEST_ASSERT(m->getAtomWithIdx(0)->getNumExplicitHs() == 1);
-    TEST_ASSERT(m->getAtomWithIdx(0)->getNumRadicalElectrons() == 1);
+    TEST_ASSERT(m->getAtomWithIdx(0)->getNumRadicalElectrons() == 3);
 
     std::string mb = MolToMolBlock(*m, true, -1, true, true);
     delete m;
@@ -2494,7 +2494,7 @@ void test3V3K() {
     TEST_ASSERT(m->getNumAtoms() == 1);
     TEST_ASSERT(m->getAtomWithIdx(0)->getNoImplicit());
     TEST_ASSERT(m->getAtomWithIdx(0)->getNumExplicitHs() == 1);
-    TEST_ASSERT(m->getAtomWithIdx(0)->getNumRadicalElectrons() == 1);
+    TEST_ASSERT(m->getAtomWithIdx(0)->getNumRadicalElectrons() == 3);
 
     delete m;
   }
@@ -3007,10 +3007,7 @@ void testIssue3359739() {
   delete m;
   m = MolBlockToMol(molBlock);
   TEST_ASSERT(m);
-  // NOTE: the following is correct according to the current
-  // state of the code and what the CTAB format supports,
-  // but it's definitely not chemically correct
-  TEST_ASSERT(m->getAtomWithIdx(0)->getNumRadicalElectrons() == 1);
+  TEST_ASSERT(m->getAtomWithIdx(0)->getNumRadicalElectrons() == 3);
   delete m;
 
   BOOST_LOG(rdInfoLog) << " Finished <---------- " << std::endl;
@@ -3528,12 +3525,12 @@ void testMolFileTotalValence() {
     RWMol *m1;
     std::string fName;
     fName = rdbase + "CH.mol";
-    m1 = MolFileToMol(fName);
+    m1 = MolFileToMol(fName);  // cxsmi = "[CH] |(-1.2375,2.1509,),^5:0|"
     TEST_ASSERT(m1);
     TEST_ASSERT(m1->getNumAtoms() == 1);
     TEST_ASSERT(m1->getAtomWithIdx(0)->getNoImplicit());
     TEST_ASSERT(m1->getAtomWithIdx(0)->getNumExplicitHs() == 1);
-    TEST_ASSERT(m1->getAtomWithIdx(0)->getNumRadicalElectrons() == 1);
+    TEST_ASSERT(m1->getAtomWithIdx(0)->getNumRadicalElectrons() == 3);
 
     delete m1;
   }
@@ -3566,12 +3563,12 @@ void testMolFileTotalValence() {
     RWMol *m1;
     std::string fName;
     fName = rdbase + "CH.v3k.mol";
-    m1 = MolFileToMol(fName);
+    m1 = MolFileToMol(fName);  // cxsmiles = "[CH] |(0,0,),^5:0|"
     TEST_ASSERT(m1);
     TEST_ASSERT(m1->getNumAtoms() == 1);
     TEST_ASSERT(m1->getAtomWithIdx(0)->getNoImplicit());
     TEST_ASSERT(m1->getAtomWithIdx(0)->getNumExplicitHs() == 1);
-    TEST_ASSERT(m1->getAtomWithIdx(0)->getNumRadicalElectrons() == 1);
+    TEST_ASSERT(m1->getAtomWithIdx(0)->getNumRadicalElectrons() == 3);
 
     delete m1;
   }
diff --git a/Code/GraphMol/FileParsers/testMolWriter.cpp b/Code/GraphMol/FileParsers/testMolWriter.cpp
index 78275f62864..8fbb0b4c7cb 100644
--- a/Code/GraphMol/FileParsers/testMolWriter.cpp
+++ b/Code/GraphMol/FileParsers/testMolWriter.cpp
@@ -739,7 +739,7 @@ void testMolFileTotalValence() {
     TEST_ASSERT(m1->getNumAtoms() == 1);
     TEST_ASSERT(m1->getAtomWithIdx(0)->getNoImplicit());
     TEST_ASSERT(m1->getAtomWithIdx(0)->getNumExplicitHs() == 1);
-    TEST_ASSERT(m1->getAtomWithIdx(0)->getNumRadicalElectrons() == 1);
+    TEST_ASSERT(m1->getAtomWithIdx(0)->getNumRadicalElectrons() == 3);
 
     delete m1;
   }
diff --git a/Code/GraphMol/MarvinParse/test_data/mrvValence_value.mrv.expected.mrv b/Code/GraphMol/MarvinParse/test_data/mrvValence_value.mrv.expected.mrv
index 99d10b08c82..2036bd947ff 100644
--- a/Code/GraphMol/MarvinParse/test_data/mrvValence_value.mrv.expected.mrv
+++ b/Code/GraphMol/MarvinParse/test_data/mrvValence_value.mrv.expected.mrv
@@ -1,2 +1,2 @@
 <?xml version="1.0" encoding="utf-8"?>
-<cml xmlns="http://www.chemaxon.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.chemaxon.com http://www.chemaxon.com/marvin/schema/mrvSchema_20_20_0.xsd"><MDocument><MChemicalStruct><molecule molID="m1"><atomArray><atom id="a1" elementType="B" x2="4.51182" y2="0.00000" formalCharge="-1" radical="monovalent"/><atom id="a2" elementType="C" x2="2.97182" y2="0.00000"/><atom id="a3" elementType="N" x2="1.43182" y2="0.00000"/></atomArray><bondArray><bond id="b1" atomRefs2="a1 a2" order="1"/><bond id="b2" atomRefs2="a2 a3" order="3"/></bondArray></molecule></MChemicalStruct></MDocument></cml>
\ No newline at end of file
+<cml xmlns="http://www.chemaxon.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.chemaxon.com http://www.chemaxon.com/marvin/schema/mrvSchema_20_20_0.xsd"><MDocument><MChemicalStruct><molecule molID="m1"><atomArray><atom id="a1" elementType="B" x2="4.51182" y2="0.00000" formalCharge="-1" radical="trivalent4"/><atom id="a2" elementType="C" x2="2.97182" y2="0.00000"/><atom id="a3" elementType="N" x2="1.43182" y2="0.00000"/></atomArray><bondArray><bond id="b1" atomRefs2="a1 a2" order="1"/><bond id="b2" atomRefs2="a2 a3" order="3"/></bondArray></molecule></MChemicalStruct></MDocument></cml>
\ No newline at end of file
diff --git a/Code/GraphMol/test1.cpp b/Code/GraphMol/test1.cpp
index dcce2fd0aa2..1d2b64e0d07 100644
--- a/Code/GraphMol/test1.cpp
+++ b/Code/GraphMol/test1.cpp
@@ -14,7 +14,6 @@
 #include <GraphMol/RDKitQueries.h>
 #include <RDGeneral/types.h>
 #include <RDGeneral/RDLog.h>
-//#include <boost/log/functions.hpp>
 #include <GraphMol/FileParsers/FileParsers.h>
 #include <GraphMol/FileParsers/MolWriters.h>
 #include <GraphMol/SmilesParse/SmilesParse.h>
@@ -22,6 +21,7 @@
 #include <GraphMol/SmilesParse/SmartsWrite.h>
 #include <sstream>
 #include <iostream>
+#include <boost/format.hpp>
 #include <boost/range/iterator_range.hpp>
 
 using namespace std;
@@ -1666,6 +1666,60 @@ void testHasValenceViolation() {
   BOOST_LOG(rdInfoLog) << "Finished" << std::endl;
 }
 
+void testGithub6370() {
+  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
+  BOOST_LOG(rdErrorLog)
+      << "    Test Github 6370: non-physical radical counts being preserved"
+      << std::endl;
+
+  auto testRadicalsForSingleAtom = [](const std::string &element, int valence) {
+    for (int explicitHCount = 0; explicitHCount <= valence; explicitHCount++) {
+      for (int radicalType = 1; radicalType <= 7; radicalType++) {
+        std::string smi;
+        if (explicitHCount == 0) {
+          smi = (boost::format("[%s] |^%d:0|") % element % radicalType).str();
+        } else {
+          smi = (boost::format("[%sH%d] |^%d:0|") % element % explicitHCount %
+                 radicalType)
+                    .str();
+        }
+        RWMol *m = SmilesToMol(smi);
+        TEST_ASSERT(
+            static_cast<int>(m->getAtomWithIdx(0)->getNumRadicalElectrons()) ==
+            valence - explicitHCount);
+      }
+    }
+  };
+
+  // Checks CXSMILES in the form of [XHn] |^m:0|
+  // where X is the element symbol,
+  // n = 0, ..., valence,
+  // m = 1, ..., 7 denotes the radical type
+  for (int atomicNum = 2; atomicNum <= 118; atomicNum++) {
+    const auto &defaultVs =
+        PeriodicTable::getTable()->getValenceList(atomicNum);
+    if (defaultVs.size() != 1) {
+      // Skips elements with multiple valences, e.g., transition metals
+      continue;
+    }
+    int valence = defaultVs[0];
+    std::string element =
+        PeriodicTable::getTable()->getElementSymbol(atomicNum);
+    testRadicalsForSingleAtom(element, valence);
+  }
+
+  // Checks CXSMILES in the form of [NH4+] |^m:0|
+  // where m = 1, ..., 7 denotes the radical type
+  for (int radicalType = 1; radicalType <= 7; radicalType++) {
+    std::string smi = (boost::format("[NH4+] |^%d:0|") % radicalType).str();
+    RWMol *m = SmilesToMol(smi);
+    TEST_ASSERT(
+        static_cast<int>(m->getAtomWithIdx(0)->getNumRadicalElectrons()) == 0);
+  }
+
+  BOOST_LOG(rdErrorLog) << "Finished" << std::endl;
+}
+
 // -------------------------------------------------------------------
 int main() {
   RDLog::InitLogs();
@@ -1695,6 +1749,7 @@ int main() {
   testRanges();
   testGithub1642();
   testGithub1843();
+  testGithub6370();
   testAtomListLineRoundTrip();
   testAtomListLineWithOtherQueries();
   testReplaceChargedAtomWithQueryAtom();