* add test

* potential fix

* whops\!

* support disabling ring stereo in rankMolAtoms

* pass atom ranks into the ring-stereo detection code

* all tests pass

* forgot a file

---------

Co-authored-by: Ric R. <ricrogz@gmail.com>
This commit is contained in:
Greg Landrum
2025-09-03 12:15:09 +02:00
committed by GitHub
parent cf269aa813
commit 6d8fca2fbf
10 changed files with 422 additions and 90 deletions

View File

@@ -1450,7 +1450,9 @@ void findAtomNeighborsHelper(const ROMol &mol, const Atom *atom,
// 4) three ring neighbors with two different ranks
// example for this last one: C[C@H]1CC2CCCC3CCCC(C1)[C@@H]23
// Note that N atoms are only candidates if they are in a 3-ring
bool atomIsCandidateForRingStereochem(const ROMol &mol, const Atom *atom) {
bool atomIsCandidateForRingStereochem(
const ROMol &mol, const Atom *atom,
const std::vector<unsigned int> &atomRanks) {
PRECONDITION(atom, "bad atom");
bool res = false;
std::set<unsigned int> nbrRanks;
@@ -1474,23 +1476,20 @@ bool atomIsCandidateForRingStereochem(const ROMol &mol, const Atom *atom) {
} else {
const Atom *nbr = bond->getOtherAtom(atom);
ringNbrs.push_back(nbr);
unsigned int rnk = 0;
nbr->getPropIfPresent(common_properties::_CIPRank, rnk);
nbrRanks.insert(rnk);
nbrRanks.insert(atomRanks[nbr->getIdx()]);
}
}
unsigned int rank1 = 0, rank2 = 0;
// std::cerr << "!!!! " << atom->getIdx() << " " << nbrRanks.size() << " "
// << ringNbrs.size() << " " << nonRingNbrs.size() << std::endl;
switch (nonRingNbrs.size()) {
case 2:
if (nonRingNbrs[0]->getPropIfPresent(common_properties::_CIPRank,
rank1) &&
nonRingNbrs[1]->getPropIfPresent(common_properties::_CIPRank,
rank2)) {
res = rank1 != rank2;
}
// they have to be different
res = atomRanks[nonRingNbrs[0]->getIdx()] !=
atomRanks[nonRingNbrs[1]->getIdx()];
break;
case 1:
if (ringNbrs.size() >= 2) {
if (ringNbrs.size() > nbrRanks.size()) {
res = true;
}
break;
@@ -1507,6 +1506,7 @@ bool atomIsCandidateForRingStereochem(const ROMol &mol, const Atom *atom) {
res = false;
}
}
// std::cerr<<" candidate? "<<res<<std::endl;
atom->setProp(common_properties::_ringStereochemCand, res, 1);
}
return res;
@@ -1515,7 +1515,8 @@ bool atomIsCandidateForRingStereochem(const ROMol &mol, const Atom *atom) {
// finds all possible chiral special cases.
// at the moment this is just candidates for ring stereochemistry
void findChiralAtomSpecialCases(ROMol &mol,
boost::dynamic_bitset<> &possibleSpecialCases) {
boost::dynamic_bitset<> &possibleSpecialCases,
const std::vector<unsigned int> &atomRanks) {
PRECONDITION(possibleSpecialCases.size() >= mol.getNumAtoms(),
"bit vector too small");
possibleSpecialCases.reset();
@@ -1534,7 +1535,7 @@ void findChiralAtomSpecialCases(ROMol &mol,
if (atom->getChiralTag() == Atom::CHI_UNSPECIFIED ||
atom->hasProp(common_properties::_CIPCode) ||
!mol.getRingInfo()->numAtomRings(atom->getIdx()) ||
!atomIsCandidateForRingStereochem(mol, atom)) {
!atomIsCandidateForRingStereochem(mol, atom, atomRanks)) {
continue;
}
// do a BFS from this ring atom along ring bonds and find other
@@ -1566,7 +1567,7 @@ void findChiralAtomSpecialCases(ROMol &mol,
atomsSeen.set(ratom->getIdx());
if (ratom->getChiralTag() != Atom::CHI_UNSPECIFIED &&
!ratom->hasProp(common_properties::_CIPCode) &&
atomIsCandidateForRingStereochem(mol, ratom)) {
atomIsCandidateForRingStereochem(mol, ratom, atomRanks)) {
int same = (ratom->getChiralTag() == atom->getChiralTag()) ? 1 : -1;
ringStereoAtoms.push_back(same * (ratom->getIdx() + 1));
INT_VECT oringatoms(0);
@@ -2369,10 +2370,6 @@ void legacyStereoPerception(ROMol &mol, bool cleanIt,
}
if (cleanIt) {
// if the ranks are needed again, this will force them to be
// re-calculated based on the stereo calculated above.
// atomRanks.clear();
for (auto atom : mol.atoms()) {
if (atom->hasProp(common_properties::_ringStereochemCand)) {
atom->clearProp(common_properties::_ringStereochemCand);
@@ -2382,7 +2379,7 @@ void legacyStereoPerception(ROMol &mol, bool cleanIt,
}
}
boost::dynamic_bitset<> possibleSpecialCases(mol.getNumAtoms());
Chirality::findChiralAtomSpecialCases(mol, possibleSpecialCases);
Chirality::findChiralAtomSpecialCases(mol, possibleSpecialCases, atomRanks);
for (auto atom : mol.atoms()) {
if (atom->getChiralTag() != Atom::CHI_UNSPECIFIED &&

View File

@@ -818,15 +818,17 @@ bool updateAtoms(ROMol &mol, const std::vector<unsigned int> &aranks,
needAnotherRound = true;
}
sinfos.push_back(std::move(sinfo));
} else if (possibleAtoms[aidx]) {
} else {
// Only do another round if we change anything here
needAnotherRound |= possibleAtoms[aidx];
possibleAtoms[aidx] = 0;
atomSymbols[aidx] = getAtomCompareSymbol(*atom);
needAnotherRound = true;
// if this was creating possible ring stereo, update that info now
if (possibleRingStereoAtoms[aidx]) {
--possibleRingStereoAtoms[aidx];
if (!possibleRingStereoAtoms[aidx]) {
needAnotherRound = true;
// we're no longer in any ring with possible ring stereo. Go
// update all the other atoms/bonds in rings that we're in:
for (unsigned int ridx = 0;
@@ -838,6 +840,12 @@ bool updateAtoms(ROMol &mol, const std::vector<unsigned int> &aranks,
--possibleRingStereoAtoms[raidx];
if (possibleRingStereoAtoms[raidx]) {
++nHere;
} else {
// In case there's no possible ring stereo anymore,
// un-fix these atoms so we can recheck them in the next
// iteration, for the case that they are no longer chiral
// after removing the current chirality
fixedAtoms[raidx] = false;
}
}
}
@@ -1032,6 +1040,10 @@ void cleanMolStereo(ROMol &mol, const boost::dynamic_bitset<> &fixedAtoms,
}
} // namespace
void findChiralAtomSpecialCases(ROMol &mol,
boost::dynamic_bitset<> &possibleSpecialCases,
const std::vector<unsigned int> &atomRanks);
std::vector<StereoInfo> runCleanup(ROMol &mol, bool flagPossible,
bool cleanIt) {
// This potentially does two passes of "canonicalization" to identify
@@ -1121,13 +1133,14 @@ std::vector<StereoInfo> runCleanup(ROMol &mol, bool flagPossible,
const bool breakTies = false;
const bool includeAtomMaps = false;
const bool includeChiralPresence = false;
const bool useRingStereo = false;
// Now apply the canonical atom ranking code with basic connectivity
// invariants The necessary condition for chirality is that an atom's
// neighbors must have unique ranks
Canon::rankFragmentAtoms(mol, aranks, atomsInPlay, bondsInPlay,
&atomSymbols, &bondSymbols, breakTies,
includeChirality, includeIsotopes, includeAtomMaps,
includeChiralPresence);
includeChiralPresence, useRingStereo);
#endif
// check if any new atoms definitely now have stereo; do another loop if
// so
@@ -1209,10 +1222,11 @@ std::vector<StereoInfo> runCleanup(ROMol &mol, bool flagPossible,
const bool includeIsotopes = false;
const bool includeAtomMaps = false;
const bool includeChiralPresence = false;
Canon::rankFragmentAtoms(mol, aranks, atomsInPlay, bondsInPlay,
&atomSymbols, &bondSymbols, breakTies,
includeChirality, includeIsotopes,
includeAtomMaps, includeChiralPresence);
const bool useRingStereo = false;
Canon::rankFragmentAtoms(
mol, aranks, atomsInPlay, bondsInPlay, &atomSymbols, &bondSymbols,
breakTies, includeChirality, includeIsotopes, includeAtomMaps,
includeChiralPresence, useRingStereo);
#endif
needAnotherRound = updateAtoms(
mol, aranks, atomSymbols, possibleAtoms, knownAtoms, fixedAtoms,
@@ -1222,6 +1236,10 @@ std::vector<StereoInfo> runCleanup(ROMol &mol, bool flagPossible,
knownAtoms, knownBonds, fixedBonds, res);
}
}
boost::dynamic_bitset<> possibleSpecialCases(mol.getNumAtoms());
findChiralAtomSpecialCases(mol, possibleSpecialCases, aranks);
for (const auto atom : mol.atoms()) {
atom->setProp<unsigned int>(common_properties::_ChiralAtomRank,
aranks[atom->getIdx()], true);

View File

@@ -1 +1 @@
CC1C[C@@]2(CC[C@H](C)CC2)CC[C@@H]1C |(3.08,-2.0006,;2.31,-0.6669,;0.77,-0.6669,;0,0.6668,;-0.77,2.0005,;-2.31,2.0005,;-3.08,0.6668,;-4.62,0.6668,;-2.31,-0.6669,;-0.77,-0.6669,;0.77,2.0005,;2.31,2.0005,;3.08,0.6668,;4.62,0.6668,),wD:12.14,wU:3.3,6.6|
CC1C[C@]2(CC[C@@H]1C)CC[C@H](C)CC2 |(3.08,-2.0006,;2.31,-0.6669,;0.77,-0.6669,;0,0.6668,;0.77,2.0005,;2.31,2.0005,;3.08,0.6668,;4.62,0.6668,;-0.77,2.0005,;-2.31,2.0005,;-3.08,0.6668,;-4.62,0.6668,;-2.31,-0.6669,;-0.77,-0.6669,),wD:6.7,wU:3.8,10.11|

View File

@@ -1 +1 @@
CC1C[C@@]2(CC[C@H](C)CC2)CC[C@@H]1C |(3.08,-2.0006,;2.31,-0.6669,;0.77,-0.6669,;0,0.6668,;-0.77,2.0005,;-2.31,2.0005,;-3.08,0.6668,;-4.62,0.6668,;-2.31,-0.6669,;-0.77,-0.6669,;0.77,2.0005,;2.31,2.0005,;3.08,0.6668,;4.62,0.6668,),wD:12.14,wU:3.3,6.6|
CC1C[C@]2(CC[C@@H]1C)CC[C@H](C)CC2 |(3.08,-2.0006,;2.31,-0.6669,;0.77,-0.6669,;0,0.6668,;0.77,2.0005,;2.31,2.0005,;3.08,0.6668,;4.62,0.6668,;-0.77,2.0005,;-2.31,2.0005,;-3.08,0.6668,;-4.62,0.6668,;-2.31,-0.6669,;-0.77,-0.6669,),wD:6.7,wU:3.8,10.11|

View File

@@ -970,10 +970,12 @@ void canonicalizeStereoGroups(std::unique_ptr<ROMol> &mol,
const bool useNonStereoRanks = false;
const bool includeChiralPresence = true;
const bool includeStereoGroups = false;
const bool includeRingStereo = false;
Canon::rankMolAtoms(*mol, ranks, breakTies, includeChirality, includeIsotopes,
includeAtomMaps, includeChiralPresence,
includeStereoGroups, useNonStereoRanks);
includeStereoGroups, useNonStereoRanks,
includeRingStereo);
for (auto atom : mol->atoms()) {
atom->setProp(common_properties::_CanonicalRankingNumber,

View File

@@ -464,15 +464,15 @@ TEST_CASE("pseudoTest1") {
"C[C@H](Cl)C[C@@H](C)Cl"},
{"N[C@H]1CC[C@@H](O)CC1 |a:1,4|", "N[C@H]1CC[C@@H](O)CC1 |o1:1,4|",
"N[C@@H]1CC[C@H](O)CC1"},
"N[C@H]1CC[C@@H](O)CC1"},
{"N[C@H]1CC[C@@H](O)CC1 |a:1,4|", "N[C@H]1CC[C@@H](O)CC1 |&1:1,4|",
"N[C@@H]1CC[C@H](O)CC1"},
"N[C@H]1CC[C@@H](O)CC1"},
{"N[C@H]1CC[C@@H](O)CC1 |a:1,4|", "N[C@@H]1CC[C@H](O)CC1 |a:1,4|",
"N[C@@H]1CC[C@H](O)CC1"},
"N[C@H]1CC[C@@H](O)CC1"},
{"N[C@H]1CC[C@@H](O)CC1 |a:1,4|", "N[C@@H]1CC[C@H](O)CC1 |o1:1,4|",
"N[C@@H]1CC[C@H](O)CC1"},
"N[C@H]1CC[C@@H](O)CC1"},
{"N[C@H]1CC[C@@H](O)CC1 |a:1,4|", "N[C@@H]1CC[C@H](O)CC1 |&1:1,4|",
"N[C@@H]1CC[C@H](O)CC1"},
"N[C@H]1CC[C@@H](O)CC1"},
// {"C[C@H]1C[C@@H](C)C[C@@H](C)C1 |o1:1,o2:6,o3:3|",
// "C[C@@H]1C[C@H](C)C[C@@H](C)C1 |a:3,o1:6,o3:1|",
@@ -484,63 +484,63 @@ TEST_CASE("pseudoTest1") {
{"C[C@H]1CC[C@H](CC1)C1CC(CC(C1)[C@@H]1CC[C@H](C)CC1)[C@@H]1CC[C@H](C)CC1 |o1:23,o2:20,o3:13,o4:16,o5:4,&6:1|",
"C[C@H]1CC[C@@H](CC1)C1CC(CC(C1)[C@H]1CC[C@H](C)CC1)[C@H]1CC[C@H](C)CC1 |a:1,13,20,o2:4,o6:16,&4:23|",
"C[C@H]1CC[C@@H](C2CC([C@@H]3CC[C@H](C)CC3)CC([C@@H]3CC[C@H](C)CC3)C2)CC1 |a:4,8,17,o1:1,o2:11,&1:20|"},
"C[C@H]1CC[C@@H](C2CC([C@H]3CC[C@@H](C)CC3)CC([C@H]3CC[C@@H](C)CC3)C2)CC1 |a:4,8,17,o1:1,o2:11,&1:20|"},
{"C[C@H]1CC[C@H](CC1)C1CC(CC(C1)[C@@H]1CC[C@H](C)CC1)[C@@H]1CC[C@H](C)CC1 |o1:23,o2:20,o3:13,o4:16,o5:4,o6:1|",
"C[C@H]1CC[C@@H](CC1)C1CC(CC(C1)[C@H]1CC[C@H](C)CC1)[C@H]1CC[C@H](C)CC1 |a:4,13,23,o2:20,o4:16,o6:1|",
"C[C@H]1CC[C@@H](C2CC([C@@H]3CC[C@H](C)CC3)CC([C@@H]3CC[C@H](C)CC3)C2)CC1 |a:4,8,17,o1:1,o2:11,o3:20|"},
"C[C@H]1CC[C@@H](C2CC([C@H]3CC[C@@H](C)CC3)CC([C@H]3CC[C@@H](C)CC3)C2)CC1 |a:4,8,17,o1:1,o2:11,o3:20|"},
{"C[C@H]1CC[C@@H](C[C@@H]2CC[C@H](C)CC2)CC1 |a:9,o1:6,&1:4,&2:1|",
"C[C@H]1CC[C@H](C[C@H]2CC[C@H](C)CC2)CC1 |a:4,9,&1:6,o2:1|",
"C[C@H]1CC[C@H](C[C@@H]2CC[C@H](C)CC2)CC1 |a:4,6,o1:1,&1:9|"},
"C[C@H]1CC[C@@H](C[C@H]2CC[C@@H](C)CC2)CC1 |a:4,6,o1:1,&1:9|"},
{"C[C@H]1CC[C@@H](C[C@@H]2CC[C@H](C)CC2)CC1 |o1:6,o2:1,9,o3:4|",
"C[C@H]1CC[C@H](C[C@H]2CC[C@H](C)CC2)CC1 |o1:6,o2:1,9,o3:4|",
"C[C@H]1CC[C@@H](C[C@@H]2CC[C@H](C)CC2)CC1 |a:4,6,o1:1,o2:9|"},
"C[C@H]1CC[C@@H](C[C@H]2CC[C@@H](C)CC2)CC1 |a:4,6,o1:1,o2:9|"},
{"C[C@H]1CC[C@@H](C[C@@H]2CC[C@H](C)CC2)CC1 |o1:6,o2:1,9,o3:4|",
"C[C@H]1CC[C@H](C[C@H]2CC[C@H](C)CC2)CC1 |a:4,9,o1:6,o2:1|",
"C[C@H]1CC[C@@H](C[C@@H]2CC[C@H](C)CC2)CC1 |a:4,6,o1:1,o2:9|"},
"C[C@H]1CC[C@@H](C[C@H]2CC[C@@H](C)CC2)CC1 |a:4,6,o1:1,o2:9|"},
{"N[C@H]1CC[C@@H](O)CC1", "N[C@@H]1CC[C@H](O)CC1",
"N[C@@H]1CC[C@H](O)CC1"},
"N[C@H]1CC[C@@H](O)CC1"},
{"N[C@H]1CC[C@@H](O)CC1 |o2:1,4|", "N[C@@H]1CC[C@H](O)CC1 |o2:1,4|",
"N[C@@H]1CC[C@H](O)CC1"},
"N[C@H]1CC[C@@H](O)CC1"},
{"N[C@H]1CC[C@@H](O)CC1 |&2:1,4|", "N[C@@H]1CC[C@H](O)CC1 |&2:1,4|",
"N[C@@H]1CC[C@H](O)CC1"},
"N[C@H]1CC[C@@H](O)CC1"},
{"N[C@H]1CC[C@@H](O)CC1 |a:1,4|", "N[C@@H]1CC[C@H](O)CC1 |a:1,4|",
"N[C@@H]1CC[C@H](O)CC1"},
"N[C@H]1CC[C@@H](O)CC1"},
// no enhanced stereo
{"CC(C)[C@H]1CCCCN1C(=O)[C@H]1CC[C@@H](C)CC1",
"CC(C)[C@H]1CCCCN1C(=O)[C@@H]1CC[C@H](C)CC1",
"CC(C)[C@H]1CCCCN1C(=O)[C@@H]1CC[C@H](C)CC1"},
"CC(C)[C@H]1CCCCN1C(=O)[C@H]1CC[C@@H](C)CC1"},
// enhance stereo abs,abs,abs
{"CC(C)[C@H]1CCCCN1C(=O)[C@H]1CC[C@@H](C)CC1 |a:3,11,14|",
"CC(C)[C@H]1CCCCN1C(=O)[C@@H]1CC[C@H](C)CC1 |a:3,11,14|",
"CC(C)[C@H]1CCCCN1C(=O)[C@@H]1CC[C@H](C)CC1"},
"CC(C)[C@H]1CCCCN1C(=O)[C@H]1CC[C@@H](C)CC1"},
// abs, abs, or and abs, or abs
{"CC(C)[C@H]1CCCCN1C(=O)[C@H]1CC[C@@H](C)CC1 |a:3,11,o1:14|",
"CC(C)[C@H]1CCCCN1C(=O)[C@H]1CC[C@H](C)CC1 |a:3,11,o1:14|",
"CC(C)[C@H]1CCCCN1C(=O)[C@@H]1CC[C@H](C)CC1 |a:3,11,o1:14|"},
"CC(C)[C@H]1CCCCN1C(=O)[C@H]1CC[C@H](C)CC1 |a:3,11,o1:14|"},
{"CC(C)[C@H]1CCCCN1C(=O)[C@H]1CC[C@@H](C)CC1 |a:3,11,o1:14|",
"CC(C)[C@H]1CCCCN1C(=O)[C@H]1CC[C@H](C)CC1 |a:3,14,o1:11|",
"CC(C)[C@H]1CCCCN1C(=O)[C@@H]1CC[C@H](C)CC1 |a:3,11,o1:14|"},
"CC(C)[C@H]1CCCCN1C(=O)[C@H]1CC[C@H](C)CC1 |a:3,11,o1:14|"},
{"CC(C)[C@H]1CCCCN1C(=O)[C@H]1CC[C@@H](C)CC1 |a:3,11,o1:14|",
"CC(C)[C@H]1CCCCN1C(=O)[C@H]1CC[C@@H](C)CC1 |a:3,o1:11,o2:14|",
"CC(C)[C@H]1CCCCN1C(=O)[C@@H]1CC[C@H](C)CC1 |a:3,11,o1:14|"},
"CC(C)[C@H]1CCCCN1C(=O)[C@H]1CC[C@H](C)CC1 |a:3,11,o1:14|"},
{"CC(C)[C@H]1CCCCN1C(=O)[C@H]1CC[C@@H](C)CC1 |a:3,11,&1:14|",
"CC(C)[C@H]1CCCCN1C(=O)[C@H]1CC[C@H](C)CC1 |a:3,11,&1:14|",
"CC(C)[C@H]1CCCCN1C(=O)[C@@H]1CC[C@H](C)CC1 |a:3,11,&1:14|"},
"CC(C)[C@H]1CCCCN1C(=O)[C@H]1CC[C@H](C)CC1 |a:3,11,&1:14|"},
{"CC(C)[C@H]1CCCCN1C(=O)[C@H]1CC[C@@H](C)CC1 |a:3,11,&1:14|",
"CC(C)[C@H]1CCCCN1C(=O)[C@H]1CC[C@H](C)CC1 |a:3,14,&1:11|",
"CC(C)[C@H]1CCCCN1C(=O)[C@@H]1CC[C@H](C)CC1 |a:3,11,&1:14|"},
"CC(C)[C@H]1CCCCN1C(=O)[C@H]1CC[C@H](C)CC1 |a:3,11,&1:14|"},
{"CC(C)[C@H]1CCCCN1C(=O)[C@H]1CC[C@@H](C)CC1 |a:3,11,&1:14|",
"CC(C)[C@H]1CCCCN1C(=O)[C@H]1CC[C@@H](C)CC1 |a:3,&1:11,&2:14|",
"CC(C)[C@H]1CCCCN1C(=O)[C@@H]1CC[C@H](C)CC1 |a:3,11,&1:14|"},
"CC(C)[C@H]1CCCCN1C(=O)[C@H]1CC[C@H](C)CC1 |a:3,11,&1:14|"},
};
@@ -563,6 +563,9 @@ TEST_CASE("pseudoTest1") {
idxV[i] = i;
}
RDKit::canonicalizeStereoGroups(mol2);
auto outSmi2 = MolToCXSmiles(*mol2);
REQUIRE(outSmi2 == smiExpected);
auto randomGen = std::mt19937(0xf00d);
for (auto i = 0; i < 100; ++i) {
INFO("i: " << i);
@@ -572,14 +575,11 @@ TEST_CASE("pseudoTest1") {
std::unique_ptr<ROMol> nmol{MolOps::renumberAtoms(*mol1, nVect)};
RDKit::canonicalizeStereoGroups(nmol);
RDKit::canonicalizeStereoGroups(mol2);
auto outSmi1 = MolToCXSmiles(*nmol);
auto outSmi2 = MolToCXSmiles(*mol2);
CHECK(outSmi1 == outSmi2);
CHECK(outSmi1 == smiExpected);
CHECK(outSmi2 == smiExpected);
}
}
}
@@ -1118,4 +1118,42 @@ TEST_CASE("meso impact on atom ranking") {
CHECK(ranks[3] != ranks[10]);
}
}
}
TEST_CASE("allow disabling ring stereo in ranking") {
UseLegacyStereoPerceptionFixture reset_stereo_perception(false);
std::string smi = "C[C@@H]1CC[C@@H](C)CC1";
auto m = v2::SmilesParse::MolFromSmiles(smi);
REQUIRE(m);
bool breakTies = false;
bool includeChirality = true;
bool includeIsotopes = true;
bool includeAtomMaps = true;
bool includeChiralPresence = false;
bool includeStereoGroups = true;
bool useNonStereoRanks = false;
bool includeRingStereo = true;
std::vector<unsigned int> res1;
Canon::rankMolAtoms(*m, res1, breakTies, includeChirality, includeIsotopes,
includeAtomMaps, includeChiralPresence,
includeStereoGroups, useNonStereoRanks,
includeRingStereo);
CHECK(res1.size() == m->getNumAtoms());
CHECK(res1[2] != res1[7]);
CHECK(res1[2] == res1[3]);
CHECK(res1[3] != res1[6]);
CHECK(res1[6] == res1[7]);
includeRingStereo = false;
Canon::rankMolAtoms(*m, res1, breakTies, includeChirality, includeIsotopes,
includeAtomMaps, includeChiralPresence,
includeStereoGroups, useNonStereoRanks,
includeRingStereo);
CHECK(res1.size() == m->getNumAtoms());
CHECK(res1[2] == res1[7]);
CHECK(res1[2] == res1[3]);
CHECK(res1[3] == res1[6]);
CHECK(res1[6] == res1[7]);
}

