Skip to content

Commit

Permalink
Release v0.4.3 (#56)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ulupo authored Dec 3, 2020
1 parent 509c030 commit 24c1d8c
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 50 deletions.
28 changes: 28 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
@@ -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
=============

Expand Down
58 changes: 33 additions & 25 deletions pyflagser/_utils.py
Original file line number Diff line number Diff line change
@@ -1,66 +1,74 @@
"""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
data = adjacency_matrix.data
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_):
Expand All @@ -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
2 changes: 1 addition & 1 deletion pyflagser/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@
# 'X.Y.dev0' is the canonical version of 'X.Y.dev'
#

__version__ = '0.4.2'
__version__ = '0.4.3'
10 changes: 4 additions & 6 deletions pyflagser/flagio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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``,
Expand Down
12 changes: 5 additions & 7 deletions pyflagser/flagser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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``
Expand All @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
15 changes: 5 additions & 10 deletions pyflagser/flagser_count.py
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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)
Expand Down Expand Up @@ -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``,
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
MAINTAINER_EMAIL = '[email protected]'
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',
Expand Down

0 comments on commit 24c1d8c

Please sign in to comment.