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 diff --git a/python/cork.i b/python/cork.i new file mode 100644 index 00000000..e9a28312 --- /dev/null +++ b/python/cork.i @@ -0,0 +1,196 @@ +/* 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 +%{ +#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(); + + /* 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 + * + * 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) { + 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"); + a = b = NULL; + 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); + a = b = NULL; + 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); +} + +%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) { + 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; + _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 */ +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..bfccd5da --- /dev/null +++ b/setup.py @@ -0,0 +1,68 @@ +""" +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 + +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], +)