From f1fdca066442a177f609ecbd60059e241b6112d7 Mon Sep 17 00:00:00 2001 From: Adam Paszke Date: Sat, 21 Mar 2020 19:26:35 +0100 Subject: [PATCH 1/3] Make PathIterator more generic Previously PathIterator has kept vertices and codes as (owned) PyArrayObjects, while after this patch is keeps them as strided memory regions. This allows to generalize the kinds of objects those iterators can be constructed from, opening up opportunities for optimized path collections. --- src/py_adaptors.h | 144 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 108 insertions(+), 36 deletions(-) diff --git a/src/py_adaptors.h b/src/py_adaptors.h index 912c93c8bf12..63e6386c2b78 100644 --- a/src/py_adaptors.h +++ b/src/py_adaptors.h @@ -21,6 +21,84 @@ int convert_path(PyObject *obj, void *pathp); namespace py { +template +class StridedMemoryBase { +protected: + char *m_data; + npy_intp *m_strides; + + explicit StridedMemoryBase(char *data, npy_intp *strides) + : m_data(data), m_strides(strides) + { + } + +public: + StridedMemoryBase() + : m_data(nullptr), m_strides(nullptr) + { + } + + explicit StridedMemoryBase(PyArrayObject *array) + { + if (PyArray_NDIM(array) != Dim) { + PyErr_SetString(PyExc_ValueError, "Invalid array dimensionality"); + throw py::exception(); + } + m_data = PyArray_BYTES(array); + m_strides = PyArray_STRIDES(array); + } + + operator bool() const { + return m_data != nullptr; + } + + void reset() { + m_data = nullptr; + m_strides = nullptr; + } + + T* data() { + return reinterpret_cast(m_data); + } +}; + +#define INHERIT_CONSTRUCTORS(CLS, DIM) \ + CLS() {} \ + explicit CLS(char *data, npy_intp *strides): StridedMemoryBase(data, strides) {} \ + explicit CLS(PyArrayObject *array): StridedMemoryBase(array) {} + + +template +class StridedMemory1D : public StridedMemoryBase { +public: + INHERIT_CONSTRUCTORS(StridedMemory1D, 1) + T operator[](size_t idx) const { + return *reinterpret_cast(this->m_data + *this->m_strides * idx); + } +}; + +template +class StridedMemory2D : public StridedMemoryBase { +public: + INHERIT_CONSTRUCTORS(StridedMemory2D, 2) + StridedMemory1D operator[](size_t idx) const { + return StridedMemory1D(this->m_data + *this->m_strides * idx, + this->m_strides + 1); + } +}; + +template +class StridedMemory3D : public StridedMemoryBase { +public: + INHERIT_CONSTRUCTORS(StridedMemory3D, 3) + StridedMemory2D operator[](size_t idx) const { + return StridedMemory2D(this->m_data + *this->m_strides * idx, + this->m_strides + 1); + } +}; + +#undef INHERIT_CONSTRUCTORS + /************************************************************ * py::PathIterator acts as a bridge between Numpy and Agg. Given a * pair of Numpy arrays, vertices and codes, it iterates over @@ -31,12 +109,12 @@ namespace py */ class PathIterator { - /* We hold references to the Python objects, not just the - underlying data arrays, so that Python reference counting - can work. + /* XXX: This class does not own the data! It should really be used as an + iterator, where it is the container that manages the lifetime of the + paths. */ - PyArrayObject *m_vertices; - PyArrayObject *m_codes; + StridedMemory2D m_vertices; + StridedMemory1D m_codes; unsigned m_iterator; unsigned m_total_vertices; @@ -50,8 +128,8 @@ class PathIterator public: inline PathIterator() - : m_vertices(NULL), - m_codes(NULL), + : m_vertices(), + m_codes(), m_iterator(0), m_total_vertices(0), m_should_simplify(false), @@ -63,14 +141,18 @@ class PathIterator PyObject *codes, bool should_simplify, double simplify_threshold) - : m_vertices(NULL), m_codes(NULL), m_iterator(0) + : m_vertices(), + m_codes(), + m_iterator(0) { if (!set(vertices, codes, should_simplify, simplify_threshold)) throw py::exception(); } inline PathIterator(PyObject *vertices, PyObject *codes) - : m_vertices(NULL), m_codes(NULL), m_iterator(0) + : m_vertices(), + m_codes(), + m_iterator(0) { if (!set(vertices, codes)) throw py::exception(); @@ -78,10 +160,7 @@ class PathIterator inline PathIterator(const PathIterator &other) { - Py_XINCREF(other.m_vertices); m_vertices = other.m_vertices; - - Py_XINCREF(other.m_codes); m_codes = other.m_codes; m_iterator = 0; @@ -91,39 +170,32 @@ class PathIterator m_simplify_threshold = other.m_simplify_threshold; } - ~PathIterator() - { - Py_XDECREF(m_vertices); - Py_XDECREF(m_codes); - } - inline int set(PyObject *vertices, PyObject *codes, bool should_simplify, double simplify_threshold) { m_should_simplify = should_simplify; m_simplify_threshold = simplify_threshold; - Py_XDECREF(m_vertices); - m_vertices = (PyArrayObject *)PyArray_FromObject(vertices, NPY_DOUBLE, 2, 2); - - if (!m_vertices || PyArray_DIM(m_vertices, 1) != 2) { + PyArrayObject *vertices_arr = + (PyArrayObject *)PyArray_FromObject(vertices, NPY_DOUBLE, 2, 2); + if (!vertices_arr || PyArray_DIM(vertices_arr, 1) != 2) { PyErr_SetString(PyExc_ValueError, "Invalid vertices array"); return 0; } - - Py_XDECREF(m_codes); - m_codes = NULL; + m_vertices = StridedMemory2D(vertices_arr); if (codes != NULL && codes != Py_None) { - m_codes = (PyArrayObject *)PyArray_FromObject(codes, NPY_UINT8, 1, 1); - - if (!m_codes || PyArray_DIM(m_codes, 0) != PyArray_DIM(m_vertices, 0)) { + PyArrayObject *codes_arr = (PyArrayObject *)PyArray_FromObject(codes, NPY_UINT8, 1, 1); + if (!codes_arr || PyArray_DIM(codes_arr, 0) != PyArray_DIM(vertices_arr, 0)) { PyErr_SetString(PyExc_ValueError, "Invalid codes array"); return 0; } + m_codes = StridedMemory1D(codes_arr); + } else { + m_codes.reset(); } - m_total_vertices = (unsigned)PyArray_DIM(m_vertices, 0); + m_total_vertices = (unsigned)PyArray_DIM(vertices_arr, 0); m_iterator = 0; return 1; @@ -144,12 +216,12 @@ class PathIterator const size_t idx = m_iterator++; - char *pair = (char *)PyArray_GETPTR2(m_vertices, idx, 0); - *x = *(double *)pair; - *y = *(double *)(pair + PyArray_STRIDE(m_vertices, 1)); + StridedMemory1D vertex = m_vertices[idx]; + *x = vertex[0]; + *y = vertex[1]; - if (m_codes != NULL) { - return (unsigned)(*(char *)PyArray_GETPTR1(m_codes, idx)); + if (m_codes) { + return static_cast(m_codes[idx]); } else { return idx == 0 ? agg::path_cmd_move_to : agg::path_cmd_line_to; } @@ -177,12 +249,12 @@ class PathIterator inline bool has_curves() const { - return m_codes != NULL; + return m_codes; } inline void *get_id() { - return (void *)m_vertices; + return (void *)m_vertices.data(); } }; From 69471602db77ce3daa9d3ca7558deef1d10ae706 Mon Sep 17 00:00:00 2001 From: Adam Paszke Date: Sat, 21 Mar 2020 20:47:49 +0100 Subject: [PATCH 2/3] Add a specialized path collection Right now most paths are usually stored as separate objects in regular Python lists. This format is unfortunately very inefficient when the paths are homogenous (i.e. have the same length and codes). This patch adds a custom collection that still allows iteration over individual paths, while also supporting a more efficient code path in the rendering pipeline. --- lib/matplotlib/collections.py | 8 +-- lib/matplotlib/path.py | 53 ++++++++++++++++++ src/py_adaptors.h | 100 +++++++++++++++++++++++++++------- 3 files changed, 135 insertions(+), 26 deletions(-) diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index aa37ca0e24a7..0384950079fa 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -1096,13 +1096,7 @@ def set_verts(self, verts, closed=True): # Fast path for arrays if isinstance(verts, np.ndarray): verts_pad = np.concatenate((verts, verts[:, :1]), axis=1) - # Creating the codes once is much faster than having Path do it - # separately each time by passing closed=True. - codes = np.empty(verts_pad.shape[1], dtype=mpath.Path.code_type) - codes[:] = mpath.Path.LINETO - codes[0] = mpath.Path.MOVETO - codes[-1] = mpath.Path.CLOSEPOLY - self._paths = [mpath.Path(xy, codes) for xy in verts_pad] + self._paths = mpath.PathCollection(verts_pad, closed=True) return self._paths = [] diff --git a/lib/matplotlib/path.py b/lib/matplotlib/path.py index 44392cc81af2..7296503d6826 100644 --- a/lib/matplotlib/path.py +++ b/lib/matplotlib/path.py @@ -19,6 +19,59 @@ from .cbook import _to_unmasked_float_array, simple_linear_interpolation +class PathCollection: + _is_uniform_path_collection = True + + def __init__(self, vertices, codes=None, _interpolation_steps=1, + closed=False): + # TODO: Should this support readonly?? + vertices = _to_unmasked_float_array(vertices) + if vertices.ndim != 3 or vertices.shape[-1] != 2: + raise ValueError( + "'vertices' must be a 3D list or array with shape CxNx2") + + if codes is not None: + codes = np.asarray(codes, Path.code_type) + if codes.shape != vertices.shape[:-1]: + raise ValueError("'codes' must be a 2D list or array with the " + "same length of 'vertices'") + if len(codes) and np.any(codes[0] != self.MOVETO): + raise ValueError("The first element of 'code' must be equal " + "to 'MOVETO' ({})".format(self.MOVETO)) + elif closed and len(vertices): + codes = np.empty((1, vertices.shape[1]), dtype=Path.code_type) + codes[:, 0] = Path.MOVETO + codes[:, 1:-1] = Path.LINETO + codes[:, -1] = Path.CLOSEPOLY + codes = np.broadcast_to(codes, vertices.shape[:-1]) + + self._vertices = vertices + self._codes = codes + self._interpolation_steps = _interpolation_steps + self._example_path = Path(vertices[0], codes[0], _interpolation_steps) + + def __len__(self): + return len(self._vertices) + + def __getitem__(self, i): + pth = Path.__new__(Path) + pth._vertices = self._vertices[i] + pth._codes = self._codes[i] + pth._readonly = False + pth._should_simplify = self._example_path._should_simplify + pth._simplify_threshold = self._example_path._simplify_threshold + pth._interpolation_steps = self._example_path._interpolation_steps + return pth + + @property + def should_simplify(self): + return self._example_path.should_simplify + + @property + def simplify_threshold(self): + return self._example_path.simplify_threshold + + class Path: """ A series of possibly disconnected, possibly closed, line and curve diff --git a/src/py_adaptors.h b/src/py_adaptors.h index 63e6386c2b78..8d398a9a75cf 100644 --- a/src/py_adaptors.h +++ b/src/py_adaptors.h @@ -9,6 +9,7 @@ */ #include +#include #include "numpy/arrayobject.h" @@ -137,6 +138,20 @@ class PathIterator { } + inline PathIterator(const StridedMemory2D& vertices, + const StridedMemory1D& codes, + unsigned total_vertices, + bool should_simplify, + double simplify_threshold) + : m_vertices(vertices), + m_codes(codes), + m_iterator(0), + m_total_vertices(total_vertices), + m_should_simplify(should_simplify), + m_simplify_threshold(simplify_threshold) + { + } + inline PathIterator(PyObject *vertices, PyObject *codes, bool should_simplify, @@ -260,28 +275,34 @@ class PathIterator class PathGenerator { - PyObject *m_paths; Py_ssize_t m_npaths; + bool m_is_optimized; - public: - typedef PathIterator path_iterator; + // Used for optimized path collections + StridedMemory3D m_vertices; + StridedMemory2D m_codes; + unsigned m_path_length; + bool m_should_simplify; + double m_simplify_threshold; - PathGenerator(PyObject *obj) : m_paths(NULL), m_npaths(0) - { - if (!set(obj)) { - throw py::exception(); - } - } + // Used for general sequences + PyObject *m_paths; - ~PathGenerator() - { - Py_XDECREF(m_paths); - } + public: + typedef PathIterator path_iterator; - int set(PyObject *obj) + PathGenerator(PyObject *obj) + : m_npaths(0), + m_is_optimized(false), + m_vertices(), + m_codes(), + m_path_length(0), + m_should_simplify(false), + m_simplify_threshold(0), + m_paths(NULL) { if (!PySequence_Check(obj)) { - return 0; + throw py::exception(); } m_paths = obj; @@ -289,7 +310,44 @@ class PathGenerator m_npaths = PySequence_Size(m_paths); - return 1; + PyObject *is_uniform_obj = PyObject_GetAttrString(obj, "_is_uniform_path_collection"); + PyErr_Clear(); // The attribute might not be there. + m_is_optimized = is_uniform_obj == Py_True; + Py_XDECREF(is_uniform_obj); + if (m_is_optimized) { + PyArrayObject *vertices_obj = (PyArrayObject*)PyObject_GetAttrString(obj, "_vertices"); + PyArrayObject *codes_obj = (PyArrayObject*)PyObject_GetAttrString(obj, "_codes"); + PyObject *should_simplify_obj = PyObject_GetAttrString(obj, "should_simplify"); + PyObject *simplify_threshold_obj = PyObject_GetAttrString(obj, "simplify_threshold"); + if (vertices_obj == NULL || codes_obj == NULL || + should_simplify_obj == NULL || simplify_threshold_obj == NULL) { + PyErr_SetString(PyExc_ValueError, "Expected a uniform path collection"); + goto end; + } + if (!PyArray_Check(vertices_obj) || !PyArray_Check(codes_obj)) { + PyErr_SetString(PyExc_ValueError, "Vertices and codes should be NumPy arrays"); + goto end; + } + m_vertices = StridedMemory3D(vertices_obj); + m_codes = StridedMemory2D(codes_obj); + m_path_length = PyArray_DIM(vertices_obj, 1); + m_should_simplify = should_simplify_obj == Py_True; + m_simplify_threshold = PyFloat_AsDouble(simplify_threshold_obj); +end: + Py_XDECREF(vertices_obj); + Py_XDECREF(codes_obj); + Py_XDECREF(should_simplify_obj); + Py_XDECREF(simplify_threshold_obj); + // Check that PyFloat_AsDouble succeeded + if (PyErr_Occurred()) { + throw py::exception(); + } + } + } + + ~PathGenerator() + { + Py_XDECREF(m_paths); } Py_ssize_t num_paths() const @@ -304,10 +362,13 @@ class PathGenerator path_iterator operator()(size_t i) { - path_iterator path; - PyObject *item; + if (m_is_optimized) { + return path_iterator(m_vertices[i], m_codes[i], m_path_length, + m_should_simplify, m_simplify_threshold); + } - item = PySequence_GetItem(m_paths, i % m_npaths); + path_iterator path; + PyObject *item = PySequence_GetItem(m_paths, i % m_npaths); if (item == NULL) { throw py::exception(); } @@ -319,6 +380,7 @@ class PathGenerator return path; } }; + } #endif From bb7197359458ad8087b0e8593f7cdf0529c3a19e Mon Sep 17 00:00:00 2001 From: Adam Paszke Date: Sun, 22 Mar 2020 13:34:51 +0100 Subject: [PATCH 3/3] Remove C++11 --- src/py_adaptors.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/py_adaptors.h b/src/py_adaptors.h index 8d398a9a75cf..07f7a4c30aff 100644 --- a/src/py_adaptors.h +++ b/src/py_adaptors.h @@ -9,7 +9,7 @@ */ #include -#include +#include #include "numpy/arrayobject.h" @@ -35,7 +35,7 @@ class StridedMemoryBase { public: StridedMemoryBase() - : m_data(nullptr), m_strides(nullptr) + : m_data(NULL), m_strides(NULL) { } @@ -50,12 +50,12 @@ class StridedMemoryBase { } operator bool() const { - return m_data != nullptr; + return m_data != NULL; } void reset() { - m_data = nullptr; - m_strides = nullptr; + m_data = NULL; + m_strides = NULL; } T* data() {