Repository: rdkit/rdkit — pull request #5136 (cpp)
Title: Fixes #5134


This is one of those "can't believe it survived that long" bugs.

The fix is reasonably straightforward. Here's a description of the change (the part after the OR):
```
    // conditions for a ring/fused ring to be considered for aromaticity here:
    //   all atoms are candidates to be aromatic
    //     OR
    //   at least two members are candidates to be aromatic AND all of the
    //     non-candidates are in at least two fused rings
    //   This last condition is there to handle the molecule
    //      C1=CC2=CC3=CC=C4C=C1C2C34
    //   where the outer envelope is aromatic, but all of the sub-rings have SP3
    //   carbons in them. (This was github #5134)
```

In the process of fixing this, I also ended up fixing #5078

Resolved issues:
title: Can't roundtrip CHEMBL4080644 through SMILES
body: **Describe the bug**
The SMILES generated from the molblock for CHEMBL4080644 cannot be sanitized:
```
In [2]: chembl4080644 = """
     RDKit          2D

 22 25  0  0  0  0  0  0  0  0999 V2000
    6.1174   -5.5956    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
    5.5355   -6.1734    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    5.9052   -6.9054    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    6.7157   -6.7800    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    6.8468   -5.9705    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    3.9896   -4.3666    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    3.9885   -5.1861    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    4.6965   -5.5951    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    4.6947   -3.9577    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    5.4033   -4.3630    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    5.4067   -5.1882    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    6.1123   -3.9452    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
    6.8293   -4.3571    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    6.8321   -5.1820    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    7.5468   -5.5902    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    8.2590   -5.1746    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    8.2522   -4.3466    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    7.5370   -3.9422    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    3.2804   -5.5941    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    8.9563   -3.9319    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    7.6601   -5.9680    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
    4.7196   -6.1277    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
  2  3  1  0
  3  4  2  0
  4  5  1  0
  5  1  1  0
  6  7  2  0
  7  8  1  0
  8 11  2  0
 10  9  2  0
  9  6  1  0
 10 11  1  0
 10 12  1  0
 11  1  1  0
  1 14  1  0
 13 12  1  0
 13 14  2  0
 14 15  1  0
 15 16  2  0
 16 17  1  0
 17 18  2  0
 18 13  1  0
  7 19  1  0
 17 20  1  0
  5 21  2  0
  2 22  2  0
  2 12  1  0
M  END
"""

In [3]: Chem.MolToSmiles(Chem.MolFromMolBlock(chembl4080644))
Out[3]: 'Cc1ccc2c(c1)-n1-c(=O)/c=c\\c(=O)-n-2-c2cc(C)ccc2-1'

In [4]: Chem.MolFromSmiles(_3)
[05:52:40] Can't kekulize mol.  Unkekulized atoms: 1 2 3 4 5 6 10 11 15 16 17 19 20 21
```
This came up in discussion #5073 

The bridgehead Ns in this molecule are somehow being marked as aromatic and that's what's causing the problem.
Additional info: when parsing the SMILES corresponding to the mol block above we don't have that problem:
```
Chem.CanonSmiles('CC1=CC=C2C(=C1)N1C(=O)/C=C\\C(=O)N2C2=C1C=CC(C)=C2')
  --> Cc1ccc2c(c1)N1C(=O)/C=C\\C(=O)N2c2cc(C)ccc21
```

The problem is that after processing the mol block we have aromatic atoms (the two Ns) which have no aromatic bonds to them. The aromaticity code should detect this case and unset the aromaticity on the atom.

**Configuration (please complete the following information):**
 - RDKit version: current release and master
 - OS: all


Hints:

Fix patch:
diff --git a/Code/GraphMol/Aromaticity.cpp b/Code/GraphMol/Aromaticity.cpp
index a8b4fc67253..95a5615e890 100644
--- a/Code/GraphMol/Aromaticity.cpp
+++ b/Code/GraphMol/Aromaticity.cpp
@@ -150,25 +150,24 @@ static void applyHuckelToFused(
     ROMol &mol,                   // molecule of interest
     const VECT_INT_VECT &srings,  // list of all ring as atom IDS
     const VECT_INT_VECT &brings,  // list of all rings as bond ids
-    const INT_VECT &fused,       // list of ring ids in the current fused system
+    const INT_VECT &fused,  // list of ring ids in the current fused system
+    const boost::dynamic_bitset<>
+        &acands,  // whether or not an atom is a candidate for aromaticity
     const VECT_EDON_TYPE &edon,  // electron donor state for each atom
     INT_INT_VECT_MAP &ringNeighs,
     int &narom,  // number of aromatic ring so far
     unsigned int maxNumFusedRings, const std::vector<Bond *> &bondsByIdx,
     unsigned int minRingSize = 0);
 
