From 1fe07a5eb7a2cd71aeea98bb443252a37020ec8d Mon Sep 17 00:00:00 2001 From: paconius Date: Thu, 24 Jul 2025 10:46:15 -0400 Subject: [PATCH] Add option to draw all CIP codes in DrawMol.cpp (#8609) * enable showAllCIPCodes in DrawMol code * Made consistent with old behavior + add test * Added python test * Fixed FREETYPE=OFF tests * Fixed FREETYPE=OFF tests * Adjusted pytest * Enabled JSON drawer option setting + new JSON test * update JS docs --- Code/GraphMol/MolDraw2D/DrawMol.cpp | 72 +++++++++++++- Code/GraphMol/MolDraw2D/DrawMol.h | 1 + Code/GraphMol/MolDraw2D/MolDraw2DHelpers.h | 2 + Code/GraphMol/MolDraw2D/MolDraw2DUtils.cpp | 1 + Code/GraphMol/MolDraw2D/Wrap/rdMolDraw2D.cpp | 3 + Code/GraphMol/MolDraw2D/Wrap/testMolDraw2D.py | 14 ++- Code/GraphMol/MolDraw2D/catch_tests.cpp | 97 ++++++++++++++++++- Code/GraphMol/MolDraw2D/test1.cpp | 4 +- Code/MinimalLib/docs/GettingStartedInJS.html | 3 +- 9 files changed, 187 insertions(+), 10 deletions(-) diff --git a/Code/GraphMol/MolDraw2D/DrawMol.cpp b/Code/GraphMol/MolDraw2D/DrawMol.cpp index 741ee59d6..27e642283 100755 --- a/Code/GraphMol/MolDraw2D/DrawMol.cpp +++ b/Code/GraphMol/MolDraw2D/DrawMol.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include namespace RDKit { @@ -194,9 +195,6 @@ void DrawMol::initDrawMolecule(const ROMol &mol) { bool removeAffectedStereoGroups = true; Chirality::simplifyEnhancedStereo(*drawMol_, removeAffectedStereoGroups); } - if (drawOptions_.addStereoAnnotation) { - Chirality::addStereoAnnotations(*drawMol_); - } if (drawOptions_.addAtomIndices) { addAtomIndices(*drawMol_); } @@ -220,9 +218,10 @@ void DrawMol::extractAll(double scale) { extractHighlights(scale); extractAttachments(); extractAtomNotes(); - if (!drawOptions_.addStereoAnnotation) { - extractStereoGroups(); + if (drawOptions_.addStereoAnnotation) { + extractCIPCodes(drawOptions_.showAllCIPCodes); } + extractStereoGroups(); // always show StereoGroups extractBondNotes(); extractRadicals(); extractSGroupData(); @@ -465,6 +464,69 @@ void DrawMol::extractAtomNotes() { } } +// **************************************************************************** +void DrawMol::extractCIPCodes(bool showAllCIPCodes) { + boost::dynamic_bitset<> maskedAtoms(drawMol_->getNumAtoms()); + boost::dynamic_bitset<> maskedBonds(drawMol_->getNumBonds()); + + if(!showAllCIPCodes) { // record atoms and bonds whose codes should be hidden + for (const StereoGroup &group : drawMol_->getStereoGroups()) { + StereoGroupType stereoGroupType; + + stereoGroupType = group.getGroupType(); + if(stereoGroupType == RDKit::StereoGroupType::STEREO_OR || \ + stereoGroupType == RDKit::StereoGroupType::STEREO_AND ) { + for (const auto atom : group.getAtoms()) { + maskedAtoms.set(atom->getIdx()); + } + for (const auto bond : group.getBonds()) { + maskedBonds.set(bond->getIdx()); + } + } + } + } + + for (auto atom : drawMol_->atoms()) { + std::string cip; + if (!maskedAtoms[atom->getIdx()] && + atom->getPropIfPresent(common_properties::_CIPCode, cip)) { + cip = "(" + cip + ")"; + DrawAnnotation *annot = new DrawAnnotation( + cip, TextAlignType::MIDDLE, "CIP_Code", + drawOptions_.annotationFontScale, Point2D(0.0, 0.0), + drawOptions_.atomNoteColour, textDrawer_); + calcAnnotationPosition(atom, *annot); + annotations_.emplace_back(annot); + } + } + + for (auto bond : drawMol_->bonds()) { + std::string cip; + // Add E or Z CIP codes if missing to be compatible with previous + // implemtnation. In future, user should be responsible for calling + // AssignCIPLabels() before drawing to harmonize behavior with + // how R,S,M,P CIP codes are handled + if (!maskedBonds[bond->getIdx()]) { + if (!bond->getPropIfPresent(common_properties::_CIPCode, cip)) { + if (bond->getStereo() == Bond::STEREOE) { + cip = "E"; + } else if (bond->getStereo() == Bond::STEREOZ) { + cip = "Z"; + } + } + if (!cip.empty()) { + cip = "(" + cip + ")"; + DrawAnnotation *annot = new DrawAnnotation( + cip, TextAlignType::MIDDLE, "CIP_Code", + drawOptions_.annotationFontScale, Point2D(0.0, 0.0), + drawOptions_.bondNoteColour, textDrawer_); + calcAnnotationPosition(bond, *annot); + annotations_.emplace_back(annot); + } + } + } +} + // **************************************************************************** void DrawMol::extractStereoGroups() { int orCount(0), andCount(0); diff --git a/Code/GraphMol/MolDraw2D/DrawMol.h b/Code/GraphMol/MolDraw2D/DrawMol.h index 8eebfbe67..555514057 100755 --- a/Code/GraphMol/MolDraw2D/DrawMol.h +++ b/Code/GraphMol/MolDraw2D/DrawMol.h @@ -112,6 +112,7 @@ class DrawMol { void extractRegions(); void extractAttachments(); void extractMolNotes(); + void extractCIPCodes(bool showAllCIPCodes); void extractStereoGroups(); void extractAtomNotes(); void extractBondNotes(); diff --git a/Code/GraphMol/MolDraw2D/MolDraw2DHelpers.h b/Code/GraphMol/MolDraw2D/MolDraw2DHelpers.h index 75c2dfa6f..d7b8aba54 100644 --- a/Code/GraphMol/MolDraw2D/MolDraw2DHelpers.h +++ b/Code/GraphMol/MolDraw2D/MolDraw2DHelpers.h @@ -259,6 +259,8 @@ struct RDKIT_MOLDRAW2D_EXPORT MolDrawOptions { bool dummyIsotopeLabels = true; // adds isotope labels to dummy atoms. bool addStereoAnnotation = false; // adds E/Z and R/S to drawings. + bool showAllCIPCodes = false; // show CIP codes for 'or' & 'and' + // StereoGroups that are normally hidden bool atomHighlightsAreCircles = false; // forces atom highlights always to be // circles. Default (false) is to put // ellipses round longer labels. diff --git a/Code/GraphMol/MolDraw2D/MolDraw2DUtils.cpp b/Code/GraphMol/MolDraw2D/MolDraw2DUtils.cpp index 077e7e312..3b83261ab 100644 --- a/Code/GraphMol/MolDraw2D/MolDraw2DUtils.cpp +++ b/Code/GraphMol/MolDraw2D/MolDraw2DUtils.cpp @@ -241,6 +241,7 @@ void updateMolDrawOptionsFromJSON(MolDrawOptions &opts, PT_OPT_GET(isotopeLabels); PT_OPT_GET(dummyIsotopeLabels); PT_OPT_GET(addStereoAnnotation); + PT_OPT_GET(showAllCIPCodes); PT_OPT_GET(atomHighlightsAreCircles); PT_OPT_GET(centreMoleculesBeforeDrawing); PT_OPT_GET(explicitMethyl); diff --git a/Code/GraphMol/MolDraw2D/Wrap/rdMolDraw2D.cpp b/Code/GraphMol/MolDraw2D/Wrap/rdMolDraw2D.cpp index ab4546ac8..701d8f65e 100644 --- a/Code/GraphMol/MolDraw2D/Wrap/rdMolDraw2D.cpp +++ b/Code/GraphMol/MolDraw2D/Wrap/rdMolDraw2D.cpp @@ -922,6 +922,9 @@ BOOST_PYTHON_MODULE(rdMolDraw2D) { .def_readwrite("addStereoAnnotation", &RDKit::MolDrawOptions::addStereoAnnotation, "adds R/S and E/Z to drawings. Default False.") + .def_readwrite("showAllCIPCodes", + &RDKit::MolDrawOptions::showAllCIPCodes, + "show all defined CIP codes (no hiding!). Default False.") .def_readwrite("addAtomIndices", &RDKit::MolDrawOptions::addAtomIndices, "adds atom indices to drawings. Default False.") .def_readwrite("addBondIndices", &RDKit::MolDrawOptions::addBondIndices, diff --git a/Code/GraphMol/MolDraw2D/Wrap/testMolDraw2D.py b/Code/GraphMol/MolDraw2D/Wrap/testMolDraw2D.py index 852a90e63..901ad6757 100644 --- a/Code/GraphMol/MolDraw2D/Wrap/testMolDraw2D.py +++ b/Code/GraphMol/MolDraw2D/Wrap/testMolDraw2D.py @@ -843,7 +843,19 @@ M END''') do = rdMolDraw2D.MolDrawOptions() with self.assertRaises(AttributeError): do.rhubarb = True - + + def testShowAllCIPCodes(self): + m = Chem.MolFromSmiles("Cc1cccc(F)c1-c1c(C)cc([C@H](C)O)cc1Cl |wU:7.7,o1:7,&1:13|") + self.assertIsNotNone(m) + d2d = rdMolDraw2D.MolDraw2DSVG(-1, -1) + opts = d2d.drawOptions() + opts.addStereoAnnotation = True + opts.showAllCIPCodes = True + d2d.DrawMolecule(m) + d2d.FinishDrawing() + text = d2d.GetDrawingText() + #confirm that at least one CIP code is present + self.assertTrue("class='CIP_Code'" in text) if __name__ == "__main__": unittest.main() diff --git a/Code/GraphMol/MolDraw2D/catch_tests.cpp b/Code/GraphMol/MolDraw2D/catch_tests.cpp index 2324a7702..4b8991b37 100644 --- a/Code/GraphMol/MolDraw2D/catch_tests.cpp +++ b/Code/GraphMol/MolDraw2D/catch_tests.cpp @@ -10751,4 +10751,99 @@ TEST_CASE("Solid arrowhead in wrong place (Github 8500)") { Catch::Matchers::WithinAbs(expVals[i].second, 1.0e-4)); } check_file_hash("testArrowheads.svg"); -} \ No newline at end of file +} + +TEST_CASE("Show all CIP codes (Github 8561)") { + auto m = R"CTAB( + RDKit 2D + + 0 0 0 0 0 0 0 0 0 0999 V3000 +M V30 BEGIN CTAB +M V30 COUNTS 19 20 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 C -0.750000 -1.299038 0.000000 0 +M V30 5 C -1.500000 0.000000 0.000000 0 +M V30 6 C -0.750000 1.299038 0.000000 0 +M V30 7 F -1.500000 2.598076 0.000000 0 +M V30 8 C 0.750000 1.299038 0.000000 0 +M V30 9 C 1.500000 2.598076 0.000000 0 +M V30 10 C 3.000000 2.598076 0.000000 0 +M V30 11 C 3.750000 1.299038 0.000000 0 +M V30 12 C 3.750000 3.897114 0.000000 0 +M V30 13 C 3.000000 5.196152 0.000000 0 +M V30 14 C 3.750000 6.495191 0.000000 0 +M V30 15 C 5.250000 6.495191 0.000000 0 +M V30 16 O 3.000000 7.794229 0.000000 0 +M V30 17 C 1.500000 5.196152 0.000000 0 +M V30 18 C 0.750000 3.897114 0.000000 0 +M V30 19 Cl -0.750000 3.897114 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 2 4 5 +M V30 5 1 5 6 +M V30 6 1 6 7 +M V30 7 2 6 8 +M V30 8 1 8 9 +M V30 9 2 9 10 +M V30 10 1 10 11 +M V30 11 1 10 12 +M V30 12 2 12 13 +M V30 13 1 13 14 +M V30 14 1 14 15 CFG=1 +M V30 15 1 14 16 +M V30 16 1 13 17 +M V30 17 2 17 18 +M V30 18 1 18 19 +M V30 19 1 8 2 CFG=3 +M V30 20 1 18 9 +M V30 END BOND +M V30 BEGIN COLLECTION +M V30 MDLV30/STEREL1 ATOMS=(1 8) +M V30 MDLV30/STERAC1 ATOMS=(1 14) +M V30 END COLLECTION +M V30 END CTAB +M END +)CTAB"_ctab; + REQUIRE(m); + CIPLabeler::assignCIPLabels(*m); + MolDraw2DSVG drawer(350, 300); + drawer.drawOptions().addStereoAnnotation = true; + drawer.drawOptions().showAllCIPCodes = true; + drawer.drawMolecule(*m); + drawer.finishDrawing(); + auto text = drawer.getDrawingText(); + std::ofstream outs("testShowAllCIPLabels.svg"); + outs << text; + outs.close(); + + // Check for Blue (M) CIP label on Bond + TEST_ASSERT( + text.find("class='CIP_Code'") != + std::string::npos); + + //try again, this tiem using JSON to set options + const char *json = + "{\"addStereoAnnotation\":true, \"showAllCIPCodes\":true}"; + MolDraw2DSVG drawer2(350, 300); + MolDrawOptions opts; + MolDraw2DUtils::updateMolDrawOptionsFromJSON(opts, json); + drawer2.drawOptions() = opts; + drawer2.drawMolecule(*m); + drawer2.finishDrawing(); + auto text2 = drawer2.getDrawingText(); + std::ofstream outs2("testShowAllCIPLabels2.svg"); + outs2 << text2; + outs2.close(); + + // Check for Blue (M) CIP label on Bond + TEST_ASSERT( + text2.find("class='CIP_Code'") != + std::string::npos); + } + diff --git a/Code/GraphMol/MolDraw2D/test1.cpp b/Code/GraphMol/MolDraw2D/test1.cpp index 3163d5ed1..7e9cb2466 100644 --- a/Code/GraphMol/MolDraw2D/test1.cpp +++ b/Code/GraphMol/MolDraw2D/test1.cpp @@ -4036,12 +4036,12 @@ void test20Annotate() { #ifdef RDK_BUILD_FREETYPE_SUPPORT #if DO_TEST_ASSERT // last note - TEST_ASSERT(text.find("E") != diff --git a/Code/MinimalLib/docs/GettingStartedInJS.html b/Code/MinimalLib/docs/GettingStartedInJS.html index 8bf5511c0..8512be8b1 100644 --- a/Code/MinimalLib/docs/GettingStartedInJS.html +++ b/Code/MinimalLib/docs/GettingStartedInJS.html @@ -185,6 +185,7 @@ addBondIndices, isotopeLabels, dummyIsotopeLabels, addStereoAnnotation, +showAllCIPLabels, atomHighlightsAreCircles, centreMoleculesBeforeDrawing, explicitMethyl, @@ -321,4 +322,4 @@ mol1.draw_to_canvas_with_highlights(canvas_mol1, JSON.stringify({ legend: `mol1 - \ No newline at end of file +