Skip to content

Commit 05173e3

Browse files
authored
Merge pull request matplotlib#27184 from QuLogic/qhull-pybind11
Use pybind11 for qhull wrapper
2 parents c0e9ebc + f8000bc commit 05173e3

File tree

2 files changed

+68
-106
lines changed

2 files changed

+68
-106
lines changed

src/_qhull_wrapper.cpp

+67-105
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
* triangulation, construct an instance of the matplotlib.tri.Triangulation
66
* class without specifying a triangles array.
77
*/
8-
#define PY_SSIZE_T_CLEAN
9-
#include "Python.h"
10-
#include "numpy_cpp.h"
8+
#include <pybind11/pybind11.h>
9+
#include <pybind11/numpy.h>
10+
1111
#ifdef _MSC_VER
1212
/* The Qhull header does not declare this as extern "C", but only MSVC seems to
1313
* do name mangling on global variables. We thus need to declare this before
@@ -16,18 +16,28 @@ extern "C" {
1616
extern const char qh_version[];
1717
}
1818
#endif
19+
1920
#include "libqhull_r/qhull_ra.h"
2021
#include <cstdio>
2122
#include <vector>
2223

23-
2424
#ifndef MPL_DEVNULL
2525
#error "MPL_DEVNULL must be defined as the OS-equivalent of /dev/null"
2626
#endif
2727

2828
#define STRINGIFY(x) STR(x)
2929
#define STR(x) #x
3030

31+
namespace py = pybind11;
32+
using namespace pybind11::literals;
33+
34+
// Input numpy array class.
35+
typedef py::array_t<double, py::array::c_style | py::array::forcecast> CoordArray;
36+
37+
// Output numpy array class.
38+
typedef py::array_t<int> IndexArray;
39+
40+
3141

3242
static const char* qhull_error_msg[6] = {
3343
"", /* 0 = qh_ERRnone */
@@ -64,17 +74,16 @@ get_facet_neighbours(const facetT* facet, std::vector<int>& tri_indices,
6474
/* Return true if the specified points arrays contain at least 3 unique points,
6575
* or false otherwise. */
6676
static bool
67-
at_least_3_unique_points(npy_intp npoints, const double* x, const double* y)
77+
at_least_3_unique_points(py::ssize_t npoints, const double* x, const double* y)
6878
{
69-
int i;
70-
const int unique1 = 0; /* First unique point has index 0. */
71-
int unique2 = 0; /* Second unique point index is 0 until set. */
79+
const py::ssize_t unique1 = 0; /* First unique point has index 0. */
80+
py::ssize_t unique2 = 0; /* Second unique point index is 0 until set. */
7281

7382
if (npoints < 3) {
7483
return false;
7584
}
7685

77-
for (i = 1; i < npoints; ++i) {
86+
for (py::ssize_t i = 1; i < npoints; ++i) {
7887
if (unique2 == 0) {
7988
/* Looking for second unique point. */
8089
if (x[i] != x[unique1] || y[i] != y[unique1]) {
@@ -125,8 +134,8 @@ class QhullInfo {
125134
/* Delaunay implementation method.
126135
* If hide_qhull_errors is true then qhull error messages are discarded;
127136
* if it is false then they are written to stderr. */
128-
static PyObject*
129-
delaunay_impl(npy_intp npoints, const double* x, const double* y,
137+
static py::tuple
138+
delaunay_impl(py::ssize_t npoints, const double* x, const double* y,
130139
bool hide_qhull_errors)
131140
{
132141
qhT qh_qh; /* qh variable type and name must be like */
@@ -179,11 +188,13 @@ delaunay_impl(npy_intp npoints, const double* x, const double* y,
179188
exitcode = qh_new_qhull(qh, ndim, (int)npoints, points.data(), False,
180189
(char*)"qhull d Qt Qbb Qc Qz", NULL, error_file);
181190
if (exitcode != qh_ERRnone) {
182-
PyErr_Format(PyExc_RuntimeError,
183-
"Error in qhull Delaunay triangulation calculation: %s (exitcode=%d)%s",
184-
qhull_error_msg[exitcode], exitcode,
185-
hide_qhull_errors ? "; use python verbose option (-v) to see original qhull error." : "");
186-
return NULL;
191+
std::string msg =
192+
py::str("Error in qhull Delaunay triangulation calculation: {} (exitcode={})")
193+
.format(qhull_error_msg[exitcode], exitcode).cast<std::string>();
194+
if (hide_qhull_errors) {
195+
msg += "; use python verbose option (-v) to see original qhull error.";
196+
}
197+
throw std::runtime_error(msg);
187198
}
188199

189200
/* Split facets so that they only have 3 points each. */
@@ -204,12 +215,12 @@ delaunay_impl(npy_intp npoints, const double* x, const double* y,
204215
std::vector<int> tri_indices(max_facet_id+1);
205216

206217
/* Allocate Python arrays to return. */
207-
npy_intp dims[2] = {ntri, 3};
208-
numpy::array_view<int, ndim> triangles(dims);
209-
int* triangles_ptr = triangles.data();
218+
int dims[2] = {ntri, 3};
219+
IndexArray triangles(dims);
220+
int* triangles_ptr = triangles.mutable_data();
210221

211-
numpy::array_view<int, ndim> neighbors(dims);
212-
int* neighbors_ptr = neighbors.data();
222+
IndexArray neighbors(dims);
223+
int* neighbors_ptr = neighbors.mutable_data();
213224

214225
/* Determine triangles array and set tri_indices array. */
215226
i = 0;
@@ -238,103 +249,54 @@ delaunay_impl(npy_intp npoints, const double* x, const double* y,
238249
}
239250
}
240251

241-
PyObject* tuple = PyTuple_New(2);
242-
if (tuple == 0) {
243-
throw std::runtime_error("Failed to create Python tuple");
244-
}
245-
246-
PyTuple_SET_ITEM(tuple, 0, triangles.pyobj());
247-
PyTuple_SET_ITEM(tuple, 1, neighbors.pyobj());
248-
return tuple;
252+
return py::make_tuple(triangles, neighbors);
249253
}
250254

251255
/* Process Python arguments and call Delaunay implementation method. */
252-
static PyObject*
253-
delaunay(PyObject *self, PyObject *args)
256+
static py::tuple
257+
delaunay(const CoordArray& x, const CoordArray& y, int verbose)
254258
{
255-
numpy::array_view<double, 1> xarray;
256-
numpy::array_view<double, 1> yarray;
257-
PyObject* ret;
258-
npy_intp npoints;
259-
const double* x;
260-
const double* y;
261-
int verbose = 0;
262-
263-
if (!PyArg_ParseTuple(args, "O&O&i:delaunay",
264-
&xarray.converter_contiguous, &xarray,
265-
&yarray.converter_contiguous, &yarray,
266-
&verbose)) {
267-
return NULL;
259+
if (x.ndim() != 1 || y.ndim() != 1) {
260+
throw std::invalid_argument("x and y must be 1D arrays");
268261
}
269262

270-
npoints = xarray.shape(0);
271-
if (npoints != yarray.shape(0)) {
272-
PyErr_SetString(PyExc_ValueError,
273-
"x and y must be 1D arrays of the same length");
274-
return NULL;
263+
auto npoints = x.shape(0);
264+
if (npoints != y.shape(0)) {
265+
throw std::invalid_argument("x and y must be 1D arrays of the same length");
275266
}
276267

277268
if (npoints < 3) {
278-
PyErr_SetString(PyExc_ValueError,
279-
"x and y arrays must have a length of at least 3");
280-
return NULL;
269+
throw std::invalid_argument("x and y arrays must have a length of at least 3");
281270
}
282271

283-
x = xarray.data();
284-
y = yarray.data();
285-
286-
if (!at_least_3_unique_points(npoints, x, y)) {
287-
PyErr_SetString(PyExc_ValueError,
288-
"x and y arrays must consist of at least 3 unique points");
289-
return NULL;
272+
if (!at_least_3_unique_points(npoints, x.data(), y.data())) {
273+
throw std::invalid_argument("x and y arrays must consist of at least 3 unique points");
290274
}
291275

292-
CALL_CPP("qhull.delaunay",
293-
(ret = delaunay_impl(npoints, x, y, verbose == 0)));
294-
295-
return ret;
296-
}
297-
298-
/* Return qhull version string for assistance in debugging. */
299-
static PyObject*
300-
version(PyObject *self, PyObject *arg)
301-
{
302-
return PyBytes_FromString(qh_version);
276+
return delaunay_impl(npoints, x.data(), y.data(), verbose == 0);
303277
}
304278

305-
static PyMethodDef qhull_methods[] = {
306-
{"delaunay", delaunay, METH_VARARGS,
307-
"delaunay(x, y, verbose, /)\n"
308-
"--\n\n"
309-
"Compute a Delaunay triangulation.\n"
310-
"\n"
311-
"Parameters\n"
312-
"----------\n"
313-
"x, y : 1d arrays\n"
314-
" The coordinates of the point set, which must consist of at least\n"
315-
" three unique points.\n"
316-
"verbose : int\n"
317-
" Python's verbosity level.\n"
318-
"\n"
319-
"Returns\n"
320-
"-------\n"
321-
"triangles, neighbors : int arrays, shape (ntri, 3)\n"
322-
" Indices of triangle vertices and indices of triangle neighbors.\n"
323-
},
324-
{"version", version, METH_NOARGS,
325-
"version()\n--\n\n"
326-
"Return the qhull version string."},
327-
{NULL, NULL, 0, NULL}
328-
};
329-
330-
static struct PyModuleDef qhull_module = {
331-
PyModuleDef_HEAD_INIT,
332-
"qhull", "Computing Delaunay triangulations.\n", -1, qhull_methods
333-
};
334-
335-
PyMODINIT_FUNC
336-
PyInit__qhull(void)
337-
{
338-
import_array();
339-
return PyModule_Create(&qhull_module);
279+
PYBIND11_MODULE(_qhull, m) {
280+
m.doc() = "Computing Delaunay triangulations.\n";
281+
282+
m.def("delaunay", &delaunay, "x"_a, "y"_a, "verbose"_a,
283+
"--\n\n"
284+
"Compute a Delaunay triangulation.\n"
285+
"\n"
286+
"Parameters\n"
287+
"----------\n"
288+
"x, y : 1d arrays\n"
289+
" The coordinates of the point set, which must consist of at least\n"
290+
" three unique points.\n"
291+
"verbose : int\n"
292+
" Python's verbosity level.\n"
293+
"\n"
294+
"Returns\n"
295+
"-------\n"
296+
"triangles, neighbors : int arrays, shape (ntri, 3)\n"
297+
" Indices of triangle vertices and indices of triangle neighbors.\n");
298+
299+
m.def("version", []() { return qh_version; },
300+
"version()\n--\n\n"
301+
"Return the qhull version string.");
340302
}

src/meson.build

+1-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ extension_data = {
127127
'sources': files(
128128
'_qhull_wrapper.cpp',
129129
),
130-
'dependencies': [numpy_dep, qhull_dep],
130+
'dependencies': [pybind11_dep, qhull_dep],
131131
'c_args': [f'-DMPL_DEVNULL=@devnull@'],
132132
'cpp_args': [f'-DMPL_DEVNULL=@devnull@'],
133133
},

0 commit comments

Comments
 (0)