-void markAtomsBondsArom(ROMol &mol, const VECT_INT_VECT &srings,
+void markAtomsBondsArom(ROMol &mol, const INT_VECT &unon,
                         const VECT_INT_VECT &brings, const INT_VECT &ringIds,
                         std::set<unsigned int> &doneBonds,
                         const std::vector<Bond *> &bondsByIdx) {
   // first mark the atoms in the rings
-  for (auto ri : ringIds) {
-    const auto &aring = srings[ri];
-
-    // first mark the atoms in the ring
-    for (auto ai : aring) {
-      mol.getAtomWithIdx(ai)->setIsAromatic(true);
-    }
+  boost::dynamic_bitset<> activeAtoms(mol.getNumAtoms());
+  for (auto ai : unon) {
+    activeAtoms.set(ai);
+    mol.getAtomWithIdx(ai)->setIsAromatic(true);
   }
 
   // mark the bonds
@@ -191,6 +190,10 @@ void markAtomsBondsArom(ROMol &mol, const VECT_INT_VECT &srings,
     // std::cerr << " " << bci->first << "(" << bci->second << ")";
     if (bci.second == 1) {
       auto bond = bondsByIdx[bci.first];
+      if (!activeAtoms[bond->getBeginAtomIdx()] ||
+          !activeAtoms[bond->getEndAtomIdx()]) {
+        continue;
+      }
       // Bond *bond = mol.get BondWithIdx(bci->first);
       bond->setIsAromatic(true);
       switch (bond->getBondType()) {
@@ -273,27 +276,60 @@ bool incidentMultipleBond(const Atom *at) {
   return at->getValence(Atom::ValenceType::EXPLICIT) != deg;
 }
 
+bool ringCanBeAromatic(ROMol &, const INT_VECT &ring,
+                       const VECT_EDON_TYPE &edon, unsigned int minRingSize) {
+  if (ring.size() < minRingSize) {
+    return false;
+  }
+  unsigned int nAnyElectronDonorType = 0;
+  for (auto idx : ring) {
+    ElectronDonorType edonType = edon[idx];
+    if (edonType == AnyElectronDonorType) {
+      ++nAnyElectronDonorType;
+      if (nAnyElectronDonorType > 1) {
+        return false;
+      }
+    } else if (edonType == NoElectronDonorType) {
+      // if the ring includes atoms which can't be aromatic we can just skip it
+      return false;
+    }
+  }
+  return true;
+}
+
 bool applyHuckel(ROMol &, const INT_VECT &ring, const VECT_EDON_TYPE &edon,
-                 unsigned int minRingSize) {
+                 unsigned int minRingSize,
+                 const boost::dynamic_bitset<> &acands) {
   if (ring.size() < minRingSize) {
     return false;
   }
+
   int atlw, atup, rlw, rup, rie;
   bool aromatic = false;
   rlw = 0;
   rup = 0;
   unsigned int nAnyElectronDonorType = 0;
   for (auto idx : ring) {
+    // we shouldn't be here if there are any atoms in the ring that can't be
+    // aromatic, but check anyway
+    if (!acands[idx]) {
+      return false;
+    }
     ElectronDonorType edonType = edon[idx];
     if (edonType == AnyElectronDonorType) {
       ++nAnyElectronDonorType;
       if (nAnyElectronDonorType > 1) {
         return false;
       }
+    } else if (edonType == NoElectronDonorType) {
+      // if the ring includes atoms which can't be aromatic we can just skip it
+      return false;
     }
     getMinMaxAtomElecs(edonType, atlw, atup);
     rlw += atlw;
     rup += atup;
+    // std::cerr << "  atom: " << idx << ": " << atlw << "-" << atup <<
+    // std::endl;
   }
 
   if (rup >= 6) {
@@ -313,8 +349,10 @@ void applyHuckelToFused(
     ROMol &mol,                   // molecule of interest
     const VECT_INT_VECT &srings,  // list of all ring as atom IDS
     const VECT_INT_VECT &brings,  // list of all rings as bond ids
-    const INT_VECT &fused,       // list of ring ids in the current fused system
-    const VECT_EDON_TYPE &edon,  // electron donor state for each atom
+    const INT_VECT &fused,  // list of ring ids in the current fused system
+    const boost::dynamic_bitset<>
+        &acands,  // whether or not an atom is a candidate for aromaticity
+    const VECT_EDON_TYPE &edon,    // electron donor state for each atom
     INT_INT_VECT_MAP &ringNeighs,  // list of neighbors for each candidate ring
     int &narom,                    // number of aromatic ring so far
     unsigned int maxNumFusedRings, const std::vector<Bond *> &bondsByIdx,
@@ -344,6 +382,7 @@ void applyHuckelToFused(
     }
     nRingBonds = rdcast<unsigned int>(fusedBonds.count());
   }
+
   std::set<unsigned int> doneBonds;
   while (1) {
     if (pos == -1) {
@@ -388,27 +427,65 @@ void applyHuckelToFused(
     if (ringNeighs.size() && !RingUtils::checkFused(curRs, ringNeighs)) {
       continue;
     }
-
+    std::vector<unsigned int> ringAdjacencies(
+        mol.getNumAtoms() * mol.getNumAtoms(), 0);
     // check aromaticity on the current fused system
     INT_VECT atsInRingSystem(mol.getNumAtoms(), 0);
     for (auto ridx : curRs) {
-      for (auto rid : srings[ridx]) {
+      const auto &sring = srings[ridx];
+      for (auto ri = 0u; ri < sring.size(); ++ri) {
+        auto rid = sring[ri];
         ++atsInRingSystem[rid];
+        unsigned nextAt;
+        if (ri < sring.size() - 1) {
+          nextAt = sring[ri + 1];
+        } else {
+          nextAt = sring[0];
+        }
+        // keep track of which ring atoms are adjacent to each other
+        ringAdjacencies[rid * mol.getNumAtoms() + nextAt]++;
+        ringAdjacencies[nextAt * mol.getNumAtoms() + rid]++;
       }
     }
     INT_VECT unon;
+    bool nonAromaticAtomsInRing = false;
     for (i = 0; i < atsInRingSystem.size(); ++i) {
       // condition for inclusion of an atom in the aromaticity of a fused ring
       // system is that it's present in one or two of the rings. this was #2895:
       // the central atom in acepentalene was being included in the count of
       // aromatic atoms
       if (atsInRingSystem[i] == 1 || atsInRingSystem[i] == 2) {
+        if (!acands[i]) {
+          // std::cerr << " atom " << i << " not a candidate" << std::endl;
+          nonAromaticAtomsInRing = true;
+          break;
+        }
         unon.push_back(i);
       }
     }
-    if (applyHuckel(mol, unon, edon, minRingSize)) {
+    if (nonAromaticAtomsInRing) {
+      continue;
+    }
+    // make sure that we still actually have a ring: each atom which is left
+    // should have at least(?) two connections to other atoms in the ring
+    std::vector<unsigned int> nbrCounts(unon.size(), 0);
+    for (auto i = 0u; i < unon.size(); ++i) {
+      for (auto j = i; j < unon.size(); ++j) {
+        if (ringAdjacencies[unon[i] * mol.getNumAtoms() + unon[j]]) {
+          nbrCounts[i] += 1;
+          nbrCounts[j] += 1;
+          // TODO: can we stop this if we exceed 2? There's probably some edge
+          // case where that happens
+        }
+      }
+    }
+    if (*std::min_element(nbrCounts.begin(), nbrCounts.end()) < 2) {
+      continue;
+    }
+
+    if (applyHuckel(mol, unon, edon, minRingSize, acands)) {
       // mark the atoms and bonds in these rings to be aromatic
-      markAtomsBondsArom(mol, srings, brings, curRs, doneBonds, bondsByIdx);
+      markAtomsBondsArom(mol, unon, brings, curRs, doneBonds, bondsByIdx);
 
       // add the ring IDs to the aromatic rings found so far
       // avoid duplicates
@@ -516,7 +593,28 @@ bool isAtomCandForArom(const Atom *at, const ElectronDonorType edon,
     }
   }
 
-  return (true);
+  // It seems like we'd want to exclude atoms for being bridgeheads since they
+  // are going to be 3D and not actually conjugated
+  // But we need to be careful about that since rings fused onto macrocycles
+  // have "bridgeheads" according to our definition and we don't want to
+  // exclude things like:
+  //   c1cc2ccc1CCCCCCCCCCCCCCCC2
+  // where atoms 2 and 5 are "bridgeheads" but in a macrocycle
+  if (queryIsAtomBridgehead(at) && !nUnsaturations) {
+    bool inMacrocycle = false;
+    for (const auto &aring : at->getOwningMol().getRingInfo()->atomRings()) {
+      if (aring.size() > 10 &&
+          std::find(aring.begin(), aring.end(), at->getIdx()) != aring.end()) {
+        inMacrocycle = true;
+        break;
+      }
+    }
+    if (!inMacrocycle) {
+      return false;
+    }
+  }
+
+  return true;
 }
 
 ElectronDonorType getAtomDonorTypeArom(
@@ -749,8 +847,8 @@ int mdlAromaticityHelper(RWMol &mol, const VECT_INT_VECT &srings) {
     RingUtils::pickFusedRings(curr, neighMap, fused, fusDone);
     const unsigned int maxFused = 6;
     const unsigned int minRingSize = 6;
-    applyHuckelToFused(mol, cRings, brings, fused, edon, neighMap, narom,
-                       maxFused, bondsByIdx, minRingSize);
+    applyHuckelToFused(mol, cRings, brings, fused, acands, edon, neighMap,
+                       narom, maxFused, bondsByIdx, minRingSize);
 
     int rix;
     for (rix = 0; rix < cnrs; ++rix) {
@@ -827,7 +925,17 @@ int aromaticityHelper(RWMol &mol, const VECT_INT_VECT &srings,
       continue;
     }
 
-    bool allAromatic = true;
+    // conditions for a ring/fused ring to be considered for aromaticity here:
+    //   all atoms are candidates to be aromatic
+    //     OR
+    //   at least two members are candidates to be aromatic AND all of the
+    //     non-candidates are in at least two fused rings
+    //   This last condition is there to handle the molecule
+    //      C1=CC2=CC3=CC=C4C=C1C2C34
+    //   where the outer envelope is aromatic, but all of the sub-rings have SP3
+    //   carbons in them. (This was github #5134)
+    unsigned int numNonAromatic = 0;
+    bool nonCandidatesInFusedRings = true;
     bool allDummy = true;
     for (auto firstIdx : sring) {
       const auto at = mol.getAtomWithIdx(firstIdx);
@@ -838,7 +946,10 @@ int aromaticityHelper(RWMol &mol, const VECT_INT_VECT &srings,
 
       if (aseen[firstIdx]) {
         if (!acands[firstIdx]) {
-          allAromatic = false;
+          ++numNonAromatic;
+          nonCandidatesInFusedRings =
+              nonCandidatesInFusedRings &&
+              (mol.getRingInfo()->numAtomRings(firstIdx) >= 2);
         }
         continue;
       }
@@ -851,14 +962,18 @@ int aromaticityHelper(RWMol &mol, const VECT_INT_VECT &srings,
       edon[firstIdx] = getAtomDonorTypeArom(at);
       acands[firstIdx] = isAtomCandForArom(at, edon[firstIdx]);
       if (!acands[firstIdx]) {
-        allAromatic = false;
+        ++numNonAromatic;
+        nonCandidatesInFusedRings =
+            nonCandidatesInFusedRings &&
+            (mol.getRingInfo()->numAtomRings(firstIdx) >= 2);
       }
     }
-    if (allAromatic && !allDummy) {
+    if (!allDummy &&
+        (!numNonAromatic ||
+         ((sring.size() - numNonAromatic) >= 2 && nonCandidatesInFusedRings))) {
       cRings.push_back(sring);
     }
   }
-
   // first convert all rings to bonds ids
   VECT_INT_VECT brings;
   RingUtils::convertToBonds(cRings, brings, mol);
@@ -878,8 +993,8 @@ int aromaticityHelper(RWMol &mol, const VECT_INT_VECT &srings,
       fused.push_back(ri);
       const unsigned int maxFused = 6;
       const unsigned int minRingSize = 0;
-      applyHuckelToFused(mol, cRings, brings, fused, edon, neighMap, narom,
-                         maxFused, bondsByIdx, minRingSize);
+      applyHuckelToFused(mol, cRings, brings, fused, acands, edon, neighMap,
+                         narom, maxFused, bondsByIdx, minRingSize);
     }
   } else {
     // make the neighbor map for the rings
@@ -900,8 +1015,8 @@ int aromaticityHelper(RWMol &mol, const VECT_INT_VECT &srings,
     while (curr < cnrs) {
       fused.clear();
       RingUtils::pickFusedRings(curr, neighMap, fused, fusDone);
-      applyHuckelToFused(mol, cRings, brings, fused, edon, neighMap, narom, 6,
-                         bondsByIdx);
+      applyHuckelToFused(mol, cRings, brings, fused, acands, edon, neighMap,
+                         narom, 6, bondsByIdx);
 
       int rix;
       for (rix = 0; rix < cnrs; ++rix) {
diff --git a/Code/GraphMol/catch_graphmol.cpp b/Code/GraphMol/catch_graphmol.cpp
index 8fd8ba0a729..3edc31fdfed 100644
--- a/Code/GraphMol/catch_graphmol.cpp
+++ b/Code/GraphMol/catch_graphmol.cpp
@@ -2662,6 +2662,145 @@ TEST_CASE("Github #5055") {
     REQUIRE(m);
   }
 }
+
+TEST_CASE("Github #5134: Fused ring system not perceived as aromatic") {
+#if 1
+  SECTION("as reported") {
+    auto m = "C1=CC2=CC3=CC=C4C=C1C2C34"_smiles;
+    REQUIRE(m);
+    for (auto i = 0u; i < 10; ++i) {
+      CHECK(m->getAtomWithIdx(i)->getIsAromatic());
+    }
+  }
+  SECTION("Related") {
+    auto m = "C1=CC2=CC=C3C=CC=C4C=CC(=C1)C2C34"_smiles;
+    REQUIRE(m);
+    for (auto i = 0u; i < 14; ++i) {
+      CHECK(m->getAtomWithIdx(i)->getIsAromatic());
+    }
+  }
+  SECTION("some edge cases where the new rule does not apply") {
+    std::vector<std::string> smis = {"C1CC2C=C3C=CC4=CC1C2C34",
+                                     "C1C=C2C=CC3=CC=C1C23"};
+    for (const auto &smi : smis) {
+      std::unique_ptr<ROMol> mol(SmilesToMol(smi));
+      REQUIRE(mol);
+      for (auto atom : mol->atoms()) {
+        CHECK(!atom->getIsAromatic());
+      }
+    }
+  }
+  SECTION("no bridgeheads!") {
+    auto m = "O=C1C2=C(C=CC=C2)C(=O)C3=C1C4C5=C(C=CC=C5)C3C6=C4C=CC=C6"_smiles;
+    REQUIRE(m);
+    CHECK(!m->getAtomWithIdx(12)->getIsAromatic());
+    CHECK(!m->getAtomWithIdx(19)->getIsAromatic());
+  }
+  SECTION("Github #5078: Can't roundtrip CHEMBL4080644 through SMILES") {
+    auto m = R"CTAB(
+     RDKit          2D
+
+ 22 25  0  0  0  0  0  0  0  0999 V2000
+    6.1174   -5.5956    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
+    5.5355   -6.1734    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    5.9052   -6.9054    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    6.7157   -6.7800    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    6.8468   -5.9705    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    3.9896   -4.3666    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    3.9885   -5.1861    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    4.6965   -5.5951    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    4.6947   -3.9577    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    5.4033   -4.3630    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    5.4067   -5.1882    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    6.1123   -3.9452    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
+    6.8293   -4.3571    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    6.8321   -5.1820    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    7.5468   -5.5902    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    8.2590   -5.1746    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    8.2522   -4.3466    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    7.5370   -3.9422    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    3.2804   -5.5941    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    8.9563   -3.9319    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    7.6601   -5.9680    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
+    4.7196   -6.1277    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
+  2  3  1  0
+  3  4  2  0
+  4  5  1  0
+  5  1  1  0
+  6  7  2  0
+  7  8  1  0
+  8 11  2  0
+ 10  9  2  0
+  9  6  1  0
+ 10 11  1  0
+ 10 12  1  0
+ 11  1  1  0
+  1 14  1  0
+ 13 12  1  0
+ 13 14  2  0
+ 14 15  1  0
+ 15 16  2  0
+ 16 17  1  0
+ 17 18  2  0
+ 18 13  1  0
+  7 19  1  0
+ 17 20  1  0
+  5 21  2  0
+  2 22  2  0
+  2 12  1  0
+M  END)CTAB"_ctab;
+    REQUIRE(m);
+    CHECK(!m->getAtomWithIdx(11)->getIsAromatic());
+    CHECK(!m->getAtomWithIdx(0)->getIsAromatic());
+    auto smiles = MolToSmiles(*m);
+    std::unique_ptr<RWMol> m2(SmilesToMol(smiles));
+    REQUIRE(m2);
+  }
+  SECTION("trickier") {
+    auto m = "C1=CC2=CC=C3C=CC4=CC=C5C=CC=C6C5C4C3C2C6=C1"_smiles;
+    REQUIRE(m);
+    for (auto i = 0u; i < 16; ++i) {
+      CHECK(m->getAtomWithIdx(i)->getIsAromatic());
+    }
+    CHECK(m->getAtomWithIdx(20)->getIsAromatic());
+  }
+  SECTION("another odd edge case") {
+    auto m = "C2=CC1=S(S2)SC=C1"_smiles;
+    REQUIRE(m);
+    for (auto atom : m->atoms()) {
+      CHECK(!atom->getIsAromatic());
+    }
+  }
+  SECTION("macrocycle bridgehead edge case") {
+    auto m = "C2CCCCCCCCCN1C=C(CC2)C=CC1=O"_smiles;
+    REQUIRE(m);
+    CHECK(m->getAtomWithIdx(10)->getIsAromatic());
+    CHECK(m->getAtomWithIdx(11)->getIsAromatic());
+    CHECK(m->getAtomWithIdx(12)->getIsAromatic());
+  }
+  SECTION("found during testing") {
+    auto m =
+        "c1ccc2c(c1)c1cc[n+]2Cc2ccc(cc2)C[n+]2ccc(c3ccccc32)NCc2ccc(cc2)CN1"_smiles;
+    REQUIRE(m);
+    CHECK(m->getAtomWithIdx(6)->getIsAromatic());
+    CHECK(m->getAtomWithIdx(11)->getIsAromatic());
+    CHECK(m->getAtomWithIdx(18)->getIsAromatic());
+    CHECK(m->getAtomWithIdx(30)->getIsAromatic());
+
+    CHECK(!m->getAtomWithIdx(28)->getIsAromatic());
+    CHECK(!m->getAtomWithIdx(29)->getIsAromatic());
+  }
+#endif
+  SECTION("more testing fun") {
+    auto m = "CC1C2c3ccccc3C(c3ccccc32)C1CN"_smiles;
+    REQUIRE(m);
+    CHECK(m->getAtomWithIdx(4)->getIsAromatic());
+    CHECK(m->getAtomWithIdx(14)->getIsAromatic());
+    CHECK(!m->getAtomWithIdx(2)->getIsAromatic());
+    CHECK(!m->getAtomWithIdx(9)->getIsAromatic());
+  }
+}
+
 TEST_CASE("Iterators") {
   auto m = "CCCCCCC=N"_smiles;
   REQUIRE(m);
@@ -2964,26 +3103,45 @@ TEST_CASE("molecules with single bond to metal atom use dative instead") {
       {"CC1=C(CCC(O)=O)C2=[N]3C1=Cc1c(C)c(C=C)c4C=C5C(C)=C(C=C)C6=[N]5[Fe]3(n14)n1c(=C6)c(C)c(CCC(O)=O)c1=C2",
        "C=CC1=C(C)C2=Cc3c(C=C)c(C)c4n3[Fe]35<-N2=C1C=c1c(C)c(CCC(=O)O)c(n13)=CC1=N->5C(=C4)C(C)=C1CCC(=O)O"},
       {"CC1=C(CCC([O-])=O)C2=[N+]3C1=Cc1c(C)c(C=C)c4C=C5C(C)=C(C=C)C6=[N+]5[Fe--]3(n14)n1c(=C6)c(C)c(CCC([O-])=O)c1=C2",
-       "C=CC1=C(C)C2=Cc3c(C=C)c(C)c4n3[Fe-2]35n6c(c(C)c(CCC(=O)[O-])c6=CC6=[N+]3C(=C4)C(C)=C6CCC(=O)[O-])=CC1=[N+]25"},
+       "C=Cc1c(C)c2cc3c(C)c(CCC(=O)[O-])c4cc5c(CCC(=O)[O-])c(C)c6cc7c(C=C)c(C)c8cc1n2[Fe-2](n65)([n+]87)[n+]34"},
       {"CC1=C(CCC(O)=O)C2=[N+]3C1=Cc1c(C)c(C=C)c4C=C5C(C)=C(C=C)C6=[N+]5[Fe--]3(n14)n1c(=C6)c(C)c(CCC(O)=O)c1=C2",
-       "C=CC1=C(C)C2=Cc3c(C=C)c(C)c4n3[Fe-2]35n6c(c(C)c(CCC(=O)O)c6=CC6=[N+]3C(=C4)C(C)=C6CCC(=O)O)=CC1=[N+]25"},
+       "C=Cc1c(C)c2cc3c(C)c(CCC(=O)O)c4cc5c(CCC(=O)O)c(C)c6cc7c(C=C)c(C)c8cc1n2[Fe-2](n65)([n+]87)[n+]34"},
       {"CCC1=[O+][Cu]2([O+]=C(CC)C1)[O+]=C(CC)CC(CC)=[O+]2",
        "CCC1=[O+][Cu]2([O+]=C(CC)C1)[O+]=C(CC)CC(CC)=[O+]2"}};
   for (size_t i = 0; i < test_vals.size(); ++i) {
     INFO(test_vals[i].first);
     RWMOL_SPTR m(RDKit::SmilesToMol(test_vals[i].first));
     CHECK(MolToSmiles(*m) == test_vals[i].second);
+    // make sure the metal atoms are not aromatic and have no aromatic bonds:
+    for (auto atom : m->atoms()) {
+      if (atom->getAtomicNum() == 26 || atom->getAtomicNum() == 29) {
+        CHECK(!atom->getIsAromatic());
+        for (auto bond : m->atomBonds(atom)) {
+          CHECK(!bond->getIsAromatic());
+        }
+      }
+    }
   }
+
   // make sure we can call cleanupOrganometallics() on non-sanitized molecules
   for (size_t i = 0; i < test_vals.size(); ++i) {
     INFO(test_vals[i].first);
     bool sanitize = false;
     RWMOL_SPTR m(RDKit::SmilesToMol(test_vals[i].first, 0, sanitize));
     MolOps::cleanUpOrganometallics(*m);
+    MolOps::sanitizeMol(*m);
     CHECK(MolToSmiles(*m) == test_vals[i].second);
+    // make sure the metal atoms are not aromatic and have no aromatic bonds:
+    for (auto atom : m->atoms()) {
+      if (atom->getAtomicNum() == 26 || atom->getAtomicNum() == 29) {
+        CHECK(!atom->getIsAromatic());
+        for (auto bond : m->atomBonds(atom)) {
+          CHECK(!bond->getIsAromatic());
+        }
+      }
+    }
   }
 }
-
 TEST_CASE(
     "cleanUpOrganometallics should produce canonical output.  cf PR6292") {
   std::vector<std::pair<std::string, std::string>> test_vals{
@@ -3207,8 +3365,8 @@ M  END
     CHECK(distToTarget.lengthSq() < 0.1);
   }
 
-  SECTION("Counterclockwise") {  // Chiral Tag (CCW) Different from CIPCode (R)
-                                 // due to different ordering of atoms
+  SECTION("Counterclockwise") {  // Chiral Tag (CCW) Different from CIPCode
+                                 // (R) due to different ordering of atoms
     std::string mb = R"CTAB(testmol
   CT1066645023
 
@@ -4529,8 +4687,8 @@ M  END
           }
         }
       }
-      // if we aren't in a five-ring (where the results of kekulize are normally
-      // fixed), wedge the double bond and flatten the other one
+      // if we aren't in a five-ring (where the results of kekulize are
+      // normally fixed), wedge the double bond and flatten the other one
       if (wedgedBond && dblBond &&
           !m->getRingInfo()->isBondInRingOfSize(dblBond->getIdx(), 5) &&
           !m->getRingInfo()->isBondInRingOfSize(wedgedBond->getIdx(), 5)) {
@@ -4548,8 +4706,8 @@ M  END
           // }
           // std::cerr << "\n\nbefore kekulize:" << std::endl;
           // m->debugMol(std::cerr);
-          // kekulize again now that we wedged the bonds that were set to double
-          // before
+          // kekulize again now that we wedged the bonds that were set to
+          // double before
           MolOps::Kekulize(*m, clearAromaticFlags);
           // and make sure that those didn't end up double again:
           for (auto atm : {bnd->getBeginAtom(), bnd->getEndAtom()}) {


Test patch:
diff --git a/Code/GraphMol/molopstest.cpp b/Code/GraphMol/molopstest.cpp
index 813f5aac1e7..64ed1a38278 100644
--- a/Code/GraphMol/molopstest.cpp
+++ b/Code/GraphMol/molopstest.cpp
@@ -536,7 +536,6 @@ void test5() {
   delete m;
 }
 
-
 void test8() {
   BOOST_LOG(rdInfoLog) << "-----------------------\n Testing Hydrogen Ops"
                        << std::endl;
@@ -3499,7 +3498,13 @@ void testSFNetIssue2196817() {
   {
     ROMOL_SPTR m = "C1=CC2=CC=C3C=CC4=CC=C5C=CN1*1*2*3*4N51"_smiles;
     for (const auto a : m->atoms()) {
-      TEST_ASSERT(a->getIsAromatic());
+      if (a->getAtomicNum() > 0 || a->getIdx() == 15 || a->getIdx() == 18) {
+        TEST_ASSERT(a->getIsAromatic());
+      } else if (a->getIdx() == 16 || a->getIdx() == 17) {
+        TEST_ASSERT(!a->getIsAromatic());
+      } else {
+        TEST_ASSERT(0);
+      }
     }
     unsigned int nNonAromaticBonds = 0;
     for (const auto b : m->bonds()) {
@@ -4111,8 +4116,8 @@ void testSFNetIssue3525076() {
 void testBasicCanon() {
   BOOST_LOG(rdInfoLog) << "-------------------------------------" << std::endl;
   BOOST_LOG(rdInfoLog) << "Testing canonicalization basics" << std::endl;
-// these are all cases that were problematic at one time or another during
-// the canonicalization rewrite.
+  // these are all cases that were problematic at one time or another during
+  // the canonicalization rewrite.
   {
     std::string smi = "FC1C(=C/Cl)\\C1";
     RWMol *m = SmilesToMol(smi);
@@ -7129,21 +7134,22 @@ void testGithubIssue868() {
   }
 
   // test atom type query merging
-  for (int aromatic =0; aromatic<2; ++aromatic) {
+  for (int aromatic = 0; aromatic < 2; ++aromatic) {
     sstrm.str("");
     TEST_ASSERT(sstrm.str() == "");
     RWMol m;
     QueryAtom *qa = new QueryAtom();
     qa->setQuery(makeAtomTypeQuery(1, aromatic));
-    qa->expandQuery(makeAtomNumQuery(6), Queries::CompositeQueryType::COMPOSITE_OR);
+    qa->expandQuery(makeAtomNumQuery(6),
+                    Queries::CompositeQueryType::COMPOSITE_OR);
     m.addAtom(qa, true, true);
     MolOps::mergeQueryHs(m);
     TEST_ASSERT(sstrm.str().find("merging explicit H queries involved in "
-				 "ORs is not supported") != std::string::npos);
+                                 "ORs is not supported") != std::string::npos);
     TEST_ASSERT(sstrm.str().find("This query will not be merged") !=
-		std::string::npos);
+                std::string::npos);
   }
-   
+
   {
     sstrm.str("");
     TEST_ASSERT(sstrm.str() == "");
@@ -8497,11 +8503,11 @@ void testHasQueryHs() {
               has_only_query_hs);
 
   auto github7687 = "[#1,#6,#7]"_smarts;
-  TEST_ASSERT(RDKit::MolOps::hasQueryHs(*github7687) == has_unmergeable_hs );
+  TEST_ASSERT(RDKit::MolOps::hasQueryHs(*github7687) == has_unmergeable_hs);
   auto github7687b = "[1;#7,#1,#6]"_smarts;
-  TEST_ASSERT(RDKit::MolOps::hasQueryHs(*github7687b) == has_unmergeable_hs );
+  TEST_ASSERT(RDKit::MolOps::hasQueryHs(*github7687b) == has_unmergeable_hs);
   auto github7687c = "[1&#7,#1,#6]"_smarts;
-  TEST_ASSERT(RDKit::MolOps::hasQueryHs(*github7687c) == has_unmergeable_hs );
+  TEST_ASSERT(RDKit::MolOps::hasQueryHs(*github7687c) == has_unmergeable_hs);
   BOOST_LOG(rdInfoLog) << "\tdone" << std::endl;
 }