/*
* Wrapper module for libqhull, providing Delaunay triangulation.
*
* This module's methods should not be accessed directly. To obtain a Delaunay
* triangulation, construct an instance of the matplotlib.tri.Triangulation
* class without specifying a triangles array.
*/
#include
#include
#ifdef _MSC_VER
/* The Qhull header does not declare this as extern "C", but only MSVC seems to
* do name mangling on global variables. We thus need to declare this before
* the header so that it treats it correctly, and doesn't mangle the name. */
extern "C" {
extern const char qh_version[];
}
#endif
#include "libqhull_r/qhull_ra.h"
#include
#include
#ifndef MPL_DEVNULL
#error "MPL_DEVNULL must be defined as the OS-equivalent of /dev/null"
#endif
#define STRINGIFY(x) STR(x)
#define STR(x) #x
namespace py = pybind11;
using namespace pybind11::literals;
// Input numpy array class.
typedef py::array_t CoordArray;
// Output numpy array class.
typedef py::array_t IndexArray;
static const char* qhull_error_msg[6] = {
"", /* 0 = qh_ERRnone */
"input inconsistency", /* 1 = qh_ERRinput */
"singular input data", /* 2 = qh_ERRsingular */
"precision error", /* 3 = qh_ERRprec */
"insufficient memory", /* 4 = qh_ERRmem */
"internal error"}; /* 5 = qh_ERRqhull */
/* Return the indices of the 3 vertices that comprise the specified facet (i.e.
* triangle). */
static void
get_facet_vertices(qhT* qh, const facetT* facet, int indices[3])
{
vertexT *vertex, **vertexp;
FOREACHvertex_(facet->vertices) {
*indices++ = qh_pointid(qh, vertex->point);
}
}
/* Return the indices of the 3 triangles that are neighbors of the specified
* facet (triangle). */
static void
get_facet_neighbours(const facetT* facet, std::vector& tri_indices,
int indices[3])
{
facetT *neighbor, **neighborp;
FOREACHneighbor_(facet) {
*indices++ = (neighbor->upperdelaunay ? -1 : tri_indices[neighbor->id]);
}
}
/* Return true if the specified points arrays contain at least 3 unique points,
* or false otherwise. */
static bool
at_least_3_unique_points(py::ssize_t npoints, const double* x, const double* y)
{
const py::ssize_t unique1 = 0; /* First unique point has index 0. */
py::ssize_t unique2 = 0; /* Second unique point index is 0 until set. */
if (npoints < 3) {
return false;
}
for (py::ssize_t i = 1; i < npoints; ++i) {
if (unique2 == 0) {
/* Looking for second unique point. */
if (x[i] != x[unique1] || y[i] != y[unique1]) {
unique2 = i;
}
}
else {
/* Looking for third unique point. */
if ( (x[i] != x[unique1] || y[i] != y[unique1]) &&
(x[i] != x[unique2] || y[i] != y[unique2]) ) {
/* 3 unique points found, with indices 0, unique2 and i. */
return true;
}
}
}
/* Run out of points before 3 unique points found. */
return false;
}
/* Holds on to info from Qhull so that it can be destructed automatically. */
class QhullInfo {
public:
QhullInfo(FILE *error_file, qhT* qh) {
this->error_file = error_file;
this->qh = qh;
}
~QhullInfo() {
qh_freeqhull(this->qh, !qh_ALL);
int curlong, totlong; /* Memory remaining. */
qh_memfreeshort(this->qh, &curlong, &totlong);
if (curlong || totlong) {
PyErr_WarnEx(PyExc_RuntimeWarning,
"Qhull could not free all allocated memory", 1);
}
if (this->error_file != stderr) {
fclose(error_file);
}
}
private:
FILE* error_file;
qhT* qh;
};
/* Delaunay implementation method.
* If hide_qhull_errors is true then qhull error messages are discarded;
* if it is false then they are written to stderr. */
static py::tuple
delaunay_impl(py::ssize_t npoints, const double* x, const double* y,
bool hide_qhull_errors)
{
qhT qh_qh; /* qh variable type and name must be like */
qhT* qh = &qh_qh; /* this for Qhull macros to work correctly. */
facetT* facet;
int i, ntri, max_facet_id;
int exitcode; /* Value returned from qh_new_qhull(). */
const int ndim = 2;
double x_mean = 0.0;
double y_mean = 0.0;
QHULL_LIB_CHECK
/* Allocate points. */
std::vector points(npoints * ndim);
/* Determine mean x, y coordinates. */
for (i = 0; i < npoints; ++i) {
x_mean += x[i];
y_mean += y[i];
}
x_mean /= npoints;
y_mean /= npoints;
/* Prepare points array to pass to qhull. */
for (i = 0; i < npoints; ++i) {
points[2*i ] = x[i] - x_mean;
points[2*i+1] = y[i] - y_mean;
}
/* qhull expects a FILE* to write errors to. */
FILE* error_file = nullptr;
if (hide_qhull_errors) {
/* qhull errors are ignored by writing to OS-equivalent of /dev/null.
* Rather than have OS-specific code here, instead it is determined by
* meson.build and passed in via the macro MPL_DEVNULL. */
error_file = fopen(STRINGIFY(MPL_DEVNULL), "w");
if (error_file == nullptr) {
throw std::runtime_error("Could not open devnull");
}
}
else {
/* qhull errors written to stderr. */
error_file = stderr;
}
/* Perform Delaunay triangulation. */
QhullInfo info(error_file, qh);
qh_zero(qh, error_file);
exitcode = qh_new_qhull(qh, ndim, (int)npoints, points.data(), False,
(char*)"qhull d Qt Qbb Qc Qz", nullptr, error_file);
if (exitcode != qh_ERRnone) {
std::string msg =
py::str("Error in qhull Delaunay triangulation calculation: {} (exitcode={})")
.format(qhull_error_msg[exitcode], exitcode).cast();
if (hide_qhull_errors) {
msg += "; use python verbose option (-v) to see original qhull error.";
}
throw std::runtime_error(msg);
}
/* Split facets so that they only have 3 points each. */
qh_triangulate(qh);
/* Determine ntri and max_facet_id.
Note that libqhull uses macros to iterate through collections. */
ntri = 0;
FORALLfacets {
if (!facet->upperdelaunay) {
++ntri;
}
}
max_facet_id = qh->facet_id - 1;
/* Create array to map facet id to triangle index. */
std::vector tri_indices(max_facet_id+1);
/* Allocate Python arrays to return. */
int dims[2] = {ntri, 3};
IndexArray triangles(dims);
int* triangles_ptr = triangles.mutable_data();
IndexArray neighbors(dims);
int* neighbors_ptr = neighbors.mutable_data();
/* Determine triangles array and set tri_indices array. */
i = 0;
FORALLfacets {
if (!facet->upperdelaunay) {
int indices[3];
tri_indices[facet->id] = i++;
get_facet_vertices(qh, facet, indices);
*triangles_ptr++ = (facet->toporient ? indices[0] : indices[2]);
*triangles_ptr++ = indices[1];
*triangles_ptr++ = (facet->toporient ? indices[2] : indices[0]);
}
else {
tri_indices[facet->id] = -1;
}
}
/* Determine neighbors array. */
FORALLfacets {
if (!facet->upperdelaunay) {
int indices[3];
get_facet_neighbours(facet, tri_indices, indices);
*neighbors_ptr++ = (facet->toporient ? indices[2] : indices[0]);
*neighbors_ptr++ = (facet->toporient ? indices[0] : indices[2]);
*neighbors_ptr++ = indices[1];
}
}
return py::make_tuple(triangles, neighbors);
}
/* Process Python arguments and call Delaunay implementation method. */
static py::tuple
delaunay(const CoordArray& x, const CoordArray& y, int verbose)
{
if (x.ndim() != 1 || y.ndim() != 1) {
throw std::invalid_argument("x and y must be 1D arrays");
}
auto npoints = x.shape(0);
if (npoints != y.shape(0)) {
throw std::invalid_argument("x and y must be 1D arrays of the same length");
}
if (npoints < 3) {
throw std::invalid_argument("x and y arrays must have a length of at least 3");
}
if (!at_least_3_unique_points(npoints, x.data(), y.data())) {
throw std::invalid_argument("x and y arrays must consist of at least 3 unique points");
}
return delaunay_impl(npoints, x.data(), y.data(), verbose == 0);
}
PYBIND11_MODULE(_qhull, m, py::mod_gil_not_used())
{
m.doc() = "Computing Delaunay triangulations.\n";
m.def("delaunay", &delaunay, "x"_a, "y"_a, "verbose"_a,
"--\n\n"
"Compute a Delaunay triangulation.\n"
"\n"
"Parameters\n"
"----------\n"
"x, y : 1d arrays\n"
" The coordinates of the point set, which must consist of at least\n"
" three unique points.\n"
"verbose : int\n"
" Python's verbosity level.\n"
"\n"
"Returns\n"
"-------\n"
"triangles, neighbors : int arrays, shape (ntri, 3)\n"
" Indices of triangle vertices and indices of triangle neighbors.\n");
m.def("version", []() { return qh_version; },
"version()\n--\n\n"
"Return the qhull version string.");
}