From d7677b1f3f5ceee9cfdb5275ef00cbf79a714122 Mon Sep 17 00:00:00 2001 From: Bryn Keller Date: Mon, 7 Mar 2016 13:51:48 -0800 Subject: Python interface to PHAT --- python/_phat.cpp | 279 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 python/_phat.cpp (limited to 'python/_phat.cpp') diff --git a/python/_phat.cpp b/python/_phat.cpp new file mode 100644 index 0000000..c43a3f5 --- /dev/null +++ b/python/_phat.cpp @@ -0,0 +1,279 @@ +//Required header for using pybind11 +#include + +//Automatic conversions of stl containers to Python ones +#include + +//Additional support for operators and numpy +#include +#include + +//All the things we're going to wrap +#include "phat/persistence_pairs.h" +#include "phat/compute_persistence_pairs.h" +#include "phat/boundary_matrix.h" +#include "phat/representations/abstract_pivot_column.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace py = pybind11; + +//## Some template functions we'll need later + +// This function defines two Python functions in the extension module, that are named +// `compute_persistence_pairs_${rep}_${reduction}` +// `compute_persistence_pairs_dualized_${rep}_${reductionx}`. +// +// The Python user will never see these, since we will use (in phat.py) the type of the +// boundary matrix and the requested reduction to dispatch to the correct function +// required. +// +// These functions are the main operations of PHAT. In the Python version, they take +// a boundary matrix, and return a persistence_pairs object. +// +// `Reduction` will be an algorithm, `Representation` is a type that controls +// how the boundary matrix stores its internal state. +// +// We will be using this function to define these two functions for every combination +// of `Representation` and `Reduction` that PHAT supports. +template +void define_compute_persistence(py::module &mod, + const std::string &representation_suffix, + const std::string &reduction_suffix) { + + auto suffix = representation_suffix + std::string("_") + reduction_suffix; + + //We don't annotate these with doc strings or py::args because + //they are only used internally by code in phat.py + mod.def((std::string("compute_persistence_pairs_") + suffix).c_str(), + [](phat::boundary_matrix &matrix){ + phat::persistence_pairs pairs; + phat::compute_persistence_pairs(pairs, matrix); + return pairs; + }); + mod.def((std::string("compute_persistence_pairs_dualized_") + suffix).c_str(), + [](phat::boundary_matrix &matrix){ + phat::persistence_pairs pairs; + phat::compute_persistence_pairs_dualized(pairs, matrix); + return pairs; + }); +} + +// Define a function to convert a `boundary_matrix` with one internal representation to a +// `boundary_matrix` with a different internal representation. Like with define_compute_persistence, +// the user will never see this function, but it is used internally by phat.py. +template +void define_converter(py::module &mod, const std::string &self_suffix, const std::string &other_suffix) { + //We don't annotate these with doc strings or py::args because + //they are only used internally by code in phat.py + mod.def((std::string("convert_") + other_suffix + "_to_" + self_suffix).c_str(), + [](phat::boundary_matrix &other) { + return phat::boundary_matrix(other); + }); +} + +// Creates a Python class for a `boundary_matrix`. Boundary matrices are one of two important types +// used by PHAT. +template +void wrap_boundary_matrix(py::module &mod, const std::string &representation_suffix) { + + using mat = phat::boundary_matrix; + + py::class_(mod, (std::string("boundary_matrix_") + representation_suffix).c_str()) + //Default no-args constructor + .def(py::init()) + //#### Loading and extracting data from Python lists + //Note we can use references to member functions (even templated ones) directly in many cases. + .def("load_vector_vector", + &mat::template load_vector_vector, + "Load this instance with the given columns and dimensions", + py::arg("columns"), py::arg("dimensions")) + .def("get_vector_vector", [](mat &m) { + std::vector< std::vector< int > > vector_vector_matrix; + std::vector< int > vector_dims; + m.save_vector_vector( vector_vector_matrix, vector_dims ); + return std::tuple>, std::vector>(vector_vector_matrix, vector_dims); + }, + "Extract the data in the boundary matrix into a list of columns, and a list of dimensions that correspond to the columns") + //#### Loading and saving files + .def("load_binary", &mat::load_binary, + "Load this instance with data from a binary file") + .def("save_binary", &mat::save_binary, + "Save this instance to a binary file") + .def("load_ascii", &mat::load_ascii, + "Load this instance with data from a text file") + .def("save_ascii", &mat::save_ascii, + "Save this instance to a text file") + //#### Getting and setting dimensions + //Note that boundary_matrix dimensions are not normal matrix dimensions, + //They refer to the dimension of the simplex stored in the given column. + .def("get_dim", &mat::get_dim, + "Get the dimension list for this boundary matrix. " + "The dimension list shows the dimension for each column in the matrix") + .def("set_dim", &mat::set_dim, "Set the dimension for a single column", + py::arg("index"), py::arg("dimension")) + //The `set_dims` method is an example of making the data structure easier to use + //from Python. This is a method that doesn't exist in the C++ class, but we add it + //using a C++ lambda. This ability to enhance the binding using lambdas + //is an *extremely* handy tool. + .def("set_dims", [](mat &m, std::vector dims) { + m.set_num_cols(dims.size()); + for(size_t i = 0; i < dims.size(); i++) { + m.set_dim(i, dims[i]); + } + }, + "Set the dimension list for this boundary matrix", + py::arg("dimensions")) + + //#### \__eq__ + //The `boundary_matrix`'s `operator==` is templated, which could make a Python wrapper + //very tricky indeed. Luckily, when we define multiple + //methods with the same name but different C++ types, pybind11 will create a Python method + //that chooses between them based on type tags that it manages. This is *also* extremely handy. + .def("__eq__", &mat::template operator==) + .def("__eq__", &mat::template operator==) + .def("__eq__", &mat::template operator==) + .def("__eq__", &mat::template operator==) + .def("__eq__", &mat::template operator==) + .def("__eq__", &mat::template operator==) + .def("__eq__", &mat::template operator==) + .def("__eq__", &mat::template operator==) + + //#### Data access + + // In `get_col`, since Python is garbage collected, the C++ idiom of passing in a collection + // to load doesn't make much sense. We can simply allocate an STL vector and + // return it. The pybind11 framework will take ownership and hook it into the + // Python reference counting system. + .def("get_col", [](mat &m, phat::index col_index) { + std::vector col; + m.get_col(col_index, col); + return col; + }, + "Extract a single column as a list", + py::arg("index")) + .def("set_col", &mat::set_col, + "Set the values for a given column", + py::arg("index"), py::arg("column")) + .def("get_num_cols", &mat::get_num_cols) + .def("is_empty", &mat::is_empty) + .def("get_num_entries", &mat::get_num_entries); + + //#### Compute persistence + // Define compute_persistence(_dualized) for all possible reductions. + define_compute_persistence(mod, representation_suffix, std::string("sr")); + define_compute_persistence(mod, representation_suffix, std::string("cr")); + define_compute_persistence(mod, representation_suffix, std::string("rr")); + define_compute_persistence(mod, representation_suffix, std::string("tr")); + define_compute_persistence(mod, representation_suffix, std::string("ssr")); + //#### Converters + //Define functions to convert from this kind of `boundary_matrix` to any of the other types + define_converter(mod, representation_suffix, std::string("btpc")); + define_converter(mod, representation_suffix, std::string("spc")); + define_converter(mod, representation_suffix, std::string("hpc")); + define_converter(mod, representation_suffix, std::string("fpc")); + define_converter(mod, representation_suffix, std::string("vv")); + define_converter(mod, representation_suffix, std::string("vh")); + define_converter(mod, representation_suffix, std:: string("vs")); + define_converter(mod, representation_suffix, std::string("vl")); +} +//fix_index checks for out-of-bounds indexes, and converts negative indices to positive ones +//e.g. pairs[-1] => pairs[len(pairs) - 1] +phat::index fix_index(const phat::persistence_pairs &p, int index) { + //Note get_num_pairs returns type index, which is not unsigned, though it comes from + //std::vector.size, which is size_t. + phat::index pairs = p.get_num_pairs(); + assert(pairs > 0); + if (index < 0) { + index = pairs + index; + } + if ((index < 0) || static_cast(index) >= static_cast(pairs)) { + //pybind11 helpfully converts C++ exceptions into Python ones + throw py::index_error(); + } + return index; +} + +//Here we define the wrapper for the persistence_pairs class. Unlike `boundary_matrix`, this +//class is not templated, so is simpler to wrap. +void wrap_persistence_pairs(py::module &m) { + py::class_(m, "persistence_pairs") + //No-args constructor + .def(py::init()) + + //This is a method that takes two ints + .def("append_pair", + &phat::persistence_pairs::append_pair, + "Appends a single (birth, death) pair", + py::arg("birth"), py::arg("death")) + + //This is a method that takes two ints + .def("set_pair", + &phat::persistence_pairs::set_pair, + "Sets the (birth, death) pair at a given index", + py::arg("index"), py::arg("birth"), py::arg("death")) + + //#### Python collection support + .def("__len__", &phat::persistence_pairs::get_num_pairs) + // Unlike set_pair, this takes a Python 2-tuple + .def("__setitem__", + [](phat::persistence_pairs &p, int index, std::pair &pair) { + phat::index idx = fix_index(p, index); + p.set_pair(idx, pair.first, pair.second); + }) + // \__len\__ and \__getitem\__ together serve to make this a Python iterable + // so you can do `for i in pairs: blah`. A nicer way is to support \__iter\__, + // which we leave for future work. + .def("__getitem__", [](const phat::persistence_pairs &p, int index) { + phat::index idx = fix_index(p, index); + return p.get_pair(idx); + }) + .def("clear", &phat::persistence_pairs::clear, "Empties the collection") + .def("sort", &phat::persistence_pairs::sort, "Sort in place") + .def("__eq__", &phat::persistence_pairs::operator==) + //#### File operations + .def("load_ascii", &phat::persistence_pairs::load_ascii, + "Load the contents of a text file into this instance") + .def("save_ascii", &phat::persistence_pairs::save_ascii, + "Save this instance to a text file") + .def("save_binary", &phat::persistence_pairs::save_binary, + "Save the contents of this instance to a binary file") + .def("load_binary", &phat::persistence_pairs::load_binary, + "Load the contents of a binary file into this instance"); +} + +//## Define the module +//This is where we actually define the `_phat` module. We'll also have a `phat` module that's written +//in Python, which will use `_phat` as an implementation detail. +PYBIND11_PLUGIN(_phat) { + //Create the module object. First arg is the name, second is the module docstring. + py::module m("_phat", "C++ wrapper for PHAT. Please use the phat module, not the _phat module"); + + //Wrap the `persistence_pairs` class + wrap_persistence_pairs(m); + + //#### Generate all the different representations of `boundary_matrix` + wrap_boundary_matrix(m, "btpc"); + wrap_boundary_matrix(m, "spc"); + wrap_boundary_matrix(m, "hpc"); + wrap_boundary_matrix(m, "fpc"); + wrap_boundary_matrix(m, "vv"); + wrap_boundary_matrix(m, "vh"); + wrap_boundary_matrix(m, "vs"); + wrap_boundary_matrix(m, "vl"); + + //We're done! + return m.ptr(); + +} -- cgit v1.2.3 From 816b21f9df89788669b15369c42b1c03eca39461 Mon Sep 17 00:00:00 2001 From: Bryn Keller Date: Thu, 10 Mar 2016 16:04:37 -0800 Subject: Simplified, more Pythonic API, with additional documentation --- python/_phat.cpp | 3 +- python/phat.py | 318 ++++++++++++++++++++++++++++++++++++------- python/src/self_test.py | 16 +-- python/src/simple_example.py | 42 +++--- 4 files changed, 294 insertions(+), 85 deletions(-) (limited to 'python/_phat.cpp') diff --git a/python/_phat.cpp b/python/_phat.cpp index c43a3f5..40a9421 100644 --- a/python/_phat.cpp +++ b/python/_phat.cpp @@ -119,8 +119,7 @@ void wrap_boundary_matrix(py::module &mod, const std::string &representation_suf //Note that boundary_matrix dimensions are not normal matrix dimensions, //They refer to the dimension of the simplex stored in the given column. .def("get_dim", &mat::get_dim, - "Get the dimension list for this boundary matrix. " - "The dimension list shows the dimension for each column in the matrix") + "Get the dimension for the requested column.") .def("set_dim", &mat::set_dim, "Set the dimension for a single column", py::arg("index"), py::arg("dimension")) //The `set_dims` method is an example of making the data structure easier to use diff --git a/python/phat.py b/python/phat.py index 7cc0ae2..6537b26 100644 --- a/python/phat.py +++ b/python/phat.py @@ -1,20 +1,75 @@ -import _phat -import enum +"""Bindings for the Persistent Homology Algorithm Toolbox -from _phat import persistence_pairs +PHAT is a tool for algebraic topology. It can be used via phat.py to compute +persistent (co)homology from boundary matrices, using various reduction +algorithms and column data representations. -__all__ = ['boundary_matrix', - 'persistence_pairs', - 'compute_persistence_pairs', - 'compute_persistence_pairs_dualized'] +Here is a simple example of usage. +We will build an ordered boundary matrix of this simplicial complex consisting of a single triangle: -"""Bindings for the Persistent Homology Algorithm Toolbox + 3 + |\\ + | \\ + | \\ + | \\ 4 + 5| \\ + | \\ + | 6 \\ + | \\ + |________\\ + 0 2 1 + +Now the code:: + import phat -Please see https://bitbucket.org/phat-code/phat for more information. + # define a boundary matrix with the chosen internal representation + boundary_matrix = phat.boundary_matrix(representation = phat.representations.vector_vector) + + # set the respective columns -- (dimension, boundary) pairs + boundary_matrix.columns = [ (0, []), + (0, []), + (1, [0,1]), + (0, []), + (1, [1,3]), + (1, [0,3]), + (2, [2,4,5])] + + # or equivalently, boundary_matrix = phat.boundary_matrix(representation = ..., columns = ...) + # would combine the creation of the matrix and the assignment of the columns + + # print some information of the boundary matrix: + print("\nThe boundary matrix has %d columns:" % len(boundary_matrix.columns)) + for col in boundary_matrix.columns: + s = "Column %d represents a cell of dimension %d." % (col.index, col.dimension) + if (col.boundary): + s = s + " Its boundary consists of the cells " + " ".join([str(c) for c in col.boundary]) + print(s) + print("Overall, the boundary matrix has %d entries." % len(boundary_matrix)) + + pairs = boundary_matrix.compute_persistence_pairs() + + pairs.sort() + + print("\nThere are %d persistence pairs: " % len(pairs)) + for pair in pairs: + print("Birth: %d, Death: %d" % pair) + +Please see https://bitbucket.org/phat-code/phat/python for more information. """ +import _phat +import enum + +from _phat import persistence_pairs + +#The public API for the module + +__all__ = ['boundary_matrix', + 'persistence_pairs', + 'representations', + 'reductions'] class representations(enum.Enum): """Available representations for internal storage of columns in @@ -29,60 +84,221 @@ class representations(enum.Enum): vector_list = 7 class reductions(enum.Enum): - "Available reduction algorithms" + """Available reduction algorithms""" twist_reduction = 1 chunk_reduction = 2 standard_reduction = 3 row_reduction = 4 spectral_sequence_reduction = 5 -def __short_name(name): - return "".join([n[0] for n in name.split("_")]) +class column: + """A view on one column of data in a boundary matrix""" + def __init__(self, matrix, index): + """INTERNAL. Columns are created automatically by boundary matrices. + There is no need to construct them directly""" + self._matrix = matrix + self._index = index -def convert(source, to_representation): - """Internal - function to convert from one `boundary_matrix` implementation to another""" - class_name = source.__class__.__name__ - source_rep_short_name = class_name[len('boundary_matrix_'):] - to_rep_short_name = __short_name(to_representation.name) - function = getattr(_phat, "convert_%s_to_%s" % (source_rep_short_name, to_rep_short_name)) - return function(source) + @property + def index(self): + """The 0-based index of this column in its boundary matrix""" + return self._index + + @property + def dimension(self): + """The dimension of the column (0 = point, 1 = line, 2 = triangle, etc.)""" + return self._matrix._matrix.get_dim(self._index) + + @dimension.setter + def dimension(self, value): + return self._matrix._matrix.set_dim(self._index, value) -def boundary_matrix(representation = representations.bit_tree_pivot_column, source = None): - """Returns an instance of a `boundary_matrix` class. - The boundary matrix will use the specified implementation for storing its column data. - If the `source` parameter is specified, it will be assumed to be another boundary matrix, - whose data should be copied into the new matrix. + @property + def boundary(self): + """The boundary values in this column, i.e. the other columns that this column is bounded by""" + return self._matrix._matrix.get_col(self._index) + + @boundary.setter + def boundary(self, values): + return self._matrix._matrix.set_col(self._index, values) + +class boundary_matrix: + """Boundary matrices that store the shape information of a cell complex. """ - if source: - return convert(source, representation) - else: - class_name = representation.name - short_name = __short_name(class_name) - function = getattr(_phat, "boundary_matrix_" + short_name) - return function() - -def compute_persistence_pairs(matrix, - reduction = reductions.twist_reduction): - """Computes persistence pairs (birth, death) for the given boundary matrix.""" - class_name = matrix.__class__.__name__ - representation_short_name = class_name[len('boundary_matrix_'):] - algo_name = reduction.name - algo_short_name = __short_name(algo_name) - function = getattr(_phat, "compute_persistence_pairs_" + representation_short_name + "_" + algo_short_name) - return function(matrix) - -def compute_persistence_pairs_dualized(matrix, - reduction = reductions.twist_reduction): - """Computes persistence pairs (birth, death) from the dualized form of the given boundary matrix.""" - class_name = matrix.__class__.__name__ - representation_short_name = class_name[len('boundary_matrix_'):] - algo_name = reduction.name - algo_short_name = __short_name(algo_name) - function = getattr(_phat, "compute_persistence_pairs_dualized_" + representation_short_name + "_" + algo_short_name) - return function(matrix) + def __init__(self, representation = representations.bit_tree_pivot_column, source = None, columns = None): + """ + The boundary matrix will use the specified implementation for storing its + column data. If the `source` parameter is specified, it will be assumed to + be another boundary matrix, whose data should be copied into the new + matrix. + + Parameters + ---------- + + representation : phat.representation, optional + The type of column storage to use in the requested boundary matrix. + source : phat.boundary_matrix, optional + If provided, creates the requested matrix as a copy of the data and dimensions + in `source`. + columns : column list, or list of (dimension, boundary) tuples, optional + If provided, loads these columns into the new boundary matrix. Note that + columns will be loaded in the order given, not according to their ``index`` properties. + Returns + ------- + matrix : boundary_matrix + """ + self._representation = representation + if source: + self._matrix = _convert(source, representation) + else: + self._matrix = self.__matrix_for_representation(representation)() + if columns: + self.columns = columns + + @property + def columns(self): + """A collection of column objects""" + return [column(self, i) for i in range(self._matrix.get_num_cols())] + + @columns.setter + def columns(self, columns): + for col in columns: + if not (isinstance(col, column) or isinstance(col, tuple)): + raise TypeError("All columns must be column objects, or (dimension, values) tuples") + if len(columns) != len(self.dimensions): + self._matrix.set_dims([0] * len(columns)) + for i, col in enumerate(columns): + if isinstance(col, column): + self._matrix.set_dim(i, col.dimension) + self._matrix.set_col(i, col.boundary) + else: + dimension, values = col + self._matrix.set_dim(i, dimension) + self._matrix.set_col(i, values) + + @property + def dimensions(self): + """A collection of dimensions, equivalent to [c.dimension for c in self.columns]""" + return [self.get_dim(i) for i in range(self._matrix.get_num_cols())] + + @dimensions.setter + def dimensions(self, dimensions): + return self._matrix.set_dims(dimensions) + + def __matrix_for_representation(self, representation): + short_name = _short_name(representation.name) + return getattr(_phat, "boundary_matrix_" + short_name) + + def __eq__(self, other): + return self._matrix == other._matrix + + def __len__(self): + return self._matrix.get_num_entries() + + #Pickle support + def __getstate__(self): + (dimensions, columns) = self._matrix.get_vector_vector() + return (self._representation, dimensions, columns) + + #Pickle support + def __setstate__(self, state): + presentation, dimensions, columns = state + self._representation = representation + self._matrix = self.__matrix_for_representation(representation) + self._matrix.set_vector_vector(dimensions, columns) + + def load(self, file_name, mode = 'b'): + """Load this boundary matrix from a file + + Parameters + ---------- + + file_name : string + The file name to load + + mode : string, optional (defaults to 'b') + The mode ('b' for binary, 't' for text) to use for working with the file + + Returns + ------- + + success : bool + + """ + if mode == 'b': + return self._matrix.load_binary(file_name) + elif mode == 't': + return self._matrix.load_ascii(file_name) + else: + raise ValueError("Only 'b' - binary and 't' - text modes are supported") + + def save(self, file_name, mode = 'b'): + """Save this boundary matrix to a file + + Parameters + ---------- + + file_name : string + The file name to load + + mode : string, optional (defaults to 'b') + The mode ('b' for binary, 't' for text) to use for working with the file + + Returns + ------- + + success : bool + + """ + if mode == 'b': + return self._matrix.save_binary(file_name) + elif mode == 't': + return self._matrix.save_ascii(file_name) + else: + raise ValueError("Only 'b' - binary and 't' - text modes are supported") + + def compute_persistence_pairs(self, + reduction = reductions.twist_reduction): + """Computes persistence pairs (birth, death) for the given boundary matrix.""" + representation_short_name = _short_name(self._representation.name) + algo_name = reduction.name + algo_short_name = _short_name(algo_name) + #Look up an implementation that matches the requested characteristics + #in the _phat module + function = getattr(_phat, "compute_persistence_pairs_" + representation_short_name + "_" + algo_short_name) + return function(self._matrix) + + def compute_persistence_pairs_dualized(self, + reduction = reductions.twist_reduction): + """Computes persistence pairs (birth, death) from the dualized form of the given boundary matrix.""" + representation_short_name = _short_name(self._representation.name) + algo_name = reduction.name + algo_short_name = _short_name(algo_name) + #Look up an implementation that matches the requested characteristics + #in the _phat module + function = getattr(_phat, "compute_persistence_pairs_dualized_" + representation_short_name + "_" + algo_short_name) + return function(self._matrix) + + def convert(self, representation): + """Copy this matrix to another with a different representation""" + return boundary_matrix(representation, self) + +def _short_name(name): + """An internal API that takes leading characters from words + For instance, 'bit_tree_pivot_column' becomes 'btpc' + """ + return "".join([n[0] for n in name.split("_")]) + +def _convert(source, to_representation): + """Internal - function to convert from one `boundary_matrix` implementation to another""" + class_name = source._representation.name + source_rep_short_name = _short_name(class_name) + to_rep_short_name = _short_name(to_representation.name) + function = getattr(_phat, "convert_%s_to_%s" % (source_rep_short_name, to_rep_short_name)) + return function(source._matrix) diff --git a/python/src/self_test.py b/python/src/self_test.py index c8174fb..3f85fc1 100644 --- a/python/src/self_test.py +++ b/python/src/self_test.py @@ -8,15 +8,14 @@ if __name__=='__main__': boundary_matrix = phat.boundary_matrix() # This is broken for some reason - if not boundary_matrix.load_binary(test_data): - # if not boundary_matrix.load_ascii(test_data): + if not boundary_matrix.load(test_data): print("Error: test data %s not found!" % test_data) sys.exit(1) error = False def compute_chunked(mat): - return phat.compute_persistence_pairs(mat, phat.reductions.chunk_reduction) + return mat.compute_persistence_pairs(phat.reductions.chunk_reduction) print("Comparing representations using Chunk algorithm ...") print("Running Chunk - Sparse ...") @@ -88,7 +87,8 @@ if __name__=='__main__': reps = phat.representations reds = phat.reductions - pairs = phat.compute_persistence_pairs + def pairs(mat, red): + return mat.compute_persistence_pairs(red) twist_boundary_matrix = bit_tree_mat() twist_pairs = pairs(twist_boundary_matrix, reds.twist_reduction) @@ -132,10 +132,10 @@ if __name__=='__main__': print("Comparing primal and dual approach using Chunk - Full ...") primal_boundary_matrix = phat.boundary_matrix(reps.full_pivot_column, boundary_matrix) - primal_pairs = phat.compute_persistence_pairs(primal_boundary_matrix, reds.chunk_reduction) + primal_pairs = primal_boundary_matrix.compute_persistence_pairs(reds.chunk_reduction) dual_boundary_matrix = phat.boundary_matrix(reps.full_pivot_column, boundary_matrix) - dual_pairs = phat.compute_persistence_pairs_dualized(dual_boundary_matrix) + dual_pairs = dual_boundary_matrix.compute_persistence_pairs_dualized() if primal_pairs != dual_pairs: print("Error: primal and dual differ!", file=sys.stderr) @@ -149,11 +149,9 @@ if __name__=='__main__': print("Testing vector interface ...") - (vector_vector_matrix, vector_dims) = boundary_matrix.get_vector_vector() - vector_vector_boundary_matrix = phat.boundary_matrix(phat.representations.bit_tree_pivot_column) - vector_vector_boundary_matrix.load_vector_vector(vector_vector_matrix, vector_dims) + vector_vector_boundary_matrix.columns = boundary_matrix.columns if vector_vector_boundary_matrix != boundary_matrix: print("Error: [load|save]_vector_vector bug", file=sys.stderr) diff --git a/python/src/simple_example.py b/python/src/simple_example.py index 82cf6be..955e213 100644 --- a/python/src/simple_example.py +++ b/python/src/simple_example.py @@ -21,39 +21,35 @@ if __name__ == "__main__": import phat - # set the dimension of the cell that each column represents: - dimensions = [0, 0, 1, 0, 1, 1, 2] - # define a boundary matrix with the chosen internal representation boundary_matrix = phat.boundary_matrix(representation = phat.representations.vector_vector) - # set the respective columns -- the columns entries have to be sorted - boundary_matrix.set_dims(dimensions) - boundary_matrix.set_col(0, []) - boundary_matrix.set_col(1, []) - boundary_matrix.set_col(2, [0,1]) - boundary_matrix.set_col(3, []) - boundary_matrix.set_col(4, [1,3]) - boundary_matrix.set_col(5, [0,3]) - boundary_matrix.set_col(6, [2,4,5]) + # set the respective columns -- (dimension, boundary) pairs + boundary_matrix.columns = [ (0, []), + (0, []), + (1, [0,1]), + (0, []), + (1, [1,3]), + (1, [0,3]), + (2, [2,4,5])] + + # or equivalently, boundary_matrix = phat.boundary_matrix(representation = ..., columns = ...) + # would combine the creation of the matrix and the assignment of the columns # print some information of the boundary matrix: - print() - print("The boundary matrix has %d columns:" % boundary_matrix.get_num_cols()) - for col_idx in range(boundary_matrix.get_num_cols()): - s = "Column %d represents a cell of dimension %d." % (col_idx, boundary_matrix.get_dim(col_idx)) - if (not boundary_matrix.is_empty(col_idx)): - s = s + " Its boundary consists of the cells " + " ".join([str(c) for c in boundary_matrix.get_col(col_idx)]) + print("\nThe boundary matrix has %d columns:" % len(boundary_matrix.columns)) + for col in boundary_matrix.columns: + s = "Column %d represents a cell of dimension %d." % (col.index, col.dimension) + if (col.boundary): + s = s + " Its boundary consists of the cells " + " ".join([str(c) for c in col.boundary]) print(s) - print("Overall, the boundary matrix has %d entries." % boundary_matrix.get_num_entries()) + print("Overall, the boundary matrix has %d entries." % len(boundary_matrix)) - pairs = phat.compute_persistence_pairs(boundary_matrix) + pairs = boundary_matrix.compute_persistence_pairs() pairs.sort() - print() - - print("There are %d persistence pairs: " % len(pairs)) + print("\nThere are %d persistence pairs: " % len(pairs)) for pair in pairs: print("Birth: %d, Death: %d" % pair) -- cgit v1.2.3 From a0d647b35e2cd7702eaffbd2074cb2914c6383cd Mon Sep 17 00:00:00 2001 From: Bryn Keller Date: Mon, 2 May 2016 13:23:42 -0700 Subject: Tested with system Python 2.7 on Ubuntu as well as Anaconda Python 3.5 on Ubuntu --- python/README.rst | 14 +++++++++----- python/_phat.cpp | 2 +- python/setup.py | 8 ++++++-- 3 files changed, 16 insertions(+), 8 deletions(-) (limited to 'python/_phat.cpp') diff --git a/python/README.rst b/python/README.rst index 3fd685d..9f7fe41 100644 --- a/python/README.rst +++ b/python/README.rst @@ -48,14 +48,18 @@ Suppose you have checked out the PHAT repository at location $PHAT. Then you can ``` cd $PHAT/python -pip install . +ln -s ../include include # (or copy, we just need $PHAT/include to be in the current folder as well) + +pip install pybind11 + +python setup.py install ``` -This will install PHAT for whatever Python installation your `pip` executable is associated with. -Also note that this will install dependencies (pybind11) as well. -If you already have pybind11 installed, then `python setup.py install` will most likely work too. +This will install PHAT for whatever Python installation your `python` executable is associated with. +Please ensure you use the `pip` that comes from the same directory where your `python` executable lives! -Currently, the PHAT Python bindings are known to work on Mac OS X with Python 3.5. Other configurations are untested but in progress. +Currently, the PHAT Python bindings are known to work on Linux with Python 2.7 and Python 3.5. +Other configurations are untested but in progress. Please let us know if there is a platform you'd like us to support, we will do so if we can. Sample usage diff --git a/python/_phat.cpp b/python/_phat.cpp index 40a9421..9883c97 100644 --- a/python/_phat.cpp +++ b/python/_phat.cpp @@ -227,7 +227,7 @@ void wrap_persistence_pairs(py::module &m) { .def("__len__", &phat::persistence_pairs::get_num_pairs) // Unlike set_pair, this takes a Python 2-tuple .def("__setitem__", - [](phat::persistence_pairs &p, int index, std::pair &pair) { + [](phat::persistence_pairs &p, int index, std::pair pair) { phat::index idx = fix_index(p, index); p.set_pair(idx, pair.first, pair.second); }) diff --git a/python/setup.py b/python/setup.py index 2c6d65d..133a204 100644 --- a/python/setup.py +++ b/python/setup.py @@ -2,12 +2,14 @@ from setuptools import setup, Extension, find_packages from setuptools.command.build_ext import build_ext import sys import os.path +from io import open ext_modules = [ Extension( '_phat', ['_phat.cpp'], - include_dirs=['include', '../include'], + include_dirs=['include', + '../include'], language='c++', ), ] @@ -15,7 +17,7 @@ ext_modules = [ here = os.path.abspath(os.path.dirname(__file__)) # Get the long description from the README file -with open(os.path.join(here, 'README.rst')) as f: +with open(os.path.join(here, 'README.rst'), encoding = 'utf8') as f: long_description = f.read() class BuildExt(build_ext): @@ -31,8 +33,10 @@ class BuildExt(build_ext): def build_extensions(self): ct = self.compiler.compiler_type opts = self.c_opts.get(ct, []) + import pybind11 for ext in self.extensions: ext.extra_compile_args = opts + ext.include_dirs.append(pybind11.get_include()) build_ext.build_extensions(self) setup( -- cgit v1.2.3 From 2f3ed798d26ae4ab31b0b45e5ef5305c58b28be1 Mon Sep 17 00:00:00 2001 From: Bryn Keller Date: Mon, 2 May 2016 15:46:07 -0700 Subject: Various tweaks to support Python 2.7 --- python/_phat.cpp | 13 +++++++++++++ python/phat.py | 15 +++++++++++++-- python/src/self_test.py | 2 +- 3 files changed, 27 insertions(+), 3 deletions(-) (limited to 'python/_phat.cpp') diff --git a/python/_phat.cpp b/python/_phat.cpp index 9883c97..df7449d 100644 --- a/python/_phat.cpp +++ b/python/_phat.cpp @@ -149,6 +149,16 @@ void wrap_boundary_matrix(py::module &mod, const std::string &representation_suf .def("__eq__", &mat::template operator==) .def("__eq__", &mat::template operator==) + //Python 3.x can figure this out for itself, but Python 2.7 needs to be told: + .def("__ne__", &mat::template operator!=) + .def("__ne__", &mat::template operator!=) + .def("__ne__", &mat::template operator!=) + .def("__ne__", &mat::template operator!=) + .def("__ne__", &mat::template operator!=) + .def("__ne__", &mat::template operator!=) + .def("__ne__", &mat::template operator!=) + .def("__ne__", &mat::template operator!=) + //#### Data access // In `get_col`, since Python is garbage collected, the C++ idiom of passing in a collection @@ -241,6 +251,9 @@ void wrap_persistence_pairs(py::module &m) { .def("clear", &phat::persistence_pairs::clear, "Empties the collection") .def("sort", &phat::persistence_pairs::sort, "Sort in place") .def("__eq__", &phat::persistence_pairs::operator==) + .def("__ne__", [](phat::persistence_pairs &p, phat::persistence_pairs &other) { + return p != other; + }) //#### File operations .def("load_ascii", &phat::persistence_pairs::load_ascii, "Load the contents of a text file into this instance") diff --git a/python/phat.py b/python/phat.py index 0aff4bd..70f5b39 100644 --- a/python/phat.py +++ b/python/phat.py @@ -77,6 +77,7 @@ __all__ = ['boundary_matrix', 'representations', 'reductions'] + class representations(enum.Enum): """Available representations for internal storage of columns in a `boundary_matrix` @@ -89,6 +90,7 @@ class representations(enum.Enum): vector_set = 6 vector_list = 7 + class reductions(enum.Enum): """Available reduction algorithms""" twist_reduction = 1 @@ -97,7 +99,8 @@ class reductions(enum.Enum): row_reduction = 4 spectral_sequence_reduction = 5 -class column: + +class column(object): """A view on one column of data in a boundary matrix""" def __init__(self, matrix, index): """INTERNAL. Columns are created automatically by boundary matrices. @@ -128,7 +131,10 @@ class column: def boundary(self, values): return self._matrix._matrix.set_col(self._index, values) -class boundary_matrix: + def __str__(self): + return "(%d, %s)" % (self.dimension, self.boundary) + +class boundary_matrix(object): """Boundary matrices that store the shape information of a cell complex. """ @@ -201,6 +207,11 @@ class boundary_matrix: def __eq__(self, other): return self._matrix == other._matrix + #Note Python 2.7 needs BOTH __eq__ and __ne__ otherwise you get things that + #are both equal and not equal + def __ne__(self, other): + return self._matrix != other._matrix + def __len__(self): return self._matrix.get_num_entries() diff --git a/python/src/self_test.py b/python/src/self_test.py index 3f85fc1..8017387 100644 --- a/python/src/self_test.py +++ b/python/src/self_test.py @@ -1,3 +1,4 @@ +from __future__ import print_function import sys import phat @@ -157,7 +158,6 @@ if __name__=='__main__': print("Error: [load|save]_vector_vector bug", file=sys.stderr) error = True - if error: sys.exit(1) else: -- cgit v1.2.3