From 9ab63f5842176737808ab025bf4c66d8a720f175 Mon Sep 17 00:00:00 2001 From: Stephen Dawson-Haggerty Date: Sun, 16 Nov 2014 21:13:10 -0800 Subject: [PATCH 1/5] Add first cut at Python bindings and test cases to verify that they produce the same answers as calling the binary. The main work we need to do is that input parameters may not be properly deallocated. --- python/cork.i | 86 +++++++++++++++++++++++++++++++++++++ python/test.py | 112 +++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 32 ++++++++++++++ 3 files changed, 230 insertions(+) create mode 100644 python/cork.i create mode 100644 python/test.py create mode 100644 setup.py diff --git a/python/cork.i b/python/cork.i new file mode 100644 index 00000000..fda2e9dd --- /dev/null +++ b/python/cork.i @@ -0,0 +1,86 @@ + +%module cork +%{ +#include +#include "cork.h" +%} + +%init %{ +/* do this, or segfault */ +import_array(); +%} + +/* Create typemaps for mapping python types to/from a CorkTriMesh */ +%typemap(in) CorkTriMesh { + PyObject *a, *b; + npy_intp *d1, *d2; + + if (!PyTuple_Check($input) || PyTuple_Size($input) != 2) { + PyErr_SetString(PyExc_TypeError, "argument must be a tuple of length 2"); + return NULL; + } + a = PyTuple_GetItem($input, 0); + b = PyTuple_GetItem($input, 1); + if (a == NULL || b == NULL) { + PyErr_SetString(PyExc_ValueError, "argument must not be none"); + return NULL; + } + + /* create the PyArrays */ + a = PyArray_FromAny(a, PyArray_DescrFromType(NPY_UINT), 2, 2, NPY_ARRAY_CARRAY, NULL); + if (a == NULL) { + return NULL; + } + b = PyArray_FromAny(b, PyArray_DescrFromType(NPY_FLOAT), 2, 2, NPY_ARRAY_CARRAY, NULL); + if (b == NULL) { + Py_DECREF(a); + return NULL; + } + + /* check the dimensions and get the heights... */ + d1 = PyArray_DIMS(a); + d2 = PyArray_DIMS(b); + if (d1[1] != 3 || d2[1] != 3) { + PyErr_SetString(PyExc_ValueError, "arrays must be Nx3"); + return NULL; + } + + /* how to track allocations.. we can't free this since */ + $1.n_triangles = d1[0]; + $1.n_vertices = d2[0]; + $1.triangles = (uint *)PyArray_DATA(a); + $1.vertices = (float *)PyArray_DATA(b); +} + +/* allocate a temporary CorkTriMesh on the way in for use as an output + parameter */ +%typemap(in, numinputs=0) CorkTriMesh *OUTPUT (CorkTriMesh temp) { + memset(&temp, 0, sizeof(temp)); + $1 = &temp; +} + +/* build numpy matrices for the return type reusing the memory + allocated by Cork. */ +%typemap(argout) CorkTriMesh *OUTPUT { + /* return some numpy arrays with the coordinates in there */ + PyObject *a, *b; + npy_intp dims[2]; + dims[1] = 3; + + dims[0] = temp$argnum.n_triangles; + a = PyArray_SimpleNewFromData(2, dims, NPY_UINT, temp$argnum.triangles); + + dims[0] = temp$argnum.n_vertices; + b = PyArray_SimpleNewFromData(2, dims, NPY_FLOAT, temp$argnum.vertices); + + $result = Py_BuildValue("(OO)", a, b); +} + +/* Export the Cork API */ +extern bool isSolid(CorkTriMesh mesh); +extern void computeUnion(CorkTriMesh in0, CorkTriMesh in1, CorkTriMesh *OUTPUT); +extern void computeDifference(CorkTriMesh in0, CorkTriMesh in1, CorkTriMesh *OUTPUT); +extern void computeIntersection(CorkTriMesh in0, CorkTriMesh in1, CorkTriMesh *OUTPUT); +extern void computeSymmetricDifference( + CorkTriMesh in0, CorkTriMesh in1, CorkTriMesh *OUTPUT); +extern void resolveIntersections(CorkTriMesh in0, CorkTriMesh in1, CorkTriMesh *OUTPUT); diff --git a/python/test.py b/python/test.py new file mode 100644 index 00000000..d9aa9361 --- /dev/null +++ b/python/test.py @@ -0,0 +1,112 @@ +""" +This file is part of the Cork library. + +Cork is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +Cork is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy +of the GNU Lesser General Public License +along with Cork. If not, see . +""" +""" +Test that the python bindings produce the same answer as the c program +on the sample input. + +For this to work, you must edit src/util/prelude.h and change initRand +to have an srand(0) in it, so we can get the same results from both +runs. + +@author Stephen Dawson-Haggerty +""" + +import os +import unittest +import subprocess +import numpy as np +import ctypes +import ctypes.util +import _cork + +class TestPythonBindings(unittest.TestCase): + """Compare the output of the cork binary to the python library + """ + BALL_A = os.path.join(os.path.dirname(__file__), + "../samples/ballA.off") + BALL_B = os.path.join(os.path.dirname(__file__), + "../samples/ballB.off") + + @staticmethod + def read_mesh(f): + fp = open(f, "r") + fp.readline() + dims = map(int, fp.readline().split(" ")) + triangles, vertices = [], [] + for i in xrange(0, dims[0]): + vertices.append(map(float, fp.readline().split(" ") )) + + for i in xrange(0, dims[1]): + triangles.append(map(float, fp.readline().split(" ")[1:] )) + return triangles, vertices + + def call_cork(self, method): + args = [os.path.join(os.path.dirname(__file__), '../bin/cork'), + '-' + method, + self.BALL_A, self.BALL_B, '/tmp/_temp_mesh.off'] + subprocess.check_call(args) + return self.read_mesh('/tmp/_temp_mesh.off') + + def assertTriMeshEquals(self, a, b): + t1, v1 = a + t2, v2 = b + + # assert these are close + self.assertTrue((np.sum(np.abs(v1 - v2)) / np.sum(np.abs(v1))) < 1e-6) + # these should be equal since they're integers to begin with + self.assertEqual(np.sum(np.abs(t1 - t2)), 0) + + def setUp(self): + self.ballA = self.read_mesh(self.BALL_A) + self.ballB = self.read_mesh(self.BALL_B) + + # are we having fun yet? cork uses srand to seed the crappy + # PRNG, but doesn't have a way to reset it. This way we can + # call it between runs so we get the same answer every time. + libc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('c')) + libc.srand(0) + + def testIsSolid(self): + """Both demo objects are solid + """ + self.assertTrue(_cork.isSolid(self.ballA)) + self.assertTrue(_cork.isSolid(self.ballB)) + + def testUnion(self): + self.assertTriMeshEquals(_cork.computeUnion(self.ballA, self.ballB), + self.call_cork('union')) + + def testDifference(self): + self.assertTriMeshEquals(_cork.computeDifference(self.ballA, self.ballB), + self.call_cork('diff')) + + def testIntersection(self): + self.assertTriMeshEquals(_cork.computeIntersection(self.ballA, self.ballB), + self.call_cork('isct')) + + def testSymmetricDifference(self): + self.assertTriMeshEquals(_cork.computeSymmetricDifference(self.ballA, self.ballB), + self.call_cork('xor')) + + def testResolve(self): + self.assertTriMeshEquals(_cork.resolveIntersections(self.ballA, self.ballB), + self.call_cork('resolve')) + + +if __name__ == '__main__': + unittest.main() diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..ff53b3b4 --- /dev/null +++ b/setup.py @@ -0,0 +1,32 @@ + +from distutils.core import setup, Extension +import numpy as np + +cork_module = Extension('_cork', + sources=['python/cork.i'], + language="c++", + + # build extension wrapper with c++11 support + swig_opts=['-c++', '-threads'], + extra_compile_args=['-std=c++11'], + + libraries=['cork', 'gmp'], + library_dirs=['lib/'], + include_dirs=['src', np.get_include()]) + +setup( + name='cork', + version='0.1', + author='Stephen Dawson-Haggerty', + description=('Python interface to the cork library'), + license='GPLv3', + requires=['numpy'], + ext_modules=[cork_module], +# packages = [ +# 'cork' +# ], +# cmdclass = { +# 'build': BacnetBuild, +# 'build_ext': BacnetBuildExt, +# }, +) From 5f967fea362472b77cf43ff8fd301aa05512719e Mon Sep 17 00:00:00 2001 From: Stephen Dawson-Haggerty Date: Sun, 16 Nov 2014 21:30:21 -0800 Subject: [PATCH 2/5] Correctly deallocate temporary python objects in typemap --- python/cork.i | 44 ++++++++++++++++++++++++++++++++++++++++---- setup.py | 50 +++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 83 insertions(+), 11 deletions(-) diff --git a/python/cork.i b/python/cork.i index fda2e9dd..7b2cfa4d 100644 --- a/python/cork.i +++ b/python/cork.i @@ -1,3 +1,22 @@ +/* This file is part of the Cork library. + + * Cork is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + + * Cork is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + + * You should have received a copy + * of the GNU Lesser General Public License + * along with Cork. If not, see . + */ +/* + * @author Stephen Dawson-Haggerty + */ %module cork %{ @@ -10,9 +29,13 @@ import_array(); %} -/* Create typemaps for mapping python types to/from a CorkTriMesh */ -%typemap(in) CorkTriMesh { - PyObject *a, *b; +/* Create typemaps for mapping python types to/from a CorkTriMesh + * + * We allocate temporary PyObjects so that we can use the + * PyArray_FromAny to accept a lot of different potential kinds of + * matrixes. These get cleaned up by the freearg typemap. + */ +%typemap(in) CorkTriMesh (PyObject *a = NULL, PyObject *b = NULL) { npy_intp *d1, *d2; if (!PyTuple_Check($input) || PyTuple_Size($input) != 2) { @@ -23,6 +46,7 @@ import_array(); b = PyTuple_GetItem($input, 1); if (a == NULL || b == NULL) { PyErr_SetString(PyExc_ValueError, "argument must not be none"); + a = b = NULL; return NULL; } @@ -34,6 +58,7 @@ import_array(); b = PyArray_FromAny(b, PyArray_DescrFromType(NPY_FLOAT), 2, 2, NPY_ARRAY_CARRAY, NULL); if (b == NULL) { Py_DECREF(a); + a = b = NULL; return NULL; } @@ -48,10 +73,21 @@ import_array(); /* how to track allocations.. we can't free this since */ $1.n_triangles = d1[0]; $1.n_vertices = d2[0]; - $1.triangles = (uint *)PyArray_DATA(a); + /* very strange -- SWIG generates incorrect code if this A doesn't + have an argnum. */ + $1.triangles = (uint *)PyArray_DATA(a$argnum); $1.vertices = (float *)PyArray_DATA(b); } +%typemap(freearg) CorkTriMesh { + if (a$argnum) { + Py_DECREF(a$argnum); + } + if (b$argnum) { + Py_DECREF(b$argnum); + } +} + /* allocate a temporary CorkTriMesh on the way in for use as an output parameter */ %typemap(in, numinputs=0) CorkTriMesh *OUTPUT (CorkTriMesh temp) { diff --git a/setup.py b/setup.py index ff53b3b4..bfccd5da 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,46 @@ +""" +This file is part of the Cork library. + +Cork is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +Cork is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy +of the GNU Lesser General Public License +along with Cork. If not, see . + +""" +""" +The setup.py file, and the associated python/cork.i file provide the +typemaps necessary to call cork from Python. + +Before building the Python module, you must have compiled the cork +library; it should be placed in lib/ (the default makefile does this. + +The swig interface contains typemaps which compile to a single python +module, named _cork. It exports the definitions from cork.h, except +for freeCorkTriMesh, which is not necessary. + +All of these function use the CorkTriMesh type for both input and +output, which is very convenient. In Python, this type is represented +as a tuple of of (triangles, vertices). For input, these can be +anything implementing the python iterator protocol. If you input +numpy matrices, with triangles represented as a uint32 Nx3 matrix, and +the vertices as a float32 Nx3 matrix, it should be possible to work +without a copy. + +The return values are also represented as numpy matrices. + +Currently, input/output from .off files is not implemented. + +@author Stephen Dawson-Haggerty +""" from distutils.core import setup, Extension import numpy as np @@ -22,11 +65,4 @@ license='GPLv3', requires=['numpy'], ext_modules=[cork_module], -# packages = [ -# 'cork' -# ], -# cmdclass = { -# 'build': BacnetBuild, -# 'build_ext': BacnetBuildExt, -# }, ) From f982d1a48afd112513781304842d476473ea088a Mon Sep 17 00:00:00 2001 From: Stephen Dawson-Haggerty Date: Sun, 16 Nov 2014 21:58:16 -0800 Subject: [PATCH 3/5] Remove bad argnum call --- python/cork.i | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/python/cork.i b/python/cork.i index 7b2cfa4d..89c75e2f 100644 --- a/python/cork.i +++ b/python/cork.i @@ -73,9 +73,7 @@ import_array(); /* how to track allocations.. we can't free this since */ $1.n_triangles = d1[0]; $1.n_vertices = d2[0]; - /* very strange -- SWIG generates incorrect code if this A doesn't - have an argnum. */ - $1.triangles = (uint *)PyArray_DATA(a$argnum); + $1.triangles = (uint *)PyArray_DATA(a); $1.vertices = (float *)PyArray_DATA(b); } From a01b9b4416b5c625067d1ce2b81d8d01c1d41f28 Mon Sep 17 00:00:00 2001 From: Stephen Dawson-Haggerty Date: Tue, 18 Nov 2014 17:21:21 -0800 Subject: [PATCH 4/5] Add -fPIC for python library compile. This may or may not be desirable for the non-python use case. There's no clear way to do this with a flag, but we should probably build out the build system --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a9fbb62c..b6ded715 100644 --- a/Makefile +++ b/Makefile @@ -62,7 +62,7 @@ INC := $(INC) $(GMPINC) # use the second line to disable profiling instrumentation # PROFILING := -pg PROFILING := -CCFLAGS := -Wall $(INC) $(CONFIG) -O2 -DNDEBUG $(PROFILING) +CCFLAGS := -Wall $(INC) $(CONFIG) -O2 -DNDEBUG $(PROFILING) -fPIC CXXFLAGS := $(CCFLAGS) $(CPP11_FLAGS) CCDFLAGS := -Wall $(INC) $(CONFIG) -ggdb CXXDFLAGS := $(CCDFLAGS) @@ -195,6 +195,7 @@ obj/isct/triangle.o: src/isct/triangle.c @echo "Compiling the Triangle library" @$(CC) -O2 -DNO_TIMER \ -DREDUCED \ + -fPIC \ -DCDT_ONLY -DTRILIBRARY \ -Wall -DANSI_DECLARATORS \ -o obj/isct/triangle.o -c src/isct/triangle.c From 8a2d3e631ccb8f920b64147e4043c4af7aa3782f Mon Sep 17 00:00:00 2001 From: Stephen Dawson-Haggerty Date: Wed, 19 Nov 2014 17:00:16 -0800 Subject: [PATCH 5/5] Properly deallocate memory passed from cork. This was a bit of an adventure. --- python/cork.i | 80 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/python/cork.i b/python/cork.i index 89c75e2f..e9a28312 100644 --- a/python/cork.i +++ b/python/cork.i @@ -22,11 +22,69 @@ %{ #include #include "cork.h" + + +/* create a "base object" that lets us use the memory returned by the + * cork library. this is DECREFed when the numpy array we return is + * deleted, and we can use that to free the c++ array using the + * appropriate deallocator. + */ +typedef struct { + PyObject_HEAD + uint *triangles; + float *vertices; +} _DeleteObject; + +static void _DeleteObject_dealloc(PyObject *self) { + _DeleteObject *real_self = (_DeleteObject *)self; + if (real_self->triangles) { + delete real_self->triangles; + } + if (real_self->vertices) { + delete real_self->vertices; + } + self->ob_type->tp_free((PyObject *)self); +} + +static PyTypeObject _DeleteObjectType = { + PyObject_HEAD_INIT(NULL) + 0, + "delete_deallocator", + sizeof(_DeleteObject), + 0, + _DeleteObject_dealloc, /* tp_dealloc */ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash */ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + "c++ delete deallocator", /* tp_doc */ +}; + %} %init %{ -/* do this, or segfault */ -import_array(); + /* do this, or segfault */ + import_array(); + + /* add the new deallocator type to the system */ + _DeleteObjectType.tp_new = PyType_GenericNew; + if (PyType_Ready(&_DeleteObjectType) < 0) { + return; + } + + Py_INCREF(&_DeleteObjectType); + PyModule_AddObject(m, "_cork", (PyObject *)&_DeleteObjectType); %} /* Create typemaps for mapping python types to/from a CorkTriMesh @@ -98,16 +156,34 @@ import_array(); %typemap(argout) CorkTriMesh *OUTPUT { /* return some numpy arrays with the coordinates in there */ PyObject *a, *b; + _DeleteObject *base_a, *base_b; npy_intp dims[2]; dims[1] = 3; + + dims[0] = temp$argnum.n_triangles; a = PyArray_SimpleNewFromData(2, dims, NPY_UINT, temp$argnum.triangles); dims[0] = temp$argnum.n_vertices; b = PyArray_SimpleNewFromData(2, dims, NPY_FLOAT, temp$argnum.vertices); + /* set a base object on the numpy array that will deallocate our c++ buffer */ + base_a = PyObject_New(_DeleteObject, &_DeleteObjectType); + base_a->triangles = temp$argnum.triangles; + base_a->vertices = NULL; + PyArray_SetBaseObject((PyArrayObject *)a, (PyObject *)base_a); + + base_b = PyObject_New(_DeleteObject, &_DeleteObjectType); + base_b->triangles = NULL; + base_b->vertices = temp$argnum.vertices; + PyArray_SetBaseObject((PyArrayObject *)b, (PyObject *)base_b); + + $result = Py_BuildValue("(OO)", a, b); + /* BuildValue calls Py_INCREF on its arguments */ + Py_DECREF(a); + Py_DECREF(b); } /* Export the Cork API */