extract continuous lines from the conrec code (#6676)

This commit is contained in:
Greg Landrum
2023-09-15 09:00:26 +02:00
committed by GitHub
parent cccee15a91
commit 83cf752eb0
11 changed files with 420 additions and 64 deletions

View File

@@ -1,5 +1,5 @@
//
// Copyright (C) 2019 Greg Landrum
// Copyright (C) 2019-2023 Greg Landrum
//
// @@ All Rights Reserved @@
// This file is part of the RDKit.
@@ -8,7 +8,15 @@
// of the RDKit source tree.
//
#include <vector>
#include <list>
#include <unordered_map>
#include <Geometry/point.h>
#include <cmath>
#include <RDGeneral/BoostStartInclude.h>
#include <boost/dynamic_bitset.hpp>
#include <boost/functional/hash.hpp>
#include <RDGeneral/BoostEndInclude.h>
namespace conrec {
struct ConrecSegment {
@@ -200,4 +208,88 @@ inline void Contour(const double *d, size_t ilb, size_t iub, size_t jlb,
} /* i */
} /* j */
}
struct tplHash {
template <class T1, class T2, class T3>
std::size_t operator()(const std::tuple<T1, T2, T3> &p) const {
std::size_t res = 0;
boost::hash_combine(res, std::get<0>(p));
boost::hash_combine(res, std::get<1>(p));
boost::hash_combine(res, std::get<2>(p));
return res;
}
};
inline std::vector<std::pair<std::vector<RDGeom::Point2D>, double>>
connectLineSegments(const std::vector<ConrecSegment> &segments,
double coordMultiplier = 1000,
double isoValMultiplier = 1e6) {
std::vector<std::pair<std::vector<RDGeom::Point2D>, double>> res;
std::unordered_map<std::tuple<int, int, long>, std::list<size_t>, tplHash>
endPointHashes;
auto makePointKey = [&coordMultiplier, &isoValMultiplier](const auto &pt,
double isoVal) {
return std::make_tuple<int, int, long>(
std::lround(coordMultiplier * pt.x),
std::lround(coordMultiplier * pt.y),
std::lround(isoValMultiplier * isoVal));
};
// first hash all of the endpoints
for (auto i = 0u; i < segments.size(); ++i) {
const auto &seg = segments[i];
endPointHashes[makePointKey(seg.p1, seg.isoVal)].push_back(i);
endPointHashes[makePointKey(seg.p2, seg.isoVal)].push_back(i);
}
boost::dynamic_bitset<> segmentsDone(segments.size());
// a candidate end point hasn't been used already.
auto isCandidate = [&segmentsDone](const auto &pr) {
return !pr.second.empty() && !segmentsDone[pr.second.front()];
};
auto singlePoint =
std::find_if(endPointHashes.begin(), endPointHashes.end(), isCandidate);
while (singlePoint != endPointHashes.end()) {
auto segId = singlePoint->second.front();
auto currKey = singlePoint->first;
auto currVal = segments[segId].isoVal;
std::vector<RDGeom::Point2D> contour;
while (1) {
segmentsDone.set(segId, true);
// move onto the next segment
const auto seg = segments[segId];
auto k1 = makePointKey(seg.p1, seg.isoVal);
auto k2 = makePointKey(seg.p2, seg.isoVal);
auto endPtKey = k2;
if (k1 == currKey) {
contour.push_back(seg.p1);
} else if (k2 == currKey) {
contour.push_back(seg.p2);
endPtKey = k1;
}
// remove this segment from the two hash lists:
auto &segs1 = endPointHashes[currKey];
segs1.erase(std::find(segs1.begin(), segs1.end(), segId));
auto &segs = endPointHashes[endPtKey];
segs.erase(std::find(segs.begin(), segs.end(), segId));
if (segs.empty()) {
// we're at the end, push on the last point
if (k1 == currKey) {
contour.push_back(seg.p2);
} else if (k2 == currKey) {
contour.push_back(seg.p1);
}
break;
}
segId = segs.front();
currKey = endPtKey;
}
res.push_back(std::make_pair(contour, currVal));
singlePoint = std::find_if(singlePoint, endPointHashes.end(), isCandidate);
}
return res;
}
} // namespace conrec

