// // Copyright (C) 2019 Greg Landrum // // @@ All Rights Reserved @@ // This file is part of the RDKit. // The contents are covered by the terms of the BSD license // which is included in the file license.txt, found at the root // of the RDKit source tree. // #define CATCH_CONFIG_MAIN // This tells Catch to provide a main() - only do // this in one cpp file #include "catch.hpp" #include #include #include #include #include #include #include #ifdef RDK_BUILD_CAIRO_SUPPORT #include #include "MolDraw2DCairo.h" #endif using namespace RDKit; TEST_CASE("prepareAndDrawMolecule", "[drawing]") { SECTION("basics") { auto m1 = "C1N[C@@H]2OCC12"_smiles; REQUIRE(m1); // we will be able to recognize that the prep worked because there // will be an H in the output: MolDraw2DSVG drawer(200, 200); MolDraw2DUtils::prepareAndDrawMolecule(drawer, *m1); drawer.finishDrawing(); std::string text = drawer.getDrawingText(); CHECK(text.find("H") != std::string::npos); } } TEST_CASE("tag atoms in SVG", "[drawing, SVG]") { SECTION("basics") { auto m1 = "C1N[C@@H]2OCC12"_smiles; REQUIRE(m1); MolDraw2DSVG drawer(200, 200); MolDraw2DUtils::prepareMolForDrawing(*m1); drawer.drawMolecule(*m1); std::map actions; actions["onclick"] = "alert"; double radius = 0.2; drawer.tagAtoms(*m1, radius, actions); drawer.finishDrawing(); std::string text = drawer.getDrawingText(); std::ofstream outs("testAtomTags_1.svg"); outs << text; outs.flush(); CHECK(text.find(" xps(gridSz); std::vector yps(gridSz); double minX = 1000, minY = 1000, maxX = -1000, maxY = -1000; const auto conf = m1->getConformer(); for (size_t i = 0; i < conf.getNumAtoms(); ++i) { minX = std::min(minX, conf.getAtomPos(i).x); minY = std::min(minY, conf.getAtomPos(i).y); maxX = std::max(maxX, conf.getAtomPos(i).x); maxY = std::max(maxY, conf.getAtomPos(i).y); } double x1 = minX - 0.5, y1 = minY - 0.5, x2 = maxX + 0.5, y2 = maxY + 0.5; double dx = (x2 - x1) / gridSz, dy = (y2 - y1) / gridSz; double maxV = 0.0; for (size_t ix = 0; ix < gridSz; ++ix) { auto px = x1 + ix * dx; xps[ix] = px; for (size_t iy = 0; iy < gridSz; ++iy) { auto py = y1 + iy * dy; if (ix == 0) { yps[iy] = py; } RDGeom::Point2D loc(px, py); double val = 0.0; for (size_t ia = 0; ia < conf.getNumAtoms(); ++ia) { auto dv = loc - RDGeom::Point2D(conf.getAtomPos(ia).x, conf.getAtomPos(ia).y); auto r = dv.length(); if (r > 0.1) { val += 1 / r; } } maxV = std::max(val, maxV); grid[ix * gridSz + iy] = val; } } std::vector levels; drawer.clearDrawing(); MolDraw2DUtils::contourAndDrawGrid(drawer, grid, xps, yps, 10, levels); drawer.drawOptions().clearBackground = false; drawer.drawMolecule(*m1); drawer.finishDrawing(); std::string text = drawer.getDrawingText(); std::ofstream outs("contourMol_1.svg"); outs << text; outs.flush(); delete[] grid; } SECTION("gaussian basics") { MolDraw2DSVG drawer(250, 250); MolDraw2DUtils::prepareMolForDrawing(*m1); drawer.drawOptions().padding = 0.1; const auto conf = m1->getConformer(); std::vector cents(conf.getNumAtoms()); std::vector weights(conf.getNumAtoms()); std::vector widths(conf.getNumAtoms()); for (size_t i = 0; i < conf.getNumAtoms(); ++i) { cents[i] = Point2D(conf.getAtomPos(i).x, conf.getAtomPos(i).y); weights[i] = 1; widths[i] = 0.4 * PeriodicTable::getTable()->getRcovalent( m1->getAtomWithIdx(i)->getAtomicNum()); } std::vector levels; drawer.clearDrawing(); MolDraw2DUtils::contourAndDrawGaussians(drawer, cents, weights, widths, 10, levels); drawer.drawOptions().clearBackground = false; drawer.drawMolecule(*m1); drawer.finishDrawing(); std::string text = drawer.getDrawingText(); std::ofstream outs("contourMol_2.svg"); outs << text; outs.flush(); } SECTION("gaussian fill") { MolDraw2DSVG drawer(250, 250); MolDraw2DUtils::prepareMolForDrawing(*m1); drawer.drawOptions().padding = 0.1; const auto conf = m1->getConformer(); std::vector cents(conf.getNumAtoms()); std::vector weights(conf.getNumAtoms()); std::vector widths(conf.getNumAtoms()); for (size_t i = 0; i < conf.getNumAtoms(); ++i) { cents[i] = Point2D(conf.getAtomPos(i).x, conf.getAtomPos(i).y); weights[i] = i % 2 ? -0.5 : 1; widths[i] = 0.4 * PeriodicTable::getTable()->getRcovalent( m1->getAtomWithIdx(i)->getAtomicNum()); } std::vector levels; MolDraw2DUtils::ContourParams cps; cps.fillGrid = true; drawer.clearDrawing(); MolDraw2DUtils::contourAndDrawGaussians(drawer, cents, weights, widths, 10, levels, cps); drawer.drawOptions().clearBackground = false; drawer.drawMolecule(*m1); drawer.finishDrawing(); std::string text = drawer.getDrawingText(); std::ofstream outs("contourMol_3.svg"); outs << text; outs.flush(); } SECTION("gaussian fill 2") { auto m2 = "C1N[C@@H]2OCC12C=CC"_smiles; REQUIRE(m2); MolDraw2DSVG drawer(450, 250); MolDraw2DUtils::prepareMolForDrawing(*m2); drawer.drawOptions().padding = 0.1; const auto conf = m2->getConformer(); std::vector cents(conf.getNumAtoms()); std::vector weights(conf.getNumAtoms()); std::vector widths(conf.getNumAtoms()); for (size_t i = 0; i < conf.getNumAtoms(); ++i) { cents[i] = Point2D(conf.getAtomPos(i).x, conf.getAtomPos(i).y); weights[i] = i % 2 ? -0.5 : 1; widths[i] = 0.3 * PeriodicTable::getTable()->getRcovalent( m2->getAtomWithIdx(i)->getAtomicNum()); } std::vector levels; MolDraw2DUtils::ContourParams cps; cps.fillGrid = true; cps.gridResolution = 0.5; drawer.clearDrawing(); MolDraw2DUtils::contourAndDrawGaussians(drawer, cents, weights, widths, 10, levels, cps); drawer.drawOptions().clearBackground = false; drawer.drawMolecule(*m2); drawer.finishDrawing(); std::string text = drawer.getDrawingText(); std::ofstream outs("contourMol_4.svg"); outs << text; outs.flush(); } } TEST_CASE("dative bonds", "[drawing, organometallics]") { SECTION("basics") { auto m1 = "N->[Pt]"_smiles; REQUIRE(m1); MolDraw2DSVG drawer(200, 200); MolDraw2DUtils::prepareMolForDrawing(*m1); drawer.drawMolecule(*m1); drawer.finishDrawing(); std::string text = drawer.getDrawingText(); std::ofstream outs("testDativeBonds_1.svg"); outs << text; outs.flush(); CHECK(text.find("HN") != std::string::npos); } { MolDraw2DSVG drawer(200, 200); assignBWPalette(drawer.drawOptions().atomColourPalette); MolDraw2DUtils::prepareAndDrawMolecule(drawer, *m1); drawer.finishDrawing(); std::string text = drawer.getDrawingText(); CHECK(text.find("fill:#0000FF' >HN") == std::string::npos); CHECK(text.find("fill:#000000' >HN") != std::string::npos); } } SECTION("test") { { MolDraw2DSVG drawer(200, 200); MolDrawOptions options = drawer.drawOptions(); assignBWPalette(options.atomColourPalette); drawer.drawOptions() = options; MolDraw2DUtils::prepareAndDrawMolecule(drawer, *m1); drawer.finishDrawing(); std::string text = drawer.getDrawingText(); CHECK(text.find("fill:#0000FF' >HN") == std::string::npos); CHECK(text.find("fill:#000000' >HN") != std::string::npos); } } } TEST_CASE("bad DrawMolecules() when molecules are not kekulized", "[drawing,bug]") { auto m1 = "CCN(CC)CCn1nc2c3ccccc3sc3c(CNS(C)(=O)=O)ccc1c32"_smiles; REQUIRE(m1); SECTION("foundations") { MolDraw2DSVG drawer(500, 200, 250, 200); drawer.drawOptions().prepareMolsBeforeDrawing = false; RWMol dm1(*m1); RWMol dm2(*m1); bool kekulize = false; MolDraw2DUtils::prepareMolForDrawing(dm1, kekulize); kekulize = true; MolDraw2DUtils::prepareMolForDrawing(dm2, kekulize); MOL_PTR_VECT ms{&dm1, &dm2}; drawer.drawMolecule(dm1); drawer.finishDrawing(); std::string text = drawer.getDrawingText(); std::ofstream outs("testKekulizationProblems_1.svg"); outs << text; outs.flush(); // this is a very crude test - really we just need to look at the SVG - but // it's better than nothing. CHECK(text.find( "") == std::string::npos); } } TEST_CASE("draw atom/bond indices", "[drawing]") { auto m1 = "C[C@H](F)N"_smiles; REQUIRE(m1); SECTION("foundations") { { MolDraw2DSVG drawer(250, 200); drawer.drawMolecule(*m1); drawer.finishDrawing(); std::string text = drawer.getDrawingText(); std::ofstream outs("testAtomBondIndices_1.svg"); outs << text; outs.flush(); CHECK(text.find("1") == std::string::npos); CHECK(text.find("1,(S)") == std::string::npos); } { MolDraw2DSVG drawer(250, 200); drawer.drawOptions().addAtomIndices = true; drawer.drawMolecule(*m1); drawer.finishDrawing(); std::string text = drawer.getDrawingText(); std::ofstream outs("testAtomBondIndices_2.svg"); outs << text; outs.flush(); CHECK(text.find("1") != std::string::npos); // it only appears once though: CHECK(text.find("1", text.find("1") + 1) == std::string::npos); CHECK(text.find("1,(S)") == std::string::npos); } { MolDraw2DSVG drawer(250, 200); drawer.drawOptions().addBondIndices = true; drawer.drawMolecule(*m1); drawer.finishDrawing(); std::string text = drawer.getDrawingText(); std::ofstream outs("testAtomBondIndices_3.svg"); outs << text; outs.flush(); CHECK(text.find("1") != std::string::npos); // it only appears once though: CHECK(text.find("1", text.find("1") + 1) == std::string::npos); } { MolDraw2DSVG drawer(250, 200); drawer.drawOptions().addAtomIndices = true; drawer.drawOptions().addBondIndices = true; drawer.drawMolecule(*m1); drawer.finishDrawing(); std::string text = drawer.getDrawingText(); std::ofstream outs("testAtomBondIndices_4.svg"); outs << text; outs.flush(); CHECK(text.find("1") != std::string::npos); // it appears twice: CHECK(text.find("1", text.find("1") + 1) != std::string::npos); } { MolDraw2DSVG drawer(250, 200); m1->getAtomWithIdx(2)->setProp(common_properties::atomNote, "foo"); drawer.drawOptions().addAtomIndices = true; drawer.drawOptions().addStereoAnnotation = true; drawer.drawMolecule(*m1); m1->getAtomWithIdx(2)->clearProp(common_properties::atomNote); drawer.finishDrawing(); std::string text = drawer.getDrawingText(); std::ofstream outs("testAtomBondIndices_5.svg"); outs << text; outs.flush(); CHECK(text.find("1,(S)") != std::string::npos); CHECK(text.find("2,foo") != std::string::npos); CHECK(text.find("1") == std::string::npos); } } } TEST_CASE("Github #3226: Lines in wedge bonds being drawn too closely together", "[drawing]") { auto m1 = "C[C@H](C1=C(C=CC(=C1Cl)F)Cl)OC2=C(N=CC(=C2)C3=CN(N=C3)C4CCNCC4)N"_smiles; REQUIRE(m1); SECTION("larger SVG") { { MolDraw2DSVG drawer(450, 400); drawer.drawMolecule(*m1); drawer.finishDrawing(); std::string text = drawer.getDrawingText(); std::ofstream outs("testGithub3226_1.svg"); outs << text; outs.flush(); std::vector tkns; boost::algorithm::find_all(tkns, text, "bond-0"); CHECK(tkns.size() == 6); } } #ifdef RDK_BUILD_CAIRO_SUPPORT SECTION("larger PNG") { { MolDraw2DCairo drawer(450, 400); drawer.drawMolecule(*m1); drawer.finishDrawing(); drawer.writeDrawingText("testGithub3226_1.png"); } } #endif SECTION("smaller SVG") { { MolDraw2DSVG drawer(200, 150); drawer.drawMolecule(*m1); drawer.finishDrawing(); std::string text = drawer.getDrawingText(); std::ofstream outs("testGithub3226_2.svg"); outs << text; outs.flush(); std::vector tkns; boost::algorithm::find_all(tkns, text, "bond-0"); CHECK(tkns.size() == 3); } } #ifdef RDK_BUILD_CAIRO_SUPPORT SECTION("smaller PNG") { { MolDraw2DCairo drawer(200, 150); drawer.drawMolecule(*m1); drawer.finishDrawing(); drawer.writeDrawingText("testGithub3226_2.png"); } } #endif SECTION("middle SVG") { { MolDraw2DSVG drawer(250, 200); drawer.drawMolecule(*m1); drawer.finishDrawing(); std::string text = drawer.getDrawingText(); std::ofstream outs("testGithub3226_3.svg"); outs << text; outs.flush(); std::vector tkns; boost::algorithm::find_all(tkns, text, "bond-0"); CHECK(tkns.size() == 3); } } #ifdef RDK_BUILD_CAIRO_SUPPORT SECTION("middle PNG") { { MolDraw2DCairo drawer(250, 200); drawer.drawMolecule(*m1); drawer.finishDrawing(); drawer.writeDrawingText("testGithub3226_3.png"); } } #endif } TEST_CASE("github #3258: ", "[drawing][bug]") { auto m1 = "CCN"_smiles; REQUIRE(m1); SECTION("foundations") { MolDraw2DSVG drawer(500, 200, 250, 200); drawer.drawOptions().addAtomIndices = true; drawer.drawOptions().addBondIndices = true; RWMol dm1(*m1); RWMol dm2(*m1); MOL_PTR_VECT ms{&dm1, &dm2}; drawer.drawMolecules(ms); drawer.finishDrawing(); std::string text = drawer.getDrawingText(); CHECK(text.find(">,") == std::string::npos); CHECK(!dm1.hasProp("_atomIndicesAdded")); CHECK(!dm1.hasProp("_bondIndicesAdded")); } } TEST_CASE( "github #3392: prepareMolForDrawing() incorrectly adds chiral Hs if no " "ring info is present", "[bug]") { SECTION("foundations") { SmilesParserParams ps; ps.sanitize = false; ps.removeHs = false; std::unique_ptr m1(SmilesToMol("C[C@H](F)Cl", ps)); REQUIRE(m1); m1->updatePropertyCache(); CHECK(m1->getNumAtoms() == 4); const bool kekulize = false; const bool addChiralHs = true; MolDraw2DUtils::prepareMolForDrawing(*m1, kekulize, addChiralHs); CHECK(m1->getNumAtoms() == 4); } }