From 24c1d8c04c0ac464bdcc46ae80148d0f8c0c70c9 Mon Sep 17 00:00:00 2001 From: Umberto Lupo <46537483+ulupo@users.noreply.github.com> Date: Thu, 3 Dec 2020 19:08:48 +0100 Subject: [PATCH] Release v0.4.3 (#56) * Support rectangular (esp. sparse) matrices in pyflagser (#55) Cleanup code in `_extract_unweighted_graph` and `_extract_weighted_graph` * Bump version to 0.4.3, write release notes * Remove square input requirement from docstrings, fix some typos * Remove unused variables from flagser_count_unweighted --- RELEASE.rst | 28 ++++++++++++++++++ pyflagser/_utils.py | 58 ++++++++++++++++++++++---------------- pyflagser/_version.py | 2 +- pyflagser/flagio.py | 10 +++---- pyflagser/flagser.py | 12 ++++---- pyflagser/flagser_count.py | 15 ++++------ setup.py | 2 +- 7 files changed, 77 insertions(+), 50 deletions(-) diff --git a/RELEASE.rst b/RELEASE.rst index 58cccea..3bfdc59 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -1,3 +1,31 @@ +Release 0.4.3 +============= + +Major Features and Improvements +------------------------------- + +All functions in ``pyflagser`` now accept rectangular adjacency matrices. However, warnings remain in the case of non-square dense input. + +Bug Fixes +--------- + +None. + +Backwards-Incompatible Changes +------------------------------ + +None. + +Thanks to our Contributors +-------------------------- + +This release contains contributions from: + +Umberto Lupo. + +We are also grateful to all who filed issues or helped resolve them, asked and answered questions, and were part of inspiring discussions. + + Release 0.4.2 ============= diff --git a/pyflagser/_utils.py b/pyflagser/_utils.py index b3d96ee..16e6729 100644 --- a/pyflagser/_utils.py +++ b/pyflagser/_utils.py @@ -1,57 +1,66 @@ """Utility functions for adjacency matrices.""" -import numpy as np import warnings +import numpy as np + def _extract_unweighted_graph(adjacency_matrix): - # Warn if the matrix is not squared - if adjacency_matrix.shape[0] != adjacency_matrix.shape[1]: - warnings.warn("adjacency_matrix should be a square matrix.") + input_shape = adjacency_matrix.shape + # Warn if dense and not square + if isinstance(adjacency_matrix, np.ndarray) and \ + (input_shape[0] != input_shape[1]): + warnings.warn("Dense `adjacency_matrix` should be square.") # Extract vertices and give them weight one - vertices = np.ones(adjacency_matrix.shape[0], dtype=np.float) + n_vertices = max(input_shape) + vertices = np.ones(n_vertices, dtype=np.float) - # Extract edges indices + # Extract edge indices if isinstance(adjacency_matrix, np.ndarray): # Off-diagonal mask - mask = np.logical_not(np.eye(adjacency_matrix.shape[0], dtype=bool)) + mask = np.logical_not(np.eye(input_shape[0], M=input_shape[1], + dtype=bool)) # Data mask mask = np.logical_and(adjacency_matrix, mask) edges = np.argwhere(mask) else: - # Data mask - mask = np.stack(np.nonzero(adjacency_matrix)).T + edges = np.argwhere(adjacency_matrix) - # Removes diagonal elements a posteriori - edges = mask[mask[:, 0] != mask[:, 1]] + # Remove diagonal elements a posteriori + edges = edges[edges[:, 0] != edges[:, 1]] # Assign weight one - edges = np.hstack([edges, np.ones(edges[:, [0]].shape, dtype=np.int)]) + edges = np.insert(edges, 2, 1, axis=1) return vertices, edges def _extract_weighted_graph(adjacency_matrix, max_edge_weight): - # Warn if the matrix is not squared - if adjacency_matrix.shape[0] != adjacency_matrix.shape[1]: - warnings.warn("adjacency_matrix should be a square matrix.") - - # Extract vertices weights - vertices = np.asarray(adjacency_matrix.diagonal()) - - # Extract edges indices and weights + input_shape = adjacency_matrix.shape + # Warn if dense and not square + if isinstance(adjacency_matrix, np.ndarray) and \ + (input_shape[0] != input_shape[1]): + warnings.warn("Dense `adjacency_matrix` should be square.") + + # Extract vertex weights + n_vertices = max(input_shape) + vertices = np.zeros(n_vertices, dtype=adjacency_matrix.dtype) + vertices[:min(input_shape)] = adjacency_matrix.diagonal() + + # Extract edge indices and weights if isinstance(adjacency_matrix, np.ndarray): row, column = np.indices(adjacency_matrix.shape) row, column = row.flat, column.flat data = adjacency_matrix.flat # Off-diagonal mask - mask = np.logical_not(np.eye(vertices.shape[0], dtype=bool).flat) + mask = np.logical_not(np.eye(input_shape[0], M=input_shape[1], + dtype=bool).flat) else: - # Convert to COO format to extract row column, and data arrays + # Convert to COO format to extract row, column, and data arrays fmt = adjacency_matrix.getformat() adjacency_matrix = adjacency_matrix.tocoo() row, column = adjacency_matrix.row, adjacency_matrix.col @@ -59,8 +68,7 @@ def _extract_weighted_graph(adjacency_matrix, max_edge_weight): adjacency_matrix = adjacency_matrix.asformat(fmt) # Off-diagonal mask - mask = np.ones(row.shape[0], dtype=np.bool) - mask[np.arange(row.shape[0])[row == column]] = False + mask = row != column # Mask infinite or thresholded weights if np.issubdtype(adjacency_matrix.dtype, np.float_): @@ -71,6 +79,6 @@ def _extract_weighted_graph(adjacency_matrix, max_edge_weight): elif max_edge_weight is not None: mask = np.logical_and(mask, data <= max_edge_weight) - edges = np.vstack([row[mask], column[mask], data[mask]]).T + edges = np.c_[row[mask], column[mask], data[mask]] return vertices, edges diff --git a/pyflagser/_version.py b/pyflagser/_version.py index 46f6604..27609ae 100644 --- a/pyflagser/_version.py +++ b/pyflagser/_version.py @@ -17,4 +17,4 @@ # 'X.Y.dev0' is the canonical version of 'X.Y.dev' # -__version__ = '0.4.2' +__version__ = '0.4.3' diff --git a/pyflagser/flagio.py b/pyflagser/flagio.py index ca9ab7d..7f3ebc9 100644 --- a/pyflagser/flagio.py +++ b/pyflagser/flagio.py @@ -32,7 +32,7 @@ def load_unweighted_flag(fname, fmt='csr', dtype=np.bool): `fmt` Adjacency matrix of a directed/undirected unweighted graph. It is understood as a boolean matrix. Off-diagonal, ``0`` or ``False`` values - denote abstent edges while non-``0`` or ``True`` values denote edges + denote absent edges while non-``0`` or ``True`` values denote edges which are present. Diagonal values are ignored. Notes @@ -168,11 +168,10 @@ def save_unweighted_flag(fname, adjacency_matrix): fname : file, str, or pathlib.Path, required Filename of extension ``.flag``. - adjacency_matrix : 2d ndarray or scipy.sparse matrix of shape \ - (n_vertices, n_vertices), required + adjacency_matrix : 2d ndarray or scipy.sparse matrix, required Adjacency matrix of a directed/undirected unweighted graph. It is understood as a boolean matrix. Off-diagonal, ``0`` or ``False`` values - denote abstent edges while non-``0`` or ``True`` values denote edges + denote absent edges while non-``0`` or ``True`` values denote edges which are present. Diagonal values are ignored. Notes @@ -206,8 +205,7 @@ def save_weighted_flag(fname, adjacency_matrix, max_edge_weight=None): fname : file, str, or pathlib.Path, required Filename of extension ``.flag``. - adjacency_matrix : 2d ndarray or scipy.sparse matrix of shape \ - (n_vertices, n_vertices), required + adjacency_matrix : 2d ndarray or scipy.sparse matrix, required Matrix representation of a directed/undirected weighted graph. Diagonal elements are vertex weights. The way zero values are handled depends on the format of the matrix. If the matrix is a dense ``numpy.ndarray``, diff --git a/pyflagser/flagser.py b/pyflagser/flagser.py index 0af38b8..934224f 100644 --- a/pyflagser/flagser.py +++ b/pyflagser/flagser.py @@ -17,11 +17,10 @@ def flagser_unweighted(adjacency_matrix, min_dimension=0, max_dimension=np.inf, Parameters ---------- - adjacency_matrix : 2d ndarray or scipy.sparse matrix of shape \ - (n_vertices, n_vertices), required + adjacency_matrix : 2d ndarray or scipy.sparse matrix, required Adjacency matrix of a directed/undirected unweighted graph. It is understood as a boolean matrix. Off-diagonal, ``0`` or ``False`` values - denote abstent edges while non-``0`` or ``True`` values denote edges + denote absent edges while non-``0`` or ``True`` values denote edges which are present. Diagonal values are ignored. min_dimension : int, optional, default: ``0`` @@ -31,7 +30,7 @@ def flagser_unweighted(adjacency_matrix, min_dimension=0, max_dimension=np.inf, Maximum homology dimension to compute. directed : bool, optional, default: ``True`` - If ``True``, computes homology for the directed flad complex determined + If ``True``, computes homology for the directed flag complex determined by `adjacency_matrix`. If ``False``, computes homology for the undirected flag complex obtained by considering all edges as undirected, and it is therefore sufficient (but not necessary) @@ -125,15 +124,14 @@ def flagser_weighted(adjacency_matrix, max_edge_weight=None, min_dimension=0, Parameters ---------- - adjacency_matrix : 2d ndarray or scipy.sparse matrix of shape \ - (n_vertices, n_vertices), required + adjacency_matrix : 2d ndarray or scipy.sparse matrix, required Matrix representation of a directed/undirected weighted graph. Diagonal elements are vertex weights. The way zero values are handled depends on the format of the matrix. If the matrix is a dense ``numpy.ndarray``, zero values denote zero-weighted edges. If the matrix is a sparse ``scipy.sparse`` matrix, explicitly stored off-diagonal zeros and all diagonal zeros denote zero-weighted edges. Off-diagonal values that - have not been explicitely stored are treated by ``scipy.sparse`` as + have not been explicitly stored are treated by ``scipy.sparse`` as zeros but will be understood as infinitely-valued edges, i.e., edges absent from the filtration. diff --git a/pyflagser/flagser_count.py b/pyflagser/flagser_count.py index 310936a..6295d53 100644 --- a/pyflagser/flagser_count.py +++ b/pyflagser/flagser_count.py @@ -1,14 +1,11 @@ """Implementation of the python API for the cell count of the flagser C++ library.""" -import numpy as np - from ._utils import _extract_unweighted_graph, _extract_weighted_graph from .modules.flagser_count_pybind import compute_cell_count -def flagser_count_unweighted(adjacency_matrix, min_dimension=0, - max_dimension=np.inf, directed=True): +def flagser_count_unweighted(adjacency_matrix, directed=True): """Compute the cell count per dimension of a directed/undirected unweighted flag complex. @@ -17,15 +14,14 @@ def flagser_count_unweighted(adjacency_matrix, min_dimension=0, Parameters ---------- - adjacency_matrix : 2d ndarray or scipy.sparse matrix of shape \ - (n_vertices, n_vertices), required + adjacency_matrix : 2d ndarray or scipy.sparse matrix, required Adjacency matrix of a directed/undirected unweighted graph. It is understood as a boolean matrix. Off-diagonal, ``0`` or ``False`` values - denote abstent edges while non-``0`` or ``True`` values denote edges + denote absent edges while non-``0`` or ``True`` values denote edges which are present. Diagonal values are ignored. directed : bool, optional, default: ``True`` - If ``True``, computes homology for the directed flad complex determined + If ``True``, computes homology for the directed flag complex determined by `adjacency_matrix`. If ``False``, computes homology for the undirected flag complex obtained by considering all edges as undirected, and it is therefore sufficient (but not necessary) @@ -70,8 +66,7 @@ def flagser_count_weighted(adjacency_matrix, max_edge_weight=None, Parameters ---------- - adjacency_matrix : 2d ndarray or scipy.sparse matrix of shape \ - (n_vertices, n_vertices), required + adjacency_matrix : 2d ndarray or scipy.sparse matrix, required Matrix representation of a directed/undirected weighted graph. Diagonal elements are vertex weights. The way zero values are handled depends on the format of the matrix. If the matrix is a dense ``numpy.ndarray``, diff --git a/setup.py b/setup.py index 5f0b1ad..1bd83b4 100755 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ MAINTAINER_EMAIL = 'maintainers@giotto.ai' URL = 'https://github.com/giotto-ai/pyflagser' LICENSE = 'GNU AGPLv3' -DOWNLOAD_URL = 'https://github.com/giotto-ai/pyflagser/tarball/v0.4.2' +DOWNLOAD_URL = 'https://github.com/giotto-ai/pyflagser/tarball/v0.4.3' VERSION = __version__ # noqa CLASSIFIERS = ['Intended Audience :: Science/Research', 'Intended Audience :: Developers',