View File

@@ -1,5 +1,5 @@
//
// Copyright (C) 2019 Greg Landrum
// Copyright (C) 2019-2023 Greg Landrum
//
// @@ All Rights Reserved @@
// This file is part of the RDKit.
@@ -23,7 +23,149 @@ TEST_CASE("Conrec basics", "[conrec]") {
SECTION("basics") {
std::vector<RDGeom::Point2D> pts = {{0., 0.}, {1., 0.}, {1., 1.}, {0., 1.}};
const size_t gridSz = 100;
auto *grid = new double[gridSz * gridSz];
auto grid = std::make_unique<double[]>(gridSz * gridSz);
double xps[gridSz];
double yps[gridSz];
double x1 = -4, y1 = -4, x2 = 6, y2 = 6;
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 (const auto &pt : pts) {
auto dv = loc - pt;
auto r = dv.length();
if (r > 0) {
val += 1 / r;
}
}
// to make the contours more visible, we cap the max val we set at 1000
maxV = std::max(std::min(val, 1000.), maxV);
grid[ix * gridSz + iy] = val;
}
}
std::vector<conrec::ConrecSegment> segs;
const size_t nContours = 10;
double isoLevels[nContours];
for (size_t i = 0; i < nContours; ++i) {
isoLevels[i] = (i + 1) * (maxV / (nContours + 1));
}
conrec::Contour(grid.get(), 0, gridSz - 1, 0, gridSz - 1, xps, yps,
nContours, isoLevels, segs);
std::ofstream outs("./blah.svg");
outs << R"SVG(<?xml version='1.0' encoding='iso-8859-1'?>
<svg version='1.1' baseProfile='full'
xmlns='http://www.w3.org/2000/svg'
xmlns:rdkit='http://www.rdkit.org/xml'
xmlns:xlink='http://www.w3.org/1999/xlink'
xml:space='preserve'
width='300px' height='300px' >
<rect style='opacity:1.0;fill:#FFFFFF;stroke:none' width='300' height='300' x='0' y='0'> </rect>
<!-- END OF HEADER -->
)SVG";
for (const auto &seg : segs) {
outs << "<path d='M " << 40 * seg.p1.x + 150 << "," << 40 * seg.p1.y + 150
<< " " << 40 * seg.p2.x + 150 << "," << 40 * seg.p2.y + 150
<< "' "
"style='fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:"
"0.5px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:"
"1' "
"/>"
<< std::endl;
;
}
outs << "</svg>" << std::endl;
}
}
TEST_CASE("connectLineSegments", "[conrec]") {
SECTION("basics") {
std::vector<RDGeom::Point2D> pts = {{0., 0.}, {1., 0.}, {1., 1.}, {0., 1.}};
const size_t gridSz = 100;
auto grid = std::make_unique<double[]>(gridSz * gridSz);
double xps[gridSz];
double yps[gridSz];
double x1 = -4, y1 = -4, x2 = 6, y2 = 6;
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 (const auto &pt : pts) {
auto dv = loc - pt;
auto r = dv.length();
if (r > 0) {
val += 1 / r;
}
}
maxV = std::max(std::min(val, 1000.), maxV);
grid[ix * gridSz + iy] = val;
}
}
std::vector<conrec::ConrecSegment> segs;
const size_t nContours = 10;
double isoLevels[nContours];
for (size_t i = 0; i < nContours; ++i) {
isoLevels[i] = (i + 1) * (maxV / (nContours + 1));
}
conrec::Contour(grid.get(), 0, gridSz - 1, 0, gridSz - 1, xps, yps,
nContours, isoLevels, segs);
auto contours = conrec::connectLineSegments(segs);
CHECK(contours.size() == 74);
std::ofstream outs("./blah.contour.svg");
outs << R"SVG(<?xml version='1.0' encoding='iso-8859-1'?>
<svg version='1.1' baseProfile='full'
xmlns='http://www.w3.org/2000/svg'
xmlns:rdkit='http://www.rdkit.org/xml'
xmlns:xlink='http://www.w3.org/1999/xlink'
xml:space='preserve'
width='300px' height='300px' >
<rect style='opacity:1.0;fill:#FFFFFF;stroke:none' width='300' height='300' x='0' y='0'> </rect>
<!-- END OF HEADER -->
)SVG";
for (const auto &pr : contours) {
auto [contour, val] = pr;
REQUIRE(contour.size());
outs << "<path d='M " << 40 * contour[0].x + 150 << ","
<< 40 * contour[0].y + 150;
for (auto i = 1u; i < contour.size(); ++i) {
outs << " L " << 40 * contour[i].x + 150 << ","
<< 40 * contour[i].y + 150;
}
outs << "' "
"style='fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:"
"0.5px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:"
"1' "
"/>"
<< std::endl;
}
outs << "</svg>" << std::endl;
}
}
TEST_CASE("super chunky", "[conrec]") {
// an example where you can really see any holes in the contours
SECTION("basics") {
std::vector<RDGeom::Point2D> pts = {{0., 0.}, {1., 0.}, {1., 1.}, {0., 1.}};
const size_t gridSz = 5;
auto grid = std::make_unique<double[]>(gridSz * gridSz);
double xps[gridSz];
double yps[gridSz];
double x1 = -4, y1 = -4, x2 = 6, y2 = 6;
@@ -51,15 +193,16 @@ TEST_CASE("Conrec basics", "[conrec]") {
}
}
std::vector<conrec::ConrecSegment> segs;
const size_t nContours = 10;
const size_t nContours = 1;
double isoLevels[nContours];
for (size_t i = 0; i < nContours; ++i) {
isoLevels[i] = (i + 1) * (maxV / (nContours + 1));
}
conrec::Contour(grid, 0, gridSz - 1, 0, gridSz - 1, xps, yps, nContours,
isoLevels, segs);
std::ofstream outs("./blah.svg");
conrec::Contour(grid.get(), 0, gridSz - 1, 0, gridSz - 1, xps, yps,
nContours, isoLevels, segs);
std::ofstream outs("./chunky.svg");
outs << R"SVG(<?xml version='1.0' encoding='iso-8859-1'?>
<svg version='1.1' baseProfile='full'
xmlns='http://www.w3.org/2000/svg'
@@ -82,6 +225,35 @@ width='300px' height='300px' >
;
}
outs << "</svg>" << std::endl;
delete[] grid;
auto contours = conrec::connectLineSegments(segs);
std::ofstream outs2("./chunky.contour.svg");
outs2 << R"SVG(<?xml version='1.0' encoding='iso-8859-1'?>
<svg version='1.1' baseProfile='full'
xmlns='http://www.w3.org/2000/svg'
xmlns:rdkit='http://www.rdkit.org/xml'
xmlns:xlink='http://www.w3.org/1999/xlink'
xml:space='preserve'
width='300px' height='300px' >
<rect style='opacity:1.0;fill:#FFFFFF;stroke:none' width='300' height='300' x='0' y='0'> </rect>
<!-- END OF HEADER -->
)SVG";
for (const auto &pr : contours) {
auto [contour, val] = pr;
REQUIRE(contour.size());
outs2 << "<path d='M " << 40 * contour[0].x + 150 << ","
<< 40 * contour[0].y + 150;
for (auto i = 1u; i < contour.size(); ++i) {
outs2 << " L " << 40 * contour[i].x + 150 << ","
<< 40 * contour[i].y + 150;
}
outs2 << "' "
"style='fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:"
"0.5px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:"
"1' "
"/>"
<< std::endl;
}
outs2 << "</svg>" << std::endl;
}
}