View File

@@ -18,6 +18,7 @@
#include <GraphMol/StereoGroup.h>
#include <GraphMol/Chirality.h>
#include <GraphMol/MolOps.h>
#include <GraphMol/new_canon.h>
#include <GraphMol/test_fixtures.h>
#include <GraphMol/FileParsers/FileParsers.h>
@@ -2149,7 +2150,7 @@ TEST_CASE(
RDLog::LogStateSetter setter; // disable irritating warning messages
auto molblock = R"CTAB(
RDKit 3D
0 0 0 0 0 0 0 0 0 0999 V3000
M V30 BEGIN CTAB
M V30 COUNTS 5 4 0 0 0
@@ -2679,7 +2680,7 @@ TEST_CASE("useLegacyStereoPerception feature flag") {
CHECK(m->getAtomWithIdx(9)->getChiralTag() != Atom::CHI_UNSPECIFIED);
}
std::string molblock = R"CTAB(
Mrv2108 05202206352D
Mrv2108 05202206352D
0 0 0 0 0 999 V3000
M V30 BEGIN CTAB
@@ -2797,7 +2798,7 @@ M END
TEST_CASE("github 5307: AssignAtomChiralTagsFromStructure ignores Hydrogens") {
std::string mb = R"CTAB(
RDKit 3D
0 0 0 0 0 0 0 0 0 0999 V3000
M V30 BEGIN CTAB
M V30 COUNTS 5 4 0 0 0
@@ -3317,7 +3318,7 @@ void testStereoValidationFromMol(std::string molBlock,
}
std::string validateStereoMolBlockSpiro = R"(
Mrv2308 06232316112D
Mrv2308 06232316112D
0 0 0 0 0 999 V3000
M V30 BEGIN CTAB
@@ -3359,7 +3360,7 @@ M END
)";
std::string validateStereoMolBlockDoubleBondNoStereo = R"(
Mrv2308 06232316392D
Mrv2308 06232316392D
0 0 0 0 0 999 V3000
M V30 BEGIN CTAB
@@ -3391,7 +3392,7 @@ M END
)";
std::string validateStereoMolBlockDoubleBondNoStereo2 = R"(
Mrv0541 07011416342D
Mrv0541 07011416342D
21 22 0 0 0 0 999 V2000
-1.9814 1.4834 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
@@ -3449,7 +3450,7 @@ $$$$
)";
std::string validateStereoError1 = R"(
-ISIS- -- StrEd --
-ISIS- -- StrEd --
29 32 0 0 0 0 0 0 0 0999 V2000
-1.2050 -4.7172 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
@@ -3525,7 +3526,7 @@ $$$$
)";
std::string validateStereoUniq1 = R"(
Mrv0541 06301412152D
Mrv0541 06301412152D
15 15 0 0 0 0 999 V2000
1.0464 -0.3197 0.0000 N 0 0 0 0 0 0 0 0 0 0 0 0
@@ -3764,7 +3765,7 @@ TEST_CASE(
}
SECTION("ensure we can enumerate stereo on either double bonds") {
auto mol = R"CTAB(
Mrv2004 11072316002D
Mrv2004 11072316002D
0 0 0 0 0 999 V3000
M V30 BEGIN CTAB
@@ -3807,7 +3808,7 @@ M END)CTAB"_ctab;
TEST_CASE("adding two wedges to chiral centers") {
SECTION("basics") {
auto mol = R"CTAB(
Mrv2219 02112315062D
Mrv2219 02112315062D
0 0 0 0 0 999 V3000
M V30 BEGIN CTAB
@@ -4102,7 +4103,7 @@ TEST_CASE("zero bond-length chirality cases") {
SECTION("basics") {
{
auto m = R"CTAB(derived from CHEMBL3183068
Mrv2211 07202306222D
Mrv2211 07202306222D
0 0 0 0 0 999 V3000
M V30 BEGIN CTAB
@@ -4140,7 +4141,7 @@ M END
}
{
auto m = R"CTAB(derived from CHEMBL3183068
Mrv2211 07202306222D
Mrv2211 07202306222D
0 0 0 0 0 999 V3000
M V30 BEGIN CTAB
@@ -4255,7 +4256,7 @@ M END
SECTION("four-coordinate") {
{
auto m = R"CTAB(
Mrv2211 07202306492D
Mrv2211 07202306492D
0 0 0 0 0 999 V3000
M V30 BEGIN CTAB
@@ -4282,7 +4283,7 @@ M END
}
{
auto m = R"CTAB(
Mrv2211 07202306492D
Mrv2211 07202306492D
0 0 0 0 0 999 V3000
M V30 BEGIN CTAB
@@ -4309,7 +4310,7 @@ M END
}
{
auto m = R"CTAB(
Mrv2211 07202306492D
Mrv2211 07202306492D
0 0 0 0 0 999 V3000
M V30 BEGIN CTAB
@@ -4470,7 +4471,7 @@ M END
auto m =
R"CTAB(derived from CHEMBL2373651. This was wrong in the RDKit implementation
Mrv2211 07212313282D
0 0 0 0 0 999 V3000
M V30 BEGIN CTAB
M V30 COUNTS 7 7 0 0 1
@@ -4666,7 +4667,7 @@ M V30 BEGIN BOND
M V30 1 1 2 1 CFG=1
M V30 2 1 3 2
M V30 3 1 4 6
M V30 4 1 5 3
M V30 4 1 5 3
M V30 5 1 6 2
M V30 6 1 5 7 CFG=1
M V30 7 1 2 8 CFG=3
@@ -4925,7 +4926,7 @@ TEST_CASE(
UseLegacyStereoPerceptionFixture reset_stereo_perception(legacy_stereo);
auto m = R"CTAB(
Mrv2311 12122315472D
Mrv2311 12122315472D
0 0 0 0 0 999 V3000
M V30 BEGIN CTAB
@@ -4961,7 +4962,7 @@ TEST_CASE(
UseLegacyStereoPerceptionFixture reset_stereo_perception(legacy_stereo);
auto m = R"CTAB(
Mrv2211 01252410552D
Mrv2211 01252410552D
10 9 0 0 0 0 999 V2000
0.0000 -1.4364 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
@@ -5237,7 +5238,7 @@ M END
}
SECTION("cage") {
auto m = R"CTAB(
Mrv2305 03052406362D
Mrv2305 03052406362D
0 0 0 0 0 999 V3000
M V30 BEGIN CTAB
@@ -5535,7 +5536,7 @@ M END
}
SECTION("favor larger rings") {
auto m = R"CTAB(
Mrv2401 04262410272D
Mrv2401 04262410272D
0 0 0 0 0 999 V3000
M V30 BEGIN CTAB
@@ -6020,7 +6021,7 @@ TEST_CASE(
TEST_CASE("Github issue #7983: stereogroup lost on chiral sulfoxide") {
SECTION("basics") {
auto m = R"CTAB(
Mrv2317 02032512242D
Mrv2317 02032512242D
0 0 0 0 0 999 V3000
M V30 BEGIN CTAB
@@ -6151,7 +6152,7 @@ TEST_CASE("Github #8420: imines and crossed bonds") {
UseLegacyStereoPerceptionFixture reset_stereo_perception(useLegacy);
SECTION("as reported") {
std::string ctab = R"CTAB(
MJ250100
MJ250100
7 7 0 0 1 0 0 0 0 0999 V2000
0.6961 0.4995 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
@@ -6173,4 +6174,177 @@ M END)CTAB";
REQUIRE(m);
CHECK(m->getBondWithIdx(0)->getStereo() == Bond::BondStereo::STEREONONE);
}
}
}
TEST_CASE(
"Github #8712: Modern stereo + 3D SD file leads to bad stereo detection for some molecules") {
UseLegacyStereoPerceptionFixture reset_stereo_perception(false);
SECTION("as reported") {
std::string ctab = R"CTAB(
RDKit 3D
0 0 0 0 0 0 0 0 0 0999 V3000
M V30 BEGIN CTAB
M V30 COUNTS 23 23 0 0 0
M V30 BEGIN ATOM
M V30 1 C 2.628222 -0.014974 0.390914 0
M V30 2 N 1.918195 0.260032 -0.835284 0
M V30 3 C 0.487110 0.206615 -0.681308 0
M V30 4 C 0.079788 -1.165009 -0.217881 0
M V30 5 C -1.428295 -1.378335 -0.390798 0
M V30 6 S -2.156694 -0.136366 0.671587 0
M V30 7 O -1.585579 -0.347357 2.045208 0
M V30 8 O -3.651696 -0.172351 0.647489 0
M V30 9 C -1.519569 1.415993 0.067834 0
M V30 10 C -0.010801 1.307036 0.207366 0
M V30 11 H 1.988100 -0.320407 1.230786 0
M V30 12 H 3.238742 0.867078 0.711200 0
M V30 13 H 3.359131 -0.825206 0.185242 0
M V30 14 H 2.223613 -0.317010 -1.647727 0
M V30 15 H 0.041778 0.347830 -1.688472 0
M V30 16 H 0.284353 -1.370499 0.832996 0
M V30 17 H 0.580676 -1.905979 -0.844599 0
M V30 18 H -1.673500 -2.379109 -0.016810 0
M V30 19 H -1.696056 -1.195326 -1.452807 0
M V30 20 H -1.827609 1.481487 -0.996993 0
M V30 21 H -1.921584 2.283368 0.626641 0
M V30 22 H 0.172726 1.108075 1.288878 0
M V30 23 H 0.468947 2.250412 -0.133459 0
M V30 END ATOM
M V30 BEGIN BOND
M V30 1 1 1 2
M V30 2 1 2 3
M V30 3 1 3 4
M V30 4 1 4 5
M V30 5 1 5 6
M V30 6 2 6 7
M V30 7 2 6 8
M V30 8 1 6 9
M V30 9 1 9 10
M V30 10 1 10 3
M V30 11 1 1 11
M V30 12 1 1 12
M V30 13 1 1 13
M V30 14 1 2 14
M V30 15 1 3 15
M V30 16 1 4 16
M V30 17 1 4 17
M V30 18 1 5 18
M V30 19 1 5 19
M V30 20 1 9 20
M V30 21 1 9 21
M V30 22 1 10 22
M V30 23 1 10 23
M V30 END BOND
M V30 END CTAB
M END
)CTAB";
auto m = v2::FileParsers::MolFromMolBlock(ctab);
REQUIRE(m);
auto smiles = MolToSmiles(*m);
CHECK(smiles.find('@') == std::string::npos);
}
SECTION("counterexample from the doc tests") {
// Do not break this one!
// I'm adding it here because it seems we don't have this case
// in any other C++ test.
auto m = R"SMI(C1C[C@H](C)[C@H](C)[C@H](C)C1)SMI"_smiles;
REQUIRE(m);
for (auto i : {2, 4, 6}) {
CHECK(m->getAtomWithIdx(i)->getChiralTag() !=
Atom::ChiralType::CHI_UNSPECIFIED);
}
}
}
#if 1
TEST_CASE(
"Github #8689: stereo canonicalization depends on bond iteration order") {
bool useLegacy = GENERATE(true, false);
CAPTURE(useLegacy);
UseLegacyStereoPerceptionFixture reset_stereo_perception(useLegacy);
std::string pathName = getenv("RDBASE");
pathName += "/Code/GraphMol/test_data/";
SECTION("simplified") {
pathName += "github8689_2.sdf";
v2::FileParsers::SDMolSupplier suppl(pathName);
auto m1 = suppl[0];
REQUIRE(m1);
auto m2 = suppl[1];
REQUIRE(m2);
// m1->debugMol(std::cerr);
// m2->debugMol(std::cerr);
CIPLabeler::assignCIPLabels(*m1);
CIPLabeler::assignCIPLabels(*m2);
REQUIRE(m1->getAtomWithIdx(7)->hasProp(common_properties::_CIPCode));
REQUIRE(m1->getAtomWithIdx(8)->hasProp(common_properties::_CIPCode));
REQUIRE(m2->getAtomWithIdx(7)->hasProp(common_properties::_CIPCode));
REQUIRE(m2->getAtomWithIdx(8)->hasProp(common_properties::_CIPCode));
CHECK(m1->getAtomWithIdx(7)->getProp<std::string>(
common_properties::_CIPCode) ==
m2->getAtomWithIdx(7)->getProp<std::string>(
common_properties::_CIPCode));
CHECK(m1->getAtomWithIdx(8)->getProp<std::string>(
common_properties::_CIPCode) ==
m2->getAtomWithIdx(8)->getProp<std::string>(
common_properties::_CIPCode));
auto smi1 = MolToSmiles(*m1);
auto smi2 = MolToSmiles(*m2);
CHECK(smi1 == smi2);
}
}
#endif
TEST_CASE("extra ring stereo with new stereo perception") {
UseLegacyStereoPerceptionFixture reset_stereo_perception(false);
SECTION("basics") {
std::string smi = "C2O[C@H]3[C@@]4([C@](CCC(C24))(O)CC=C3)C";
auto m = v2::SmilesParse::MolFromSmiles(smi);
REQUIRE(m);
m->debugMol(std::cerr);
// for (const auto atm : m->atoms()) {
// std::cerr << atm->getIdx() << ": "
// << atm->getProp<unsigned int>(
// common_properties::_ChiralAtomRank)
// << std::endl;
// }
for (auto idx : {2, 3, 4}) {
INFO(idx);
const auto atm = m->getAtomWithIdx(idx);
REQUIRE(atm);
CHECK(atm->getChiralTag() != Atom::ChiralType::CHI_UNSPECIFIED);
CHECK(!atm->hasProp(common_properties::_ringStereoAtoms));
}
}
SECTION("don't destroy actual ring stereo") {
std::string smi = "C[C@@H]1CC[C@@H](C)CC1";
auto m = v2::SmilesParse::MolFromSmiles(smi);
REQUIRE(m);
// m->debugMol(std::cerr);
// for (const auto atm : m->atoms()) {
// std::cerr << atm->getIdx() << ": "
// << atm->getProp<unsigned int>(
// common_properties::_ChiralAtomRank)
// << std::endl;
// }
for (auto idx : {1, 4}) {
INFO(idx);
const auto atm = m->getAtomWithIdx(idx);
REQUIRE(atm);
CHECK(atm->getChiralTag() != Atom::ChiralType::CHI_UNSPECIFIED);
CHECK(atm->hasProp(common_properties::_ringStereoAtoms));
}
}
}
TEST_CASE("ring stereo basics with new stereo") {
UseLegacyStereoPerceptionFixture reset_stereo_perception(false);
auto m = "CC(C)[C@H]1CCCCN1C(=O)[C@H]1CC[C@@H](C)CC1 |a:3,11,&1:14|"_smiles;
REQUIRE(m);
auto smi = MolToCXSmiles(*m);
CHECK(smi == "CC(C)[C@H]1CCCCN1C(=O)[C@H]1CC[C@@H](C)CC1 |a:3,11,&1:14|");
}

View File

@@ -245,7 +245,7 @@ void compareRingAtomsConcerningNumNeighbors(Canon::canon_atom *atoms,
namespace detail {
template <typename T>
void rankWithFunctor(T &ftor, bool breakTies, int *order, bool useSpecial,
bool useChirality,
bool useChirality, bool includeRingStereo,
const boost::dynamic_bitset<> *atomsInPlay,
const boost::dynamic_bitset<> *bondsInPlay) {
PRECONDITION(order, "bad pointer");
@@ -297,7 +297,7 @@ void rankWithFunctor(T &ftor, bool breakTies, int *order, bool useSpecial,
ties = true;
}
}
if (useChirality && ties) {
if (useChirality && ties && includeRingStereo) {
SpecialChiralityAtomCompareFunctor scftor(atoms, mol, atomsInPlay,
bondsInPlay);
ActivatePartitions(nAts, order, count.get(), activeset, next.get(),
@@ -760,7 +760,8 @@ void updateAtomNeighborNumSwaps(
void rankMolAtoms(const ROMol &mol, std::vector<unsigned int> &res,
bool breakTies, bool includeChirality, bool includeIsotopes,
bool includeAtomMaps, bool includeChiralPresence,
bool includeStereoGroups, bool useNonStereoRanks) {
bool includeStereoGroups, bool useNonStereoRanks,
bool includeRingStereo) {
if (!mol.getNumAtoms()) {
return;
}
@@ -777,13 +778,14 @@ void rankMolAtoms(const ROMol &mol, std::vector<unsigned int> &res,
AtomCompareFunctor ftor(&atoms.front(), mol);
ftor.df_useIsotopes = includeIsotopes;
ftor.df_useChirality = includeChirality;
ftor.df_useChiralityRings = includeChirality;
ftor.df_useChiralityRings = includeChirality && includeRingStereo;
ftor.df_useAtomMaps = includeAtomMaps;
ftor.df_useNonStereoRanks = useNonStereoRanks;
ftor.df_useChiralPresence = includeChiralPresence;
auto order = std::make_unique<int[]>(mol.getNumAtoms());
detail::rankWithFunctor(ftor, breakTies, order.get(), true, includeChirality);
detail::rankWithFunctor(ftor, breakTies, order.get(), true, includeChirality,
includeRingStereo);
for (unsigned int i = 0; i < mol.getNumAtoms(); ++i) {
res[order[i]] = atoms[order[i]].index;
@@ -801,7 +803,7 @@ void rankFragmentAtoms(const ROMol &mol, std::vector<unsigned int> &res,
const std::vector<std::string> *bondSymbols,
bool breakTies, bool includeChirality,
bool includeIsotopes, bool includeAtomMaps,
bool includeChiralPresence) {
bool includeChiralPresence, bool includeRingStereo) {
PRECONDITION(atomsInPlay.size() == mol.getNumAtoms(), "bad atomsInPlay size");
PRECONDITION(bondsInPlay.size() == mol.getNumBonds(), "bad bondsInPlay size");
PRECONDITION(!atomSymbols || atomSymbols->size() == mol.getNumAtoms(),
@@ -833,7 +835,7 @@ void rankFragmentAtoms(const ROMol &mol, std::vector<unsigned int> &res,
auto order = std::make_unique<int[]>(mol.getNumAtoms());
detail::rankWithFunctor(ftor, breakTies, order.get(), true, includeChirality,
&atomsInPlay, &bondsInPlay);
includeRingStereo, &atomsInPlay, &bondsInPlay);
for (unsigned int i = 0; i < mol.getNumAtoms(); ++i) {
res[order[i]] = atoms[order[i]].index;

View File

@@ -179,12 +179,15 @@ class RDKIT_GRAPHMOL_EXPORT SpecialChiralityAtomCompareFunctor {
if (!dp_atomsInPlay || (*dp_atomsInPlay)[j]) {
updateAtomNeighborNumSwaps(dp_atoms, dp_atoms[j].bonds, j, swapsj);
}
for (unsigned int ii = 0; ii < swapsi.size() && ii < swapsj.size(); ++ii) {
int cmp = swapsi[ii].second - swapsj[ii].second;
if (cmp) {
return cmp;
}
}
return 0;
}
};
@@ -852,7 +855,8 @@ RDKIT_GRAPHMOL_EXPORT void rankMolAtoms(
const ROMol &mol, std::vector<unsigned int> &res, bool breakTies = true,
bool includeChirality = true, bool includeIsotopes = true,
bool includeAtomMaps = true, bool includeChiralPresence = false,
bool includeStereoGroups = true, bool useNonStereoRanks = false);
bool includeStereoGroups = true, bool useNonStereoRanks = false,
bool includeRingStereo = true);
//! Note that atom maps on dummy atoms will always be used
RDKIT_GRAPHMOL_EXPORT void rankFragmentAtoms(
@@ -862,7 +866,7 @@ RDKIT_GRAPHMOL_EXPORT void rankFragmentAtoms(
const std::vector<std::string> *atomSymbols,
const std::vector<std::string> *bondSymbols, bool breakTies,
bool includeChirality, bool includeIsotope, bool includeAtomMaps,
bool includeChiralPresence);
bool includeChiralPresence, bool includeRingStereo = true);
//! Note that atom maps on dummy atoms will always be used
inline void rankFragmentAtoms(
@@ -872,10 +876,10 @@ inline void rankFragmentAtoms(
const std::vector<std::string> *atomSymbols = nullptr,
bool breakTies = true, bool includeChirality = true,
bool includeIsotopes = true, bool includeAtomMaps = true,
bool includeChiralPresence = false) {
bool includeChiralPresence = false, bool includeRingStereo = true) {
rankFragmentAtoms(mol, res, atomsInPlay, bondsInPlay, atomSymbols, nullptr,
breakTies, includeChirality, includeIsotopes,
includeAtomMaps, includeChiralPresence);
includeAtomMaps, includeChiralPresence, includeRingStereo);
};
RDKIT_GRAPHMOL_EXPORT void chiralRankMolAtoms(const ROMol &mol,
@@ -898,6 +902,7 @@ void initFragmentCanonAtoms(const ROMol &mol,
template <typename T>
void rankWithFunctor(T &ftor, bool breakTies, int *order,
bool useSpecial = false, bool useChirality = false,
bool includeRingStereo = true,
const boost::dynamic_bitset<> *atomsInPlay = nullptr,
const boost::dynamic_bitset<> *bondsInPlay = nullptr);

View File

@@ -0,0 +1,96 @@
Original molblock
2D
Structure written by MMmdl.
0 0 0 0 0 999 V3000
M V30 BEGIN CTAB
M V30 COUNTS 17 18 0 0 0
M V30 BEGIN ATOM
M V30 1 C 5.9462 5.2500 0.0000 0
M V30 2 C 8.5442 5.2500 0.0000 0
M V30 3 C 3.3481 5.2500 0.0000 0
M V30 4 C 4.6471 6.0000 0.0000 0
M V30 5 C 5.9462 3.7500 0.0000 0
M V30 6 C 3.3481 3.7500 0.0000 0
M V30 7 C 9.8433 6.0000 0.0000 0
M V30 8 C 8.5442 3.7500 0.0000 0
M V30 9 C 11.1423 5.2500 0.0000 0
M V30 10 C 9.8433 3.0000 0.0000 0
M V30 11 C 11.1423 3.7500 0.0000 0
M V30 12 C 4.6471 3.0000 0.0000 0
M V30 13 C 7.2452 3.0000 0.0000 0
M V30 14 C 2.0490 6.0000 0.0000 0
M V30 15 C 12.4414 6.0000 0.0000 0
M V30 16 O 0.7500 5.2500 0.0000 0
M V30 17 N 2.0490 7.5000 0.0000 0
M V30 END ATOM
M V30 BEGIN BOND
M V30 1 1 1 4
M V30 2 1 1 5
M V30 3 1 2 7
M V30 4 1 2 8
M V30 5 1 3 4
M V30 6 1 3 6
M V30 7 1 3 14
M V30 8 1 5 12
M V30 9 1 5 13
M V30 10 1 6 12
M V30 11 1 7 9
M V30 12 1 8 10
M V30 13 1 8 13 CFG=3
M V30 14 1 9 11
M V30 15 1 9 15 CFG=3
M V30 16 1 10 11
M V30 17 2 14 16
M V30 18 1 14 17
M V30 END BOND
M V30 END CTAB
M END
$$$$
shuffled bonds
2D
Structure written by MMmdl.
0 0 0 0 0 999 V3000
M V30 BEGIN CTAB
M V30 COUNTS 17 18 0 0 0
M V30 BEGIN ATOM
M V30 1 C 5.9462 5.2500 0.0000 0
M V30 2 C 8.5442 5.2500 0.0000 0
M V30 3 C 3.3481 5.2500 0.0000 0
M V30 4 C 4.6471 6.0000 0.0000 0
M V30 5 C 5.9462 3.7500 0.0000 0
M V30 6 C 3.3481 3.7500 0.0000 0
M V30 7 C 9.8433 6.0000 0.0000 0
M V30 8 C 8.5442 3.7500 0.0000 0
M V30 9 C 11.1423 5.2500 0.0000 0
M V30 10 C 9.8433 3.0000 0.0000 0
M V30 11 C 11.1423 3.7500 0.0000 0
M V30 12 C 4.6471 3.0000 0.0000 0
M V30 13 C 7.2452 3.0000 0.0000 0
M V30 14 C 2.0490 6.0000 0.0000 0
M V30 15 C 12.4414 6.0000 0.0000 0
M V30 16 O 0.7500 5.2500 0.0000 0
M V30 17 N 2.0490 7.5000 0.0000 0
M V30 END ATOM
M V30 BEGIN BOND
M V30 14 1 9 11
M V30 2 1 1 5
M V30 5 1 3 4
M V30 15 1 9 15 CFG=3
M V30 18 1 14 17
M V30 11 1 7 9
M V30 8 1 5 12
M V30 16 1 10 11
M V30 9 1 5 13
M V30 4 1 2 8
M V30 13 1 8 13 CFG=3
M V30 6 1 3 6
M V30 17 2 14 16
M V30 12 1 8 10
M V30 1 1 1 4
M V30 7 1 3 14
M V30 10 1 6 12
M V30 3 1 2 7
M V30 END BOND
M V30 END CTAB
M END
$$$$