From 8381ef92db418ca7f5001ef2563309b0b7ab6e49 Mon Sep 17 00:00:00 2001 From: Ian Thomas Date: Sun, 28 Aug 2022 21:06:07 +0100 Subject: [PATCH 1/9] Use pybind11 for qhull wrapper --- setup.py | 2 + setupext.py | 5 +- src/_qhull_wrapper.cpp | 174 ++++++++++++++++------------------------- 3 files changed, 74 insertions(+), 107 deletions(-) diff --git a/setup.py b/setup.py index 29bf310df93d..26c871dacb0f 100644 --- a/setup.py +++ b/setup.py @@ -302,6 +302,8 @@ def make_release_tree(self, base_dir, files): setup_requires=[ "certifi>=2020.06.20", "numpy>=1.19", + "pybind11>=2.6", + "setuptools>=42", "setuptools_scm>=7", ], install_requires=[ diff --git a/setupext.py b/setupext.py index 69835c12f72c..5f11bb27ee15 100644 --- a/setupext.py +++ b/setupext.py @@ -16,6 +16,7 @@ import textwrap import urllib.request +from pybind11.setup_helpers import Pybind11Extension from setuptools import Distribution, Extension _log = logging.getLogger(__name__) @@ -432,10 +433,10 @@ def get_extensions(self): add_libagg_flags_and_sources(ext) yield ext # qhull - ext = Extension( + ext = Pybind11Extension( "matplotlib._qhull", ["src/_qhull_wrapper.cpp"], + cxx_std=11, define_macros=[("MPL_DEVNULL", os.devnull)]) - add_numpy_flags(ext) Qhull.add_flags(ext) yield ext # tkagg diff --git a/src/_qhull_wrapper.cpp b/src/_qhull_wrapper.cpp index e27c4215b96e..0cd18dd64454 100644 --- a/src/_qhull_wrapper.cpp +++ b/src/_qhull_wrapper.cpp @@ -5,9 +5,9 @@ * triangulation, construct an instance of the matplotlib.tri.Triangulation * class without specifying a triangles array. */ -#define PY_SSIZE_T_CLEAN -#include "Python.h" -#include "numpy_cpp.h" +#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 @@ -16,11 +16,12 @@ extern "C" { extern const char qh_version[]; } #endif + #include "libqhull_r/qhull_ra.h" #include +#include #include - #ifndef MPL_DEVNULL #error "MPL_DEVNULL must be defined as the OS-equivalent of /dev/null" #endif @@ -28,6 +29,15 @@ extern const char qh_version[]; #define STRINGIFY(x) STR(x) #define STR(x) #x +namespace py = pybind11; + +// 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 */ @@ -64,11 +74,11 @@ get_facet_neighbours(const facetT* facet, std::vector& tri_indices, /* Return true if the specified points arrays contain at least 3 unique points, * or false otherwise. */ static bool -at_least_3_unique_points(npy_intp npoints, const double* x, const double* y) +at_least_3_unique_points(py::ssize_t npoints, const double* x, const double* y) { - int i; - const int unique1 = 0; /* First unique point has index 0. */ - int unique2 = 0; /* Second unique point index is 0 until set. */ + py::ssize_t i; + 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; @@ -125,8 +135,8 @@ class QhullInfo { /* 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 PyObject* -delaunay_impl(npy_intp npoints, const double* x, const double* y, +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 */ @@ -179,11 +189,12 @@ delaunay_impl(npy_intp npoints, const double* x, const double* y, exitcode = qh_new_qhull(qh, ndim, (int)npoints, points.data(), False, (char*)"qhull d Qt Qbb Qc Qz", NULL, error_file); if (exitcode != qh_ERRnone) { - PyErr_Format(PyExc_RuntimeError, - "Error in qhull Delaunay triangulation calculation: %s (exitcode=%d)%s", - qhull_error_msg[exitcode], exitcode, - hide_qhull_errors ? "; use python verbose option (-v) to see original qhull error." : ""); - return NULL; + std::ostringstream oss; + oss << "Error in qhull Delaunay triangulation calculation: " << + qhull_error_msg[exitcode] << " (exitcode=" << exitcode << ")"; + if (hide_qhull_errors) + oss << "; use python verbose option (-v) to see original qhull error."; + throw std::runtime_error(oss.str()); } /* 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, std::vector tri_indices(max_facet_id+1); /* Allocate Python arrays to return. */ - npy_intp dims[2] = {ntri, 3}; - numpy::array_view triangles(dims); - int* triangles_ptr = triangles.data(); + int dims[2] = {ntri, 3}; + IndexArray triangles(dims); + int* triangles_ptr = triangles.mutable_data(); - numpy::array_view neighbors(dims); - int* neighbors_ptr = neighbors.data(); + IndexArray neighbors(dims); + int* neighbors_ptr = neighbors.mutable_data(); /* Determine triangles array and set tri_indices array. */ i = 0; @@ -238,103 +249,56 @@ delaunay_impl(npy_intp npoints, const double* x, const double* y, } } - PyObject* tuple = PyTuple_New(2); - if (tuple == 0) { - throw std::runtime_error("Failed to create Python tuple"); - } - - PyTuple_SET_ITEM(tuple, 0, triangles.pyobj()); - PyTuple_SET_ITEM(tuple, 1, neighbors.pyobj()); - return tuple; + return py::make_tuple(triangles, neighbors); } /* Process Python arguments and call Delaunay implementation method. */ -static PyObject* -delaunay(PyObject *self, PyObject *args) +py::tuple +delaunay(const CoordArray& x, const CoordArray& y) { - numpy::array_view xarray; - numpy::array_view yarray; - PyObject* ret; - npy_intp npoints; - const double* x; - const double* y; - - if (!PyArg_ParseTuple(args, "O&O&", - &xarray.converter_contiguous, &xarray, - &yarray.converter_contiguous, &yarray)) { - return NULL; - } + if (x.ndim() != 1 || y.ndim() != 1) + throw std::invalid_argument("x and y must be 1D arrays"); - npoints = xarray.dim(0); - if (npoints != yarray.dim(0)) { - PyErr_SetString(PyExc_ValueError, - "x and y must be 1D arrays of the same length"); - return NULL; - } + auto npoints = x.shape(0); + if (y.shape(0) != npoints) + throw std::invalid_argument("x and y must be 1D arrays of the same length"); - if (npoints < 3) { - PyErr_SetString(PyExc_ValueError, - "x and y arrays must have a length of at least 3"); - return NULL; - } - - x = xarray.data(); - y = yarray.data(); - - if (!at_least_3_unique_points(npoints, x, y)) { - PyErr_SetString(PyExc_ValueError, - "x and y arrays must consist of at least 3 unique points"); - return NULL; - } + if (npoints < 3) + throw std::invalid_argument("x and y arrays must have a length of at least 3"); - CALL_CPP("qhull.delaunay", - (ret = delaunay_impl(npoints, x, y, Py_VerboseFlag == 0))); + 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 ret; + return delaunay_impl(npoints, x.data(), y.data(), Py_VerboseFlag == 0); } /* Return qhull version string for assistance in debugging. */ -static PyObject* -version(PyObject *self, PyObject *arg) +std::string +version() { - return PyBytes_FromString(qh_version); + return std::string(qh_version); } -static PyMethodDef qhull_methods[] = { - {"delaunay", delaunay, METH_VARARGS, - "delaunay(x, y, /)\n" - "--\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" - "\n" - "Returns\n" - "-------\n" - "triangles, neighbors : int arrays, shape (ntri, 3)\n" - " Indices of triangle vertices and indices of triangle neighbors.\n" - }, - {"version", version, METH_NOARGS, - "version()\n--\n\n" - "Return the qhull version string."}, - {NULL, NULL, 0, NULL} -}; - -static struct PyModuleDef qhull_module = { - PyModuleDef_HEAD_INIT, - "qhull", "Computing Delaunay triangulations.\n", -1, qhull_methods -}; - -#pragma GCC visibility push(default) - -PyMODINIT_FUNC -PyInit__qhull(void) -{ - import_array(); - return PyModule_Create(&qhull_module); +PYBIND11_MODULE(_qhull, m) { + m.doc() = "Computing Delaunay triangulations.\n"; + + m.def("delaunay", &delaunay, + "delaunay(x, y, /)\n" + "--\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" + "\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", &version, + "version()\n--\n\n" + "Return the qhull version string."); } - -#pragma GCC visibility pop From f2f597feb1cb4ae20f0d5ec31be77535074329d9 Mon Sep 17 00:00:00 2001 From: Ian Thomas Date: Wed, 31 Aug 2022 20:46:08 +0100 Subject: [PATCH 2/9] Use pybind11 for tri module --- lib/matplotlib/tests/test_triangulation.py | 23 +- lib/matplotlib/tri/triangulation.py | 8 +- setupext.py | 6 +- src/tri/_tri.cpp | 282 +++++----- src/tri/_tri.h | 44 +- src/tri/_tri_wrapper.cpp | 588 +++------------------ 6 files changed, 283 insertions(+), 668 deletions(-) diff --git a/lib/matplotlib/tests/test_triangulation.py b/lib/matplotlib/tests/test_triangulation.py index 365f9a0360cd..f11b553a8abd 100644 --- a/lib/matplotlib/tests/test_triangulation.py +++ b/lib/matplotlib/tests/test_triangulation.py @@ -1167,43 +1167,43 @@ def test_internal_cpp_api(): # C++ Triangulation. with pytest.raises( TypeError, - match=r'function takes exactly 7 arguments \(0 given\)'): + match=r'__init__\(\): incompatible constructor arguments.'): mpl._tri.Triangulation() with pytest.raises( ValueError, match=r'x and y must be 1D arrays of the same length'): - mpl._tri.Triangulation([], [1], [[]], None, None, None, False) + mpl._tri.Triangulation([], [1], [[]], (), (), (), False) x = [0, 1, 1] y = [0, 0, 1] with pytest.raises( ValueError, match=r'triangles must be a 2D array of shape \(\?,3\)'): - mpl._tri.Triangulation(x, y, [[0, 1]], None, None, None, False) + mpl._tri.Triangulation(x, y, [[0, 1]], (), (), (), False) tris = [[0, 1, 2]] with pytest.raises( ValueError, match=r'mask must be a 1D array with the same length as the ' r'triangles array'): - mpl._tri.Triangulation(x, y, tris, [0, 1], None, None, False) + mpl._tri.Triangulation(x, y, tris, [0, 1], (), (), False) with pytest.raises( ValueError, match=r'edges must be a 2D array with shape \(\?,2\)'): - mpl._tri.Triangulation(x, y, tris, None, [[1]], None, False) + mpl._tri.Triangulation(x, y, tris, (), [[1]], (), False) with pytest.raises( ValueError, match=r'neighbors must be a 2D array with the same shape as the ' r'triangles array'): - mpl._tri.Triangulation(x, y, tris, None, None, [[-1]], False) + mpl._tri.Triangulation(x, y, tris, (), (), [[-1]], False) - triang = mpl._tri.Triangulation(x, y, tris, None, None, None, False) + triang = mpl._tri.Triangulation(x, y, tris, (), (), (), False) with pytest.raises( ValueError, - match=r'z array must have same length as triangulation x and y ' - r'array'): + match=r'z must be a 1D array with the same length as the ' + r'triangulation x and y arrays'): triang.calculate_plane_coefficients([]) with pytest.raises( @@ -1215,7 +1215,7 @@ def test_internal_cpp_api(): # C++ TriContourGenerator. with pytest.raises( TypeError, - match=r'function takes exactly 2 arguments \(0 given\)'): + match=r'__init__\(\): incompatible constructor arguments.'): mpl._tri.TriContourGenerator() with pytest.raises( @@ -1233,7 +1233,8 @@ def test_internal_cpp_api(): # C++ TrapezoidMapTriFinder. with pytest.raises( - TypeError, match=r'function takes exactly 1 argument \(0 given\)'): + TypeError, + match=r'__init__\(\): incompatible constructor arguments.'): mpl._tri.TrapezoidMapTriFinder() trifinder = mpl._tri.TrapezoidMapTriFinder(triang) diff --git a/lib/matplotlib/tri/triangulation.py b/lib/matplotlib/tri/triangulation.py index 00d8da4f4d28..be301cc2cc02 100644 --- a/lib/matplotlib/tri/triangulation.py +++ b/lib/matplotlib/tri/triangulation.py @@ -120,8 +120,12 @@ def get_cpp_triangulation(self): from matplotlib import _tri if self._cpp_triangulation is None: self._cpp_triangulation = _tri.Triangulation( - self.x, self.y, self.triangles, self.mask, self._edges, - self._neighbors, not self.is_delaunay) + # For unset arrays use empty tuple which has size of zero. + self.x, self.y, self.triangles, + self.mask if self.mask is not None else (), + self._edges if self._edges is not None else (), + self._neighbors if self._neighbors is not None else (), + not self.is_delaunay) return self._cpp_triangulation def get_masked_triangles(self): diff --git a/setupext.py b/setupext.py index 5f11bb27ee15..68243db0de71 100644 --- a/setupext.py +++ b/setupext.py @@ -453,12 +453,12 @@ def get_extensions(self): add_libagg_flags(ext) yield ext # tri - ext = Extension( + ext = Pybind11Extension( "matplotlib._tri", [ "src/tri/_tri.cpp", "src/tri/_tri_wrapper.cpp", - ]) - add_numpy_flags(ext) + ], + cxx_std=11) yield ext # ttconv ext = Extension( diff --git a/src/tri/_tri.cpp b/src/tri/_tri.cpp index b7a87783de29..512bf7a28eea 100644 --- a/src/tri/_tri.cpp +++ b/src/tri/_tri.cpp @@ -5,8 +5,6 @@ * undef_macros=['NDEBUG'] * to the appropriate make_extension call in setupext.py, and then rebuilding. */ -#define NO_IMPORT_ARRAY - #include "../mplutils.h" #include "_tri.h" @@ -220,7 +218,7 @@ Triangulation::Triangulation(const CoordinateArray& x, const MaskArray& mask, const EdgeArray& edges, const NeighborArray& neighbors, - int correct_triangle_orientations) + bool correct_triangle_orientations) : _x(x), _y(y), _triangles(triangles), @@ -228,6 +226,29 @@ Triangulation::Triangulation(const CoordinateArray& x, _edges(edges), _neighbors(neighbors) { + if (_x.ndim() != 1 || _y.ndim() != 1 || _x.shape(0) != _y.shape(0)) + throw std::invalid_argument("x and y must be 1D arrays of the same length"); + + if (_triangles.ndim() != 2 || _triangles.shape(1) != 3) + throw std::invalid_argument("triangles must be a 2D array of shape (?,3)"); + + // Optional mask. + if (_mask.size() > 0 && + (_mask.ndim() != 1 || _mask.shape(0) != _triangles.shape(0))) + throw std::invalid_argument( + "mask must be a 1D array with the same length as the triangles array"); + + // Optional edges. + if (_edges.size() > 0 && + (_edges.ndim() != 2 || _edges.shape(1) != 2)) + throw std::invalid_argument("edges must be a 2D array with shape (?,2)"); + + // Optional neighbors. + if (_neighbors.size() > 0 && + (_neighbors.ndim() != 2 || _neighbors.shape() != _triangles.shape())) + throw std::invalid_argument( + "neighbors must be a 2D array with the same shape as the triangles array"); + if (correct_triangle_orientations) correct_triangles(); } @@ -290,7 +311,7 @@ void Triangulation::calculate_boundaries() void Triangulation::calculate_edges() { - assert(_edges.empty() && "Expected empty edges array"); + assert(!has_edges() && "Expected empty edges array"); // Create set of all edges, storing them with start point index less than // end point index. @@ -307,29 +328,28 @@ void Triangulation::calculate_edges() } // Convert to python _edges array. - npy_intp dims[2] = {static_cast(edge_set.size()), 2}; + py::ssize_t dims[2] = {static_cast(edge_set.size()), 2}; _edges = EdgeArray(dims); + auto edges = _edges.mutable_unchecked<2>(); int i = 0; for (EdgeSet::const_iterator it = edge_set.begin(); it != edge_set.end(); ++it) { - _edges(i, 0) = it->start; - _edges(i++, 1) = it->end; + edges(i, 0) = it->start; + edges(i++, 1) = it->end; } } void Triangulation::calculate_neighbors() { - assert(_neighbors.empty() && "Expected empty neighbors array"); + assert(!has_neighbors() && "Expected empty neighbors array"); // Create _neighbors array with shape (ntri,3) and initialise all to -1. - npy_intp dims[2] = {get_ntri(), 3}; + py::ssize_t dims[2] = {get_ntri(), 3}; _neighbors = NeighborArray(dims); + auto* neighbors = _neighbors.mutable_data(); int tri, edge; - for (tri = 0; tri < get_ntri(); ++tri) { - for (edge = 0; edge < 3; ++edge) - _neighbors(tri, edge) = -1; - } + std::fill(neighbors, neighbors+3*get_ntri(), -1); // For each triangle edge (start to end point), find corresponding neighbor // edge from end to start point. Do this by traversing all edges and @@ -352,8 +372,8 @@ void Triangulation::calculate_neighbors() } else { // Neighbor edge found, set the two elements of _neighbors // and remove edge from edge_to_tri_edge_map. - _neighbors(tri, edge)= it->second.tri; - _neighbors(it->second.tri, it->second.edge) = tri; + neighbors[3*tri + edge] = it->second.tri; + neighbors[3*it->second.tri + it->second.edge] = tri; edge_to_tri_edge_map.erase(it); } } @@ -367,8 +387,17 @@ void Triangulation::calculate_neighbors() Triangulation::TwoCoordinateArray Triangulation::calculate_plane_coefficients( const CoordinateArray& z) { - npy_intp dims[2] = {get_ntri(), 3}; - Triangulation::TwoCoordinateArray planes(dims); + if (z.ndim() != 1 || z.shape(0) != _x.shape(0)) + throw std::invalid_argument( + "z must be a 1D array with the same length as the triangulation x and y arrays"); + + int dims[2] = {get_ntri(), 3}; + Triangulation::TwoCoordinateArray planes_array(dims); + auto planes = planes_array.mutable_unchecked<2>(); + auto triangles = _triangles.unchecked<2>(); + auto x = _x.unchecked<1>(); + auto y = _y.unchecked<1>(); + auto z_ptr = z.unchecked<1>(); int point; for (int tri = 0; tri < get_ntri(); ++tri) { @@ -385,12 +414,12 @@ Triangulation::TwoCoordinateArray Triangulation::calculate_plane_coefficients( // and rearrange to give // r_z = (-normal_x/normal_z)*r_x + (-normal_y/normal_z)*r_y + // p/normal_z - point = _triangles(tri, 0); - XYZ point0(_x(point), _y(point), z(point)); - point = _triangles(tri, 1); - XYZ side01 = XYZ(_x(point), _y(point), z(point)) - point0; - point = _triangles(tri, 2); - XYZ side02 = XYZ(_x(point), _y(point), z(point)) - point0; + point = triangles(tri, 0); + XYZ point0(x(point), y(point), z_ptr(point)); + point = triangles(tri, 1); + XYZ side01 = XYZ(x(point), y(point), z_ptr(point)) - point0; + point = triangles(tri, 2); + XYZ side02 = XYZ(x(point), y(point), z_ptr(point)) - point0; XYZ normal = side01.cross(side02); @@ -414,20 +443,23 @@ Triangulation::TwoCoordinateArray Triangulation::calculate_plane_coefficients( } } - return planes; + return planes_array; } void Triangulation::correct_triangles() { + auto triangles = _triangles.mutable_data(); + auto neighbors = _neighbors.mutable_data(); + for (int tri = 0; tri < get_ntri(); ++tri) { - XY point0 = get_point_coords(_triangles(tri, 0)); - XY point1 = get_point_coords(_triangles(tri, 1)); - XY point2 = get_point_coords(_triangles(tri, 2)); + XY point0 = get_point_coords(triangles[3*tri]); + XY point1 = get_point_coords(triangles[3*tri+1]); + XY point2 = get_point_coords(triangles[3*tri+2]); if ( (point1 - point0).cross_z(point2 - point0) < 0.0) { // Triangle points are clockwise, so change them to anticlockwise. - std::swap(_triangles(tri, 1), _triangles(tri, 2)); - if (!_neighbors.empty()) - std::swap(_neighbors(tri, 1), _neighbors(tri, 2)); + std::swap(triangles[3*tri+1], triangles[3*tri+2]); + if (has_neighbors()) + std::swap(neighbors[3*tri+1], neighbors[3*tri+2]); } } } @@ -456,8 +488,11 @@ int Triangulation::get_edge_in_triangle(int tri, int point) const { assert(tri >= 0 && tri < get_ntri() && "Triangle index out of bounds"); assert(point >= 0 && point < get_npoints() && "Point index out of bounds."); + + auto triangles = _triangles.data(); + for (int edge = 0; edge < 3; ++edge) { - if (_triangles(tri, edge) == point) + if (triangles[3*tri + edge] == point) return edge; } return -1; // point is not in triangle. @@ -465,7 +500,7 @@ int Triangulation::get_edge_in_triangle(int tri, int point) const Triangulation::EdgeArray& Triangulation::get_edges() { - if (_edges.empty()) + if (!has_edges()) calculate_edges(); return _edges; } @@ -474,9 +509,9 @@ int Triangulation::get_neighbor(int tri, int edge) const { assert(tri >= 0 && tri < get_ntri() && "Triangle index out of bounds"); assert(edge >= 0 && edge < 3 && "Edge index out of bounds"); - if (_neighbors.empty()) + if (!has_neighbors()) const_cast(*this).calculate_neighbors(); - return _neighbors(tri, edge); + return _neighbors.data()[3*tri + edge]; } TriEdge Triangulation::get_neighbor_edge(int tri, int edge) const @@ -493,32 +528,32 @@ TriEdge Triangulation::get_neighbor_edge(int tri, int edge) const Triangulation::NeighborArray& Triangulation::get_neighbors() { - if (_neighbors.empty()) + if (!has_neighbors()) calculate_neighbors(); return _neighbors; } int Triangulation::get_npoints() const { - return _x.size(); + return _x.shape(0); } int Triangulation::get_ntri() const { - return _triangles.size(); + return _triangles.shape(0); } XY Triangulation::get_point_coords(int point) const { assert(point >= 0 && point < get_npoints() && "Point index out of bounds."); - return XY(_x(point), _y(point)); + return XY(_x.data()[point], _y.data()[point]); } int Triangulation::get_triangle_point(int tri, int edge) const { assert(tri >= 0 && tri < get_ntri() && "Triangle index out of bounds"); assert(edge >= 0 && edge < 3 && "Edge index out of bounds"); - return _triangles(tri, edge); + return _triangles.data()[3*tri + edge]; } int Triangulation::get_triangle_point(const TriEdge& tri_edge) const @@ -526,15 +561,34 @@ int Triangulation::get_triangle_point(const TriEdge& tri_edge) const return get_triangle_point(tri_edge.tri, tri_edge.edge); } +bool Triangulation::has_edges() const +{ + return _edges.size() > 0; +} + +bool Triangulation::has_mask() const +{ + return _mask.size() > 0; +} + +bool Triangulation::has_neighbors() const +{ + return _neighbors.size() > 0; +} + bool Triangulation::is_masked(int tri) const { assert(tri >= 0 && tri < get_ntri() && "Triangle index out of bounds."); - const npy_bool* mask_ptr = reinterpret_cast(_mask.data()); - return !_mask.empty() && mask_ptr[tri]; + return has_mask() && _mask.data()[tri]; } void Triangulation::set_mask(const MaskArray& mask) { + if (mask.size() > 0 && + (mask.ndim() != 1 || mask.shape(0) != _triangles.shape(0))) + throw std::invalid_argument( + "mask must be a 1D array with the same length as the triangles array"); + _mask = mask; // Clear derived fields so they are recalculated when needed. @@ -566,7 +620,11 @@ TriContourGenerator::TriContourGenerator(Triangulation& triangulation, _interior_visited(2*_triangulation.get_ntri()), _boundaries_visited(0), _boundaries_used(0) -{} +{ + if (_z.ndim() != 1 || _z.shape(0) != _triangulation.get_npoints()) + throw std::invalid_argument( + "z must be a 1D array with the same length as the x and y arrays"); +} void TriContourGenerator::clear_visited_flags(bool include_boundaries) { @@ -597,7 +655,7 @@ void TriContourGenerator::clear_visited_flags(bool include_boundaries) } } -PyObject* TriContourGenerator::contour_line_to_segs_and_kinds(const Contour& contour) +py::tuple TriContourGenerator::contour_line_to_segs_and_kinds(const Contour& contour) { // Convert all of the lines generated by a call to create_contour() into // their Python equivalents for return to the calling function. @@ -611,27 +669,20 @@ PyObject* TriContourGenerator::contour_line_to_segs_and_kinds(const Contour& con // and they are appended to the Python lists vertices_list and codes_list // respectively for return to the Python calling function. - PyObject* vertices_list = PyList_New(contour.size()); - if (vertices_list == 0) - throw std::runtime_error("Failed to create Python list"); - - PyObject* codes_list = PyList_New(contour.size()); - if (codes_list == 0) { - Py_XDECREF(vertices_list); - throw std::runtime_error("Failed to create Python list"); - } + py::list vertices_list(contour.size()); + py::list codes_list(contour.size()); for (Contour::size_type i = 0; i < contour.size(); ++i) { const ContourLine& contour_line = contour[i]; - npy_intp npoints = static_cast(contour_line.size()); + py::ssize_t npoints = static_cast(contour_line.size()); - npy_intp segs_dims[2] = {npoints, 2}; - numpy::array_view segs(segs_dims); - double* segs_ptr = segs.data(); + py::ssize_t segs_dims[2] = {npoints, 2}; + CoordinateArray segs(segs_dims); + double* segs_ptr = segs.mutable_data(); - npy_intp codes_dims[1] = {npoints}; - numpy::array_view codes(codes_dims); - unsigned char* codes_ptr = codes.data(); + py::ssize_t codes_dims[1] = {npoints}; + CodeArray codes(codes_dims); + unsigned char* codes_ptr = codes.mutable_data(); for (ContourLine::const_iterator it = contour_line.begin(); it != contour_line.end(); ++it) { @@ -645,25 +696,14 @@ PyObject* TriContourGenerator::contour_line_to_segs_and_kinds(const Contour& con contour_line.front() == contour_line.back()) *(codes_ptr-1) = CLOSEPOLY; - PyList_SET_ITEM(vertices_list, i, segs.pyobj()); - PyList_SET_ITEM(codes_list, i, codes.pyobj()); - } - - PyObject* result = PyTuple_New(2); - if (result == 0) { - Py_XDECREF(vertices_list); - Py_XDECREF(codes_list); - throw std::runtime_error("Failed to create Python tuple"); + vertices_list[i] = segs; + codes_list[i] = codes; } - // No error checking here as filling in a brand new pre-allocated result. - PyTuple_SET_ITEM(result, 0, vertices_list); - PyTuple_SET_ITEM(result, 1, codes_list); - - return result; + return py::make_tuple(vertices_list, codes_list); } -PyObject* TriContourGenerator::contour_to_segs_and_kinds(const Contour& contour) +py::tuple TriContourGenerator::contour_to_segs_and_kinds(const Contour& contour) { // Convert all of the polygons generated by a call to // create_filled_contour() into their Python equivalents for return to the @@ -683,19 +723,19 @@ PyObject* TriContourGenerator::contour_to_segs_and_kinds(const Contour& contour) ContourLine::const_iterator point; // Find total number of points in all contour lines. - npy_intp n_points = 0; + py::ssize_t n_points = 0; for (line = contour.begin(); line != contour.end(); ++line) - n_points += (npy_intp)line->size(); + n_points += static_cast(line->size()); // Create segs array for point coordinates. - npy_intp segs_dims[2] = {n_points, 2}; - numpy::array_view segs(segs_dims); - double* segs_ptr = segs.data(); + py::ssize_t segs_dims[2] = {n_points, 2}; + TwoCoordinateArray segs(segs_dims); + double* segs_ptr = segs.mutable_data(); // Create kinds array for code types. - npy_intp codes_dims[1] = {n_points}; - numpy::array_view codes(codes_dims); - unsigned char* codes_ptr = codes.data(); + py::ssize_t codes_dims[1] = {n_points}; + CodeArray codes(codes_dims); + unsigned char* codes_ptr = codes.mutable_data(); for (line = contour.begin(); line != contour.end(); ++line) { for (point = line->begin(); point != line->end(); point++) { @@ -705,38 +745,16 @@ PyObject* TriContourGenerator::contour_to_segs_and_kinds(const Contour& contour) } } - PyObject* vertices_list = PyList_New(0); - if (vertices_list == 0) - throw std::runtime_error("Failed to create Python list"); + py::list vertices_list(1); + vertices_list[0] = segs; - PyObject* codes_list = PyList_New(0); - if (codes_list == 0) { - Py_XDECREF(vertices_list); - throw std::runtime_error("Failed to create Python list"); - } + py::list codes_list(1); + codes_list[0] = codes; - if (PyList_Append(vertices_list, segs.pyobj_steal()) || - PyList_Append(codes_list, codes.pyobj_steal())) { - Py_XDECREF(vertices_list); - Py_XDECREF(codes_list); - throw std::runtime_error("Unable to add contour to vertices and codes lists"); - } - - PyObject* result = PyTuple_New(2); - if (result == 0) { - Py_XDECREF(vertices_list); - Py_XDECREF(codes_list); - throw std::runtime_error("Failed to create Python tuple"); - } - - // No error checking here as filling in a brand new pre-allocated tuple. - PyTuple_SET_ITEM(result, 0, vertices_list); - PyTuple_SET_ITEM(result, 1, codes_list); - - return result; + return py::make_tuple(vertices_list, codes_list); } -PyObject* TriContourGenerator::create_contour(const double& level) +py::tuple TriContourGenerator::create_contour(const double& level) { clear_visited_flags(false); Contour contour; @@ -747,9 +765,12 @@ PyObject* TriContourGenerator::create_contour(const double& level) return contour_line_to_segs_and_kinds(contour); } -PyObject* TriContourGenerator::create_filled_contour(const double& lower_level, +py::tuple TriContourGenerator::create_filled_contour(const double& lower_level, const double& upper_level) { + if (lower_level >= upper_level) + throw std::invalid_argument("filled contour levels must be increasing"); + clear_visited_flags(true); Contour contour; @@ -1046,7 +1067,7 @@ const double& TriContourGenerator::get_z(int point) const { assert(point >= 0 && point < _triangulation.get_npoints() && "Point index out of bounds."); - return _z(point); + return _z.data()[point]; } XY TriContourGenerator::interp(int point1, @@ -1307,16 +1328,22 @@ TrapezoidMapTriFinder::TriIndexArray TrapezoidMapTriFinder::find_many(const CoordinateArray& x, const CoordinateArray& y) { + if (x.ndim() != 1 || x.shape(0) != y.shape(0)) + throw std::invalid_argument( + "x and y must be array-like with same shape"); + // Create integer array to return. - npy_intp n = x.dim(0); - npy_intp dims[1] = {n}; - TriIndexArray tri_indices(dims); + auto n = x.shape(0); + TriIndexArray tri_indices_array(n); + auto tri_indices = tri_indices_array.mutable_unchecked<1>(); + auto x_data = x.data(); + auto y_data = y.data(); // Fill returned array. - for (npy_intp i = 0; i < n; ++i) - tri_indices(i) = find_one(XY(x(i), y(i))); + for (py::ssize_t i = 0; i < n; ++i) + tri_indices(i) = find_one(XY(x_data[i], y_data[i])); - return tri_indices; + return tri_indices_array; } int @@ -1370,20 +1397,21 @@ TrapezoidMapTriFinder::find_trapezoids_intersecting_edge( return true; } -PyObject* +py::list TrapezoidMapTriFinder::get_tree_stats() { NodeStats stats; _tree->get_stats(0, stats); - return Py_BuildValue("[l,l,l,l,l,l,d]", - stats.node_count, - stats.unique_nodes.size(), - stats.trapezoid_count, - stats.unique_trapezoid_nodes.size(), - stats.max_parent_count, - stats.max_depth, - stats.sum_trapezoid_depth / stats.trapezoid_count); + py::list ret(7); + ret[0] = stats.node_count; + ret[1] = stats.unique_nodes.size(), + ret[2] = stats.trapezoid_count, + ret[3] = stats.unique_trapezoid_nodes.size(), + ret[4] = stats.max_parent_count, + ret[5] = stats.max_depth, + ret[6] = stats.sum_trapezoid_depth / stats.trapezoid_count; + return ret; } void diff --git a/src/tri/_tri.h b/src/tri/_tri.h index 28c8e07933cc..ab96fd8d8cb3 100644 --- a/src/tri/_tri.h +++ b/src/tri/_tri.h @@ -63,7 +63,8 @@ #ifndef MPL_TRI_H #define MPL_TRI_H -#include "../numpy_cpp.h" +#include +#include #include #include @@ -71,6 +72,7 @@ #include #include +namespace py = pybind11; /* An edge of a triangle consisting of an triangle index in the range 0 to @@ -161,12 +163,12 @@ void write_contour(const Contour& contour); class Triangulation { public: - typedef numpy::array_view CoordinateArray; - typedef numpy::array_view TwoCoordinateArray; - typedef numpy::array_view TriangleArray; - typedef numpy::array_view MaskArray; - typedef numpy::array_view EdgeArray; - typedef numpy::array_view NeighborArray; + typedef py::array_t CoordinateArray; + typedef py::array_t TwoCoordinateArray; + typedef py::array_t TriangleArray; + typedef py::array_t MaskArray; + typedef py::array_t EdgeArray; + typedef py::array_t NeighborArray; /* A single boundary is a vector of the TriEdges that make up that boundary * following it around with unmasked triangles on the left. */ @@ -196,7 +198,7 @@ class Triangulation const MaskArray& mask, const EdgeArray& edges, const NeighborArray& neighbors, - int correct_triangle_orientations); + bool correct_triangle_orientations); /* Calculate plane equation coefficients for all unmasked triangles from * the point (x,y) coordinates and point z-array of shape (npoints) passed @@ -296,7 +298,11 @@ class Triangulation * the specified triangle, or -1 if the point is not in the triangle. */ int get_edge_in_triangle(int tri, int point) const; + bool has_edges() const; + bool has_mask() const; + + bool has_neighbors() const; // Variables shared with python, always set. @@ -304,11 +310,11 @@ class Triangulation TriangleArray _triangles; // int array (ntri,3) of triangle point indices, // ordered anticlockwise. - // Variables shared with python, may be zero. + // Variables shared with python, may be unset (size == 0). MaskArray _mask; // bool array (ntri). - // Derived variables shared with python, may be zero. If zero, are - // recalculated when needed. + // Derived variables shared with python, may be unset (size == 0). + // If unset, are recalculated when needed. EdgeArray _edges; // int array (?,2) of start & end point indices. NeighborArray _neighbors; // int array (ntri,3), neighbor triangle indices // or -1 if no neighbor. @@ -329,6 +335,8 @@ class TriContourGenerator { public: typedef Triangulation::CoordinateArray CoordinateArray; + typedef Triangulation::TwoCoordinateArray TwoCoordinateArray; + typedef py::array_t CodeArray; /* Constructor. * triangulation: Triangulation to generate contours for. @@ -342,7 +350,7 @@ class TriContourGenerator * Returns new python list [segs0, segs1, ...] where * segs0: double array of shape (?,2) of point coordinates of first * contour line, etc. */ - PyObject* create_contour(const double& level); + py::tuple create_contour(const double& level); /* Create and return a filled contour. * lower_level: Lower contour level. @@ -350,7 +358,7 @@ class TriContourGenerator * Returns new python tuple (segs, kinds) where * segs: double array of shape (n_points,2) of all point coordinates, * kinds: ubyte array of shape (n_points) of all point code types. */ - PyObject* create_filled_contour(const double& lower_level, + py::tuple create_filled_contour(const double& lower_level, const double& upper_level); private: @@ -369,13 +377,13 @@ class TriContourGenerator * contour line, etc. * kinds0: ubyte array of shape (n_points) of kinds codes of first contour * line, etc. */ - PyObject* contour_line_to_segs_and_kinds(const Contour& contour); + py::tuple contour_line_to_segs_and_kinds(const Contour& contour); /* Convert a filled Contour from C++ to Python. * Returns new python tuple ([segs], [kinds]) where * segs: double array of shape (n_points,2) of all point coordinates, * kinds: ubyte array of shape (n_points) of all point code types. */ - PyObject* contour_to_segs_and_kinds(const Contour& contour); + py::tuple contour_to_segs_and_kinds(const Contour& contour); /* Return the point on the specified TriEdge that intersects the specified * level. */ @@ -460,7 +468,7 @@ class TriContourGenerator // Variables shared with python, always set. - Triangulation& _triangulation; + Triangulation _triangulation; CoordinateArray _z; // double array (npoints). // Variables internal to C++ only. @@ -507,7 +515,7 @@ class TrapezoidMapTriFinder { public: typedef Triangulation::CoordinateArray CoordinateArray; - typedef numpy::array_view TriIndexArray; + typedef py::array_t TriIndexArray; /* Constructor. A separate call to initialize() is required to initialize * the object before use. @@ -533,7 +541,7 @@ class TrapezoidMapTriFinder * comparisons needed to search through the tree) * 6: mean of all trapezoid depths (one more than the average number of * comparisons needed to search through the tree) */ - PyObject* get_tree_stats(); + py::list get_tree_stats(); /* Initialize this object before use. May be called multiple times, if, * for example, the triangulation is changed by setting the mask. */ diff --git a/src/tri/_tri_wrapper.cpp b/src/tri/_tri_wrapper.cpp index bf58ca02879f..945a3bed45cf 100644 --- a/src/tri/_tri_wrapper.cpp +++ b/src/tri/_tri_wrapper.cpp @@ -1,510 +1,84 @@ #include "_tri.h" -#include "../mplutils.h" -#include "../py_exceptions.h" - -/* Triangulation */ - -typedef struct -{ - PyObject_HEAD - Triangulation* ptr; -} PyTriangulation; - -static PyTypeObject PyTriangulationType; - -static PyObject* PyTriangulation_new(PyTypeObject* type, PyObject* args, PyObject* kwds) -{ - PyTriangulation* self; - self = (PyTriangulation*)type->tp_alloc(type, 0); - self->ptr = NULL; - return (PyObject*)self; -} - -const char* PyTriangulation_init__doc__ = - "Triangulation(x, y, triangles, mask, edges, neighbors)\n" - "--\n\n" - "Create a new C++ Triangulation object.\n" - "This should not be called directly, instead use the python class\n" - "matplotlib.tri.Triangulation instead.\n"; - -static int PyTriangulation_init(PyTriangulation* self, PyObject* args, PyObject* kwds) -{ - Triangulation::CoordinateArray x, y; - Triangulation::TriangleArray triangles; - Triangulation::MaskArray mask; - Triangulation::EdgeArray edges; - Triangulation::NeighborArray neighbors; - int correct_triangle_orientations; - - if (!PyArg_ParseTuple(args, - "O&O&O&O&O&O&i", - &x.converter, &x, - &y.converter, &y, - &triangles.converter, &triangles, - &mask.converter, &mask, - &edges.converter, &edges, - &neighbors.converter, &neighbors, - &correct_triangle_orientations)) { - return -1; - } - - // x and y. - if (x.empty() || y.empty() || x.dim(0) != y.dim(0)) { - PyErr_SetString(PyExc_ValueError, - "x and y must be 1D arrays of the same length"); - return -1; - } - - // triangles. - if (triangles.empty() || triangles.dim(1) != 3) { - PyErr_SetString(PyExc_ValueError, - "triangles must be a 2D array of shape (?,3)"); - return -1; - } - - // Optional mask. - if (!mask.empty() && mask.dim(0) != triangles.dim(0)) { - PyErr_SetString(PyExc_ValueError, - "mask must be a 1D array with the same length as the triangles array"); - return -1; - } - - // Optional edges. - if (!edges.empty() && edges.dim(1) != 2) { - PyErr_SetString(PyExc_ValueError, - "edges must be a 2D array with shape (?,2)"); - return -1; - } - - // Optional neighbors. - if (!neighbors.empty() && (neighbors.dim(0) != triangles.dim(0) || - neighbors.dim(1) != triangles.dim(1))) { - PyErr_SetString(PyExc_ValueError, - "neighbors must be a 2D array with the same shape as the triangles array"); - return -1; - } - - CALL_CPP_INIT("Triangulation", - (self->ptr = new Triangulation(x, y, triangles, mask, - edges, neighbors, - correct_triangle_orientations))); - return 0; -} - -static void PyTriangulation_dealloc(PyTriangulation* self) -{ - delete self->ptr; - Py_TYPE(self)->tp_free((PyObject*)self); -} - -const char* PyTriangulation_calculate_plane_coefficients__doc__ = - "calculate_plane_coefficients(self, z, plane_coefficients)\n" - "--\n\n" - "Calculate plane equation coefficients for all unmasked triangles."; - -static PyObject* PyTriangulation_calculate_plane_coefficients(PyTriangulation* self, PyObject* args) -{ - Triangulation::CoordinateArray z; - if (!PyArg_ParseTuple(args, "O&:calculate_plane_coefficients", - &z.converter, &z)) { - return NULL; - } - - if (z.empty() || z.dim(0) != self->ptr->get_npoints()) { - PyErr_SetString(PyExc_ValueError, - "z array must have same length as triangulation x and y arrays"); - return NULL; - } - - Triangulation::TwoCoordinateArray result; - CALL_CPP("calculate_plane_coefficients", - (result = self->ptr->calculate_plane_coefficients(z))); - return result.pyobj(); -} - -const char* PyTriangulation_get_edges__doc__ = - "get_edges(self)\n" - "--\n\n" - "Return edges array."; - -static PyObject* PyTriangulation_get_edges(PyTriangulation* self, PyObject* args) -{ - Triangulation::EdgeArray* result; - CALL_CPP("get_edges", (result = &self->ptr->get_edges())); - - if (result->empty()) { - Py_RETURN_NONE; - } - else - return result->pyobj(); -} - -const char* PyTriangulation_get_neighbors__doc__ = - "get_neighbors(self)\n" - "--\n\n" - "Return neighbors array."; - -static PyObject* PyTriangulation_get_neighbors(PyTriangulation* self, PyObject* args) -{ - Triangulation::NeighborArray* result; - CALL_CPP("get_neighbors", (result = &self->ptr->get_neighbors())); - - if (result->empty()) { - Py_RETURN_NONE; - } - else - return result->pyobj(); -} - -const char* PyTriangulation_set_mask__doc__ = - "set_mask(self, mask)\n" - "--\n\n" - "Set or clear the mask array."; - -static PyObject* PyTriangulation_set_mask(PyTriangulation* self, PyObject* args) -{ - Triangulation::MaskArray mask; - - if (!PyArg_ParseTuple(args, "O&:set_mask", &mask.converter, &mask)) { - return NULL; - } - - if (!mask.empty() && mask.dim(0) != self->ptr->get_ntri()) { - PyErr_SetString(PyExc_ValueError, - "mask must be a 1D array with the same length as the triangles array"); - return NULL; - } - - CALL_CPP("set_mask", (self->ptr->set_mask(mask))); - Py_RETURN_NONE; -} - -static PyTypeObject *PyTriangulation_init_type() -{ - static PyMethodDef methods[] = { - {"calculate_plane_coefficients", - (PyCFunction)PyTriangulation_calculate_plane_coefficients, - METH_VARARGS, - PyTriangulation_calculate_plane_coefficients__doc__}, - {"get_edges", - (PyCFunction)PyTriangulation_get_edges, - METH_NOARGS, - PyTriangulation_get_edges__doc__}, - {"get_neighbors", - (PyCFunction)PyTriangulation_get_neighbors, - METH_NOARGS, - PyTriangulation_get_neighbors__doc__}, - {"set_mask", - (PyCFunction)PyTriangulation_set_mask, - METH_VARARGS, - PyTriangulation_set_mask__doc__}, - {NULL} - }; - PyTriangulationType.tp_name = "matplotlib._tri.Triangulation"; - PyTriangulationType.tp_doc = PyTriangulation_init__doc__; - PyTriangulationType.tp_basicsize = sizeof(PyTriangulation); - PyTriangulationType.tp_dealloc = (destructor)PyTriangulation_dealloc; - PyTriangulationType.tp_flags = Py_TPFLAGS_DEFAULT; - PyTriangulationType.tp_methods = methods; - PyTriangulationType.tp_new = PyTriangulation_new; - PyTriangulationType.tp_init = (initproc)PyTriangulation_init; - - return &PyTriangulationType; -} - - -/* TriContourGenerator */ - -typedef struct -{ - PyObject_HEAD - TriContourGenerator* ptr; - PyTriangulation* py_triangulation; -} PyTriContourGenerator; - -static PyTypeObject PyTriContourGeneratorType; - -static PyObject* PyTriContourGenerator_new(PyTypeObject* type, PyObject* args, PyObject* kwds) -{ - PyTriContourGenerator* self; - self = (PyTriContourGenerator*)type->tp_alloc(type, 0); - self->ptr = NULL; - self->py_triangulation = NULL; - return (PyObject*)self; -} - -const char* PyTriContourGenerator_init__doc__ = - "TriContourGenerator(triangulation, z)\n" - "--\n\n" - "Create a new C++ TriContourGenerator object.\n" - "This should not be called directly, instead use the functions\n" - "matplotlib.axes.tricontour and tricontourf instead.\n"; - -static int PyTriContourGenerator_init(PyTriContourGenerator* self, PyObject* args, PyObject* kwds) -{ - PyObject* triangulation_arg; - TriContourGenerator::CoordinateArray z; - - if (!PyArg_ParseTuple(args, "O!O&", - &PyTriangulationType, &triangulation_arg, - &z.converter, &z)) { - return -1; - } - - PyTriangulation* py_triangulation = (PyTriangulation*)triangulation_arg; - Py_INCREF(py_triangulation); - self->py_triangulation = py_triangulation; - Triangulation& triangulation = *(py_triangulation->ptr); - - if (z.empty() || z.dim(0) != triangulation.get_npoints()) { - PyErr_SetString(PyExc_ValueError, - "z must be a 1D array with the same length as the x and y arrays"); - return -1; - } - - CALL_CPP_INIT("TriContourGenerator", - (self->ptr = new TriContourGenerator(triangulation, z))); - return 0; -} - -static void PyTriContourGenerator_dealloc(PyTriContourGenerator* self) -{ - delete self->ptr; - Py_XDECREF(self->py_triangulation); - Py_TYPE(self)->tp_free((PyObject *)self); -} - -const char* PyTriContourGenerator_create_contour__doc__ = - "create_contour(self, level)\n" - "--\n\n" - "Create and return a non-filled contour."; - -static PyObject* PyTriContourGenerator_create_contour(PyTriContourGenerator* self, PyObject* args) -{ - double level; - if (!PyArg_ParseTuple(args, "d:create_contour", &level)) { - return NULL; - } - - PyObject* result; - CALL_CPP("create_contour", (result = self->ptr->create_contour(level))); - return result; -} - -const char* PyTriContourGenerator_create_filled_contour__doc__ = - "create_filled_contour(self, lower_level, upper_level)\n" - "--\n\n" - "Create and return a filled contour."; - -static PyObject* PyTriContourGenerator_create_filled_contour(PyTriContourGenerator* self, PyObject* args) -{ - double lower_level, upper_level; - if (!PyArg_ParseTuple(args, "dd:create_filled_contour", - &lower_level, &upper_level)) { - return NULL; - } - - if (lower_level >= upper_level) - { - PyErr_SetString(PyExc_ValueError, - "filled contour levels must be increasing"); - return NULL; - } - - PyObject* result; - CALL_CPP("create_filled_contour", - (result = self->ptr->create_filled_contour(lower_level, - upper_level))); - return result; -} - -static PyTypeObject *PyTriContourGenerator_init_type() -{ - static PyMethodDef methods[] = { - {"create_contour", - (PyCFunction)PyTriContourGenerator_create_contour, - METH_VARARGS, - PyTriContourGenerator_create_contour__doc__}, - {"create_filled_contour", - (PyCFunction)PyTriContourGenerator_create_filled_contour, - METH_VARARGS, - PyTriContourGenerator_create_filled_contour__doc__}, - {NULL} - }; - PyTriContourGeneratorType.tp_name = "matplotlib._tri.TriContourGenerator"; - PyTriContourGeneratorType.tp_doc = PyTriContourGenerator_init__doc__; - PyTriContourGeneratorType.tp_basicsize = sizeof(PyTriContourGenerator); - PyTriContourGeneratorType.tp_dealloc = (destructor)PyTriContourGenerator_dealloc; - PyTriContourGeneratorType.tp_flags = Py_TPFLAGS_DEFAULT; - PyTriContourGeneratorType.tp_methods = methods; - PyTriContourGeneratorType.tp_new = PyTriContourGenerator_new; - PyTriContourGeneratorType.tp_init = (initproc)PyTriContourGenerator_init; - - return &PyTriContourGeneratorType; -} - - -/* TrapezoidMapTriFinder */ - -typedef struct -{ - PyObject_HEAD - TrapezoidMapTriFinder* ptr; - PyTriangulation* py_triangulation; -} PyTrapezoidMapTriFinder; - -static PyTypeObject PyTrapezoidMapTriFinderType; - -static PyObject* PyTrapezoidMapTriFinder_new(PyTypeObject* type, PyObject* args, PyObject* kwds) -{ - PyTrapezoidMapTriFinder* self; - self = (PyTrapezoidMapTriFinder*)type->tp_alloc(type, 0); - self->ptr = NULL; - self->py_triangulation = NULL; - return (PyObject*)self; -} - -const char* PyTrapezoidMapTriFinder_init__doc__ = - "TrapezoidMapTriFinder(triangulation)\n" - "--\n\n" - "Create a new C++ TrapezoidMapTriFinder object.\n" - "This should not be called directly, instead use the python class\n" - "matplotlib.tri.TrapezoidMapTriFinder instead.\n"; - -static int PyTrapezoidMapTriFinder_init(PyTrapezoidMapTriFinder* self, PyObject* args, PyObject* kwds) -{ - PyObject* triangulation_arg; - if (!PyArg_ParseTuple(args, "O!", - &PyTriangulationType, &triangulation_arg)) { - return -1; - } - - PyTriangulation* py_triangulation = (PyTriangulation*)triangulation_arg; - Py_INCREF(py_triangulation); - self->py_triangulation = py_triangulation; - Triangulation& triangulation = *(py_triangulation->ptr); - - CALL_CPP_INIT("TrapezoidMapTriFinder", - (self->ptr = new TrapezoidMapTriFinder(triangulation))); - return 0; -} - -static void PyTrapezoidMapTriFinder_dealloc(PyTrapezoidMapTriFinder* self) -{ - delete self->ptr; - Py_XDECREF(self->py_triangulation); - Py_TYPE(self)->tp_free((PyObject *)self); +PYBIND11_MODULE(_tri, m) { + py::class_(m, "Triangulation", + .def(py::init(), + py::arg("x"), + py::arg("y"), + py::arg("triangles"), + py::arg("mask"), + py::arg("edges"), + py::arg("neighbors"), + py::arg("correct_triangle_orientations"), + "Triangulation(x, y, triangles, mask, edges, neighbors)\n" + "--\n\n" + "Create a new C++ Triangulation object.\n" + "This should not be called directly, instead use the python class\n" + "matplotlib.tri.Triangulation instead.\n") + .def("calculate_plane_coefficients", &Triangulation::calculate_plane_coefficients, + "calculate_plane_coefficients(self, z, plane_coefficients)\n" + "--\n\n" + "Calculate plane equation coefficients for all unmasked triangles.") + .def("get_edges", &Triangulation::get_edges, + "get_edges(self)\n" + "--\n\n" + "Return edges array.") + .def("get_neighbors", &Triangulation::get_neighbors, + "get_neighbors(self)\n" + "--\n\n" + "Return neighbors array.") + .def("set_mask", &Triangulation::set_mask, + "set_mask(self, mask)\n" + "--\n\n" + "Set or clear the mask array."); + + py::class_(m, "TriContourGenerator", + .def(py::init(), + py::arg("triangulation"), + py::arg("z"), + "TriContourGenerator(triangulation, z)\n" + "--\n\n" + "Create a new C++ TriContourGenerator object.\n" + "This should not be called directly, instead use the functions\n" + "matplotlib.axes.tricontour and tricontourf instead.\n") + .def("create_contour", &TriContourGenerator::create_contour, + "create_contour(self, level)\n" + "--\n\n" + "Create and return a non-filled contour.") + .def("create_filled_contour", &TriContourGenerator::create_filled_contour, + "create_filled_contour(self, lower_level, upper_level)\n" + "--\n\n" + "Create and return a filled contour."); + + py::class_(m, "TrapezoidMapTriFinder", + .def(py::init(), + py::arg("triangulation"), + "TrapezoidMapTriFinder(triangulation)\n" + "--\n\n" + "Create a new C++ TrapezoidMapTriFinder object.\n" + "This should not be called directly, instead use the python class\n" + "matplotlib.tri.TrapezoidMapTriFinder instead.\n") + .def("find_many", &TrapezoidMapTriFinder::find_many, + "find_many(self, x, y)\n" + "--\n\n" + "Find indices of triangles containing the point coordinates (x, y).") + .def("get_tree_stats", &TrapezoidMapTriFinder::get_tree_stats, + "get_tree_stats(self)\n" + "--\n\n" + "Return statistics about the tree used by the trapezoid map.") + .def("initialize", &TrapezoidMapTriFinder::initialize, + "initialize(self)\n" + "--\n\n" + "Initialize this object, creating the trapezoid map from the triangulation.") + .def("print_tree", &TrapezoidMapTriFinder::print_tree, + "print_tree(self)\n" + "--\n\n" + "Print the search tree as text to stdout; useful for debug purposes."); } - -const char* PyTrapezoidMapTriFinder_find_many__doc__ = - "find_many(self, x, y)\n" - "--\n\n" - "Find indices of triangles containing the point coordinates (x, y)."; - -static PyObject* PyTrapezoidMapTriFinder_find_many(PyTrapezoidMapTriFinder* self, PyObject* args) -{ - TrapezoidMapTriFinder::CoordinateArray x, y; - if (!PyArg_ParseTuple(args, "O&O&:find_many", - &x.converter, &x, - &y.converter, &y)) { - return NULL; - } - - if (x.empty() || y.empty() || x.dim(0) != y.dim(0)) { - PyErr_SetString(PyExc_ValueError, - "x and y must be array-like with same shape"); - return NULL; - } - - TrapezoidMapTriFinder::TriIndexArray result; - CALL_CPP("find_many", (result = self->ptr->find_many(x, y))); - return result.pyobj(); -} - -const char* PyTrapezoidMapTriFinder_get_tree_stats__doc__ = - "get_tree_stats(self)\n" - "--\n\n" - "Return statistics about the tree used by the trapezoid map."; - -static PyObject* PyTrapezoidMapTriFinder_get_tree_stats(PyTrapezoidMapTriFinder* self, PyObject* args) -{ - PyObject* result; - CALL_CPP("get_tree_stats", (result = self->ptr->get_tree_stats())); - return result; -} - -const char* PyTrapezoidMapTriFinder_initialize__doc__ = - "initialize(self)\n" - "--\n\n" - "Initialize this object, creating the trapezoid map from the triangulation."; - -static PyObject* PyTrapezoidMapTriFinder_initialize(PyTrapezoidMapTriFinder* self, PyObject* args) -{ - CALL_CPP("initialize", (self->ptr->initialize())); - Py_RETURN_NONE; -} - -const char* PyTrapezoidMapTriFinder_print_tree__doc__ = - "print_tree(self)\n" - "--\n\n" - "Print the search tree as text to stdout; useful for debug purposes."; - -static PyObject* PyTrapezoidMapTriFinder_print_tree(PyTrapezoidMapTriFinder* self, PyObject* args) -{ - CALL_CPP("print_tree", (self->ptr->print_tree())); - Py_RETURN_NONE; -} - -static PyTypeObject *PyTrapezoidMapTriFinder_init_type() -{ - static PyMethodDef methods[] = { - {"find_many", - (PyCFunction)PyTrapezoidMapTriFinder_find_many, - METH_VARARGS, - PyTrapezoidMapTriFinder_find_many__doc__}, - {"get_tree_stats", - (PyCFunction)PyTrapezoidMapTriFinder_get_tree_stats, - METH_NOARGS, - PyTrapezoidMapTriFinder_get_tree_stats__doc__}, - {"initialize", - (PyCFunction)PyTrapezoidMapTriFinder_initialize, - METH_NOARGS, - PyTrapezoidMapTriFinder_initialize__doc__}, - {"print_tree", - (PyCFunction)PyTrapezoidMapTriFinder_print_tree, - METH_NOARGS, - PyTrapezoidMapTriFinder_print_tree__doc__}, - {NULL} - }; - PyTrapezoidMapTriFinderType.tp_name = "matplotlib._tri.TrapezoidMapTriFinder"; - PyTrapezoidMapTriFinderType.tp_doc = PyTrapezoidMapTriFinder_init__doc__; - PyTrapezoidMapTriFinderType.tp_basicsize = sizeof(PyTrapezoidMapTriFinder); - PyTrapezoidMapTriFinderType.tp_dealloc = (destructor)PyTrapezoidMapTriFinder_dealloc; - PyTrapezoidMapTriFinderType.tp_flags = Py_TPFLAGS_DEFAULT; - PyTrapezoidMapTriFinderType.tp_methods = methods; - PyTrapezoidMapTriFinderType.tp_new = PyTrapezoidMapTriFinder_new; - PyTrapezoidMapTriFinderType.tp_init = (initproc)PyTrapezoidMapTriFinder_init; - - return &PyTrapezoidMapTriFinderType; -} - -static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, "_tri" }; - -#pragma GCC visibility push(default) - -PyMODINIT_FUNC PyInit__tri(void) -{ - import_array(); - PyObject *m; - if (!(m = PyModule_Create(&moduledef)) - || prepare_and_add_type(PyTriangulation_init_type(), m) - || prepare_and_add_type(PyTriContourGenerator_init_type(), m) - || prepare_and_add_type(PyTrapezoidMapTriFinder_init_type(), m)) { - Py_XDECREF(m); - return NULL; - } - return m; -} - -#pragma GCC visibility pop From cdfaf20f3ae190a79ccd2a67337041286c7fabd7 Mon Sep 17 00:00:00 2001 From: Ian Thomas Date: Wed, 31 Aug 2022 21:41:50 +0100 Subject: [PATCH 3/9] Add CI dependencies --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 489b3834b036..0ae7435fab16 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -158,7 +158,7 @@ jobs: # Install dependencies from PyPI. python -m pip install --upgrade $PRE \ 'contourpy>=1.0.1' cycler fonttools kiwisolver numpy packaging \ - pillow pyparsing python-dateutil setuptools-scm \ + pillow pybind11 pyparsing python-dateutil setuptools-scm \ -r requirements/testing/all.txt \ ${{ matrix.extra-requirements }} From b15fd9cba3b5f091c0aba0371b17eb0fbe70e359 Mon Sep 17 00:00:00 2001 From: Ian Thomas Date: Thu, 1 Sep 2022 21:27:16 +0100 Subject: [PATCH 4/9] Fix module wrapper --- src/tri/_tri_wrapper.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tri/_tri_wrapper.cpp b/src/tri/_tri_wrapper.cpp index 945a3bed45cf..d448bc4da45b 100644 --- a/src/tri/_tri_wrapper.cpp +++ b/src/tri/_tri_wrapper.cpp @@ -1,7 +1,7 @@ #include "_tri.h" PYBIND11_MODULE(_tri, m) { - py::class_(m, "Triangulation", + py::class_(m, "Triangulation") .def(py::init(m, "TriContourGenerator", + py::class_(m, "TriContourGenerator") .def(py::init(), py::arg("triangulation"), @@ -57,7 +57,7 @@ PYBIND11_MODULE(_tri, m) { "--\n\n" "Create and return a filled contour."); - py::class_(m, "TrapezoidMapTriFinder", + py::class_(m, "TrapezoidMapTriFinder") .def(py::init(), py::arg("triangulation"), "TrapezoidMapTriFinder(triangulation)\n" From 293dffca5b306e592aafd85d83ad8388ea0f3c33 Mon Sep 17 00:00:00 2001 From: Ian Thomas Date: Fri, 2 Sep 2022 10:11:19 +0100 Subject: [PATCH 5/9] Avoid using -std=c++11 when compiling C files --- setupext.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setupext.py b/setupext.py index 68243db0de71..164e64dd2155 100644 --- a/setupext.py +++ b/setupext.py @@ -435,7 +435,7 @@ def get_extensions(self): # qhull ext = Pybind11Extension( "matplotlib._qhull", ["src/_qhull_wrapper.cpp"], - cxx_std=11, + # Do not set cxx_std as C compilers do not recognise it. define_macros=[("MPL_DEVNULL", os.devnull)]) Qhull.add_flags(ext) yield ext From 7da3539cafc5cc14804f48e45e10e4a8eb6c6355 Mon Sep 17 00:00:00 2001 From: Ian Thomas Date: Fri, 2 Sep 2022 10:43:07 +0100 Subject: [PATCH 6/9] Address review comments --- src/_qhull_wrapper.cpp | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/src/_qhull_wrapper.cpp b/src/_qhull_wrapper.cpp index 0cd18dd64454..b7a75cc41d21 100644 --- a/src/_qhull_wrapper.cpp +++ b/src/_qhull_wrapper.cpp @@ -19,7 +19,6 @@ extern const char qh_version[]; #include "libqhull_r/qhull_ra.h" #include -#include #include #ifndef MPL_DEVNULL @@ -135,7 +134,7 @@ class QhullInfo { /* 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. */ -py::tuple +static py::tuple delaunay_impl(py::ssize_t npoints, const double* x, const double* y, bool hide_qhull_errors) { @@ -189,12 +188,12 @@ delaunay_impl(py::ssize_t npoints, const double* x, const double* y, exitcode = qh_new_qhull(qh, ndim, (int)npoints, points.data(), False, (char*)"qhull d Qt Qbb Qc Qz", NULL, error_file); if (exitcode != qh_ERRnone) { - std::ostringstream oss; - oss << "Error in qhull Delaunay triangulation calculation: " << - qhull_error_msg[exitcode] << " (exitcode=" << exitcode << ")"; + std::string msg = + py::str("Error in qhull Delaunay triangulation calculation: {} (exitcode={})") + .format(qhull_error_msg[exitcode], exitcode).cast(); if (hide_qhull_errors) - oss << "; use python verbose option (-v) to see original qhull error."; - throw std::runtime_error(oss.str()); + 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. */ @@ -253,7 +252,7 @@ delaunay_impl(py::ssize_t npoints, const double* x, const double* y, } /* Process Python arguments and call Delaunay implementation method. */ -py::tuple +static py::tuple delaunay(const CoordArray& x, const CoordArray& y) { if (x.ndim() != 1 || y.ndim() != 1) @@ -272,17 +271,10 @@ delaunay(const CoordArray& x, const CoordArray& y) return delaunay_impl(npoints, x.data(), y.data(), Py_VerboseFlag == 0); } -/* Return qhull version string for assistance in debugging. */ -std::string -version() -{ - return std::string(qh_version); -} - PYBIND11_MODULE(_qhull, m) { m.doc() = "Computing Delaunay triangulations.\n"; - m.def("delaunay", &delaunay, + m.def("delaunay", &delaunay, py::arg("x"), py::arg("y"), "delaunay(x, y, /)\n" "--\n\n" "Compute a Delaunay triangulation.\n" @@ -298,7 +290,7 @@ PYBIND11_MODULE(_qhull, m) { "triangles, neighbors : int arrays, shape (ntri, 3)\n" " Indices of triangle vertices and indices of triangle neighbors.\n"); - m.def("version", &version, + m.def("version", []() { return qh_version; }, "version()\n--\n\n" "Return the qhull version string."); } From f8226d8df23e46a2382b8ff4379802c6b2a4016c Mon Sep 17 00:00:00 2001 From: Ian Thomas Date: Fri, 2 Sep 2022 11:30:00 +0100 Subject: [PATCH 7/9] Try altering CI so pybind11 extensions build OK --- .circleci/config.yml | 1 + azure-pipelines.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 557f38165977..ae80aac51242 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -102,6 +102,7 @@ commands: python -m pip install --user \ numpy<< parameters.numpy_version >> codecov coverage \ -r requirements/doc/doc-requirements.txt + python -m pip install pybind11 mpl-install: steps: diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 640136a00e9b..27bc6d2e1253 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -125,6 +125,7 @@ stages: - bash: | python -m pip install --upgrade pip + python -m pip install pybind11 python -m pip install -r requirements/testing/all.txt -r requirements/testing/extra.txt || [[ "$PYTHON_VERSION" = 'Pre' ]] displayName: 'Install dependencies with pip' From 2f8cfec1e0ed01e46c16bea057ae9b4e8eed1dcd Mon Sep 17 00:00:00 2001 From: Ian Thomas Date: Sun, 4 Sep 2022 15:30:31 +0100 Subject: [PATCH 8/9] Remove C++ flags when compiling C files in qhull extension --- setup.py | 20 ++++++++++++++++++++ setupext.py | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 26c871dacb0f..32c6d4b13b86 100644 --- a/setup.py +++ b/setup.py @@ -66,6 +66,15 @@ def has_flag(self, flagname): return True +# Wrapper for distutils.ccompiler.CCompiler._compile to remove C++-specific +# flags when compiling C files. +def compile_wrapper(compiler, obj, src, ext, cc_args, extra_postargs, pp_opts): + if src.lower().endswith(".c"): + extra_postargs = list(filter(lambda x: x[1:4] != "std", + extra_postargs)) + compiler._compile_old(obj, src, ext, cc_args, extra_postargs, pp_opts) + + class BuildExtraLibraries(setuptools.command.build_ext.build_ext): def finalize_options(self): self.distribution.ext_modules[:] = [ @@ -184,9 +193,20 @@ def build_extension(self, ext): orig_build_temp = self.build_temp self.build_temp = os.path.join(self.build_temp, ext.name) try: + if ext.name == "matplotlib._qhull": + # For qhull extension some C++ flags must be removed before + # compiling C files. + from distutils.ccompiler import CCompiler + self.compiler._compile_old = self.compiler._compile + self.compiler._compile = compile_wrapper.__get__( + self.compiler, CCompiler) super().build_extension(ext) finally: self.build_temp = orig_build_temp + if ext.name == "matplotlib._qhull" and hasattr( + self.compiler, "_compile_old"): + self.compiler._compile = self.compiler._compile_old + delattr(self.compiler, "_compile_old") def update_matplotlibrc(path): diff --git a/setupext.py b/setupext.py index 164e64dd2155..68243db0de71 100644 --- a/setupext.py +++ b/setupext.py @@ -435,7 +435,7 @@ def get_extensions(self): # qhull ext = Pybind11Extension( "matplotlib._qhull", ["src/_qhull_wrapper.cpp"], - # Do not set cxx_std as C compilers do not recognise it. + cxx_std=11, define_macros=[("MPL_DEVNULL", os.devnull)]) Qhull.add_flags(ext) yield ext From 846f4c76642214dd76ab3a9f31b6285d6b6e04f4 Mon Sep 17 00:00:00 2001 From: Ian Thomas Date: Sun, 4 Sep 2022 21:08:53 +0100 Subject: [PATCH 9/9] More CI installation of pybind11 --- .appveyor.yml | 1 + .circleci/config.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.appveyor.yml b/.appveyor.yml index df7536f16c7e..190483845370 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -62,6 +62,7 @@ install: - echo %PYTHON_VERSION% %TARGET_ARCH% # Install dependencies from PyPI. - python -m pip install --upgrade -r requirements/testing/all.txt %EXTRAREQS% %PINNEDVERS% + - python -m pip install "pybind11<2.10" # Install optional dependencies from PyPI. # Sphinx is needed to run sphinxext tests - python -m pip install --upgrade sphinx diff --git a/.circleci/config.yml b/.circleci/config.yml index ae80aac51242..f8e0af33a020 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -87,6 +87,7 @@ commands: python -m pip install --upgrade --user pip python -m pip install --upgrade --user wheel python -m pip install --upgrade --user 'setuptools!=60.6.0' + python -m pip install pybind11 doc-deps-install: parameters: