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
This commit is contained in:
paconius
2025-07-24 10:46:15 -04:00
committed by GitHub
parent e712d201a6
commit 1fe07a5eb7
9 changed files with 187 additions and 10 deletions

View File

@@ -33,6 +33,7 @@
#include <GraphMol/MolEnumerator/LinkNode.h>
#include <GraphMol/MolTransforms/MolTransforms.h>
#include <GraphMol/Depictor/RDDepictor.h>
#include <GraphMol/StereoGroup.h>
#include <GraphMol/Atropisomers.h>
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);

View File

@@ -112,6 +112,7 @@ class DrawMol {
void extractRegions();
void extractAttachments();
void extractMolNotes();
void extractCIPCodes(bool showAllCIPCodes);
void extractStereoGroups();
void extractAtomNotes();
void extractBondNotes();

View File

@@ -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.

View File

@@ -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);

View File

@@ -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,

View File

@@ -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()

View File

@@ -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");
}
}
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);
}

View File

@@ -4036,12 +4036,12 @@ void test20Annotate() {
#ifdef RDK_BUILD_FREETYPE_SUPPORT
#if DO_TEST_ASSERT
// last note
TEST_ASSERT(text.find("<path class='note' d='M 273.9 236.3") !=
TEST_ASSERT(text.find("<path class='CIP_Code' d='M 273.9 236.3") !=
std::string::npos);
#endif
#else
// this is the (E)
TEST_ASSERT(text.find("<text x='260.3' y='232.0' class='note' "
TEST_ASSERT(text.find("<text x='260.3' y='232.0' class='CIP_Code' "
"style='font-size:20px;font-style:normal;font-weight:"
"normal;fill-opacity:1;stroke:none;font-family:sans-"
"serif;text-anchor:start;fill:#7F7FFF' >E</text>") !=

View File

@@ -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
</div>
</body>
</html>
</html>