From a2b149a806352a7caea8f5ca23bb3763bed54646 Mon Sep 17 00:00:00 2001 From: tadhurst-cdd <112502803+tadhurst-cdd@users.noreply.github.com> Date: Tue, 7 May 2024 10:06:33 -0500 Subject: [PATCH] No coords atropisomers - fix smiles output of atrop wedges after reordering (#7418) * removed string_view in favor of string for catch test * add parsing and generation of atropisomers when coords not present * changed string_view to string in catch test * more docs * reformulation of the docs * make an error message a little bit more useful * small optimization clang-format * add `BondWedgingParameters` to new function * changes for CIP test errors * Updated internal doc to match what it does * changes per PR review * removed cout statements in tests --------- Co-authored-by: Greg Landrum --- Code/GraphMol/Atropisomers.cpp | 361 ++++++++++++++++-- Code/GraphMol/CIPLabeler/catch_tests.cpp | 97 +++++ Code/GraphMol/Chirality.h | 8 +- Code/GraphMol/FileParsers/CDXMLParser.cpp | 2 + .../FileParsers/MolFileStereochem.cpp | 2 +- .../FileParsers/file_parsers_catch.cpp | 30 +- Code/GraphMol/MarvinParse/MarvinParser.cpp | 9 +- Code/GraphMol/SmilesParse/CXSmilesOps.cpp | 107 +++++- Code/GraphMol/SmilesParse/SmilesParse.cpp | 9 +- Code/GraphMol/SmilesParse/cxsmiles_test.cpp | 18 +- .../test_data/ShortAtropisomerNoCoords.cxsmi | 1 + ...rtAtropisomerNoCoords.cxsmi.expected.cxsmi | 1 + ...hortAtropisomerNoCoords.cxsmi.expected.mrv | 2 + ...hortAtropisomerNoCoords.cxsmi.expected.sdf | 41 ++ ...tAtropisomerNoCoords.cxsmi.expected2.cxsmi | 1 + ...tAtropisomerNoCoords.cxsmi.expected3.cxsmi | 1 + Code/GraphMol/WedgeBonds.cpp | 22 +- Docs/Book/RDKit_Book.rst | 89 ++++- Docs/Book/images/atrop_representation1.png | Bin 0 -> 15622 bytes Docs/Book/images/atrop_representation2.png | Bin 0 -> 28078 bytes 20 files changed, 700 insertions(+), 101 deletions(-) create mode 100644 Code/GraphMol/SmilesParse/test_data/ShortAtropisomerNoCoords.cxsmi create mode 100644 Code/GraphMol/SmilesParse/test_data/ShortAtropisomerNoCoords.cxsmi.expected.cxsmi create mode 100644 Code/GraphMol/SmilesParse/test_data/ShortAtropisomerNoCoords.cxsmi.expected.mrv create mode 100644 Code/GraphMol/SmilesParse/test_data/ShortAtropisomerNoCoords.cxsmi.expected.sdf create mode 100644 Code/GraphMol/SmilesParse/test_data/ShortAtropisomerNoCoords.cxsmi.expected2.cxsmi create mode 100644 Code/GraphMol/SmilesParse/test_data/ShortAtropisomerNoCoords.cxsmi.expected3.cxsmi create mode 100644 Docs/Book/images/atrop_representation1.png create mode 100644 Docs/Book/images/atrop_representation2.png diff --git a/Code/GraphMol/Atropisomers.cpp b/Code/GraphMol/Atropisomers.cpp index 18b9e1658..ad2c357eb 100644 --- a/Code/GraphMol/Atropisomers.cpp +++ b/Code/GraphMol/Atropisomers.cpp @@ -108,6 +108,46 @@ bool getBondFrameOfReference(const Bond *bond, const Conformer *conf, return true; } +Bond::BondDir getBondDirForAtropisomerNoConf(Bond::BondStereo bondStereo, + unsigned int whichEnd, + unsigned int whichBond) { + // the convention is that in the absence of coords, the coordiates are choosen + // with the lowest numbered atom of the atrop bond down, and the other atom + // straight up. + // On each end, the lowest numbered connecting atom is on the left + // + // a b + // \ / + // c + // | + // d + // / \ aaa + // e f + // + // where c > d + // a < b + // e < f + + PRECONDITION(whichEnd <= 1, "whichEnd must be 0 or 1"); + PRECONDITION(whichBond <= 1, "whichBond must be 0 or 1"); + PRECONDITION(bondStereo == Bond::BondStereo::STEREOATROPCW || + bondStereo == Bond::BondStereo::STEREOATROPCCW, + "bondStereo must be BondAtropisomerCW or BondAtropisomerCCW"); + + int flips = 0; + if (bondStereo == Bond::BondStereo::STEREOATROPCW) { + ++flips; + } + if (whichBond == 1) { + ++flips; + } + if (whichEnd == 1) { + ++flips; + } + + return flips % 2 ? Bond::BEGINDASH : Bond::BEGINWEDGE; +} + Bond::BondDir getBondDirForAtropisomer2d(RDGeom::Point3D bondVecs[2], Bond::BondStereo bondStereo, unsigned int whichEnd, @@ -129,12 +169,13 @@ Bond::BondDir getBondDirForAtropisomer2d(RDGeom::Point3D bondVecs[2], ++flips; } if (bondVecs[1 - whichEnd].y < 0) { - ++flips; // if the OTHER end if negative for the low index bond vec, it + ++flips; // if the OTHER end is negative for the low index bond vec, it // is a flip } return flips % 2 ? Bond::BEGINWEDGE : Bond::BEGINDASH; } + Bond::BondDir getBondDirForAtropisomer3d(Bond *whichBond, const Conformer *conf) { // for 3D we mark it as wedge or hash depending on the z-value of the bond @@ -204,6 +245,44 @@ bool getAtropIsomerEndVect(const AtropAtomAndBondVec &atomAndBondVec, return true; } +std::pair getBondDir( + const Bond *bond, const AtropAtomAndBondVec &atomAndBondVec) { + // get the wedge dir for this end of the bond + // if the first bond 1 has a bondDir, use it + // if the second bond has a bond dir use the opposite of if + // if both bonds have a dir, make sure they are different + + auto bond1Dir = atomAndBondVec.second[0]->getBondDir(); + if (bond1Dir != Bond::BEGINWEDGE && bond1Dir != Bond::BEGINDASH) { + bond1Dir = Bond::NONE; // we dont care if it any thing else + } + auto bond2Dir = atomAndBondVec.second.size() == 2 + ? atomAndBondVec.second[1]->getBondDir() + : Bond::NONE; + if (bond2Dir != Bond::BEGINWEDGE && bond2Dir != Bond::BEGINDASH) { + bond2Dir = Bond::NONE; + } + + // if both are set to a direction, they must NOT be the same - one + // must be a dash and the other a hash + + if (bond1Dir != Bond::NONE && bond2Dir != Bond::NONE && + bond1Dir == bond2Dir) { + BOOST_LOG(rdWarningLog) + << "The bonds on one end of an atropisomer are both UP or both DOWN - atoms are: " + << bond->getBeginAtomIdx() << " " << bond->getEndAtomIdx() << std::endl; + return {false, Bond::BondDir::NONE}; + } + + if (bond1Dir == Bond::BEGINWEDGE || bond2Dir == Bond::BEGINDASH) { + return {true, Bond::BondDir::BEGINWEDGE}; + } + if (bond1Dir == Bond::BEGINDASH || bond2Dir == Bond::BEGINWEDGE) { + return {true, Bond::BondDir::BEGINDASH}; + } + return {true, Bond::BondDir::NONE}; +} + bool DetectAtropisomerChiralityOneBond(Bond *bond, ROMol &mol, const Conformer *conf) { // the approach is this: @@ -217,11 +296,11 @@ bool DetectAtropisomerChiralityOneBond(Bond *bond, ROMol &mol, // atrop bond. for each end of the main bond, we find a vector to reprent // the neighbor atom with the smallest index as its projection onto the // x=0 plane. - // (In 2d, this projection is on the y-AXIS for the end that does NOT have a - // wedge/hash bond, and on the z axis - out of the plane - for the end that - // does have a wedge/hash). The chirality is recorded as the direction we - // rotate from, atom 1's projection to atom2's proejection - either - // clockwise or counter clockwise + // (In 2d, this projection is on the y-AXIS for the end that does NOT have + // a wedge/hash bond, and on the z axis - out of the plane - for the end + // that does have a wedge/hash). The chirality is recorded as the + // direction we rotate from, atom 1's projection to atom2's proejection - + // either clockwise or counter clockwise PRECONDITION(bond, "bad bond"); @@ -241,6 +320,52 @@ bool DetectAtropisomerChiralityOneBond(Bond *bond, ROMol &mol, } } + // the convention is that in the absence of coords, the coordiates are choosen + // with the lowest numbered atom of the atrop bond down, and the other atom + // straight up. + // On each end, the lowest numbered connecting atom is on the left + // + // a b + // \ / + // c + // | + // d + // / \ aaa + // e f + // + // where c > d + // a < b + // e < f + + if (conf == nullptr) { + std::pair bond1DirResult; + bond1DirResult = getBondDir(bond, atomAndBondVecs[0]); + if (!bond1DirResult.first) { + return false; + } + std::pair bond2DirResult; + bond2DirResult = getBondDir(bond, atomAndBondVecs[1]); + if (!bond2DirResult.first) { + return false; + } + if (bond1DirResult.second == bond2DirResult.second) { + BOOST_LOG(rdWarningLog) + << "inconsistent bond wedging for an atropisomer. Atoms are: " + << bond->getBeginAtomIdx() << " " << bond->getEndAtomIdx() + << std::endl; + return false; + } + if (bond1DirResult.second == Bond::BEGINWEDGE || + bond2DirResult.second == Bond::BEGINDASH) { + bond->setStereo(Bond::BondStereo::STEREOATROPCCW); + } else if (bond1DirResult.second == Bond::BEGINDASH || + bond2DirResult.second == Bond::BEGINWEDGE) { + bond->setStereo(Bond::BondStereo::STEREOATROPCW); + } + + return true; + } + // create a frame of reference that has its X-axis along the atrop bond RDGeom::Point3D xAxis, yAxis, zAxis; @@ -265,27 +390,10 @@ bool DetectAtropisomerChiralityOneBond(Bond *bond, ROMol &mol, // if the second bond has a bond dir use the opposite of if // if both bonds have a dir, make sure they are different - auto bond1Dir = atomAndBondVecs[bondAtomIndex].second[0]->getBondDir(); - if (bond1Dir != Bond::BEGINWEDGE && bond1Dir != Bond::BEGINDASH) { - bond1Dir = Bond::NONE; // we dont care if it any thing else - } - auto bond2Dir = - atomAndBondVecs[bondAtomIndex].second.size() == 2 - ? atomAndBondVecs[bondAtomIndex].second[1]->getBondDir() - : Bond::NONE; - if (bond2Dir != Bond::BEGINWEDGE && bond2Dir != Bond::BEGINDASH) { - bond2Dir = Bond::NONE; - } + std::pair bondDirResult; - // if both are set to a direction, they must NOT be the same - one - // must be a dash and the other a hash - - if (bond1Dir != Bond::NONE && bond2Dir != Bond::NONE && - bond1Dir == bond2Dir) { - BOOST_LOG(rdWarningLog) - << "The bonds on one end of an atropisomer are both UP or both DOWN - atoms are: " - << bond->getBeginAtomIdx() << " " << bond->getEndAtomIdx() - << std::endl; + bondDirResult = getBondDir(bond, atomAndBondVecs[bondAtomIndex]); + if (!bondDirResult.first) { return false; } @@ -294,10 +402,10 @@ bool DetectAtropisomerChiralityOneBond(Bond *bond, ROMol &mol, return false; } - if (bond1Dir == Bond::BEGINWEDGE || bond2Dir == Bond::BEGINDASH) { + if (bondDirResult.second == Bond::BEGINWEDGE) { bondVecs[bondAtomIndex].y *= 0.707; bondVecs[bondAtomIndex].z = fabs(bondVecs[bondAtomIndex].y); - } else if (bond1Dir == Bond::BEGINDASH || bond2Dir == Bond::BEGINWEDGE) { + } else if (bondDirResult.second == Bond::BEGINDASH) { bondVecs[bondAtomIndex].y *= 0.707; bondVecs[bondAtomIndex].z = -fabs(bondVecs[bondAtomIndex].y); } @@ -410,8 +518,7 @@ void cleanupAtropisomerStereoGroups(ROMol &mol) { } void detectAtropisomerChirality(ROMol &mol, const Conformer *conf) { - PRECONDITION(conf, "no conformer"); - PRECONDITION(&(conf->getOwningMol()) == &mol, + PRECONDITION(conf == nullptr || &(conf->getOwningMol()) == &mol, "conformer does not belong to molecule"); std::set bondsToTry; @@ -492,6 +599,169 @@ void getAllAtomIdsForStereoGroup( } } +bool WedgeBondFromAtropisomerOneBondNoConf( + Bond *bond, const ROMol &mol, + std::map> + &wedgeBonds) { + PRECONDITION(bond, "no bond"); + + AtropAtomAndBondVec atomAndBondVecs[2]; + if (!getAtropisomerAtomsAndBonds(bond, atomAndBondVecs, mol)) { + return false; // not an atropisomer + } + + // make sure we do not have wiggle bonds + + for (auto atomAndBondVec : atomAndBondVecs) { + for (auto endBond : atomAndBondVec.second) { + if (endBond->getBondDir() == Bond::UNKNOWN) { + return false; // not an atropisomer) + } + } + } + + // first see if any candidate bond is already set to a wedge or hash + // if so, we will use that bond as a wedge or hash + + std::vector useBondsAtEnd[2]; + bool foundBondDir = false; + + for (unsigned int whichEnd = 0; whichEnd < 2; ++whichEnd) { + for (unsigned int whichBond = 0; + whichBond < atomAndBondVecs[whichEnd].second.size(); ++whichBond) { + auto bondDir = atomAndBondVecs[whichEnd].second[whichBond]->getBondDir(); + + // see if it is a wedge or hash and its origin is the atom in the + // main bond + + if ((bondDir == Bond::BEGINWEDGE || bondDir == Bond::BEGINDASH) && + atomAndBondVecs[whichEnd].second[whichBond]->getBeginAtom() == + atomAndBondVecs[whichEnd].first && + canHaveDirection(*bond)) { + useBondsAtEnd[whichEnd].push_back(whichBond); + foundBondDir = true; + } + } + } + + if (foundBondDir) { + for (unsigned int whichEnd = 0; whichEnd < 2; ++whichEnd) { + for (unsigned int whichBondIndex = 0; + whichBondIndex < useBondsAtEnd[whichEnd].size(); ++whichBondIndex) { + atomAndBondVecs[whichEnd] + .second[useBondsAtEnd[whichEnd][whichBondIndex]] + ->setBondDir(getBondDirForAtropisomerNoConf( + bond->getStereo(), whichEnd, + useBondsAtEnd[whichEnd][whichBondIndex])); + } + } + + return true; + } + + // did not find a good bond dir - pick one to use + // we would like to have one that is not in a ring, and will be a wedge + + const RingInfo *ri = bond->getOwningMol().getRingInfo(); + + int bestBondEnd = -1, bestBondNumber = -1; + bool bestBondIsSingle = false; + unsigned int bestRingCount = INT_MAX; + Bond::BondDir bestBondDir = Bond::BondDir::NONE; + for (unsigned int whichEnd = 0; whichEnd < 2; ++whichEnd) { + for (unsigned int whichBond = 0; + whichBond < atomAndBondVecs[whichEnd].second.size(); ++whichBond) { + auto bondToTry = atomAndBondVecs[whichEnd].second[whichBond]; + + if (!canHaveDirection(*bondToTry) || + wedgeBonds.find(bondToTry->getIdx()) != wedgeBonds.end()) { + continue; // must be a single OR aromatic bond and not already + // spoken for by a chiral center + } + + if (bondToTry->getBondDir() != Bond::BondDir::NONE) { + if (bondToTry->getBeginAtom()->getIdx() == + atomAndBondVecs[whichEnd].first->getIdx()) { + BOOST_LOG(rdWarningLog) + << "Wedge or hash bond found on atropisomer where not expected - atoms are: " + << bond->getBeginAtomIdx() << " " << bond->getEndAtomIdx() + << std::endl; + return false; + } else { + continue; // wedge or hash bond affecting the OTHER atom + // = perhaps a chiral center + } + } + auto ringCount = ri->numBondRings(bondToTry->getIdx()); + if (ringCount > bestRingCount) { + continue; + } + + else if (ringCount < bestRingCount) { + bestBondEnd = whichEnd; + bestBondNumber = whichBond; + bestRingCount = ringCount; + bestBondIsSingle = (bondToTry->getBondType() == Bond::BondType::SINGLE); + bestBondDir = getBondDirForAtropisomerNoConf(bond->getStereo(), + whichEnd, whichBond); + } else if (bestBondIsSingle && + bondToTry->getBondType() != Bond::BondType::SINGLE) { + continue; + + } else if (!bestBondIsSingle && + bondToTry->getBondType() == Bond::BondType::SINGLE) { + bestBondEnd = whichEnd; + bestBondNumber = whichBond; + bestRingCount = ringCount; + bestBondIsSingle = true; + bestBondDir = getBondDirForAtropisomerNoConf(bond->getStereo(), + whichEnd, whichBond); + + } else { + auto bondDir = getBondDirForAtropisomerNoConf(bond->getStereo(), + whichEnd, whichBond); + if (bestBondDir == Bond::BondDir::NONE || + (bestBondDir == Bond::BondDir::BEGINDASH && + bondDir == Bond::BondDir::BEGINWEDGE)) { + bestBondEnd = whichEnd; + bestBondNumber = whichBond; + bestRingCount = ringCount; + bestBondIsSingle = + (bondToTry->getBondType() == Bond::BondType::SINGLE); + bestBondDir = bondDir; + } + } + } + } + + if (bestBondEnd >= 0) // we found a good one + { + // make sure the atoms on the bond are in the right order for the + // wedge/hash the atom on the end of the main bond must be listed + // first for the wedge/has bond + + auto bestBond = atomAndBondVecs[bestBondEnd].second[bestBondNumber]; + if (bestBond->getBeginAtom() != atomAndBondVecs[bestBondEnd].first) { + bestBond->setEndAtom(bestBond->getBeginAtom()); + bestBond->setBeginAtom(atomAndBondVecs[bestBondEnd].first); + } + + bestBond->setBondDir(bestBondDir); + + auto newWedgeInfo = std::unique_ptr( + new RDKit::Chirality::WedgeInfoAtropisomer(bond->getIdx(), + bestBondDir)); + wedgeBonds[bestBond->getIdx()] = std::move(newWedgeInfo); + } else { + BOOST_LOG(rdWarningLog) + << "Failed to find a good bond to set as UP or DOWN for an atropisomer - atoms are: " + << bond->getBeginAtomIdx() << " " << bond->getEndAtomIdx() << std::endl; + return false; + } + + return true; +} + bool WedgeBondFromAtropisomerOneBond2d( Bond *bond, const ROMol &mol, const Conformer *conf, std::map> @@ -605,8 +875,8 @@ bool WedgeBondFromAtropisomerOneBond2d( if (!canHaveDirection(*bondToTry) || wedgeBonds.find(bondToTry->getIdx()) != wedgeBonds.end()) { - continue; // must be a single OR aromatic bond and not already spoken - // for by a chiral center + continue; // must be a single OR aromatic bond and not already + // spoken for by a chiral center } if (bondToTry->getBondDir() != Bond::BondDir::NONE) { @@ -680,11 +950,13 @@ bool WedgeBondFromAtropisomerOneBond2d( bestBond->setEndAtom(bestBond->getBeginAtom()); bestBond->setBeginAtom(atomAndBondVecs[bestBondEnd].first); } - // bonds[bestBondEnd][bestBondNumber]->setBondDir(bestBondDir); + bestBond->setBondDir(bestBondDir); + auto newWedgeInfo = std::unique_ptr( new RDKit::Chirality::WedgeInfoAtropisomer(bond->getIdx(), bestBondDir)); wedgeBonds[bestBond->getIdx()] = std::move(newWedgeInfo); + } else { BOOST_LOG(rdWarningLog) << "Failed to find a good bond to set as UP or DOWN for an atropisomer - atoms are: " @@ -738,8 +1010,9 @@ bool WedgeBondFromAtropisomerOneBond3d( } } - // the following may seem redundant, since we just found the useBonds based - // on their bond dir PRESENCE, but this endures that the values are correct. + // the following may seem redundant, since we just found the useBonds + // based on their bond dir PRESENCE, but this endures that the values are + // correct. if (useBonds.size() > 0) { for (auto useBond : useBonds) { @@ -843,10 +1116,11 @@ bool WedgeBondFromAtropisomerOneBond3d( bestBond->setEndAtom(bestBond->getBeginAtom()); bestBond->setBeginAtom(atomAndBondVecs[bestBondEnd].first); } - // bestBond->setBondDir(bestBondDir); + bestBond->setBondDir(bestBondDir); auto newWedgeInfo = std::unique_ptr( new RDKit::Chirality::WedgeInfoAtropisomer(bond->getIdx(), bestBondDir)); + wedgeBonds[bestBond->getIdx()] = std::move(newWedgeInfo); } else { BOOST_LOG(rdWarningLog) @@ -862,8 +1136,7 @@ void wedgeBondsFromAtropisomers( const ROMol &mol, const Conformer *conf, std::map> &wedgeBonds) { - PRECONDITION(conf, "no conformer"); - PRECONDITION(&(conf->getOwningMol()) == &mol, + PRECONDITION(conf == nullptr || &(conf->getOwningMol()) == &mol, "conformer does not belong to molecule"); for (auto bond : mol.bonds()) { auto bondStereo = bond->getStereo(); @@ -878,10 +1151,14 @@ void wedgeBondsFromAtropisomers( continue; } - if (conf->is3D()) { - WedgeBondFromAtropisomerOneBond3d(bond, mol, conf, wedgeBonds); - } else { - WedgeBondFromAtropisomerOneBond2d(bond, mol, conf, wedgeBonds); + if (conf) { + if (conf->is3D()) { + WedgeBondFromAtropisomerOneBond3d(bond, mol, conf, wedgeBonds); + } else { + WedgeBondFromAtropisomerOneBond2d(bond, mol, conf, wedgeBonds); + } + } else { // no conformer + WedgeBondFromAtropisomerOneBondNoConf(bond, mol, wedgeBonds); } } } diff --git a/Code/GraphMol/CIPLabeler/catch_tests.cpp b/Code/GraphMol/CIPLabeler/catch_tests.cpp index ea0526515..eb7c68bbd 100644 --- a/Code/GraphMol/CIPLabeler/catch_tests.cpp +++ b/Code/GraphMol/CIPLabeler/catch_tests.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include "CIPLabeler.h" #include "Digraph.h" @@ -840,3 +841,99 @@ TEST_CASE("upsertTest", "[basic]") { CHECK(smilesOut != ""); } } + +TEST_CASE("atropisomers", "[basic]") { + SECTION("atropisomers1") { + UseLegacyStereoPerceptionFixture useLegacy(false); + + std::string rdbase = getenv("RDBASE"); + + std::vector files = { + "BMS-986142_atrop5.sdf", "BMS-986142_atrop1.sdf", + "BMS-986142_atrop2.sdf", "JDQ443_atrop1.sdf", + "JDQ443_atrop2.sdf", "Mrtx1719_atrop1.sdf", + "Mrtx1719_atrop2.sdf", "RP-6306_atrop1.sdf", + "RP-6306_atrop2.sdf", "Sotorasib_atrop1.sdf", + "Sotorasib_atrop2.sdf", "ZM374979_atrop1.sdf", + "ZM374979_atrop2.sdf"}; + + for (auto file : files) { + auto fName = + rdbase + "/Code/GraphMol/FileParsers/test_data/atropisomers/" + file; + + auto molsdf = MolFileToMol(fName, true, true, true); + REQUIRE(molsdf); + CIPLabeler::assignCIPLabels(*molsdf, 100000); + + std::map, std::string> CIPVals; + for (auto bond : molsdf->bonds()) { + auto a1 = bond->getBeginAtomIdx(); + auto a2 = bond->getEndAtomIdx(); + if (a1 > a2) { + std::swap(a1, a2); + } + + if (bond->hasProp(common_properties::_CIPCode)) { + CIPVals[std::make_pair(a1, a2)] = + bond->getProp(common_properties::_CIPCode); + } else { + CIPVals[std::make_pair(a1, a2)] = "N/A"; + } + } + + molsdf->clearConformers(); + + SmilesWriteParams wp; + wp.canonical = false; + wp.doKekule = false; + unsigned int flags = + RDKit::SmilesWrite::CXSmilesFields::CX_MOLFILE_VALUES | + RDKit::SmilesWrite::CXSmilesFields::CX_ATOM_PROPS | + RDKit::SmilesWrite::CXSmilesFields::CX_BOND_CFG | + RDKit::SmilesWrite::CXSmilesFields::CX_BOND_ATROPISOMER; + SmilesParserParams pp; + pp.allowCXSMILES = true; + pp.sanitize = true; + + auto smi = MolToCXSmiles(*molsdf, wp, flags); + auto newMol = SmilesToMol(smi, pp); + CIPLabeler::assignCIPLabels(*newMol, 100000); + + std::map, std::string> newCIPVals; + for (auto bond : newMol->bonds()) { + auto a1 = bond->getBeginAtomIdx(); + auto a2 = bond->getEndAtomIdx(); + if (a1 > a2) { + std::swap(a1, a2); + } + if (bond->hasProp(common_properties::_CIPCode)) { + newCIPVals[std::make_pair(a1, a2)] = + bond->getProp(common_properties::_CIPCode); + } else { + newCIPVals[std::make_pair(a1, a2)] = "N/A"; + } + } + + RDKit::SubstructMatchParameters params; + + auto match = RDKit::SubstructMatch(*molsdf, *newMol, params); + + for (auto thisBond : newMol->bonds()) { + unsigned int a1 = thisBond->getBeginAtomIdx(); + unsigned int a2 = thisBond->getEndAtomIdx(); + if (a1 > a2) { + std::swap(a1, a2); + } + + unsigned int a1match = match[0][a1].second; + unsigned int a2match = match[0][a2].second; + + if (a1match > a2match) { + std::swap(a1match, a2match); + } + CHECK(CIPVals[std::make_pair(a1match, a2match)] == + newCIPVals[std::make_pair(a1, a2)]); + } + } + } +} \ No newline at end of file diff --git a/Code/GraphMol/Chirality.h b/Code/GraphMol/Chirality.h index 52a732e64..b1b4fbbda 100644 --- a/Code/GraphMol/Chirality.h +++ b/Code/GraphMol/Chirality.h @@ -301,8 +301,12 @@ RDKIT_GRAPHMOL_EXPORT void setStereoForBond(ROMol &mol, Bond *bond, /// returns a map from bond idx -> controlling atom idx RDKIT_GRAPHMOL_EXPORT std::map> pickBondsToWedge( - const ROMol &mol, const BondWedgingParameters *params = nullptr, - const Conformer *conf = nullptr); + const ROMol &mol, const BondWedgingParameters *params = nullptr); + +RDKIT_GRAPHMOL_EXPORT +std::map> pickBondsToWedge( + const ROMol &mol, const BondWedgingParameters *params, + const Conformer *conf); RDKIT_GRAPHMOL_EXPORT void wedgeMolBonds( ROMol &mol, const Conformer *conf = nullptr, diff --git a/Code/GraphMol/FileParsers/CDXMLParser.cpp b/Code/GraphMol/FileParsers/CDXMLParser.cpp index 5c8579d4c..74903a9cc 100644 --- a/Code/GraphMol/FileParsers/CDXMLParser.cpp +++ b/Code/GraphMol/FileParsers/CDXMLParser.cpp @@ -613,6 +613,8 @@ std::vector> MolsFromCDXMLDataStream( Atropisomers::detectAtropisomerChirality( *res, &res->getConformer(confidx)); + } else { // no Conformer + Atropisomers::detectAtropisomerChirality(*res, nullptr); } // now that atom stereochem has been perceived, the wedging diff --git a/Code/GraphMol/FileParsers/MolFileStereochem.cpp b/Code/GraphMol/FileParsers/MolFileStereochem.cpp index f667e3059..c22a2371c 100644 --- a/Code/GraphMol/FileParsers/MolFileStereochem.cpp +++ b/Code/GraphMol/FileParsers/MolFileStereochem.cpp @@ -228,8 +228,8 @@ void invertMolBlockWedgingInfo(ROMol &mol) { } void markUnspecifiedStereoAsUnknown(ROMol &mol, int confId) { - auto wedgeBonds = RDKit::Chirality::pickBondsToWedge(mol); const auto conf = mol.getConformer(confId); + auto wedgeBonds = RDKit::Chirality::pickBondsToWedge(mol, nullptr, &conf); for (auto b : mol.bonds()) { if (b->getBondType() == Bond::DOUBLE) { int dirCode; diff --git a/Code/GraphMol/FileParsers/file_parsers_catch.cpp b/Code/GraphMol/FileParsers/file_parsers_catch.cpp index ce69361ff..0f106842a 100644 --- a/Code/GraphMol/FileParsers/file_parsers_catch.cpp +++ b/Code/GraphMol/FileParsers/file_parsers_catch.cpp @@ -11,7 +11,7 @@ #include #include #include -#include +// #include #include #include "RDGeneral/test.h" @@ -1618,7 +1618,6 @@ H 0.635000 0.635000 0.635000 mol->addConformer(conf); const std::string xyzblock = MolToXYZBlock(*mol, 0, 15); - std::cout << xyzblock << std::endl; std::string xyzblock_expected = R"XYZ(7 CHEMBL506259 O 0.402012650000000 -0.132994360000000 1.000000170000000 @@ -5074,7 +5073,8 @@ M END Chirality::reapplyMolBlockWedging(*m); CHECK(m->getBondWithIdx(2)->getBondDir() == Bond::BondDir::NONE); } - SECTION("Reapply the original wedging, regardless the bond type of wedged bonds") { + SECTION( + "Reapply the original wedging, regardless the bond type of wedged bonds") { auto m = R"CTAB( Mrv2311 04232413302D @@ -6041,10 +6041,10 @@ TEST_CASE("MaeWriter basic testing", "[mae][MaeWriter][writer]") { auto bondBlockStart = mae.find("m_bond[6]"); REQUIRE(bondBlockStart != std::string::npos); - std::string_view ctBlock(&mae[ctBlockStart], atomBlockStart - ctBlockStart); - std::string_view atomBlock(&mae[atomBlockStart], - bondBlockStart - atomBlockStart); - std::string_view bondBlock(&mae[bondBlockStart]); + std::string ctBlock(&mae[ctBlockStart], atomBlockStart - ctBlockStart); + std::string atomBlock(&mae[atomBlockStart], + bondBlockStart - atomBlockStart); + std::string bondBlock(&mae[bondBlockStart]); // Check mol properties CHECK(ctBlock.find("s_m_title") != std::string::npos); @@ -6173,10 +6173,10 @@ TEST_CASE("MaeWriter basic testing", "[mae][MaeWriter][writer]") { auto bondBlockStart = mae.find("m_bond[6]"); REQUIRE(bondBlockStart != std::string::npos); - std::string_view ctBlock(&mae[ctBlockStart], atomBlockStart - ctBlockStart); - std::string_view atomBlock(&mae[atomBlockStart], - bondBlockStart - atomBlockStart); - std::string_view bondBlock(&mae[bondBlockStart]); + std::string ctBlock(&mae[ctBlockStart], atomBlockStart - ctBlockStart); + std::string atomBlock(&mae[atomBlockStart], + bondBlockStart - atomBlockStart); + std::string bondBlock(&mae[bondBlockStart]); // Check mol properties CHECK(ctBlock.find("s_m_title") != std::string::npos); @@ -6258,7 +6258,7 @@ TEST_CASE("MaeWriter basic testing", "[mae][MaeWriter][writer]") { // Maeparser does not parse bond properties, so don't check them. } SECTION("Check reverse roundtrip") { - constexpr std::string_view maeBlock = R"MAE(f_m_ct { + std::string maeBlock = R"MAE(f_m_ct { s_m_title ::: "" @@ -6348,7 +6348,7 @@ TEST_CASE("MaeWriter basic testing", "[mae][MaeWriter][writer]") { auto ctBlockStart = mae.find("f_m_ct"); REQUIRE(ctBlockStart != std::string::npos); - std::string_view ctBlock(&mae[ctBlockStart]); + std::string ctBlock(&mae[ctBlockStart]); CHECK(ctBlock == MaeWriter::getText(*mol)); } @@ -6616,8 +6616,8 @@ TEST_CASE( auto bondBlockStart = mae.find("m_bond[2]"); REQUIRE(bondBlockStart != std::string::npos); - const std::string_view atomBlock(&mae[atomBlockStart], - bondBlockStart - atomBlockStart); + const std::string atomBlock(&mae[atomBlockStart], + bondBlockStart - atomBlockStart); CHECK(atomBlock.find(" -2 ") != std::string::npos); } diff --git a/Code/GraphMol/MarvinParse/MarvinParser.cpp b/Code/GraphMol/MarvinParse/MarvinParser.cpp index f502091b8..e53bc620b 100644 --- a/Code/GraphMol/MarvinParse/MarvinParser.cpp +++ b/Code/GraphMol/MarvinParse/MarvinParser.cpp @@ -611,9 +611,12 @@ class MarvinCMLReader { MolOps::assignChiralTypesFrom3D(*mol, conf3d->getId(), true); } - if (conf || conf3d) { - Atropisomers::detectAtropisomerChirality( - *mol, conf != nullptr ? conf : conf3d); + if (conf) { + Atropisomers::detectAtropisomerChirality(*mol, conf); + } else if (conf3d) { + Atropisomers::detectAtropisomerChirality(*mol, conf3d); + } else { + Atropisomers::detectAtropisomerChirality(*mol, nullptr); } ClearSingleBondDirFlags(*mol); diff --git a/Code/GraphMol/SmilesParse/CXSmilesOps.cpp b/Code/GraphMol/SmilesParse/CXSmilesOps.cpp index a1279f752..a1292ca66 100644 --- a/Code/GraphMol/SmilesParse/CXSmilesOps.cpp +++ b/Code/GraphMol/SmilesParse/CXSmilesOps.cpp @@ -1097,7 +1097,9 @@ bool parse_wedged_bonds(Iterator &first, Iterator last, RDKit::RWMol &mol, if (atom->getIdx() != bond->getEndAtomIdx()) { BOOST_LOG(rdWarningLog) << "atom " << atomIdx << " is not associated with bond " - << bondIdx << " in w block" << std::endl; + << bondIdx << "(" << bond->getBeginAtomIdx() + startAtomIdx << "-" + << bond->getEndAtomIdx() + startAtomIdx << ")" + << " in w block" << std::endl; return false; } auto eidx = bond->getBeginAtomIdx(); @@ -2038,23 +2040,96 @@ std::string get_bond_config_block( bd = Bond::BondDir::NONE; } - if (atropisomerOnly) { - // on of the bonds on the beging atom of this bond must be an atropisomer + if (atropisomerOnly && bd == Bond::BondDir::NONE) { + continue; + } - if (bd == Bond::BondDir::NONE) { - continue; - } - bool foundAtropisomer = false; + // see if this one is an atropisomer - const Atom *firstAtom = bond->getBeginAtom(); + bool isAnAtropisomer = false; + + const Atom *firstAtom = bond->getBeginAtom(); + if (bd == Bond::BondDir::BEGINDASH || bd == Bond::BondDir::BEGINWEDGE) { for (auto bondNbr : mol.atomBonds(firstAtom)) { + if (bondNbr->getIdx() == bond->getIdx()) { + continue; // a bond is not its own neighbor + } if (bondNbr->getStereo() == Bond::BondStereo::STEREOATROPCW || bondNbr->getStereo() == Bond::BondStereo::STEREOATROPCCW) { - foundAtropisomer = true; + isAnAtropisomer = true; + + // if it is for an atropisomer and there are no coords, check to see + // if the wedge needs to be flipped based on the smiles reordering + if (!coordsIncluded && isAnAtropisomer) { + Atropisomers::AtropAtomAndBondVec atomAndBondVecs[2]; + if (!Atropisomers::getAtropisomerAtomsAndBonds( + bondNbr, atomAndBondVecs, mol)) { + throw ValueErrorException("Internal error - should not occur"); + // should not happend + } else { + unsigned int swaps = 0; + + unsigned int firstReorderedIdx = + std::find(atomOrder.begin(), atomOrder.end(), + bondNbr->getBeginAtom()->getIdx()) - + atomOrder.begin(); + unsigned int secondReorderedIdx = + std::find(atomOrder.begin(), atomOrder.end(), + bondNbr->getEndAtom()->getIdx()) - + atomOrder.begin(); + if (firstReorderedIdx > secondReorderedIdx) { + ++swaps; + } + + for (unsigned int bondAtomIndex = 0; bondAtomIndex < 2; + ++bondAtomIndex) { + if (atomAndBondVecs[bondAtomIndex].first == firstAtom) + continue; // swapped atoms on the side where the wedge bond + // is does NOT change the wedge bond + if (atomAndBondVecs[bondAtomIndex].second.size() == 2) { + unsigned int firstOtherAtomIdx = + atomAndBondVecs[bondAtomIndex] + .second[0] + ->getOtherAtom(atomAndBondVecs[bondAtomIndex].first) + ->getIdx(); + unsigned int secondOtherAtomIdx = + atomAndBondVecs[bondAtomIndex] + .second[1] + ->getOtherAtom(atomAndBondVecs[bondAtomIndex].first) + ->getIdx(); + + unsigned int firstReorderedAtomIdx = + std::find(atomOrder.begin(), atomOrder.end(), + firstOtherAtomIdx) - + atomOrder.begin(); + unsigned int secondReorderedAtomIdx = + std::find(atomOrder.begin(), atomOrder.end(), + secondOtherAtomIdx) - + atomOrder.begin(); + + if (firstReorderedAtomIdx > secondReorderedAtomIdx) { + ++swaps; + } + } + } + if (swaps % 2) { + bd = (bd == Bond::BondDir::BEGINWEDGE) + ? Bond::BondDir::BEGINDASH + : Bond::BondDir::BEGINWEDGE; + } + } + } + break; } } - if (!foundAtropisomer) { + } + + if (atropisomerOnly) { + // one of the bonds on the beginning atom of this bond must be an + // atropisomer + + if (!isAnAtropisomer) { continue; } } else { // atropisomeronly is FALSE - check for a wedging caused by @@ -2109,8 +2184,9 @@ std::string get_bond_config_block( std::string wType = ""; if (bd == Bond::BondDir::UNKNOWN) { wType = "w"; - } else if (coordsIncluded) { + } else if (coordsIncluded || isAnAtropisomer) { // we only do wedgeUp and wedgeDown if coordinates are being output + // or its an atropisomer if (bd == Bond::BondDir::BEGINWEDGE) { wType = "wU"; } else if (bd == Bond::BondDir::BEGINDASH) { @@ -2354,12 +2430,13 @@ std::string getCXExtensions(const ROMol &mol, std::uint32_t flags) { // do the CX_BOND_ATROPISOMER only if CX_BOND_CFG s not done. CX_BOND_CFG // includes the atropisomer wedging else if (flags & SmilesWrite::CXSmilesFields::CX_BOND_ATROPISOMER) { - if (conf) { - Atropisomers::wedgeBondsFromAtropisomers(mol, conf, wedgeBonds); - } - bool includeCoords = flags & SmilesWrite::CXSmilesFields::CX_COORDS && mol.getNumConformers(); + if (includeCoords) { + Atropisomers::wedgeBondsFromAtropisomers(mol, conf, wedgeBonds); + } else { + Atropisomers::wedgeBondsFromAtropisomers(mol, nullptr, wedgeBonds); + } const auto cfgblock = get_bond_config_block( mol, atomOrder, bondOrder, includeCoords, wedgeBonds, true); appendToCXExtension(cfgblock, res); diff --git a/Code/GraphMol/SmilesParse/SmilesParse.cpp b/Code/GraphMol/SmilesParse/SmilesParse.cpp index 5bb1bfff0..6415ca8d8 100644 --- a/Code/GraphMol/SmilesParse/SmilesParse.cpp +++ b/Code/GraphMol/SmilesParse/SmilesParse.cpp @@ -458,8 +458,13 @@ std::unique_ptr MolFromSmiles(const std::string &smiles, res->updatePropertyCache(false); MolOps::assignChiralTypesFrom3D(*res, conf3d->getId(), true); } - if (conf || conf3d) { - Atropisomers::detectAtropisomerChirality(*res, conf ? conf : conf3d); + + if (conf) { + Atropisomers::detectAtropisomerChirality(*res, conf); + } else if (conf3d) { + Atropisomers::detectAtropisomerChirality(*res, conf3d); + } else { + Atropisomers::detectAtropisomerChirality(*res, nullptr); } if (res && (params.sanitize || params.removeHs)) { diff --git a/Code/GraphMol/SmilesParse/cxsmiles_test.cpp b/Code/GraphMol/SmilesParse/cxsmiles_test.cpp index 4a4d3bad2..061e759ce 100644 --- a/Code/GraphMol/SmilesParse/cxsmiles_test.cpp +++ b/Code/GraphMol/SmilesParse/cxsmiles_test.cpp @@ -790,7 +790,8 @@ void testOneAtropisomers(const SmilesTest *smilesTest) { MolToCXSmiles(*smilesMol, ps, flags, RestoreBondDirOptionClear); generateNewExpectedFilesIfSoSpecified(fName + ".NEW3.cxsmi", smilesOut); - CHECK(getExpectedValue(expectedFileName) == smilesOut); + auto expected = getExpectedValue(expectedFileName); + CHECK(expected == smilesOut); } smilesMol = @@ -826,7 +827,7 @@ void testOneAtropisomers(const SmilesTest *smilesTest) { } catch (const RDKit::KekulizeException &e) { outMolStr = ""; } catch (...) { - throw; // re-trhow the error if not a kekule error + throw; // re-throw the error if not a kekule error } if (outMolStr == "") { RDKit::Chirality::reapplyMolBlockWedging(*smilesMol); @@ -1023,6 +1024,7 @@ void testOne3dChiral(const SmilesTest *smilesTest) { TEST_CASE("testAtropisomersInCXSmiles") { { std::list smiTests{ + SmilesTest("ShortAtropisomerNoCoords.cxsmi", true, 14, 15), SmilesTest("ShortAtropisomer.cxsmi", true, 14, 15), SmilesTest("ShortAtropisomerArom.cxsmi", true, 14, 15), SmilesTest("AtropManyChirals.cxsmi", true, 20, 20), @@ -1344,15 +1346,11 @@ TEST_CASE("SMILES CANONICALIZATION") { } molCount++; - try { - if (molBlock.length() > 25) { - std::unique_ptr mol(MolBlockToMol(molBlock)); - std::string smiles = MolToCXSmiles(*mol); + if (molBlock.length() > 25) { + std::unique_ptr mol(MolBlockToMol(molBlock)); + std::string smiles = MolToCXSmiles(*mol); - testSmilesCanonicalization(smiles); - } - } catch (...) { - std::cout << "failed on mol " << molCount << std::endl; + testSmilesCanonicalization(smiles); } } } diff --git a/Code/GraphMol/SmilesParse/test_data/ShortAtropisomerNoCoords.cxsmi b/Code/GraphMol/SmilesParse/test_data/ShortAtropisomerNoCoords.cxsmi new file mode 100644 index 000000000..692935f52 --- /dev/null +++ b/Code/GraphMol/SmilesParse/test_data/ShortAtropisomerNoCoords.cxsmi @@ -0,0 +1 @@ +CC1=C(N2C=CC=C2C)C(C)CCC1 |wU:2.9| diff --git a/Code/GraphMol/SmilesParse/test_data/ShortAtropisomerNoCoords.cxsmi.expected.cxsmi b/Code/GraphMol/SmilesParse/test_data/ShortAtropisomerNoCoords.cxsmi.expected.cxsmi new file mode 100644 index 000000000..51e75f74b --- /dev/null +++ b/Code/GraphMol/SmilesParse/test_data/ShortAtropisomerNoCoords.cxsmi.expected.cxsmi @@ -0,0 +1 @@ +CC1=C(n2cccc2C)C(C)CCC1 |wU:2.9| \ No newline at end of file diff --git a/Code/GraphMol/SmilesParse/test_data/ShortAtropisomerNoCoords.cxsmi.expected.mrv b/Code/GraphMol/SmilesParse/test_data/ShortAtropisomerNoCoords.cxsmi.expected.mrv new file mode 100644 index 000000000..a73ca06fe --- /dev/null +++ b/Code/GraphMol/SmilesParse/test_data/ShortAtropisomerNoCoords.cxsmi.expected.mrv @@ -0,0 +1,2 @@ + +W \ No newline at end of file diff --git a/Code/GraphMol/SmilesParse/test_data/ShortAtropisomerNoCoords.cxsmi.expected.sdf b/Code/GraphMol/SmilesParse/test_data/ShortAtropisomerNoCoords.cxsmi.expected.sdf new file mode 100644 index 000000000..6b6b03c12 --- /dev/null +++ b/Code/GraphMol/SmilesParse/test_data/ShortAtropisomerNoCoords.cxsmi.expected.sdf @@ -0,0 +1,41 @@ + + RDKit 2D + + 0 0 0 0 0 0 0 0 0 0999 V3000 +M V30 BEGIN CTAB +M V30 COUNTS 14 15 0 0 0 +M V30 BEGIN ATOM +M V30 1 C 3.000000 0.000000 0.000000 0 +M V30 2 C 1.500000 0.000000 0.000000 0 +M V30 3 C 0.750000 -1.299038 0.000000 0 +M V30 4 N 1.500000 -2.598076 0.000000 0 +M V30 5 C 2.991783 -2.754869 0.000000 0 +M V30 6 C 3.303650 -4.222090 0.000000 0 +M V30 7 C 2.004612 -4.972090 0.000000 0 +M V30 8 C 0.889895 -3.968394 0.000000 0 +M V30 9 C -0.577326 -4.280262 0.000000 0 +M V30 10 C -0.750000 -1.299038 0.000000 0 +M V30 11 C -1.500000 -2.598076 0.000000 0 +M V30 12 C -1.500000 0.000000 0.000000 0 +M V30 13 C -0.750000 1.299038 0.000000 0 +M V30 14 C 0.750000 1.299038 0.000000 0 +M V30 END ATOM +M V30 BEGIN BOND +M V30 1 1 1 2 +M V30 2 2 2 3 +M V30 3 1 3 4 +M V30 4 1 4 5 +M V30 5 2 5 6 +M V30 6 1 6 7 +M V30 7 2 7 8 +M V30 8 1 8 9 +M V30 9 1 3 10 CFG=1 +M V30 10 1 10 11 +M V30 11 1 10 12 +M V30 12 1 12 13 +M V30 13 1 13 14 +M V30 14 1 14 2 +M V30 15 1 8 4 +M V30 END BOND +M V30 END CTAB +M END diff --git a/Code/GraphMol/SmilesParse/test_data/ShortAtropisomerNoCoords.cxsmi.expected2.cxsmi b/Code/GraphMol/SmilesParse/test_data/ShortAtropisomerNoCoords.cxsmi.expected2.cxsmi new file mode 100644 index 000000000..51e75f74b --- /dev/null +++ b/Code/GraphMol/SmilesParse/test_data/ShortAtropisomerNoCoords.cxsmi.expected2.cxsmi @@ -0,0 +1 @@ +CC1=C(n2cccc2C)C(C)CCC1 |wU:2.9| \ No newline at end of file diff --git a/Code/GraphMol/SmilesParse/test_data/ShortAtropisomerNoCoords.cxsmi.expected3.cxsmi b/Code/GraphMol/SmilesParse/test_data/ShortAtropisomerNoCoords.cxsmi.expected3.cxsmi new file mode 100644 index 000000000..51e75f74b --- /dev/null +++ b/Code/GraphMol/SmilesParse/test_data/ShortAtropisomerNoCoords.cxsmi.expected3.cxsmi @@ -0,0 +1 @@ +CC1=C(n2cccc2C)C(C)CCC1 |wU:2.9| \ No newline at end of file diff --git a/Code/GraphMol/WedgeBonds.cpp b/Code/GraphMol/WedgeBonds.cpp index 49be35361..6b8f96b45 100644 --- a/Code/GraphMol/WedgeBonds.cpp +++ b/Code/GraphMol/WedgeBonds.cpp @@ -341,6 +341,16 @@ int pickBondToWedge( // returns map of bondIdx -> bond begin atom for those bonds that // need wedging. + +std::map> pickBondsToWedge( + const ROMol &mol, const BondWedgingParameters *params) { + const Conformer *conf = nullptr; + if (mol.getNumConformers()) { + conf = &mol.getConformer(); + } + return pickBondsToWedge(mol, params, conf); +} + std::map> pickBondsToWedge( const ROMol &mol, const BondWedgingParameters *params, const Conformer *conf) { @@ -388,15 +398,7 @@ std::map> pickBondsToWedge( wedgeInfo[bnd1] = std::move(wi); } } - - if (conf == nullptr) { - if (mol.getNumConformers()) { - conf = &mol.getConformer(); - } - } - if (conf) { - RDKit::Atropisomers::wedgeBondsFromAtropisomers(mol, conf, wedgeInfo); - } + RDKit::Atropisomers::wedgeBondsFromAtropisomers(mol, conf, wedgeInfo); return wedgeInfo; } @@ -472,7 +474,7 @@ void wedgeMolBonds(ROMol &mol, const Conformer *conf, MolOps::findSSSR(mol); } - auto wedgeBonds = Chirality::pickBondsToWedge(mol, params); + auto wedgeBonds = Chirality::pickBondsToWedge(mol, params, conf); // loop over the bonds we need to wedge: for (const auto &[wbi, wedgeInfo] : wedgeBonds) { diff --git a/Docs/Book/RDKit_Book.rst b/Docs/Book/RDKit_Book.rst index ade970ee0..e98da4f09 100644 --- a/Docs/Book/RDKit_Book.rst +++ b/Docs/Book/RDKit_Book.rst @@ -2234,6 +2234,46 @@ Note: the RDKit software makes no attempt to determine if the bond is actually rotationally constrained. If the bond meets the requirements above, it is marked as an atropisomer. +Internal Representation of Atropisomers +======================================= + +To help with the rest of the explanation, we include the bond indices in the molecule drawing: + +.. testsetup:: + + 'CC1=CC=CC(I)=C1N1C(C)=CC=C1Br |wU:7.7,(18.31,-9.57,;17.45,-9.08,;17.44,-8.08,;16.58,-7.58,;15.71,-8.08,;15.71,-9.08,;14.84,-9.58,;16.58,-9.58,;16.58,-10.58,;17.38,-11.17,;18.34,-10.87,;17.08,-12.12,;16.07,-12.12,;15.77,-11.17,;14.81,-10.87,)|' + +.. image:: images/atrop_representation1.png + +Here's part of the Debug output for that molecule:: + + Atoms: + ... + 1 6 C chg: 0 deg: 3 exp: 4 imp: 0 hyb: SP2 arom?: 1 + ... + 5 6 C chg: 0 deg: 3 exp: 4 imp: 0 hyb: SP2 arom?: 1 + ... + 7 6 C chg: 0 deg: 3 exp: 4 imp: 0 hyb: SP2 arom?: 1 + 8 7 N chg: 0 deg: 3 exp: 3 imp: 0 hyb: SP2 arom?: 1 + 9 6 C chg: 0 deg: 3 exp: 4 imp: 0 hyb: SP2 arom?: 1 + ... + 13 6 C chg: 0 deg: 3 exp: 4 imp: 0 hyb: SP2 arom?: 1 + ... + Bonds: + ... + 6 5->7 order: a conj?: 1 aromatic?: 1 + 7 7->8 order: 1 stereo: CCW bonds: (14 6 8 15) conj?: 1 + 8 8->9 order: a conj?: 1 aromatic?: 1 + ... + 14 7->1 order: a dir: wedge conj?: 1 aromatic?: 1 + 15 13->8 order: a conj?: 1 aromatic?: 1 + +This tells us that bond 7 is atropisomeric and that when looking down the bond +(from atom C7 to atom N8), the rotation direction between bonds 14 and 8 (these +are the bonds to the lowest numbered atoms on each of the atropisomeric bond) is +counterclockwise (CCW). + + Formats supporting atropisomers =============================== @@ -2280,6 +2320,54 @@ an atropisomer bond, the actual stereochemistry of the bond is determined by the .. image:: images/atrop_example5.png +Interpreting atropisomers without coordinates +============================================= + +Just as it is possible to interpret atomic and double bond stereochemistry from +SMILES without providing atomic coordinates, the RDKit adopts (starting with the +2024.03.2 release) a convention for interpreting bond wedge information in +CXSMILES that do not have coordinates provided. + +In order to understand how this representation works, a quick digression is necessary to explain the difference in the way bonds are numbered in CXSMILES and the RDKit. +In the RDKit ring closure bonds are added last; they are the highest numbered bonds in the molecule. In CXSMILES, the ring closure bonds are added immediately upon closing the ring. +Here's an illustration of this for the SMILES ``CC1=CC=CC(I)=C1N1C(C)=CC=C1Br``; the RDKit bond numbering (what we saw above) is shown first and the CXSMILES numbering is shown second. + +.. testsetup:: + + 'CC1=CC=CC(I)=C1N1C(C)=CC=C1Br |wU:7.7|' + +.. image:: images/atrop_representation2.png + +To describe this atropisomer in CXSMILES without using coordinates, we adopt the convention that the atropisomeric bond's stereochemistry is ``CCW`` when the bond to the lowest-numbered neighbor of the start atom +(in this case the neighbor connected via bond 7) is wedged and write the CXSMILES for this molecule as ``CC1=CC=CC(I)=C1N1C(C)=CC=C1Br |wU:7.7||``. The stereo values for all possible combinations are shown here: + + ++-------+----+-------+-------+--------+------------+ +| | | bond | | | | +| from | to | index | type | stereo | | ++=======+====+=======+=======+========+============+ +| 7 | 1 | 7 | wedge | CCW | ``wU7.7`` | ++-------+----+-------+-------+--------+------------+ +| 7 | 5 | 6 | wedge | CW | ``wU7.6`` | ++-------+----+-------+-------+--------+------------+ +| 8 | 9 | 9 | wedge | CW | ``wU8.9`` | ++-------+----+-------+-------+--------+------------+ +| 8 | 13 | 14 | wedge | CCW | ``wU8.14`` | ++-------+----+-------+-------+--------+------------+ +| 7 | 1 | 7 | dash | CW | ``wD7.7`` | ++-------+----+-------+-------+--------+------------+ +| 7 | 5 | 6 | dash | CCW | ``wD7.6`` | ++-------+----+-------+-------+--------+------------+ +| 8 | 9 | 9 | dash | CCW | ``wD8.9`` | ++-------+----+-------+-------+--------+------------+ +| 8 | 13 | 14 | dash | CW | ``wD8.14`` | ++-------+----+-------+-------+--------+------------+ + + +It's also possible to wedge multiple neighbor bonds around an atropisomeric bond, but the wedging must be consistent based on the table above; +so the CXSMILES ``CC1=CC=CC(I)=C1N1C(C)=CC=C1Br |wD:7.7,wU:8.9|`` is valid, but ``CC1=CC=CC(I)=C1N1C(C)=CC=C1Br |wD:7.7,wU:8.14|`` is not. + + Validation of Atropisomers ========================== @@ -2294,7 +2382,6 @@ Atropisomers are supported in the canonicalization algorithm of RDKit. They are substructure searching and similarity searching at this time. - Query Features in Molecule Drawings *********************************** diff --git a/Docs/Book/images/atrop_representation1.png b/Docs/Book/images/atrop_representation1.png new file mode 100644 index 0000000000000000000000000000000000000000..28214554a1de7808ecd39283fcd04059d9a32b6f GIT binary patch literal 15622 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

{e6rW^GYhgN*oaQ#>!JblYXuCP2DUiOM?Zz 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